From d90103513619b38ca5ddcd15bedafae5955aa5b2 Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Fri, 29 May 2026 10:18:53 -0700 Subject: [PATCH 001/188] Atomic fast path for "locale" encoding registration The "locale" encoding is only registered once, so we can use an atomic to avoid the full lock and hash lookup. --- encoding.c | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/encoding.c b/encoding.c index 73fad8f1a6fd2b..c17f118eef3f33 100644 --- a/encoding.c +++ b/encoding.c @@ -98,6 +98,7 @@ static rb_encoding *global_enc_ascii, *global_enc_us_ascii; static int filesystem_encindex = ENCINDEX_ASCII_8BIT; +static rb_atomic_t locale_alias_registered; #define GLOBAL_ENC_TABLE_LOCKING(tbl) \ for (struct enc_table *tbl = &global_enc_table, **locking = &tbl; \ @@ -1568,15 +1569,16 @@ rb_locale_encindex(void) if (idx < 0) idx = ENCINDEX_UTF_8; - GLOBAL_ENC_TABLE_LOCKING(enc_table) { - if (enc_registered(enc_table, "locale") < 0) { + if (!RUBY_ATOMIC_LOAD(locale_alias_registered)) { + GLOBAL_ENC_TABLE_LOCKING(enc_table) { + if (enc_registered(enc_table, "locale") < 0) { # if defined _WIN32 - void Init_w32_codepage(void); - Init_w32_codepage(); + void Init_w32_codepage(void); + Init_w32_codepage(); # endif - GLOBAL_ENC_TABLE_LOCKING(enc_table) { enc_alias_internal(enc_table, "locale", idx); } + RUBY_ATOMIC_SET(locale_alias_registered, 1); } } From 63d9f090b5d9461cf0b9446e0039d9c56156b826 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Fri, 29 May 2026 15:09:34 -0700 Subject: [PATCH 002/188] Reserve 2 bits for expressing object layout (#17139) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Reserve 2 bits for expressing object layout We would like to make instance variable reads in the JIT compiler faster (as well as simplify the JIT implementation). Currently, in order to read an instance variable, we have to: 1. Test for heap object 2. Load object to a 64 bit register 3. Mask the object header 4. Bit test against the masked header 5. JNE 6. Load field We would like to: 1. Test for heap object 2. Load object shape to a 32 bit register 3. Bit test against the shape 4. JNE 5. Load field The way we fetch instance variables is not consistent across objects. In order to realize our goal, we need to encode object layout inside the shape. If we encode object layout inside the shape, then the shape itself will guarantee that the access pattern generated by the JIT compiler is correct. We should encode the following load patterns into the shape tag bits. This way we can share shapes on transitions, but be able to differentiate the access patterns for the JIT compiler. In other words, two objects can have an `@a -> @b -> @c` transition and share the same shape, but the tag bits can differentiate the access pattern so that the JIT compiler can be confident that the machine code is correct. Here are the patterns: 1. Embedded/Extended T_OBJECT Instance Variables Objects with direct references to instance variables or via malloc buffer 2. Objects with fields_objects fields These are Data and TypedData objects. They have an associated axillary imemo/fields object that stores the instance variables. The access pattern is `object[2] + 2`. The fields object is the 3rd field, and the instance variables start at +2 inside the fields object. The fields object itself is a Ruby object, so it contains the usual header bits + class headers. 3. Non Boxable Classes / Modules This is similar to Objects with fields_objects, but the fields object is stored at a different offset. We’re differentiating this from boxable classes and modules because those are harder to support. 4. Other "Other" pattern is for objects that are rare, or have difficult-to-implement access patterns. This includes: * Boxable classes and modules * Structs (for now) * Objects that use the geniv table Proposed shape bit layout: ``` Current shape_id_t is 32 bits: 31 28 27 26 25 24 23 22 19 18 0 +-----------+--+--+--+--+--+------------+----------------------------+ | unused |L1|L0|OI|FR|CX| heap index | shape tree offset | +-----------+--+--+--+--+--+------------+----------------------------+ | | | | | | | | | | | | | +-- bits 0-18: SHAPE_ID_OFFSET_MASK | | | | | +--------------- bits 19-22: SHAPE_ID_HEAP_INDEX_MASK | | | | +------------------ bit 23: SHAPE_ID_FL_COMPLEX | | | +--------------------- bit 24: SHAPE_ID_FL_FROZEN | | +------------------------ bit 25: SHAPE_ID_FL_HAS_OBJECT_ID +--+--------------------------- bits 26-27: SHAPE_ID_LAYOUT_MASK ``` The important part about these layout patterns is that they do not reflect the _type_ of object, only how the object is laid out in memory. For example, we currently treat structs as "other", but we can refactor them to have the same layout as "Objects with fields_objects", and when we do that they should get a different bit in the shape header. This commit only reserves the two bits, it doesn't use them in the JIT compiler yet. Co-Authored-By: John Hawthorn Co-Authored-By: Max Bernstein * Update gc.c Co-authored-by: Nobuyoshi Nakada * Update shape.h Co-authored-by: Jean Boussier * fix function name * Update shape.c Co-authored-by: Jean Boussier * fix function name * Revert "Update shape.c" This reverts commit 900711defc6c541a93f3393a350819ae88cf87f1. * add comment --------- Co-authored-by: John Hawthorn Co-authored-by: Max Bernstein Co-authored-by: Nobuyoshi Nakada Co-authored-by: Jean Boussier --- array.c | 2 + class.c | 10 ++++- depend | 1 + gc.c | 24 ++++++++--- imemo.c | 18 ++++++-- internal/gc.h | 2 +- ractor.c | 2 +- shape.c | 78 ++++++++++++++++++++++++++++++---- shape.h | 54 ++++++++++++++++++----- string.c | 2 +- test/ruby/test_shapes.rb | 31 ++++++++++++++ variable.c | 22 ++++++---- vm_insnhelper.c | 7 ++- zjit/src/cruby_bindings.inc.rs | 7 ++- 14 files changed, 216 insertions(+), 44 deletions(-) diff --git a/array.c b/array.c index 08d8d17c90f79f..7f06e3c9c72f66 100644 --- a/array.c +++ b/array.c @@ -30,6 +30,7 @@ #include "ruby/thread.h" #include "ruby/util.h" #include "ruby/ractor.h" +#include "shape.h" #include "vm_core.h" #include "builtin.h" @@ -909,6 +910,7 @@ init_fake_ary_flags(void) struct RArray fake_ary = {0}; fake_ary.basic.flags = T_ARRAY; VALUE ary = (VALUE)&fake_ary; + RBASIC_SET_SHAPE_ID(ary, ROOT_SHAPE_ID | SHAPE_ID_LAYOUT_OTHER); rb_ary_freeze(ary); return fake_ary.basic.flags; } diff --git a/class.c b/class.c index 69194ccab23756..02078cc9bc8baf 100644 --- a/class.c +++ b/class.c @@ -585,7 +585,15 @@ class_alloc0(enum ruby_value_type type, VALUE klass, bool boxable) VALUE flags = type | FL_SHAREABLE; if (boxable) flags |= RCLASS_BOXABLE; - NEWOBJ_OF(obj, struct RClass, klass, flags, alloc_size); + shape_id_t shape_id = ROOT_SHAPE_ID; + if (boxable) { + shape_id |= SHAPE_ID_LAYOUT_OTHER; + } + else { + shape_id |= SHAPE_ID_LAYOUT_RCLASS; + } + + struct RClass *obj = (struct RClass *)rb_newobj(GET_EC(), klass, flags, shape_id, true, alloc_size); obj->object_id = 0; diff --git a/depend b/depend index e61b6856704712..a2e8312298e7ea 100644 --- a/depend +++ b/depend @@ -80,6 +80,7 @@ array.$(OBJEXT): $(top_srcdir)/internal/sanitizers.h array.$(OBJEXT): $(top_srcdir)/internal/serial.h array.$(OBJEXT): $(top_srcdir)/internal/set_table.h array.$(OBJEXT): $(top_srcdir)/internal/static_assert.h +array.$(OBJEXT): $(top_srcdir)/internal/struct.h array.$(OBJEXT): $(top_srcdir)/internal/variable.h array.$(OBJEXT): $(top_srcdir)/internal/vm.h array.$(OBJEXT): $(top_srcdir)/internal/warnings.h diff --git a/gc.c b/gc.c index 9eb72329404a3b..6357577e63705f 100644 --- a/gc.c +++ b/gc.c @@ -1056,7 +1056,15 @@ rb_newobj(rb_execution_context_t *ec, VALUE klass, VALUE flags, shape_id_t shape VALUE rb_ec_newobj_of(rb_execution_context_t *ec, VALUE klass, VALUE flags, size_t size) { - return rb_newobj(ec, klass, flags, ROOT_SHAPE_ID, true, size); + VALUE type = flags & T_MASK; + RUBY_ASSERT(type != T_OBJECT); + RUBY_ASSERT(type != T_DATA); + RUBY_ASSERT(type != T_CLASS); + RUBY_ASSERT(type != T_MODULE); + RUBY_ASSERT(type != T_ICLASS); + (void)type; + + return rb_newobj(ec, klass, flags, ROOT_SHAPE_ID | SHAPE_ID_LAYOUT_OTHER, true, size); } VALUE @@ -1068,13 +1076,13 @@ rb_newobj_of_with_shape(VALUE klass, VALUE flags, shape_id_t shape_id, size_t si VALUE rb_newobj_of(VALUE klass, VALUE flags, size_t size) { - return rb_newobj(GET_EC(), klass, flags, ROOT_SHAPE_ID, true, size); + return rb_newobj(GET_EC(), klass, flags, ROOT_SHAPE_ID | SHAPE_ID_LAYOUT_OTHER, true, size); } static VALUE class_allocate_complex_instance(VALUE klass, uint32_t capacity) { - shape_id_t initial_shape_id = rb_shape_root(rb_gc_heap_id_for_size(sizeof(struct RObject))); + shape_id_t initial_shape_id = rb_shape_id_with_robject_layout(rb_shape_root(rb_gc_heap_id_for_size(sizeof(struct RObject)))); VALUE obj = rb_newobj_of_with_shape(klass, T_OBJECT, initial_shape_id, sizeof(struct RObject)); rb_obj_init_complex(obj, rb_st_init_numtable_with_size(capacity)); return obj; @@ -1099,7 +1107,7 @@ rb_class_allocate_instance(VALUE klass) // There might be a NEWOBJ tracepoint callback, and it may set fields. // So the shape must be passed to `NEWOBJ_OF`. - obj = rb_newobj_of_with_shape(klass, T_OBJECT, rb_shape_root(rb_gc_heap_id_for_size(size)), size); + obj = rb_newobj_of_with_shape(klass, T_OBJECT, rb_shape_id_with_robject_layout(rb_shape_root(rb_gc_heap_id_for_size(size))), size); #if RUBY_DEBUG VALUE *ptr = ROBJECT_FIELDS(obj); @@ -1149,7 +1157,7 @@ typed_data_alloc(VALUE klass, VALUE typed_flag, void *datap, const rb_data_type_ RBIMPL_NONNULL_ARG(type); if (klass) rb_data_object_check(klass); bool wb_protected = (type->flags & RUBY_FL_WB_PROTECTED) || !type->function.dmark; - VALUE obj = rb_newobj(GET_EC(), klass, T_DATA, ROOT_SHAPE_ID, wb_protected, size); + VALUE obj = rb_newobj(GET_EC(), klass, T_DATA, ROOT_SHAPE_ID | SHAPE_ID_LAYOUT_RDATA, wb_protected, size); rb_gc_register_pinning_obj(obj); @@ -4189,7 +4197,11 @@ vm_weak_table_gen_fields_foreach(st_data_t key, st_data_t value, st_data_t data) break; case ST_DELETE: - RBASIC_SET_SHAPE_ID((VALUE)key, ROOT_SHAPE_ID); + // When we're removing an object from the weak ref table, we need to + // set the shape on it so that the GC finalizer won't try to remove + // it again. A "root shape" indicates to the GC that this object + // has no fields on it, hence it won't be in the gen fields table. + RBASIC_SET_SHAPE_ID((VALUE)key, ROOT_SHAPE_ID | SHAPE_ID_LAYOUT_OTHER); return ST_DELETE; case ST_REPLACE: { diff --git a/imemo.c b/imemo.c index 3448a8dcd3c54f..796e078c89565f 100644 --- a/imemo.c +++ b/imemo.c @@ -141,7 +141,11 @@ rb_imemo_fields_new(VALUE owner, shape_id_t shape_id, bool shareable) size_t embedded_size = offsetof(struct rb_fields, as.embed) + capa * sizeof(VALUE); RUBY_ASSERT(rb_gc_size_allocatable_p(embedded_size)); VALUE fields = rb_imemo_new(imemo_fields, owner, embedded_size, shareable); - RBASIC_SET_SHAPE_ID(fields, shape_id); + // imemo fields objects should always have "RObject" layout. The + // layout in the shape describes the layout of the thing on which it is set. + // Imemo fields have the same layout as robject, therefore the layout + // should reflect that fact. + RBASIC_SET_SHAPE_ID(fields, rb_shape_id_with_robject_layout(shape_id)); RUBY_ASSERT(IMEMO_TYPE_P(fields, imemo_fields)); return fields; } @@ -152,7 +156,11 @@ rb_imemo_fields_new_complex(VALUE owner, shape_id_t shape_id, size_t capa, bool VALUE fields = rb_imemo_new(imemo_fields, owner, sizeof(struct rb_fields), shareable); IMEMO_OBJ_FIELDS(fields)->as.complex.table = st_init_numtable_with_size(capa); FL_SET_RAW(fields, OBJ_FIELD_HEAP); - RBASIC_SET_SHAPE_ID(fields, shape_id); + // imemo fields objects should always have "RObject" layout. The + // layout in the shape describes the layout of the thing on which it is set. + // Imemo fields have the same layout as robject, therefore the layout + // should reflect that fact. + RBASIC_SET_SHAPE_ID(fields, rb_shape_id_with_robject_layout(shape_id)); return fields; } @@ -177,7 +185,11 @@ rb_imemo_fields_new_complex_tbl(VALUE owner, shape_id_t shape_id, st_table *tbl, VALUE fields = rb_imemo_new(imemo_fields, owner, sizeof(struct rb_fields), shareable); IMEMO_OBJ_FIELDS(fields)->as.complex.table = tbl; FL_SET_RAW(fields, OBJ_FIELD_HEAP); - RBASIC_SET_SHAPE_ID(fields, shape_id); + // imemo fields objects should always have "RObject" layout. The + // layout in the shape describes the layout of the thing on which it is set. + // Imemo fields have the same layout as robject, therefore the layout + // should reflect that fact. + RBASIC_SET_SHAPE_ID(fields, rb_shape_id_with_robject_layout(shape_id)); st_foreach(tbl, imemo_fields_trigger_wb_i, (st_data_t)fields); return fields; } diff --git a/internal/gc.h b/internal/gc.h index 41675810c722c4..b78808370cd33c 100644 --- a/internal/gc.h +++ b/internal/gc.h @@ -124,7 +124,7 @@ struct rb_objspace; /* in vm_core.h */ T *(var) = (T *)rb_ec_newobj_of((ec), (c), (f), s) #define NEWOBJ_OF(var, T, c, f, s) EC_NEWOBJ_OF(var, T, c, f, s, GET_EC()) #define UNPROTECTED_NEWOBJ_OF(var, T, c, f, s) \ - T *(var) = (T *)rb_newobj((GET_EC()), (c), (f), 0 /* ROOT_SHAPE_ID */, false, s) + T *(var) = (T *)rb_newobj((GET_EC()), (c), (f), ROOT_SHAPE_ID | SHAPE_ID_LAYOUT_OTHER, false, s) #ifndef RB_GC_OBJECT_METADATA_ENTRY_DEFINED # define RB_GC_OBJECT_METADATA_ENTRY_DEFINED diff --git a/ractor.c b/ractor.c index d611ca97c273df..f94c06cc738bf4 100644 --- a/ractor.c +++ b/ractor.c @@ -2015,7 +2015,7 @@ move_enter(VALUE obj, struct obj_traverse_replace_data *data) else { VALUE type = RB_BUILTIN_TYPE(obj); size_t slot_size = rb_gc_obj_slot_size(obj); - VALUE moved = rb_newobj(GET_EC(), 0, type, 0, wb_protected_types[type], slot_size); + VALUE moved = rb_newobj(GET_EC(), 0, type, RBASIC_SHAPE_ID(obj), wb_protected_types[type], slot_size); MEMZERO(((struct RBasic *)moved) + 1, char, slot_size - sizeof(struct RBasic)); data->replacement = (VALUE)moved; return traverse_cont; diff --git a/shape.c b/shape.c index 24f1394f6cd32f..7a02b230733043 100644 --- a/shape.c +++ b/shape.c @@ -409,10 +409,14 @@ rb_obj_shape_id(VALUE obj) if (BUILTIN_TYPE(obj) == T_CLASS || BUILTIN_TYPE(obj) == T_MODULE) { VALUE fields_obj = RCLASS_WRITABLE_FIELDS_OBJ(obj); + shape_id_t base = ROOT_SHAPE_ID; if (fields_obj) { - return RBASIC_SHAPE_ID(fields_obj); + // Remove the layout from the fields object. We want to + // combine the shape of the fields object with the layout of the + // class / module object. + base = RBASIC_SHAPE_ID(fields_obj) & ~SHAPE_ID_LAYOUT_MASK; } - return ROOT_SHAPE_ID; + return rb_shape_layout(RBASIC_SHAPE_ID(obj)) | base; } return RBASIC_SHAPE_ID(obj); } @@ -697,7 +701,7 @@ rb_shape_transition_object_id(shape_id_t original_shape_id) bool dont_care; rb_shape_t *shape = get_next_shape_internal(RSHAPE(original_shape_id), id_object_id, SHAPE_OBJ_ID, &dont_care, true); if (!shape) { - return ROOT_COMPLEX_WITH_OBJ_ID | RSHAPE_FLAGS(original_shape_id); + return rb_shape_layout(original_shape_id) | ROOT_COMPLEX_WITH_OBJ_ID | RSHAPE_FLAGS(original_shape_id); } RUBY_ASSERT(shape); @@ -1225,17 +1229,55 @@ rb_shape_foreach_field(shape_id_t initial_shape_id, rb_shape_foreach_transition_ } #if RUBY_DEBUG +/* + * Get the layout of this object. The "layout" indicates what strategy + * we should use for fetching instance variables from `obj`. It's based + * on the C struct layout for each particular object. + * + * TODO: make Struct have a similar layout to RDATA + */ +static shape_id_t +rb_shape_expected_layout(VALUE obj) +{ + switch (BUILTIN_TYPE(obj)) { + case T_OBJECT: + return SHAPE_ID_LAYOUT_ROBJECT; + case T_CLASS: + case T_MODULE: + if (FL_TEST_RAW(obj, RCLASS_BOXABLE)) { + return SHAPE_ID_LAYOUT_OTHER; + } + return SHAPE_ID_LAYOUT_RCLASS; + case T_DATA: + return SHAPE_ID_LAYOUT_RDATA; + case T_IMEMO: + if (IMEMO_TYPE_P(obj, imemo_fields)) { + return SHAPE_ID_LAYOUT_ROBJECT; + } + return SHAPE_ID_LAYOUT_OTHER; + default: + return SHAPE_ID_LAYOUT_OTHER; + } +} + bool rb_shape_verify_consistency(VALUE obj, shape_id_t shape_id) { - if (shape_id == ROOT_SHAPE_ID) { - return true; - } - if (shape_id == INVALID_SHAPE_ID) { rb_bug("Can't set INVALID_SHAPE_ID on an object"); } + shape_id_t actual_layout = rb_shape_layout(rb_obj_shape_id(obj)); + shape_id_t expected_layout = rb_shape_expected_layout(obj); + if (actual_layout != expected_layout) { + rb_bug("shape_id layout mismatch: expected=%x actual=%x shape_id=%u obj=%s", + expected_layout, actual_layout, shape_id, rb_obj_info(obj)); + } + + if (shape_id == ROOT_SHAPE_ID) { + return true; + } + rb_shape_t *shape = RSHAPE(shape_id); bool has_object_id = false; @@ -1249,13 +1291,11 @@ rb_shape_verify_consistency(VALUE obj, shape_id_t shape_id) if (rb_shape_has_object_id(shape_id)) { if (!has_object_id) { - rb_p(obj); rb_bug("shape_id claim having obj_id but doesn't shape_id=%u, obj=%s", shape_id, rb_obj_info(obj)); } } else { if (has_object_id) { - rb_p(obj); rb_bug("shape_id claim not having obj_id but it does shape_id=%u, obj=%s", shape_id, rb_obj_info(obj)); } } @@ -1329,6 +1369,25 @@ shape_has_object_id_p(VALUE self) return RBOOL(rb_shape_has_object_id(shape_id)); } +static VALUE +shape_layout(VALUE self) +{ + shape_id_t shape_id = NUM2UINT(rb_struct_getmember(self, rb_intern("id"))); + + switch (rb_shape_layout(shape_id)) { + case SHAPE_ID_LAYOUT_ROBJECT: + return ID2SYM(rb_intern("robject")); + case SHAPE_ID_LAYOUT_RCLASS: + return ID2SYM(rb_intern("rclass")); + case SHAPE_ID_LAYOUT_RDATA: + return ID2SYM(rb_intern("rdata")); + case SHAPE_ID_LAYOUT_OTHER: + return ID2SYM(rb_intern("other")); + default: + rb_bug("unknown shape layout: %u", rb_shape_layout(shape_id)); + } +} + static VALUE parse_key(ID key) { @@ -1628,6 +1687,7 @@ Init_shape(void) rb_define_method(rb_cShape, "complex?", shape_complex, 0); rb_define_method(rb_cShape, "shape_frozen?", shape_frozen, 0); rb_define_method(rb_cShape, "has_object_id?", shape_has_object_id_p, 0); + rb_define_method(rb_cShape, "layout", shape_layout, 0); rb_define_const(rb_cShape, "SHAPE_ROOT", INT2NUM(SHAPE_ROOT)); rb_define_const(rb_cShape, "SHAPE_IVAR", INT2NUM(SHAPE_IVAR)); diff --git a/shape.h b/shape.h index 61fadca5bace2d..a319449988e8fc 100644 --- a/shape.h +++ b/shape.h @@ -27,12 +27,14 @@ STATIC_ASSERT(shape_id_num_bits, SHAPE_ID_NUM_BITS == sizeof(shape_id_t) * CHAR_ // 19-22 SHAPE_ID_HEAP_INDEX_MASK // index in rb_shape_tree.capacities. Allow to access slot size. // Currently always 0 except for T_OBJECT. -// 23 SHAPE_ID_FL_FROZEN +// 23 SHAPE_ID_FL_COMPLEX +// The object is backed by a `st_table`. +// 24 SHAPE_ID_FL_FROZEN // Whether the object is frozen or not. -// 24 SHAPE_ID_FL_HAS_OBJECT_ID +// 25 SHAPE_ID_FL_HAS_OBJECT_ID // Whether the object has an `SHAPE_OBJ_ID` transition. -// 25 SHAPE_ID_FL_COMPLEX -// The object is backed by a `st_table`. +// 26-27 SHAPE_ID_LAYOUT_MASK +// The object's physical field layout. enum shape_id_fl_type { #define RBIMPL_SHAPE_ID_FL(n) (1<<(SHAPE_ID_FL_USHIFT+n)) @@ -43,8 +45,26 @@ enum shape_id_fl_type { SHAPE_ID_FL_FROZEN = RBIMPL_SHAPE_ID_FL(1), SHAPE_ID_FL_HAS_OBJECT_ID = RBIMPL_SHAPE_ID_FL(2), + // Means IVs are found at an offset from the object's addr, or in a + // malloc allocated side table + SHAPE_ID_LAYOUT_ROBJECT = 0, + + // Means this object is a class/module that is NOT RCLASS_BOXABLE, and IV's + // are found in the fields_obj found on the rclass struct + SHAPE_ID_LAYOUT_RCLASS = RBIMPL_SHAPE_ID_FL(3), + + // Means this object is an RData or RTypedData and IVs are found in the + // fields_obj found on the RData/RTypedData struct + SHAPE_ID_LAYOUT_RDATA = RBIMPL_SHAPE_ID_FL(4), + + // Means this is a complicated object: boxable classes, structs, objects + // that store IVs on the geniv table + SHAPE_ID_LAYOUT_OTHER = SHAPE_ID_LAYOUT_RCLASS | SHAPE_ID_LAYOUT_RDATA, + + SHAPE_ID_LAYOUT_MASK = SHAPE_ID_LAYOUT_OTHER, + SHAPE_ID_FL_NON_CANONICAL_MASK = SHAPE_ID_FL_FROZEN | SHAPE_ID_FL_HAS_OBJECT_ID, - SHAPE_ID_FLAGS_MASK = SHAPE_ID_HEAP_INDEX_MASK | SHAPE_ID_FL_NON_CANONICAL_MASK | SHAPE_ID_FL_COMPLEX, + SHAPE_ID_FLAGS_MASK = SHAPE_ID_HEAP_INDEX_MASK | SHAPE_ID_FL_NON_CANONICAL_MASK | SHAPE_ID_FL_COMPLEX | SHAPE_ID_LAYOUT_MASK, #undef RBIMPL_SHAPE_ID_FL }; @@ -55,12 +75,13 @@ enum shape_id_mask { SHAPE_ID_HAS_IVAR_MASK = SHAPE_ID_FL_COMPLEX | (SHAPE_ID_OFFSET_MASK - 1), }; -// The interpreter doesn't care about frozen status, slot size or object id when reading ivars. +// The interpreter doesn't care about frozen status, slot size, or object id, and +// has its own checks for physical field layout when reading ivars. // So we normalize shape_id by clearing these bits to improve cache hits. // JITs however might care about some of it. -#define SHAPE_ID_READ_ONLY_MASK (~(SHAPE_ID_FL_FROZEN | SHAPE_ID_HEAP_INDEX_MASK | SHAPE_ID_FL_HAS_OBJECT_ID)) +#define SHAPE_ID_READ_ONLY_MASK (~(SHAPE_ID_FL_FROZEN | SHAPE_ID_HEAP_INDEX_MASK | SHAPE_ID_FL_HAS_OBJECT_ID | SHAPE_ID_LAYOUT_MASK)) // For write it's the same idea, but here we do care about frozen status. -#define SHAPE_ID_WRITE_MASK (~(SHAPE_ID_HEAP_INDEX_MASK | SHAPE_ID_FL_HAS_OBJECT_ID)) +#define SHAPE_ID_WRITE_MASK (~(SHAPE_ID_HEAP_INDEX_MASK | SHAPE_ID_FL_HAS_OBJECT_ID | SHAPE_ID_LAYOUT_MASK)) typedef uint32_t redblack_id_t; @@ -153,11 +174,18 @@ RBASIC_SET_SHAPE_ID_NO_CHECKS(VALUE obj, shape_id_t shape_id) #endif } +static inline shape_id_t +rb_shape_layout(shape_id_t shape_id) +{ + return shape_id & SHAPE_ID_LAYOUT_MASK; +} + static inline void RBASIC_SET_SHAPE_ID(VALUE obj, shape_id_t shape_id) { RUBY_ASSERT(!RB_SPECIAL_CONST_P(obj)); RUBY_ASSERT(!RB_TYPE_P(obj, T_IMEMO) || IMEMO_TYPE_P(obj, imemo_fields)); + RUBY_ASSERT(!IMEMO_TYPE_P(obj, imemo_fields) || rb_shape_layout(shape_id) == SHAPE_ID_LAYOUT_ROBJECT); RBASIC_SET_SHAPE_ID_NO_CHECKS(obj, shape_id); @@ -232,6 +260,12 @@ rb_shape_canonical_p(shape_id_t shape_id) return !(shape_id & SHAPE_ID_FL_NON_CANONICAL_MASK); } +static inline shape_id_t +rb_shape_id_with_robject_layout(shape_id_t shape_id) +{ + return (shape_id & ~SHAPE_ID_LAYOUT_MASK) | SHAPE_ID_LAYOUT_ROBJECT; +} + static inline uint8_t rb_shape_heap_index(shape_id_t shape_id) { @@ -450,10 +484,10 @@ rb_shape_transition_frozen(shape_id_t shape_id) static inline shape_id_t rb_shape_transition_complex(shape_id_t shape_id) { - shape_id_t next_shape_id = ROOT_COMPLEX_SHAPE_ID; + shape_id_t next_shape_id = rb_shape_layout(shape_id) | ROOT_COMPLEX_SHAPE_ID; if (rb_shape_has_object_id(shape_id)) { - next_shape_id = ROOT_COMPLEX_WITH_OBJ_ID; + next_shape_id = rb_shape_layout(shape_id) | ROOT_COMPLEX_WITH_OBJ_ID; } uint8_t heap_index = rb_shape_heap_index(shape_id); diff --git a/string.c b/string.c index 6865b0d8e658f3..61d4f16679d26b 100644 --- a/string.c +++ b/string.c @@ -630,7 +630,7 @@ static VALUE setup_fake_str(struct RString *fake_str, const char *name, long len, int encidx) { fake_str->basic.flags = T_STRING|RSTRING_NOEMBED|STR_NOFREE|STR_FAKESTR; - RBASIC_SET_SHAPE_ID((VALUE)fake_str, ROOT_SHAPE_ID); + RBASIC_SET_SHAPE_ID((VALUE)fake_str, ROOT_SHAPE_ID | SHAPE_ID_LAYOUT_OTHER); if (!name) { RUBY_ASSERT_ALWAYS(len == 0); diff --git a/test/ruby/test_shapes.rb b/test/ruby/test_shapes.rb index ef5dbd9fb15ee5..bace69658adfb5 100644 --- a/test/ruby/test_shapes.rb +++ b/test/ruby/test_shapes.rb @@ -1067,6 +1067,37 @@ def test_new_obj_has_t_object_shape assert_nil shape.parent end + def test_shape_layout + assert_equal :robject, RubyVM::Shape.of(TestObject.new).layout + + if ENV["RUBY_BOX"] + assert_equal :other, RubyVM::Shape.of(Kernel).layout + assert_equal :other, RubyVM::Shape.of(String).layout + else + assert_equal :rclass, RubyVM::Shape.of(Kernel).layout + assert_equal :rclass, RubyVM::Shape.of(String).layout + end + + assert_equal :rclass, RubyVM::Shape.of(Class.new).layout + assert_equal :rclass, RubyVM::Shape.of(Module.new).layout + + klass = Class.new + assert_equal :rclass, RubyVM::Shape.of(klass).layout + klass.instance_variable_set(:@a, 123) + assert_equal :rclass, RubyVM::Shape.of(klass).layout + + assert_equal :rdata, RubyVM::Shape.of(Thread.current).layout + assert_equal :rdata, RubyVM::Shape.of(lambda {}).layout + + assert_equal :other, RubyVM::Shape.of(Struct.new(:x).new(1)).layout + assert_equal :other, RubyVM::Shape.of([]).layout + assert_equal :other, RubyVM::Shape.of("hello").layout + assert_equal :other, RubyVM::Shape.of(/foo/).layout + assert_equal :other, RubyVM::Shape.of(2..3).layout + assert_equal :other, RubyVM::Shape.of(2**67).layout + assert_equal :other, RubyVM::Shape.of(:"aaroniscool#{123}").layout + end + def test_str_has_root_shape assert_shape_equal(RubyVM::Shape.root_shape, RubyVM::Shape.of("")) end diff --git a/variable.c b/variable.c index 857d870413881f..687fa03631b6bb 100644 --- a/variable.c +++ b/variable.c @@ -1343,7 +1343,7 @@ rb_free_generic_ivar(VALUE obj) } } } - RBASIC_SET_SHAPE_ID(obj, ROOT_SHAPE_ID); + RBASIC_SET_SHAPE_ID(obj, rb_shape_layout(RBASIC_SHAPE_ID(obj)) | ROOT_SHAPE_ID); } } @@ -1395,7 +1395,7 @@ rb_obj_set_fields(VALUE obj, VALUE fields_obj, ID field_name, VALUE original_fie } } - RBASIC_SET_SHAPE_ID(obj, RBASIC_SHAPE_ID(fields_obj)); + RBASIC_SET_SHAPE_ID(obj, rb_shape_layout(RBASIC_SHAPE_ID(obj)) | RBASIC_SHAPE_ID(fields_obj)); } void @@ -1710,6 +1710,7 @@ rb_ivar_delete(VALUE obj, ID id, VALUE undef) size_t trailing_fields = new_fields_count - removed_index; MEMMOVE(&fields[removed_index], &fields[removed_index + 1], VALUE, trailing_fields); + RUBY_ASSERT(rb_shape_layout(next_shape_id) == SHAPE_ID_LAYOUT_ROBJECT); RBASIC_SET_SHAPE_ID(fields_obj, next_shape_id); if (FL_TEST_RAW(fields_obj, OBJ_FIELD_HEAP)) { @@ -1732,7 +1733,7 @@ rb_ivar_delete(VALUE obj, ID id, VALUE undef) } } - RBASIC_SET_SHAPE_ID(obj, next_shape_id); + RBASIC_SET_SHAPE_ID(obj, rb_shape_layout(RBASIC_SHAPE_ID(obj)) | next_shape_id); if (fields_obj != original_fields_obj) { switch (type) { case T_OBJECT: @@ -1843,7 +1844,7 @@ imemo_fields_set(VALUE owner, VALUE fields_obj, shape_id_t target_shape_id, ID f RUBY_ASSERT(field_name); st_insert(table, (st_data_t)field_name, (st_data_t)val); RB_OBJ_WRITTEN(fields_obj, Qundef, val); - RBASIC_SET_SHAPE_ID(fields_obj, target_shape_id); + RBASIC_SET_SHAPE_ID(fields_obj, rb_shape_id_with_robject_layout(target_shape_id)); } else { attr_index_t index = RSHAPE_INDEX(target_shape_id); @@ -1855,7 +1856,7 @@ imemo_fields_set(VALUE owner, VALUE fields_obj, shape_id_t target_shape_id, ID f RB_OBJ_WRITE(fields_obj, &table[index], val); if (index >= RSHAPE_LEN(current_shape_id)) { - RBASIC_SET_SHAPE_ID(fields_obj, target_shape_id); + RBASIC_SET_SHAPE_ID(fields_obj, rb_shape_id_with_robject_layout(target_shape_id)); } } @@ -2010,11 +2011,12 @@ rb_obj_freeze_inline(VALUE x) RB_FL_UNSET_RAW(x, FL_USER2 | FL_USER3); // STR_CHILLED } + // rb_obj_freeze_inline(String) shape_id_t shape_id = rb_obj_shape_transition_frozen(x); switch (BUILTIN_TYPE(x)) { case T_CLASS: case T_MODULE: - RBASIC_SET_SHAPE_ID(RCLASS_WRITABLE_ENSURE_FIELDS_OBJ(x), shape_id); + rb_obj_freeze_inline(RCLASS_WRITABLE_ENSURE_FIELDS_OBJ(x)); // FIXME: How to do multi-shape? RBASIC_SET_SHAPE_ID(x, shape_id); break; @@ -2303,7 +2305,7 @@ rb_copy_generic_ivar(VALUE dest, VALUE obj) } if (!RSHAPE_LEN(dest_shape_id)) { - RBASIC_SET_SHAPE_ID(dest, dest_shape_id); + RBASIC_SET_SHAPE_ID(dest, rb_shape_layout(RBASIC_SHAPE_ID(dest)) | dest_shape_id); return; } @@ -4636,6 +4638,7 @@ class_fields_ivar_set(VALUE klass, VALUE fields_obj, ID id, VALUE val, bool conc } if (new_ivar) { + RUBY_ASSERT(rb_shape_layout(next_shape_id) == SHAPE_ID_LAYOUT_ROBJECT); RBASIC_SET_SHAPE_ID(fields_obj, next_shape_id); } } @@ -4658,6 +4661,7 @@ class_fields_ivar_set(VALUE klass, VALUE fields_obj, ID id, VALUE val, bool conc RB_OBJ_WRITTEN(fields_obj, Qundef, val); if (fields_obj != original_fields_obj) { + RUBY_ASSERT(rb_shape_layout(next_shape_id) == SHAPE_ID_LAYOUT_ROBJECT); RBASIC_SET_SHAPE_ID(fields_obj, next_shape_id); } } @@ -4684,7 +4688,7 @@ class_ivar_set(VALUE obj, ID id, VALUE val, bool *new_ivar) // TODO: What should we set as the T_CLASS shape_id? // In most case we can replicate the single `fields_obj` shape // but in namespaced case? Perhaps INVALID_SHAPE_ID? - RBASIC_SET_SHAPE_ID(obj, RBASIC_SHAPE_ID(new_fields_obj)); + RBASIC_SET_SHAPE_ID(obj, rb_shape_layout(RBASIC_SHAPE_ID(obj)) | RBASIC_SHAPE_ID(new_fields_obj)); return index; } @@ -4709,7 +4713,7 @@ rb_fields_tbl_copy(VALUE dst, VALUE src) VALUE fields_obj = RCLASS_WRITABLE_FIELDS_OBJ(src); if (fields_obj) { RCLASS_WRITABLE_SET_FIELDS_OBJ(dst, rb_imemo_fields_clone(fields_obj)); - RBASIC_SET_SHAPE_ID(dst, RBASIC_SHAPE_ID(src)); + RBASIC_SET_SHAPE_ID(dst, rb_shape_layout(RBASIC_SHAPE_ID(dst)) | RBASIC_SHAPE_ID(src)); } } diff --git a/vm_insnhelper.c b/vm_insnhelper.c index ac0d81092d890d..f515662bf07765 100644 --- a/vm_insnhelper.c +++ b/vm_insnhelper.c @@ -1412,7 +1412,8 @@ vm_setivar_class(VALUE obj, VALUE val, rb_setivar_cache cache) RB_OBJ_WRITE(fields_obj, &rb_imemo_fields_ptr(fields_obj)[cache.index], val); if (shape_id != dest_shape_id) { - RBASIC_SET_SHAPE_ID(obj, dest_shape_id); + // The dest_shape_id comes from the fields_obj + RBASIC_SET_SHAPE_ID(obj, SHAPE_ID_LAYOUT_RCLASS | (dest_shape_id & ~SHAPE_ID_LAYOUT_MASK)); RBASIC_SET_SHAPE_ID(fields_obj, dest_shape_id); } @@ -1437,7 +1438,9 @@ vm_setivar_default(VALUE obj, ID id, VALUE val, rb_setivar_cache cache) if (shape_id != dest_shape_id) { RBASIC_SET_SHAPE_ID(obj, dest_shape_id); - RBASIC_SET_SHAPE_ID(fields_obj, dest_shape_id); + // The dest_shape_id comes from the owner, but fields_obj must always + // have layout RObject, so give the fields_object the right layout. + RBASIC_SET_SHAPE_ID(fields_obj, rb_shape_id_with_robject_layout(dest_shape_id)); } RB_DEBUG_COUNTER_INC(ivar_set_ic_hit); diff --git a/zjit/src/cruby_bindings.inc.rs b/zjit/src/cruby_bindings.inc.rs index bd832acc9604cd..08c502b0d84e47 100644 --- a/zjit/src/cruby_bindings.inc.rs +++ b/zjit/src/cruby_bindings.inc.rs @@ -1491,8 +1491,13 @@ pub const SHAPE_ID_HEAP_INDEX_MASK: shape_id_fl_type = 7864320; pub const SHAPE_ID_FL_COMPLEX: shape_id_fl_type = 8388608; pub const SHAPE_ID_FL_FROZEN: shape_id_fl_type = 16777216; pub const SHAPE_ID_FL_HAS_OBJECT_ID: shape_id_fl_type = 33554432; +pub const SHAPE_ID_LAYOUT_ROBJECT: shape_id_fl_type = 0; +pub const SHAPE_ID_LAYOUT_RCLASS: shape_id_fl_type = 67108864; +pub const SHAPE_ID_LAYOUT_RDATA: shape_id_fl_type = 134217728; +pub const SHAPE_ID_LAYOUT_OTHER: shape_id_fl_type = 201326592; +pub const SHAPE_ID_LAYOUT_MASK: shape_id_fl_type = 201326592; pub const SHAPE_ID_FL_NON_CANONICAL_MASK: shape_id_fl_type = 50331648; -pub const SHAPE_ID_FLAGS_MASK: shape_id_fl_type = 66584576; +pub const SHAPE_ID_FLAGS_MASK: shape_id_fl_type = 267911168; pub type shape_id_fl_type = u32; pub const CONST_DEPRECATED: rb_const_flag_t = 256; pub const CONST_VISIBILITY_MASK: rb_const_flag_t = 255; From ddb5055d961d970aded287cfebd07b78efee3ca7 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Fri, 29 May 2026 16:41:31 -0700 Subject: [PATCH 003/188] Revert "Reserve 2 bits for expressing object layout (#17139)" This reverts commit 63d9f090b5d9461cf0b9446e0039d9c56156b826. --- array.c | 2 - class.c | 10 +---- depend | 1 - gc.c | 24 +++-------- imemo.c | 18 ++------ internal/gc.h | 2 +- ractor.c | 2 +- shape.c | 78 ++++------------------------------ shape.h | 54 +++++------------------ string.c | 2 +- test/ruby/test_shapes.rb | 31 -------------- variable.c | 22 ++++------ vm_insnhelper.c | 7 +-- zjit/src/cruby_bindings.inc.rs | 7 +-- 14 files changed, 44 insertions(+), 216 deletions(-) diff --git a/array.c b/array.c index 7f06e3c9c72f66..08d8d17c90f79f 100644 --- a/array.c +++ b/array.c @@ -30,7 +30,6 @@ #include "ruby/thread.h" #include "ruby/util.h" #include "ruby/ractor.h" -#include "shape.h" #include "vm_core.h" #include "builtin.h" @@ -910,7 +909,6 @@ init_fake_ary_flags(void) struct RArray fake_ary = {0}; fake_ary.basic.flags = T_ARRAY; VALUE ary = (VALUE)&fake_ary; - RBASIC_SET_SHAPE_ID(ary, ROOT_SHAPE_ID | SHAPE_ID_LAYOUT_OTHER); rb_ary_freeze(ary); return fake_ary.basic.flags; } diff --git a/class.c b/class.c index 02078cc9bc8baf..69194ccab23756 100644 --- a/class.c +++ b/class.c @@ -585,15 +585,7 @@ class_alloc0(enum ruby_value_type type, VALUE klass, bool boxable) VALUE flags = type | FL_SHAREABLE; if (boxable) flags |= RCLASS_BOXABLE; - shape_id_t shape_id = ROOT_SHAPE_ID; - if (boxable) { - shape_id |= SHAPE_ID_LAYOUT_OTHER; - } - else { - shape_id |= SHAPE_ID_LAYOUT_RCLASS; - } - - struct RClass *obj = (struct RClass *)rb_newobj(GET_EC(), klass, flags, shape_id, true, alloc_size); + NEWOBJ_OF(obj, struct RClass, klass, flags, alloc_size); obj->object_id = 0; diff --git a/depend b/depend index a2e8312298e7ea..e61b6856704712 100644 --- a/depend +++ b/depend @@ -80,7 +80,6 @@ array.$(OBJEXT): $(top_srcdir)/internal/sanitizers.h array.$(OBJEXT): $(top_srcdir)/internal/serial.h array.$(OBJEXT): $(top_srcdir)/internal/set_table.h array.$(OBJEXT): $(top_srcdir)/internal/static_assert.h -array.$(OBJEXT): $(top_srcdir)/internal/struct.h array.$(OBJEXT): $(top_srcdir)/internal/variable.h array.$(OBJEXT): $(top_srcdir)/internal/vm.h array.$(OBJEXT): $(top_srcdir)/internal/warnings.h diff --git a/gc.c b/gc.c index 6357577e63705f..9eb72329404a3b 100644 --- a/gc.c +++ b/gc.c @@ -1056,15 +1056,7 @@ rb_newobj(rb_execution_context_t *ec, VALUE klass, VALUE flags, shape_id_t shape VALUE rb_ec_newobj_of(rb_execution_context_t *ec, VALUE klass, VALUE flags, size_t size) { - VALUE type = flags & T_MASK; - RUBY_ASSERT(type != T_OBJECT); - RUBY_ASSERT(type != T_DATA); - RUBY_ASSERT(type != T_CLASS); - RUBY_ASSERT(type != T_MODULE); - RUBY_ASSERT(type != T_ICLASS); - (void)type; - - return rb_newobj(ec, klass, flags, ROOT_SHAPE_ID | SHAPE_ID_LAYOUT_OTHER, true, size); + return rb_newobj(ec, klass, flags, ROOT_SHAPE_ID, true, size); } VALUE @@ -1076,13 +1068,13 @@ rb_newobj_of_with_shape(VALUE klass, VALUE flags, shape_id_t shape_id, size_t si VALUE rb_newobj_of(VALUE klass, VALUE flags, size_t size) { - return rb_newobj(GET_EC(), klass, flags, ROOT_SHAPE_ID | SHAPE_ID_LAYOUT_OTHER, true, size); + return rb_newobj(GET_EC(), klass, flags, ROOT_SHAPE_ID, true, size); } static VALUE class_allocate_complex_instance(VALUE klass, uint32_t capacity) { - shape_id_t initial_shape_id = rb_shape_id_with_robject_layout(rb_shape_root(rb_gc_heap_id_for_size(sizeof(struct RObject)))); + shape_id_t initial_shape_id = rb_shape_root(rb_gc_heap_id_for_size(sizeof(struct RObject))); VALUE obj = rb_newobj_of_with_shape(klass, T_OBJECT, initial_shape_id, sizeof(struct RObject)); rb_obj_init_complex(obj, rb_st_init_numtable_with_size(capacity)); return obj; @@ -1107,7 +1099,7 @@ rb_class_allocate_instance(VALUE klass) // There might be a NEWOBJ tracepoint callback, and it may set fields. // So the shape must be passed to `NEWOBJ_OF`. - obj = rb_newobj_of_with_shape(klass, T_OBJECT, rb_shape_id_with_robject_layout(rb_shape_root(rb_gc_heap_id_for_size(size))), size); + obj = rb_newobj_of_with_shape(klass, T_OBJECT, rb_shape_root(rb_gc_heap_id_for_size(size)), size); #if RUBY_DEBUG VALUE *ptr = ROBJECT_FIELDS(obj); @@ -1157,7 +1149,7 @@ typed_data_alloc(VALUE klass, VALUE typed_flag, void *datap, const rb_data_type_ RBIMPL_NONNULL_ARG(type); if (klass) rb_data_object_check(klass); bool wb_protected = (type->flags & RUBY_FL_WB_PROTECTED) || !type->function.dmark; - VALUE obj = rb_newobj(GET_EC(), klass, T_DATA, ROOT_SHAPE_ID | SHAPE_ID_LAYOUT_RDATA, wb_protected, size); + VALUE obj = rb_newobj(GET_EC(), klass, T_DATA, ROOT_SHAPE_ID, wb_protected, size); rb_gc_register_pinning_obj(obj); @@ -4197,11 +4189,7 @@ vm_weak_table_gen_fields_foreach(st_data_t key, st_data_t value, st_data_t data) break; case ST_DELETE: - // When we're removing an object from the weak ref table, we need to - // set the shape on it so that the GC finalizer won't try to remove - // it again. A "root shape" indicates to the GC that this object - // has no fields on it, hence it won't be in the gen fields table. - RBASIC_SET_SHAPE_ID((VALUE)key, ROOT_SHAPE_ID | SHAPE_ID_LAYOUT_OTHER); + RBASIC_SET_SHAPE_ID((VALUE)key, ROOT_SHAPE_ID); return ST_DELETE; case ST_REPLACE: { diff --git a/imemo.c b/imemo.c index 796e078c89565f..3448a8dcd3c54f 100644 --- a/imemo.c +++ b/imemo.c @@ -141,11 +141,7 @@ rb_imemo_fields_new(VALUE owner, shape_id_t shape_id, bool shareable) size_t embedded_size = offsetof(struct rb_fields, as.embed) + capa * sizeof(VALUE); RUBY_ASSERT(rb_gc_size_allocatable_p(embedded_size)); VALUE fields = rb_imemo_new(imemo_fields, owner, embedded_size, shareable); - // imemo fields objects should always have "RObject" layout. The - // layout in the shape describes the layout of the thing on which it is set. - // Imemo fields have the same layout as robject, therefore the layout - // should reflect that fact. - RBASIC_SET_SHAPE_ID(fields, rb_shape_id_with_robject_layout(shape_id)); + RBASIC_SET_SHAPE_ID(fields, shape_id); RUBY_ASSERT(IMEMO_TYPE_P(fields, imemo_fields)); return fields; } @@ -156,11 +152,7 @@ rb_imemo_fields_new_complex(VALUE owner, shape_id_t shape_id, size_t capa, bool VALUE fields = rb_imemo_new(imemo_fields, owner, sizeof(struct rb_fields), shareable); IMEMO_OBJ_FIELDS(fields)->as.complex.table = st_init_numtable_with_size(capa); FL_SET_RAW(fields, OBJ_FIELD_HEAP); - // imemo fields objects should always have "RObject" layout. The - // layout in the shape describes the layout of the thing on which it is set. - // Imemo fields have the same layout as robject, therefore the layout - // should reflect that fact. - RBASIC_SET_SHAPE_ID(fields, rb_shape_id_with_robject_layout(shape_id)); + RBASIC_SET_SHAPE_ID(fields, shape_id); return fields; } @@ -185,11 +177,7 @@ rb_imemo_fields_new_complex_tbl(VALUE owner, shape_id_t shape_id, st_table *tbl, VALUE fields = rb_imemo_new(imemo_fields, owner, sizeof(struct rb_fields), shareable); IMEMO_OBJ_FIELDS(fields)->as.complex.table = tbl; FL_SET_RAW(fields, OBJ_FIELD_HEAP); - // imemo fields objects should always have "RObject" layout. The - // layout in the shape describes the layout of the thing on which it is set. - // Imemo fields have the same layout as robject, therefore the layout - // should reflect that fact. - RBASIC_SET_SHAPE_ID(fields, rb_shape_id_with_robject_layout(shape_id)); + RBASIC_SET_SHAPE_ID(fields, shape_id); st_foreach(tbl, imemo_fields_trigger_wb_i, (st_data_t)fields); return fields; } diff --git a/internal/gc.h b/internal/gc.h index b78808370cd33c..41675810c722c4 100644 --- a/internal/gc.h +++ b/internal/gc.h @@ -124,7 +124,7 @@ struct rb_objspace; /* in vm_core.h */ T *(var) = (T *)rb_ec_newobj_of((ec), (c), (f), s) #define NEWOBJ_OF(var, T, c, f, s) EC_NEWOBJ_OF(var, T, c, f, s, GET_EC()) #define UNPROTECTED_NEWOBJ_OF(var, T, c, f, s) \ - T *(var) = (T *)rb_newobj((GET_EC()), (c), (f), ROOT_SHAPE_ID | SHAPE_ID_LAYOUT_OTHER, false, s) + T *(var) = (T *)rb_newobj((GET_EC()), (c), (f), 0 /* ROOT_SHAPE_ID */, false, s) #ifndef RB_GC_OBJECT_METADATA_ENTRY_DEFINED # define RB_GC_OBJECT_METADATA_ENTRY_DEFINED diff --git a/ractor.c b/ractor.c index f94c06cc738bf4..d611ca97c273df 100644 --- a/ractor.c +++ b/ractor.c @@ -2015,7 +2015,7 @@ move_enter(VALUE obj, struct obj_traverse_replace_data *data) else { VALUE type = RB_BUILTIN_TYPE(obj); size_t slot_size = rb_gc_obj_slot_size(obj); - VALUE moved = rb_newobj(GET_EC(), 0, type, RBASIC_SHAPE_ID(obj), wb_protected_types[type], slot_size); + VALUE moved = rb_newobj(GET_EC(), 0, type, 0, wb_protected_types[type], slot_size); MEMZERO(((struct RBasic *)moved) + 1, char, slot_size - sizeof(struct RBasic)); data->replacement = (VALUE)moved; return traverse_cont; diff --git a/shape.c b/shape.c index 7a02b230733043..24f1394f6cd32f 100644 --- a/shape.c +++ b/shape.c @@ -409,14 +409,10 @@ rb_obj_shape_id(VALUE obj) if (BUILTIN_TYPE(obj) == T_CLASS || BUILTIN_TYPE(obj) == T_MODULE) { VALUE fields_obj = RCLASS_WRITABLE_FIELDS_OBJ(obj); - shape_id_t base = ROOT_SHAPE_ID; if (fields_obj) { - // Remove the layout from the fields object. We want to - // combine the shape of the fields object with the layout of the - // class / module object. - base = RBASIC_SHAPE_ID(fields_obj) & ~SHAPE_ID_LAYOUT_MASK; + return RBASIC_SHAPE_ID(fields_obj); } - return rb_shape_layout(RBASIC_SHAPE_ID(obj)) | base; + return ROOT_SHAPE_ID; } return RBASIC_SHAPE_ID(obj); } @@ -701,7 +697,7 @@ rb_shape_transition_object_id(shape_id_t original_shape_id) bool dont_care; rb_shape_t *shape = get_next_shape_internal(RSHAPE(original_shape_id), id_object_id, SHAPE_OBJ_ID, &dont_care, true); if (!shape) { - return rb_shape_layout(original_shape_id) | ROOT_COMPLEX_WITH_OBJ_ID | RSHAPE_FLAGS(original_shape_id); + return ROOT_COMPLEX_WITH_OBJ_ID | RSHAPE_FLAGS(original_shape_id); } RUBY_ASSERT(shape); @@ -1229,55 +1225,17 @@ rb_shape_foreach_field(shape_id_t initial_shape_id, rb_shape_foreach_transition_ } #if RUBY_DEBUG -/* - * Get the layout of this object. The "layout" indicates what strategy - * we should use for fetching instance variables from `obj`. It's based - * on the C struct layout for each particular object. - * - * TODO: make Struct have a similar layout to RDATA - */ -static shape_id_t -rb_shape_expected_layout(VALUE obj) -{ - switch (BUILTIN_TYPE(obj)) { - case T_OBJECT: - return SHAPE_ID_LAYOUT_ROBJECT; - case T_CLASS: - case T_MODULE: - if (FL_TEST_RAW(obj, RCLASS_BOXABLE)) { - return SHAPE_ID_LAYOUT_OTHER; - } - return SHAPE_ID_LAYOUT_RCLASS; - case T_DATA: - return SHAPE_ID_LAYOUT_RDATA; - case T_IMEMO: - if (IMEMO_TYPE_P(obj, imemo_fields)) { - return SHAPE_ID_LAYOUT_ROBJECT; - } - return SHAPE_ID_LAYOUT_OTHER; - default: - return SHAPE_ID_LAYOUT_OTHER; - } -} - bool rb_shape_verify_consistency(VALUE obj, shape_id_t shape_id) { - if (shape_id == INVALID_SHAPE_ID) { - rb_bug("Can't set INVALID_SHAPE_ID on an object"); - } - - shape_id_t actual_layout = rb_shape_layout(rb_obj_shape_id(obj)); - shape_id_t expected_layout = rb_shape_expected_layout(obj); - if (actual_layout != expected_layout) { - rb_bug("shape_id layout mismatch: expected=%x actual=%x shape_id=%u obj=%s", - expected_layout, actual_layout, shape_id, rb_obj_info(obj)); - } - if (shape_id == ROOT_SHAPE_ID) { return true; } + if (shape_id == INVALID_SHAPE_ID) { + rb_bug("Can't set INVALID_SHAPE_ID on an object"); + } + rb_shape_t *shape = RSHAPE(shape_id); bool has_object_id = false; @@ -1291,11 +1249,13 @@ rb_shape_verify_consistency(VALUE obj, shape_id_t shape_id) if (rb_shape_has_object_id(shape_id)) { if (!has_object_id) { + rb_p(obj); rb_bug("shape_id claim having obj_id but doesn't shape_id=%u, obj=%s", shape_id, rb_obj_info(obj)); } } else { if (has_object_id) { + rb_p(obj); rb_bug("shape_id claim not having obj_id but it does shape_id=%u, obj=%s", shape_id, rb_obj_info(obj)); } } @@ -1369,25 +1329,6 @@ shape_has_object_id_p(VALUE self) return RBOOL(rb_shape_has_object_id(shape_id)); } -static VALUE -shape_layout(VALUE self) -{ - shape_id_t shape_id = NUM2UINT(rb_struct_getmember(self, rb_intern("id"))); - - switch (rb_shape_layout(shape_id)) { - case SHAPE_ID_LAYOUT_ROBJECT: - return ID2SYM(rb_intern("robject")); - case SHAPE_ID_LAYOUT_RCLASS: - return ID2SYM(rb_intern("rclass")); - case SHAPE_ID_LAYOUT_RDATA: - return ID2SYM(rb_intern("rdata")); - case SHAPE_ID_LAYOUT_OTHER: - return ID2SYM(rb_intern("other")); - default: - rb_bug("unknown shape layout: %u", rb_shape_layout(shape_id)); - } -} - static VALUE parse_key(ID key) { @@ -1687,7 +1628,6 @@ Init_shape(void) rb_define_method(rb_cShape, "complex?", shape_complex, 0); rb_define_method(rb_cShape, "shape_frozen?", shape_frozen, 0); rb_define_method(rb_cShape, "has_object_id?", shape_has_object_id_p, 0); - rb_define_method(rb_cShape, "layout", shape_layout, 0); rb_define_const(rb_cShape, "SHAPE_ROOT", INT2NUM(SHAPE_ROOT)); rb_define_const(rb_cShape, "SHAPE_IVAR", INT2NUM(SHAPE_IVAR)); diff --git a/shape.h b/shape.h index a319449988e8fc..61fadca5bace2d 100644 --- a/shape.h +++ b/shape.h @@ -27,14 +27,12 @@ STATIC_ASSERT(shape_id_num_bits, SHAPE_ID_NUM_BITS == sizeof(shape_id_t) * CHAR_ // 19-22 SHAPE_ID_HEAP_INDEX_MASK // index in rb_shape_tree.capacities. Allow to access slot size. // Currently always 0 except for T_OBJECT. -// 23 SHAPE_ID_FL_COMPLEX -// The object is backed by a `st_table`. -// 24 SHAPE_ID_FL_FROZEN +// 23 SHAPE_ID_FL_FROZEN // Whether the object is frozen or not. -// 25 SHAPE_ID_FL_HAS_OBJECT_ID +// 24 SHAPE_ID_FL_HAS_OBJECT_ID // Whether the object has an `SHAPE_OBJ_ID` transition. -// 26-27 SHAPE_ID_LAYOUT_MASK -// The object's physical field layout. +// 25 SHAPE_ID_FL_COMPLEX +// The object is backed by a `st_table`. enum shape_id_fl_type { #define RBIMPL_SHAPE_ID_FL(n) (1<<(SHAPE_ID_FL_USHIFT+n)) @@ -45,26 +43,8 @@ enum shape_id_fl_type { SHAPE_ID_FL_FROZEN = RBIMPL_SHAPE_ID_FL(1), SHAPE_ID_FL_HAS_OBJECT_ID = RBIMPL_SHAPE_ID_FL(2), - // Means IVs are found at an offset from the object's addr, or in a - // malloc allocated side table - SHAPE_ID_LAYOUT_ROBJECT = 0, - - // Means this object is a class/module that is NOT RCLASS_BOXABLE, and IV's - // are found in the fields_obj found on the rclass struct - SHAPE_ID_LAYOUT_RCLASS = RBIMPL_SHAPE_ID_FL(3), - - // Means this object is an RData or RTypedData and IVs are found in the - // fields_obj found on the RData/RTypedData struct - SHAPE_ID_LAYOUT_RDATA = RBIMPL_SHAPE_ID_FL(4), - - // Means this is a complicated object: boxable classes, structs, objects - // that store IVs on the geniv table - SHAPE_ID_LAYOUT_OTHER = SHAPE_ID_LAYOUT_RCLASS | SHAPE_ID_LAYOUT_RDATA, - - SHAPE_ID_LAYOUT_MASK = SHAPE_ID_LAYOUT_OTHER, - SHAPE_ID_FL_NON_CANONICAL_MASK = SHAPE_ID_FL_FROZEN | SHAPE_ID_FL_HAS_OBJECT_ID, - SHAPE_ID_FLAGS_MASK = SHAPE_ID_HEAP_INDEX_MASK | SHAPE_ID_FL_NON_CANONICAL_MASK | SHAPE_ID_FL_COMPLEX | SHAPE_ID_LAYOUT_MASK, + SHAPE_ID_FLAGS_MASK = SHAPE_ID_HEAP_INDEX_MASK | SHAPE_ID_FL_NON_CANONICAL_MASK | SHAPE_ID_FL_COMPLEX, #undef RBIMPL_SHAPE_ID_FL }; @@ -75,13 +55,12 @@ enum shape_id_mask { SHAPE_ID_HAS_IVAR_MASK = SHAPE_ID_FL_COMPLEX | (SHAPE_ID_OFFSET_MASK - 1), }; -// The interpreter doesn't care about frozen status, slot size, or object id, and -// has its own checks for physical field layout when reading ivars. +// The interpreter doesn't care about frozen status, slot size or object id when reading ivars. // So we normalize shape_id by clearing these bits to improve cache hits. // JITs however might care about some of it. -#define SHAPE_ID_READ_ONLY_MASK (~(SHAPE_ID_FL_FROZEN | SHAPE_ID_HEAP_INDEX_MASK | SHAPE_ID_FL_HAS_OBJECT_ID | SHAPE_ID_LAYOUT_MASK)) +#define SHAPE_ID_READ_ONLY_MASK (~(SHAPE_ID_FL_FROZEN | SHAPE_ID_HEAP_INDEX_MASK | SHAPE_ID_FL_HAS_OBJECT_ID)) // For write it's the same idea, but here we do care about frozen status. -#define SHAPE_ID_WRITE_MASK (~(SHAPE_ID_HEAP_INDEX_MASK | SHAPE_ID_FL_HAS_OBJECT_ID | SHAPE_ID_LAYOUT_MASK)) +#define SHAPE_ID_WRITE_MASK (~(SHAPE_ID_HEAP_INDEX_MASK | SHAPE_ID_FL_HAS_OBJECT_ID)) typedef uint32_t redblack_id_t; @@ -174,18 +153,11 @@ RBASIC_SET_SHAPE_ID_NO_CHECKS(VALUE obj, shape_id_t shape_id) #endif } -static inline shape_id_t -rb_shape_layout(shape_id_t shape_id) -{ - return shape_id & SHAPE_ID_LAYOUT_MASK; -} - static inline void RBASIC_SET_SHAPE_ID(VALUE obj, shape_id_t shape_id) { RUBY_ASSERT(!RB_SPECIAL_CONST_P(obj)); RUBY_ASSERT(!RB_TYPE_P(obj, T_IMEMO) || IMEMO_TYPE_P(obj, imemo_fields)); - RUBY_ASSERT(!IMEMO_TYPE_P(obj, imemo_fields) || rb_shape_layout(shape_id) == SHAPE_ID_LAYOUT_ROBJECT); RBASIC_SET_SHAPE_ID_NO_CHECKS(obj, shape_id); @@ -260,12 +232,6 @@ rb_shape_canonical_p(shape_id_t shape_id) return !(shape_id & SHAPE_ID_FL_NON_CANONICAL_MASK); } -static inline shape_id_t -rb_shape_id_with_robject_layout(shape_id_t shape_id) -{ - return (shape_id & ~SHAPE_ID_LAYOUT_MASK) | SHAPE_ID_LAYOUT_ROBJECT; -} - static inline uint8_t rb_shape_heap_index(shape_id_t shape_id) { @@ -484,10 +450,10 @@ rb_shape_transition_frozen(shape_id_t shape_id) static inline shape_id_t rb_shape_transition_complex(shape_id_t shape_id) { - shape_id_t next_shape_id = rb_shape_layout(shape_id) | ROOT_COMPLEX_SHAPE_ID; + shape_id_t next_shape_id = ROOT_COMPLEX_SHAPE_ID; if (rb_shape_has_object_id(shape_id)) { - next_shape_id = rb_shape_layout(shape_id) | ROOT_COMPLEX_WITH_OBJ_ID; + next_shape_id = ROOT_COMPLEX_WITH_OBJ_ID; } uint8_t heap_index = rb_shape_heap_index(shape_id); diff --git a/string.c b/string.c index 61d4f16679d26b..6865b0d8e658f3 100644 --- a/string.c +++ b/string.c @@ -630,7 +630,7 @@ static VALUE setup_fake_str(struct RString *fake_str, const char *name, long len, int encidx) { fake_str->basic.flags = T_STRING|RSTRING_NOEMBED|STR_NOFREE|STR_FAKESTR; - RBASIC_SET_SHAPE_ID((VALUE)fake_str, ROOT_SHAPE_ID | SHAPE_ID_LAYOUT_OTHER); + RBASIC_SET_SHAPE_ID((VALUE)fake_str, ROOT_SHAPE_ID); if (!name) { RUBY_ASSERT_ALWAYS(len == 0); diff --git a/test/ruby/test_shapes.rb b/test/ruby/test_shapes.rb index bace69658adfb5..ef5dbd9fb15ee5 100644 --- a/test/ruby/test_shapes.rb +++ b/test/ruby/test_shapes.rb @@ -1067,37 +1067,6 @@ def test_new_obj_has_t_object_shape assert_nil shape.parent end - def test_shape_layout - assert_equal :robject, RubyVM::Shape.of(TestObject.new).layout - - if ENV["RUBY_BOX"] - assert_equal :other, RubyVM::Shape.of(Kernel).layout - assert_equal :other, RubyVM::Shape.of(String).layout - else - assert_equal :rclass, RubyVM::Shape.of(Kernel).layout - assert_equal :rclass, RubyVM::Shape.of(String).layout - end - - assert_equal :rclass, RubyVM::Shape.of(Class.new).layout - assert_equal :rclass, RubyVM::Shape.of(Module.new).layout - - klass = Class.new - assert_equal :rclass, RubyVM::Shape.of(klass).layout - klass.instance_variable_set(:@a, 123) - assert_equal :rclass, RubyVM::Shape.of(klass).layout - - assert_equal :rdata, RubyVM::Shape.of(Thread.current).layout - assert_equal :rdata, RubyVM::Shape.of(lambda {}).layout - - assert_equal :other, RubyVM::Shape.of(Struct.new(:x).new(1)).layout - assert_equal :other, RubyVM::Shape.of([]).layout - assert_equal :other, RubyVM::Shape.of("hello").layout - assert_equal :other, RubyVM::Shape.of(/foo/).layout - assert_equal :other, RubyVM::Shape.of(2..3).layout - assert_equal :other, RubyVM::Shape.of(2**67).layout - assert_equal :other, RubyVM::Shape.of(:"aaroniscool#{123}").layout - end - def test_str_has_root_shape assert_shape_equal(RubyVM::Shape.root_shape, RubyVM::Shape.of("")) end diff --git a/variable.c b/variable.c index 687fa03631b6bb..857d870413881f 100644 --- a/variable.c +++ b/variable.c @@ -1343,7 +1343,7 @@ rb_free_generic_ivar(VALUE obj) } } } - RBASIC_SET_SHAPE_ID(obj, rb_shape_layout(RBASIC_SHAPE_ID(obj)) | ROOT_SHAPE_ID); + RBASIC_SET_SHAPE_ID(obj, ROOT_SHAPE_ID); } } @@ -1395,7 +1395,7 @@ rb_obj_set_fields(VALUE obj, VALUE fields_obj, ID field_name, VALUE original_fie } } - RBASIC_SET_SHAPE_ID(obj, rb_shape_layout(RBASIC_SHAPE_ID(obj)) | RBASIC_SHAPE_ID(fields_obj)); + RBASIC_SET_SHAPE_ID(obj, RBASIC_SHAPE_ID(fields_obj)); } void @@ -1710,7 +1710,6 @@ rb_ivar_delete(VALUE obj, ID id, VALUE undef) size_t trailing_fields = new_fields_count - removed_index; MEMMOVE(&fields[removed_index], &fields[removed_index + 1], VALUE, trailing_fields); - RUBY_ASSERT(rb_shape_layout(next_shape_id) == SHAPE_ID_LAYOUT_ROBJECT); RBASIC_SET_SHAPE_ID(fields_obj, next_shape_id); if (FL_TEST_RAW(fields_obj, OBJ_FIELD_HEAP)) { @@ -1733,7 +1732,7 @@ rb_ivar_delete(VALUE obj, ID id, VALUE undef) } } - RBASIC_SET_SHAPE_ID(obj, rb_shape_layout(RBASIC_SHAPE_ID(obj)) | next_shape_id); + RBASIC_SET_SHAPE_ID(obj, next_shape_id); if (fields_obj != original_fields_obj) { switch (type) { case T_OBJECT: @@ -1844,7 +1843,7 @@ imemo_fields_set(VALUE owner, VALUE fields_obj, shape_id_t target_shape_id, ID f RUBY_ASSERT(field_name); st_insert(table, (st_data_t)field_name, (st_data_t)val); RB_OBJ_WRITTEN(fields_obj, Qundef, val); - RBASIC_SET_SHAPE_ID(fields_obj, rb_shape_id_with_robject_layout(target_shape_id)); + RBASIC_SET_SHAPE_ID(fields_obj, target_shape_id); } else { attr_index_t index = RSHAPE_INDEX(target_shape_id); @@ -1856,7 +1855,7 @@ imemo_fields_set(VALUE owner, VALUE fields_obj, shape_id_t target_shape_id, ID f RB_OBJ_WRITE(fields_obj, &table[index], val); if (index >= RSHAPE_LEN(current_shape_id)) { - RBASIC_SET_SHAPE_ID(fields_obj, rb_shape_id_with_robject_layout(target_shape_id)); + RBASIC_SET_SHAPE_ID(fields_obj, target_shape_id); } } @@ -2011,12 +2010,11 @@ rb_obj_freeze_inline(VALUE x) RB_FL_UNSET_RAW(x, FL_USER2 | FL_USER3); // STR_CHILLED } - // rb_obj_freeze_inline(String) shape_id_t shape_id = rb_obj_shape_transition_frozen(x); switch (BUILTIN_TYPE(x)) { case T_CLASS: case T_MODULE: - rb_obj_freeze_inline(RCLASS_WRITABLE_ENSURE_FIELDS_OBJ(x)); + RBASIC_SET_SHAPE_ID(RCLASS_WRITABLE_ENSURE_FIELDS_OBJ(x), shape_id); // FIXME: How to do multi-shape? RBASIC_SET_SHAPE_ID(x, shape_id); break; @@ -2305,7 +2303,7 @@ rb_copy_generic_ivar(VALUE dest, VALUE obj) } if (!RSHAPE_LEN(dest_shape_id)) { - RBASIC_SET_SHAPE_ID(dest, rb_shape_layout(RBASIC_SHAPE_ID(dest)) | dest_shape_id); + RBASIC_SET_SHAPE_ID(dest, dest_shape_id); return; } @@ -4638,7 +4636,6 @@ class_fields_ivar_set(VALUE klass, VALUE fields_obj, ID id, VALUE val, bool conc } if (new_ivar) { - RUBY_ASSERT(rb_shape_layout(next_shape_id) == SHAPE_ID_LAYOUT_ROBJECT); RBASIC_SET_SHAPE_ID(fields_obj, next_shape_id); } } @@ -4661,7 +4658,6 @@ class_fields_ivar_set(VALUE klass, VALUE fields_obj, ID id, VALUE val, bool conc RB_OBJ_WRITTEN(fields_obj, Qundef, val); if (fields_obj != original_fields_obj) { - RUBY_ASSERT(rb_shape_layout(next_shape_id) == SHAPE_ID_LAYOUT_ROBJECT); RBASIC_SET_SHAPE_ID(fields_obj, next_shape_id); } } @@ -4688,7 +4684,7 @@ class_ivar_set(VALUE obj, ID id, VALUE val, bool *new_ivar) // TODO: What should we set as the T_CLASS shape_id? // In most case we can replicate the single `fields_obj` shape // but in namespaced case? Perhaps INVALID_SHAPE_ID? - RBASIC_SET_SHAPE_ID(obj, rb_shape_layout(RBASIC_SHAPE_ID(obj)) | RBASIC_SHAPE_ID(new_fields_obj)); + RBASIC_SET_SHAPE_ID(obj, RBASIC_SHAPE_ID(new_fields_obj)); return index; } @@ -4713,7 +4709,7 @@ rb_fields_tbl_copy(VALUE dst, VALUE src) VALUE fields_obj = RCLASS_WRITABLE_FIELDS_OBJ(src); if (fields_obj) { RCLASS_WRITABLE_SET_FIELDS_OBJ(dst, rb_imemo_fields_clone(fields_obj)); - RBASIC_SET_SHAPE_ID(dst, rb_shape_layout(RBASIC_SHAPE_ID(dst)) | RBASIC_SHAPE_ID(src)); + RBASIC_SET_SHAPE_ID(dst, RBASIC_SHAPE_ID(src)); } } diff --git a/vm_insnhelper.c b/vm_insnhelper.c index f515662bf07765..ac0d81092d890d 100644 --- a/vm_insnhelper.c +++ b/vm_insnhelper.c @@ -1412,8 +1412,7 @@ vm_setivar_class(VALUE obj, VALUE val, rb_setivar_cache cache) RB_OBJ_WRITE(fields_obj, &rb_imemo_fields_ptr(fields_obj)[cache.index], val); if (shape_id != dest_shape_id) { - // The dest_shape_id comes from the fields_obj - RBASIC_SET_SHAPE_ID(obj, SHAPE_ID_LAYOUT_RCLASS | (dest_shape_id & ~SHAPE_ID_LAYOUT_MASK)); + RBASIC_SET_SHAPE_ID(obj, dest_shape_id); RBASIC_SET_SHAPE_ID(fields_obj, dest_shape_id); } @@ -1438,9 +1437,7 @@ vm_setivar_default(VALUE obj, ID id, VALUE val, rb_setivar_cache cache) if (shape_id != dest_shape_id) { RBASIC_SET_SHAPE_ID(obj, dest_shape_id); - // The dest_shape_id comes from the owner, but fields_obj must always - // have layout RObject, so give the fields_object the right layout. - RBASIC_SET_SHAPE_ID(fields_obj, rb_shape_id_with_robject_layout(dest_shape_id)); + RBASIC_SET_SHAPE_ID(fields_obj, dest_shape_id); } RB_DEBUG_COUNTER_INC(ivar_set_ic_hit); diff --git a/zjit/src/cruby_bindings.inc.rs b/zjit/src/cruby_bindings.inc.rs index 08c502b0d84e47..bd832acc9604cd 100644 --- a/zjit/src/cruby_bindings.inc.rs +++ b/zjit/src/cruby_bindings.inc.rs @@ -1491,13 +1491,8 @@ pub const SHAPE_ID_HEAP_INDEX_MASK: shape_id_fl_type = 7864320; pub const SHAPE_ID_FL_COMPLEX: shape_id_fl_type = 8388608; pub const SHAPE_ID_FL_FROZEN: shape_id_fl_type = 16777216; pub const SHAPE_ID_FL_HAS_OBJECT_ID: shape_id_fl_type = 33554432; -pub const SHAPE_ID_LAYOUT_ROBJECT: shape_id_fl_type = 0; -pub const SHAPE_ID_LAYOUT_RCLASS: shape_id_fl_type = 67108864; -pub const SHAPE_ID_LAYOUT_RDATA: shape_id_fl_type = 134217728; -pub const SHAPE_ID_LAYOUT_OTHER: shape_id_fl_type = 201326592; -pub const SHAPE_ID_LAYOUT_MASK: shape_id_fl_type = 201326592; pub const SHAPE_ID_FL_NON_CANONICAL_MASK: shape_id_fl_type = 50331648; -pub const SHAPE_ID_FLAGS_MASK: shape_id_fl_type = 267911168; +pub const SHAPE_ID_FLAGS_MASK: shape_id_fl_type = 66584576; pub type shape_id_fl_type = u32; pub const CONST_DEPRECATED: rb_const_flag_t = 256; pub const CONST_VISIBILITY_MASK: rb_const_flag_t = 255; From 6fa38d36257dea94dd1e68f8e6447e1863854848 Mon Sep 17 00:00:00 2001 From: Jun Aruga Date: Thu, 21 May 2026 15:56:46 +0100 Subject: [PATCH 004/188] [ruby/openssl] Fix test_ts.rb in FIPS. Replace RSA keys for intermediate_key and ee_key with RSA 4096-bit keys rsa-1.pem and rsa-2.pem. At least RSA 2048-bit keys are required for signing and encryption in FIPS. SP 800-131A Rev. 2 * 3. Digital Signatures * 6. Key Agreement and Key Transport Using RSA https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-131Ar2.pdf https://github.com/openssl/openssl/blob/71943544885ff364a10bcc5ffc62d0e651c9a021/providers/common/securitycheck.c#L72-L73 ``` $ openssl rsa -in test/openssl/fixtures/pkey/rsa-1.pem -text -noout | head -1 Private-Key: (4096 bit, 2 primes) $ openssl rsa -in test/openssl/fixtures/pkey/rsa-2.pem -text -noout | head -1 Private-Key: (4096 bit, 2 primes) ``` https://github.com/ruby/openssl/commit/f130312442 --- test/openssl/test_ts.rb | 36 ++---------------------------------- 1 file changed, 2 insertions(+), 34 deletions(-) diff --git a/test/openssl/test_ts.rb b/test/openssl/test_ts.rb index cca7898bc13d86..69780a65797318 100644 --- a/test/openssl/test_ts.rb +++ b/test/openssl/test_ts.rb @@ -4,43 +4,11 @@ class OpenSSL::TestTimestamp < OpenSSL::TestCase def intermediate_key - @intermediate_key ||= OpenSSL::PKey::RSA.new <<-_end_of_pem_ ------BEGIN RSA PRIVATE KEY----- -MIICWwIBAAKBgQCcyODxH+oTrr7l7MITWcGaYnnBma6vidCCJjuSzZpaRmXZHAyH -0YcY4ttC0BdJ4uV+cE05IySVC7tyvVfFb8gFQ6XJV+AEktP+XkLbcxZgj9d2NVu1 -ziXdI+ldXkPnMhyWpMS5E7SD6gflv9NhUYEsmAGsUgdK6LDmm2W2/4TlewIDAQAB -AoGAYgx6KDFWONLqjW3f/Sv/mGYHUNykUyDzpcD1Npyf797gqMMSzwlo3FZa2tC6 -D7n23XirwpTItvEsW9gvgMikJDPlThAeGLZ+L0UbVNNBHVxGP998Nda1kxqKvhRE -pfZCKc7PLM9ZXc6jBTmgxdcAYfVCCVUoa2mEf9Ktr3BlI4kCQQDQAM09+wHDXGKP -o2UnCwCazGtyGU2r0QCzHlh9BVY+KD2KjjhuWh86rEbdWN7hEW23Je1vXIhuM6Pa -/Ccd+XYnAkEAwPZ91PK6idEONeGQ4I3dyMKV2SbaUjfq3MDL4iIQPQPuj7QsBO/5 -3Nf9ReSUUTRFCUVwoC8k4Z1KAJhR/K/ejQJANE7PTnPuGJQGETs09+GTcFpR9uqY -FspDk8fg1ufdrVnvSAXF+TJewiGK3KU5v33jinhWQngRsyz3Wt2odKhEZwJACbjh -oicQqvzzgFd7GzVKpWDYd/ZzLY1PsgusuhoJQ2m9TVRAm4cTycLAKhNYPbcqe0sa -X5fAffWU0u7ZwqeByQJAOUAbYET4RU3iymAvAIDFj8LiQnizG9t5Ty3HXlijKQYv -y8gsvWd4CdxwOPatWpBUX9L7IXcMJmD44xXTUvpbfQ== ------END RSA PRIVATE KEY----- -_end_of_pem_ + @intermediate_key ||= Fixtures.pkey("rsa-1") end def ee_key - @ee_key ||= OpenSSL::PKey::RSA.new <<-_end_of_pem_ ------BEGIN RSA PRIVATE KEY----- -MIICWwIBAAKBgQDA6eB5r2O5KOKNbKMBhzadl43lgpwqq28m+G0gH38kKCL1f3o9 -P8xUZm7sZqcWEervZMSSXMGBV9DgeoSR+U6FMJywgQGx/JNRx7wZTMNym3PvgLkl -xCXh6ZA0/xbtJtcNI+UUv0ENBkTIuUWBhkAf3jQclAr9aQ0ktYBuHAcRcQIDAQAB -AoGAKNhcAuezwZx6e18pFEXAtpVEIfgJgK9TlXi8AjUpAkrNPBWFmDpN1QDrM3p4 -nh+lEpLPW/3vqqchPqYyM4YJraMLpS3KUG+s7+m9QIia0ri2WV5Cig7WL+Tl9p7K -b3oi2Aj/wti8GfOLFQXOQQ4Ea4GoCv2Sxe0GZR39UBxzTsECQQD1zuVIwBvqU2YR -8innsoa+j4u2hulRmQO6Zgpzj5vyRYfA9uZxQ9nKbfJvzuWwUv+UzyS9RqxarqrP -5nQw5EmVAkEAyOmJg6+AfGrgvSWfSpXEds/WA/sHziCO3rE4/sd6cnDc6XcTgeMs -mT8Z3kAYGpqFDew5orUylPfJJa+PUueJbQJAY+gkvw3+Cp69FLw1lgu0wo07fwOU -n2qu3jsNMm0DOFRUWfTAMvcd9S385L7WEnWZldUfnKK1+OGXYYrMXPbchQJAChU2 -UoaHQzc16iguM1cK0g+iJPb/MEgQA3sPajHmokGpxIm2T+lvvo0dJjs/Om6QyN8X -EWRYkoNQ8/Q4lCeMjQJAfvDIGtyqF4PieFHYgluQAv5pGgYpakdc8SYyeRH9NKey -GaL27FRs4fRWf9OmxPhUVgIyGzLGXrueemvQUDHObA== ------END RSA PRIVATE KEY----- -_end_of_pem_ + @ee_key ||= Fixtures.pkey("rsa-2") end def ca_cert From 0765e35cba266e577db3b8542226791a2122d184 Mon Sep 17 00:00:00 2001 From: Kazuki Yamaguchi Date: Mon, 13 Apr 2026 19:16:59 +0900 Subject: [PATCH 005/188] [ruby/openssl] asn1: limit nesting depth in OpenSSL::ASN1.decode Feeding a deeply nested constructed encoding to OpenSSL::ASN1.decode, .decode_all, or .traverse can cause unbounded recursion and result in SystemStackError. Add an explicit nesting depth limit of 200 levels and raise OpenSSL::ASN1::ASN1Error if it is exceeded. This limit is arbitrary and currently not configurable, but should be sufficient for any practical use cases. Fixes https://hackerone.com/reports/3662125 https://github.com/ruby/openssl/commit/fc753239cc --- ext/openssl/ossl_asn1.c | 6 ++++++ test/openssl/test_asn1.rb | 14 ++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/ext/openssl/ossl_asn1.c b/ext/openssl/ossl_asn1.c index a3e4afe7c7d60e..517212b4d5c11b 100644 --- a/ext/openssl/ossl_asn1.c +++ b/ext/openssl/ossl_asn1.c @@ -895,6 +895,8 @@ int_ossl_asn1_decode0_cons(unsigned char **pp, long max_len, long length, return asn1data; } +#define MAX_NESTING_DEPTH 200 + static VALUE ossl_asn1_decode0(unsigned char **pp, long length, long *offset, int depth, int yield, long *num_read) @@ -905,6 +907,10 @@ ossl_asn1_decode0(unsigned char **pp, long length, long *offset, int depth, int tag, tc, j; VALUE asn1data, tag_class; + if (depth > MAX_NESTING_DEPTH) { + ossl_raise(eASN1Error, "nesting depth %d exceeds limit", depth); + } + p = *pp; start = p; p0 = p; diff --git a/test/openssl/test_asn1.rb b/test/openssl/test_asn1.rb index 5978ecf6736e10..0293813a8dd158 100644 --- a/test/openssl/test_asn1.rb +++ b/test/openssl/test_asn1.rb @@ -696,6 +696,20 @@ def test_decode_constructed_overread assert_equal 17, ret[0][6] end + def test_decode_constructed_deeply_nested + bool = OpenSSL::ASN1::Boolean.new(true) + nested_100 = B(%w{ 30 80 }) * 100 + bool.to_der + B(%w{ 00 00 }) * 100 + decoded = OpenSSL::ASN1.decode(nested_100) + assert_equal(nested_100, decoded.to_der) + content = 100.times.inject(decoded) { |a,| a.value[0] } + assert_kind_of(OpenSSL::ASN1::Boolean, content) + + nested_500 = B(%w{ 30 80 }) * 500 + bool.to_der + B(%w{ 00 00 }) * 500 + assert_raise_with_message(OpenSSL::ASN1::ASN1Error, /nesting depth/) { + OpenSSL::ASN1.decode(nested_500) + } + end + def test_constructive_each data = [OpenSSL::ASN1::Integer.new(0), OpenSSL::ASN1::Integer.new(1)] seq = OpenSSL::ASN1::Sequence.new data From f77290ccf451ba688371d6e36fcb57749a0bfd8e Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Sat, 30 May 2026 20:13:07 +0900 Subject: [PATCH 006/188] [DOC] Improve docs for Method#source_location --- proc.c | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/proc.c b/proc.c index 739de9d8ffc55a..621dc602b49d09 100644 --- a/proc.c +++ b/proc.c @@ -3211,10 +3211,18 @@ rb_method_entry_location(const rb_method_entry_t *me) /* * call-seq: - * meth.source_location -> [String, Integer] + * source_location -> location * - * Returns the Ruby source filename and line number containing this method - * or nil if this method was not defined in Ruby (i.e. native). + * Returns a two-element array containing the Ruby source filename + * as a string and the line number integer where +self+ is defined: + * + * def greeting = "hello" + * method(:greeting).source_location # => ["test.rb", 1] + * + * Returns nil if +self+ is not a method defined in Ruby (i.e. defined + * using native code): + * + * Kernel.method(:puts).source_location # => nil */ VALUE From 2750e9a6ab51dfd4bf894c6d13591d038a9e8cb9 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Sat, 30 May 2026 21:25:01 +0900 Subject: [PATCH 007/188] Update the RBS test for ruby/rbs#2981 --- gems/bundled_gems | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gems/bundled_gems b/gems/bundled_gems index e42c7afd3966f6..8e244af59810c3 100644 --- a/gems/bundled_gems +++ b/gems/bundled_gems @@ -16,7 +16,7 @@ net-imap 0.6.4 https://github.com/ruby/net-imap net-smtp 0.5.1 https://github.com/ruby/net-smtp matrix 0.4.3 https://github.com/ruby/matrix prime 0.1.4 https://github.com/ruby/prime -rbs 4.0.2 https://github.com/ruby/rbs 36a7e8e38df9efd33db83c3f30f0308bdeb84bd9 +rbs 4.0.2 https://github.com/ruby/rbs 1582ce76429810b057a2816e5000cf5de4b1363d typeprof 0.32.0 https://github.com/ruby/typeprof debug 1.11.1 https://github.com/ruby/debug 9dc2024a5a05116b3d38afbc5579d9503d8913f3 racc 1.8.1 https://github.com/ruby/racc From f78cd18c91e6765484b96a8444b9d19a32cef1b9 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Tue, 17 Mar 2026 20:00:24 +0900 Subject: [PATCH 008/188] Reduce unnecessary intermediate strings `"%" PRIsVALUE` just copies/appends the corresponding argument as a string. --- compile.c | 2 +- ext/socket/init.c | 3 +-- ext/socket/ipsocket.c | 2 +- proc.c | 52 ++++++++++++++++++------------------------- 4 files changed, 25 insertions(+), 34 deletions(-) diff --git a/compile.c b/compile.c index 009b83105d40b4..65ced44f9cc98b 100644 --- a/compile.c +++ b/compile.c @@ -11762,7 +11762,7 @@ insn_data_to_s_detail(INSN *iobj) { const struct rb_callinfo *ci = (struct rb_callinfo *)OPERAND_AT(iobj, j); rb_str_cat2(str, "", vm_ci_argc(ci)); break; } diff --git a/ext/socket/init.c b/ext/socket/init.c index b761d601c39d57..60913855611405 100644 --- a/ext/socket/init.c +++ b/ext/socket/init.c @@ -516,8 +516,7 @@ wait_connectable(VALUE self, VALUE timeout, const struct sockaddr *sockaddr, int if (result == Qfalse) { VALUE rai = rsock_addrinfo_new((struct sockaddr *)sockaddr, len, PF_UNSPEC, 0, 0, Qnil, Qnil); VALUE addr_str = rsock_addrinfo_inspect_sockaddr(rai); - VALUE message = rb_sprintf("user specified timeout for %" PRIsVALUE, addr_str); - rb_raise(rb_eIOTimeoutError, "%" PRIsVALUE, message); + rb_raise(rb_eIOTimeoutError, "user specified timeout for %" PRIsVALUE, addr_str); } int revents = RB_NUM2INT(result); diff --git a/ext/socket/ipsocket.c b/ext/socket/ipsocket.c index 931a1a629c87f6..e1943b8496bb32 100644 --- a/ext/socket/ipsocket.c +++ b/ext/socket/ipsocket.c @@ -39,7 +39,7 @@ rsock_raise_user_specified_timeout(struct addrinfo *ai, VALUE host, VALUE port) message = rb_sprintf("user specified timeout for %" PRIsVALUE " port %" PRIsVALUE, host, port); } - rb_raise(rb_eIOTimeoutError, "%" PRIsVALUE, message); + rb_exc_raise(rb_exc_new_str(rb_eIOTimeoutError, message)); } static VALUE diff --git a/proc.c b/proc.c index 621dc602b49d09..bfdc2cc25b00bc 100644 --- a/proc.c +++ b/proc.c @@ -3319,6 +3319,18 @@ rb_method_parameters(VALUE method) return method_def_parameters(rb_method_def(method)); } +static inline VALUE +append_param_name(VALUE str, VALUE name, const char *unnamed) +{ + if (!NIL_P(name)) { + rb_str_append(str, rb_sym2str(name)); + } + else if (unnamed) { + rb_str_cat_cstr(str, unnamed); + } + return str; +} + /* * call-seq: * meth.to_s -> string @@ -3467,50 +3479,30 @@ method_inspect(VALUE method) name = RARRAY_AREF(pair, 1); } else { - // FIXME: can it be reduced to switch/case? - if (kind == req || kind == opt) { - name = rb_str_new2("_"); - } - else if (kind == rest || kind == keyrest) { - name = rb_str_new2(""); - } - else if (kind == block) { - name = rb_str_new2("block"); - } - else if (kind == nokey) { - name = rb_str_new2("nil"); - } - else if (kind == noblock) { - name = rb_str_new2("nil"); - } - else { - name = Qnil; - } + name = Qnil; } if (kind == req) { - rb_str_catf(str, "%"PRIsVALUE, name); + append_param_name(str, name, "_"); } else if (kind == opt) { - rb_str_catf(str, "%"PRIsVALUE"=...", name); + rb_str_cat_cstr(append_param_name(str, name, "_"), "=..."); } else if (kind == keyreq) { - rb_str_catf(str, "%"PRIsVALUE":", name); + rb_str_cat_cstr(append_param_name(str, name, NULL), ":"); } else if (kind == key) { - rb_str_catf(str, "%"PRIsVALUE": ...", name); + rb_str_cat_cstr(append_param_name(str, name, NULL), ": ..."); } else if (kind == rest) { - if (name == ID2SYM('*')) { - rb_str_cat_cstr(str, forwarding ? "..." : "*"); - } - else { - rb_str_catf(str, "*%"PRIsVALUE, name); + rb_str_cat_cstr(str, forwarding ? "..." : "*"); + if (name != ID2SYM('*')) { + append_param_name(str, name, NULL); } } else if (kind == keyrest) { if (name != ID2SYM(idPow)) { - rb_str_catf(str, "**%"PRIsVALUE, name); + append_param_name(rb_str_cat_cstr(str, "**"), name, NULL); } else if (i > 0) { rb_str_set_len(str, RSTRING_LEN(str) - 2); @@ -3529,7 +3521,7 @@ method_inspect(VALUE method) } } else { - rb_str_catf(str, "&%"PRIsVALUE, name); + append_param_name(rb_str_cat_cstr(str, "&"), name, NULL); } } else if (kind == nokey) { From 9c2ca0e1b7f776432c48a9bd91c5226c5635d848 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Thu, 28 May 2026 12:07:44 +0900 Subject: [PATCH 009/188] Free already closed IO immediately Suggested by @jhawthorn. --- gc.c | 5 +++-- internal/io.h | 1 + io.c | 9 +++++++++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/gc.c b/gc.c index 9eb72329404a3b..cde1b44d05b115 100644 --- a/gc.c +++ b/gc.c @@ -1637,9 +1637,10 @@ rb_gc_obj_free(void *objspace, VALUE obj) break; case T_FILE: if (RFILE(obj)->fptr) { - make_io_zombie(objspace, obj); + bool closed = rb_io_fptr_finalize_closed(RFILE(obj)->fptr); + if (!closed) make_io_zombie(objspace, obj); RB_DEBUG_COUNTER_INC(obj_file_ptr); - return FALSE; + return closed; } break; case T_RATIONAL: diff --git a/internal/io.h b/internal/io.h index b81774e0a715df..2110f0b0876271 100644 --- a/internal/io.h +++ b/internal/io.h @@ -149,6 +149,7 @@ VALUE rb_io_prep_stdout(void); VALUE rb_io_prep_stderr(void); int rb_io_notify_close(struct rb_io *fptr); +bool rb_io_fptr_finalize_closed(struct rb_io *fptr); RUBY_SYMBOL_EXPORT_BEGIN /* io.c (export) */ diff --git a/io.c b/io.c index 15a05c930b8485..effcb349c3c47b 100644 --- a/io.c +++ b/io.c @@ -5707,6 +5707,15 @@ rb_io_fptr_finalize(struct rb_io *io) return 1; } +bool +rb_io_fptr_finalize_closed(struct rb_io *io) +{ + if (!io) return true; + if (io->fd >= 0) return false; + rb_io_fptr_finalize(io); + return true; +} + size_t rb_io_memsize(const rb_io_t *io) { From 8a12816b005b3cabdde0c168a52550dc4ad8bf78 Mon Sep 17 00:00:00 2001 From: Jack Bowlin Date: Mon, 25 May 2026 12:38:11 -0500 Subject: [PATCH 010/188] [DOC] Fix typos in String#dump and String#bytesplice docs --- doc/string/bytesplice.rdoc | 5 ++--- doc/string/dump.rdoc | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/doc/string/bytesplice.rdoc b/doc/string/bytesplice.rdoc index 5689ef4a2ba79b..790f9eb9a0b491 100644 --- a/doc/string/bytesplice.rdoc +++ b/doc/string/bytesplice.rdoc @@ -20,7 +20,7 @@ And either count may be zero (i.e., specifying an empty string): '0123456789'.bytesplice(0, 0, 'abc') # => "abc0123456789" # Empty target. In the second form, just as in the first, -arugments +offset+ and +length+ determine the target bytes; +arguments +offset+ and +length+ determine the target bytes; argument +str+ _contains_ the source bytes, and the additional arguments +str_offset+ and +str_length+ determine the actual source bytes: @@ -42,7 +42,7 @@ and the source bytes are all of the given +str+: '0123456789'.bytesplice(0...0, 'abc') # => "abc0123456789" # Empty target. In the fourth form, just as in the third, -arugment +range+ determines the target bytes; +argument +range+ determines the target bytes; argument +str+ _contains_ the source bytes, and the additional argument +str_range+ determines the actual source bytes: @@ -63,4 +63,3 @@ and so has character boundaries at offsets 0, 3, 6, 9, 12, and 15. 'こんにちは'.bytesplice(0, 3, 'abc') # => "abcんにちは" 'こんにちは'.bytesplice(1, 3, 'abc') # Raises IndexError. 'こんにちは'.bytesplice(0, 2, 'abc') # Raises IndexError. - diff --git a/doc/string/dump.rdoc b/doc/string/dump.rdoc index add3c356623b15..7b688c28a64d6f 100644 --- a/doc/string/dump.rdoc +++ b/doc/string/dump.rdoc @@ -46,7 +46,7 @@ In a dump, certain special characters are escaped: In a dump, unprintable characters are replaced by printable ones; the unprintable characters are the whitespace characters (other than space itself); -here we see the ordinals for those characers, together with explanatory text: +here we see the ordinals for those characters, together with explanatory text: h = { 7 => 'Alert (BEL)', From 5e7a7f94041d9c8f940b169c145fe5fe186700cd Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Sat, 30 May 2026 20:53:41 +0900 Subject: [PATCH 011/188] Make TracePoint support compaction --- vm_trace.c | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/vm_trace.c b/vm_trace.c index 1a907da8809ec3..305802f469df62 100644 --- a/vm_trace.c +++ b/vm_trace.c @@ -890,20 +890,21 @@ typedef struct rb_tp_struct { } rb_tp_t; static void -tp_mark(void *ptr) +tp_mark_and_move(void *ptr) { rb_tp_t *tp = ptr; - rb_gc_mark(tp->proc); - rb_gc_mark(tp->local_target_set); - if (tp->target_th) rb_gc_mark(tp->target_th->self); + rb_gc_mark_and_move(&tp->proc); + rb_gc_mark_and_move(&tp->local_target_set); + if (tp->target_th) rb_gc_mark_and_move(&tp->target_th->self); } static const rb_data_type_t tp_data_type = { "tracepoint", { - tp_mark, + tp_mark_and_move, RUBY_TYPED_DEFAULT_FREE, NULL, // Nothing allocated externally, so don't need a memsize function + tp_mark_and_move, }, 0, 0, RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED | RUBY_TYPED_EMBEDDABLE }; From 1589add171516b748ab1cfb574adb8379bebe409 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Fri, 19 Dec 2025 19:22:39 +0900 Subject: [PATCH 012/188] [DOC] Move packed_data from rdoc to markdown To use tables mainly. --- doc/language/packed_data.md | 886 ++++++++++++++++++++++++++++++++++ doc/language/packed_data.rdoc | 729 ---------------------------- pack.rb | 6 +- 3 files changed, 889 insertions(+), 732 deletions(-) create mode 100644 doc/language/packed_data.md delete mode 100644 doc/language/packed_data.rdoc diff --git a/doc/language/packed_data.md b/doc/language/packed_data.md new file mode 100644 index 00000000000000..1b133367d6845d --- /dev/null +++ b/doc/language/packed_data.md @@ -0,0 +1,886 @@ +# Packed \Data + +## Quick Reference + +These tables summarize the directives for packing and unpacking. + +### For Integers + +| Directive | Meaning | +|-----------------------|-----------------------------------------------------------------------------------------------------| +| `C` | 8-bit unsigned (`unsigned char`) | +| `S` | 16-bit unsigned, native endian (`uint16_t`) | +| `L` | 32-bit unsigned, native endian (`uint32_t`) | +| `Q` | 64-bit unsigned, native endian (`uint64_t`) | +| `J` | pointer width unsigned, native endian (`uintptr_t`) | +| | | +| `c` | 8-bit signed (`signed char`) | +| `s` | 16-bit signed, native endian (`int16_t`) | +| `l` | 32-bit signed, native endian (`int32_t`) | +| `q` | 64-bit signed, native endian (`int64_t`) | +| `j` | pointer width signed, native endian (`intptr_t`) | +| | | +| `S_` `S!` | `unsigned short`, native endian | +| `I` `I_` `I!` | `unsigned int`, native endian | +| `L_` `L!` | `unsigned long`, native endian | +| `Q_` `Q!` | `unsigned long long`, native endian; (raises ArgumentError if the platform has no `long long` type) | +| `J!` | `uintptr_t`, native endian (same with `J`) | +| | | +| `s_` `s!` | `signed short`, native endian | +| `i` `i_` `i!` | `signed int`, native endian | +| `l_` `l!` | `signed long`, native endian | +| `q_` `q!` | `signed long long`, native endian; (raises ArgumentError if the platform has no `long long` type) | +| `j!` | `intptr_t`, native endian (same with `j`) | +| | | +| `S>` `s>` `S!>` `s!>` | each the same as the directive without `>`, but big endian; `S>` is the same as `n` | +| `L>` `l>` `L!>` `l!>` | `L>` is the same as `N` | +| `I!>` `i!>` | | +| `Q>` `q>` `Q!>` `q!>` | | +| `J>` `j>` `J!>` `j!>` | | +| | | +| `S<` `s<` `S!<` `s!<` | each the same as the directive without `<`, but little endian; `S<` is the same as `v` | +| `L<` `l<` `L!<` `l!<` | `L<` is the same as `V` | +| `I!<` `i!<` | | +| `Q<` `q<` `Q!<` `q!<` | | +| `J<` `j<` `J!<` `j!<` | | +| | | +| `n` | 16-bit unsigned, network (big-endian) byte order | +| `N` | 32-bit unsigned, network (big-endian) byte order | +| `v` | 16-bit unsigned, VAX (little-endian) byte order | +| `V` | 32-bit unsigned, VAX (little-endian) byte order | +| | | +| `U` | UTF-8 character | +| `w` | BER-compressed integer | +| `R` | LEB128 encoded unsigned integer | +| `r` | LEB128 encoded signed integer | + +### For Floats + +| Directive | Meaning | +|-----------|---------------------------------------------------| +| `D` `d` | double-precision, native format | +| `F` `f` | single-precision, native format | +| `E` | double-precision, little-endian byte order | +| `e` | single-precision, little-endian byte order | +| `G` | double-precision, network (big-endian) byte order | +| `g` | single-precision, network (big-endian) byte order | + +### For Strings + +| Directive | Meaning | +|-----------|------------------------------------------------------------------------------------------------| +| `A` | arbitrary binary string (remove trailing nulls and ASCII spaces) | +| `a` | arbitrary binary string | +| `Z` | null-terminated string | +| `B` | bit string (MSB first) | +| `b` | bit string (LSB first) | +| `H` | hex string (high nibble first) | +| `h` | hex string (low nibble first) | +| `u` | UU-encoded string | +| `M` | quoted-printable, MIME encoding (see RFC2045) | +| `m` | base64 encoded string (RFC 2045) (default) (base64 encoded string (RFC 4648) if followed by 0) | +| `P` | pointer to a structure (fixed-length string) | +| `p` | pointer to a null-terminated string | + +### Additional Directives for Packing + +| Directive | Meaning | +|-----------|----------------------------| +| `@` | moves to absolute position | +| `X` | back up a byte | +| `x` | null byte | + +### Additional Directives for Unpacking + +| Directive | Meaning | +|-----------|-------------------------------------------------| +| `@` | skip to the offset given by the length argument | +| `X` | skip backward one byte | +| `x` | skip forward one byte | +| `^` | return the current offset | + +## Packing and Unpacking + +Certain Ruby core methods deal with packing and unpacking data: + +- Method Array#pack: + Formats each element in array `self` into a binary string; + returns that string. +- Method String#unpack: + Extracts data from string `self`, + forming objects that become the elements of a new array; + returns that array. +- Method String#unpack1: + Does the same, but unpacks and returns only the first extracted object. + +Each of these methods accepts a string `template`, +consisting of zero or more _directive_ characters, +each followed by zero or more _modifier_ characters. + +Examples (directive `'C'` specifies '`unsigned character`'): + +```ruby +[65].pack('C') # => "A" # One element, one directive. +[65, 66].pack('CC') # => "AB" # Two elements, two directives. +[65, 66].pack('C') # => "A" # Extra element is ignored. +[65].pack('') # => "" # No directives. +[65].pack('CC') # Extra directive raises ArgumentError. +``` + +```ruby +'A'.unpack('C') # => [65] # One character, one directive. +'AB'.unpack('CC') # => [65, 66] # Two characters, two directives. +'AB'.unpack('C') # => [65] # Extra character is ignored. +'A'.unpack('CC') # => [65, nil] # Extra directive generates nil. +'AB'.unpack('') # => [] # No directives. +``` + +The string `template` may contain any mixture of valid directives +(directive `'c'` specifies 'signed character'): + +```ruby +[65, -1].pack('cC') # => "A\xFF" +"A\xFF".unpack('cC') # => [65, 255] +``` + +The string `template` may contain whitespace (which is ignored) +and comments, each of which begins with character `'#'` +and continues up to and including the next following newline: + +```ruby +[0,1].pack(" C #foo \n C ") # => "\x00\x01" +"\0\1".unpack(" C #foo \n C ") # => [0, 1] +``` + +Any directive may be followed by either of these modifiers: + +- `'*'` - The directive is to be applied as many times as needed: + + ```ruby + [65, 66].pack('C*') # => "AB" + 'AB'.unpack('C*') # => [65, 66] + ``` + +- \Integer `count` - The directive is to be applied `count` times: + + ```ruby + [65, 66].pack('C2') # => "AB" + [65, 66].pack('C3') # Raises ArgumentError. + 'AB'.unpack('C2') # => [65, 66] + 'AB'.unpack('C3') # => [65, 66, nil] + ``` + + Note: Directives in `%w[A a Z m]` use `count` differently; + see [\String Directives][rdoc-ref:@String+Directives]. + +If elements don't fit the provided directive, only least significant bits are encoded: + +```ruby +[257].pack("C").unpack("C") # => [1] +``` + +## Packing Method + +Method Array#pack accepts optional keyword argument +`buffer` that specifies the target string (instead of a new string): + +```ruby +[65, 66].pack('C*', buffer: 'foo') # => "fooAB" +``` + +The method can accept a block: + +```ruby +# Packed string is passed to the block. +[65, 66].pack('C*') {|s| p s } # => "AB" +``` + +## Unpacking Methods + +Methods String#unpack and String#unpack1 each accept +an optional keyword argument `offset` that specifies an offset +into the string: + +```ruby +'ABC'.unpack('C*', offset: 1) # => [66, 67] +'ABC'.unpack1('C*', offset: 1) # => 66 +``` + +Both methods can accept a block: + +```ruby +# Each unpacked object is passed to the block. +ret = [] +"ABCD".unpack("C*") {|c| ret << c } +ret # => [65, 66, 67, 68] +``` + +```ruby +# The single unpacked object is passed to the block. +'AB'.unpack1('C*') {|ele| p ele } # => 65 +``` + +## \Integer Directives + +Each integer directive specifies the packing or unpacking +for one element in the input or output array. + +### 8-Bit \Integer Directives + +- `'c'` - 8-bit signed integer + (like C `signed char`): + + ```ruby + [0, 1, 255].pack('c*') # => "\x00\x01\xFF" + s = [0, 1, -1].pack('c*') # => "\x00\x01\xFF" + s.unpack('c*') # => [0, 1, -1] + ``` + +- `'C'` - 8-bit unsigned integer + (like C `unsigned char`): + + ```ruby + [0, 1, 255].pack('C*') # => "\x00\x01\xFF" + s = [0, 1, -1].pack('C*') # => "\x00\x01\xFF" + s.unpack('C*') # => [0, 1, 255] + ``` + +### 16-Bit \Integer Directives + +- `'s'` - 16-bit signed integer, native-endian + (like C `int16_t`): + + ```ruby + [513, -514].pack('s*') # => "\x01\x02\xFE\xFD" + s = [513, 65022].pack('s*') # => "\x01\x02\xFE\xFD" + s.unpack('s*') # => [513, -514] + ``` + +- `'S'` - 16-bit unsigned integer, native-endian + (like C `uint16_t`): + + ```ruby + [513, -514].pack('S*') # => "\x01\x02\xFE\xFD" + s = [513, 65022].pack('S*') # => "\x01\x02\xFE\xFD" + s.unpack('S*') # => [513, 65022] + ``` + +- `'n'` - 16-bit network integer, big-endian: + + ```ruby + s = [0, 1, -1, 32767, -32768, 65535].pack('n*') + # => "\x00\x00\x00\x01\xFF\xFF\x7F\xFF\x80\x00\xFF\xFF" + s.unpack('n*') + # => [0, 1, 65535, 32767, 32768, 65535] + ``` + +- `'v'` - 16-bit VAX integer, little-endian: + + ```ruby + s = [0, 1, -1, 32767, -32768, 65535].pack('v*') + # => "\x00\x00\x01\x00\xFF\xFF\xFF\x7F\x00\x80\xFF\xFF" + s.unpack('v*') + # => [0, 1, 65535, 32767, 32768, 65535] + ``` + +### 32-Bit \Integer Directives + +- `'l'` - 32-bit signed integer, native-endian + (like C `int32_t`): + + ```ruby + s = [67305985, -50462977].pack('l*') + # => "\x01\x02\x03\x04\xFF\xFE\xFD\xFC" + s.unpack('l*') + # => [67305985, -50462977] + ``` + +- `'L'` - 32-bit unsigned integer, native-endian + (like C `uint32_t`): + + ```ruby + s = [67305985, 4244504319].pack('L*') + # => "\x01\x02\x03\x04\xFF\xFE\xFD\xFC" + s.unpack('L*') + # => [67305985, 4244504319] + ``` + +- `'N'` - 32-bit network integer, big-endian: + + ```ruby + s = [0,1,-1].pack('N*') + # => "\x00\x00\x00\x00\x00\x00\x00\x01\xFF\xFF\xFF\xFF" + s.unpack('N*') + # => [0, 1, 4294967295] + ``` + +- `'V'` - 32-bit VAX integer, little-endian: + + ```ruby + s = [0,1,-1].pack('V*') + # => "\x00\x00\x00\x00\x01\x00\x00\x00\xFF\xFF\xFF\xFF" + s.unpack('v*') + # => [0, 0, 1, 0, 65535, 65535] + ``` + +### 64-Bit \Integer Directives + +- `'q'` - 64-bit signed integer, native-endian + (like C `int64_t`): + + ```ruby + s = [578437695752307201, -506097522914230529].pack('q*') + # => "\x01\x02\x03\x04\x05\x06\a\b\xFF\xFE\xFD\xFC\xFB\xFA\xF9\xF8" + s.unpack('q*') + # => [578437695752307201, -506097522914230529] + ``` + +- `'Q'` - 64-bit unsigned integer, native-endian + (like C `uint64_t`): + + ```ruby + s = [578437695752307201, 17940646550795321087].pack('Q*') + # => "\x01\x02\x03\x04\x05\x06\a\b\xFF\xFE\xFD\xFC\xFB\xFA\xF9\xF8" + s.unpack('Q*') + # => [578437695752307201, 17940646550795321087] + ``` + +### Platform-Dependent \Integer Directives + +- `'i'` - Platform-dependent width signed integer, + native-endian (like C `int`): + + ```ruby + s = [67305985, -50462977].pack('i*') + # => "\x01\x02\x03\x04\xFF\xFE\xFD\xFC" + s.unpack('i*') + # => [67305985, -50462977] + ``` + +- `'I'` - Platform-dependent width unsigned integer, + native-endian (like C `unsigned int`): + + ```ruby + s = [67305985, -50462977].pack('I*') + # => "\x01\x02\x03\x04\xFF\xFE\xFD\xFC" + s.unpack('I*') + # => [67305985, 4244504319] + ``` + +- `'j'` - Pointer-width signed integer, native-endian + (like C `intptr_t`): + + ```ruby + s = [67305985, -50462977].pack('j*') + # => "\x01\x02\x03\x04\x00\x00\x00\x00\xFF\xFE\xFD\xFC\xFF\xFF\xFF\xFF" + s.unpack('j*') + # => [67305985, -50462977] + ``` + +- `'J'` - Pointer-width unsigned integer, native-endian + (like C `uintptr_t`): + + ```ruby + s = [67305985, 4244504319].pack('J*') + # => "\x01\x02\x03\x04\x00\x00\x00\x00\xFF\xFE\xFD\xFC\x00\x00\x00\x00" + s.unpack('J*') + # => [67305985, 4244504319] + ``` + +### Other \Integer Directives + +- `'U'` - UTF-8 character: + + ```ruby + s = [4194304].pack('U*') + # => "\xF8\x90\x80\x80\x80" + s.unpack('U*') + # => [4194304] + ``` + +- `'r'` - Signed LEB128-encoded integer + (see [Signed LEB128](https://en.wikipedia.org/wiki/LEB128#Signed_LEB128)) + + ```ruby + s = [1, 127, -128, 16383, -16384].pack("r*") + # => "\x01\xFF\x00\x80\x7F\xFF\xFF\x00\x80\x80\x7F" + s.unpack('r*') + # => [1, 127, -128, 16383, -16384] + ``` + +- `'R'` - Unsigned LEB128-encoded integer + (see [Unsigned LEB128](https://en.wikipedia.org/wiki/LEB128#Unsigned_LEB128)) + + ```ruby + s = [1, 127, 128, 16383, 16384].pack("R*") + # => "\x01\x7F\x80\x01\xFF\x7F\x80\x80\x01" + s.unpack('R*') + # => [1, 127, 128, 16383, 16384] + ``` + +- `'w'` - BER-encoded integer + (see [BER encoding](https://en.wikipedia.org/wiki/X.690#BER_encoding)): + + ```ruby + s = [1073741823].pack('w*') + # => "\x83\xFF\xFF\xFF\x7F" + s.unpack('w*') + # => [1073741823] + ``` + +### Modifiers for \Integer Directives + +For the following directives, `'!'` or `'_'` modifiers may be +suffixed as underlying platform’s native size. + +- `'i'`, `'I'` - C `int`, always native size. +- `'s'`, `'S'` - C `short`. +- `'l'`, `'L'` - C `long`. +- `'q'`, `'Q'` - C `long long`, if available. +- `'j'`, `'J'` - C `intptr_t`, always native size. + +Native size modifiers are silently ignored for always native size directives. + +The endian modifiers also may be suffixed in the directives above: + +- `'>'` - Big-endian. +- `'<'` - Little-endian. + +## \Float Directives + +Each float directive specifies the packing or unpacking +for one element in the input or output array. + +### Single-Precision \Float Directives + +- `'F'` or `'f'` - Native format: + + ```ruby + s = [3.0].pack('F') # => "\x00\x00@@" + s.unpack('F') # => [3.0] + ``` + +- `'e'` - Little-endian: + + ```ruby + s = [3.0].pack('e') # => "\x00\x00@@" + s.unpack('e') # => [3.0] + ``` + +- `'g'` - Big-endian: + + ```ruby + s = [3.0].pack('g') # => "@@\x00\x00" + s.unpack('g') # => [3.0] + ``` + +### Double-Precision \Float Directives + +- `'D'` or `'d'` - Native format: + + ```ruby + s = [3.0].pack('D') # => "\x00\x00\x00\x00\x00\x00\b@" + s.unpack('D') # => [3.0] + ``` + +- `'E'` - Little-endian: + + ```ruby + s = [3.0].pack('E') # => "\x00\x00\x00\x00\x00\x00\b@" + s.unpack('E') # => [3.0] + ``` + +- `'G'` - Big-endian: + + ```ruby + s = [3.0].pack('G') # => "@\b\x00\x00\x00\x00\x00\x00" + s.unpack('G') # => [3.0] + ``` + +A float directive may be infinity or not-a-number: + +```ruby +inf = 1.0/0.0 # => Infinity +[inf].pack('f') # => "\x00\x00\x80\x7F" +"\x00\x00\x80\x7F".unpack('f') # => [Infinity] + +nan = inf/inf # => NaN +[nan].pack('f') # => "\x00\x00\xC0\x7F" +"\x00\x00\xC0\x7F".unpack('f') # => [NaN] +``` + +## \String Directives + +Each string directive specifies the packing or unpacking +for one byte in the input or output string. + +### Binary \String Directives + +- `'A'` - Arbitrary binary string (space padded; count is width); + `nil` is treated as the empty string: + + ```ruby + ['foo'].pack('A') # => "f" + ['foo'].pack('A*') # => "foo" + ['foo'].pack('A2') # => "fo" + ['foo'].pack('A4') # => "foo " + [nil].pack('A') # => " " + [nil].pack('A*') # => "" + [nil].pack('A2') # => " " + [nil].pack('A4') # => " " + ``` + + ```ruby + "foo\0".unpack('A') # => ["f"] + "foo\0".unpack('A4') # => ["foo"] + "foo\0bar".unpack('A10') # => ["foo\x00bar"] # Reads past "\0". + "foo ".unpack('A') # => ["f"] + "foo ".unpack('A4') # => ["foo"] + "foo".unpack('A4') # => ["foo"] + ``` + + ```ruby + japanese = 'こんにちは' + japanese.size # => 5 + japanese.bytesize # => 15 + [japanese].pack('A') # => "\xE3" + [japanese].pack('A*') # => "\xE3\x81\x93\xE3\x82\x93\xE3\x81\xAB\xE3\x81\xA1\xE3\x81\xAF" + japanese.unpack('A') # => ["\xE3"] + japanese.unpack('A2') # => ["\xE3\x81"] + japanese.unpack('A4') # => ["\xE3\x81\x93\xE3"] + japanese.unpack('A*') # => ["\xE3\x81\x93\xE3\x82\x93\xE3\x81\xAB\xE3\x81\xA1\xE3\x81\xAF"] + ``` + +- `'a'` - Arbitrary binary string (null padded; count is width): + + ```ruby + ["foo"].pack('a') # => "f" + ["foo"].pack('a*') # => "foo" + ["foo"].pack('a2') # => "fo" + ["foo\0"].pack('a4') # => "foo\x00" + [nil].pack('a') # => "\x00" + [nil].pack('a*') # => "" + [nil].pack('a2') # => "\x00\x00" + [nil].pack('a4') # => "\x00\x00\x00\x00" + ``` + + ```ruby + "foo\0".unpack('a') # => ["f"] + "foo\0".unpack('a4') # => ["foo\x00"] + "foo ".unpack('a4') # => ["foo "] + "foo".unpack('a4') # => ["foo"] + "foo\0bar".unpack('a4') # => ["foo\x00"] # Reads past "\0". + ``` + +- `'Z'` - Same as `'a'`, + except that null is added or ignored with `'*'`: + + ```ruby + ["foo"].pack('Z*') # => "foo\x00" + [nil].pack('Z*') # => "\x00" + ``` + + ```ruby + "foo\0".unpack('Z*') # => ["foo"] + "foo".unpack('Z*') # => ["foo"] + "foo\0bar".unpack('Z*') # => ["foo"] # Does not read past "\0". + ``` + +### Bit \String Directives + +- `'B'` - Bit string (high byte first): + + ```ruby + ['11111111' + '00000000'].pack('B*') # => "\xFF\x00" + ['10000000' + '01000000'].pack('B*') # => "\x80@" + ``` + + ```ruby + ['1'].pack('B0') # => "" + ['1'].pack('B1') # => "\x80" + ['1'].pack('B2') # => "\x80\x00" + ['1'].pack('B3') # => "\x80\x00" + ['1'].pack('B4') # => "\x80\x00\x00" + ['1'].pack('B5') # => "\x80\x00\x00" + ['1'].pack('B6') # => "\x80\x00\x00\x00" + ``` + + ```ruby + "\xff\x00".unpack("B*") # => ["1111111100000000"] + "\x01\x02".unpack("B*") # => ["0000000100000010"] + ``` + + ```ruby + "".unpack("B0") # => [""] + "\x80".unpack("B1") # => ["1"] + "\x80".unpack("B2") # => ["10"] + "\x80".unpack("B3") # => ["100"] + ``` + +- `'b'` - Bit string (low byte first): + + ```ruby + ['11111111' + '00000000'].pack('b*') # => "\xFF\x00" + ['10000000' + '01000000'].pack('b*') # => "\x01\x02" + ``` + + ```ruby + ['1'].pack('b0') # => "" + ['1'].pack('b1') # => "\x01" + ['1'].pack('b2') # => "\x01\x00" + ['1'].pack('b3') # => "\x01\x00" + ['1'].pack('b4') # => "\x01\x00\x00" + ['1'].pack('b5') # => "\x01\x00\x00" + ['1'].pack('b6') # => "\x01\x00\x00\x00" + ``` + + ```ruby + "\xff\x00".unpack("b*") # => ["1111111100000000"] + "\x01\x02".unpack("b*") # => ["1000000001000000"] + ``` + + ```ruby + "".unpack("b0") # => [""] + "\x01".unpack("b1") # => ["1"] + "\x01".unpack("b2") # => ["10"] + "\x01".unpack("b3") # => ["100"] + ``` + +### Hex \String Directives + +- `'H'` - Hex string (high nibble first): + + ```ruby + ['10ef'].pack('H*') # => "\x10\xEF" + ['10ef'].pack('H0') # => "" + ['10ef'].pack('H3') # => "\x10\xE0" + ['10ef'].pack('H5') # => "\x10\xEF\x00" + ``` + + ```ruby + ['fff'].pack('H3') # => "\xFF\xF0" + ['fff'].pack('H4') # => "\xFF\xF0" + ['fff'].pack('H5') # => "\xFF\xF0\x00" + ['fff'].pack('H6') # => "\xFF\xF0\x00" + ['fff'].pack('H7') # => "\xFF\xF0\x00\x00" + ['fff'].pack('H8') # => "\xFF\xF0\x00\x00" + ``` + + ```ruby + "\x10\xef".unpack('H*') # => ["10ef"] + "\x10\xef".unpack('H0') # => [""] + "\x10\xef".unpack('H1') # => ["1"] + "\x10\xef".unpack('H2') # => ["10"] + "\x10\xef".unpack('H3') # => ["10e"] + "\x10\xef".unpack('H4') # => ["10ef"] + "\x10\xef".unpack('H5') # => ["10ef"] + ``` + +- `'h'` - Hex string (low nibble first): + + ```ruby + ['10ef'].pack('h*') # => "\x01\xFE" + ['10ef'].pack('h0') # => "" + ['10ef'].pack('h3') # => "\x01\x0E" + ['10ef'].pack('h5') # => "\x01\xFE\x00" + ``` + + ```ruby + ['fff'].pack('h3') # => "\xFF\x0F" + ['fff'].pack('h4') # => "\xFF\x0F" + ['fff'].pack('h5') # => "\xFF\x0F\x00" + ['fff'].pack('h6') # => "\xFF\x0F\x00" + ['fff'].pack('h7') # => "\xFF\x0F\x00\x00" + ['fff'].pack('h8') # => "\xFF\x0F\x00\x00" + ``` + + ```ruby + "\x01\xfe".unpack('h*') # => ["10ef"] + "\x01\xfe".unpack('h0') # => [""] + "\x01\xfe".unpack('h1') # => ["1"] + "\x01\xfe".unpack('h2') # => ["10"] + "\x01\xfe".unpack('h3') # => ["10e"] + "\x01\xfe".unpack('h4') # => ["10ef"] + "\x01\xfe".unpack('h5') # => ["10ef"] + ``` + +### Pointer \String Directives + +- `'P'` - Pointer to a structure (fixed-length string): + + ```ruby + s = ['abc'].pack('P') # => "\xE0O\x7F\xE5\xA1\x01\x00\x00" + s.unpack('P*') # => ["abc"] + ".".unpack("P") # => [] + ("\0" * 8).unpack("P") # => [nil] + [nil].pack("P") # => "\x00\x00\x00\x00\x00\x00\x00\x00" + ``` + +- `'p'` - Pointer to a null-terminated string: + + ```ruby + s = ['abc'].pack('p') # => "(\xE4u\xE5\xA1\x01\x00\x00" + s.unpack('p*') # => ["abc"] + ".".unpack("p") # => [] + ("\0" * 8).unpack("p") # => [nil] + [nil].pack("p") # => "\x00\x00\x00\x00\x00\x00\x00\x00" + ``` + +### Other \String Directives + +- `'M'` - Quoted printable, MIME encoding; + text mode, but input must use LF and output LF; + (see [RFC 2045](https://www.ietf.org/rfc/rfc2045.txt)): + + ```ruby + ["a b c\td \ne"].pack('M') # => "a b c\td =\n\ne=\n" + ["\0"].pack('M') # => "=00=\n" + ``` + + ```ruby + ["a"*1023].pack('M') == ("a"*73+"=\n")*14+"a=\n" # => true + ("a"*73+"=\na=\n").unpack('M') == ["a"*74] # => true + (("a"*73+"=\n")*14+"a=\n").unpack('M') == ["a"*1023] # => true + ``` + + ```ruby + "a b c\td =\n\ne=\n".unpack('M') # => ["a b c\td \ne"] + "=00=\n".unpack('M') # => ["\x00"] + ``` + + ```ruby + "pre=31=32=33after".unpack('M') # => ["pre123after"] + "pre=\nafter".unpack('M') # => ["preafter"] + "pre=\r\nafter".unpack('M') # => ["preafter"] + "pre=".unpack('M') # => ["pre="] + "pre=\r".unpack('M') # => ["pre=\r"] + "pre=hoge".unpack('M') # => ["pre=hoge"] + "pre==31after".unpack('M') # => ["pre==31after"] + "pre===31after".unpack('M') # => ["pre===31after"] + ``` + +- `'m'` - Base64 encoded string; + count specifies input bytes between each newline, + rounded down to nearest multiple of 3; + if count is zero, no newlines are added; + (see [RFC 4648](https://www.ietf.org/rfc/rfc4648.txt)): + + ```ruby + [""].pack('m') # => "" + ["\0"].pack('m') # => "AA==\n" + ["\0\0"].pack('m') # => "AAA=\n" + ["\0\0\0"].pack('m') # => "AAAA\n" + ["\377"].pack('m') # => "/w==\n" + ["\377\377"].pack('m') # => "//8=\n" + ["\377\377\377"].pack('m') # => "////\n" + ``` + + ```ruby + "".unpack('m') # => [""] + "AA==\n".unpack('m') # => ["\x00"] + "AAA=\n".unpack('m') # => ["\x00\x00"] + "AAAA\n".unpack('m') # => ["\x00\x00\x00"] + "/w==\n".unpack('m') # => ["\xFF"] + "//8=\n".unpack('m') # => ["\xFF\xFF"] + "////\n".unpack('m') # => ["\xFF\xFF\xFF"] + "A\n".unpack('m') # => [""] + "AA\n".unpack('m') # => ["\x00"] + "AA=\n".unpack('m') # => ["\x00"] + "AAA\n".unpack('m') # => ["\x00\x00"] + ``` + + ```ruby + [""].pack('m0') # => "" + ["\0"].pack('m0') # => "AA==" + ["\0\0"].pack('m0') # => "AAA=" + ["\0\0\0"].pack('m0') # => "AAAA" + ["\377"].pack('m0') # => "/w==" + ["\377\377"].pack('m0') # => "//8=" + ["\377\377\377"].pack('m0') # => "////" + ``` + + ```ruby + "".unpack('m0') # => [""] + "AA==".unpack('m0') # => ["\x00"] + "AAA=".unpack('m0') # => ["\x00\x00"] + "AAAA".unpack('m0') # => ["\x00\x00\x00"] + "/w==".unpack('m0') # => ["\xFF"] + "//8=".unpack('m0') # => ["\xFF\xFF"] + "////".unpack('m0') # => ["\xFF\xFF\xFF"] + ``` + +- `'u'` - UU-encoded string: + + ```ruby + [""].pack("u") # => "" + ["a"].pack("u") # => "!80``\n" + ["aaa"].pack("u") # => "#86%A\n" + ``` + + ```ruby + "".unpack("u") # => [""] + "#86)C\n".unpack("u") # => ["abc"] + ``` + +## Offset Directives + +- `'@'` - Begin packing at the given byte offset; + for packing, null fill or shrink if necessary: + + ```ruby + [1, 2].pack("C@0C") # => "\x02" + [1, 2].pack("C@1C") # => "\x01\x02" + [1, 2].pack("C@5C") # => "\x01\x00\x00\x00\x00\x02" + [*1..5].pack("CCCC@2C") # => "\x01\x02\x05" + ``` + + For unpacking, cannot to move to outside the string: + + ```ruby + "\x01\x00\x00\x02".unpack("C@3C") # => [1, 2] + "\x00".unpack("@1C") # => [nil] + "\x00".unpack("@2C") # Raises ArgumentError. + ``` + +- `'X'` - For packing, shrink for the given byte offset: + + ```ruby + [0, 1, 2].pack("CCXC") # => "\x00\x02" + [0, 1, 2].pack("CCX2C") # => "\x02" + ``` + + For unpacking; rewind unpacking position for the given byte offset: + + ```ruby + "\x00\x02".unpack("CCXC") # => [0, 2, 2] + ``` + + Cannot to move to outside the string: + + ```ruby + [0, 1, 2].pack("CCX3C") # Raises ArgumentError. + "\x00\x02".unpack("CX3C") # Raises ArgumentError. + ``` + +- `'x'` - Begin packing at after the given byte offset; + for packing, null fill if necessary: + + ```ruby + [].pack("x0") # => "" + [].pack("x") # => "\x00" + [].pack("x8") # => "\x00\x00\x00\x00\x00\x00\x00\x00" + ``` + + For unpacking, cannot to move to outside the string: + + ```ruby + "\x00\x00\x02".unpack("CxC") # => [0, 2] + "\x00\x00\x02".unpack("x3C") # => [nil] + "\x00\x00\x02".unpack("x4C") # Raises ArgumentError + ``` + +- `'^'` - Only for unpacking; the current position: + + ```ruby + "foo\0\0\0".unpack("Z*^") # => ["foo", 4] + ``` diff --git a/doc/language/packed_data.rdoc b/doc/language/packed_data.rdoc deleted file mode 100644 index 0c84113643d7e6..00000000000000 --- a/doc/language/packed_data.rdoc +++ /dev/null @@ -1,729 +0,0 @@ -= Packed \Data - -== Quick Reference - -These tables summarize the directives for packing and unpacking. - -=== For Integers - - Directive | Meaning - --------------|--------------------------------------------------------------- - C | 8-bit unsigned (unsigned char) - S | 16-bit unsigned, native endian (uint16_t) - L | 32-bit unsigned, native endian (uint32_t) - Q | 64-bit unsigned, native endian (uint64_t) - J | pointer width unsigned, native endian (uintptr_t) - - c | 8-bit signed (signed char) - s | 16-bit signed, native endian (int16_t) - l | 32-bit signed, native endian (int32_t) - q | 64-bit signed, native endian (int64_t) - j | pointer width signed, native endian (intptr_t) - - S_ S! | unsigned short, native endian - I I_ I! | unsigned int, native endian - L_ L! | unsigned long, native endian - Q_ Q! | unsigned long long, native endian - | (raises ArgumentError if the platform has no long long type) - J! | uintptr_t, native endian (same with J) - - s_ s! | signed short, native endian - i i_ i! | signed int, native endian - l_ l! | signed long, native endian - q_ q! | signed long long, native endian - | (raises ArgumentError if the platform has no long long type) - j! | intptr_t, native endian (same with j) - - S> s> S!> s!> | each the same as the directive without >, but big endian - L> l> L!> l!> | S> is the same as n - I!> i!> | L> is the same as N - Q> q> Q!> q!> | - J> j> J!> j!> | - - S< s< S!< s!< | each the same as the directive without <, but little endian - L< l< L!< l!< | S< is the same as v - I!< i!< | L< is the same as V - Q< q< Q!< q!< | - J< j< J!< j!< | - - n | 16-bit unsigned, network (big-endian) byte order - N | 32-bit unsigned, network (big-endian) byte order - v | 16-bit unsigned, VAX (little-endian) byte order - V | 32-bit unsigned, VAX (little-endian) byte order - - U | UTF-8 character - w | BER-compressed integer - R | LEB128 encoded unsigned integer - r | LEB128 encoded signed integer - -=== For Floats - - Directive | Meaning - ----------|-------------------------------------------------- - D d | double-precision, native format - F f | single-precision, native format - E | double-precision, little-endian byte order - e | single-precision, little-endian byte order - G | double-precision, network (big-endian) byte order - g | single-precision, network (big-endian) byte order - -=== For Strings - - Directive | Meaning - ----------|----------------------------------------------------------------- - A | arbitrary binary string (remove trailing nulls and ASCII spaces) - a | arbitrary binary string - Z | null-terminated string - B | bit string (MSB first) - b | bit string (LSB first) - H | hex string (high nibble first) - h | hex string (low nibble first) - u | UU-encoded string - M | quoted-printable, MIME encoding (see RFC2045) - m | base64 encoded string (RFC 2045) (default) - | (base64 encoded string (RFC 4648) if followed by 0) - P | pointer to a structure (fixed-length string) - p | pointer to a null-terminated string - -=== Additional Directives for Packing - - Directive | Meaning - ----------|---------------------------------------------------------------- - @ | moves to absolute position - X | back up a byte - x | null byte - -=== Additional Directives for Unpacking - - Directive | Meaning - ----------|---------------------------------------------------------------- - @ | skip to the offset given by the length argument - X | skip backward one byte - x | skip forward one byte - ^ | return the current offset - -== Packing and Unpacking - -Certain Ruby core methods deal with packing and unpacking data: - -- Method Array#pack: - Formats each element in array +self+ into a binary string; - returns that string. -- Method String#unpack: - Extracts data from string +self+, - forming objects that become the elements of a new array; - returns that array. -- Method String#unpack1: - Does the same, but unpacks and returns only the first extracted object. - -Each of these methods accepts a string +template+, -consisting of zero or more _directive_ characters, -each followed by zero or more _modifier_ characters. - -Examples (directive 'C' specifies 'unsigned character'): - - [65].pack('C') # => "A" # One element, one directive. - [65, 66].pack('CC') # => "AB" # Two elements, two directives. - [65, 66].pack('C') # => "A" # Extra element is ignored. - [65].pack('') # => "" # No directives. - [65].pack('CC') # Extra directive raises ArgumentError. - - 'A'.unpack('C') # => [65] # One character, one directive. - 'AB'.unpack('CC') # => [65, 66] # Two characters, two directives. - 'AB'.unpack('C') # => [65] # Extra character is ignored. - 'A'.unpack('CC') # => [65, nil] # Extra directive generates nil. - 'AB'.unpack('') # => [] # No directives. - -The string +template+ may contain any mixture of valid directives -(directive 'c' specifies 'signed character'): - - [65, -1].pack('cC') # => "A\xFF" - "A\xFF".unpack('cC') # => [65, 255] - -The string +template+ may contain whitespace (which is ignored) -and comments, each of which begins with character '#' -and continues up to and including the next following newline: - - [0,1].pack(" C #foo \n C ") # => "\x00\x01" - "\0\1".unpack(" C #foo \n C ") # => [0, 1] - -Any directive may be followed by either of these modifiers: - -- '*' - The directive is to be applied as many times as needed: - - [65, 66].pack('C*') # => "AB" - 'AB'.unpack('C*') # => [65, 66] - -- \Integer +count+ - The directive is to be applied +count+ times: - - [65, 66].pack('C2') # => "AB" - [65, 66].pack('C3') # Raises ArgumentError. - 'AB'.unpack('C2') # => [65, 66] - 'AB'.unpack('C3') # => [65, 66, nil] - - Note: Directives in %w[A a Z m] use +count+ differently; - see {\String Directives}[rdoc-ref:@String+Directives]. - -If elements don't fit the provided directive, only least significant bits are encoded: - - [257].pack("C").unpack("C") # => [1] - -== Packing Method - -Method Array#pack accepts optional keyword argument -+buffer+ that specifies the target string (instead of a new string): - - [65, 66].pack('C*', buffer: 'foo') # => "fooAB" - -The method can accept a block: - - # Packed string is passed to the block. - [65, 66].pack('C*') {|s| p s } # => "AB" - -== Unpacking Methods - -Methods String#unpack and String#unpack1 each accept -an optional keyword argument +offset+ that specifies an offset -into the string: - - 'ABC'.unpack('C*', offset: 1) # => [66, 67] - 'ABC'.unpack1('C*', offset: 1) # => 66 - -Both methods can accept a block: - - # Each unpacked object is passed to the block. - ret = [] - "ABCD".unpack("C*") {|c| ret << c } - ret # => [65, 66, 67, 68] - - # The single unpacked object is passed to the block. - 'AB'.unpack1('C*') {|ele| p ele } # => 65 - -== \Integer Directives - -Each integer directive specifies the packing or unpacking -for one element in the input or output array. - -=== 8-Bit \Integer Directives - -- 'c' - 8-bit signed integer - (like C signed char): - - [0, 1, 255].pack('c*') # => "\x00\x01\xFF" - s = [0, 1, -1].pack('c*') # => "\x00\x01\xFF" - s.unpack('c*') # => [0, 1, -1] - -- 'C' - 8-bit unsigned integer - (like C unsigned char): - - [0, 1, 255].pack('C*') # => "\x00\x01\xFF" - s = [0, 1, -1].pack('C*') # => "\x00\x01\xFF" - s.unpack('C*') # => [0, 1, 255] - -=== 16-Bit \Integer Directives - -- 's' - 16-bit signed integer, native-endian - (like C int16_t): - - [513, -514].pack('s*') # => "\x01\x02\xFE\xFD" - s = [513, 65022].pack('s*') # => "\x01\x02\xFE\xFD" - s.unpack('s*') # => [513, -514] - -- 'S' - 16-bit unsigned integer, native-endian - (like C uint16_t): - - [513, -514].pack('S*') # => "\x01\x02\xFE\xFD" - s = [513, 65022].pack('S*') # => "\x01\x02\xFE\xFD" - s.unpack('S*') # => [513, 65022] - -- 'n' - 16-bit network integer, big-endian: - - s = [0, 1, -1, 32767, -32768, 65535].pack('n*') - # => "\x00\x00\x00\x01\xFF\xFF\x7F\xFF\x80\x00\xFF\xFF" - s.unpack('n*') - # => [0, 1, 65535, 32767, 32768, 65535] - -- 'v' - 16-bit VAX integer, little-endian: - - s = [0, 1, -1, 32767, -32768, 65535].pack('v*') - # => "\x00\x00\x01\x00\xFF\xFF\xFF\x7F\x00\x80\xFF\xFF" - s.unpack('v*') - # => [0, 1, 65535, 32767, 32768, 65535] - -=== 32-Bit \Integer Directives - -- 'l' - 32-bit signed integer, native-endian - (like C int32_t): - - s = [67305985, -50462977].pack('l*') - # => "\x01\x02\x03\x04\xFF\xFE\xFD\xFC" - s.unpack('l*') - # => [67305985, -50462977] - -- 'L' - 32-bit unsigned integer, native-endian - (like C uint32_t): - - s = [67305985, 4244504319].pack('L*') - # => "\x01\x02\x03\x04\xFF\xFE\xFD\xFC" - s.unpack('L*') - # => [67305985, 4244504319] - -- 'N' - 32-bit network integer, big-endian: - - s = [0,1,-1].pack('N*') - # => "\x00\x00\x00\x00\x00\x00\x00\x01\xFF\xFF\xFF\xFF" - s.unpack('N*') - # => [0, 1, 4294967295] - -- 'V' - 32-bit VAX integer, little-endian: - - s = [0,1,-1].pack('V*') - # => "\x00\x00\x00\x00\x01\x00\x00\x00\xFF\xFF\xFF\xFF" - s.unpack('v*') - # => [0, 0, 1, 0, 65535, 65535] - -=== 64-Bit \Integer Directives - -- 'q' - 64-bit signed integer, native-endian - (like C int64_t): - - s = [578437695752307201, -506097522914230529].pack('q*') - # => "\x01\x02\x03\x04\x05\x06\a\b\xFF\xFE\xFD\xFC\xFB\xFA\xF9\xF8" - s.unpack('q*') - # => [578437695752307201, -506097522914230529] - -- 'Q' - 64-bit unsigned integer, native-endian - (like C uint64_t): - - s = [578437695752307201, 17940646550795321087].pack('Q*') - # => "\x01\x02\x03\x04\x05\x06\a\b\xFF\xFE\xFD\xFC\xFB\xFA\xF9\xF8" - s.unpack('Q*') - # => [578437695752307201, 17940646550795321087] - -=== Platform-Dependent \Integer Directives - -- 'i' - Platform-dependent width signed integer, - native-endian (like C int): - - s = [67305985, -50462977].pack('i*') - # => "\x01\x02\x03\x04\xFF\xFE\xFD\xFC" - s.unpack('i*') - # => [67305985, -50462977] - -- 'I' - Platform-dependent width unsigned integer, - native-endian (like C unsigned int): - - s = [67305985, -50462977].pack('I*') - # => "\x01\x02\x03\x04\xFF\xFE\xFD\xFC" - s.unpack('I*') - # => [67305985, 4244504319] - -- 'j' - Pointer-width signed integer, native-endian - (like C intptr_t): - - s = [67305985, -50462977].pack('j*') - # => "\x01\x02\x03\x04\x00\x00\x00\x00\xFF\xFE\xFD\xFC\xFF\xFF\xFF\xFF" - s.unpack('j*') - # => [67305985, -50462977] - -- 'J' - Pointer-width unsigned integer, native-endian - (like C uintptr_t): - - s = [67305985, 4244504319].pack('J*') - # => "\x01\x02\x03\x04\x00\x00\x00\x00\xFF\xFE\xFD\xFC\x00\x00\x00\x00" - s.unpack('J*') - # => [67305985, 4244504319] - -=== Other \Integer Directives - -- 'U' - UTF-8 character: - - s = [4194304].pack('U*') - # => "\xF8\x90\x80\x80\x80" - s.unpack('U*') - # => [4194304] - -- 'r' - Signed LEB128-encoded integer - (see {Signed LEB128}[https://en.wikipedia.org/wiki/LEB128#Signed_LEB128]) - - s = [1, 127, -128, 16383, -16384].pack("r*") - # => "\x01\xFF\x00\x80\x7F\xFF\xFF\x00\x80\x80\x7F" - s.unpack('r*') - # => [1, 127, -128, 16383, -16384] - -- 'R' - Unsigned LEB128-encoded integer - (see {Unsigned LEB128}[https://en.wikipedia.org/wiki/LEB128#Unsigned_LEB128]) - - s = [1, 127, 128, 16383, 16384].pack("R*") - # => "\x01\x7F\x80\x01\xFF\x7F\x80\x80\x01" - s.unpack('R*') - # => [1, 127, 128, 16383, 16384] - -- 'w' - BER-encoded integer - (see {BER encoding}[https://en.wikipedia.org/wiki/X.690#BER_encoding]): - - s = [1073741823].pack('w*') - # => "\x83\xFF\xFF\xFF\x7F" - s.unpack('w*') - # => [1073741823] - -=== Modifiers for \Integer Directives - -For the following directives, '!' or '_' modifiers may be -suffixed as underlying platform’s native size. - -- 'i', 'I' - C int, always native size. -- 's', 'S' - C short. -- 'l', 'L' - C long. -- 'q', 'Q' - C long long, if available. -- 'j', 'J' - C intptr_t, always native size. - -Native size modifiers are silently ignored for always native size directives. - -The endian modifiers also may be suffixed in the directives above: - -- '>' - Big-endian. -- '<' - Little-endian. - -== \Float Directives - -Each float directive specifies the packing or unpacking -for one element in the input or output array. - -=== Single-Precision \Float Directives - -- 'F' or 'f' - Native format: - - s = [3.0].pack('F') # => "\x00\x00@@" - s.unpack('F') # => [3.0] - -- 'e' - Little-endian: - - s = [3.0].pack('e') # => "\x00\x00@@" - s.unpack('e') # => [3.0] - -- 'g' - Big-endian: - - s = [3.0].pack('g') # => "@@\x00\x00" - s.unpack('g') # => [3.0] - -=== Double-Precision \Float Directives - -- 'D' or 'd' - Native format: - - s = [3.0].pack('D') # => "\x00\x00\x00\x00\x00\x00\b@" - s.unpack('D') # => [3.0] - -- 'E' - Little-endian: - - s = [3.0].pack('E') # => "\x00\x00\x00\x00\x00\x00\b@" - s.unpack('E') # => [3.0] - -- 'G' - Big-endian: - - s = [3.0].pack('G') # => "@\b\x00\x00\x00\x00\x00\x00" - s.unpack('G') # => [3.0] - -A float directive may be infinity or not-a-number: - - inf = 1.0/0.0 # => Infinity - [inf].pack('f') # => "\x00\x00\x80\x7F" - "\x00\x00\x80\x7F".unpack('f') # => [Infinity] - - nan = inf/inf # => NaN - [nan].pack('f') # => "\x00\x00\xC0\x7F" - "\x00\x00\xC0\x7F".unpack('f') # => [NaN] - -== \String Directives - -Each string directive specifies the packing or unpacking -for one byte in the input or output string. - -=== Binary \String Directives - -- 'A' - Arbitrary binary string (space padded; count is width); - +nil+ is treated as the empty string: - - ['foo'].pack('A') # => "f" - ['foo'].pack('A*') # => "foo" - ['foo'].pack('A2') # => "fo" - ['foo'].pack('A4') # => "foo " - [nil].pack('A') # => " " - [nil].pack('A*') # => "" - [nil].pack('A2') # => " " - [nil].pack('A4') # => " " - - "foo\0".unpack('A') # => ["f"] - "foo\0".unpack('A4') # => ["foo"] - "foo\0bar".unpack('A10') # => ["foo\x00bar"] # Reads past "\0". - "foo ".unpack('A') # => ["f"] - "foo ".unpack('A4') # => ["foo"] - "foo".unpack('A4') # => ["foo"] - - japanese = 'こんにちは' - japanese.size # => 5 - japanese.bytesize # => 15 - [japanese].pack('A') # => "\xE3" - [japanese].pack('A*') # => "\xE3\x81\x93\xE3\x82\x93\xE3\x81\xAB\xE3\x81\xA1\xE3\x81\xAF" - japanese.unpack('A') # => ["\xE3"] - japanese.unpack('A2') # => ["\xE3\x81"] - japanese.unpack('A4') # => ["\xE3\x81\x93\xE3"] - japanese.unpack('A*') # => ["\xE3\x81\x93\xE3\x82\x93\xE3\x81\xAB\xE3\x81\xA1\xE3\x81\xAF"] - -- 'a' - Arbitrary binary string (null padded; count is width): - - ["foo"].pack('a') # => "f" - ["foo"].pack('a*') # => "foo" - ["foo"].pack('a2') # => "fo" - ["foo\0"].pack('a4') # => "foo\x00" - [nil].pack('a') # => "\x00" - [nil].pack('a*') # => "" - [nil].pack('a2') # => "\x00\x00" - [nil].pack('a4') # => "\x00\x00\x00\x00" - - "foo\0".unpack('a') # => ["f"] - "foo\0".unpack('a4') # => ["foo\x00"] - "foo ".unpack('a4') # => ["foo "] - "foo".unpack('a4') # => ["foo"] - "foo\0bar".unpack('a4') # => ["foo\x00"] # Reads past "\0". - -- 'Z' - Same as 'a', - except that null is added or ignored with '*': - - ["foo"].pack('Z*') # => "foo\x00" - [nil].pack('Z*') # => "\x00" - - "foo\0".unpack('Z*') # => ["foo"] - "foo".unpack('Z*') # => ["foo"] - "foo\0bar".unpack('Z*') # => ["foo"] # Does not read past "\0". - -=== Bit \String Directives - -- 'B' - Bit string (high byte first): - - ['11111111' + '00000000'].pack('B*') # => "\xFF\x00" - ['10000000' + '01000000'].pack('B*') # => "\x80@" - - ['1'].pack('B0') # => "" - ['1'].pack('B1') # => "\x80" - ['1'].pack('B2') # => "\x80\x00" - ['1'].pack('B3') # => "\x80\x00" - ['1'].pack('B4') # => "\x80\x00\x00" - ['1'].pack('B5') # => "\x80\x00\x00" - ['1'].pack('B6') # => "\x80\x00\x00\x00" - - "\xff\x00".unpack("B*") # => ["1111111100000000"] - "\x01\x02".unpack("B*") # => ["0000000100000010"] - - "".unpack("B0") # => [""] - "\x80".unpack("B1") # => ["1"] - "\x80".unpack("B2") # => ["10"] - "\x80".unpack("B3") # => ["100"] - -- 'b' - Bit string (low byte first): - - ['11111111' + '00000000'].pack('b*') # => "\xFF\x00" - ['10000000' + '01000000'].pack('b*') # => "\x01\x02" - - ['1'].pack('b0') # => "" - ['1'].pack('b1') # => "\x01" - ['1'].pack('b2') # => "\x01\x00" - ['1'].pack('b3') # => "\x01\x00" - ['1'].pack('b4') # => "\x01\x00\x00" - ['1'].pack('b5') # => "\x01\x00\x00" - ['1'].pack('b6') # => "\x01\x00\x00\x00" - - "\xff\x00".unpack("b*") # => ["1111111100000000"] - "\x01\x02".unpack("b*") # => ["1000000001000000"] - - "".unpack("b0") # => [""] - "\x01".unpack("b1") # => ["1"] - "\x01".unpack("b2") # => ["10"] - "\x01".unpack("b3") # => ["100"] - -=== Hex \String Directives - -- 'H' - Hex string (high nibble first): - - ['10ef'].pack('H*') # => "\x10\xEF" - ['10ef'].pack('H0') # => "" - ['10ef'].pack('H3') # => "\x10\xE0" - ['10ef'].pack('H5') # => "\x10\xEF\x00" - - ['fff'].pack('H3') # => "\xFF\xF0" - ['fff'].pack('H4') # => "\xFF\xF0" - ['fff'].pack('H5') # => "\xFF\xF0\x00" - ['fff'].pack('H6') # => "\xFF\xF0\x00" - ['fff'].pack('H7') # => "\xFF\xF0\x00\x00" - ['fff'].pack('H8') # => "\xFF\xF0\x00\x00" - - "\x10\xef".unpack('H*') # => ["10ef"] - "\x10\xef".unpack('H0') # => [""] - "\x10\xef".unpack('H1') # => ["1"] - "\x10\xef".unpack('H2') # => ["10"] - "\x10\xef".unpack('H3') # => ["10e"] - "\x10\xef".unpack('H4') # => ["10ef"] - "\x10\xef".unpack('H5') # => ["10ef"] - -- 'h' - Hex string (low nibble first): - - ['10ef'].pack('h*') # => "\x01\xFE" - ['10ef'].pack('h0') # => "" - ['10ef'].pack('h3') # => "\x01\x0E" - ['10ef'].pack('h5') # => "\x01\xFE\x00" - - ['fff'].pack('h3') # => "\xFF\x0F" - ['fff'].pack('h4') # => "\xFF\x0F" - ['fff'].pack('h5') # => "\xFF\x0F\x00" - ['fff'].pack('h6') # => "\xFF\x0F\x00" - ['fff'].pack('h7') # => "\xFF\x0F\x00\x00" - ['fff'].pack('h8') # => "\xFF\x0F\x00\x00" - - "\x01\xfe".unpack('h*') # => ["10ef"] - "\x01\xfe".unpack('h0') # => [""] - "\x01\xfe".unpack('h1') # => ["1"] - "\x01\xfe".unpack('h2') # => ["10"] - "\x01\xfe".unpack('h3') # => ["10e"] - "\x01\xfe".unpack('h4') # => ["10ef"] - "\x01\xfe".unpack('h5') # => ["10ef"] - -=== Pointer \String Directives - -- 'P' - Pointer to a structure (fixed-length string): - - s = ['abc'].pack('P') # => "\xE0O\x7F\xE5\xA1\x01\x00\x00" - s.unpack('P*') # => ["abc"] - ".".unpack("P") # => [] - ("\0" * 8).unpack("P") # => [nil] - [nil].pack("P") # => "\x00\x00\x00\x00\x00\x00\x00\x00" - -- 'p' - Pointer to a null-terminated string: - - s = ['abc'].pack('p') # => "(\xE4u\xE5\xA1\x01\x00\x00" - s.unpack('p*') # => ["abc"] - ".".unpack("p") # => [] - ("\0" * 8).unpack("p") # => [nil] - [nil].pack("p") # => "\x00\x00\x00\x00\x00\x00\x00\x00" - -=== Other \String Directives - -- 'M' - Quoted printable, MIME encoding; - text mode, but input must use LF and output LF; - (see {RFC 2045}[https://www.ietf.org/rfc/rfc2045.txt]): - - ["a b c\td \ne"].pack('M') # => "a b c\td =\n\ne=\n" - ["\0"].pack('M') # => "=00=\n" - - ["a"*1023].pack('M') == ("a"*73+"=\n")*14+"a=\n" # => true - ("a"*73+"=\na=\n").unpack('M') == ["a"*74] # => true - (("a"*73+"=\n")*14+"a=\n").unpack('M') == ["a"*1023] # => true - - "a b c\td =\n\ne=\n".unpack('M') # => ["a b c\td \ne"] - "=00=\n".unpack('M') # => ["\x00"] - - "pre=31=32=33after".unpack('M') # => ["pre123after"] - "pre=\nafter".unpack('M') # => ["preafter"] - "pre=\r\nafter".unpack('M') # => ["preafter"] - "pre=".unpack('M') # => ["pre="] - "pre=\r".unpack('M') # => ["pre=\r"] - "pre=hoge".unpack('M') # => ["pre=hoge"] - "pre==31after".unpack('M') # => ["pre==31after"] - "pre===31after".unpack('M') # => ["pre===31after"] - -- 'm' - Base64 encoded string; - count specifies input bytes between each newline, - rounded down to nearest multiple of 3; - if count is zero, no newlines are added; - (see {RFC 4648}[https://www.ietf.org/rfc/rfc4648.txt]): - - [""].pack('m') # => "" - ["\0"].pack('m') # => "AA==\n" - ["\0\0"].pack('m') # => "AAA=\n" - ["\0\0\0"].pack('m') # => "AAAA\n" - ["\377"].pack('m') # => "/w==\n" - ["\377\377"].pack('m') # => "//8=\n" - ["\377\377\377"].pack('m') # => "////\n" - - "".unpack('m') # => [""] - "AA==\n".unpack('m') # => ["\x00"] - "AAA=\n".unpack('m') # => ["\x00\x00"] - "AAAA\n".unpack('m') # => ["\x00\x00\x00"] - "/w==\n".unpack('m') # => ["\xFF"] - "//8=\n".unpack('m') # => ["\xFF\xFF"] - "////\n".unpack('m') # => ["\xFF\xFF\xFF"] - "A\n".unpack('m') # => [""] - "AA\n".unpack('m') # => ["\x00"] - "AA=\n".unpack('m') # => ["\x00"] - "AAA\n".unpack('m') # => ["\x00\x00"] - - [""].pack('m0') # => "" - ["\0"].pack('m0') # => "AA==" - ["\0\0"].pack('m0') # => "AAA=" - ["\0\0\0"].pack('m0') # => "AAAA" - ["\377"].pack('m0') # => "/w==" - ["\377\377"].pack('m0') # => "//8=" - ["\377\377\377"].pack('m0') # => "////" - - "".unpack('m0') # => [""] - "AA==".unpack('m0') # => ["\x00"] - "AAA=".unpack('m0') # => ["\x00\x00"] - "AAAA".unpack('m0') # => ["\x00\x00\x00"] - "/w==".unpack('m0') # => ["\xFF"] - "//8=".unpack('m0') # => ["\xFF\xFF"] - "////".unpack('m0') # => ["\xFF\xFF\xFF"] - -- 'u' - UU-encoded string: - - [""].pack("u") # => "" - ["a"].pack("u") # => "!80``\n" - ["aaa"].pack("u") # => "#86%A\n" - - "".unpack("u") # => [""] - "#86)C\n".unpack("u") # => ["abc"] - -== Offset Directives - -- '@' - Begin packing at the given byte offset; - for packing, null fill or shrink if necessary: - - [1, 2].pack("C@0C") # => "\x02" - [1, 2].pack("C@1C") # => "\x01\x02" - [1, 2].pack("C@5C") # => "\x01\x00\x00\x00\x00\x02" - [*1..5].pack("CCCC@2C") # => "\x01\x02\x05" - - For unpacking, cannot to move to outside the string: - - "\x01\x00\x00\x02".unpack("C@3C") # => [1, 2] - "\x00".unpack("@1C") # => [nil] - "\x00".unpack("@2C") # Raises ArgumentError. - -- 'X' - For packing, shrink for the given byte offset: - - [0, 1, 2].pack("CCXC") # => "\x00\x02" - [0, 1, 2].pack("CCX2C") # => "\x02" - - For unpacking; rewind unpacking position for the given byte offset: - - "\x00\x02".unpack("CCXC") # => [0, 2, 2] - - Cannot to move to outside the string: - - [0, 1, 2].pack("CCX3C") # Raises ArgumentError. - "\x00\x02".unpack("CX3C") # Raises ArgumentError. - -- 'x' - Begin packing at after the given byte offset; - for packing, null fill if necessary: - - [].pack("x0") # => "" - [].pack("x") # => "\x00" - [].pack("x8") # => "\x00\x00\x00\x00\x00\x00\x00\x00" - - For unpacking, cannot to move to outside the string: - - "\x00\x00\x02".unpack("CxC") # => [0, 2] - "\x00\x00\x02".unpack("x3C") # => [nil] - "\x00\x00\x02".unpack("x4C") # Raises ArgumentError - -- '^' - Only for unpacking; the current position: - - "foo\0\0\0".unpack("Z*^") # => ["foo", 4] diff --git a/pack.rb b/pack.rb index a8b9e74514fdf5..78ef6973d4dfbc 100644 --- a/pack.rb +++ b/pack.rb @@ -3,7 +3,7 @@ class Array # pack(template, buffer: nil) -> string # # Formats each element in +self+ into a binary string; returns that string. - # See {Packed Data}[rdoc-ref:language/packed_data.rdoc]. + # See {Packed Data}[rdoc-ref:language/packed_data.md]. def pack(fmt, buffer: nil) Primitive.pack_pack(fmt, buffer) end @@ -15,7 +15,7 @@ class String # unpack(template, offset: 0) -> array # # Extracts data from +self+ to form new objects; - # see {Packed Data}[rdoc-ref:language/packed_data.rdoc]. + # see {Packed Data}[rdoc-ref:language/packed_data.md]. # # With a block given, calls the block with each unpacked object. # @@ -31,7 +31,7 @@ def unpack(fmt, offset: 0) # unpack1(template, offset: 0) -> object # # Like String#unpack with no block, but unpacks and returns only the first extracted object. - # See {Packed Data}[rdoc-ref:language/packed_data.rdoc]. + # See {Packed Data}[rdoc-ref:language/packed_data.md]. # # Related: see {Converting to Non-String}[rdoc-ref:String@Converting+to+Non--5CString]. def unpack1(fmt, offset: 0) From 12c8599ffa3c0a0b8df7741e06ae352e52cb463e Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Sun, 31 May 2026 10:21:27 +0900 Subject: [PATCH 013/188] [ruby/mmtk] Fix compiler warnings in mmtk.c gc/mmtk/mmtk.c:505:1: warning: function 'rb_mmtk_gc_thread_panic_handler' could be declared with attribute 'noreturn' [-Wmissing-noreturn] 505 | { | ^ gc/mmtk/mmtk.c:987:27: warning: variable length array folded to constant array as an extension [-Wgnu-folding-constant] 987 | char obj_info_buf[info_size]; | ^~~~~~~~~ gc/mmtk/mmtk.c:990:34: warning: variable length array folded to constant array as an extension [-Wgnu-folding-constant] 990 | char parent_obj_info_buf[info_size]; | ^~~~~~~~~ https://github.com/ruby/mmtk/commit/8ce21cdf10 --- gc/mmtk/mmtk.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gc/mmtk/mmtk.c b/gc/mmtk/mmtk.c index 96e9e32ef6a340..95176b692b4de8 100644 --- a/gc/mmtk/mmtk.c +++ b/gc/mmtk/mmtk.c @@ -500,6 +500,7 @@ rb_mmtk_gc_thread_bug(const char *msg, ...) rb_bug("rb_mmtk_gc_thread_bug"); } +RBIMPL_ATTR_NORETURN() static void rb_mmtk_gc_thread_panic_handler(void) { @@ -983,7 +984,7 @@ static inline VALUE rb_mmtk_call_object_closure(VALUE obj, bool pin) { if (RB_UNLIKELY(RB_BUILTIN_TYPE(obj) == T_NONE)) { - const size_t info_size = 256; + enum { info_size = 256 }; char obj_info_buf[info_size]; rb_raw_obj_info(obj_info_buf, info_size, obj); From 57910b3e43af6769a048e8972dd3ba149f106545 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Jun 2026 03:53:55 +0000 Subject: [PATCH 014/188] Bump taiki-e/install-action Bumps the github-actions group with 1 update in the / directory: [taiki-e/install-action](https://github.com/taiki-e/install-action). Updates `taiki-e/install-action` from 2.79.11 to 2.81.1 - [Release notes](https://github.com/taiki-e/install-action/releases) - [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/taiki-e/install-action/compare/13608cbb45b01feb47ef444ab1a42dc41ad56f1a...e49978b799e49ff429d162b7a30601a569ab6538) --- updated-dependencies: - dependency-name: taiki-e/install-action dependency-version: 2.81.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions ... Signed-off-by: dependabot[bot] --- .github/workflows/zjit-macos.yml | 2 +- .github/workflows/zjit-ubuntu.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/zjit-macos.yml b/.github/workflows/zjit-macos.yml index f9282d64e84b0a..e68bd897fd55d2 100644 --- a/.github/workflows/zjit-macos.yml +++ b/.github/workflows/zjit-macos.yml @@ -93,7 +93,7 @@ jobs: rustup install ${{ matrix.rust_version }} --profile minimal rustup default ${{ matrix.rust_version }} - - uses: taiki-e/install-action@13608cbb45b01feb47ef444ab1a42dc41ad56f1a # v2.79.11 + - uses: taiki-e/install-action@e49978b799e49ff429d162b7a30601a569ab6538 # v2.81.1 with: tool: nextest@0.9 if: ${{ matrix.test_task == 'zjit-check' }} diff --git a/.github/workflows/zjit-ubuntu.yml b/.github/workflows/zjit-ubuntu.yml index 1e7bec38599911..8d146b7bdc8cf5 100644 --- a/.github/workflows/zjit-ubuntu.yml +++ b/.github/workflows/zjit-ubuntu.yml @@ -119,7 +119,7 @@ jobs: ruby-version: '3.1' bundler: none - - uses: taiki-e/install-action@13608cbb45b01feb47ef444ab1a42dc41ad56f1a # v2.79.11 + - uses: taiki-e/install-action@e49978b799e49ff429d162b7a30601a569ab6538 # v2.81.1 with: tool: nextest@0.9 if: ${{ matrix.test_task == 'zjit-check' }} From 7225c7952edfbb0e6cc6777667cd59c3286dd082 Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Sun, 31 May 2026 21:28:33 +0900 Subject: [PATCH 015/188] Make hash_replace_ref to evaluate rb_gc_location once In hash_replace_ref, it currently evaluates rb_gc_location twice for the key and value. We can reduce that to once each. --- gc/gc.h | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/gc/gc.h b/gc/gc.h index ea8056c6716f08..d147a3122a853a 100644 --- a/gc/gc.h +++ b/gc/gc.h @@ -219,12 +219,14 @@ hash_foreach_replace(st_data_t key, st_data_t value, st_data_t argp, int error) static int hash_replace_ref(st_data_t *key, st_data_t *value, st_data_t argp, int existing) { - if (rb_gc_location((VALUE)*key) != (VALUE)*key) { - *key = rb_gc_location((VALUE)*key); + VALUE new_key = rb_gc_location((VALUE)*key); + if (new_key != (VALUE)*key) { + *key = new_key; } - if (rb_gc_location((VALUE)*value) != (VALUE)*value) { - *value = rb_gc_location((VALUE)*value); + VALUE new_value = rb_gc_location((VALUE)*value); + if (new_value != (VALUE)*value) { + *value = new_value; } return ST_CONTINUE; From 2a6353e2e51e5d6bb88e8aa18c50be55f2d49333 Mon Sep 17 00:00:00 2001 From: Jun Aruga Date: Tue, 26 May 2026 18:17:18 +0100 Subject: [PATCH 016/188] [ruby/rubygems] Document and test all available gemrc configuration keys Document missing gemrc configuration keys. The documented configuration key order aligns with the following part. https://github.com/ruby/rubygems/blob/287175d4ae57340e22c6f1d21da1489d7dac35c5/lib/rubygems/config_file.rb#L240-L257 Add and update tests for missing gemrc configuration use_psych and gemhome key assertions. Fix test_initialize_global_gem_cache_gemrc and test_initialize_concurrent_downloads to test with symbolic keys, as global_gem_cache and use_psych keys are documented as a symbolic key. https://github.com/ruby/rubygems/commit/8baf4d49a4 --- lib/rubygems/config_file.rb | 15 ++++++++++++++- test/rubygems/test_gem_config_file.rb | 18 ++++++++++++++++-- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/lib/rubygems/config_file.rb b/lib/rubygems/config_file.rb index 19718829fccbdd..d5e9eb4e33aec6 100644 --- a/lib/rubygems/config_file.rb +++ b/lib/rubygems/config_file.rb @@ -26,9 +26,22 @@ # RubyGems options use symbol keys. Valid options are: # # +:backtrace+:: See #backtrace -# +:sources+:: Sets Gem::sources +# +:bulk_threshold+:: See #bulk_threshold # +:verbose+:: See #verbose +# +:update_sources+:: See #update_sources # +:concurrent_downloads+:: See #concurrent_downloads +# +:cert_expiration_length_days+:: See #cert_expiration_length_days +# +:install_extension_in_lib+:: See #install_extension_in_lib +# +:ipv4_fallback_enabled+:: See #ipv4_fallback_enabled +# +:global_gem_cache+:: See #global_gem_cache +# +:use_psych+:: See #use_psych +# +:gemhome+:: See #home +# +:gempath+:: See #path +# +:sources+:: Sets Gem::sources +# +:disable_default_gem_server+:: See #disable_default_gem_server +# +:ssl_verify_mode+:: See #ssl_verify_mode +# +:ssl_ca_cert+:: See #ssl_ca_cert +# +:ssl_client_cert+:: See #ssl_client_cert # # gemrc files may exist in various locations and are read and merged in # the following order: diff --git a/test/rubygems/test_gem_config_file.rb b/test/rubygems/test_gem_config_file.rb index e85d00530e3089..3c79cb0762d783 100644 --- a/test/rubygems/test_gem_config_file.rb +++ b/test/rubygems/test_gem_config_file.rb @@ -53,6 +53,7 @@ def test_initialize fp.puts ":sources:" fp.puts " - http://more-gems.example.com" fp.puts "install: --wrappers" + fp.puts ":gemhome: /tmp/gems" fp.puts ":gempath:" fp.puts "- /usr/ruby/1.8/lib/ruby/gems/1.8" fp.puts "- /var/ruby/1.8/gem_home" @@ -61,6 +62,7 @@ def test_initialize fp.puts ":cert_expiration_length_days: 28" fp.puts ":install_extension_in_lib: false" fp.puts ":ipv4_fallback_enabled: true" + fp.puts ":use_psych: true" end util_config_file @@ -70,6 +72,7 @@ def test_initialize assert_equal false, @cfg.update_sources assert_equal %w[http://more-gems.example.com], @cfg.sources assert_equal "--wrappers", @cfg[:install] + assert_equal "/tmp/gems", @cfg.home assert_equal(["/usr/ruby/1.8/lib/ruby/gems/1.8", "/var/ruby/1.8/gem_home"], @cfg.path) assert_equal 0, @cfg.ssl_verify_mode @@ -77,6 +80,7 @@ def test_initialize assert_equal 28, @cfg.cert_expiration_length_days assert_equal false, @cfg.install_extension_in_lib assert_equal true, @cfg.ipv4_fallback_enabled + assert_equal true, @cfg.use_psych end def test_initialize_ipv4_fallback_enabled_env @@ -105,7 +109,7 @@ def test_initialize_global_gem_cache_env def test_initialize_global_gem_cache_gemrc File.open @temp_conf, "w" do |fp| - fp.puts "global_gem_cache: true" + fp.puts ":global_gem_cache: true" end util_config_file %W[--config-file #{@temp_conf}] @@ -113,9 +117,19 @@ def test_initialize_global_gem_cache_gemrc assert_equal true, @cfg.global_gem_cache end + def test_initialize_use_psych_env + orig_use_psych = ENV["RUBYGEMS_USE_PSYCH"] + ENV["RUBYGEMS_USE_PSYCH"] = "true" + util_config_file %W[--config-file #{@temp_conf}] + + assert_equal true, @cfg.use_psych + ensure + ENV["RUBYGEMS_USE_PSYCH"] = orig_use_psych + end + def test_initialize_concurrent_downloads File.open @temp_conf, "w" do |fp| - fp.puts "concurrent_downloads: 2" + fp.puts ":concurrent_downloads: 2" end util_config_file %W[--config-file #{@temp_conf}] From e73e4f2d4cbbd741649572b840e3a9816c31bb17 Mon Sep 17 00:00:00 2001 From: Sutou Kouhei Date: Mon, 1 Jun 2026 16:54:36 +0900 Subject: [PATCH 017/188] [ruby/strscan] [Feature #21943] Add `StringScanner#integer_at` (https://github.com/ruby/strscan/pull/205) See also: https://bugs.ruby-lang.org/issues/21943 This is semantically equivalent to `scanner[specifier]&.to_i(base)` but this is faster than `scanner[specifier]&.to_i(base)` because `integer_at` doesn't create a temporary String when possible. This PR also includes a benchmark for them: ```console $ ruby -v -S benchmark-driver benchmark/integer_at.yaml ruby 4.1.0dev (2026-05-01T19:25:51Z master https://github.com/ruby/strscan/commit/f2845eab29) +PRISM [x86_64-linux] Warming up -------------------------------------- [].to_i 24.272M i/s - 25.109M times in 1.034481s (41.20ns/i, 32clocks/i) integer_at 61.188M i/s - 62.491M times in 1.021289s (16.34ns/i, 62clocks/i) Calculating ------------------------------------- [].to_i 26.831M i/s - 72.816M times in 2.713883s (37.27ns/i, 169clocks/i) integer_at 81.331M i/s - 183.564M times in 2.256998s (12.30ns/i, 43clocks/i) Comparison: integer_at: 81331225.5 i/s [].to_i: 26831046.3 i/s - 3.03x slower ``` In this environment, `integer_at` is 3.03x faster than `[].to_i`. https://github.com/ruby/strscan/commit/8a60879b2d Co-authored-by: jinroq --- ext/strscan/extconf.rb | 1 + ext/strscan/lib/strscan/strscan.rb | 6 ++ ext/strscan/strscan.c | 130 ++++++++++++++++++++++++----- test/strscan/test_stringscanner.rb | 53 ++++++++++++ 4 files changed, 170 insertions(+), 20 deletions(-) diff --git a/ext/strscan/extconf.rb b/ext/strscan/extconf.rb index 2b4ec25be30909..4e8d851fdb5a54 100644 --- a/ext/strscan/extconf.rb +++ b/ext/strscan/extconf.rb @@ -5,6 +5,7 @@ have_func("onig_region_memsize(NULL)") have_func("rb_reg_onig_match", "ruby/re.h") have_func("rb_deprecate_constant") + have_func("rb_int_parse_cstr", "ruby.h") # RUBY_VERSION >= 2.5 have_func("rb_gc_location", "ruby.h") # RUBY_VERSION >= 2.7 have_const("RUBY_TYPED_EMBEDDABLE", "ruby.h") # RUBY_VERSION >= 3.3 create_makefile 'strscan' diff --git a/ext/strscan/lib/strscan/strscan.rb b/ext/strscan/lib/strscan/strscan.rb index 07ed102d9a8cfe..5e262f4007b497 100644 --- a/ext/strscan/lib/strscan/strscan.rb +++ b/ext/strscan/lib/strscan/strscan.rb @@ -1,6 +1,12 @@ # frozen_string_literal: true class StringScanner + unless method_defined?(:integer_at) # For JRuby + def integer_at(specifier, *to_i_args) + self[specifier]&.to_i(*to_i_args) + end + end + # :markup: markdown # # call-seq: diff --git a/ext/strscan/strscan.c b/ext/strscan/strscan.c index d35df7e43b1a5f..dede57218bd173 100644 --- a/ext/strscan/strscan.c +++ b/ext/strscan/strscan.c @@ -1689,6 +1689,38 @@ name_to_backref_number(struct re_registers *regs, VALUE regexp, const char* name rb_long2int(name_end - name), name); } +/* + * Resolve capture group index from Integer, Symbol, or String. + * Returns the resolved register index, or -1 if unmatched/out of range. + * For Symbol/String specifiers, raises IndexError if the named group + * does not exist. + */ +static long +resolve_capture_index(struct strscanner *p, VALUE specifier) +{ + const char *name; + long i; + if (! MATCHED_P(p)) return -1; + switch (TYPE(specifier)) { + case T_SYMBOL: + specifier = rb_sym2str(specifier); + /* fall through */ + case T_STRING: + RSTRING_GETMEM(specifier, name, i); + i = name_to_backref_number(&(p->regs), p->regex, name, name + i, + rb_enc_get(specifier)); + break; + default: + i = NUM2LONG(specifier); + } + if (i < 0) + i += p->regs.num_regs; + if (i < 0) return -1; + if (i >= p->regs.num_regs) return -1; + if (p->regs.beg[i] == -1) return -1; + return i; +} + /* * * :markup: markdown @@ -1763,36 +1795,93 @@ name_to_backref_number(struct re_registers *regs, VALUE regexp, const char* name static VALUE strscan_aref(VALUE self, VALUE idx) { - const char *name; struct strscanner *p; long i; GET_SCANNER(self, p); - if (! MATCHED_P(p)) return Qnil; - - switch (TYPE(idx)) { - case T_SYMBOL: - idx = rb_sym2str(idx); - /* fall through */ - case T_STRING: - RSTRING_GETMEM(idx, name, i); - i = name_to_backref_number(&(p->regs), p->regex, name, name + i, rb_enc_get(idx)); - break; - default: - i = NUM2LONG(idx); - } - - if (i < 0) - i += p->regs.num_regs; - if (i < 0) return Qnil; - if (i >= p->regs.num_regs) return Qnil; - if (p->regs.beg[i] == -1) return Qnil; + i = resolve_capture_index(p, idx); + if (i < 0) return Qnil; return extract_range(p, adjust_register_position(p, p->regs.beg[i]), adjust_register_position(p, p->regs.end[i])); } +/* + * :markup: markdown + * + * call-seq: + * integer_at(specifier, base=10) -> integer or nil + * + * Returns the captured substring at the given `specifier` as an Integer, + * following the behavior of `String#to_i(base)`. + * + * `specifier` can be an Integer (positive, negative, or zero), a Symbol, + * or a String for named capture groups. + * + * Returns `nil` if: + * - No match has been performed or the last match failed + * - The `specifier` is an Integer and is out of range + * - The group at `specifier` did not participate in the match + * + * Raises IndexError if `specifier` is a Symbol or String that does not + * correspond to a named capture group, consistent with + * `StringScanner#[]`. + * + * This is semantically equivalent to `self[specifier]&.to_i(base)` + * but avoids the allocation of a temporary String when possible. + * + * ```rb + * scanner = StringScanner.new("2024-06-15") + * scanner.scan(/(\d{4})-(\d{2})-(\d{2})/) + * scanner.integer_at(1) # => 2024 + * scanner.integer_at(1, 16) # => 8228 + * ``` + */ +static VALUE +strscan_integer_at(int argc, VALUE *argv, VALUE self) +{ + struct strscanner *p; + long i; + long beg, end, len; + const char *ptr; + VALUE rb_specifier; + VALUE rb_base; + int base = 10; + + GET_SCANNER(self, p); + rb_scan_args(argc, argv, "11", &rb_specifier, &rb_base); + if (argc > 1) + base = NUM2INT(rb_base); + i = resolve_capture_index(p, rb_specifier); + if (i < 0) + return Qnil; + + beg = adjust_register_position(p, p->regs.beg[i]); + end = adjust_register_position(p, p->regs.end[i]); + len = end - beg; + ptr = S_PBEG(p) + beg; +#ifdef HAVE_RB_INT_PARSE_CSTR + { + /* + * Ruby 2.5 or later export the rb_int_parse_cstr() symbol but + * prototype definition isn't provided. Ruby 4.1 or later + * provide prototype definition. + */ +# ifndef RB_INT_PARSE_DEFAULT + VALUE rb_int_parse_cstr(const char *str, ssize_t len, char **endp, + size_t *ndigits, int base, int flags); +# define RB_INT_PARSE_DEFAULT 0x07 +# endif + char *endp; + return rb_int_parse_cstr(ptr, len, &endp, NULL, base, + RB_INT_PARSE_DEFAULT); + } +#else + return rb_str_to_inum(rb_str_new(ptr, len), base, 0); +#endif +} + /* * :markup: markdown * :include: strscan/link_refs.txt @@ -2353,6 +2442,7 @@ Init_strscan(void) rb_define_method(StringScanner, "matched", strscan_matched, 0); rb_define_method(StringScanner, "matched_size", strscan_matched_size, 0); rb_define_method(StringScanner, "[]", strscan_aref, 1); + rb_define_method(StringScanner, "integer_at", strscan_integer_at, -1); rb_define_method(StringScanner, "pre_match", strscan_pre_match, 0); rb_define_method(StringScanner, "post_match", strscan_post_match, 0); rb_define_method(StringScanner, "size", strscan_size, 0); diff --git a/test/strscan/test_stringscanner.rb b/test/strscan/test_stringscanner.rb index 3b6223709cf6f7..96a1badb1f1087 100644 --- a/test/strscan/test_stringscanner.rb +++ b/test/strscan/test_stringscanner.rb @@ -525,6 +525,59 @@ def test_AREF end end + def assert_integer_at(s, specifier, *to_i_args) + assert_equal(s[specifier]&.to_i(*to_i_args), + s.integer_at(specifier, *to_i_args)) + end + + def test_integer_at + s = create_string_scanner("before 20260514 after") + s.skip_until(" ") + assert_equal("20260514", s.scan(/(\d{4})(\d{2})(\d{2})/)) + assert_integer_at(s, 0) # 20260514 + assert_integer_at(s, 1) # 2026 + assert_integer_at(s, 2) # 5 + assert_integer_at(s, 3) # 14 + assert_integer_at(s, 4) # nil + assert_integer_at(s, -1) # 14 + assert_integer_at(s, -2) # 5 + assert_integer_at(s, -3) # 2026 + assert_integer_at(s, -4) # 20260514 + assert_integer_at(s, -5) # nil + end + + def test_integer_at_name_string + s = create_string_scanner("before 20260514 after") + s.skip_until(" ") + assert_equal("20260514", s.scan(/(?\d{4})(?\d{2})(?\d{2})/)) + assert_integer_at(s, "y") + assert_integer_at(s, "m") + assert_integer_at(s, "d") + end + + def test_integer_at_name_symbol + s = create_string_scanner("before 20260514 after") + s.skip_until(" ") + assert_equal("20260514", s.scan(/(?\d{4})(?\d{2})(?\d{2})/)) + assert_integer_at(s, :y) + assert_integer_at(s, :m) + assert_integer_at(s, :d) + end + + def test_integer_at_base + s = create_string_scanner("before 111 after") + s.skip_until(" ") + assert_equal("111", s.scan(/\d+/)) + assert_integer_at(s, 0, 2) + end + + def test_integer_at_base_auto + s = create_string_scanner("before 0xa_f after") + s.skip_until(" ") + assert_equal("0xa_f", s.scan(/0x[\h_]+/)) + assert_integer_at(s, 0, 0) # 0xaf + end + def test_pre_match s = create_string_scanner('a b c d e') s.scan(/\w/) From 807159283eacc613fa4ceee2a9c986711d619327 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Mon, 1 Jun 2026 10:20:56 +0900 Subject: [PATCH 018/188] [ruby/rubygems] Vendor compact_index during install_test_deps The earlier `rake vendor:compact_index` hook into `dev:deps` and the hard-copy step in ruby-core.yml fell apart in ruby/ruby's test-bundler runner, which sets TMPDIR per process and does not invoke our rake tasks. Pull the fetch logic into `Spec::Rubygems.install_vendored_compact_index` and call it from `install_test_deps` so every test setup path - local `bin/rspec`, `bin/parallel_rspec`, GHA bundler.yml, and ruby/ruby's test-bundler - lands the files at `Path.tmp_root.join("compact_index")` exactly where the artifice already looks. The standalone rake task and its workflow hop are no longer needed. https://github.com/ruby/rubygems/commit/d8536e115e Co-Authored-By: Claude Opus 4.7 --- spec/bundler/support/rubygems_ext.rb | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/spec/bundler/support/rubygems_ext.rb b/spec/bundler/support/rubygems_ext.rb index cf639a660a04fd..7d1ca550ba9543 100644 --- a/spec/bundler/support/rubygems_ext.rb +++ b/spec/bundler/support/rubygems_ext.rb @@ -73,6 +73,33 @@ def install_test_deps require_relative "helpers" Helpers.install_dev_bundler + + install_vendored_compact_index + end + + # Vendor `rubygems/rubygems.org#lib/compact_index/` under `tmp/compact_index/` + # so the artifice can serve compact-index responses without a runtime gem + # dependency. Pinned to a reviewed commit; override with COMPACT_INDEX_REF. + def install_vendored_compact_index + target_root = Path.tmp_root.join("compact_index") + return if File.exist?(target_root.join("lib/compact_index.rb")) + + require "open-uri" + require "fileutils" + + ref = ENV["COMPACT_INDEX_REF"] || "7c68a7b39761c61a66f9299f85b889ec39afc02c" + %w[ + lib/compact_index.rb + lib/compact_index/dependency.rb + lib/compact_index/gem.rb + lib/compact_index/gem_version.rb + lib/compact_index/versions_file.rb + ].each do |path| + url = "https://raw.githubusercontent.com/rubygems/rubygems.org/#{ref}/#{path}" + target = target_root.join(path) + FileUtils.mkdir_p(File.dirname(target)) + File.write(target, URI.parse(url).open(&:read)) + end end def check_source_control_changes(success_message:, error_message:) From 4f04e6ab8aea92bd4433cf4da01a8282c8d5ec5d Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Mon, 1 Jun 2026 10:25:38 +0900 Subject: [PATCH 019/188] [ruby/rubygems] Refresh vendored compact_index when COMPACT_INDEX_REF is set Previously the install_vendored_compact_index short-circuit only checked whether `tmp/compact_index/lib/compact_index.rb` existed, so once any ref was vendored a subsequent `COMPACT_INDEX_REF= bin/rspec ...` kept serving the stale copy. Drop the vendor tree first when the env var is explicitly set so an override always re-fetches against the requested ref. https://github.com/ruby/rubygems/commit/db5d06953f Co-Authored-By: Claude Opus 4.7 --- spec/bundler/support/rubygems_ext.rb | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/spec/bundler/support/rubygems_ext.rb b/spec/bundler/support/rubygems_ext.rb index 7d1ca550ba9543..9ff0138f2ccd03 100644 --- a/spec/bundler/support/rubygems_ext.rb +++ b/spec/bundler/support/rubygems_ext.rb @@ -79,14 +79,19 @@ def install_test_deps # Vendor `rubygems/rubygems.org#lib/compact_index/` under `tmp/compact_index/` # so the artifice can serve compact-index responses without a runtime gem - # dependency. Pinned to a reviewed commit; override with COMPACT_INDEX_REF. + # dependency. Pinned to a reviewed commit; override with COMPACT_INDEX_REF + # to refresh against another ref (the existing vendor copy is discarded). def install_vendored_compact_index target_root = Path.tmp_root.join("compact_index") - return if File.exist?(target_root.join("lib/compact_index.rb")) - - require "open-uri" require "fileutils" + if ENV["COMPACT_INDEX_REF"] + FileUtils.rm_rf(target_root) + elsif File.exist?(target_root.join("lib/compact_index.rb")) + return + end + + require "open-uri" ref = ENV["COMPACT_INDEX_REF"] || "7c68a7b39761c61a66f9299f85b889ec39afc02c" %w[ lib/compact_index.rb From e431c98e31876042a689396a0603f63802914018 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Mon, 1 Jun 2026 19:56:08 +0900 Subject: [PATCH 020/188] [ruby/rubygems] Lock compact_index vendoring against parallel races The vendored compact_index install ran without any coordination, so two test setups starting at once could both write into tmp/compact_index/ at the same time. The skip guard also checked a single file, which meant an interrupted download leaving only lib/compact_index.rb behind would be treated as a complete vendor tree on the next run. Take an exclusive file lock around the install and only skip the download once every expected file is present, removing the tree under the same lock when COMPACT_INDEX_REF forces a refresh. https://github.com/ruby/rubygems/commit/0451700769 Co-Authored-By: Claude Opus 4.8 (1M context) --- spec/bundler/support/rubygems_ext.rb | 36 +++++++++++++++++----------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/spec/bundler/support/rubygems_ext.rb b/spec/bundler/support/rubygems_ext.rb index 9ff0138f2ccd03..8e3d84212d31fd 100644 --- a/spec/bundler/support/rubygems_ext.rb +++ b/spec/bundler/support/rubygems_ext.rb @@ -84,26 +84,34 @@ def install_test_deps def install_vendored_compact_index target_root = Path.tmp_root.join("compact_index") require "fileutils" + FileUtils.mkdir_p(Path.tmp_root) - if ENV["COMPACT_INDEX_REF"] - FileUtils.rm_rf(target_root) - elsif File.exist?(target_root.join("lib/compact_index.rb")) - return - end - - require "open-uri" - ref = ENV["COMPACT_INDEX_REF"] || "7c68a7b39761c61a66f9299f85b889ec39afc02c" - %w[ + files = %w[ lib/compact_index.rb lib/compact_index/dependency.rb lib/compact_index/gem.rb lib/compact_index/gem_version.rb lib/compact_index/versions_file.rb - ].each do |path| - url = "https://raw.githubusercontent.com/rubygems/rubygems.org/#{ref}/#{path}" - target = target_root.join(path) - FileUtils.mkdir_p(File.dirname(target)) - File.write(target, URI.parse(url).open(&:read)) + ] + + # Serialize installs so parallel test setups don't race on the same + # vendor tree, and only skip the download when every file is present so + # an interrupted run can't leave a partial copy behind. + File.open(Path.tmp_root.join("compact_index.lock"), File::CREAT | File::RDWR) do |lock| + lock.flock(File::LOCK_EX) + + FileUtils.rm_rf(target_root) if ENV["COMPACT_INDEX_REF"] + + next if files.all? {|path| File.exist?(target_root.join(path)) } + + require "open-uri" + ref = ENV["COMPACT_INDEX_REF"] || "7c68a7b39761c61a66f9299f85b889ec39afc02c" + files.each do |path| + url = "https://raw.githubusercontent.com/rubygems/rubygems.org/#{ref}/#{path}" + target = target_root.join(path) + FileUtils.mkdir_p(File.dirname(target)) + File.write(target, URI.parse(url).open(&:read)) + end end end From 344b5eaa67f90671ce0d18c01f0b83c65a3f97d5 Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Mon, 1 Jun 2026 15:15:09 +0900 Subject: [PATCH 021/188] Wrap functions in USE_MODULAR_GC rb_gc_modular_gc_loaded_p and rb_gc_active_gc_name are only used when compiling with modular GC enabled. --- gc.c | 6 ++---- internal/gc.h | 2 ++ 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/gc.c b/gc.c index cde1b44d05b115..f0ec0f79efe692 100644 --- a/gc.c +++ b/gc.c @@ -3650,14 +3650,11 @@ rb_gc_copy_attributes(VALUE dest, VALUE obj) rb_gc_impl_copy_attributes(rb_gc_get_objspace(), dest, obj); } +#if USE_MODULAR_GC int rb_gc_modular_gc_loaded_p(void) { -#if USE_MODULAR_GC return rb_gc_functions.modular_gc_loaded_p; -#else - return false; -#endif } const char * @@ -3673,6 +3670,7 @@ rb_gc_active_gc_name(void) return gc_name; } +#endif struct rb_gc_object_metadata_entry * rb_gc_object_metadata(VALUE obj) diff --git a/internal/gc.h b/internal/gc.h index 41675810c722c4..ee2a0c28050a8a 100644 --- a/internal/gc.h +++ b/internal/gc.h @@ -257,8 +257,10 @@ void rb_gc_update_values(long n, VALUE *values); void rb_gc_mark_set_no_pin(st_table *); void rb_gc_update_set_refs(st_table *); +#if USE_MODULAR_GC const char *rb_gc_active_gc_name(void); int rb_gc_modular_gc_loaded_p(void); +#endif RUBY_SYMBOL_EXPORT_END From 607df5d706d2b24443c36f509c2aa25c6b2fa880 Mon Sep 17 00:00:00 2001 From: Andrii Konchyn Date: Mon, 1 Jun 2026 17:24:14 +0300 Subject: [PATCH 022/188] Update to ruby/spec@6b3b96d --- spec/ruby/.rubocop.yml | 4 - spec/ruby/.rubocop_todo.yml | 36 +- spec/ruby/CONTRIBUTING.md | 24 +- spec/ruby/bin/rubocop | 2 +- .../fixtures/bin/bad_embedded_ruby.txt | 2 +- .../fixtures/bin/embedded_ruby.txt | 2 +- spec/ruby/core/argf/each_byte_spec.rb | 58 +- spec/ruby/core/argf/each_char_spec.rb | 58 +- spec/ruby/core/argf/each_codepoint_spec.rb | 58 +- spec/ruby/core/argf/each_line_spec.rb | 62 +- spec/ruby/core/argf/each_spec.rb | 5 +- spec/ruby/core/argf/eof_spec.rb | 28 +- spec/ruby/core/argf/filename_spec.rb | 28 +- spec/ruby/core/argf/fileno_spec.rb | 24 +- spec/ruby/core/argf/inspect_spec.rb | 7 + spec/ruby/core/argf/path_spec.rb | 5 +- spec/ruby/core/argf/pos_spec.rb | 31 +- spec/ruby/core/argf/readlines_spec.rb | 22 +- spec/ruby/core/argf/shared/each_byte.rb | 58 -- spec/ruby/core/argf/shared/each_char.rb | 58 -- spec/ruby/core/argf/shared/each_codepoint.rb | 58 -- spec/ruby/core/argf/shared/each_line.rb | 62 -- spec/ruby/core/argf/shared/eof.rb | 24 - spec/ruby/core/argf/shared/filename.rb | 28 - spec/ruby/core/argf/shared/fileno.rb | 24 - spec/ruby/core/argf/shared/pos.rb | 31 - spec/ruby/core/argf/shared/readlines.rb | 22 - spec/ruby/core/argf/tell_spec.rb | 5 +- spec/ruby/core/argf/to_a_spec.rb | 5 +- spec/ruby/core/argf/to_i_spec.rb | 5 +- spec/ruby/core/array/append_spec.rb | 5 +- spec/ruby/core/array/collect_spec.rb | 138 ++- .../ruby/core/array/element_reference_spec.rb | 857 +++++++++++++++++- spec/ruby/core/array/filter_spec.rb | 11 +- spec/ruby/core/array/find_index_spec.rb | 40 +- spec/ruby/core/array/index_spec.rb | 5 +- spec/ruby/core/array/inspect_spec.rb | 105 ++- spec/ruby/core/array/join_spec.rb | 98 +- spec/ruby/core/array/length_spec.rb | 11 +- spec/ruby/core/array/map_spec.rb | 10 +- spec/ruby/core/array/prepend_spec.rb | 6 +- spec/ruby/core/array/push_spec.rb | 33 +- spec/ruby/core/array/replace_spec.rb | 60 +- spec/ruby/core/array/select_spec.rb | 33 +- spec/ruby/core/array/shared/collect.rb | 141 --- spec/ruby/core/array/shared/index.rb | 41 - spec/ruby/core/array/shared/inspect.rb | 107 --- spec/ruby/core/array/shared/join.rb | 97 -- spec/ruby/core/array/shared/length.rb | 11 - spec/ruby/core/array/shared/push.rb | 33 - spec/ruby/core/array/shared/replace.rb | 60 -- spec/ruby/core/array/shared/select.rb | 35 - spec/ruby/core/array/shared/slice.rb | 857 ------------------ spec/ruby/core/array/shared/unshift.rb | 64 -- spec/ruby/core/array/size_spec.rb | 6 +- spec/ruby/core/array/slice_spec.rb | 5 +- spec/ruby/core/array/to_s_spec.rb | 7 +- spec/ruby/core/array/unshift_spec.rb | 64 +- spec/ruby/core/complex/abs_spec.rb | 10 +- spec/ruby/core/complex/angle_spec.rb | 5 +- spec/ruby/core/complex/arg_spec.rb | 9 +- spec/ruby/core/complex/conj_spec.rb | 5 +- spec/ruby/core/complex/conjugate_spec.rb | 8 +- spec/ruby/core/complex/divide_spec.rb | 82 +- spec/ruby/core/complex/imag_spec.rb | 5 +- spec/ruby/core/complex/imaginary_spec.rb | 8 +- spec/ruby/core/complex/magnitude_spec.rb | 5 +- spec/ruby/core/complex/phase_spec.rb | 5 +- spec/ruby/core/complex/quo_spec.rb | 5 +- spec/ruby/core/complex/rect_spec.rb | 9 +- spec/ruby/core/complex/rectangular_spec.rb | 110 ++- spec/ruby/core/complex/shared/abs.rb | 10 - spec/ruby/core/complex/shared/arg.rb | 9 - spec/ruby/core/complex/shared/conjugate.rb | 8 - spec/ruby/core/complex/shared/divide.rb | 82 -- spec/ruby/core/complex/shared/image.rb | 8 - spec/ruby/core/complex/shared/rect.rb | 94 -- spec/ruby/core/data/deconstruct_keys_spec.rb | 2 +- spec/ruby/core/data/initialize_spec.rb | 48 + spec/ruby/core/data/inspect_spec.rb | 61 +- spec/ruby/core/data/shared/inspect.rb | 62 -- spec/ruby/core/data/to_s_spec.rb | 7 +- spec/ruby/core/dir/exist_spec.rb | 57 +- spec/ruby/core/dir/getwd_spec.rb | 12 +- spec/ruby/core/dir/open_spec.rb | 73 +- spec/ruby/core/dir/path_spec.rb | 26 +- spec/ruby/core/dir/pos_spec.rb | 23 +- spec/ruby/core/dir/pwd_spec.rb | 45 +- spec/ruby/core/dir/shared/exist.rb | 57 -- spec/ruby/core/dir/shared/open.rb | 73 -- spec/ruby/core/dir/shared/path.rb | 30 - spec/ruby/core/dir/shared/pos.rb | 27 - spec/ruby/core/dir/shared/pwd.rb | 45 - spec/ruby/core/dir/tell_spec.rb | 27 +- spec/ruby/core/dir/to_path_spec.rb | 12 +- spec/ruby/core/encoding/name_spec.rb | 13 +- spec/ruby/core/encoding/shared/name.rb | 15 - spec/ruby/core/encoding/to_s_spec.rb | 5 +- .../core/enumerable/shared/value_packing.rb | 26 + spec/ruby/core/enumerable/take_spec.rb | 8 + .../core/enumerator/generator/each_spec.rb | 40 - .../enumerator/generator/initialize_spec.rb | 26 - spec/ruby/core/enumerator/lazy/take_spec.rb | 8 + spec/ruby/core/enumerator/new_spec.rb | 59 +- spec/ruby/core/enumerator/produce_spec.rb | 44 + .../ruby/core/enumerator/product/each_spec.rb | 14 + .../ruby/core/enumerator/product/size_spec.rb | 10 + .../core/enumerator/yielder/append_spec.rb | 35 - .../enumerator/yielder/initialize_spec.rb | 18 - .../core/enumerator/yielder/to_proc_spec.rb | 16 - .../core/enumerator/yielder/yield_spec.rb | 33 - spec/ruby/core/hash/each_pair_spec.rb | 106 ++- spec/ruby/core/hash/each_spec.rb | 10 +- spec/ruby/core/hash/element_set_spec.rb | 118 ++- spec/ruby/core/hash/filter_spec.rb | 9 +- spec/ruby/core/hash/has_key_spec.rb | 6 +- spec/ruby/core/hash/has_value_spec.rb | 15 +- spec/ruby/core/hash/include_spec.rb | 39 +- spec/ruby/core/hash/inspect_spec.rb | 122 ++- spec/ruby/core/hash/key_spec.rb | 30 +- spec/ruby/core/hash/length_spec.rb | 6 +- spec/ruby/core/hash/member_spec.rb | 6 +- spec/ruby/core/hash/merge_spec.rb | 6 +- spec/ruby/core/hash/select_spec.rb | 108 ++- spec/ruby/core/hash/shared/each.rb | 105 --- spec/ruby/core/hash/shared/index.rb | 37 - spec/ruby/core/hash/shared/key.rb | 38 - spec/ruby/core/hash/shared/length.rb | 12 - spec/ruby/core/hash/shared/select.rb | 112 --- spec/ruby/core/hash/shared/store.rb | 115 --- spec/ruby/core/hash/shared/to_s.rb | 124 --- spec/ruby/core/hash/shared/update.rb | 76 -- spec/ruby/core/hash/shared/value.rb | 14 - spec/ruby/core/hash/shared/values_at.rb | 9 - spec/ruby/core/hash/size_spec.rb | 13 +- spec/ruby/core/hash/store_spec.rb | 6 +- spec/ruby/core/hash/to_s_spec.rb | 6 +- spec/ruby/core/hash/update_spec.rb | 76 +- spec/ruby/core/hash/value_spec.rb | 6 +- spec/ruby/core/hash/values_at_spec.rb | 10 +- spec/ruby/core/io/buffer/for_spec.rb | 1 + spec/ruby/core/io/buffer/map_spec.rb | 4 +- spec/ruby/core/io/buffer/shared_spec.rb | 4 +- spec/ruby/core/io/buffer/transfer_spec.rb | 3 +- spec/ruby/core/io/buffer/valid_spec.rb | 11 - spec/ruby/core/kernel/Integer_spec.rb | 43 +- .../ruby/core/kernel/caller_locations_spec.rb | 5 +- spec/ruby/core/kernel/caller_spec.rb | 5 +- spec/ruby/core/kernel/inspect_spec.rb | 13 + spec/ruby/core/kernel/require_spec.rb | 28 +- spec/ruby/core/kernel/shared/sprintf.rb | 44 +- spec/ruby/core/method/inspect_spec.rb | 2 + spec/ruby/core/method/original_name_spec.rb | 16 + .../core/method/shared/aliased_inspect.rb | 31 + spec/ruby/core/method/to_s_spec.rb | 2 + spec/ruby/core/mutex/sleep_spec.rb | 8 + spec/ruby/core/process/constants_spec.rb | 46 +- spec/ruby/core/string/shared/each_line.rb | 36 +- spec/ruby/core/thread/raise_spec.rb | 25 + spec/ruby/core/unboundmethod/inspect_spec.rb | 2 + .../core/unboundmethod/original_name_spec.rb | 16 + spec/ruby/core/unboundmethod/to_s_spec.rb | 2 + spec/ruby/language/array_spec.rb | 10 +- spec/ruby/language/case_spec.rb | 35 + spec/ruby/language/defined_spec.rb | 16 + spec/ruby/language/fixtures/defined.rb | 27 + spec/ruby/language/lambda_spec.rb | 6 + .../library/socket/ipsocket/inspect_spec.rb | 24 + spec/ruby/library/socket/socket/tcp_spec.rb | 24 +- spec/ruby/library/socket/spec_helper.rb | 20 +- .../library/socket/tcpsocket/shared/new.rb | 20 +- .../library/socket/udpsocket/inspect_spec.rb | 17 - spec/ruby/optional/capi/ext/gc_spec.c | 29 + spec/ruby/optional/capi/gc_spec.rb | 18 + spec/ruby/optional/capi/string_spec.rb | 6 +- spec/ruby/shared/kernel/raise.rb | 2 +- 176 files changed, 3735 insertions(+), 3516 deletions(-) create mode 100644 spec/ruby/core/argf/inspect_spec.rb delete mode 100644 spec/ruby/core/argf/shared/each_byte.rb delete mode 100644 spec/ruby/core/argf/shared/each_char.rb delete mode 100644 spec/ruby/core/argf/shared/each_codepoint.rb delete mode 100644 spec/ruby/core/argf/shared/each_line.rb delete mode 100644 spec/ruby/core/argf/shared/eof.rb delete mode 100644 spec/ruby/core/argf/shared/filename.rb delete mode 100644 spec/ruby/core/argf/shared/fileno.rb delete mode 100644 spec/ruby/core/argf/shared/pos.rb delete mode 100644 spec/ruby/core/argf/shared/readlines.rb delete mode 100644 spec/ruby/core/array/shared/collect.rb delete mode 100644 spec/ruby/core/array/shared/index.rb delete mode 100644 spec/ruby/core/array/shared/inspect.rb delete mode 100644 spec/ruby/core/array/shared/length.rb delete mode 100644 spec/ruby/core/array/shared/push.rb delete mode 100644 spec/ruby/core/array/shared/replace.rb delete mode 100644 spec/ruby/core/array/shared/select.rb delete mode 100644 spec/ruby/core/array/shared/slice.rb delete mode 100644 spec/ruby/core/array/shared/unshift.rb delete mode 100644 spec/ruby/core/complex/shared/abs.rb delete mode 100644 spec/ruby/core/complex/shared/arg.rb delete mode 100644 spec/ruby/core/complex/shared/conjugate.rb delete mode 100644 spec/ruby/core/complex/shared/divide.rb delete mode 100644 spec/ruby/core/complex/shared/image.rb delete mode 100644 spec/ruby/core/complex/shared/rect.rb delete mode 100644 spec/ruby/core/data/shared/inspect.rb delete mode 100644 spec/ruby/core/dir/shared/exist.rb delete mode 100644 spec/ruby/core/dir/shared/open.rb delete mode 100644 spec/ruby/core/dir/shared/path.rb delete mode 100644 spec/ruby/core/dir/shared/pwd.rb delete mode 100644 spec/ruby/core/encoding/shared/name.rb create mode 100644 spec/ruby/core/enumerable/shared/value_packing.rb delete mode 100644 spec/ruby/core/enumerator/generator/each_spec.rb delete mode 100644 spec/ruby/core/enumerator/generator/initialize_spec.rb delete mode 100644 spec/ruby/core/enumerator/yielder/append_spec.rb delete mode 100644 spec/ruby/core/enumerator/yielder/initialize_spec.rb delete mode 100644 spec/ruby/core/enumerator/yielder/to_proc_spec.rb delete mode 100644 spec/ruby/core/enumerator/yielder/yield_spec.rb delete mode 100644 spec/ruby/core/hash/shared/each.rb delete mode 100644 spec/ruby/core/hash/shared/index.rb delete mode 100644 spec/ruby/core/hash/shared/key.rb delete mode 100644 spec/ruby/core/hash/shared/length.rb delete mode 100644 spec/ruby/core/hash/shared/select.rb delete mode 100644 spec/ruby/core/hash/shared/store.rb delete mode 100644 spec/ruby/core/hash/shared/to_s.rb delete mode 100644 spec/ruby/core/hash/shared/update.rb delete mode 100644 spec/ruby/core/hash/shared/value.rb delete mode 100644 spec/ruby/core/hash/shared/values_at.rb create mode 100644 spec/ruby/core/method/shared/aliased_inspect.rb create mode 100644 spec/ruby/library/socket/ipsocket/inspect_spec.rb delete mode 100644 spec/ruby/library/socket/udpsocket/inspect_spec.rb diff --git a/spec/ruby/.rubocop.yml b/spec/ruby/.rubocop.yml index 0b59a11512f445..0b5dcb80a23c9f 100644 --- a/spec/ruby/.rubocop.yml +++ b/spec/ruby/.rubocop.yml @@ -54,10 +54,6 @@ Lint/RedundantRequireStatement: - library/fiber/**/*.rb - optional/capi/fiber_spec.rb -Lint/RedundantSafeNavigation: - Exclude: - - language/safe_navigator_spec.rb - Lint/RedundantSplatExpansion: Enabled: false diff --git a/spec/ruby/.rubocop_todo.yml b/spec/ruby/.rubocop_todo.yml index bd30f3f14af1ba..f998002c6de44a 100644 --- a/spec/ruby/.rubocop_todo.yml +++ b/spec/ruby/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by # `rubocop --auto-gen-config` -# on 2024-10-12 16:01:45 UTC using RuboCop version 1.66.1. +# on 2026-05-29 08:10:07 UTC using RuboCop version 1.86.2. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new @@ -11,17 +11,22 @@ Lint/DuplicateCaseCondition: Exclude: - 'language/case_spec.rb' -# Offense count: 6 +# Offense count: 20 Lint/DuplicateMethods: Exclude: - 'core/array/fixtures/encoded_strings.rb' - 'core/method/fixtures/classes.rb' + - 'core/module/const_added_spec.rb' + - 'core/module/define_method_spec.rb' - 'core/module/fixtures/classes.rb' + - 'core/module/method_added_spec.rb' + - 'core/module/name_spec.rb' + - 'core/proc/new_spec.rb' - 'core/unboundmethod/fixtures/classes.rb' - 'fixtures/class.rb' + - 'language/assignments_spec.rb' # Offense count: 8 -# This cop supports safe autocorrection (--autocorrect). Lint/EnsureReturn: Exclude: - 'language/fixtures/ensure.rb' @@ -39,12 +44,12 @@ Lint/FloatOutOfRange: Exclude: - 'core/string/modulo_spec.rb' -# Offense count: 2 +# Offense count: 3 # This cop supports safe autocorrection (--autocorrect). Lint/ImplicitStringConcatenation: Exclude: - - 'language/string_spec.rb' - 'core/string/chilled_string_spec.rb' + - 'language/string_spec.rb' # Offense count: 4 Lint/IneffectiveAccessModifier: @@ -53,12 +58,11 @@ Lint/IneffectiveAccessModifier: - 'core/module/fixtures/classes.rb' - 'language/fixtures/private.rb' -# Offense count: 71 +# Offense count: 12 # This cop supports safe autocorrection (--autocorrect). Lint/LiteralInInterpolation: Exclude: - 'core/module/refine_spec.rb' - - 'core/regexp/shared/new.rb' - 'core/string/shared/to_sym.rb' - 'language/alias_spec.rb' - 'language/defined_spec.rb' @@ -80,6 +84,16 @@ Lint/ParenthesesAsGroupedExpression: - 'language/block_spec.rb' - 'language/method_spec.rb' +# Offense count: 1 +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: AllowedMethods, InferNonNilReceiver, AdditionalNilMethods. +# AllowedMethods: instance_of?, kind_of?, is_a?, eql?, respond_to?, equal? +# AdditionalNilMethods: present?, blank?, try, try! +Lint/RedundantSafeNavigation: + Exclude: + - 'language/safe_navigator_spec.rb' + - 'language/fixtures/rescue_captures.rb' + # Offense count: 2 # This cop supports safe autocorrection (--autocorrect). Lint/RedundantStringCoercion: @@ -87,6 +101,7 @@ Lint/RedundantStringCoercion: - 'core/io/print_spec.rb' # Offense count: 1 +# Configuration parameters: AllowRBSInlineAnnotation. Lint/SelfAssignment: Exclude: - 'core/gc/auto_compact_spec.rb' @@ -97,7 +112,7 @@ Lint/ShadowedArgument: Exclude: - 'language/fixtures/super.rb' -# Offense count: 45 +# Offense count: 49 # Configuration parameters: AllowComments, AllowNil. Lint/SuppressedException: Enabled: false @@ -110,13 +125,14 @@ Lint/UnderscorePrefixedVariableName: - 'core/io/popen_spec.rb' - 'language/block_spec.rb' -# Offense count: 7 +# Offense count: 9 # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AutoCorrect, ContextCreatingMethods, MethodCreatingMethods. +# Configuration parameters: ContextCreatingMethods, MethodCreatingMethods. Lint/UselessAccessModifier: Exclude: - 'core/module/define_method_spec.rb' - 'core/module/fixtures/classes.rb' - 'core/module/module_function_spec.rb' - 'core/module/private_class_method_spec.rb' + - 'language/fixtures/def.rb' - 'language/fixtures/send.rb' diff --git a/spec/ruby/CONTRIBUTING.md b/spec/ruby/CONTRIBUTING.md index 0b0f2514409681..a474e205f09e2b 100644 --- a/spec/ruby/CONTRIBUTING.md +++ b/spec/ruby/CONTRIBUTING.md @@ -229,7 +229,7 @@ to avoid duplication of specs, we have shared specs that are re-used in other sp bit tricky however, so let's go over it. Commonly, if a shared spec is only reused within its own module, the shared spec will live within a -shared directory inside that module's directory. For example, the `core/hash/shared/key.rb` spec is +shared directory inside that module's directory. For example, the `core/hash/shared/iteration.rb` spec is only used by `Hash` specs, and so it lives inside `core/hash/shared/`. When a shared spec is used across multiple modules or classes, it lives within the `shared/` directory. @@ -243,25 +243,25 @@ variables from the implementor spec: `@method` and `@object`, which the implemen Here's an example of a snippet of a shared spec and two specs which integrates it: ```ruby -# core/hash/shared/key.rb -describe :hash_key_p, shared: true do - it "returns true if the key's matching value was false" do - { xyz: false }.send(@method, :xyz).should == true +# core/hash/shared/iteration.rb +describe :hash_iteration_no_block, shared: true do + it "returns an Enumerator if called on a non-empty hash without a block" do + { 1 => 2 }.send(@method).should.instance_of?(Enumerator) end end -# core/hash/key_spec.rb -describe "Hash#key?" do - it_behaves_like :hash_key_p, :key? +# core/hash/select_spec.rb +describe "Hash#select" do + it_behaves_like :hash_iteration_no_block, :select end -# core/hash/include_spec.rb -describe "Hash#include?" do - it_behaves_like :hash_key_p, :include? +# core/hash/reject_spec.rb +describe "Hash#reject" do + it_behaves_like :hash_iteration_no_block, :reject end ``` -In the example, the first `describe` defines the shared spec `:hash_key_p`, which defines a spec that +In the example, the first `describe` defines the shared spec `:hash_iteration_no_block`, which defines a spec that calls the `@method` method with an expectation. In the implementor spec, we use `it_behaves_like` to integrate the shared spec. `it_behaves_like` takes 3 parameters: the key of the shared spec, a method, and an object. These last two parameters are accessible via `@method` and `@object` in the shared spec. diff --git a/spec/ruby/bin/rubocop b/spec/ruby/bin/rubocop index 38254f13e48771..0937c479063430 100755 --- a/spec/ruby/bin/rubocop +++ b/spec/ruby/bin/rubocop @@ -6,7 +6,7 @@ require 'bundler/inline' gemfile do source 'https://rubygems.org' - gem 'rubocop', '1.66.1' + gem 'rubocop', '1.86.2' end exec(Gem.bin_path('rubocop', 'rubocop'), *ARGV) diff --git a/spec/ruby/command_line/fixtures/bin/bad_embedded_ruby.txt b/spec/ruby/command_line/fixtures/bin/bad_embedded_ruby.txt index a2b7ad085f5515..61b946977a75d0 100644 --- a/spec/ruby/command_line/fixtures/bin/bad_embedded_ruby.txt +++ b/spec/ruby/command_line/fixtures/bin/bad_embedded_ruby.txt @@ -1,3 +1,3 @@ -@@@This line is not value Ruby +@@@This line is not valid Ruby #!rub_y puts 'success' diff --git a/spec/ruby/command_line/fixtures/bin/embedded_ruby.txt b/spec/ruby/command_line/fixtures/bin/embedded_ruby.txt index 1da779b1b96b37..0ec0f358db7458 100644 --- a/spec/ruby/command_line/fixtures/bin/embedded_ruby.txt +++ b/spec/ruby/command_line/fixtures/bin/embedded_ruby.txt @@ -1,3 +1,3 @@ -@@@This line is not value Ruby +@@@This line is not valid Ruby #!ruby puts 'success' diff --git a/spec/ruby/core/argf/each_byte_spec.rb b/spec/ruby/core/argf/each_byte_spec.rb index c5cce9f2509f97..d9e4e7fe5b9679 100644 --- a/spec/ruby/core/argf/each_byte_spec.rb +++ b/spec/ruby/core/argf/each_byte_spec.rb @@ -1,6 +1,60 @@ require_relative '../../spec_helper' -require_relative 'shared/each_byte' describe "ARGF.each_byte" do - it_behaves_like :argf_each_byte, :each_byte + before :each do + @file1_name = fixture __FILE__, "file1.txt" + @file2_name = fixture __FILE__, "file2.txt" + + @bytes = [] + File.read(@file1_name).each_byte { |b| @bytes << b } + File.read(@file2_name).each_byte { |b| @bytes << b } + end + + it "yields each byte of all streams to the passed block" do + argf [@file1_name, @file2_name] do + bytes = [] + @argf.each_byte { |b| bytes << b } + bytes.should == @bytes + end + end + + it "returns self when passed a block" do + argf [@file1_name, @file2_name] do + @argf.each_byte {}.should.equal?(@argf) + end + end + + it "returns an Enumerator when passed no block" do + argf [@file1_name, @file2_name] do + enum = @argf.each_byte + enum.should.instance_of?(Enumerator) + + bytes = [] + enum.each { |b| bytes << b } + bytes.should == @bytes + end + end + + describe "when no block is given" do + it "returns an Enumerator" do + argf [@file1_name, @file2_name] do + enum = @argf.each_byte + enum.should.instance_of?(Enumerator) + + bytes = [] + enum.each { |b| bytes << b } + bytes.should == @bytes + end + end + + describe "returned Enumerator" do + describe "size" do + it "should return nil" do + argf [@file1_name, @file2_name] do + @argf.each_byte.size.should == nil + end + end + end + end + end end diff --git a/spec/ruby/core/argf/each_char_spec.rb b/spec/ruby/core/argf/each_char_spec.rb index 724e5e6e3e1581..62d19b6574619b 100644 --- a/spec/ruby/core/argf/each_char_spec.rb +++ b/spec/ruby/core/argf/each_char_spec.rb @@ -1,6 +1,60 @@ require_relative '../../spec_helper' -require_relative 'shared/each_char' describe "ARGF.each_char" do - it_behaves_like :argf_each_char, :each_char + before :each do + @file1_name = fixture __FILE__, "file1.txt" + @file2_name = fixture __FILE__, "file2.txt" + + @chars = [] + File.read(@file1_name).each_char { |c| @chars << c } + File.read(@file2_name).each_char { |c| @chars << c } + end + + it "yields each char of all streams to the passed block" do + argf [@file1_name, @file2_name] do + chars = [] + @argf.each_char { |c| chars << c } + chars.should == @chars + end + end + + it "returns self when passed a block" do + argf [@file1_name, @file2_name] do + @argf.each_char {}.should.equal?(@argf) + end + end + + it "returns an Enumerator when passed no block" do + argf [@file1_name, @file2_name] do + enum = @argf.each_char + enum.should.instance_of?(Enumerator) + + chars = [] + enum.each { |c| chars << c } + chars.should == @chars + end + end + + describe "when no block is given" do + it "returns an Enumerator" do + argf [@file1_name, @file2_name] do + enum = @argf.each_char + enum.should.instance_of?(Enumerator) + + chars = [] + enum.each { |c| chars << c } + chars.should == @chars + end + end + + describe "returned Enumerator" do + describe "size" do + it "should return nil" do + argf [@file1_name, @file2_name] do + @argf.each_char.size.should == nil + end + end + end + end + end end diff --git a/spec/ruby/core/argf/each_codepoint_spec.rb b/spec/ruby/core/argf/each_codepoint_spec.rb index 0bf8bf9764bbe9..ad55785cbace0b 100644 --- a/spec/ruby/core/argf/each_codepoint_spec.rb +++ b/spec/ruby/core/argf/each_codepoint_spec.rb @@ -1,6 +1,60 @@ require_relative '../../spec_helper' -require_relative 'shared/each_codepoint' describe "ARGF.each_codepoint" do - it_behaves_like :argf_each_codepoint, :each_codepoint + before :each do + file1_name = fixture __FILE__, "file1.txt" + file2_name = fixture __FILE__, "file2.txt" + @filenames = [file1_name, file2_name] + + @codepoints = File.read(file1_name).codepoints + @codepoints.concat File.read(file2_name).codepoints + end + + it "is a public method" do + argf @filenames do + @argf.public_methods(false).should.include?(:each_codepoint) + end + end + + it "does not require arguments" do + argf @filenames do + @argf.method(:each_codepoint).arity.should == 0 + end + end + + it "returns self when passed a block" do + argf @filenames do + @argf.each_codepoint {}.should.equal?(@argf) + end + end + + it "returns an Enumerator when passed no block" do + argf @filenames do + @argf.each_codepoint.should.instance_of?(Enumerator) + end + end + + it "yields each codepoint of all streams" do + argf @filenames do + @argf.each_codepoint.to_a.should == @codepoints + end + end + + describe "when no block is given" do + it "returns an Enumerator" do + argf @filenames do + @argf.each_codepoint.should.instance_of?(Enumerator) + end + end + + describe "returned Enumerator" do + describe "size" do + it "should return nil" do + argf @filenames do + @argf.each_codepoint.size.should == nil + end + end + end + end + end end diff --git a/spec/ruby/core/argf/each_line_spec.rb b/spec/ruby/core/argf/each_line_spec.rb index 52a7e5c4119307..fc4d6433b881a4 100644 --- a/spec/ruby/core/argf/each_line_spec.rb +++ b/spec/ruby/core/argf/each_line_spec.rb @@ -1,6 +1,64 @@ require_relative '../../spec_helper' -require_relative 'shared/each_line' describe "ARGF.each_line" do - it_behaves_like :argf_each_line, :each_line + before :each do + @file1_name = fixture __FILE__, "file1.txt" + @file2_name = fixture __FILE__, "file2.txt" + + @lines = File.readlines @file1_name + @lines += File.readlines @file2_name + end + + it "is a public method" do + argf [@file1_name, @file2_name] do + @argf.public_methods(false).should.include?(:each_line) + end + end + + it "requires multiple arguments" do + argf [@file1_name, @file2_name] do + @argf.method(:each_line).arity.should < 0 + end + end + + it "reads each line of files" do + argf [@file1_name, @file2_name] do + lines = [] + @argf.each_line { |b| lines << b } + lines.should == @lines + end + end + + it "returns self when passed a block" do + argf [@file1_name, @file2_name] do + @argf.each_line {}.should.equal?(@argf) + end + end + + describe "with a separator" do + it "yields each separated section of all streams" do + argf [@file1_name, @file2_name] do + @argf.send(:each_line, '.').to_a.should == + (File.readlines(@file1_name, '.') + File.readlines(@file2_name, '.')) + end + end + end + + describe "when no block is given" do + it "returns an Enumerator" do + argf [@file1_name, @file2_name] do + @argf.each_line.should.instance_of?(Enumerator) + end + end + + describe "returned Enumerator" do + describe "size" do + it "should return nil" do + argf [@file1_name, @file2_name] do + @argf.each_line.size.should == nil + end + end + end + end + end end diff --git a/spec/ruby/core/argf/each_spec.rb b/spec/ruby/core/argf/each_spec.rb index 5742ba43bdf85d..25f60b31d29dec 100644 --- a/spec/ruby/core/argf/each_spec.rb +++ b/spec/ruby/core/argf/each_spec.rb @@ -1,6 +1,7 @@ require_relative '../../spec_helper' -require_relative 'shared/each_line' describe "ARGF.each" do - it_behaves_like :argf_each_line, :each + it "is an alias of ARGF.each_line" do + ARGF.method(:each).should == ARGF.method(:each_line) + end end diff --git a/spec/ruby/core/argf/eof_spec.rb b/spec/ruby/core/argf/eof_spec.rb index 518f6e566e2c75..940d104d693d55 100644 --- a/spec/ruby/core/argf/eof_spec.rb +++ b/spec/ruby/core/argf/eof_spec.rb @@ -1,10 +1,32 @@ require_relative '../../spec_helper' -require_relative 'shared/eof' describe "ARGF.eof" do - it_behaves_like :argf_eof, :eof + it "is an alias of ARGF.eof?" do + ARGF.method(:eof).should == ARGF.method(:eof?) + end end describe "ARGF.eof?" do - it_behaves_like :argf_eof, :eof? + before :each do + @file1 = fixture __FILE__, "file1.txt" + @file2 = fixture __FILE__, "file2.txt" + end + + # NOTE: this test assumes that fixtures files have two lines each + it "returns true when reaching the end of a file" do + argf [@file1, @file2] do + result = [] + while @argf.gets + result << @argf.eof? + end + result.should == [false, true, false, true] + end + end + + it "raises IOError when called on a closed stream" do + argf [@file1] do + @argf.read + -> { @argf.eof? }.should.raise(IOError) + end + end end diff --git a/spec/ruby/core/argf/filename_spec.rb b/spec/ruby/core/argf/filename_spec.rb index 7c0446269d1483..f4b6e922c6bdc9 100644 --- a/spec/ruby/core/argf/filename_spec.rb +++ b/spec/ruby/core/argf/filename_spec.rb @@ -1,6 +1,30 @@ require_relative '../../spec_helper' -require_relative 'shared/filename' describe "ARGF.filename" do - it_behaves_like :argf_filename, :filename + before :each do + @file1 = fixture __FILE__, "file1.txt" + @file2 = fixture __FILE__, "file2.txt" + end + + # NOTE: this test assumes that fixtures files have two lines each + it "returns the current file name on each file" do + argf [@file1, @file2] do + result = [] + # returns first current file even when not yet open + result << @argf.filename + result << @argf.filename while @argf.gets + # returns last current file even when closed + result << @argf.filename + + result.map! { |f| File.expand_path(f) } + result.should == [@file1, @file1, @file1, @file2, @file2, @file2] + end + end + + # NOTE: this test assumes that fixtures files have two lines each + it "sets the $FILENAME global variable with the current file name on each file" do + script = fixture __FILE__, "filename.rb" + out = ruby_exe(script, args: [@file1, @file2]) + out.should == "#{@file1}\n#{@file1}\n#{@file2}\n#{@file2}\n#{@file2}\n" + end end diff --git a/spec/ruby/core/argf/fileno_spec.rb b/spec/ruby/core/argf/fileno_spec.rb index 29d50c35829657..99245f043c306b 100644 --- a/spec/ruby/core/argf/fileno_spec.rb +++ b/spec/ruby/core/argf/fileno_spec.rb @@ -1,6 +1,26 @@ require_relative '../../spec_helper' -require_relative 'shared/fileno' describe "ARGF.fileno" do - it_behaves_like :argf_fileno, :fileno + before :each do + @file1 = fixture __FILE__, "file1.txt" + @file2 = fixture __FILE__, "file2.txt" + end + + # NOTE: this test assumes that fixtures files have two lines each + it "returns the current file number on each file" do + argf [@file1, @file2] do + result = [] + # returns first current file even when not yet open + result << @argf.fileno while @argf.gets + # returns last current file even when closed + result.map { |d| d.class }.should == [Integer, Integer, Integer, Integer] + end + end + + it "raises an ArgumentError when called on a closed stream" do + argf [@file1] do + @argf.read + -> { @argf.fileno }.should.raise(ArgumentError) + end + end end diff --git a/spec/ruby/core/argf/inspect_spec.rb b/spec/ruby/core/argf/inspect_spec.rb new file mode 100644 index 00000000000000..df0e3ba8dca82d --- /dev/null +++ b/spec/ruby/core/argf/inspect_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' + +describe "ARGF.inspect" do + it "is an alias of ARGF.to_s" do + ARGF.method(:inspect).should == ARGF.method(:to_s) + end +end diff --git a/spec/ruby/core/argf/path_spec.rb b/spec/ruby/core/argf/path_spec.rb index 7120f7d0e33874..2f7b91999f0b52 100644 --- a/spec/ruby/core/argf/path_spec.rb +++ b/spec/ruby/core/argf/path_spec.rb @@ -1,6 +1,7 @@ require_relative '../../spec_helper' -require_relative 'shared/filename' describe "ARGF.path" do - it_behaves_like :argf_filename, :path + it "is an alias of ARGF.filename" do + ARGF.method(:path).should == ARGF.method(:filename) + end end diff --git a/spec/ruby/core/argf/pos_spec.rb b/spec/ruby/core/argf/pos_spec.rb index fb3f25b945b955..1ff16e4f17bca9 100644 --- a/spec/ruby/core/argf/pos_spec.rb +++ b/spec/ruby/core/argf/pos_spec.rb @@ -1,8 +1,35 @@ require_relative '../../spec_helper' -require_relative 'shared/pos' describe "ARGF.pos" do - it_behaves_like :argf_pos, :pos + before :each do + @file1 = fixture __FILE__, "file1.txt" + @file2 = fixture __FILE__, "file2.txt" + end + + it "gives the correct position for each read operation" do + argf [@file1, @file2] do + size1 = File.size(@file1) + size2 = File.size(@file2) + + @argf.read(2) + @argf.pos.should == 2 + @argf.read(size1-2) + @argf.pos.should == size1 + @argf.read(6) + @argf.pos.should == 6 + @argf.rewind + @argf.pos.should == 0 + @argf.read(size2) + @argf.pos.should == size2 + end + end + + it "raises an ArgumentError when called on a closed stream" do + argf [@file1] do + @argf.read + -> { @argf.pos }.should.raise(ArgumentError) + end + end end describe "ARGF.pos=" do diff --git a/spec/ruby/core/argf/readlines_spec.rb b/spec/ruby/core/argf/readlines_spec.rb index 30be936dab0fd4..156bb6a33f1b3f 100644 --- a/spec/ruby/core/argf/readlines_spec.rb +++ b/spec/ruby/core/argf/readlines_spec.rb @@ -1,6 +1,24 @@ require_relative '../../spec_helper' -require_relative 'shared/readlines' describe "ARGF.readlines" do - it_behaves_like :argf_readlines, :readlines + before :each do + @file1 = fixture __FILE__, "file1.txt" + @file2 = fixture __FILE__, "file2.txt" + + @lines = File.readlines(@file1) + @lines += File.readlines(@file2) + end + + it "reads all lines of all files" do + argf [@file1, @file2] do + @argf.readlines.should == @lines + end + end + + it "returns an empty Array when end of stream reached" do + argf [@file1, @file2] do + @argf.read + @argf.readlines.should == [] + end + end end diff --git a/spec/ruby/core/argf/shared/each_byte.rb b/spec/ruby/core/argf/shared/each_byte.rb deleted file mode 100644 index 48c9ae04f88b15..00000000000000 --- a/spec/ruby/core/argf/shared/each_byte.rb +++ /dev/null @@ -1,58 +0,0 @@ -describe :argf_each_byte, shared: true do - before :each do - @file1_name = fixture __FILE__, "file1.txt" - @file2_name = fixture __FILE__, "file2.txt" - - @bytes = [] - File.read(@file1_name).each_byte { |b| @bytes << b } - File.read(@file2_name).each_byte { |b| @bytes << b } - end - - it "yields each byte of all streams to the passed block" do - argf [@file1_name, @file2_name] do - bytes = [] - @argf.send(@method) { |b| bytes << b } - bytes.should == @bytes - end - end - - it "returns self when passed a block" do - argf [@file1_name, @file2_name] do - @argf.send(@method) {}.should.equal?(@argf) - end - end - - it "returns an Enumerator when passed no block" do - argf [@file1_name, @file2_name] do - enum = @argf.send(@method) - enum.should.instance_of?(Enumerator) - - bytes = [] - enum.each { |b| bytes << b } - bytes.should == @bytes - end - end - - describe "when no block is given" do - it "returns an Enumerator" do - argf [@file1_name, @file2_name] do - enum = @argf.send(@method) - enum.should.instance_of?(Enumerator) - - bytes = [] - enum.each { |b| bytes << b } - bytes.should == @bytes - end - end - - describe "returned Enumerator" do - describe "size" do - it "should return nil" do - argf [@file1_name, @file2_name] do - @argf.send(@method).size.should == nil - end - end - end - end - end -end diff --git a/spec/ruby/core/argf/shared/each_char.rb b/spec/ruby/core/argf/shared/each_char.rb deleted file mode 100644 index 4b5e8452ab9193..00000000000000 --- a/spec/ruby/core/argf/shared/each_char.rb +++ /dev/null @@ -1,58 +0,0 @@ -describe :argf_each_char, shared: true do - before :each do - @file1_name = fixture __FILE__, "file1.txt" - @file2_name = fixture __FILE__, "file2.txt" - - @chars = [] - File.read(@file1_name).each_char { |c| @chars << c } - File.read(@file2_name).each_char { |c| @chars << c } - end - - it "yields each char of all streams to the passed block" do - argf [@file1_name, @file2_name] do - chars = [] - @argf.send(@method) { |c| chars << c } - chars.should == @chars - end - end - - it "returns self when passed a block" do - argf [@file1_name, @file2_name] do - @argf.send(@method) {}.should.equal?(@argf) - end - end - - it "returns an Enumerator when passed no block" do - argf [@file1_name, @file2_name] do - enum = @argf.send(@method) - enum.should.instance_of?(Enumerator) - - chars = [] - enum.each { |c| chars << c } - chars.should == @chars - end - end - - describe "when no block is given" do - it "returns an Enumerator" do - argf [@file1_name, @file2_name] do - enum = @argf.send(@method) - enum.should.instance_of?(Enumerator) - - chars = [] - enum.each { |c| chars << c } - chars.should == @chars - end - end - - describe "returned Enumerator" do - describe "size" do - it "should return nil" do - argf [@file1_name, @file2_name] do - @argf.send(@method).size.should == nil - end - end - end - end - end -end diff --git a/spec/ruby/core/argf/shared/each_codepoint.rb b/spec/ruby/core/argf/shared/each_codepoint.rb deleted file mode 100644 index 3137306ad5f572..00000000000000 --- a/spec/ruby/core/argf/shared/each_codepoint.rb +++ /dev/null @@ -1,58 +0,0 @@ -describe :argf_each_codepoint, shared: true do - before :each do - file1_name = fixture __FILE__, "file1.txt" - file2_name = fixture __FILE__, "file2.txt" - @filenames = [file1_name, file2_name] - - @codepoints = File.read(file1_name).codepoints - @codepoints.concat File.read(file2_name).codepoints - end - - it "is a public method" do - argf @filenames do - @argf.public_methods(false).should.include?(@method) - end - end - - it "does not require arguments" do - argf @filenames do - @argf.method(@method).arity.should == 0 - end - end - - it "returns self when passed a block" do - argf @filenames do - @argf.send(@method) {}.should.equal?(@argf) - end - end - - it "returns an Enumerator when passed no block" do - argf @filenames do - @argf.send(@method).should.instance_of?(Enumerator) - end - end - - it "yields each codepoint of all streams" do - argf @filenames do - @argf.send(@method).to_a.should == @codepoints - end - end - - describe "when no block is given" do - it "returns an Enumerator" do - argf @filenames do - @argf.send(@method).should.instance_of?(Enumerator) - end - end - - describe "returned Enumerator" do - describe "size" do - it "should return nil" do - argf @filenames do - @argf.send(@method).size.should == nil - end - end - end - end - end -end diff --git a/spec/ruby/core/argf/shared/each_line.rb b/spec/ruby/core/argf/shared/each_line.rb deleted file mode 100644 index 7e66e38803302c..00000000000000 --- a/spec/ruby/core/argf/shared/each_line.rb +++ /dev/null @@ -1,62 +0,0 @@ -describe :argf_each_line, shared: true do - before :each do - @file1_name = fixture __FILE__, "file1.txt" - @file2_name = fixture __FILE__, "file2.txt" - - @lines = File.readlines @file1_name - @lines += File.readlines @file2_name - end - - it "is a public method" do - argf [@file1_name, @file2_name] do - @argf.public_methods(false).should.include?(@method) - end - end - - it "requires multiple arguments" do - argf [@file1_name, @file2_name] do - @argf.method(@method).arity.should < 0 - end - end - - it "reads each line of files" do - argf [@file1_name, @file2_name] do - lines = [] - @argf.send(@method) { |b| lines << b } - lines.should == @lines - end - end - - it "returns self when passed a block" do - argf [@file1_name, @file2_name] do - @argf.send(@method) {}.should.equal?(@argf) - end - end - - describe "with a separator" do - it "yields each separated section of all streams" do - argf [@file1_name, @file2_name] do - @argf.send(@method, '.').to_a.should == - (File.readlines(@file1_name, '.') + File.readlines(@file2_name, '.')) - end - end - end - - describe "when no block is given" do - it "returns an Enumerator" do - argf [@file1_name, @file2_name] do - @argf.send(@method).should.instance_of?(Enumerator) - end - end - - describe "returned Enumerator" do - describe "size" do - it "should return nil" do - argf [@file1_name, @file2_name] do - @argf.send(@method).size.should == nil - end - end - end - end - end -end diff --git a/spec/ruby/core/argf/shared/eof.rb b/spec/ruby/core/argf/shared/eof.rb deleted file mode 100644 index 8b3897b95204f3..00000000000000 --- a/spec/ruby/core/argf/shared/eof.rb +++ /dev/null @@ -1,24 +0,0 @@ -describe :argf_eof, shared: true do - before :each do - @file1 = fixture __FILE__, "file1.txt" - @file2 = fixture __FILE__, "file2.txt" - end - - # NOTE: this test assumes that fixtures files have two lines each - it "returns true when reaching the end of a file" do - argf [@file1, @file2] do - result = [] - while @argf.gets - result << @argf.send(@method) - end - result.should == [false, true, false, true] - end - end - - it "raises IOError when called on a closed stream" do - argf [@file1] do - @argf.read - -> { @argf.send(@method) }.should.raise(IOError) - end - end -end diff --git a/spec/ruby/core/argf/shared/filename.rb b/spec/ruby/core/argf/shared/filename.rb deleted file mode 100644 index f47c673dc0dec7..00000000000000 --- a/spec/ruby/core/argf/shared/filename.rb +++ /dev/null @@ -1,28 +0,0 @@ -describe :argf_filename, shared: true do - before :each do - @file1 = fixture __FILE__, "file1.txt" - @file2 = fixture __FILE__, "file2.txt" - end - - # NOTE: this test assumes that fixtures files have two lines each - it "returns the current file name on each file" do - argf [@file1, @file2] do - result = [] - # returns first current file even when not yet open - result << @argf.send(@method) - result << @argf.send(@method) while @argf.gets - # returns last current file even when closed - result << @argf.send(@method) - - result.map! { |f| File.expand_path(f) } - result.should == [@file1, @file1, @file1, @file2, @file2, @file2] - end - end - - # NOTE: this test assumes that fixtures files have two lines each - it "sets the $FILENAME global variable with the current file name on each file" do - script = fixture __FILE__, "filename.rb" - out = ruby_exe(script, args: [@file1, @file2]) - out.should == "#{@file1}\n#{@file1}\n#{@file2}\n#{@file2}\n#{@file2}\n" - end -end diff --git a/spec/ruby/core/argf/shared/fileno.rb b/spec/ruby/core/argf/shared/fileno.rb deleted file mode 100644 index e605be46e31e4a..00000000000000 --- a/spec/ruby/core/argf/shared/fileno.rb +++ /dev/null @@ -1,24 +0,0 @@ -describe :argf_fileno, shared: true do - before :each do - @file1 = fixture __FILE__, "file1.txt" - @file2 = fixture __FILE__, "file2.txt" - end - - # NOTE: this test assumes that fixtures files have two lines each - it "returns the current file number on each file" do - argf [@file1, @file2] do - result = [] - # returns first current file even when not yet open - result << @argf.send(@method) while @argf.gets - # returns last current file even when closed - result.map { |d| d.class }.should == [Integer, Integer, Integer, Integer] - end - end - - it "raises an ArgumentError when called on a closed stream" do - argf [@file1] do - @argf.read - -> { @argf.send(@method) }.should.raise(ArgumentError) - end - end -end diff --git a/spec/ruby/core/argf/shared/pos.rb b/spec/ruby/core/argf/shared/pos.rb deleted file mode 100644 index f859d3a29d0ee9..00000000000000 --- a/spec/ruby/core/argf/shared/pos.rb +++ /dev/null @@ -1,31 +0,0 @@ -describe :argf_pos, shared: true do - before :each do - @file1 = fixture __FILE__, "file1.txt" - @file2 = fixture __FILE__, "file2.txt" - end - - it "gives the correct position for each read operation" do - argf [@file1, @file2] do - size1 = File.size(@file1) - size2 = File.size(@file2) - - @argf.read(2) - @argf.send(@method).should == 2 - @argf.read(size1-2) - @argf.send(@method).should == size1 - @argf.read(6) - @argf.send(@method).should == 6 - @argf.rewind - @argf.send(@method).should == 0 - @argf.read(size2) - @argf.send(@method).should == size2 - end - end - - it "raises an ArgumentError when called on a closed stream" do - argf [@file1] do - @argf.read - -> { @argf.send(@method) }.should.raise(ArgumentError) - end - end -end diff --git a/spec/ruby/core/argf/shared/readlines.rb b/spec/ruby/core/argf/shared/readlines.rb deleted file mode 100644 index 505fa94acbf069..00000000000000 --- a/spec/ruby/core/argf/shared/readlines.rb +++ /dev/null @@ -1,22 +0,0 @@ -describe :argf_readlines, shared: true do - before :each do - @file1 = fixture __FILE__, "file1.txt" - @file2 = fixture __FILE__, "file2.txt" - - @lines = File.readlines(@file1) - @lines += File.readlines(@file2) - end - - it "reads all lines of all files" do - argf [@file1, @file2] do - @argf.send(@method).should == @lines - end - end - - it "returns an empty Array when end of stream reached" do - argf [@file1, @file2] do - @argf.read - @argf.send(@method).should == [] - end - end -end diff --git a/spec/ruby/core/argf/tell_spec.rb b/spec/ruby/core/argf/tell_spec.rb index 16d9f2992076bb..bb28df74a252b1 100644 --- a/spec/ruby/core/argf/tell_spec.rb +++ b/spec/ruby/core/argf/tell_spec.rb @@ -1,6 +1,7 @@ require_relative '../../spec_helper' -require_relative 'shared/pos' describe "ARGF.tell" do - it_behaves_like :argf_pos, :tell + it "is an alias of ARGF.pos" do + ARGF.method(:tell).should == ARGF.method(:pos) + end end diff --git a/spec/ruby/core/argf/to_a_spec.rb b/spec/ruby/core/argf/to_a_spec.rb index b17a93db33d4b9..d95dc732ec9649 100644 --- a/spec/ruby/core/argf/to_a_spec.rb +++ b/spec/ruby/core/argf/to_a_spec.rb @@ -1,6 +1,7 @@ require_relative '../../spec_helper' -require_relative 'shared/readlines' describe "ARGF.to_a" do - it_behaves_like :argf_readlines, :to_a + it "is an alias of ARGF.readlines" do + ARGF.method(:to_a).should == ARGF.method(:readlines) + end end diff --git a/spec/ruby/core/argf/to_i_spec.rb b/spec/ruby/core/argf/to_i_spec.rb index 2183de6cd49a71..e8df378f4ecfd0 100644 --- a/spec/ruby/core/argf/to_i_spec.rb +++ b/spec/ruby/core/argf/to_i_spec.rb @@ -1,6 +1,7 @@ require_relative '../../spec_helper' -require_relative 'shared/fileno' describe "ARGF.to_i" do - it_behaves_like :argf_fileno, :to_i + it "is an alias of ARGF.fileno" do + ARGF.method(:to_i).should == ARGF.method(:fileno) + end end diff --git a/spec/ruby/core/array/append_spec.rb b/spec/ruby/core/array/append_spec.rb index de0e56b5133bcb..5480d9f65ebc60 100644 --- a/spec/ruby/core/array/append_spec.rb +++ b/spec/ruby/core/array/append_spec.rb @@ -1,6 +1,5 @@ require_relative '../../spec_helper' require_relative 'fixtures/classes' -require_relative 'shared/push' describe "Array#<<" do it "pushes the object onto the end of the array" do @@ -36,5 +35,7 @@ end describe "Array#append" do - it_behaves_like :array_push, :append + it "is an alias of Array#push" do + Array.instance_method(:append).should == Array.instance_method(:push) + end end diff --git a/spec/ruby/core/array/collect_spec.rb b/spec/ruby/core/array/collect_spec.rb index 0ad4c283b1562c..43a539f805668a 100644 --- a/spec/ruby/core/array/collect_spec.rb +++ b/spec/ruby/core/array/collect_spec.rb @@ -1,11 +1,143 @@ require_relative '../../spec_helper' require_relative 'fixtures/classes' -require_relative 'shared/collect' +require_relative '../enumerable/shared/enumeratorized' +require_relative 'shared/iterable_and_tolerating_size_increasing' describe "Array#collect" do - it_behaves_like :array_collect, :collect + it "returns a copy of array with each element replaced by the value returned by block" do + a = ['a', 'b', 'c', 'd'] + b = a.collect { |i| i + '!' } + b.should == ["a!", "b!", "c!", "d!"] + b.should_not.equal? a + end + + it "does not return subclass instance" do + ArraySpecs::MyArray[1, 2, 3].collect { |x| x + 1 }.should.instance_of?(Array) + end + + it "does not change self" do + a = ['a', 'b', 'c', 'd'] + a.collect { |i| i + '!' } + a.should == ['a', 'b', 'c', 'd'] + end + + it "returns the evaluated value of block if it broke in the block" do + a = ['a', 'b', 'c', 'd'] + b = a.collect {|i| + if i == 'c' + break 0 + else + i + '!' + end + } + b.should == 0 + end + + it "returns an Enumerator when no block given" do + a = [1, 2, 3] + a.collect.should.instance_of?(Enumerator) + end + + it "raises an ArgumentError when no block and with arguments" do + a = [1, 2, 3] + -> { + a.collect(:foo) + }.should.raise(ArgumentError) + end + + before :each do + @object = [1, 2, 3, 4] + end + it_behaves_like :enumeratorized_with_origin_size, :collect + + it_behaves_like :array_iterable_and_tolerating_size_increasing, :collect end describe "Array#collect!" do - it_behaves_like :array_collect_b, :collect! + it "replaces each element with the value returned by block" do + a = [7, 9, 3, 5] + a.collect! { |i| i - 1 }.should.equal?(a) + a.should == [6, 8, 2, 4] + end + + it "returns self" do + a = [1, 2, 3, 4, 5] + b = a.collect! {|i| i+1 } + a.should.equal? b + end + + it "returns the evaluated value of block but its contents is partially modified, if it broke in the block" do + a = ['a', 'b', 'c', 'd'] + b = a.collect! {|i| + if i == 'c' + break 0 + else + i + '!' + end + } + b.should == 0 + a.should == ['a!', 'b!', 'c', 'd'] + end + + it "returns an Enumerator when no block given, and the enumerator can modify the original array" do + a = [1, 2, 3] + enum = a.collect! + enum.should.instance_of?(Enumerator) + enum.each{|i| "#{i}!" } + a.should == ["1!", "2!", "3!"] + end + + describe "when frozen" do + it "raises a FrozenError" do + -> { ArraySpecs.frozen_array.collect! {} }.should.raise(FrozenError) + end + + it "raises a FrozenError when empty" do + -> { ArraySpecs.empty_frozen_array.collect! {} }.should.raise(FrozenError) + end + + it "raises a FrozenError when calling #each on the returned Enumerator" do + enumerator = ArraySpecs.frozen_array.collect! + -> { enumerator.each {|x| x } }.should.raise(FrozenError) + end + + it "raises a FrozenError when calling #each on the returned Enumerator when empty" do + enumerator = ArraySpecs.empty_frozen_array.collect! + -> { enumerator.each {|x| x } }.should.raise(FrozenError) + end + end + + it "does not truncate the array is the block raises an exception" do + a = [1, 2, 3] + begin + a.collect! { raise StandardError, 'Oops' } + rescue + end + + a.should == [1, 2, 3] + end + + it "only changes elements before error is raised, keeping the element which raised an error." do + a = [1, 2, 3, 4] + begin + a.collect! do |e| + case e + when 1 then -1 + when 2 then -2 + when 3 then raise StandardError, 'Oops' + else 0 + end + end + rescue StandardError + end + + a.should == [-1, -2, 3, 4] + end + + before :each do + @object = [1, 2, 3, 4] + end + it_behaves_like :enumeratorized_with_origin_size, :collect! + + it_behaves_like :array_iterable_and_tolerating_size_increasing, :collect! end diff --git a/spec/ruby/core/array/element_reference_spec.rb b/spec/ruby/core/array/element_reference_spec.rb index eb41a9e199c041..d5f4b54961d23d 100644 --- a/spec/ruby/core/array/element_reference_spec.rb +++ b/spec/ruby/core/array/element_reference_spec.rb @@ -1,9 +1,862 @@ require_relative '../../spec_helper' require_relative 'fixtures/classes' -require_relative 'shared/slice' describe "Array#[]" do - it_behaves_like :array_slice, :[] + it "returns the element at index with [index]" do + [ "a", "b", "c", "d", "e" ][1].should == "b" + + a = [1, 2, 3, 4] + + a[0].should == 1 + a[1].should == 2 + a[2].should == 3 + a[3].should == 4 + a[4].should == nil + a[10].should == nil + + a.should == [1, 2, 3, 4] + end + + it "returns the element at index from the end of the array with [-index]" do + [ "a", "b", "c", "d", "e" ][-2].should == "d" + + a = [1, 2, 3, 4] + + a[-1].should == 4 + a[-2].should == 3 + a[-3].should == 2 + a[-4].should == 1 + a[-5].should == nil + a[-10].should == nil + + a.should == [1, 2, 3, 4] + end + + it "returns count elements starting from index with [index, count]" do + [ "a", "b", "c", "d", "e" ][2, 3].should == ["c", "d", "e"] + + a = [1, 2, 3, 4] + + a[0, 0].should == [] + a[0, 1].should == [1] + a[0, 2].should == [1, 2] + a[0, 4].should == [1, 2, 3, 4] + a[0, 6].should == [1, 2, 3, 4] + a[0, -1].should == nil + a[0, -2].should == nil + a[0, -4].should == nil + + a[2, 0].should == [] + a[2, 1].should == [3] + a[2, 2].should == [3, 4] + a[2, 4].should == [3, 4] + a[2, -1].should == nil + + a[4, 0].should == [] + a[4, 2].should == [] + a[4, -1].should == nil + + a[5, 0].should == nil + a[5, 2].should == nil + a[5, -1].should == nil + + a[6, 0].should == nil + a[6, 2].should == nil + a[6, -1].should == nil + + a.should == [1, 2, 3, 4] + end + + it "returns count elements starting at index from the end of array with [-index, count]" do + [ "a", "b", "c", "d", "e" ][-2, 2].should == ["d", "e"] + + a = [1, 2, 3, 4] + + a[-1, 0].should == [] + a[-1, 1].should == [4] + a[-1, 2].should == [4] + a[-1, -1].should == nil + + a[-2, 0].should == [] + a[-2, 1].should == [3] + a[-2, 2].should == [3, 4] + a[-2, 4].should == [3, 4] + a[-2, -1].should == nil + + a[-4, 0].should == [] + a[-4, 1].should == [1] + a[-4, 2].should == [1, 2] + a[-4, 4].should == [1, 2, 3, 4] + a[-4, 6].should == [1, 2, 3, 4] + a[-4, -1].should == nil + + a[-5, 0].should == nil + a[-5, 1].should == nil + a[-5, 10].should == nil + a[-5, -1].should == nil + + a.should == [1, 2, 3, 4] + end + + it "returns the first count elements with [0, count]" do + [ "a", "b", "c", "d", "e" ][0, 3].should == ["a", "b", "c"] + end + + it "returns the subarray which is independent to self with [index,count]" do + a = [1, 2, 3] + sub = a[1, 2] + sub.replace([:a, :b]) + a.should == [1, 2, 3] + end + + it "tries to convert the passed argument to an Integer using #to_int" do + obj = mock('to_int') + obj.stub!(:to_int).and_return(2) + + a = [1, 2, 3, 4] + a[obj].should == 3 + a[obj, 1].should == [3] + a[obj, obj].should == [3, 4] + a[0, obj].should == [1, 2] + end + + it "raises TypeError if to_int returns non-integer" do + from = mock('from') + to = mock('to') + + # So we can construct a range out of them... + def from.<=>(o) 0 end + def to.<=>(o) 0 end + + a = [1, 2, 3, 4, 5] + + def from.to_int() 'cat' end + def to.to_int() -2 end + + -> { a[from..to] }.should.raise(TypeError) + + def from.to_int() 1 end + def to.to_int() 'cat' end + + -> { a[from..to] }.should.raise(TypeError) + end + + it "returns the elements specified by Range indexes with [m..n]" do + [ "a", "b", "c", "d", "e" ][1..3].should == ["b", "c", "d"] + [ "a", "b", "c", "d", "e" ][4..-1].should == ['e'] + [ "a", "b", "c", "d", "e" ][3..3].should == ['d'] + [ "a", "b", "c", "d", "e" ][3..-2].should == ['d'] + ['a'][0..-1].should == ['a'] + + a = [1, 2, 3, 4] + + a[0..-10].should == [] + a[0..0].should == [1] + a[0..1].should == [1, 2] + a[0..2].should == [1, 2, 3] + a[0..3].should == [1, 2, 3, 4] + a[0..4].should == [1, 2, 3, 4] + a[0..10].should == [1, 2, 3, 4] + + a[2..-10].should == [] + a[2..0].should == [] + a[2..2].should == [3] + a[2..3].should == [3, 4] + a[2..4].should == [3, 4] + + a[3..0].should == [] + a[3..3].should == [4] + a[3..4].should == [4] + + a[4..0].should == [] + a[4..4].should == [] + a[4..5].should == [] + + a[5..0].should == nil + a[5..5].should == nil + a[5..6].should == nil + + a.should == [1, 2, 3, 4] + end + + it "returns elements specified by Range indexes except the element at index n with [m...n]" do + [ "a", "b", "c", "d", "e" ][1...3].should == ["b", "c"] + + a = [1, 2, 3, 4] + + a[0...-10].should == [] + a[0...0].should == [] + a[0...1].should == [1] + a[0...2].should == [1, 2] + a[0...3].should == [1, 2, 3] + a[0...4].should == [1, 2, 3, 4] + a[0...10].should == [1, 2, 3, 4] + + a[2...-10].should == [] + a[2...0].should == [] + a[2...2].should == [] + a[2...3].should == [3] + a[2...4].should == [3, 4] + + a[3...0].should == [] + a[3...3].should == [] + a[3...4].should == [4] + + a[4...0].should == [] + a[4...4].should == [] + a[4...5].should == [] + + a[5...0].should == nil + a[5...5].should == nil + a[5...6].should == nil + + a.should == [1, 2, 3, 4] + end + + it "returns elements that exist if range start is in the array but range end is not with [m..n]" do + [ "a", "b", "c", "d", "e" ][4..7].should == ["e"] + end + + it "accepts Range instances having a negative m and both signs for n with [m..n] and [m...n]" do + a = [1, 2, 3, 4] + + a[-1..-1].should == [4] + a[-1...-1].should == [] + a[-1..3].should == [4] + a[-1...3].should == [] + a[-1..4].should == [4] + a[-1...4].should == [4] + a[-1..10].should == [4] + a[-1...10].should == [4] + a[-1..0].should == [] + a[-1..-4].should == [] + a[-1...-4].should == [] + a[-1..-6].should == [] + a[-1...-6].should == [] + + a[-2..-2].should == [3] + a[-2...-2].should == [] + a[-2..-1].should == [3, 4] + a[-2...-1].should == [3] + a[-2..10].should == [3, 4] + a[-2...10].should == [3, 4] + + a[-4..-4].should == [1] + a[-4..-2].should == [1, 2, 3] + a[-4...-2].should == [1, 2] + a[-4..-1].should == [1, 2, 3, 4] + a[-4...-1].should == [1, 2, 3] + a[-4..3].should == [1, 2, 3, 4] + a[-4...3].should == [1, 2, 3] + a[-4..4].should == [1, 2, 3, 4] + a[-4...4].should == [1, 2, 3, 4] + a[-4...4].should == [1, 2, 3, 4] + a[-4..0].should == [1] + a[-4...0].should == [] + a[-4..1].should == [1, 2] + a[-4...1].should == [1] + + a[-5..-5].should == nil + a[-5...-5].should == nil + a[-5..-4].should == nil + a[-5..-1].should == nil + a[-5..10].should == nil + + a.should == [1, 2, 3, 4] + end + + it "returns the subarray which is independent to self with [m..n]" do + a = [1, 2, 3] + sub = a[1..2] + sub.replace([:a, :b]) + a.should == [1, 2, 3] + end + + it "tries to convert Range elements to Integers using #to_int with [m..n] and [m...n]" do + from = mock('from') + to = mock('to') + + # So we can construct a range out of them... + def from.<=>(o) 0 end + def to.<=>(o) 0 end + + def from.to_int() 1 end + def to.to_int() -2 end + + a = [1, 2, 3, 4] + + a[from..to].should == [2, 3] + a[from...to].should == [2] + a[1..0].should == [] + a[1...0].should == [] + + -> { a["a" .. "b"] }.should.raise(TypeError) + -> { a["a" ... "b"] }.should.raise(TypeError) + -> { a[from .. "b"] }.should.raise(TypeError) + -> { a[from ... "b"] }.should.raise(TypeError) + end + + it "returns the same elements as [m..n] and [m...n] with Range subclasses" do + a = [1, 2, 3, 4] + range_incl = ArraySpecs::MyRange.new(1, 2) + range_excl = ArraySpecs::MyRange.new(-3, -1, true) + + a[range_incl].should == [2, 3] + a[range_excl].should == [2, 3] + end + + it "returns nil for a requested index not in the array with [index]" do + [ "a", "b", "c", "d", "e" ][5].should == nil + end + + it "returns [] if the index is valid but length is zero with [index, length]" do + [ "a", "b", "c", "d", "e" ][0, 0].should == [] + [ "a", "b", "c", "d", "e" ][2, 0].should == [] + end + + it "returns nil if length is zero but index is invalid with [index, length]" do + [ "a", "b", "c", "d", "e" ][100, 0].should == nil + [ "a", "b", "c", "d", "e" ][-50, 0].should == nil + end + + # This is by design. It is in the official documentation. + it "returns [] if index == array.size with [index, length]" do + %w|a b c d e|[5, 2].should == [] + end + + it "returns nil if index > array.size with [index, length]" do + %w|a b c d e|[6, 2].should == nil + end + + it "returns nil if length is negative with [index, length]" do + %w|a b c d e|[3, -1].should == nil + %w|a b c d e|[2, -2].should == nil + %w|a b c d e|[1, -100].should == nil + end + + it "returns nil if no requested index is in the array with [m..n]" do + [ "a", "b", "c", "d", "e" ][6..10].should == nil + end + + it "returns nil if range start is not in the array with [m..n]" do + [ "a", "b", "c", "d", "e" ][-10..2].should == nil + [ "a", "b", "c", "d", "e" ][10..12].should == nil + end + + it "returns an empty array when m == n with [m...n]" do + [1, 2, 3, 4, 5][1...1].should == [] + end + + it "returns an empty array with [0...0]" do + [1, 2, 3, 4, 5][0...0].should == [] + end + + it "returns a subarray where m, n negatives and m < n with [m..n]" do + [ "a", "b", "c", "d", "e" ][-3..-2].should == ["c", "d"] + end + + it "returns an array containing the first element with [0..0]" do + [1, 2, 3, 4, 5][0..0].should == [1] + end + + it "returns the entire array with [0..-1]" do + [1, 2, 3, 4, 5][0..-1].should == [1, 2, 3, 4, 5] + end + + it "returns all but the last element with [0...-1]" do + [1, 2, 3, 4, 5][0...-1].should == [1, 2, 3, 4] + end + + it "returns [3] for [2..-1] out of [1, 2, 3]" do + [1,2,3][2..-1].should == [3] + end + + it "returns an empty array when m > n and m, n are positive with [m..n]" do + [1, 2, 3, 4, 5][3..2].should == [] + end + + it "returns an empty array when m > n and m, n are negative with [m..n]" do + [1, 2, 3, 4, 5][-2..-3].should == [] + end + + it "does not expand array when the indices are outside of the array bounds" do + a = [1, 2] + a[4].should == nil + a.should == [1, 2] + a[4, 0].should == nil + a.should == [1, 2] + a[6, 1].should == nil + a.should == [1, 2] + a[8...8].should == nil + a.should == [1, 2] + a[10..10].should == nil + a.should == [1, 2] + end + + describe "with a subclass of Array" do + before :each do + ScratchPad.clear + + @array = ArraySpecs::MyArray[1, 2, 3, 4, 5] + end + + it "returns a Array instance with [n, m]" do + @array[0, 2].should.instance_of?(Array) + end + + it "returns a Array instance with [-n, m]" do + @array[-3, 2].should.instance_of?(Array) + end + + it "returns a Array instance with [n..m]" do + @array[1..3].should.instance_of?(Array) + end + + it "returns a Array instance with [n...m]" do + @array[1...3].should.instance_of?(Array) + end + + it "returns a Array instance with [-n..-m]" do + @array[-3..-1].should.instance_of?(Array) + end + + it "returns a Array instance with [-n...-m]" do + @array[-3...-1].should.instance_of?(Array) + end + + it "returns an empty array when m == n with [m...n]" do + @array[1...1].should == [] + ScratchPad.recorded.should == nil + end + + it "returns an empty array with [0...0]" do + @array[0...0].should == [] + ScratchPad.recorded.should == nil + end + + it "returns an empty array when m > n and m, n are positive with [m..n]" do + @array[3..2].should == [] + ScratchPad.recorded.should == nil + end + + it "returns an empty array when m > n and m, n are negative with [m..n]" do + @array[-2..-3].should == [] + ScratchPad.recorded.should == nil + end + + it "returns [] if index == array.size with [index, length]" do + @array[5, 2].should == [] + ScratchPad.recorded.should == nil + end + + it "returns [] if the index is valid but length is zero with [index, length]" do + @array[0, 0].should == [] + @array[2, 0].should == [] + ScratchPad.recorded.should == nil + end + + it "does not call #initialize on the subclass instance" do + @array[0, 3].should == [1, 2, 3] + ScratchPad.recorded.should == nil + end + end + + it "raises a RangeError when the start index is out of range of Fixnum" do + array = [1, 2, 3, 4, 5, 6] + obj = mock('large value') + obj.should_receive(:to_int).and_return(bignum_value) + -> { array[obj] }.should.raise(RangeError) + + obj = 8e19 + -> { array[obj] }.should.raise(RangeError) + + # boundary value when longs are 64 bits + -> { array[2.0**63] }.should.raise(RangeError) + + # just under the boundary value when longs are 64 bits + array[max_long.to_f.prev_float].should == nil + end + + it "raises a RangeError when the length is out of range of Fixnum" do + array = [1, 2, 3, 4, 5, 6] + obj = mock('large value') + obj.should_receive(:to_int).and_return(bignum_value) + -> { array[1, obj] }.should.raise(RangeError) + + obj = 8e19 + -> { array[1, obj] }.should.raise(RangeError) + end + + it "raises a type error if a range is passed with a length" do + ->{ [1, 2, 3][1..2, 1] }.should.raise(TypeError) + end + + it "raises a RangeError if passed a range with a bound that is too large" do + array = [1, 2, 3, 4, 5, 6] + -> { array[bignum_value..(bignum_value + 1)] }.should.raise(RangeError) + -> { array[0..bignum_value] }.should.raise(RangeError) + end + + it "can accept endless ranges" do + a = [0, 1, 2, 3, 4, 5] + a[eval("(2..)")].should == [2, 3, 4, 5] + a[eval("(2...)")].should == [2, 3, 4, 5] + a[eval("(-2..)")].should == [4, 5] + a[eval("(-2...)")].should == [4, 5] + a[eval("(9..)")].should == nil + a[eval("(9...)")].should == nil + a[eval("(-9..)")].should == nil + a[eval("(-9...)")].should == nil + end + + describe "can be sliced with Enumerator::ArithmeticSequence" do + before :each do + @array = [0, 1, 2, 3, 4, 5] + end + + it "has endless range and positive steps" do + @array[eval("(0..).step(1)")].should == [0, 1, 2, 3, 4, 5] + @array[eval("(0..).step(2)")].should == [0, 2, 4] + @array[eval("(0..).step(10)")].should == [0] + + @array[eval("(2..).step(1)")].should == [2, 3, 4, 5] + @array[eval("(2..).step(2)")].should == [2, 4] + @array[eval("(2..).step(10)")].should == [2] + + @array[eval("(-3..).step(1)")].should == [3, 4, 5] + @array[eval("(-3..).step(2)")].should == [3, 5] + @array[eval("(-3..).step(10)")].should == [3] + end + + it "has beginless range and positive steps" do + # end with zero index + @array[(..0).step(1)].should == [0] + @array[(...0).step(1)].should == [] + + @array[(..0).step(2)].should == [0] + @array[(...0).step(2)].should == [] + + @array[(..0).step(10)].should == [0] + @array[(...0).step(10)].should == [] + + # end with positive index + @array[(..3).step(1)].should == [0, 1, 2, 3] + @array[(...3).step(1)].should == [0, 1, 2] + + @array[(..3).step(2)].should == [0, 2] + @array[(...3).step(2)].should == [0, 2] + + @array[(..3).step(10)].should == [0] + @array[(...3).step(10)].should == [0] + + # end with negative index + @array[(..-2).step(1)].should == [0, 1, 2, 3, 4,] + @array[(...-2).step(1)].should == [0, 1, 2, 3] + + @array[(..-2).step(2)].should == [0, 2, 4] + @array[(...-2).step(2)].should == [0, 2] + + @array[(..-2).step(10)].should == [0] + @array[(...-2).step(10)].should == [0] + end + + it "has endless range and negative steps" do + @array[eval("(0..).step(-1)")].should == [0] + @array[eval("(0..).step(-2)")].should == [0] + @array[eval("(0..).step(-10)")].should == [0] + + @array[eval("(2..).step(-1)")].should == [2, 1, 0] + @array[eval("(2..).step(-2)")].should == [2, 0] + + @array[eval("(-3..).step(-1)")].should == [3, 2, 1, 0] + @array[eval("(-3..).step(-2)")].should == [3, 1] + end + + it "has closed range and positive steps" do + # start and end with 0 + @array[eval("(0..0).step(1)")].should == [0] + @array[eval("(0...0).step(1)")].should == [] + + @array[eval("(0..0).step(2)")].should == [0] + @array[eval("(0...0).step(2)")].should == [] + + @array[eval("(0..0).step(10)")].should == [0] + @array[eval("(0...0).step(10)")].should == [] + + # start and end with positive index + @array[eval("(1..3).step(1)")].should == [1, 2, 3] + @array[eval("(1...3).step(1)")].should == [1, 2] + + @array[eval("(1..3).step(2)")].should == [1, 3] + @array[eval("(1...3).step(2)")].should == [1] + + @array[eval("(1..3).step(10)")].should == [1] + @array[eval("(1...3).step(10)")].should == [1] + + # start with positive index, end with negative index + @array[eval("(1..-2).step(1)")].should == [1, 2, 3, 4] + @array[eval("(1...-2).step(1)")].should == [1, 2, 3] + + @array[eval("(1..-2).step(2)")].should == [1, 3] + @array[eval("(1...-2).step(2)")].should == [1, 3] + + @array[eval("(1..-2).step(10)")].should == [1] + @array[eval("(1...-2).step(10)")].should == [1] + + # start with negative index, end with positive index + @array[eval("(-4..4).step(1)")].should == [2, 3, 4] + @array[eval("(-4...4).step(1)")].should == [2, 3] + + @array[eval("(-4..4).step(2)")].should == [2, 4] + @array[eval("(-4...4).step(2)")].should == [2] + + @array[eval("(-4..4).step(10)")].should == [2] + @array[eval("(-4...4).step(10)")].should == [2] + + # start with negative index, end with negative index + @array[eval("(-4..-2).step(1)")].should == [2, 3, 4] + @array[eval("(-4...-2).step(1)")].should == [2, 3] + + @array[eval("(-4..-2).step(2)")].should == [2, 4] + @array[eval("(-4...-2).step(2)")].should == [2] + + @array[eval("(-4..-2).step(10)")].should == [2] + @array[eval("(-4...-2).step(10)")].should == [2] + end + + it "has closed range and negative steps" do + # start and end with 0 + @array[eval("(0..0).step(-1)")].should == [0] + @array[eval("(0...0).step(-1)")].should == [] + + @array[eval("(0..0).step(-2)")].should == [0] + @array[eval("(0...0).step(-2)")].should == [] + + @array[eval("(0..0).step(-10)")].should == [0] + @array[eval("(0...0).step(-10)")].should == [] + + # start and end with positive index + @array[eval("(1..3).step(-1)")].should == [] + @array[eval("(1...3).step(-1)")].should == [] + + @array[eval("(1..3).step(-2)")].should == [] + @array[eval("(1...3).step(-2)")].should == [] + + @array[eval("(1..3).step(-10)")].should == [] + @array[eval("(1...3).step(-10)")].should == [] + + # start with positive index, end with negative index + @array[eval("(1..-2).step(-1)")].should == [] + @array[eval("(1...-2).step(-1)")].should == [] + + @array[eval("(1..-2).step(-2)")].should == [] + @array[eval("(1...-2).step(-2)")].should == [] + + @array[eval("(1..-2).step(-10)")].should == [] + @array[eval("(1...-2).step(-10)")].should == [] + + # start with negative index, end with positive index + @array[eval("(-4..4).step(-1)")].should == [] + @array[eval("(-4...4).step(-1)")].should == [] + + @array[eval("(-4..4).step(-2)")].should == [] + @array[eval("(-4...4).step(-2)")].should == [] + + @array[eval("(-4..4).step(-10)")].should == [] + @array[eval("(-4...4).step(-10)")].should == [] + + # start with negative index, end with negative index + @array[eval("(-4..-2).step(-1)")].should == [] + @array[eval("(-4...-2).step(-1)")].should == [] + + @array[eval("(-4..-2).step(-2)")].should == [] + @array[eval("(-4...-2).step(-2)")].should == [] + + @array[eval("(-4..-2).step(-10)")].should == [] + @array[eval("(-4...-2).step(-10)")].should == [] + end + + it "has inverted closed range and positive steps" do + # start and end with positive index + @array[eval("(3..1).step(1)")].should == [] + @array[eval("(3...1).step(1)")].should == [] + + @array[eval("(3..1).step(2)")].should == [] + @array[eval("(3...1).step(2)")].should == [] + + @array[eval("(3..1).step(10)")].should == [] + @array[eval("(3...1).step(10)")].should == [] + + # start with negative index, end with positive index + @array[eval("(-2..1).step(1)")].should == [] + @array[eval("(-2...1).step(1)")].should == [] + + @array[eval("(-2..1).step(2)")].should == [] + @array[eval("(-2...1).step(2)")].should == [] + + @array[eval("(-2..1).step(10)")].should == [] + @array[eval("(-2...1).step(10)")].should == [] + + # start with positive index, end with negative index + @array[eval("(4..-4).step(1)")].should == [] + @array[eval("(4...-4).step(1)")].should == [] + + @array[eval("(4..-4).step(2)")].should == [] + @array[eval("(4...-4).step(2)")].should == [] + + @array[eval("(4..-4).step(10)")].should == [] + @array[eval("(4...-4).step(10)")].should == [] + + # start with negative index, end with negative index + @array[eval("(-2..-4).step(1)")].should == [] + @array[eval("(-2...-4).step(1)")].should == [] + + @array[eval("(-2..-4).step(2)")].should == [] + @array[eval("(-2...-4).step(2)")].should == [] + + @array[eval("(-2..-4).step(10)")].should == [] + @array[eval("(-2...-4).step(10)")].should == [] + end + + it "has range with bounds outside of array" do + # end is equal to array's length + @array[(0..6).step(1)].should == [0, 1, 2, 3, 4, 5] + -> { @array[(0..6).step(2)] }.should.raise(RangeError) + + # end is greater than length with positive steps + @array[(1..6).step(2)].should == [1, 3, 5] + @array[(2..7).step(2)].should == [2, 4] + -> { @array[(2..8).step(2)] }.should.raise(RangeError) + + # begin is greater than length with negative steps + @array[(6..1).step(-2)].should == [5, 3, 1] + @array[(7..2).step(-2)].should == [5, 3] + -> { @array[(8..2).step(-2)] }.should.raise(RangeError) + end + + it "has endless range with start outside of array's bounds" do + @array[eval("(6..).step(1)")].should == [] + @array[eval("(7..).step(1)")].should == nil + + @array[eval("(6..).step(2)")].should == [] + -> { @array[eval("(7..).step(2)")] }.should.raise(RangeError) + end + end + + it "can accept beginless ranges" do + a = [0, 1, 2, 3, 4, 5] + a[(..3)].should == [0, 1, 2, 3] + a[(...3)].should == [0, 1, 2] + a[(..-3)].should == [0, 1, 2, 3] + a[(...-3)].should == [0, 1, 2] + a[(..0)].should == [0] + a[(...0)].should == [] + a[(..9)].should == [0, 1, 2, 3, 4, 5] + a[(...9)].should == [0, 1, 2, 3, 4, 5] + a[(..-9)].should == [] + a[(...-9)].should == [] + end + + describe "can be sliced with Enumerator::ArithmeticSequence" do + it "with infinite/inverted ranges and negative steps" do + array = [0, 1, 2, 3, 4, 5] + array[(2..).step(-1)].should == [2, 1, 0] + array[(2..).step(-2)].should == [2, 0] + array[(2..).step(-3)].should == [2] + array[(2..).step(-4)].should == [2] + + array[(-3..).step(-1)].should == [3, 2, 1, 0] + array[(-3..).step(-2)].should == [3, 1] + array[(-3..).step(-3)].should == [3, 0] + array[(-3..).step(-4)].should == [3] + array[(-3..).step(-5)].should == [3] + + array[(..0).step(-1)].should == [5, 4, 3, 2, 1, 0] + array[(..0).step(-2)].should == [5, 3, 1] + array[(..0).step(-3)].should == [5, 2] + array[(..0).step(-4)].should == [5, 1] + array[(..0).step(-5)].should == [5, 0] + array[(..0).step(-6)].should == [5] + array[(..0).step(-7)].should == [5] + + array[(...0).step(-1)].should == [5, 4, 3, 2, 1] + array[(...0).step(-2)].should == [5, 3, 1] + array[(...0).step(-3)].should == [5, 2] + array[(...0).step(-4)].should == [5, 1] + array[(...0).step(-5)].should == [5] + array[(...0).step(-6)].should == [5] + + array[(...1).step(-1)].should == [5, 4, 3, 2] + array[(...1).step(-2)].should == [5, 3] + array[(...1).step(-3)].should == [5, 2] + array[(...1).step(-4)].should == [5] + array[(...1).step(-5)].should == [5] + + array[(..-5).step(-1)].should == [5, 4, 3, 2, 1] + array[(..-5).step(-2)].should == [5, 3, 1] + array[(..-5).step(-3)].should == [5, 2] + array[(..-5).step(-4)].should == [5, 1] + array[(..-5).step(-5)].should == [5] + array[(..-5).step(-6)].should == [5] + + array[(...-5).step(-1)].should == [5, 4, 3, 2] + array[(...-5).step(-2)].should == [5, 3] + array[(...-5).step(-3)].should == [5, 2] + array[(...-5).step(-4)].should == [5] + array[(...-5).step(-5)].should == [5] + + array[(4..1).step(-1)].should == [4, 3, 2, 1] + array[(4..1).step(-2)].should == [4, 2] + array[(4..1).step(-3)].should == [4, 1] + array[(4..1).step(-4)].should == [4] + array[(4..1).step(-5)].should == [4] + + array[(4...1).step(-1)].should == [4, 3, 2] + array[(4...1).step(-2)].should == [4, 2] + array[(4...1).step(-3)].should == [4] + array[(4...1).step(-4)].should == [4] + + array[(-2..1).step(-1)].should == [4, 3, 2, 1] + array[(-2..1).step(-2)].should == [4, 2] + array[(-2..1).step(-3)].should == [4, 1] + array[(-2..1).step(-4)].should == [4] + array[(-2..1).step(-5)].should == [4] + + array[(-2...1).step(-1)].should == [4, 3, 2] + array[(-2...1).step(-2)].should == [4, 2] + array[(-2...1).step(-3)].should == [4] + array[(-2...1).step(-4)].should == [4] + + array[(4..-5).step(-1)].should == [4, 3, 2, 1] + array[(4..-5).step(-2)].should == [4, 2] + array[(4..-5).step(-3)].should == [4, 1] + array[(4..-5).step(-4)].should == [4] + array[(4..-5).step(-5)].should == [4] + + array[(4...-5).step(-1)].should == [4, 3, 2] + array[(4...-5).step(-2)].should == [4, 2] + array[(4...-5).step(-3)].should == [4] + array[(4...-5).step(-4)].should == [4] + + array[(-2..-5).step(-1)].should == [4, 3, 2, 1] + array[(-2..-5).step(-2)].should == [4, 2] + array[(-2..-5).step(-3)].should == [4, 1] + array[(-2..-5).step(-4)].should == [4] + array[(-2..-5).step(-5)].should == [4] + + array[(-2...-5).step(-1)].should == [4, 3, 2] + array[(-2...-5).step(-2)].should == [4, 2] + array[(-2...-5).step(-3)].should == [4] + array[(-2...-5).step(-4)].should == [4] + end + end + + it "can accept nil...nil ranges" do + a = [0, 1, 2, 3, 4, 5] + a[eval("(nil...nil)")].should == a + a[(...nil)].should == a + a[eval("(nil..)")].should == a + end end describe "Array.[]" do diff --git a/spec/ruby/core/array/filter_spec.rb b/spec/ruby/core/array/filter_spec.rb index 7807c3886d4929..6ebda61069c421 100644 --- a/spec/ruby/core/array/filter_spec.rb +++ b/spec/ruby/core/array/filter_spec.rb @@ -1,14 +1,13 @@ require_relative '../../spec_helper' -require_relative 'shared/select' describe "Array#filter" do - it_behaves_like :array_select, :filter + it "is an alias of Array#select" do + Array.instance_method(:filter).should == Array.instance_method(:select) + end end describe "Array#filter!" do - it "returns nil if no changes were made in the array" do - [1, 2, 3].filter! { true }.should == nil + it "is an alias of Array#select!" do + Array.instance_method(:filter!).should == Array.instance_method(:select!) end - - it_behaves_like :keep_if, :filter! end diff --git a/spec/ruby/core/array/find_index_spec.rb b/spec/ruby/core/array/find_index_spec.rb index 759472024ad03a..17ff6c04a7515f 100644 --- a/spec/ruby/core/array/find_index_spec.rb +++ b/spec/ruby/core/array/find_index_spec.rb @@ -1,6 +1,42 @@ require_relative '../../spec_helper' -require_relative 'shared/index' +require_relative 'shared/iterable_and_tolerating_size_increasing' describe "Array#find_index" do - it_behaves_like :array_index, :find_index + it "returns the index of the first element == to object" do + x = mock('3') + def x.==(obj) 3 == obj; end + + [2, x, 3, 1, 3, 1].find_index(3).should == 1 + [2, 3.0, 3, x, 1, 3, 1].find_index(x).should == 1 + end + + it "returns 0 if first element == to object" do + [2, 1, 3, 2, 5].find_index(2).should == 0 + end + + it "returns size-1 if only last element == to object" do + [2, 1, 3, 1, 5].find_index(5).should == 4 + end + + it "returns nil if no element == to object" do + [2, 1, 1, 1, 1].find_index(3).should == nil + end + + it "accepts a block instead of an argument" do + [4, 2, 1, 5, 1, 3].find_index {|x| x < 2}.should == 2 + end + + it "ignores the block if there is an argument" do + -> { + [4, 2, 1, 5, 1, 3].find_index(5) {|x| x < 2}.should == 3 + }.should complain(/given block not used/) + end + + describe "given no argument and no block" do + it "produces an Enumerator" do + [].find_index.should.instance_of?(Enumerator) + end + end + + it_behaves_like :array_iterable_and_tolerating_size_increasing, :find_index end diff --git a/spec/ruby/core/array/index_spec.rb b/spec/ruby/core/array/index_spec.rb index 3acb7d0ef30471..b66cb6eb5340ca 100644 --- a/spec/ruby/core/array/index_spec.rb +++ b/spec/ruby/core/array/index_spec.rb @@ -1,6 +1,7 @@ require_relative '../../spec_helper' -require_relative 'shared/index' describe "Array#index" do - it_behaves_like :array_index, :index + it "is an alias of Array#find_index" do + Array.instance_method(:index).should == Array.instance_method(:find_index) + end end diff --git a/spec/ruby/core/array/inspect_spec.rb b/spec/ruby/core/array/inspect_spec.rb index 0832224f5ace9d..e5dca828895e0d 100644 --- a/spec/ruby/core/array/inspect_spec.rb +++ b/spec/ruby/core/array/inspect_spec.rb @@ -1,7 +1,108 @@ require_relative '../../spec_helper' require_relative 'fixtures/classes' -require_relative 'shared/inspect' describe "Array#inspect" do - it_behaves_like :array_inspect, :inspect + it "returns a string" do + [1, 2, 3].inspect.should.instance_of?(String) + end + + it "returns '[]' for an empty Array" do + [].inspect.should == "[]" + end + + it "calls inspect on its elements and joins the results with commas" do + items = Array.new(3) do |i| + obj = mock(i.to_s) + obj.should_receive(:inspect).and_return(i.to_s) + obj + end + items.inspect.should == "[0, 1, 2]" + end + + it "does not call #to_s on a String returned from #inspect" do + str = +"abc" + str.should_not_receive(:to_s) + + [str].inspect.should == '["abc"]' + end + + it "calls #to_s on the object returned from #inspect if the Object isn't a String" do + obj = mock("Array#inspect/to_s calls #to_s") + obj.should_receive(:inspect).and_return(obj) + obj.should_receive(:to_s).and_return("abc") + + [obj].inspect.should == "[abc]" + end + + it "does not call #to_str on the object returned from #inspect when it is not a String" do + obj = mock("Array#inspect/to_s does not call #to_str") + obj.should_receive(:inspect).and_return(obj) + obj.should_not_receive(:to_str) + + [obj].inspect.should =~ /^\[#\]$/ + end + + it "does not call #to_str on the object returned from #to_s when it is not a String" do + obj = mock("Array#inspect/to_s does not call #to_str on #to_s result") + obj.should_receive(:inspect).and_return(obj) + obj.should_receive(:to_s).and_return(obj) + obj.should_not_receive(:to_str) + + [obj].inspect.should =~ /^\[#\]$/ + end + + it "does not swallow exceptions raised by #to_s" do + obj = mock("Array#inspect/to_s does not swallow #to_s exceptions") + obj.should_receive(:inspect).and_return(obj) + obj.should_receive(:to_s).and_raise(Exception) + + -> { [obj].inspect }.should.raise(Exception) + end + + it "represents a recursive element with '[...]'" do + ArraySpecs.recursive_array.inspect.should == "[1, \"two\", 3.0, [...], [...], [...], [...], [...]]" + ArraySpecs.head_recursive_array.inspect.should == "[[...], [...], [...], [...], [...], 1, \"two\", 3.0]" + ArraySpecs.empty_recursive_array.inspect.should == "[[...]]" + end + + describe "with encoding" do + before :each do + @default_external_encoding = Encoding.default_external + end + + after :each do + Encoding.default_external = @default_external_encoding + end + + it "returns a US-ASCII string for an empty Array" do + [].inspect.encoding.should == Encoding::US_ASCII + end + + it "use the default external encoding if it is ascii compatible" do + Encoding.default_external = Encoding.find('UTF-8') + + utf8 = "utf8".encode("UTF-8") + jp = "jp".encode("EUC-JP") + array = [jp, utf8] + + array.inspect.encoding.name.should == "UTF-8" + end + + it "use US-ASCII encoding if the default external encoding is not ascii compatible" do + Encoding.default_external = Encoding.find('UTF-32') + + utf8 = "utf8".encode("UTF-8") + jp = "jp".encode("EUC-JP") + array = [jp, utf8] + + array.inspect.encoding.name.should == "US-ASCII" + end + + it "does not raise if inspected result is not default external encoding" do + utf_16be = mock(+"utf_16be") + utf_16be.should_receive(:inspect).and_return(%<"utf_16be \u3042">.encode(Encoding::UTF_16BE)) + + [utf_16be].inspect.should == '["utf_16be \u3042"]' + end + end end diff --git a/spec/ruby/core/array/join_spec.rb b/spec/ruby/core/array/join_spec.rb index 811db036a844c5..3b4946a99fd6e6 100644 --- a/spec/ruby/core/array/join_spec.rb +++ b/spec/ruby/core/array/join_spec.rb @@ -4,7 +4,6 @@ describe "Array#join" do it_behaves_like :array_join_with_string_separator, :join - it_behaves_like :array_join_with_default_separator, :join it "does not separate elements when the passed separator is nil" do [1, 2, 3].join(nil).should == '123' @@ -32,6 +31,103 @@ end end +describe "Array#join with default separator" do + before :each do + @separator = $, + end + + after :each do + $, = @separator + end + + it "returns an empty string if the Array is empty" do + [].join.should == '' + end + + it "returns a US-ASCII string for an empty Array" do + [].join.encoding.should == Encoding::US_ASCII + end + + it "returns a string formed by concatenating each String element separated by $," do + suppress_warning { + $, = " | " + ["1", "2", "3"].join.should == "1 | 2 | 3" + } + end + + it "attempts coercion via #to_str first" do + obj = mock('foo') + obj.should_receive(:to_str).any_number_of_times.and_return("foo") + [obj].join.should == "foo" + end + + it "attempts coercion via #to_ary second" do + obj = mock('foo') + obj.should_receive(:to_str).any_number_of_times.and_return(nil) + obj.should_receive(:to_ary).any_number_of_times.and_return(["foo"]) + [obj].join.should == "foo" + end + + it "attempts coercion via #to_s third" do + obj = mock('foo') + obj.should_receive(:to_str).any_number_of_times.and_return(nil) + obj.should_receive(:to_ary).any_number_of_times.and_return(nil) + obj.should_receive(:to_s).any_number_of_times.and_return("foo") + [obj].join.should == "foo" + end + + it "raises a NoMethodError if an element does not respond to #to_str, #to_ary, or #to_s" do + obj = mock('o') + class << obj; undef :to_s; end + -> { [1, obj].join }.should.raise(NoMethodError) + end + + it "raises an ArgumentError when the Array is recursive" do + -> { ArraySpecs.recursive_array.join }.should.raise(ArgumentError) + -> { ArraySpecs.head_recursive_array.join }.should.raise(ArgumentError) + -> { ArraySpecs.empty_recursive_array.join }.should.raise(ArgumentError) + end + + it "uses the first encoding when other strings are compatible" do + ary1 = ArraySpecs.array_with_7bit_utf8_and_usascii_strings + ary2 = ArraySpecs.array_with_usascii_and_7bit_utf8_strings + ary3 = ArraySpecs.array_with_utf8_and_7bit_binary_strings + ary4 = ArraySpecs.array_with_usascii_and_7bit_binary_strings + + ary1.join.encoding.should == Encoding::UTF_8 + ary2.join.encoding.should == Encoding::US_ASCII + ary3.join.encoding.should == Encoding::UTF_8 + ary4.join.encoding.should == Encoding::US_ASCII + end + + it "uses the widest common encoding when other strings are incompatible" do + ary1 = ArraySpecs.array_with_utf8_and_usascii_strings + ary2 = ArraySpecs.array_with_usascii_and_utf8_strings + + ary1.join.encoding.should == Encoding::UTF_8 + ary2.join.encoding.should == Encoding::UTF_8 + end + + it "fails for arrays with incompatibly-encoded strings" do + ary_utf8_bad_binary = ArraySpecs.array_with_utf8_and_binary_strings + + -> { ary_utf8_bad_binary.join }.should.raise(EncodingError) + end + + context "when $, is not nil" do + before do + suppress_warning do + $, = '*' + end + end + + it "warns" do + -> { [].join }.should complain(/warning: \$, is set to non-nil value/) + -> { [].join(nil) }.should complain(/warning: \$, is set to non-nil value/) + end + end +end + describe "Array#join with $," do before :each do @before_separator = $, diff --git a/spec/ruby/core/array/length_spec.rb b/spec/ruby/core/array/length_spec.rb index a90c00130088b4..74b2eb3a08cc9b 100644 --- a/spec/ruby/core/array/length_spec.rb +++ b/spec/ruby/core/array/length_spec.rb @@ -1,7 +1,14 @@ require_relative '../../spec_helper' require_relative 'fixtures/classes' -require_relative 'shared/length' describe "Array#length" do - it_behaves_like :array_length, :length + it "returns the number of elements" do + [].length.should == 0 + [1, 2, 3].length.should == 3 + end + + it "properly handles recursive arrays" do + ArraySpecs.empty_recursive_array.length.should == 1 + ArraySpecs.recursive_array.length.should == 8 + end end diff --git a/spec/ruby/core/array/map_spec.rb b/spec/ruby/core/array/map_spec.rb index 0c7f3afa8c029a..f5e88c86245767 100644 --- a/spec/ruby/core/array/map_spec.rb +++ b/spec/ruby/core/array/map_spec.rb @@ -1,11 +1,13 @@ require_relative '../../spec_helper' -require_relative 'fixtures/classes' -require_relative 'shared/collect' describe "Array#map" do - it_behaves_like :array_collect, :map + it "is an alias of Array#collect" do + Array.instance_method(:map).should == Array.instance_method(:collect) + end end describe "Array#map!" do - it_behaves_like :array_collect_b, :map! + it "is an alias of Array#collect!" do + Array.instance_method(:map!).should == Array.instance_method(:collect!) + end end diff --git a/spec/ruby/core/array/prepend_spec.rb b/spec/ruby/core/array/prepend_spec.rb index 368b8dcfcd5fa4..2d0ce31c7141ca 100644 --- a/spec/ruby/core/array/prepend_spec.rb +++ b/spec/ruby/core/array/prepend_spec.rb @@ -1,7 +1,7 @@ require_relative '../../spec_helper' -require_relative 'fixtures/classes' -require_relative 'shared/unshift' describe "Array#prepend" do - it_behaves_like :array_unshift, :prepend + it "is an alias of Array#unshift" do + Array.instance_method(:prepend).should == Array.instance_method(:unshift) + end end diff --git a/spec/ruby/core/array/push_spec.rb b/spec/ruby/core/array/push_spec.rb index 607cbc7b4dc4c4..6255a84371748d 100644 --- a/spec/ruby/core/array/push_spec.rb +++ b/spec/ruby/core/array/push_spec.rb @@ -1,7 +1,36 @@ require_relative '../../spec_helper' require_relative 'fixtures/classes' -require_relative 'shared/push' describe "Array#push" do - it_behaves_like :array_push, :push + it "appends the arguments to the array" do + a = [ "a", "b", "c" ] + a.push("d", "e", "f").should.equal?(a) + a.push.should == ["a", "b", "c", "d", "e", "f"] + a.push(5) + a.should == ["a", "b", "c", "d", "e", "f", 5] + + a = [0, 1] + a.push(2) + a.should == [0, 1, 2] + end + + it "isn't confused by previous shift" do + a = [ "a", "b", "c" ] + a.shift + a.push("foo") + a.should == ["b", "c", "foo"] + end + + it "properly handles recursive arrays" do + empty = ArraySpecs.empty_recursive_array + empty.push(:last).should == [empty, :last] + + array = ArraySpecs.recursive_array + array.push(:last).should == [1, 'two', 3.0, array, array, array, array, array, :last] + end + + it "raises a FrozenError on a frozen array" do + -> { ArraySpecs.frozen_array.push(1) }.should.raise(FrozenError) + -> { ArraySpecs.frozen_array.push }.should.raise(FrozenError) + end end diff --git a/spec/ruby/core/array/replace_spec.rb b/spec/ruby/core/array/replace_spec.rb index 2f53338f5e4b04..ee6a98a6461ec5 100644 --- a/spec/ruby/core/array/replace_spec.rb +++ b/spec/ruby/core/array/replace_spec.rb @@ -1,7 +1,63 @@ require_relative '../../spec_helper' require_relative 'fixtures/classes' -require_relative 'shared/replace' describe "Array#replace" do - it_behaves_like :array_replace, :replace + it "replaces the elements with elements from other array" do + a = [1, 2, 3, 4, 5] + b = ['a', 'b', 'c'] + a.replace(b).should.equal?(a) + a.should == b + a.should_not.equal?(b) + + a.replace([4] * 10) + a.should == [4] * 10 + + a.replace([]) + a.should == [] + end + + it "properly handles recursive arrays" do + orig = [1, 2, 3] + empty = ArraySpecs.empty_recursive_array + orig.replace(empty) + orig.should == empty + + array = ArraySpecs.recursive_array + orig.replace(array) + orig.should == array + end + + it "returns self" do + ary = [1, 2, 3] + other = [:a, :b, :c] + ary.replace(other).should.equal?(ary) + end + + it "does not make self dependent to the original array" do + ary = [1, 2, 3] + other = [:a, :b, :c] + ary.replace(other) + ary.should == [:a, :b, :c] + ary << :d + ary.should == [:a, :b, :c, :d] + other.should == [:a, :b, :c] + end + + it "tries to convert the passed argument to an Array using #to_ary" do + obj = mock('to_ary') + obj.stub!(:to_ary).and_return([1, 2, 3]) + [].replace(obj).should == [1, 2, 3] + end + + it "does not call #to_ary on Array subclasses" do + obj = ArraySpecs::ToAryArray[5, 6, 7] + obj.should_not_receive(:to_ary) + [].replace(ArraySpecs::ToAryArray[5, 6, 7]).should == [5, 6, 7] + end + + it "raises a FrozenError on a frozen array" do + -> { + ArraySpecs.frozen_array.replace(ArraySpecs.frozen_array) + }.should.raise(FrozenError) + end end diff --git a/spec/ruby/core/array/select_spec.rb b/spec/ruby/core/array/select_spec.rb index e8775ee5ac5223..57ec0b2540eace 100644 --- a/spec/ruby/core/array/select_spec.rb +++ b/spec/ruby/core/array/select_spec.rb @@ -1,8 +1,37 @@ require_relative '../../spec_helper' -require_relative 'shared/select' +require_relative '../enumerable/shared/enumeratorized' +require_relative 'fixtures/classes' +require_relative 'shared/enumeratorize' +require_relative 'shared/iterable_and_tolerating_size_increasing' +require_relative 'shared/keep_if' describe "Array#select" do - it_behaves_like :array_select, :select + it_behaves_like :enumeratorize, :select + + it_behaves_like :array_iterable_and_tolerating_size_increasing, :select + + before :each do + @object = [1,2,3] + end + it_behaves_like :enumeratorized_with_origin_size, :select + + it "returns a new array of elements for which block is true" do + [1, 3, 4, 5, 6, 9].select { |i| i % ((i + 1) / 2) == 0}.should == [1, 4, 6] + end + + it "does not return subclass instance on Array subclasses" do + ArraySpecs::MyArray[1, 2, 3].select { true }.should.instance_of?(Array) + end + + it "properly handles recursive arrays" do + empty = ArraySpecs.empty_recursive_array + empty.select { true }.should == empty + empty.select { false }.should == [] + + array = ArraySpecs.recursive_array + array.select { true }.should == [1, 'two', 3.0, array, array, array, array, array] + array.select { false }.should == [] + end end describe "Array#select!" do diff --git a/spec/ruby/core/array/shared/collect.rb b/spec/ruby/core/array/shared/collect.rb deleted file mode 100644 index aec51c9dc9623f..00000000000000 --- a/spec/ruby/core/array/shared/collect.rb +++ /dev/null @@ -1,141 +0,0 @@ -require_relative '../../enumerable/shared/enumeratorized' -require_relative '../shared/iterable_and_tolerating_size_increasing' - -describe :array_collect, shared: true do - it "returns a copy of array with each element replaced by the value returned by block" do - a = ['a', 'b', 'c', 'd'] - b = a.send(@method) { |i| i + '!' } - b.should == ["a!", "b!", "c!", "d!"] - b.should_not.equal? a - end - - it "does not return subclass instance" do - ArraySpecs::MyArray[1, 2, 3].send(@method) { |x| x + 1 }.should.instance_of?(Array) - end - - it "does not change self" do - a = ['a', 'b', 'c', 'd'] - a.send(@method) { |i| i + '!' } - a.should == ['a', 'b', 'c', 'd'] - end - - it "returns the evaluated value of block if it broke in the block" do - a = ['a', 'b', 'c', 'd'] - b = a.send(@method) {|i| - if i == 'c' - break 0 - else - i + '!' - end - } - b.should == 0 - end - - it "returns an Enumerator when no block given" do - a = [1, 2, 3] - a.send(@method).should.instance_of?(Enumerator) - end - - it "raises an ArgumentError when no block and with arguments" do - a = [1, 2, 3] - -> { - a.send(@method, :foo) - }.should.raise(ArgumentError) - end - - before :all do - @object = [1, 2, 3, 4] - end - it_should_behave_like :enumeratorized_with_origin_size - - it_should_behave_like :array_iterable_and_tolerating_size_increasing -end - -describe :array_collect_b, shared: true do - it "replaces each element with the value returned by block" do - a = [7, 9, 3, 5] - a.send(@method) { |i| i - 1 }.should.equal?(a) - a.should == [6, 8, 2, 4] - end - - it "returns self" do - a = [1, 2, 3, 4, 5] - b = a.send(@method) {|i| i+1 } - a.should.equal? b - end - - it "returns the evaluated value of block but its contents is partially modified, if it broke in the block" do - a = ['a', 'b', 'c', 'd'] - b = a.send(@method) {|i| - if i == 'c' - break 0 - else - i + '!' - end - } - b.should == 0 - a.should == ['a!', 'b!', 'c', 'd'] - end - - it "returns an Enumerator when no block given, and the enumerator can modify the original array" do - a = [1, 2, 3] - enum = a.send(@method) - enum.should.instance_of?(Enumerator) - enum.each{|i| "#{i}!" } - a.should == ["1!", "2!", "3!"] - end - - describe "when frozen" do - it "raises a FrozenError" do - -> { ArraySpecs.frozen_array.send(@method) {} }.should.raise(FrozenError) - end - - it "raises a FrozenError when empty" do - -> { ArraySpecs.empty_frozen_array.send(@method) {} }.should.raise(FrozenError) - end - - it "raises a FrozenError when calling #each on the returned Enumerator" do - enumerator = ArraySpecs.frozen_array.send(@method) - -> { enumerator.each {|x| x } }.should.raise(FrozenError) - end - - it "raises a FrozenError when calling #each on the returned Enumerator when empty" do - enumerator = ArraySpecs.empty_frozen_array.send(@method) - -> { enumerator.each {|x| x } }.should.raise(FrozenError) - end - end - - it "does not truncate the array is the block raises an exception" do - a = [1, 2, 3] - begin - a.send(@method) { raise StandardError, 'Oops' } - rescue - end - - a.should == [1, 2, 3] - end - - it "only changes elements before error is raised, keeping the element which raised an error." do - a = [1, 2, 3, 4] - begin - a.send(@method) do |e| - case e - when 1 then -1 - when 2 then -2 - when 3 then raise StandardError, 'Oops' - else 0 - end - end - rescue StandardError - end - - a.should == [-1, -2, 3, 4] - end - - before :all do - @object = [1, 2, 3, 4] - end - it_should_behave_like :enumeratorized_with_origin_size - - it_should_behave_like :array_iterable_and_tolerating_size_increasing -end diff --git a/spec/ruby/core/array/shared/index.rb b/spec/ruby/core/array/shared/index.rb deleted file mode 100644 index cc6d6cfb5b6f83..00000000000000 --- a/spec/ruby/core/array/shared/index.rb +++ /dev/null @@ -1,41 +0,0 @@ -require_relative '../shared/iterable_and_tolerating_size_increasing' - -describe :array_index, shared: true do - it "returns the index of the first element == to object" do - x = mock('3') - def x.==(obj) 3 == obj; end - - [2, x, 3, 1, 3, 1].send(@method, 3).should == 1 - [2, 3.0, 3, x, 1, 3, 1].send(@method, x).should == 1 - end - - it "returns 0 if first element == to object" do - [2, 1, 3, 2, 5].send(@method, 2).should == 0 - end - - it "returns size-1 if only last element == to object" do - [2, 1, 3, 1, 5].send(@method, 5).should == 4 - end - - it "returns nil if no element == to object" do - [2, 1, 1, 1, 1].send(@method, 3).should == nil - end - - it "accepts a block instead of an argument" do - [4, 2, 1, 5, 1, 3].send(@method) {|x| x < 2}.should == 2 - end - - it "ignores the block if there is an argument" do - -> { - [4, 2, 1, 5, 1, 3].send(@method, 5) {|x| x < 2}.should == 3 - }.should complain(/given block not used/) - end - - describe "given no argument and no block" do - it "produces an Enumerator" do - [].send(@method).should.instance_of?(Enumerator) - end - end - - it_should_behave_like :array_iterable_and_tolerating_size_increasing -end diff --git a/spec/ruby/core/array/shared/inspect.rb b/spec/ruby/core/array/shared/inspect.rb deleted file mode 100644 index 7197cd7f264813..00000000000000 --- a/spec/ruby/core/array/shared/inspect.rb +++ /dev/null @@ -1,107 +0,0 @@ -require_relative '../fixtures/encoded_strings' - -describe :array_inspect, shared: true do - it "returns a string" do - [1, 2, 3].send(@method).should.instance_of?(String) - end - - it "returns '[]' for an empty Array" do - [].send(@method).should == "[]" - end - - it "calls inspect on its elements and joins the results with commas" do - items = Array.new(3) do |i| - obj = mock(i.to_s) - obj.should_receive(:inspect).and_return(i.to_s) - obj - end - items.send(@method).should == "[0, 1, 2]" - end - - it "does not call #to_s on a String returned from #inspect" do - str = +"abc" - str.should_not_receive(:to_s) - - [str].send(@method).should == '["abc"]' - end - - it "calls #to_s on the object returned from #inspect if the Object isn't a String" do - obj = mock("Array#inspect/to_s calls #to_s") - obj.should_receive(:inspect).and_return(obj) - obj.should_receive(:to_s).and_return("abc") - - [obj].send(@method).should == "[abc]" - end - - it "does not call #to_str on the object returned from #inspect when it is not a String" do - obj = mock("Array#inspect/to_s does not call #to_str") - obj.should_receive(:inspect).and_return(obj) - obj.should_not_receive(:to_str) - - [obj].send(@method).should =~ /^\[#\]$/ - end - - it "does not call #to_str on the object returned from #to_s when it is not a String" do - obj = mock("Array#inspect/to_s does not call #to_str on #to_s result") - obj.should_receive(:inspect).and_return(obj) - obj.should_receive(:to_s).and_return(obj) - obj.should_not_receive(:to_str) - - [obj].send(@method).should =~ /^\[#\]$/ - end - - it "does not swallow exceptions raised by #to_s" do - obj = mock("Array#inspect/to_s does not swallow #to_s exceptions") - obj.should_receive(:inspect).and_return(obj) - obj.should_receive(:to_s).and_raise(Exception) - - -> { [obj].send(@method) }.should.raise(Exception) - end - - it "represents a recursive element with '[...]'" do - ArraySpecs.recursive_array.send(@method).should == "[1, \"two\", 3.0, [...], [...], [...], [...], [...]]" - ArraySpecs.head_recursive_array.send(@method).should == "[[...], [...], [...], [...], [...], 1, \"two\", 3.0]" - ArraySpecs.empty_recursive_array.send(@method).should == "[[...]]" - end - - describe "with encoding" do - before :each do - @default_external_encoding = Encoding.default_external - end - - after :each do - Encoding.default_external = @default_external_encoding - end - - it "returns a US-ASCII string for an empty Array" do - [].send(@method).encoding.should == Encoding::US_ASCII - end - - it "use the default external encoding if it is ascii compatible" do - Encoding.default_external = Encoding.find('UTF-8') - - utf8 = "utf8".encode("UTF-8") - jp = "jp".encode("EUC-JP") - array = [jp, utf8] - - array.send(@method).encoding.name.should == "UTF-8" - end - - it "use US-ASCII encoding if the default external encoding is not ascii compatible" do - Encoding.default_external = Encoding.find('UTF-32') - - utf8 = "utf8".encode("UTF-8") - jp = "jp".encode("EUC-JP") - array = [jp, utf8] - - array.send(@method).encoding.name.should == "US-ASCII" - end - - it "does not raise if inspected result is not default external encoding" do - utf_16be = mock(+"utf_16be") - utf_16be.should_receive(:inspect).and_return(%<"utf_16be \u3042">.encode(Encoding::UTF_16BE)) - - [utf_16be].send(@method).should == '["utf_16be \u3042"]' - end - end -end diff --git a/spec/ruby/core/array/shared/join.rb b/spec/ruby/core/array/shared/join.rb index 2be60a4dbcd226..93d5329ee39f0d 100644 --- a/spec/ruby/core/array/shared/join.rb +++ b/spec/ruby/core/array/shared/join.rb @@ -1,103 +1,6 @@ require_relative '../fixtures/classes' require_relative '../fixtures/encoded_strings' -describe :array_join_with_default_separator, shared: true do - before :each do - @separator = $, - end - - after :each do - $, = @separator - end - - it "returns an empty string if the Array is empty" do - [].send(@method).should == '' - end - - it "returns a US-ASCII string for an empty Array" do - [].send(@method).encoding.should == Encoding::US_ASCII - end - - it "returns a string formed by concatenating each String element separated by $," do - suppress_warning { - $, = " | " - ["1", "2", "3"].send(@method).should == "1 | 2 | 3" - } - end - - it "attempts coercion via #to_str first" do - obj = mock('foo') - obj.should_receive(:to_str).any_number_of_times.and_return("foo") - [obj].send(@method).should == "foo" - end - - it "attempts coercion via #to_ary second" do - obj = mock('foo') - obj.should_receive(:to_str).any_number_of_times.and_return(nil) - obj.should_receive(:to_ary).any_number_of_times.and_return(["foo"]) - [obj].send(@method).should == "foo" - end - - it "attempts coercion via #to_s third" do - obj = mock('foo') - obj.should_receive(:to_str).any_number_of_times.and_return(nil) - obj.should_receive(:to_ary).any_number_of_times.and_return(nil) - obj.should_receive(:to_s).any_number_of_times.and_return("foo") - [obj].send(@method).should == "foo" - end - - it "raises a NoMethodError if an element does not respond to #to_str, #to_ary, or #to_s" do - obj = mock('o') - class << obj; undef :to_s; end - -> { [1, obj].send(@method) }.should.raise(NoMethodError) - end - - it "raises an ArgumentError when the Array is recursive" do - -> { ArraySpecs.recursive_array.send(@method) }.should.raise(ArgumentError) - -> { ArraySpecs.head_recursive_array.send(@method) }.should.raise(ArgumentError) - -> { ArraySpecs.empty_recursive_array.send(@method) }.should.raise(ArgumentError) - end - - it "uses the first encoding when other strings are compatible" do - ary1 = ArraySpecs.array_with_7bit_utf8_and_usascii_strings - ary2 = ArraySpecs.array_with_usascii_and_7bit_utf8_strings - ary3 = ArraySpecs.array_with_utf8_and_7bit_binary_strings - ary4 = ArraySpecs.array_with_usascii_and_7bit_binary_strings - - ary1.send(@method).encoding.should == Encoding::UTF_8 - ary2.send(@method).encoding.should == Encoding::US_ASCII - ary3.send(@method).encoding.should == Encoding::UTF_8 - ary4.send(@method).encoding.should == Encoding::US_ASCII - end - - it "uses the widest common encoding when other strings are incompatible" do - ary1 = ArraySpecs.array_with_utf8_and_usascii_strings - ary2 = ArraySpecs.array_with_usascii_and_utf8_strings - - ary1.send(@method).encoding.should == Encoding::UTF_8 - ary2.send(@method).encoding.should == Encoding::UTF_8 - end - - it "fails for arrays with incompatibly-encoded strings" do - ary_utf8_bad_binary = ArraySpecs.array_with_utf8_and_binary_strings - - -> { ary_utf8_bad_binary.send(@method) }.should.raise(EncodingError) - end - - context "when $, is not nil" do - before do - suppress_warning do - $, = '*' - end - end - - it "warns" do - -> { [].join }.should complain(/warning: \$, is set to non-nil value/) - -> { [].join(nil) }.should complain(/warning: \$, is set to non-nil value/) - end - end -end - describe :array_join_with_string_separator, shared: true do it "returns a string formed by concatenating each element.to_str separated by separator" do obj = mock('foo') diff --git a/spec/ruby/core/array/shared/length.rb b/spec/ruby/core/array/shared/length.rb deleted file mode 100644 index f84966d0baad6f..00000000000000 --- a/spec/ruby/core/array/shared/length.rb +++ /dev/null @@ -1,11 +0,0 @@ -describe :array_length, shared: true do - it "returns the number of elements" do - [].send(@method).should == 0 - [1, 2, 3].send(@method).should == 3 - end - - it "properly handles recursive arrays" do - ArraySpecs.empty_recursive_array.send(@method).should == 1 - ArraySpecs.recursive_array.send(@method).should == 8 - end -end diff --git a/spec/ruby/core/array/shared/push.rb b/spec/ruby/core/array/shared/push.rb deleted file mode 100644 index ec406e506e19b3..00000000000000 --- a/spec/ruby/core/array/shared/push.rb +++ /dev/null @@ -1,33 +0,0 @@ -describe :array_push, shared: true do - it "appends the arguments to the array" do - a = [ "a", "b", "c" ] - a.send(@method, "d", "e", "f").should.equal?(a) - a.send(@method).should == ["a", "b", "c", "d", "e", "f"] - a.send(@method, 5) - a.should == ["a", "b", "c", "d", "e", "f", 5] - - a = [0, 1] - a.send(@method, 2) - a.should == [0, 1, 2] - end - - it "isn't confused by previous shift" do - a = [ "a", "b", "c" ] - a.shift - a.send(@method, "foo") - a.should == ["b", "c", "foo"] - end - - it "properly handles recursive arrays" do - empty = ArraySpecs.empty_recursive_array - empty.send(@method, :last).should == [empty, :last] - - array = ArraySpecs.recursive_array - array.send(@method, :last).should == [1, 'two', 3.0, array, array, array, array, array, :last] - end - - it "raises a FrozenError on a frozen array" do - -> { ArraySpecs.frozen_array.send(@method, 1) }.should.raise(FrozenError) - -> { ArraySpecs.frozen_array.send(@method) }.should.raise(FrozenError) - end -end diff --git a/spec/ruby/core/array/shared/replace.rb b/spec/ruby/core/array/shared/replace.rb deleted file mode 100644 index 06bfd00795c9b4..00000000000000 --- a/spec/ruby/core/array/shared/replace.rb +++ /dev/null @@ -1,60 +0,0 @@ -describe :array_replace, shared: true do - it "replaces the elements with elements from other array" do - a = [1, 2, 3, 4, 5] - b = ['a', 'b', 'c'] - a.send(@method, b).should.equal?(a) - a.should == b - a.should_not.equal?(b) - - a.send(@method, [4] * 10) - a.should == [4] * 10 - - a.send(@method, []) - a.should == [] - end - - it "properly handles recursive arrays" do - orig = [1, 2, 3] - empty = ArraySpecs.empty_recursive_array - orig.send(@method, empty) - orig.should == empty - - array = ArraySpecs.recursive_array - orig.send(@method, array) - orig.should == array - end - - it "returns self" do - ary = [1, 2, 3] - other = [:a, :b, :c] - ary.send(@method, other).should.equal?(ary) - end - - it "does not make self dependent to the original array" do - ary = [1, 2, 3] - other = [:a, :b, :c] - ary.send(@method, other) - ary.should == [:a, :b, :c] - ary << :d - ary.should == [:a, :b, :c, :d] - other.should == [:a, :b, :c] - end - - it "tries to convert the passed argument to an Array using #to_ary" do - obj = mock('to_ary') - obj.stub!(:to_ary).and_return([1, 2, 3]) - [].send(@method, obj).should == [1, 2, 3] - end - - it "does not call #to_ary on Array subclasses" do - obj = ArraySpecs::ToAryArray[5, 6, 7] - obj.should_not_receive(:to_ary) - [].send(@method, ArraySpecs::ToAryArray[5, 6, 7]).should == [5, 6, 7] - end - - it "raises a FrozenError on a frozen array" do - -> { - ArraySpecs.frozen_array.send(@method, ArraySpecs.frozen_array) - }.should.raise(FrozenError) - end -end diff --git a/spec/ruby/core/array/shared/select.rb b/spec/ruby/core/array/shared/select.rb deleted file mode 100644 index cb4f9acbb799b4..00000000000000 --- a/spec/ruby/core/array/shared/select.rb +++ /dev/null @@ -1,35 +0,0 @@ -require_relative '../../../spec_helper' -require_relative '../fixtures/classes' -require_relative '../shared/enumeratorize' -require_relative '../shared/keep_if' -require_relative '../shared/iterable_and_tolerating_size_increasing' -require_relative '../../enumerable/shared/enumeratorized' - -describe :array_select, shared: true do - it_should_behave_like :enumeratorize - - it_should_behave_like :array_iterable_and_tolerating_size_increasing - - before :each do - @object = [1,2,3] - end - it_should_behave_like :enumeratorized_with_origin_size - - it "returns a new array of elements for which block is true" do - [1, 3, 4, 5, 6, 9].send(@method) { |i| i % ((i + 1) / 2) == 0}.should == [1, 4, 6] - end - - it "does not return subclass instance on Array subclasses" do - ArraySpecs::MyArray[1, 2, 3].send(@method) { true }.should.instance_of?(Array) - end - - it "properly handles recursive arrays" do - empty = ArraySpecs.empty_recursive_array - empty.send(@method) { true }.should == empty - empty.send(@method) { false }.should == [] - - array = ArraySpecs.recursive_array - array.send(@method) { true }.should == [1, 'two', 3.0, array, array, array, array, array] - array.send(@method) { false }.should == [] - end -end diff --git a/spec/ruby/core/array/shared/slice.rb b/spec/ruby/core/array/shared/slice.rb deleted file mode 100644 index b838d86118c7ee..00000000000000 --- a/spec/ruby/core/array/shared/slice.rb +++ /dev/null @@ -1,857 +0,0 @@ -describe :array_slice, shared: true do - it "returns the element at index with [index]" do - [ "a", "b", "c", "d", "e" ].send(@method, 1).should == "b" - - a = [1, 2, 3, 4] - - a.send(@method, 0).should == 1 - a.send(@method, 1).should == 2 - a.send(@method, 2).should == 3 - a.send(@method, 3).should == 4 - a.send(@method, 4).should == nil - a.send(@method, 10).should == nil - - a.should == [1, 2, 3, 4] - end - - it "returns the element at index from the end of the array with [-index]" do - [ "a", "b", "c", "d", "e" ].send(@method, -2).should == "d" - - a = [1, 2, 3, 4] - - a.send(@method, -1).should == 4 - a.send(@method, -2).should == 3 - a.send(@method, -3).should == 2 - a.send(@method, -4).should == 1 - a.send(@method, -5).should == nil - a.send(@method, -10).should == nil - - a.should == [1, 2, 3, 4] - end - - it "returns count elements starting from index with [index, count]" do - [ "a", "b", "c", "d", "e" ].send(@method, 2, 3).should == ["c", "d", "e"] - - a = [1, 2, 3, 4] - - a.send(@method, 0, 0).should == [] - a.send(@method, 0, 1).should == [1] - a.send(@method, 0, 2).should == [1, 2] - a.send(@method, 0, 4).should == [1, 2, 3, 4] - a.send(@method, 0, 6).should == [1, 2, 3, 4] - a.send(@method, 0, -1).should == nil - a.send(@method, 0, -2).should == nil - a.send(@method, 0, -4).should == nil - - a.send(@method, 2, 0).should == [] - a.send(@method, 2, 1).should == [3] - a.send(@method, 2, 2).should == [3, 4] - a.send(@method, 2, 4).should == [3, 4] - a.send(@method, 2, -1).should == nil - - a.send(@method, 4, 0).should == [] - a.send(@method, 4, 2).should == [] - a.send(@method, 4, -1).should == nil - - a.send(@method, 5, 0).should == nil - a.send(@method, 5, 2).should == nil - a.send(@method, 5, -1).should == nil - - a.send(@method, 6, 0).should == nil - a.send(@method, 6, 2).should == nil - a.send(@method, 6, -1).should == nil - - a.should == [1, 2, 3, 4] - end - - it "returns count elements starting at index from the end of array with [-index, count]" do - [ "a", "b", "c", "d", "e" ].send(@method, -2, 2).should == ["d", "e"] - - a = [1, 2, 3, 4] - - a.send(@method, -1, 0).should == [] - a.send(@method, -1, 1).should == [4] - a.send(@method, -1, 2).should == [4] - a.send(@method, -1, -1).should == nil - - a.send(@method, -2, 0).should == [] - a.send(@method, -2, 1).should == [3] - a.send(@method, -2, 2).should == [3, 4] - a.send(@method, -2, 4).should == [3, 4] - a.send(@method, -2, -1).should == nil - - a.send(@method, -4, 0).should == [] - a.send(@method, -4, 1).should == [1] - a.send(@method, -4, 2).should == [1, 2] - a.send(@method, -4, 4).should == [1, 2, 3, 4] - a.send(@method, -4, 6).should == [1, 2, 3, 4] - a.send(@method, -4, -1).should == nil - - a.send(@method, -5, 0).should == nil - a.send(@method, -5, 1).should == nil - a.send(@method, -5, 10).should == nil - a.send(@method, -5, -1).should == nil - - a.should == [1, 2, 3, 4] - end - - it "returns the first count elements with [0, count]" do - [ "a", "b", "c", "d", "e" ].send(@method, 0, 3).should == ["a", "b", "c"] - end - - it "returns the subarray which is independent to self with [index,count]" do - a = [1, 2, 3] - sub = a.send(@method, 1,2) - sub.replace([:a, :b]) - a.should == [1, 2, 3] - end - - it "tries to convert the passed argument to an Integer using #to_int" do - obj = mock('to_int') - obj.stub!(:to_int).and_return(2) - - a = [1, 2, 3, 4] - a.send(@method, obj).should == 3 - a.send(@method, obj, 1).should == [3] - a.send(@method, obj, obj).should == [3, 4] - a.send(@method, 0, obj).should == [1, 2] - end - - it "raises TypeError if to_int returns non-integer" do - from = mock('from') - to = mock('to') - - # So we can construct a range out of them... - def from.<=>(o) 0 end - def to.<=>(o) 0 end - - a = [1, 2, 3, 4, 5] - - def from.to_int() 'cat' end - def to.to_int() -2 end - - -> { a.send(@method, from..to) }.should.raise(TypeError) - - def from.to_int() 1 end - def to.to_int() 'cat' end - - -> { a.send(@method, from..to) }.should.raise(TypeError) - end - - it "returns the elements specified by Range indexes with [m..n]" do - [ "a", "b", "c", "d", "e" ].send(@method, 1..3).should == ["b", "c", "d"] - [ "a", "b", "c", "d", "e" ].send(@method, 4..-1).should == ['e'] - [ "a", "b", "c", "d", "e" ].send(@method, 3..3).should == ['d'] - [ "a", "b", "c", "d", "e" ].send(@method, 3..-2).should == ['d'] - ['a'].send(@method, 0..-1).should == ['a'] - - a = [1, 2, 3, 4] - - a.send(@method, 0..-10).should == [] - a.send(@method, 0..0).should == [1] - a.send(@method, 0..1).should == [1, 2] - a.send(@method, 0..2).should == [1, 2, 3] - a.send(@method, 0..3).should == [1, 2, 3, 4] - a.send(@method, 0..4).should == [1, 2, 3, 4] - a.send(@method, 0..10).should == [1, 2, 3, 4] - - a.send(@method, 2..-10).should == [] - a.send(@method, 2..0).should == [] - a.send(@method, 2..2).should == [3] - a.send(@method, 2..3).should == [3, 4] - a.send(@method, 2..4).should == [3, 4] - - a.send(@method, 3..0).should == [] - a.send(@method, 3..3).should == [4] - a.send(@method, 3..4).should == [4] - - a.send(@method, 4..0).should == [] - a.send(@method, 4..4).should == [] - a.send(@method, 4..5).should == [] - - a.send(@method, 5..0).should == nil - a.send(@method, 5..5).should == nil - a.send(@method, 5..6).should == nil - - a.should == [1, 2, 3, 4] - end - - it "returns elements specified by Range indexes except the element at index n with [m...n]" do - [ "a", "b", "c", "d", "e" ].send(@method, 1...3).should == ["b", "c"] - - a = [1, 2, 3, 4] - - a.send(@method, 0...-10).should == [] - a.send(@method, 0...0).should == [] - a.send(@method, 0...1).should == [1] - a.send(@method, 0...2).should == [1, 2] - a.send(@method, 0...3).should == [1, 2, 3] - a.send(@method, 0...4).should == [1, 2, 3, 4] - a.send(@method, 0...10).should == [1, 2, 3, 4] - - a.send(@method, 2...-10).should == [] - a.send(@method, 2...0).should == [] - a.send(@method, 2...2).should == [] - a.send(@method, 2...3).should == [3] - a.send(@method, 2...4).should == [3, 4] - - a.send(@method, 3...0).should == [] - a.send(@method, 3...3).should == [] - a.send(@method, 3...4).should == [4] - - a.send(@method, 4...0).should == [] - a.send(@method, 4...4).should == [] - a.send(@method, 4...5).should == [] - - a.send(@method, 5...0).should == nil - a.send(@method, 5...5).should == nil - a.send(@method, 5...6).should == nil - - a.should == [1, 2, 3, 4] - end - - it "returns elements that exist if range start is in the array but range end is not with [m..n]" do - [ "a", "b", "c", "d", "e" ].send(@method, 4..7).should == ["e"] - end - - it "accepts Range instances having a negative m and both signs for n with [m..n] and [m...n]" do - a = [1, 2, 3, 4] - - a.send(@method, -1..-1).should == [4] - a.send(@method, -1...-1).should == [] - a.send(@method, -1..3).should == [4] - a.send(@method, -1...3).should == [] - a.send(@method, -1..4).should == [4] - a.send(@method, -1...4).should == [4] - a.send(@method, -1..10).should == [4] - a.send(@method, -1...10).should == [4] - a.send(@method, -1..0).should == [] - a.send(@method, -1..-4).should == [] - a.send(@method, -1...-4).should == [] - a.send(@method, -1..-6).should == [] - a.send(@method, -1...-6).should == [] - - a.send(@method, -2..-2).should == [3] - a.send(@method, -2...-2).should == [] - a.send(@method, -2..-1).should == [3, 4] - a.send(@method, -2...-1).should == [3] - a.send(@method, -2..10).should == [3, 4] - a.send(@method, -2...10).should == [3, 4] - - a.send(@method, -4..-4).should == [1] - a.send(@method, -4..-2).should == [1, 2, 3] - a.send(@method, -4...-2).should == [1, 2] - a.send(@method, -4..-1).should == [1, 2, 3, 4] - a.send(@method, -4...-1).should == [1, 2, 3] - a.send(@method, -4..3).should == [1, 2, 3, 4] - a.send(@method, -4...3).should == [1, 2, 3] - a.send(@method, -4..4).should == [1, 2, 3, 4] - a.send(@method, -4...4).should == [1, 2, 3, 4] - a.send(@method, -4...4).should == [1, 2, 3, 4] - a.send(@method, -4..0).should == [1] - a.send(@method, -4...0).should == [] - a.send(@method, -4..1).should == [1, 2] - a.send(@method, -4...1).should == [1] - - a.send(@method, -5..-5).should == nil - a.send(@method, -5...-5).should == nil - a.send(@method, -5..-4).should == nil - a.send(@method, -5..-1).should == nil - a.send(@method, -5..10).should == nil - - a.should == [1, 2, 3, 4] - end - - it "returns the subarray which is independent to self with [m..n]" do - a = [1, 2, 3] - sub = a.send(@method, 1..2) - sub.replace([:a, :b]) - a.should == [1, 2, 3] - end - - it "tries to convert Range elements to Integers using #to_int with [m..n] and [m...n]" do - from = mock('from') - to = mock('to') - - # So we can construct a range out of them... - def from.<=>(o) 0 end - def to.<=>(o) 0 end - - def from.to_int() 1 end - def to.to_int() -2 end - - a = [1, 2, 3, 4] - - a.send(@method, from..to).should == [2, 3] - a.send(@method, from...to).should == [2] - a.send(@method, 1..0).should == [] - a.send(@method, 1...0).should == [] - - -> { a.send(@method, "a" .. "b") }.should.raise(TypeError) - -> { a.send(@method, "a" ... "b") }.should.raise(TypeError) - -> { a.send(@method, from .. "b") }.should.raise(TypeError) - -> { a.send(@method, from ... "b") }.should.raise(TypeError) - end - - it "returns the same elements as [m..n] and [m...n] with Range subclasses" do - a = [1, 2, 3, 4] - range_incl = ArraySpecs::MyRange.new(1, 2) - range_excl = ArraySpecs::MyRange.new(-3, -1, true) - - a.send(@method, range_incl).should == [2, 3] - a.send(@method, range_excl).should == [2, 3] - end - - it "returns nil for a requested index not in the array with [index]" do - [ "a", "b", "c", "d", "e" ].send(@method, 5).should == nil - end - - it "returns [] if the index is valid but length is zero with [index, length]" do - [ "a", "b", "c", "d", "e" ].send(@method, 0, 0).should == [] - [ "a", "b", "c", "d", "e" ].send(@method, 2, 0).should == [] - end - - it "returns nil if length is zero but index is invalid with [index, length]" do - [ "a", "b", "c", "d", "e" ].send(@method, 100, 0).should == nil - [ "a", "b", "c", "d", "e" ].send(@method, -50, 0).should == nil - end - - # This is by design. It is in the official documentation. - it "returns [] if index == array.size with [index, length]" do - %w|a b c d e|.send(@method, 5, 2).should == [] - end - - it "returns nil if index > array.size with [index, length]" do - %w|a b c d e|.send(@method, 6, 2).should == nil - end - - it "returns nil if length is negative with [index, length]" do - %w|a b c d e|.send(@method, 3, -1).should == nil - %w|a b c d e|.send(@method, 2, -2).should == nil - %w|a b c d e|.send(@method, 1, -100).should == nil - end - - it "returns nil if no requested index is in the array with [m..n]" do - [ "a", "b", "c", "d", "e" ].send(@method, 6..10).should == nil - end - - it "returns nil if range start is not in the array with [m..n]" do - [ "a", "b", "c", "d", "e" ].send(@method, -10..2).should == nil - [ "a", "b", "c", "d", "e" ].send(@method, 10..12).should == nil - end - - it "returns an empty array when m == n with [m...n]" do - [1, 2, 3, 4, 5].send(@method, 1...1).should == [] - end - - it "returns an empty array with [0...0]" do - [1, 2, 3, 4, 5].send(@method, 0...0).should == [] - end - - it "returns a subarray where m, n negatives and m < n with [m..n]" do - [ "a", "b", "c", "d", "e" ].send(@method, -3..-2).should == ["c", "d"] - end - - it "returns an array containing the first element with [0..0]" do - [1, 2, 3, 4, 5].send(@method, 0..0).should == [1] - end - - it "returns the entire array with [0..-1]" do - [1, 2, 3, 4, 5].send(@method, 0..-1).should == [1, 2, 3, 4, 5] - end - - it "returns all but the last element with [0...-1]" do - [1, 2, 3, 4, 5].send(@method, 0...-1).should == [1, 2, 3, 4] - end - - it "returns [3] for [2..-1] out of [1, 2, 3]" do - [1,2,3].send(@method, 2..-1).should == [3] - end - - it "returns an empty array when m > n and m, n are positive with [m..n]" do - [1, 2, 3, 4, 5].send(@method, 3..2).should == [] - end - - it "returns an empty array when m > n and m, n are negative with [m..n]" do - [1, 2, 3, 4, 5].send(@method, -2..-3).should == [] - end - - it "does not expand array when the indices are outside of the array bounds" do - a = [1, 2] - a.send(@method, 4).should == nil - a.should == [1, 2] - a.send(@method, 4, 0).should == nil - a.should == [1, 2] - a.send(@method, 6, 1).should == nil - a.should == [1, 2] - a.send(@method, 8...8).should == nil - a.should == [1, 2] - a.send(@method, 10..10).should == nil - a.should == [1, 2] - end - - describe "with a subclass of Array" do - before :each do - ScratchPad.clear - - @array = ArraySpecs::MyArray[1, 2, 3, 4, 5] - end - - it "returns a Array instance with [n, m]" do - @array.send(@method, 0, 2).should.instance_of?(Array) - end - - it "returns a Array instance with [-n, m]" do - @array.send(@method, -3, 2).should.instance_of?(Array) - end - - it "returns a Array instance with [n..m]" do - @array.send(@method, 1..3).should.instance_of?(Array) - end - - it "returns a Array instance with [n...m]" do - @array.send(@method, 1...3).should.instance_of?(Array) - end - - it "returns a Array instance with [-n..-m]" do - @array.send(@method, -3..-1).should.instance_of?(Array) - end - - it "returns a Array instance with [-n...-m]" do - @array.send(@method, -3...-1).should.instance_of?(Array) - end - - it "returns an empty array when m == n with [m...n]" do - @array.send(@method, 1...1).should == [] - ScratchPad.recorded.should == nil - end - - it "returns an empty array with [0...0]" do - @array.send(@method, 0...0).should == [] - ScratchPad.recorded.should == nil - end - - it "returns an empty array when m > n and m, n are positive with [m..n]" do - @array.send(@method, 3..2).should == [] - ScratchPad.recorded.should == nil - end - - it "returns an empty array when m > n and m, n are negative with [m..n]" do - @array.send(@method, -2..-3).should == [] - ScratchPad.recorded.should == nil - end - - it "returns [] if index == array.size with [index, length]" do - @array.send(@method, 5, 2).should == [] - ScratchPad.recorded.should == nil - end - - it "returns [] if the index is valid but length is zero with [index, length]" do - @array.send(@method, 0, 0).should == [] - @array.send(@method, 2, 0).should == [] - ScratchPad.recorded.should == nil - end - - it "does not call #initialize on the subclass instance" do - @array.send(@method, 0, 3).should == [1, 2, 3] - ScratchPad.recorded.should == nil - end - end - - it "raises a RangeError when the start index is out of range of Fixnum" do - array = [1, 2, 3, 4, 5, 6] - obj = mock('large value') - obj.should_receive(:to_int).and_return(bignum_value) - -> { array.send(@method, obj) }.should.raise(RangeError) - - obj = 8e19 - -> { array.send(@method, obj) }.should.raise(RangeError) - - # boundary value when longs are 64 bits - -> { array.send(@method, 2.0**63) }.should.raise(RangeError) - - # just under the boundary value when longs are 64 bits - array.send(@method, max_long.to_f.prev_float).should == nil - end - - it "raises a RangeError when the length is out of range of Fixnum" do - array = [1, 2, 3, 4, 5, 6] - obj = mock('large value') - obj.should_receive(:to_int).and_return(bignum_value) - -> { array.send(@method, 1, obj) }.should.raise(RangeError) - - obj = 8e19 - -> { array.send(@method, 1, obj) }.should.raise(RangeError) - end - - it "raises a type error if a range is passed with a length" do - ->{ [1, 2, 3].send(@method, 1..2, 1) }.should.raise(TypeError) - end - - it "raises a RangeError if passed a range with a bound that is too large" do - array = [1, 2, 3, 4, 5, 6] - -> { array.send(@method, bignum_value..(bignum_value + 1)) }.should.raise(RangeError) - -> { array.send(@method, 0..bignum_value) }.should.raise(RangeError) - end - - it "can accept endless ranges" do - a = [0, 1, 2, 3, 4, 5] - a.send(@method, eval("(2..)")).should == [2, 3, 4, 5] - a.send(@method, eval("(2...)")).should == [2, 3, 4, 5] - a.send(@method, eval("(-2..)")).should == [4, 5] - a.send(@method, eval("(-2...)")).should == [4, 5] - a.send(@method, eval("(9..)")).should == nil - a.send(@method, eval("(9...)")).should == nil - a.send(@method, eval("(-9..)")).should == nil - a.send(@method, eval("(-9...)")).should == nil - end - - describe "can be sliced with Enumerator::ArithmeticSequence" do - before :each do - @array = [0, 1, 2, 3, 4, 5] - end - - it "has endless range and positive steps" do - @array.send(@method, eval("(0..).step(1)")).should == [0, 1, 2, 3, 4, 5] - @array.send(@method, eval("(0..).step(2)")).should == [0, 2, 4] - @array.send(@method, eval("(0..).step(10)")).should == [0] - - @array.send(@method, eval("(2..).step(1)")).should == [2, 3, 4, 5] - @array.send(@method, eval("(2..).step(2)")).should == [2, 4] - @array.send(@method, eval("(2..).step(10)")).should == [2] - - @array.send(@method, eval("(-3..).step(1)")).should == [3, 4, 5] - @array.send(@method, eval("(-3..).step(2)")).should == [3, 5] - @array.send(@method, eval("(-3..).step(10)")).should == [3] - end - - it "has beginless range and positive steps" do - # end with zero index - @array.send(@method, (..0).step(1)).should == [0] - @array.send(@method, (...0).step(1)).should == [] - - @array.send(@method, (..0).step(2)).should == [0] - @array.send(@method, (...0).step(2)).should == [] - - @array.send(@method, (..0).step(10)).should == [0] - @array.send(@method, (...0).step(10)).should == [] - - # end with positive index - @array.send(@method, (..3).step(1)).should == [0, 1, 2, 3] - @array.send(@method, (...3).step(1)).should == [0, 1, 2] - - @array.send(@method, (..3).step(2)).should == [0, 2] - @array.send(@method, (...3).step(2)).should == [0, 2] - - @array.send(@method, (..3).step(10)).should == [0] - @array.send(@method, (...3).step(10)).should == [0] - - # end with negative index - @array.send(@method, (..-2).step(1)).should == [0, 1, 2, 3, 4,] - @array.send(@method, (...-2).step(1)).should == [0, 1, 2, 3] - - @array.send(@method, (..-2).step(2)).should == [0, 2, 4] - @array.send(@method, (...-2).step(2)).should == [0, 2] - - @array.send(@method, (..-2).step(10)).should == [0] - @array.send(@method, (...-2).step(10)).should == [0] - end - - it "has endless range and negative steps" do - @array.send(@method, eval("(0..).step(-1)")).should == [0] - @array.send(@method, eval("(0..).step(-2)")).should == [0] - @array.send(@method, eval("(0..).step(-10)")).should == [0] - - @array.send(@method, eval("(2..).step(-1)")).should == [2, 1, 0] - @array.send(@method, eval("(2..).step(-2)")).should == [2, 0] - - @array.send(@method, eval("(-3..).step(-1)")).should == [3, 2, 1, 0] - @array.send(@method, eval("(-3..).step(-2)")).should == [3, 1] - end - - it "has closed range and positive steps" do - # start and end with 0 - @array.send(@method, eval("(0..0).step(1)")).should == [0] - @array.send(@method, eval("(0...0).step(1)")).should == [] - - @array.send(@method, eval("(0..0).step(2)")).should == [0] - @array.send(@method, eval("(0...0).step(2)")).should == [] - - @array.send(@method, eval("(0..0).step(10)")).should == [0] - @array.send(@method, eval("(0...0).step(10)")).should == [] - - # start and end with positive index - @array.send(@method, eval("(1..3).step(1)")).should == [1, 2, 3] - @array.send(@method, eval("(1...3).step(1)")).should == [1, 2] - - @array.send(@method, eval("(1..3).step(2)")).should == [1, 3] - @array.send(@method, eval("(1...3).step(2)")).should == [1] - - @array.send(@method, eval("(1..3).step(10)")).should == [1] - @array.send(@method, eval("(1...3).step(10)")).should == [1] - - # start with positive index, end with negative index - @array.send(@method, eval("(1..-2).step(1)")).should == [1, 2, 3, 4] - @array.send(@method, eval("(1...-2).step(1)")).should == [1, 2, 3] - - @array.send(@method, eval("(1..-2).step(2)")).should == [1, 3] - @array.send(@method, eval("(1...-2).step(2)")).should == [1, 3] - - @array.send(@method, eval("(1..-2).step(10)")).should == [1] - @array.send(@method, eval("(1...-2).step(10)")).should == [1] - - # start with negative index, end with positive index - @array.send(@method, eval("(-4..4).step(1)")).should == [2, 3, 4] - @array.send(@method, eval("(-4...4).step(1)")).should == [2, 3] - - @array.send(@method, eval("(-4..4).step(2)")).should == [2, 4] - @array.send(@method, eval("(-4...4).step(2)")).should == [2] - - @array.send(@method, eval("(-4..4).step(10)")).should == [2] - @array.send(@method, eval("(-4...4).step(10)")).should == [2] - - # start with negative index, end with negative index - @array.send(@method, eval("(-4..-2).step(1)")).should == [2, 3, 4] - @array.send(@method, eval("(-4...-2).step(1)")).should == [2, 3] - - @array.send(@method, eval("(-4..-2).step(2)")).should == [2, 4] - @array.send(@method, eval("(-4...-2).step(2)")).should == [2] - - @array.send(@method, eval("(-4..-2).step(10)")).should == [2] - @array.send(@method, eval("(-4...-2).step(10)")).should == [2] - end - - it "has closed range and negative steps" do - # start and end with 0 - @array.send(@method, eval("(0..0).step(-1)")).should == [0] - @array.send(@method, eval("(0...0).step(-1)")).should == [] - - @array.send(@method, eval("(0..0).step(-2)")).should == [0] - @array.send(@method, eval("(0...0).step(-2)")).should == [] - - @array.send(@method, eval("(0..0).step(-10)")).should == [0] - @array.send(@method, eval("(0...0).step(-10)")).should == [] - - # start and end with positive index - @array.send(@method, eval("(1..3).step(-1)")).should == [] - @array.send(@method, eval("(1...3).step(-1)")).should == [] - - @array.send(@method, eval("(1..3).step(-2)")).should == [] - @array.send(@method, eval("(1...3).step(-2)")).should == [] - - @array.send(@method, eval("(1..3).step(-10)")).should == [] - @array.send(@method, eval("(1...3).step(-10)")).should == [] - - # start with positive index, end with negative index - @array.send(@method, eval("(1..-2).step(-1)")).should == [] - @array.send(@method, eval("(1...-2).step(-1)")).should == [] - - @array.send(@method, eval("(1..-2).step(-2)")).should == [] - @array.send(@method, eval("(1...-2).step(-2)")).should == [] - - @array.send(@method, eval("(1..-2).step(-10)")).should == [] - @array.send(@method, eval("(1...-2).step(-10)")).should == [] - - # start with negative index, end with positive index - @array.send(@method, eval("(-4..4).step(-1)")).should == [] - @array.send(@method, eval("(-4...4).step(-1)")).should == [] - - @array.send(@method, eval("(-4..4).step(-2)")).should == [] - @array.send(@method, eval("(-4...4).step(-2)")).should == [] - - @array.send(@method, eval("(-4..4).step(-10)")).should == [] - @array.send(@method, eval("(-4...4).step(-10)")).should == [] - - # start with negative index, end with negative index - @array.send(@method, eval("(-4..-2).step(-1)")).should == [] - @array.send(@method, eval("(-4...-2).step(-1)")).should == [] - - @array.send(@method, eval("(-4..-2).step(-2)")).should == [] - @array.send(@method, eval("(-4...-2).step(-2)")).should == [] - - @array.send(@method, eval("(-4..-2).step(-10)")).should == [] - @array.send(@method, eval("(-4...-2).step(-10)")).should == [] - end - - it "has inverted closed range and positive steps" do - # start and end with positive index - @array.send(@method, eval("(3..1).step(1)")).should == [] - @array.send(@method, eval("(3...1).step(1)")).should == [] - - @array.send(@method, eval("(3..1).step(2)")).should == [] - @array.send(@method, eval("(3...1).step(2)")).should == [] - - @array.send(@method, eval("(3..1).step(10)")).should == [] - @array.send(@method, eval("(3...1).step(10)")).should == [] - - # start with negative index, end with positive index - @array.send(@method, eval("(-2..1).step(1)")).should == [] - @array.send(@method, eval("(-2...1).step(1)")).should == [] - - @array.send(@method, eval("(-2..1).step(2)")).should == [] - @array.send(@method, eval("(-2...1).step(2)")).should == [] - - @array.send(@method, eval("(-2..1).step(10)")).should == [] - @array.send(@method, eval("(-2...1).step(10)")).should == [] - - # start with positive index, end with negative index - @array.send(@method, eval("(4..-4).step(1)")).should == [] - @array.send(@method, eval("(4...-4).step(1)")).should == [] - - @array.send(@method, eval("(4..-4).step(2)")).should == [] - @array.send(@method, eval("(4...-4).step(2)")).should == [] - - @array.send(@method, eval("(4..-4).step(10)")).should == [] - @array.send(@method, eval("(4...-4).step(10)")).should == [] - - # start with negative index, end with negative index - @array.send(@method, eval("(-2..-4).step(1)")).should == [] - @array.send(@method, eval("(-2...-4).step(1)")).should == [] - - @array.send(@method, eval("(-2..-4).step(2)")).should == [] - @array.send(@method, eval("(-2...-4).step(2)")).should == [] - - @array.send(@method, eval("(-2..-4).step(10)")).should == [] - @array.send(@method, eval("(-2...-4).step(10)")).should == [] - end - - it "has range with bounds outside of array" do - # end is equal to array's length - @array.send(@method, (0..6).step(1)).should == [0, 1, 2, 3, 4, 5] - -> { @array.send(@method, (0..6).step(2)) }.should.raise(RangeError) - - # end is greater than length with positive steps - @array.send(@method, (1..6).step(2)).should == [1, 3, 5] - @array.send(@method, (2..7).step(2)).should == [2, 4] - -> { @array.send(@method, (2..8).step(2)) }.should.raise(RangeError) - - # begin is greater than length with negative steps - @array.send(@method, (6..1).step(-2)).should == [5, 3, 1] - @array.send(@method, (7..2).step(-2)).should == [5, 3] - -> { @array.send(@method, (8..2).step(-2)) }.should.raise(RangeError) - end - - it "has endless range with start outside of array's bounds" do - @array.send(@method, eval("(6..).step(1)")).should == [] - @array.send(@method, eval("(7..).step(1)")).should == nil - - @array.send(@method, eval("(6..).step(2)")).should == [] - -> { @array.send(@method, eval("(7..).step(2)")) }.should.raise(RangeError) - end - end - - it "can accept beginless ranges" do - a = [0, 1, 2, 3, 4, 5] - a.send(@method, (..3)).should == [0, 1, 2, 3] - a.send(@method, (...3)).should == [0, 1, 2] - a.send(@method, (..-3)).should == [0, 1, 2, 3] - a.send(@method, (...-3)).should == [0, 1, 2] - a.send(@method, (..0)).should == [0] - a.send(@method, (...0)).should == [] - a.send(@method, (..9)).should == [0, 1, 2, 3, 4, 5] - a.send(@method, (...9)).should == [0, 1, 2, 3, 4, 5] - a.send(@method, (..-9)).should == [] - a.send(@method, (...-9)).should == [] - end - - describe "can be sliced with Enumerator::ArithmeticSequence" do - it "with infinite/inverted ranges and negative steps" do - @array = [0, 1, 2, 3, 4, 5] - @array.send(@method, (2..).step(-1)).should == [2, 1, 0] - @array.send(@method, (2..).step(-2)).should == [2, 0] - @array.send(@method, (2..).step(-3)).should == [2] - @array.send(@method, (2..).step(-4)).should == [2] - - @array.send(@method, (-3..).step(-1)).should == [3, 2, 1, 0] - @array.send(@method, (-3..).step(-2)).should == [3, 1] - @array.send(@method, (-3..).step(-3)).should == [3, 0] - @array.send(@method, (-3..).step(-4)).should == [3] - @array.send(@method, (-3..).step(-5)).should == [3] - - @array.send(@method, (..0).step(-1)).should == [5, 4, 3, 2, 1, 0] - @array.send(@method, (..0).step(-2)).should == [5, 3, 1] - @array.send(@method, (..0).step(-3)).should == [5, 2] - @array.send(@method, (..0).step(-4)).should == [5, 1] - @array.send(@method, (..0).step(-5)).should == [5, 0] - @array.send(@method, (..0).step(-6)).should == [5] - @array.send(@method, (..0).step(-7)).should == [5] - - @array.send(@method, (...0).step(-1)).should == [5, 4, 3, 2, 1] - @array.send(@method, (...0).step(-2)).should == [5, 3, 1] - @array.send(@method, (...0).step(-3)).should == [5, 2] - @array.send(@method, (...0).step(-4)).should == [5, 1] - @array.send(@method, (...0).step(-5)).should == [5] - @array.send(@method, (...0).step(-6)).should == [5] - - @array.send(@method, (...1).step(-1)).should == [5, 4, 3, 2] - @array.send(@method, (...1).step(-2)).should == [5, 3] - @array.send(@method, (...1).step(-3)).should == [5, 2] - @array.send(@method, (...1).step(-4)).should == [5] - @array.send(@method, (...1).step(-5)).should == [5] - - @array.send(@method, (..-5).step(-1)).should == [5, 4, 3, 2, 1] - @array.send(@method, (..-5).step(-2)).should == [5, 3, 1] - @array.send(@method, (..-5).step(-3)).should == [5, 2] - @array.send(@method, (..-5).step(-4)).should == [5, 1] - @array.send(@method, (..-5).step(-5)).should == [5] - @array.send(@method, (..-5).step(-6)).should == [5] - - @array.send(@method, (...-5).step(-1)).should == [5, 4, 3, 2] - @array.send(@method, (...-5).step(-2)).should == [5, 3] - @array.send(@method, (...-5).step(-3)).should == [5, 2] - @array.send(@method, (...-5).step(-4)).should == [5] - @array.send(@method, (...-5).step(-5)).should == [5] - - @array.send(@method, (4..1).step(-1)).should == [4, 3, 2, 1] - @array.send(@method, (4..1).step(-2)).should == [4, 2] - @array.send(@method, (4..1).step(-3)).should == [4, 1] - @array.send(@method, (4..1).step(-4)).should == [4] - @array.send(@method, (4..1).step(-5)).should == [4] - - @array.send(@method, (4...1).step(-1)).should == [4, 3, 2] - @array.send(@method, (4...1).step(-2)).should == [4, 2] - @array.send(@method, (4...1).step(-3)).should == [4] - @array.send(@method, (4...1).step(-4)).should == [4] - - @array.send(@method, (-2..1).step(-1)).should == [4, 3, 2, 1] - @array.send(@method, (-2..1).step(-2)).should == [4, 2] - @array.send(@method, (-2..1).step(-3)).should == [4, 1] - @array.send(@method, (-2..1).step(-4)).should == [4] - @array.send(@method, (-2..1).step(-5)).should == [4] - - @array.send(@method, (-2...1).step(-1)).should == [4, 3, 2] - @array.send(@method, (-2...1).step(-2)).should == [4, 2] - @array.send(@method, (-2...1).step(-3)).should == [4] - @array.send(@method, (-2...1).step(-4)).should == [4] - - @array.send(@method, (4..-5).step(-1)).should == [4, 3, 2, 1] - @array.send(@method, (4..-5).step(-2)).should == [4, 2] - @array.send(@method, (4..-5).step(-3)).should == [4, 1] - @array.send(@method, (4..-5).step(-4)).should == [4] - @array.send(@method, (4..-5).step(-5)).should == [4] - - @array.send(@method, (4...-5).step(-1)).should == [4, 3, 2] - @array.send(@method, (4...-5).step(-2)).should == [4, 2] - @array.send(@method, (4...-5).step(-3)).should == [4] - @array.send(@method, (4...-5).step(-4)).should == [4] - - @array.send(@method, (-2..-5).step(-1)).should == [4, 3, 2, 1] - @array.send(@method, (-2..-5).step(-2)).should == [4, 2] - @array.send(@method, (-2..-5).step(-3)).should == [4, 1] - @array.send(@method, (-2..-5).step(-4)).should == [4] - @array.send(@method, (-2..-5).step(-5)).should == [4] - - @array.send(@method, (-2...-5).step(-1)).should == [4, 3, 2] - @array.send(@method, (-2...-5).step(-2)).should == [4, 2] - @array.send(@method, (-2...-5).step(-3)).should == [4] - @array.send(@method, (-2...-5).step(-4)).should == [4] - end - end - - it "can accept nil...nil ranges" do - a = [0, 1, 2, 3, 4, 5] - a.send(@method, eval("(nil...nil)")).should == a - a.send(@method, (...nil)).should == a - a.send(@method, eval("(nil..)")).should == a - end -end diff --git a/spec/ruby/core/array/shared/unshift.rb b/spec/ruby/core/array/shared/unshift.rb deleted file mode 100644 index b636347cd36d33..00000000000000 --- a/spec/ruby/core/array/shared/unshift.rb +++ /dev/null @@ -1,64 +0,0 @@ -describe :array_unshift, shared: true do - it "prepends object to the original array" do - a = [1, 2, 3] - a.send(@method, "a").should.equal?(a) - a.should == ['a', 1, 2, 3] - a.send(@method).should.equal?(a) - a.should == ['a', 1, 2, 3] - a.send(@method, 5, 4, 3) - a.should == [5, 4, 3, 'a', 1, 2, 3] - - # shift all but one element - a = [1, 2] - a.shift - a.send(@method, 3, 4) - a.should == [3, 4, 2] - - # now shift all elements - a.shift - a.shift - a.shift - a.send(@method, 3, 4) - a.should == [3, 4] - end - - it "returns self" do - a = [1, 2, 3] - a.send(@method, "a").should.equal?(a) - end - - it "quietly ignores unshifting nothing" do - [].send(@method).should == [] - end - - it "properly handles recursive arrays" do - empty = ArraySpecs.empty_recursive_array - empty.send(@method, :new).should == [:new, empty] - - array = ArraySpecs.recursive_array - array.send(@method, :new) - array[0..5].should == [:new, 1, 'two', 3.0, array, array] - end - - it "raises a FrozenError on a frozen array when the array is modified" do - -> { ArraySpecs.frozen_array.send(@method, 1) }.should.raise(FrozenError) - end - - # see [ruby-core:23666] - it "raises a FrozenError on a frozen array when the array would not be modified" do - -> { ArraySpecs.frozen_array.send(@method) }.should.raise(FrozenError) - end - - # https://github.com/truffleruby/truffleruby/issues/2772 - it "doesn't rely on Array#[]= so it can be overridden" do - subclass = Class.new(Array) do - def []=(*) - raise "[]= is called" - end - end - - array = subclass.new - array.send(@method, 1) - array.should == [1] - end -end diff --git a/spec/ruby/core/array/size_spec.rb b/spec/ruby/core/array/size_spec.rb index d68f956a832f35..83e89690124143 100644 --- a/spec/ruby/core/array/size_spec.rb +++ b/spec/ruby/core/array/size_spec.rb @@ -1,7 +1,7 @@ require_relative '../../spec_helper' -require_relative 'fixtures/classes' -require_relative 'shared/length' describe "Array#size" do - it_behaves_like :array_length, :size + it "is an alias of Array#length" do + Array.instance_method(:size).should == Array.instance_method(:length) + end end diff --git a/spec/ruby/core/array/slice_spec.rb b/spec/ruby/core/array/slice_spec.rb index eb7e47d9476ea3..230d1dc5d16361 100644 --- a/spec/ruby/core/array/slice_spec.rb +++ b/spec/ruby/core/array/slice_spec.rb @@ -1,6 +1,5 @@ require_relative '../../spec_helper' require_relative 'fixtures/classes' -require_relative 'shared/slice' describe "Array#slice!" do it "removes and return the element at index" do @@ -214,5 +213,7 @@ def to.to_int() -2 end end describe "Array#slice" do - it_behaves_like :array_slice, :slice + it "is an alias of Array#[]" do + Array.instance_method(:slice).should == Array.instance_method(:[]) + end end diff --git a/spec/ruby/core/array/to_s_spec.rb b/spec/ruby/core/array/to_s_spec.rb index e8476702ec0e1d..483e14f9025871 100644 --- a/spec/ruby/core/array/to_s_spec.rb +++ b/spec/ruby/core/array/to_s_spec.rb @@ -1,8 +1,7 @@ require_relative '../../spec_helper' -require_relative 'fixtures/classes' -require_relative 'shared/join' -require_relative 'shared/inspect' describe "Array#to_s" do - it_behaves_like :array_inspect, :to_s + it "is an alias of Array#inspect" do + Array.instance_method(:to_s).should == Array.instance_method(:inspect) + end end diff --git a/spec/ruby/core/array/unshift_spec.rb b/spec/ruby/core/array/unshift_spec.rb index b8b675e5f8617b..c190db4d02d876 100644 --- a/spec/ruby/core/array/unshift_spec.rb +++ b/spec/ruby/core/array/unshift_spec.rb @@ -1,7 +1,67 @@ require_relative '../../spec_helper' require_relative 'fixtures/classes' -require_relative 'shared/unshift' describe "Array#unshift" do - it_behaves_like :array_unshift, :unshift + it "prepends object to the original array" do + a = [1, 2, 3] + a.unshift("a").should.equal?(a) + a.should == ['a', 1, 2, 3] + a.unshift.should.equal?(a) + a.should == ['a', 1, 2, 3] + a.unshift(5, 4, 3) + a.should == [5, 4, 3, 'a', 1, 2, 3] + + # shift all but one element + a = [1, 2] + a.shift + a.unshift(3, 4) + a.should == [3, 4, 2] + + # now shift all elements + a.shift + a.shift + a.shift + a.unshift(3, 4) + a.should == [3, 4] + end + + it "returns self" do + a = [1, 2, 3] + a.unshift("a").should.equal?(a) + end + + it "quietly ignores unshifting nothing" do + [].unshift.should == [] + end + + it "properly handles recursive arrays" do + empty = ArraySpecs.empty_recursive_array + empty.unshift(:new).should == [:new, empty] + + array = ArraySpecs.recursive_array + array.unshift(:new) + array[0..5].should == [:new, 1, 'two', 3.0, array, array] + end + + it "raises a FrozenError on a frozen array when the array is modified" do + -> { ArraySpecs.frozen_array.unshift(1) }.should.raise(FrozenError) + end + + # see [ruby-core:23666] + it "raises a FrozenError on a frozen array when the array would not be modified" do + -> { ArraySpecs.frozen_array.unshift }.should.raise(FrozenError) + end + + # https://github.com/truffleruby/truffleruby/issues/2772 + it "doesn't rely on Array#[]= so it can be overridden" do + subclass = Class.new(Array) do + def []=(*) + raise "[]= is called" + end + end + + array = subclass.new + array.unshift(1) + array.should == [1] + end end diff --git a/spec/ruby/core/complex/abs_spec.rb b/spec/ruby/core/complex/abs_spec.rb index 43912c517f1314..ed5aebb11d411b 100644 --- a/spec/ruby/core/complex/abs_spec.rb +++ b/spec/ruby/core/complex/abs_spec.rb @@ -1,6 +1,12 @@ require_relative '../../spec_helper' -require_relative 'shared/abs' describe "Complex#abs" do - it_behaves_like :complex_abs, :abs + it "returns the modulus: |a + bi| = sqrt((a ^ 2) + (b ^ 2))" do + Complex(0, 0).abs.should == 0 + Complex(3, 4).abs.should == 5 # well-known integer case + Complex(-3, 4).abs.should == 5 + Complex(1, -1).abs.should be_close(Math.sqrt(2), TOLERANCE) + Complex(6.5, 0).abs.should be_close(6.5, TOLERANCE) + Complex(0, -7.2).abs.should be_close(7.2, TOLERANCE) + end end diff --git a/spec/ruby/core/complex/angle_spec.rb b/spec/ruby/core/complex/angle_spec.rb index 4aa176956fa5f4..7551214d2bdf74 100644 --- a/spec/ruby/core/complex/angle_spec.rb +++ b/spec/ruby/core/complex/angle_spec.rb @@ -1,6 +1,7 @@ require_relative '../../spec_helper' -require_relative 'shared/arg' describe "Complex#angle" do - it_behaves_like :complex_arg, :angle + it "is an alias of Complex#arg" do + Complex.instance_method(:angle).should == Complex.instance_method(:arg) + end end diff --git a/spec/ruby/core/complex/arg_spec.rb b/spec/ruby/core/complex/arg_spec.rb index 009f19429fffdc..dd64102d77ab4e 100644 --- a/spec/ruby/core/complex/arg_spec.rb +++ b/spec/ruby/core/complex/arg_spec.rb @@ -1,6 +1,11 @@ require_relative '../../spec_helper' -require_relative 'shared/arg' describe "Complex#arg" do - it_behaves_like :complex_arg, :arg + it "returns the argument -- i.e., the angle from (1, 0) in the complex plane" do + two_pi = 2 * Math::PI + (Complex(1, 0).arg % two_pi).should be_close(0, TOLERANCE) + (Complex(0, 2).arg % two_pi).should be_close(Math::PI * 0.5, TOLERANCE) + (Complex(-100, 0).arg % two_pi).should be_close(Math::PI, TOLERANCE) + (Complex(0, -75.3).arg % two_pi).should be_close(Math::PI * 1.5, TOLERANCE) + end end diff --git a/spec/ruby/core/complex/conj_spec.rb b/spec/ruby/core/complex/conj_spec.rb index 5e3bc1acb8c47a..063c85faec31c7 100644 --- a/spec/ruby/core/complex/conj_spec.rb +++ b/spec/ruby/core/complex/conj_spec.rb @@ -1,6 +1,7 @@ require_relative '../../spec_helper' -require_relative 'shared/conjugate' describe "Complex#conj" do - it_behaves_like :complex_conjugate, :conj + it "is an alias of Complex#conjugate" do + Complex.instance_method(:conj).should == Complex.instance_method(:conjugate) + end end diff --git a/spec/ruby/core/complex/conjugate_spec.rb b/spec/ruby/core/complex/conjugate_spec.rb index f658bab4da7d80..256fe4b3be1420 100644 --- a/spec/ruby/core/complex/conjugate_spec.rb +++ b/spec/ruby/core/complex/conjugate_spec.rb @@ -1,6 +1,10 @@ require_relative '../../spec_helper' -require_relative 'shared/conjugate' describe "Complex#conjugate" do - it_behaves_like :complex_conjugate, :conjugate + it "returns the complex conjugate: conj a + bi = a - bi" do + Complex(3, 5).conjugate.should == Complex(3, -5) + Complex(3, -5).conjugate.should == Complex(3, 5) + Complex(-3.0, 5.2).conjugate.should be_close(Complex(-3.0, -5.2), TOLERANCE) + Complex(3.0, -5.2).conjugate.should be_close(Complex(3.0, 5.2), TOLERANCE) + end end diff --git a/spec/ruby/core/complex/divide_spec.rb b/spec/ruby/core/complex/divide_spec.rb index bebf862312515b..d5b4aa38854ad2 100644 --- a/spec/ruby/core/complex/divide_spec.rb +++ b/spec/ruby/core/complex/divide_spec.rb @@ -1,6 +1,84 @@ require_relative '../../spec_helper' -require_relative 'shared/divide' describe "Complex#/" do - it_behaves_like :complex_divide, :/ + describe "with Complex" do + it "divides according to the usual rule for complex numbers" do + a = Complex((1 * 10) - (2 * 20), (1 * 20) + (2 * 10)) + b = Complex(1, 2) + (a / b).should == Complex(10, 20) + + c = Complex((1.5 * 100.2) - (2.1 * -30.3), (1.5 * -30.3) + (2.1 * 100.2)) + d = Complex(1.5, 2.1) + # remember the floating-point arithmetic + (c / d).should be_close(Complex(100.2, -30.3), TOLERANCE) + end + end + + describe "with Fixnum" do + it "divides both parts of the Complex number" do + (Complex(20, 40) / 2).should == Complex(10, 20) + (Complex(30, 30) / 10).should == Complex(3, 3) + end + + it "raises a ZeroDivisionError when given zero" do + -> { Complex(20, 40) / 0 }.should.raise(ZeroDivisionError) + end + + it "produces Rational parts" do + (Complex(5, 9) / 2).should.eql?(Complex(Rational(5,2), Rational(9,2))) + end + end + + describe "with Bignum" do + it "divides both parts of the Complex number" do + (Complex(20, 40) / 2).should == Complex(10, 20) + (Complex(15, 16) / 2.0).should be_close(Complex(7.5, 8), TOLERANCE) + end + end + + describe "with Float" do + it "divides both parts of the Complex number" do + (Complex(3, 9) / 1.5).should == Complex(2, 6) + (Complex(15, 16) / 2.0).should be_close(Complex(7.5, 8), TOLERANCE) + end + + it "returns Complex(Infinity, Infinity) when given zero" do + (Complex(20, 40) / 0.0).real.infinite?.should == 1 + (Complex(20, 40) / 0.0).imag.infinite?.should == 1 + (Complex(-20, 40) / 0.0).real.infinite?.should == -1 + (Complex(-20, 40) / 0.0).imag.infinite?.should == 1 + end + end + + describe "with Object" do + it "tries to coerce self into other" do + value = Complex(3, 9) + + obj = mock("Object") + obj.should_receive(:coerce).with(value).and_return([4, 2]) + (value / obj).should == 2 + end + end + + describe "with a Numeric which responds to #real? with true" do + it "returns Complex(real.quo(other), imag.quo(other))" do + other = mock_numeric('other') + real = mock_numeric('real') + imag = mock_numeric('imag') + other.should_receive(:real?).and_return(true) + real.should_receive(:quo).with(other).and_return(1) + imag.should_receive(:quo).with(other).and_return(2) + (Complex(real, imag) / other).should == Complex(1, 2) + end + end + + describe "with a Numeric which responds to #real? with false" do + it "coerces the passed argument to Complex and divides the resulting elements" do + complex = Complex(3, 0) + other = mock_numeric('other') + other.should_receive(:real?).any_number_of_times.and_return(false) + other.should_receive(:coerce).with(complex).and_return([5, 2]) + (complex / other).should.eql?(Rational(5, 2)) + end + end end diff --git a/spec/ruby/core/complex/imag_spec.rb b/spec/ruby/core/complex/imag_spec.rb index 2bafd1ab545d01..225f168e78ab0e 100644 --- a/spec/ruby/core/complex/imag_spec.rb +++ b/spec/ruby/core/complex/imag_spec.rb @@ -1,6 +1,7 @@ require_relative '../../spec_helper' -require_relative 'shared/image' describe "Complex#imag" do - it_behaves_like :complex_image, :imag + it "is an alias of Complex#imaginary" do + Complex.instance_method(:imag).should == Complex.instance_method(:imaginary) + end end diff --git a/spec/ruby/core/complex/imaginary_spec.rb b/spec/ruby/core/complex/imaginary_spec.rb index a8a1bfea90fd7a..ac9284e934e639 100644 --- a/spec/ruby/core/complex/imaginary_spec.rb +++ b/spec/ruby/core/complex/imaginary_spec.rb @@ -1,6 +1,10 @@ require_relative '../../spec_helper' -require_relative 'shared/image' describe "Complex#imaginary" do - it_behaves_like :complex_image, :imaginary + it "returns the imaginary part of self" do + Complex(1, 0).imaginary.should == 0 + Complex(2, 1).imaginary.should == 1 + Complex(6.7, 8.9).imaginary.should == 8.9 + Complex(1, bignum_value).imaginary.should == bignum_value + end end diff --git a/spec/ruby/core/complex/magnitude_spec.rb b/spec/ruby/core/complex/magnitude_spec.rb index 86f3b29868fb3c..6341b4eec8f193 100644 --- a/spec/ruby/core/complex/magnitude_spec.rb +++ b/spec/ruby/core/complex/magnitude_spec.rb @@ -1,6 +1,7 @@ require_relative '../../spec_helper' -require_relative 'shared/abs' describe "Complex#magnitude" do - it_behaves_like :complex_abs, :magnitude + it "is an alias of Complex#abs" do + Complex.instance_method(:magnitude).should == Complex.instance_method(:abs) + end end diff --git a/spec/ruby/core/complex/phase_spec.rb b/spec/ruby/core/complex/phase_spec.rb index 89574bf533bf19..2ab90989e12f35 100644 --- a/spec/ruby/core/complex/phase_spec.rb +++ b/spec/ruby/core/complex/phase_spec.rb @@ -1,6 +1,7 @@ require_relative '../../spec_helper' -require_relative 'shared/arg' describe "Complex#phase" do - it_behaves_like :complex_arg, :phase + it "is an alias of Complex#arg" do + Complex.instance_method(:phase).should == Complex.instance_method(:arg) + end end diff --git a/spec/ruby/core/complex/quo_spec.rb b/spec/ruby/core/complex/quo_spec.rb index ee6fd65c79e7a9..be0a44d5321633 100644 --- a/spec/ruby/core/complex/quo_spec.rb +++ b/spec/ruby/core/complex/quo_spec.rb @@ -1,6 +1,7 @@ require_relative '../../spec_helper' -require_relative 'shared/divide' describe "Complex#quo" do - it_behaves_like :complex_divide, :quo + it "is an alias of Complex#/" do + Complex.instance_method(:quo).should == Complex.instance_method(:/) + end end diff --git a/spec/ruby/core/complex/rect_spec.rb b/spec/ruby/core/complex/rect_spec.rb index 9e95f3efc264a3..72f2bf9930bf67 100644 --- a/spec/ruby/core/complex/rect_spec.rb +++ b/spec/ruby/core/complex/rect_spec.rb @@ -1,10 +1,13 @@ require_relative '../../spec_helper' -require_relative 'shared/rect' describe "Complex#rect" do - it_behaves_like :complex_rect, :rect + it "is an alias of Complex#rectangular" do + Complex.instance_method(:rect).should == Complex.instance_method(:rectangular) + end end describe "Complex.rect" do - it_behaves_like :complex_rect_class, :rect + it "is an alias of Complex#rectangular" do + Complex.method(:rect).should == Complex.method(:rectangular) + end end diff --git a/spec/ruby/core/complex/rectangular_spec.rb b/spec/ruby/core/complex/rectangular_spec.rb index d4b8ad9782a6e7..7789bf925eddfe 100644 --- a/spec/ruby/core/complex/rectangular_spec.rb +++ b/spec/ruby/core/complex/rectangular_spec.rb @@ -1,10 +1,114 @@ require_relative '../../spec_helper' -require_relative 'shared/rect' describe "Complex#rectangular" do - it_behaves_like :complex_rect, :rectangular + before :each do + @numbers = [ + Complex(1), + Complex(0, 20), + Complex(0, 0), + Complex(0.0), + Complex(9999999**99), + Complex(-20), + Complex.polar(76, 10) + ] + end + + it "returns an Array" do + @numbers.each do |number| + number.rectangular.should.instance_of?(Array) + end + end + + it "returns a two-element Array" do + @numbers.each do |number| + number.rectangular.size.should == 2 + end + end + + it "returns the real part of self as the first element" do + @numbers.each do |number| + number.rectangular.first.should == number.real + end + end + + it "returns the imaginary part of self as the last element" do + @numbers.each do |number| + number.rectangular.last.should == number.imaginary + end + end + + it "raises an ArgumentError if given any arguments" do + @numbers.each do |number| + -> { number.rectangular(number) }.should.raise(ArgumentError) + end + end end describe "Complex.rectangular" do - it_behaves_like :complex_rect_class, :rectangular + describe "passed a Numeric n which responds to #real? with true" do + it "returns a Complex with real part n and imaginary part 0" do + n = mock_numeric('n') + n.should_receive(:real?).any_number_of_times.and_return(true) + result = Complex.rectangular(n) + result.real.should == n + result.imag.should == 0 + end + end + + describe "passed a Numeric which responds to #real? with false" do + it "raises TypeError" do + n = mock_numeric('n') + n.should_receive(:real?).any_number_of_times.and_return(false) + -> { Complex.rectangular(n) }.should.raise(TypeError) + end + end + + describe "passed Numerics n1 and n2 and at least one responds to #real? with false" do + [[false, false], [false, true], [true, false]].each do |r1, r2| + it "raises TypeError" do + n1 = mock_numeric('n1') + n2 = mock_numeric('n2') + n1.should_receive(:real?).any_number_of_times.and_return(r1) + n2.should_receive(:real?).any_number_of_times.and_return(r2) + -> { Complex.rectangular(n1, n2) }.should.raise(TypeError) + end + end + end + + describe "passed Numerics n1 and n2 and both respond to #real? with true" do + it "returns a Complex with real part n1 and imaginary part n2" do + n1 = mock_numeric('n1') + n2 = mock_numeric('n2') + n1.should_receive(:real?).any_number_of_times.and_return(true) + n2.should_receive(:real?).any_number_of_times.and_return(true) + result = Complex.rectangular(n1, n2) + result.real.should == n1 + result.imag.should == n2 + end + end + + describe "when passed a Complex" do + it "raises a TypeError when the imaginary part is not zero" do + -> { + Complex.rectangular(1.0+1i, 2) + }.should.raise(TypeError) + + -> { + Complex.rectangular(1.0, 2i) + }.should.raise(TypeError) + end + + it "ignores the imaginary part if it is zero" do + result = Complex.rectangular(1.0+0i, 2+0.0i) + result.real.should == 1.0 + result.imag.should == 2 + end + end + + describe "passed a non-Numeric" do + it "raises TypeError" do + -> { Complex.rectangular(:sym) }.should.raise(TypeError) + -> { Complex.rectangular(0, :sym) }.should.raise(TypeError) + end + end end diff --git a/spec/ruby/core/complex/shared/abs.rb b/spec/ruby/core/complex/shared/abs.rb deleted file mode 100644 index 22994793415bca..00000000000000 --- a/spec/ruby/core/complex/shared/abs.rb +++ /dev/null @@ -1,10 +0,0 @@ -describe :complex_abs, shared: true do - it "returns the modulus: |a + bi| = sqrt((a ^ 2) + (b ^ 2))" do - Complex(0, 0).send(@method).should == 0 - Complex(3, 4).send(@method).should == 5 # well-known integer case - Complex(-3, 4).send(@method).should == 5 - Complex(1, -1).send(@method).should be_close(Math.sqrt(2), TOLERANCE) - Complex(6.5, 0).send(@method).should be_close(6.5, TOLERANCE) - Complex(0, -7.2).send(@method).should be_close(7.2, TOLERANCE) - end -end diff --git a/spec/ruby/core/complex/shared/arg.rb b/spec/ruby/core/complex/shared/arg.rb deleted file mode 100644 index c81f1974339dbe..00000000000000 --- a/spec/ruby/core/complex/shared/arg.rb +++ /dev/null @@ -1,9 +0,0 @@ -describe :complex_arg, shared: true do - it "returns the argument -- i.e., the angle from (1, 0) in the complex plane" do - two_pi = 2 * Math::PI - (Complex(1, 0).send(@method) % two_pi).should be_close(0, TOLERANCE) - (Complex(0, 2).send(@method) % two_pi).should be_close(Math::PI * 0.5, TOLERANCE) - (Complex(-100, 0).send(@method) % two_pi).should be_close(Math::PI, TOLERANCE) - (Complex(0, -75.3).send(@method) % two_pi).should be_close(Math::PI * 1.5, TOLERANCE) - end -end diff --git a/spec/ruby/core/complex/shared/conjugate.rb b/spec/ruby/core/complex/shared/conjugate.rb deleted file mode 100644 index d1ae47bcb678b6..00000000000000 --- a/spec/ruby/core/complex/shared/conjugate.rb +++ /dev/null @@ -1,8 +0,0 @@ -describe :complex_conjugate, shared: true do - it "returns the complex conjugate: conj a + bi = a - bi" do - Complex(3, 5).send(@method).should == Complex(3, -5) - Complex(3, -5).send(@method).should == Complex(3, 5) - Complex(-3.0, 5.2).send(@method).should be_close(Complex(-3.0, -5.2), TOLERANCE) - Complex(3.0, -5.2).send(@method).should be_close(Complex(3.0, 5.2), TOLERANCE) - end -end diff --git a/spec/ruby/core/complex/shared/divide.rb b/spec/ruby/core/complex/shared/divide.rb deleted file mode 100644 index ef79ecdf751fa0..00000000000000 --- a/spec/ruby/core/complex/shared/divide.rb +++ /dev/null @@ -1,82 +0,0 @@ -describe :complex_divide, shared: true do - describe "with Complex" do - it "divides according to the usual rule for complex numbers" do - a = Complex((1 * 10) - (2 * 20), (1 * 20) + (2 * 10)) - b = Complex(1, 2) - a.send(@method, b).should == Complex(10, 20) - - c = Complex((1.5 * 100.2) - (2.1 * -30.3), (1.5 * -30.3) + (2.1 * 100.2)) - d = Complex(1.5, 2.1) - # remember the floating-point arithmetic - c.send(@method, d).should be_close(Complex(100.2, -30.3), TOLERANCE) - end - end - - describe "with Fixnum" do - it "divides both parts of the Complex number" do - Complex(20, 40).send(@method, 2).should == Complex(10, 20) - Complex(30, 30).send(@method, 10).should == Complex(3, 3) - end - - it "raises a ZeroDivisionError when given zero" do - -> { Complex(20, 40).send(@method, 0) }.should.raise(ZeroDivisionError) - end - - it "produces Rational parts" do - Complex(5, 9).send(@method, 2).should.eql?(Complex(Rational(5,2), Rational(9,2))) - end - end - - describe "with Bignum" do - it "divides both parts of the Complex number" do - Complex(20, 40).send(@method, 2).should == Complex(10, 20) - Complex(15, 16).send(@method, 2.0).should be_close(Complex(7.5, 8), TOLERANCE) - end - end - - describe "with Float" do - it "divides both parts of the Complex number" do - Complex(3, 9).send(@method, 1.5).should == Complex(2, 6) - Complex(15, 16).send(@method, 2.0).should be_close(Complex(7.5, 8), TOLERANCE) - end - - it "returns Complex(Infinity, Infinity) when given zero" do - Complex(20, 40).send(@method, 0.0).real.infinite?.should == 1 - Complex(20, 40).send(@method, 0.0).imag.infinite?.should == 1 - Complex(-20, 40).send(@method, 0.0).real.infinite?.should == -1 - Complex(-20, 40).send(@method, 0.0).imag.infinite?.should == 1 - end - end - - describe "with Object" do - it "tries to coerce self into other" do - value = Complex(3, 9) - - obj = mock("Object") - obj.should_receive(:coerce).with(value).and_return([4, 2]) - value.send(@method, obj).should == 2 - end - end - - describe "with a Numeric which responds to #real? with true" do - it "returns Complex(real.quo(other), imag.quo(other))" do - other = mock_numeric('other') - real = mock_numeric('real') - imag = mock_numeric('imag') - other.should_receive(:real?).and_return(true) - real.should_receive(:quo).with(other).and_return(1) - imag.should_receive(:quo).with(other).and_return(2) - Complex(real, imag).send(@method, other).should == Complex(1, 2) - end - end - - describe "with a Numeric which responds to #real? with false" do - it "coerces the passed argument to Complex and divides the resulting elements" do - complex = Complex(3, 0) - other = mock_numeric('other') - other.should_receive(:real?).any_number_of_times.and_return(false) - other.should_receive(:coerce).with(complex).and_return([5, 2]) - complex.send(@method, other).should.eql?(Rational(5, 2)) - end - end -end diff --git a/spec/ruby/core/complex/shared/image.rb b/spec/ruby/core/complex/shared/image.rb deleted file mode 100644 index f839dbcaf909fb..00000000000000 --- a/spec/ruby/core/complex/shared/image.rb +++ /dev/null @@ -1,8 +0,0 @@ -describe :complex_image, shared: true do - it "returns the imaginary part of self" do - Complex(1, 0).send(@method).should == 0 - Complex(2, 1).send(@method).should == 1 - Complex(6.7, 8.9).send(@method).should == 8.9 - Complex(1, bignum_value).send(@method).should == bignum_value - end -end diff --git a/spec/ruby/core/complex/shared/rect.rb b/spec/ruby/core/complex/shared/rect.rb deleted file mode 100644 index 858234961b648b..00000000000000 --- a/spec/ruby/core/complex/shared/rect.rb +++ /dev/null @@ -1,94 +0,0 @@ -describe :complex_rect, shared: true do - before :each do - @numbers = [ - Complex(1), - Complex(0, 20), - Complex(0, 0), - Complex(0.0), - Complex(9999999**99), - Complex(-20), - Complex.polar(76, 10) - ] - end - - it "returns an Array" do - @numbers.each do |number| - number.send(@method).should.instance_of?(Array) - end - end - - it "returns a two-element Array" do - @numbers.each do |number| - number.send(@method).size.should == 2 - end - end - - it "returns the real part of self as the first element" do - @numbers.each do |number| - number.send(@method).first.should == number.real - end - end - - it "returns the imaginary part of self as the last element" do - @numbers.each do |number| - number.send(@method).last.should == number.imaginary - end - end - - it "raises an ArgumentError if given any arguments" do - @numbers.each do |number| - -> { number.send(@method, number) }.should.raise(ArgumentError) - end - end -end - -describe :complex_rect_class, shared: true do - describe "passed a Numeric n which responds to #real? with true" do - it "returns a Complex with real part n and imaginary part 0" do - n = mock_numeric('n') - n.should_receive(:real?).any_number_of_times.and_return(true) - result = Complex.send(@method, n) - result.real.should == n - result.imag.should == 0 - end - end - - describe "passed a Numeric which responds to #real? with false" do - it "raises TypeError" do - n = mock_numeric('n') - n.should_receive(:real?).any_number_of_times.and_return(false) - -> { Complex.send(@method, n) }.should.raise(TypeError) - end - end - - describe "passed Numerics n1 and n2 and at least one responds to #real? with false" do - [[false, false], [false, true], [true, false]].each do |r1, r2| - it "raises TypeError" do - n1 = mock_numeric('n1') - n2 = mock_numeric('n2') - n1.should_receive(:real?).any_number_of_times.and_return(r1) - n2.should_receive(:real?).any_number_of_times.and_return(r2) - -> { Complex.send(@method, n1, n2) }.should.raise(TypeError) - end - end - end - - describe "passed Numerics n1 and n2 and both respond to #real? with true" do - it "returns a Complex with real part n1 and imaginary part n2" do - n1 = mock_numeric('n1') - n2 = mock_numeric('n2') - n1.should_receive(:real?).any_number_of_times.and_return(true) - n2.should_receive(:real?).any_number_of_times.and_return(true) - result = Complex.send(@method, n1, n2) - result.real.should == n1 - result.imag.should == n2 - end - end - - describe "passed a non-Numeric" do - it "raises TypeError" do - -> { Complex.send(@method, :sym) }.should.raise(TypeError) - -> { Complex.send(@method, 0, :sym) }.should.raise(TypeError) - end - end -end diff --git a/spec/ruby/core/data/deconstruct_keys_spec.rb b/spec/ruby/core/data/deconstruct_keys_spec.rb index 53f2334546a25c..7e81f966ea0c0f 100644 --- a/spec/ruby/core/data/deconstruct_keys_spec.rb +++ b/spec/ruby/core/data/deconstruct_keys_spec.rb @@ -56,7 +56,7 @@ d.deconstruct_keys(nil).should == {x: 1, y: 2} end - ruby_bug "Bug #21844", ""..."4.1" do + ruby_version_is "4.0" do # https://bugs.ruby-lang.org/issues/21844 it "tries to convert a key with #to_str if index is not a String nor a Symbol, but responds to #to_str" do klass = Data.define(:x, :y) d = klass.new(1, 2) diff --git a/spec/ruby/core/data/initialize_spec.rb b/spec/ruby/core/data/initialize_spec.rb index 63d49e9c84da56..0320ca880c7317 100644 --- a/spec/ruby/core/data/initialize_spec.rb +++ b/spec/ruby/core/data/initialize_spec.rb @@ -47,6 +47,12 @@ data.unit.should == "km" end + it "accepts the last entry when a keyword is given as both String and Symbol" do + data = DataSpecs::Single.new("value" => -1, value: 42) + + data.value.should == 42 + end + it "accepts positional arguments with empty keyword arguments" do data = DataSpecs::Single.new(42, **{}) @@ -74,6 +80,16 @@ } end + ruby_version_is "4.0" do # https://bugs.ruby-lang.org/issues/21844 + it "raises ArgumentError if at least one argument is missing and other is provided as both String and Symbol" do + -> { + DataSpecs::Measure.new(unit: "km", "unit" => "km") + }.should.raise(ArgumentError) { |e| + e.message.should.include?("missing keyword: :amount") + } + end + end + it "raises ArgumentError if unknown keyword is given" do -> { DataSpecs::Measure.new(amount: 42, unit: "km", system: "metric") @@ -82,6 +98,38 @@ } end + ruby_version_is "4.0" do # https://bugs.ruby-lang.org/issues/21844 + it "raises ArgumentError if unknown keyword is given which is convertable to String" do + key = mock("to_str") + key.should_receive(:to_str).and_return("system") + + -> { + DataSpecs::Measure.new(amount: 42, unit: "km", key => "metric") + }.should.raise(ArgumentError) { |e| + e.message.should.include?('unknown keyword: "system"') + } + end + + it "raises TypeError when the keyword is not convertable to String" do + -> { + DataSpecs::Measure.new(1 => 2) + }.should.raise(TypeError) { |e| + e.message.should == "1 is not a symbol nor a string" + } + end + + it "raises TypeError if the conversion with #to_str does not return a String" do + klass = Data.define(:x, :y) + + key = mock("to_str") + key.should_receive(:to_str).and_return(0) + + -> { + klass.new(key => 2) + }.should raise_consistent_error(TypeError, /can't convert MockObject into String/) + end + end + it "supports super from a subclass" do ms = DataSpecs::MeasureSubclass.new(amount: 1, unit: "km") diff --git a/spec/ruby/core/data/inspect_spec.rb b/spec/ruby/core/data/inspect_spec.rb index 38642910a04563..6c97a719e3c51d 100644 --- a/spec/ruby/core/data/inspect_spec.rb +++ b/spec/ruby/core/data/inspect_spec.rb @@ -1,6 +1,63 @@ require_relative '../../spec_helper' -require_relative 'shared/inspect' +require_relative 'fixtures/classes' describe "Data#inspect" do - it_behaves_like :data_inspect, :inspect + it "returns a string representation showing members and values" do + a = DataSpecs::Measure.new(42, "km") + a.inspect.should == '#' + end + + it "returns a string representation without the class name for anonymous structs" do + Data.define(:a).new("").inspect.should == '#' + end + + it "returns a string representation without the class name for structs nested in anonymous classes" do + c = Class.new + c.class_eval <<~DOC + Foo = Data.define(:a) + DOC + + c::Foo.new("").inspect.should == '#' + end + + it "returns a string representation without the class name for structs nested in anonymous modules" do + m = Module.new + m.class_eval <<~DOC + Foo = Data.define(:a) + DOC + + m::Foo.new("").inspect.should == '#' + end + + it "does not call #name method" do + struct = DataSpecs::MeasureWithOverriddenName.new(42, "km") + struct.inspect.should == '#' + end + + it "does not call #name method when struct is anonymous" do + klass = Class.new(DataSpecs::Measure) do + def self.name + "A" + end + end + struct = klass.new(42, "km") + struct.inspect.should == '#' + end + + context "recursive structure" do + it "returns string representation with recursive attribute replaced with ..." do + a = DataSpecs::Measure.allocate + a.send(:initialize, amount: 42, unit: a) + + a.inspect.should == "#>" + end + + it "returns string representation with recursive attribute replaced with ... when an anonymous class" do + klass = Class.new(DataSpecs::Measure) + a = klass.allocate + a.send(:initialize, amount: 42, unit: a) + + a.inspect.should =~ /#:\.\.\.>>/ + end + end end diff --git a/spec/ruby/core/data/shared/inspect.rb b/spec/ruby/core/data/shared/inspect.rb deleted file mode 100644 index 6cd5664da7757e..00000000000000 --- a/spec/ruby/core/data/shared/inspect.rb +++ /dev/null @@ -1,62 +0,0 @@ -require_relative '../fixtures/classes' - -describe :data_inspect, shared: true do - it "returns a string representation showing members and values" do - a = DataSpecs::Measure.new(42, "km") - a.send(@method).should == '#' - end - - it "returns a string representation without the class name for anonymous structs" do - Data.define(:a).new("").send(@method).should == '#' - end - - it "returns a string representation without the class name for structs nested in anonymous classes" do - c = Class.new - c.class_eval <<~DOC - Foo = Data.define(:a) - DOC - - c::Foo.new("").send(@method).should == '#' - end - - it "returns a string representation without the class name for structs nested in anonymous modules" do - m = Module.new - m.class_eval <<~DOC - Foo = Data.define(:a) - DOC - - m::Foo.new("").send(@method).should == '#' - end - - it "does not call #name method" do - struct = DataSpecs::MeasureWithOverriddenName.new(42, "km") - struct.send(@method).should == '#' - end - - it "does not call #name method when struct is anonymous" do - klass = Class.new(DataSpecs::Measure) do - def self.name - "A" - end - end - struct = klass.new(42, "km") - struct.send(@method).should == '#' - end - - context "recursive structure" do - it "returns string representation with recursive attribute replaced with ..." do - a = DataSpecs::Measure.allocate - a.send(:initialize, amount: 42, unit: a) - - a.send(@method).should == "#>" - end - - it "returns string representation with recursive attribute replaced with ... when an anonymous class" do - klass = Class.new(DataSpecs::Measure) - a = klass.allocate - a.send(:initialize, amount: 42, unit: a) - - a.send(@method).should =~ /#:\.\.\.>>/ - end - end -end diff --git a/spec/ruby/core/data/to_s_spec.rb b/spec/ruby/core/data/to_s_spec.rb index 2b4a670e8e301b..a552e4659b8833 100644 --- a/spec/ruby/core/data/to_s_spec.rb +++ b/spec/ruby/core/data/to_s_spec.rb @@ -1,6 +1,9 @@ require_relative '../../spec_helper' -require_relative 'shared/inspect' +require_relative 'fixtures/classes' describe "Data#to_s" do - it_behaves_like :data_inspect, :to_s + it "is an alias of Data#inspect" do + a = DataSpecs::Measure.new(42, "km") + a.method(:to_s).should == a.method(:inspect) + end end diff --git a/spec/ruby/core/dir/exist_spec.rb b/spec/ruby/core/dir/exist_spec.rb index 0b8e521894d5e1..05ad67dd030e90 100644 --- a/spec/ruby/core/dir/exist_spec.rb +++ b/spec/ruby/core/dir/exist_spec.rb @@ -1,6 +1,5 @@ require_relative '../../spec_helper' require_relative 'fixtures/common' -require_relative 'shared/exist' describe "Dir.exist?" do before :all do @@ -11,7 +10,61 @@ DirSpecs.delete_mock_dirs end - it_behaves_like :dir_exist, :exist? + it "returns true if the given directory exists" do + Dir.exist?(__dir__).should == true + end + + it "returns true for '.'" do + Dir.exist?('.').should == true + end + + it "returns true for '..'" do + Dir.exist?('..').should == true + end + + it "understands non-ASCII paths" do + subdir = File.join(tmp("\u{9876}\u{665}")) + Dir.exist?(subdir).should == false + Dir.mkdir(subdir) + Dir.exist?(subdir).should == true + Dir.rmdir(subdir) + end + + it "understands relative paths" do + Dir.exist?(__dir__ + '/../').should == true + end + + it "returns false if the given directory doesn't exist" do + Dir.exist?('y26dg27n2nwjs8a/').should == false + end + + it "doesn't require the name to have a trailing slash" do + dir = __dir__ + dir.sub!(/\/$/,'') + Dir.exist?(dir).should == true + end + + it "doesn't expand paths" do + skip "$HOME not valid directory" unless ENV['HOME'] && File.directory?(ENV['HOME']) + Dir.exist?(File.expand_path('~')).should == true + Dir.exist?('~').should == false + end + + it "returns false if the argument exists but is a file" do + File.should.exist?(__FILE__) + Dir.exist?(__FILE__).should == false + end + + it "doesn't set $! when file doesn't exist" do + Dir.exist?("/path/to/non/existent/dir") + $!.should == nil + end + + it "calls #to_path on non String arguments" do + p = mock('path') + p.should_receive(:to_path).and_return(__dir__) + Dir.exist?(p) + end end describe "Dir.exists?" do diff --git a/spec/ruby/core/dir/getwd_spec.rb b/spec/ruby/core/dir/getwd_spec.rb index 132634347c4ea0..138481821f3dee 100644 --- a/spec/ruby/core/dir/getwd_spec.rb +++ b/spec/ruby/core/dir/getwd_spec.rb @@ -1,15 +1,7 @@ require_relative '../../spec_helper' -require_relative 'fixtures/common' -require_relative 'shared/pwd' describe "Dir.getwd" do - before :all do - DirSpecs.create_mock_dirs + it "is an alias of Dir.pwd" do + Dir.method(:getwd).should == Dir.method(:pwd) end - - after :all do - DirSpecs.delete_mock_dirs - end - - it_behaves_like :dir_pwd, :getwd end diff --git a/spec/ruby/core/dir/open_spec.rb b/spec/ruby/core/dir/open_spec.rb index 27f362320b194b..be01638fbc5fa1 100644 --- a/spec/ruby/core/dir/open_spec.rb +++ b/spec/ruby/core/dir/open_spec.rb @@ -1,6 +1,5 @@ require_relative '../../spec_helper' require_relative 'fixtures/common' -require_relative 'shared/open' describe "Dir.open" do before :all do @@ -11,5 +10,75 @@ DirSpecs.delete_mock_dirs end - it_behaves_like :dir_open, :open + it "returns a Dir instance representing the specified directory" do + dir = Dir.open(DirSpecs.mock_dir) + dir.should.is_a?(Dir) + dir.close + end + + it "raises a SystemCallError if the directory does not exist" do + -> do + Dir.open(DirSpecs.nonexistent) + end.should.raise(SystemCallError) + end + + it "may take a block which is yielded to with the Dir instance" do + Dir.open(DirSpecs.mock_dir) {|dir| dir.should.is_a?(Dir)} + end + + it "returns the value of the block if a block is given" do + Dir.open(DirSpecs.mock_dir) {|dir| :value }.should == :value + end + + it "closes the Dir instance when the block exits if given a block" do + closed_dir = Dir.open(DirSpecs.mock_dir) { |dir| dir } + -> { closed_dir.read }.should.raise(IOError) + end + + it "closes the Dir instance when the block exits the block even due to an exception" do + closed_dir = nil + + -> do + Dir.open(DirSpecs.mock_dir) do |dir| + closed_dir = dir + raise "dir specs" + end + end.should.raise(RuntimeError, "dir specs") + + -> { closed_dir.read }.should.raise(IOError) + end + + it "calls #to_path on non-String arguments" do + p = mock('path') + p.should_receive(:to_path).and_return(DirSpecs.mock_dir) + Dir.open(p) { true } + end + + it "accepts an options Hash" do + dir = Dir.open(DirSpecs.mock_dir, encoding: "utf-8") {|d| d } + dir.should.is_a?(Dir) + end + + it "calls #to_hash to convert the options object" do + options = mock("dir_open") + options.should_receive(:to_hash).and_return({ encoding: Encoding::UTF_8 }) + + dir = Dir.open(DirSpecs.mock_dir, **options) {|d| d } + dir.should.is_a?(Dir) + end + + it "ignores the :encoding option if it is nil" do + dir = Dir.open(DirSpecs.mock_dir, encoding: nil) {|d| d } + dir.should.is_a?(Dir) + end + + platform_is_not :windows do + it 'sets the close-on-exec flag for the directory file descriptor' do + Dir.open(DirSpecs.mock_dir) do |dir| + io = IO.for_fd(dir.fileno) + io.autoclose = false + io.should.close_on_exec? + end + end + end end diff --git a/spec/ruby/core/dir/path_spec.rb b/spec/ruby/core/dir/path_spec.rb index b1c24c406b76cd..02ddd2cd4257cd 100644 --- a/spec/ruby/core/dir/path_spec.rb +++ b/spec/ruby/core/dir/path_spec.rb @@ -1,6 +1,5 @@ require_relative '../../spec_helper' require_relative 'fixtures/common' -require_relative 'shared/path' describe "Dir#path" do before :all do @@ -11,5 +10,28 @@ DirSpecs.delete_mock_dirs end - it_behaves_like :dir_path, :path + it "returns the path that was supplied to .new or .open" do + dir = Dir.open DirSpecs.mock_dir + begin + dir.path.should == DirSpecs.mock_dir + ensure + dir.close rescue nil + end + end + + it "returns the path even when called on a closed Dir instance" do + dir = Dir.open DirSpecs.mock_dir + dir.close + dir.path.should == DirSpecs.mock_dir + end + + it "returns a String with the same encoding as the argument to .open" do + path = DirSpecs.mock_dir.force_encoding Encoding::IBM866 + dir = Dir.open path + begin + dir.path.encoding.should.equal?(Encoding::IBM866) + ensure + dir.close + end + end end diff --git a/spec/ruby/core/dir/pos_spec.rb b/spec/ruby/core/dir/pos_spec.rb index b382bff81fa81b..f8ca06d7780124 100644 --- a/spec/ruby/core/dir/pos_spec.rb +++ b/spec/ruby/core/dir/pos_spec.rb @@ -1,30 +1,11 @@ require_relative '../../spec_helper' require_relative 'fixtures/common' -require_relative 'shared/closed' require_relative 'shared/pos' describe "Dir#pos" do - before :all do - DirSpecs.create_mock_dirs - end - - after :all do - DirSpecs.delete_mock_dirs - end - - it_behaves_like :dir_pos, :pos -end - -describe "Dir#pos" do - before :all do - DirSpecs.create_mock_dirs + it "is an alias of Dir#tell" do + Dir.instance_method(:pos).should == Dir.instance_method(:tell) end - - after :all do - DirSpecs.delete_mock_dirs - end - - it_behaves_like :dir_closed, :pos end describe "Dir#pos=" do diff --git a/spec/ruby/core/dir/pwd_spec.rb b/spec/ruby/core/dir/pwd_spec.rb index ad01286c9012b9..70208b03d673a1 100644 --- a/spec/ruby/core/dir/pwd_spec.rb +++ b/spec/ruby/core/dir/pwd_spec.rb @@ -1,7 +1,6 @@ # -*- encoding: utf-8 -*- require_relative '../../spec_helper' require_relative 'fixtures/common' -require_relative 'shared/pwd' describe "Dir.pwd" do before :all do @@ -12,7 +11,49 @@ DirSpecs.delete_mock_dirs end - it_behaves_like :dir_pwd, :pwd + before :each do + @fs_encoding = Encoding.find('filesystem') + end + + it "returns the current working directory" do + pwd = Dir.pwd + + File.directory?(pwd).should == true + + # On ubuntu gutsy, for example, /bin/pwd does not + # understand -P. With just `pwd -P`, /bin/pwd is run. + + # The following uses inode rather than file names to account for + # case insensitive file systems like default OS/X file systems + platform_is_not :windows do + File.stat(pwd).ino.should == File.stat(`/bin/sh -c "pwd -P"`.chomp).ino + end + platform_is :windows do + File.stat(pwd).ino.should == File.stat(File.expand_path(`cd`.chomp)).ino + end + end + + it "returns an absolute path" do + pwd = Dir.pwd + pwd.should == File.expand_path(pwd) + end + + it "returns an absolute path even when chdir to a relative path" do + Dir.chdir(".") do + pwd = Dir.pwd + File.directory?(pwd).should == true + pwd.should == File.expand_path(pwd) + end + end + + it "returns a String with the filesystem encoding" do + enc = Dir.pwd.encoding + if @fs_encoding == Encoding::US_ASCII + [Encoding::US_ASCII, Encoding::BINARY].should.include?(enc) + else + enc.should.equal?(@fs_encoding) + end + end end describe "Dir.pwd" do diff --git a/spec/ruby/core/dir/shared/exist.rb b/spec/ruby/core/dir/shared/exist.rb deleted file mode 100644 index 4ceaccea66fe6b..00000000000000 --- a/spec/ruby/core/dir/shared/exist.rb +++ /dev/null @@ -1,57 +0,0 @@ -describe :dir_exist, shared: true do - it "returns true if the given directory exists" do - Dir.send(@method, __dir__).should == true - end - - it "returns true for '.'" do - Dir.send(@method, '.').should == true - end - - it "returns true for '..'" do - Dir.send(@method, '..').should == true - end - - it "understands non-ASCII paths" do - subdir = File.join(tmp("\u{9876}\u{665}")) - Dir.send(@method, subdir).should == false - Dir.mkdir(subdir) - Dir.send(@method, subdir).should == true - Dir.rmdir(subdir) - end - - it "understands relative paths" do - Dir.send(@method, __dir__ + '/../').should == true - end - - it "returns false if the given directory doesn't exist" do - Dir.send(@method, 'y26dg27n2nwjs8a/').should == false - end - - it "doesn't require the name to have a trailing slash" do - dir = __dir__ - dir.sub!(/\/$/,'') - Dir.send(@method, dir).should == true - end - - it "doesn't expand paths" do - skip "$HOME not valid directory" unless ENV['HOME'] && File.directory?(ENV['HOME']) - Dir.send(@method, File.expand_path('~')).should == true - Dir.send(@method, '~').should == false - end - - it "returns false if the argument exists but is a file" do - File.should.exist?(__FILE__) - Dir.send(@method, __FILE__).should == false - end - - it "doesn't set $! when file doesn't exist" do - Dir.send(@method, "/path/to/non/existent/dir") - $!.should == nil - end - - it "calls #to_path on non String arguments" do - p = mock('path') - p.should_receive(:to_path).and_return(__dir__) - Dir.send(@method, p) - end -end diff --git a/spec/ruby/core/dir/shared/open.rb b/spec/ruby/core/dir/shared/open.rb deleted file mode 100644 index 9ac3a40694375d..00000000000000 --- a/spec/ruby/core/dir/shared/open.rb +++ /dev/null @@ -1,73 +0,0 @@ -describe :dir_open, shared: true do - it "returns a Dir instance representing the specified directory" do - dir = Dir.send(@method, DirSpecs.mock_dir) - dir.should.is_a?(Dir) - dir.close - end - - it "raises a SystemCallError if the directory does not exist" do - -> do - Dir.send @method, DirSpecs.nonexistent - end.should.raise(SystemCallError) - end - - it "may take a block which is yielded to with the Dir instance" do - Dir.send(@method, DirSpecs.mock_dir) {|dir| dir.should.is_a?(Dir)} - end - - it "returns the value of the block if a block is given" do - Dir.send(@method, DirSpecs.mock_dir) {|dir| :value }.should == :value - end - - it "closes the Dir instance when the block exits if given a block" do - closed_dir = Dir.send(@method, DirSpecs.mock_dir) { |dir| dir } - -> { closed_dir.read }.should.raise(IOError) - end - - it "closes the Dir instance when the block exits the block even due to an exception" do - closed_dir = nil - - -> do - Dir.send(@method, DirSpecs.mock_dir) do |dir| - closed_dir = dir - raise "dir specs" - end - end.should.raise(RuntimeError, "dir specs") - - -> { closed_dir.read }.should.raise(IOError) - end - - it "calls #to_path on non-String arguments" do - p = mock('path') - p.should_receive(:to_path).and_return(DirSpecs.mock_dir) - Dir.send(@method, p) { true } - end - - it "accepts an options Hash" do - dir = Dir.send(@method, DirSpecs.mock_dir, encoding: "utf-8") {|d| d } - dir.should.is_a?(Dir) - end - - it "calls #to_hash to convert the options object" do - options = mock("dir_open") - options.should_receive(:to_hash).and_return({ encoding: Encoding::UTF_8 }) - - dir = Dir.send(@method, DirSpecs.mock_dir, **options) {|d| d } - dir.should.is_a?(Dir) - end - - it "ignores the :encoding option if it is nil" do - dir = Dir.send(@method, DirSpecs.mock_dir, encoding: nil) {|d| d } - dir.should.is_a?(Dir) - end - - platform_is_not :windows do - it 'sets the close-on-exec flag for the directory file descriptor' do - Dir.send(@method, DirSpecs.mock_dir) do |dir| - io = IO.for_fd(dir.fileno) - io.autoclose = false - io.should.close_on_exec? - end - end - end -end diff --git a/spec/ruby/core/dir/shared/path.rb b/spec/ruby/core/dir/shared/path.rb deleted file mode 100644 index 7647c04421127b..00000000000000 --- a/spec/ruby/core/dir/shared/path.rb +++ /dev/null @@ -1,30 +0,0 @@ -require_relative '../../../spec_helper' -require_relative '../fixtures/common' -require_relative 'closed' - -describe :dir_path, shared: true do - it "returns the path that was supplied to .new or .open" do - dir = Dir.open DirSpecs.mock_dir - begin - dir.send(@method).should == DirSpecs.mock_dir - ensure - dir.close rescue nil - end - end - - it "returns the path even when called on a closed Dir instance" do - dir = Dir.open DirSpecs.mock_dir - dir.close - dir.send(@method).should == DirSpecs.mock_dir - end - - it "returns a String with the same encoding as the argument to .open" do - path = DirSpecs.mock_dir.force_encoding Encoding::IBM866 - dir = Dir.open path - begin - dir.send(@method).encoding.should.equal?(Encoding::IBM866) - ensure - dir.close - end - end -end diff --git a/spec/ruby/core/dir/shared/pos.rb b/spec/ruby/core/dir/shared/pos.rb index 11712cc75df463..741de8918ced50 100644 --- a/spec/ruby/core/dir/shared/pos.rb +++ b/spec/ruby/core/dir/shared/pos.rb @@ -1,30 +1,3 @@ -describe :dir_pos, shared: true do - before :each do - @dir = Dir.open DirSpecs.mock_dir - end - - after :each do - @dir.close rescue nil - end - - it "returns an Integer representing the current position in the directory" do - @dir.send(@method).should.is_a?(Integer) - @dir.send(@method).should.is_a?(Integer) - @dir.send(@method).should.is_a?(Integer) - end - - it "returns a different Integer if moved from previous position" do - a = @dir.send(@method) - @dir.read - b = @dir.send(@method) - - a.should.is_a?(Integer) - b.should.is_a?(Integer) - - a.should_not == b - end -end - describe :dir_pos_set, shared: true do before :each do @dir = Dir.open DirSpecs.mock_dir diff --git a/spec/ruby/core/dir/shared/pwd.rb b/spec/ruby/core/dir/shared/pwd.rb deleted file mode 100644 index ed47fe382af309..00000000000000 --- a/spec/ruby/core/dir/shared/pwd.rb +++ /dev/null @@ -1,45 +0,0 @@ -describe :dir_pwd, shared: true do - before :each do - @fs_encoding = Encoding.find('filesystem') - end - - it "returns the current working directory" do - pwd = Dir.send(@method) - - File.directory?(pwd).should == true - - # On ubuntu gutsy, for example, /bin/pwd does not - # understand -P. With just `pwd -P`, /bin/pwd is run. - - # The following uses inode rather than file names to account for - # case insensitive file systems like default OS/X file systems - platform_is_not :windows do - File.stat(pwd).ino.should == File.stat(`/bin/sh -c "pwd -P"`.chomp).ino - end - platform_is :windows do - File.stat(pwd).ino.should == File.stat(File.expand_path(`cd`.chomp)).ino - end - end - - it "returns an absolute path" do - pwd = Dir.send(@method) - pwd.should == File.expand_path(pwd) - end - - it "returns an absolute path even when chdir to a relative path" do - Dir.chdir(".") do - pwd = Dir.send(@method) - File.directory?(pwd).should == true - pwd.should == File.expand_path(pwd) - end - end - - it "returns a String with the filesystem encoding" do - enc = Dir.send(@method).encoding - if @fs_encoding == Encoding::US_ASCII - [Encoding::US_ASCII, Encoding::BINARY].should.include?(enc) - else - enc.should.equal?(@fs_encoding) - end - end -end diff --git a/spec/ruby/core/dir/tell_spec.rb b/spec/ruby/core/dir/tell_spec.rb index af86dc1598d999..dcbb40438f2b40 100644 --- a/spec/ruby/core/dir/tell_spec.rb +++ b/spec/ruby/core/dir/tell_spec.rb @@ -12,7 +12,30 @@ DirSpecs.delete_mock_dirs end - it_behaves_like :dir_pos, :tell - it_behaves_like :dir_closed, :tell + + before :each do + @dir = Dir.open DirSpecs.mock_dir + end + + after :each do + @dir.close rescue nil + end + + it "returns an Integer representing the current position in the directory" do + @dir.tell.should.is_a?(Integer) + @dir.tell.should.is_a?(Integer) + @dir.tell.should.is_a?(Integer) + end + + it "returns a different Integer if moved from previous position" do + a = @dir.tell + @dir.read + b = @dir.tell + + a.should.is_a?(Integer) + b.should.is_a?(Integer) + + a.should_not == b + end end diff --git a/spec/ruby/core/dir/to_path_spec.rb b/spec/ruby/core/dir/to_path_spec.rb index 77404a3dc85911..2ed533e757df68 100644 --- a/spec/ruby/core/dir/to_path_spec.rb +++ b/spec/ruby/core/dir/to_path_spec.rb @@ -1,15 +1,7 @@ require_relative '../../spec_helper' -require_relative 'fixtures/common' -require_relative 'shared/path' describe "Dir#to_path" do - before :all do - DirSpecs.create_mock_dirs + it "is an alias of Dir#path" do + Dir.instance_method(:to_path).should == Dir.instance_method(:path) end - - after :all do - DirSpecs.delete_mock_dirs - end - - it_behaves_like :dir_path, :to_path end diff --git a/spec/ruby/core/encoding/name_spec.rb b/spec/ruby/core/encoding/name_spec.rb index dce9347978e4e0..1d625c937966f2 100644 --- a/spec/ruby/core/encoding/name_spec.rb +++ b/spec/ruby/core/encoding/name_spec.rb @@ -1,6 +1,15 @@ require_relative "../../spec_helper" -require_relative 'shared/name' describe "Encoding#name" do - it_behaves_like :encoding_name, :name + it "returns a String" do + Encoding.list.each do |e| + e.name.should.instance_of?(String) + end + end + + it "uniquely identifies an encoding" do + Encoding.list.each do |e| + e.should == Encoding.find(e.name) + end + end end diff --git a/spec/ruby/core/encoding/shared/name.rb b/spec/ruby/core/encoding/shared/name.rb deleted file mode 100644 index 4d4b860a1f652f..00000000000000 --- a/spec/ruby/core/encoding/shared/name.rb +++ /dev/null @@ -1,15 +0,0 @@ -require_relative '../../../spec_helper' - -describe :encoding_name, shared: true do - it "returns a String" do - Encoding.list.each do |e| - e.send(@method).should.instance_of?(String) - end - end - - it "uniquely identifies an encoding" do - Encoding.list.each do |e| - e.should == Encoding.find(e.send(@method)) - end - end -end diff --git a/spec/ruby/core/encoding/to_s_spec.rb b/spec/ruby/core/encoding/to_s_spec.rb index bab394a888888f..ee5e3b4abeeb73 100644 --- a/spec/ruby/core/encoding/to_s_spec.rb +++ b/spec/ruby/core/encoding/to_s_spec.rb @@ -1,6 +1,7 @@ require_relative "../../spec_helper" -require_relative 'shared/name' describe "Encoding#to_s" do - it_behaves_like :encoding_name, :to_s + it "is an alias of Encoding#name" do + Encoding.instance_method(:to_s).should == Encoding.instance_method(:name) + end end diff --git a/spec/ruby/core/enumerable/shared/value_packing.rb b/spec/ruby/core/enumerable/shared/value_packing.rb new file mode 100644 index 00000000000000..ff77f45cdfe156 --- /dev/null +++ b/spec/ruby/core/enumerable/shared/value_packing.rb @@ -0,0 +1,26 @@ +# This is the behavior of rb_enum_values_pack() in CRuby +describe :enumerable_value_packing, shared: true do + # @take must be set to a Proc that returns the take-result whose #each + # yields packed values (e.g. -> e { e.take(1) } or -> e { e.lazy.take(1) }). + + it "yields a single nil for a zero-argument source yield" do + e = Enumerator.new { |y| y.yield } + args = nil + @take.call(e).each { |*a| args = a } + args.should == [nil] + end + + it "yields the value for a single-argument source yield" do + e = Enumerator.new { |y| y.yield :v } + args = nil + @take.call(e).each { |*a| args = a } + args.should == [:v] + end + + it "yields a packed Array for a multi-argument source yield" do + e = Enumerator.new { |y| y.yield 1, 2 } + args = nil + @take.call(e).each { |*a| args = a } + args.should == [[1, 2]] + end +end diff --git a/spec/ruby/core/enumerable/take_spec.rb b/spec/ruby/core/enumerable/take_spec.rb index 8cc746f88d9edd..ca439b750d5715 100644 --- a/spec/ruby/core/enumerable/take_spec.rb +++ b/spec/ruby/core/enumerable/take_spec.rb @@ -1,6 +1,7 @@ require_relative '../../spec_helper' require_relative 'fixtures/classes' require_relative 'shared/take' +require_relative 'shared/value_packing' describe "Enumerable#take" do it "requires an argument" do @@ -10,4 +11,11 @@ describe "when passed an argument" do it_behaves_like :enumerable_take, :take end + + describe "value packing of source yields" do + before :each do + @take = -> e { e.take(1) } + end + it_behaves_like :enumerable_value_packing, nil + end end diff --git a/spec/ruby/core/enumerator/generator/each_spec.rb b/spec/ruby/core/enumerator/generator/each_spec.rb deleted file mode 100644 index 41a494298b8817..00000000000000 --- a/spec/ruby/core/enumerator/generator/each_spec.rb +++ /dev/null @@ -1,40 +0,0 @@ -require_relative '../../../spec_helper' - -describe "Enumerator::Generator#each" do - before :each do - @generator = Enumerator::Generator.new do |y, *args| - y << 3 << 2 << 1 - y << args unless args.empty? - :block_returned - end - end - - it "is an enumerable" do - @generator.should.is_a?(Enumerable) - end - - it "supports enumeration with a block" do - r = [] - @generator.each { |v| r << v } - - r.should == [3, 2, 1] - end - - it "raises a LocalJumpError if no block given" do - -> { @generator.each }.should.raise(LocalJumpError) - end - - it "returns the block returned value" do - @generator.each {}.should.equal?(:block_returned) - end - - it "requires multiple arguments" do - Enumerator::Generator.instance_method(:each).arity.should < 0 - end - - it "appends given arguments to receiver.each" do - yields = [] - @generator.each(:each0, :each1) { |yielded| yields << yielded } - yields.should == [3, 2, 1, [:each0, :each1]] - end -end diff --git a/spec/ruby/core/enumerator/generator/initialize_spec.rb b/spec/ruby/core/enumerator/generator/initialize_spec.rb deleted file mode 100644 index 0f77d591d90fc2..00000000000000 --- a/spec/ruby/core/enumerator/generator/initialize_spec.rb +++ /dev/null @@ -1,26 +0,0 @@ -# -*- encoding: us-ascii -*- - -require_relative '../../../spec_helper' - -describe "Enumerator::Generator#initialize" do - before :each do - @class = Enumerator::Generator - @uninitialized = @class.allocate - end - - it "is a private method" do - @class.private_instance_methods(false).should.include?(:initialize) - end - - it "returns self when given a block" do - @uninitialized.send(:initialize) {}.should.equal?(@uninitialized) - end - - describe "on frozen instance" do - it "raises a FrozenError" do - -> { - @uninitialized.freeze.send(:initialize) {} - }.should.raise(FrozenError) - end - end -end diff --git a/spec/ruby/core/enumerator/lazy/take_spec.rb b/spec/ruby/core/enumerator/lazy/take_spec.rb index f92360f543e7ef..2dd5b939e2b4b0 100644 --- a/spec/ruby/core/enumerator/lazy/take_spec.rb +++ b/spec/ruby/core/enumerator/lazy/take_spec.rb @@ -2,8 +2,16 @@ require_relative '../../../spec_helper' require_relative 'fixtures/classes' +require_relative '../../enumerable/shared/value_packing' describe "Enumerator::Lazy#take" do + describe "value packing of source yields (matches Enumerable#take)" do + before :each do + @take = -> e { e.lazy.take(1) } + end + it_behaves_like :enumerable_value_packing, nil + end + before :each do @yieldsmixed = EnumeratorLazySpecs::YieldsMixed.new.to_enum.lazy @eventsmixed = EnumeratorLazySpecs::EventsMixed.new.to_enum.lazy diff --git a/spec/ruby/core/enumerator/new_spec.rb b/spec/ruby/core/enumerator/new_spec.rb index 539328a6c9dc87..eb6c13759e4043 100644 --- a/spec/ruby/core/enumerator/new_spec.rb +++ b/spec/ruby/core/enumerator/new_spec.rb @@ -42,35 +42,74 @@ enum.to_a.should == ["a\n", "b\n", "c"] end - describe 'yielded values' do - it 'handles yield arguments properly' do + describe '#yield' do + it 'accepts a single argument' do Enumerator.new { |y| y.yield(1) }.to_a.should == [1] Enumerator.new { |y| y.yield(1) }.first.should == 1 + end - Enumerator.new { |y| y.yield([1]) }.to_a.should == [[1]] - Enumerator.new { |y| y.yield([1]) }.first.should == [1] - + it 'accepts multiple arguments' do Enumerator.new { |y| y.yield(1, 2) }.to_a.should == [[1, 2]] Enumerator.new { |y| y.yield(1, 2) }.first.should == [1, 2] + end + + it "doesn't double-wrap arrays" do + Enumerator.new { |y| y.yield([1]) }.to_a.should == [[1]] + Enumerator.new { |y| y.yield([1]) }.first.should == [1] Enumerator.new { |y| y.yield([1, 2]) }.to_a.should == [[1, 2]] Enumerator.new { |y| y.yield([1, 2]) }.first.should == [1, 2] end - it 'handles << arguments properly' do + it 'returns nil' do + ScratchPad.record [] + Enumerator.new do |y| + ScratchPad << y.yield(1) + end.to_a + + ScratchPad.recorded.should == [nil] + end + + it 'accepts keyword arguments and treats them as a positional hash' do + Enumerator.new { |y| y.yield(foo: 42) }.to_a.should == [{ foo: 42 }] + Enumerator.new { |y| y.yield(foo: 42) }.first.should == { foo: 42 } + + Enumerator.new { |y| y.yield(123, foo: 42) }.to_a.should == [[123, { foo: 42 }]] + Enumerator.new { |y| y.yield(123, foo: 42) }.first.should == [123, { foo: 42 }] + end + end + + describe '#<<' do + it 'accepts a single argument' do Enumerator.new { |y| y.<<(1) }.to_a.should == [1] Enumerator.new { |y| y.<<(1) }.first.should == 1 + end + it "doesn't double-wrap arrays" do Enumerator.new { |y| y.<<([1]) }.to_a.should == [[1]] Enumerator.new { |y| y.<<([1]) }.first.should == [1] - # << doesn't accept multiple arguments - # Enumerator.new { |y| y.<<(1, 2) }.to_a.should == [[1, 2]] - # Enumerator.new { |y| y.<<(1, 2) }.first.should == [1, 2] - Enumerator.new { |y| y.<<([1, 2]) }.to_a.should == [[1, 2]] Enumerator.new { |y| y.<<([1, 2]) }.first.should == [1, 2] end + + it 'accepts keyword arguments and treats them as a positional hash' do + Enumerator.new { |y| y.<<(foo: 42) }.to_a.should == [{ foo: 42 }] + Enumerator.new { |y| y.<<(foo: 42) }.first.should == { foo: 42 } + end + + it 'can be chained' do + enum = Enumerator.new do |y| + y << 1 << 2 + end + enum.to_a.should == [1, 2] + end + + it 'raises ArgumentError when given more than one argument' do + -> { + Enumerator.new { |y| y.<<(1, 2) }.to_a + }.should.raise(ArgumentError, "wrong number of arguments (given 2, expected 1)") + end end end end diff --git a/spec/ruby/core/enumerator/produce_spec.rb b/spec/ruby/core/enumerator/produce_spec.rb index c69fb493033e2a..eb1b09294e8f5a 100644 --- a/spec/ruby/core/enumerator/produce_spec.rb +++ b/spec/ruby/core/enumerator/produce_spec.rb @@ -3,6 +3,8 @@ describe "Enumerator.produce" do it "creates an infinite enumerator" do enum = Enumerator.produce(0) { |prev| prev + 1 } + + enum.size.should == Float::INFINITY enum.take(5).should == [0, 1, 2, 3, 4] end @@ -31,4 +33,46 @@ lines.should == ["a\n", "b\n", "c\n", "d"] end end + + it "raises ArgumentError when no block is given" do + -> { Enumerator.produce }.should.raise(ArgumentError, "no block given") + end + + ruby_version_is ""..."4.0" do + it "accepts keyword arguments as the initial value" do + enum = Enumerator.produce(a: 1, b: 1) {} + enum.take(1).should == [{a: 1, b: 1}] + end + end + + ruby_version_is "4.0" do + it "raises ArgumentError for unknown keyword arguments" do + -> { Enumerator.produce(a: 1, b: 1) {} }.should.raise(ArgumentError, /unknown keywords/) + end + end + + ruby_version_is "4.0" do + context "with size keyword argument" do + it "sets the size of the enumerator" do + enum = Enumerator.produce(0, size: 10) { |n| n + 1 } + + enum.size.should == 10 + enum.take(5).should == [0, 1, 2, 3, 4] + end + + it "accepts a callable" do + enum = Enumerator.produce(0, size: -> { 5 * 5 }) { |n| n + 1 } + + enum.size.should == 25 + enum.take(5).should == [0, 1, 2, 3, 4] + end + + it "accepts nil" do + enum = Enumerator.produce(0, size: nil) { |n| n + 1 } + + enum.size.should == nil + enum.take(5).should == [0, 1, 2, 3, 4] + end + end + end end diff --git a/spec/ruby/core/enumerator/product/each_spec.rb b/spec/ruby/core/enumerator/product/each_spec.rb index 164998404d418f..a5dced4db17323 100644 --- a/spec/ruby/core/enumerator/product/each_spec.rb +++ b/spec/ruby/core/enumerator/product/each_spec.rb @@ -68,4 +68,18 @@ def object2.each_entry enum.each { |x, y, z| acc << z } acc.should == [nil, nil, nil, nil] end + + it "yields no element when any enumerable is empty" do + enum = Enumerator::Product.new([], [1]) + + acc = [] + enum.each { |x| acc << x } + acc.should == [] + + enum = Enumerator::Product.new([1], []) + + acc = [] + enum.each { |x| acc << x } + acc.should == [] + end end diff --git a/spec/ruby/core/enumerator/product/size_spec.rb b/spec/ruby/core/enumerator/product/size_spec.rb index 96632d6eeecb1a..0ba427af9a1cd6 100644 --- a/spec/ruby/core/enumerator/product/size_spec.rb +++ b/spec/ruby/core/enumerator/product/size_spec.rb @@ -51,4 +51,14 @@ def enum.size; 1.0; end product = Enumerator::Product.new(1..2, enum) product.size.should == nil end + + ruby_version_is "3.4" do + it "returns zero when any enumerable reports zero" do + enum = Enumerator::Product.new(1...1, ["A", "B"]) + enum.size.should == 0 + + enum = Enumerator::Product.new(["A", "B"], 1...1) + enum.size.should == 0 + end + end end diff --git a/spec/ruby/core/enumerator/yielder/append_spec.rb b/spec/ruby/core/enumerator/yielder/append_spec.rb deleted file mode 100644 index 2e1f5203d9447e..00000000000000 --- a/spec/ruby/core/enumerator/yielder/append_spec.rb +++ /dev/null @@ -1,35 +0,0 @@ -require_relative '../../../spec_helper' - -describe "Enumerator::Yielder#<<" do - # TODO: There's some common behavior between yield and <<; move to a shared spec - it "yields the value to the block" do - ary = [] - y = Enumerator::Yielder.new {|x| ary << x} - y << 1 - - ary.should == [1] - end - - it "doesn't double-wrap Arrays" do - yields = [] - y = Enumerator::Yielder.new {|args| yields << args } - y << [1] - yields.should == [[1]] - end - - it "returns self" do - y = Enumerator::Yielder.new {|x| x + 1} - (y << 1).should.equal?(y) - end - - context "when multiple arguments passed" do - it "raises an ArgumentError" do - ary = [] - y = Enumerator::Yielder.new { |*x| ary << x } - - -> { - y.<<(1, 2) - }.should.raise(ArgumentError, /wrong number of arguments/) - end - end -end diff --git a/spec/ruby/core/enumerator/yielder/initialize_spec.rb b/spec/ruby/core/enumerator/yielder/initialize_spec.rb deleted file mode 100644 index 925f561ec43db7..00000000000000 --- a/spec/ruby/core/enumerator/yielder/initialize_spec.rb +++ /dev/null @@ -1,18 +0,0 @@ -# -*- encoding: us-ascii -*- - -require_relative '../../../spec_helper' - -describe "Enumerator::Yielder#initialize" do - before :each do - @class = Enumerator::Yielder - @uninitialized = @class.allocate - end - - it "is a private method" do - @class.private_instance_methods(false).should.include?(:initialize) - end - - it "returns self when given a block" do - @uninitialized.send(:initialize) {}.should.equal?(@uninitialized) - end -end diff --git a/spec/ruby/core/enumerator/yielder/to_proc_spec.rb b/spec/ruby/core/enumerator/yielder/to_proc_spec.rb deleted file mode 100644 index 1d3681ab503277..00000000000000 --- a/spec/ruby/core/enumerator/yielder/to_proc_spec.rb +++ /dev/null @@ -1,16 +0,0 @@ -require_relative '../../../spec_helper' - -describe "Enumerator::Yielder#to_proc" do - it "returns a Proc object that takes an argument and yields it to the block" do - ScratchPad.record [] - y = Enumerator::Yielder.new { |*args| ScratchPad << args; "foobar" } - - callable = y.to_proc - callable.class.should == Proc - - result = callable.call(1, 2) - ScratchPad.recorded.should == [[1, 2]] - - result.should == "foobar" - end -end diff --git a/spec/ruby/core/enumerator/yielder/yield_spec.rb b/spec/ruby/core/enumerator/yielder/yield_spec.rb deleted file mode 100644 index acfdf114b68d9e..00000000000000 --- a/spec/ruby/core/enumerator/yielder/yield_spec.rb +++ /dev/null @@ -1,33 +0,0 @@ -require_relative '../../../spec_helper' - -describe "Enumerator::Yielder#yield" do - it "yields the value to the block" do - ary = [] - y = Enumerator::Yielder.new {|x| ary << x} - y.yield 1 - - ary.should == [1] - end - - it "yields with passed arguments" do - yields = [] - y = Enumerator::Yielder.new {|*args| yields << args } - y.yield 1, 2 - yields.should == [[1, 2]] - end - - it "returns the result of the block for the given value" do - y = Enumerator::Yielder.new {|x| x + 1} - y.yield(1).should == 2 - end - - context "when multiple arguments passed" do - it "yields the arguments list to the block" do - ary = [] - y = Enumerator::Yielder.new { |*x| ary << x } - y.yield(1, 2) - - ary.should == [[1, 2]] - end - end -end diff --git a/spec/ruby/core/hash/each_pair_spec.rb b/spec/ruby/core/hash/each_pair_spec.rb index eb6656681d238d..01810c130c71d4 100644 --- a/spec/ruby/core/hash/each_pair_spec.rb +++ b/spec/ruby/core/hash/each_pair_spec.rb @@ -1,11 +1,111 @@ require_relative '../../spec_helper' -require_relative 'fixtures/classes' require_relative 'shared/iteration' -require_relative 'shared/each' require_relative '../enumerable/shared/enumeratorized' describe "Hash#each_pair" do - it_behaves_like :hash_each, :each_pair it_behaves_like :hash_iteration_no_block, :each_pair it_behaves_like :enumeratorized_with_origin_size, :each_pair, { 1 => 2, 3 => 4, 5 => 6 } + + # This is inconsistent with below, MRI checks the block arity in rb_hash_each_pair() + it "yields a [[key, value]] Array for each pair to a block expecting |*args|" do + all_args = [] + { 1 => 2, 3 => 4 }.each_pair { |*args| all_args << args } + all_args.sort.should == [[[1, 2]], [[3, 4]]] + end + + it "yields the key and value of each pair to a block expecting |key, value|" do + r = {} + h = { a: 1, b: 2, c: 3, d: 5 } + h.each_pair { |k,v| r[k.to_s] = v.to_s }.should.equal?(h) + r.should == { "a" => "1", "b" => "2", "c" => "3", "d" => "5" } + end + + it "yields the key only to a block expecting |key,|" do + ary = [] + h = { "a" => 1, "b" => 2, "c" => 3 } + h.each_pair { |k,| ary << k } + ary.sort.should == ["a", "b", "c"] + end + + it "always yields an Array of 2 elements, even when given a callable of arity 2" do + obj = Object.new + def obj.foo(key, value) + end + + -> { + { "a" => 1 }.each_pair(&obj.method(:foo)) + }.should.raise(ArgumentError) + + -> { + { "a" => 1 }.each_pair(&-> key, value { }) + }.should.raise(ArgumentError) + end + + it "yields an Array of 2 elements when given a callable of arity 1" do + obj = Object.new + def obj.foo(key_value) + ScratchPad << key_value + end + + ScratchPad.record([]) + { "a" => 1 }.each_pair(&obj.method(:foo)) + ScratchPad.recorded.should == [["a", 1]] + end + + it "raises an error for a Hash when an arity enforcing callable of arity >2 is passed in" do + obj = Object.new + def obj.foo(key, value, extra) + end + + -> { + { "a" => 1 }.each_pair(&obj.method(:foo)) + }.should.raise(ArgumentError) + end + + it "uses the same order as keys() and values()" do + h = { a: 1, b: 2, c: 3, d: 5 } + keys = [] + values = [] + + h.each_pair do |k, v| + keys << k + values << v + end + + keys.should == h.keys + values.should == h.values + end + + # Confirming the argument-splatting works from child class for both k, v and [k, v] + it "properly expands (or not) child class's 'each'-yielded args" do + cls1 = Class.new(Hash) do + attr_accessor :k_v + def each + super do |k, v| + @k_v = [k, v] + yield k, v + end + end + end + + cls2 = Class.new(Hash) do + attr_accessor :k_v + def each + super do |k, v| + @k_v = [k, v] + yield([k, v]) + end + end + end + + obj1 = cls1.new + obj1['a'] = 'b' + obj1.map {|k, v| [k, v]}.should == [['a', 'b']] + obj1.k_v.should == ['a', 'b'] + + obj2 = cls2.new + obj2['a'] = 'b' + obj2.map {|k, v| [k, v]}.should == [['a', 'b']] + obj2.k_v.should == ['a', 'b'] + end end diff --git a/spec/ruby/core/hash/each_spec.rb b/spec/ruby/core/hash/each_spec.rb index f0de0bdee56107..1644b63216abef 100644 --- a/spec/ruby/core/hash/each_spec.rb +++ b/spec/ruby/core/hash/each_spec.rb @@ -1,11 +1,7 @@ require_relative '../../spec_helper' -require_relative 'fixtures/classes' -require_relative 'shared/iteration' -require_relative 'shared/each' -require_relative '../enumerable/shared/enumeratorized' describe "Hash#each" do - it_behaves_like :hash_each, :each - it_behaves_like :hash_iteration_no_block, :each - it_behaves_like :enumeratorized_with_origin_size, :each, { 1 => 2, 3 => 4, 5 => 6 } + it "is an alias of Hash#each_pair" do + Hash.instance_method(:each).should == Hash.instance_method(:each_pair) + end end diff --git a/spec/ruby/core/hash/element_set_spec.rb b/spec/ruby/core/hash/element_set_spec.rb index 67c5a04d733767..bb805477cadbf0 100644 --- a/spec/ruby/core/hash/element_set_spec.rb +++ b/spec/ruby/core/hash/element_set_spec.rb @@ -1,7 +1,121 @@ require_relative '../../spec_helper' require_relative 'fixtures/classes' -require_relative 'shared/store' describe "Hash#[]=" do - it_behaves_like :hash_store, :[]= + it "associates the key with the value" do + h = { a: 1 } + h[:b] = 2 + h.should == { b:2, a:1 } + end + + it "returns the assigned value" do + h = {} + h.send(:[]=, 1, 2).should == 2 + end + + it "duplicates string keys using dup semantics" do + # dup doesn't copy singleton methods + key = +"foo" + def key.reverse() "bar" end + h = {} + h[key] = 0 + h.keys[0].reverse.should == "oof" + end + + it "stores unequal keys that hash to the same value" do + h = {} + k1 = ["x"] + k2 = ["y"] + # So they end up in the same bucket + k1.should_receive(:hash).and_return(0) + k2.should_receive(:hash).and_return(0) + + h[k1] = 1 + h[k2] = 2 + h.size.should == 2 + end + + it "accepts keys with private #hash method" do + key = HashSpecs::KeyWithPrivateHash.new + h = {} + h[key] = "foo" + h[key].should == "foo" + end + + it " accepts keys with an Integer hash" do + o = mock(hash: 1 << 100) + h = {} + h[o] = 1 + h[o].should == 1 + end + + it "duplicates and freezes string keys" do + key = +"foo" + h = {} + h[key] = 0 + key << "bar" + + h.should == { "foo" => 0 } + h.keys[0].should.frozen? + end + + it "doesn't duplicate and freeze already frozen string keys" do + key = "foo".freeze + h = {} + h[key] = 0 + h.keys[0].should.equal?(key) + end + + it "keeps the existing key in the hash if there is a matching one" do + h = { "a" => 1, "b" => 2, "c" => 3, "d" => 4 } + key1 = HashSpecs::ByValueKey.new(13) + key2 = HashSpecs::ByValueKey.new(13) + h[key1] = 41 + key_in_hash = h.keys.last + key_in_hash.should.equal?(key1) + h[key2] = 42 + last_key = h.keys.last + last_key.should.equal?(key_in_hash) + last_key.should_not.equal?(key2) + end + + it "keeps the existing String key in the hash if there is a matching one" do + h = { "a" => 1, "b" => 2, "c" => 3, "d" => 4 } + key1 = "foo".dup + key2 = "foo".dup + key1.should_not.equal?(key2) + h[key1] = 41 + frozen_key = h.keys.last + frozen_key.should_not.equal?(key1) + h[key2] = 42 + h.keys.last.should.equal?(frozen_key) + h.keys.last.should_not.equal?(key2) + end + + it "raises a FrozenError if called on a frozen instance" do + -> { HashSpecs.frozen_hash[1] = 2 }.should.raise(FrozenError) + end + + it "does not raise an exception if changing the value of an existing key during iteration" do + hash = {1 => 2, 3 => 4, 5 => 6} + hash.each { hash[1] = :foo } + hash.should == {1 => :foo, 3 => 4, 5 => 6} + end + + it "does not dispatch to hash for Boolean, Integer, Float, String, or Symbol" do + code = <<-EOC + load '#{fixture __FILE__, "name.rb"}' + hash = {} + [true, false, 1, 2.0, "hello", :ok].each do |value| + hash[value] = 42 + raise "incorrect value" unless hash[value] == 42 + hash[value] = 43 + raise "incorrect value" unless hash[value] == 43 + end + puts "OK" + puts hash.size + EOC + result = ruby_exe(code, args: "2>&1") + result.should == "OK\n6\n" + end end diff --git a/spec/ruby/core/hash/filter_spec.rb b/spec/ruby/core/hash/filter_spec.rb index 7dabe44984271b..625a7feb901cc2 100644 --- a/spec/ruby/core/hash/filter_spec.rb +++ b/spec/ruby/core/hash/filter_spec.rb @@ -1,10 +1,13 @@ require_relative '../../spec_helper' -require_relative 'shared/select' describe "Hash#filter" do - it_behaves_like :hash_select, :filter + it "is an alias of Hash#select" do + Hash.instance_method(:filter).should == Hash.instance_method(:select) + end end describe "Hash#filter!" do - it_behaves_like :hash_select!, :filter! + it "is an alias of Hash#select!" do + Hash.instance_method(:filter!).should == Hash.instance_method(:select!) + end end diff --git a/spec/ruby/core/hash/has_key_spec.rb b/spec/ruby/core/hash/has_key_spec.rb index 4af53579e57b82..9a6244c6f68d2f 100644 --- a/spec/ruby/core/hash/has_key_spec.rb +++ b/spec/ruby/core/hash/has_key_spec.rb @@ -1,7 +1,7 @@ require_relative '../../spec_helper' -require_relative 'fixtures/classes' -require_relative 'shared/key' describe "Hash#has_key?" do - it_behaves_like :hash_key_p, :has_key? + it "is an alias of Hash#include?" do + Hash.instance_method(:has_key?).should == Hash.instance_method(:include?) + end end diff --git a/spec/ruby/core/hash/has_value_spec.rb b/spec/ruby/core/hash/has_value_spec.rb index 39f1627fd3e160..d40e52ebfd78f0 100644 --- a/spec/ruby/core/hash/has_value_spec.rb +++ b/spec/ruby/core/hash/has_value_spec.rb @@ -1,7 +1,16 @@ require_relative '../../spec_helper' -require_relative 'fixtures/classes' -require_relative 'shared/value' describe "Hash#has_value?" do - it_behaves_like :hash_value_p, :has_value? + it "returns true if the value exists in the hash" do + { a: :b }.has_value?(:a).should == false + { 1 => 2 }.has_value?(2).should == true + h = Hash.new(5) + h.has_value?(5).should == false + h = Hash.new { 5 } + h.has_value?(5).should == false + end + + it "uses == semantics for comparing values" do + { 5 => 2.0 }.has_value?(2).should == true + end end diff --git a/spec/ruby/core/hash/include_spec.rb b/spec/ruby/core/hash/include_spec.rb index f3959dc5891233..1c32e9fb239369 100644 --- a/spec/ruby/core/hash/include_spec.rb +++ b/spec/ruby/core/hash/include_spec.rb @@ -1,7 +1,40 @@ require_relative '../../spec_helper' -require_relative 'fixtures/classes' -require_relative 'shared/key' describe "Hash#include?" do - it_behaves_like :hash_key_p, :include? + it "returns true if argument is a key" do + h = { a: 1, b: 2, c: 3, 4 => 0 } + h.include?(:a).should == true + h.include?(:b).should == true + h.include?(2).should == false + h.include?(4).should == true + + not_supported_on :opal do + h.include?('b').should == false + h.include?(4.0).should == false + end + end + + it "returns true if the key's matching value was nil" do + { xyz: nil }.include?(:xyz).should == true + end + + it "returns true if the key's matching value was false" do + { xyz: false }.include?(:xyz).should == true + end + + it "returns true if the key is nil" do + { nil => 'b' }.include?(nil).should == true + { nil => nil }.include?(nil).should == true + end + + it "compares keys with the same #hash value via #eql?" do + x = mock('x') + x.stub!(:hash).and_return(42) + + y = mock('y') + y.stub!(:hash).and_return(42) + y.should_receive(:eql?).and_return(false) + + { x => nil }.include?(y).should == false + end end diff --git a/spec/ruby/core/hash/inspect_spec.rb b/spec/ruby/core/hash/inspect_spec.rb index f41ebb70a6fd8c..359b13360f375c 100644 --- a/spec/ruby/core/hash/inspect_spec.rb +++ b/spec/ruby/core/hash/inspect_spec.rb @@ -1,7 +1,123 @@ require_relative '../../spec_helper' -require_relative 'fixtures/classes' -require_relative 'shared/to_s' describe "Hash#inspect" do - it_behaves_like :hash_to_s, :inspect + it "returns a string representation with same order as each()" do + h = { a: [1, 2], b: -2, d: -6, nil => nil } + expected = ruby_version_is("3.4") ? "{a: [1, 2], b: -2, d: -6, nil => nil}" : "{:a=>[1, 2], :b=>-2, :d=>-6, nil=>nil}" + h.inspect.should == expected + end + + it "calls #inspect on keys and values" do + key = mock('key') + val = mock('val') + key.should_receive(:inspect).and_return('key') + val.should_receive(:inspect).and_return('val') + expected = ruby_version_is("3.4") ? "{key => val}" : "{key=>val}" + { key => val }.inspect.should == expected + end + + it "does not call #to_s on a String returned from #inspect" do + str = +"abc" + str.should_not_receive(:to_s) + expected = ruby_version_is("3.4") ? '{a: "abc"}' : '{:a=>"abc"}' + { a: str }.inspect.should == expected + end + + it "calls #to_s on the object returned from #inspect if the Object isn't a String" do + obj = mock("Hash#inspect/to_s calls #to_s") + obj.should_receive(:inspect).and_return(obj) + obj.should_receive(:to_s).and_return("abc") + expected = ruby_version_is("3.4") ? "{a: abc}" : "{:a=>abc}" + { a: obj }.inspect.should == expected + end + + it "does not call #to_str on the object returned from #inspect when it is not a String" do + obj = mock("Hash#inspect/to_s does not call #to_str") + obj.should_receive(:inspect).and_return(obj) + obj.should_not_receive(:to_str) + expected_pattern = ruby_version_is("3.4") ? /^\{a: #\}$/ : /^\{:a=>#\}$/ + { a: obj }.inspect.should =~ expected_pattern + end + + it "does not call #to_str on the object returned from #to_s when it is not a String" do + obj = mock("Hash#inspect/to_s does not call #to_str on #to_s result") + obj.should_receive(:inspect).and_return(obj) + obj.should_receive(:to_s).and_return(obj) + obj.should_not_receive(:to_str) + expected_pattern = ruby_version_is("3.4") ? /^\{a: #\}$/ : /^\{:a=>#\}$/ + { a: obj }.inspect.should =~ expected_pattern + end + + it "does not swallow exceptions raised by #to_s" do + obj = mock("Hash#inspect/to_s does not swallow #to_s exceptions") + obj.should_receive(:inspect).and_return(obj) + obj.should_receive(:to_s).and_raise(Exception) + + -> { { a: obj }.inspect }.should.raise(Exception) + end + + it "handles hashes with recursive values" do + x = {} + x[0] = x + expected = ruby_version_is("3.4") ? '{0 => {...}}' : '{0=>{...}}' + x.inspect.should == expected + + x = {} + y = {} + x[0] = y + y[1] = x + expected_x = ruby_version_is("3.4") ? '{0 => {1 => {...}}}' : '{0=>{1=>{...}}}' + expected_y = ruby_version_is("3.4") ? '{1 => {0 => {...}}}' : '{1=>{0=>{...}}}' + x.inspect.should == expected_x + y.inspect.should == expected_y + end + + it "does not raise if inspected result is not default external encoding" do + utf_16be = mock("utf_16be") + utf_16be.should_receive(:inspect).and_return(%<"utf_16be \u3042">.encode(Encoding::UTF_16BE)) + expected = ruby_version_is("3.4") ? '{a: "utf_16be \u3042"}' : '{:a=>"utf_16be \u3042"}' + {a: utf_16be}.inspect.should == expected + end + + it "works for keys and values whose #inspect return a frozen String" do + expected = ruby_version_is("3.4") ? "{true => false}" : "{true=>false}" + { true => false }.inspect.should == expected + end + + ruby_version_is "3.4" do + it "adds quotes to symbol keys that are not valid symbol literals" do + { "needs-quotes": 1 }.inspect.should == '{"needs-quotes": 1}' + end + + it "can be evaled" do + no_quote = '{a: 1, a!: 1, a?: 1}' + eval(no_quote).inspect.should == no_quote + [ + '{"": 1}', + '{"0": 1, "!": 1, "%": 1, "&": 1, "*": 1, "+": 1, "-": 1, "/": 1, "<": 1, ">": 1, "^": 1, "`": 1, "|": 1, "~": 1}', + '{"@a": 1, "$a": 1, "+@": 1, "a=": 1, "[]": 1}', + '{"a\"b": 1, "@@a": 1, "<=>": 1, "===": 1, "[]=": 1}', + ].each do |quote| + eval(quote).inspect.should == quote + end + end + + it "can be evaled when Encoding.default_external is changed" do + external = Encoding.default_external + + Encoding.default_external = Encoding::ASCII + utf8_ascii_hash = '{"\\u3042": 1}' + eval(utf8_ascii_hash).inspect.should == utf8_ascii_hash + + Encoding.default_external = Encoding::UTF_8 + utf8_hash = "{\u3042: 1}" + eval(utf8_hash).inspect.should == utf8_hash + + Encoding.default_external = Encoding::Windows_31J + sjis_hash = "{\x87]: 1}".dup.force_encoding('sjis') + eval(sjis_hash).inspect.should == sjis_hash + ensure + Encoding.default_external = external + end + end end diff --git a/spec/ruby/core/hash/key_spec.rb b/spec/ruby/core/hash/key_spec.rb index 73eecbc98e02f9..c2d7049aed8f3d 100644 --- a/spec/ruby/core/hash/key_spec.rb +++ b/spec/ruby/core/hash/key_spec.rb @@ -1,12 +1,32 @@ require_relative '../../spec_helper' -require_relative 'fixtures/classes' -require_relative 'shared/key' -require_relative 'shared/index' describe "Hash#key?" do - it_behaves_like :hash_key_p, :key? + it "is an alias of Hash#include?" do + Hash.instance_method(:key?).should == Hash.instance_method(:include?) + end end describe "Hash#key" do - it_behaves_like :hash_index, :key + it "returns the corresponding key for value" do + { 2 => 'a', 1 => 'b' }.key('b').should == 1 + end + + it "returns nil if the value is not found" do + { a: -1, b: 3.14, c: 2.718 }.key(1).should == nil + end + + it "doesn't return default value if the value is not found" do + Hash.new(5).key(5).should == nil + end + + it "compares values using ==" do + { 1 => 0 }.key(0.0).should == 1 + { 1 => 0.0 }.key(0).should == 1 + + needle = mock('needle') + inhash = mock('inhash') + inhash.should_receive(:==).with(needle).and_return(true) + + { 1 => inhash }.key(needle).should == 1 + end end diff --git a/spec/ruby/core/hash/length_spec.rb b/spec/ruby/core/hash/length_spec.rb index d0af0945df053c..325973306f1d4f 100644 --- a/spec/ruby/core/hash/length_spec.rb +++ b/spec/ruby/core/hash/length_spec.rb @@ -1,7 +1,7 @@ require_relative '../../spec_helper' -require_relative 'fixtures/classes' -require_relative 'shared/length' describe "Hash#length" do - it_behaves_like :hash_length, :length + it "is an alias of Hash#size" do + Hash.instance_method(:size).should == Hash.instance_method(:length) + end end diff --git a/spec/ruby/core/hash/member_spec.rb b/spec/ruby/core/hash/member_spec.rb index 37c04145596419..e7309c3f7d1600 100644 --- a/spec/ruby/core/hash/member_spec.rb +++ b/spec/ruby/core/hash/member_spec.rb @@ -1,7 +1,7 @@ require_relative '../../spec_helper' -require_relative 'fixtures/classes' -require_relative 'shared/key' describe "Hash#member?" do - it_behaves_like :hash_key_p, :member? + it "is an alias of Hash#include?" do + Hash.instance_method(:member?).should == Hash.instance_method(:include?) + end end diff --git a/spec/ruby/core/hash/merge_spec.rb b/spec/ruby/core/hash/merge_spec.rb index 5fb278ad471809..9e566fcee92b11 100644 --- a/spec/ruby/core/hash/merge_spec.rb +++ b/spec/ruby/core/hash/merge_spec.rb @@ -1,7 +1,5 @@ require_relative '../../spec_helper' require_relative 'fixtures/classes' -require_relative 'shared/iteration' -require_relative 'shared/update' describe "Hash#merge" do it "returns a new hash by combining self with the contents of other" do @@ -119,5 +117,7 @@ end describe "Hash#merge!" do - it_behaves_like :hash_update, :merge! + it "is an alias of Hash#update" do + Hash.instance_method(:merge!).should == Hash.instance_method(:update) + end end diff --git a/spec/ruby/core/hash/select_spec.rb b/spec/ruby/core/hash/select_spec.rb index 38b0180b0e6e78..60b2ce67c16f99 100644 --- a/spec/ruby/core/hash/select_spec.rb +++ b/spec/ruby/core/hash/select_spec.rb @@ -1,10 +1,112 @@ require_relative '../../spec_helper' -require_relative 'shared/select' +require_relative '../enumerable/shared/enumeratorized' +require_relative 'fixtures/classes' +require_relative 'shared/iteration' describe "Hash#select" do - it_behaves_like :hash_select, :select + before :each do + @hsh = { 1 => 2, 3 => 4, 5 => 6 } + @empty = {} + end + + it "yields two arguments: key and value" do + all_args = [] + { 1 => 2, 3 => 4 }.select { |*args| all_args << args } + all_args.sort.should == [[1, 2], [3, 4]] + end + + it "returns a Hash of entries for which block is true" do + a_pairs = { 'a' => 9, 'c' => 4, 'b' => 5, 'd' => 2 }.select { |k,v| v % 2 == 0 } + a_pairs.should.instance_of?(Hash) + a_pairs.sort.should == [['c', 4], ['d', 2]] + end + + it "processes entries with the same order as reject" do + h = { a: 9, c: 4, b: 5, d: 2 } + + select_pairs = [] + reject_pairs = [] + h.dup.select{ |*pair| select_pairs << pair } + h.reject { |*pair| reject_pairs << pair } + + select_pairs.should == reject_pairs + end + + it "returns an Enumerator when called on a non-empty hash without a block" do + @hsh.select.should.instance_of?(Enumerator) + end + + it "returns an Enumerator when called on an empty hash without a block" do + @empty.select.should.instance_of?(Enumerator) + end + + it "does not retain the default value" do + h = Hash.new(1) + h.select { true }.default.should == nil + h[:a] = 1 + h.select { true }.default.should == nil + end + + it "does not retain the default_proc" do + pr = proc { |h, k| h[k] = [] } + h = Hash.new(&pr) + h.select { true }.default_proc.should == nil + h[:a] = 1 + h.select { true }.default_proc.should == nil + end + + it "retains compare_by_identity flag" do + h = { a: 9, c: 4 }.compare_by_identity + h2 = h.select { |k, _| k == :a } + h2.compare_by_identity?.should == true + end + + it_behaves_like :hash_iteration_no_block, :select + + before :each do + @object = { 1 => 2, 3 => 4, 5 => 6 } + end + it_behaves_like :enumeratorized_with_origin_size, :select end describe "Hash#select!" do - it_behaves_like :hash_select!, :select! + before :each do + @hsh = { 1 => 2, 3 => 4, 5 => 6 } + @empty = {} + end + + it "is equivalent to keep_if if changes are made" do + h = { a: 2 } + h.select! { |k,v| v <= 1 }.should.equal? h + + h = { 1 => 2, 3 => 4 } + all_args_select = [] + h.dup.select! { |*args| all_args_select << args } + all_args_select.should == [[1, 2], [3, 4]] + end + + it "removes all entries if the block is false" do + h = { a: 1, b: 2, c: 3 } + h.select! { |k,v| false }.should.equal?(h) + h.should == {} + end + + it "returns nil if no changes were made" do + { a: 1 }.select! { |k,v| v <= 1 }.should == nil + end + + it "raises a FrozenError if called on an empty frozen instance" do + -> { HashSpecs.empty_frozen_hash.select! { false } }.should.raise(FrozenError) + end + + it "raises a FrozenError if called on a frozen instance that would not be modified" do + -> { HashSpecs.frozen_hash.select! { true } }.should.raise(FrozenError) + end + + it_behaves_like :hash_iteration_no_block, :select! + + before :each do + @object = { 1 => 2, 3 => 4, 5 => 6 } + end + it_behaves_like :enumeratorized_with_origin_size, :select! end diff --git a/spec/ruby/core/hash/shared/each.rb b/spec/ruby/core/hash/shared/each.rb deleted file mode 100644 index 657c5d2c529da7..00000000000000 --- a/spec/ruby/core/hash/shared/each.rb +++ /dev/null @@ -1,105 +0,0 @@ -describe :hash_each, shared: true do - - # This is inconsistent with below, MRI checks the block arity in rb_hash_each_pair() - it "yields a [[key, value]] Array for each pair to a block expecting |*args|" do - all_args = [] - { 1 => 2, 3 => 4 }.send(@method) { |*args| all_args << args } - all_args.sort.should == [[[1, 2]], [[3, 4]]] - end - - it "yields the key and value of each pair to a block expecting |key, value|" do - r = {} - h = { a: 1, b: 2, c: 3, d: 5 } - h.send(@method) { |k,v| r[k.to_s] = v.to_s }.should.equal?(h) - r.should == { "a" => "1", "b" => "2", "c" => "3", "d" => "5" } - end - - it "yields the key only to a block expecting |key,|" do - ary = [] - h = { "a" => 1, "b" => 2, "c" => 3 } - h.send(@method) { |k,| ary << k } - ary.sort.should == ["a", "b", "c"] - end - - it "always yields an Array of 2 elements, even when given a callable of arity 2" do - obj = Object.new - def obj.foo(key, value) - end - - -> { - { "a" => 1 }.send(@method, &obj.method(:foo)) - }.should.raise(ArgumentError) - - -> { - { "a" => 1 }.send(@method, &-> key, value { }) - }.should.raise(ArgumentError) - end - - it "yields an Array of 2 elements when given a callable of arity 1" do - obj = Object.new - def obj.foo(key_value) - ScratchPad << key_value - end - - ScratchPad.record([]) - { "a" => 1 }.send(@method, &obj.method(:foo)) - ScratchPad.recorded.should == [["a", 1]] - end - - it "raises an error for a Hash when an arity enforcing callable of arity >2 is passed in" do - obj = Object.new - def obj.foo(key, value, extra) - end - - -> { - { "a" => 1 }.send(@method, &obj.method(:foo)) - }.should.raise(ArgumentError) - end - - it "uses the same order as keys() and values()" do - h = { a: 1, b: 2, c: 3, d: 5 } - keys = [] - values = [] - - h.send(@method) do |k, v| - keys << k - values << v - end - - keys.should == h.keys - values.should == h.values - end - - # Confirming the argument-splatting works from child class for both k, v and [k, v] - it "properly expands (or not) child class's 'each'-yielded args" do - cls1 = Class.new(Hash) do - attr_accessor :k_v - def each - super do |k, v| - @k_v = [k, v] - yield k, v - end - end - end - - cls2 = Class.new(Hash) do - attr_accessor :k_v - def each - super do |k, v| - @k_v = [k, v] - yield([k, v]) - end - end - end - - obj1 = cls1.new - obj1['a'] = 'b' - obj1.map {|k, v| [k, v]}.should == [['a', 'b']] - obj1.k_v.should == ['a', 'b'] - - obj2 = cls2.new - obj2['a'] = 'b' - obj2.map {|k, v| [k, v]}.should == [['a', 'b']] - obj2.k_v.should == ['a', 'b'] - end -end diff --git a/spec/ruby/core/hash/shared/index.rb b/spec/ruby/core/hash/shared/index.rb deleted file mode 100644 index dd4e89a9b4752d..00000000000000 --- a/spec/ruby/core/hash/shared/index.rb +++ /dev/null @@ -1,37 +0,0 @@ -require_relative '../../../spec_helper' -require_relative '../fixtures/classes' - -describe :hash_index, shared: true do - it "returns the corresponding key for value" do - suppress_warning do # for Hash#index - { 2 => 'a', 1 => 'b' }.send(@method, 'b').should == 1 - end - end - - it "returns nil if the value is not found" do - suppress_warning do # for Hash#index - { a: -1, b: 3.14, c: 2.718 }.send(@method, 1).should == nil - end - end - - it "doesn't return default value if the value is not found" do - suppress_warning do # for Hash#index - Hash.new(5).send(@method, 5).should == nil - end - end - - it "compares values using ==" do - suppress_warning do # for Hash#index - { 1 => 0 }.send(@method, 0.0).should == 1 - { 1 => 0.0 }.send(@method, 0).should == 1 - end - - needle = mock('needle') - inhash = mock('inhash') - inhash.should_receive(:==).with(needle).and_return(true) - - suppress_warning do # for Hash#index - { 1 => inhash }.send(@method, needle).should == 1 - end - end -end diff --git a/spec/ruby/core/hash/shared/key.rb b/spec/ruby/core/hash/shared/key.rb deleted file mode 100644 index 17f9f814573d04..00000000000000 --- a/spec/ruby/core/hash/shared/key.rb +++ /dev/null @@ -1,38 +0,0 @@ -describe :hash_key_p, shared: true do - it "returns true if argument is a key" do - h = { a: 1, b: 2, c: 3, 4 => 0 } - h.send(@method, :a).should == true - h.send(@method, :b).should == true - h.send(@method, 2).should == false - h.send(@method, 4).should == true - - not_supported_on :opal do - h.send(@method, 'b').should == false - h.send(@method, 4.0).should == false - end - end - - it "returns true if the key's matching value was nil" do - { xyz: nil }.send(@method, :xyz).should == true - end - - it "returns true if the key's matching value was false" do - { xyz: false }.send(@method, :xyz).should == true - end - - it "returns true if the key is nil" do - { nil => 'b' }.send(@method, nil).should == true - { nil => nil }.send(@method, nil).should == true - end - - it "compares keys with the same #hash value via #eql?" do - x = mock('x') - x.stub!(:hash).and_return(42) - - y = mock('y') - y.stub!(:hash).and_return(42) - y.should_receive(:eql?).and_return(false) - - { x => nil }.send(@method, y).should == false - end -end diff --git a/spec/ruby/core/hash/shared/length.rb b/spec/ruby/core/hash/shared/length.rb deleted file mode 100644 index 24f5563759091f..00000000000000 --- a/spec/ruby/core/hash/shared/length.rb +++ /dev/null @@ -1,12 +0,0 @@ -describe :hash_length, shared: true do - it "returns the number of entries" do - { a: 1, b: 'c' }.send(@method).should == 2 - h = { a: 1, b: 2 } - h[:a] = 2 - h.send(@method).should == 2 - { a: 1, b: 1, c: 1 }.send(@method).should == 3 - {}.send(@method).should == 0 - Hash.new(5).send(@method).should == 0 - Hash.new { 5 }.send(@method).should == 0 - end -end diff --git a/spec/ruby/core/hash/shared/select.rb b/spec/ruby/core/hash/shared/select.rb deleted file mode 100644 index b4f6d1b3acb2d8..00000000000000 --- a/spec/ruby/core/hash/shared/select.rb +++ /dev/null @@ -1,112 +0,0 @@ -require_relative '../../../spec_helper' -require_relative '../fixtures/classes' -require_relative '../shared/iteration' -require_relative '../../enumerable/shared/enumeratorized' - -describe :hash_select, shared: true do - before :each do - @hsh = { 1 => 2, 3 => 4, 5 => 6 } - @empty = {} - end - - it "yields two arguments: key and value" do - all_args = [] - { 1 => 2, 3 => 4 }.send(@method) { |*args| all_args << args } - all_args.sort.should == [[1, 2], [3, 4]] - end - - it "returns a Hash of entries for which block is true" do - a_pairs = { 'a' => 9, 'c' => 4, 'b' => 5, 'd' => 2 }.send(@method) { |k,v| v % 2 == 0 } - a_pairs.should.instance_of?(Hash) - a_pairs.sort.should == [['c', 4], ['d', 2]] - end - - it "processes entries with the same order as reject" do - h = { a: 9, c: 4, b: 5, d: 2 } - - select_pairs = [] - reject_pairs = [] - h.dup.send(@method) { |*pair| select_pairs << pair } - h.reject { |*pair| reject_pairs << pair } - - select_pairs.should == reject_pairs - end - - it "returns an Enumerator when called on a non-empty hash without a block" do - @hsh.send(@method).should.instance_of?(Enumerator) - end - - it "returns an Enumerator when called on an empty hash without a block" do - @empty.send(@method).should.instance_of?(Enumerator) - end - - it "does not retain the default value" do - h = Hash.new(1) - h.send(@method) { true }.default.should == nil - h[:a] = 1 - h.send(@method) { true }.default.should == nil - end - - it "does not retain the default_proc" do - pr = proc { |h, k| h[k] = [] } - h = Hash.new(&pr) - h.send(@method) { true }.default_proc.should == nil - h[:a] = 1 - h.send(@method) { true }.default_proc.should == nil - end - - it "retains compare_by_identity flag" do - h = { a: 9, c: 4 }.compare_by_identity - h2 = h.send(@method) { |k, _| k == :a } - h2.compare_by_identity?.should == true - end - - it_should_behave_like :hash_iteration_no_block - - before :each do - @object = { 1 => 2, 3 => 4, 5 => 6 } - end - it_should_behave_like :enumeratorized_with_origin_size -end - -describe :hash_select!, shared: true do - before :each do - @hsh = { 1 => 2, 3 => 4, 5 => 6 } - @empty = {} - end - - it "is equivalent to keep_if if changes are made" do - h = { a: 2 } - h.send(@method) { |k,v| v <= 1 }.should.equal? h - - h = { 1 => 2, 3 => 4 } - all_args_select = [] - h.dup.send(@method) { |*args| all_args_select << args } - all_args_select.should == [[1, 2], [3, 4]] - end - - it "removes all entries if the block is false" do - h = { a: 1, b: 2, c: 3 } - h.send(@method) { |k,v| false }.should.equal?(h) - h.should == {} - end - - it "returns nil if no changes were made" do - { a: 1 }.send(@method) { |k,v| v <= 1 }.should == nil - end - - it "raises a FrozenError if called on an empty frozen instance" do - -> { HashSpecs.empty_frozen_hash.send(@method) { false } }.should.raise(FrozenError) - end - - it "raises a FrozenError if called on a frozen instance that would not be modified" do - -> { HashSpecs.frozen_hash.send(@method) { true } }.should.raise(FrozenError) - end - - it_should_behave_like :hash_iteration_no_block - - before :each do - @object = { 1 => 2, 3 => 4, 5 => 6 } - end - it_should_behave_like :enumeratorized_with_origin_size -end diff --git a/spec/ruby/core/hash/shared/store.rb b/spec/ruby/core/hash/shared/store.rb deleted file mode 100644 index 05ef63face67fe..00000000000000 --- a/spec/ruby/core/hash/shared/store.rb +++ /dev/null @@ -1,115 +0,0 @@ -require_relative '../fixtures/classes' - -describe :hash_store, shared: true do - it "associates the key with the value and return the value" do - h = { a: 1 } - h.send(@method, :b, 2).should == 2 - h.should == { b:2, a:1 } - end - - it "duplicates string keys using dup semantics" do - # dup doesn't copy singleton methods - key = +"foo" - def key.reverse() "bar" end - h = {} - h.send(@method, key, 0) - h.keys[0].reverse.should == "oof" - end - - it "stores unequal keys that hash to the same value" do - h = {} - k1 = ["x"] - k2 = ["y"] - # So they end up in the same bucket - k1.should_receive(:hash).and_return(0) - k2.should_receive(:hash).and_return(0) - - h.send(@method, k1, 1) - h.send(@method, k2, 2) - h.size.should == 2 - end - - it "accepts keys with private #hash method" do - key = HashSpecs::KeyWithPrivateHash.new - h = {} - h.send(@method, key, "foo") - h[key].should == "foo" - end - - it " accepts keys with an Integer hash" do - o = mock(hash: 1 << 100) - h = {} - h[o] = 1 - h[o].should == 1 - end - - it "duplicates and freezes string keys" do - key = +"foo" - h = {} - h.send(@method, key, 0) - key << "bar" - - h.should == { "foo" => 0 } - h.keys[0].should.frozen? - end - - it "doesn't duplicate and freeze already frozen string keys" do - key = "foo".freeze - h = {} - h.send(@method, key, 0) - h.keys[0].should.equal?(key) - end - - it "keeps the existing key in the hash if there is a matching one" do - h = { "a" => 1, "b" => 2, "c" => 3, "d" => 4 } - key1 = HashSpecs::ByValueKey.new(13) - key2 = HashSpecs::ByValueKey.new(13) - h[key1] = 41 - key_in_hash = h.keys.last - key_in_hash.should.equal?(key1) - h[key2] = 42 - last_key = h.keys.last - last_key.should.equal?(key_in_hash) - last_key.should_not.equal?(key2) - end - - it "keeps the existing String key in the hash if there is a matching one" do - h = { "a" => 1, "b" => 2, "c" => 3, "d" => 4 } - key1 = "foo".dup - key2 = "foo".dup - key1.should_not.equal?(key2) - h[key1] = 41 - frozen_key = h.keys.last - frozen_key.should_not.equal?(key1) - h[key2] = 42 - h.keys.last.should.equal?(frozen_key) - h.keys.last.should_not.equal?(key2) - end - - it "raises a FrozenError if called on a frozen instance" do - -> { HashSpecs.frozen_hash.send(@method, 1, 2) }.should.raise(FrozenError) - end - - it "does not raise an exception if changing the value of an existing key during iteration" do - hash = {1 => 2, 3 => 4, 5 => 6} - hash.each { hash.send(@method, 1, :foo) } - hash.should == {1 => :foo, 3 => 4, 5 => 6} - end - - it "does not dispatch to hash for Boolean, Integer, Float, String, or Symbol" do - code = <<-EOC - load '#{fixture __FILE__, "name.rb"}' - hash = {} - [true, false, 1, 2.0, "hello", :ok].each do |value| - hash[value] = 42 - raise "incorrect value" unless hash[value] == 42 - hash[value] = 43 - raise "incorrect value" unless hash[value] == 43 - end - puts "OK" - puts hash.size - EOC - result = ruby_exe(code, args: "2>&1") - result.should == "OK\n6\n" - end -end diff --git a/spec/ruby/core/hash/shared/to_s.rb b/spec/ruby/core/hash/shared/to_s.rb deleted file mode 100644 index f88ca738a598e6..00000000000000 --- a/spec/ruby/core/hash/shared/to_s.rb +++ /dev/null @@ -1,124 +0,0 @@ -require_relative '../../../spec_helper' -require_relative '../fixtures/classes' - -describe :hash_to_s, shared: true do - it "returns a string representation with same order as each()" do - h = { a: [1, 2], b: -2, d: -6, nil => nil } - expected = ruby_version_is("3.4") ? "{a: [1, 2], b: -2, d: -6, nil => nil}" : "{:a=>[1, 2], :b=>-2, :d=>-6, nil=>nil}" - h.send(@method).should == expected - end - - it "calls #inspect on keys and values" do - key = mock('key') - val = mock('val') - key.should_receive(:inspect).and_return('key') - val.should_receive(:inspect).and_return('val') - expected = ruby_version_is("3.4") ? "{key => val}" : "{key=>val}" - { key => val }.send(@method).should == expected - end - - it "does not call #to_s on a String returned from #inspect" do - str = +"abc" - str.should_not_receive(:to_s) - expected = ruby_version_is("3.4") ? '{a: "abc"}' : '{:a=>"abc"}' - { a: str }.send(@method).should == expected - end - - it "calls #to_s on the object returned from #inspect if the Object isn't a String" do - obj = mock("Hash#inspect/to_s calls #to_s") - obj.should_receive(:inspect).and_return(obj) - obj.should_receive(:to_s).and_return("abc") - expected = ruby_version_is("3.4") ? "{a: abc}" : "{:a=>abc}" - { a: obj }.send(@method).should == expected - end - - it "does not call #to_str on the object returned from #inspect when it is not a String" do - obj = mock("Hash#inspect/to_s does not call #to_str") - obj.should_receive(:inspect).and_return(obj) - obj.should_not_receive(:to_str) - expected_pattern = ruby_version_is("3.4") ? /^\{a: #\}$/ : /^\{:a=>#\}$/ - { a: obj }.send(@method).should =~ expected_pattern - end - - it "does not call #to_str on the object returned from #to_s when it is not a String" do - obj = mock("Hash#inspect/to_s does not call #to_str on #to_s result") - obj.should_receive(:inspect).and_return(obj) - obj.should_receive(:to_s).and_return(obj) - obj.should_not_receive(:to_str) - expected_pattern = ruby_version_is("3.4") ? /^\{a: #\}$/ : /^\{:a=>#\}$/ - { a: obj }.send(@method).should =~ expected_pattern - end - - it "does not swallow exceptions raised by #to_s" do - obj = mock("Hash#inspect/to_s does not swallow #to_s exceptions") - obj.should_receive(:inspect).and_return(obj) - obj.should_receive(:to_s).and_raise(Exception) - - -> { { a: obj }.send(@method) }.should.raise(Exception) - end - - it "handles hashes with recursive values" do - x = {} - x[0] = x - expected = ruby_version_is("3.4") ? '{0 => {...}}' : '{0=>{...}}' - x.send(@method).should == expected - - x = {} - y = {} - x[0] = y - y[1] = x - expected_x = ruby_version_is("3.4") ? '{0 => {1 => {...}}}' : '{0=>{1=>{...}}}' - expected_y = ruby_version_is("3.4") ? '{1 => {0 => {...}}}' : '{1=>{0=>{...}}}' - x.send(@method).should == expected_x - y.send(@method).should == expected_y - end - - it "does not raise if inspected result is not default external encoding" do - utf_16be = mock("utf_16be") - utf_16be.should_receive(:inspect).and_return(%<"utf_16be \u3042">.encode(Encoding::UTF_16BE)) - expected = ruby_version_is("3.4") ? '{a: "utf_16be \u3042"}' : '{:a=>"utf_16be \u3042"}' - {a: utf_16be}.send(@method).should == expected - end - - it "works for keys and values whose #inspect return a frozen String" do - expected = ruby_version_is("3.4") ? "{true => false}" : "{true=>false}" - { true => false }.to_s.should == expected - end - - ruby_version_is "3.4" do - it "adds quotes to symbol keys that are not valid symbol literals" do - { "needs-quotes": 1 }.send(@method).should == '{"needs-quotes": 1}' - end - - it "can be evaled" do - no_quote = '{a: 1, a!: 1, a?: 1}' - eval(no_quote).inspect.should == no_quote - [ - '{"": 1}', - '{"0": 1, "!": 1, "%": 1, "&": 1, "*": 1, "+": 1, "-": 1, "/": 1, "<": 1, ">": 1, "^": 1, "`": 1, "|": 1, "~": 1}', - '{"@a": 1, "$a": 1, "+@": 1, "a=": 1, "[]": 1}', - '{"a\"b": 1, "@@a": 1, "<=>": 1, "===": 1, "[]=": 1}', - ].each do |quote| - eval(quote).inspect.should == quote - end - end - - it "can be evaled when Encoding.default_external is changed" do - external = Encoding.default_external - - Encoding.default_external = Encoding::ASCII - utf8_ascii_hash = '{"\\u3042": 1}' - eval(utf8_ascii_hash).inspect.should == utf8_ascii_hash - - Encoding.default_external = Encoding::UTF_8 - utf8_hash = "{\u3042: 1}" - eval(utf8_hash).inspect.should == utf8_hash - - Encoding.default_external = Encoding::Windows_31J - sjis_hash = "{\x87]: 1}".dup.force_encoding('sjis') - eval(sjis_hash).inspect.should == sjis_hash - ensure - Encoding.default_external = external - end - end -end diff --git a/spec/ruby/core/hash/shared/update.rb b/spec/ruby/core/hash/shared/update.rb deleted file mode 100644 index 6dbad1d6d01b2c..00000000000000 --- a/spec/ruby/core/hash/shared/update.rb +++ /dev/null @@ -1,76 +0,0 @@ -describe :hash_update, shared: true do - it "adds the entries from other, overwriting duplicate keys. Returns self" do - h = { _1: 'a', _2: '3' } - h.send(@method, _1: '9', _9: 2).should.equal?(h) - h.should == { _1: "9", _2: "3", _9: 2 } - end - - it "sets any duplicate key to the value of block if passed a block" do - h1 = { a: 2, b: -1 } - h2 = { a: -2, c: 1 } - h1.send(@method, h2) { |k,x,y| 3.14 }.should.equal?(h1) - h1.should == { c: 1, b: -1, a: 3.14 } - - h1.send(@method, h1) { nil } - h1.should == { a: nil, b: nil, c: nil } - end - - it "tries to convert the passed argument to a hash using #to_hash" do - obj = mock('{1=>2}') - obj.should_receive(:to_hash).and_return({ 1 => 2 }) - { 3 => 4 }.send(@method, obj).should == { 1 => 2, 3 => 4 } - end - - it "does not call to_hash on hash subclasses" do - { 3 => 4 }.send(@method, HashSpecs::ToHashHash[1 => 2]).should == { 1 => 2, 3 => 4 } - end - - it "processes entries with same order as merge()" do - h = { 1 => 2, 3 => 4, 5 => 6, "x" => nil, nil => 5, [] => [] } - merge_bang_pairs = [] - merge_pairs = [] - h.merge(h) { |*arg| merge_pairs << arg } - h.send(@method, h) { |*arg| merge_bang_pairs << arg } - merge_bang_pairs.should == merge_pairs - end - - it "raises a FrozenError on a frozen instance that is modified" do - -> do - HashSpecs.frozen_hash.send(@method, 1 => 2) - end.should.raise(FrozenError) - end - - it "checks frozen status before coercing an object with #to_hash" do - obj = mock("to_hash frozen") - # This is necessary because mock cleanup code cannot run on the frozen - # object. - def obj.to_hash() raise Exception, "should not receive #to_hash" end - obj.freeze - - -> { HashSpecs.frozen_hash.send(@method, obj) }.should.raise(FrozenError) - end - - # see redmine #1571 - it "raises a FrozenError on a frozen instance that would not be modified" do - -> do - HashSpecs.frozen_hash.send(@method, HashSpecs.empty_frozen_hash) - end.should.raise(FrozenError) - end - - it "does not raise an exception if changing the value of an existing key during iteration" do - hash = {1 => 2, 3 => 4, 5 => 6} - hash2 = {1 => :foo, 3 => :bar} - hash.each { hash.send(@method, hash2) } - hash.should == {1 => :foo, 3 => :bar, 5 => 6} - end - - it "accepts multiple hashes" do - result = { a: 1 }.send(@method, { b: 2 }, { c: 3 }, { d: 4 }) - result.should == { a: 1, b: 2, c: 3, d: 4 } - end - - it "accepts zero arguments" do - hash = { a: 1 } - hash.send(@method).should.eql?(hash) - end -end diff --git a/spec/ruby/core/hash/shared/value.rb b/spec/ruby/core/hash/shared/value.rb deleted file mode 100644 index aac76c253e55f6..00000000000000 --- a/spec/ruby/core/hash/shared/value.rb +++ /dev/null @@ -1,14 +0,0 @@ -describe :hash_value_p, shared: true do - it "returns true if the value exists in the hash" do - { a: :b }.send(@method, :a).should == false - { 1 => 2 }.send(@method, 2).should == true - h = Hash.new(5) - h.send(@method, 5).should == false - h = Hash.new { 5 } - h.send(@method, 5).should == false - end - - it "uses == semantics for comparing values" do - { 5 => 2.0 }.send(@method, 2).should == true - end -end diff --git a/spec/ruby/core/hash/shared/values_at.rb b/spec/ruby/core/hash/shared/values_at.rb deleted file mode 100644 index 4e4e60e7d6bca2..00000000000000 --- a/spec/ruby/core/hash/shared/values_at.rb +++ /dev/null @@ -1,9 +0,0 @@ -describe :hash_values_at, shared: true do - it "returns an array of values for the given keys" do - h = { a: 9, b: 'a', c: -10, d: nil } - h.send(@method).should.is_a?(Array) - h.send(@method).should == [] - h.send(@method, :a, :d, :b).should.is_a?(Array) - h.send(@method, :a, :d, :b).should == [9, nil, 'a'] - end -end diff --git a/spec/ruby/core/hash/size_spec.rb b/spec/ruby/core/hash/size_spec.rb index 1e8abd8d978107..5e5008a5dc7c38 100644 --- a/spec/ruby/core/hash/size_spec.rb +++ b/spec/ruby/core/hash/size_spec.rb @@ -1,7 +1,14 @@ require_relative '../../spec_helper' -require_relative 'fixtures/classes' -require_relative 'shared/length' describe "Hash#size" do - it_behaves_like :hash_length, :size + it "returns the number of entries" do + { a: 1, b: 'c' }.size.should == 2 + h = { a: 1, b: 2 } + h[:a] = 2 + h.size.should == 2 + { a: 1, b: 1, c: 1 }.size.should == 3 + {}.size.should == 0 + Hash.new(5).size.should == 0 + Hash.new { 5 }.size.should == 0 + end end diff --git a/spec/ruby/core/hash/store_spec.rb b/spec/ruby/core/hash/store_spec.rb index 7e975380ec0f2e..7017d8ba2b2ba3 100644 --- a/spec/ruby/core/hash/store_spec.rb +++ b/spec/ruby/core/hash/store_spec.rb @@ -1,7 +1,7 @@ require_relative '../../spec_helper' -require_relative 'fixtures/classes' -require_relative 'shared/store' describe "Hash#store" do - it_behaves_like :hash_store, :store + it "is an alias of Hash#[]=" do + Hash.instance_method(:store).should == Hash.instance_method(:[]=) + end end diff --git a/spec/ruby/core/hash/to_s_spec.rb b/spec/ruby/core/hash/to_s_spec.rb index e52b09962eb4fc..2915db6ef8757d 100644 --- a/spec/ruby/core/hash/to_s_spec.rb +++ b/spec/ruby/core/hash/to_s_spec.rb @@ -1,7 +1,7 @@ require_relative '../../spec_helper' -require_relative 'fixtures/classes' -require_relative 'shared/to_s' describe "Hash#to_s" do - it_behaves_like :hash_to_s, :to_s + it "is an alias of Hash#inspect" do + Hash.instance_method(:to_s).should == Hash.instance_method(:inspect) + end end diff --git a/spec/ruby/core/hash/update_spec.rb b/spec/ruby/core/hash/update_spec.rb index 0975045ad10b5f..f3a3e6b4db2931 100644 --- a/spec/ruby/core/hash/update_spec.rb +++ b/spec/ruby/core/hash/update_spec.rb @@ -1,7 +1,79 @@ require_relative '../../spec_helper' require_relative 'fixtures/classes' -require_relative 'shared/update' describe "Hash#update" do - it_behaves_like :hash_update, :update + it "adds the entries from other, overwriting duplicate keys. Returns self" do + h = { _1: 'a', _2: '3' } + h.update(_1: '9', _9: 2).should.equal?(h) + h.should == { _1: "9", _2: "3", _9: 2 } + end + + it "sets any duplicate key to the value of block if passed a block" do + h1 = { a: 2, b: -1 } + h2 = { a: -2, c: 1 } + h1.update(h2) { |k,x,y| 3.14 }.should.equal?(h1) + h1.should == { c: 1, b: -1, a: 3.14 } + + h1.update(h1) { nil } + h1.should == { a: nil, b: nil, c: nil } + end + + it "tries to convert the passed argument to a hash using #to_hash" do + obj = mock('{1=>2}') + obj.should_receive(:to_hash).and_return({ 1 => 2 }) + { 3 => 4 }.update(obj).should == { 1 => 2, 3 => 4 } + end + + it "does not call to_hash on hash subclasses" do + { 3 => 4 }.update(HashSpecs::ToHashHash[1 => 2]).should == { 1 => 2, 3 => 4 } + end + + it "processes entries with same order as merge()" do + h = { 1 => 2, 3 => 4, 5 => 6, "x" => nil, nil => 5, [] => [] } + merge_bang_pairs = [] + merge_pairs = [] + h.merge(h) { |*arg| merge_pairs << arg } + h.update(h) { |*arg| merge_bang_pairs << arg } + merge_bang_pairs.should == merge_pairs + end + + it "raises a FrozenError on a frozen instance that is modified" do + -> do + HashSpecs.frozen_hash.update(1 => 2) + end.should.raise(FrozenError) + end + + it "checks frozen status before coercing an object with #to_hash" do + obj = mock("to_hash frozen") + # This is necessary because mock cleanup code cannot run on the frozen + # object. + def obj.to_hash() raise Exception, "should not receive #to_hash" end + obj.freeze + + -> { HashSpecs.frozen_hash.update(obj) }.should.raise(FrozenError) + end + + # see redmine #1571 + it "raises a FrozenError on a frozen instance that would not be modified" do + -> do + HashSpecs.frozen_hash.update(HashSpecs.empty_frozen_hash) + end.should.raise(FrozenError) + end + + it "does not raise an exception if changing the value of an existing key during iteration" do + hash = {1 => 2, 3 => 4, 5 => 6} + hash2 = {1 => :foo, 3 => :bar} + hash.each { hash.update(hash2) } + hash.should == {1 => :foo, 3 => :bar, 5 => 6} + end + + it "accepts multiple hashes" do + result = { a: 1 }.update({ b: 2 }, { c: 3 }, { d: 4 }) + result.should == { a: 1, b: 2, c: 3, d: 4 } + end + + it "accepts zero arguments" do + hash = { a: 1 } + hash.update.should.eql?(hash) + end end diff --git a/spec/ruby/core/hash/value_spec.rb b/spec/ruby/core/hash/value_spec.rb index 0ab16a5d1b4e3d..9cfbe576d26a40 100644 --- a/spec/ruby/core/hash/value_spec.rb +++ b/spec/ruby/core/hash/value_spec.rb @@ -1,7 +1,7 @@ require_relative '../../spec_helper' -require_relative 'fixtures/classes' -require_relative 'shared/value' describe "Hash#value?" do - it_behaves_like :hash_value_p, :value? + it "is an alias of Hash#has_value?" do + Hash.instance_method(:value?).should == Hash.instance_method(:has_value?) + end end diff --git a/spec/ruby/core/hash/values_at_spec.rb b/spec/ruby/core/hash/values_at_spec.rb index b620a279ba6d27..78dcd8df6ac22d 100644 --- a/spec/ruby/core/hash/values_at_spec.rb +++ b/spec/ruby/core/hash/values_at_spec.rb @@ -1,7 +1,11 @@ require_relative '../../spec_helper' -require_relative 'fixtures/classes' -require_relative 'shared/values_at' describe "Hash#values_at" do - it_behaves_like :hash_values_at, :values_at + it "returns an array of values for the given keys" do + h = { a: 9, b: 'a', c: -10, d: nil } + h.values_at.should.is_a?(Array) + h.values_at.should == [] + h.values_at(:a, :d, :b).should.is_a?(Array) + h.values_at(:a, :d, :b).should == [9, nil, 'a'] + end end diff --git a/spec/ruby/core/io/buffer/for_spec.rb b/spec/ruby/core/io/buffer/for_spec.rb index 7971ce0b719ef1..4c614f74b05a6a 100644 --- a/spec/ruby/core/io/buffer/for_spec.rb +++ b/spec/ruby/core/io/buffer/for_spec.rb @@ -66,6 +66,7 @@ buffer.get_string.should == @string.b buffer.should_not.readonly? + buffer.should_not.locked? buffer.set_string("ghost shell") @string.should == "ghost shellg" diff --git a/spec/ruby/core/io/buffer/map_spec.rb b/spec/ruby/core/io/buffer/map_spec.rb index 23e837ce078dd1..4b28539ad82762 100644 --- a/spec/ruby/core/io/buffer/map_spec.rb +++ b/spec/ruby/core/io/buffer/map_spec.rb @@ -73,7 +73,7 @@ def open_big_file_fixture @buffer.should.valid? end - platform_is_not :windows, :openbsd do + guard -> { Process.respond_to?(:fork) } do it "is shareable across processes" do file_name = tmp("shared_buffer") @file = File.open(file_name, "w+") @@ -312,7 +312,7 @@ def open_big_file_fixture @file.read.should == "abcâdef\n".b end - platform_is_not :windows do + guard -> { Process.respond_to?(:fork) } do it "is not shared across processes" do file_name = tmp("shared_buffer") @file = File.open(file_name, "w+") diff --git a/spec/ruby/core/io/buffer/shared_spec.rb b/spec/ruby/core/io/buffer/shared_spec.rb index daaa66c5ad8b3d..2cc93e6d08c07a 100644 --- a/spec/ruby/core/io/buffer/shared_spec.rb +++ b/spec/ruby/core/io/buffer/shared_spec.rb @@ -12,15 +12,13 @@ end it "is true for a non-private buffer created with .map" do - path = tmp("read_text.txt") - File.copy_stream(fixture(__dir__, "read_text.txt"), path) + path = fixture(__dir__, "read_text.txt") file = File.open(path, "r+") @buffer = IO::Buffer.map(file) @buffer.shared?.should == true ensure @buffer.free file.close - File.unlink(path) end it "is false for an unshared buffer" do diff --git a/spec/ruby/core/io/buffer/transfer_spec.rb b/spec/ruby/core/io/buffer/transfer_spec.rb index 02e029016ada3b..3bc08998dde737 100644 --- a/spec/ruby/core/io/buffer/transfer_spec.rb +++ b/spec/ruby/core/io/buffer/transfer_spec.rb @@ -91,8 +91,9 @@ context "with a slice of a buffer" do it "transfers source to a new slice, not touching the buffer" do @buffer = IO::Buffer.new(4) - slice = @buffer.slice(0, 2) @buffer.set_string("test") + slice = @buffer.slice(0, 2) + slice.get_string.should == "te" new_slice = slice.transfer slice.null?.should == true diff --git a/spec/ruby/core/io/buffer/valid_spec.rb b/spec/ruby/core/io/buffer/valid_spec.rb index 0a401728696861..b84bdd0cfd942e 100644 --- a/spec/ruby/core/io/buffer/valid_spec.rb +++ b/spec/ruby/core/io/buffer/valid_spec.rb @@ -57,17 +57,6 @@ @buffer.resize(3) slice.valid?.should == false end - - platform_is_not :linux do - # This test does not cause a copy-resize on Linux. - # `#resize` MAY cause the buffer to move, but there is no guarantee. - it "is false when buffer is copied on resize" do - @buffer = IO::Buffer.new(4, IO::Buffer::MAPPED) - slice = @buffer.slice(0, 2) - @buffer.resize(8) - slice.valid?.should == false - end - end end it "is false for a slice of a transferred buffer" do diff --git a/spec/ruby/core/kernel/Integer_spec.rb b/spec/ruby/core/kernel/Integer_spec.rb index f61cca74633fcf..978fd8ef08c427 100644 --- a/spec/ruby/core/kernel/Integer_spec.rb +++ b/spec/ruby/core/kernel/Integer_spec.rb @@ -14,7 +14,9 @@ obj = mock("object") obj.should_receive(:to_int).and_return("1") obj.should_receive(:to_i).and_return(nil) - -> { Integer(obj) }.should.raise(TypeError) + -> { + Integer(obj) + }.should raise_consistent_error(TypeError, "can't convert MockObject into Integer (MockObject#to_i gives nil)") end it "return a result of to_i when to_int does not return an Integer" do @@ -24,8 +26,31 @@ Integer(obj).should == 42 end + it "returns a result of to_str" do + obj = mock("obj") + obj.should_receive(:to_str).and_return("1") + + Integer(obj).should == 1 + end + + it "returns a result of to_int when both to_int and to_str are defined" do + obj = mock("obj") + obj.should_receive(:to_int).and_return(1) + obj.should_not_receive(:to_str) + + Integer(obj).should == 1 + end + + it "returns a result of to_str when both to_str and to_i are defined" do + obj = mock("obj") + obj.should_receive(:to_str).and_return("1") + obj.should_not_receive(:to_i) + + Integer(obj).should == 1 + end + it "raises a TypeError when passed nil" do - -> { Integer(nil) }.should.raise(TypeError) + -> { Integer(nil) }.should.raise(TypeError, "can't convert nil into Integer") end it "returns an Integer object" do @@ -67,18 +92,24 @@ it "raises a TypeError if to_i returns a value that is not an Integer" do obj = mock("object") obj.should_receive(:to_i).and_return("1") - -> { Integer(obj) }.should.raise(TypeError) + -> { + Integer(obj) + }.should raise_consistent_error(TypeError, "can't convert MockObject into Integer (MockObject#to_i gives String)") end it "raises a TypeError if no to_int or to_i methods exist" do obj = mock("object") - -> { Integer(obj) }.should.raise(TypeError) + -> { + Integer(obj) + }.should.raise(TypeError, "can't convert MockObject into Integer") end it "raises a TypeError if to_int returns nil and no to_i exists" do obj = mock("object") obj.should_receive(:to_i).and_return(nil) - -> { Integer(obj) }.should.raise(TypeError) + -> { + Integer(obj) + }.should raise_consistent_error(TypeError, "can't convert MockObject into Integer (MockObject#to_i gives nil)") end it "raises a FloatDomainError when passed NaN" do @@ -576,7 +607,7 @@ end it "raises an ArgumentError if a base is given for a non-String value" do - -> { Integer(98, 15) }.should.raise(ArgumentError) + -> { Integer(98, 15) }.should.raise(ArgumentError, "base specified for non string value") end it "tries to convert the base to an integer using to_int" do diff --git a/spec/ruby/core/kernel/caller_locations_spec.rb b/spec/ruby/core/kernel/caller_locations_spec.rb index 24ee0745321a2f..04cf806ef4bdf8 100644 --- a/spec/ruby/core/kernel/caller_locations_spec.rb +++ b/spec/ruby/core/kernel/caller_locations_spec.rb @@ -103,7 +103,10 @@ loc = nil tap { loc = caller_locations(1, 1)[0] } loc.label.should == "Kernel#tap" - loc.path.should == __FILE__ + # CRuby hides the file which defines the method: https://bugs.ruby-lang.org/issues/20968 + unless loc.path == __FILE__ + loc.path.should.start_with? "} end + it "displays all instance variables if #instance_variables_to_inspect is not defined" do + obj = BasicObject.new + obj.instance_eval do + @host = "localhost" + @user = "root" + @password = "hunter2" + end + method_inspect = Kernel.instance_method(:inspect) + + inspected = method_inspect.bind(obj).call.sub(/^#} + end + it "raises an error if #instance_variables_to_inspect returns an invalid value" do obj = Object.new obj.instance_eval do diff --git a/spec/ruby/core/kernel/require_spec.rb b/spec/ruby/core/kernel/require_spec.rb index 19c19e55948777..62d954c2bf7eee 100644 --- a/spec/ruby/core/kernel/require_spec.rb +++ b/spec/ruby/core/kernel/require_spec.rb @@ -26,30 +26,24 @@ end out = ruby_exe("puts $LOADED_FEATURES", options: '--disable-gems --disable-did-you-mean') - features = out.lines.map { |line| File.basename(line.chomp, '.*') } + features = out.lines.map(&:chomp) # Ignore engine-specific internals case RUBY_ENGINE when "jruby" - features -= %w[java util] - else - features -= %w[encdb transdb windows_1252 windows_31] + features -= %w[java.rb jruby/util.rb] + when "ruby" + so = RbConfig::CONFIG['DLEXT'] + features -= ["windows_1252.#{so}", "windows_31.#{so}"] + features.reject! { |feature| feature.end_with? "encdb.#{so}" } + features.reject! { |feature| feature.end_with? "transdb.#{so}" } + features.reject! { |feature| feature.include?('-fake') } end - features.reject! { |feature| feature.end_with?('-fake') } - features.sort.should == provided.sort - - requires = provided - ruby_version_is "4.0" do - if RUBY_ENGINE != "jruby" - requires = requires.map { |f| f == "pathname" ? "pathname.so" : f } - end - end - - ruby_version_is "4.1" do - requires = requires.map { |f| f == "monitor" ? "monitor.so" : f } - end + features_no_ext = features.map { |path| File.basename(path, '.*') } + features_no_ext.sort.should == provided.sort + requires = features code = requires.map { |f| "puts require #{f.inspect}\n" }.join required = ruby_exe(code, options: '--disable-gems') required.should == "false\n" * requires.size diff --git a/spec/ruby/core/kernel/shared/sprintf.rb b/spec/ruby/core/kernel/shared/sprintf.rb index fe77b5f45bc4bf..b40bd95f594673 100644 --- a/spec/ruby/core/kernel/shared/sprintf.rb +++ b/spec/ruby/core/kernel/shared/sprintf.rb @@ -58,11 +58,6 @@ def obj.to_i; 10; end it "works well with large numbers" do @method.call("%#{f}", 1234567890987654321).should == "1234567890987654321" end - - it "converts to the empty string if precision is 0 and value is 0" do - @method.call("%.#{f}", 0).should == "" - @method.call("%.0#{f}", 0).should == "" - end end end @@ -110,6 +105,20 @@ def obj.to_i; 10; end @method.call("%X", -1).should == "..F" end end + + %w[b B d i u o x X].each do |f| + describe f do + it "converts to the empty string if precision is 0 and value is 0" do + @method.call("%.#{f}", 0).should == "" + @method.call("%.0#{f}", 0).should == "" + end + + it "pads the empty string if precision is 0 and value is 0" do + @method.call("%2.#{f}", 0).should == " " + @method.call("%2.0#{f}", 0).should == " " + end + end + end end describe "float formats" do @@ -299,6 +308,10 @@ def obj.to_i; 10; end @method.call("%c", "abc").should == "a" end + it "displays only the first character if argument is a string of several multibyte characters" do + @method.call("%c", "あいうえお").should == "あ" + end + it "displays no characters if argument is an empty string" do @method.call("%c", "").should == "" end @@ -594,11 +607,28 @@ def obj.to_str @method.call("%#b", 0).should == "0" @method.call("%#B", 0).should == "0" - @method.call("%#o", 0).should == "0" - @method.call("%#x", 0).should == "0" @method.call("%#X", 0).should == "0" end + + it "does nothing for zero argument when combined with zero precision" do + @method.call("%#.0b", 0).should == "" + @method.call("%#.0B", 0).should == "" + + @method.call("%#.0x", 0).should == "" + @method.call("%#.0X", 0).should == "" + end + end + + context "applies to format o" do + it "does nothing for zero argument" do + @method.call("%#o", 0).should == "0" + @method.call("%#.1o", 0).should == "0" + end + + it "increases the precision if precision zero is requested with zero argument" do + @method.call("%#.0o", 0).should == "0" + end end context "applies to formats aAeEfgG" do diff --git a/spec/ruby/core/method/inspect_spec.rb b/spec/ruby/core/method/inspect_spec.rb index e0fe1afdd0c0dc..97ff2d8c11de63 100644 --- a/spec/ruby/core/method/inspect_spec.rb +++ b/spec/ruby/core/method/inspect_spec.rb @@ -1,6 +1,8 @@ require_relative '../../spec_helper' require_relative 'shared/to_s' +require_relative 'shared/aliased_inspect' describe "Method#inspect" do it_behaves_like :method_to_s, :inspect + it_behaves_like :method_to_s_aliased, :inspect, -> meth { meth } end diff --git a/spec/ruby/core/method/original_name_spec.rb b/spec/ruby/core/method/original_name_spec.rb index 8fec0e7c33cee5..b92cf35154eff8 100644 --- a/spec/ruby/core/method/original_name_spec.rb +++ b/spec/ruby/core/method/original_name_spec.rb @@ -40,4 +40,20 @@ klass.new.method(:renamed).original_name.should == :my_method klass.new.method(:aliased).original_name.should == :my_method end + + it "returns the source UnboundMethod's name for Kernel#is_a? and Kernel#kind_of?" do + klass = Class.new { define_method(:my_is_a?, ::Kernel.instance_method(:is_a?)) } + klass.new.method(:my_is_a?).original_name.should == :is_a? + + klass = Class.new { define_method(:my_kind_of?, ::Kernel.instance_method(:kind_of?)) } + klass.new.method(:my_kind_of?).original_name.should == :kind_of? + end + + it "preserves the source name when aliasing a define_method'd Kernel method" do + klass = Class.new do + define_method(:my_is_a?, ::Kernel.instance_method(:is_a?)) + alias_method :renamed_is_a?, :my_is_a? + end + klass.new.method(:renamed_is_a?).original_name.should == :is_a? + end end diff --git a/spec/ruby/core/method/shared/aliased_inspect.rb b/spec/ruby/core/method/shared/aliased_inspect.rb new file mode 100644 index 00000000000000..2a622c2f97f93a --- /dev/null +++ b/spec/ruby/core/method/shared/aliased_inspect.rb @@ -0,0 +1,31 @@ +describe :method_to_s_aliased, shared: true do + # @object converts a bound Method to either a Method (identity) or an + # UnboundMethod (-> meth { meth.unbind }), so these expectations cover both + # Method#to_s/#inspect and UnboundMethod#to_s/#inspect. + + it "shows the original name in parentheses for an aliased method" do + klass = Class.new do + def original_method; end + alias_method :renamed_method, :original_method + end + @object.call(klass.new.method(:renamed_method)).send(@method).should.include? '#renamed_method(original_method)' + end + + it "shows the source UnboundMethod's name in parentheses for a define_method'd method" do + klass = Class.new { define_method(:renamed_is_a?, ::Kernel.instance_method(:is_a?)) } + @object.call(klass.new.method(:renamed_is_a?)).send(@method).should.include? '#renamed_is_a?(is_a?)' + end + + it "does not annotate a directly looked-up Kernel method with a shared internal name" do + @object.call(Object.new.method(:is_a?)).send(@method).should_not.include? '(kind_of?)' + @object.call(Object.new.method(:kind_of?)).send(@method).should_not.include? '(is_a?)' + end + + it "shows the source name when aliasing a define_method'd Kernel method" do + klass = Class.new do + define_method(:my_is_a?, ::Kernel.instance_method(:is_a?)) + alias_method :renamed_is_a?, :my_is_a? + end + @object.call(klass.new.method(:renamed_is_a?)).send(@method).should.include? '#renamed_is_a?(is_a?)' + end +end diff --git a/spec/ruby/core/method/to_s_spec.rb b/spec/ruby/core/method/to_s_spec.rb index 9f190113029559..ba0b4fa3c6361d 100644 --- a/spec/ruby/core/method/to_s_spec.rb +++ b/spec/ruby/core/method/to_s_spec.rb @@ -1,6 +1,8 @@ require_relative '../../spec_helper' require_relative 'shared/to_s' +require_relative 'shared/aliased_inspect' describe "Method#to_s" do it_behaves_like :method_to_s, :to_s + it_behaves_like :method_to_s_aliased, :to_s, -> meth { meth } end diff --git a/spec/ruby/core/mutex/sleep_spec.rb b/spec/ruby/core/mutex/sleep_spec.rb index a78e4c4b623db8..71b089d251713c 100644 --- a/spec/ruby/core/mutex/sleep_spec.rb +++ b/spec/ruby/core/mutex/sleep_spec.rb @@ -82,6 +82,14 @@ th.value.should.is_a?(Integer) end + it "accepts nil as a sleep duration" do + m = Mutex.new + -> { + m.lock + m.sleep(nil) + }.should block_caller + end + it "wakes up when requesting sleep times near or equal to zero" do times = [] val = 1 diff --git a/spec/ruby/core/process/constants_spec.rb b/spec/ruby/core/process/constants_spec.rb index 62e8f2ee4ae910..5a4c478e7499a2 100644 --- a/spec/ruby/core/process/constants_spec.rb +++ b/spec/ruby/core/process/constants_spec.rb @@ -2,7 +2,7 @@ describe "Process::Constants" do platform_is :darwin, :netbsd, :freebsd do - it "are all present on BSD-like systems" do + describe "on BSD-like systems" do %i[ WNOHANG WUNTRACED @@ -20,27 +20,31 @@ RLIMIT_NPROC RLIMIT_NOFILE ].each do |const| - Process.const_defined?(const).should == true - Process.const_get(const).should.instance_of?(Integer) + it "defines #{const}" do + Process.const_defined?(const).should == true + Process.const_get(const).should.instance_of?(Integer) + end end end end platform_is :darwin do - it "are all present on Darwin" do + describe "on Darwin" do %i[ RLIM_SAVED_MAX RLIM_SAVED_CUR RLIMIT_AS ].each do |const| - Process.const_defined?(const).should == true - Process.const_get(const).should.instance_of?(Integer) + it "defines #{const}" do + Process.const_defined?(const).should == true + Process.const_get(const).should.instance_of?(Integer) + end end end end platform_is :linux do - it "are all present on Linux" do + describe "on Linux" do %i[ WNOHANG WUNTRACED @@ -61,37 +65,43 @@ RLIM_SAVED_MAX RLIM_SAVED_CUR ].each do |const| - Process.const_defined?(const).should == true - Process.const_get(const).should.instance_of?(Integer) + it "defines #{const}" do + Process.const_defined?(const).should == true + Process.const_get(const).should.instance_of?(Integer) + end end end end platform_is :netbsd, :freebsd do - it "are all present on NetBSD and FreeBSD" do + describe "on NetBSD and FreeBSD" do %i[ RLIMIT_SBSIZE RLIMIT_AS ].each do |const| - Process.const_defined?(const).should == true - Process.const_get(const).should.instance_of?(Integer) + it "defines #{const}" do + Process.const_defined?(const).should == true + Process.const_get(const).should.instance_of?(Integer) + end end end end platform_is :freebsd do - it "are all present on FreeBSD" do + describe "on FreeBSD" do %i[ RLIMIT_NPTS ].each do |const| - Process.const_defined?(const).should == true - Process.const_get(const).should.instance_of?(Integer) + it "defines #{const}" do + Process.const_defined?(const).should == true + Process.const_get(const).should.instance_of?(Integer) + end end end end platform_is :windows do - it "does not define RLIMIT constants" do + describe "on Windows" do %i[ RLIMIT_CPU RLIMIT_FSIZE @@ -107,7 +117,9 @@ RLIM_SAVED_MAX RLIM_SAVED_CUR ].each do |const| - Process.const_defined?(const).should == false + it "does not define #{const}" do + Process.const_defined?(const).should == false + end end end end diff --git a/spec/ruby/core/string/shared/each_line.rb b/spec/ruby/core/string/shared/each_line.rb index d79c2b74c4aeee..127db876ad0848 100644 --- a/spec/ruby/core/string/shared/each_line.rb +++ b/spec/ruby/core/string/shared/each_line.rb @@ -46,14 +46,36 @@ a.should == ["one\ntwo\r\nthree"] end - it "yields paragraphs (broken by 2 or more successive newlines) when passed '' and replaces multiple newlines with only two ones" do - a = [] - "hello\nworld\n\n\nand\nuniverse\n\n\n\n\n".send(@method, '') { |s| a << s } - a.should == ["hello\nworld\n\n", "and\nuniverse\n\n"] + context "when passed '' (paragraph mode, broken by 2 or more successive newlines)" do + it "replaces multiple newlines with only two ones" do + a = [] + "hello\nworld\n\n\nand\nuniverse\n\n\n\n\n".send(@method, '') { |s| a << s } + a.should == ["hello\nworld\n\n", "and\nuniverse\n\n"] - a = [] - "hello\nworld\n\n\nand\nuniverse\n\n\n\n\ndog".send(@method, '') { |s| a << s } - a.should == ["hello\nworld\n\n", "and\nuniverse\n\n", "dog"] + a = [] + "hello\nworld\n\n\nand\nuniverse\n\n\n\n\ndog".send(@method, '') { |s| a << s } + a.should == ["hello\nworld\n\n", "and\nuniverse\n\n", "dog"] + end + + it 'handles \r\n-style newlines' do + a = [] + "hello\nworld\r\n\r\n\nand\nuniverse\n\r\n\n\n\n".send(@method, '') { |s| a << s } + a.should == ["hello\nworld\r\n\r\n", "and\nuniverse\n\r\n"] + + a = [] + "hello\r\nworld\n\n\nand\nuniverse\n\n\n\r\n\r\ndog".send(@method, '') { |s| a << s } + a.should == ["hello\r\nworld\n\n", "and\nuniverse\n\n", "dog"] + end + + it "removes trailing newlines with `chomp: true`" do + a = [] + "hello\nworld\n\n\nand\nuniverse\n\n\n\n\n".send(@method, '', chomp: true) { |s| a << s } + a.should == ["hello\nworld", "and\nuniverse"] + + a = [] + "hello\nworld\n\n\nand\nuniverse\n\n\n\n\ndog".send(@method, '', chomp: true) { |s| a << s } + a.should == ["hello\nworld", "and\nuniverse", "dog"] + end end describe "uses $/" do diff --git a/spec/ruby/core/thread/raise_spec.rb b/spec/ruby/core/thread/raise_spec.rb index 3b02a2e005481b..efc09d4a356a6e 100644 --- a/spec/ruby/core/thread/raise_spec.rb +++ b/spec/ruby/core/thread/raise_spec.rb @@ -136,6 +136,31 @@ def exception(*args) [ScratchPad.recorded, @thr, []] ] end + + it "calls #set_backtrace only in the caller thread" do + cls = Class.new(Exception) do + attr_accessor :log + def initialize(*args) + @log = [] # This is shared because the super #exception uses a shallow clone + super + end + + def set_backtrace(backtrace) + @log << [Thread.current, backtrace] + super + end + end + exc = cls.new + + backtrace = ["a.rb:1"] + + @thr.raise exc, "Thread#raise #set_backtrace spec", backtrace + @thr.join + ScratchPad.recorded.should.is_a?(cls) + exc.log.should == [ + [Thread.current, backtrace] + ] + end end describe "Thread#raise on a running thread" do diff --git a/spec/ruby/core/unboundmethod/inspect_spec.rb b/spec/ruby/core/unboundmethod/inspect_spec.rb index cecf542fcd4567..3abed94f7f82fc 100644 --- a/spec/ruby/core/unboundmethod/inspect_spec.rb +++ b/spec/ruby/core/unboundmethod/inspect_spec.rb @@ -1,7 +1,9 @@ require_relative '../../spec_helper' require_relative 'fixtures/classes' require_relative 'shared/to_s' +require_relative '../method/shared/aliased_inspect' describe "UnboundMethod#inspect" do it_behaves_like :unboundmethod_to_s, :inspect + it_behaves_like :method_to_s_aliased, :inspect, -> meth { meth.unbind } end diff --git a/spec/ruby/core/unboundmethod/original_name_spec.rb b/spec/ruby/core/unboundmethod/original_name_spec.rb index fa9a6fcc637f50..cd5f55805dfa1a 100644 --- a/spec/ruby/core/unboundmethod/original_name_spec.rb +++ b/spec/ruby/core/unboundmethod/original_name_spec.rb @@ -40,4 +40,20 @@ klass.instance_method(:renamed).original_name.should == :my_method klass.instance_method(:aliased).original_name.should == :my_method end + + it "returns the source UnboundMethod's name for Kernel#is_a? and Kernel#kind_of?" do + klass = Class.new { define_method(:my_is_a?, ::Kernel.instance_method(:is_a?)) } + klass.instance_method(:my_is_a?).original_name.should == :is_a? + + klass = Class.new { define_method(:my_kind_of?, ::Kernel.instance_method(:kind_of?)) } + klass.instance_method(:my_kind_of?).original_name.should == :kind_of? + end + + it "preserves the source name when aliasing a define_method'd Kernel method" do + klass = Class.new do + define_method(:my_is_a?, ::Kernel.instance_method(:is_a?)) + alias_method :renamed_is_a?, :my_is_a? + end + klass.instance_method(:renamed_is_a?).original_name.should == :is_a? + end end diff --git a/spec/ruby/core/unboundmethod/to_s_spec.rb b/spec/ruby/core/unboundmethod/to_s_spec.rb index a508229b49c9c6..615d88675b1e69 100644 --- a/spec/ruby/core/unboundmethod/to_s_spec.rb +++ b/spec/ruby/core/unboundmethod/to_s_spec.rb @@ -1,7 +1,9 @@ require_relative '../../spec_helper' require_relative 'fixtures/classes' require_relative 'shared/to_s' +require_relative '../method/shared/aliased_inspect' describe "UnboundMethod#to_s" do it_behaves_like :unboundmethod_to_s, :to_s + it_behaves_like :method_to_s_aliased, :to_s, -> meth { meth.unbind } end diff --git a/spec/ruby/language/array_spec.rb b/spec/ruby/language/array_spec.rb index 3601eb0e00ba21..78cf36c201cae7 100644 --- a/spec/ruby/language/array_spec.rb +++ b/spec/ruby/language/array_spec.rb @@ -155,8 +155,12 @@ def obj.to_a; [2, 3, 4]; end b = [1, 0] [*a, 3, *a, *b].should == [1, 2, 3, 1, 2, 1, 0] end -end - -describe "The packing splat operator (*)" do + ruby_version_is "4.0" do + it "does not call #to_a on nil" do + e = nil + e.should_not_receive(:to_a) + [*e].should == [] + end + end end diff --git a/spec/ruby/language/case_spec.rb b/spec/ruby/language/case_spec.rb index 8355062e446327..41881bf20ae111 100644 --- a/spec/ruby/language/case_spec.rb +++ b/spec/ruby/language/case_spec.rb @@ -27,6 +27,41 @@ def bar; @calls << :bar; end @calls.should == [:foo, :bar] end + it "matches an Integer literal whose value does not fit in a 32-bit int" do + big = 10_000_000_000 + case big + when 10_000_000_000; true + else false + end.should == true + + case -3_000_000_000 + when -3_000_000_000; true + else false + end.should == true + end + + it "matches an arbitrary-precision Integer literal" do + huge = 1267650600228229401496703205376 + case huge + when 1267650600228229401496703205376; true + else false + end.should == true + end + + it "dispatches correctly with mixed small and large Integer literals" do + pick = -> x { + case x + when 1267650600228229401496703205376 then :beyond_long + when 10_000_000_000 then :beyond_int + when 1 then :fits_int + else :other + end + } + + [1267650600228229401496703205376, 10_000_000_000, 1, :nope].map(&pick).should == + [:beyond_long, :beyond_int, :fits_int, :other] + end + it "evaluates the body of the when clause whose range expression includes the case target expression" do case 5 when 21..30; false diff --git a/spec/ruby/language/defined_spec.rb b/spec/ruby/language/defined_spec.rb index 3fd611d09e6f96..6846179a7c38b6 100644 --- a/spec/ruby/language/defined_spec.rb +++ b/spec/ruby/language/defined_spec.rb @@ -211,6 +211,22 @@ }.should complain(/warning: possibly useless use of defined\? in void context/, verbose: true) end end + + describe "for a protected method" do + it "returns 'method' when the receiver is a subclass instance" do + DefinedSpecs::ProtectedBase.new.defined_on(DefinedSpecs::ProtectedSubclass.new).should == "method" + end + + it "returns 'method' when the receiver is the base class instance" do + DefinedSpecs::ProtectedSubclass.new.defined_on(DefinedSpecs::ProtectedBase.new).should == "method" + end + + ruby_bug "#22076", ""..."4.1" do + it "returns 'method' when the receiver is a sibling class instance via a shared included module" do + DefinedSpecs::ProtectedIncluderA.new.defined_on(DefinedSpecs::ProtectedIncluderB.new).should == "method" + end + end + end end describe "The defined? keyword for an expression" do diff --git a/spec/ruby/language/fixtures/defined.rb b/spec/ruby/language/fixtures/defined.rb index 3761cfa5bd4087..15bd7c50cf41f2 100644 --- a/spec/ruby/language/fixtures/defined.rb +++ b/spec/ruby/language/fixtures/defined.rb @@ -299,6 +299,33 @@ def method_no_args super end end + + class ProtectedBase + def m; end + protected :m + def defined_on(o) + defined?(o.m) + end + end + + class ProtectedSubclass < ProtectedBase + end + + module ProtectedInModule + def m; end + protected :m + def defined_on(o) + defined?(o.m) + end + end + + class ProtectedIncluderA + include ProtectedInModule + end + + class ProtectedIncluderB + include ProtectedInModule + end end class Object diff --git a/spec/ruby/language/lambda_spec.rb b/spec/ruby/language/lambda_spec.rb index 2a2953bd97a8e9..c6239e32bb8afe 100644 --- a/spec/ruby/language/lambda_spec.rb +++ b/spec/ruby/language/lambda_spec.rb @@ -14,6 +14,12 @@ def create_lambda klass.new.create_lambda.should.instance_of?(Proc) end + it "is not just syntactic sugar for Kernel#lambda" do + should_not_receive(:lambda) + + -> {} + end + it "does not execute the block" do -> { fail }.should.instance_of?(Proc) end diff --git a/spec/ruby/library/socket/ipsocket/inspect_spec.rb b/spec/ruby/library/socket/ipsocket/inspect_spec.rb new file mode 100644 index 00000000000000..85780a16f6f35d --- /dev/null +++ b/spec/ruby/library/socket/ipsocket/inspect_spec.rb @@ -0,0 +1,24 @@ +require_relative '../spec_helper' + +describe 'IPSocket#inspect' do + it "returns a String with the fd, family, address and port for TCPSocket" do + @server = TCPServer.new("127.0.0.1", 0) + @socket = TCPSocket.new("127.0.0.1", @server.addr[1]) + port = @socket.addr[1] + + @socket.inspect.should == "#" + ensure + @socket&.close + @server&.close + end + + it 'returns a String with the fd, family, address and port for UDPSocket' do + @socket = UDPSocket.new + @socket.bind('127.0.0.1', 0) + port = @socket.addr[1] + + @socket.inspect.should == "#" + ensure + @socket&.close + end +end diff --git a/spec/ruby/library/socket/socket/tcp_spec.rb b/spec/ruby/library/socket/socket/tcp_spec.rb index f52198b0028be2..cc3c9381c7285d 100644 --- a/spec/ruby/library/socket/socket/tcp_spec.rb +++ b/spec/ruby/library/socket/socket/tcp_spec.rb @@ -56,15 +56,33 @@ it 'connects to the server' do @client = Socket.tcp(@host, @port) - @client.write('hello') - connection, _ = @server.accept - begin connection.recv(5).should == 'hello' ensure connection.close end end + + ruby_version_is "4.0" do + it 'connects to the server when passed open_timeout argument' do + @client = Socket.tcp(@host, @port, open_timeout: 60) + @client.write('open_timeout') + connection, _ = @server.accept + begin + connection.recv(12).should == 'open_timeout' + ensure + connection.close + end + end + + it 'raises Errno::ETIMEDOUT with :open_timeout when no server is listening on the given address' do + -> { + Socket.tcp("192.0.2.1", 80, open_timeout: 0) + }.should.raise(Errno::ETIMEDOUT) + rescue Errno::ENETUNREACH + skip "all network interfaces down" + end + end end diff --git a/spec/ruby/library/socket/spec_helper.rb b/spec/ruby/library/socket/spec_helper.rb index b33663e02da119..86f3a610869ecc 100644 --- a/spec/ruby/library/socket/spec_helper.rb +++ b/spec/ruby/library/socket/spec_helper.rb @@ -1,12 +1,14 @@ require_relative '../../spec_helper' require 'socket' -MSpec.enable_feature :sock_packet if Socket.const_defined?(:SOCK_PACKET) -MSpec.enable_feature :udp_cork if Socket.const_defined?(:UDP_CORK) -MSpec.enable_feature :tcp_cork if Socket.const_defined?(:TCP_CORK) -MSpec.enable_feature :pktinfo if Socket.const_defined?(:IP_PKTINFO) -MSpec.enable_feature :ipv6_pktinfo if Socket.const_defined?(:IPV6_PKTINFO) -MSpec.enable_feature :ip_mtu if Socket.const_defined?(:IP_MTU) -MSpec.enable_feature :ipv6_nexthop if Socket.const_defined?(:IPV6_NEXTHOP) -MSpec.enable_feature :tcp_info if Socket.const_defined?(:TCP_INFO) -MSpec.enable_feature :ancillary_data if Socket.const_defined?(:AncillaryData) +# We force enable all features on Linux because anyway Linux implements all these features, +# and we want a constant number of spec examples across Ruby implementations, even if they don't define these constants. +MSpec.enable_feature :sock_packet if platform_is(:linux) || Socket.const_defined?(:SOCK_PACKET) +MSpec.enable_feature :udp_cork if platform_is(:linux) || Socket.const_defined?(:UDP_CORK) +MSpec.enable_feature :tcp_cork if platform_is(:linux) || Socket.const_defined?(:TCP_CORK) +MSpec.enable_feature :pktinfo if platform_is(:linux) || Socket.const_defined?(:IP_PKTINFO) +MSpec.enable_feature :ipv6_pktinfo if platform_is(:linux) || Socket.const_defined?(:IPV6_PKTINFO) +MSpec.enable_feature :ip_mtu if platform_is(:linux) || Socket.const_defined?(:IP_MTU) +MSpec.enable_feature :ipv6_nexthop if platform_is(:linux) || Socket.const_defined?(:IPV6_NEXTHOP) +MSpec.enable_feature :tcp_info if platform_is(:linux) || Socket.const_defined?(:TCP_INFO) +MSpec.enable_feature :ancillary_data if platform_is(:linux) || Socket.const_defined?(:AncillaryData) diff --git a/spec/ruby/library/socket/tcpsocket/shared/new.rb b/spec/ruby/library/socket/tcpsocket/shared/new.rb index cf4834526d391b..9c15dced4f2e63 100644 --- a/spec/ruby/library/socket/tcpsocket/shared/new.rb +++ b/spec/ruby/library/socket/tcpsocket/shared/new.rb @@ -19,8 +19,17 @@ TCPSocket.send(@method, "192.0.2.1", 80, connect_timeout: 0) }.should.raise(IO::TimeoutError) rescue Errno::ENETUNREACH - # In the case all network interfaces down. - # raise_error cannot deal with multiple expected exceptions + skip "all network interfaces down" + end + + ruby_version_is "4.0" do + it 'raises IO::TimeoutError with :open_timeout when no server is listening on the given address' do + -> { + TCPSocket.send(@method, "192.0.2.1", 80, open_timeout: 0) + }.should.raise(IO::TimeoutError) + rescue Errno::ENETUNREACH + skip "all network interfaces down" + end end describe "with a running server" do @@ -98,5 +107,12 @@ @socket = TCPSocket.send(@method, @hostname, @server.port, connect_timeout: 1) @socket.should.instance_of?(TCPSocket) end + + ruby_version_is "4.0" do + it "connects to a server when passed open_timeout argument" do + @socket = TCPSocket.send(@method, @hostname, @server.port, open_timeout: 1) + @socket.should.instance_of?(TCPSocket) + end + end end end diff --git a/spec/ruby/library/socket/udpsocket/inspect_spec.rb b/spec/ruby/library/socket/udpsocket/inspect_spec.rb deleted file mode 100644 index e212120b1442df..00000000000000 --- a/spec/ruby/library/socket/udpsocket/inspect_spec.rb +++ /dev/null @@ -1,17 +0,0 @@ -require_relative '../spec_helper' - -describe 'UDPSocket#inspect' do - before do - @socket = UDPSocket.new - @socket.bind('127.0.0.1', 0) - end - - after do - @socket.close - end - - it 'returns a String with the fd, family, address and port' do - port = @socket.addr[1] - @socket.inspect.should == "#" - end -end diff --git a/spec/ruby/optional/capi/ext/gc_spec.c b/spec/ruby/optional/capi/ext/gc_spec.c index 2637ad27ac570a..0baa114d7b1a42 100644 --- a/spec/ruby/optional/capi/ext/gc_spec.c +++ b/spec/ruby/optional/capi/ext/gc_spec.c @@ -18,6 +18,32 @@ VALUE rb_gc_register_address_outside_init; VALUE rb_gc_register_mark_object_not_referenced_float; +static VALUE spec_RB_GC_GUARD_keep_alive(VALUE self, VALUE array_with_string) { + VALUE string = rb_ary_entry(array_with_string, 0); + char* ptr = RSTRING_PTR(string); + // Without the RB_GC_GUARD(string) below, string could be GC'd, and ptr become invalid + rb_gc(); + char copy[4]; + copy[0] = ptr[0]; + copy[1] = ptr[1]; + copy[2] = ptr[2]; + copy[3] = '\0'; + RB_GC_GUARD(string); + return rb_str_new_cstr(copy); +} + +static VALUE spec_RB_GC_GUARD(VALUE self, VALUE object) { + RB_GC_GUARD(object); + return object; +} + +static VALUE spec_RB_GC_GUARD_raw(VALUE self, VALUE number) { + long l = NUM2LONG(number); + VALUE value = (VALUE) l; + RB_GC_GUARD(value); + return Qnil; +} + static VALUE registered_tagged_address(VALUE self) { return registered_tagged_value; } @@ -124,6 +150,9 @@ void Init_gc_spec(void) { rb_gc_register_mark_object_not_referenced_float = DBL2NUM(1.61); rb_gc_register_mark_object(rb_gc_register_mark_object_not_referenced_float); + rb_define_method(cls, "RB_GC_GUARD_keep_alive", spec_RB_GC_GUARD_keep_alive, 1); + rb_define_method(cls, "RB_GC_GUARD", spec_RB_GC_GUARD, 1); + rb_define_method(cls, "RB_GC_GUARD_raw", spec_RB_GC_GUARD_raw, 1); rb_define_method(cls, "registered_tagged_address", registered_tagged_address, 0); rb_define_method(cls, "registered_reference_address", registered_reference_address, 0); rb_define_method(cls, "registered_before_rb_gc_register_address", get_registered_before_rb_gc_register_address, 0); diff --git a/spec/ruby/optional/capi/gc_spec.rb b/spec/ruby/optional/capi/gc_spec.rb index 6695026c6f9d88..8b70ea4758a55e 100644 --- a/spec/ruby/optional/capi/gc_spec.rb +++ b/spec/ruby/optional/capi/gc_spec.rb @@ -7,6 +7,24 @@ @f = CApiGCSpecs.new end + describe "RB_GC_GUARD" do + it "forces an object to on stack so it cannot be GC'd early" do + @f.RB_GC_GUARD_keep_alive([%w[ab cd ef].join]).should == "abc" + end + + it "can be used with any Ruby object" do + @f.RB_GC_GUARD(true).should == true + @f.RB_GC_GUARD(42).should == 42 + @f.RB_GC_GUARD(self).should == self + end + + it "tolerates being passed invalid pointers" do + (0...256).each do |address| + @f.RB_GC_GUARD_raw(address).should == nil + end + end + end + describe "rb_gc_register_address" do it "correctly gets the value from a registered address" do @f.registered_tagged_address.should == 10 diff --git a/spec/ruby/optional/capi/string_spec.rb b/spec/ruby/optional/capi/string_spec.rb index 3e095f05c83c44..569fc8891a1a9d 100644 --- a/spec/ruby/optional/capi/string_spec.rb +++ b/spec/ruby/optional/capi/string_spec.rb @@ -1149,7 +1149,7 @@ def inspect end it "can format a nil VALUE as a pointer and gives the same output as sprintf in C" do - res = @s.rb_sprintf7("%p", nil); + res = @s.rb_sprintf7("%p", nil) res[0].should == res[1] end @@ -1159,14 +1159,14 @@ def inspect end it "can format a raw number a pointer and gives the same output as sprintf in C" do - res = @s.rb_sprintf7("%p", 0x223643); + res = @s.rb_sprintf7("%p", 0x223643) res[0].should == res[1] end end describe "rb_vsprintf" do it "returns a formatted String from a variable number of arguments" do - s = @s.rb_vsprintf("%s, %d, %.2f", "abc", 42, 2.7); + s = @s.rb_vsprintf("%s, %d, %.2f", "abc", 42, 2.7) s.should == "abc, 42, 2.70" end end diff --git a/spec/ruby/shared/kernel/raise.rb b/spec/ruby/shared/kernel/raise.rb index 07b6a30de2c5f1..84591669060c37 100644 --- a/spec/ruby/shared/kernel/raise.rb +++ b/spec/ruby/shared/kernel/raise.rb @@ -152,7 +152,7 @@ def e.exception it "supports automatic cause chaining from a previous exception" do begin - raise StandardError,"first error" + raise StandardError, "first error" rescue => cause -> { @object.raise("second error") }.should.raise(RuntimeError, "second error", cause:) end From 9e8ad9ac0489d8390bc11e5b18f4af065d60898c Mon Sep 17 00:00:00 2001 From: kai-matsudate Date: Sun, 31 May 2026 23:12:28 +0900 Subject: [PATCH 023/188] [ruby/prism] Reject modifier conditionals in `if`/`unless` predicates Prism accepted a modifier conditional as the predicate of an `if`/`unless` (and `elsif`), while parse.y rejects it: if a if b then end # parse.y: SyntaxError, prism: accepted unless a unless b then end The `if`/`unless`/`elsif` predicate was parsed with a floor of `PM_BINDING_POWER_MODIFIER`, which is inclusive of the modifier conditionals (`if`/`unless`/`while`/`until`, left binding power `PM_BINDING_POWER_MODIFIER`), so they were absorbed into the predicate. `while`/`until` predicates already use `PM_BINDING_POWER_COMPOSITION` and reject these correctly. Raise the floor to `PM_BINDING_POWER_MODIFIER + 1` so the predicate still absorbs `and`/`or` (and tighter operators) but excludes the modifier conditionals, matching parse.y and aligning `if`/`unless` with `while`/`until`. https://github.com/ruby/prism/commit/0fa8d8f8d8 --- prism/prism.c | 4 ++-- .../errors/modifier_conditional_in_predicate.txt | 12 ++++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 test/prism/errors/modifier_conditional_in_predicate.txt diff --git a/prism/prism.c b/prism/prism.c index a8bbcea09745c1..c9d501520aba02 100644 --- a/prism/prism.c +++ b/prism/prism.c @@ -15486,7 +15486,7 @@ parse_conditional(pm_parser_t *parser, pm_context_t context, size_t opening_newl pm_token_t keyword = parser->previous; pm_token_t then_keyword = { 0 }; - pm_node_t *predicate = parse_predicate(parser, PM_BINDING_POWER_MODIFIER, context, &then_keyword, (uint16_t) (depth + 1)); + pm_node_t *predicate = parse_predicate(parser, PM_BINDING_POWER_MODIFIER + 1, context, &then_keyword, (uint16_t) (depth + 1)); pm_statements_node_t *statements = NULL; if (!match3(parser, PM_TOKEN_KEYWORD_ELSIF, PM_TOKEN_KEYWORD_ELSE, PM_TOKEN_KEYWORD_END)) { @@ -15524,7 +15524,7 @@ parse_conditional(pm_parser_t *parser, pm_context_t context, size_t opening_newl pm_token_t elsif_keyword = parser->current; parser_lex(parser); - pm_node_t *predicate = parse_predicate(parser, PM_BINDING_POWER_MODIFIER, PM_CONTEXT_ELSIF, &then_keyword, (uint16_t) (depth + 1)); + pm_node_t *predicate = parse_predicate(parser, PM_BINDING_POWER_MODIFIER + 1, PM_CONTEXT_ELSIF, &then_keyword, (uint16_t) (depth + 1)); pm_accepts_block_stack_push(parser, true); pm_statements_node_t *statements = parse_statements(parser, PM_CONTEXT_ELSIF, (uint16_t) (depth + 1)); diff --git a/test/prism/errors/modifier_conditional_in_predicate.txt b/test/prism/errors/modifier_conditional_in_predicate.txt new file mode 100644 index 00000000000000..5b89ee4a260bfd --- /dev/null +++ b/test/prism/errors/modifier_conditional_in_predicate.txt @@ -0,0 +1,12 @@ +if a if b then end + ^~ expected `then` or `;` or '\n' + ^~ unexpected 'if', ignoring it + ^~~~ unexpected 'then', expecting end-of-input + ^~~~ unexpected 'then', ignoring it + +unless a unless b then end + ^~~~~~ expected `then` or `;` or '\n' + ^~~~~~ unexpected 'unless', ignoring it + ^~~~ unexpected 'then', expecting end-of-input + ^~~~ unexpected 'then', ignoring it + From c49e778a80e0979224d810f9909023b43f80a435 Mon Sep 17 00:00:00 2001 From: kai-matsudate Date: Tue, 2 Jun 2026 02:12:31 +0900 Subject: [PATCH 024/188] [ruby/prism] Use PM_BINDING_POWER_COMPOSITION for the conditional predicate floor The `+ 1` form is meant to express associativity, not a binding-power floor; use the named constant instead, matching while/until conditions. https://github.com/ruby/prism/commit/53d1ee76c6 --- prism/prism.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/prism/prism.c b/prism/prism.c index c9d501520aba02..a2e04ed106db84 100644 --- a/prism/prism.c +++ b/prism/prism.c @@ -15486,7 +15486,7 @@ parse_conditional(pm_parser_t *parser, pm_context_t context, size_t opening_newl pm_token_t keyword = parser->previous; pm_token_t then_keyword = { 0 }; - pm_node_t *predicate = parse_predicate(parser, PM_BINDING_POWER_MODIFIER + 1, context, &then_keyword, (uint16_t) (depth + 1)); + pm_node_t *predicate = parse_predicate(parser, PM_BINDING_POWER_COMPOSITION, context, &then_keyword, (uint16_t) (depth + 1)); pm_statements_node_t *statements = NULL; if (!match3(parser, PM_TOKEN_KEYWORD_ELSIF, PM_TOKEN_KEYWORD_ELSE, PM_TOKEN_KEYWORD_END)) { @@ -15524,7 +15524,7 @@ parse_conditional(pm_parser_t *parser, pm_context_t context, size_t opening_newl pm_token_t elsif_keyword = parser->current; parser_lex(parser); - pm_node_t *predicate = parse_predicate(parser, PM_BINDING_POWER_MODIFIER + 1, PM_CONTEXT_ELSIF, &then_keyword, (uint16_t) (depth + 1)); + pm_node_t *predicate = parse_predicate(parser, PM_BINDING_POWER_COMPOSITION, PM_CONTEXT_ELSIF, &then_keyword, (uint16_t) (depth + 1)); pm_accepts_block_stack_push(parser, true); pm_statements_node_t *statements = parse_statements(parser, PM_CONTEXT_ELSIF, (uint16_t) (depth + 1)); From cc2547595ebd13e48b08e4f86c1d1a5d16f43756 Mon Sep 17 00:00:00 2001 From: Kunshan Wang Date: Mon, 1 Jun 2026 15:11:05 +0800 Subject: [PATCH 025/188] Ensure DTrace probes capture all GC marking events `gc_prof_mark_timer_start` and `gc_prof_mark_timer_stop` include DTrace hooks for the `MARK_BEGIN` and `MARK_END` events, respectively. Previously, those probes are only triggered in `gc_marks`. However, `gc_marks_continue` and `gc_rest` also contain marking activities, but are not captured by the probes. We move the invocation of `gc_prof_mark_timer_start` and `gc_prof_mark_timer_stop` into `gc_marking_enter` and `gc_marking_exit` to ensure all marking activities are captured by the probes. --- gc/default/default.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/gc/default/default.c b/gc/default/default.c index 291ff91b810002..145140dfbfbc4e 100644 --- a/gc/default/default.c +++ b/gc/default/default.c @@ -5965,7 +5965,6 @@ gc_marks_start(rb_objspace_t *objspace, int full_mark) static bool gc_marks(rb_objspace_t *objspace, int full_mark) { - gc_prof_mark_timer_start(objspace); gc_marking_enter(objspace); bool marking_finished = false; @@ -5986,7 +5985,6 @@ gc_marks(rb_objspace_t *objspace, int full_mark) #endif gc_marking_exit(objspace); - gc_prof_mark_timer_stop(objspace); return marking_finished; } @@ -6882,6 +6880,8 @@ gc_marking_enter(rb_objspace_t *objspace) { GC_ASSERT(during_gc != 0); + gc_prof_mark_timer_start(objspace); + if (MEASURE_GC) { gc_clock_start(&objspace->profile.marking_start_time); } @@ -6897,6 +6897,8 @@ gc_marking_exit(rb_objspace_t *objspace) if (MEASURE_GC) { objspace->profile.marking_time_ns += gc_clock_end(&objspace->profile.marking_start_time); } + + gc_prof_mark_timer_stop(objspace); } static void From c4d09f416ce29f5e818323555fc62e7cba5d4089 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Mon, 1 Jun 2026 12:30:58 -0700 Subject: [PATCH 026/188] Reapply "Reserve 2 bits for expressing object layout (#17139)" (#17158) This reverts commit ddb5055d961d970aded287cfebd07b78efee3ca7. --- array.c | 2 + class.c | 10 ++++- depend | 1 + gc.c | 24 ++++++++--- imemo.c | 18 ++++++-- internal/gc.h | 2 +- ractor.c | 2 +- shape.c | 78 ++++++++++++++++++++++++++++++---- shape.h | 54 ++++++++++++++++++----- string.c | 2 +- test/ruby/test_shapes.rb | 31 ++++++++++++++ variable.c | 22 ++++++---- vm_insnhelper.c | 7 ++- zjit/src/cruby_bindings.inc.rs | 7 ++- 14 files changed, 216 insertions(+), 44 deletions(-) diff --git a/array.c b/array.c index 08d8d17c90f79f..7f06e3c9c72f66 100644 --- a/array.c +++ b/array.c @@ -30,6 +30,7 @@ #include "ruby/thread.h" #include "ruby/util.h" #include "ruby/ractor.h" +#include "shape.h" #include "vm_core.h" #include "builtin.h" @@ -909,6 +910,7 @@ init_fake_ary_flags(void) struct RArray fake_ary = {0}; fake_ary.basic.flags = T_ARRAY; VALUE ary = (VALUE)&fake_ary; + RBASIC_SET_SHAPE_ID(ary, ROOT_SHAPE_ID | SHAPE_ID_LAYOUT_OTHER); rb_ary_freeze(ary); return fake_ary.basic.flags; } diff --git a/class.c b/class.c index 69194ccab23756..02078cc9bc8baf 100644 --- a/class.c +++ b/class.c @@ -585,7 +585,15 @@ class_alloc0(enum ruby_value_type type, VALUE klass, bool boxable) VALUE flags = type | FL_SHAREABLE; if (boxable) flags |= RCLASS_BOXABLE; - NEWOBJ_OF(obj, struct RClass, klass, flags, alloc_size); + shape_id_t shape_id = ROOT_SHAPE_ID; + if (boxable) { + shape_id |= SHAPE_ID_LAYOUT_OTHER; + } + else { + shape_id |= SHAPE_ID_LAYOUT_RCLASS; + } + + struct RClass *obj = (struct RClass *)rb_newobj(GET_EC(), klass, flags, shape_id, true, alloc_size); obj->object_id = 0; diff --git a/depend b/depend index e61b6856704712..a2e8312298e7ea 100644 --- a/depend +++ b/depend @@ -80,6 +80,7 @@ array.$(OBJEXT): $(top_srcdir)/internal/sanitizers.h array.$(OBJEXT): $(top_srcdir)/internal/serial.h array.$(OBJEXT): $(top_srcdir)/internal/set_table.h array.$(OBJEXT): $(top_srcdir)/internal/static_assert.h +array.$(OBJEXT): $(top_srcdir)/internal/struct.h array.$(OBJEXT): $(top_srcdir)/internal/variable.h array.$(OBJEXT): $(top_srcdir)/internal/vm.h array.$(OBJEXT): $(top_srcdir)/internal/warnings.h diff --git a/gc.c b/gc.c index f0ec0f79efe692..7d65f1152a316f 100644 --- a/gc.c +++ b/gc.c @@ -1056,7 +1056,15 @@ rb_newobj(rb_execution_context_t *ec, VALUE klass, VALUE flags, shape_id_t shape VALUE rb_ec_newobj_of(rb_execution_context_t *ec, VALUE klass, VALUE flags, size_t size) { - return rb_newobj(ec, klass, flags, ROOT_SHAPE_ID, true, size); + VALUE type = flags & T_MASK; + RUBY_ASSERT(type != T_OBJECT); + RUBY_ASSERT(type != T_DATA); + RUBY_ASSERT(type != T_CLASS); + RUBY_ASSERT(type != T_MODULE); + RUBY_ASSERT(type != T_ICLASS); + (void)type; + + return rb_newobj(ec, klass, flags, ROOT_SHAPE_ID | SHAPE_ID_LAYOUT_OTHER, true, size); } VALUE @@ -1068,13 +1076,13 @@ rb_newobj_of_with_shape(VALUE klass, VALUE flags, shape_id_t shape_id, size_t si VALUE rb_newobj_of(VALUE klass, VALUE flags, size_t size) { - return rb_newobj(GET_EC(), klass, flags, ROOT_SHAPE_ID, true, size); + return rb_newobj(GET_EC(), klass, flags, ROOT_SHAPE_ID | SHAPE_ID_LAYOUT_OTHER, true, size); } static VALUE class_allocate_complex_instance(VALUE klass, uint32_t capacity) { - shape_id_t initial_shape_id = rb_shape_root(rb_gc_heap_id_for_size(sizeof(struct RObject))); + shape_id_t initial_shape_id = rb_shape_id_with_robject_layout(rb_shape_root(rb_gc_heap_id_for_size(sizeof(struct RObject)))); VALUE obj = rb_newobj_of_with_shape(klass, T_OBJECT, initial_shape_id, sizeof(struct RObject)); rb_obj_init_complex(obj, rb_st_init_numtable_with_size(capacity)); return obj; @@ -1099,7 +1107,7 @@ rb_class_allocate_instance(VALUE klass) // There might be a NEWOBJ tracepoint callback, and it may set fields. // So the shape must be passed to `NEWOBJ_OF`. - obj = rb_newobj_of_with_shape(klass, T_OBJECT, rb_shape_root(rb_gc_heap_id_for_size(size)), size); + obj = rb_newobj_of_with_shape(klass, T_OBJECT, rb_shape_id_with_robject_layout(rb_shape_root(rb_gc_heap_id_for_size(size))), size); #if RUBY_DEBUG VALUE *ptr = ROBJECT_FIELDS(obj); @@ -1149,7 +1157,7 @@ typed_data_alloc(VALUE klass, VALUE typed_flag, void *datap, const rb_data_type_ RBIMPL_NONNULL_ARG(type); if (klass) rb_data_object_check(klass); bool wb_protected = (type->flags & RUBY_FL_WB_PROTECTED) || !type->function.dmark; - VALUE obj = rb_newobj(GET_EC(), klass, T_DATA, ROOT_SHAPE_ID, wb_protected, size); + VALUE obj = rb_newobj(GET_EC(), klass, T_DATA, ROOT_SHAPE_ID | SHAPE_ID_LAYOUT_RDATA, wb_protected, size); rb_gc_register_pinning_obj(obj); @@ -4188,7 +4196,11 @@ vm_weak_table_gen_fields_foreach(st_data_t key, st_data_t value, st_data_t data) break; case ST_DELETE: - RBASIC_SET_SHAPE_ID((VALUE)key, ROOT_SHAPE_ID); + // When we're removing an object from the weak ref table, we need to + // set the shape on it so that the GC finalizer won't try to remove + // it again. A "root shape" indicates to the GC that this object + // has no fields on it, hence it won't be in the gen fields table. + RBASIC_SET_SHAPE_ID((VALUE)key, ROOT_SHAPE_ID | SHAPE_ID_LAYOUT_OTHER); return ST_DELETE; case ST_REPLACE: { diff --git a/imemo.c b/imemo.c index 3448a8dcd3c54f..796e078c89565f 100644 --- a/imemo.c +++ b/imemo.c @@ -141,7 +141,11 @@ rb_imemo_fields_new(VALUE owner, shape_id_t shape_id, bool shareable) size_t embedded_size = offsetof(struct rb_fields, as.embed) + capa * sizeof(VALUE); RUBY_ASSERT(rb_gc_size_allocatable_p(embedded_size)); VALUE fields = rb_imemo_new(imemo_fields, owner, embedded_size, shareable); - RBASIC_SET_SHAPE_ID(fields, shape_id); + // imemo fields objects should always have "RObject" layout. The + // layout in the shape describes the layout of the thing on which it is set. + // Imemo fields have the same layout as robject, therefore the layout + // should reflect that fact. + RBASIC_SET_SHAPE_ID(fields, rb_shape_id_with_robject_layout(shape_id)); RUBY_ASSERT(IMEMO_TYPE_P(fields, imemo_fields)); return fields; } @@ -152,7 +156,11 @@ rb_imemo_fields_new_complex(VALUE owner, shape_id_t shape_id, size_t capa, bool VALUE fields = rb_imemo_new(imemo_fields, owner, sizeof(struct rb_fields), shareable); IMEMO_OBJ_FIELDS(fields)->as.complex.table = st_init_numtable_with_size(capa); FL_SET_RAW(fields, OBJ_FIELD_HEAP); - RBASIC_SET_SHAPE_ID(fields, shape_id); + // imemo fields objects should always have "RObject" layout. The + // layout in the shape describes the layout of the thing on which it is set. + // Imemo fields have the same layout as robject, therefore the layout + // should reflect that fact. + RBASIC_SET_SHAPE_ID(fields, rb_shape_id_with_robject_layout(shape_id)); return fields; } @@ -177,7 +185,11 @@ rb_imemo_fields_new_complex_tbl(VALUE owner, shape_id_t shape_id, st_table *tbl, VALUE fields = rb_imemo_new(imemo_fields, owner, sizeof(struct rb_fields), shareable); IMEMO_OBJ_FIELDS(fields)->as.complex.table = tbl; FL_SET_RAW(fields, OBJ_FIELD_HEAP); - RBASIC_SET_SHAPE_ID(fields, shape_id); + // imemo fields objects should always have "RObject" layout. The + // layout in the shape describes the layout of the thing on which it is set. + // Imemo fields have the same layout as robject, therefore the layout + // should reflect that fact. + RBASIC_SET_SHAPE_ID(fields, rb_shape_id_with_robject_layout(shape_id)); st_foreach(tbl, imemo_fields_trigger_wb_i, (st_data_t)fields); return fields; } diff --git a/internal/gc.h b/internal/gc.h index ee2a0c28050a8a..8b136e6572022c 100644 --- a/internal/gc.h +++ b/internal/gc.h @@ -124,7 +124,7 @@ struct rb_objspace; /* in vm_core.h */ T *(var) = (T *)rb_ec_newobj_of((ec), (c), (f), s) #define NEWOBJ_OF(var, T, c, f, s) EC_NEWOBJ_OF(var, T, c, f, s, GET_EC()) #define UNPROTECTED_NEWOBJ_OF(var, T, c, f, s) \ - T *(var) = (T *)rb_newobj((GET_EC()), (c), (f), 0 /* ROOT_SHAPE_ID */, false, s) + T *(var) = (T *)rb_newobj((GET_EC()), (c), (f), ROOT_SHAPE_ID | SHAPE_ID_LAYOUT_OTHER, false, s) #ifndef RB_GC_OBJECT_METADATA_ENTRY_DEFINED # define RB_GC_OBJECT_METADATA_ENTRY_DEFINED diff --git a/ractor.c b/ractor.c index d611ca97c273df..f94c06cc738bf4 100644 --- a/ractor.c +++ b/ractor.c @@ -2015,7 +2015,7 @@ move_enter(VALUE obj, struct obj_traverse_replace_data *data) else { VALUE type = RB_BUILTIN_TYPE(obj); size_t slot_size = rb_gc_obj_slot_size(obj); - VALUE moved = rb_newobj(GET_EC(), 0, type, 0, wb_protected_types[type], slot_size); + VALUE moved = rb_newobj(GET_EC(), 0, type, RBASIC_SHAPE_ID(obj), wb_protected_types[type], slot_size); MEMZERO(((struct RBasic *)moved) + 1, char, slot_size - sizeof(struct RBasic)); data->replacement = (VALUE)moved; return traverse_cont; diff --git a/shape.c b/shape.c index 24f1394f6cd32f..7a02b230733043 100644 --- a/shape.c +++ b/shape.c @@ -409,10 +409,14 @@ rb_obj_shape_id(VALUE obj) if (BUILTIN_TYPE(obj) == T_CLASS || BUILTIN_TYPE(obj) == T_MODULE) { VALUE fields_obj = RCLASS_WRITABLE_FIELDS_OBJ(obj); + shape_id_t base = ROOT_SHAPE_ID; if (fields_obj) { - return RBASIC_SHAPE_ID(fields_obj); + // Remove the layout from the fields object. We want to + // combine the shape of the fields object with the layout of the + // class / module object. + base = RBASIC_SHAPE_ID(fields_obj) & ~SHAPE_ID_LAYOUT_MASK; } - return ROOT_SHAPE_ID; + return rb_shape_layout(RBASIC_SHAPE_ID(obj)) | base; } return RBASIC_SHAPE_ID(obj); } @@ -697,7 +701,7 @@ rb_shape_transition_object_id(shape_id_t original_shape_id) bool dont_care; rb_shape_t *shape = get_next_shape_internal(RSHAPE(original_shape_id), id_object_id, SHAPE_OBJ_ID, &dont_care, true); if (!shape) { - return ROOT_COMPLEX_WITH_OBJ_ID | RSHAPE_FLAGS(original_shape_id); + return rb_shape_layout(original_shape_id) | ROOT_COMPLEX_WITH_OBJ_ID | RSHAPE_FLAGS(original_shape_id); } RUBY_ASSERT(shape); @@ -1225,17 +1229,55 @@ rb_shape_foreach_field(shape_id_t initial_shape_id, rb_shape_foreach_transition_ } #if RUBY_DEBUG +/* + * Get the layout of this object. The "layout" indicates what strategy + * we should use for fetching instance variables from `obj`. It's based + * on the C struct layout for each particular object. + * + * TODO: make Struct have a similar layout to RDATA + */ +static shape_id_t +rb_shape_expected_layout(VALUE obj) +{ + switch (BUILTIN_TYPE(obj)) { + case T_OBJECT: + return SHAPE_ID_LAYOUT_ROBJECT; + case T_CLASS: + case T_MODULE: + if (FL_TEST_RAW(obj, RCLASS_BOXABLE)) { + return SHAPE_ID_LAYOUT_OTHER; + } + return SHAPE_ID_LAYOUT_RCLASS; + case T_DATA: + return SHAPE_ID_LAYOUT_RDATA; + case T_IMEMO: + if (IMEMO_TYPE_P(obj, imemo_fields)) { + return SHAPE_ID_LAYOUT_ROBJECT; + } + return SHAPE_ID_LAYOUT_OTHER; + default: + return SHAPE_ID_LAYOUT_OTHER; + } +} + bool rb_shape_verify_consistency(VALUE obj, shape_id_t shape_id) { - if (shape_id == ROOT_SHAPE_ID) { - return true; - } - if (shape_id == INVALID_SHAPE_ID) { rb_bug("Can't set INVALID_SHAPE_ID on an object"); } + shape_id_t actual_layout = rb_shape_layout(rb_obj_shape_id(obj)); + shape_id_t expected_layout = rb_shape_expected_layout(obj); + if (actual_layout != expected_layout) { + rb_bug("shape_id layout mismatch: expected=%x actual=%x shape_id=%u obj=%s", + expected_layout, actual_layout, shape_id, rb_obj_info(obj)); + } + + if (shape_id == ROOT_SHAPE_ID) { + return true; + } + rb_shape_t *shape = RSHAPE(shape_id); bool has_object_id = false; @@ -1249,13 +1291,11 @@ rb_shape_verify_consistency(VALUE obj, shape_id_t shape_id) if (rb_shape_has_object_id(shape_id)) { if (!has_object_id) { - rb_p(obj); rb_bug("shape_id claim having obj_id but doesn't shape_id=%u, obj=%s", shape_id, rb_obj_info(obj)); } } else { if (has_object_id) { - rb_p(obj); rb_bug("shape_id claim not having obj_id but it does shape_id=%u, obj=%s", shape_id, rb_obj_info(obj)); } } @@ -1329,6 +1369,25 @@ shape_has_object_id_p(VALUE self) return RBOOL(rb_shape_has_object_id(shape_id)); } +static VALUE +shape_layout(VALUE self) +{ + shape_id_t shape_id = NUM2UINT(rb_struct_getmember(self, rb_intern("id"))); + + switch (rb_shape_layout(shape_id)) { + case SHAPE_ID_LAYOUT_ROBJECT: + return ID2SYM(rb_intern("robject")); + case SHAPE_ID_LAYOUT_RCLASS: + return ID2SYM(rb_intern("rclass")); + case SHAPE_ID_LAYOUT_RDATA: + return ID2SYM(rb_intern("rdata")); + case SHAPE_ID_LAYOUT_OTHER: + return ID2SYM(rb_intern("other")); + default: + rb_bug("unknown shape layout: %u", rb_shape_layout(shape_id)); + } +} + static VALUE parse_key(ID key) { @@ -1628,6 +1687,7 @@ Init_shape(void) rb_define_method(rb_cShape, "complex?", shape_complex, 0); rb_define_method(rb_cShape, "shape_frozen?", shape_frozen, 0); rb_define_method(rb_cShape, "has_object_id?", shape_has_object_id_p, 0); + rb_define_method(rb_cShape, "layout", shape_layout, 0); rb_define_const(rb_cShape, "SHAPE_ROOT", INT2NUM(SHAPE_ROOT)); rb_define_const(rb_cShape, "SHAPE_IVAR", INT2NUM(SHAPE_IVAR)); diff --git a/shape.h b/shape.h index 61fadca5bace2d..a319449988e8fc 100644 --- a/shape.h +++ b/shape.h @@ -27,12 +27,14 @@ STATIC_ASSERT(shape_id_num_bits, SHAPE_ID_NUM_BITS == sizeof(shape_id_t) * CHAR_ // 19-22 SHAPE_ID_HEAP_INDEX_MASK // index in rb_shape_tree.capacities. Allow to access slot size. // Currently always 0 except for T_OBJECT. -// 23 SHAPE_ID_FL_FROZEN +// 23 SHAPE_ID_FL_COMPLEX +// The object is backed by a `st_table`. +// 24 SHAPE_ID_FL_FROZEN // Whether the object is frozen or not. -// 24 SHAPE_ID_FL_HAS_OBJECT_ID +// 25 SHAPE_ID_FL_HAS_OBJECT_ID // Whether the object has an `SHAPE_OBJ_ID` transition. -// 25 SHAPE_ID_FL_COMPLEX -// The object is backed by a `st_table`. +// 26-27 SHAPE_ID_LAYOUT_MASK +// The object's physical field layout. enum shape_id_fl_type { #define RBIMPL_SHAPE_ID_FL(n) (1<<(SHAPE_ID_FL_USHIFT+n)) @@ -43,8 +45,26 @@ enum shape_id_fl_type { SHAPE_ID_FL_FROZEN = RBIMPL_SHAPE_ID_FL(1), SHAPE_ID_FL_HAS_OBJECT_ID = RBIMPL_SHAPE_ID_FL(2), + // Means IVs are found at an offset from the object's addr, or in a + // malloc allocated side table + SHAPE_ID_LAYOUT_ROBJECT = 0, + + // Means this object is a class/module that is NOT RCLASS_BOXABLE, and IV's + // are found in the fields_obj found on the rclass struct + SHAPE_ID_LAYOUT_RCLASS = RBIMPL_SHAPE_ID_FL(3), + + // Means this object is an RData or RTypedData and IVs are found in the + // fields_obj found on the RData/RTypedData struct + SHAPE_ID_LAYOUT_RDATA = RBIMPL_SHAPE_ID_FL(4), + + // Means this is a complicated object: boxable classes, structs, objects + // that store IVs on the geniv table + SHAPE_ID_LAYOUT_OTHER = SHAPE_ID_LAYOUT_RCLASS | SHAPE_ID_LAYOUT_RDATA, + + SHAPE_ID_LAYOUT_MASK = SHAPE_ID_LAYOUT_OTHER, + SHAPE_ID_FL_NON_CANONICAL_MASK = SHAPE_ID_FL_FROZEN | SHAPE_ID_FL_HAS_OBJECT_ID, - SHAPE_ID_FLAGS_MASK = SHAPE_ID_HEAP_INDEX_MASK | SHAPE_ID_FL_NON_CANONICAL_MASK | SHAPE_ID_FL_COMPLEX, + SHAPE_ID_FLAGS_MASK = SHAPE_ID_HEAP_INDEX_MASK | SHAPE_ID_FL_NON_CANONICAL_MASK | SHAPE_ID_FL_COMPLEX | SHAPE_ID_LAYOUT_MASK, #undef RBIMPL_SHAPE_ID_FL }; @@ -55,12 +75,13 @@ enum shape_id_mask { SHAPE_ID_HAS_IVAR_MASK = SHAPE_ID_FL_COMPLEX | (SHAPE_ID_OFFSET_MASK - 1), }; -// The interpreter doesn't care about frozen status, slot size or object id when reading ivars. +// The interpreter doesn't care about frozen status, slot size, or object id, and +// has its own checks for physical field layout when reading ivars. // So we normalize shape_id by clearing these bits to improve cache hits. // JITs however might care about some of it. -#define SHAPE_ID_READ_ONLY_MASK (~(SHAPE_ID_FL_FROZEN | SHAPE_ID_HEAP_INDEX_MASK | SHAPE_ID_FL_HAS_OBJECT_ID)) +#define SHAPE_ID_READ_ONLY_MASK (~(SHAPE_ID_FL_FROZEN | SHAPE_ID_HEAP_INDEX_MASK | SHAPE_ID_FL_HAS_OBJECT_ID | SHAPE_ID_LAYOUT_MASK)) // For write it's the same idea, but here we do care about frozen status. -#define SHAPE_ID_WRITE_MASK (~(SHAPE_ID_HEAP_INDEX_MASK | SHAPE_ID_FL_HAS_OBJECT_ID)) +#define SHAPE_ID_WRITE_MASK (~(SHAPE_ID_HEAP_INDEX_MASK | SHAPE_ID_FL_HAS_OBJECT_ID | SHAPE_ID_LAYOUT_MASK)) typedef uint32_t redblack_id_t; @@ -153,11 +174,18 @@ RBASIC_SET_SHAPE_ID_NO_CHECKS(VALUE obj, shape_id_t shape_id) #endif } +static inline shape_id_t +rb_shape_layout(shape_id_t shape_id) +{ + return shape_id & SHAPE_ID_LAYOUT_MASK; +} + static inline void RBASIC_SET_SHAPE_ID(VALUE obj, shape_id_t shape_id) { RUBY_ASSERT(!RB_SPECIAL_CONST_P(obj)); RUBY_ASSERT(!RB_TYPE_P(obj, T_IMEMO) || IMEMO_TYPE_P(obj, imemo_fields)); + RUBY_ASSERT(!IMEMO_TYPE_P(obj, imemo_fields) || rb_shape_layout(shape_id) == SHAPE_ID_LAYOUT_ROBJECT); RBASIC_SET_SHAPE_ID_NO_CHECKS(obj, shape_id); @@ -232,6 +260,12 @@ rb_shape_canonical_p(shape_id_t shape_id) return !(shape_id & SHAPE_ID_FL_NON_CANONICAL_MASK); } +static inline shape_id_t +rb_shape_id_with_robject_layout(shape_id_t shape_id) +{ + return (shape_id & ~SHAPE_ID_LAYOUT_MASK) | SHAPE_ID_LAYOUT_ROBJECT; +} + static inline uint8_t rb_shape_heap_index(shape_id_t shape_id) { @@ -450,10 +484,10 @@ rb_shape_transition_frozen(shape_id_t shape_id) static inline shape_id_t rb_shape_transition_complex(shape_id_t shape_id) { - shape_id_t next_shape_id = ROOT_COMPLEX_SHAPE_ID; + shape_id_t next_shape_id = rb_shape_layout(shape_id) | ROOT_COMPLEX_SHAPE_ID; if (rb_shape_has_object_id(shape_id)) { - next_shape_id = ROOT_COMPLEX_WITH_OBJ_ID; + next_shape_id = rb_shape_layout(shape_id) | ROOT_COMPLEX_WITH_OBJ_ID; } uint8_t heap_index = rb_shape_heap_index(shape_id); diff --git a/string.c b/string.c index 6865b0d8e658f3..61d4f16679d26b 100644 --- a/string.c +++ b/string.c @@ -630,7 +630,7 @@ static VALUE setup_fake_str(struct RString *fake_str, const char *name, long len, int encidx) { fake_str->basic.flags = T_STRING|RSTRING_NOEMBED|STR_NOFREE|STR_FAKESTR; - RBASIC_SET_SHAPE_ID((VALUE)fake_str, ROOT_SHAPE_ID); + RBASIC_SET_SHAPE_ID((VALUE)fake_str, ROOT_SHAPE_ID | SHAPE_ID_LAYOUT_OTHER); if (!name) { RUBY_ASSERT_ALWAYS(len == 0); diff --git a/test/ruby/test_shapes.rb b/test/ruby/test_shapes.rb index ef5dbd9fb15ee5..bace69658adfb5 100644 --- a/test/ruby/test_shapes.rb +++ b/test/ruby/test_shapes.rb @@ -1067,6 +1067,37 @@ def test_new_obj_has_t_object_shape assert_nil shape.parent end + def test_shape_layout + assert_equal :robject, RubyVM::Shape.of(TestObject.new).layout + + if ENV["RUBY_BOX"] + assert_equal :other, RubyVM::Shape.of(Kernel).layout + assert_equal :other, RubyVM::Shape.of(String).layout + else + assert_equal :rclass, RubyVM::Shape.of(Kernel).layout + assert_equal :rclass, RubyVM::Shape.of(String).layout + end + + assert_equal :rclass, RubyVM::Shape.of(Class.new).layout + assert_equal :rclass, RubyVM::Shape.of(Module.new).layout + + klass = Class.new + assert_equal :rclass, RubyVM::Shape.of(klass).layout + klass.instance_variable_set(:@a, 123) + assert_equal :rclass, RubyVM::Shape.of(klass).layout + + assert_equal :rdata, RubyVM::Shape.of(Thread.current).layout + assert_equal :rdata, RubyVM::Shape.of(lambda {}).layout + + assert_equal :other, RubyVM::Shape.of(Struct.new(:x).new(1)).layout + assert_equal :other, RubyVM::Shape.of([]).layout + assert_equal :other, RubyVM::Shape.of("hello").layout + assert_equal :other, RubyVM::Shape.of(/foo/).layout + assert_equal :other, RubyVM::Shape.of(2..3).layout + assert_equal :other, RubyVM::Shape.of(2**67).layout + assert_equal :other, RubyVM::Shape.of(:"aaroniscool#{123}").layout + end + def test_str_has_root_shape assert_shape_equal(RubyVM::Shape.root_shape, RubyVM::Shape.of("")) end diff --git a/variable.c b/variable.c index 857d870413881f..687fa03631b6bb 100644 --- a/variable.c +++ b/variable.c @@ -1343,7 +1343,7 @@ rb_free_generic_ivar(VALUE obj) } } } - RBASIC_SET_SHAPE_ID(obj, ROOT_SHAPE_ID); + RBASIC_SET_SHAPE_ID(obj, rb_shape_layout(RBASIC_SHAPE_ID(obj)) | ROOT_SHAPE_ID); } } @@ -1395,7 +1395,7 @@ rb_obj_set_fields(VALUE obj, VALUE fields_obj, ID field_name, VALUE original_fie } } - RBASIC_SET_SHAPE_ID(obj, RBASIC_SHAPE_ID(fields_obj)); + RBASIC_SET_SHAPE_ID(obj, rb_shape_layout(RBASIC_SHAPE_ID(obj)) | RBASIC_SHAPE_ID(fields_obj)); } void @@ -1710,6 +1710,7 @@ rb_ivar_delete(VALUE obj, ID id, VALUE undef) size_t trailing_fields = new_fields_count - removed_index; MEMMOVE(&fields[removed_index], &fields[removed_index + 1], VALUE, trailing_fields); + RUBY_ASSERT(rb_shape_layout(next_shape_id) == SHAPE_ID_LAYOUT_ROBJECT); RBASIC_SET_SHAPE_ID(fields_obj, next_shape_id); if (FL_TEST_RAW(fields_obj, OBJ_FIELD_HEAP)) { @@ -1732,7 +1733,7 @@ rb_ivar_delete(VALUE obj, ID id, VALUE undef) } } - RBASIC_SET_SHAPE_ID(obj, next_shape_id); + RBASIC_SET_SHAPE_ID(obj, rb_shape_layout(RBASIC_SHAPE_ID(obj)) | next_shape_id); if (fields_obj != original_fields_obj) { switch (type) { case T_OBJECT: @@ -1843,7 +1844,7 @@ imemo_fields_set(VALUE owner, VALUE fields_obj, shape_id_t target_shape_id, ID f RUBY_ASSERT(field_name); st_insert(table, (st_data_t)field_name, (st_data_t)val); RB_OBJ_WRITTEN(fields_obj, Qundef, val); - RBASIC_SET_SHAPE_ID(fields_obj, target_shape_id); + RBASIC_SET_SHAPE_ID(fields_obj, rb_shape_id_with_robject_layout(target_shape_id)); } else { attr_index_t index = RSHAPE_INDEX(target_shape_id); @@ -1855,7 +1856,7 @@ imemo_fields_set(VALUE owner, VALUE fields_obj, shape_id_t target_shape_id, ID f RB_OBJ_WRITE(fields_obj, &table[index], val); if (index >= RSHAPE_LEN(current_shape_id)) { - RBASIC_SET_SHAPE_ID(fields_obj, target_shape_id); + RBASIC_SET_SHAPE_ID(fields_obj, rb_shape_id_with_robject_layout(target_shape_id)); } } @@ -2010,11 +2011,12 @@ rb_obj_freeze_inline(VALUE x) RB_FL_UNSET_RAW(x, FL_USER2 | FL_USER3); // STR_CHILLED } + // rb_obj_freeze_inline(String) shape_id_t shape_id = rb_obj_shape_transition_frozen(x); switch (BUILTIN_TYPE(x)) { case T_CLASS: case T_MODULE: - RBASIC_SET_SHAPE_ID(RCLASS_WRITABLE_ENSURE_FIELDS_OBJ(x), shape_id); + rb_obj_freeze_inline(RCLASS_WRITABLE_ENSURE_FIELDS_OBJ(x)); // FIXME: How to do multi-shape? RBASIC_SET_SHAPE_ID(x, shape_id); break; @@ -2303,7 +2305,7 @@ rb_copy_generic_ivar(VALUE dest, VALUE obj) } if (!RSHAPE_LEN(dest_shape_id)) { - RBASIC_SET_SHAPE_ID(dest, dest_shape_id); + RBASIC_SET_SHAPE_ID(dest, rb_shape_layout(RBASIC_SHAPE_ID(dest)) | dest_shape_id); return; } @@ -4636,6 +4638,7 @@ class_fields_ivar_set(VALUE klass, VALUE fields_obj, ID id, VALUE val, bool conc } if (new_ivar) { + RUBY_ASSERT(rb_shape_layout(next_shape_id) == SHAPE_ID_LAYOUT_ROBJECT); RBASIC_SET_SHAPE_ID(fields_obj, next_shape_id); } } @@ -4658,6 +4661,7 @@ class_fields_ivar_set(VALUE klass, VALUE fields_obj, ID id, VALUE val, bool conc RB_OBJ_WRITTEN(fields_obj, Qundef, val); if (fields_obj != original_fields_obj) { + RUBY_ASSERT(rb_shape_layout(next_shape_id) == SHAPE_ID_LAYOUT_ROBJECT); RBASIC_SET_SHAPE_ID(fields_obj, next_shape_id); } } @@ -4684,7 +4688,7 @@ class_ivar_set(VALUE obj, ID id, VALUE val, bool *new_ivar) // TODO: What should we set as the T_CLASS shape_id? // In most case we can replicate the single `fields_obj` shape // but in namespaced case? Perhaps INVALID_SHAPE_ID? - RBASIC_SET_SHAPE_ID(obj, RBASIC_SHAPE_ID(new_fields_obj)); + RBASIC_SET_SHAPE_ID(obj, rb_shape_layout(RBASIC_SHAPE_ID(obj)) | RBASIC_SHAPE_ID(new_fields_obj)); return index; } @@ -4709,7 +4713,7 @@ rb_fields_tbl_copy(VALUE dst, VALUE src) VALUE fields_obj = RCLASS_WRITABLE_FIELDS_OBJ(src); if (fields_obj) { RCLASS_WRITABLE_SET_FIELDS_OBJ(dst, rb_imemo_fields_clone(fields_obj)); - RBASIC_SET_SHAPE_ID(dst, RBASIC_SHAPE_ID(src)); + RBASIC_SET_SHAPE_ID(dst, rb_shape_layout(RBASIC_SHAPE_ID(dst)) | RBASIC_SHAPE_ID(src)); } } diff --git a/vm_insnhelper.c b/vm_insnhelper.c index ac0d81092d890d..f515662bf07765 100644 --- a/vm_insnhelper.c +++ b/vm_insnhelper.c @@ -1412,7 +1412,8 @@ vm_setivar_class(VALUE obj, VALUE val, rb_setivar_cache cache) RB_OBJ_WRITE(fields_obj, &rb_imemo_fields_ptr(fields_obj)[cache.index], val); if (shape_id != dest_shape_id) { - RBASIC_SET_SHAPE_ID(obj, dest_shape_id); + // The dest_shape_id comes from the fields_obj + RBASIC_SET_SHAPE_ID(obj, SHAPE_ID_LAYOUT_RCLASS | (dest_shape_id & ~SHAPE_ID_LAYOUT_MASK)); RBASIC_SET_SHAPE_ID(fields_obj, dest_shape_id); } @@ -1437,7 +1438,9 @@ vm_setivar_default(VALUE obj, ID id, VALUE val, rb_setivar_cache cache) if (shape_id != dest_shape_id) { RBASIC_SET_SHAPE_ID(obj, dest_shape_id); - RBASIC_SET_SHAPE_ID(fields_obj, dest_shape_id); + // The dest_shape_id comes from the owner, but fields_obj must always + // have layout RObject, so give the fields_object the right layout. + RBASIC_SET_SHAPE_ID(fields_obj, rb_shape_id_with_robject_layout(dest_shape_id)); } RB_DEBUG_COUNTER_INC(ivar_set_ic_hit); diff --git a/zjit/src/cruby_bindings.inc.rs b/zjit/src/cruby_bindings.inc.rs index bd832acc9604cd..08c502b0d84e47 100644 --- a/zjit/src/cruby_bindings.inc.rs +++ b/zjit/src/cruby_bindings.inc.rs @@ -1491,8 +1491,13 @@ pub const SHAPE_ID_HEAP_INDEX_MASK: shape_id_fl_type = 7864320; pub const SHAPE_ID_FL_COMPLEX: shape_id_fl_type = 8388608; pub const SHAPE_ID_FL_FROZEN: shape_id_fl_type = 16777216; pub const SHAPE_ID_FL_HAS_OBJECT_ID: shape_id_fl_type = 33554432; +pub const SHAPE_ID_LAYOUT_ROBJECT: shape_id_fl_type = 0; +pub const SHAPE_ID_LAYOUT_RCLASS: shape_id_fl_type = 67108864; +pub const SHAPE_ID_LAYOUT_RDATA: shape_id_fl_type = 134217728; +pub const SHAPE_ID_LAYOUT_OTHER: shape_id_fl_type = 201326592; +pub const SHAPE_ID_LAYOUT_MASK: shape_id_fl_type = 201326592; pub const SHAPE_ID_FL_NON_CANONICAL_MASK: shape_id_fl_type = 50331648; -pub const SHAPE_ID_FLAGS_MASK: shape_id_fl_type = 66584576; +pub const SHAPE_ID_FLAGS_MASK: shape_id_fl_type = 267911168; pub type shape_id_fl_type = u32; pub const CONST_DEPRECATED: rb_const_flag_t = 256; pub const CONST_VISIBILITY_MASK: rb_const_flag_t = 255; From 9c62016982cd18d39853c2f00d8374ea2c6709cf Mon Sep 17 00:00:00 2001 From: B6 <52599949+a5-stable@users.noreply.github.com> Date: Tue, 2 Jun 2026 04:55:37 +0900 Subject: [PATCH 027/188] ZJIT: Fold arithmetic identity operations (#17131) Fold arithmetic identity operations such as `x + 0`, `x - 0`, `x * 1`, `x / 1` in `fold_constants`. --- zjit/src/hir.rs | 18 ++++++ zjit/src/hir/opt_tests.rs | 128 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 142 insertions(+), 4 deletions(-) diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 6911129ad34896..c2ae897d9aa9cb 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -5195,18 +5195,32 @@ impl Function { } } Insn::FixnumAdd { left, right, .. } => { + match (self.type_of(left).fixnum_value(), self.type_of(right).fixnum_value()) { + (Some(0), _) => { self.make_equal_to(insn_id, right); continue; } + (_, Some(0)) => { self.make_equal_to(insn_id, left); continue; } + _ => {} + } self.fold_fixnum_bop(insn_id, left, right, |l, r| match (l, r) { (Some(l), Some(r)) => l.checked_add(r), _ => None, }) } Insn::FixnumSub { left, right, .. } => { + match (self.type_of(left).fixnum_value(), self.type_of(right).fixnum_value()) { + (_, Some(0)) => { self.make_equal_to(insn_id, left); continue; } + _ => {} + } self.fold_fixnum_bop(insn_id, left, right, |l, r| match (l, r) { (Some(l), Some(r)) => l.checked_sub(r), _ => None, }) } Insn::FixnumMult { left, right, .. } => { + match (self.type_of(left).fixnum_value(), self.type_of(right).fixnum_value()) { + (Some(1), _) => { self.make_equal_to(insn_id, right); continue; } + (_, Some(1)) => { self.make_equal_to(insn_id, left); continue; } + _ => {} + } self.fold_fixnum_bop(insn_id, left, right, |l, r| match (l, r) { (Some(l), Some(r)) => l.checked_mul(r), (Some(0), _) | (_, Some(0)) => Some(0), @@ -5214,6 +5228,10 @@ impl Function { }) } Insn::FixnumDiv { left, right, .. } => { + match (self.type_of(left).fixnum_value(), self.type_of(right).fixnum_value()) { + (_, Some(1)) => { self.make_equal_to(insn_id, left); continue; } + _ => {} + } self.fold_fixnum_bop(insn_id, left, right, |l, r| match (l, r) { (Some(l), Some(r)) if l == (RUBY_FIXNUM_MIN as i64) && r == -1 => None, // Avoid Fixnum overflow (Some(_l), Some(r)) if r == 0 => None, // Avoid Divide by zero. diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index 74c0af710e7363..53f65db4ccd502 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -117,6 +117,36 @@ mod hir_opt_tests { "); } + #[test] + fn test_fold_fixnum_add_zero() { + eval(" + def test(n) + 0 + n + 0 + end + test 1; test 2 + "); + assert_snapshot!(hir_string("test"), @" + fn test@:3: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:CPtr = LoadSP + v3:BasicObject = LoadField v2, :n@0x1000 + Jump bb3(v1, v3) + bb2(): + EntryPoint JIT(0) + v6:BasicObject = LoadArg :self@0 + v7:BasicObject = LoadArg :n@1 + Jump bb3(v6, v7) + bb3(v9:BasicObject, v10:BasicObject): + v14:Fixnum[0] = Const Value(0) + PatchPoint MethodRedefined(Integer@0x1008, +@0x1010, cme:0x1018) + v32:Fixnum = GuardType v10, Fixnum + CheckInterrupts + Return v32 + "); + } + #[test] fn test_fold_fixnum_sub() { eval(" @@ -171,6 +201,36 @@ mod hir_opt_tests { "); } + #[test] + fn test_fold_fixnum_sub_zero() { + eval(" + def test(n) + n - 0 + end + test 1; test 2 + "); + assert_snapshot!(hir_string("test"), @" + fn test@:3: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:CPtr = LoadSP + v3:BasicObject = LoadField v2, :n@0x1000 + Jump bb3(v1, v3) + bb2(): + EntryPoint JIT(0) + v6:BasicObject = LoadArg :self@0 + v7:BasicObject = LoadArg :n@1 + Jump bb3(v6, v7) + bb3(v9:BasicObject, v10:BasicObject): + v15:Fixnum[0] = Const Value(0) + PatchPoint MethodRedefined(Integer@0x1008, -@0x1010, cme:0x1018) + v26:Fixnum = GuardType v10, Fixnum + CheckInterrupts + Return v26 + "); + } + #[test] fn test_fold_fixnum_mult() { eval(" @@ -226,9 +286,40 @@ mod hir_opt_tests { v46:Fixnum[0] = Const Value(0) v47:Fixnum[0] = Const Value(0) PatchPoint MethodRedefined(Integer@0x1008, +@0x1040, cme:0x1048) - v48:Fixnum[0] = Const Value(0) CheckInterrupts - Return v48 + Return v47 + "); + } + + #[test] + fn test_fold_fixnum_mult_one() { + eval(" + def test(n) + 1 * n + n * 1 + end + test 1; test 2 + "); + assert_snapshot!(hir_string("test"), @" + fn test@:3: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:CPtr = LoadSP + v3:BasicObject = LoadField v2, :n@0x1000 + Jump bb3(v1, v3) + bb2(): + EntryPoint JIT(0) + v6:BasicObject = LoadArg :self@0 + v7:BasicObject = LoadArg :n@1 + Jump bb3(v6, v7) + bb3(v9:BasicObject, v10:BasicObject): + v14:Fixnum[1] = Const Value(1) + PatchPoint MethodRedefined(Integer@0x1008, *@0x1010, cme:0x1018) + v36:Fixnum = GuardType v10, Fixnum + PatchPoint MethodRedefined(Integer@0x1008, +@0x1040, cme:0x1048) + v45:Fixnum = FixnumAdd v36, v36 + CheckInterrupts + Return v45 "); } @@ -340,6 +431,36 @@ mod hir_opt_tests { "); } + #[test] + fn test_fold_fixnum_div_one() { + eval(" + def test(n) + n / 1 + end + test 1; test 2 + "); + assert_snapshot!(hir_string("test"), @" + fn test@:3: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:CPtr = LoadSP + v3:BasicObject = LoadField v2, :n@0x1000 + Jump bb3(v1, v3) + bb2(): + EntryPoint JIT(0) + v6:BasicObject = LoadArg :self@0 + v7:BasicObject = LoadArg :n@1 + Jump bb3(v6, v7) + bb3(v9:BasicObject, v10:BasicObject): + v15:Fixnum[1] = Const Value(1) + PatchPoint MethodRedefined(Integer@0x1008, /@0x1010, cme:0x1018) + v26:Fixnum = GuardType v10, Fixnum + CheckInterrupts + Return v26 + "); + } + #[test] fn test_fold_fixnum_mod_zero_by_zero() { eval(" @@ -16519,9 +16640,8 @@ mod hir_opt_tests { v139:BasicObject = LoadField v138, :var@0x1040 PatchPoint MethodRedefined(Integer@0x1048, +@0x1050, cme:0x1058) v179:Fixnum = GuardType v139, Fixnum - v180:Fixnum = FixnumAdd v17, v179 PatchPoint NoEPEscape(test) - v185:Fixnum = FixnumAdd v180, v179 + v185:Fixnum = FixnumAdd v179, v179 v190:Fixnum = FixnumAdd v185, v179 v195:Fixnum = FixnumAdd v190, v179 v200:Fixnum = FixnumAdd v195, v179 From 3a5a46f8adee3ccdf5cd7de9b826e45f22dc321a Mon Sep 17 00:00:00 2001 From: Alan Wu Date: Fri, 29 May 2026 18:39:02 -0400 Subject: [PATCH 028/188] Avoid infinite recursion when raising SIGABRT in SIGABRT handler When VM state is corrupted enough, we can call abort() from the SIGABRT handler. Previously, we would spam until the stack is full: ABRT received in SEGV handler SEGV received in ABRT handler ABRT received in SEGV handler ABRT received in ABRT handler ABRT received in ABRT handler ABRT received in ABRT handler [...] We've seen this on CI: https://github.com/ruby/ruby/actions/runs/26591192708/job/78350130653 To test this situation locally, temporarily patch in a call to abort() in rb_bug_for_fatal_signal() then use `Process.kill(:ABRT, Process.pid)`. Fix by restoring the default signal handler before aborting. --- signal.c | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/signal.c b/signal.c index 5110ea4401a4be..f37aaf971ad997 100644 --- a/signal.c +++ b/signal.c @@ -1020,8 +1020,20 @@ check_reserved_signal_(const char *name, size_t name_len, int signo) #if __has_feature(address_sanitizer) || \ __has_feature(memory_sanitizer) || \ defined(HAVE_VALGRIND_MEMCHECK_H) - ruby_posix_signal(signo, SIG_DFL); +# define SANITIZING true +#else +# define SANITIZING false +#endif + +#ifdef SIGABRT +// Avoid infinite loop when already aborting +# define RECURSIVE (signo == SIGABRT) +#else +# define RECURSIVE false #endif + if (SANITIZING || RECURSIVE) ruby_signal(signo, SIG_DFL); +# undef SANITIZING +# undef RECURSIVE W(name, name_len); W(msg1, sizeof(msg1)); W(prev, strlen(prev)); From e8b8c4e7986ec1d45e1239f141607545c298c3ce Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Mon, 1 Jun 2026 20:45:21 +0900 Subject: [PATCH 029/188] [ruby/rubygems] Write vendored compact_index files atomically The completeness check skips the download once every expected file exists, but File.write truncates before writing, so an install interrupted partway leaves a half-written file in place. A later run would see it, treat the vendor tree as complete, and load a truncated source. Download each file to a sibling .tmp path and rename it into place. The rename stays within the vendor tree, so it is atomic and a target file is only ever observed fully written. https://github.com/ruby/rubygems/commit/a2f265f579 Co-Authored-By: Claude Opus 4.8 (1M context) --- spec/bundler/support/rubygems_ext.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/spec/bundler/support/rubygems_ext.rb b/spec/bundler/support/rubygems_ext.rb index 8e3d84212d31fd..812dc4deaa9cd1 100644 --- a/spec/bundler/support/rubygems_ext.rb +++ b/spec/bundler/support/rubygems_ext.rb @@ -110,7 +110,9 @@ def install_vendored_compact_index url = "https://raw.githubusercontent.com/rubygems/rubygems.org/#{ref}/#{path}" target = target_root.join(path) FileUtils.mkdir_p(File.dirname(target)) - File.write(target, URI.parse(url).open(&:read)) + tmp = "#{target}.tmp" + File.write(tmp, URI.parse(url).open(&:read)) + File.rename(tmp, target) end end end From ad14452e3bdcf255a217de7f8c64496e3428bb0f Mon Sep 17 00:00:00 2001 From: Alan Wu Date: Fri, 29 May 2026 19:23:18 -0400 Subject: [PATCH 030/188] [DOC] Description and soundness reasoning for `Primitive.rb_jit_ary_*` --- array.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/array.c b/array.c index 7f06e3c9c72f66..6720406d89b2f9 100644 --- a/array.c +++ b/array.c @@ -2684,6 +2684,12 @@ ary_enum_length(VALUE ary, VALUE args, VALUE eobj) return rb_ary_length(ary); } +// These array primitives enable tight compatibility with the C implementation +// in terms of what method calls happen. They can use unchecked utilities such as +// FIX2LONG since unlike userland Ruby code, these methods cannot be traced with +// TracePoint (or ruby/debug.h APIs) and have their local variables changed from +// underneath them. + // Return true if the index is at or past the end of the array. VALUE rb_jit_ary_at_end(rb_execution_context_t *ec, VALUE self, VALUE index) From 72e816289730c119e78b82e8098bf3bde6f61e74 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Mon, 1 Jun 2026 20:58:44 +0900 Subject: [PATCH 031/188] [ruby/rubygems] Scrub invalid bytes when normalizing command output When captured subprocess output contained a byte that isn't valid UTF-8, normalize tagged the buffer as UTF-8 and then ran gsub over it, which scans the whole string and raised Encoding::CompatibilityError: invalid byte sequence in UTF-8. The error surfaced from inside the test harness rather than on an assertion, so RSpec reported "Unable to find matching line from backtrace" and the real output was lost. Scrub the string after forcing the encoding so normalize never raises on malformed bytes, and dup it first so forcing the encoding no longer mutates the stored capture buffer. https://github.com/ruby/rubygems/commit/e236edcb6d Co-Authored-By: Claude Opus 4.8 (1M context) --- spec/bundler/support/command_execution.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/bundler/support/command_execution.rb b/spec/bundler/support/command_execution.rb index 979a46549a3a96..e2915b996d9d3d 100644 --- a/spec/bundler/support/command_execution.rb +++ b/spec/bundler/support/command_execution.rb @@ -72,7 +72,7 @@ def failure? attr_reader :failure_reason def normalize(string) - string.force_encoding(Encoding::UTF_8).strip.gsub("\r\n", "\n") + string.dup.force_encoding(Encoding::UTF_8).scrub.strip.gsub("\r\n", "\n") end end end From 1ca25b81abb29a9a44a80d16a73a865ac6546494 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 2 Jun 2026 06:46:14 +0900 Subject: [PATCH 032/188] [ruby/rubygems] Disable git auto maintenance during git source operations Installing a git source clones the local cache repo into bundler/gems with git's hardlink optimization. Git 2.54 kicks off commit-graph writes as part of automatic maintenance after a fetch or clone, and when that background write touches the cache while the hardlinking clone is enumerating it, the clone aborts with "hardlink different from source" pointing at a tmp_graph_* file. It shows up as a flaky failure on CI. Pass gc.auto=0 and maintenance.auto=false to every git invocation so no background maintenance is ever spawned for these throwaway repos, removing the race. The options are injected only at the command-execution layer, so redacted command strings in error messages are unaffected. https://github.com/ruby/rubygems/commit/7bc41a21fa Co-Authored-By: Claude Opus 4.8 (1M context) --- lib/bundler/source/git/git_proxy.rb | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/bundler/source/git/git_proxy.rb b/lib/bundler/source/git/git_proxy.rb index 72f7dc771038ac..8094dcaa9d94ff 100644 --- a/lib/bundler/source/git/git_proxy.rb +++ b/lib/bundler/source/git/git_proxy.rb @@ -432,9 +432,14 @@ def capture(cmd, dir, ignore_err: false) end def capture3_args_for(cmd, dir) - return ["git", *cmd] unless dir + # Disable automatic maintenance so a background commit-graph write in + # the source repo can't race the hardlinking local clone and fail with + # "hardlink different from source". + opts = ["-c", "gc.auto=0", "-c", "maintenance.auto=false"] - ["git", "-C", dir.to_s, *cmd] + return ["git", *opts, *cmd] unless dir + + ["git", "-C", dir.to_s, *opts, *cmd] end def extra_clone_args From cabe750b802727e0ea18839ec4c14047f6714532 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 2 Jun 2026 06:46:23 +0900 Subject: [PATCH 033/188] [ruby/rubygems] Assert webauthn request auth instead of last request test_with_webauthn_enabled_failure checked the Authorization header on @stub_fetcher.last_request, but the webauthn flow runs a real polling thread that issues its own request concurrently. On Rubies without a GIL the poll request can land last, so last_request was not the webauthn_verification request and the assertion saw a nil header. Look up the webauthn_verification request explicitly and assert on its Authorization header, which the main thread always records. This is the invariant the test means to check and it is no longer order dependent, so the TruffleRuby pend that papered over the same race can go too. https://github.com/ruby/rubygems/commit/98efee308a Co-Authored-By: Claude Opus 4.8 (1M context) --- test/rubygems/test_gem_commands_owner_command.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/rubygems/test_gem_commands_owner_command.rb b/test/rubygems/test_gem_commands_owner_command.rb index cf0fe521a21518..f6d4d03f84963a 100644 --- a/test/rubygems/test_gem_commands_owner_command.rb +++ b/test/rubygems/test_gem_commands_owner_command.rb @@ -399,7 +399,6 @@ def test_with_webauthn_enabled_success end def test_with_webauthn_enabled_failure - pend "Flaky on TruffleRuby" if RUBY_ENGINE == "truffleruby" response_success = "Owner added successfully." server = Gem::MockTCPServer.new error = Gem::WebauthnVerificationError.new("Something went wrong") @@ -417,7 +416,8 @@ def test_with_webauthn_enabled_failure end end - assert_match @stub_fetcher.last_request["Authorization"], Gem.configuration.rubygems_api_key + webauthn_verification_request = @stub_fetcher.requests.find {|req| req.path == "/api/v1/webauthn_verification" } + assert_match webauthn_verification_request["Authorization"], Gem.configuration.rubygems_api_key assert_match "You have enabled multi-factor authentication. Please visit the following URL " \ "to authenticate via security device. If you can't verify using WebAuthn but have OTP enabled, " \ "you can re-run the gem signin command with the `--otp [your_code]` option.", @stub_ui.output From 770ccaa6fb2425f560dd8e69b5f617a5ca2cf7cd Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 2 Jun 2026 08:19:07 +0900 Subject: [PATCH 034/188] [ruby/psych] Clamp io_reader copy to libyaml's buffer size io_reader trusted IO#read to honour its size argument and copied RSTRING_LEN(string) bytes into libyaml's fixed-capacity buffer. An IO-like object whose #read over-returns wrote past the buffer end, a heap out-of-bounds write reachable from Psych.load/safe_load/parse. Clamp the copied length to the requested size. https://github.com/ruby/psych/commit/99ecd94560 Co-Authored-By: Claude Opus 4.8 --- ext/psych/psych_parser.c | 11 +++++++++-- test/psych/test_parser.rb | 23 +++++++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/ext/psych/psych_parser.c b/ext/psych/psych_parser.c index d973496284f1d6..41637713930195 100644 --- a/ext/psych/psych_parser.c +++ b/ext/psych/psych_parser.c @@ -33,8 +33,15 @@ static int io_reader(void * data, unsigned char *buf, size_t size, size_t *read) if(! NIL_P(string)) { void * str = (void *)StringValuePtr(string); - *read = (size_t)RSTRING_LEN(string); - memcpy(buf, str, *read); + size_t len = (size_t)RSTRING_LEN(string); + + /* IO#read(size) is documented to return at most `size` bytes, but a + * misbehaving IO-like object may return more. Clamp the copy to the + * buffer libyaml gave us to avoid writing past its end. */ + if(len > size) len = size; + + *read = len; + memcpy(buf, str, len); } return 1; diff --git a/test/psych/test_parser.rb b/test/psych/test_parser.rb index c1e0abb89d0806..b5db4e96151f45 100644 --- a/test/psych/test_parser.rb +++ b/test/psych/test_parser.rb @@ -198,6 +198,29 @@ def test_parse_io assert_called :end_stream end + def test_parse_io_returns_more_bytes_than_requested + # An IO-like source whose #read returns more bytes than the size it was + # asked for must not overflow libyaml's read buffer. + io = Object.new + def io.external_encoding; Encoding::UTF_8 end + def io.read len + return nil if @done + @done = true + "--- a\n" + ("#" * (len + (1 << 20))) + end + + # CRuby clamps the over-read and parses; JRuby's parser rejects the + # over-reading IO with an IOError. Either way there is no overflow. + begin + @parser.parse io + rescue IOError + return + end + assert_called :start_stream + assert_called :scalar + assert_called :end_stream + end + def test_syntax_error assert_raise(Psych::SyntaxError) do @parser.parse("---\n\"foo\"\n\"bar\"\n") From 14bc2930e1cfbd8e6bd9985a47998583b0824d1f Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 2 Jun 2026 09:55:25 +0900 Subject: [PATCH 035/188] [ruby/psych] Round the io_reader clamp down to a character boundary Clamping the over-read at exactly the requested size could split a multibyte character, since the string an IO returns may carry a non-binary encoding. Round the cut down to the last character boundary at or before the size so the bytes handed to libyaml are always whole characters. https://github.com/ruby/psych/commit/6201ae17c1 Co-Authored-By: Claude Opus 4.8 --- ext/psych/psych_parser.c | 10 +++++++--- test/psych/test_parser.rb | 19 +++++++++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/ext/psych/psych_parser.c b/ext/psych/psych_parser.c index 41637713930195..00a2207b58911c 100644 --- a/ext/psych/psych_parser.c +++ b/ext/psych/psych_parser.c @@ -32,13 +32,17 @@ static int io_reader(void * data, unsigned char *buf, size_t size, size_t *read) *read = 0; if(! NIL_P(string)) { - void * str = (void *)StringValuePtr(string); + char * str = StringValuePtr(string); size_t len = (size_t)RSTRING_LEN(string); /* IO#read(size) is documented to return at most `size` bytes, but a * misbehaving IO-like object may return more. Clamp the copy to the - * buffer libyaml gave us to avoid writing past its end. */ - if(len > size) len = size; + * buffer libyaml gave us to avoid writing past its end, rounding down + * to a character boundary so a multibyte character is never split. */ + if(len > size) { + rb_encoding * enc = rb_enc_get(string); + len = (size_t)(rb_enc_left_char_head(str, str + size, str + len, enc) - str); + } *read = len; memcpy(buf, str, len); diff --git a/test/psych/test_parser.rb b/test/psych/test_parser.rb index b5db4e96151f45..4ca4d63d80c483 100644 --- a/test/psych/test_parser.rb +++ b/test/psych/test_parser.rb @@ -221,6 +221,25 @@ def io.read len assert_called :end_stream end + def test_parse_io_returns_more_bytes_than_requested_multibyte + # The over-read is rounded down to a character boundary so a multibyte + # character is never split when the copy is clamped. + io = Object.new + def io.external_encoding; Encoding::UTF_8 end + def io.read len + return nil if @done + @done = true + "--- a\n#" + ("あ" * (len + (1 << 20))) + end + + begin + @parser.parse io + rescue IOError + return + end + assert_called :scalar + end + def test_syntax_error assert_raise(Psych::SyntaxError) do @parser.parse("---\n\"foo\"\n\"bar\"\n") From aecfd0ae5be6f39482a88204b986e1b58ebf8448 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Tue, 2 Jun 2026 09:20:53 +0900 Subject: [PATCH 036/188] win32: Release crypt provider in end proc End procs are executed in reverse order of registration. Since the crypt provider is initialized very early on via `Init_RandomSeedCore`, no other end procs are executed after the crypt provider is released. Finalizers are called after end procs, but their order is unspecified, so there is still no guarantee that this crypt provider will be available for use by other end procs. --- random.c | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/random.c b/random.c index b6c96f1b4d25ff..fcbe3b3bade877 100644 --- a/random.c +++ b/random.c @@ -566,36 +566,29 @@ fill_random_bytes_lib(void *buf, size_t size) static const HCRYPTPROV INVALID_HCRYPTPROV = (HCRYPTPROV)INVALID_HANDLE_VALUE; static void -release_crypt(void *p) +release_crypt(VALUE arg) { - HCRYPTPROV *ptr = p; + HCRYPTPROV *ptr = (void *)arg; HCRYPTPROV prov = (HCRYPTPROV)ATOMIC_PTR_EXCHANGE(*ptr, INVALID_HCRYPTPROV); if (prov && prov != INVALID_HCRYPTPROV) { CryptReleaseContext(prov, 0); } } -static const rb_data_type_t crypt_prov_type = { - "HCRYPTPROV", - {0, release_crypt,}, - 0, 0, RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_EMBEDDABLE -}; - static int fill_random_bytes_crypt(void *seed, size_t size) { static HCRYPTPROV perm_prov; HCRYPTPROV prov = perm_prov, old_prov; if (!prov) { - VALUE wrapper = TypedData_Wrap_Struct(0, &crypt_prov_type, 0); if (!CryptAcquireContext(&prov, NULL, NULL, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT)) { prov = INVALID_HCRYPTPROV; } old_prov = (HCRYPTPROV)ATOMIC_PTR_CAS(perm_prov, 0, prov); if (LIKELY(!old_prov)) { /* no other threads acquired */ if (prov != INVALID_HCRYPTPROV) { - DATA_PTR(wrapper) = (void *)prov; - rb_vm_register_global_object(wrapper); + /* register only once; perm_prov == 0 at the first call only */ + rb_set_end_proc(release_crypt, (VALUE)&perm_prov); } } else { /* another thread acquired */ From e21a4b0174e2e9dba9b856bab01531575291455a Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Tue, 2 Jun 2026 10:31:47 +0900 Subject: [PATCH 037/188] win32: Limit the size up to the rounded-up half of the max range --- random.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/random.c b/random.c index fcbe3b3bade877..63f43a161ae252 100644 --- a/random.c +++ b/random.c @@ -600,7 +600,7 @@ fill_random_bytes_crypt(void *seed, size_t size) } if (prov == INVALID_HCRYPTPROV) return -1; while (size > 0) { - DWORD n = (size > (size_t)DWORD_MAX) ? DWORD_MAX : (DWORD)size; + DWORD n = (size > (size_t)DWORD_MAX) ? DWORD_MAX/2+1 : (DWORD)size; if (!CryptGenRandom(prov, n, seed)) return -1; seed = (char *)seed + n; size -= n; @@ -615,7 +615,7 @@ static int fill_random_bytes_bcrypt(void *seed, size_t size) { while (size > 0) { - ULONG n = (size > (size_t)ULONG_MAX) ? LONG_MAX : (ULONG)size; + ULONG n = (size > (size_t)ULONG_MAX) ? ULONG_MAX/2+1 : (ULONG)size; if (BCryptGenRandom(NULL, seed, n, BCRYPT_USE_SYSTEM_PREFERRED_RNG)) return -1; seed = (char *)seed + n; From 4f6b9d80fb27c876ae5a07395680e5c6a5c4f4c0 Mon Sep 17 00:00:00 2001 From: Earlopain <14981592+Earlopain@users.noreply.github.com> Date: Sat, 23 May 2026 12:01:17 +0200 Subject: [PATCH 038/188] [DOC] Fix code sample formatting for `TrueClass#&` --- object.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/object.c b/object.c index 8dd701ec5b22aa..2e962b1c3ce107 100644 --- a/object.c +++ b/object.c @@ -1555,10 +1555,10 @@ rb_true_to_s(VALUE obj) /* - * call-seq: - * true & object -> true or false + * call-seq: + * true & object -> true or false * - * Returns +false+ if +object+ is +false+ or +nil+, +true+ otherwise: + * Returns +false+ if +object+ is +false+ or +nil+, +true+ otherwise: * * true & Object.new # => true * true & false # => false From f41fa0404e588311dedc682b3973871064e36e02 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Thu, 28 May 2026 12:59:08 +0900 Subject: [PATCH 039/188] [ruby/rubygems] Limit compact index dependency parser to split on first colon The upcoming compact index v2 format introduced by rubygems.org appends a `created_at:ISO8601` field to each info line, and the timestamp's internal colons were being treated as additional split delimiters. Splitting only on the first colon keeps existing keys like `ruby:>= 2.7.0` intact while letting multi-colon values pass through untouched. https://github.com/rubygems/rubygems.org/pull/6504 https://github.com/ruby/rubygems/commit/227b3d6f94 Co-Authored-By: Claude Opus 4.7 --- lib/rubygems/resolver/api_set/gem_parser.rb | 2 +- spec/bundler/bundler/compact_index_client/parser_spec.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/rubygems/resolver/api_set/gem_parser.rb b/lib/rubygems/resolver/api_set/gem_parser.rb index 7dd9a89ebcdf18..4d827f49808841 100644 --- a/lib/rubygems/resolver/api_set/gem_parser.rb +++ b/lib/rubygems/resolver/api_set/gem_parser.rb @@ -13,7 +13,7 @@ def parse(line) private def parse_dependency(string) - dependency = string.split(":") + dependency = string.split(":", 2) dependency[-1] = dependency[-1].split("&") if dependency.size > 1 dependency[0] = -dependency[0] dependency diff --git a/spec/bundler/bundler/compact_index_client/parser_spec.rb b/spec/bundler/bundler/compact_index_client/parser_spec.rb index 6015f66f33a79b..6aa867f058f9f5 100644 --- a/spec/bundler/bundler/compact_index_client/parser_spec.rb +++ b/spec/bundler/bundler/compact_index_client/parser_spec.rb @@ -47,7 +47,7 @@ def set_info_data(name, value) INFO let(:c_info) { <<~INFO } 3.0.0 a:= 1.0.0,b:~> 2.0|checksum:ccc1,ruby:>= 2.7.0,rubygems:>= 3.0.0 - 3.3.3 a:>= 1.1.0,b:~> 2.0|checksum:ccc3,ruby:>= 3.0.0,rubygems:>= 3.2.3 + 3.3.3 a:>= 1.1.0,b:~> 2.0|checksum:ccc3,ruby:>= 3.0.0,rubygems:>= 3.2.3,created_at:2026-05-12T10:00:00Z INFO describe "#available?" do @@ -195,7 +195,7 @@ def set_info_data(name, value) "3.3.3", nil, [["a", [">= 1.1.0"]], ["b", ["~> 2.0"]]], - [["checksum", ["ccc3"]], ["ruby", [">= 3.0.0"]], ["rubygems", [">= 3.2.3"]]], + [["checksum", ["ccc3"]], ["ruby", [">= 3.0.0"]], ["rubygems", [">= 3.2.3"]], ["created_at", ["2026-05-12T10:00:00Z"]]], ], ] end From 2c57ae9e7f88f515219e94620c2fb00b8ebe5150 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Thu, 28 May 2026 12:59:16 +0900 Subject: [PATCH 040/188] [ruby/rubygems] Capture created_at metadata on EndpointSpecification The cooldown feature needs each gem version's publish timestamp on the client side. Compact index v2 exposes it as a `created_at:ISO8601` entry in the info-line metadata; expose a Time-typed `created_at` attribute on the spec so the resolver can consult it later. Parsing is defensive against older rubygems whose APISet GemParser splits ISO8601 timestamps on every colon, accepting both Array and flat String shapes and silently dropping malformed values. The time stdlib is required lazily inside the case branch so loading the file does not activate the `time` default gem during Bundler.setup. https://github.com/ruby/rubygems/commit/cd61070cfc Co-Authored-By: Claude Opus 4.7 --- lib/bundler/endpoint_specification.rb | 13 +++++- .../bundler/endpoint_specification_spec.rb | 40 +++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/lib/bundler/endpoint_specification.rb b/lib/bundler/endpoint_specification.rb index c06684657d598b..89ffe3d740df39 100644 --- a/lib/bundler/endpoint_specification.rb +++ b/lib/bundler/endpoint_specification.rb @@ -5,7 +5,7 @@ module Bundler class EndpointSpecification < Gem::Specification include MatchRemoteMetadata - attr_reader :name, :version, :platform, :checksum + attr_reader :name, :version, :platform, :checksum, :created_at attr_writer :dependencies attr_accessor :remote, :locked_platform @@ -145,6 +145,7 @@ def parse_metadata(data) unless data @required_ruby_version = nil @required_rubygems_version = nil + @created_at = nil return end @@ -161,6 +162,16 @@ def parse_metadata(data) @required_rubygems_version = Gem::Requirement.new(v) when "ruby" @required_ruby_version = Gem::Requirement.new(v) + when "created_at" + value = v.is_a?(Array) ? v.last : v + if value.is_a?(String) + require "time" + begin + @created_at = Time.iso8601(value) + rescue ArgumentError + @created_at = nil + end + end end end rescue StandardError => e diff --git a/spec/bundler/bundler/endpoint_specification_spec.rb b/spec/bundler/bundler/endpoint_specification_spec.rb index 6518f125ba5e52..4fbd59d48f23a3 100644 --- a/spec/bundler/bundler/endpoint_specification_spec.rb +++ b/spec/bundler/bundler/endpoint_specification_spec.rb @@ -46,6 +46,46 @@ ) end end + + context "when the metadata has created_at" do + let(:metadata) { { "created_at" => ["2026-05-12T10:00:00Z"] } } + + it "parses created_at as a Time" do + expect(subject.created_at).to eq(Time.utc(2026, 5, 12, 10, 0, 0)) + end + end + + context "when the metadata has a string created_at (older rubygems shape)" do + let(:metadata) { { "created_at" => "2026-05-12T10:00:00Z" } } + + it "still parses created_at" do + expect(subject.created_at).to eq(Time.utc(2026, 5, 12, 10, 0, 0)) + end + end + + context "when created_at is truncated (older rubygems splits on colons)" do + let(:metadata) { { "created_at" => "2026-05-12T10" } } + + it "leaves created_at as nil instead of raising" do + expect(subject.created_at).to be_nil + end + end + + context "when the metadata has no created_at" do + let(:metadata) { { "checksum" => ["abc"] } } + let(:spec_fetcher) { double(:spec_fetcher, uri: "https://rubygems.org") } + + it "leaves created_at as nil" do + allow(Bundler::Checksum).to receive(:from_api).and_return(nil) + expect(subject.created_at).to be_nil + end + end + + context "when the metadata is nil" do + it "leaves created_at as nil" do + expect(subject.created_at).to be_nil + end + end end describe "#required_ruby_version" do From 0713f355d1f500d88d01c4760236ebeca4003c86 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Thu, 28 May 2026 13:10:31 +0900 Subject: [PATCH 041/188] [ruby/rubygems] Recognize cooldown as an integer Bundler setting Adds `cooldown` to NUMBER_KEYS so that `BUNDLE_COOLDOWN` and `bundle config set cooldown` are parsed as integer days. Reading the value is enough for now; later commits plumb it into the resolver and the Gemfile DSL. https://github.com/ruby/rubygems/discussions/9113 https://github.com/ruby/rubygems/commit/478c3ff09f Co-Authored-By: Claude Opus 4.7 --- lib/bundler/settings.rb | 1 + spec/bundler/bundler/settings_spec.rb | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/lib/bundler/settings.rb b/lib/bundler/settings.rb index 95b48da31e927a..fd77c2f7fc76d2 100644 --- a/lib/bundler/settings.rb +++ b/lib/bundler/settings.rb @@ -44,6 +44,7 @@ class Settings ].freeze NUMBER_KEYS = %w[ + cooldown jobs redirect retry diff --git a/spec/bundler/bundler/settings_spec.rb b/spec/bundler/bundler/settings_spec.rb index e91e1641b3da20..5e1aaaa5551109 100644 --- a/spec/bundler/bundler/settings_spec.rb +++ b/spec/bundler/bundler/settings_spec.rb @@ -119,6 +119,11 @@ settings.set_local :ssl_verify_mode, "1" expect(settings[:ssl_verify_mode]).to be 1 end + + it "coerces cooldown to integer" do + settings.set_local :cooldown, "7" + expect(settings[:cooldown]).to be 7 + end end context "when it's not possible to create the settings directory" do From 63fa7019401e63c1ffca9ac69ed664a67f30e7c8 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Thu, 28 May 2026 13:10:39 +0900 Subject: [PATCH 042/188] [ruby/rubygems] Plumb per-source cooldown from the Gemfile DSL Lets `source "https://rubygems.org", cooldown: 7` attach a per-remote value to the global Rubygems source, which the new `Remote#effective_cooldown` reads with CLI > config > Gemfile per-source precedence. The cooldown is stored on Source::Rubygems keyed by URI so that several top-level `source` lines can carry independent values onto the same global source. Non-negative Integer values are required at parse time; everything else (strings, floats, arrays, negative numbers) raises InvalidOption so a typo can't silently disable the filter. https://github.com/ruby/rubygems/commit/5bd253f11f Co-Authored-By: Claude Opus 4.7 --- lib/bundler/dsl.rb | 8 +++- lib/bundler/source/rubygems.rb | 13 ++++-- lib/bundler/source/rubygems/remote.rb | 14 ++++++- lib/bundler/source_list.rb | 4 +- spec/bundler/bundler/dsl_spec.rb | 42 +++++++++++++++++++ .../bundler/source/rubygems/remote_spec.rb | 35 ++++++++++++++++ spec/bundler/bundler/source_list_spec.rb | 6 +++ 7 files changed, 113 insertions(+), 9 deletions(-) diff --git a/lib/bundler/dsl.rb b/lib/bundler/dsl.rb index d67291514bd807..6e2638a8be4cc9 100644 --- a/lib/bundler/dsl.rb +++ b/lib/bundler/dsl.rb @@ -117,6 +117,10 @@ def source(source, *args, &blk) options = args.last.is_a?(Hash) ? args.pop.dup : {} options = normalize_hash(options) source = normalize_source(source) + cooldown = options["cooldown"] + if cooldown && !(cooldown.is_a?(Integer) && cooldown >= 0) + raise InvalidOption, "Expected `cooldown` to be a non-negative integer, got #{cooldown.inspect}" + end if options.key?("type") options["type"] = options["type"].to_s @@ -131,9 +135,9 @@ def source(source, *args, &blk) source_opts = options.merge("uri" => source) with_source(@sources.add_plugin_source(options["type"], source_opts), &blk) elsif block_given? - with_source(@sources.add_rubygems_source("remotes" => source), &blk) + with_source(@sources.add_rubygems_source("remotes" => source, "cooldown" => cooldown), &blk) else - @sources.add_global_rubygems_remote(source) + @sources.add_global_rubygems_remote(source, cooldown: cooldown) end end diff --git a/lib/bundler/source/rubygems.rb b/lib/bundler/source/rubygems.rb index d610ce3fdf92e0..66b36645199e85 100644 --- a/lib/bundler/source/rubygems.rb +++ b/lib/bundler/source/rubygems.rb @@ -16,6 +16,7 @@ class Rubygems < Source def initialize(options = {}) @options = options @remotes = [] + @remote_cooldowns = {} @dependency_names = [] @allow_remote = false @allow_cached = false @@ -25,7 +26,8 @@ def initialize(options = {}) @gem_installers = {} @gem_installers_mutex = Mutex.new - Array(options["remotes"]).reverse_each {|r| add_remote(r) } + cooldown = options["cooldown"] + Array(options["remotes"]).reverse_each {|r| add_remote(r, cooldown: cooldown) } @lockfile_remotes = @remotes if options["from_lockfile"] end @@ -243,9 +245,14 @@ def cached_built_in_gem(spec, local: false) cached_path end - def add_remote(source) + def add_remote(source, cooldown: nil) uri = normalize_uri(source) @remotes.unshift(uri) unless @remotes.include?(uri) + @remote_cooldowns[uri] = cooldown if cooldown + end + + def cooldown_for(uri) + @remote_cooldowns[uri] end def spec_names @@ -266,7 +273,7 @@ def unmet_deps def remote_fetchers @remote_fetchers ||= remotes.to_h do |uri| - remote = Source::Rubygems::Remote.new(uri) + remote = Source::Rubygems::Remote.new(uri, cooldown: cooldown_for(uri)) [remote, Bundler::Fetcher.new(remote)] end.freeze end diff --git a/lib/bundler/source/rubygems/remote.rb b/lib/bundler/source/rubygems/remote.rb index ed55912a9952be..3d847424b7aeb3 100644 --- a/lib/bundler/source/rubygems/remote.rb +++ b/lib/bundler/source/rubygems/remote.rb @@ -4,9 +4,9 @@ module Bundler class Source class Rubygems class Remote - attr_reader :uri, :anonymized_uri, :original_uri + attr_reader :uri, :anonymized_uri, :original_uri, :cooldown - def initialize(uri) + def initialize(uri, cooldown: nil) orig_uri = uri uri = Bundler.settings.mirror_for(uri) @original_uri = orig_uri if orig_uri != uri @@ -14,6 +14,16 @@ def initialize(uri) @uri = apply_auth(uri, fallback_auth).freeze @anonymized_uri = remove_auth(@uri).freeze + @cooldown = cooldown + end + + # Returns the cooldown days that apply to this remote, resolving the + # precedence CLI > config > Gemfile per-source. Returns nil if no + # cooldown applies. + def effective_cooldown + override = Bundler.settings[:cooldown] + return override if override + @cooldown end MAX_CACHE_SLUG_HOST_SIZE = 255 - 1 - 32 # 255 minus dot minus MD5 length diff --git a/lib/bundler/source_list.rb b/lib/bundler/source_list.rb index ac141299e5d010..ab7002d6e5b3d7 100644 --- a/lib/bundler/source_list.rb +++ b/lib/bundler/source_list.rb @@ -59,8 +59,8 @@ def add_plugin_source(source, options = {}) add_source_to_list Plugin.source(source).new(options), @plugin_sources end - def add_global_rubygems_remote(uri) - global_rubygems_source.add_remote(uri) + def add_global_rubygems_remote(uri, cooldown: nil) + global_rubygems_source.add_remote(uri, cooldown: cooldown) global_rubygems_source end diff --git a/spec/bundler/bundler/dsl_spec.rb b/spec/bundler/bundler/dsl_spec.rb index a04528a57fc7d0..b6e67a312c0d41 100644 --- a/spec/bundler/bundler/dsl_spec.rb +++ b/spec/bundler/bundler/dsl_spec.rb @@ -367,6 +367,48 @@ end end + describe "#source with cooldown" do + before do + allow(@rubygems).to receive(:add_remote) + end + + it "accepts a non-negative integer" do + expect do + subject.source("https://rubygems.org", cooldown: 7) + end.not_to raise_error + end + + it "accepts 0 as an explicit disable" do + expect do + subject.source("https://rubygems.org", cooldown: 0) + end.not_to raise_error + end + + it "rejects a string" do + expect do + subject.source("https://rubygems.org", cooldown: "7") + end.to raise_error(Bundler::InvalidOption, /non-negative integer/) + end + + it "rejects a float" do + expect do + subject.source("https://rubygems.org", cooldown: 7.5) + end.to raise_error(Bundler::InvalidOption, /non-negative integer/) + end + + it "rejects a negative integer" do + expect do + subject.source("https://rubygems.org", cooldown: -7) + end.to raise_error(Bundler::InvalidOption, /non-negative integer/) + end + + it "rejects an array" do + expect do + subject.source("https://rubygems.org", cooldown: [7]) + end.to raise_error(Bundler::InvalidOption, /non-negative integer/) + end + end + describe "#override" do it "stores an Override for a gem with a version: operation" do subject.override("rails", version: ">= 8.0") diff --git a/spec/bundler/bundler/source/rubygems/remote_spec.rb b/spec/bundler/bundler/source/rubygems/remote_spec.rb index f2214ca8fe129a..27430d4a3bb72e 100644 --- a/spec/bundler/bundler/source/rubygems/remote_spec.rb +++ b/spec/bundler/bundler/source/rubygems/remote_spec.rb @@ -169,4 +169,39 @@ def remote(uri) end end end + + describe "#cooldown" do + it "is nil by default" do + expect(remote(uri_no_auth).cooldown).to be_nil + end + + it "returns the value passed to the constructor" do + r = Bundler::Source::Rubygems::Remote.new(uri_no_auth, cooldown: 7) + expect(r.cooldown).to eq(7) + end + end + + describe "#effective_cooldown" do + it "returns the per-remote value when no override is set" do + r = Bundler::Source::Rubygems::Remote.new(uri_no_auth, cooldown: 7) + expect(r.effective_cooldown).to eq(7) + end + + it "returns nil when neither override nor per-remote value is set" do + expect(remote(uri_no_auth).effective_cooldown).to be_nil + end + + it "settings override per-remote value" do + r = Bundler::Source::Rubygems::Remote.new(uri_no_auth, cooldown: 7) + Bundler.settings.temporary(cooldown: 14) do + expect(r.effective_cooldown).to eq(14) + end + end + + it "settings override even when per-remote value is absent" do + Bundler.settings.temporary(cooldown: 14) do + expect(remote(uri_no_auth).effective_cooldown).to eq(14) + end + end + end end diff --git a/spec/bundler/bundler/source_list_spec.rb b/spec/bundler/bundler/source_list_spec.rb index 3ed58b867d9641..61bd99b063b4f6 100644 --- a/spec/bundler/bundler/source_list_spec.rb +++ b/spec/bundler/bundler/source_list_spec.rb @@ -129,6 +129,12 @@ Gem::URI("https://rubygems.org/"), ] end + + it "records the per-remote cooldown when supplied" do + source_list.add_global_rubygems_remote("https://othersource.org", cooldown: 7) + expect(returned_source.cooldown_for(Gem::URI("https://othersource.org/"))).to eq(7) + expect(returned_source.cooldown_for(Gem::URI("https://rubygems.org/"))).to be_nil + end end describe "#add_plugin_source" do From 64dddf907349484c75644caff1d872d19d1f8d21 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Thu, 28 May 2026 13:10:47 +0900 Subject: [PATCH 043/188] [ruby/rubygems] Accept --cooldown on install, update, add, and outdated Adds the CLI flag and routes it through `set_command_option_if_given` so the value lands in Bundler.settings' temporary store. This keeps the CLI > config > Gemfile precedence implicit in the existing settings layering. The resolver does not yet consult the value. https://github.com/ruby/rubygems/discussions/9113 https://github.com/ruby/rubygems/commit/b3a8b2e082 Co-Authored-By: Claude Opus 4.7 --- lib/bundler/cli.rb | 4 ++++ lib/bundler/cli/add.rb | 2 ++ lib/bundler/cli/install.rb | 2 ++ lib/bundler/cli/outdated.rb | 2 ++ lib/bundler/cli/update.rb | 1 + 5 files changed, 11 insertions(+) diff --git a/lib/bundler/cli.rb b/lib/bundler/cli.rb index 40a9dc35b82c85..9d8a68fff9b886 100644 --- a/lib/bundler/cli.rb +++ b/lib/bundler/cli.rb @@ -274,6 +274,7 @@ def remove(*gems) method_option "target-rbconfig", type: :string, banner: "Path to rbconfig.rb for the deployment target platform" method_option "without", type: :array, banner: "Exclude gems that are part of the specified named group (removed)." method_option "with", type: :array, banner: "Include gems that are part of the specified named group (removed)." + method_option "cooldown", type: :numeric, banner: "Only consider gem versions published at least N days ago. Use 0 to disable." def install %w[clean deployment frozen no-prune path shebang without with].each do |option| remembered_flag_deprecation(option) @@ -324,6 +325,7 @@ def install method_option "strict", type: :boolean, banner: "Do not allow any gem to be updated past latest --patch | --minor | --major" method_option "conservative", type: :boolean, banner: "Use bundle install conservative update behavior and do not allow shared dependencies to be updated." method_option "all", type: :boolean, banner: "Update everything." + method_option "cooldown", type: :numeric, banner: "Only consider gem versions published at least N days ago. Use 0 to disable." def update(*gems) require_relative "cli/update" Bundler.settings.temporary(no_install: false) do @@ -406,6 +408,7 @@ def binstubs(*gems) method_option "optimistic", type: :boolean, banner: "Ignored (now default behavior)" method_option "pessimistic", type: :boolean, banner: "Adds pessimistic declaration of version to gem" method_option "strict", type: :boolean, banner: "Adds strict declaration of version to gem" + method_option "cooldown", type: :numeric, banner: "Only consider gem versions published at least N days ago. Use 0 to disable." def add(*gems) require_relative "cli/add" Add.new(options.dup, gems).run @@ -436,6 +439,7 @@ def add(*gems) method_option "filter-patch", type: :boolean, banner: "Only list patch newer versions" method_option "parseable", aliases: "--porcelain", type: :boolean, banner: "Use minimal formatting for more parseable output" method_option "only-explicit", type: :boolean, banner: "Only list gems specified in your Gemfile, not their dependencies" + method_option "cooldown", type: :numeric, banner: "Only consider gem versions published at least N days ago. Use 0 to disable." def outdated(*gems) require_relative "cli/outdated" Outdated.new(options, gems).run diff --git a/lib/bundler/cli/add.rb b/lib/bundler/cli/add.rb index d65ed68b4af87e..7c0d7231f6ee8c 100644 --- a/lib/bundler/cli/add.rb +++ b/lib/bundler/cli/add.rb @@ -14,6 +14,8 @@ def initialize(options, gems) def run Bundler.ui.level = "warn" if options[:quiet] + Bundler.settings.set_command_option_if_given :cooldown, options[:cooldown] + validate_options! inject_dependencies perform_bundle_install unless options["skip-install"] diff --git a/lib/bundler/cli/install.rb b/lib/bundler/cli/install.rb index 67feba84bdb06e..7dd196fbfbe442 100644 --- a/lib/bundler/cli/install.rb +++ b/lib/bundler/cli/install.rb @@ -112,6 +112,8 @@ def normalize_settings Bundler.settings.set_command_option_if_given :jobs, options["jobs"] + Bundler.settings.set_command_option_if_given :cooldown, options["cooldown"] + Bundler.settings.set_command_option_if_given :no_prune, options["no-prune"] Bundler.settings.set_command_option_if_given :no_install, options["no-install"] diff --git a/lib/bundler/cli/outdated.rb b/lib/bundler/cli/outdated.rb index ae827dbb4b97ca..7e2f327dfa7110 100644 --- a/lib/bundler/cli/outdated.rb +++ b/lib/bundler/cli/outdated.rb @@ -26,6 +26,8 @@ def initialize(options, gems) def run check_for_deployment_mode! + Bundler.settings.set_command_option_if_given :cooldown, options[:cooldown] + Bundler.definition.validate_runtime! current_specs = Bundler.ui.silence { Bundler.definition.resolve } diff --git a/lib/bundler/cli/update.rb b/lib/bundler/cli/update.rb index 9cc90acc58536a..7ac472bd4b48c3 100644 --- a/lib/bundler/cli/update.rb +++ b/lib/bundler/cli/update.rb @@ -66,6 +66,7 @@ def run opts["force"] = options[:redownload] if options[:redownload] Bundler.settings.set_command_option_if_given :jobs, opts["jobs"] + Bundler.settings.set_command_option_if_given :cooldown, options[:cooldown] Bundler.definition.validate_runtime! From 05008e2a0104f82395fc123a8bb3568adcb85107 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Thu, 28 May 2026 13:18:45 +0900 Subject: [PATCH 044/188] [ruby/rubygems] Filter cooldown-excluded versions and surface a hint in errors Adds a per-version filter to `Resolver#filter_specs` that drops specs whose `created_at` falls within the effective cooldown window. The filter only runs over `@all_specs`, so lockfile-pinned versions in `@base` survive even if a newer release would now be excluded. Specs without `created_at` are kept so historical versions and indexes that do not yet expose timestamps remain usable. A shared `cooldown_now` memoization ensures every comparison within one resolve sees the same timestamp, stabilizing tests near a threshold boundary. When the resolver fails because every matching version is in cooldown, both `raise_not_found!` and `no_versions_incompatibility_for` surface a hint suggesting `--cooldown 0` to bypass; a small `cooldown_hint` helper keeps the wording in sync between the two error paths and is locked down with unit specs. https://github.com/ruby/rubygems/commit/82dcf047ff Co-Authored-By: Claude Opus 4.7 --- lib/bundler/resolver.rb | 38 +++++- .../bundler/bundler/resolver/cooldown_spec.rb | 119 ++++++++++++++++++ spec/bundler/support/shards.rb | 1 + 3 files changed, 157 insertions(+), 1 deletion(-) create mode 100644 spec/bundler/bundler/resolver/cooldown_spec.rb diff --git a/lib/bundler/resolver.rb b/lib/bundler/resolver.rb index 096a349249168b..7e2ce321b6452a 100644 --- a/lib/bundler/resolver.rb +++ b/lib/bundler/resolver.rb @@ -203,6 +203,9 @@ def no_versions_incompatibility_for(package, unsatisfied_term) platforms_explanation = specs_matching_other_platforms.any? ? " for any resolution platforms (#{package.platforms.join(", ")})" : "" custom_explanation = "#{constraint} could not be found in #{repository_for(package)}#{platforms_explanation}" + if hint = cooldown_hint(specs_matching_other_platforms) + custom_explanation += " (#{hint})" + end label = "#{name} (#{constraint_string})" extended_explanation = other_specs_matching_message(specs_matching_other_platforms, label) if specs_matching_other_platforms.any? @@ -372,6 +375,10 @@ def raise_not_found!(package) message << "\n#{other_specs_matching_message(specs, matching_part)}" end + if hint = cooldown_hint(specs_matching_requirement) + message << "\n\n#{hint}." + end + if specs_matching_requirement.any? && (hint = platform_mismatch_hint) message << "\n\n#{hint}" end @@ -415,7 +422,7 @@ def filter_matching_specs(specs, requirements) end def filter_specs(specs, package) - filter_remote_specs(filter_prereleases(specs, package), package) + filter_cooldown(filter_remote_specs(filter_prereleases(specs, package), package)) end def filter_prereleases(specs, package) @@ -424,6 +431,35 @@ def filter_prereleases(specs, package) specs.reject {|s| s.version.prerelease? } end + def filter_cooldown(specs) + return specs if specs.empty? + excluded = cooldown_excluded_specs(specs) + return specs if excluded.empty? + specs - excluded + end + + def cooldown_excluded_specs(specs) + specs.select {|spec| cooldown_excluded?(spec) } + end + + def cooldown_hint(specs) + excluded = cooldown_excluded_specs(specs) + return nil if excluded.empty? + "#{excluded.size} version#{"s" if excluded.size > 1} excluded by the cooldown setting; pass `--cooldown 0` to bypass" + end + + def cooldown_excluded?(spec) + return false unless spec.respond_to?(:created_at) && spec.created_at + return false unless spec.respond_to?(:remote) && spec.remote + days = spec.remote.effective_cooldown + return false if days.nil? || days <= 0 + (cooldown_now - spec.created_at) < (days * 86_400) + end + + def cooldown_now + @cooldown_now ||= Time.now + end + def filter_remote_specs(specs, package) if package.prefer_local? local_specs = specs.select {|s| s.is_a?(StubSpecification) } diff --git a/spec/bundler/bundler/resolver/cooldown_spec.rb b/spec/bundler/bundler/resolver/cooldown_spec.rb new file mode 100644 index 00000000000000..834842145fa615 --- /dev/null +++ b/spec/bundler/bundler/resolver/cooldown_spec.rb @@ -0,0 +1,119 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::Resolver do + let(:resolver) { described_class.allocate } + + def remote(cooldown:) + instance_double(Bundler::Source::Rubygems::Remote, effective_cooldown: cooldown) + end + + def spec(created_at:, remote:) + Struct.new(:created_at, :remote).new(created_at, remote) + end + + describe "#filter_cooldown" do + let(:now) { Time.now } + + context "with a 7-day cooldown" do + let(:r) { remote(cooldown: 7) } + + it "rejects versions published within the window" do + recent = spec(created_at: now - (2 * 86_400), remote: r) + old = spec(created_at: now - (30 * 86_400), remote: r) + + expect(resolver.send(:filter_cooldown, [recent, old])).to eq([old]) + end + + it "keeps versions published exactly at the threshold" do + boundary = spec(created_at: now - (7 * 86_400), remote: r) + + expect(resolver.send(:filter_cooldown, [boundary])).to eq([boundary]) + end + + it "leaves rolling-delay history intact" do + # 7-day cooldown with frequent releases must still expose an older candidate. + in_cooldown = spec(created_at: now - 86_400, remote: r) + also_in_cooldown = spec(created_at: now - (3 * 86_400), remote: r) + eligible = spec(created_at: now - (10 * 86_400), remote: r) + + result = resolver.send(:filter_cooldown, [in_cooldown, also_in_cooldown, eligible]) + + expect(result).to eq([eligible]) + end + end + + context "when created_at is missing (blank metadata)" do + it "keeps the spec regardless of cooldown" do + s = spec(created_at: nil, remote: remote(cooldown: 7)) + + expect(resolver.send(:filter_cooldown, [s])).to eq([s]) + end + end + + context "when the remote has no cooldown" do + it "keeps every spec" do + s = spec(created_at: now - 3600, remote: remote(cooldown: nil)) + + expect(resolver.send(:filter_cooldown, [s])).to eq([s]) + end + end + + context "when cooldown is 0" do + it "keeps every spec (escape hatch)" do + s = spec(created_at: now - 3600, remote: remote(cooldown: 0)) + + expect(resolver.send(:filter_cooldown, [s])).to eq([s]) + end + end + + context "when the spec does not respond to created_at" do + it "keeps the spec" do + bare = Struct.new(:version).new("1.0.0") + + expect(resolver.send(:filter_cooldown, [bare])).to eq([bare]) + end + end + + context "when the spec has no remote" do + it "keeps the spec" do + s = spec(created_at: now - 86_400, remote: nil) + + expect(resolver.send(:filter_cooldown, [s])).to eq([s]) + end + end + + it "returns the same array when input is empty" do + expect(resolver.send(:filter_cooldown, [])).to eq([]) + end + end + + describe "#cooldown_hint" do + let(:now) { Time.now } + let(:r) { remote(cooldown: 7) } + + it "returns nil when no spec is excluded" do + expect(resolver.send(:cooldown_hint, [])).to be_nil + end + + it "returns nil when every spec is outside the cooldown window" do + eligible = [spec(created_at: now - (30 * 86_400), remote: r)] + + expect(resolver.send(:cooldown_hint, eligible)).to be_nil + end + + it "mentions the count and the bypass flag for one excluded version" do + excluded = [spec(created_at: now - 86_400, remote: r)] + + hint = resolver.send(:cooldown_hint, excluded) + + expect(hint).to match(/1 version excluded by the cooldown setting/) + expect(hint).to match(/--cooldown 0/) + end + + it "uses plural wording when multiple versions are excluded" do + excluded = Array.new(3) { spec(created_at: now - 86_400, remote: r) } + + expect(resolver.send(:cooldown_hint, excluded)).to match(/3 versions excluded/) + end + end +end diff --git a/spec/bundler/support/shards.rb b/spec/bundler/support/shards.rb index 580997eb72df69..b27aecad1697c0 100644 --- a/spec/bundler/support/shards.rb +++ b/spec/bundler/support/shards.rb @@ -143,6 +143,7 @@ module Shards "spec/bundler/ci_detector_spec.rb", ], shard_d: [ + "spec/bundler/resolver/cooldown_spec.rb", "spec/commands/outdated_spec.rb", "spec/commands/update_spec.rb", "spec/lock/lockfile_spec.rb", From fbae6ac3e000e24444721831ad83e98de4f76f3f Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Thu, 28 May 2026 13:30:56 +0900 Subject: [PATCH 045/188] [ruby/rubygems] Annotate in-cooldown versions in bundle outdated output bundle outdated still surfaces newer-but-cooldown'd versions instead of hiding them, so the user knows an upgrade is pending rather than silently missing. The prose form gains ", in cooldown for Nd more days" and the table form appends "(cooldown Nd)" to the Latest column. https://github.com/ruby/rubygems/commit/66b5ab78e0 Co-Authored-By: Claude Opus 4.7 --- lib/bundler/cli/outdated.rb | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/lib/bundler/cli/outdated.rb b/lib/bundler/cli/outdated.rb index 7e2f327dfa7110..f298c2c8d86057 100644 --- a/lib/bundler/cli/outdated.rb +++ b/lib/bundler/cli/outdated.rb @@ -205,6 +205,10 @@ def print_gem(current_spec, active_spec, dependency, groups) release_date = release_date_for(active_spec) spec_outdated_info += ", released #{release_date}" unless release_date.empty? + + remaining = cooldown_days_remaining(active_spec) + spec_outdated_info += ", in cooldown for #{remaining} more day#{"s" if remaining > 1}" if remaining + spec_outdated_info += ")" output_message = if options[:parseable] @@ -221,6 +225,8 @@ def print_gem(current_spec, active_spec, dependency, groups) def gem_column_for(current_spec, active_spec, dependency, groups) current_version = "#{current_spec.version}#{current_spec.git_version}" spec_version = "#{active_spec.version}#{active_spec.git_version}" + remaining = cooldown_days_remaining(active_spec) + spec_version += " (cooldown #{remaining}d)" if remaining dependency = dependency.requirement if dependency ret_val = [active_spec.name, current_version, spec_version, dependency.to_s, groups.to_s] @@ -229,6 +235,15 @@ def gem_column_for(current_spec, active_spec, dependency, groups) ret_val end + def cooldown_days_remaining(spec, now = Time.now) + return nil unless spec.respond_to?(:created_at) && spec.created_at + return nil unless spec.respond_to?(:remote) && spec.remote + days = spec.remote.effective_cooldown + return nil if days.nil? || days <= 0 + remaining = days - ((now - spec.created_at) / 86_400.0) + remaining > 0 ? remaining.ceil : nil + end + def check_for_deployment_mode! return unless Bundler.frozen_bundle? suggested_command = if Bundler.settings.locations("frozen").keys.&([:global, :local]).any? From bfa762d142b565237595dabbfe439226b4628016 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 29 May 2026 11:59:15 +0900 Subject: [PATCH 046/188] [ruby/rubygems] Document cooldown in CLI and config man pages Adds the `--cooldown=NUMBER` option to the install, update, add, and outdated man pages and describes the `cooldown` / `BUNDLE_COOLDOWN` setting in bundle-config, regenerating the rendered .1 files via `rake man:build`. https://github.com/ruby/rubygems/commit/6f4b30491d Co-Authored-By: Claude Opus 4.7 --- lib/bundler/man/bundle-add.1 | 5 +- lib/bundler/man/bundle-add.1.ronn | 7 +- lib/bundler/man/bundle-config.1 | 286 +++++++++++-------------- lib/bundler/man/bundle-config.1.ronn | 30 +++ lib/bundler/man/bundle-install.1 | 5 +- lib/bundler/man/bundle-install.1.ronn | 10 +- lib/bundler/man/bundle-outdated.1 | 5 +- lib/bundler/man/bundle-outdated.1.ronn | 7 + lib/bundler/man/bundle-update.1 | 5 +- lib/bundler/man/bundle-update.1.ronn | 8 + 10 files changed, 200 insertions(+), 168 deletions(-) diff --git a/lib/bundler/man/bundle-add.1 b/lib/bundler/man/bundle-add.1 index 5b1e3e23406268..0956aa83f14f54 100644 --- a/lib/bundler/man/bundle-add.1 +++ b/lib/bundler/man/bundle-add.1 @@ -4,7 +4,7 @@ .SH "NAME" \fBbundle\-add\fR \- Add gem to the Gemfile and run bundle install .SH "SYNOPSIS" -\fBbundle add\fR \fIGEM_NAME\fR [\-\-group=GROUP] [\-\-version=VERSION] [\-\-source=SOURCE] [\-\-path=PATH] [\-\-git=GIT|\-\-github=GITHUB] [\-\-branch=BRANCH] [\-\-ref=REF] [\-\-quiet] [\-\-skip\-install] [\-\-strict|\-\-optimistic] +\fBbundle add\fR \fIGEM_NAME\fR [\-\-group=GROUP] [\-\-version=VERSION] [\-\-source=SOURCE] [\-\-path=PATH] [\-\-git=GIT|\-\-github=GITHUB] [\-\-branch=BRANCH] [\-\-ref=REF] [\-\-cooldown=NUMBER] [\-\-quiet] [\-\-skip\-install] [\-\-strict|\-\-optimistic] .SH "DESCRIPTION" Adds the named gem to the [\fBGemfile(5)\fR][Gemfile(5)] and run \fBbundle install\fR\. \fBbundle install\fR can be avoided by using the flag \fB\-\-skip\-install\fR\. .SH "OPTIONS" @@ -53,6 +53,9 @@ Adds pessimistic declaration of version\. .TP \fB\-\-strict\fR Adds strict declaration of version\. +.TP +\fB\-\-cooldown=\fR +Only consider gem versions published at least \fInumber\fR days ago when resolving\. Pass \fB0\fR to disable cooldown for this run\. See \fBcooldown\fR in bundle\-config(1) for precedence rules\. .SH "EXAMPLES" .IP "1." 4 You can add the \fBrails\fR gem to the Gemfile without any version restriction\. The source of the gem will be the global source\. diff --git a/lib/bundler/man/bundle-add.1.ronn b/lib/bundler/man/bundle-add.1.ronn index 1741e8e3759d37..8c65af0cc00fd0 100644 --- a/lib/bundler/man/bundle-add.1.ronn +++ b/lib/bundler/man/bundle-add.1.ronn @@ -5,7 +5,7 @@ bundle-add(1) -- Add gem to the Gemfile and run bundle install `bundle add` [--group=GROUP] [--version=VERSION] [--source=SOURCE] [--path=PATH] [--git=GIT|--github=GITHUB] [--branch=BRANCH] [--ref=REF] - [--quiet] [--skip-install] [--strict|--optimistic] + [--cooldown=NUMBER] [--quiet] [--skip-install] [--strict|--optimistic] ## DESCRIPTION @@ -59,6 +59,11 @@ Adds the named gem to the [`Gemfile(5)`][Gemfile(5)] and run `bundle install`. * `--strict`: Adds strict declaration of version. +* `--cooldown=`: + Only consider gem versions published at least days ago when + resolving. Pass `0` to disable cooldown for this run. See `cooldown` + in bundle-config(1) for precedence rules. + ## EXAMPLES 1. You can add the `rails` gem to the Gemfile without any version restriction. diff --git a/lib/bundler/man/bundle-config.1 b/lib/bundler/man/bundle-config.1 index 61487ca55e7921..c055c8a415b060 100644 --- a/lib/bundler/man/bundle-config.1 +++ b/lib/bundler/man/bundle-config.1 @@ -69,168 +69,130 @@ The canonical form of this configuration is \fB"without"\fR\. To convert the can Any periods in the configuration keys must be replaced with two underscores when setting it via environment variables\. The configuration key \fBlocal\.rack\fR becomes the environment variable \fBBUNDLE_LOCAL__RACK\fR\. .SH "LIST OF AVAILABLE KEYS" The following is a list of all configuration keys and their purpose\. You can learn more about their operation in bundle install(1) \fIbundle\-install\.1\.html\fR\. -.TP -\fBapi_request_size\fR (\fBBUNDLE_API_REQUEST_SIZE\fR) -Configure how many dependencies to fetch when resolving the specifications\. This configuration is only used when fetching specifications from RubyGems servers that didn't implement the Compact Index API\. Defaults to 100\. -.TP -\fBauto_install\fR (\fBBUNDLE_AUTO_INSTALL\fR) -Automatically run \fBbundle install\fR when gems are missing\. -.TP -\fBbin\fR (\fBBUNDLE_BIN\fR) -If configured, \fBbundle binstubs\fR will install executables from gems in the bundle to the specified directory\. Otherwise it will create them in a \fBbin\fR directory relative to the Gemfile directory\. These executables run in Bundler's context\. If used, you might add this directory to your environment's \fBPATH\fR variable\. For instance, if the \fBrails\fR gem comes with a \fBrails\fR executable, \fBbundle binstubs\fR will create a \fBbin/rails\fR executable that ensures that all referred dependencies will be resolved using the bundled gems\. -.TP -\fBcache_all\fR (\fBBUNDLE_CACHE_ALL\fR) -Cache all gems, including path and git gems\. This needs to be explicitly before bundler 4, but will be the default on bundler 4\. -.TP -\fBcache_all_platforms\fR (\fBBUNDLE_CACHE_ALL_PLATFORMS\fR) -Cache gems for all platforms\. -.TP -\fBcache_path\fR (\fBBUNDLE_CACHE_PATH\fR) -The directory that bundler will place cached gems in when running \fBbundle package\fR, and that bundler will look in when installing gems\. Defaults to \fBvendor/cache\fR\. -.TP -\fBclean\fR (\fBBUNDLE_CLEAN\fR) -Whether Bundler should run \fBbundle clean\fR automatically after \fBbundle install\fR\. Defaults to \fBtrue\fR in Bundler 4, as long as \fBpath\fR is not explicitly configured\. -.TP -\fBconsole\fR (\fBBUNDLE_CONSOLE\fR) -The console that \fBbundle console\fR starts\. Defaults to \fBirb\fR\. -.TP -\fBdefault_cli_command\fR (\fBBUNDLE_DEFAULT_CLI_COMMAND\fR) -The command that running \fBbundle\fR without arguments should run\. Defaults to \fBcli_help\fR since Bundler 4, but can also be \fBinstall\fR which was the previous default\. -.TP -\fBdeployment\fR (\fBBUNDLE_DEPLOYMENT\fR) -Equivalent to setting \fBfrozen\fR to \fBtrue\fR and \fBpath\fR to \fBvendor/bundle\fR\. -.TP -\fBdisable_checksum_validation\fR (\fBBUNDLE_DISABLE_CHECKSUM_VALIDATION\fR) -Allow installing gems even if they do not match the checksum provided by RubyGems\. -.TP -\fBdisable_exec_load\fR (\fBBUNDLE_DISABLE_EXEC_LOAD\fR) -Stop Bundler from using \fBload\fR to launch an executable in\-process in \fBbundle exec\fR\. -.TP -\fBdisable_local_branch_check\fR (\fBBUNDLE_DISABLE_LOCAL_BRANCH_CHECK\fR) -Allow Bundler to use a local git override without a branch specified in the Gemfile\. -.TP -\fBdisable_local_revision_check\fR (\fBBUNDLE_DISABLE_LOCAL_REVISION_CHECK\fR) -Allow Bundler to use a local git override without checking if the revision present in the lockfile is present in the repository\. -.TP -\fBdisable_shared_gems\fR (\fBBUNDLE_DISABLE_SHARED_GEMS\fR) -Stop Bundler from accessing gems installed to RubyGems' normal location\. -.TP -\fBdisable_version_check\fR (\fBBUNDLE_DISABLE_VERSION_CHECK\fR) -Stop Bundler from checking if a newer Bundler version is available on rubygems\.org\. -.TP -\fBforce_ruby_platform\fR (\fBBUNDLE_FORCE_RUBY_PLATFORM\fR) -Ignore the current machine's platform and install only \fBruby\fR platform gems\. As a result, gems with native extensions will be compiled from source\. -.TP -\fBfrozen\fR (\fBBUNDLE_FROZEN\fR) -Disallow any automatic changes to \fBGemfile\.lock\fR\. Bundler commands will be blocked unless the lockfile can be installed exactly as written\. Usually this will happen when changing the \fBGemfile\fR manually and forgetting to update the lockfile through \fBbundle lock\fR or \fBbundle install\fR\. -.TP -\fBgem\.github_username\fR (\fBBUNDLE_GEM__GITHUB_USERNAME\fR) -Sets a GitHub username or organization to be used in the \fBREADME\fR and \fB\.gemspec\fR files when you create a new gem via \fBbundle gem\fR command\. It can be overridden by passing an explicit \fB\-\-github\-username\fR flag to \fBbundle gem\fR\. -.TP -\fBgem\.push_key\fR (\fBBUNDLE_GEM__PUSH_KEY\fR) -Sets the \fB\-\-key\fR parameter for \fBgem push\fR when using the \fBrake release\fR command with a private gemstash server\. -.TP -\fBgemfile\fR (\fBBUNDLE_GEMFILE\fR) -The name of the file that bundler should use as the \fBGemfile\fR\. This location of this file also sets the root of the project, which is used to resolve relative paths in the \fBGemfile\fR, among other things\. By default, bundler will search up from the current working directory until it finds a \fBGemfile\fR\. -.TP -\fBglobal_gem_cache\fR (\fBBUNDLE_GLOBAL_GEM_CACHE\fR) -Whether Bundler should cache all gems and compiled extensions globally, rather than locally to the configured installation path\. -.TP -\fBignore_funding_requests\fR (\fBBUNDLE_IGNORE_FUNDING_REQUESTS\fR) -When set, no funding requests will be printed\. -.TP -\fBignore_messages\fR (\fBBUNDLE_IGNORE_MESSAGES\fR) -When set, no post install messages will be printed\. To silence a single gem, use dot notation like \fBignore_messages\.httparty true\fR\. -.TP -\fBinit_gems_rb\fR (\fBBUNDLE_INIT_GEMS_RB\fR) -Generate a \fBgems\.rb\fR instead of a \fBGemfile\fR when running \fBbundle init\fR\. -.TP -\fBjobs\fR (\fBBUNDLE_JOBS\fR) -The number of gems Bundler can download and install in parallel\. Defaults to the number of available processors\. -.TP -\fBlockfile\fR (\fBBUNDLE_LOCKFILE\fR) -The path to the lockfile that bundler should use\. By default, Bundler adds \fB\.lock\fR to the end of the \fBgemfile\fR entry\. Can be set to \fBfalse\fR in the Gemfile to disable lockfile creation entirely (see gemfile(5))\. -.TP -\fBlockfile_checksums\fR (\fBBUNDLE_LOCKFILE_CHECKSUMS\fR) -Whether Bundler should include a checksums section in new lockfiles, to protect from compromised gem sources\. Defaults to true\. -.TP -\fBno_build_extension\fR (\fBBUNDLE_NO_BUILD_EXTENSION\fR) -Whether Bundler should skip building native extensions during installation\. When set, gems are installed without compiling their C extensions\. To build extensions later, unset this setting and run \fBbundle pristine \fR\. -.TP -\fBno_install\fR (\fBBUNDLE_NO_INSTALL\fR) -Whether \fBbundle package\fR should skip installing gems\. -.TP -\fBno_install_plugin\fR (\fBBUNDLE_NO_INSTALL_PLUGIN\fR) -Whether Bundler should skip installing RubyGems plugins during installation\. When set, plugin files are not written to the plugins directory\. To install plugins later, unset this setting and run \fBbundle pristine \fR\. -.TP -\fBno_prune\fR (\fBBUNDLE_NO_PRUNE\fR) -Whether Bundler should leave outdated gems unpruned when caching\. -.TP -\fBonly\fR (\fBBUNDLE_ONLY\fR) -A space\-separated list of groups to install only gems of the specified groups\. Please check carefully if you want to install also gems without a group, because they get put inside \fBdefault\fR group\. For example \fBonly test:default\fR will install all gems specified in test group and without one\. -.TP -\fBpath\fR (\fBBUNDLE_PATH\fR) -The location on disk where all gems in your bundle will be located regardless of \fB$GEM_HOME\fR or \fB$GEM_PATH\fR values\. Bundle gems not found in this location will be installed by \fBbundle install\fR\. When not set, Bundler install by default to a \fB\.bundle\fR directory relative to repository root in Bundler 4, and to the default system path (\fBGem\.dir\fR) before Bundler 4\. That means that before Bundler 4, Bundler shares this location with Rubygems, and \fBgem install \|\.\|\.\|\.\fR will have gems installed in the same location and therefore, gems installed without \fBpath\fR set will show up by calling \fBgem list\fR\. This will not be the case in Bundler 4\. -.TP -\fBpath\.system\fR (\fBBUNDLE_PATH__SYSTEM\fR) -Whether Bundler will install gems into the default system path (\fBGem\.dir\fR)\. -.TP -\fBplugins\fR (\fBBUNDLE_PLUGINS\fR) -Enable Bundler's experimental plugin system\. -.TP -\fBprefer_patch\fR (\fBBUNDLE_PREFER_PATCH\fR) -Prefer updating only to next patch version during updates\. Makes \fBbundle update\fR calls equivalent to \fBbundler update \-\-patch\fR\. -.TP -\fBredirect\fR (\fBBUNDLE_REDIRECT\fR) -The number of redirects allowed for network requests\. Defaults to \fB5\fR\. -.TP -\fBretry\fR (\fBBUNDLE_RETRY\fR) -The number of times to retry failed network requests\. Defaults to \fB3\fR\. -.TP -\fBshebang\fR (\fBBUNDLE_SHEBANG\fR) -The program name that should be invoked for generated binstubs\. Defaults to the ruby install name used to generate the binstub\. -.TP -\fBsilence_deprecations\fR (\fBBUNDLE_SILENCE_DEPRECATIONS\fR) -Whether Bundler should silence deprecation warnings for behavior that will be changed in the next major version\. -.TP -\fBsilence_root_warning\fR (\fBBUNDLE_SILENCE_ROOT_WARNING\fR) -Silence the warning Bundler prints when installing gems as root\. -.TP -\fBsimulate_version\fR (\fBBUNDLE_SIMULATE_VERSION\fR) -The virtual version Bundler should use for activating feature flags\. Can be used to simulate all the new functionality that will be enabled in a future major version\. -.TP -\fBssl_ca_cert\fR (\fBBUNDLE_SSL_CA_CERT\fR) -Path to a designated CA certificate file or folder containing multiple certificates for trusted CAs in PEM format\. -.TP -\fBssl_client_cert\fR (\fBBUNDLE_SSL_CLIENT_CERT\fR) -Path to a designated file containing a X\.509 client certificate and key in PEM format\. -.TP -\fBssl_verify_mode\fR (\fBBUNDLE_SSL_VERIFY_MODE\fR) -The SSL verification mode Bundler uses when making HTTPS requests\. Defaults to verify peer\. -.TP -\fBsystem_bindir\fR (\fBBUNDLE_SYSTEM_BINDIR\fR) -The location where RubyGems installs binstubs\. Defaults to \fBGem\.bindir\fR\. -.TP -\fBtimeout\fR (\fBBUNDLE_TIMEOUT\fR) -The seconds allowed before timing out for network requests\. Defaults to \fB10\fR\. -.TP -\fBupdate_requires_all_flag\fR (\fBBUNDLE_UPDATE_REQUIRES_ALL_FLAG\fR) -Require passing \fB\-\-all\fR to \fBbundle update\fR when everything should be updated, and disallow passing no options to \fBbundle update\fR\. -.TP -\fBuser_agent\fR (\fBBUNDLE_USER_AGENT\fR) -The custom user agent fragment Bundler includes in API requests\. -.TP -\fBverbose\fR (\fBBUNDLE_VERBOSE\fR) -Whether Bundler should print verbose output\. Defaults to \fBfalse\fR, unless the \fB\-\-verbose\fR CLI flag is used\. -.TP -\fBversion\fR (\fBBUNDLE_VERSION\fR) -The version of Bundler to use when running under Bundler environment\. Defaults to \fBlockfile\fR\. You can also specify \fBsystem\fR or \fBx\.y\.z\fR\. \fBlockfile\fR will use the Bundler version specified in the \fBGemfile\.lock\fR, \fBsystem\fR will use the system version of Bundler, and \fBx\.y\.z\fR will use the specified version of Bundler\. -.TP -\fBwith\fR (\fBBUNDLE_WITH\fR) -A space\-separated or \fB:\fR\-separated list of groups whose gems bundler should install\. -.TP -\fBwithout\fR (\fBBUNDLE_WITHOUT\fR) -A space\-separated or \fB:\fR\-separated list of groups whose gems bundler should not install\. +.IP "\(bu" 4 +\fBapi_request_size\fR (\fBBUNDLE_API_REQUEST_SIZE\fR): Configure how many dependencies to fetch when resolving the specifications\. This configuration is only used when fetching specifications from RubyGems servers that didn't implement the Compact Index API\. Defaults to 100\. +.IP "\(bu" 4 +\fBauto_install\fR (\fBBUNDLE_AUTO_INSTALL\fR): Automatically run \fBbundle install\fR when gems are missing\. +.IP "\(bu" 4 +\fBbin\fR (\fBBUNDLE_BIN\fR): If configured, \fBbundle binstubs\fR will install executables from gems in the bundle to the specified directory\. Otherwise it will create them in a \fBbin\fR directory relative to the Gemfile directory\. These executables run in Bundler's context\. If used, you might add this directory to your environment's \fBPATH\fR variable\. For instance, if the \fBrails\fR gem comes with a \fBrails\fR executable, \fBbundle binstubs\fR will create a \fBbin/rails\fR executable that ensures that all referred dependencies will be resolved using the bundled gems\. +.IP "\(bu" 4 +\fBcache_all\fR (\fBBUNDLE_CACHE_ALL\fR): Cache all gems, including path and git gems\. This needs to be explicitly before bundler 4, but will be the default on bundler 4\. +.IP "\(bu" 4 +\fBcache_all_platforms\fR (\fBBUNDLE_CACHE_ALL_PLATFORMS\fR): Cache gems for all platforms\. +.IP "\(bu" 4 +\fBcache_path\fR (\fBBUNDLE_CACHE_PATH\fR): The directory that bundler will place cached gems in when running \fBbundle package\fR, and that bundler will look in when installing gems\. Defaults to \fBvendor/cache\fR\. +.IP "\(bu" 4 +\fBclean\fR (\fBBUNDLE_CLEAN\fR): Whether Bundler should run \fBbundle clean\fR automatically after \fBbundle install\fR\. Defaults to \fBtrue\fR in Bundler 4, as long as \fBpath\fR is not explicitly configured\. +.IP "\(bu" 4 +\fBconsole\fR (\fBBUNDLE_CONSOLE\fR): The console that \fBbundle console\fR starts\. Defaults to \fBirb\fR\. +.IP "\(bu" 4 +\fBcooldown\fR (\fBBUNDLE_COOLDOWN\fR): Number of days a published gem version must age before bundler will resolve to it\. Defaults to unset (no cooldown)\. Pass \fB0\fR to disable cooldown for an individual run\. +.IP +The effective cooldown for any given gem is resolved from three layers, highest precedence first: +.IP "1." 4 +CLI flag \fB\-\-cooldown N\fR on \fBinstall\fR, \fBupdate\fR, \fBadd\fR, and \fBoutdated\fR\. +.IP "2." 4 +This setting (\fBbundle config set cooldown N\fR or \fBBUNDLE_COOLDOWN=N\fR)\. +.IP "3." 4 +The per\-source \fBcooldown:\fR keyword in the Gemfile, such as \fBsource "https://rubygems\.org", cooldown: 7\fR\. +.IP "" 0 +.IP +The CLI flag and this setting apply uniformly to every source, including ones declared with their own \fBcooldown:\fR value\. To keep a private registry permanently exempt while still cooling down public gems, declare \fBsource "https://internal", cooldown: 0\fR in the Gemfile; remember that \fB\-\-cooldown N\fR on the command line will still override it for that single run\. +.IP +Cooldown filtering depends on the gem server providing a per\-version \fBcreated_at\fR timestamp in the v2 compact\-index format\. Versions without that metadata \- older gem servers, historical entries that predate the v2 cutover on \fBrubygems\.org\fR, or private registries that still emit the v1 format \- are treated as outside the cooldown window and remain resolvable\. If you rely on cooldown for supply\-chain protection, confirm that the gem server emits \fBcreated_at\fR in its \fB/info/\fR responses\. +.IP "\(bu" 4 +\fBdefault_cli_command\fR (\fBBUNDLE_DEFAULT_CLI_COMMAND\fR): The command that running \fBbundle\fR without arguments should run\. Defaults to \fBcli_help\fR since Bundler 4, but can also be \fBinstall\fR which was the previous default\. +.IP "\(bu" 4 +\fBdeployment\fR (\fBBUNDLE_DEPLOYMENT\fR): Equivalent to setting \fBfrozen\fR to \fBtrue\fR and \fBpath\fR to \fBvendor/bundle\fR\. +.IP "\(bu" 4 +\fBdisable_checksum_validation\fR (\fBBUNDLE_DISABLE_CHECKSUM_VALIDATION\fR): Allow installing gems even if they do not match the checksum provided by RubyGems\. +.IP "\(bu" 4 +\fBdisable_exec_load\fR (\fBBUNDLE_DISABLE_EXEC_LOAD\fR): Stop Bundler from using \fBload\fR to launch an executable in\-process in \fBbundle exec\fR\. +.IP "\(bu" 4 +\fBdisable_local_branch_check\fR (\fBBUNDLE_DISABLE_LOCAL_BRANCH_CHECK\fR): Allow Bundler to use a local git override without a branch specified in the Gemfile\. +.IP "\(bu" 4 +\fBdisable_local_revision_check\fR (\fBBUNDLE_DISABLE_LOCAL_REVISION_CHECK\fR): Allow Bundler to use a local git override without checking if the revision present in the lockfile is present in the repository\. +.IP "\(bu" 4 +\fBdisable_shared_gems\fR (\fBBUNDLE_DISABLE_SHARED_GEMS\fR): Stop Bundler from accessing gems installed to RubyGems' normal location\. +.IP "\(bu" 4 +\fBdisable_version_check\fR (\fBBUNDLE_DISABLE_VERSION_CHECK\fR): Stop Bundler from checking if a newer Bundler version is available on rubygems\.org\. +.IP "\(bu" 4 +\fBforce_ruby_platform\fR (\fBBUNDLE_FORCE_RUBY_PLATFORM\fR): Ignore the current machine's platform and install only \fBruby\fR platform gems\. As a result, gems with native extensions will be compiled from source\. +.IP "\(bu" 4 +\fBfrozen\fR (\fBBUNDLE_FROZEN\fR): Disallow any automatic changes to \fBGemfile\.lock\fR\. Bundler commands will be blocked unless the lockfile can be installed exactly as written\. Usually this will happen when changing the \fBGemfile\fR manually and forgetting to update the lockfile through \fBbundle lock\fR or \fBbundle install\fR\. +.IP "\(bu" 4 +\fBgem\.github_username\fR (\fBBUNDLE_GEM__GITHUB_USERNAME\fR): Sets a GitHub username or organization to be used in the \fBREADME\fR and \fB\.gemspec\fR files when you create a new gem via \fBbundle gem\fR command\. It can be overridden by passing an explicit \fB\-\-github\-username\fR flag to \fBbundle gem\fR\. +.IP "\(bu" 4 +\fBgem\.push_key\fR (\fBBUNDLE_GEM__PUSH_KEY\fR): Sets the \fB\-\-key\fR parameter for \fBgem push\fR when using the \fBrake release\fR command with a private gemstash server\. +.IP "\(bu" 4 +\fBgemfile\fR (\fBBUNDLE_GEMFILE\fR): The name of the file that bundler should use as the \fBGemfile\fR\. This location of this file also sets the root of the project, which is used to resolve relative paths in the \fBGemfile\fR, among other things\. By default, bundler will search up from the current working directory until it finds a \fBGemfile\fR\. +.IP "\(bu" 4 +\fBglobal_gem_cache\fR (\fBBUNDLE_GLOBAL_GEM_CACHE\fR): Whether Bundler should cache all gems and compiled extensions globally, rather than locally to the configured installation path\. +.IP "\(bu" 4 +\fBignore_funding_requests\fR (\fBBUNDLE_IGNORE_FUNDING_REQUESTS\fR): When set, no funding requests will be printed\. +.IP "\(bu" 4 +\fBignore_messages\fR (\fBBUNDLE_IGNORE_MESSAGES\fR): When set, no post install messages will be printed\. To silence a single gem, use dot notation like \fBignore_messages\.httparty true\fR\. +.IP "\(bu" 4 +\fBinit_gems_rb\fR (\fBBUNDLE_INIT_GEMS_RB\fR): Generate a \fBgems\.rb\fR instead of a \fBGemfile\fR when running \fBbundle init\fR\. +.IP "\(bu" 4 +\fBjobs\fR (\fBBUNDLE_JOBS\fR): The number of gems Bundler can download and install in parallel\. Defaults to the number of available processors\. +.IP "\(bu" 4 +\fBlockfile\fR (\fBBUNDLE_LOCKFILE\fR): The path to the lockfile that bundler should use\. By default, Bundler adds \fB\.lock\fR to the end of the \fBgemfile\fR entry\. Can be set to \fBfalse\fR in the Gemfile to disable lockfile creation entirely (see gemfile(5))\. +.IP "\(bu" 4 +\fBlockfile_checksums\fR (\fBBUNDLE_LOCKFILE_CHECKSUMS\fR): Whether Bundler should include a checksums section in new lockfiles, to protect from compromised gem sources\. Defaults to true\. +.IP "\(bu" 4 +\fBno_build_extension\fR (\fBBUNDLE_NO_BUILD_EXTENSION\fR): Whether Bundler should skip building native extensions during installation\. When set, gems are installed without compiling their C extensions\. To build extensions later, unset this setting and run \fBbundle pristine \fR\. +.IP "\(bu" 4 +\fBno_install\fR (\fBBUNDLE_NO_INSTALL\fR): Whether \fBbundle package\fR should skip installing gems\. +.IP "\(bu" 4 +\fBno_install_plugin\fR (\fBBUNDLE_NO_INSTALL_PLUGIN\fR): Whether Bundler should skip installing RubyGems plugins during installation\. When set, plugin files are not written to the plugins directory\. To install plugins later, unset this setting and run \fBbundle pristine \fR\. +.IP "\(bu" 4 +\fBno_prune\fR (\fBBUNDLE_NO_PRUNE\fR): Whether Bundler should leave outdated gems unpruned when caching\. +.IP "\(bu" 4 +\fBonly\fR (\fBBUNDLE_ONLY\fR): A space\-separated list of groups to install only gems of the specified groups\. Please check carefully if you want to install also gems without a group, because they get put inside \fBdefault\fR group\. For example \fBonly test:default\fR will install all gems specified in test group and without one\. +.IP "\(bu" 4 +\fBpath\fR (\fBBUNDLE_PATH\fR): The location on disk where all gems in your bundle will be located regardless of \fB$GEM_HOME\fR or \fB$GEM_PATH\fR values\. Bundle gems not found in this location will be installed by \fBbundle install\fR\. When not set, Bundler install by default to a \fB\.bundle\fR directory relative to repository root in Bundler 4, and to the default system path (\fBGem\.dir\fR) before Bundler 4\. That means that before Bundler 4, Bundler shares this location with Rubygems, and \fBgem install \|\.\|\.\|\.\fR will have gems installed in the same location and therefore, gems installed without \fBpath\fR set will show up by calling \fBgem list\fR\. This will not be the case in Bundler 4\. +.IP "\(bu" 4 +\fBpath\.system\fR (\fBBUNDLE_PATH__SYSTEM\fR): Whether Bundler will install gems into the default system path (\fBGem\.dir\fR)\. +.IP "\(bu" 4 +\fBplugins\fR (\fBBUNDLE_PLUGINS\fR): Enable Bundler's experimental plugin system\. +.IP "\(bu" 4 +\fBprefer_patch\fR (\fBBUNDLE_PREFER_PATCH\fR): Prefer updating only to next patch version during updates\. Makes \fBbundle update\fR calls equivalent to \fBbundler update \-\-patch\fR\. +.IP "\(bu" 4 +\fBredirect\fR (\fBBUNDLE_REDIRECT\fR): The number of redirects allowed for network requests\. Defaults to \fB5\fR\. +.IP "\(bu" 4 +\fBretry\fR (\fBBUNDLE_RETRY\fR): The number of times to retry failed network requests\. Defaults to \fB3\fR\. +.IP "\(bu" 4 +\fBshebang\fR (\fBBUNDLE_SHEBANG\fR): The program name that should be invoked for generated binstubs\. Defaults to the ruby install name used to generate the binstub\. +.IP "\(bu" 4 +\fBsilence_deprecations\fR (\fBBUNDLE_SILENCE_DEPRECATIONS\fR): Whether Bundler should silence deprecation warnings for behavior that will be changed in the next major version\. +.IP "\(bu" 4 +\fBsilence_root_warning\fR (\fBBUNDLE_SILENCE_ROOT_WARNING\fR): Silence the warning Bundler prints when installing gems as root\. +.IP "\(bu" 4 +\fBsimulate_version\fR (\fBBUNDLE_SIMULATE_VERSION\fR): The virtual version Bundler should use for activating feature flags\. Can be used to simulate all the new functionality that will be enabled in a future major version\. +.IP "\(bu" 4 +\fBssl_ca_cert\fR (\fBBUNDLE_SSL_CA_CERT\fR): Path to a designated CA certificate file or folder containing multiple certificates for trusted CAs in PEM format\. +.IP "\(bu" 4 +\fBssl_client_cert\fR (\fBBUNDLE_SSL_CLIENT_CERT\fR): Path to a designated file containing a X\.509 client certificate and key in PEM format\. +.IP "\(bu" 4 +\fBssl_verify_mode\fR (\fBBUNDLE_SSL_VERIFY_MODE\fR): The SSL verification mode Bundler uses when making HTTPS requests\. Defaults to verify peer\. +.IP "\(bu" 4 +\fBsystem_bindir\fR (\fBBUNDLE_SYSTEM_BINDIR\fR): The location where RubyGems installs binstubs\. Defaults to \fBGem\.bindir\fR\. +.IP "\(bu" 4 +\fBtimeout\fR (\fBBUNDLE_TIMEOUT\fR): The seconds allowed before timing out for network requests\. Defaults to \fB10\fR\. +.IP "\(bu" 4 +\fBupdate_requires_all_flag\fR (\fBBUNDLE_UPDATE_REQUIRES_ALL_FLAG\fR): Require passing \fB\-\-all\fR to \fBbundle update\fR when everything should be updated, and disallow passing no options to \fBbundle update\fR\. +.IP "\(bu" 4 +\fBuser_agent\fR (\fBBUNDLE_USER_AGENT\fR): The custom user agent fragment Bundler includes in API requests\. +.IP "\(bu" 4 +\fBverbose\fR (\fBBUNDLE_VERBOSE\fR): Whether Bundler should print verbose output\. Defaults to \fBfalse\fR, unless the \fB\-\-verbose\fR CLI flag is used\. +.IP "\(bu" 4 +\fBversion\fR (\fBBUNDLE_VERSION\fR): The version of Bundler to use when running under Bundler environment\. Defaults to \fBlockfile\fR\. You can also specify \fBsystem\fR or \fBx\.y\.z\fR\. \fBlockfile\fR will use the Bundler version specified in the \fBGemfile\.lock\fR, \fBsystem\fR will use the system version of Bundler, and \fBx\.y\.z\fR will use the specified version of Bundler\. +.IP "\(bu" 4 +\fBwith\fR (\fBBUNDLE_WITH\fR): A space\-separated or \fB:\fR\-separated list of groups whose gems bundler should install\. +.IP "\(bu" 4 +\fBwithout\fR (\fBBUNDLE_WITHOUT\fR): A space\-separated or \fB:\fR\-separated list of groups whose gems bundler should not install\. +.IP "" 0 .SH "BUILD OPTIONS" You can use \fBbundle config\fR to give Bundler the flags to pass to the gem installer every time bundler tries to install a particular gem\. .P diff --git a/lib/bundler/man/bundle-config.1.ronn b/lib/bundler/man/bundle-config.1.ronn index 9657e7414514ef..72f891b428d58b 100644 --- a/lib/bundler/man/bundle-config.1.ronn +++ b/lib/bundler/man/bundle-config.1.ronn @@ -137,6 +137,36 @@ learn more about their operation in [bundle install(1)](bundle-install.1.html). explicitly configured. * `console` (`BUNDLE_CONSOLE`): The console that `bundle console` starts. Defaults to `irb`. +* `cooldown` (`BUNDLE_COOLDOWN`): + Number of days a published gem version must age before bundler will + resolve to it. Defaults to unset (no cooldown). Pass `0` to disable + cooldown for an individual run. + + The effective cooldown for any given gem is resolved from three + layers, highest precedence first: + + 1. CLI flag `--cooldown N` on `install`, `update`, `add`, and + `outdated`. + 2. This setting (`bundle config set cooldown N` or + `BUNDLE_COOLDOWN=N`). + 3. The per-source `cooldown:` keyword in the Gemfile, such as + `source "https://rubygems.org", cooldown: 7`. + + The CLI flag and this setting apply uniformly to every source, + including ones declared with their own `cooldown:` value. To keep a + private registry permanently exempt while still cooling down public + gems, declare `source "https://internal", cooldown: 0` in the + Gemfile; remember that `--cooldown N` on the command line will + still override it for that single run. + + Cooldown filtering depends on the gem server providing a per-version + `created_at` timestamp in the v2 compact-index format. Versions + without that metadata - older gem servers, historical entries that + predate the v2 cutover on `rubygems.org`, or private registries that + still emit the v1 format - are treated as outside the cooldown + window and remain resolvable. If you rely on cooldown for + supply-chain protection, confirm that the gem server emits + `created_at` in its `/info/` responses. * `default_cli_command` (`BUNDLE_DEFAULT_CLI_COMMAND`): The command that running `bundle` without arguments should run. Defaults to `cli_help` since Bundler 4, but can also be `install` which was the previous diff --git a/lib/bundler/man/bundle-install.1 b/lib/bundler/man/bundle-install.1 index a764d031ed12c3..801768c7ecefdc 100644 --- a/lib/bundler/man/bundle-install.1 +++ b/lib/bundler/man/bundle-install.1 @@ -4,7 +4,7 @@ .SH "NAME" \fBbundle\-install\fR \- Install the dependencies specified in your Gemfile .SH "SYNOPSIS" -\fBbundle install\fR [\-\-force] [\-\-full\-index] [\-\-gemfile=GEMFILE] [\-\-jobs=NUMBER] [\-\-local] [\-\-lockfile=LOCKFILE] [\-\-no\-cache] [\-\-no\-lock] [\-\-prefer\-local] [\-\-quiet] [\-\-retry=NUMBER] [\-\-standalone[=GROUP[ GROUP\|\.\|\.\|\.]]] [\-\-trust\-policy=TRUST\-POLICY] [\-\-target\-rbconfig=TARGET\-RBCONFIG] +\fBbundle install\fR [\-\-cooldown=NUMBER] [\-\-force] [\-\-full\-index] [\-\-gemfile=GEMFILE] [\-\-jobs=NUMBER] [\-\-local] [\-\-lockfile=LOCKFILE] [\-\-no\-cache] [\-\-no\-lock] [\-\-prefer\-local] [\-\-quiet] [\-\-retry=NUMBER] [\-\-standalone[=GROUP[ GROUP\|\.\|\.\|\.]]] [\-\-trust\-policy=TRUST\-POLICY] [\-\-target\-rbconfig=TARGET\-RBCONFIG] .SH "DESCRIPTION" Install the gems specified in your Gemfile(5)\. If this is the first time you run bundle install (and a \fBGemfile\.lock\fR does not exist), Bundler will fetch all remote sources, resolve dependencies and install all needed gems\. .P @@ -13,6 +13,9 @@ If a \fBGemfile\.lock\fR does exist, and you have not updated your Gemfile(5), B If a \fBGemfile\.lock\fR does exist, and you have updated your Gemfile(5), Bundler will use the dependencies in the \fBGemfile\.lock\fR for all gems that you did not update, but will re\-resolve the dependencies of gems that you did update\. You can find more information about this update process below under \fICONSERVATIVE UPDATING\fR\. .SH "OPTIONS" .TP +\fB\-\-cooldown=\fR +Only consider gem versions published at least \fInumber\fR days ago when resolving\. Pass \fB0\fR to disable cooldown for this run, overriding any per\-source or global configuration\. See \fBcooldown\fR in bundle\-config(1) for details on the precedence between the CLI flag, Bundler config, and Gemfile per\-source settings\. +.TP \fB\-\-force\fR, \fB\-\-redownload\fR Force reinstalling every gem, even if already installed\. .TP diff --git a/lib/bundler/man/bundle-install.1.ronn b/lib/bundler/man/bundle-install.1.ronn index c7d88bfb73c3e4..56fd8bdf42a171 100644 --- a/lib/bundler/man/bundle-install.1.ronn +++ b/lib/bundler/man/bundle-install.1.ronn @@ -3,7 +3,8 @@ bundle-install(1) -- Install the dependencies specified in your Gemfile ## SYNOPSIS -`bundle install` [--force] +`bundle install` [--cooldown=NUMBER] + [--force] [--full-index] [--gemfile=GEMFILE] [--jobs=NUMBER] @@ -37,6 +38,13 @@ update process below under [CONSERVATIVE UPDATING][]. ## OPTIONS +* `--cooldown=`: + Only consider gem versions published at least days ago when + resolving. Pass `0` to disable cooldown for this run, overriding any + per-source or global configuration. See `cooldown` in bundle-config(1) + for details on the precedence between the CLI flag, Bundler config, + and Gemfile per-source settings. + * `--force`, `--redownload`: Force reinstalling every gem, even if already installed. diff --git a/lib/bundler/man/bundle-outdated.1 b/lib/bundler/man/bundle-outdated.1 index b739234d8daf07..c2f8086e241070 100644 --- a/lib/bundler/man/bundle-outdated.1 +++ b/lib/bundler/man/bundle-outdated.1 @@ -4,7 +4,7 @@ .SH "NAME" \fBbundle\-outdated\fR \- List installed gems with newer versions available .SH "SYNOPSIS" -\fBbundle outdated\fR [GEM] [\-\-local] [\-\-pre] [\-\-source] [\-\-filter\-strict | \-\-strict] [\-\-update\-strict] [\-\-parseable | \-\-porcelain] [\-\-group=GROUP] [\-\-groups] [\-\-patch|\-\-minor|\-\-major] [\-\-filter\-major] [\-\-filter\-minor] [\-\-filter\-patch] [\-\-only\-explicit] +\fBbundle outdated\fR [GEM] [\-\-local] [\-\-pre] [\-\-source] [\-\-filter\-strict | \-\-strict] [\-\-update\-strict] [\-\-parseable | \-\-porcelain] [\-\-group=GROUP] [\-\-groups] [\-\-patch|\-\-minor|\-\-major] [\-\-filter\-major] [\-\-filter\-minor] [\-\-filter\-patch] [\-\-only\-explicit] [\-\-cooldown=NUMBER] .SH "DESCRIPTION" Outdated lists the names and versions of gems that have a newer version available in the given source\. Calling outdated with [GEM [GEM]] will only check for newer versions of the given gems\. Prerelease gems are ignored by default\. If your gems are up to date, Bundler will exit with a status of 0\. Otherwise, it will exit 1\. .SH "OPTIONS" @@ -53,6 +53,9 @@ Only list patch newer versions\. .TP \fB\-\-only\-explicit\fR Only list gems specified in your Gemfile, not their dependencies\. +.TP +\fB\-\-cooldown=\fR +Annotate (rather than hide) versions that are still inside the cooldown window of \fInumber\fR days\. The prose output appends "in cooldown for Nd more days" and the table form adds "(cooldown Nd)" to the Latest column\. See \fBcooldown\fR in bundle\-config(1)\. .SH "PATCH LEVEL OPTIONS" See bundle update(1) \fIbundle\-update\.1\.html\fR for details\. .SH "FILTERING OUTPUT" diff --git a/lib/bundler/man/bundle-outdated.1.ronn b/lib/bundler/man/bundle-outdated.1.ronn index 2c692c929be617..e5badac2e99484 100644 --- a/lib/bundler/man/bundle-outdated.1.ronn +++ b/lib/bundler/man/bundle-outdated.1.ronn @@ -16,6 +16,7 @@ bundle-outdated(1) -- List installed gems with newer versions available [--filter-minor] [--filter-patch] [--only-explicit] + [--cooldown=NUMBER] ## DESCRIPTION @@ -71,6 +72,12 @@ are up to date, Bundler will exit with a status of 0. Otherwise, it will exit 1. * `--only-explicit`: Only list gems specified in your Gemfile, not their dependencies. +* `--cooldown=`: + Annotate (rather than hide) versions that are still inside the + cooldown window of days. The prose output appends "in + cooldown for Nd more days" and the table form adds "(cooldown Nd)" to + the Latest column. See `cooldown` in bundle-config(1). + ## PATCH LEVEL OPTIONS See [bundle update(1)](bundle-update.1.html) for details. diff --git a/lib/bundler/man/bundle-update.1 b/lib/bundler/man/bundle-update.1 index 6a749644e31a1d..94161083fc45f6 100644 --- a/lib/bundler/man/bundle-update.1 +++ b/lib/bundler/man/bundle-update.1 @@ -4,7 +4,7 @@ .SH "NAME" \fBbundle\-update\fR \- Update your gems to the latest available versions .SH "SYNOPSIS" -\fBbundle update\fR \fI*gems\fR [\-\-all] [\-\-group=NAME] [\-\-source=NAME] [\-\-local] [\-\-ruby] [\-\-bundler[=VERSION]] [\-\-force] [\-\-full\-index] [\-\-gemfile=GEMFILE] [\-\-jobs=NUMBER] [\-\-quiet] [\-\-patch|\-\-minor|\-\-major] [\-\-pre] [\-\-strict] [\-\-conservative] +\fBbundle update\fR \fI*gems\fR [\-\-all] [\-\-group=NAME] [\-\-source=NAME] [\-\-local] [\-\-ruby] [\-\-bundler[=VERSION]] [\-\-cooldown=NUMBER] [\-\-force] [\-\-full\-index] [\-\-gemfile=GEMFILE] [\-\-jobs=NUMBER] [\-\-quiet] [\-\-patch|\-\-minor|\-\-major] [\-\-pre] [\-\-strict] [\-\-conservative] .SH "DESCRIPTION" Update the gems specified (all gems, if \fB\-\-all\fR flag is used), ignoring the previously installed gems specified in the \fBGemfile\.lock\fR\. In general, you should use bundle install(1) \fIbundle\-install\.1\.html\fR to install the same exact gems and versions across machines\. .P @@ -64,6 +64,9 @@ Do not allow any gem to be updated past latest \fB\-\-patch\fR | \fB\-\-minor\fR .TP \fB\-\-conservative\fR Use bundle install conservative update behavior and do not allow indirect dependencies to be updated\. +.TP +\fB\-\-cooldown=\fR +Only consider gem versions published at least \fInumber\fR days ago when resolving\. Pass \fB0\fR to disable cooldown for this run, overriding any per\-source or global configuration\. Combine with \fB\-\-conservative\fR to minimize transitive churn when bypassing cooldown for an urgent update\. See \fBcooldown\fR in bundle\-config(1)\. .SH "UPDATING ALL GEMS" If you run \fBbundle update \-\-all\fR, bundler will ignore any previously installed gems and resolve all dependencies again based on the latest versions of all gems available in the sources\. .P diff --git a/lib/bundler/man/bundle-update.1.ronn b/lib/bundler/man/bundle-update.1.ronn index bfe381677c8efc..72fbf054d157eb 100644 --- a/lib/bundler/man/bundle-update.1.ronn +++ b/lib/bundler/man/bundle-update.1.ronn @@ -9,6 +9,7 @@ bundle-update(1) -- Update your gems to the latest available versions [--local] [--ruby] [--bundler[=VERSION]] + [--cooldown=NUMBER] [--force] [--full-index] [--gemfile=GEMFILE] @@ -91,6 +92,13 @@ gem. * `--conservative`: Use bundle install conservative update behavior and do not allow indirect dependencies to be updated. +* `--cooldown=`: + Only consider gem versions published at least days ago when + resolving. Pass `0` to disable cooldown for this run, overriding any + per-source or global configuration. Combine with `--conservative` to + minimize transitive churn when bypassing cooldown for an urgent + update. See `cooldown` in bundle-config(1). + ## UPDATING ALL GEMS If you run `bundle update --all`, bundler will ignore From 249b2b37106c7fac6f75a269a50c251634bba2e7 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Thu, 28 May 2026 13:31:05 +0900 Subject: [PATCH 047/188] [ruby/rubygems] Add a v2 compact index artifice and cover cooldown end-to-end A `CompactIndexCooldownAPI` subclass overrides `build_gem_version` to emit `CompactIndex::GemVersionV2` with `created_at` sourced from `spec.date`, letting tests carry a deterministic timestamp per built gem. `spec/install/cooldown_spec.rb` exercises the source DSL keyword, `--cooldown` flag, `BUNDLE_COOLDOWN`, `bundle config set cooldown`, the rolling-delay filter, the lockfile bypass, and the CLI vs Gemfile precedence against the new artifice. https://github.com/ruby/rubygems/commit/1f1309a809 Co-Authored-By: Claude Opus 4.7 --- spec/bundler/install/cooldown_spec.rb | 158 ++++++++++++++++++ .../artifice/compact_index_cooldown.rb | 6 + .../helpers/compact_index_cooldown.rb | 13 ++ spec/bundler/support/shards.rb | 1 + 4 files changed, 178 insertions(+) create mode 100644 spec/bundler/install/cooldown_spec.rb create mode 100644 spec/bundler/support/artifice/compact_index_cooldown.rb create mode 100644 spec/bundler/support/artifice/helpers/compact_index_cooldown.rb diff --git a/spec/bundler/install/cooldown_spec.rb b/spec/bundler/install/cooldown_spec.rb new file mode 100644 index 00000000000000..35415950d82b00 --- /dev/null +++ b/spec/bundler/install/cooldown_spec.rb @@ -0,0 +1,158 @@ +# frozen_string_literal: true + +RSpec.describe "bundle install with the cooldown setting" do + before do + build_repo2 + end + + context "Gemfile DSL" do + it "accepts `source ..., cooldown: N` without error" do + install_gemfile <<-G, artifice: "compact_index" + source "https://gem.repo2", cooldown: 5 + gem "myrack" + G + + expect(the_bundle).to include_gems("myrack 1.0.0") + end + + it "accepts `cooldown: 0` to disable cooldown for a source" do + install_gemfile <<-G, artifice: "compact_index" + source "https://gem.repo2", cooldown: 0 + gem "myrack" + G + + expect(the_bundle).to include_gems("myrack 1.0.0") + end + end + + context "CLI flag" do + before do + gemfile <<-G + source "https://gem.repo2" + gem "myrack" + G + end + + it "accepts --cooldown N on install" do + bundle "install --cooldown 7", artifice: "compact_index" + + expect(the_bundle).to include_gems("myrack 1.0.0") + end + + it "accepts --cooldown 0 as an escape hatch" do + bundle "install --cooldown 0", artifice: "compact_index" + + expect(the_bundle).to include_gems("myrack 1.0.0") + end + end + + context "configuration" do + it "reads BUNDLE_COOLDOWN as an integer" do + gemfile <<-G + source "https://gem.repo2" + gem "myrack" + G + + bundle "install", env: { "BUNDLE_COOLDOWN" => "7" }, artifice: "compact_index" + + expect(the_bundle).to include_gems("myrack 1.0.0") + end + + it "reads `bundle config set cooldown N`" do + gemfile <<-G + source "https://gem.repo2" + gem "myrack" + G + + bundle "config set cooldown 7" + bundle "install", artifice: "compact_index" + + expect(the_bundle).to include_gems("myrack 1.0.0") + end + end + + context "end-to-end with v2 compact index" do + before do + now = Time.now.utc + build_repo3 do + build_gem "ripe_gem", "1.0.0" do |s| + s.date = now - (30 * 86_400) + end + build_gem "ripe_gem", "2.0.0" do |s| + s.date = now - (1 * 86_400) + end + end + end + + it "excludes versions within the cooldown window" do + gemfile <<-G + source "https://gem.repo3" + gem "ripe_gem" + G + + bundle "install --cooldown 7", artifice: "compact_index_cooldown" + + expect(the_bundle).to include_gems("ripe_gem 1.0.0") + end + + it "selects the latest version when --cooldown 0 is passed" do + gemfile <<-G + source "https://gem.repo3" + gem "ripe_gem" + G + + bundle "install --cooldown 0", artifice: "compact_index_cooldown" + + expect(the_bundle).to include_gems("ripe_gem 2.0.0") + end + + it "applies cooldown declared per-source in the Gemfile" do + gemfile <<-G + source "https://gem.repo3", cooldown: 7 + gem "ripe_gem" + G + + bundle "install", artifice: "compact_index_cooldown" + + expect(the_bundle).to include_gems("ripe_gem 1.0.0") + end + + it "is overridden by CLI --cooldown when Gemfile sets a different per-source value" do + gemfile <<-G + source "https://gem.repo3", cooldown: 0 + gem "ripe_gem" + G + + bundle "install --cooldown 7", artifice: "compact_index_cooldown" + + expect(the_bundle).to include_gems("ripe_gem 1.0.0") + end + + it "bypasses cooldown when bundle install uses an existing lockfile" do + gemfile <<-G + source "https://gem.repo3" + gem "ripe_gem" + G + + lockfile <<-L + GEM + remote: https://gem.repo3/ + specs: + ripe_gem (2.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + ripe_gem + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "install --cooldown 7", artifice: "compact_index_cooldown" + + expect(the_bundle).to include_gems("ripe_gem 2.0.0") + end + end +end diff --git a/spec/bundler/support/artifice/compact_index_cooldown.rb b/spec/bundler/support/artifice/compact_index_cooldown.rb new file mode 100644 index 00000000000000..85e3173c989cac --- /dev/null +++ b/spec/bundler/support/artifice/compact_index_cooldown.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +require_relative "helpers/compact_index_cooldown" +require_relative "helpers/artifice" + +Artifice.activate_with(CompactIndexCooldownAPI) diff --git a/spec/bundler/support/artifice/helpers/compact_index_cooldown.rb b/spec/bundler/support/artifice/helpers/compact_index_cooldown.rb new file mode 100644 index 00000000000000..9920fd2c9520cb --- /dev/null +++ b/spec/bundler/support/artifice/helpers/compact_index_cooldown.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require_relative "compact_index" + +class CompactIndexCooldownAPI < CompactIndexAPI + helpers do + def build_gem_version(spec, deps, checksum) + created_at = spec.date&.utc&.iso8601 + CompactIndex::GemVersionV2.new(spec.version.version, spec.platform.to_s, checksum, nil, + deps, spec.required_ruby_version.to_s, spec.required_rubygems_version.to_s, created_at) + end + end +end diff --git a/spec/bundler/support/shards.rb b/spec/bundler/support/shards.rb index b27aecad1697c0..25563c765c004d 100644 --- a/spec/bundler/support/shards.rb +++ b/spec/bundler/support/shards.rb @@ -144,6 +144,7 @@ module Shards ], shard_d: [ "spec/bundler/resolver/cooldown_spec.rb", + "spec/install/cooldown_spec.rb", "spec/commands/outdated_spec.rb", "spec/commands/update_spec.rb", "spec/lock/lockfile_spec.rb", From 5e7d68f99e99623a233bb7e36a4eceb13ce79532 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 29 May 2026 16:37:50 +0900 Subject: [PATCH 048/188] [ruby/rubygems] Reject negative --cooldown values from the CLI The Gemfile DSL already rejects `cooldown: -7`, but `--cooldown -7` on install/update/add/outdated slipped through Thor and ended up disabling the filter silently via the `days <= 0` short-circuit in the resolver. Add a shared CLI::Common.validate_cooldown! guard so the CLI surface fails loud with the same message as the DSL. https://github.com/ruby/rubygems/commit/45e54d5b4c Co-Authored-By: Claude Opus 4.7 --- lib/bundler/cli/add.rb | 1 + lib/bundler/cli/common.rb | 6 ++++++ lib/bundler/cli/install.rb | 1 + lib/bundler/cli/outdated.rb | 1 + lib/bundler/cli/update.rb | 1 + 5 files changed, 10 insertions(+) diff --git a/lib/bundler/cli/add.rb b/lib/bundler/cli/add.rb index 7c0d7231f6ee8c..20f76b59d10a90 100644 --- a/lib/bundler/cli/add.rb +++ b/lib/bundler/cli/add.rb @@ -14,6 +14,7 @@ def initialize(options, gems) def run Bundler.ui.level = "warn" if options[:quiet] + Bundler::CLI::Common.validate_cooldown!(options[:cooldown]) Bundler.settings.set_command_option_if_given :cooldown, options[:cooldown] validate_options! diff --git a/lib/bundler/cli/common.rb b/lib/bundler/cli/common.rb index 2f332ff364404d..b44fbc30964157 100644 --- a/lib/bundler/cli/common.rb +++ b/lib/bundler/cli/common.rb @@ -2,6 +2,12 @@ module Bundler module CLI::Common + def self.validate_cooldown!(value) + return if value.nil? + return if value.is_a?(Integer) && value >= 0 + raise InvalidOption, "Expected `--cooldown` to be a non-negative integer, got #{value.inspect}" + end + def self.output_post_install_messages(messages) return if Bundler.settings["ignore_messages"] messages.to_a.each do |name, msg| diff --git a/lib/bundler/cli/install.rb b/lib/bundler/cli/install.rb index 7dd196fbfbe442..69affd1a109ab4 100644 --- a/lib/bundler/cli/install.rb +++ b/lib/bundler/cli/install.rb @@ -112,6 +112,7 @@ def normalize_settings Bundler.settings.set_command_option_if_given :jobs, options["jobs"] + Bundler::CLI::Common.validate_cooldown!(options["cooldown"]) Bundler.settings.set_command_option_if_given :cooldown, options["cooldown"] Bundler.settings.set_command_option_if_given :no_prune, options["no-prune"] diff --git a/lib/bundler/cli/outdated.rb b/lib/bundler/cli/outdated.rb index f298c2c8d86057..465e56ada2cc78 100644 --- a/lib/bundler/cli/outdated.rb +++ b/lib/bundler/cli/outdated.rb @@ -26,6 +26,7 @@ def initialize(options, gems) def run check_for_deployment_mode! + Bundler::CLI::Common.validate_cooldown!(options[:cooldown]) Bundler.settings.set_command_option_if_given :cooldown, options[:cooldown] Bundler.definition.validate_runtime! diff --git a/lib/bundler/cli/update.rb b/lib/bundler/cli/update.rb index 7ac472bd4b48c3..d92ffd995f3604 100644 --- a/lib/bundler/cli/update.rb +++ b/lib/bundler/cli/update.rb @@ -66,6 +66,7 @@ def run opts["force"] = options[:redownload] if options[:redownload] Bundler.settings.set_command_option_if_given :jobs, opts["jobs"] + Bundler::CLI::Common.validate_cooldown!(options[:cooldown]) Bundler.settings.set_command_option_if_given :cooldown, options[:cooldown] Bundler.definition.validate_runtime! From 325228e2b802d350afa01a37689fac6f2ca33f1d Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 29 May 2026 16:38:03 +0900 Subject: [PATCH 049/188] [ruby/rubygems] Cover cooldown outdated annotation and update error path Adds three E2E checks against the v2 cooldown artifice: the negative CLI value is rejected at parse time, `bundle outdated --cooldown` tags the latest-but-cooled version in both table and parseable output, and `bundle update --cooldown 99999` surfaces the cooldown hint when every candidate is filtered. These were the remaining coverage gaps called out in the adversarial review. https://github.com/ruby/rubygems/commit/3df6b300f7 Co-Authored-By: Claude Opus 4.7 --- spec/bundler/install/cooldown_spec.rb | 88 +++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/spec/bundler/install/cooldown_spec.rb b/spec/bundler/install/cooldown_spec.rb index 35415950d82b00..1368726c2c7722 100644 --- a/spec/bundler/install/cooldown_spec.rb +++ b/spec/bundler/install/cooldown_spec.rb @@ -44,6 +44,12 @@ expect(the_bundle).to include_gems("myrack 1.0.0") end + + it "rejects a negative --cooldown value" do + bundle "install --cooldown=-7", artifice: "compact_index", raise_on_error: false + + expect(err).to match(/non-negative integer/) + end end context "configuration" do @@ -154,5 +160,87 @@ expect(the_bundle).to include_gems("ripe_gem 2.0.0") end + + it "annotates in-cooldown versions in bundle outdated table output" do + gemfile <<-G + source "https://gem.repo3" + gem "ripe_gem", "1.0.0" + G + + lockfile <<-L + GEM + remote: https://gem.repo3/ + specs: + ripe_gem (1.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + ripe_gem (= 1.0.0) + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "outdated --cooldown 7", artifice: "compact_index_cooldown", raise_on_error: false + + expect(out).to match(/ripe_gem.*\(cooldown \d+d\)/) + end + + it "annotates in-cooldown versions in bundle outdated --parseable output" do + gemfile <<-G + source "https://gem.repo3" + gem "ripe_gem", "1.0.0" + G + + lockfile <<-L + GEM + remote: https://gem.repo3/ + specs: + ripe_gem (1.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + ripe_gem (= 1.0.0) + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "outdated --cooldown 7 --parseable", artifice: "compact_index_cooldown", raise_on_error: false + + expect(out).to match(/ripe_gem.*in cooldown for \d+ more day/) + end + + it "surfaces a cooldown hint when bundle update filters every candidate" do + gemfile <<-G + source "https://gem.repo3" + gem "ripe_gem" + G + + lockfile <<-L + GEM + remote: https://gem.repo3/ + specs: + ripe_gem (1.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + ripe_gem + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "update ripe_gem --cooldown 99999", artifice: "compact_index_cooldown", raise_on_error: false + + expect(err).to match(/excluded by the cooldown setting/) + expect(err).to match(/--cooldown 0/) + end end end From 056ae1f9263fd78d8a087a3744d868b52d957651 Mon Sep 17 00:00:00 2001 From: FletcherDares <52580867+FletcherDares@users.noreply.github.com> Date: Tue, 2 Jun 2026 00:45:55 -0400 Subject: [PATCH 050/188] Centralize `ISASCII` early exit for `search_nonascii` (#17166) --- benchmark/string_coderange_scan.yml | 10 ++++ string.c | 74 +++++++++++++---------------- 2 files changed, 42 insertions(+), 42 deletions(-) create mode 100644 benchmark/string_coderange_scan.yml diff --git a/benchmark/string_coderange_scan.yml b/benchmark/string_coderange_scan.yml new file mode 100644 index 00000000000000..d47bbd2b30313c --- /dev/null +++ b/benchmark/string_coderange_scan.yml @@ -0,0 +1,10 @@ +prelude: | + def unknown(s) = s.b.force_encoding("UTF-8") + multibyte = unknown("\u{00e9}" * 16384) # best case: every byte non-ASCII + alternating = unknown("\u{00e9}a" * 10922) # worst case: non-ASCII then ASCII + ascii = unknown("a" * 32768) # baseline + +benchmark: + coderange_multibyte: multibyte.dup.valid_encoding? + coderange_alternating: alternating.dup.valid_encoding? + coderange_ascii: ascii.dup.valid_encoding? diff --git a/string.c b/string.c index 61d4f16679d26b..134e1254318f74 100644 --- a/string.c +++ b/string.c @@ -713,6 +713,10 @@ search_nonascii(const char *p, const char *e) { const char *s, *t; + if (p < e && !ISASCII(*p)) { + return p; + } + #if defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L) # if SIZEOF_UINTPTR_T == 8 # define NONASCII_MASK UINT64_C(0x8080808080808080) @@ -2297,26 +2301,22 @@ enc_strlen(const char *p, const char *e, rb_encoding *enc, int cr) c = 0; if (ENC_CODERANGE_CLEAN_P(cr)) { while (p < e) { - if (ISASCII(*p)) { - q = search_nonascii(p, e); - if (!q) - return c + (e - p); - c += q - p; - p = q; - } + q = search_nonascii(p, e); + if (!q) + return c + (e - p); + c += q - p; + p = q; p += rb_enc_fast_mbclen(p, e, enc); c++; } } else { while (p < e) { - if (ISASCII(*p)) { - q = search_nonascii(p, e); - if (!q) - return c + (e - p); - c += q - p; - p = q; - } + q = search_nonascii(p, e); + if (!q) + return c + (e - p); + c += q - p; + p = q; p += rb_enc_mbclen(p, e, enc); c++; } @@ -2354,15 +2354,13 @@ rb_enc_strlen_cr(const char *p, const char *e, rb_encoding *enc, int *cr) else if (rb_enc_asciicompat(enc)) { c = 0; while (p < e) { - if (ISASCII(*p)) { - q = search_nonascii(p, e); - if (!q) { - if (!*cr) *cr = ENC_CODERANGE_7BIT; - return c + (e - p); - } - c += q - p; - p = q; + q = search_nonascii(p, e); + if (!q) { + if (!*cr) *cr = ENC_CODERANGE_7BIT; + return c + (e - p); } + c += q - p; + p = q; ret = rb_enc_precise_mbclen(p, e, enc); if (MBCLEN_CHARFOUND_P(ret)) { *cr |= ENC_CODERANGE_VALID; @@ -3020,16 +3018,14 @@ str_nth_len(const char *p, const char *e, long *nthp, rb_encoding *enc) *nthp = nth; return (char *)e; } - if (ISASCII(*p)) { - p2 = search_nonascii(p, e2); - if (!p2) { - nth -= e2 - p; - *nthp = nth; - return (char *)e2; - } - nth -= p2 - p; - p = p2; + p2 = search_nonascii(p, e2); + if (!p2) { + nth -= e2 - p; + *nthp = nth; + return (char *)e2; } + nth -= p2 - p; + p = p2; n = rb_enc_mbclen(p, e, enc); p += n; nth--; @@ -11850,17 +11846,11 @@ enc_str_scrub(rb_encoding *enc, VALUE str, VALUE repl, int cr) else if (MBCLEN_CHARFOUND_P(ret)) { cr = ENC_CODERANGE_VALID; p += MBCLEN_CHARFOUND_LEN(ret); - /* - * After a valid multibyte character, skip the following ASCII run. - * If the next byte is already non-ASCII, search_nonascii would only - * rediscover p after its word-at-a-time setup. - */ - if (p < e && ISASCII(*p)) { - p = search_nonascii(p, e); - if (!p) { - p = e; - break; - } + /* After a multibyte character, fast-skip the following ASCII run. */ + p = search_nonascii(p, e); + if (!p) { + p = e; + break; } } else if (MBCLEN_INVALID_P(ret)) { From 85005f9d3797c137a8388d7cf2299ce0e0849d4f Mon Sep 17 00:00:00 2001 From: Burdette Lamar Date: Tue, 2 Jun 2026 00:17:23 -0500 Subject: [PATCH 051/188] [ruby/find] [DOC] Doc for Find (https://github.com/ruby/find/pull/24) https://github.com/ruby/find/commit/e49a5cf1bd --- lib/find.rb | 82 +++++++++++++++++++++++++++++++++-------------------- 1 file changed, 52 insertions(+), 30 deletions(-) diff --git a/lib/find.rb b/lib/find.rb index 8223eea4560319..d9b81eb92d1920 100644 --- a/lib/find.rb +++ b/lib/find.rb @@ -3,41 +3,50 @@ # find.rb: the Find module for processing all files under a given directory. # +# :markup: markdown # -# The +Find+ module supports the top-down traversal of a set of file paths. -# -# For example, to total the size of all files under your home directory, -# ignoring anything in a "dot" directory (e.g. $HOME/.ssh): -# -# require 'find' -# -# total_size = 0 -# -# Find.find(ENV["HOME"]) do |path| -# if FileTest.directory?(path) -# if File.basename(path).start_with?('.') -# Find.prune # Don't look any further into this directory. -# else -# next -# end -# else -# total_size += FileTest.size(path) -# end -# end -# +# \Module \Find supports the top-down traversal of entries in the file system. module Find # The version string VERSION = "0.2.0" + # :markup: markdown # - # Calls the associated block with the name of every file and directory listed - # as arguments, then recursively on their subdirectories, and so on. + # With a block given, performs a depth-first traversal of each given path in `paths`; + # calls the block with each found file or directory path: # - # Returns an enumerator if no block is given. + # ```ruby + # paths = [] + # Find.find('bin', 'jit') {|path| paths << path } + # paths + # # => + # # ["bin", + # # "bin/gem", + # # "jit", + # # "jit/Cargo.toml", + # # "jit/src", + # # "jit/src/lib.rs"] + # ``` # - # See the +Find+ module documentation for an example. + # Raises an exception if a given path cannot be read. # + # When keyword argument `ignore_error` is given as `true` (the default), + # certain exceptions during traversal are ignored (i.e., silently rescued): + # Errno::ENOENT, Errno::EACCES, Errno::ENOTDIR, Errno::ELOOP, Errno::ENAMETOOLONG, Errno::EINVAL; + # when given as `false`, no exceptions are rescued. + # + # Note that these exceptions may be ignored only in `Find` traversal code; + # an exception raised before traversal begins, + # or raised while in the block is not ignored. + # Each of the calls below raises an Errno::ENOENT exception that is not ignored: + # + # ```ruby + # Find.find('nosuch') { } + # Find.find('lib') {|entry| raise Errno::ENOENT } + # ``` + # + # With no block given, returns a new Enumerator. def find(*paths, ignore_error: true) # :yield: path block_given? or return enum_for(__method__, *paths, ignore_error: ignore_error) @@ -75,13 +84,26 @@ def find(*paths, ignore_error: true) # :yield: path nil end + # :markup: markdown + # + # call-seq: + # Find.prune + # + # This method is meaningful only within a block given with Find.find. # - # Skips the current file or directory, restarting the loop with the next - # entry. If the current file is a directory, that directory will not be - # recursively entered. Meaningful only within the block associated with - # Find::find. + # Inside such a block, + # "prunes" the traversed file tree by not descending into the current directory: # - # See the +Find+ module documentation for an example. + # ```ruby + # files = [] + # Find.find('.') do |path| + # Find.prune if File.basename(path) == 'test' + # next unless File.file?(path) && File.extname(path) == '.rb' + # files << path + # end + # files.size # => 6690 + # files.take(3) # => ["./KNOWNBUGS.rb", "./array.rb", "./ast.rb"] + # ``` # def prune throw :prune From a078e6bac2351b42db35dba0b85999f181a28c25 Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Mon, 1 Jun 2026 21:11:59 +0900 Subject: [PATCH 052/188] Remove redundant include darray.h in gc.c --- gc.c | 1 - 1 file changed, 1 deletion(-) diff --git a/gc.c b/gc.c index 7d65f1152a316f..2772f956387bfd 100644 --- a/gc.c +++ b/gc.c @@ -79,7 +79,6 @@ #undef LIST_HEAD /* ccan/list conflicts with BSD-origin sys/queue.h. */ #include "constant.h" -#include "darray.h" #include "debug_counter.h" #include "eval_intern.h" #include "gc/gc.h" From a4031fcacb1856fe6f846cc56fc3835a029fd40a Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 2 Jun 2026 15:11:19 +0900 Subject: [PATCH 053/188] [ruby/psych] v5.4.0 https://github.com/ruby/psych/commit/f7066d8f5e --- ext/psych/lib/psych/versions.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/psych/lib/psych/versions.rb b/ext/psych/lib/psych/versions.rb index 4c7a80d5c84274..6c1679bf65bd00 100644 --- a/ext/psych/lib/psych/versions.rb +++ b/ext/psych/lib/psych/versions.rb @@ -2,7 +2,7 @@ module Psych # The version of Psych you are using - VERSION = '5.3.1' + VERSION = '5.4.0' if RUBY_ENGINE == 'jruby' DEFAULT_SNAKEYAML_VERSION = '2.10'.freeze From 24e0c321190577140ffc91da409462b4ca0c89ae Mon Sep 17 00:00:00 2001 From: git Date: Tue, 2 Jun 2026 06:13:00 +0000 Subject: [PATCH 054/188] Update default gems list at a4031fcacb1856fe6f846cc56fc383 [ci skip] --- NEWS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/NEWS.md b/NEWS.md index 038f1a63c6a67b..8699bc0bff3faa 100644 --- a/NEWS.md +++ b/NEWS.md @@ -88,6 +88,7 @@ releases. * 4.0.0 to [v4.0.1][openssl-v4.0.1], [v4.0.2][openssl-v4.0.2] * prism 1.9.0 * 1.7.0 to [v1.8.0][prism-v1.8.0], [v1.8.1][prism-v1.8.1], [v1.9.0][prism-v1.9.0] +* psych 5.4.0 * resolv 0.7.1 * 0.7.0 to [v0.7.1][resolv-v0.7.1] * stringio 3.2.1.dev From abef26e062bb94525bc9891351aa226e00f9df6c Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 2 Jun 2026 15:01:47 +0900 Subject: [PATCH 055/188] [ruby/rubygems] Parse created_at via Time.new instead of Time.iso8601 A full ISO-8601 timestamp with seconds (the format compact index v2 sends, e.g. 2026-05-30T08:52:10Z) is recognised by Time.new directly, so we can drop the require "time" and the indirection through Time.iso8601 while keeping the same ArgumentError fallback for malformed input. https://github.com/ruby/rubygems/commit/d14e3ce964 Co-Authored-By: Claude Opus 4.7 --- lib/bundler/endpoint_specification.rb | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/bundler/endpoint_specification.rb b/lib/bundler/endpoint_specification.rb index 89ffe3d740df39..7c7ce107e205db 100644 --- a/lib/bundler/endpoint_specification.rb +++ b/lib/bundler/endpoint_specification.rb @@ -165,11 +165,10 @@ def parse_metadata(data) when "created_at" value = v.is_a?(Array) ? v.last : v if value.is_a?(String) - require "time" - begin - @created_at = Time.iso8601(value) + @created_at = begin + Time.new(value) rescue ArgumentError - @created_at = nil + nil end end end From 1ee42d290dedc1430c1befd80ed9757c8b68fd2d Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 2 Jun 2026 14:45:46 +0900 Subject: [PATCH 056/188] [ruby/rubygems] Apply cooldown to locally installed gem versions `Source::Rubygems#specs` merges `installed_specs` on top of `remote_specs`, so a `Bundler::StubSpecification` for an already-installed gem overwrites the matching `EndpointSpecification` and erases its `created_at`. The cooldown filter then short-circuited on `spec.respond_to?(:created_at)` and let the local stub through, which made `bundle install --cooldown N` keep selecting a brand-new version that happened to be on disk already. Snapshot the remote `created_at` per `[name, version]` before merging and back-fill it onto stubs that lack one, attaching the source's first remote so `effective_cooldown` is reachable. The filter now runs ahead of `filter_remote_specs` and rejects every spec that shares an `[name, version]` flagged by `cooldown_excluded?`, so a stub and the endpoint that carries its date drop together. `RemoteSpecification` gains `attr_accessor :created_at` so any subclass without an explicit setter participates. `spec/bundler/resolver/cooldown_spec.rb` gets `name`/`version` on the shared spec helper, plus dedicated coverage for the version-grouped exclusion and stub-only fallback. `spec/install/cooldown_spec.rb` adds two end-to-end cases that pre-install `ripe_gem-2.0.0` and verify the in-cooldown copy is excluded while `--cooldown 0` continues to bypass the filter. https://github.com/ruby/rubygems/commit/3920a092da Co-Authored-By: Claude Opus 4.7 --- lib/bundler/remote_specification.rb | 2 +- lib/bundler/resolver.rb | 18 ++++---- lib/bundler/source/rubygems.rb | 37 +++++++++++++++ .../bundler/bundler/resolver/cooldown_spec.rb | 45 +++++++++++++++---- spec/bundler/install/cooldown_spec.rb | 26 +++++++++++ 5 files changed, 110 insertions(+), 18 deletions(-) diff --git a/lib/bundler/remote_specification.rb b/lib/bundler/remote_specification.rb index ab163e2b046acd..dcaaf6af2e61ed 100644 --- a/lib/bundler/remote_specification.rb +++ b/lib/bundler/remote_specification.rb @@ -12,7 +12,7 @@ class RemoteSpecification attr_reader :name, :version, :platform attr_writer :dependencies - attr_accessor :source, :remote, :locked_platform + attr_accessor :source, :remote, :locked_platform, :created_at def initialize(name, version, platform, spec_fetcher) @name = name diff --git a/lib/bundler/resolver.rb b/lib/bundler/resolver.rb index 7e2ce321b6452a..43b19440e642fb 100644 --- a/lib/bundler/resolver.rb +++ b/lib/bundler/resolver.rb @@ -422,7 +422,7 @@ def filter_matching_specs(specs, requirements) end def filter_specs(specs, package) - filter_cooldown(filter_remote_specs(filter_prereleases(specs, package), package)) + filter_remote_specs(filter_cooldown(filter_prereleases(specs, package)), package) end def filter_prereleases(specs, package) @@ -433,19 +433,19 @@ def filter_prereleases(specs, package) def filter_cooldown(specs) return specs if specs.empty? - excluded = cooldown_excluded_specs(specs) - return specs if excluded.empty? - specs - excluded + excluded_versions = cooldown_excluded_versions(specs) + return specs if excluded_versions.empty? + specs.reject {|s| excluded_versions.include?([s.name, s.version]) } end - def cooldown_excluded_specs(specs) - specs.select {|spec| cooldown_excluded?(spec) } + def cooldown_excluded_versions(specs) + specs.select {|spec| cooldown_excluded?(spec) }.map {|spec| [spec.name, spec.version] }.uniq end def cooldown_hint(specs) - excluded = cooldown_excluded_specs(specs) - return nil if excluded.empty? - "#{excluded.size} version#{"s" if excluded.size > 1} excluded by the cooldown setting; pass `--cooldown 0` to bypass" + excluded_versions = cooldown_excluded_versions(specs) + return nil if excluded_versions.empty? + "#{excluded_versions.size} version#{"s" if excluded_versions.size > 1} excluded by the cooldown setting; pass `--cooldown 0` to bypass" end def cooldown_excluded?(spec) diff --git a/lib/bundler/source/rubygems.rb b/lib/bundler/source/rubygems.rb index 66b36645199e85..9dde405e2c740b 100644 --- a/lib/bundler/source/rubygems.rb +++ b/lib/bundler/source/rubygems.rb @@ -150,6 +150,13 @@ def specs # sources, and large_idx.merge! small_idx is way faster than # small_idx.merge! large_idx. index = @allow_remote ? remote_specs.dup : Index.new + + # Snapshot per-version `created_at` from the remote info before installed + # / cached specs overwrite the EndpointSpecification objects that carry + # it. The cooldown filter consults `created_at` on every candidate, so + # local stubs need the published date back-filled to participate. + remote_created_at = collect_remote_created_at(index) + index.merge!(cached_specs) if @allow_cached index.merge!(installed_specs) if @allow_local @@ -163,6 +170,8 @@ def specs end end + backfill_created_at(index, remote_created_at) unless remote_created_at.empty? + index end end @@ -470,6 +479,34 @@ def cache_path private + def collect_remote_created_at(index) + return {} unless @allow_remote + + snapshot = {} + index.each do |spec| + next unless spec.respond_to?(:created_at) && spec.created_at + snapshot[[spec.name, spec.version]] = spec.created_at + end + snapshot + end + + def backfill_created_at(index, snapshot) + first_remote = remote_fetchers.keys.first + index.each do |spec| + next unless spec.respond_to?(:created_at=) + next if spec.created_at + remote_created_at = snapshot[[spec.name, spec.version]] + next unless remote_created_at + spec.created_at = remote_created_at + # The cooldown filter consults `spec.remote.effective_cooldown`, so a + # backfilled stub also needs a Source::Rubygems::Remote reference. Any + # remote on the source carries the right `effective_cooldown` because + # the setting is source-wide and `Bundler.settings[:cooldown]` + # overrides per-source. + spec.remote ||= first_remote if first_remote && spec.respond_to?(:remote=) + end + end + def lockfile_remotes @lockfile_remotes || credless_remotes end diff --git a/spec/bundler/bundler/resolver/cooldown_spec.rb b/spec/bundler/bundler/resolver/cooldown_spec.rb index 834842145fa615..37ec158cba4fcf 100644 --- a/spec/bundler/bundler/resolver/cooldown_spec.rb +++ b/spec/bundler/bundler/resolver/cooldown_spec.rb @@ -7,8 +7,8 @@ def remote(cooldown:) instance_double(Bundler::Source::Rubygems::Remote, effective_cooldown: cooldown) end - def spec(created_at:, remote:) - Struct.new(:created_at, :remote).new(created_at, remote) + def spec(created_at:, remote:, name: "myrack", version: "1.0.0") + Struct.new(:name, :version, :created_at, :remote).new(name, Gem::Version.new(version), created_at, remote) end describe "#filter_cooldown" do @@ -18,8 +18,8 @@ def spec(created_at:, remote:) let(:r) { remote(cooldown: 7) } it "rejects versions published within the window" do - recent = spec(created_at: now - (2 * 86_400), remote: r) - old = spec(created_at: now - (30 * 86_400), remote: r) + recent = spec(version: "1.1.0", created_at: now - (2 * 86_400), remote: r) + old = spec(version: "1.0.0", created_at: now - (30 * 86_400), remote: r) expect(resolver.send(:filter_cooldown, [recent, old])).to eq([old]) end @@ -32,14 +32,37 @@ def spec(created_at:, remote:) it "leaves rolling-delay history intact" do # 7-day cooldown with frequent releases must still expose an older candidate. - in_cooldown = spec(created_at: now - 86_400, remote: r) - also_in_cooldown = spec(created_at: now - (3 * 86_400), remote: r) - eligible = spec(created_at: now - (10 * 86_400), remote: r) + in_cooldown = spec(version: "1.2.0", created_at: now - 86_400, remote: r) + also_in_cooldown = spec(version: "1.1.0", created_at: now - (3 * 86_400), remote: r) + eligible = spec(version: "1.0.0", created_at: now - (10 * 86_400), remote: r) result = resolver.send(:filter_cooldown, [in_cooldown, also_in_cooldown, eligible]) expect(result).to eq([eligible]) end + + it "drops every spec sharing an excluded [name, version] tuple" do + # The cooldown check is by version, not per-spec: a StubSpecification for an + # in-cooldown release would otherwise slip through on local install paths. + endpoint = spec(version: "2.0.0", created_at: now - 86_400, remote: r) + local_stub = Struct.new(:name, :version).new("myrack", Gem::Version.new("2.0.0")) + eligible = spec(version: "1.0.0", created_at: now - (30 * 86_400), remote: r) + + result = resolver.send(:filter_cooldown, [endpoint, local_stub, eligible]) + + expect(result).to eq([eligible]) + end + + it "keeps stub-only versions that no endpoint marks as in cooldown" do + # If no remote spec carries created_at for a version, cooldown cannot judge it; + # the stub stays in. + local_only = Struct.new(:name, :version).new("myrack", Gem::Version.new("2.0.0")) + eligible = spec(version: "1.0.0", created_at: now - (30 * 86_400), remote: r) + + result = resolver.send(:filter_cooldown, [local_only, eligible]) + + expect(result).to eq([local_only, eligible]) + end end context "when created_at is missing (blank metadata)" do @@ -111,9 +134,15 @@ def spec(created_at:, remote:) end it "uses plural wording when multiple versions are excluded" do - excluded = Array.new(3) { spec(created_at: now - 86_400, remote: r) } + excluded = %w[1.0.0 1.1.0 1.2.0].map {|v| spec(version: v, created_at: now - 86_400, remote: r) } expect(resolver.send(:cooldown_hint, excluded)).to match(/3 versions excluded/) end + + it "counts each unique version once even when multiple spec instances share it" do + duplicates = Array.new(3) { spec(created_at: now - 86_400, remote: r) } + + expect(resolver.send(:cooldown_hint, duplicates)).to match(/1 version excluded/) + end end end diff --git a/spec/bundler/install/cooldown_spec.rb b/spec/bundler/install/cooldown_spec.rb index 1368726c2c7722..b3f57d93ccf5c8 100644 --- a/spec/bundler/install/cooldown_spec.rb +++ b/spec/bundler/install/cooldown_spec.rb @@ -215,6 +215,32 @@ expect(out).to match(/ripe_gem.*in cooldown for \d+ more day/) end + it "excludes a locally-installed version that is still within the cooldown window" do + system_gems "ripe_gem-2.0.0", gem_repo: gem_repo3 + + gemfile <<-G + source "https://gem.repo3" + gem "ripe_gem" + G + + bundle "install --cooldown 7", artifice: "compact_index_cooldown" + + expect(the_bundle).to include_gems("ripe_gem 1.0.0") + end + + it "selects a locally-installed in-cooldown version when --cooldown 0 bypasses the filter" do + system_gems "ripe_gem-2.0.0", gem_repo: gem_repo3 + + gemfile <<-G + source "https://gem.repo3" + gem "ripe_gem" + G + + bundle "install --cooldown 0", artifice: "compact_index_cooldown" + + expect(the_bundle).to include_gems("ripe_gem 2.0.0") + end + it "surfaces a cooldown hint when bundle update filters every candidate" do gemfile <<-G source "https://gem.repo3" From 4af3a57bb69752cc79ee853f3ddb70f119732ac4 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 2 Jun 2026 15:30:51 +0900 Subject: [PATCH 057/188] [ruby/rubygems] Address PR review on cooldown local stub bypass Snapshot the remote alongside `created_at` and restore it during backfill so per-remote cooldown settings (`source ..., cooldown: N`) survive into installed stubs instead of being replaced by whichever remote happens to be first. Build `cooldown_excluded_versions` as a hash so `filter_cooldown`'s membership check is O(1) per spec; large Gemfiles with many candidate versions no longer pay an O(n*m) scan. https://github.com/ruby/rubygems/commit/58669f2890 Co-Authored-By: Claude Opus 4.7 --- lib/bundler/resolver.rb | 7 ++++++- lib/bundler/source/rubygems.rb | 15 ++++++--------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/lib/bundler/resolver.rb b/lib/bundler/resolver.rb index 43b19440e642fb..422b726980d6fa 100644 --- a/lib/bundler/resolver.rb +++ b/lib/bundler/resolver.rb @@ -439,7 +439,12 @@ def filter_cooldown(specs) end def cooldown_excluded_versions(specs) - specs.select {|spec| cooldown_excluded?(spec) }.map {|spec| [spec.name, spec.version] }.uniq + excluded = {} + specs.each do |spec| + next unless cooldown_excluded?(spec) + excluded[[spec.name, spec.version]] = true + end + excluded end def cooldown_hint(specs) diff --git a/lib/bundler/source/rubygems.rb b/lib/bundler/source/rubygems.rb index 9dde405e2c740b..ed864604fe1649 100644 --- a/lib/bundler/source/rubygems.rb +++ b/lib/bundler/source/rubygems.rb @@ -485,25 +485,22 @@ def collect_remote_created_at(index) snapshot = {} index.each do |spec| next unless spec.respond_to?(:created_at) && spec.created_at - snapshot[[spec.name, spec.version]] = spec.created_at + # Remember the remote that supplied the date too: when a source has + # several remotes with different per-URI cooldown settings we must + # restore the same one during backfill so `effective_cooldown` agrees. + snapshot[[spec.name, spec.version]] = [spec.created_at, spec.remote] end snapshot end def backfill_created_at(index, snapshot) - first_remote = remote_fetchers.keys.first index.each do |spec| next unless spec.respond_to?(:created_at=) next if spec.created_at - remote_created_at = snapshot[[spec.name, spec.version]] + remote_created_at, remote = snapshot[[spec.name, spec.version]] next unless remote_created_at spec.created_at = remote_created_at - # The cooldown filter consults `spec.remote.effective_cooldown`, so a - # backfilled stub also needs a Source::Rubygems::Remote reference. Any - # remote on the source carries the right `effective_cooldown` because - # the setting is source-wide and `Bundler.settings[:cooldown]` - # overrides per-source. - spec.remote ||= first_remote if first_remote && spec.respond_to?(:remote=) + spec.remote ||= remote if remote && spec.respond_to?(:remote=) end end From 3220428912b020e23caea42271ff030d195457b9 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 2 Jun 2026 15:49:36 +0900 Subject: [PATCH 058/188] Inline the skip-detection run command Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/tarball-test.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/tarball-test.yml b/.github/workflows/tarball-test.yml index 52c4d31fc81f2b..c862ab76cc1886 100644 --- a/.github/workflows/tarball-test.yml +++ b/.github/workflows/tarball-test.yml @@ -38,8 +38,7 @@ jobs: skip: ${{ steps.skipping.outputs.skip }} steps: - id: skipping - run: - echo 'skip=true' >> $GITHUB_OUTPUT + run: echo 'skip=true' >> $GITHUB_OUTPUT if: >- ${{(false || contains(github.event.head_commit.message, '[DOC]') From 220f66a33eae1370aa964e7c07efdfdaf7bccddf Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 2 Jun 2026 15:50:06 +0900 Subject: [PATCH 059/188] Build full path for HOME stat snapshot Dir.each_child yields entry names, so stat/digest/children ran against the working directory instead of HOME. Join dir and name first. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/tarball-ubuntu.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tarball-ubuntu.yml b/.github/workflows/tarball-ubuntu.yml index 03f2f946b5a630..e227dc29aa59bd 100644 --- a/.github/workflows/tarball-ubuntu.yml +++ b/.github/workflows/tarball-ubuntu.yml @@ -77,7 +77,8 @@ jobs: [ Dir.home, ].each do |dir| - Dir.each_child(dir) do |pn| + Dir.each_child(dir) do |name| + pn = File.join(dir, name) st = File.stat(pn) if st.file? content = Digest::SHA1.file(pn).hexdigest From 6da78caea71b78e9ae4e108f43c7d5e02c950acf Mon Sep 17 00:00:00 2001 From: Matt Larraz Date: Mon, 16 Mar 2026 18:14:29 -0400 Subject: [PATCH 060/188] [ruby/rubygems] Replace Molinillo with PubGrub for dependency resolution Molinillo (a backtracking resolver) is replaced by PubGrub (a CDCL SAT-based solver) which provides better conflict explanations, smarter backjumping, and provable completeness. PubGrub is already used by Bundler; this vendors the same library re-namespaced under Gem::PubGrub. Key changes: - Vendor PubGrub from bundler/lib/bundler/vendor/pub_grub/, re-namespaced Bundler::PubGrub -> Gem::PubGrub - Rewrite Gem::Resolver to implement PubGrub's source interface (all_versions_for, versions_for, incompatibilities_for, etc.) - Add Gem::Resolver::PubGrubFailure for error reporting via PubGrub's superior conflict explanation format - Add Source::Local#find_all_gems to expose all local gem versions (PubGrub needs complete version information upfront) - Prefer installed specs in version ordering to avoid unnecessary upgrades - Delete Molinillo (21 files, ~2400 lines) and resolver/stats.rb https://github.com/ruby/rubygems/commit/df02d4110c Co-Authored-By: Claude Opus 4.6 --- lib/rubygems/commands/install_command.rb | 3 + lib/rubygems/exceptions.rb | 8 +- lib/rubygems/resolver.rb | 319 ++++--- lib/rubygems/resolver/installer_set.rb | 2 +- lib/rubygems/resolver/pub_grub_failure.rb | 18 + lib/rubygems/resolver/stats.rb | 46 - lib/rubygems/source/local.rb | 6 +- .../vendor/molinillo/lib/molinillo.rb | 11 - .../molinillo/delegates/resolution_state.rb | 57 -- .../delegates/specification_provider.rb | 88 -- .../lib/molinillo/dependency_graph.rb | 255 ------ .../lib/molinillo/dependency_graph/action.rb | 36 - .../dependency_graph/add_edge_no_circular.rb | 66 -- .../molinillo/dependency_graph/add_vertex.rb | 62 -- .../molinillo/dependency_graph/delete_edge.rb | 63 -- .../dependency_graph/detach_vertex_named.rb | 61 -- .../lib/molinillo/dependency_graph/log.rb | 126 --- .../molinillo/dependency_graph/set_payload.rb | 46 - .../lib/molinillo/dependency_graph/tag.rb | 36 - .../lib/molinillo/dependency_graph/vertex.rb | 164 ---- .../vendor/molinillo/lib/molinillo/errors.rb | 149 ---- .../molinillo/lib/molinillo/gem_metadata.rb | 6 - .../modules/specification_provider.rb | 112 --- .../molinillo/lib/molinillo/modules/ui.rb | 67 -- .../molinillo/lib/molinillo/resolution.rb | 839 ------------------ .../molinillo/lib/molinillo/resolver.rb | 46 - .../vendor/molinillo/lib/molinillo/state.rb | 58 -- lib/rubygems/vendor/pub_grub/lib/pub_grub.rb | 53 ++ .../pub_grub/lib/pub_grub/assignment.rb | 20 + .../lib/pub_grub/basic_package_source.rb | 169 ++++ .../pub_grub/lib/pub_grub/failure_writer.rb | 182 ++++ .../pub_grub/lib/pub_grub/incompatibility.rb | 150 ++++ .../vendor/pub_grub/lib/pub_grub/package.rb | 43 + .../pub_grub/lib/pub_grub/partial_solution.rb | 121 +++ .../vendor/pub_grub/lib/pub_grub/rubygems.rb | 45 + .../pub_grub/lib/pub_grub/solve_failure.rb | 19 + .../lib/pub_grub/static_package_source.rb | 61 ++ .../vendor/pub_grub/lib/pub_grub/strategy.rb | 42 + .../vendor/pub_grub/lib/pub_grub/term.rb | 105 +++ .../vendor/pub_grub/lib/pub_grub/version.rb | 3 + .../lib/pub_grub/version_constraint.rb | 129 +++ .../pub_grub/lib/pub_grub/version_range.rb | 423 +++++++++ .../pub_grub/lib/pub_grub/version_solver.rb | 236 +++++ .../pub_grub/lib/pub_grub/version_union.rb | 178 ++++ lib/rubygems/vendored_molinillo.rb | 3 - lib/rubygems/vendored_pub_grub.rb | 3 + .../test_gem_commands_install_command.rb | 12 +- .../rubygems/test_gem_dependency_installer.rb | 6 +- test/rubygems/test_gem_resolver.rb | 98 +- 49 files changed, 2228 insertions(+), 2623 deletions(-) create mode 100644 lib/rubygems/resolver/pub_grub_failure.rb delete mode 100644 lib/rubygems/resolver/stats.rb delete mode 100644 lib/rubygems/vendor/molinillo/lib/molinillo.rb delete mode 100644 lib/rubygems/vendor/molinillo/lib/molinillo/delegates/resolution_state.rb delete mode 100644 lib/rubygems/vendor/molinillo/lib/molinillo/delegates/specification_provider.rb delete mode 100644 lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph.rb delete mode 100644 lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/action.rb delete mode 100644 lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/add_edge_no_circular.rb delete mode 100644 lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/add_vertex.rb delete mode 100644 lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/delete_edge.rb delete mode 100644 lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/detach_vertex_named.rb delete mode 100644 lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/log.rb delete mode 100644 lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/set_payload.rb delete mode 100644 lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/tag.rb delete mode 100644 lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/vertex.rb delete mode 100644 lib/rubygems/vendor/molinillo/lib/molinillo/errors.rb delete mode 100644 lib/rubygems/vendor/molinillo/lib/molinillo/gem_metadata.rb delete mode 100644 lib/rubygems/vendor/molinillo/lib/molinillo/modules/specification_provider.rb delete mode 100644 lib/rubygems/vendor/molinillo/lib/molinillo/modules/ui.rb delete mode 100644 lib/rubygems/vendor/molinillo/lib/molinillo/resolution.rb delete mode 100644 lib/rubygems/vendor/molinillo/lib/molinillo/resolver.rb delete mode 100644 lib/rubygems/vendor/molinillo/lib/molinillo/state.rb create mode 100644 lib/rubygems/vendor/pub_grub/lib/pub_grub.rb create mode 100644 lib/rubygems/vendor/pub_grub/lib/pub_grub/assignment.rb create mode 100644 lib/rubygems/vendor/pub_grub/lib/pub_grub/basic_package_source.rb create mode 100644 lib/rubygems/vendor/pub_grub/lib/pub_grub/failure_writer.rb create mode 100644 lib/rubygems/vendor/pub_grub/lib/pub_grub/incompatibility.rb create mode 100644 lib/rubygems/vendor/pub_grub/lib/pub_grub/package.rb create mode 100644 lib/rubygems/vendor/pub_grub/lib/pub_grub/partial_solution.rb create mode 100644 lib/rubygems/vendor/pub_grub/lib/pub_grub/rubygems.rb create mode 100644 lib/rubygems/vendor/pub_grub/lib/pub_grub/solve_failure.rb create mode 100644 lib/rubygems/vendor/pub_grub/lib/pub_grub/static_package_source.rb create mode 100644 lib/rubygems/vendor/pub_grub/lib/pub_grub/strategy.rb create mode 100644 lib/rubygems/vendor/pub_grub/lib/pub_grub/term.rb create mode 100644 lib/rubygems/vendor/pub_grub/lib/pub_grub/version.rb create mode 100644 lib/rubygems/vendor/pub_grub/lib/pub_grub/version_constraint.rb create mode 100644 lib/rubygems/vendor/pub_grub/lib/pub_grub/version_range.rb create mode 100644 lib/rubygems/vendor/pub_grub/lib/pub_grub/version_solver.rb create mode 100644 lib/rubygems/vendor/pub_grub/lib/pub_grub/version_union.rb delete mode 100644 lib/rubygems/vendored_molinillo.rb create mode 100644 lib/rubygems/vendored_pub_grub.rb diff --git a/lib/rubygems/commands/install_command.rb b/lib/rubygems/commands/install_command.rb index 28a2fb7c71de40..6d3beec0b43261 100644 --- a/lib/rubygems/commands/install_command.rb +++ b/lib/rubygems/commands/install_command.rb @@ -224,6 +224,9 @@ def install_gems # :nodoc: rescue Gem::InstallError => e alert_error "Error installing #{gem_name}:\n\t#{e.message}" exit_code |= 1 + rescue Gem::DependencyResolutionError => e + alert_error "Error installing #{gem_name}:\n\t#{e.message}" + exit_code |= 2 rescue Gem::UnsatisfiableDependencyError => e show_lookup_failure e.name, e.version, e.errors, suppress_suggestions, "'#{gem_name}' (#{gem_version})" diff --git a/lib/rubygems/exceptions.rb b/lib/rubygems/exceptions.rb index 40485bbadff81a..1bba014245a8d2 100644 --- a/lib/rubygems/exceptions.rb +++ b/lib/rubygems/exceptions.rb @@ -42,9 +42,13 @@ class Gem::DependencyResolutionError < Gem::DependencyError def initialize(conflict) @conflict = conflict - a, b = conflicting_dependencies - super "conflicting dependencies #{a} and #{b}\n#{@conflict.explanation}" + if conflict.respond_to?(:solve_failure) + super conflict.explanation + else + a, b = conflicting_dependencies + super "conflicting dependencies #{a} and #{b}\n#{@conflict.explanation}" + end end def conflicting_dependencies diff --git a/lib/rubygems/resolver.rb b/lib/rubygems/resolver.rb index bc4fef893ead65..5578135a83e81f 100644 --- a/lib/rubygems/resolver.rb +++ b/lib/rubygems/resolver.rb @@ -10,7 +10,7 @@ # all the requirements. class Gem::Resolver - require_relative "vendored_molinillo" + require_relative "vendored_pub_grub" ## # If the DEBUG_RESOLVER environment variable is set then debugging mode is @@ -34,11 +34,6 @@ class Gem::Resolver attr_accessor :ignore_dependencies - ## - # List of dependencies that could not be found in the configured sources. - - attr_reader :stats - ## # Hash of gems to skip resolution. Keyed by gem name, with arrays of # gem specifications as values. @@ -104,7 +99,15 @@ def initialize(needed, set = nil) @ignore_dependencies = false @skip_gems = {} @soft_missing = false - @stats = Gem::Resolver::Stats.new + + @root_package = Gem::PubGrub::Package.root + @root_version = Gem::PubGrub::Package.root_version + + @packages = {} + + @all_specs = Hash.new {|h, name| h[name] = find_all_specs_for(name) } + @sorted_versions = Hash.new {|h, pkg| h[pkg] = @all_specs[pkg.to_s].map(&:version).uniq.sort } + @cached_dependencies = Hash.new {|h, pkg| h[pkg] = Hash.new {|v, ver| v[ver] = compute_dependencies(pkg, ver) } } end def explain(stage, *data) # :nodoc: @@ -126,68 +129,154 @@ def explain_list(stage) # :nodoc: end ## - # Creates an ActivationRequest for the given +dep+ and the last +possible+ - # specification. - # - # Returns the Specification and the ActivationRequest + # Proceed with resolution! Returns an array of ActivationRequest objects. - def activation_request(dep, possible) # :nodoc: - spec = possible.pop + def resolve + # Pre-check: raise UnsatisfiableDependencyError for root deps with no matches + @needed.each do |dep| + next if @soft_missing + dep_request = DependencyRequest.new(dep, nil) + all = @set.find_all(dep_request) + matching = select_local_platforms(all) + + if matching.empty? + exc = Gem::UnsatisfiableDependencyError.new(dep_request, all) + exc.errors = @set.errors + raise exc + end - explain :activate, [spec.full_name, possible.size] - explain :possible, possible + specs_matching_requirement = matching.select {|s| dep.requirement.satisfied_by?(s.version) } + next unless specs_matching_requirement.empty? + exc = Gem::UnsatisfiableDependencyError.new(dep_request, all) + exc.errors = @set.errors + raise exc + end - activation_request = - Gem::Resolver::ActivationRequest.new spec, dep, possible + # Build root deps from @needed + root_deps = {} + @needed.each do |dep| + range = Gem::PubGrub::RubyGems.requirement_to_range(dep.requirement) + constraint = Gem::PubGrub::VersionConstraint.new(package_for(dep.name), range: range) + root_deps[dep.name] = root_deps.key?(dep.name) ? root_deps[dep.name].intersect(constraint) : constraint + end - [spec, activation_request] + @sorted_versions[@root_package] = [@root_version] + @cached_dependencies[@root_package] = { @root_version => root_deps } + + solver = Gem::PubGrub::VersionSolver.new( + source: self, + root: @root_package, + logger: make_logger + ) + result = solver.solve + + # Convert to Array + result.filter_map do |package, version| + next if Gem::PubGrub::Package.root?(package) + spec = spec_for(package.to_s, version) + dep_request = DependencyRequest.new(Gem::Dependency.new(package.to_s), nil) + ActivationRequest.new(spec, dep_request) + end + rescue Gem::PubGrub::SolveFailure => e + failure = Gem::Resolver::PubGrubFailure.new(e) + raise Gem::DependencyResolutionError, failure end - def requests(s, act, reqs = []) # :nodoc: - return reqs if @ignore_dependencies + # PubGrub source interface methods - s.fetch_development_dependencies if @development + def all_versions_for(package) + return [@root_version] if package == @root_package - s.dependencies.reverse_each do |d| - next if d.type == :development && !@development - next if d.type == :development && @development_shallow && - act.development? - next if d.type == :development && @development_shallow && - act.parent + all_versions = @sorted_versions[package] - reqs << Gem::Resolver::DependencyRequest.new(d, act) - @stats.requirement! + # Exclude prerelease versions unless the set has prerelease enabled. + # Prereleases are still available via versions_for when a range + # specifically includes them (e.g., "= 2.a"), with low priority + # in the Strategy. + if @set.respond_to?(:prerelease) && @set.prerelease + versions = all_versions + else + stable = all_versions.reject(&:prerelease?) + versions = stable.empty? ? all_versions : stable end - @set.prefetch reqs + versions = versions.reverse # highest first + name = package.to_s - @stats.record_requirements reqs - - reqs + if (skip_dep_gems = skip_gems[name]) && !skip_dep_gems.empty? + skip_versions = skip_dep_gems.map(&:version) + preferred, rest = versions.partition {|v| skip_versions.include?(v) } + preferred + rest + else + # Prefer already-installed versions to avoid unnecessary upgrades + installed_versions = @all_specs[name]. + select {|s| s.is_a?(Gem::Resolver::InstalledSpecification) }. + map(&:version) + if installed_versions.any? + preferred, rest = versions.partition {|v| installed_versions.include?(v) } + preferred + rest + else + versions + end + end end - include Gem::Molinillo::UI - - def output - @output ||= debug? ? $stdout : File.open(IO::NULL, "w") + def versions_for(package, range = Gem::PubGrub::VersionRange.any) + range.select_versions(@sorted_versions[package]) end - def debug? - DEBUG_RESOLVER + def no_versions_incompatibility_for(_package, unsatisfied_term) + cause = Gem::PubGrub::Incompatibility::NoVersions.new(unsatisfied_term) + Gem::PubGrub::Incompatibility.new([unsatisfied_term], cause: cause) end - include Gem::Molinillo::SpecificationProvider - - ## - # Proceed with resolution! Returns an array of ActivationRequest objects. + def incompatibilities_for(package, version) + package_deps = @cached_dependencies[package] + sorted_versions = @sorted_versions[package] + package_deps[version].filter_map do |dep_package_name, dep_constraint| + dep_package = dep_constraint.package + low = high = sorted_versions.index(version) + + # find version low such that all >= low share the same dep + while low > 0 && + package_deps[sorted_versions[low - 1]][dep_package_name] == dep_constraint + low -= 1 + end + low = + if low == 0 + nil + else + sorted_versions[low] + end + + # find version high such that all < high share the same dep + while high < sorted_versions.length && + package_deps[sorted_versions[high]][dep_package_name] == dep_constraint + high += 1 + end + high = + if high == sorted_versions.length + nil + else + sorted_versions[high] + end + + range = Gem::PubGrub::VersionRange.new(min: low, max: high, include_min: !low.nil?) + self_constraint = Gem::PubGrub::VersionConstraint.new(package, range: range) + + if dep_constraint.range.empty? + cause = Gem::PubGrub::Incompatibility::InvalidDependency.new(dep_package, dep_constraint) + next Gem::PubGrub::Incompatibility.new( + [Gem::PubGrub::Term.new(self_constraint, true)], + cause: cause + ) + end - def resolve - Gem::Molinillo::Resolver.new(self, self).resolve(@needed.map {|d| DependencyRequest.new d, nil }).tsort.filter_map(&:payload) - rescue Gem::Molinillo::VersionConflict => e - conflict = e.conflicts.values.first - raise Gem::DependencyResolutionError, Conflict.new(conflict.requirement_trees.first.first, conflict.existing, conflict.requirement) - ensure - @output.close if defined?(@output) && !debug? + Gem::PubGrub::Incompatibility.new( + [Gem::PubGrub::Term.new(self_constraint, true), Gem::PubGrub::Term.new(dep_constraint, false)], + cause: :dependency + ) + end end ## @@ -219,103 +308,89 @@ def select_local_platforms(specs) # :nodoc: end end - def search_for(dependency) - possibles, all = find_possible(dependency) - if !@soft_missing && possibles.empty? - exc = Gem::UnsatisfiableDependencyError.new dependency, all - exc.errors = @set.errors - raise exc - end + private - groups = Hash.new {|hash, key| hash[key] = [] } + def package_for(name) + @packages[name] ||= Gem::PubGrub::Package.new(name) + end - # create groups & sources in the same loop - sources = possibles.map do |spec| - source = spec.source - groups[source] << spec - source - end.uniq.reverse + def find_all_specs_for(name) + dep = Gem::Dependency.new(name, ">= 0.a") + dep_request = DependencyRequest.new(dep, nil) + all = @set.find_all(dep_request) - activation_requests = [] + specs = select_local_platforms(all) - sources.each do |source| - groups[source]. - sort_by {|spec| [spec.version, -Gem::Platform.platform_specificity_match(spec.platform, Gem::Platform.local)] }. - map {|spec| ActivationRequest.new spec, dependency }. - each {|activation_request| activation_requests << activation_request } + unless @soft_missing + specs = specs.select do |s| + actual = s.respond_to?(:spec) ? s.spec : s + actual.required_ruby_version.satisfied_by?(Gem.ruby_version) && + actual.required_rubygems_version.satisfied_by?(Gem.rubygems_version) + rescue StandardError + true + end end - activation_requests + specs end - def dependencies_for(specification) - return [] if @ignore_dependencies - spec = specification.spec - requests(spec, specification) - end + def spec_for(name, version) + candidates = @all_specs[name].select {|s| s.version == version } + candidates = @all_specs[name].select {|s| s.version.to_s == version.to_s } if candidates.empty? - def requirement_satisfied_by?(requirement, activated, spec) - matches_spec = requirement.matches_spec? spec - return matches_spec if @soft_missing + if candidates.length > 1 + # Prefer already-installed specs to avoid unnecessary downloads + installed = candidates.select {|s| s.is_a?(Gem::Resolver::InstalledSpecification) } + return installed.first if installed.length == 1 + candidates = installed if installed.any? - matches_spec && - spec.spec.required_ruby_version.satisfied_by?(Gem.ruby_version) && - spec.spec.required_rubygems_version.satisfied_by?(Gem.rubygems_version) + # Among remaining candidates, prefer the most specific platform + candidates.min_by {|s| Gem::Platform.platform_specificity_match(s.platform, Gem::Platform.local) } + else + candidates.first + end end - def name_for(dependency) - dependency.name - end + def compute_dependencies(package, version) + return {} if package == @root_package - def allow_missing?(dependency) - @soft_missing - end + spec = spec_for(package.to_s, version) + return {} unless spec + return {} if @ignore_dependencies - def sort_dependencies(dependencies, activated, conflicts) - dependencies.sort_by.with_index do |dependency, i| - name = name_for(dependency) - [ - activated.vertex_named(name).payload ? 0 : 1, - amount_constrained(dependency), - conflicts[name] ? 0 : 1, - activated.vertex_named(name).payload ? 0 : search_for(dependency).count, - i, # for stable sort - ] - end - end + deps = {} + root_names = @needed.map(&:name) - SINGLE_POSSIBILITY_CONSTRAINT_PENALTY = 1_000_000 - private_constant :SINGLE_POSSIBILITY_CONSTRAINT_PENALTY if defined?(private_constant) + actual_spec = spec.respond_to?(:spec) ? spec.spec : spec + actual_spec.dependencies.each do |d| + next if d.type == :development && !@development + next if d.type == :development && @development_shallow && !root_names.include?(package.to_s) - # returns an integer \in (-\infty, 0] - # a number closer to 0 means the dependency is less constraining - # - # dependencies w/ 0 or 1 possibilities (ignoring version requirements) - # are given very negative values, so they _always_ sort first, - # before dependencies that are unconstrained - def amount_constrained(dependency) - @amount_constrained ||= {} - @amount_constrained[dependency.name] ||= begin - name_dependency = Gem::Dependency.new(dependency.name) - dependency_request_for_name = Gem::Resolver::DependencyRequest.new(name_dependency, dependency.requester) - all = @set.find_all(dependency_request_for_name).size - - if all <= 1 - all - SINGLE_POSSIBILITY_CONSTRAINT_PENALTY - else - search = search_for(dependency).size - search - all + dep_package = package_for(d.name) + + # Check if the dependency has any available versions + dep_specs = @all_specs[d.name] + if dep_specs.empty? && @soft_missing + next end + + range = Gem::PubGrub::RubyGems.requirement_to_range(d.requirement) + deps[d.name] = Gem::PubGrub::VersionConstraint.new(dep_package, range: range) end + + deps + end + + def make_logger + DEBUG_RESOLVER ? Gem::PubGrub::StderrLogger.new : Gem::PubGrub::NullLogger.new end - private :amount_constrained end require_relative "resolver/activation_request" require_relative "resolver/conflict" require_relative "resolver/dependency_request" require_relative "resolver/requirement_list" -require_relative "resolver/stats" +require_relative "resolver/pub_grub_failure" require_relative "resolver/set" require_relative "resolver/api_set" diff --git a/lib/rubygems/resolver/installer_set.rb b/lib/rubygems/resolver/installer_set.rb index d9fe36c589a54a..42ce0890e2b634 100644 --- a/lib/rubygems/resolver/installer_set.rb +++ b/lib/rubygems/resolver/installer_set.rb @@ -160,7 +160,7 @@ def find_all(req) res.concat matching_local begin - if local_spec = @local_source.find_gem(name, dep.requirement) + @local_source.find_all_gems(name, dep.requirement).each do |local_spec| res << Gem::Resolver::IndexSpecification.new( self, local_spec.name, local_spec.version, @local_source, local_spec.platform diff --git a/lib/rubygems/resolver/pub_grub_failure.rb b/lib/rubygems/resolver/pub_grub_failure.rb new file mode 100644 index 00000000000000..3ace7ef51590a6 --- /dev/null +++ b/lib/rubygems/resolver/pub_grub_failure.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class Gem::Resolver::PubGrubFailure + attr_reader :solve_failure + + def initialize(solve_failure) + @solve_failure = solve_failure + end + + def explanation + @solve_failure.explanation + end + + def conflicting_dependencies + terms = @solve_failure.incompatibility.terms + terms.map {|t| t.package.to_s } + end +end diff --git a/lib/rubygems/resolver/stats.rb b/lib/rubygems/resolver/stats.rb deleted file mode 100644 index 9920976b2a0950..00000000000000 --- a/lib/rubygems/resolver/stats.rb +++ /dev/null @@ -1,46 +0,0 @@ -# frozen_string_literal: true - -class Gem::Resolver::Stats - def initialize - @max_depth = 0 - @max_requirements = 0 - @requirements = 0 - @backtracking = 0 - @iterations = 0 - end - - def record_depth(stack) - if stack.size > @max_depth - @max_depth = stack.size - end - end - - def record_requirements(reqs) - if reqs.size > @max_requirements - @max_requirements = reqs.size - end - end - - def requirement! - @requirements += 1 - end - - def backtracking! - @backtracking += 1 - end - - def iteration! - @iterations += 1 - end - - PATTERN = "%20s: %d\n" - - def display - $stdout.puts "=== Resolver Statistics ===" - $stdout.printf PATTERN, "Max Depth", @max_depth - $stdout.printf PATTERN, "Total Requirements", @requirements - $stdout.printf PATTERN, "Max Requirements", @max_requirements - $stdout.printf PATTERN, "Backtracking #", @backtracking - $stdout.printf PATTERN, "Iteration #", @iterations - end -end diff --git a/lib/rubygems/source/local.rb b/lib/rubygems/source/local.rb index ba6eea1f9aa5c7..4bef31a2655fa6 100644 --- a/lib/rubygems/source/local.rb +++ b/lib/rubygems/source/local.rb @@ -76,6 +76,10 @@ def load_specs(type) # :nodoc: end def find_gem(gem_name, version = Gem::Requirement.default, prerelease = false) # :nodoc: + find_all_gems(gem_name, version, prerelease).max_by(&:version) + end + + def find_all_gems(gem_name, version = Gem::Requirement.default, prerelease = false) # :nodoc: load_specs :complete found = [] @@ -93,7 +97,7 @@ def find_gem(gem_name, version = Gem::Requirement.default, prerelease = false) # end end - found.max_by(&:version) + found end def fetch_spec(name) # :nodoc: diff --git a/lib/rubygems/vendor/molinillo/lib/molinillo.rb b/lib/rubygems/vendor/molinillo/lib/molinillo.rb deleted file mode 100644 index dd5600c9e38c89..00000000000000 --- a/lib/rubygems/vendor/molinillo/lib/molinillo.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true - -require_relative 'molinillo/gem_metadata' -require_relative 'molinillo/errors' -require_relative 'molinillo/resolver' -require_relative 'molinillo/modules/ui' -require_relative 'molinillo/modules/specification_provider' - -# Gem::Molinillo is a generic dependency resolution algorithm. -module Gem::Molinillo -end diff --git a/lib/rubygems/vendor/molinillo/lib/molinillo/delegates/resolution_state.rb b/lib/rubygems/vendor/molinillo/lib/molinillo/delegates/resolution_state.rb deleted file mode 100644 index 34842d46d5f9e4..00000000000000 --- a/lib/rubygems/vendor/molinillo/lib/molinillo/delegates/resolution_state.rb +++ /dev/null @@ -1,57 +0,0 @@ -# frozen_string_literal: true - -module Gem::Molinillo - # @!visibility private - module Delegates - # Delegates all {Gem::Molinillo::ResolutionState} methods to a `#state` property. - module ResolutionState - # (see Gem::Molinillo::ResolutionState#name) - def name - current_state = state || Gem::Molinillo::ResolutionState.empty - current_state.name - end - - # (see Gem::Molinillo::ResolutionState#requirements) - def requirements - current_state = state || Gem::Molinillo::ResolutionState.empty - current_state.requirements - end - - # (see Gem::Molinillo::ResolutionState#activated) - def activated - current_state = state || Gem::Molinillo::ResolutionState.empty - current_state.activated - end - - # (see Gem::Molinillo::ResolutionState#requirement) - def requirement - current_state = state || Gem::Molinillo::ResolutionState.empty - current_state.requirement - end - - # (see Gem::Molinillo::ResolutionState#possibilities) - def possibilities - current_state = state || Gem::Molinillo::ResolutionState.empty - current_state.possibilities - end - - # (see Gem::Molinillo::ResolutionState#depth) - def depth - current_state = state || Gem::Molinillo::ResolutionState.empty - current_state.depth - end - - # (see Gem::Molinillo::ResolutionState#conflicts) - def conflicts - current_state = state || Gem::Molinillo::ResolutionState.empty - current_state.conflicts - end - - # (see Gem::Molinillo::ResolutionState#unused_unwind_options) - def unused_unwind_options - current_state = state || Gem::Molinillo::ResolutionState.empty - current_state.unused_unwind_options - end - end - end -end diff --git a/lib/rubygems/vendor/molinillo/lib/molinillo/delegates/specification_provider.rb b/lib/rubygems/vendor/molinillo/lib/molinillo/delegates/specification_provider.rb deleted file mode 100644 index 8417721537219d..00000000000000 --- a/lib/rubygems/vendor/molinillo/lib/molinillo/delegates/specification_provider.rb +++ /dev/null @@ -1,88 +0,0 @@ -# frozen_string_literal: true - -module Gem::Molinillo - module Delegates - # Delegates all {Gem::Molinillo::SpecificationProvider} methods to a - # `#specification_provider` property. - module SpecificationProvider - # (see Gem::Molinillo::SpecificationProvider#search_for) - def search_for(dependency) - with_no_such_dependency_error_handling do - specification_provider.search_for(dependency) - end - end - - # (see Gem::Molinillo::SpecificationProvider#dependencies_for) - def dependencies_for(specification) - with_no_such_dependency_error_handling do - specification_provider.dependencies_for(specification) - end - end - - # (see Gem::Molinillo::SpecificationProvider#requirement_satisfied_by?) - def requirement_satisfied_by?(requirement, activated, spec) - with_no_such_dependency_error_handling do - specification_provider.requirement_satisfied_by?(requirement, activated, spec) - end - end - - # (see Gem::Molinillo::SpecificationProvider#dependencies_equal?) - def dependencies_equal?(dependencies, other_dependencies) - with_no_such_dependency_error_handling do - specification_provider.dependencies_equal?(dependencies, other_dependencies) - end - end - - # (see Gem::Molinillo::SpecificationProvider#name_for) - def name_for(dependency) - with_no_such_dependency_error_handling do - specification_provider.name_for(dependency) - end - end - - # (see Gem::Molinillo::SpecificationProvider#name_for_explicit_dependency_source) - def name_for_explicit_dependency_source - with_no_such_dependency_error_handling do - specification_provider.name_for_explicit_dependency_source - end - end - - # (see Gem::Molinillo::SpecificationProvider#name_for_locking_dependency_source) - def name_for_locking_dependency_source - with_no_such_dependency_error_handling do - specification_provider.name_for_locking_dependency_source - end - end - - # (see Gem::Molinillo::SpecificationProvider#sort_dependencies) - def sort_dependencies(dependencies, activated, conflicts) - with_no_such_dependency_error_handling do - specification_provider.sort_dependencies(dependencies, activated, conflicts) - end - end - - # (see Gem::Molinillo::SpecificationProvider#allow_missing?) - def allow_missing?(dependency) - with_no_such_dependency_error_handling do - specification_provider.allow_missing?(dependency) - end - end - - private - - # Ensures any raised {NoSuchDependencyError} has its - # {NoSuchDependencyError#required_by} set. - # @yield - def with_no_such_dependency_error_handling - yield - rescue NoSuchDependencyError => error - if state - vertex = activated.vertex_named(name_for(error.dependency)) - error.required_by += vertex.incoming_edges.map { |e| e.origin.name } - error.required_by << name_for_explicit_dependency_source unless vertex.explicit_requirements.empty? - end - raise - end - end - end -end diff --git a/lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph.rb b/lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph.rb deleted file mode 100644 index 2dbbc589dc7d5e..00000000000000 --- a/lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph.rb +++ /dev/null @@ -1,255 +0,0 @@ -# frozen_string_literal: true - -require_relative '../../../../vendored_tsort' - -require_relative 'dependency_graph/log' -require_relative 'dependency_graph/vertex' - -module Gem::Molinillo - # A directed acyclic graph that is tuned to hold named dependencies - class DependencyGraph - include Enumerable - - # Enumerates through the vertices of the graph. - # @return [Array] The graph's vertices. - def each - return vertices.values.each unless block_given? - vertices.values.each { |v| yield v } - end - - include Gem::TSort - - # @!visibility private - alias tsort_each_node each - - # @!visibility private - def tsort_each_child(vertex, &block) - vertex.successors.each(&block) - end - - # Topologically sorts the given vertices. - # @param [Enumerable] vertices the vertices to be sorted, which must - # all belong to the same graph. - # @return [Array] The sorted vertices. - def self.tsort(vertices) - Gem::TSort.tsort( - lambda { |b| vertices.each(&b) }, - lambda { |v, &b| (v.successors & vertices).each(&b) } - ) - end - - # A directed edge of a {DependencyGraph} - # @attr [Vertex] origin The origin of the directed edge - # @attr [Vertex] destination The destination of the directed edge - # @attr [Object] requirement The requirement the directed edge represents - Edge = Struct.new(:origin, :destination, :requirement) - - # @return [{String => Vertex}] the vertices of the dependency graph, keyed - # by {Vertex#name} - attr_reader :vertices - - # @return [Log] the op log for this graph - attr_reader :log - - # Initializes an empty dependency graph - def initialize - @vertices = {} - @log = Log.new - end - - # Tags the current state of the dependency as the given tag - # @param [Object] tag an opaque tag for the current state of the graph - # @return [Void] - def tag(tag) - log.tag(self, tag) - end - - # Rewinds the graph to the state tagged as `tag` - # @param [Object] tag the tag to rewind to - # @return [Void] - def rewind_to(tag) - log.rewind_to(self, tag) - end - - # Initializes a copy of a {DependencyGraph}, ensuring that all {#vertices} - # are properly copied. - # @param [DependencyGraph] other the graph to copy. - def initialize_copy(other) - super - @vertices = {} - @log = other.log.dup - traverse = lambda do |new_v, old_v| - return if new_v.outgoing_edges.size == old_v.outgoing_edges.size - old_v.outgoing_edges.each do |edge| - destination = add_vertex(edge.destination.name, edge.destination.payload) - add_edge_no_circular(new_v, destination, edge.requirement) - traverse.call(destination, edge.destination) - end - end - other.vertices.each do |name, vertex| - new_vertex = add_vertex(name, vertex.payload, vertex.root?) - new_vertex.explicit_requirements.replace(vertex.explicit_requirements) - traverse.call(new_vertex, vertex) - end - end - - # @return [String] a string suitable for debugging - def inspect - "#{self.class}:#{vertices.values.inspect}" - end - - # @param [Hash] options options for dot output. - # @return [String] Returns a dot format representation of the graph - def to_dot(options = {}) - edge_label = options.delete(:edge_label) - raise ArgumentError, "Unknown options: #{options.keys}" unless options.empty? - - dot_vertices = [] - dot_edges = [] - vertices.each do |n, v| - dot_vertices << " #{n} [label=\"{#{n}|#{v.payload}}\"]" - v.outgoing_edges.each do |e| - label = edge_label ? edge_label.call(e) : e.requirement - dot_edges << " #{e.origin.name} -> #{e.destination.name} [label=#{label.to_s.dump}]" - end - end - - dot_vertices.uniq! - dot_vertices.sort! - dot_edges.uniq! - dot_edges.sort! - - dot = dot_vertices.unshift('digraph G {').push('') + dot_edges.push('}') - dot.join("\n") - end - - # @param [DependencyGraph] other - # @return [Boolean] whether the two dependency graphs are equal, determined - # by a recursive traversal of each {#root_vertices} and its - # {Vertex#successors} - def ==(other) - return false unless other - return true if equal?(other) - vertices.each do |name, vertex| - other_vertex = other.vertex_named(name) - return false unless other_vertex - return false unless vertex.payload == other_vertex.payload - return false unless other_vertex.successors.to_set == vertex.successors.to_set - end - end - - # @param [String] name - # @param [Object] payload - # @param [Array] parent_names - # @param [Object] requirement the requirement that is requiring the child - # @return [void] - def add_child_vertex(name, payload, parent_names, requirement) - root = !parent_names.delete(nil) { true } - vertex = add_vertex(name, payload, root) - vertex.explicit_requirements << requirement if root - parent_names.each do |parent_name| - parent_vertex = vertex_named(parent_name) - add_edge(parent_vertex, vertex, requirement) - end - vertex - end - - # Adds a vertex with the given name, or updates the existing one. - # @param [String] name - # @param [Object] payload - # @return [Vertex] the vertex that was added to `self` - def add_vertex(name, payload, root = false) - log.add_vertex(self, name, payload, root) - end - - # Detaches the {#vertex_named} `name` {Vertex} from the graph, recursively - # removing any non-root vertices that were orphaned in the process - # @param [String] name - # @return [Array] the vertices which have been detached - def detach_vertex_named(name) - log.detach_vertex_named(self, name) - end - - # @param [String] name - # @return [Vertex,nil] the vertex with the given name - def vertex_named(name) - vertices[name] - end - - # @param [String] name - # @return [Vertex,nil] the root vertex with the given name - def root_vertex_named(name) - vertex = vertex_named(name) - vertex if vertex && vertex.root? - end - - # Adds a new {Edge} to the dependency graph - # @param [Vertex] origin - # @param [Vertex] destination - # @param [Object] requirement the requirement that this edge represents - # @return [Edge] the added edge - def add_edge(origin, destination, requirement) - if destination.path_to?(origin) - raise CircularDependencyError.new(path(destination, origin)) - end - add_edge_no_circular(origin, destination, requirement) - end - - # Deletes an {Edge} from the dependency graph - # @param [Edge] edge - # @return [Void] - def delete_edge(edge) - log.delete_edge(self, edge.origin.name, edge.destination.name, edge.requirement) - end - - # Sets the payload of the vertex with the given name - # @param [String] name the name of the vertex - # @param [Object] payload the payload - # @return [Void] - def set_payload(name, payload) - log.set_payload(self, name, payload) - end - - private - - # Adds a new {Edge} to the dependency graph without checking for - # circularity. - # @param (see #add_edge) - # @return (see #add_edge) - def add_edge_no_circular(origin, destination, requirement) - log.add_edge_no_circular(self, origin.name, destination.name, requirement) - end - - # Returns the path between two vertices - # @raise [ArgumentError] if there is no path between the vertices - # @param [Vertex] from - # @param [Vertex] to - # @return [Array] the shortest path from `from` to `to` - def path(from, to) - distances = Hash.new(vertices.size + 1) - distances[from.name] = 0 - predecessors = {} - each do |vertex| - vertex.successors.each do |successor| - if distances[successor.name] > distances[vertex.name] + 1 - distances[successor.name] = distances[vertex.name] + 1 - predecessors[successor] = vertex - end - end - end - - path = [to] - while before = predecessors[to] - path << before - to = before - break if to == from - end - - unless path.last.equal?(from) - raise ArgumentError, "There is no path from #{from.name} to #{to.name}" - end - - path.reverse - end - end -end diff --git a/lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/action.rb b/lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/action.rb deleted file mode 100644 index 8707ec451db997..00000000000000 --- a/lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/action.rb +++ /dev/null @@ -1,36 +0,0 @@ -# frozen_string_literal: true - -module Gem::Molinillo - class DependencyGraph - # An action that modifies a {DependencyGraph} that is reversible. - # @abstract - class Action - # rubocop:disable Lint/UnusedMethodArgument - - # @return [Symbol] The name of the action. - def self.action_name - raise 'Abstract' - end - - # Performs the action on the given graph. - # @param [DependencyGraph] graph the graph to perform the action on. - # @return [Void] - def up(graph) - raise 'Abstract' - end - - # Reverses the action on the given graph. - # @param [DependencyGraph] graph the graph to reverse the action on. - # @return [Void] - def down(graph) - raise 'Abstract' - end - - # @return [Action,Nil] The previous action - attr_accessor :previous - - # @return [Action,Nil] The next action - attr_accessor :next - end - end -end diff --git a/lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/add_edge_no_circular.rb b/lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/add_edge_no_circular.rb deleted file mode 100644 index aa9815c5ae8d8d..00000000000000 --- a/lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/add_edge_no_circular.rb +++ /dev/null @@ -1,66 +0,0 @@ -# frozen_string_literal: true - -require_relative 'action' -module Gem::Molinillo - class DependencyGraph - # @!visibility private - # (see DependencyGraph#add_edge_no_circular) - class AddEdgeNoCircular < Action - # @!group Action - - # (see Action.action_name) - def self.action_name - :add_vertex - end - - # (see Action#up) - def up(graph) - edge = make_edge(graph) - edge.origin.outgoing_edges << edge - edge.destination.incoming_edges << edge - edge - end - - # (see Action#down) - def down(graph) - edge = make_edge(graph) - delete_first(edge.origin.outgoing_edges, edge) - delete_first(edge.destination.incoming_edges, edge) - end - - # @!group AddEdgeNoCircular - - # @return [String] the name of the origin of the edge - attr_reader :origin - - # @return [String] the name of the destination of the edge - attr_reader :destination - - # @return [Object] the requirement that the edge represents - attr_reader :requirement - - # @param [DependencyGraph] graph the graph to find vertices from - # @return [Edge] The edge this action adds - def make_edge(graph) - Edge.new(graph.vertex_named(origin), graph.vertex_named(destination), requirement) - end - - # Initialize an action to add an edge to a dependency graph - # @param [String] origin the name of the origin of the edge - # @param [String] destination the name of the destination of the edge - # @param [Object] requirement the requirement that the edge represents - def initialize(origin, destination, requirement) - @origin = origin - @destination = destination - @requirement = requirement - end - - private - - def delete_first(array, item) - return unless index = array.index(item) - array.delete_at(index) - end - end - end -end diff --git a/lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/add_vertex.rb b/lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/add_vertex.rb deleted file mode 100644 index 9c7066a669a799..00000000000000 --- a/lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/add_vertex.rb +++ /dev/null @@ -1,62 +0,0 @@ -# frozen_string_literal: true - -require_relative 'action' -module Gem::Molinillo - class DependencyGraph - # @!visibility private - # (see DependencyGraph#add_vertex) - class AddVertex < Action # :nodoc: - # @!group Action - - # (see Action.action_name) - def self.action_name - :add_vertex - end - - # (see Action#up) - def up(graph) - if existing = graph.vertices[name] - @existing_payload = existing.payload - @existing_root = existing.root - end - vertex = existing || Vertex.new(name, payload) - graph.vertices[vertex.name] = vertex - vertex.payload ||= payload - vertex.root ||= root - vertex - end - - # (see Action#down) - def down(graph) - if defined?(@existing_payload) - vertex = graph.vertices[name] - vertex.payload = @existing_payload - vertex.root = @existing_root - else - graph.vertices.delete(name) - end - end - - # @!group AddVertex - - # @return [String] the name of the vertex - attr_reader :name - - # @return [Object] the payload for the vertex - attr_reader :payload - - # @return [Boolean] whether the vertex is root or not - attr_reader :root - - # Initialize an action to add a vertex to a dependency graph - # @param [String] name the name of the vertex - # @param [Object] payload the payload for the vertex - # @param [Boolean] root whether the vertex is root or not - def initialize(name, payload, root) - @name = name - @payload = payload - @root = root - end - end - end -end diff --git a/lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/delete_edge.rb b/lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/delete_edge.rb deleted file mode 100644 index 1e62c0a0b6442b..00000000000000 --- a/lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/delete_edge.rb +++ /dev/null @@ -1,63 +0,0 @@ -# frozen_string_literal: true - -require_relative 'action' -module Gem::Molinillo - class DependencyGraph - # @!visibility private - # (see DependencyGraph#delete_edge) - class DeleteEdge < Action - # @!group Action - - # (see Action.action_name) - def self.action_name - :delete_edge - end - - # (see Action#up) - def up(graph) - edge = make_edge(graph) - edge.origin.outgoing_edges.delete(edge) - edge.destination.incoming_edges.delete(edge) - end - - # (see Action#down) - def down(graph) - edge = make_edge(graph) - edge.origin.outgoing_edges << edge - edge.destination.incoming_edges << edge - edge - end - - # @!group DeleteEdge - - # @return [String] the name of the origin of the edge - attr_reader :origin_name - - # @return [String] the name of the destination of the edge - attr_reader :destination_name - - # @return [Object] the requirement that the edge represents - attr_reader :requirement - - # @param [DependencyGraph] graph the graph to find vertices from - # @return [Edge] The edge this action adds - def make_edge(graph) - Edge.new( - graph.vertex_named(origin_name), - graph.vertex_named(destination_name), - requirement - ) - end - - # Initialize an action to add an edge to a dependency graph - # @param [String] origin_name the name of the origin of the edge - # @param [String] destination_name the name of the destination of the edge - # @param [Object] requirement the requirement that the edge represents - def initialize(origin_name, destination_name, requirement) - @origin_name = origin_name - @destination_name = destination_name - @requirement = requirement - end - end - end -end diff --git a/lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/detach_vertex_named.rb b/lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/detach_vertex_named.rb deleted file mode 100644 index 6132f969b99308..00000000000000 --- a/lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/detach_vertex_named.rb +++ /dev/null @@ -1,61 +0,0 @@ -# frozen_string_literal: true - -require_relative 'action' -module Gem::Molinillo - class DependencyGraph - # @!visibility private - # @see DependencyGraph#detach_vertex_named - class DetachVertexNamed < Action - # @!group Action - - # (see Action#name) - def self.action_name - :add_vertex - end - - # (see Action#up) - def up(graph) - return [] unless @vertex = graph.vertices.delete(name) - - removed_vertices = [@vertex] - @vertex.outgoing_edges.each do |e| - v = e.destination - v.incoming_edges.delete(e) - if !v.root? && v.incoming_edges.empty? - removed_vertices.concat graph.detach_vertex_named(v.name) - end - end - - @vertex.incoming_edges.each do |e| - v = e.origin - v.outgoing_edges.delete(e) - end - - removed_vertices - end - - # (see Action#down) - def down(graph) - return unless @vertex - graph.vertices[@vertex.name] = @vertex - @vertex.outgoing_edges.each do |e| - e.destination.incoming_edges << e - end - @vertex.incoming_edges.each do |e| - e.origin.outgoing_edges << e - end - end - - # @!group DetachVertexNamed - - # @return [String] the name of the vertex to detach - attr_reader :name - - # Initialize an action to detach a vertex from a dependency graph - # @param [String] name the name of the vertex to detach - def initialize(name) - @name = name - end - end - end -end diff --git a/lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/log.rb b/lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/log.rb deleted file mode 100644 index 6954c4b1f8cace..00000000000000 --- a/lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/log.rb +++ /dev/null @@ -1,126 +0,0 @@ -# frozen_string_literal: true - -require_relative 'add_edge_no_circular' -require_relative 'add_vertex' -require_relative 'delete_edge' -require_relative 'detach_vertex_named' -require_relative 'set_payload' -require_relative 'tag' - -module Gem::Molinillo - class DependencyGraph - # A log for dependency graph actions - class Log - # Initializes an empty log - def initialize - @current_action = @first_action = nil - end - - # @!macro [new] action - # {include:DependencyGraph#$0} - # @param [Graph] graph the graph to perform the action on - # @param (see DependencyGraph#$0) - # @return (see DependencyGraph#$0) - - # @macro action - def tag(graph, tag) - push_action(graph, Tag.new(tag)) - end - - # @macro action - def add_vertex(graph, name, payload, root) - push_action(graph, AddVertex.new(name, payload, root)) - end - - # @macro action - def detach_vertex_named(graph, name) - push_action(graph, DetachVertexNamed.new(name)) - end - - # @macro action - def add_edge_no_circular(graph, origin, destination, requirement) - push_action(graph, AddEdgeNoCircular.new(origin, destination, requirement)) - end - - # {include:DependencyGraph#delete_edge} - # @param [Graph] graph the graph to perform the action on - # @param [String] origin_name - # @param [String] destination_name - # @param [Object] requirement - # @return (see DependencyGraph#delete_edge) - def delete_edge(graph, origin_name, destination_name, requirement) - push_action(graph, DeleteEdge.new(origin_name, destination_name, requirement)) - end - - # @macro action - def set_payload(graph, name, payload) - push_action(graph, SetPayload.new(name, payload)) - end - - # Pops the most recent action from the log and undoes the action - # @param [DependencyGraph] graph - # @return [Action] the action that was popped off the log - def pop!(graph) - return unless action = @current_action - unless @current_action = action.previous - @first_action = nil - end - action.down(graph) - action - end - - extend Enumerable - - # @!visibility private - # Enumerates each action in the log - # @yield [Action] - def each - return enum_for unless block_given? - action = @first_action - loop do - break unless action - yield action - action = action.next - end - self - end - - # @!visibility private - # Enumerates each action in the log in reverse order - # @yield [Action] - def reverse_each - return enum_for(:reverse_each) unless block_given? - action = @current_action - loop do - break unless action - yield action - action = action.previous - end - self - end - - # @macro action - def rewind_to(graph, tag) - loop do - action = pop!(graph) - raise "No tag #{tag.inspect} found" unless action - break if action.class.action_name == :tag && action.tag == tag - end - end - - private - - # Adds the given action to the log, running the action - # @param [DependencyGraph] graph - # @param [Action] action - # @return The value returned by `action.up` - def push_action(graph, action) - action.previous = @current_action - @current_action.next = action if @current_action - @current_action = action - @first_action ||= action - action.up(graph) - end - end - end -end diff --git a/lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/set_payload.rb b/lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/set_payload.rb deleted file mode 100644 index 9bcaaae0f97a96..00000000000000 --- a/lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/set_payload.rb +++ /dev/null @@ -1,46 +0,0 @@ -# frozen_string_literal: true - -require_relative 'action' -module Gem::Molinillo - class DependencyGraph - # @!visibility private - # @see DependencyGraph#set_payload - class SetPayload < Action # :nodoc: - # @!group Action - - # (see Action.action_name) - def self.action_name - :set_payload - end - - # (see Action#up) - def up(graph) - vertex = graph.vertex_named(name) - @old_payload = vertex.payload - vertex.payload = payload - end - - # (see Action#down) - def down(graph) - graph.vertex_named(name).payload = @old_payload - end - - # @!group SetPayload - - # @return [String] the name of the vertex - attr_reader :name - - # @return [Object] the payload for the vertex - attr_reader :payload - - # Initialize an action to add set the payload for a vertex in a dependency - # graph - # @param [String] name the name of the vertex - # @param [Object] payload the payload for the vertex - def initialize(name, payload) - @name = name - @payload = payload - end - end - end -end diff --git a/lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/tag.rb b/lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/tag.rb deleted file mode 100644 index 62f243a2aff63d..00000000000000 --- a/lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/tag.rb +++ /dev/null @@ -1,36 +0,0 @@ -# frozen_string_literal: true - -require_relative 'action' -module Gem::Molinillo - class DependencyGraph - # @!visibility private - # @see DependencyGraph#tag - class Tag < Action - # @!group Action - - # (see Action.action_name) - def self.action_name - :tag - end - - # (see Action#up) - def up(graph) - end - - # (see Action#down) - def down(graph) - end - - # @!group Tag - - # @return [Object] An opaque tag - attr_reader :tag - - # Initialize an action to tag a state of a dependency graph - # @param [Object] tag an opaque tag - def initialize(tag) - @tag = tag - end - end - end -end diff --git a/lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/vertex.rb b/lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/vertex.rb deleted file mode 100644 index 074de369bed89b..00000000000000 --- a/lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/vertex.rb +++ /dev/null @@ -1,164 +0,0 @@ -# frozen_string_literal: true - -module Gem::Molinillo - class DependencyGraph - # A vertex in a {DependencyGraph} that encapsulates a {#name} and a - # {#payload} - class Vertex - # @return [String] the name of the vertex - attr_accessor :name - - # @return [Object] the payload the vertex holds - attr_accessor :payload - - # @return [Array] the explicit requirements that required - # this vertex - attr_reader :explicit_requirements - - # @return [Boolean] whether the vertex is considered a root vertex - attr_accessor :root - alias root? root - - # Initializes a vertex with the given name and payload. - # @param [String] name see {#name} - # @param [Object] payload see {#payload} - def initialize(name, payload) - @name = name.frozen? ? name : name.dup.freeze - @payload = payload - @explicit_requirements = [] - @outgoing_edges = [] - @incoming_edges = [] - end - - # @return [Array] all of the requirements that required - # this vertex - def requirements - (incoming_edges.map(&:requirement) + explicit_requirements).uniq - end - - # @return [Array] the edges of {#graph} that have `self` as their - # {Edge#origin} - attr_accessor :outgoing_edges - - # @return [Array] the edges of {#graph} that have `self` as their - # {Edge#destination} - attr_accessor :incoming_edges - - # @return [Array] the vertices of {#graph} that have an edge with - # `self` as their {Edge#destination} - def predecessors - incoming_edges.map(&:origin) - end - - # @return [Set] the vertices of {#graph} where `self` is a - # {#descendent?} - def recursive_predecessors - _recursive_predecessors - end - - # @param [Set] vertices the set to add the predecessors to - # @return [Set] the vertices of {#graph} where `self` is a - # {#descendent?} - def _recursive_predecessors(vertices = new_vertex_set) - incoming_edges.each do |edge| - vertex = edge.origin - next unless vertices.add?(vertex) - vertex._recursive_predecessors(vertices) - end - - vertices - end - protected :_recursive_predecessors - - # @return [Array] the vertices of {#graph} that have an edge with - # `self` as their {Edge#origin} - def successors - outgoing_edges.map(&:destination) - end - - # @return [Set] the vertices of {#graph} where `self` is an - # {#ancestor?} - def recursive_successors - _recursive_successors - end - - # @param [Set] vertices the set to add the successors to - # @return [Set] the vertices of {#graph} where `self` is an - # {#ancestor?} - def _recursive_successors(vertices = new_vertex_set) - outgoing_edges.each do |edge| - vertex = edge.destination - next unless vertices.add?(vertex) - vertex._recursive_successors(vertices) - end - - vertices - end - protected :_recursive_successors - - # @return [String] a string suitable for debugging - def inspect - "#{self.class}:#{name}(#{payload.inspect})" - end - - # @return [Boolean] whether the two vertices are equal, determined - # by a recursive traversal of each {Vertex#successors} - def ==(other) - return true if equal?(other) - shallow_eql?(other) && - successors.to_set == other.successors.to_set - end - - # @param [Vertex] other the other vertex to compare to - # @return [Boolean] whether the two vertices are equal, determined - # solely by {#name} and {#payload} equality - def shallow_eql?(other) - return true if equal?(other) - other && - name == other.name && - payload == other.payload - end - - alias eql? == - - # @return [Fixnum] a hash for the vertex based upon its {#name} - def hash - name.hash - end - - # Is there a path from `self` to `other` following edges in the - # dependency graph? - # @return whether there is a path following edges within this {#graph} - def path_to?(other) - _path_to?(other) - end - - alias descendent? path_to? - - # @param [Vertex] other the vertex to check if there's a path to - # @param [Set] visited the vertices of {#graph} that have been visited - # @return [Boolean] whether there is a path to `other` from `self` - def _path_to?(other, visited = new_vertex_set) - return false unless visited.add?(self) - return true if equal?(other) - successors.any? { |v| v._path_to?(other, visited) } - end - protected :_path_to? - - # Is there a path from `other` to `self` following edges in the - # dependency graph? - # @return whether there is a path following edges within this {#graph} - def ancestor?(other) - other.path_to?(self) - end - - alias is_reachable_from? ancestor? - - def new_vertex_set - require 'set' - Set.new - end - private :new_vertex_set - end - end -end diff --git a/lib/rubygems/vendor/molinillo/lib/molinillo/errors.rb b/lib/rubygems/vendor/molinillo/lib/molinillo/errors.rb deleted file mode 100644 index 07ea5fdf3746c3..00000000000000 --- a/lib/rubygems/vendor/molinillo/lib/molinillo/errors.rb +++ /dev/null @@ -1,149 +0,0 @@ -# frozen_string_literal: true - -module Gem::Molinillo - # An error that occurred during the resolution process - class ResolverError < StandardError; end - - # An error caused by searching for a dependency that is completely unknown, - # i.e. has no versions available whatsoever. - class NoSuchDependencyError < ResolverError - # @return [Object] the dependency that could not be found - attr_accessor :dependency - - # @return [Array] the specifications that depended upon {#dependency} - attr_accessor :required_by - - # Initializes a new error with the given missing dependency. - # @param [Object] dependency @see {#dependency} - # @param [Array] required_by @see {#required_by} - def initialize(dependency, required_by = []) - @dependency = dependency - @required_by = required_by.uniq - super() - end - - # The error message for the missing dependency, including the specifications - # that had this dependency. - def message - sources = required_by.map { |r| "`#{r}`" }.join(' and ') - message = "Unable to find a specification for `#{dependency}`" - message += " depended upon by #{sources}" unless sources.empty? - message - end - end - - # An error caused by attempting to fulfil a dependency that was circular - # - # @note This exception will be thrown if and only if a {Vertex} is added to a - # {DependencyGraph} that has a {DependencyGraph::Vertex#path_to?} an - # existing {DependencyGraph::Vertex} - class CircularDependencyError < ResolverError - # [Set] the dependencies responsible for causing the error - attr_reader :dependencies - - # Initializes a new error with the given circular vertices. - # @param [Array] vertices the vertices in the dependency - # that caused the error - def initialize(vertices) - super "There is a circular dependency between #{vertices.map(&:name).join(' and ')}" - @dependencies = vertices.map { |vertex| vertex.payload.possibilities.last }.to_set - end - end - - # An error caused by conflicts in version - class VersionConflict < ResolverError - # @return [{String => Resolution::Conflict}] the conflicts that caused - # resolution to fail - attr_reader :conflicts - - # @return [SpecificationProvider] the specification provider used during - # resolution - attr_reader :specification_provider - - # Initializes a new error with the given version conflicts. - # @param [{String => Resolution::Conflict}] conflicts see {#conflicts} - # @param [SpecificationProvider] specification_provider see {#specification_provider} - def initialize(conflicts, specification_provider) - pairs = [] - conflicts.values.flat_map(&:requirements).each do |conflicting| - conflicting.each do |source, conflict_requirements| - conflict_requirements.each do |c| - pairs << [c, source] - end - end - end - - super "Unable to satisfy the following requirements:\n\n" \ - "#{pairs.map { |r, d| "- `#{r}` required by `#{d}`" }.join("\n")}" - - @conflicts = conflicts - @specification_provider = specification_provider - end - - require_relative 'delegates/specification_provider' - include Delegates::SpecificationProvider - - # @return [String] An error message that includes requirement trees, - # which is much more detailed & customizable than the default message - # @param [Hash] opts the options to create a message with. - # @option opts [String] :solver_name The user-facing name of the solver - # @option opts [String] :possibility_type The generic name of a possibility - # @option opts [Proc] :reduce_trees A proc that reduced the list of requirement trees - # @option opts [Proc] :printable_requirement A proc that pretty-prints requirements - # @option opts [Proc] :additional_message_for_conflict A proc that appends additional - # messages for each conflict - # @option opts [Proc] :version_for_spec A proc that returns the version number for a - # possibility - def message_with_trees(opts = {}) - solver_name = opts.delete(:solver_name) { self.class.name.split('::').first } - possibility_type = opts.delete(:possibility_type) { 'possibility named' } - reduce_trees = opts.delete(:reduce_trees) { proc { |trees| trees.uniq.sort_by(&:to_s) } } - printable_requirement = opts.delete(:printable_requirement) { proc { |req| req.to_s } } - additional_message_for_conflict = opts.delete(:additional_message_for_conflict) { proc {} } - version_for_spec = opts.delete(:version_for_spec) { proc(&:to_s) } - incompatible_version_message_for_conflict = opts.delete(:incompatible_version_message_for_conflict) do - proc do |name, _conflict| - %(#{solver_name} could not find compatible versions for #{possibility_type} "#{name}":) - end - end - - full_message_for_conflict = opts.delete(:full_message_for_conflict) do - proc do |name, conflict| - o = "\n".dup << incompatible_version_message_for_conflict.call(name, conflict) << "\n" - if conflict.locked_requirement - o << %( In snapshot (#{name_for_locking_dependency_source}):\n) - o << %( #{printable_requirement.call(conflict.locked_requirement)}\n) - o << %(\n) - end - o << %( In #{name_for_explicit_dependency_source}:\n) - trees = reduce_trees.call(conflict.requirement_trees) - - o << trees.map do |tree| - t = ''.dup - depth = 2 - tree.each do |req| - t << ' ' * depth << printable_requirement.call(req) - unless tree.last == req - if spec = conflict.activated_by_name[name_for(req)] - t << %( was resolved to #{version_for_spec.call(spec)}, which) - end - t << %( depends on) - end - t << %(\n) - depth += 1 - end - t - end.join("\n") - - additional_message_for_conflict.call(o, name, conflict) - - o - end - end - - conflicts.sort.reduce(''.dup) do |o, (name, conflict)| - o << full_message_for_conflict.call(name, conflict) - end.strip - end - end -end diff --git a/lib/rubygems/vendor/molinillo/lib/molinillo/gem_metadata.rb b/lib/rubygems/vendor/molinillo/lib/molinillo/gem_metadata.rb deleted file mode 100644 index 8ed3a920a2f386..00000000000000 --- a/lib/rubygems/vendor/molinillo/lib/molinillo/gem_metadata.rb +++ /dev/null @@ -1,6 +0,0 @@ -# frozen_string_literal: true - -module Gem::Molinillo - # The version of Gem::Molinillo. - VERSION = '0.8.0'.freeze -end diff --git a/lib/rubygems/vendor/molinillo/lib/molinillo/modules/specification_provider.rb b/lib/rubygems/vendor/molinillo/lib/molinillo/modules/specification_provider.rb deleted file mode 100644 index 85860902fca563..00000000000000 --- a/lib/rubygems/vendor/molinillo/lib/molinillo/modules/specification_provider.rb +++ /dev/null @@ -1,112 +0,0 @@ -# frozen_string_literal: true - -module Gem::Molinillo - # Provides information about specifications and dependencies to the resolver, - # allowing the {Resolver} class to remain generic while still providing power - # and flexibility. - # - # This module contains the methods that users of Gem::Molinillo must to implement, - # using knowledge of their own model classes. - module SpecificationProvider - # Search for the specifications that match the given dependency. - # The specifications in the returned array will be considered in reverse - # order, so the latest version ought to be last. - # @note This method should be 'pure', i.e. the return value should depend - # only on the `dependency` parameter. - # - # @param [Object] dependency - # @return [Array] the specifications that satisfy the given - # `dependency`. - def search_for(dependency) - [] - end - - # Returns the dependencies of `specification`. - # @note This method should be 'pure', i.e. the return value should depend - # only on the `specification` parameter. - # - # @param [Object] specification - # @return [Array] the dependencies that are required by the given - # `specification`. - def dependencies_for(specification) - [] - end - - # Determines whether the given `requirement` is satisfied by the given - # `spec`, in the context of the current `activated` dependency graph. - # - # @param [Object] requirement - # @param [DependencyGraph] activated the current dependency graph in the - # resolution process. - # @param [Object] spec - # @return [Boolean] whether `requirement` is satisfied by `spec` in the - # context of the current `activated` dependency graph. - def requirement_satisfied_by?(requirement, activated, spec) - true - end - - # Determines whether two arrays of dependencies are equal, and thus can be - # grouped. - # - # @param [Array] dependencies - # @param [Array] other_dependencies - # @return [Boolean] whether `dependencies` and `other_dependencies` should - # be considered equal. - def dependencies_equal?(dependencies, other_dependencies) - dependencies == other_dependencies - end - - # Returns the name for the given `dependency`. - # @note This method should be 'pure', i.e. the return value should depend - # only on the `dependency` parameter. - # - # @param [Object] dependency - # @return [String] the name for the given `dependency`. - def name_for(dependency) - dependency.to_s - end - - # @return [String] the name of the source of explicit dependencies, i.e. - # those passed to {Resolver#resolve} directly. - def name_for_explicit_dependency_source - 'user-specified dependency' - end - - # @return [String] the name of the source of 'locked' dependencies, i.e. - # those passed to {Resolver#resolve} directly as the `base` - def name_for_locking_dependency_source - 'Lockfile' - end - - # Sort dependencies so that the ones that are easiest to resolve are first. - # Easiest to resolve is (usually) defined by: - # 1) Is this dependency already activated? - # 2) How relaxed are the requirements? - # 3) Are there any conflicts for this dependency? - # 4) How many possibilities are there to satisfy this dependency? - # - # @param [Array] dependencies - # @param [DependencyGraph] activated the current dependency graph in the - # resolution process. - # @param [{String => Array}] conflicts - # @return [Array] a sorted copy of `dependencies`. - def sort_dependencies(dependencies, activated, conflicts) - dependencies.sort_by do |dependency| - name = name_for(dependency) - [ - activated.vertex_named(name).payload ? 0 : 1, - conflicts[name] ? 0 : 1, - ] - end - end - - # Returns whether this dependency, which has no possible matching - # specifications, can safely be ignored. - # - # @param [Object] dependency - # @return [Boolean] whether this dependency can safely be skipped. - def allow_missing?(dependency) - false - end - end -end diff --git a/lib/rubygems/vendor/molinillo/lib/molinillo/modules/ui.rb b/lib/rubygems/vendor/molinillo/lib/molinillo/modules/ui.rb deleted file mode 100644 index 464722902e24d0..00000000000000 --- a/lib/rubygems/vendor/molinillo/lib/molinillo/modules/ui.rb +++ /dev/null @@ -1,67 +0,0 @@ -# frozen_string_literal: true - -module Gem::Molinillo - # Conveys information about the resolution process to a user. - module UI - # The {IO} object that should be used to print output. `STDOUT`, by default. - # - # @return [IO] - def output - STDOUT - end - - # Called roughly every {#progress_rate}, this method should convey progress - # to the user. - # - # @return [void] - def indicate_progress - output.print '.' unless debug? - end - - # How often progress should be conveyed to the user via - # {#indicate_progress}, in seconds. A third of a second, by default. - # - # @return [Float] - def progress_rate - 0.33 - end - - # Called before resolution begins. - # - # @return [void] - def before_resolution - output.print 'Resolving dependencies...' - end - - # Called after resolution ends (either successfully or with an error). - # By default, prints a newline. - # - # @return [void] - def after_resolution - output.puts - end - - # Conveys debug information to the user. - # - # @param [Integer] depth the current depth of the resolution process. - # @return [void] - def debug(depth = 0) - if debug? - debug_info = yield - debug_info = debug_info.inspect unless debug_info.is_a?(String) - debug_info = debug_info.split("\n").map { |s| ":#{depth.to_s.rjust 4}: #{s}" } - output.puts debug_info - end - end - - # Whether or not debug messages should be printed. - # By default, whether or not the `MOLINILLO_DEBUG` environment variable is - # set. - # - # @return [Boolean] - def debug? - return @debug_mode if defined?(@debug_mode) - @debug_mode = ENV['MOLINILLO_DEBUG'] - end - end -end diff --git a/lib/rubygems/vendor/molinillo/lib/molinillo/resolution.rb b/lib/rubygems/vendor/molinillo/lib/molinillo/resolution.rb deleted file mode 100644 index 84ec6cb095977b..00000000000000 --- a/lib/rubygems/vendor/molinillo/lib/molinillo/resolution.rb +++ /dev/null @@ -1,839 +0,0 @@ -# frozen_string_literal: true - -module Gem::Molinillo - class Resolver - # A specific resolution from a given {Resolver} - class Resolution - # A conflict that the resolution process encountered - # @attr [Object] requirement the requirement that immediately led to the conflict - # @attr [{String,Nil=>[Object]}] requirements the requirements that caused the conflict - # @attr [Object, nil] existing the existing spec that was in conflict with - # the {#possibility} - # @attr [Object] possibility_set the set of specs that was unable to be - # activated due to a conflict. - # @attr [Object] locked_requirement the relevant locking requirement. - # @attr [Array>] requirement_trees the different requirement - # trees that led to every requirement for the conflicting name. - # @attr [{String=>Object}] activated_by_name the already-activated specs. - # @attr [Object] underlying_error an error that has occurred during resolution, and - # will be raised at the end of it if no resolution is found. - Conflict = Struct.new( - :requirement, - :requirements, - :existing, - :possibility_set, - :locked_requirement, - :requirement_trees, - :activated_by_name, - :underlying_error - ) - - class Conflict - # @return [Object] a spec that was unable to be activated due to a conflict - def possibility - possibility_set && possibility_set.latest_version - end - end - - # A collection of possibility states that share the same dependencies - # @attr [Array] dependencies the dependencies for this set of possibilities - # @attr [Array] possibilities the possibilities - PossibilitySet = Struct.new(:dependencies, :possibilities) - - class PossibilitySet - # String representation of the possibility set, for debugging - def to_s - "[#{possibilities.join(', ')}]" - end - - # @return [Object] most up-to-date dependency in the possibility set - def latest_version - possibilities.last - end - end - - # Details of the state to unwind to when a conflict occurs, and the cause of the unwind - # @attr [Integer] state_index the index of the state to unwind to - # @attr [Object] state_requirement the requirement of the state we're unwinding to - # @attr [Array] requirement_tree for the requirement we're relaxing - # @attr [Array] conflicting_requirements the requirements that combined to cause the conflict - # @attr [Array] requirement_trees for the conflict - # @attr [Array] requirements_unwound_to_instead array of unwind requirements that were chosen over this unwind - UnwindDetails = Struct.new( - :state_index, - :state_requirement, - :requirement_tree, - :conflicting_requirements, - :requirement_trees, - :requirements_unwound_to_instead - ) - - class UnwindDetails - include Comparable - - # We compare UnwindDetails when choosing which state to unwind to. If - # two options have the same state_index we prefer the one most - # removed from a requirement that caused the conflict. Both options - # would unwind to the same state, but a `grandparent` option will - # filter out fewer of its possibilities after doing so - where a state - # is both a `parent` and a `grandparent` to requirements that have - # caused a conflict this is the correct behaviour. - # @param [UnwindDetail] other UnwindDetail to be compared - # @return [Integer] integer specifying ordering - def <=>(other) - if state_index > other.state_index - 1 - elsif state_index == other.state_index - reversed_requirement_tree_index <=> other.reversed_requirement_tree_index - else - -1 - end - end - - # @return [Integer] index of state requirement in reversed requirement tree - # (the conflicting requirement itself will be at position 0) - def reversed_requirement_tree_index - @reversed_requirement_tree_index ||= - if state_requirement - requirement_tree.reverse.index(state_requirement) - else - 999_999 - end - end - - # @return [Boolean] where the requirement of the state we're unwinding - # to directly caused the conflict. Note: in this case, it is - # impossible for the state we're unwinding to be a parent of - # any of the other conflicting requirements (or we would have - # circularity) - def unwinding_to_primary_requirement? - requirement_tree.last == state_requirement - end - - # @return [Array] array of sub-dependencies to avoid when choosing a - # new possibility for the state we've unwound to. Only relevant for - # non-primary unwinds - def sub_dependencies_to_avoid - @requirements_to_avoid ||= - requirement_trees.map do |tree| - index = tree.index(state_requirement) - tree[index + 1] if index - end.compact - end - - # @return [Array] array of all the requirements that led to the need for - # this unwind - def all_requirements - @all_requirements ||= requirement_trees.flatten(1) - end - end - - # @return [SpecificationProvider] the provider that knows about - # dependencies, requirements, specifications, versions, etc. - attr_reader :specification_provider - - # @return [UI] the UI that knows how to communicate feedback about the - # resolution process back to the user - attr_reader :resolver_ui - - # @return [DependencyGraph] the base dependency graph to which - # dependencies should be 'locked' - attr_reader :base - - # @return [Array] the dependencies that were explicitly required - attr_reader :original_requested - - # Initializes a new resolution. - # @param [SpecificationProvider] specification_provider - # see {#specification_provider} - # @param [UI] resolver_ui see {#resolver_ui} - # @param [Array] requested see {#original_requested} - # @param [DependencyGraph] base see {#base} - def initialize(specification_provider, resolver_ui, requested, base) - @specification_provider = specification_provider - @resolver_ui = resolver_ui - @original_requested = requested - @base = base - @states = [] - @iteration_counter = 0 - @parents_of = Hash.new { |h, k| h[k] = [] } - end - - # Resolves the {#original_requested} dependencies into a full dependency - # graph - # @raise [ResolverError] if successful resolution is impossible - # @return [DependencyGraph] the dependency graph of successfully resolved - # dependencies - def resolve - start_resolution - - while state - break if !state.requirement && state.requirements.empty? - indicate_progress - if state.respond_to?(:pop_possibility_state) # DependencyState - debug(depth) { "Creating possibility state for #{requirement} (#{possibilities.count} remaining)" } - state.pop_possibility_state.tap do |s| - if s - states.push(s) - activated.tag(s) - end - end - end - process_topmost_state - end - - resolve_activated_specs - ensure - end_resolution - end - - # @return [Integer] the number of resolver iterations in between calls to - # {#resolver_ui}'s {UI#indicate_progress} method - attr_accessor :iteration_rate - private :iteration_rate - - # @return [Time] the time at which resolution began - attr_accessor :started_at - private :started_at - - # @return [Array] the stack of states for the resolution - attr_accessor :states - private :states - - private - - # Sets up the resolution process - # @return [void] - def start_resolution - @started_at = Time.now - - push_initial_state - - debug { "Starting resolution (#{@started_at})\nUser-requested dependencies: #{original_requested}" } - resolver_ui.before_resolution - end - - def resolve_activated_specs - activated.vertices.each do |_, vertex| - next unless vertex.payload - - latest_version = vertex.payload.possibilities.reverse_each.find do |possibility| - vertex.requirements.all? { |req| requirement_satisfied_by?(req, activated, possibility) } - end - - activated.set_payload(vertex.name, latest_version) - end - activated.freeze - end - - # Ends the resolution process - # @return [void] - def end_resolution - resolver_ui.after_resolution - debug do - "Finished resolution (#{@iteration_counter} steps) " \ - "(Took #{(ended_at = Time.now) - @started_at} seconds) (#{ended_at})" - end - debug { 'Unactivated: ' + Hash[activated.vertices.reject { |_n, v| v.payload }].keys.join(', ') } if state - debug { 'Activated: ' + Hash[activated.vertices.select { |_n, v| v.payload }].keys.join(', ') } if state - end - - require_relative 'state' - require_relative 'modules/specification_provider' - - require_relative 'delegates/resolution_state' - require_relative 'delegates/specification_provider' - - include Gem::Molinillo::Delegates::ResolutionState - include Gem::Molinillo::Delegates::SpecificationProvider - - # Processes the topmost available {RequirementState} on the stack - # @return [void] - def process_topmost_state - if possibility - attempt_to_activate - else - create_conflict - unwind_for_conflict - end - rescue CircularDependencyError => underlying_error - create_conflict(underlying_error) - unwind_for_conflict - end - - # @return [Object] the current possibility that the resolution is trying - # to activate - def possibility - possibilities.last - end - - # @return [RequirementState] the current state the resolution is - # operating upon - def state - states.last - end - - # Creates and pushes the initial state for the resolution, based upon the - # {#requested} dependencies - # @return [void] - def push_initial_state - graph = DependencyGraph.new.tap do |dg| - original_requested.each do |requested| - vertex = dg.add_vertex(name_for(requested), nil, true) - vertex.explicit_requirements << requested - end - dg.tag(:initial_state) - end - - push_state_for_requirements(original_requested, true, graph) - end - - # Unwinds the states stack because a conflict has been encountered - # @return [void] - def unwind_for_conflict - details_for_unwind = build_details_for_unwind - unwind_options = unused_unwind_options - debug(depth) { "Unwinding for conflict: #{requirement} to #{details_for_unwind.state_index / 2}" } - conflicts.tap do |c| - sliced_states = states.slice!((details_for_unwind.state_index + 1)..-1) - raise_error_unless_state(c) - activated.rewind_to(sliced_states.first || :initial_state) if sliced_states - state.conflicts = c - state.unused_unwind_options = unwind_options - filter_possibilities_after_unwind(details_for_unwind) - index = states.size - 1 - @parents_of.each { |_, a| a.reject! { |i| i >= index } } - state.unused_unwind_options.reject! { |uw| uw.state_index >= index } - end - end - - # Raises a VersionConflict error, or any underlying error, if there is no - # current state - # @return [void] - def raise_error_unless_state(conflicts) - return if state - - error = conflicts.values.map(&:underlying_error).compact.first - raise error || VersionConflict.new(conflicts, specification_provider) - end - - # @return [UnwindDetails] Details of the nearest index to which we could unwind - def build_details_for_unwind - # Get the possible unwinds for the current conflict - current_conflict = conflicts[name] - binding_requirements = binding_requirements_for_conflict(current_conflict) - unwind_details = unwind_options_for_requirements(binding_requirements) - - last_detail_for_current_unwind = unwind_details.sort.last - current_detail = last_detail_for_current_unwind - - # Look for past conflicts that could be unwound to affect the - # requirement tree for the current conflict - all_reqs = last_detail_for_current_unwind.all_requirements - all_reqs_size = all_reqs.size - relevant_unused_unwinds = unused_unwind_options.select do |alternative| - diff_reqs = all_reqs - alternative.requirements_unwound_to_instead - next if diff_reqs.size == all_reqs_size - # Find the highest index unwind whilst looping through - current_detail = alternative if alternative > current_detail - alternative - end - - # Add the current unwind options to the `unused_unwind_options` array. - # The "used" option will be filtered out during `unwind_for_conflict`. - state.unused_unwind_options += unwind_details.reject { |detail| detail.state_index == -1 } - - # Update the requirements_unwound_to_instead on any relevant unused unwinds - relevant_unused_unwinds.each do |d| - (d.requirements_unwound_to_instead << current_detail.state_requirement).uniq! - end - unwind_details.each do |d| - (d.requirements_unwound_to_instead << current_detail.state_requirement).uniq! - end - - current_detail - end - - # @param [Array] binding_requirements array of requirements that combine to create a conflict - # @return [Array] array of UnwindDetails that have a chance - # of resolving the passed requirements - def unwind_options_for_requirements(binding_requirements) - unwind_details = [] - - trees = [] - binding_requirements.reverse_each do |r| - partial_tree = [r] - trees << partial_tree - unwind_details << UnwindDetails.new(-1, nil, partial_tree, binding_requirements, trees, []) - - # If this requirement has alternative possibilities, check if any would - # satisfy the other requirements that created this conflict - requirement_state = find_state_for(r) - if conflict_fixing_possibilities?(requirement_state, binding_requirements) - unwind_details << UnwindDetails.new( - states.index(requirement_state), - r, - partial_tree, - binding_requirements, - trees, - [] - ) - end - - # Next, look at the parent of this requirement, and check if the requirement - # could have been avoided if an alternative PossibilitySet had been chosen - parent_r = parent_of(r) - next if parent_r.nil? - partial_tree.unshift(parent_r) - requirement_state = find_state_for(parent_r) - if requirement_state.possibilities.any? { |set| !set.dependencies.include?(r) } - unwind_details << UnwindDetails.new( - states.index(requirement_state), - parent_r, - partial_tree, - binding_requirements, - trees, - [] - ) - end - - # Finally, look at the grandparent and up of this requirement, looking - # for any possibilities that wouldn't create their parent requirement - grandparent_r = parent_of(parent_r) - until grandparent_r.nil? - partial_tree.unshift(grandparent_r) - requirement_state = find_state_for(grandparent_r) - if requirement_state.possibilities.any? { |set| !set.dependencies.include?(parent_r) } - unwind_details << UnwindDetails.new( - states.index(requirement_state), - grandparent_r, - partial_tree, - binding_requirements, - trees, - [] - ) - end - parent_r = grandparent_r - grandparent_r = parent_of(parent_r) - end - end - - unwind_details - end - - # @param [DependencyState] state - # @param [Array] binding_requirements array of requirements - # @return [Boolean] whether or not the given state has any possibilities - # that could satisfy the given requirements - def conflict_fixing_possibilities?(state, binding_requirements) - return false unless state - - state.possibilities.any? do |possibility_set| - possibility_set.possibilities.any? do |poss| - possibility_satisfies_requirements?(poss, binding_requirements) - end - end - end - - # Filter's a state's possibilities to remove any that would not fix the - # conflict we've just rewound from - # @param [UnwindDetails] unwind_details details of the conflict just - # unwound from - # @return [void] - def filter_possibilities_after_unwind(unwind_details) - return unless state && !state.possibilities.empty? - - if unwind_details.unwinding_to_primary_requirement? - filter_possibilities_for_primary_unwind(unwind_details) - else - filter_possibilities_for_parent_unwind(unwind_details) - end - end - - # Filter's a state's possibilities to remove any that would not satisfy - # the requirements in the conflict we've just rewound from - # @param [UnwindDetails] unwind_details details of the conflict just unwound from - # @return [void] - def filter_possibilities_for_primary_unwind(unwind_details) - unwinds_to_state = unused_unwind_options.select { |uw| uw.state_index == unwind_details.state_index } - unwinds_to_state << unwind_details - unwind_requirement_sets = unwinds_to_state.map(&:conflicting_requirements) - - state.possibilities.reject! do |possibility_set| - possibility_set.possibilities.none? do |poss| - unwind_requirement_sets.any? do |requirements| - possibility_satisfies_requirements?(poss, requirements) - end - end - end - end - - # @param [Object] possibility a single possibility - # @param [Array] requirements an array of requirements - # @return [Boolean] whether the possibility satisfies all of the - # given requirements - def possibility_satisfies_requirements?(possibility, requirements) - name = name_for(possibility) - - activated.tag(:swap) - activated.set_payload(name, possibility) if activated.vertex_named(name) - satisfied = requirements.all? { |r| requirement_satisfied_by?(r, activated, possibility) } - activated.rewind_to(:swap) - - satisfied - end - - # Filter's a state's possibilities to remove any that would (eventually) - # create a requirement in the conflict we've just rewound from - # @param [UnwindDetails] unwind_details details of the conflict just unwound from - # @return [void] - def filter_possibilities_for_parent_unwind(unwind_details) - unwinds_to_state = unused_unwind_options.select { |uw| uw.state_index == unwind_details.state_index } - unwinds_to_state << unwind_details - - primary_unwinds = unwinds_to_state.select(&:unwinding_to_primary_requirement?).uniq - parent_unwinds = unwinds_to_state.uniq - primary_unwinds - - allowed_possibility_sets = primary_unwinds.flat_map do |unwind| - states[unwind.state_index].possibilities.select do |possibility_set| - possibility_set.possibilities.any? do |poss| - possibility_satisfies_requirements?(poss, unwind.conflicting_requirements) - end - end - end - - requirements_to_avoid = parent_unwinds.flat_map(&:sub_dependencies_to_avoid) - - state.possibilities.reject! do |possibility_set| - !allowed_possibility_sets.include?(possibility_set) && - (requirements_to_avoid - possibility_set.dependencies).empty? - end - end - - # @param [Conflict] conflict - # @return [Array] minimal array of requirements that would cause the passed - # conflict to occur. - def binding_requirements_for_conflict(conflict) - return [conflict.requirement] if conflict.possibility.nil? - - possible_binding_requirements = conflict.requirements.values.flatten(1).uniq - - # When there's a `CircularDependency` error the conflicting requirement - # (the one causing the circular) won't be `conflict.requirement` - # (which won't be for the right state, because we won't have created it, - # because it's circular). - # We need to make sure we have that requirement in the conflict's list, - # otherwise we won't be able to unwind properly, so we just return all - # the requirements for the conflict. - return possible_binding_requirements if conflict.underlying_error - - possibilities = search_for(conflict.requirement) - - # If all the requirements together don't filter out all possibilities, - # then the only two requirements we need to consider are the initial one - # (where the dependency's version was first chosen) and the last - if binding_requirement_in_set?(nil, possible_binding_requirements, possibilities) - return [conflict.requirement, requirement_for_existing_name(name_for(conflict.requirement))].compact - end - - # Loop through the possible binding requirements, removing each one - # that doesn't bind. Use a `reverse_each` as we want the earliest set of - # binding requirements, and don't use `reject!` as we wish to refine the - # array *on each iteration*. - binding_requirements = possible_binding_requirements.dup - possible_binding_requirements.reverse_each do |req| - next if req == conflict.requirement - unless binding_requirement_in_set?(req, binding_requirements, possibilities) - binding_requirements -= [req] - end - end - - binding_requirements - end - - # @param [Object] requirement we wish to check - # @param [Array] possible_binding_requirements array of requirements - # @param [Array] possibilities array of possibilities the requirements will be used to filter - # @return [Boolean] whether or not the given requirement is required to filter - # out all elements of the array of possibilities. - def binding_requirement_in_set?(requirement, possible_binding_requirements, possibilities) - possibilities.any? do |poss| - possibility_satisfies_requirements?(poss, possible_binding_requirements - [requirement]) - end - end - - # @param [Object] requirement - # @return [Object] the requirement that led to `requirement` being added - # to the list of requirements. - def parent_of(requirement) - return unless requirement - return unless index = @parents_of[requirement].last - return unless parent_state = @states[index] - parent_state.requirement - end - - # @param [String] name - # @return [Object] the requirement that led to a version of a possibility - # with the given name being activated. - def requirement_for_existing_name(name) - return nil unless vertex = activated.vertex_named(name) - return nil unless vertex.payload - states.find { |s| s.name == name }.requirement - end - - # @param [Object] requirement - # @return [ResolutionState] the state whose `requirement` is the given - # `requirement`. - def find_state_for(requirement) - return nil unless requirement - states.find { |i| requirement == i.requirement } - end - - # @param [Object] underlying_error - # @return [Conflict] a {Conflict} that reflects the failure to activate - # the {#possibility} in conjunction with the current {#state} - def create_conflict(underlying_error = nil) - vertex = activated.vertex_named(name) - locked_requirement = locked_requirement_named(name) - - requirements = {} - unless vertex.explicit_requirements.empty? - requirements[name_for_explicit_dependency_source] = vertex.explicit_requirements - end - requirements[name_for_locking_dependency_source] = [locked_requirement] if locked_requirement - vertex.incoming_edges.each do |edge| - (requirements[edge.origin.payload.latest_version] ||= []).unshift(edge.requirement) - end - - activated_by_name = {} - activated.each { |v| activated_by_name[v.name] = v.payload.latest_version if v.payload } - conflicts[name] = Conflict.new( - requirement, - requirements, - vertex.payload && vertex.payload.latest_version, - possibility, - locked_requirement, - requirement_trees, - activated_by_name, - underlying_error - ) - end - - # @return [Array>] The different requirement - # trees that led to every requirement for the current spec. - def requirement_trees - vertex = activated.vertex_named(name) - vertex.requirements.map { |r| requirement_tree_for(r) } - end - - # @param [Object] requirement - # @return [Array] the list of requirements that led to - # `requirement` being required. - def requirement_tree_for(requirement) - tree = [] - while requirement - tree.unshift(requirement) - requirement = parent_of(requirement) - end - tree - end - - # Indicates progress roughly once every second - # @return [void] - def indicate_progress - @iteration_counter += 1 - @progress_rate ||= resolver_ui.progress_rate - if iteration_rate.nil? - if Time.now - started_at >= @progress_rate - self.iteration_rate = @iteration_counter - end - end - - if iteration_rate && (@iteration_counter % iteration_rate) == 0 - resolver_ui.indicate_progress - end - end - - # Calls the {#resolver_ui}'s {UI#debug} method - # @param [Integer] depth the depth of the {#states} stack - # @param [Proc] block a block that yields a {#to_s} - # @return [void] - def debug(depth = 0, &block) - resolver_ui.debug(depth, &block) - end - - # Attempts to activate the current {#possibility} - # @return [void] - def attempt_to_activate - debug(depth) { 'Attempting to activate ' + possibility.to_s } - existing_vertex = activated.vertex_named(name) - if existing_vertex.payload - debug(depth) { "Found existing spec (#{existing_vertex.payload})" } - attempt_to_filter_existing_spec(existing_vertex) - else - latest = possibility.latest_version - possibility.possibilities.select! do |possibility| - requirement_satisfied_by?(requirement, activated, possibility) - end - if possibility.latest_version.nil? - # ensure there's a possibility for better error messages - possibility.possibilities << latest if latest - create_conflict - unwind_for_conflict - else - activate_new_spec - end - end - end - - # Attempts to update the existing vertex's `PossibilitySet` with a filtered version - # @return [void] - def attempt_to_filter_existing_spec(vertex) - filtered_set = filtered_possibility_set(vertex) - if !filtered_set.possibilities.empty? - activated.set_payload(name, filtered_set) - new_requirements = requirements.dup - push_state_for_requirements(new_requirements, false) - else - create_conflict - debug(depth) { "Unsatisfied by existing spec (#{vertex.payload})" } - unwind_for_conflict - end - end - - # Generates a filtered version of the existing vertex's `PossibilitySet` using the - # current state's `requirement` - # @param [Object] vertex existing vertex - # @return [PossibilitySet] filtered possibility set - def filtered_possibility_set(vertex) - PossibilitySet.new(vertex.payload.dependencies, vertex.payload.possibilities & possibility.possibilities) - end - - # @param [String] requirement_name the spec name to search for - # @return [Object] the locked spec named `requirement_name`, if one - # is found on {#base} - def locked_requirement_named(requirement_name) - vertex = base.vertex_named(requirement_name) - vertex && vertex.payload - end - - # Add the current {#possibility} to the dependency graph of the current - # {#state} - # @return [void] - def activate_new_spec - conflicts.delete(name) - debug(depth) { "Activated #{name} at #{possibility}" } - activated.set_payload(name, possibility) - require_nested_dependencies_for(possibility) - end - - # Requires the dependencies that the recently activated spec has - # @param [Object] possibility_set the PossibilitySet that has just been - # activated - # @return [void] - def require_nested_dependencies_for(possibility_set) - nested_dependencies = dependencies_for(possibility_set.latest_version) - debug(depth) { "Requiring nested dependencies (#{nested_dependencies.join(', ')})" } - nested_dependencies.each do |d| - activated.add_child_vertex(name_for(d), nil, [name_for(possibility_set.latest_version)], d) - parent_index = states.size - 1 - parents = @parents_of[d] - parents << parent_index if parents.empty? - end - - push_state_for_requirements(requirements + nested_dependencies, !nested_dependencies.empty?) - end - - # Pushes a new {DependencyState} that encapsulates both existing and new - # requirements - # @param [Array] new_requirements - # @param [Boolean] requires_sort - # @param [Object] new_activated - # @return [void] - def push_state_for_requirements(new_requirements, requires_sort = true, new_activated = activated) - new_requirements = sort_dependencies(new_requirements.uniq, new_activated, conflicts) if requires_sort - new_requirement = nil - loop do - new_requirement = new_requirements.shift - break if new_requirement.nil? || states.none? { |s| s.requirement == new_requirement } - end - new_name = new_requirement ? name_for(new_requirement) : ''.freeze - possibilities = possibilities_for_requirement(new_requirement) - handle_missing_or_push_dependency_state DependencyState.new( - new_name, new_requirements, new_activated, - new_requirement, possibilities, depth, conflicts.dup, unused_unwind_options.dup - ) - end - - # Checks a proposed requirement with any existing locked requirement - # before generating an array of possibilities for it. - # @param [Object] requirement the proposed requirement - # @param [Object] activated - # @return [Array] possibilities - def possibilities_for_requirement(requirement, activated = self.activated) - return [] unless requirement - if locked_requirement_named(name_for(requirement)) - return locked_requirement_possibility_set(requirement, activated) - end - - group_possibilities(search_for(requirement)) - end - - # @param [Object] requirement the proposed requirement - # @param [Object] activated - # @return [Array] possibility set containing only the locked requirement, if any - def locked_requirement_possibility_set(requirement, activated = self.activated) - all_possibilities = search_for(requirement) - locked_requirement = locked_requirement_named(name_for(requirement)) - - # Longwinded way to build a possibilities array with either the locked - # requirement or nothing in it. Required, since the API for - # locked_requirement isn't guaranteed. - locked_possibilities = all_possibilities.select do |possibility| - requirement_satisfied_by?(locked_requirement, activated, possibility) - end - - group_possibilities(locked_possibilities) - end - - # Build an array of PossibilitySets, with each element representing a group of - # dependency versions that all have the same sub-dependency version constraints - # and are contiguous. - # @param [Array] possibilities an array of possibilities - # @return [Array] an array of possibility sets - def group_possibilities(possibilities) - possibility_sets = [] - current_possibility_set = nil - - possibilities.reverse_each do |possibility| - dependencies = dependencies_for(possibility) - if current_possibility_set && dependencies_equal?(current_possibility_set.dependencies, dependencies) - current_possibility_set.possibilities.unshift(possibility) - else - possibility_sets.unshift(PossibilitySet.new(dependencies, [possibility])) - current_possibility_set = possibility_sets.first - end - end - - possibility_sets - end - - # Pushes a new {DependencyState}. - # If the {#specification_provider} says to - # {SpecificationProvider#allow_missing?} that particular requirement, and - # there are no possibilities for that requirement, then `state` is not - # pushed, and the vertex in {#activated} is removed, and we continue - # resolving the remaining requirements. - # @param [DependencyState] state - # @return [void] - def handle_missing_or_push_dependency_state(state) - if state.requirement && state.possibilities.empty? && allow_missing?(state.requirement) - state.activated.detach_vertex_named(state.name) - push_state_for_requirements(state.requirements.dup, false, state.activated) - else - states.push(state).tap { activated.tag(state) } - end - end - end - end -end diff --git a/lib/rubygems/vendor/molinillo/lib/molinillo/resolver.rb b/lib/rubygems/vendor/molinillo/lib/molinillo/resolver.rb deleted file mode 100644 index 86229c3fa12046..00000000000000 --- a/lib/rubygems/vendor/molinillo/lib/molinillo/resolver.rb +++ /dev/null @@ -1,46 +0,0 @@ -# frozen_string_literal: true - -require_relative 'dependency_graph' - -module Gem::Molinillo - # This class encapsulates a dependency resolver. - # The resolver is responsible for determining which set of dependencies to - # activate, with feedback from the {#specification_provider} - # - # - class Resolver - require_relative 'resolution' - - # @return [SpecificationProvider] the specification provider used - # in the resolution process - attr_reader :specification_provider - - # @return [UI] the UI module used to communicate back to the user - # during the resolution process - attr_reader :resolver_ui - - # Initializes a new resolver. - # @param [SpecificationProvider] specification_provider - # see {#specification_provider} - # @param [UI] resolver_ui - # see {#resolver_ui} - def initialize(specification_provider, resolver_ui) - @specification_provider = specification_provider - @resolver_ui = resolver_ui - end - - # Resolves the requested dependencies into a {DependencyGraph}, - # locking to the base dependency graph (if specified) - # @param [Array] requested an array of 'requested' dependencies that the - # {#specification_provider} can understand - # @param [DependencyGraph,nil] base the base dependency graph to which - # dependencies should be 'locked' - def resolve(requested, base = DependencyGraph.new) - Resolution.new(specification_provider, - resolver_ui, - requested, - base). - resolve - end - end -end diff --git a/lib/rubygems/vendor/molinillo/lib/molinillo/state.rb b/lib/rubygems/vendor/molinillo/lib/molinillo/state.rb deleted file mode 100644 index c48ec6af9c1234..00000000000000 --- a/lib/rubygems/vendor/molinillo/lib/molinillo/state.rb +++ /dev/null @@ -1,58 +0,0 @@ -# frozen_string_literal: true - -module Gem::Molinillo - # A state that a {Resolution} can be in - # @attr [String] name the name of the current requirement - # @attr [Array] requirements currently unsatisfied requirements - # @attr [DependencyGraph] activated the graph of activated dependencies - # @attr [Object] requirement the current requirement - # @attr [Object] possibilities the possibilities to satisfy the current requirement - # @attr [Integer] depth the depth of the resolution - # @attr [Hash] conflicts unresolved conflicts, indexed by dependency name - # @attr [Array] unused_unwind_options unwinds for previous conflicts that weren't explored - ResolutionState = Struct.new( - :name, - :requirements, - :activated, - :requirement, - :possibilities, - :depth, - :conflicts, - :unused_unwind_options - ) - - class ResolutionState - # Returns an empty resolution state - # @return [ResolutionState] an empty state - def self.empty - new(nil, [], DependencyGraph.new, nil, nil, 0, {}, []) - end - end - - # A state that encapsulates a set of {#requirements} with an {Array} of - # possibilities - class DependencyState < ResolutionState - # Removes a possibility from `self` - # @return [PossibilityState] a state with a single possibility, - # the possibility that was removed from `self` - def pop_possibility_state - PossibilityState.new( - name, - requirements.dup, - activated, - requirement, - [possibilities.pop], - depth + 1, - conflicts.dup, - unused_unwind_options.dup - ).tap do |state| - state.activated.tag(state) - end - end - end - - # A state that encapsulates a single possibility to fulfill the given - # {#requirement} - class PossibilityState < ResolutionState - end -end diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub.rb new file mode 100644 index 00000000000000..818e947477cf2c --- /dev/null +++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub.rb @@ -0,0 +1,53 @@ +require_relative "pub_grub/package" +require_relative "pub_grub/static_package_source" +require_relative "pub_grub/term" +require_relative "pub_grub/version_range" +require_relative "pub_grub/version_constraint" +require_relative "pub_grub/version_union" +require_relative "pub_grub/version_solver" +require_relative "pub_grub/incompatibility" +require_relative 'pub_grub/solve_failure' +require_relative 'pub_grub/failure_writer' +require_relative 'pub_grub/version' + +module Gem::PubGrub + # Minimal logger that doesn't require the 'logger' gem + class NullLogger + def info(&block); end + def debug(&block); end + def warn(&block); end + def error(&block); end + end + + class StderrLogger + def info(&block) + $stderr.puts "INFO: #{block.call}" if block + end + + def debug(&block) + $stderr.puts "DEBUG: #{block.call}" if block + end + + def warn(&block) + $stderr.puts "WARN: #{block.call}" if block + end + + def error(&block) + $stderr.puts "ERROR: #{block.call}" if block + end + end + + class << self + attr_writer :logger + + def logger + @logger || default_logger + end + + private + + def default_logger + @logger = $DEBUG ? StderrLogger.new : NullLogger.new + end + end +end diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub/assignment.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub/assignment.rb new file mode 100644 index 00000000000000..7a11cf0933c9be --- /dev/null +++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub/assignment.rb @@ -0,0 +1,20 @@ +module Gem::PubGrub + class Assignment + attr_reader :term, :cause, :decision_level, :index + def initialize(term, cause, decision_level, index) + @term = term + @cause = cause + @decision_level = decision_level + @index = index + end + + def self.decision(package, version, decision_level, index) + term = Term.new(VersionConstraint.exact(package, version), true) + new(term, :decision, decision_level, index) + end + + def decision? + cause == :decision + end + end +end diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub/basic_package_source.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub/basic_package_source.rb new file mode 100644 index 00000000000000..c8dbf2a5ab15ec --- /dev/null +++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub/basic_package_source.rb @@ -0,0 +1,169 @@ +require_relative 'version_constraint' +require_relative 'incompatibility' + +module Gem::PubGrub + # Types: + # + # Where possible, Gem::PubGrub will accept user-defined types, so long as they quack. + # + # ## "Package": + # + # This class will be used to represent the various packages being solved for. + # .to_s will be called when displaying errors and debugging info, it should + # probably return the package's name. + # It must also have a reasonable definition of #== and #hash + # + # Example classes: String ("rails") + # + # + # ## "Version": + # + # This class will be used to represent a single version number. + # + # Versions don't need to store their associated package, however they will + # only be compared against other versions of the same package. + # + # It must be Comparible (and implement <=> reasonably) + # + # Example classes: Gem::Version, Integer + # + # + # ## "Dependency" + # + # This class represents the requirement one package has on another. It is + # returned by dependencies_for(package, version) and will be passed to + # parse_dependency to convert it to a format Gem::PubGrub understands. + # + # It must also have a reasonable definition of #== + # + # Example classes: String ("~> 1.0"), Gem::Requirement + # + class BasicPackageSource + # Override me! + # + # This is called per package to find all possible versions of a package. + # + # It is called at most once per-package + # + # Returns: Array of versions for a package, in preferred order of selection + def all_versions_for(package) + raise NotImplementedError + end + + # Override me! + # + # Returns: Hash in the form of { package => requirement, ... } + def dependencies_for(package, version) + raise NotImplementedError + end + + # Override me! + # + # Convert a (user-defined) dependency into a format Gem::PubGrub understands. + # + # Package is passed to this method but for many implementations is not + # needed. + # + # Returns: either a Gem::PubGrub::VersionRange, Gem::PubGrub::VersionUnion, or a + # Gem::PubGrub::VersionConstraint + def parse_dependency(package, dependency) + raise NotImplementedError + end + + # Override me! + # + # If not overridden, this will call dependencies_for with the root package. + # + # Returns: Hash in the form of { package => requirement, ... } (see dependencies_for) + def root_dependencies + dependencies_for(@root_package, @root_version) + end + + def initialize + @root_package = Package.root + @root_version = Package.root_version + + @sorted_versions = Hash.new do |h,k| + if k == @root_package + h[k] = [@root_version] + else + h[k] = all_versions_for(k).sort + end + end + + @cached_dependencies = Hash.new do |packages, package| + if package == @root_package + packages[package] = { + @root_version => root_dependencies + } + else + packages[package] = Hash.new do |versions, version| + versions[version] = dependencies_for(package, version) + end + end + end + end + + def versions_for(package, range=VersionRange.any) + range.select_versions(@sorted_versions[package]) + end + + def no_versions_incompatibility_for(_package, unsatisfied_term) + cause = Incompatibility::NoVersions.new(unsatisfied_term) + + Incompatibility.new([unsatisfied_term], cause: cause) + end + + def incompatibilities_for(package, version) + package_deps = @cached_dependencies[package] + sorted_versions = @sorted_versions[package] + package_deps[version].map do |dep_package, dep_constraint_name| + low = high = sorted_versions.index(version) + + # find version low such that all >= low share the same dep + while low > 0 && + package_deps[sorted_versions[low - 1]][dep_package] == dep_constraint_name + low -= 1 + end + low = + if low == 0 + nil + else + sorted_versions[low] + end + + # find version high such that all < high share the same dep + while high < sorted_versions.length && + package_deps[sorted_versions[high]][dep_package] == dep_constraint_name + high += 1 + end + high = + if high == sorted_versions.length + nil + else + sorted_versions[high] + end + + range = VersionRange.new(min: low, max: high, include_min: !low.nil?) + + self_constraint = VersionConstraint.new(package, range: range) + + if !@packages.include?(dep_package) + # no such package -> this version is invalid + end + + dep_constraint = parse_dependency(dep_package, dep_constraint_name) + if !dep_constraint + # falsey indicates this dependency was invalid + cause = Gem::PubGrub::Incompatibility::InvalidDependency.new(dep_package, dep_constraint_name) + return [Incompatibility.new([Term.new(self_constraint, true)], cause: cause)] + elsif !dep_constraint.is_a?(VersionConstraint) + # Upgrade range/union to VersionConstraint + dep_constraint = VersionConstraint.new(dep_package, range: dep_constraint) + end + + Incompatibility.new([Term.new(self_constraint, true), Term.new(dep_constraint, false)], cause: :dependency) + end + end + end +end diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub/failure_writer.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub/failure_writer.rb new file mode 100644 index 00000000000000..d8bfde0286224c --- /dev/null +++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub/failure_writer.rb @@ -0,0 +1,182 @@ +module Gem::PubGrub + class FailureWriter + def initialize(root) + @root = root + + # { Incompatibility => Integer } + @derivations = {} + + # [ [ String, Integer or nil ] ] + @lines = [] + + # { Incompatibility => Integer } + @line_numbers = {} + + count_derivations(root) + end + + def write + return @root.to_s unless @root.conflict? + + visit(@root) + + padding = @line_numbers.empty? ? 0 : "(#{@line_numbers.values.last}) ".length + + @lines.map do |message, number| + next "" if message.empty? + + lead = number ? "(#{number}) " : "" + lead = lead.ljust(padding) + message = message.gsub("\n", "\n" + " " * (padding + 2)) + "#{lead}#{message}" + end.join("\n") + end + + private + + def write_line(incompatibility, message, numbered:) + if numbered + number = @line_numbers.length + 1 + @line_numbers[incompatibility] = number + end + + @lines << [message, number] + end + + def visit(incompatibility, conclusion: false) + raise unless incompatibility.conflict? + + numbered = conclusion || @derivations[incompatibility] > 1; + conjunction = conclusion || incompatibility == @root ? "So," : "And" + + cause = incompatibility.cause + + if cause.conflict.conflict? && cause.other.conflict? + conflict_line = @line_numbers[cause.conflict] + other_line = @line_numbers[cause.other] + + if conflict_line && other_line + write_line( + incompatibility, + "Because #{cause.conflict} (#{conflict_line})\nand #{cause.other} (#{other_line}),\n#{incompatibility}.", + numbered: numbered + ) + elsif conflict_line || other_line + with_line = conflict_line ? cause.conflict : cause.other + without_line = conflict_line ? cause.other : cause.conflict + line = @line_numbers[with_line] + + visit(without_line); + write_line( + incompatibility, + "#{conjunction} because #{with_line} (#{line}),\n#{incompatibility}.", + numbered: numbered + ) + else + single_line_conflict = single_line?(cause.conflict.cause) + single_line_other = single_line?(cause.other.cause) + + if single_line_conflict || single_line_other + first = single_line_other ? cause.conflict : cause.other + second = single_line_other ? cause.other : cause.conflict + visit(first) + visit(second) + write_line( + incompatibility, + "Thus, #{incompatibility}.", + numbered: numbered + ) + else + visit(cause.conflict, conclusion: true) + @lines << ["", nil] + visit(cause.other) + + write_line( + incompatibility, + "#{conjunction} because #{cause.conflict} (#{@line_numbers[cause.conflict]}),\n#{incompatibility}.", + numbered: numbered + ) + end + end + elsif cause.conflict.conflict? || cause.other.conflict? + derived = cause.conflict.conflict? ? cause.conflict : cause.other + ext = cause.conflict.conflict? ? cause.other : cause.conflict + + derived_line = @line_numbers[derived] + if derived_line + write_line( + incompatibility, + "Because #{ext}\nand #{derived} (#{derived_line}),\n#{incompatibility}.", + numbered: numbered + ) + elsif collapsible?(derived) + derived_cause = derived.cause + if derived_cause.conflict.conflict? + collapsed_derived = derived_cause.conflict + collapsed_ext = derived_cause.other + else + collapsed_derived = derived_cause.other + collapsed_ext = derived_cause.conflict + end + + visit(collapsed_derived) + + write_line( + incompatibility, + "#{conjunction} because #{collapsed_ext}\nand #{ext},\n#{incompatibility}.", + numbered: numbered + ) + else + visit(derived) + write_line( + incompatibility, + "#{conjunction} because #{ext},\n#{incompatibility}.", + numbered: numbered + ) + end + else + write_line( + incompatibility, + "Because #{cause.conflict}\nand #{cause.other},\n#{incompatibility}.", + numbered: numbered + ) + end + end + + def single_line?(cause) + !cause.conflict.conflict? && !cause.other.conflict? + end + + def collapsible?(incompatibility) + return false if @derivations[incompatibility] > 1 + + cause = incompatibility.cause + # If incompatibility is derived from two derived incompatibilities, + # there are too many transitive causes to display concisely. + return false if cause.conflict.conflict? && cause.other.conflict? + + # If incompatibility is derived from two external incompatibilities, it + # tends to be confusing to collapse it. + return false unless cause.conflict.conflict? || cause.other.conflict? + + # If incompatibility's internal cause is numbered, collapsing it would + # get too noisy. + complex = cause.conflict.conflict? ? cause.conflict : cause.other + + !@line_numbers.has_key?(complex) + end + + def count_derivations(incompatibility) + if @derivations.has_key?(incompatibility) + @derivations[incompatibility] += 1 + else + @derivations[incompatibility] = 1 + if incompatibility.conflict? + cause = incompatibility.cause + count_derivations(cause.conflict) + count_derivations(cause.other) + end + end + end + end +end diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub/incompatibility.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub/incompatibility.rb new file mode 100644 index 00000000000000..b5652b5e01226c --- /dev/null +++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub/incompatibility.rb @@ -0,0 +1,150 @@ +module Gem::PubGrub + class Incompatibility + ConflictCause = Struct.new(:incompatibility, :satisfier) do + alias_method :conflict, :incompatibility + alias_method :other, :satisfier + end + + InvalidDependency = Struct.new(:package, :constraint) do + end + + NoVersions = Struct.new(:constraint) do + end + + attr_reader :terms, :cause + + def initialize(terms, cause:, custom_explanation: nil) + @cause = cause + @terms = cleanup_terms(terms) + @custom_explanation = custom_explanation + + if cause == :dependency && @terms.length != 2 + raise ArgumentError, "a dependency Incompatibility must have exactly two terms. Got #{@terms.inspect}" + end + end + + def hash + cause.hash ^ terms.hash + end + + def eql?(other) + cause.eql?(other.cause) && + terms.eql?(other.terms) + end + + def failure? + terms.empty? || (terms.length == 1 && Package.root?(terms[0].package) && terms[0].positive?) + end + + def conflict? + ConflictCause === cause + end + + # Returns all external incompatibilities in this incompatibility's + # derivation graph + def external_incompatibilities + if conflict? + [ + cause.conflict, + cause.other + ].flat_map(&:external_incompatibilities) + else + [this] + end + end + + def to_s + return @custom_explanation if @custom_explanation + + case cause + when :root + "(root dependency)" + when :dependency + "#{terms[0].to_s(allow_every: true)} depends on #{terms[1].invert}" + when Gem::PubGrub::Incompatibility::InvalidDependency + "#{terms[0].to_s(allow_every: true)} depends on unknown package #{cause.package}" + when Gem::PubGrub::Incompatibility::NoVersions + "no versions satisfy #{cause.constraint}" + when Gem::PubGrub::Incompatibility::ConflictCause + if failure? + "version solving has failed" + elsif terms.length == 1 + term = terms[0] + if term.positive? + if term.constraint.any? + "#{term.package} cannot be used" + else + "#{term.to_s(allow_every: true)} cannot be used" + end + else + "#{term.invert} is required" + end + else + if terms.all?(&:positive?) + if terms.length == 2 + "#{terms[0].to_s(allow_every: true)} is incompatible with #{terms[1]}" + else + "one of #{terms.map(&:to_s).join(" or ")} must be false" + end + elsif terms.all?(&:negative?) + if terms.length == 2 + "either #{terms[0].invert} or #{terms[1].invert}" + else + "one of #{terms.map(&:invert).join(" or ")} must be true"; + end + else + positive = terms.select(&:positive?) + negative = terms.select(&:negative?).map(&:invert) + + if positive.length == 1 + "#{positive[0].to_s(allow_every: true)} requires #{negative.join(" or ")}" + else + "if #{positive.join(" and ")} then #{negative.join(" or ")}" + end + end + end + else + raise "unhandled cause: #{cause.inspect}" + end + end + + def inspect + "#<#{self.class} #{to_s}>" + end + + def pretty_print(q) + q.group 2, "#<#{self.class}", ">" do + q.breakable + q.text to_s + + q.breakable + q.text " caused by " + q.pp @cause + end + end + + private + + def cleanup_terms(terms) + terms.each do |term| + raise "#{term.inspect} must be a term" unless term.is_a?(Term) + end + + if terms.length != 1 && ConflictCause === cause + terms = terms.reject do |term| + term.positive? && Package.root?(term.package) + end + end + + # Optimized simple cases + return terms if terms.length <= 1 + return terms if terms.length == 2 && terms[0].package != terms[1].package + + terms.group_by(&:package).map do |package, common_terms| + common_terms.inject do |acc, term| + acc.intersect(term) + end + end + end + end +end diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub/package.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub/package.rb new file mode 100644 index 00000000000000..6baa908f60a2af --- /dev/null +++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub/package.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +module Gem::PubGrub + class Package + + attr_reader :name + + def initialize(name) + @name = name + end + + def inspect + "#<#{self.class} #{name.inspect}>" + end + + def <=>(other) + name <=> other.name + end + + ROOT = Package.new(:root) + ROOT_VERSION = 0 + + def self.root + ROOT + end + + def self.root_version + ROOT_VERSION + end + + def self.root?(package) + if package.respond_to?(:root?) + package.root? + else + package == root + end + end + + def to_s + name.to_s + end + end +end diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub/partial_solution.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub/partial_solution.rb new file mode 100644 index 00000000000000..f6a6ae6964f7fa --- /dev/null +++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub/partial_solution.rb @@ -0,0 +1,121 @@ +require_relative 'assignment' + +module Gem::PubGrub + class PartialSolution + attr_reader :assignments, :decisions + attr_reader :attempted_solutions + + def initialize + reset! + + @attempted_solutions = 1 + @backtracking = false + end + + def decision_level + @decisions.length + end + + def relation(term) + package = term.package + return :overlap if !@terms.key?(package) + + @relation_cache[package][term] ||= + @terms[package].relation(term) + end + + def satisfies?(term) + relation(term) == :subset + end + + def derive(term, cause) + add_assignment(Assignment.new(term, cause, decision_level, assignments.length)) + end + + def satisfier(term) + assignment = + @assignments_by[term.package].bsearch do |assignment_by| + @cumulative_assignments[assignment_by].satisfies?(term) + end + + assignment || raise("#{term} unsatisfied") + end + + # A list of unsatisfied terms + def unsatisfied + @required.keys.reject do |package| + @decisions.key?(package) + end.map do |package| + @terms[package] + end + end + + def decide(package, version) + @attempted_solutions += 1 if @backtracking + @backtracking = false; + + decisions[package] = version + assignment = Assignment.decision(package, version, decision_level, assignments.length) + add_assignment(assignment) + end + + def backtrack(previous_level) + @backtracking = true + + new_assignments = assignments.select do |assignment| + assignment.decision_level <= previous_level + end + + new_decisions = Hash[decisions.first(previous_level)] + + reset! + + @decisions = new_decisions + + new_assignments.each do |assignment| + add_assignment(assignment) + end + end + + private + + def reset! + # { Array } + @assignments = [] + + # { Package => Array } + @assignments_by = Hash.new { |h,k| h[k] = [] } + @cumulative_assignments = {}.compare_by_identity + + # { Package => Package::Version } + @decisions = {} + + # { Package => Term } + @terms = {} + @relation_cache = Hash.new { |h,k| h[k] = {} } + + # { Package => Boolean } + @required = {} + end + + def add_assignment(assignment) + term = assignment.term + package = term.package + + @assignments << assignment + @assignments_by[package] << assignment + + @required[package] = true if term.positive? + + if @terms.key?(package) + old_term = @terms[package] + @terms[package] = old_term.intersect(term) + else + @terms[package] = term + end + @relation_cache[package].clear + + @cumulative_assignments[assignment] = @terms[package] + end + end +end diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub/rubygems.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub/rubygems.rb new file mode 100644 index 00000000000000..60ca3ca2eacbcd --- /dev/null +++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub/rubygems.rb @@ -0,0 +1,45 @@ +module Gem::PubGrub + module RubyGems + extend self + + def requirement_to_range(requirement) + ranges = requirement.requirements.map do |(op, ver)| + case op + when "~>" + name = "~> #{ver}" + bump = ver.class.new(ver.bump.to_s + ".A") + VersionRange.new(name: name, min: ver, max: bump, include_min: true) + when ">" + VersionRange.new(min: ver) + when ">=" + VersionRange.new(min: ver, include_min: true) + when "<" + VersionRange.new(max: ver) + when "<=" + VersionRange.new(max: ver, include_max: true) + when "=" + VersionRange.new(min: ver, max: ver, include_min: true, include_max: true) + when "!=" + VersionRange.new(min: ver, max: ver, include_min: true, include_max: true).invert + else + raise "bad version specifier: #{op}" + end + end + + ranges.inject(&:intersect) + end + + def requirement_to_constraint(package, requirement) + Gem::PubGrub::VersionConstraint.new(package, range: requirement_to_range(requirement)) + end + + def parse_range(dep) + requirement_to_range(Gem::Requirement.new(dep)) + end + + def parse_constraint(package, dep) + range = parse_range(dep) + Gem::PubGrub::VersionConstraint.new(package, range: range) + end + end +end diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub/solve_failure.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub/solve_failure.rb new file mode 100644 index 00000000000000..c4181d2b2551ad --- /dev/null +++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub/solve_failure.rb @@ -0,0 +1,19 @@ +require_relative 'failure_writer' + +module Gem::PubGrub + class SolveFailure < StandardError + attr_reader :incompatibility + + def initialize(incompatibility) + @incompatibility = incompatibility + end + + def to_s + "Could not find compatible versions\n\n#{explanation}" + end + + def explanation + @explanation ||= FailureWriter.new(@incompatibility).write + end + end +end diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub/static_package_source.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub/static_package_source.rb new file mode 100644 index 00000000000000..9e1de7d7a1d11b --- /dev/null +++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub/static_package_source.rb @@ -0,0 +1,61 @@ +require_relative 'package' +require_relative 'rubygems' +require_relative 'version_constraint' +require_relative 'incompatibility' +require_relative 'basic_package_source' + +module Gem::PubGrub + class StaticPackageSource < BasicPackageSource + class DSL + def initialize(packages, root_deps) + @packages = packages + @root_deps = root_deps + end + + def root(deps:) + @root_deps.update(deps) + end + + def add(name, version, deps: {}) + version = Gem::Version.new(version) + @packages[name] ||= {} + raise ArgumentError, "#{name} #{version} declared twice" if @packages[name].key?(version) + @packages[name][version] = clean_deps(name, version, deps) + end + + private + + # Exclude redundant self-referencing dependencies + def clean_deps(name, version, deps) + deps.reject {|dep_name, req| name == dep_name && Gem::PubGrub::RubyGems.parse_range(req).include?(version) } + end + end + + def initialize + @root_deps = {} + @packages = {} + + yield DSL.new(@packages, @root_deps) + + super() + end + + def all_versions_for(package) + @packages[package].keys + end + + def root_dependencies + @root_deps + end + + def dependencies_for(package, version) + @packages[package][version] + end + + def parse_dependency(package, dependency) + return false unless @packages.key?(package) + + Gem::PubGrub::RubyGems.parse_constraint(package, dependency) + end + end +end diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub/strategy.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub/strategy.rb new file mode 100644 index 00000000000000..b9874cdece5f47 --- /dev/null +++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub/strategy.rb @@ -0,0 +1,42 @@ +module Gem::PubGrub + class Strategy + def initialize(source) + @source = source + + @root_package = Package.root + @root_version = Package.root_version + + @version_indexes = Hash.new do |h,k| + if k == @root_package + h[k] = { @root_version => 0 } + else + h[k] = @source.all_versions_for(k).each.with_index.to_h + end + end + end + + def next_package_and_version(unsatisfied) + package, range = next_term_to_try_from(unsatisfied) + + [package, most_preferred_version_of(package, range)] + end + + private + + def most_preferred_version_of(package, range) + versions = @source.versions_for(package, range) + + indexes = @version_indexes[package] + versions.min_by { |version| indexes[version] || Float::INFINITY } + end + + def next_term_to_try_from(unsatisfied) + unsatisfied.min_by do |package, range| + matching_versions = @source.versions_for(package, range) + higher_versions = @source.versions_for(package, range.upper_invert) + + [matching_versions.count <= 1 ? 0 : 1, higher_versions.count] + end + end + end +end diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub/term.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub/term.rb new file mode 100644 index 00000000000000..bb26bdc911782a --- /dev/null +++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub/term.rb @@ -0,0 +1,105 @@ +module Gem::PubGrub + class Term + attr_reader :package, :constraint, :positive + + def initialize(constraint, positive) + @constraint = constraint + @package = @constraint.package + @positive = positive + end + + def to_s(allow_every: false) + if positive + @constraint.to_s(allow_every: allow_every) + else + "not #{@constraint}" + end + end + + def hash + constraint.hash ^ positive.hash + end + + def eql?(other) + positive == other.positive && + constraint.eql?(other.constraint) + end + + def invert + self.class.new(@constraint, !@positive) + end + alias_method :inverse, :invert + + def intersect(other) + raise ArgumentError, "packages must match" if package != other.package + + if positive? && other.positive? + self.class.new(constraint.intersect(other.constraint), true) + elsif negative? && other.negative? + self.class.new(constraint.union(other.constraint), false) + else + positive = positive? ? self : other + negative = negative? ? self : other + self.class.new(positive.constraint.intersect(negative.constraint.invert), true) + end + end + + def difference(other) + intersect(other.invert) + end + + def relation(other) + if positive? && other.positive? + constraint.relation(other.constraint) + elsif negative? && other.positive? + if constraint.allows_all?(other.constraint) + :disjoint + else + :overlap + end + elsif positive? && other.negative? + if !other.constraint.allows_any?(constraint) + :subset + elsif other.constraint.allows_all?(constraint) + :disjoint + else + :overlap + end + elsif negative? && other.negative? + if constraint.allows_all?(other.constraint) + :subset + else + :overlap + end + else + raise + end + end + + def normalized_constraint + @normalized_constraint ||= positive ? constraint : constraint.invert + end + + def satisfies?(other) + raise ArgumentError, "packages must match" unless package == other.package + + relation(other) == :subset + end + + def positive? + @positive + end + + def negative? + !positive? + end + + def empty? + @empty ||= normalized_constraint.empty? + end + + def inspect + "#<#{self.class} #{self}>" + end + end +end diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub/version.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub/version.rb new file mode 100644 index 00000000000000..5701bf0656f840 --- /dev/null +++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub/version.rb @@ -0,0 +1,3 @@ +module Gem::PubGrub + VERSION = "0.5.0" +end diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub/version_constraint.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub/version_constraint.rb new file mode 100644 index 00000000000000..ee998b32711019 --- /dev/null +++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub/version_constraint.rb @@ -0,0 +1,129 @@ +require_relative 'version_range' + +module Gem::PubGrub + class VersionConstraint + attr_reader :package, :range + + # @param package [Gem::PubGrub::Package] + # @param range [Gem::PubGrub::VersionRange] + def initialize(package, range: nil) + @package = package + @range = range + end + + def hash + package.hash ^ range.hash + end + + def ==(other) + package == other.package && + range == other.range + end + + def eql?(other) + package.eql?(other.package) && + range.eql?(other.range) + end + + class << self + def exact(package, version) + range = VersionRange.new(min: version, max: version, include_min: true, include_max: true) + new(package, range: range) + end + + def any(package) + new(package, range: VersionRange.any) + end + + def empty(package) + new(package, range: VersionRange.empty) + end + end + + def intersect(other) + unless package == other.package + raise ArgumentError, "Can only intersect between VersionConstraint of the same package" + end + + self.class.new(package, range: range.intersect(other.range)) + end + + def union(other) + unless package == other.package + raise ArgumentError, "Can only intersect between VersionConstraint of the same package" + end + + self.class.new(package, range: range.union(other.range)) + end + + def invert + new_range = range.invert + self.class.new(package, range: new_range) + end + + def difference(other) + intersect(other.invert) + end + + def allows_all?(other) + range.allows_all?(other.range) + end + + def allows_any?(other) + range.intersects?(other.range) + end + + def subset?(other) + other.allows_all?(self) + end + + def overlap?(other) + other.allows_any?(self) + end + + def disjoint?(other) + !overlap?(other) + end + + def relation(other) + if subset?(other) + :subset + elsif overlap?(other) + :overlap + else + :disjoint + end + end + + def to_s(allow_every: false) + if Package.root?(package) + package.to_s + elsif allow_every && any? + "every version of #{package}" + else + "#{package} #{constraint_string}" + end + end + + def constraint_string + if any? + ">= 0" + else + range.to_s + end + end + + def empty? + range.empty? + end + + # Does this match every version of the package + def any? + range.any? + end + + def inspect + "#<#{self.class} #{self}>" + end + end +end diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub/version_range.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub/version_range.rb new file mode 100644 index 00000000000000..fa0e2d5742b07e --- /dev/null +++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub/version_range.rb @@ -0,0 +1,423 @@ +# frozen_string_literal: true + +module Gem::PubGrub + class VersionRange + attr_reader :min, :max, :include_min, :include_max + + alias_method :include_min?, :include_min + alias_method :include_max?, :include_max + + class Empty < VersionRange + undef_method :min, :max + undef_method :include_min, :include_min? + undef_method :include_max, :include_max? + + def initialize + end + + def empty? + true + end + + def eql?(other) + other.empty? + end + + def hash + [].hash + end + + def intersects?(_) + false + end + + def intersect(other) + self + end + + def allows_all?(other) + other.empty? + end + + def include?(_) + false + end + + def any? + false + end + + def to_s + "(no versions)" + end + + def ==(other) + other.class == self.class + end + + def invert + VersionRange.any + end + + def select_versions(_) + [] + end + end + + EMPTY = Empty.new + Empty.singleton_class.undef_method(:new) + + def self.empty + EMPTY + end + + def self.any + new + end + + def initialize(min: nil, max: nil, include_min: false, include_max: false, name: nil) + raise ArgumentError, "Ranges without a lower bound cannot have include_min == true" if !min && include_min == true + raise ArgumentError, "Ranges without an upper bound cannot have include_max == true" if !max && include_max == true + + @min = min + @max = max + @include_min = include_min + @include_max = include_max + @name = name + end + + def hash + @hash ||= min.hash ^ max.hash ^ include_min.hash ^ include_max.hash + end + + def eql?(other) + if other.is_a?(VersionRange) + !other.empty? && + min.eql?(other.min) && + max.eql?(other.max) && + include_min.eql?(other.include_min) && + include_max.eql?(other.include_max) + else + ranges.eql?(other.ranges) + end + end + + def ranges + [self] + end + + def include?(version) + compare_version(version) == 0 + end + + # Partitions passed versions into [lower, within, higher] + # + # versions must be sorted + def partition_versions(versions) + min_index = + if !min || versions.empty? + 0 + elsif include_min? + (0..versions.size).bsearch { |i| versions[i].nil? || versions[i] >= min } + else + (0..versions.size).bsearch { |i| versions[i].nil? || versions[i] > min } + end + + lower = versions.slice(0, min_index) + versions = versions.slice(min_index, versions.size) + + max_index = + if !max || versions.empty? + versions.size + elsif include_max? + (0..versions.size).bsearch { |i| versions[i].nil? || versions[i] > max } + else + (0..versions.size).bsearch { |i| versions[i].nil? || versions[i] >= max } + end + + [ + lower, + versions.slice(0, max_index), + versions.slice(max_index, versions.size) + ] + end + + # Returns versions which are included by this range. + # + # versions must be sorted + def select_versions(versions) + return versions if any? + + partition_versions(versions)[1] + end + + def compare_version(version) + if min + case version <=> min + when -1 + return -1 + when 0 + return -1 if !include_min + when 1 + end + end + + if max + case version <=> max + when -1 + when 0 + return 1 if !include_max + when 1 + return 1 + end + end + + 0 + end + + def strictly_lower?(other) + return false if !max || !other.min + + case max <=> other.min + when 0 + !include_max || !other.include_min + when -1 + true + when 1 + false + end + end + + def strictly_higher?(other) + other.strictly_lower?(self) + end + + def intersects?(other) + return false if other.empty? + return other.intersects?(self) if other.is_a?(VersionUnion) + !strictly_lower?(other) && !strictly_higher?(other) + end + alias_method :allows_any?, :intersects? + + def intersect(other) + return other if other.empty? + return other.intersect(self) if other.is_a?(VersionUnion) + + min_range = + if !min + other + elsif !other.min + self + else + case min <=> other.min + when 0 + include_min ? other : self + when -1 + other + when 1 + self + end + end + + max_range = + if !max + other + elsif !other.max + self + else + case max <=> other.max + when 0 + include_max ? other : self + when -1 + self + when 1 + other + end + end + + if !min_range.equal?(max_range) && min_range.min && max_range.max + case min_range.min <=> max_range.max + when -1 + when 0 + if !min_range.include_min || !max_range.include_max + return EMPTY + end + when 1 + return EMPTY + end + end + + VersionRange.new( + min: min_range.min, + include_min: min_range.include_min, + max: max_range.max, + include_max: max_range.include_max + ) + end + + # The span covered by two ranges + # + # If self and other are contiguous, this builds a union of the two ranges. + # (if they aren't you are probably calling the wrong method) + def span(other) + return self if other.empty? + + min_range = + if !min + self + elsif !other.min + other + else + case min <=> other.min + when 0 + include_min ? self : other + when -1 + self + when 1 + other + end + end + + max_range = + if !max + self + elsif !other.max + other + else + case max <=> other.max + when 0 + include_max ? self : other + when -1 + other + when 1 + self + end + end + + VersionRange.new( + min: min_range.min, + include_min: min_range.include_min, + max: max_range.max, + include_max: max_range.include_max + ) + end + + def union(other) + return other.union(self) if other.is_a?(VersionUnion) + + if contiguous_to?(other) + span(other) + else + VersionUnion.union([self, other]) + end + end + + def contiguous_to?(other) + return false if other.empty? + return true if any? + + intersects?(other) || contiguous_below?(other) || contiguous_above?(other) + end + + def contiguous_below?(other) + return false if !max || !other.min + + max == other.min && (include_max || other.include_min) + end + + def contiguous_above?(other) + other.contiguous_below?(self) + end + + def allows_all?(other) + return true if other.empty? + + if other.is_a?(VersionUnion) + return VersionUnion.new([self]).allows_all?(other) + end + + return false if max && !other.max + return false if min && !other.min + + if min + case min <=> other.min + when -1 + when 0 + return false if !include_min && other.include_min + when 1 + return false + end + end + + if max + case max <=> other.max + when -1 + return false + when 0 + return false if !include_max && other.include_max + when 1 + end + end + + true + end + + def any? + !min && !max + end + + def empty? + false + end + + def to_s + @name ||= constraints.join(", ") + end + + def inspect + "#<#{self.class} #{to_s}>" + end + + def upper_invert + return self.class.empty unless max + + VersionRange.new(min: max, include_min: !include_max) + end + + def invert + return self.class.empty if any? + + low = -> { VersionRange.new(max: min, include_max: !include_min) } + high = -> { VersionRange.new(min: max, include_min: !include_max) } + + if !min + high.call + elsif !max + low.call + else + low.call.union(high.call) + end + end + + def ==(other) + self.class == other.class && + min == other.min && + max == other.max && + include_min == other.include_min && + include_max == other.include_max + end + + private + + def constraints + return ["any"] if any? + return ["= #{min}"] if min.to_s == max.to_s + + c = [] + c << "#{include_min ? ">=" : ">"} #{min}" if min + c << "#{include_max ? "<=" : "<"} #{max}" if max + c + end + + end +end diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub/version_solver.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub/version_solver.rb new file mode 100644 index 00000000000000..3341d8fe3b0076 --- /dev/null +++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub/version_solver.rb @@ -0,0 +1,236 @@ +require_relative 'partial_solution' +require_relative 'term' +require_relative 'incompatibility' +require_relative 'solve_failure' +require_relative 'strategy' + +module Gem::PubGrub + class VersionSolver + attr_reader :logger + attr_reader :source + attr_reader :solution + attr_reader :strategy + + def initialize(source:, root: Package.root, strategy: Strategy.new(source), logger: Gem::PubGrub.logger) + @logger = logger + + @source = source + @strategy = strategy + + # { package => [incompatibility, ...]} + @incompatibilities = Hash.new do |h, k| + h[k] = [] + end + + @seen_incompatibilities = {} + + @solution = PartialSolution.new + + add_incompatibility Incompatibility.new([ + Term.new(VersionConstraint.any(root), false) + ], cause: :root) + + propagate(root) + end + + def solved? + solution.unsatisfied.empty? + end + + # Returns true if there is more work to be done, false otherwise + def work + unsatisfied_terms = solution.unsatisfied + if unsatisfied_terms.empty? + logger.info { "Solution found after #{solution.attempted_solutions} attempts:" } + solution.decisions.each do |package, version| + next if Package.root?(package) + logger.info { "* #{package} #{version}" } + end + + return false + end + + next_package = choose_package_version_from(unsatisfied_terms) + propagate(next_package) + + true + end + + def solve + while work; end + + solution.decisions + end + + alias_method :result, :solve + + private + + def propagate(initial_package) + changed = [initial_package] + while package = changed.shift + @incompatibilities[package].reverse_each do |incompatibility| + result = propagate_incompatibility(incompatibility) + if result == :conflict + root_cause = resolve_conflict(incompatibility) + changed.clear + changed << propagate_incompatibility(root_cause) + elsif result # should be a Package + changed << result + end + end + changed.uniq! + end + end + + def propagate_incompatibility(incompatibility) + unsatisfied = nil + incompatibility.terms.each do |term| + relation = solution.relation(term) + if relation == :disjoint + return nil + elsif relation == :overlap + # If more than one term is inconclusive, we can't deduce anything + return nil if unsatisfied + unsatisfied = term + end + end + + if !unsatisfied + return :conflict + end + + logger.debug { "derived: #{unsatisfied.invert}" } + + solution.derive(unsatisfied.invert, incompatibility) + + unsatisfied.package + end + + def choose_package_version_from(unsatisfied_terms) + remaining = unsatisfied_terms.map { |t| [t.package, t.constraint.range] }.to_h + + package, version = strategy.next_package_and_version(remaining) + + logger.debug { "attempting #{package} #{version}" } + + if version.nil? + unsatisfied_term = unsatisfied_terms.find { |t| t.package == package } + add_incompatibility source.no_versions_incompatibility_for(package, unsatisfied_term) + return package + end + + conflict = false + + source.incompatibilities_for(package, version).each do |incompatibility| + if @seen_incompatibilities.include?(incompatibility) + logger.debug { "knew: #{incompatibility}" } + next + end + @seen_incompatibilities[incompatibility] = true + + add_incompatibility incompatibility + + conflict ||= incompatibility.terms.all? do |term| + term.package == package || solution.satisfies?(term) + end + end + + unless conflict + logger.info { "selected #{package} #{version}" } + + solution.decide(package, version) + else + logger.info { "conflict: #{conflict.inspect}" } + end + + package + end + + def resolve_conflict(incompatibility) + logger.info { "conflict: #{incompatibility}" } + + new_incompatibility = nil + + while !incompatibility.failure? + most_recent_term = nil + most_recent_satisfier = nil + difference = nil + + previous_level = 1 + + incompatibility.terms.each do |term| + satisfier = solution.satisfier(term) + + if most_recent_satisfier.nil? + most_recent_term = term + most_recent_satisfier = satisfier + elsif most_recent_satisfier.index < satisfier.index + previous_level = [previous_level, most_recent_satisfier.decision_level].max + most_recent_term = term + most_recent_satisfier = satisfier + difference = nil + else + previous_level = [previous_level, satisfier.decision_level].max + end + + if most_recent_term == term + difference = most_recent_satisfier.term.difference(most_recent_term) + if difference.empty? + difference = nil + else + difference_satisfier = solution.satisfier(difference.inverse) + previous_level = [previous_level, difference_satisfier.decision_level].max + end + end + end + + if previous_level < most_recent_satisfier.decision_level || + most_recent_satisfier.decision? + + logger.info { "backtracking to #{previous_level}" } + solution.backtrack(previous_level) + + if new_incompatibility + add_incompatibility(new_incompatibility) + end + + return incompatibility + end + + new_terms = [] + new_terms += incompatibility.terms - [most_recent_term] + new_terms += most_recent_satisfier.cause.terms.reject { |term| + term.package == most_recent_satisfier.term.package + } + if difference + new_terms << difference.invert + end + + new_incompatibility = Incompatibility.new(new_terms, cause: Incompatibility::ConflictCause.new(incompatibility, most_recent_satisfier.cause)) + + if incompatibility.to_s == new_incompatibility.to_s + logger.info { "!! failed to resolve conflicts, this shouldn't have happened" } + break + end + + incompatibility = new_incompatibility + + partially = difference ? " partially" : "" + logger.info { "! #{most_recent_term} is#{partially} satisfied by #{most_recent_satisfier.term}" } + logger.info { "! which is caused by #{most_recent_satisfier.cause}" } + logger.info { "! thus #{incompatibility}" } + end + + raise SolveFailure.new(incompatibility) + end + + def add_incompatibility(incompatibility) + logger.debug { "fact: #{incompatibility}" } + incompatibility.terms.each do |term| + package = term.package + @incompatibilities[package] << incompatibility + end + end + end +end diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub/version_union.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub/version_union.rb new file mode 100644 index 00000000000000..4166318a98930a --- /dev/null +++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub/version_union.rb @@ -0,0 +1,178 @@ +# frozen_string_literal: true + +module Gem::PubGrub + class VersionUnion + attr_reader :ranges + + def self.normalize_ranges(ranges) + ranges = ranges.flat_map do |range| + range.ranges + end + + ranges.reject!(&:empty?) + + return [] if ranges.empty? + + mins, ranges = ranges.partition { |r| !r.min } + original_ranges = mins + ranges.sort_by { |r| [r.min, r.include_min ? 0 : 1] } + ranges = [original_ranges.shift] + original_ranges.each do |range| + if ranges.last.contiguous_to?(range) + ranges << ranges.pop.span(range) + else + ranges << range + end + end + + ranges + end + + def self.union(ranges, normalize: true) + ranges = normalize_ranges(ranges) if normalize + + if ranges.size == 0 + VersionRange.empty + elsif ranges.size == 1 + ranges[0] + else + new(ranges) + end + end + + def initialize(ranges) + raise ArgumentError unless ranges.all? { |r| r.instance_of?(VersionRange) } + @ranges = ranges + end + + def hash + ranges.hash + end + + def eql?(other) + ranges.eql?(other.ranges) + end + + def include?(version) + !!ranges.bsearch {|r| r.compare_version(version) } + end + + def select_versions(all_versions) + versions = [] + ranges.inject(all_versions) do |acc, range| + _, matching, higher = range.partition_versions(acc) + versions.concat matching + higher + end + versions + end + + def intersects?(other) + my_ranges = ranges.dup + other_ranges = other.ranges.dup + + my_range = my_ranges.shift + other_range = other_ranges.shift + while my_range && other_range + if my_range.intersects?(other_range) + return true + end + + if !my_range.max || other_range.empty? || (other_range.max && other_range.max < my_range.max) + other_range = other_ranges.shift + else + my_range = my_ranges.shift + end + end + end + alias_method :allows_any?, :intersects? + + def allows_all?(other) + my_ranges = ranges.dup + + my_range = my_ranges.shift + + other.ranges.all? do |other_range| + while my_range + break if my_range.allows_all?(other_range) + my_range = my_ranges.shift + end + + !!my_range + end + end + + def empty? + false + end + + def any? + false + end + + def intersect(other) + my_ranges = ranges.dup + other_ranges = other.ranges.dup + new_ranges = [] + + my_range = my_ranges.shift + other_range = other_ranges.shift + while my_range && other_range + new_ranges << my_range.intersect(other_range) + + if !my_range.max || other_range.empty? || (other_range.max && other_range.max < my_range.max) + other_range = other_ranges.shift + else + my_range = my_ranges.shift + end + end + new_ranges.reject!(&:empty?) + VersionUnion.union(new_ranges, normalize: false) + end + + def upper_invert + ranges.last.upper_invert + end + + def invert + ranges.map(&:invert).inject(:intersect) + end + + def union(other) + VersionUnion.union([self, other]) + end + + def to_s + output = [] + + ranges = self.ranges.dup + while !ranges.empty? + ne = [] + range = ranges.shift + while !ranges.empty? && ranges[0].min.to_s == range.max.to_s + ne << range.max + range = range.span(ranges.shift) + end + + ne.map! {|x| "!= #{x}" } + if ne.empty? + output << range.to_s + elsif range.any? + output << ne.join(', ') + else + output << "#{range}, #{ne.join(', ')}" + end + end + + output.join(" OR ") + end + + def inspect + "#<#{self.class} #{to_s}>" + end + + def ==(other) + self.class == other.class && + self.ranges == other.ranges + end + end +end diff --git a/lib/rubygems/vendored_molinillo.rb b/lib/rubygems/vendored_molinillo.rb deleted file mode 100644 index 45906c0e5c71b4..00000000000000 --- a/lib/rubygems/vendored_molinillo.rb +++ /dev/null @@ -1,3 +0,0 @@ -# frozen_string_literal: true - -require_relative "vendor/molinillo/lib/molinillo" diff --git a/lib/rubygems/vendored_pub_grub.rb b/lib/rubygems/vendored_pub_grub.rb new file mode 100644 index 00000000000000..844d243ab320fb --- /dev/null +++ b/lib/rubygems/vendored_pub_grub.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +require_relative "vendor/pub_grub/lib/pub_grub" diff --git a/test/rubygems/test_gem_commands_install_command.rb b/test/rubygems/test_gem_commands_install_command.rb index cc58d7d105df85..d75ba349f96ada 100644 --- a/test/rubygems/test_gem_commands_install_command.rb +++ b/test/rubygems/test_gem_commands_install_command.rb @@ -119,11 +119,7 @@ def test_execute_local_dependency_nonexistent end end - expected = <<-EXPECTED -ERROR: Could not find a valid gem 'bar' (= 0.5) (required by 'foo' (>= 0)) in any repository - EXPECTED - - assert_equal expected, @ui.error + assert_match(/ERROR:.*foo.*bar/m, @ui.error) end def test_execute_local_dependency_nonexistent_ignore_dependencies @@ -303,11 +299,7 @@ def test_execute_dependency_nonexistent assert_equal 2, e.exit_code end - expected = <<-EXPECTED -ERROR: Could not find a valid gem 'bar' (= 0.5) (required by 'foo' (>= 0)) in any repository - EXPECTED - - assert_equal expected, @ui.error + assert_match(/ERROR:.*foo.*bar/m, @ui.error) end def test_execute_http_proxy diff --git a/test/rubygems/test_gem_dependency_installer.rb b/test/rubygems/test_gem_dependency_installer.rb index 8d9caf7d90b9a4..bb52df4271fee6 100644 --- a/test/rubygems/test_gem_dependency_installer.rb +++ b/test/rubygems/test_gem_dependency_installer.rb @@ -793,13 +793,13 @@ def test_install_domain_local inst = nil Dir.chdir @tempdir do - e = assert_raise Gem::UnsatisfiableDependencyError do + e = assert_raise Gem::DependencyResolutionError do inst = Gem::DependencyInstaller.new domain: :local inst.install "b" end - expected = "Unable to resolve dependency: 'b (>= 0)' requires 'a (>= 0)'" - assert_equal expected, e.message + assert_match(/depends on a/, e.message) + assert_match(/no versions satisfy a/, e.message) end assert_equal [], inst.installed_gems.map(&:full_name) diff --git a/test/rubygems/test_gem_resolver.rb b/test/rubygems/test_gem_resolver.rb index 4990d5d2dd1339..b9b005f70ea7e6 100644 --- a/test/rubygems/test_gem_resolver.rb +++ b/test/rubygems/test_gem_resolver.rb @@ -86,63 +86,6 @@ def test_self_compose_sets_single assert_same index_set, composed end - def test_requests - a1 = util_spec "a", 1, "b" => 2 - - r1 = Gem::Resolver::DependencyRequest.new dep("a", "= 1"), nil - - act = Gem::Resolver::ActivationRequest.new a1, r1 - - res = Gem::Resolver.new [a1] - - reqs = [] - - res.requests a1, act, reqs - - assert_equal ["b (= 2)"], reqs.map(&:to_s) - end - - def test_requests_development - a1 = util_spec "a", 1, "b" => 2 - - spec = Gem::Resolver::SpecSpecification.new nil, a1 - def spec.fetch_development_dependencies - @called = true - end - - r1 = Gem::Resolver::DependencyRequest.new dep("a", "= 1"), nil - - act = Gem::Resolver::ActivationRequest.new spec, r1 - - res = Gem::Resolver.new [act] - res.development = true - - reqs = [] - - res.requests spec, act, reqs - - assert_equal ["b (= 2)"], reqs.map(&:to_s) - - assert spec.instance_variable_defined? :@called - end - - def test_requests_ignore_dependencies - a1 = util_spec "a", 1, "b" => 2 - - r1 = Gem::Resolver::DependencyRequest.new dep("a", "= 1"), nil - - act = Gem::Resolver::ActivationRequest.new a1, r1 - - res = Gem::Resolver.new [a1] - res.ignore_dependencies = true - - reqs = [] - - res.requests a1, act, reqs - - assert_empty reqs - end - def test_resolve_conservative a1_spec = util_spec "a", 1 @@ -511,19 +454,9 @@ def test_raises_dependency_error r.resolve end - deps = [make_dep("c", "= 2"), make_dep("c", "= 1")] - assert_equal deps, e.conflicting_dependencies - - con = e.conflict - - act = con.activated - assert_equal "c-1", act.spec.full_name - - parent = act.parent - assert_equal "a-1", parent.spec.full_name - - act = con.requester - assert_equal "b-1", act.spec.full_name + assert_kind_of Gem::Resolver::PubGrubFailure, e.conflict + assert_match(/a depends on c/, e.message) + assert_match(/b depends on c/, e.message) end def test_raises_when_a_gem_is_missing @@ -578,12 +511,12 @@ def test_raises_and_reports_an_implicit_request_properly r = Gem::Resolver.new([ad], set(a1)) - e = assert_raise Gem::UnsatisfiableDependencyError do + e = assert_raise Gem::DependencyResolutionError do r.resolve end - assert_equal "Unable to resolve dependency: 'a (= 1)' requires 'b (= 2)'", - e.message + assert_match(/depends on b/, e.message) + assert_match(/no versions satisfy b/, e.message) end def test_raises_when_possibles_are_exhausted @@ -605,18 +538,9 @@ def test_raises_when_possibles_are_exhausted r.resolve end - dependency = e.conflict.dependency - - assert_includes %w[a b], dependency.name - assert_equal req(">= 0"), dependency.requirement - - activated = e.conflict.activated - assert_equal "c-1", activated.full_name - - assert_equal dep("c", "= 1"), activated.request.dependency - - assert_equal [dep("c", ">= 2"), dep("c", "= 1")], - e.conflict.conflicting_dependencies + assert_kind_of Gem::Resolver::PubGrubFailure, e.conflict + assert_match(/a depends on c/, e.message) + assert_match(/b depends on c/, e.message) end def test_keeps_resolving_after_seeing_satisfied_dep @@ -772,7 +696,7 @@ def test_second_level_backout assert_resolves_to [b1, c1, d2], r end - def test_sorts_by_source_then_version + def test_picks_highest_version_across_sources source_a = Gem::Source.new "http://example.com/a" source_b = Gem::Source.new "http://example.com/b" source_c = Gem::Source.new "http://example.com/c" @@ -795,7 +719,7 @@ def test_sorts_by_source_then_version resolver = Gem::Resolver.new [dependency], set - assert_resolves_to [spec_b_2], resolver + assert_resolves_to [spec_a_2], resolver end def test_select_local_platforms From 456a2dbdd3ed58f7d16b06c945ce382dbf7aec2c Mon Sep 17 00:00:00 2001 From: Matt Larraz Date: Mon, 16 Mar 2026 19:26:47 -0400 Subject: [PATCH 061/188] [ruby/rubygems] Fix lockfile requirement preservation and orphaned deps test Preserve the original dependency requirement from root deps when building ActivationRequests, so lockfiles correctly record constraints like "a (>= 1)" instead of bare "a". Update the orphaned dependencies test: PubGrub correctly backtracks from b-2 (missing c-2) to b-1 (has c-1), finding a valid solution that Molinillo's simpler backtracking missed. https://github.com/ruby/rubygems/commit/ff04cd9d90 Co-Authored-By: Claude Opus 4.6 --- lib/rubygems/resolver.rb | 4 +++- test/rubygems/test_gem.rb | 10 +++------- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/lib/rubygems/resolver.rb b/lib/rubygems/resolver.rb index 5578135a83e81f..e0643dd249e6fd 100644 --- a/lib/rubygems/resolver.rb +++ b/lib/rubygems/resolver.rb @@ -171,10 +171,12 @@ def resolve result = solver.solve # Convert to Array + needed_by_name = @needed.group_by(&:name) result.filter_map do |package, version| next if Gem::PubGrub::Package.root?(package) spec = spec_for(package.to_s, version) - dep_request = DependencyRequest.new(Gem::Dependency.new(package.to_s), nil) + dep = needed_by_name[package.to_s]&.first || Gem::Dependency.new(package.to_s) + dep_request = DependencyRequest.new(dep, nil) ActivationRequest.new(spec, dep_request) end rescue Gem::PubGrub::SolveFailure => e diff --git a/test/rubygems/test_gem.rb b/test/rubygems/test_gem.rb index b9a4cf1ce032ce..bce50a1d369bc2 100644 --- a/test/rubygems/test_gem.rb +++ b/test/rubygems/test_gem.rb @@ -331,13 +331,9 @@ def test_activate_bin_path_raises_a_meaningful_error_if_a_gem_thats_finally_acti install_specs c1, b1, b2, a1 - # c2 is missing, and b2 which has it as a dependency will be activated, so we should get an error about the orphaned dependency - - e = assert_raise Gem::UnsatisfiableDependencyError do - load Gem.activate_bin_path("a", "exec", ">= 0") - end - - assert_equal "Unable to resolve dependency: 'b (>= 0)' requires 'c (= 2)'", e.message + # c2 is missing, but the resolver backtracks from b2 to b1 which + # works with c1, finding a valid solution despite partial installation + load Gem.activate_bin_path("a", "exec", ">= 0") end def test_activate_bin_path_in_debug_mode From 600240d6852416021ad6d435837316b18f1902ba Mon Sep 17 00:00:00 2001 From: Colby Swandale Date: Sun, 12 Apr 2026 12:15:08 +1000 Subject: [PATCH 062/188] [ruby/rubygems] Extend catching Gem::DependencyResolutionError error to exec command https://github.com/ruby/rubygems/commit/486bbca187 --- lib/rubygems/commands/exec_command.rb | 3 ++ .../test_gem_commands_exec_command.rb | 29 +++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/lib/rubygems/commands/exec_command.rb b/lib/rubygems/commands/exec_command.rb index c24ebbf711c2d7..1feafbdd358316 100644 --- a/lib/rubygems/commands/exec_command.rb +++ b/lib/rubygems/commands/exec_command.rb @@ -173,6 +173,9 @@ def install rescue Gem::InstallError => e alert_error "Error installing #{gem_name}:\n\t#{e.message}" terminate_interaction 1 + rescue Gem::DependencyResolutionError => e + alert_error "Error installing #{gem_name}:\n\t#{e.message}" + terminate_interaction 2 rescue Gem::GemNotFoundException => e show_lookup_failure e.name, e.version, e.errors, false diff --git a/test/rubygems/test_gem_commands_exec_command.rb b/test/rubygems/test_gem_commands_exec_command.rb index db738b5e9f58e0..b949cd34a67b3a 100644 --- a/test/rubygems/test_gem_commands_exec_command.rb +++ b/test/rubygems/test_gem_commands_exec_command.rb @@ -856,4 +856,33 @@ def test_newer_prerelease_available assert_equal %w[a-1.1.a], @installed_specs.map(&:full_name) end end + + def test_install_dependency_resolution_error + spec_fetcher do |fetcher| + fetcher.gem "a", 2 do |s| + s.executables = %w[a] + s.add_dependency "b", "~> 1.0" + s.add_dependency "c", "~> 1.0" + end + fetcher.gem "b", 1 do |s| + s.add_dependency "d", "= 1.0" + end + fetcher.gem "c", 1 do |s| + s.add_dependency "d", "= 2.0" + end + fetcher.gem "d", 1 + fetcher.gem "d", 2 + end + + util_clear_gems + + use_ui @ui do + e = assert_raise Gem::MockGemUi::TermError do + @cmd.invoke "a:2" + end + assert_equal 2, e.exit_code + end + + assert_match(/ERROR:.*Error installing a:/, @ui.error) + end end From 14298299455ba3f83f3a638808fc872a699c810e Mon Sep 17 00:00:00 2001 From: Colby Swandale Date: Sun, 12 Apr 2026 12:33:55 +1000 Subject: [PATCH 063/188] [ruby/rubygems] Simplify DependencyResolutionError by removing PubGrubFailure wrapper https://github.com/ruby/rubygems/commit/ca268e2042 --- lib/rubygems/exceptions.rb | 24 +++++++++---------- lib/rubygems/resolver.rb | 5 +--- lib/rubygems/resolver/pub_grub_failure.rb | 18 -------------- .../test_gem_dependency_resolution_error.rb | 12 ++++++++-- test/rubygems/test_gem_resolver.rb | 4 ++-- 5 files changed, 24 insertions(+), 39 deletions(-) delete mode 100644 lib/rubygems/resolver/pub_grub_failure.rb diff --git a/lib/rubygems/exceptions.rb b/lib/rubygems/exceptions.rb index 1bba014245a8d2..2c0c38617f045e 100644 --- a/lib/rubygems/exceptions.rb +++ b/lib/rubygems/exceptions.rb @@ -33,26 +33,24 @@ class Gem::DependencyError < Gem::Exception; end class Gem::DependencyRemovalException < Gem::Exception; end ## -# Raised by Gem::Resolver when a Gem::Dependency::Conflict reaches the -# toplevel. Indicates which dependencies were incompatible through #conflict -# and #conflicting_dependencies +# Raised by Gem::Resolver when dependency resolution fails. class Gem::DependencyResolutionError < Gem::DependencyError - attr_reader :conflict - def initialize(conflict) - @conflict = conflict + @explanation = conflict.explanation + super @explanation + end - if conflict.respond_to?(:solve_failure) - super conflict.explanation - else - a, b = conflicting_dependencies - super "conflicting dependencies #{a} and #{b}\n#{@conflict.explanation}" - end + def explanation + @explanation + end + + def conflict + nil end def conflicting_dependencies - @conflict.conflicting_dependencies + [] end end diff --git a/lib/rubygems/resolver.rb b/lib/rubygems/resolver.rb index e0643dd249e6fd..5e4b29579a682a 100644 --- a/lib/rubygems/resolver.rb +++ b/lib/rubygems/resolver.rb @@ -180,8 +180,7 @@ def resolve ActivationRequest.new(spec, dep_request) end rescue Gem::PubGrub::SolveFailure => e - failure = Gem::Resolver::PubGrubFailure.new(e) - raise Gem::DependencyResolutionError, failure + raise Gem::DependencyResolutionError, e end # PubGrub source interface methods @@ -392,8 +391,6 @@ def make_logger require_relative "resolver/conflict" require_relative "resolver/dependency_request" require_relative "resolver/requirement_list" -require_relative "resolver/pub_grub_failure" - require_relative "resolver/set" require_relative "resolver/api_set" require_relative "resolver/composed_set" diff --git a/lib/rubygems/resolver/pub_grub_failure.rb b/lib/rubygems/resolver/pub_grub_failure.rb deleted file mode 100644 index 3ace7ef51590a6..00000000000000 --- a/lib/rubygems/resolver/pub_grub_failure.rb +++ /dev/null @@ -1,18 +0,0 @@ -# frozen_string_literal: true - -class Gem::Resolver::PubGrubFailure - attr_reader :solve_failure - - def initialize(solve_failure) - @solve_failure = solve_failure - end - - def explanation - @solve_failure.explanation - end - - def conflicting_dependencies - terms = @solve_failure.incompatibility.terms - terms.map {|t| t.package.to_s } - end -end diff --git a/test/rubygems/test_gem_dependency_resolution_error.rb b/test/rubygems/test_gem_dependency_resolution_error.rb index 98a6b6b8fd0e08..e67ade5939ff73 100644 --- a/test/rubygems/test_gem_dependency_resolution_error.rb +++ b/test/rubygems/test_gem_dependency_resolution_error.rb @@ -19,7 +19,15 @@ def setup end def test_message - assert_match(/^conflicting dependencies a \(= 1\) and a \(= 2\)$/, - @error.message) + assert_match(/Activated a-2/, @error.message) + assert_match(/conflicting dependency/, @error.message) + end + + def test_conflict + assert_nil @error.conflict + end + + def test_conflicting_dependencies + assert_equal [], @error.conflicting_dependencies end end diff --git a/test/rubygems/test_gem_resolver.rb b/test/rubygems/test_gem_resolver.rb index b9b005f70ea7e6..8c032bf73a30c4 100644 --- a/test/rubygems/test_gem_resolver.rb +++ b/test/rubygems/test_gem_resolver.rb @@ -454,7 +454,7 @@ def test_raises_dependency_error r.resolve end - assert_kind_of Gem::Resolver::PubGrubFailure, e.conflict + assert_nil e.conflict assert_match(/a depends on c/, e.message) assert_match(/b depends on c/, e.message) end @@ -538,7 +538,7 @@ def test_raises_when_possibles_are_exhausted r.resolve end - assert_kind_of Gem::Resolver::PubGrubFailure, e.conflict + assert_nil e.conflict assert_match(/a depends on c/, e.message) assert_match(/b depends on c/, e.message) end From f07eabd6c7e133647ef9f0527db0d6325b0ac7e8 Mon Sep 17 00:00:00 2001 From: Colby Swandale Date: Sun, 12 Apr 2026 12:42:29 +1000 Subject: [PATCH 064/188] [ruby/rubygems] find_all_gems test coverage https://github.com/ruby/rubygems/commit/8cb62e54a6 --- test/rubygems/test_gem_source_local.rb | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/test/rubygems/test_gem_source_local.rb b/test/rubygems/test_gem_source_local.rb index e9d7f454820a3e..60621736296766 100644 --- a/test/rubygems/test_gem_source_local.rb +++ b/test/rubygems/test_gem_source_local.rb @@ -63,6 +63,30 @@ def test_find_gem_prerelease assert_equal "a-2.a", @sl.find_gem("a", req, true).full_name end + def test_find_all_gems + _, a2_gem = util_gem "a", "2" + FileUtils.mv a2_gem, @tempdir + + results = @sl.find_all_gems("a") + assert_equal ["a-1", "a-2"], results.map(&:full_name).sort + end + + def test_find_all_gems_excludes_prerelease_by_default + results = @sl.find_all_gems("a") + assert_equal ["a-1"], results.map(&:full_name) + end + + def test_find_all_gems_includes_prerelease_when_requested + results = @sl.find_all_gems("a", Gem::Requirement.create(">= 0"), true) + assert_equal ["a-1", "a-2.a"], results.map(&:full_name).sort + end + + def test_find_all_gems_includes_prerelease_when_requirement_is_prerelease + req = Gem::Requirement.create("= 2.a") + results = @sl.find_all_gems("a", req) + assert_equal ["a-2.a"], results.map(&:full_name) + end + def test_fetch_spec s = @sl.fetch_spec @a.name_tuple assert_equal s, @a From b1b53a2ff581db3b2ab828ef8f1fe61d7f611d8a Mon Sep 17 00:00:00 2001 From: Colby Swandale Date: Sun, 12 Apr 2026 12:51:33 +1000 Subject: [PATCH 065/188] [ruby/rubygems] Remove dead Molinillo-era error classes and simplify DependencyResolutionError https://github.com/ruby/rubygems/commit/6592033131 --- lib/rubygems/exceptions.rb | 34 ---- lib/rubygems/resolver.rb | 1 - lib/rubygems/resolver/conflict.rb | 146 ------------------ .../test_gem_dependency_resolution_error.rb | 19 +-- .../test_gem_impossible_dependencies_error.rb | 60 ------- test/rubygems/test_gem_resolver_conflict.rb | 80 ---------- 6 files changed, 7 insertions(+), 333 deletions(-) delete mode 100644 lib/rubygems/resolver/conflict.rb delete mode 100644 test/rubygems/test_gem_impossible_dependencies_error.rb delete mode 100644 test/rubygems/test_gem_resolver_conflict.rb diff --git a/lib/rubygems/exceptions.rb b/lib/rubygems/exceptions.rb index 2c0c38617f045e..e00a70c66249a3 100644 --- a/lib/rubygems/exceptions.rb +++ b/lib/rubygems/exceptions.rb @@ -128,40 +128,6 @@ def initialize(name, version, errors = nil) Gem.deprecate_constant :SpecificGemNotFoundException -## -# Raised by Gem::Resolver when dependencies conflict and create the -# inability to find a valid possible spec for a request. - -class Gem::ImpossibleDependenciesError < Gem::Exception - attr_reader :conflicts - attr_reader :request - - def initialize(request, conflicts) - @request = request - @conflicts = conflicts - - super build_message - end - - def build_message # :nodoc: - requester = @request.requester - requester = requester ? requester.spec.full_name : "The user" - dependency = @request.dependency - - message = "#{requester} requires #{dependency} but it conflicted:\n".dup - - @conflicts.each do |_, conflict| - message << conflict.explanation - end - - message - end - - def dependency - @request.dependency - end -end - class Gem::InstallError < Gem::Exception; end class Gem::RuntimeRequirementNotMetError < Gem::InstallError diff --git a/lib/rubygems/resolver.rb b/lib/rubygems/resolver.rb index 5e4b29579a682a..1f63a7b17b5e23 100644 --- a/lib/rubygems/resolver.rb +++ b/lib/rubygems/resolver.rb @@ -388,7 +388,6 @@ def make_logger end require_relative "resolver/activation_request" -require_relative "resolver/conflict" require_relative "resolver/dependency_request" require_relative "resolver/requirement_list" require_relative "resolver/set" diff --git a/lib/rubygems/resolver/conflict.rb b/lib/rubygems/resolver/conflict.rb deleted file mode 100644 index 77c3add4b32d21..00000000000000 --- a/lib/rubygems/resolver/conflict.rb +++ /dev/null @@ -1,146 +0,0 @@ -# frozen_string_literal: true - -## -# Used internally to indicate that a dependency conflicted -# with a spec that would be activated. - -class Gem::Resolver::Conflict - ## - # The specification that was activated prior to the conflict - - attr_reader :activated - - ## - # The dependency that is in conflict with the activated gem. - - attr_reader :dependency - - attr_reader :failed_dep # :nodoc: - - ## - # Creates a new resolver conflict when +dependency+ is in conflict with an - # already +activated+ specification. - - def initialize(dependency, activated, failed_dep = dependency) - @dependency = dependency - @activated = activated - @failed_dep = failed_dep - end - - def ==(other) # :nodoc: - self.class === other && - @dependency == other.dependency && - @activated == other.activated && - @failed_dep == other.failed_dep - end - - ## - # A string explanation of the conflict. - - def explain - "" - end - - ## - # Return the 2 dependency objects that conflicted - - def conflicting_dependencies - [@failed_dep.dependency, @activated.request.dependency] - end - - ## - # Explanation of the conflict used by exceptions to print useful messages - - def explanation - activated = @activated.spec.full_name - dependency = @failed_dep.dependency - requirement = dependency.requirement - alternates = dependency.matching_specs.map(&:full_name) - - unless alternates.empty? - matching = <<-MATCHING.chomp - - Gems matching %s: - %s - MATCHING - - matching = format(matching, dependency, alternates.join(", ")) - end - - explanation = <<-EXPLANATION - Activated %s - which does not match conflicting dependency (%s) - - Conflicting dependency chains: - %s - - versus: - %s -%s - EXPLANATION - - format(explanation, activated, requirement, request_path(@activated).reverse.join(", depends on\n "), request_path(@failed_dep).reverse.join(", depends on\n "), matching) - end - - ## - # Returns true if the conflicting dependency's name matches +spec+. - - def for_spec?(spec) - @dependency.name == spec.name - end - - def pretty_print(q) # :nodoc: - q.group 2, "[Dependency conflict: ", "]" do - q.breakable - - q.text "activated " - q.pp @activated - - q.breakable - q.text " dependency " - q.pp @dependency - - q.breakable - if @dependency == @failed_dep - q.text " failed" - else - q.text " failed dependency " - q.pp @failed_dep - end - end - end - - ## - # Path of activations from the +current+ list. - - def request_path(current) - path = [] - - while current do - case current - when Gem::Resolver::ActivationRequest then - path << - "#{current.request.dependency}, #{current.spec.version} activated" - - current = current.parent - when Gem::Resolver::DependencyRequest then - path << current.dependency.to_s - - current = current.requester - else - raise Gem::Exception, "[BUG] unknown request class #{current.class}" - end - end - - path = ["user request (gem command or Gemfile)"] if path.empty? - - path - end - - ## - # Return the Specification that listed the dependency - - def requester - @failed_dep.requester - end -end diff --git a/test/rubygems/test_gem_dependency_resolution_error.rb b/test/rubygems/test_gem_dependency_resolution_error.rb index e67ade5939ff73..d8fa96a2602ac4 100644 --- a/test/rubygems/test_gem_dependency_resolution_error.rb +++ b/test/rubygems/test_gem_dependency_resolution_error.rb @@ -6,21 +6,16 @@ class TestGemDependencyResolutionError < Gem::TestCase def setup super - @spec = util_spec "a", 2 - - @a1_req = Gem::Resolver::DependencyRequest.new dep("a", "= 1"), nil - @a2_req = Gem::Resolver::DependencyRequest.new dep("a", "= 2"), nil - - @activated = Gem::Resolver::ActivationRequest.new @spec, @a2_req - - @conflict = Gem::Resolver::Conflict.new @a1_req, @activated - - @error = Gem::DependencyResolutionError.new @conflict + failure = Struct.new(:explanation).new("a depends on b (= 1.0) but no versions match") + @error = Gem::DependencyResolutionError.new failure end def test_message - assert_match(/Activated a-2/, @error.message) - assert_match(/conflicting dependency/, @error.message) + assert_equal "a depends on b (= 1.0) but no versions match", @error.message + end + + def test_explanation + assert_equal "a depends on b (= 1.0) but no versions match", @error.explanation end def test_conflict diff --git a/test/rubygems/test_gem_impossible_dependencies_error.rb b/test/rubygems/test_gem_impossible_dependencies_error.rb deleted file mode 100644 index 94c0290ea1e79c..00000000000000 --- a/test/rubygems/test_gem_impossible_dependencies_error.rb +++ /dev/null @@ -1,60 +0,0 @@ -# frozen_string_literal: true - -require_relative "helper" - -class TestGemImpossibleDependenciesError < Gem::TestCase - def test_message_conflict - request = dependency_request dep("net-ssh", ">= 2.0.13"), "rye", "0.9.8" - - conflicts = [] - - # These conflicts are lies as their dependencies does not have the correct - # requested-by entries, but they are suitable for testing the message. - # See #485 to construct a correct conflict. - net_ssh_2_2_2 = - dependency_request dep("net-ssh", ">= 2.6.5"), "net-ssh", "2.2.2", request - net_ssh_2_6_5 = - dependency_request dep("net-ssh", "~> 2.2.2"), "net-ssh", "2.6.5", request - - conflict1 = Gem::Resolver::Conflict.new \ - net_ssh_2_6_5, net_ssh_2_6_5.requester - - conflict2 = Gem::Resolver::Conflict.new \ - net_ssh_2_2_2, net_ssh_2_2_2.requester - - conflicts << [net_ssh_2_6_5.requester.spec, conflict1] - conflicts << [net_ssh_2_2_2.requester.spec, conflict2] - - error = Gem::ImpossibleDependenciesError.new request, conflicts - - expected = <<-EXPECTED -rye-0.9.8 requires net-ssh (>= 2.0.13) but it conflicted: - Activated net-ssh-2.6.5 - which does not match conflicting dependency (~> 2.2.2) - - Conflicting dependency chains: - rye (= 0.9.8), 0.9.8 activated, depends on - net-ssh (>= 2.0.13), 2.6.5 activated - - versus: - rye (= 0.9.8), 0.9.8 activated, depends on - net-ssh (>= 2.0.13), 2.6.5 activated, depends on - net-ssh (~> 2.2.2) - - Activated net-ssh-2.2.2 - which does not match conflicting dependency (>= 2.6.5) - - Conflicting dependency chains: - rye (= 0.9.8), 0.9.8 activated, depends on - net-ssh (>= 2.0.13), 2.2.2 activated - - versus: - rye (= 0.9.8), 0.9.8 activated, depends on - net-ssh (>= 2.0.13), 2.2.2 activated, depends on - net-ssh (>= 2.6.5) - - EXPECTED - - assert_equal expected, error.message - end -end diff --git a/test/rubygems/test_gem_resolver_conflict.rb b/test/rubygems/test_gem_resolver_conflict.rb deleted file mode 100644 index 5696ff266d0023..00000000000000 --- a/test/rubygems/test_gem_resolver_conflict.rb +++ /dev/null @@ -1,80 +0,0 @@ -# frozen_string_literal: true - -require_relative "helper" - -class TestGemResolverConflict < Gem::TestCase - def test_explanation - root = - dependency_request dep("net-ssh", ">= 2.0.13"), "rye", "0.9.8" - child = - dependency_request dep("net-ssh", ">= 2.6.5"), "net-ssh", "2.2.2", root - - dep = Gem::Resolver::DependencyRequest.new dep("net-ssh", ">= 2.0.13"), nil - - spec = util_spec "net-ssh", "2.2.2" - active = - Gem::Resolver::ActivationRequest.new spec, dep - - conflict = - Gem::Resolver::Conflict.new child, active - - expected = <<-EXPECTED - Activated net-ssh-2.2.2 - which does not match conflicting dependency (>= 2.6.5) - - Conflicting dependency chains: - net-ssh (>= 2.0.13), 2.2.2 activated - - versus: - rye (= 0.9.8), 0.9.8 activated, depends on - net-ssh (>= 2.0.13), 2.2.2 activated, depends on - net-ssh (>= 2.6.5) - - EXPECTED - - assert_equal expected, conflict.explanation - end - - def test_explanation_user_request - spec = util_spec "a", 2 - - a1_req = Gem::Resolver::DependencyRequest.new dep("a", "= 1"), nil - a2_req = Gem::Resolver::DependencyRequest.new dep("a", "= 2"), nil - - activated = Gem::Resolver::ActivationRequest.new spec, a2_req - - conflict = Gem::Resolver::Conflict.new a1_req, activated - - expected = <<-EXPECTED - Activated a-2 - which does not match conflicting dependency (= 1) - - Conflicting dependency chains: - a (= 2), 2 activated - - versus: - a (= 1) - - EXPECTED - - assert_equal expected, conflict.explanation - end - - def test_request_path - root = - dependency_request dep("net-ssh", ">= 2.0.13"), "rye", "0.9.8" - - child = - dependency_request dep("other", ">= 1.0"), "net-ssh", "2.2.2", root - - conflict = - Gem::Resolver::Conflict.new nil, nil - - expected = [ - "net-ssh (>= 2.0.13), 2.2.2 activated", - "rye (= 0.9.8), 0.9.8 activated", - ] - - assert_equal expected, conflict.request_path(child.requester) - end -end From c131a631f0a94d29891c55d5a1f770a1e8c710f7 Mon Sep 17 00:00:00 2001 From: Colby Swandale Date: Sun, 12 Apr 2026 13:46:09 +1000 Subject: [PATCH 066/188] [ruby/rubygems] Rename orphaned deps test to reflect PubGrub backtracking behavior https://github.com/ruby/rubygems/commit/081245a511 --- test/rubygems/test_gem.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/rubygems/test_gem.rb b/test/rubygems/test_gem.rb index bce50a1d369bc2..c81b0b0547aeca 100644 --- a/test/rubygems/test_gem.rb +++ b/test/rubygems/test_gem.rb @@ -313,7 +313,7 @@ def test_activate_bin_path_does_not_error_if_a_gem_thats_not_finally_activated_h assert_equal %w[a-1 b-2 c-2], loaded_spec_names end - def test_activate_bin_path_raises_a_meaningful_error_if_a_gem_thats_finally_activated_has_orphaned_dependencies + def test_activate_bin_path_backtracks_when_highest_version_has_orphaned_dependencies a1 = util_spec "a", "1" do |s| s.executables = ["exec"] s.add_dependency "b" @@ -334,6 +334,8 @@ def test_activate_bin_path_raises_a_meaningful_error_if_a_gem_thats_finally_acti # c2 is missing, but the resolver backtracks from b2 to b1 which # works with c1, finding a valid solution despite partial installation load Gem.activate_bin_path("a", "exec", ">= 0") + + assert_equal %w[a-1 b-1 c-1], loaded_spec_names end def test_activate_bin_path_in_debug_mode From c6682793b2e18875e90111f1d67baa49e87bbf2e Mon Sep 17 00:00:00 2001 From: Colby Swandale Date: Sun, 12 Apr 2026 14:31:29 +1000 Subject: [PATCH 067/188] [ruby/rubygems] Fix prerelease version inconsistency between all_versions_for and versions_for PubGrub requires both methods to agree on the version universe. https://github.com/ruby/rubygems/commit/23b9505131 --- lib/rubygems/resolver.rb | 31 ++++++++--------- test/rubygems/test_gem_resolver.rb | 53 ++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 15 deletions(-) diff --git a/lib/rubygems/resolver.rb b/lib/rubygems/resolver.rb index 1f63a7b17b5e23..224526116f5800 100644 --- a/lib/rubygems/resolver.rb +++ b/lib/rubygems/resolver.rb @@ -106,7 +106,8 @@ def initialize(needed, set = nil) @packages = {} @all_specs = Hash.new {|h, name| h[name] = find_all_specs_for(name) } - @sorted_versions = Hash.new {|h, pkg| h[pkg] = @all_specs[pkg.to_s].map(&:version).uniq.sort } + @all_versions = Hash.new {|h, pkg| h[pkg] = @all_specs[pkg.to_s].map(&:version).uniq.sort } + @sorted_versions = Hash.new {|h, pkg| h[pkg] = filter_versions(pkg) } @cached_dependencies = Hash.new {|h, pkg| h[pkg] = Hash.new {|v, ver| v[ver] = compute_dependencies(pkg, ver) } } end @@ -188,20 +189,7 @@ def resolve def all_versions_for(package) return [@root_version] if package == @root_package - all_versions = @sorted_versions[package] - - # Exclude prerelease versions unless the set has prerelease enabled. - # Prereleases are still available via versions_for when a range - # specifically includes them (e.g., "= 2.a"), with low priority - # in the Strategy. - if @set.respond_to?(:prerelease) && @set.prerelease - versions = all_versions - else - stable = all_versions.reject(&:prerelease?) - versions = stable.empty? ? all_versions : stable - end - - versions = versions.reverse # highest first + versions = @sorted_versions[package].reverse # highest first name = package.to_s if (skip_dep_gems = skip_gems[name]) && !skip_dep_gems.empty? @@ -315,6 +303,19 @@ def package_for(name) @packages[name] ||= Gem::PubGrub::Package.new(name) end + # Filter versions to exclude prereleases unless prerelease is enabled. + # Both all_versions_for and versions_for use this filtered set to ensure + # PubGrub's constraint propagation and version selection are consistent. + def filter_versions(package) + all_versions = @all_versions[package] + if @set.respond_to?(:prerelease) && @set.prerelease + all_versions + else + stable = all_versions.reject(&:prerelease?) + stable.empty? ? all_versions : stable + end + end + def find_all_specs_for(name) dep = Gem::Dependency.new(name, ">= 0.a") dep_request = DependencyRequest.new(dep, nil) diff --git a/test/rubygems/test_gem_resolver.rb b/test/rubygems/test_gem_resolver.rb index 8c032bf73a30c4..5cdf2fae3596a7 100644 --- a/test/rubygems/test_gem_resolver.rb +++ b/test/rubygems/test_gem_resolver.rb @@ -774,4 +774,57 @@ def test_raises_and_explains_when_platform_prevents_install assert_match "No match for 'a (= 1)' on this platform. Found: c-p-1", e.message end + + def test_resolve_prerelease_not_considered_when_stable_exists + # a-1.0 depends on b ~> 2.0 — only b-2.0.pre satisfies that, but + # b also has a stable version (1.0), so prereleases are filtered out. + # The resolver must fail, not silently use b-2.0.pre during propagation. + a_stable = util_spec "a", "1.0" do |s| + s.add_dependency "b", "~> 2.0" + end + + b_stable = util_spec "b", "1.0" + b_pre = util_spec "b", "2.0.pre" + + s = set(a_stable, b_stable, b_pre) + + ad = make_dep "a" + r = Gem::Resolver.new([ad], s) + + assert_raise Gem::DependencyResolutionError do + r.resolve + end + end + + def test_resolve_prerelease_considered_when_enabled + a_stable = util_spec "a", "1.0" do |s| + s.add_dependency "b", ">= 1.0" + end + + b_pre = util_spec "b", "2.0.pre" + + s = set(a_stable, b_pre) + s.prerelease = true + + ad = make_dep "a" + r = Gem::Resolver.new([ad], s) + + assert_resolves_to [a_stable, b_pre], r + end + + def test_resolve_prerelease_used_when_no_stable_versions_exist + a_stable = util_spec "a", "1.0" do |s| + s.add_dependency "b", ">= 1.0" + end + + b_pre = util_spec "b", "2.0.pre" + b_other_pre = util_spec "b", "1.0.pre" + + s = set(a_stable, b_pre, b_other_pre) + + ad = make_dep "a" + r = Gem::Resolver.new([ad], s) + + assert_resolves_to [a_stable, b_pre], r + end end From 87607bf6b57ac9d0a2b4b455653190f1c0106bdd Mon Sep 17 00:00:00 2001 From: Colby Swandale Date: Sun, 12 Apr 2026 16:39:13 +1000 Subject: [PATCH 068/188] [ruby/rubygems] Remove unreachable spec_for string fallback https://github.com/ruby/rubygems/commit/fa30c4d95e --- lib/rubygems/resolver.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/rubygems/resolver.rb b/lib/rubygems/resolver.rb index 224526116f5800..ae6dd2c65ab725 100644 --- a/lib/rubygems/resolver.rb +++ b/lib/rubygems/resolver.rb @@ -338,7 +338,6 @@ def find_all_specs_for(name) def spec_for(name, version) candidates = @all_specs[name].select {|s| s.version == version } - candidates = @all_specs[name].select {|s| s.version.to_s == version.to_s } if candidates.empty? if candidates.length > 1 # Prefer already-installed specs to avoid unnecessary downloads From 270f7de108926d3cf558dc334b8f708d465f7e91 Mon Sep 17 00:00:00 2001 From: Colby Swandale Date: Sun, 12 Apr 2026 16:50:25 +1000 Subject: [PATCH 069/188] [ruby/rubygems] Remove dead resolver methods https://github.com/ruby/rubygems/commit/5fd0abb214 --- lib/rubygems/resolver.rb | 38 -------------------------------------- 1 file changed, 38 deletions(-) diff --git a/lib/rubygems/resolver.rb b/lib/rubygems/resolver.rb index ae6dd2c65ab725..253e02659cd339 100644 --- a/lib/rubygems/resolver.rb +++ b/lib/rubygems/resolver.rb @@ -111,24 +111,6 @@ def initialize(needed, set = nil) @cached_dependencies = Hash.new {|h, pkg| h[pkg] = Hash.new {|v, ver| v[ver] = compute_dependencies(pkg, ver) } } end - def explain(stage, *data) # :nodoc: - return unless DEBUG_RESOLVER - - d = data.map(&:pretty_inspect).join(", ") - $stderr.printf "%10s %s\n", stage.to_s.upcase, d - end - - def explain_list(stage) # :nodoc: - return unless DEBUG_RESOLVER - - data = yield - $stderr.printf "%10s (%d entries)\n", stage.to_s.upcase, data.size - unless data.empty? - require "pp" - PP.pp data, $stderr - end - end - ## # Proceed with resolution! Returns an array of ActivationRequest objects. @@ -268,26 +250,6 @@ def incompatibilities_for(package, version) end end - ## - # Extracts the specifications that may be able to fulfill +dependency+ and - # returns those that match the local platform and all those that match. - - def find_possible(dependency) # :nodoc: - all = @set.find_all dependency - - if (skip_dep_gems = skip_gems[dependency.name]) && !skip_dep_gems.empty? - matching = all.select do |api_spec| - skip_dep_gems.any? {|s| api_spec.version == s.version } - end - - all = matching unless matching.empty? - end - - matching_platform = select_local_platforms all - - [matching_platform, all] - end - ## # Returns the gems in +specs+ that match the local platform. From c7fc867b7af058e52811fa3ba77acb16f1e0b3f6 Mon Sep 17 00:00:00 2001 From: Colby Swandale Date: Mon, 13 Apr 2026 09:13:43 +1000 Subject: [PATCH 070/188] [ruby/rubygems] Define custom PubGrub Root Package to customise error https://github.com/ruby/rubygems/commit/302e5c83a0 --- lib/rubygems/resolver.rb | 18 +++++++++++++++++- test/rubygems/test_gem_resolver.rb | 1 + 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/lib/rubygems/resolver.rb b/lib/rubygems/resolver.rb index 253e02659cd339..4a04d41d932687 100644 --- a/lib/rubygems/resolver.rb +++ b/lib/rubygems/resolver.rb @@ -100,7 +100,7 @@ def initialize(needed, set = nil) @skip_gems = {} @soft_missing = false - @root_package = Gem::PubGrub::Package.root + @root_package = RootPackage.new @root_version = Gem::PubGrub::Package.root_version @packages = {} @@ -347,6 +347,22 @@ def compute_dependencies(package, version) def make_logger DEBUG_RESOLVER ? Gem::PubGrub::StderrLogger.new : Gem::PubGrub::NullLogger.new end + + # Custom root package so error messages say "your request depends on..." + # instead of PubGrub's default "root depends on...". + class RootPackage < Gem::PubGrub::Package + def initialize + super(:root) + end + + def root? + true + end + + def to_s + "your request" + end + end end require_relative "resolver/activation_request" diff --git a/test/rubygems/test_gem_resolver.rb b/test/rubygems/test_gem_resolver.rb index 5cdf2fae3596a7..5869a1d75eb8c1 100644 --- a/test/rubygems/test_gem_resolver.rb +++ b/test/rubygems/test_gem_resolver.rb @@ -455,6 +455,7 @@ def test_raises_dependency_error end assert_nil e.conflict + assert_match(/your request/, e.message) assert_match(/a depends on c/, e.message) assert_match(/b depends on c/, e.message) end From 91ee03fd6310b8cee8ea635547c1dc2be639f946 Mon Sep 17 00:00:00 2001 From: Colby Swandale Date: Mon, 13 Apr 2026 09:41:28 +1000 Subject: [PATCH 071/188] [ruby/rubygems] Add extended explanations for platform and Ruby version hints https://github.com/ruby/rubygems/commit/b3e24fd709 --- lib/rubygems/resolver.rb | 82 +++++++++++++++++++++++- lib/rubygems/resolver/incompatibility.rb | 10 +++ test/rubygems/test_gem_resolver.rb | 43 +++++++++++++ 3 files changed, 133 insertions(+), 2 deletions(-) create mode 100644 lib/rubygems/resolver/incompatibility.rb diff --git a/lib/rubygems/resolver.rb b/lib/rubygems/resolver.rb index 4a04d41d932687..9147027d56dd05 100644 --- a/lib/rubygems/resolver.rb +++ b/lib/rubygems/resolver.rb @@ -163,7 +163,13 @@ def resolve ActivationRequest.new(spec, dep_request) end rescue Gem::PubGrub::SolveFailure => e - raise Gem::DependencyResolutionError, e + extended = extract_extended_explanation(e.incompatibility) + if extended + message = "#{e.explanation}\n\n#{extended}" + raise Gem::DependencyResolutionError, Struct.new(:explanation).new(message) + else + raise Gem::DependencyResolutionError, e + end end # PubGrub source interface methods @@ -198,7 +204,16 @@ def versions_for(package, range = Gem::PubGrub::VersionRange.any) def no_versions_incompatibility_for(_package, unsatisfied_term) cause = Gem::PubGrub::Incompatibility::NoVersions.new(unsatisfied_term) - Gem::PubGrub::Incompatibility.new([unsatisfied_term], cause: cause) + + name = unsatisfied_term.package.to_s + constraint = unsatisfied_term.constraint + extended_explanation = build_extended_explanation(name, constraint) + + Gem::Resolver::Incompatibility.new( + [unsatisfied_term], + cause: cause, + extended_explanation: extended_explanation + ) end def incompatibilities_for(package, version) @@ -344,6 +359,68 @@ def compute_dependencies(package, version) deps end + def build_extended_explanation(name, constraint) + dep = Gem::Dependency.new(name, ">= 0.a") + dep_request = DependencyRequest.new(dep, nil) + unfiltered = @set.find_all(dep_request) + return if unfiltered.empty? + + filtered = @all_specs[name] + return if filtered.length == unfiltered.length + + hints = [] + + # Check for specs that exist for other platforms + platform_specs = unfiltered.select do |s| + !Gem::Platform.installable?(s) && constraint.range.include?(s.version) + end + if platform_specs.any? + label = "#{name} (#{constraint.constraint_string})" + hints << "The source contains the following gems matching '#{label}':" + platform_specs.each do |s| + actual = s.respond_to?(:spec) ? s.spec : s + hints << " * #{actual.full_name}" + end + end + + # Check for specs filtered by Ruby version + installable = select_local_platforms(unfiltered) + ruby_specs = installable.select do |s| + actual = s.respond_to?(:spec) ? s.spec : s + constraint.range.include?(s.version) && + !actual.required_ruby_version.satisfied_by?(Gem.ruby_version) + rescue StandardError + false + end + if ruby_specs.any? + versions = ruby_specs.map(&:version).uniq.sort.reverse.first(3) + sample = ruby_specs.find {|s| s.version == versions.first } + actual = sample.respond_to?(:spec) ? sample.spec : sample + ruby_req = actual.required_ruby_version + hints << "#{name} #{versions.join(", ")} requires Ruby #{ruby_req} (you have #{Gem.ruby_version})" + end + + hints.empty? ? nil : hints.join("\n") + end + + def extract_extended_explanation(incompatibility) + while incompatibility.cause.is_a?(Gem::PubGrub::Incompatibility::ConflictCause) + cause = incompatibility.cause + + [cause.conflict, cause.other].each do |incompat| + if incompat.cause.is_a?(Gem::PubGrub::Incompatibility::NoVersions) && + incompat.respond_to?(:extended_explanation) && + incompat.extended_explanation + return incompat.extended_explanation + end + end + + incompatibility = cause.conflict + end + + nil + end + def make_logger DEBUG_RESOLVER ? Gem::PubGrub::StderrLogger.new : Gem::PubGrub::NullLogger.new end @@ -367,6 +444,7 @@ def to_s require_relative "resolver/activation_request" require_relative "resolver/dependency_request" +require_relative "resolver/incompatibility" require_relative "resolver/requirement_list" require_relative "resolver/set" require_relative "resolver/api_set" diff --git a/lib/rubygems/resolver/incompatibility.rb b/lib/rubygems/resolver/incompatibility.rb new file mode 100644 index 00000000000000..57a60affb47392 --- /dev/null +++ b/lib/rubygems/resolver/incompatibility.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class Gem::Resolver::Incompatibility < Gem::PubGrub::Incompatibility + attr_reader :extended_explanation + + def initialize(terms, cause:, custom_explanation: nil, extended_explanation: nil) + @extended_explanation = extended_explanation + super(terms, cause: cause, custom_explanation: custom_explanation) + end +end diff --git a/test/rubygems/test_gem_resolver.rb b/test/rubygems/test_gem_resolver.rb index 5869a1d75eb8c1..e6edc7553e9cd2 100644 --- a/test/rubygems/test_gem_resolver.rb +++ b/test/rubygems/test_gem_resolver.rb @@ -828,4 +828,47 @@ def test_resolve_prerelease_used_when_no_stable_versions_exist assert_resolves_to [a_stable, b_pre], r end + + def test_error_includes_platform_hint_when_specs_exist_for_other_platforms + a = util_spec "a", "1.0" do |s| + s.add_dependency "b", ">= 1.0" + end + + b_foreign = util_spec "b", "1.0" do |s| + s.platform = "java" + end + + s = set(a, b_foreign) + + ad = make_dep "a" + r = Gem::Resolver.new([ad], s) + + e = assert_raise Gem::DependencyResolutionError do + r.resolve + end + + assert_match(/b-1.0-java/, e.message) + end + + def test_error_includes_ruby_version_hint_when_filtered + a = util_spec "a", "1.0" do |s| + s.add_dependency "b", ">= 1.0" + end + + b = util_spec "b", "1.0" do |s| + s.required_ruby_version = ">= 999.0" + end + + s = set(a, b) + + ad = make_dep "a" + r = Gem::Resolver.new([ad], s) + + e = assert_raise Gem::DependencyResolutionError do + r.resolve + end + + assert_match(/requires Ruby/, e.message) + assert_match(/you have/, e.message) + end end From 56a402c0afd533dda80ca67f6bdc1fe318bd3fa4 Mon Sep 17 00:00:00 2001 From: Colby Swandale Date: Mon, 13 Apr 2026 09:50:18 +1000 Subject: [PATCH 072/188] [ruby/rubygems] Add custom inline explanation when gems are filtered from resolution https://github.com/ruby/rubygems/commit/353ecaa178 --- lib/rubygems/resolver.rb | 5 +++++ test/rubygems/test_gem_resolver.rb | 1 + 2 files changed, 6 insertions(+) diff --git a/lib/rubygems/resolver.rb b/lib/rubygems/resolver.rb index 9147027d56dd05..15d023976505c5 100644 --- a/lib/rubygems/resolver.rb +++ b/lib/rubygems/resolver.rb @@ -209,9 +209,14 @@ def no_versions_incompatibility_for(_package, unsatisfied_term) constraint = unsatisfied_term.constraint extended_explanation = build_extended_explanation(name, constraint) + custom_explanation = if extended_explanation + "#{constraint} could not be found in any repository" + end + Gem::Resolver::Incompatibility.new( [unsatisfied_term], cause: cause, + custom_explanation: custom_explanation, extended_explanation: extended_explanation ) end diff --git a/test/rubygems/test_gem_resolver.rb b/test/rubygems/test_gem_resolver.rb index e6edc7553e9cd2..d76738f92b2cb2 100644 --- a/test/rubygems/test_gem_resolver.rb +++ b/test/rubygems/test_gem_resolver.rb @@ -847,6 +847,7 @@ def test_error_includes_platform_hint_when_specs_exist_for_other_platforms r.resolve end + assert_match(/could not be found in any repository/, e.message) assert_match(/b-1.0-java/, e.message) end From c7d6a21f5ba5c2fe145cf6bc6822b39399cd895b Mon Sep 17 00:00:00 2001 From: Colby Swandale Date: Mon, 13 Apr 2026 10:37:56 +1000 Subject: [PATCH 073/188] [ruby/rubygems] Optimize PubGrub resolver performance https://github.com/ruby/rubygems/commit/56738545e9 --- lib/rubygems/resolver.rb | 8 +- lib/rubygems/resolver/strategy.rb | 44 ++++++ test/rubygems/test_gem_resolver_strategy.rb | 163 ++++++++++++++++++++ 3 files changed, 213 insertions(+), 2 deletions(-) create mode 100644 lib/rubygems/resolver/strategy.rb create mode 100644 test/rubygems/test_gem_resolver_strategy.rb diff --git a/lib/rubygems/resolver.rb b/lib/rubygems/resolver.rb index 15d023976505c5..01372c0cd27e0d 100644 --- a/lib/rubygems/resolver.rb +++ b/lib/rubygems/resolver.rb @@ -109,6 +109,8 @@ def initialize(needed, set = nil) @all_versions = Hash.new {|h, pkg| h[pkg] = @all_specs[pkg.to_s].map(&:version).uniq.sort } @sorted_versions = Hash.new {|h, pkg| h[pkg] = filter_versions(pkg) } @cached_dependencies = Hash.new {|h, pkg| h[pkg] = Hash.new {|v, ver| v[ver] = compute_dependencies(pkg, ver) } } + @version_to_index = Hash.new {|h, pkg| h[pkg] = @sorted_versions[pkg].each_with_index.to_h } + @versions_for_cache = {} end ## @@ -149,6 +151,7 @@ def resolve solver = Gem::PubGrub::VersionSolver.new( source: self, root: @root_package, + strategy: Gem::Resolver::Strategy.new(self, @root_package), logger: make_logger ) result = solver.solve @@ -199,7 +202,7 @@ def all_versions_for(package) end def versions_for(package, range = Gem::PubGrub::VersionRange.any) - range.select_versions(@sorted_versions[package]) + @versions_for_cache[[package, range]] ||= range.select_versions(@sorted_versions[package]) end def no_versions_incompatibility_for(_package, unsatisfied_term) @@ -226,7 +229,7 @@ def incompatibilities_for(package, version) sorted_versions = @sorted_versions[package] package_deps[version].filter_map do |dep_package_name, dep_constraint| dep_package = dep_constraint.package - low = high = sorted_versions.index(version) + low = high = @version_to_index[package][version] # find version low such that all >= low share the same dep while low > 0 && @@ -450,6 +453,7 @@ def to_s require_relative "resolver/activation_request" require_relative "resolver/dependency_request" require_relative "resolver/incompatibility" +require_relative "resolver/strategy" require_relative "resolver/requirement_list" require_relative "resolver/set" require_relative "resolver/api_set" diff --git a/lib/rubygems/resolver/strategy.rb b/lib/rubygems/resolver/strategy.rb new file mode 100644 index 00000000000000..1a1c16261906f5 --- /dev/null +++ b/lib/rubygems/resolver/strategy.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +# Custom PubGrub strategy with caching for version selection. +# Modeled after Bundler's strategy to avoid redundant versions_for +# calls during the solver's package selection loop. + +class Gem::Resolver::Strategy + def initialize(source, root_package) + @source = source + @package_priority_cache = {} + + @version_indexes = Hash.new do |h, k| + if Gem::PubGrub::Package.root?(k) + h[k] = { Gem::PubGrub::Package.root_version => 0 } + else + h[k] = @source.all_versions_for(k).each.with_index.to_h + end + end + end + + def next_package_and_version(unsatisfied) + package, range = next_term_to_try_from(unsatisfied) + [package, most_preferred_version_of(package, range)] + end + + private + + def most_preferred_version_of(package, range) + versions = @source.versions_for(package, range) + indexes = @version_indexes[package] + versions.min_by {|version| indexes[version] || Float::INFINITY } + end + + def next_term_to_try_from(unsatisfied) + unsatisfied.min_by do |package, range| + @package_priority_cache[[package, range]] ||= begin + matching_versions = @source.versions_for(package, range) + higher_versions = @source.versions_for(package, range.upper_invert) + + [matching_versions.count <= 1 ? 0 : 1, higher_versions.count] + end + end + end +end diff --git a/test/rubygems/test_gem_resolver_strategy.rb b/test/rubygems/test_gem_resolver_strategy.rb new file mode 100644 index 00000000000000..f84e8b6fdaa312 --- /dev/null +++ b/test/rubygems/test_gem_resolver_strategy.rb @@ -0,0 +1,163 @@ +# frozen_string_literal: true + +require_relative "helper" + +class TestGemResolverStrategy < Gem::TestCase + # Minimal source that implements the two methods Strategy calls: + # all_versions_for(package) — returns versions in preference order + # versions_for(package, range) — returns versions matching a range + # + # Tracks call counts so we can assert on caching behavior. + class StubSource + attr_reader :versions_for_calls + + def initialize(versions_by_package) + @versions_by_package = versions_by_package + @versions_for_calls = 0 + end + + def all_versions_for(package) + @versions_by_package.fetch(package.to_s, []) + end + + def versions_for(package, range) + @versions_for_calls += 1 + all = @versions_by_package.fetch(package.to_s, []) + all.select {|v| range.include?(v) } + end + end + + def v(version_string) + Gem::Version.new(version_string) + end + + def make_package(name) + Gem::PubGrub::Package.new(name) + end + + def make_range_any + Gem::PubGrub::VersionRange.any + end + + # A range >= min (unbounded above) + def make_range_gte(version) + Gem::PubGrub::VersionRange.new(min: version, include_min: true) + end + + # A range >= min AND < max + def make_range_between(min, max) + Gem::PubGrub::VersionRange.new( + min: min, max: max, + include_min: true, include_max: false + ) + end + + def test_most_preferred_version_respects_all_versions_for_ordering + # all_versions_for returns [2.0, 1.0, 3.0] — so 2.0 is most preferred + # even though 3.0 is numerically highest. + pkg = make_package("a") + source = StubSource.new("a" => [v("2.0"), v("1.0"), v("3.0")]) + + strategy = Gem::Resolver::Strategy.new(source, Gem::PubGrub::Package.root) + unsatisfied = { pkg => make_range_any } + + _package, version = strategy.next_package_and_version(unsatisfied) + + assert_equal v("2.0"), version + end + + def test_picks_most_constrained_package + # "a" has 3 matching versions, "b" has 1 matching version. + # Strategy should pick "b" because it's more constrained. + pkg_a = make_package("a") + pkg_b = make_package("b") + + source = StubSource.new( + "a" => [v("3.0"), v("2.0"), v("1.0")], + "b" => [v("1.0")] + ) + + strategy = Gem::Resolver::Strategy.new(source, Gem::PubGrub::Package.root) + + unsatisfied = { + pkg_a => make_range_any, + pkg_b => make_range_any, + } + + package, _version = strategy.next_package_and_version(unsatisfied) + + assert_equal pkg_b, package + end + + def test_picks_package_with_fewer_higher_versions_as_tiebreaker + # Both "a" and "b" have 2 matching versions (so both get priority [1, ...]). + # "a" has matching [2.0, 1.0] with higher (above range) = [] (0 higher) + # "b" has matching [2.0, 1.0] with higher [3.0] (1 higher) + # Tiebreaker: fewer higher versions wins, so "a" is picked. + pkg_a = make_package("a") + pkg_b = make_package("b") + + range = make_range_between(v("0.5"), v("2.5")) + + source = StubSource.new( + "a" => [v("2.0"), v("1.0")], + "b" => [v("3.0"), v("2.0"), v("1.0")] + ) + + strategy = Gem::Resolver::Strategy.new(source, Gem::PubGrub::Package.root) + + unsatisfied = { + pkg_a => range, + pkg_b => range, + } + + package, _version = strategy.next_package_and_version(unsatisfied) + + assert_equal pkg_a, package + end + + def test_cache_prevents_redundant_versions_for_calls + pkg = make_package("a") + source = StubSource.new("a" => [v("2.0"), v("1.0")]) + + strategy = Gem::Resolver::Strategy.new(source, Gem::PubGrub::Package.root) + + range = make_range_any + unsatisfied = { pkg => range } + + # First call: should call versions_for for matching + upper_invert + most_preferred + strategy.next_package_and_version(unsatisfied) + calls_after_first = source.versions_for_calls + + # Second call with same package+range: next_term_to_try_from should + # hit the cache, so only most_preferred_version_of adds a call. + strategy.next_package_and_version(unsatisfied) + calls_after_second = source.versions_for_calls + + # The cached path saves the 2 calls in next_term_to_try_from, + # so only the 1 call from most_preferred_version_of is added. + assert_equal 1, calls_after_second - calls_after_first + end + + def test_cache_is_keyed_by_package_and_range + pkg = make_package("a") + source = StubSource.new("a" => [v("3.0"), v("2.0"), v("1.0")]) + + strategy = Gem::Resolver::Strategy.new(source, Gem::PubGrub::Package.root) + + range_any = make_range_any + range_gte = make_range_gte(v("2.0")) + + # First call with range_any + strategy.next_package_and_version({ pkg => range_any }) + calls_after_first = source.versions_for_calls + + # Second call with different range — cache miss, so versions_for is called again + strategy.next_package_and_version({ pkg => range_gte }) + calls_after_second = source.versions_for_calls + + # A cache miss means 2 new versions_for calls (matching + upper_invert) + # plus 1 from most_preferred_version_of = 3 total new calls + assert_equal 3, calls_after_second - calls_after_first + end +end From f609e7e60a58e5b0d97b9c206b1f95170e679a14 Mon Sep 17 00:00:00 2001 From: Colby Swandale Date: Mon, 13 Apr 2026 11:12:19 +1000 Subject: [PATCH 074/188] [ruby/rubygems] Skip self-dependencies in compute_dependencies https://github.com/ruby/rubygems/commit/310263f948 --- lib/rubygems/resolver.rb | 1 + test/rubygems/test_gem_resolver.rb | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/lib/rubygems/resolver.rb b/lib/rubygems/resolver.rb index 01372c0cd27e0d..6538a6c270f7a2 100644 --- a/lib/rubygems/resolver.rb +++ b/lib/rubygems/resolver.rb @@ -349,6 +349,7 @@ def compute_dependencies(package, version) actual_spec = spec.respond_to?(:spec) ? spec.spec : spec actual_spec.dependencies.each do |d| + next if d.name == package.to_s next if d.type == :development && !@development next if d.type == :development && @development_shallow && !root_names.include?(package.to_s) diff --git a/test/rubygems/test_gem_resolver.rb b/test/rubygems/test_gem_resolver.rb index d76738f92b2cb2..b7d3aeda3f9047 100644 --- a/test/rubygems/test_gem_resolver.rb +++ b/test/rubygems/test_gem_resolver.rb @@ -872,4 +872,17 @@ def test_error_includes_ruby_version_hint_when_filtered assert_match(/requires Ruby/, e.message) assert_match(/you have/, e.message) end + + def test_self_dependency_does_not_crash + a = util_spec "a", "1.0" do |s| + s.add_dependency "a" + end + + s = set(a) + ad = make_dep "a" + r = Gem::Resolver.new([ad], s) + + assert_resolves_to [a], r + end + end From 6b3ae73f1824dbfe573934750766561919a17974 Mon Sep 17 00:00:00 2001 From: Colby Swandale Date: Mon, 13 Apr 2026 11:18:47 +1000 Subject: [PATCH 075/188] [ruby/rubygems] Use PubGrub's intended root dependency pattern https://github.com/ruby/rubygems/commit/b70c071e98 --- lib/rubygems/resolver.rb | 37 ++++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/lib/rubygems/resolver.rb b/lib/rubygems/resolver.rb index 6538a6c270f7a2..7ddffb3901a405 100644 --- a/lib/rubygems/resolver.rb +++ b/lib/rubygems/resolver.rb @@ -107,8 +107,16 @@ def initialize(needed, set = nil) @all_specs = Hash.new {|h, name| h[name] = find_all_specs_for(name) } @all_versions = Hash.new {|h, pkg| h[pkg] = @all_specs[pkg.to_s].map(&:version).uniq.sort } - @sorted_versions = Hash.new {|h, pkg| h[pkg] = filter_versions(pkg) } - @cached_dependencies = Hash.new {|h, pkg| h[pkg] = Hash.new {|v, ver| v[ver] = compute_dependencies(pkg, ver) } } + @sorted_versions = Hash.new do |h, pkg| + h[pkg] = Gem::PubGrub::Package.root?(pkg) ? [@root_version] : filter_versions(pkg) + end + @cached_dependencies = Hash.new do |h, pkg| + h[pkg] = if Gem::PubGrub::Package.root?(pkg) + { @root_version => root_dependencies } + else + Hash.new {|v, ver| v[ver] = compute_dependencies(pkg, ver) } + end + end @version_to_index = Hash.new {|h, pkg| h[pkg] = @sorted_versions[pkg].each_with_index.to_h } @versions_for_cache = {} end @@ -137,17 +145,6 @@ def resolve raise exc end - # Build root deps from @needed - root_deps = {} - @needed.each do |dep| - range = Gem::PubGrub::RubyGems.requirement_to_range(dep.requirement) - constraint = Gem::PubGrub::VersionConstraint.new(package_for(dep.name), range: range) - root_deps[dep.name] = root_deps.key?(dep.name) ? root_deps[dep.name].intersect(constraint) : constraint - end - - @sorted_versions[@root_package] = [@root_version] - @cached_dependencies[@root_package] = { @root_version => root_deps } - solver = Gem::PubGrub::VersionSolver.new( source: self, root: @root_package, @@ -178,8 +175,6 @@ def resolve # PubGrub source interface methods def all_versions_for(package) - return [@root_version] if package == @root_package - versions = @sorted_versions[package].reverse # highest first name = package.to_s @@ -291,6 +286,16 @@ def package_for(name) # Filter versions to exclude prereleases unless prerelease is enabled. # Both all_versions_for and versions_for use this filtered set to ensure # PubGrub's constraint propagation and version selection are consistent. + def root_dependencies + deps = {} + @needed.each do |dep| + range = Gem::PubGrub::RubyGems.requirement_to_range(dep.requirement) + constraint = Gem::PubGrub::VersionConstraint.new(package_for(dep.name), range: range) + deps[dep.name] = deps.key?(dep.name) ? deps[dep.name].intersect(constraint) : constraint + end + deps + end + def filter_versions(package) all_versions = @all_versions[package] if @set.respond_to?(:prerelease) && @set.prerelease @@ -338,8 +343,6 @@ def spec_for(name, version) end def compute_dependencies(package, version) - return {} if package == @root_package - spec = spec_for(package.to_s, version) return {} unless spec return {} if @ignore_dependencies From a687a6b954184b7786e6f4e372a2de211d82a77f Mon Sep 17 00:00:00 2001 From: Colby Swandale Date: Mon, 13 Apr 2026 11:27:20 +1000 Subject: [PATCH 076/188] [ruby/rubygems] Fetch development dependencies for API-sourced specs https://github.com/ruby/rubygems/commit/aa9d86ee22 --- lib/rubygems/resolver.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/rubygems/resolver.rb b/lib/rubygems/resolver.rb index 7ddffb3901a405..9fd1cd0f357467 100644 --- a/lib/rubygems/resolver.rb +++ b/lib/rubygems/resolver.rb @@ -347,6 +347,8 @@ def compute_dependencies(package, version) return {} unless spec return {} if @ignore_dependencies + spec.fetch_development_dependencies if @development && spec.respond_to?(:fetch_development_dependencies) + deps = {} root_names = @needed.map(&:name) From 43128a82773b5db719c407c376ad365de64b15dc Mon Sep 17 00:00:00 2001 From: Colby Swandale Date: Mon, 13 Apr 2026 11:40:13 +1000 Subject: [PATCH 077/188] [ruby/rubygems] Align incompatibilities_for with BasicPackageSource design https://github.com/ruby/rubygems/commit/390d035339 --- lib/rubygems/resolver.rb | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/lib/rubygems/resolver.rb b/lib/rubygems/resolver.rb index 9fd1cd0f357467..f96d794eefcaa5 100644 --- a/lib/rubygems/resolver.rb +++ b/lib/rubygems/resolver.rb @@ -224,6 +224,7 @@ def incompatibilities_for(package, version) sorted_versions = @sorted_versions[package] package_deps[version].filter_map do |dep_package_name, dep_constraint| dep_package = dep_constraint.package + low = high = @version_to_index[package][version] # find version low such that all >= low share the same dep @@ -255,10 +256,10 @@ def incompatibilities_for(package, version) if dep_constraint.range.empty? cause = Gem::PubGrub::Incompatibility::InvalidDependency.new(dep_package, dep_constraint) - next Gem::PubGrub::Incompatibility.new( + return [Gem::PubGrub::Incompatibility.new( [Gem::PubGrub::Term.new(self_constraint, true)], cause: cause - ) + )] end Gem::PubGrub::Incompatibility.new( @@ -289,8 +290,7 @@ def package_for(name) def root_dependencies deps = {} @needed.each do |dep| - range = Gem::PubGrub::RubyGems.requirement_to_range(dep.requirement) - constraint = Gem::PubGrub::VersionConstraint.new(package_for(dep.name), range: range) + constraint = Gem::PubGrub::RubyGems.requirement_to_constraint(package_for(dep.name), dep.requirement) deps[dep.name] = deps.key?(dep.name) ? deps[dep.name].intersect(constraint) : constraint end deps @@ -366,8 +366,7 @@ def compute_dependencies(package, version) next end - range = Gem::PubGrub::RubyGems.requirement_to_range(d.requirement) - deps[d.name] = Gem::PubGrub::VersionConstraint.new(dep_package, range: range) + deps[d.name] = Gem::PubGrub::RubyGems.requirement_to_constraint(dep_package, d.requirement) end deps From d6a233c5d30385c2cb6f5972233f0428f8d0627f Mon Sep 17 00:00:00 2001 From: Colby Swandale Date: Mon, 13 Apr 2026 11:44:38 +1000 Subject: [PATCH 078/188] [ruby/rubygems] Distinguish unknown packages from filtered packages in error messages https://github.com/ruby/rubygems/commit/638af500a2 --- lib/rubygems/resolver.rb | 32 +++++++++++++++++++++--------- test/rubygems/test_gem_resolver.rb | 3 +-- 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/lib/rubygems/resolver.rb b/lib/rubygems/resolver.rb index f96d794eefcaa5..eeda6ec30e5bef 100644 --- a/lib/rubygems/resolver.rb +++ b/lib/rubygems/resolver.rb @@ -105,7 +105,8 @@ def initialize(needed, set = nil) @packages = {} - @all_specs = Hash.new {|h, name| h[name] = find_all_specs_for(name) } + @unfiltered_specs = Hash.new {|h, name| h[name] = find_unfiltered_specs_for(name) } + @all_specs = Hash.new {|h, name| h[name] = filter_specs(@unfiltered_specs[name]) } @all_versions = Hash.new {|h, pkg| h[pkg] = @all_specs[pkg.to_s].map(&:version).uniq.sort } @sorted_versions = Hash.new do |h, pkg| h[pkg] = Gem::PubGrub::Package.root?(pkg) ? [@root_version] : filter_versions(pkg) @@ -225,6 +226,19 @@ def incompatibilities_for(package, version) package_deps[version].filter_map do |dep_package_name, dep_constraint| dep_package = dep_constraint.package + # If no specs exist at all for this dependency (not even for other + # platforms or Ruby versions), mark this version as invalid. + # When specs exist but were filtered out, let PubGrub discover it + # via NoVersions so platform/ruby hints are generated. + if @unfiltered_specs[dep_package_name].empty? + self_constraint = Gem::PubGrub::VersionConstraint.new(package, range: Gem::PubGrub::VersionRange.any) + cause = Gem::PubGrub::Incompatibility::InvalidDependency.new(dep_package, dep_constraint) + return [Gem::PubGrub::Incompatibility.new( + [Gem::PubGrub::Term.new(self_constraint, true)], + cause: cause + )] + end + low = high = @version_to_index[package][version] # find version low such that all >= low share the same dep @@ -306,15 +320,17 @@ def filter_versions(package) end end - def find_all_specs_for(name) + def find_unfiltered_specs_for(name) dep = Gem::Dependency.new(name, ">= 0.a") dep_request = DependencyRequest.new(dep, nil) - all = @set.find_all(dep_request) + @set.find_all(dep_request) + end - specs = select_local_platforms(all) + def filter_specs(specs) + filtered = select_local_platforms(specs) unless @soft_missing - specs = specs.select do |s| + filtered = filtered.select do |s| actual = s.respond_to?(:spec) ? s.spec : s actual.required_ruby_version.satisfied_by?(Gem.ruby_version) && actual.required_rubygems_version.satisfied_by?(Gem.rubygems_version) @@ -323,7 +339,7 @@ def find_all_specs_for(name) end end - specs + filtered end def spec_for(name, version) @@ -373,9 +389,7 @@ def compute_dependencies(package, version) end def build_extended_explanation(name, constraint) - dep = Gem::Dependency.new(name, ">= 0.a") - dep_request = DependencyRequest.new(dep, nil) - unfiltered = @set.find_all(dep_request) + unfiltered = @unfiltered_specs[name] return if unfiltered.empty? filtered = @all_specs[name] diff --git a/test/rubygems/test_gem_resolver.rb b/test/rubygems/test_gem_resolver.rb index b7d3aeda3f9047..85ebd8fabe105b 100644 --- a/test/rubygems/test_gem_resolver.rb +++ b/test/rubygems/test_gem_resolver.rb @@ -516,8 +516,7 @@ def test_raises_and_reports_an_implicit_request_properly r.resolve end - assert_match(/depends on b/, e.message) - assert_match(/no versions satisfy b/, e.message) + assert_match(/depends on unknown package b/, e.message) end def test_raises_when_possibles_are_exhausted From ffde879a64d04aa6ee41eef0d3859ba639ba03ad Mon Sep 17 00:00:00 2001 From: Colby Swandale Date: Mon, 13 Apr 2026 12:01:02 +1000 Subject: [PATCH 079/188] [ruby/rubygems] Improve error messages for contradictory requirements and prereleases https://github.com/ruby/rubygems/commit/4d131aa532 --- lib/rubygems/resolver.rb | 34 +++++++++++++++---- test/rubygems/test_gem_resolver.rb | 54 ++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 6 deletions(-) diff --git a/lib/rubygems/resolver.rb b/lib/rubygems/resolver.rb index eeda6ec30e5bef..39873ebd89dfa1 100644 --- a/lib/rubygems/resolver.rb +++ b/lib/rubygems/resolver.rb @@ -269,11 +269,20 @@ def incompatibilities_for(package, version) self_constraint = Gem::PubGrub::VersionConstraint.new(package, range: range) if dep_constraint.range.empty? - cause = Gem::PubGrub::Incompatibility::InvalidDependency.new(dep_package, dep_constraint) - return [Gem::PubGrub::Incompatibility.new( - [Gem::PubGrub::Term.new(self_constraint, true)], - cause: cause - )] + if @unfiltered_specs[dep_package_name].any? + # Package exists but requirement is self-contradictory + return [Gem::Resolver::Incompatibility.new( + [Gem::PubGrub::Term.new(self_constraint, true)], + cause: Gem::PubGrub::Incompatibility::NoVersions.new(dep_constraint), + custom_explanation: "#{dep_package_name} cannot satisfy contradictory requirements #{dep_constraint.constraint_string}" + )] + else + cause = Gem::PubGrub::Incompatibility::InvalidDependency.new(dep_package, dep_constraint) + return [Gem::PubGrub::Incompatibility.new( + [Gem::PubGrub::Term.new(self_constraint, true)], + cause: cause + )] + end end Gem::PubGrub::Incompatibility.new( @@ -393,7 +402,10 @@ def build_extended_explanation(name, constraint) return if unfiltered.empty? filtered = @all_specs[name] - return if filtered.length == unfiltered.length + pkg = package_for(name) + has_prerelease_filtering = @all_versions[pkg].length > @sorted_versions[pkg].length + + return if filtered.length == unfiltered.length && !has_prerelease_filtering hints = [] @@ -427,6 +439,16 @@ def build_extended_explanation(name, constraint) hints << "#{name} #{versions.join(", ")} requires Ruby #{ruby_req} (you have #{Gem.ruby_version})" end + # Check for specs filtered by prerelease status + unless @set.respond_to?(:prerelease) && @set.prerelease + pkg = package_for(name) + prerelease_versions = @all_versions[pkg].select(&:prerelease?) - @sorted_versions[pkg] + if prerelease_versions.any? + versions = prerelease_versions.sort.reverse.first(3) # limit to avoid cluttering error output + hints << "#{name} #{versions.join(", ")} are pre-release versions. Use --prerelease to allow pre-release gems." + end + end + hints.empty? ? nil : hints.join("\n") end diff --git a/test/rubygems/test_gem_resolver.rb b/test/rubygems/test_gem_resolver.rb index 85ebd8fabe105b..384d8ca40327ca 100644 --- a/test/rubygems/test_gem_resolver.rb +++ b/test/rubygems/test_gem_resolver.rb @@ -884,4 +884,58 @@ def test_self_dependency_does_not_crash assert_resolves_to [a], r end + def test_contradictory_root_requirements_give_clear_error + a1 = util_spec "a", "1" + a2 = util_spec "a", "2" + + s = set(a1, a2) + r = Gem::Resolver.new([make_dep("a", "= 1"), make_dep("a", "= 2")], s) + + e = assert_raise Gem::DependencyResolutionError do + r.resolve + end + + assert_match(/contradictory/, e.message) + refute_match(/unknown package/, e.message) + end + + def test_empty_range_transitive_dep_does_not_say_unknown + a = util_spec "a", "1.0" do |s| + s.add_dependency "b", "> 2", "< 1" + end + + b = util_spec "b", "1.5" + + s = set(a, b) + ad = make_dep "a" + r = Gem::Resolver.new([ad], s) + + e = assert_raise Gem::DependencyResolutionError do + r.resolve + end + + assert_match(/contradictory/, e.message) + refute_match(/unknown package/, e.message) + end + + def test_error_hints_about_prerelease_when_filtered + a = util_spec "a", "1.0" do |s| + s.add_dependency "b", "~> 2.0" + end + + b_stable = util_spec "b", "1.0" + b_pre = util_spec "b", "2.0.pre" + + s = set(a, b_stable, b_pre) + ad = make_dep "a" + r = Gem::Resolver.new([ad], s) + + e = assert_raise Gem::DependencyResolutionError do + r.resolve + end + + assert_match(/pre-release/, e.message) + assert_match(/--prerelease/, e.message) + end + end From 488ac14e0ebd48260ba79c2ae685847bd9497827 Mon Sep 17 00:00:00 2001 From: Colby Swandale Date: Mon, 13 Apr 2026 12:29:11 +1000 Subject: [PATCH 080/188] [ruby/rubygems] Fix --force to skip unsatisfiable version constraints https://github.com/ruby/rubygems/commit/24c69d4769 --- lib/rubygems/resolver.rb | 10 +++++---- .../rubygems/test_gem_dependency_installer.rb | 22 +++++++++++++++++-- test/rubygems/test_gem_resolver.rb | 17 ++++++++++++++ 3 files changed, 43 insertions(+), 6 deletions(-) diff --git a/lib/rubygems/resolver.rb b/lib/rubygems/resolver.rb index 39873ebd89dfa1..da2413befb27e7 100644 --- a/lib/rubygems/resolver.rb +++ b/lib/rubygems/resolver.rb @@ -385,10 +385,12 @@ def compute_dependencies(package, version) dep_package = package_for(d.name) - # Check if the dependency has any available versions - dep_specs = @all_specs[d.name] - if dep_specs.empty? && @soft_missing - next + # In force mode, skip deps that can't be satisfied — either no + # specs at all, or no specs matching the version requirement. + if @soft_missing + dep_specs = @all_specs[d.name] + matching = dep_specs.select {|s| d.requirement.satisfied_by?(s.version) } + next if matching.empty? end deps[d.name] = Gem::PubGrub::RubyGems.requirement_to_constraint(dep_package, d.requirement) diff --git a/test/rubygems/test_gem_dependency_installer.rb b/test/rubygems/test_gem_dependency_installer.rb index bb52df4271fee6..e4da6cabd56f50 100644 --- a/test/rubygems/test_gem_dependency_installer.rb +++ b/test/rubygems/test_gem_dependency_installer.rb @@ -688,6 +688,25 @@ def test_install_force assert_equal %w[b-1], inst.installed_gems.map(&:full_name) end + def test_install_force_with_unsatisfiable_dep + # foo depends on bar >= 2.0, but only bar-1.0 exists. + # With --force, the unsatisfiable dep should be skipped. + _, foo_gem = util_gem "foo", "1" do |s| + s.add_dependency "bar", ">= 2.0" + end + + util_setup_spec_fetcher(util_spec("bar", "1.0")) + FileUtils.mv foo_gem, @tempdir + inst = nil + + Dir.chdir @tempdir do + inst = Gem::DependencyInstaller.new force: true + inst.install "foo" + end + + assert_equal %w[foo-1], inst.installed_gems.map(&:full_name) + end + def test_install_build_args util_setup_gems @@ -798,8 +817,7 @@ def test_install_domain_local inst.install "b" end - assert_match(/depends on a/, e.message) - assert_match(/no versions satisfy a/, e.message) + assert_match(/depends on unknown package a/, e.message) end assert_equal [], inst.installed_gems.map(&:full_name) diff --git a/test/rubygems/test_gem_resolver.rb b/test/rubygems/test_gem_resolver.rb index 384d8ca40327ca..18a0551448033a 100644 --- a/test/rubygems/test_gem_resolver.rb +++ b/test/rubygems/test_gem_resolver.rb @@ -938,4 +938,21 @@ def test_error_hints_about_prerelease_when_filtered assert_match(/--prerelease/, e.message) end + def test_soft_missing_skips_dep_with_wrong_version + a = util_spec "a", "1.0" do |s| + s.add_dependency "b", ">= 2.0" + end + + b = util_spec "b", "1.0" + + s = set(a, b) + ad = make_dep "a" + r = Gem::Resolver.new([ad], s) + r.soft_missing = true + + # b exists but only 1.0, which doesn't satisfy >= 2.0. + # With soft_missing (--force), the dep should be skipped. + assert_resolves_to [a], r + end + end From 759acb779390185e6840c4ab6c64c137d952fde0 Mon Sep 17 00:00:00 2001 From: Colby Swandale Date: Mon, 20 Apr 2026 15:27:30 +0900 Subject: [PATCH 081/188] [ruby/rubygems] Resolve issue with pre-release root dependencies being filtered incorrectly https://github.com/ruby/rubygems/commit/4c280ac057 --- lib/rubygems/resolver.rb | 16 ++++++++++++---- test/rubygems/test_gem_resolver.rb | 19 +++++++++++++++++-- test/rubygems/test_gem_resolver_strategy.rb | 8 ++++---- 3 files changed, 33 insertions(+), 10 deletions(-) diff --git a/lib/rubygems/resolver.rb b/lib/rubygems/resolver.rb index da2413befb27e7..67941b62cf1daa 100644 --- a/lib/rubygems/resolver.rb +++ b/lib/rubygems/resolver.rb @@ -321,7 +321,7 @@ def root_dependencies def filter_versions(package) all_versions = @all_versions[package] - if @set.respond_to?(:prerelease) && @set.prerelease + if (@set.respond_to?(:prerelease) && @set.prerelease) || root_requires_prerelease?(package) all_versions else stable = all_versions.reject(&:prerelease?) @@ -329,6 +329,14 @@ def filter_versions(package) end end + # Root deps with an explicit prerelease requirement (e.g. `= 4.1.0.dev`) + # must keep their prerelease versions in the filtered set; otherwise + # PubGrub cannot match them even though `@set.find_all` returned them. + def root_requires_prerelease?(package) + name = package.to_s + @needed.any? {|dep| dep.name == name && dep.requirement.prerelease? } + end + def find_unfiltered_specs_for(name) dep = Gem::Dependency.new(name, ">= 0.a") dep_request = DependencyRequest.new(dep, nil) @@ -385,7 +393,7 @@ def compute_dependencies(package, version) dep_package = package_for(d.name) - # In force mode, skip deps that can't be satisfied — either no + # In force mode, skip deps that can't be satisfied - either no # specs at all, or no specs matching the version requirement. if @soft_missing dep_specs = @all_specs[d.name] @@ -460,8 +468,8 @@ def extract_extended_explanation(incompatibility) [cause.conflict, cause.other].each do |incompat| if incompat.cause.is_a?(Gem::PubGrub::Incompatibility::NoVersions) && - incompat.respond_to?(:extended_explanation) && - incompat.extended_explanation + incompat.respond_to?(:extended_explanation) && + incompat.extended_explanation return incompat.extended_explanation end end diff --git a/test/rubygems/test_gem_resolver.rb b/test/rubygems/test_gem_resolver.rb index 18a0551448033a..18433c5e0f0739 100644 --- a/test/rubygems/test_gem_resolver.rb +++ b/test/rubygems/test_gem_resolver.rb @@ -776,7 +776,7 @@ def test_raises_and_explains_when_platform_prevents_install end def test_resolve_prerelease_not_considered_when_stable_exists - # a-1.0 depends on b ~> 2.0 — only b-2.0.pre satisfies that, but + # a-1.0 depends on b ~> 2.0 - only b-2.0.pre satisfies that, but # b also has a stable version (1.0), so prereleases are filtered out. # The resolver must fail, not silently use b-2.0.pre during propagation. a_stable = util_spec "a", "1.0" do |s| @@ -828,6 +828,22 @@ def test_resolve_prerelease_used_when_no_stable_versions_exist assert_resolves_to [a_stable, b_pre], r end + def test_resolve_prerelease_required_by_exact_requirement + # A root dep with an exact prerelease version must resolve to that + # version even when stable versions of the same gem are in the set. + # Gem.finish_resolve hits this: it imports loaded_specs as exact-version + # deps, so the currently-activated prerelease bundler becomes a root dep. + a_stable = util_spec "a", "1.0" + a_pre = util_spec "a", "2.0.pre" + + s = set(a_stable, a_pre) + + ad = make_dep "a", "= 2.0.pre" + r = Gem::Resolver.new([ad], s) + + assert_resolves_to [a_pre], r + end + def test_error_includes_platform_hint_when_specs_exist_for_other_platforms a = util_spec "a", "1.0" do |s| s.add_dependency "b", ">= 1.0" @@ -954,5 +970,4 @@ def test_soft_missing_skips_dep_with_wrong_version # With soft_missing (--force), the dep should be skipped. assert_resolves_to [a], r end - end diff --git a/test/rubygems/test_gem_resolver_strategy.rb b/test/rubygems/test_gem_resolver_strategy.rb index f84e8b6fdaa312..b8c6c86adfe321 100644 --- a/test/rubygems/test_gem_resolver_strategy.rb +++ b/test/rubygems/test_gem_resolver_strategy.rb @@ -4,8 +4,8 @@ class TestGemResolverStrategy < Gem::TestCase # Minimal source that implements the two methods Strategy calls: - # all_versions_for(package) — returns versions in preference order - # versions_for(package, range) — returns versions matching a range + # all_versions_for(package) - returns versions in preference order + # versions_for(package, range) - returns versions matching a range # # Tracks call counts so we can assert on caching behavior. class StubSource @@ -53,7 +53,7 @@ def make_range_between(min, max) end def test_most_preferred_version_respects_all_versions_for_ordering - # all_versions_for returns [2.0, 1.0, 3.0] — so 2.0 is most preferred + # all_versions_for returns [2.0, 1.0, 3.0] - so 2.0 is most preferred # even though 3.0 is numerically highest. pkg = make_package("a") source = StubSource.new("a" => [v("2.0"), v("1.0"), v("3.0")]) @@ -152,7 +152,7 @@ def test_cache_is_keyed_by_package_and_range strategy.next_package_and_version({ pkg => range_any }) calls_after_first = source.versions_for_calls - # Second call with different range — cache miss, so versions_for is called again + # Second call with different range - cache miss, so versions_for is called again strategy.next_package_and_version({ pkg => range_gte }) calls_after_second = source.versions_for_calls From a56f5b463582a92b5b4924f53a1c0229f04c43e4 Mon Sep 17 00:00:00 2001 From: Colby Swandale Date: Mon, 20 Apr 2026 23:56:35 +0900 Subject: [PATCH 082/188] [ruby/rubygems] Fix transitive prerelease filtering and clean up resolver internals https://github.com/ruby/rubygems/commit/a920da05ce --- lib/rubygems/resolver.rb | 70 +++++++++++---------- lib/rubygems/resolver/strategy.rb | 2 +- test/rubygems/test_gem_resolver.rb | 21 +++++++ test/rubygems/test_gem_resolver_strategy.rb | 10 +-- 4 files changed, 64 insertions(+), 39 deletions(-) diff --git a/lib/rubygems/resolver.rb b/lib/rubygems/resolver.rb index 67941b62cf1daa..bf75733d6bf640 100644 --- a/lib/rubygems/resolver.rb +++ b/lib/rubygems/resolver.rb @@ -109,7 +109,7 @@ def initialize(needed, set = nil) @all_specs = Hash.new {|h, name| h[name] = filter_specs(@unfiltered_specs[name]) } @all_versions = Hash.new {|h, pkg| h[pkg] = @all_specs[pkg.to_s].map(&:version).uniq.sort } @sorted_versions = Hash.new do |h, pkg| - h[pkg] = Gem::PubGrub::Package.root?(pkg) ? [@root_version] : filter_versions(pkg) + h[pkg] = Gem::PubGrub::Package.root?(pkg) ? [@root_version] : @all_versions[pkg] end @cached_dependencies = Hash.new do |h, pkg| h[pkg] = if Gem::PubGrub::Package.root?(pkg) @@ -133,14 +133,8 @@ def resolve all = @set.find_all(dep_request) matching = select_local_platforms(all) - if matching.empty? - exc = Gem::UnsatisfiableDependencyError.new(dep_request, all) - exc.errors = @set.errors - raise exc - end + next unless matching.empty? - specs_matching_requirement = matching.select {|s| dep.requirement.satisfied_by?(s.version) } - next unless specs_matching_requirement.empty? exc = Gem::UnsatisfiableDependencyError.new(dep_request, all) exc.errors = @set.errors raise exc @@ -149,7 +143,7 @@ def resolve solver = Gem::PubGrub::VersionSolver.new( source: self, root: @root_package, - strategy: Gem::Resolver::Strategy.new(self, @root_package), + strategy: Gem::Resolver::Strategy.new(self), logger: make_logger ) result = solver.solve @@ -198,7 +192,21 @@ def all_versions_for(package) end def versions_for(package, range = Gem::PubGrub::VersionRange.any) - @versions_for_cache[[package, range]] ||= range.select_versions(@sorted_versions[package]) + @versions_for_cache[[package, range]] ||= begin + candidates = range.select_versions(@sorted_versions[package]) + + if Gem::PubGrub::Package.root?(package) || + (@set.respond_to?(:prerelease) && @set.prerelease) || + range_admits_prerelease?(range) + candidates + elsif @all_versions[package].any? {|v| !v.prerelease? } + candidates.reject(&:prerelease?) + else + # Only prereleases exist for this gem; fall back to them so + # dependencies like `>= 1.0` can still be satisfied. + candidates + end + end end def no_versions_incompatibility_for(_package, unsatisfied_term) @@ -307,9 +315,6 @@ def package_for(name) @packages[name] ||= Gem::PubGrub::Package.new(name) end - # Filter versions to exclude prereleases unless prerelease is enabled. - # Both all_versions_for and versions_for use this filtered set to ensure - # PubGrub's constraint propagation and version selection are consistent. def root_dependencies deps = {} @needed.each do |dep| @@ -319,24 +324,16 @@ def root_dependencies deps end - def filter_versions(package) - all_versions = @all_versions[package] - if (@set.respond_to?(:prerelease) && @set.prerelease) || root_requires_prerelease?(package) - all_versions - else - stable = all_versions.reject(&:prerelease?) - stable.empty? ? all_versions : stable + # Only the min bound is inspected: `~>` synthesises a max like `X.A` + # whose suffix looks prerelease to Gem::Version but is not the user's + # intent, so checking max would mis-admit prereleases for every `~>`. + def range_admits_prerelease?(range) + range.ranges.any? do |r| + next false if r.empty? + r.min&.prerelease? end end - # Root deps with an explicit prerelease requirement (e.g. `= 4.1.0.dev`) - # must keep their prerelease versions in the filtered set; otherwise - # PubGrub cannot match them even though `@set.find_all` returned them. - def root_requires_prerelease?(package) - name = package.to_s - @needed.any? {|dep| dep.name == name && dep.requirement.prerelease? } - end - def find_unfiltered_specs_for(name) dep = Gem::Dependency.new(name, ">= 0.a") dep_request = DependencyRequest.new(dep, nil) @@ -413,9 +410,17 @@ def build_extended_explanation(name, constraint) filtered = @all_specs[name] pkg = package_for(name) - has_prerelease_filtering = @all_versions[pkg].length > @sorted_versions[pkg].length - return if filtered.length == unfiltered.length && !has_prerelease_filtering + # A prerelease hint applies when the source would strip prereleases for + # this constraint (global prerelease flag off and the constraint's range + # doesn't itself reach into prerelease territory) AND a prerelease of + # the gem exists somewhere. + prerelease_gated = !(@set.respond_to?(:prerelease) && @set.prerelease) && + !range_admits_prerelease?(constraint.range) + has_prerelease_candidate = prerelease_gated && + @all_versions[pkg].any?(&:prerelease?) + + return if filtered.length == unfiltered.length && !has_prerelease_candidate hints = [] @@ -450,9 +455,8 @@ def build_extended_explanation(name, constraint) end # Check for specs filtered by prerelease status - unless @set.respond_to?(:prerelease) && @set.prerelease - pkg = package_for(name) - prerelease_versions = @all_versions[pkg].select(&:prerelease?) - @sorted_versions[pkg] + if prerelease_gated + prerelease_versions = @all_versions[pkg].select(&:prerelease?) if prerelease_versions.any? versions = prerelease_versions.sort.reverse.first(3) # limit to avoid cluttering error output hints << "#{name} #{versions.join(", ")} are pre-release versions. Use --prerelease to allow pre-release gems." diff --git a/lib/rubygems/resolver/strategy.rb b/lib/rubygems/resolver/strategy.rb index 1a1c16261906f5..8f3bebc068f953 100644 --- a/lib/rubygems/resolver/strategy.rb +++ b/lib/rubygems/resolver/strategy.rb @@ -5,7 +5,7 @@ # calls during the solver's package selection loop. class Gem::Resolver::Strategy - def initialize(source, root_package) + def initialize(source) @source = source @package_priority_cache = {} diff --git a/test/rubygems/test_gem_resolver.rb b/test/rubygems/test_gem_resolver.rb index 18433c5e0f0739..d8ffc904738dfa 100644 --- a/test/rubygems/test_gem_resolver.rb +++ b/test/rubygems/test_gem_resolver.rb @@ -844,6 +844,27 @@ def test_resolve_prerelease_required_by_exact_requirement assert_resolves_to [a_pre], r end + def test_resolve_transitive_prerelease_required_by_exact_requirement + # A transitive dep with an exact prerelease version must resolve to that + # version even when stable versions of the same gem are in the set. + # The gate on prereleases lives in versions_for and is per-constraint: + # `= 2.0.pre` carries a prerelease bound, so prereleases are admitted for + # this range even though the global prerelease flag is off. + a = util_spec "a", "1.0" do |s| + s.add_dependency "b", "= 2.0.pre" + end + + b_stable = util_spec "b", "1.0" + b_pre = util_spec "b", "2.0.pre" + + s = set(a, b_stable, b_pre) + + ad = make_dep "a" + r = Gem::Resolver.new([ad], s) + + assert_resolves_to [a, b_pre], r + end + def test_error_includes_platform_hint_when_specs_exist_for_other_platforms a = util_spec "a", "1.0" do |s| s.add_dependency "b", ">= 1.0" diff --git a/test/rubygems/test_gem_resolver_strategy.rb b/test/rubygems/test_gem_resolver_strategy.rb index b8c6c86adfe321..57c9aadde8ab9f 100644 --- a/test/rubygems/test_gem_resolver_strategy.rb +++ b/test/rubygems/test_gem_resolver_strategy.rb @@ -58,7 +58,7 @@ def test_most_preferred_version_respects_all_versions_for_ordering pkg = make_package("a") source = StubSource.new("a" => [v("2.0"), v("1.0"), v("3.0")]) - strategy = Gem::Resolver::Strategy.new(source, Gem::PubGrub::Package.root) + strategy = Gem::Resolver::Strategy.new(source) unsatisfied = { pkg => make_range_any } _package, version = strategy.next_package_and_version(unsatisfied) @@ -77,7 +77,7 @@ def test_picks_most_constrained_package "b" => [v("1.0")] ) - strategy = Gem::Resolver::Strategy.new(source, Gem::PubGrub::Package.root) + strategy = Gem::Resolver::Strategy.new(source) unsatisfied = { pkg_a => make_range_any, @@ -104,7 +104,7 @@ def test_picks_package_with_fewer_higher_versions_as_tiebreaker "b" => [v("3.0"), v("2.0"), v("1.0")] ) - strategy = Gem::Resolver::Strategy.new(source, Gem::PubGrub::Package.root) + strategy = Gem::Resolver::Strategy.new(source) unsatisfied = { pkg_a => range, @@ -120,7 +120,7 @@ def test_cache_prevents_redundant_versions_for_calls pkg = make_package("a") source = StubSource.new("a" => [v("2.0"), v("1.0")]) - strategy = Gem::Resolver::Strategy.new(source, Gem::PubGrub::Package.root) + strategy = Gem::Resolver::Strategy.new(source) range = make_range_any unsatisfied = { pkg => range } @@ -143,7 +143,7 @@ def test_cache_is_keyed_by_package_and_range pkg = make_package("a") source = StubSource.new("a" => [v("3.0"), v("2.0"), v("1.0")]) - strategy = Gem::Resolver::Strategy.new(source, Gem::PubGrub::Package.root) + strategy = Gem::Resolver::Strategy.new(source) range_any = make_range_any range_gte = make_range_gte(v("2.0")) From d9fb42701a184f8218eaf5c2c3684c8148c1be33 Mon Sep 17 00:00:00 2001 From: Colby Swandale Date: Tue, 21 Apr 2026 00:29:58 +0900 Subject: [PATCH 083/188] [ruby/rubygems] Performance optimisations https://github.com/ruby/rubygems/commit/efdbd007b8 --- lib/rubygems/resolver.rb | 17 ++++++++++------- lib/rubygems/resolver/strategy.rb | 4 ++-- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/lib/rubygems/resolver.rb b/lib/rubygems/resolver.rb index bf75733d6bf640..fafc357c10a72a 100644 --- a/lib/rubygems/resolver.rb +++ b/lib/rubygems/resolver.rb @@ -119,7 +119,8 @@ def initialize(needed, set = nil) end end @version_to_index = Hash.new {|h, pkg| h[pkg] = @sorted_versions[pkg].each_with_index.to_h } - @versions_for_cache = {} + @versions_for_cache = Hash.new {|h, pkg| h[pkg] = {} } + @spec_for_cache = Hash.new {|h, name| h[name] = build_spec_for_cache(name) } end ## @@ -192,7 +193,7 @@ def all_versions_for(package) end def versions_for(package, range = Gem::PubGrub::VersionRange.any) - @versions_for_cache[[package, range]] ||= begin + @versions_for_cache[package][range] ||= begin candidates = range.select_versions(@sorted_versions[package]) if Gem::PubGrub::Package.root?(package) || @@ -357,18 +358,20 @@ def filter_specs(specs) end def spec_for(name, version) - candidates = @all_specs[name].select {|s| s.version == version } + @spec_for_cache[name][version] + end + + def build_spec_for_cache(name) + @all_specs[name].group_by(&:version).transform_values do |candidates| + next candidates.first if candidates.length == 1 - if candidates.length > 1 # Prefer already-installed specs to avoid unnecessary downloads installed = candidates.select {|s| s.is_a?(Gem::Resolver::InstalledSpecification) } - return installed.first if installed.length == 1 + next installed.first if installed.length == 1 candidates = installed if installed.any? # Among remaining candidates, prefer the most specific platform candidates.min_by {|s| Gem::Platform.platform_specificity_match(s.platform, Gem::Platform.local) } - else - candidates.first end end diff --git a/lib/rubygems/resolver/strategy.rb b/lib/rubygems/resolver/strategy.rb index 8f3bebc068f953..bf0dbb6adc3580 100644 --- a/lib/rubygems/resolver/strategy.rb +++ b/lib/rubygems/resolver/strategy.rb @@ -7,7 +7,7 @@ class Gem::Resolver::Strategy def initialize(source) @source = source - @package_priority_cache = {} + @package_priority_cache = Hash.new {|h, pkg| h[pkg] = {} } @version_indexes = Hash.new do |h, k| if Gem::PubGrub::Package.root?(k) @@ -33,7 +33,7 @@ def most_preferred_version_of(package, range) def next_term_to_try_from(unsatisfied) unsatisfied.min_by do |package, range| - @package_priority_cache[[package, range]] ||= begin + @package_priority_cache[package][range] ||= begin matching_versions = @source.versions_for(package, range) higher_versions = @source.versions_for(package, range.upper_invert) From df20297a3e6ab094f1bdc7160e252052cd7b1853 Mon Sep 17 00:00:00 2001 From: Colby Swandale Date: Tue, 26 May 2026 16:19:21 +1000 Subject: [PATCH 084/188] [ruby/rubygems] resolver performance optimisations https://github.com/ruby/rubygems/commit/9f585e6d0e --- lib/rubygems/resolver.rb | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/rubygems/resolver.rb b/lib/rubygems/resolver.rb index fafc357c10a72a..350cfd964bb809 100644 --- a/lib/rubygems/resolver.rb +++ b/lib/rubygems/resolver.rb @@ -346,9 +346,8 @@ def filter_specs(specs) unless @soft_missing filtered = filtered.select do |s| - actual = s.respond_to?(:spec) ? s.spec : s - actual.required_ruby_version.satisfied_by?(Gem.ruby_version) && - actual.required_rubygems_version.satisfied_by?(Gem.rubygems_version) + s.required_ruby_version.satisfied_by?(Gem.ruby_version) && + s.required_rubygems_version.satisfied_by?(Gem.rubygems_version) rescue StandardError true end @@ -385,8 +384,7 @@ def compute_dependencies(package, version) deps = {} root_names = @needed.map(&:name) - actual_spec = spec.respond_to?(:spec) ? spec.spec : spec - actual_spec.dependencies.each do |d| + spec.dependencies.each do |d| next if d.name == package.to_s next if d.type == :development && !@development next if d.type == :development && @development_shallow && !root_names.include?(package.to_s) From 1a39445013e6fc8801e3b61ce7208f70d90825c5 Mon Sep 17 00:00:00 2001 From: Colby Swandale Date: Tue, 2 Jun 2026 13:50:48 +1000 Subject: [PATCH 085/188] [ruby/rubygems] Fix resolver eliminating all gem versions on a missing dependency https://github.com/ruby/rubygems/commit/22134cc22b --- lib/rubygems/resolver.rb | 45 ++++++-------- test/rubygems/test_gem_resolver.rb | 95 ++++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+), 27 deletions(-) diff --git a/lib/rubygems/resolver.rb b/lib/rubygems/resolver.rb index 350cfd964bb809..c142017efb4498 100644 --- a/lib/rubygems/resolver.rb +++ b/lib/rubygems/resolver.rb @@ -235,19 +235,6 @@ def incompatibilities_for(package, version) package_deps[version].filter_map do |dep_package_name, dep_constraint| dep_package = dep_constraint.package - # If no specs exist at all for this dependency (not even for other - # platforms or Ruby versions), mark this version as invalid. - # When specs exist but were filtered out, let PubGrub discover it - # via NoVersions so platform/ruby hints are generated. - if @unfiltered_specs[dep_package_name].empty? - self_constraint = Gem::PubGrub::VersionConstraint.new(package, range: Gem::PubGrub::VersionRange.any) - cause = Gem::PubGrub::Incompatibility::InvalidDependency.new(dep_package, dep_constraint) - return [Gem::PubGrub::Incompatibility.new( - [Gem::PubGrub::Term.new(self_constraint, true)], - cause: cause - )] - end - low = high = @version_to_index[package][version] # find version low such that all >= low share the same dep @@ -277,21 +264,25 @@ def incompatibilities_for(package, version) range = Gem::PubGrub::VersionRange.new(min: low, max: high, include_min: !low.nil?) self_constraint = Gem::PubGrub::VersionConstraint.new(package, range: range) + # No specs anywhere means an unknown package. Check @unfiltered_specs, not + # the filtered set, so a dep filtered out by platform/Ruby/prerelease falls + # through to NoVersions for proper hints instead. The band-scoped + # self_constraint lets clean sibling versions still resolve via backtracking. + if @unfiltered_specs[dep_package_name].empty? + cause = Gem::PubGrub::Incompatibility::InvalidDependency.new(dep_package, dep_constraint) + return [Gem::PubGrub::Incompatibility.new( + [Gem::PubGrub::Term.new(self_constraint, true)], + cause: cause + )] + end + + # An empty range means the requirement is self-contradictory (e.g. `> 2, < 1`). if dep_constraint.range.empty? - if @unfiltered_specs[dep_package_name].any? - # Package exists but requirement is self-contradictory - return [Gem::Resolver::Incompatibility.new( - [Gem::PubGrub::Term.new(self_constraint, true)], - cause: Gem::PubGrub::Incompatibility::NoVersions.new(dep_constraint), - custom_explanation: "#{dep_package_name} cannot satisfy contradictory requirements #{dep_constraint.constraint_string}" - )] - else - cause = Gem::PubGrub::Incompatibility::InvalidDependency.new(dep_package, dep_constraint) - return [Gem::PubGrub::Incompatibility.new( - [Gem::PubGrub::Term.new(self_constraint, true)], - cause: cause - )] - end + return [Gem::Resolver::Incompatibility.new( + [Gem::PubGrub::Term.new(self_constraint, true)], + cause: Gem::PubGrub::Incompatibility::NoVersions.new(dep_constraint), + custom_explanation: "#{dep_package_name} cannot satisfy contradictory requirements #{dep_constraint.constraint_string}" + )] end Gem::PubGrub::Incompatibility.new( diff --git a/test/rubygems/test_gem_resolver.rb b/test/rubygems/test_gem_resolver.rb index d8ffc904738dfa..a0caa0cbc8f87a 100644 --- a/test/rubygems/test_gem_resolver.rb +++ b/test/rubygems/test_gem_resolver.rb @@ -991,4 +991,99 @@ def test_soft_missing_skips_dep_with_wrong_version # With soft_missing (--force), the dep should be skipped. assert_resolves_to [a], r end + + def test_backtracks_to_clean_sibling_when_higher_version_has_missing_dep + a1 = util_spec "a", "1" + a2 = util_spec "a", "2" do |s| + s.add_dependency "zzz", ">= 1" + end + + r = Gem::Resolver.new([make_dep("a")], set(a1, a2)) + + # 'zzz' has zero specs anywhere, so a-2 is unusable, but a-1 is clean + # and resolution must backtrack to it rather than declaring every + # version of 'a' invalid. + assert_resolves_to [a1], r + end + + def test_backtracks_over_band_of_bad_high_versions_to_clean_lower + a1 = util_spec "a", "1" + a2 = util_spec "a", "2" do |s| + s.add_dependency "zzz", ">= 1" + end + a3 = util_spec "a", "3" do |s| + s.add_dependency "zzz", ">= 1" + end + + r = Gem::Resolver.new([make_dep("a")], set(a1, a2, a3)) + + # Only the a-2..a-3 band shares the missing 'zzz' dep and should be + # eliminated; band scoping is load-bearing here, not just sibling + # presence. + assert_resolves_to [a1], r + end + + def test_backtracks_when_one_of_several_deps_is_missing + good = util_spec "good", "1" + a1 = util_spec "a", "1" do |s| + s.add_dependency "good", ">= 1" + end + a2 = util_spec "a", "2" do |s| + s.add_dependency "good", ">= 1" + s.add_dependency "zzz", ">= 1" + end + + r = Gem::Resolver.new([make_dep("a")], set(a1, a2, good)) + + # Only a-2, which carries the missing 'zzz' dep, is eliminated; the + # per-dep check inside a multi-dep version must not poison a-1. + assert_resolves_to [a1, good], r + end + + def test_fails_when_every_version_depends_on_missing_package + a1 = util_spec "a", "1" do |s| + s.add_dependency "zzz", ">= 1" + end + a2 = util_spec "a", "2" do |s| + s.add_dependency "zzz", ">= 1" + end + + r = Gem::Resolver.new([make_dep("a")], set(a1, a2)) + + e = assert_raise Gem::DependencyResolutionError do + r.resolve + end + + assert_match(/every version of a depends on unknown package zzz/, e.message) + end + + def test_resolves_when_only_lowest_version_has_missing_dep + a1 = util_spec "a", "1" do |s| + s.add_dependency "zzz", ">= 1" + end + a2 = util_spec "a", "2" + + r = Gem::Resolver.new([make_dep("a")], set(a1, a2)) + + # a-2 is preferred/tried first, so this is already green; it guards + # against the bug being re-introduced in an order-sensitive way. + assert_resolves_to [a2], r + end + + def test_filtered_platform_dep_lets_clean_sibling_backtrack + a1 = util_spec "a", "1" + a2 = util_spec "a", "2" do |s| + s.add_dependency "b", ">= 1.0" + end + b_java = util_spec "b", "1.0" do |s| + s.platform = "java" + end + + r = Gem::Resolver.new([make_dep("a")], set(a1, a2, b_java)) + + # 'b' EXISTS in the unfiltered specs but is platform-filtered, so a-2 + # is unusable via NoVersions (not InvalidDependency). Resolution must + # backtrack to the clean a-1 rather than eliminating it. + assert_resolves_to [a1], r + end end From 156935faa9f9ae65073ab0d08d9452919c63f87d Mon Sep 17 00:00:00 2001 From: Colby Swandale Date: Tue, 2 Jun 2026 14:09:46 +1000 Subject: [PATCH 086/188] [ruby/rubygems] Remove dangling resolver stats call from --explain path https://github.com/ruby/rubygems/commit/8bc787e863 --- lib/rubygems/request_set.rb | 4 ---- test/rubygems/test_gem_request_set.rb | 28 +++++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/lib/rubygems/request_set.rb b/lib/rubygems/request_set.rb index c06ef32da9498a..eb8b4658f3b6c6 100644 --- a/lib/rubygems/request_set.rb +++ b/lib/rubygems/request_set.rb @@ -234,10 +234,6 @@ def install_from_gemdeps(options, &block) sorted_requests.each do |spec| puts " #{spec.full_name}" end - - if Gem.configuration.really_verbose - @resolver.stats.display - end else installed = install options, &block diff --git a/test/rubygems/test_gem_request_set.rb b/test/rubygems/test_gem_request_set.rb index 6ebc95ea20f218..33054aa8e50cc7 100644 --- a/test/rubygems/test_gem_request_set.rb +++ b/test/rubygems/test_gem_request_set.rb @@ -93,6 +93,34 @@ def test_install_from_gemdeps_explain end end + def test_install_from_gemdeps_explain_verbose + spec_fetcher do |fetcher| + fetcher.gem "a", 2 + end + + rs = Gem::RequestSet.new + + verbose = Gem.configuration.verbose + Gem.configuration.verbose = :really + + File.open "gem.deps.rb", "w" do |io| + io.puts 'gem "a"' + io.flush + + expected = <<-EXPECTED +Gems to install: + a-2 + EXPECTED + + actual, _ = capture_output do + rs.install_from_gemdeps gemdeps: io.path, explain: true + end + assert_equal(expected, actual) + end + ensure + Gem.configuration.verbose = verbose + end + def test_install_from_gemdeps_install_dir spec_fetcher do |fetcher| fetcher.gem "a", 2 From f7932a5318b233302ae3e21cf55c5f26ecc6b4f8 Mon Sep 17 00:00:00 2001 From: Colby Swandale Date: Tue, 2 Jun 2026 15:16:43 +1000 Subject: [PATCH 087/188] [ruby/rubygems] Prefer the earlier source for same-version dependency ties When version and platform match across sources, prefer the earlier source. This formalises existing behaviour and matches Bundler. Cross-version selection is unchanged. https://github.com/ruby/rubygems/commit/8f2cd72650 --- lib/rubygems/resolver.rb | 15 +++++++++++-- test/rubygems/test_gem_resolver.rb | 36 ++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/lib/rubygems/resolver.rb b/lib/rubygems/resolver.rb index c142017efb4498..208ee623a2709d 100644 --- a/lib/rubygems/resolver.rb +++ b/lib/rubygems/resolver.rb @@ -352,6 +352,13 @@ def spec_for(name, version) end def build_spec_for_cache(name) + # Rank sources by the order they were first supplied so that, when multiple + # sources offer the same version and platform, the earlier source wins. + source_rank = {} + @all_specs[name].each do |s| + source_rank[s.source] ||= source_rank.size + end + @all_specs[name].group_by(&:version).transform_values do |candidates| next candidates.first if candidates.length == 1 @@ -360,8 +367,12 @@ def build_spec_for_cache(name) next installed.first if installed.length == 1 candidates = installed if installed.any? - # Among remaining candidates, prefer the most specific platform - candidates.min_by {|s| Gem::Platform.platform_specificity_match(s.platform, Gem::Platform.local) } + # Among remaining candidates, prefer the most specific platform, then the + # earlier-supplied source. + candidates.min_by do |s| + [Gem::Platform.platform_specificity_match(s.platform, Gem::Platform.local), + source_rank[s.source]] + end end end diff --git a/test/rubygems/test_gem_resolver.rb b/test/rubygems/test_gem_resolver.rb index a0caa0cbc8f87a..a6319e4a65516a 100644 --- a/test/rubygems/test_gem_resolver.rb +++ b/test/rubygems/test_gem_resolver.rb @@ -722,6 +722,42 @@ def test_picks_highest_version_across_sources assert_resolves_to [spec_a_2], resolver end + def test_same_version_prefers_earlier_source + source_a = Gem::Source.new "http://example.com/a" + source_b = Gem::Source.new "http://example.com/b" + + spec_a = util_spec "some-dep", "1.0.0" + spec_b = util_spec "some-dep", "1.0.0" + + set = StaticSet.new [ + Gem::Resolver::SpecSpecification.new(nil, spec_a, source_a), + Gem::Resolver::SpecSpecification.new(nil, spec_b, source_b), + ] + + resolver = Gem::Resolver.new [make_dep("some-dep", "> 0")], set + result = resolver.resolve + + assert_equal source_a, result.first.spec.source + end + + def test_same_version_prefers_earlier_source_when_order_flipped + source_a = Gem::Source.new "http://example.com/a" + source_b = Gem::Source.new "http://example.com/b" + + spec_a = util_spec "some-dep", "1.0.0" + spec_b = util_spec "some-dep", "1.0.0" + + set = StaticSet.new [ + Gem::Resolver::SpecSpecification.new(nil, spec_b, source_b), + Gem::Resolver::SpecSpecification.new(nil, spec_a, source_a), + ] + + resolver = Gem::Resolver.new [make_dep("some-dep", "> 0")], set + result = resolver.resolve + + assert_equal source_b, result.first.spec.source + end + def test_select_local_platforms r = Gem::Resolver.new nil, nil From 5e3e2a4047415f59df59f6fd3fad7058d53773eb Mon Sep 17 00:00:00 2001 From: Colby Swandale Date: Tue, 2 Jun 2026 17:12:08 +1000 Subject: [PATCH 088/188] [ruby/rubygems] Include the missing dependency's version in resolver errors & test and document Bundler-aligned resolver behaviors https://github.com/ruby/rubygems/commit/3d5dfa91b9 --- lib/rubygems/resolver.rb | 35 ++++++++++-- .../rubygems/test_gem_dependency_installer.rb | 2 +- test/rubygems/test_gem_resolver.rb | 54 ++++++++++++++++++- 3 files changed, 85 insertions(+), 6 deletions(-) diff --git a/lib/rubygems/resolver.rb b/lib/rubygems/resolver.rb index 208ee623a2709d..788206c0566fd5 100644 --- a/lib/rubygems/resolver.rb +++ b/lib/rubygems/resolver.rb @@ -127,7 +127,14 @@ def initialize(needed, set = nil) # Proceed with resolution! Returns an array of ActivationRequest objects. def resolve - # Pre-check: raise UnsatisfiableDependencyError for root deps with no matches + # Pre-check: raise UnsatisfiableDependencyError for root deps with no + # platform match. We filter by platform ONLY here (not required_ruby_version + # / required_rubygems_version): a foreign-platform gem is genuinely "not + # found", but a gem that exists yet is incompatible with the running Ruby + # should flow through the solver to a DependencyResolutionError that names + # the Ruby requirement. That matches Bundler (which models Ruby as a + # synthetic dependency, so this surfaces as a solve failure) and gives a + # clearer message than the platform-oriented UnsatisfiableDependencyError. @needed.each do |dep| next if @soft_missing dep_request = DependencyRequest.new(dep, nil) @@ -175,6 +182,21 @@ def all_versions_for(package) name = package.to_s if (skip_dep_gems = skip_gems[name]) && !skip_dep_gems.empty? + # Conservative mode: float the already-installed (skip) versions to the + # front so the solver prefers them. This sets *preference* only (it feeds + # the strategy's version-index map); it does not restrict availability, so + # every version stays selectable via versions_for. When an installed + # version is made impossible by a downstream conflict, the solver + # backtracks to a newer version instead of failing. Molinillo instead + # hard-restricted the candidate set to skip versions and raised. + # + # This reaches the same outcome as Bundler (upgrade-over-raise) for the + # common single-blocked-gem case, though the mechanism differs: Bundler + # hard-pins locked gems and selectively unlocks + re-solves on conflict, + # whereas we float as a preference and let PubGrub backtrack in one solve. + # The float can therefore over-upgrade when several installed gems are + # jointly involved in a conflict; that outcome-level divergence is + # accepted (see test_conservative_upgrades_when_installed_blocked). skip_versions = skip_dep_gems.map(&:version) preferred, rest = versions.partition {|v| skip_versions.include?(v) } preferred + rest @@ -270,9 +292,16 @@ def incompatibilities_for(package, version) # self_constraint lets clean sibling versions still resolve via backtracking. if @unfiltered_specs[dep_package_name].empty? cause = Gem::PubGrub::Incompatibility::InvalidDependency.new(dep_package, dep_constraint) + self_term = Gem::PubGrub::Term.new(self_constraint, true) + # PubGrub's default InvalidDependency rendering drops the version + # requirement ("depends on unknown package bar"). Supply a custom + # explanation so the missing dependency's constraint is preserved + # ("depends on bar = 0.5 which could not be found in any repository"), + # matching Molinillo's diagnostics. return [Gem::PubGrub::Incompatibility.new( - [Gem::PubGrub::Term.new(self_constraint, true)], - cause: cause + [self_term], + cause: cause, + custom_explanation: "#{self_term.to_s(allow_every: true)} depends on #{dep_constraint} which could not be found in any repository" )] end diff --git a/test/rubygems/test_gem_dependency_installer.rb b/test/rubygems/test_gem_dependency_installer.rb index e4da6cabd56f50..c2fb6f264b92a1 100644 --- a/test/rubygems/test_gem_dependency_installer.rb +++ b/test/rubygems/test_gem_dependency_installer.rb @@ -817,7 +817,7 @@ def test_install_domain_local inst.install "b" end - assert_match(/depends on unknown package a/, e.message) + assert_match(/depends on a >= 0 which could not be found in any repository/, e.message) end assert_equal [], inst.installed_gems.map(&:full_name) diff --git a/test/rubygems/test_gem_resolver.rb b/test/rubygems/test_gem_resolver.rb index a6319e4a65516a..84ede36b6c85d7 100644 --- a/test/rubygems/test_gem_resolver.rb +++ b/test/rubygems/test_gem_resolver.rb @@ -140,6 +140,34 @@ def test_resolve_conservative assert_resolves_to [a2_spec, b2_spec, c1_spec, d2_spec, e1_spec], res end + def test_conservative_upgrades_when_installed_blocked + # Conservative mode floats the installed (skip) version to the front but + # keeps newer versions selectable. When the installed version cannot be + # used because its own dependency is unsatisfiable, the solver backtracks + # to a newer version instead of failing. This intentionally diverges from + # Molinillo (which hard-restricted to skip versions and raised) and reaches + # Bundler's upgrade-over-raise outcome. See the comment in + # Gem::Resolver#all_versions_for. + a1_spec = util_spec "a", 1 do |s| + s.add_dependency "b", ">= 2" + end + a2_spec = util_spec "a", 2 do |s| + s.add_dependency "b", ">= 1" + end + b1_spec = util_spec "b", 1 + + # b-2 is intentionally absent, so a-1's `b >= 2` cannot be satisfied. + deps = [make_dep("a", ">= 1")] + s = set a1_spec, a2_spec, b1_spec + + res = Gem::Resolver.new deps, s + # a-1 is already installed and satisfies `a >= 1`, so conservative mode + # prefers it - but it is blocked by the missing b-2, forcing an upgrade. + res.skip_gems = { "a" => [a1_spec] } + + assert_resolves_to [a2_spec, b1_spec], res + end + def test_resolve_development a_spec = util_spec "a", 1 do |s| s.add_development_dependency "b" @@ -516,7 +544,7 @@ def test_raises_and_reports_an_implicit_request_properly r.resolve end - assert_match(/depends on unknown package b/, e.message) + assert_match(/depends on b = 2 which could not be found in any repository/, e.message) end def test_raises_when_possibles_are_exhausted @@ -945,6 +973,28 @@ def test_error_includes_ruby_version_hint_when_filtered assert_match(/you have/, e.message) end + def test_root_gem_incompatible_ruby_version_names_ruby_requirement + # A requested (root) gem available only for an incompatible Ruby version + # flows through the solver to a DependencyResolutionError whose message + # names the Ruby requirement. This matches Bundler (which models Ruby as a + # synthetic dependency and reports a solve failure) and is clearer than the + # platform-oriented UnsatisfiableDependencyError. Contrast the foreign- + # *platform* case (test_raises_and_explains_when_platform_prevents_install), + # which is genuinely "not found" and does raise UnsatisfiableDependencyError. + a = util_spec "a", "1.0" do |s| + s.required_ruby_version = ">= 999.0" + end + + ad = make_dep "a", "= 1.0" + r = Gem::Resolver.new([ad], set(a)) + + e = assert_raise Gem::DependencyResolutionError do + r.resolve + end + + assert_match(/requires Ruby >= 999.0/, e.message) + end + def test_self_dependency_does_not_crash a = util_spec "a", "1.0" do |s| s.add_dependency "a" @@ -1090,7 +1140,7 @@ def test_fails_when_every_version_depends_on_missing_package r.resolve end - assert_match(/every version of a depends on unknown package zzz/, e.message) + assert_match(/every version of a depends on zzz >= 1 which could not be found in any repository/, e.message) end def test_resolves_when_only_lowest_version_has_missing_dep From f916141a2db972c39ad73ad584d505cc4dc63cb8 Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Tue, 2 Jun 2026 15:52:47 +0900 Subject: [PATCH 089/188] [DOC] Fix hash style for Struct --- struct.c | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/struct.c b/struct.c index 5ac67dad2c3eb0..7a592b42c548c8 100644 --- a/struct.c +++ b/struct.c @@ -628,7 +628,7 @@ rb_struct_define_under(VALUE outer, const char *name, ...) * PositionalOnly.new(0, 1) * # => # * PositionalOnly.new(bar: 1, foo: 0) - * # => #1, :bar=>2}, bar=nil> + * # => # * # Note that no error is raised, but arguments treated as one hash value * * # Same as not providing keyword_init: @@ -1076,14 +1076,14 @@ rb_struct_to_a(VALUE s) * Customer = Struct.new(:name, :address, :zip) * joe = Customer.new("Joe Smith", "123 Maple, Anytown NC", 12345) * h = joe.to_h - * h # => {:name=>"Joe Smith", :address=>"123 Maple, Anytown NC", :zip=>12345} + * h # => {name: "Joe Smith", address: "123 Maple, Anytown NC", zip: 12345} * * If a block is given, it is called with each name/value pair; * the block should return a 2-element array whose elements will become * a key/value pair in the returned hash: * * h = joe.to_h{|name, value| [name.upcase, value.to_s.upcase]} - * h # => {:NAME=>"JOE SMITH", :ADDRESS=>"123 MAPLE, ANYTOWN NC", :ZIP=>"12345"} + * h # => {NAME: "JOE SMITH", ADDRESS: "123 MAPLE, ANYTOWN NC", ZIP: "12345"} * * Raises ArgumentError if the block returns an inappropriate value. * @@ -1116,12 +1116,12 @@ rb_struct_to_h(VALUE s) * Customer = Struct.new(:name, :address, :zip) * joe = Customer.new("Joe Smith", "123 Maple, Anytown NC", 12345) * h = joe.deconstruct_keys([:zip, :address]) - * h # => {:zip=>12345, :address=>"123 Maple, Anytown NC"} + * h # => {zip: 12345, address: "123 Maple, Anytown NC"} * * Returns all names and values if +array_of_names+ is +nil+: * * h = joe.deconstruct_keys(nil) - * h # => {:name=>"Joseph Smith, Jr.", :address=>"123 Maple, Anytown NC", :zip=>12345} + * h # => {name: "Joseph Smith, Jr.", address: "123 Maple, Anytown NC", zip: 12345} * */ static VALUE @@ -1565,8 +1565,8 @@ rb_struct_size(VALUE s) * * Foo = Struct.new(:a) * f = Foo.new(Foo.new({b: [1, 2, 3]})) - * f.dig(:a) # => #[1, 2, 3]}> - * f.dig(:a, :a) # => {:b=>[1, 2, 3]} + * f.dig(:a) # => # + * f.dig(:a, :a) # => {b: [1, 2, 3]} * f.dig(:a, :a, :b) # => [1, 2, 3] * f.dig(:a, :a, :b, 0) # => 1 * f.dig(:b, 0) # => nil @@ -1574,8 +1574,8 @@ rb_struct_size(VALUE s) * Given integer argument +n+, * returns the object that is specified by +n+ and +identifiers+: * - * f.dig(0) # => #[1, 2, 3]}> - * f.dig(0, 0) # => {:b=>[1, 2, 3]} + * f.dig(0) # => # + * f.dig(0, 0) # => {b: [1, 2, 3]} * f.dig(0, 0, :b) # => [1, 2, 3] * f.dig(0, 0, :b, 0) # => 1 * f.dig(:b, 0) # => nil @@ -2034,7 +2034,7 @@ rb_data_inspect(VALUE s) * distance = Measure[10, 'km'] * * distance.to_h - * #=> {:amount=>10, :unit=>"km"} + * #=> {amount: 10, unit: "km"} * * Like Enumerable#to_h, if the block is provided, it is expected to * produce key-value pairs to construct a hash: @@ -2108,8 +2108,8 @@ rb_data_inspect(VALUE s) * Measure = Data.define(:amount, :unit) * * distance = Measure[10, 'km'] - * distance.deconstruct_keys(nil) #=> {:amount=>10, :unit=>"km"} - * distance.deconstruct_keys([:amount]) #=> {:amount=>10} + * distance.deconstruct_keys(nil) #=> {amount: 10, unit: "km"} + * distance.deconstruct_keys([:amount]) #=> {amount: 10} * * # usage * case distance From 9feac5d10eeddb6d3138c3290287c2460ad9577b Mon Sep 17 00:00:00 2001 From: Daichi Kamiyama <32436625+dak2@users.noreply.github.com> Date: Tue, 2 Jun 2026 23:09:39 +0900 Subject: [PATCH 090/188] ZJIT: Change return type of FixnumDiv to Integer (#17165) `FIXNUM_MIN / -1` overflows to a `Bignum` rather than a `Fixnum`, so type it as returning an `Integer` rather than `Fixnum`. Users that expect a `Fixnum` can guard to narrow the result back to `Fixnum`. Closes: https://github.com/Shopify/ruby/issues/990 --- zjit/src/codegen_tests.rs | 33 +++++++++++++++++++++++++++++++++ zjit/src/hir.rs | 4 +++- zjit/src/hir/opt_tests.rs | 6 +++--- 3 files changed, 39 insertions(+), 4 deletions(-) diff --git a/zjit/src/codegen_tests.rs b/zjit/src/codegen_tests.rs index b0993717187d83..698212241e503d 100644 --- a/zjit/src/codegen_tests.rs +++ b/zjit/src/codegen_tests.rs @@ -2285,6 +2285,39 @@ fn test_fixnum_floor() { assert_snapshot!(assert_compiles("test(4)"), @"0"); } +#[test] +fn test_fixnum_div_min_by_neg_one() { + // FIXNUM_MIN / -1 overflows to a Bignum: the JIT must side exit, not return a mistyped Fixnum. + eval(" + def test(a, b) = a / b + test(10, 3) # profile opt_div + "); + assert_contains_opcode("test", YARVINSN_opt_div); + assert_snapshot!(assert_compiles_allowing_exits("test(-4611686018427387904, -1)"), @"4611686018427387904"); +} + +#[test] +fn test_fixnum_div_overflow_propagation() { + // The div must side exit before its Bignum result reaches the specialized (a / b) & 1 op. + eval(" + def test(a, b) = (a / b) & 1 + test(10, 3) # profile opt_div + "); + assert_contains_opcode("test", YARVINSN_opt_div); + assert_snapshot!(assert_compiles_allowing_exits("test(-4611686018427387904, -1)"), @"0"); +} + +#[test] +fn test_fixnum_div_by_neg_one_is_fine() { + // x / -1 (x != FIXNUM_MIN) is a normal Fixnum and must NOT trip the overflow guard. + eval(" + def test(a, b) = a / b + test(10, 3) # profile opt_div + "); + assert_contains_opcode("test", YARVINSN_opt_div); + assert_snapshot!(assert_compiles("test(10, -1)"), @"-10"); +} + #[test] fn test_opt_not() { eval(" diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index c2ae897d9aa9cb..01bc6c00c4980e 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -2955,7 +2955,9 @@ impl Function { Insn::FixnumAdd { .. } => types::Fixnum, Insn::FixnumSub { .. } => types::Fixnum, Insn::FixnumMult { .. } => types::Fixnum, - Insn::FixnumDiv { .. } => types::Fixnum, + // FIXNUM_MIN / -1 overflows to a Bignum, so the result is Integer, not Fixnum. + // Downstream Fixnum ops insert their own GuardType(Fixnum) + Insn::FixnumDiv { .. } => types::Integer, Insn::FixnumMod { .. } => types::Fixnum, Insn::FloatAdd { .. } => types::Float, Insn::FloatSub { .. } => types::Float, diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index 53f65db4ccd502..f88172abcd6ea1 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -371,7 +371,7 @@ mod hir_opt_tests { v10:Fixnum[7] = Const Value(7) v12:Fixnum[0] = Const Value(0) PatchPoint MethodRedefined(Integer@0x1000, /@0x1008, cme:0x1010) - v23:Fixnum = FixnumDiv v10, v12 + v23:Integer = FixnumDiv v10, v12 CheckInterrupts Return v23 "); @@ -425,7 +425,7 @@ mod hir_opt_tests { v10:Fixnum[-4611686018427387904] = Const Value(-4611686018427387904) v12:Fixnum[-1] = Const Value(-1) PatchPoint MethodRedefined(Integer@0x1000, /@0x1008, cme:0x1010) - v23:Fixnum = FixnumDiv v10, v12 + v23:Integer = FixnumDiv v10, v12 CheckInterrupts Return v23 "); @@ -2916,7 +2916,7 @@ mod hir_opt_tests { PatchPoint MethodRedefined(Integer@0x1008, /@0x1010, cme:0x1018) v32:Fixnum = GuardType v12, Fixnum v33:Fixnum = GuardType v13, Fixnum - v34:Fixnum = FixnumDiv v32, v33 + v34:Integer = FixnumDiv v32, v33 v24:Fixnum[5] = Const Value(5) CheckInterrupts Return v24 From f261909fa9b720a4dd5bd2989906048069060c96 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Tue, 2 Jun 2026 11:53:09 -0400 Subject: [PATCH 091/188] ZJIT: Upgrade self to HeapBasicObject in normal methods on most classes (#17155) If the ISEQ we're compiling is a `ISEQ_TYPE_METHOD` and it's defined on a class that no immediate object's class inherits from (exclude `BasicObject`, `Object`, `Numeric`, etc), and the allocator is the default allocator, then we can assume the `self` is heap-allocated. This lets us skip HeapBasicObject guards for a lot of instance variable lookups, for example: ```ruby class C def foo = @foo end ``` Thanks to @jhawthorn for the idea and @tenderlove for convincing me it was reasonable. --- zjit/src/codegen.rs | 20 +++ zjit/src/cruby.rs | 32 ++++ zjit/src/hir.rs | 14 +- zjit/src/hir/opt_tests.rs | 367 ++++++++++++++++++++++++++++++++------ zjit/src/payload.rs | 9 + 5 files changed, 384 insertions(+), 58 deletions(-) diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index d5d381acfa6a87..1b20018130bfed 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -159,6 +159,17 @@ define_split_jumps! { jz => Jz, } +/// Record on the ISEQ payload whether `self` is guaranteed to be a heap object, +/// derived from the owning class of the method entry on `cfp`. Called from compile +/// triggers before the HIR is built so the `self`-producing instructions can be +/// typed precisely. Must be called while holding the VM lock (it writes the payload). +fn update_self_is_heap_object(iseq: IseqPtr, cfp: CfpPtr) { + let cme = unsafe { rb_vm_frame_method_entry(cfp) }; + let self_is_heap_object = !cme.is_null() + && iseq_self_is_heap_object(iseq, unsafe { (*cme).owner }); + get_or_create_iseq_payload(iseq).self_is_heap_object = self_is_heap_object; +} + /// CRuby API to compile a given ISEQ. /// If jit_exception is true, compile JIT code for handling exceptions. /// See jit_compile_exception() for details. @@ -173,6 +184,10 @@ pub extern "C" fn rb_zjit_iseq_gen_entry_point(iseq: IseqPtr, ec: EcPtr, jit_exc // Take a lock to avoid writing to ISEQ in parallel with Ractors. // with_vm_lock() does nothing if the program doesn't use Ractors. with_vm_lock(src_loc!(), || { + // The current frame is this ISEQ's method frame, so its method entry tells + // us the owning class and thus whether `self` is always a heap object. + update_self_is_heap_object(iseq, unsafe { get_ec_cfp(ec) }); + let cb = ZJITState::get_code_block(); let mut code_ptr = with_time_stat(compile_time_ns, || gen_iseq_entry_point(cb, iseq, jit_exception)); @@ -3152,6 +3167,11 @@ c_callable! { let cb = ZJITState::get_code_block(); let native_stack_full = unsafe { rb_ec_stack_check(ec as _) } != 0; let payload = get_or_create_iseq_payload(iseq); + // cfp is the callee's (this ISEQ's) frame here, so its method entry gives + // the owning class and thus whether `self` is always a heap object. + let cme = unsafe { rb_vm_frame_method_entry(cfp) }; + payload.self_is_heap_object = !cme.is_null() + && iseq_self_is_heap_object(iseq, unsafe { (*cme).owner }); let last_status = payload.versions.last().map(|version| &unsafe { version.as_ref() }.status); let compile_error = match last_status { Some(IseqStatus::CantCompile(err)) => Some(err), diff --git a/zjit/src/cruby.rs b/zjit/src/cruby.rs index eb3241b0a2fb08..4e4a246b8a1f72 100644 --- a/zjit/src/cruby.rs +++ b/zjit/src/cruby.rs @@ -1547,6 +1547,38 @@ pub fn class_has_leaf_allocator(class: VALUE) -> bool { unsafe { rb_zjit_class_has_default_allocator(class) } } +/// Whether a method ISEQ defined on `owner` is guaranteed to run with a `self` +/// that is a heap (non-immediate) object. +/// +/// True only for plain `def` methods (`ISEQ_TYPE_METHOD`) defined on a normal, +/// initialized, non-singleton class that uses the default allocator +/// (`rb_class_allocate_instance`). The receiver of such a method is always +/// `kind_of?` the owner, and no user class with the default allocator can be +/// inserted into the ancestry of an immediate, so `self` cannot be an immediate. +/// +/// The default-allocator check alone is not sufficient: `Object`, `BasicObject`, +/// and `Numeric` use the default allocator yet are ancestors of immediates (e.g. +/// `Integer`). Every such class is also an ancestor of `Integer`, so a single +/// `rb_obj_is_kind_of(, owner)` check rules all of them out. +/// +/// Returns `false` conservatively for anything that doesn't clearly qualify +/// (modules, singleton classes, custom allocators, non-`def` ISEQs, etc.). +pub fn iseq_self_is_heap_object(iseq: IseqPtr, owner: VALUE) -> bool { + if unsafe { rb_get_iseq_body_type(iseq) } != ISEQ_TYPE_METHOD { return false; } + if !unsafe { RB_TYPE_P(owner, RUBY_T_CLASS) } { return false; } + // Check initialized + non-singleton before reading the allocator (reading it otherwise + // aborts). + // TODO(max): Determine if we can loosen this to allow methods defined on singleton classes. + if !unsafe { rb_zjit_class_initialized_p(owner) } { return false; } + if unsafe { rb_zjit_singleton_class_p(owner) } { return false; } + if !unsafe { rb_zjit_class_has_default_allocator(owner) } { return false; } + // Exclude Object/BasicObject/Numeric and friends: classes that use the default + // allocator but sit above an immediate class in the ancestry chain. They are + // all ancestors of Integer, so this single check covers every immediate type. + if unsafe { rb_obj_is_kind_of(VALUE::fixnum_from_usize(0), owner) }.test() { return false; } + true +} + /// Interned ID values for Ruby symbols and method names. /// See [type@crate::cruby::ID] and usages outside of ZJIT. pub(crate) mod ids { diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 01bc6c00c4980e..cfa3c01d475658 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -2539,6 +2539,11 @@ pub struct Function { /// Whether previously, a function for this ISEQ was invalidated due to /// singleton class creation (violation of NoSingletonClass invariant). was_invalidated_for_singleton_class_creation: bool, + /// Whether `self` is guaranteed to be a heap (non-immediate) object. When set, + /// the `self`-producing instructions (`LoadSelf` and the `SelfParam` `LoadArg`) + /// are typed `HeapBasicObject` instead of `BasicObject`. Sourced from + /// `IseqPayload::self_is_heap_object`. + self_is_heap_object: bool, /// Controls code generation strategy for optimization passes. policy: CompilePolicy, /// The types for the parameters of this function. They are copied to the type @@ -2648,6 +2653,7 @@ impl Function { Function { iseq, was_invalidated_for_singleton_class_creation: false, + self_is_heap_object: false, policy: CompilePolicy::new(iseq), insns: vec![], insn_types: vec![], @@ -3005,7 +3011,7 @@ impl Function { Insn::LoadSP => types::CPtr, Insn::LoadEC => types::CPtr, Insn::GetEP { .. } => types::CPtr, - Insn::LoadSelf => types::BasicObject, + Insn::LoadSelf => if self.self_is_heap_object { types::HeapBasicObject } else { types::BasicObject }, &Insn::LoadField { return_type, .. } => return_type, Insn::GetSpecialSymbol { .. } => types::StringExact.union(types::NilClass), Insn::GetSpecialNumber { .. } => types::StringExact.union(types::NilClass), @@ -6689,6 +6695,7 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { let mut profiles = ProfileOracle::new(payload); let mut fun = Function::new(iseq); fun.was_invalidated_for_singleton_class_creation = payload.was_invalidated_for_singleton_class_creation; + fun.self_is_heap_object = payload.self_is_heap_object; // Compute a map of PC->Block by finding jump targets let jit_entry_insns = unsafe { iseq.params() }.opt_table_slice().iter().copied().map(VALUE::as_u32).collect::>(); @@ -8596,7 +8603,10 @@ fn compile_jit_entry_state(fun: &mut Function, jit_entry_block: BlockId, jit_ent }; let mut arg_idx: u32 = 0; - let self_param = fun.push_insn(jit_entry_block, Insn::LoadArg { idx: arg_idx, id: FieldName::SelfParam, val_type: types::BasicObject }); + // For `def` methods on classes that can only produce heap (non-immediate) + // instances, `self` is a HeapBasicObject. See `iseq_self_is_heap_object`. + let self_type = if fun.self_is_heap_object { types::HeapBasicObject } else { types::BasicObject }; + let self_param = fun.push_insn(jit_entry_block, Insn::LoadArg { idx: arg_idx, id: FieldName::SelfParam, val_type: self_type }); arg_idx += 1; let mut entry_state = FrameState::new(iseq); let mut ep: Option = None; diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index f88172abcd6ea1..149c36fb840bb9 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -7696,16 +7696,15 @@ mod hir_opt_tests { fn foo@:7: bb1(): EntryPoint interpreter - v1:BasicObject = LoadSelf + v1:HeapBasicObject = LoadSelf Jump bb3(v1) bb2(): EntryPoint JIT(0) - v4:BasicObject = LoadArg :self@0 + v4:HeapBasicObject = LoadArg :self@0 Jump bb3(v4) - bb3(v6:BasicObject): + bb3(v6:HeapBasicObject): PatchPoint SingleRactorMode - v11:HeapBasicObject = GuardType v6, HeapBasicObject - v12:CUInt64 = LoadField v11, :RBASIC_FLAGS@0x1000 + v12:CUInt64 = LoadField v6, :RBASIC_FLAGS@0x1000 v14:CUInt64[0xffffffff0000001f] = Const CUInt64(0xffffffff0000001f) v15:CPtr[CPtr(0x1001)] = Const CPtr(0x1001) v16 = RefineType v15, CUInt64 @@ -7713,7 +7712,7 @@ mod hir_opt_tests { v18:CBool = IsBitEqual v17, v16 CondBranch v18, bb5(), bb6() bb5(): - v20:BasicObject = LoadField v11, :@foo@0x1002 + v20:BasicObject = LoadField v6, :@foo@0x1002 Jump bb4(v20) bb6(): v22:CUInt64[0xffffffff0000001f] = Const CUInt64(0xffffffff0000001f) @@ -7723,10 +7722,10 @@ mod hir_opt_tests { v26:CBool = IsBitEqual v25, v24 CondBranch v26, bb7(), bb8() bb7(): - v28:BasicObject = LoadField v11, :@foo@0x1004 + v28:BasicObject = LoadField v6, :@foo@0x1004 Jump bb4(v28) bb8(): - v30:BasicObject = GetIvar v11, :@foo + v30:BasicObject = GetIvar v6, :@foo Jump bb4(v30) bb4(v13:BasicObject): CheckInterrupts @@ -7734,6 +7733,263 @@ mod hir_opt_tests { "); } + // The following tests pin down the soundness boundary of the `self: + // HeapBasicObject` inference (see `iseq_self_is_heap_object`). A `def` method + // gets `self: HeapBasicObject` only when its owning class can never produce an + // immediate receiver. For each class below, `self` must stay `BasicObject`: + // the six immediate classes have no default allocator, and Object/BasicObject/ + // Numeric use the default allocator but are ancestors of immediates (caught by + // the Integer kind_of check). Each test reopens the class, compiles the method + // (call threshold is 30), then checks the resulting `self` type. + + #[test] + fn test_self_not_heap_object_owner_integer() { + eval(" + class Integer + def probe = @foo + end + 100.times { 5.probe } + "); + assert_snapshot!(hir_string_proc("5.method(:probe)"), @" + fn probe@:3: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb3(v1) + bb2(): + EntryPoint JIT(0) + v4:BasicObject = LoadArg :self@0 + Jump bb3(v4) + bb3(v6:BasicObject): + PatchPoint SingleRactorMode + v11:BasicObject = GetIvar v6, :@foo + CheckInterrupts + Return v11 + "); + } + + #[test] + fn test_self_not_heap_object_owner_symbol() { + eval(" + class Symbol + def probe = @foo + end + 100.times { :sym.probe } + "); + assert_snapshot!(hir_string_proc(":sym.method(:probe)"), @" + fn probe@:3: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb3(v1) + bb2(): + EntryPoint JIT(0) + v4:BasicObject = LoadArg :self@0 + Jump bb3(v4) + bb3(v6:BasicObject): + PatchPoint SingleRactorMode + v11:BasicObject = GetIvar v6, :@foo + CheckInterrupts + Return v11 + "); + } + + #[test] + fn test_self_not_heap_object_owner_float() { + eval(" + class Float + def probe = @foo + end + 100.times { 1.5.probe } + "); + assert_snapshot!(hir_string_proc("1.5.method(:probe)"), @" + fn probe@:3: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb3(v1) + bb2(): + EntryPoint JIT(0) + v4:BasicObject = LoadArg :self@0 + Jump bb3(v4) + bb3(v6:BasicObject): + PatchPoint SingleRactorMode + v11:BasicObject = GetIvar v6, :@foo + CheckInterrupts + Return v11 + "); + } + + #[test] + fn test_self_not_heap_object_owner_nil_class() { + eval(" + class NilClass + def probe = @foo + end + 100.times { nil.probe } + "); + assert_snapshot!(hir_string_proc("nil.method(:probe)"), @" + fn probe@:3: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb3(v1) + bb2(): + EntryPoint JIT(0) + v4:BasicObject = LoadArg :self@0 + Jump bb3(v4) + bb3(v6:BasicObject): + PatchPoint SingleRactorMode + v11:BasicObject = GetIvar v6, :@foo + CheckInterrupts + Return v11 + "); + } + + #[test] + fn test_self_not_heap_object_owner_true_class() { + eval(" + class TrueClass + def probe = @foo + end + 100.times { true.probe } + "); + assert_snapshot!(hir_string_proc("true.method(:probe)"), @" + fn probe@:3: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb3(v1) + bb2(): + EntryPoint JIT(0) + v4:BasicObject = LoadArg :self@0 + Jump bb3(v4) + bb3(v6:BasicObject): + PatchPoint SingleRactorMode + v11:BasicObject = GetIvar v6, :@foo + CheckInterrupts + Return v11 + "); + } + + #[test] + fn test_self_not_heap_object_owner_false_class() { + eval(" + class FalseClass + def probe = @foo + end + 100.times { false.probe } + "); + assert_snapshot!(hir_string_proc("false.method(:probe)"), @" + fn probe@:3: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb3(v1) + bb2(): + EntryPoint JIT(0) + v4:BasicObject = LoadArg :self@0 + Jump bb3(v4) + bb3(v6:BasicObject): + PatchPoint SingleRactorMode + v11:BasicObject = GetIvar v6, :@foo + CheckInterrupts + Return v11 + "); + } + + #[test] + fn test_self_not_heap_object_owner_object() { + // Object uses the default allocator, but Integer (and every other immediate) + // descends from it, so a method on Object can run with an immediate self. + eval(" + class Object + def probe = @foo + end + o = Object.new + 100.times { o.probe } + "); + assert_snapshot!(hir_string_proc("Object.new.method(:probe)"), @" + fn probe@:3: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb3(v1) + bb2(): + EntryPoint JIT(0) + v4:BasicObject = LoadArg :self@0 + Jump bb3(v4) + bb3(v6:BasicObject): + PatchPoint SingleRactorMode + v17:HeapBasicObject = GuardType v6, HeapBasicObject + v18:CShape = LoadField v17, :shape_id@0x1000 + v19:CShape[0x1001] = GuardBitEquals v18, CShape(0x1001) recompile + v20:NilClass = Const Value(nil) + CheckInterrupts + Return v20 + "); + } + + #[test] + fn test_self_not_heap_object_owner_basic_object() { + // Same as Object: BasicObject has the default allocator but is the root of + // the immediate classes' ancestry. + eval(" + class BasicObject + def probe = @foo + end + o = Object.new + 100.times { o.probe } + "); + assert_snapshot!(hir_string_proc("Object.new.method(:probe)"), @" + fn probe@:3: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb3(v1) + bb2(): + EntryPoint JIT(0) + v4:BasicObject = LoadArg :self@0 + Jump bb3(v4) + bb3(v6:BasicObject): + PatchPoint SingleRactorMode + v17:HeapBasicObject = GuardType v6, HeapBasicObject + v18:CShape = LoadField v17, :shape_id@0x1000 + v19:CShape[0x1001] = GuardBitEquals v18, CShape(0x1001) recompile + v20:NilClass = Const Value(nil) + CheckInterrupts + Return v20 + "); + } + + #[test] + fn test_self_not_heap_object_owner_numeric() { + // Numeric has the default allocator but Integer/Float descend from it, so a + // method on Numeric can run with an immediate self. + eval(" + class Numeric + def probe = @foo + end + 100.times { 5.probe } + "); + assert_snapshot!(hir_string_proc("5.method(:probe)"), @" + fn probe@:3: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb3(v1) + bb2(): + EntryPoint JIT(0) + v4:BasicObject = LoadArg :self@0 + Jump bb3(v4) + bb3(v6:BasicObject): + PatchPoint SingleRactorMode + v11:BasicObject = GetIvar v6, :@foo + CheckInterrupts + Return v11 + "); + } + #[test] fn test_definedivar_shape_guard_recompile() { // Call with one shape to compile, then call with a different shape to @@ -7759,13 +8015,13 @@ mod hir_opt_tests { fn has_foo@:7: bb1(): EntryPoint interpreter - v1:BasicObject = LoadSelf + v1:HeapBasicObject = LoadSelf Jump bb3(v1) bb2(): EntryPoint JIT(0) - v4:BasicObject = LoadArg :self@0 + v4:HeapBasicObject = LoadArg :self@0 Jump bb3(v4) - bb3(v6:BasicObject): + bb3(v6:HeapBasicObject): v10:StringExact|NilClass = DefinedIvar v6, :@foo CheckInterrupts Return v10 @@ -7797,13 +8053,13 @@ mod hir_opt_tests { fn foo@:7: bb1(): EntryPoint interpreter - v1:BasicObject = LoadSelf + v1:HeapBasicObject = LoadSelf Jump bb3(v1) bb2(): EntryPoint JIT(0) - v4:BasicObject = LoadArg :self@0 + v4:HeapBasicObject = LoadArg :self@0 Jump bb3(v4) - bb3(v6:BasicObject): + bb3(v6:HeapBasicObject): v10:Fixnum[5] = Const Value(5) PatchPoint SingleRactorMode SetIvar v6, :@foo, v10 @@ -7842,16 +8098,16 @@ mod hir_opt_tests { fn write@:12: bb1(): EntryPoint interpreter - v1:BasicObject = LoadSelf + v1:HeapBasicObject = LoadSelf v2:CPtr = LoadSP v3:BasicObject = LoadField v2, :obj@0x1000 Jump bb3(v1, v3) bb2(): EntryPoint JIT(0) - v6:BasicObject = LoadArg :self@0 + v6:HeapBasicObject = LoadArg :self@0 v7:BasicObject = LoadArg :obj@1 Jump bb3(v6, v7) - bb3(v9:BasicObject, v10:BasicObject): + bb3(v9:HeapBasicObject, v10:BasicObject): v17:Fixnum[5] = Const Value(5) PatchPoint MethodRedefined(C@0x1008, foo=@0x1010, cme:0x1018) v28:ObjectSubclass[class_exact:C] = GuardType v10, ObjectSubclass[class_exact:C] @@ -14574,13 +14830,13 @@ mod hir_opt_tests { fn foo@:10: bb1(): EntryPoint interpreter - v1:BasicObject = LoadSelf + v1:HeapBasicObject = LoadSelf Jump bb3(v1) bb2(): EntryPoint JIT(0) - v4:BasicObject = LoadArg :self@0 + v4:HeapBasicObject = LoadArg :self@0 Jump bb3(v4) - bb3(v6:BasicObject): + bb3(v6:HeapBasicObject): PatchPoint MethodRedefined(A@0x1000, foo@0x1008, cme:0x1010) v18:CPtr = GetEP 0 v19:RubyValue = LoadField v18, :VM_ENV_DATA_INDEX_ME_CREF@0x1038 @@ -14643,16 +14899,16 @@ mod hir_opt_tests { fn foo@:10: bb1(): EntryPoint interpreter - v1:BasicObject = LoadSelf + v1:HeapBasicObject = LoadSelf v2:CPtr = LoadSP v3:BasicObject = LoadField v2, :x@0x1000 Jump bb3(v1, v3) bb2(): EntryPoint JIT(0) - v6:BasicObject = LoadArg :self@0 + v6:HeapBasicObject = LoadArg :self@0 v7:BasicObject = LoadArg :x@1 Jump bb3(v6, v7) - bb3(v9:BasicObject, v10:BasicObject): + bb3(v9:HeapBasicObject, v10:BasicObject): PatchPoint MethodRedefined(A@0x1008, foo@0x1010, cme:0x1018) v28:CPtr = GetEP 0 v29:RubyValue = LoadField v28, :VM_ENV_DATA_INDEX_ME_CREF@0x1040 @@ -14695,16 +14951,16 @@ mod hir_opt_tests { fn foo@:10: bb1(): EntryPoint interpreter - v1:BasicObject = LoadSelf + v1:HeapBasicObject = LoadSelf v2:CPtr = LoadSP v3:ArrayExact = LoadField v2, :x@0x1000 Jump bb3(v1, v3) bb2(): EntryPoint JIT(0) - v6:BasicObject = LoadArg :self@0 + v6:HeapBasicObject = LoadArg :self@0 v7:BasicObject = LoadArg :x@1 Jump bb3(v6, v7) - bb3(v9:BasicObject, v10:BasicObject): + bb3(v9:HeapBasicObject, v10:BasicObject): v16:ArrayExact = ToArray v10 v18:BasicObject = InvokeSuper v9, 0x1008, v16 # SendFallbackReason: super: complex argument passing to `super` call CheckInterrupts @@ -14739,13 +14995,13 @@ mod hir_opt_tests { fn foo@:10: bb1(): EntryPoint interpreter - v1:BasicObject = LoadSelf + v1:HeapBasicObject = LoadSelf Jump bb3(v1) bb2(): EntryPoint JIT(0) - v4:BasicObject = LoadArg :self@0 + v4:HeapBasicObject = LoadArg :self@0 Jump bb3(v4) - bb3(v6:BasicObject): + bb3(v6:HeapBasicObject): v11:BasicObject = InvokeSuper v6, 0x1000 # SendFallbackReason: super: call made with a block CheckInterrupts Return v11 @@ -14909,18 +15165,18 @@ mod hir_opt_tests { fn foo@:10: bb1(): EntryPoint interpreter - v1:BasicObject = LoadSelf + v1:HeapBasicObject = LoadSelf v2:CPtr = LoadSP v3:BasicObject = LoadField v2, :blk@0x1000 v4:NilClass = Const Value(nil) Jump bb3(v1, v3, v4) bb2(): EntryPoint JIT(0) - v7:BasicObject = LoadArg :self@0 + v7:HeapBasicObject = LoadArg :self@0 v8:BasicObject = LoadArg :blk@1 v9:NilClass = Const Value(nil) Jump bb3(v7, v8, v9) - bb3(v11:BasicObject, v12:BasicObject, v13:NilClass): + bb3(v11:HeapBasicObject, v12:BasicObject, v13:NilClass): PatchPoint NoSingletonClass(B@0x1008) PatchPoint MethodRedefined(B@0x1008, proc@0x1010, cme:0x1018) v39:ObjectSubclass[class_exact:B] = GuardType v11, ObjectSubclass[class_exact:B] @@ -14962,16 +15218,16 @@ mod hir_opt_tests { fn foo@:10: bb1(): EntryPoint interpreter - v1:BasicObject = LoadSelf + v1:HeapBasicObject = LoadSelf v2:CPtr = LoadSP v3:BasicObject = LoadField v2, :items@0x1000 Jump bb3(v1, v3) bb2(): EntryPoint JIT(0) - v6:BasicObject = LoadArg :self@0 + v6:HeapBasicObject = LoadArg :self@0 v7:BasicObject = LoadArg :items@1 Jump bb3(v6, v7) - bb3(v9:BasicObject, v10:BasicObject): + bb3(v9:HeapBasicObject, v10:BasicObject): v16:StaticSymbol[:succ] = Const Value(VALUE(0x1008)) v18:BasicObject = InvokeSuper v9, 0x1010, v10, v16 # SendFallbackReason: super: complex argument passing to `super` call CheckInterrupts @@ -15004,7 +15260,7 @@ mod hir_opt_tests { fn foo@:9: bb1(): EntryPoint interpreter - v1:BasicObject = LoadSelf + v1:HeapBasicObject = LoadSelf v2:CPtr = LoadSP v3:BasicObject = LoadField v2, :content@0x1000 v4:CPtr = LoadPC @@ -15015,19 +15271,19 @@ mod hir_opt_tests { Jump bb5(v1, v3) bb2(): EntryPoint JIT(0) - v10:BasicObject = LoadArg :self@0 + v10:HeapBasicObject = LoadArg :self@0 v11:NilClass = Const Value(nil) Jump bb3(v10, v11) - bb3(v17:BasicObject, v18:BasicObject): + bb3(v17:HeapBasicObject, v18:BasicObject): v21:StringExact[VALUE(0x1008)] = Const Value(VALUE(0x1008)) v22:StringExact = StringCopy v21 Jump bb5(v17, v22) bb4(): EntryPoint JIT(1) - v14:BasicObject = LoadArg :self@0 + v14:HeapBasicObject = LoadArg :self@0 v15:BasicObject = LoadArg :content@1 Jump bb5(v14, v15) - bb5(v25:BasicObject, v26:BasicObject): + bb5(v25:HeapBasicObject, v26:BasicObject): v32:BasicObject = InvokeSuper v25, 0x1010, v26 # SendFallbackReason: super: complex argument passing to `super` call CheckInterrupts Return v32 @@ -16116,29 +16372,28 @@ mod hir_opt_tests { fn set_value_loop@:4: bb1(): EntryPoint interpreter - v1:BasicObject = LoadSelf + v1:HeapBasicObject = LoadSelf v2:NilClass = Const Value(nil) Jump bb3(v1, v2) bb2(): EntryPoint JIT(0) - v5:BasicObject = LoadArg :self@0 + v5:HeapBasicObject = LoadArg :self@0 v6:NilClass = Const Value(nil) Jump bb3(v5, v6) - bb3(v8:BasicObject, v9:NilClass): + bb3(v8:HeapBasicObject, v9:NilClass): v13:Fixnum[0] = Const Value(0) CheckInterrupts Jump bb6(v8, v13) - bb6(v19:BasicObject, v20:Fixnum): + bb6(v19:HeapBasicObject, v20:Fixnum): v24:Fixnum[10] = Const Value(10) PatchPoint MethodRedefined(Integer@0x1000, <@0x1008, cme:0x1010) v110:BoolExact = FixnumLt v20, v24 CheckInterrupts v30:CBool = Test v110 CondBranch v30, bb4(v19, v20), bb7() - bb4(v40:BasicObject, v41:Fixnum): + bb4(v40:HeapBasicObject, v41:Fixnum): PatchPoint SingleRactorMode - v46:HeapBasicObject = GuardType v40, HeapBasicObject - v47:CUInt64 = LoadField v46, :RBASIC_FLAGS@0x1038 + v47:CUInt64 = LoadField v40, :RBASIC_FLAGS@0x1038 v49:CUInt64[0xffffffff0000001f] = Const CUInt64(0xffffffff0000001f) v50:CPtr[CPtr(0x1039)] = Const CPtr(0x1039) v51 = RefineType v50, CUInt64 @@ -16146,7 +16401,7 @@ mod hir_opt_tests { v53:CBool = IsBitEqual v52, v51 CondBranch v53, bb9(), bb10() bb9(): - v55:BasicObject = LoadField v46, :@levar@0x103a + v55:BasicObject = LoadField v40, :@levar@0x103a Jump bb8(v55) bb10(): v57:CUInt64[0xffffffff0000001f] = Const CUInt64(0xffffffff0000001f) @@ -16159,24 +16414,24 @@ mod hir_opt_tests { v63:NilClass = Const Value(nil) Jump bb8(v63) bb12(): - v97:CShape = LoadField v46, :shape_id@0x103c + v97:CShape = LoadField v40, :shape_id@0x103c v98:CShape[0x103d] = GuardBitEquals v97, CShape(0x103d) recompile - v99:BasicObject = LoadField v46, :@levar@0x103a + v99:BasicObject = LoadField v40, :@levar@0x103a Jump bb8(v99) bb8(v48:BasicObject): CheckInterrupts v69:CBool = Test v48 - CondBranch v69, bb5(v46, v41), bb13() + CondBranch v69, bb5(v40, v41), bb13() bb13(): PatchPoint NoEPEscape(set_value_loop) PatchPoint SingleRactorMode - v101:CShape = LoadField v46, :shape_id@0x103c + v101:CShape = LoadField v40, :shape_id@0x103c v102:CShape[0x103e] = GuardBitEquals v101, CShape(0x103e) recompile - StoreField v46, :@levar@0x103a, v41 - WriteBarrier v46, v41 + StoreField v40, :@levar@0x103a, v41 + WriteBarrier v40, v41 v105:CShape[0x103d] = Const CShape(0x103d) - StoreField v46, :shape_id@0x103c, v105 - v79:HeapBasicObject = RefineType v46, HeapBasicObject + StoreField v40, :shape_id@0x103c, v105 + v79:HeapBasicObject = RefineType v40, HeapBasicObject Jump bb5(v79, v41) bb5(v81:HeapBasicObject, v82:Fixnum): PatchPoint NoEPEscape(set_value_loop) diff --git a/zjit/src/payload.rs b/zjit/src/payload.rs index 010972bdae19a9..807c33b8b76fe2 100644 --- a/zjit/src/payload.rs +++ b/zjit/src/payload.rs @@ -16,6 +16,14 @@ pub struct IseqPayload { /// Whether a previous compilation of this ISEQ was invalidated due to /// singleton class creation (violation of [`crate::hir::Invariant::NoSingletonClass`]). pub was_invalidated_for_singleton_class_creation: bool, + /// Whether `self` is guaranteed to be a heap (non-immediate) object for this + /// ISEQ. Set at compile triggers (entry point / function stub hit) where the + /// owning class is known via the method entry, and consumed in `iseq_to_hir` + /// to type the `self`-producing instructions (`LoadSelf` / `SelfParam` + /// `LoadArg`) as `HeapBasicObject`. Defaults to `false` (the conservative + /// `BasicObject`) when the owner is unknown. + /// See [`crate::cruby::iseq_self_is_heap_object`]. + pub self_is_heap_object: bool, } impl IseqPayload { @@ -24,6 +32,7 @@ impl IseqPayload { profile: IseqProfile::new(), versions: vec![], was_invalidated_for_singleton_class_creation: false, + self_is_heap_object: false, } } } From 3b4761cc8e432c5c5dbc51738b7795cceaab42df Mon Sep 17 00:00:00 2001 From: Randy Stauner Date: Tue, 2 Jun 2026 13:42:09 -0700 Subject: [PATCH 092/188] ZJIT: Add HIR Comment insn (#15166) Useful for adding comments to HIR/LIR/disasm that are not specific to an instruction. Possibly most useful for temporarily printing more information while debugging by keeping the statements interleaved with the HIR. With a local change like this diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index bff384fa65..de5b41caa1 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -2491,6 +2491,8 @@ fn type_specialize(&mut self) { cme = unsafe { rb_aliased_callable_method_entry(cme) }; def_type = unsafe { get_cme_def_type(cme) }; } + eprintln!("WTH"); + hir_comment!(self, block, "HOWDY def_type {:?}", def_type); if def_type == VM_METHOD_TYPE_ISEQ { // TODO(max): Allow non-iseq; cache cme // Only specialize positional-positional calls The print statement (1) shows up early (anywhere) whereas the HIR comment (2) is interleaved with the rest of the HIR so you can easily correlate debug info to the compilation of whatever function you are observing: 1. WTH Optimized HIR: fn test@test.rb:2: bb0(): EntryPoint interpreter v1:BasicObject = LoadSelf Jump bb2(v1) bb1(v4:BasicObject): EntryPoint JIT(0) Jump bb2(v4) bb2(v6:BasicObject): v11:Fixnum[1] = Const Value(1) 2. # HOWDY def_type 0 PatchPoint MethodRedefined(Object@0x10184ed10, foo@0x9c01, cme:0x101967060) The comments show up in HIR, LIR, and disasm: bb2(v6:BasicObject): v11:Fixnum[1] = Const Value(1) # HOWDY def_type 0 PatchPoint MethodRedefined(Object@0x1220fed10, foo@0x9c01, cme:0x123717060) -- PatchPoint Exit(PatchPoint(NoTracePoint)) # Insn: v11 Const Value(1) # Insn: v18 # HOWDY def_type 0 # Insn: v19 PatchPoint MethodRedefined(Object@0x103aaed00, foo@0x9c01, cme:0x103bc7080) PatchPoint Exit(PatchPoint(MethodRedefined(Object@0x103aaed00, foo@0x9c01, cme:0x103bc7080))) -- 0x1230bc0f0: nop # Insn: v11 Const Value(1) # Insn: v18 # HOWDY def_type 0 # Insn: v19 PatchPoint MethodRedefined(Object@0x103c6ed20, foo@0x9c01, cme:0x103d87070) 0x1230bc0f4: nop --- zjit/src/codegen.rs | 1 + zjit/src/hir.rs | 43 +++++++++++++++++++++++++++++++++++++-- zjit/src/hir/opt_tests.rs | 18 ++++++++++++++++ 3 files changed, 60 insertions(+), 2 deletions(-) diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index 1b20018130bfed..f0ba2fdd8ad7b0 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -594,6 +594,7 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio } let out_opnd = match insn { + Insn::Comment { .. } => return Ok(()), // comment instruction, no code generation &Insn::Const { val: Const::Value(val) } => gen_const_value(val), &Insn::Const { val: Const::CPtr(val) } => gen_const_cptr(val), &Insn::Const { val: Const::CInt64(val) } => gen_const_long(val), diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index cfa3c01d475658..55f5fd949cd146 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -24,6 +24,26 @@ use SendFallbackReason::*; pub(crate) mod tests; mod opt_tests; +#[allow(unused_macros)] +macro_rules! hir_comment { + ($func:expr, $block:expr, $($arg:tt)*) => { + // If a diagnostic dump is requested, enrich it with HIR comments. Otherwise, avoid + // allocating comment strings or adding comment instructions that nobody can observe. + let enable_comment = $crate::options::get_option_ref!(dump_hir_init).is_some() || + $crate::options::get_option_ref!(dump_hir_opt).is_some() || + $crate::options::get_option_ref!(dump_hir_graphviz).is_some() || + $crate::options::get_option!(dump_hir_iongraph) || + $crate::options::get_option_ref!(dump_lir).is_some() || + $crate::options::get_option_ref!(dump_disasm).is_some(); + if enable_comment { + $func.push_comment($block, format!($($arg)*)); + } + }; +} + +#[allow(unused_imports)] +pub(crate) use hir_comment; + /// An index of an [`Insn`] in a [`Function`]. This is a popular /// type since this effectively acts as a pointer to an [`Insn`]. /// See also: [`Function::find`]. @@ -829,6 +849,9 @@ impl From for FieldName { /// helps with editing. #[derive(Debug, Clone)] pub enum Insn { + /// Comment that can be inserted into HIR for diagnostics. + Comment { message: String }, + Const { val: Const }, /// SSA block parameter. Also used for function parameters in the function's entry block. Param, @@ -1192,7 +1215,8 @@ pub enum Insn { macro_rules! for_each_operand_impl { ($self:expr, $visit_one:ident, $visit_many:ident) => { match $self { - Insn::Const { .. } + Insn::Comment { .. } + | Insn::Const { .. } | Insn::Param | Insn::LoadArg { .. } | Insn::Entries { .. } @@ -1501,7 +1525,8 @@ impl Insn { /// Not every instruction returns a value. Return true if the instruction does and false otherwise. pub fn has_output(&self) -> bool { match self { - Insn::Jump(_) + Insn::Comment { .. } + | Insn::Jump(_) | Insn::Entries { .. } | Insn::CondBranch { .. } | Insn::EntryPoint { .. } | Insn::Return { .. } | Insn::PatchPoint { .. } | Insn::SetIvar { .. } | Insn::SetClassVar { .. } | Insn::ArrayExtend { .. } @@ -1561,6 +1586,7 @@ impl Insn { fn effects_of(&self) -> Effect { const allocates: Effect = Effect::read_write(abstract_heaps::PC.union(abstract_heaps::Allocator), abstract_heaps::Allocator); match &self { + Insn::Comment { .. } => effects::Empty, Insn::Const { .. } => effects::Empty, Insn::Param { .. } => effects::Empty, Insn::LoadArg { .. } => effects::Empty, @@ -1745,6 +1771,12 @@ impl Insn { /// Note: These are restrictions on the `write` `EffectSet` only. Even instructions with /// `read: effects::Any` could potentially be omitted. fn is_elidable(&self) -> bool { + // Comments intentionally have no semantic effect, but they are diagnostics that should + // survive DCE so optimized HIR dumps retain the information callers inserted. + if matches!(self, Insn::Comment { .. }) { + return false; + } + abstract_heaps::Allocator.includes(self.effects_of().write_bits()) } } @@ -1813,6 +1845,7 @@ static REGEXP_FLAGS: &[(u32, &str)] = &[ impl<'a> std::fmt::Display for InsnPrinter<'a> { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match &self.inner { + Insn::Comment { message } => write!(f, "# {message}"), Insn::Const { val } => { write!(f, "Const {}", val.print(self.ptr_map)) } Insn::Param => { write!(f, "Param") } Insn::LoadArg { idx, id, .. } => { write!(f, "LoadArg :{id}@{idx}") } @@ -2695,6 +2728,10 @@ impl Function { id } + pub fn push_comment(&mut self, block: BlockId, message: String) -> InsnId { + self.push_insn(block, Insn::Comment { message }) + } + // Add an instruction to an SSA block fn push_insn_id(&mut self, block: BlockId, insn_id: InsnId) -> InsnId { self.blocks[block.0].insns.push(insn_id); @@ -2887,6 +2924,7 @@ impl Function { Insn::Param => unimplemented!("params should not be present in block.insns"), Insn::LoadArg { val_type, .. } => *val_type, Insn::SetGlobal { .. } | Insn::Jump(_) | Insn::Entries { .. } | Insn::EntryPoint { .. } + | Insn::Comment { .. } | Insn::CondBranch { .. } | Insn::Return { .. } | Insn::Throw { .. } | Insn::PatchPoint { .. } | Insn::SetIvar { .. } | Insn::SetClassVar { .. } | Insn::ArrayExtend { .. } | Insn::ArrayPush { .. } | Insn::SideExit { .. } | Insn::SetLocal { .. } @@ -5952,6 +5990,7 @@ impl Function { match insn { // Instructions with no InsnId operands (except state) or nothing to assert Insn::Const { .. } + | Insn::Comment { .. } | Insn::Param | Insn::LoadArg { .. } | Insn::PutSpecialObject { .. } diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index 149c36fb840bb9..20c74fecb1e085 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -2413,6 +2413,24 @@ mod hir_opt_tests { "); } + #[test] + fn test_do_not_eliminate_comment() { + let mut function = Function::new(std::ptr::null()); + let block = function.entry_block; + + let comment = function.push_comment(block, "diagnostic".to_string()); + let dead_const = function.push_insn(block, Insn::Const { val: Const::CBool(false) }); + let return_val = function.push_insn(block, Insn::Const { val: Const::CBool(true) }); + function.push_insn(block, Insn::Return { val: return_val }); + function.seal_entries(); + + function.eliminate_dead_code(); + + let insns = &function.blocks[block.0].insns; + assert!(insns.contains(&comment)); + assert!(!insns.contains(&dead_const)); + } + #[test] fn test_eliminate_new_array() { eval(" From 4aa721f79108cca5720f101d037a18f44a9797d9 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Tue, 2 Jun 2026 17:13:27 -0400 Subject: [PATCH 093/188] ZJIT: Fold unnecessary RefineType (#17183) If it's already the type we're refining, we can delete the RefineType. --- zjit/src/hir.rs | 5 +++++ zjit/src/hir/opt_tests.rs | 25 +++++++++---------------- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 55f5fd949cd146..f5363aebe261bb 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -5121,6 +5121,11 @@ impl Function { // Don't bother re-inferring the type of val; we already know it. continue; } + Insn::RefineType { val, new_type, .. } if self.is_a(val, new_type) => { + self.make_equal_to(insn_id, val); + // Don't bother re-inferring the type of val; we already know it. + continue; + } Insn::LoadField { recv, offset, return_type, .. } if return_type.is_subtype(types::BasicObject) && u32::try_from(offset).is_ok() => { let offset = (offset as u32).to_usize(); diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index 20c74fecb1e085..3212390ecf948f 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -5831,13 +5831,12 @@ mod hir_opt_tests { WriteBarrier v28, v10 v33:CShape[0x1003] = Const CShape(0x1003) StoreField v28, :shape_id@0x1000, v33 - v14:HeapBasicObject = RefineType v28, HeapBasicObject v17:Fixnum[2] = Const Value(2) PatchPoint SingleRactorMode - StoreField v14, :@bar@0x1004, v17 - WriteBarrier v14, v17 + StoreField v28, :@bar@0x1004, v17 + WriteBarrier v28, v17 v40:CShape[0x1005] = Const CShape(0x1005) - StoreField v14, :shape_id@0x1000, v40 + StoreField v28, :shape_id@0x1000, v40 CheckInterrupts Return v17 "); @@ -6567,9 +6566,8 @@ mod hir_opt_tests { bb3(v8:BasicObject, v9:NilClass): v13:Fixnum[1] = Const Value(1) CheckInterrupts - v23:Fixnum[1] = RefineType v13, NotNil PatchPoint MethodRedefined(Integer@0x1000, itself@0x1008, cme:0x1010) - Return v23 + Return v13 "); } @@ -15939,10 +15937,9 @@ mod hir_opt_tests { WriteBarrier v35, v13 v40:CShape[0x1003] = Const CShape(0x1003) StoreField v35, :shape_id@0x1000, v40 - v20:HeapBasicObject = RefineType v35, HeapBasicObject PatchPoint NoEPEscape(initialize) PatchPoint SingleRactorMode - WriteBarrier v20, v13 + WriteBarrier v35, v13 CheckInterrupts Return v13 "); @@ -15987,13 +15984,12 @@ mod hir_opt_tests { WriteBarrier v49, v16 v54:CShape[0x1003] = Const CShape(0x1003) StoreField v49, :shape_id@0x1000, v54 - v23:HeapBasicObject = RefineType v49, HeapBasicObject v26:Fixnum[5] = Const Value(5) PatchPoint NoEPEscape(initialize) PatchPoint MethodRedefined(Integer@0x1008, +@0x1010, cme:0x1018) v65:Fixnum[6] = Const Value(6) PatchPoint SingleRactorMode - WriteBarrier v23, v16 + WriteBarrier v49, v16 CheckInterrupts Return v16 "); @@ -16035,12 +16031,10 @@ mod hir_opt_tests { WriteBarrier v43, v13 v48:CShape[0x1003] = Const CShape(0x1003) StoreField v43, :shape_id@0x1000, v48 - v20:HeapBasicObject = RefineType v43, HeapBasicObject PatchPoint NoEPEscape(initialize) PatchPoint SingleRactorMode - WriteBarrier v20, v13 - v28:HeapBasicObject = RefineType v20, HeapBasicObject - WriteBarrier v28, v13 + WriteBarrier v43, v13 + WriteBarrier v43, v13 CheckInterrupts Return v13 "); @@ -16449,8 +16443,7 @@ mod hir_opt_tests { WriteBarrier v40, v41 v105:CShape[0x103d] = Const CShape(0x103d) StoreField v40, :shape_id@0x103c, v105 - v79:HeapBasicObject = RefineType v40, HeapBasicObject - Jump bb5(v79, v41) + Jump bb5(v40, v41) bb5(v81:HeapBasicObject, v82:Fixnum): PatchPoint NoEPEscape(set_value_loop) v89:Fixnum[1] = Const Value(1) From 6fa8fcf0059d33a76b79589a029be19b3415bc8e Mon Sep 17 00:00:00 2001 From: Daichi Kamiyama <32436625+dak2@users.noreply.github.com> Date: Wed, 3 Jun 2026 06:13:44 +0900 Subject: [PATCH 094/188] ZJIT: Add codegen tests for fixnum mod (#17182) Cover the basic case, negative operands, and division by zero. --- zjit/src/codegen_tests.rs | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/zjit/src/codegen_tests.rs b/zjit/src/codegen_tests.rs index 698212241e503d..c1eec5b875350a 100644 --- a/zjit/src/codegen_tests.rs +++ b/zjit/src/codegen_tests.rs @@ -2285,6 +2285,36 @@ fn test_fixnum_floor() { assert_snapshot!(assert_compiles("test(4)"), @"0"); } +#[test] +fn test_fixnum_mod() { + eval(" + def test(a, b) = a % b + test(13, 4) # profile opt_mod + "); + assert_contains_opcode("test", YARVINSN_opt_mod); + assert_snapshot!(assert_compiles("[test(13, 4), test(13, 13), test(5, 7)]"), @"[1, 0, 5]"); +} + +#[test] +fn test_fixnum_mod_negative() { + eval(" + def test(a, b) = a % b + test(7, 3) # profile opt_mod + "); + assert_contains_opcode("test", YARVINSN_opt_mod); + assert_snapshot!(assert_compiles("[test(-7, 3), test(7, -3), test(-7, -3)]"), @"[2, -2, -1]"); +} + +#[test] +fn test_fixnum_mod_by_zero() { + eval(" + def test(a, b) = a % b rescue :zero_div + test(13, 4) # profile opt_mod + "); + assert_contains_opcode("test", YARVINSN_opt_mod); + assert_snapshot!(assert_compiles_allowing_exits("test(13, 0)"), @":zero_div"); +} + #[test] fn test_fixnum_div_min_by_neg_one() { // FIXNUM_MIN / -1 overflows to a Bignum: the JIT must side exit, not return a mistyped Fixnum. From 63ba697b6079c6fc4b66270c9498ab32884aef22 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 2 Jun 2026 19:34:39 +0900 Subject: [PATCH 095/188] [ruby/rubygems] Split compact index entries on the first colon on older RubyGems Bundler delegates compact index parsing to the host RubyGems' Gem::Resolver::APISet::GemParser. Before RubyGems 4.0.13 that splits each entry on every colon, mangling metadata values that contain colons such as the `created_at` timestamps the cooldown feature reads, so cooldown silently stops filtering when Bundler runs on an older RubyGems. Shim parse_dependency to split only on the first colon in that case. https://github.com/ruby/rubygems/commit/f3a3bda98e Co-Authored-By: Claude Opus 4.8 --- lib/bundler/rubygems_ext.rb | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/lib/bundler/rubygems_ext.rb b/lib/bundler/rubygems_ext.rb index fedf44b0e69b14..d4cbf6d87596cc 100644 --- a/lib/bundler/rubygems_ext.rb +++ b/lib/bundler/rubygems_ext.rb @@ -465,6 +465,23 @@ def parse(line) Resolver::APISet::GemParser.prepend(UnfreezeCompactIndexParsedResponse) end + # RubyGems before 4.0.13 split compact index dependency/requirement entries + # on every colon, which mangles metadata values that contain colons such as + # the `created_at` timestamps the cooldown feature relies on. Split only on + # the first colon so those values survive on older RubyGems. + unless Gem.rubygems_version >= Gem::Version.new("4.0.13") + module SplitCompactIndexEntryOnFirstColon + def parse_dependency(string) + dependency = string.split(":", 2) + dependency[-1] = dependency[-1].split("&") if dependency.size > 1 + dependency[0] = -dependency[0] + dependency + end + end + + Resolver::APISet::GemParser.prepend(SplitCompactIndexEntryOnFirstColon) + end + if Gem.rubygems_version < Gem::Version.new("3.6.0") class Package; end require "rubygems/package/tar_reader" From f956ef766d85304efd91a14f5611a6c46ece818f Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 2 Jun 2026 19:51:36 +0900 Subject: [PATCH 096/188] [ruby/rubygems] Keep the parse_dependency override private and testable Define SplitCompactIndexEntryOnFirstColon unconditionally so it can be exercised on any RubyGems, prepending it only when the host RubyGems still splits compact index entries on every colon. Mark the override private to preserve the visibility of the original Gem::Resolver::APISet::GemParser method, and add a spec covering the colon-preserving behavior. https://github.com/ruby/rubygems/commit/d6af5c724d Co-Authored-By: Claude Opus 4.8 --- lib/bundler/rubygems_ext.rb | 21 +++++++----- spec/bundler/bundler/rubygems_ext_spec.rb | 39 +++++++++++++++++++++++ 2 files changed, 52 insertions(+), 8 deletions(-) create mode 100644 spec/bundler/bundler/rubygems_ext_spec.rb diff --git a/lib/bundler/rubygems_ext.rb b/lib/bundler/rubygems_ext.rb index d4cbf6d87596cc..4ad2bdf46f04e7 100644 --- a/lib/bundler/rubygems_ext.rb +++ b/lib/bundler/rubygems_ext.rb @@ -469,16 +469,21 @@ def parse(line) # on every colon, which mangles metadata values that contain colons such as # the `created_at` timestamps the cooldown feature relies on. Split only on # the first colon so those values survive on older RubyGems. - unless Gem.rubygems_version >= Gem::Version.new("4.0.13") - module SplitCompactIndexEntryOnFirstColon - def parse_dependency(string) - dependency = string.split(":", 2) - dependency[-1] = dependency[-1].split("&") if dependency.size > 1 - dependency[0] = -dependency[0] - dependency - end + # + # The module is defined unconditionally so it stays testable on any RubyGems, + # but only prepended when the host RubyGems still has the buggy behavior. + module SplitCompactIndexEntryOnFirstColon + private + + def parse_dependency(string) + dependency = string.split(":", 2) + dependency[-1] = dependency[-1].split("&") if dependency.size > 1 + dependency[0] = -dependency[0] + dependency end + end + unless Gem.rubygems_version >= Gem::Version.new("4.0.13") Resolver::APISet::GemParser.prepend(SplitCompactIndexEntryOnFirstColon) end diff --git a/spec/bundler/bundler/rubygems_ext_spec.rb b/spec/bundler/bundler/rubygems_ext_spec.rb new file mode 100644 index 00000000000000..0fc528f78c7b72 --- /dev/null +++ b/spec/bundler/bundler/rubygems_ext_spec.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require "bundler/rubygems_ext" + +RSpec.describe Gem::SplitCompactIndexEntryOnFirstColon do + # Reproduces the RubyGems < 4.0.13 `Gem::Resolver::APISet::GemParser` that + # split each compact index entry on every colon, corrupting metadata values + # that themselves contain colons. + let(:legacy_parser_class) do + Class.new do + def parse_dependency(string) + dependency = string.split(":") + dependency[-1] = dependency[-1].split("&") if dependency.size > 1 + dependency[0] = -dependency[0] + dependency + end + end + end + + before { legacy_parser_class.prepend(described_class) } + + it "preserves colon-bearing metadata values such as created_at timestamps" do + parser = legacy_parser_class.new + + expect(parser.send(:parse_dependency, "created_at:2026-05-12T10:00:00Z")).to eq(["created_at", ["2026-05-12T10:00:00Z"]]) + end + + it "still parses ordinary name:requirement entries" do + parser = legacy_parser_class.new + + expect(parser.send(:parse_dependency, "myrack:>= 1.0")).to eq(["myrack", [">= 1.0"]]) + end + + it "keeps parse_dependency private" do + parser = legacy_parser_class.new + + expect { parser.parse_dependency("created_at:x") }.to raise_error(NoMethodError, /private method/) + end +end From ad8bf0e6365f164991ed427e30aff5d18c110238 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 2 Jun 2026 19:55:22 +0900 Subject: [PATCH 097/188] [ruby/rubygems] Assign rubygems_ext_spec to a test shard The new spec was unassigned, so the shard-based CI jobs would skip it and spec_helper would warn. Add it to shard_d per the file's guidance. https://github.com/ruby/rubygems/commit/35a0782cd2 Co-Authored-By: Claude Opus 4.8 --- spec/bundler/support/shards.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/bundler/support/shards.rb b/spec/bundler/support/shards.rb index 25563c765c004d..ce33896539bfc2 100644 --- a/spec/bundler/support/shards.rb +++ b/spec/bundler/support/shards.rb @@ -143,6 +143,7 @@ module Shards "spec/bundler/ci_detector_spec.rb", ], shard_d: [ + "spec/bundler/rubygems_ext_spec.rb", "spec/bundler/resolver/cooldown_spec.rb", "spec/install/cooldown_spec.rb", "spec/commands/outdated_spec.rb", From f0d45e01eeb4be90a20e56945dcdc33c9c924259 Mon Sep 17 00:00:00 2001 From: Burdette Lamar Date: Tue, 2 Jun 2026 19:31:19 -0500 Subject: [PATCH 098/188] [DOC] Harmonize find methods (#17150) --- lib/pathname.rb | 74 ++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 64 insertions(+), 10 deletions(-) diff --git a/lib/pathname.rb b/lib/pathname.rb index 5474fb6358c4a0..0e51e1fdf60383 100644 --- a/lib/pathname.rb +++ b/lib/pathname.rb @@ -9,23 +9,77 @@ # # For documentation, see class Pathname. # -class Pathname # * Find * +class Pathname + + # :markup: markdown + # + # call-seq: + # Pathname.find(ignore_error: true) -> nil # - # Iterates over the directory tree in a depth first manner, yielding a - # Pathname for each file under "this" directory. + # With a block given, performs a depth-first traversal of the path in `self`; + # calls the block with each found path: # - # Note that you need to require 'pathname' to use this method. + # ```ruby + # paths = [] + # Pathname('lib').find {|path| paths << path } + # paths.size # => 909 + # paths.take(3) + # # => + # # [#, + # # #, + # # #] + # ``` + # + # When `self` contains `'.'`, the found paths omit the leading `'./'`: + # + # ```ruby + # paths = [] + # Dir.chdir('lib') do + # Pathname('.').find {|path| paths << path } + # end + # paths.take(3) + # # # => + # # [#, + # # #, + # # #] + # ``` + # + # This method calls method Find.find; + # therefore method Find.prune may be used in the block: + # + # ```ruby + # files = [] + # Pathname('.').find do |path| + # Find.prune if File.basename(path) == 'test' + # next unless File.file?(path) && File.extname(path) == '.rb' + # files << path + # end + # files.size # => 6690 + # files.take(3) + # # # => + # # [#, + # # #, + # # #] + # ``` # - # Returns an Enumerator if no block is given. + # Raises an exception if the path in `self` cannot be read. # - # Since it is implemented by the standard library module Find, Find.prune can - # be used to control the traversal. + # When keyword argument `ignore_error` is given as `true` (the default), + # certain exceptions during traversal are ignored (i.e., silently rescued): + # Errno::ENOENT, Errno::EACCES, Errno::ENOTDIR, Errno::ELOOP, Errno::ENAMETOOLONG, Errno::EINVAL; + # when given as `false`, no exceptions are rescued. # - # If +self+ is +.+, yielded pathnames begin with a filename in the - # current directory, not +./+. + # Note that these exceptions may be ignored only in `Pathname#find` traversal code; + # an exception raised before traversal begins, + # or raised while in the block is not ignored. + # Each of the calls below raises an Errno::ENOENT exception that is not ignored: # - # See Find.find + # ```ruby + # Pathname('nosuch').find { } + # Pathname('lib').find {|entry| raise Errno::ENOENT } + # ``` # + # With no block given, returns a new Enumerator. def find(ignore_error: true) # :yield: pathname return to_enum(__method__, ignore_error: ignore_error) unless block_given? require 'find' From 1d3e2bd2b8baa6137b8eac5fd576fcda19cab618 Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Tue, 2 Jun 2026 21:10:42 +0900 Subject: [PATCH 099/188] [DOC] Improve docs for ObjectSpace.reachable_objects_from_root --- ext/objspace/objspace.c | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/ext/objspace/objspace.c b/ext/objspace/objspace.c index 38bffb07f70d2a..b880c6a5c70856 100644 --- a/ext/objspace/objspace.c +++ b/ext/objspace/objspace.c @@ -665,7 +665,30 @@ collect_values_of_values(VALUE category, VALUE category_objects, VALUE categorie * call-seq: * ObjectSpace.reachable_objects_from_root -> hash * - * [MRI specific feature] Return all reachable objects from root. + * Returns a hash of objects directly reachable from the VM roots, + * grouped by the root that reaches them. + * + * The roots are the entry points the garbage collector starts from when it + * marks live objects, such as the virtual machine and the global variable + * table. The keys of the returned hash are strings naming each root, and each + * value is an array of the objects reachable from that root: + * + * require 'objspace' + * + * reachable = ObjectSpace.reachable_objects_from_root + * reachable.keys # => ["vm", "global_tbl", "machine_context", "global_symbols"] + * reachable.values.first # => [#, ...] + * + * The returned hash compares its keys by identity, so it cannot be indexed + * with a string literal; iterate over it (or over its #values) instead. + * + * Any reference to an internal object is wrapped in an + * ObjectSpace::InternalObjectWrapper object. + * + * This method is useful for debugging the object graph, for example when + * tracking down the cause of a memory leak. + * + * This method is only expected to work with C Ruby. */ static VALUE reachable_objects_from_root(VALUE self) From be696c9c241840aa366fd7444b2297f1db55d8f3 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Wed, 3 Jun 2026 11:38:38 +0900 Subject: [PATCH 100/188] [Bug #22092] Improve `Array#sum` when the initial value is a `Float` --- array.c | 9 +++++++-- test/ruby/test_array.rb | 1 + 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/array.c b/array.c index 6720406d89b2f9..db4c2c4802dbaa 100644 --- a/array.c +++ b/array.c @@ -8255,7 +8255,11 @@ rb_ary_sum(int argc, VALUE *argv, VALUE ary) n = 0; r = Qundef; - if (!FIXNUM_P(v) && !RB_BIGNUM_TYPE_P(v) && !RB_TYPE_P(v, T_RATIONAL)) { + bool init_is_float = RB_FLOAT_TYPE_P(v); + if (init_is_float) { + v = LONG2FIX(0); + } + else if (!RB_INTEGER_TYPE_P(v) && !RB_TYPE_P(v, T_RATIONAL)) { i = 0; goto init_is_a_value; } @@ -8283,12 +8287,13 @@ rb_ary_sum(int argc, VALUE *argv, VALUE ary) goto not_exact; } v = finish_exact_sum(n, r, v, argc!=0); + if (init_is_float) v = rb_float_plus(argv[0], v); return v; not_exact: v = finish_exact_sum(n, r, v, i!=0); - if (RB_FLOAT_TYPE_P(e)) { + if (init_is_float ? (--i, e = argv[0], true) : RB_FLOAT_TYPE_P(e)) { /* * Kahan-Babuska balancing compensated summation algorithm * See https://link.springer.com/article/10.1007/s00607-005-0139-x diff --git a/test/ruby/test_array.rb b/test/ruby/test_array.rb index cad9bf5cc89a6d..76455187a5cf08 100644 --- a/test/ruby/test_array.rb +++ b/test/ruby/test_array.rb @@ -3550,6 +3550,7 @@ def test_sum assert_float_equal(3.5, [3].sum(0.5)) assert_float_equal(8.5, [3.5, 5].sum) assert_float_equal(10.5, [2, 8.5].sum) + assert_float_equal(1_000 * 0.1, Array.new(1_000, 0.1).sum(0.0)) assert_float_equal((FIXNUM_MAX+1).to_f, [FIXNUM_MAX, 1, 0.0].sum) assert_float_equal((FIXNUM_MAX+1).to_f, [0.0, FIXNUM_MAX+1].sum) From b36bf9d98cb96b527ab092fa8fa42da5e51c1144 Mon Sep 17 00:00:00 2001 From: Burdette Lamar Date: Tue, 2 Jun 2026 22:39:07 -0500 Subject: [PATCH 101/188] [DOC] Adds page "Filename Matching" --- doc/file/filename_matching.md | 356 ++++++++++++++++++++++++++++++++++ 1 file changed, 356 insertions(+) create mode 100644 doc/file/filename_matching.md diff --git a/doc/file/filename_matching.md b/doc/file/filename_matching.md new file mode 100644 index 00000000000000..9f13da90126fc6 --- /dev/null +++ b/doc/file/filename_matching.md @@ -0,0 +1,356 @@ +## Filename Matching + +Filename matching is a pattern-matching feature implemented in certain Ruby methods: + +- File.fnmatch. +- Pathname#fnmatch. + +Each `fnmatch` method matches a pattern against a string _path_; +these methods operate only on strings, and do not access the file system. + +These are quite different from filename globbing methods (not discussed here), +which match patterns against string paths found in the actual file system: + +- Dir.glob. +- Pathname.glob. +- Pathname#glob. + +### Patterns + +These are the basic elements of filename matching patterns; +see the sections below for details: + +| Pattern | Meaning | Examples | +|:------------------------:|--------------------------------------------|------------------------------| +| Simple string. | Matches itself. | `'Rakefile'`, `'LEGAL'` | +| `'*'` | Matches any sequence of characters. | `'*.txt'` | +| `'?'` | Matches any single character. | `'?.txt'` | +| `'[abc]'`,
`'[^abc]'` | Matches a single character from a set. | `'x[abc]y'`,
`'x[^abc]y'` | +| `'[a-z]`',
`'[^a-z]'` | Matches a single character from a range. | `'x[0-9]y'`,
`'x[^0-9]y'` | +| `'\'` | Escapes the next character. | `'\\*'`, `'\?'` | + +There are two other patterns that are disabled by default: + +- Directory-like substring (`'**'`); + see [`File::FNM_PATHNAME`](#constant-filefnmpathname) below. +- Alternatives (`'{ , }'`); + see [`File::FNM_EXTGLOB`](#constant-filefnmextglob) below. + +#### Simple \String + +A "simple string" is one that does not contain special filename-matching patterns; +see the table above. + +A simple string matches itself: + +```ruby +File.fnmatch('xyzzy', 'xyzzy') # => true +File.fnmatch('one_two_three', 'one_two_three') # => true +File.fnmatch('123', '123') # => true +File.fnmatch('Form 27B/6', 'Form 27B/6') # => true +File.fnmatch('bcd', 'abcde') # => false # Must be exact. +``` + +By default, the matching is case-sensitive: + +```ruby +File.fnmatch('abc', 'ABC') # => false +``` + +Case-sensitivity may be modified by flags: + +- [`File::FNM_CASEFOLD`](#constant-filefnmcasefold). +- [`File::FNM_SYSCASE`](#constant-filefnmsyscase). + +By default, the alternatives pattern is disabled: + +```rutby +File.fnmatch('R{ub,foo}y', 'Ruby') # => false +``` + +It may be enabled by flag [`File::FNM_EXTGLOB`](#constant-filefnmextglob). + +By default, the Windows short name pattern is disabled: + +```ruby +File.fnmatch('PROGRAM~1', 'Program Files') # => false +``` + +It may be enabled by flag [`File::FNM_SHORTNAME`](#constant-filefnmshortname). + +#### Any Sequence of Characters (`'*'`) + +The asterisk pattern (`'*'`) matches any sequence of characters: + +```ruby +File.fnmatch('*', 'foo') # => true +File.fnmatch('*', '') # => true +File.fnmatch('*', '*') # => true +File.fnmatch('\*', 'foo') # => false # Escaped. +``` + +By default, the asterisk pattern does not match a leading period (as in a dot-file): + +```ruby +File.fnmatch('*', '.document') # => false +``` + +That matching may be enabled by flag [`File::FNM_DOTMATCH`](#constant-filefnmdotmatch). + +By default, the asterisk pattern matches across file separators: + +```ruby +File.fnmatch('*.rb', 'lib/test.rb') # => true +``` + +That matching may be disabled by flag [`File::FNM_PATHNAME`](#constant-filefnmpathname). + +#### Single Character (`'?'`) + +The question-mark pattern (`'?'`) matches any single character: + +```ruby +File.fnmatch('?', 'f') # => true +File.fnmatch("foo-?.txt", "foo-1.txt") # => true +File.fnmatch('?', 'foo') # => false +File.fnmatch('?', '') # => false +File.fnmatch('\?', 'f') # => false # Escaped. +``` + +By default, pattern `'?'` matches the file separator: + +```ruby +File.fnmatch('foo?boo', 'foo/boo') # => true +``` + +That matching may be disabled by flag [`File::FNM_PATHNAME`](#constant-filefnmpathname). + +#### Single Character from a Set (`'[abc]'`, `'[^abc]'`) + +Characters enclosed in square brackets define a set of characters, +any of which matches a single character: + +```ruby +File.fnmatch('[ruby]', 'r') # => true +File.fnmatch('[ruby]', 'u') # => true +File.fnmatch('[ruby]', 'y') # => true +File.fnmatch('[ruby]', 'ruby') # => false +File.fnmatch('\[ruby]', 'r') # => false # Escaped. +``` + +The character set may be negated: + +```ruby +File.fnmatch('[^ruby]', 'r') # => false +File.fnmatch('[^ruby]', 'u') # => false +``` + +#### Single Character from a \Range (`'[a-c]'`, `'[^a-c]'`) + +A range of characters enclosed in square brackets defines a set of characters, +any of which matches a single character: + +```ruby +File.fnmatch('[a-c]', 'b') # => true +File.fnmatch('[a-c]', 'd') # => false +File.fnmatch('[a-c]', 'abc') # => false +File.fnmatch('R[t-v][a-c]y', 'Ruby') # => true # Multiple ranges allowed. +File.fnmatch('\[a-c]', 'b') # => false # Escaped. +``` + +The range may be negated: + +```ruby +File.fnmatch('[^a-c]', 'b') # => false +File.fnmatch('[^a-c]', 'd') # => true +``` + +#### Escape (`'\'`) + +The backslash character (`'\'`) may be used to escape any of the characters +that filename matching treats as special: + +```ruby +File.fnmatch('[a-c]', 'b') # => true +File.fnmatch('\[a-c]', 'b') # => false +File.fnmatch('[a-c\]', 'b') # => false +File.fnmatch('[a\-c]', 'b') # => false + +File.fnmatch('{a,b}', 'b', File::FNM_EXTGLOB) # => true +File.fnmatch('\{a,b}', 'b', File::FNM_EXTGLOB) # => false +File.fnmatch('{a\,b}', 'b', File::FNM_EXTGLOB) # => false +File.fnmatch('{a,b\}', 'b', File::FNM_EXTGLOB) # => false +``` + +Use a double-backslash to represent an ordinary backslash: + +```ruby +File.fnmatch('\\\\', '\\') # => true +``` + +By default escape pattern `'\'` is enabled; +it may be disabled by flag [`File::FNM_NOESCAPE`](#constant-filefnmnoescape). + +### Flags + +Optional argument `flags` (defaults to `0`) may be the bitwise OR +of the constants `File::FNM*`. + +These are the constants for filename-matching patterns; +see the sections below for details: + +| Constant | Meaning | +|-----------------------------------------------------|-------------------------------------------------------------| +| [`File::FNM_CASEFOLD`](#constant-filefnmcasefold) | Make the pattern case-insensitive. | +| [`File::FNM_DOTMATCH`](#constant-filefnmdotmatch) | Make pattern `*` match a leading period.. | +| [`File::FNM_EXTGLOB`](#constant-filefnmextglob) | Enable alternatives in pattern. | +| [`File::FNM_NOESCAPE`](#constant-filefnmnoescape) | Disable escaping. | +| [`File::FNM_PATHNAME`](#constant-filefnmpathname) | Make patterns `'*'` and `'?'` not match the file separator. | +| [`File::FNM_SHORTNAME`](#constant-filefnmshortname) | Enable short-name matching (Windows only). | +| [`File::FNM_SYSCASE`](#constant-filefnmsyscase) | Make the pattern use OS's case sensitivity. | + + +#### Constant File::FNM_CASEFOLD + +By default, filename matching is case-sensitive; +use constant [`File::FNM_CASEFOLD`](#constant-filefnmcasefold) +to make the matching case-insensitive: + +```ruby +File.fnmatch('abc', 'ABC') # => false +File.fnmatch('abc', 'ABC', File::FNM_CASEFOLD) # => true +``` + +#### Constant File::FNM_DOTMATCH + +By default, filename matching does not allow pattern `'*'` to match a dotfile name +(i.e, a filename beginning with a dot); +use constant [`File::FNM_DOTMATCH`](#constant-filefnmdotmatch) +to enable the match: + +```ruby +File.fnmatch('*', '.document') # => false +File.fnmatch('*', '.document', File::FNM_DOTMATCH) # => true +``` +#### Constant File::FNM_EXTGLOB + +By default, filename matching has the alternative notation disabled; +use constant [`File::FNM_EXTGLOB`](#constant-filefnmextglob) +to enable it: + +```ruby +File.fnmatch('R{ub,foo}y', 'Ruby') # => false +File.fnmatch('R{ub,foo}y', 'Ruby', File::FNM_EXTGLOB) # => true +``` + +The alternatives pattern consists of zero or more unquoted strings, +separated by commas, and enclosed in curly braces: + +```ruby +File.fnmatch('R{ub,foo,bar}y', 'Ruby') # => false # Not enabled. +File.fnmatch('R{ub,foo,bar}y', 'Ruby', File::FNM_EXTGLOB) # => true +# Whitespace matters. +File.fnmatch('R{ub ,foo,bar}y', 'Ruby', File::FNM_EXTGLOB) # => false +File.fnmatch('R{ ub,foo,bar}y', 'Ruby', File::FNM_EXTGLOB) # => false +# Special characters remain in force: +File.fnmatch('{*,?}', 'hello', File::FNM_EXTGLOB) # => true +File.fnmatch('{*ello,?}', 'hello', File::FNM_EXTGLOB) # => true +File.fnmatch('{*ELLO,?}', 'hello', File::FNM_EXTGLOB) # => false +File.fnmatch('{*ELLO,?????}', 'hello', File::FNM_EXTGLOB) # => true +# With the flag not given. +File.fnmatch('R{ub,foo,bar}y', 'Ruby') # => false +``` + +#### Constant File::FNM_NOESCAPE + +By default filename matching has escaping enabled; +use constant [`File::FNM_NOESCAPE`](#constant-filefnmnoescape) +to disable it: + +```ruby +File.fnmatch('\*\?\*\*', '*?**') # => true +File.fnmatch('\*\?\*\*', '*?**', File::FNM_NOESCAPE) # => false +``` + +#### Constant File::FNM_PATHNAME + +Flag [`File::FNM_PATHNAME`](#constant-filefnmpathname) affects +patterns `'**'`, `'*'`, and `'?'`. + +By default, the double-asterisk pattern (`'**'`) is equivalent to pattern `'*'`, +and matches any sequence of directory-like substrings: + +```ruby +File.fnmatch('**', 'a/b/c') # => true +File.fnmatch('*', 'a/b/c') # => true +``` + +When flag [`File::FNM_PATHNAME`](#constant-filefnmpathname) is given, +the pattern matches only one component of a file path: + +```ruby +File.fnmatch('**', 'a/b/c') # => true # Matches 'a/b/c'. +File.fnmatch('**', 'a/b/c', File::FNM_PATHNAME) # => false # Matches only 'a'. +File.fnmatch('**', 'a/b/c', File::FNM_PATHNAME) # => false # Matches only 'a/b'. +File.fnmatch('**/*', 'a/b/c', File::FNM_PATHNAME) # => true # Matches 'a/b', then 'c'. +``` + +By default, filename matching enables pattern `'*'` to match +at or across the file separator (`File::SEPARATOR`); +use constant [`File::FNM_PATHNAME`](#constant-filefnmpathname) +to disable such matching: + +```ruby +File::SEPARATOR # => "/" +File.fnmatch('*.rb', 'lib/test.rb') # => true +File.fnmatch('*.rb', 'lib/test.rb', File::FNM_PATHNAME) # => false +``` + +By default, filename matching enables pattern `'?'` to match +at or across the file separator (`File::SEPARATOR`); +use constant [`File::FNM_PATHNAME`](#constant-filefnmpathname) +to disable such matching: + +```ruby +File.fnmatch('foo?boo', 'foo/boo') # => true +File.fnmatch('foo?boo', 'foo/boo', File::FNM_PATHNAME) # => false +``` + +#### Constant File::FNM_SHORTNAME + +By default, Windows shortname matching is disabled; +use constant [`File::FNM_SHORTNAME`](#constant-filefnmshortname) +to enable it (on Windows only). + +Using that constant allows patterns to match short names +in filename matching on Windows, +which can be useful for compatibility with legacy applications +that rely on these short names; +see [8.3 filename](https://en.wikipedia.org/wiki/8.3_filename). +This feature helps ensure that file operations work correctly +even when dealing with files that have long names. + +```ruby +File::FNM_SHORTNAME.zero? # => false # On Windows, not zero; may be enabled. +File::FNM_SHORTNAME.zero? # => true # Elsewhere, always zero; may not be enabled. + +File.fnmatch('PROGRAM~1', 'Program Files') # => false +# This will be true if and only if on Windows and short name 'PROGRAM~1' exists. +File.fnmatch('PROGRAM~1', 'Program Files', File::FNM_SHORTNAME) # => true +``` + +#### Constant File::FNM_SYSCASE + +By default, filename matching uses Ruby's own case-sensitivity rules; +use constant [`File::FNM_SYSCASE`](#constant-filefnmsyscase) +to use the case-sensitivity rules of the underlying file system: + +```ruby +File::FNM_SYSCASE.zero? # => false # On Windows, not zero; may be enabled. +File::FNM_SYSCASE.zero? # => true # Elsewhere, always zero; may not be enabled. + +File.fnmatch('abc', 'ABC') # => false # Ruby; case-sensitive. +File.fnmatch('abc', 'ABC', File::FNM_SYSCASE) # => true # Windows; case-insensitive. +File.fnmatch('abc', 'ABC', File::FNM_SYSCASE) # => false # Linus; case-sensitive. +``` + From be5be55177d7099e98aba0f77836fc5535b18307 Mon Sep 17 00:00:00 2001 From: Burdette Lamar Date: Tue, 2 Jun 2026 22:39:31 -0500 Subject: [PATCH 102/188] [DOC] New page for "Filename Globbing" (#17173) --- doc/file/filename_globbing.md | 301 ++++++++++++++++++++++++++++++++++ 1 file changed, 301 insertions(+) create mode 100644 doc/file/filename_globbing.md diff --git a/doc/file/filename_globbing.md b/doc/file/filename_globbing.md new file mode 100644 index 00000000000000..7981964c5c33c3 --- /dev/null +++ b/doc/file/filename_globbing.md @@ -0,0 +1,301 @@ +# Filename Globbing + +Filename globbing is a pattern-matching feature implemented in certain Ruby methods. + +Filename-globbing methods find filesystem entries (files and directories) +that match certain patterns; +these methods are: + +- Dir.glob. +- [`Dir[]`](https://docs.ruby-lang.org/en/master/Dir.html#method-c-5B-5D). +- Pathname.glob. +- Pathname#glob. + +These methods are quite different from filename-matching methods (not discussed here), +which match patterns against string paths, and do not access the filesystem; +those methods are: + +- File.fnmatch. +- Pathname#fnmatch. + +These are the basic elements of filename-globbing patterns; +see the sections below for details: + +| Pattern | Meaning | Examples | +|:------------------------:|------------------------------------------|------------------------------| +| Simple string. | Matches itself. | `'LEGAL'` | +| `'*'` | Matches any sequence of characters. | `'*.txt'` | +| `'?'` | Matches any single character. | `'?.txt'` | +| `'[abc]'`,
`'[^abc]'` | Matches a single character from a set. | `'x[abc]y'`,
`'x[^abc]y'` | +| `'[a-z]`',
`'[^a-z]'` | Matches a single character from a range. | `'x[0-9]y'`,
`'x[^0-9]y'` | +| `'{ , }'` | Matches alternatives. | `'{abc,def}'` | +| `'**'` | Matches directories recursively. | `'**/test.rb'` | +| `'\'` | Escapes the next character. | `'\\*'`, `'\?'` | + +## Patterns + +### Simple \String + +A "simple string" is one that does not contain special filename-globbing patterns; +see the table above. + +A simple string matches itself: + +```ruby +Dir.glob('LEGAL') # => ["LEGAL"] +Dir.glob('LEGA') # => [] # Must be exact. +Dir.glob('legal') # => [] # Case-sensitive. +``` + +Note that case-sensitivity may _not_ be modified by flags. + +By default, the Windows short name pattern is disabled: + +```ruby +Dir.glob('PROGRAM~1') # => [] +``` + +It may be enabled by flag [`File::FNM_SHORTNAME`](#constant-filefnmshortname). + + +### Any Sequence of Characters (`'*'`) + +The asterisk pattern (`'*'`) matches any sequence of characters: + +```ruby +Dir.glob('*').take(3) # => ["BSDL", "CONTRIBUTING.md", "COPYING"] +Dir.glob('\*') # => [] # Escaped. +``` + +By default, the asterisk pattern does not match a leading period (as in a dot-file): + +```ruby +Dir.glob('*').select {|entry| entry.start_with?('.') } # => [] +``` + +That matching may be enabled by flag [`File::FNM_DOTMATCH`](#constant-filefnmdotmatch). + +The asterisk pattern does not match across file separators: + +```ruby +Dir.glob('*.rb').select {|entry| entry.include?('/') } # => [] +``` + +Therefore flag File::FNM_PATHNAME does not affect the pattern. + +### Single Character (`'?'`) + +The question-mark pattern (`'?'`) matches any single character: + +```ruby +Dir.glob('???') # => ["GPL", "bin", "doc", "enc", "ext", "jit", "lib", "man"] +Dir.glob('??') # => ["gc"] # Only one entry with a 2-character name. +Dir.glob('?') # => [] # No entries with a 1-character name. +Dir.glob('\?') # => [] # No entries containing character '?'. +``` + +By default, the question-mark pattern does not match a leading period (as in a dot-file): + +```ruby +Dir.glob(".???") # => [".git"] +Dir.glob("????").select {|entry| entry.start_with?('.') } # => [] +``` + +That matching may be enabled by flag [`File::FNM_DOTMATCH`](#constant-filefnmdotmatch). + +### Single Character from a Set (`'[abc]'`, `'[^abc]'`) + +Characters enclosed in square brackets define a set of characters, +any of which matches a single character: + +```ruby +Dir.glob('[efgh][abcd]') # => ["gc"] +Dir.glob('\[efgh][abcd]') # => [] # Escaped. +``` + +The character set may be negated: + +```ruby +Dir.glob('[^abcd][^efgh]') # => ["gc"] +``` + +### Single Character from a \Range (`'[a-c]'`, `'[^a-c]'`) + +A range of characters enclosed in square brackets defines a set of characters, +any of which matches a single character: + +```ruby +Dir.glob('[k-m][h-j][a-c]') # => ["lib"] +Dir.glob('\[k-m][h-j][a-c]') # => [] # Escaped. +``` + +The range may be negated: + +```ruby +Dir.glob('[^k-m][h-j][a-c]') # => [] +Dir.glob('[^a-c][^k-m][^h-j]') # => ["GPL", "doc", "enc", "ext", "jit", "lib", "man"] +``` + +### Alternatives (`'{ , }'`) + +The alternatives pattern consists of comma-separated strings +enclosed in curly braces: + +```ruby +Dir.glob('{k,L,R}*') # => ["kernel.rb", "LEGAL", "README.ja.md", "README.md"] +Dir.glob('{R,L,k}*') # => ["README.ja.md", "README.md", "LEGAL", "kernel.rb"] +# Whitespace matters: +Dir.glob('{k ,L,R}*') # => ["LEGAL", "README.ja.md", "README.md"] +``` + +### Recursive Directory Matching (`'**'`) + +The double-asterisk pattern (`'**'`) matches directories recursively: + +```ruby +# Find all entries everywhere ending with '.ja'. +Dir.glob('**/*.ja') +# => ["COPYING.ja", "doc/pty/README.expect.ja", "doc/pty/README.ja"] + +# Find all entries everywhere ending with '.rb'. +Dir.glob('**/*.rb').size # => 7574 +Dir.glob('**/*.rb').take(3) +# => ["KNOWNBUGS.rb", "array.rb", "ast.rb"] + +# Find all entries in directory 'lib' ending with `.rb'. +Dir.glob('lib/**/*.rb').size # => 626 +Dir.glob('lib/**/*.rb').take(3) +# # => +# ["lib/English.rb", +# "lib/bundled_gems.rb", +# "lib/bundler/build_metadata.rb"] + +# Find all entries in directory 'test/ruby' ending with '.rb'. +Dir.glob('test/ruby/**/*.rb').size # => 200 +Dir.glob('test/ruby/**/*.rb').take(3) +# # => +# ["test/ruby/allpairs.rb", +# "test/ruby/beginmainend.rb", +# "test/ruby/box/a.1_1_0.rb"] + +# Escaped. +Dir.glob('\**/*.rb') # => [] +``` + + +### Escape (`'\'`) + +The backslash character (`'\'`) may be used to escape any of the characters +that filename globbing treats as special: + +```ruby +Dir.glob('\*') # => [] +Dir.glob('\?') # => [] +Dir.glob('\[efgh][abcd]') # => [] +Dir.glob('\[k-m][h-j][a-c]') # => [] +Dir.glob('\**/*.rb') # => [] +``` + +## Keyword Arguments + +| Keyword | Value | Default | Meaning | +|-------------------|--------------------------|:-------:|-----------------------------------------| +| [`base`](#base) | \String path. | `'.'` | Root for searching. | +| [`flags`](#flags) | Logical OR of constants. | `0` | Modify globbing behavior. | +| [`sort`](#sort) | `true` or `false` | `true` | Whether returned array is to be sorted. | + +### `base` + +Optional keyword argument `base` (defaults to `'.'`) +specifies where in the filesystem the searching is to begin: + +```ruby +Dir.glob('*').size # => 241 +Dir.glob('*').take(3) +# => ["BSDL", "CONTRIBUTING.md", "COPYING"] + +Dir.glob('*', base: 'lib').size # => 72 +Dir.glob('*', base: 'lib').take(3) +# => ["English.gemspec", "English.rb", "bundled_gems.rb"] + +Dir.glob('*', base: 'lib/net').size # => 5 +Dir.glob('*', base: 'lib/net').take(3) +# => ["http", "http.rb", "https.rb"] +``` + +### `flags` + +Optional keyword argument `flags` (defaults to `0`) may be the bitwise OR +of the constants `File::FNM*`: + +```ruby +Dir.glob('*', flags: File::FNM_DOTMATCH | File::FNM_NOESCAPE) +``` + +These are the constants for filename-globbing patterns; +see the sections below for details: + + +| Constant | Meaning | +|-----------------------------------------------------|--------------------------------------------| +| [`File::FNM_DOTMATCH`](#constant-filefnmdotmatch) | Make pattern `'*'` match a leading period. | +| [`File::FNM_NOESCAPE`](#constant-filefnmnoescape) | Disable escaping. | +| [`File::FNM_SHORTNAME`](#constant-filefnmshortname) | Enable short-name matching (Windows only). | + +These constants do not affect filename globbing: + +- File::FNM_CASEFOLD. +- File::FNM_EXTGLOB. +- File::FNM_PATHNAME. +- File::FNM_SYSCASE. + +#### Constant File::FNM_DOTMATCH + +By default, filename globbing does not allow patterns `'*'` and `'?'` to match a dotfile name +(i.e, an entry name beginning with a dot); +use constant [`File::FNM_DOTMATCH`](#constant-filefnmdotmatch) +to enable the match: + +```ruby +Dir.glob('*').size # => 241 +Dir.glob('*', flags: File::FNM_DOTMATCH).size # => 256 +Dir.glob('*', flags: File::FNM_DOTMATCH).take(3) # => [".", ".dir-locals.el", ".document"] +``` + +#### Constant File::FNM_NOESCAPE + +By default filename globbing has escaping enabled; +use constant [`File::FNM_NOESCAPE`](#constant-filefnmnoescape) +to disable it: + +```ruby +Dir.glob('*').size # => 241 +Dir.glob('\*').size # => 0 +``` + +#### Constant File::FNM_SHORTNAME + +By default, Windows shortname matching is disabled; +use constant [`File::FNM_SHORTNAME`](#constant-filefnmshortname) +to enable it (on Windows only). + +Using that constant allows patterns to match short names +in filename globbing on Windows, +which can be useful for compatibility with legacy applications +that rely on these short names; +see [8.3 filename](https://en.wikipedia.org/wiki/8.3_filename). +This feature helps ensure that file operations work correctly +even when dealing with files that have long names. + +### `sort` + +Optional keyword argument `sort` (defaults to `'true'`) +specifies whether the returned array is to be sorted: + +```ruby +Dir.glob('*').take(3) +# => ["BSDL", "CONTRIBUTING.md", "COPYING"] +Dir.glob('*', sort: false).take(3) +# => ["gc.rb", "yjit.rb", "iseq.h"] +``` + From 2ff671c93d337cc7c4793f9958557f3f8f4eb623 Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Wed, 3 Jun 2026 10:26:08 +0900 Subject: [PATCH 103/188] [DOC] Improve docs for ObjectSpace.internal_class_of --- ext/objspace/objspace.c | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/ext/objspace/objspace.c b/ext/objspace/objspace.c index b880c6a5c70856..112355ad275c6a 100644 --- a/ext/objspace/objspace.c +++ b/ext/objspace/objspace.c @@ -720,12 +720,28 @@ wrap_klass_iow(VALUE klass) /* * call-seq: - * ObjectSpace.internal_class_of(obj) -> Class or Module + * ObjectSpace.internal_class_of(obj) -> class or module * - * [MRI specific feature] Return internal class of obj. - * obj can be an instance of InternalObjectWrapper. + * Returns the real class of +obj+, which may differ from the class returned + * by Object#class. + * + * Ruby inserts hidden classes into an object's ancestry, such as a singleton + * class or an included module's iclass. Object#class skips over these, but + * this method returns the first one, including any hidden class: + * + * require 'objspace' + * + * s = "x" + * def s.foo; end # gives +s+ a singleton class + * s.class # => String + * ObjectSpace.internal_class_of(s) # => #> + * + * +obj+ may be an ObjectSpace::InternalObjectWrapper, in which case the class + * of the wrapped internal object is returned. * * Note that you should not use this method in your application. + * + * This method is only expected to work with C Ruby. */ static VALUE objspace_internal_class_of(VALUE self, VALUE obj) From 2faad3767c506ad4ee0e22f3141bd8e3bc3d4b06 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Wed, 3 Jun 2026 08:28:31 +0200 Subject: [PATCH 104/188] [ruby/json] Release 2.19.8 https://github.com/ruby/json/commit/5233dd9b85 --- ext/json/lib/json/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/json/lib/json/version.rb b/ext/json/lib/json/version.rb index a69590ff9c762f..30c0a71d2f28fb 100644 --- a/ext/json/lib/json/version.rb +++ b/ext/json/lib/json/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module JSON - VERSION = '2.19.7' + VERSION = '2.19.8' end From 6d39970a85fb51b2da87bfee574f749cab99d1fc Mon Sep 17 00:00:00 2001 From: git Date: Wed, 3 Jun 2026 06:31:37 +0000 Subject: [PATCH 105/188] Update default gems list at 2faad3767c506ad4ee0e22f3141bd8 [ci skip] --- NEWS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NEWS.md b/NEWS.md index 8699bc0bff3faa..b5884b5bdb512d 100644 --- a/NEWS.md +++ b/NEWS.md @@ -82,7 +82,7 @@ releases. * 6.0.1 to [v6.0.1.1][erb-v6.0.1.1], [v6.0.2][erb-v6.0.2], [v6.0.3][erb-v6.0.3], [v6.0.4][erb-v6.0.4] * ipaddr 1.2.9 * 1.2.8 to [v1.2.9][ipaddr-v1.2.9] -* json 2.19.7 +* json 2.19.8 * 2.18.0 to [v2.18.1][json-v2.18.1], [v2.19.0][json-v2.19.0], [v2.19.1][json-v2.19.1], [v2.19.2][json-v2.19.2], [v2.19.3][json-v2.19.3], [v2.19.4][json-v2.19.4], [v2.19.5][json-v2.19.5], [v2.19.6][json-v2.19.6], [v2.19.7][json-v2.19.7] * openssl 4.0.2 * 4.0.0 to [v4.0.1][openssl-v4.0.1], [v4.0.2][openssl-v4.0.2] From 695996c9764d7d49a8e3a87962f2475afb09a25c Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Wed, 3 Jun 2026 08:48:47 +0200 Subject: [PATCH 106/188] [ruby/json] parser.c: noinline `json_eat_comments` Comments shouldn't be present in performance sensitive documents, so it's best not to inline it to make space for more important code. https://github.com/ruby/json/commit/f7fe86ea69 --- ext/json/parser/parser.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/json/parser/parser.c b/ext/json/parser/parser.c index 503bed1fd477d3..56b214fc42381d 100644 --- a/ext/json/parser/parser.c +++ b/ext/json/parser/parser.c @@ -529,7 +529,7 @@ static uint32_t unescape_unicode(JSON_ParserState *state, const char *sp, const static const rb_data_type_t JSON_ParserConfig_type; -static void +NOINLINE(static) void json_eat_comments(JSON_ParserState *state) { const char *start = state->cursor; From 8fefa5537c58b80e08e0bb87f6bb6a999519dd95 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Wed, 3 Jun 2026 09:25:51 +0200 Subject: [PATCH 107/188] [ruby/json] Integrate with Ruby 4.1 `ruby_sized_xfree` Ensure that buffer sizes have been properly tracked when running in debug mode, and appease the GC slightly when running in release mode. Ref: https://bugs.ruby-lang.org/issues/21861 https://github.com/ruby/json/commit/90354007ea --- ext/json/fbuffer/fbuffer.h | 4 ++-- ext/json/generator/extconf.rb | 1 + ext/json/json.h | 18 ++++++++++++++++++ ext/json/parser/extconf.rb | 1 + ext/json/parser/parser.c | 6 +++--- 5 files changed, 25 insertions(+), 5 deletions(-) diff --git a/ext/json/fbuffer/fbuffer.h b/ext/json/fbuffer/fbuffer.h index b84a073554cf8c..b4f5266ca57106 100644 --- a/ext/json/fbuffer/fbuffer.h +++ b/ext/json/fbuffer/fbuffer.h @@ -64,7 +64,7 @@ static inline void fbuffer_consumed(FBuffer *fb, size_t consumed) static void fbuffer_free(FBuffer *fb) { if (fb->ptr && fb->type == FBUFFER_HEAP_ALLOCATED) { - ruby_xfree(fb->ptr); + JSON_SIZED_FREE_N(fb->ptr, fb->capa); } } @@ -88,7 +88,7 @@ static void fbuffer_realloc(FBuffer *fb, size_t required) fb->type = FBUFFER_HEAP_ALLOCATED; MEMCPY(fb->ptr, old_buffer, char, fb->len); } else { - REALLOC_N(fb->ptr, char, required); + JSON_SIZED_REALLOC_N(fb->ptr, char, required, fb->capa); } fb->capa = required; } diff --git a/ext/json/generator/extconf.rb b/ext/json/generator/extconf.rb index 205a887e78f3bd..33af03ea3058f8 100644 --- a/ext/json/generator/extconf.rb +++ b/ext/json/generator/extconf.rb @@ -6,6 +6,7 @@ else append_cflags("-std=c99") have_const("RUBY_TYPED_EMBEDDABLE", "ruby.h") # RUBY_VERSION >= 3.3 + have_func("ruby_xfree_sized", "ruby.h") # RUBY_VERSION >= 4.1 $defs << "-DJSON_GENERATOR" $defs << "-DJSON_DEBUG" if ENV.fetch("JSON_DEBUG", "0") != "0" diff --git a/ext/json/json.h b/ext/json/json.h index 8737989a6c9aa6..cf9420d4dd2456 100644 --- a/ext/json/json.h +++ b/ext/json/json.h @@ -49,6 +49,24 @@ typedef unsigned char _Bool; #endif #endif +#ifndef HAVE_RUBY_XFREE_SIZED +static inline void ruby_xfree_sized(void *ptr, size_t oldsize) +{ + ruby_xfree(ptr); +} + +static inline void *ruby_xrealloc2_sized(void *ptr, size_t new_elems, size_t elem_size, size_t old_elems) +{ + return ruby_xrealloc2(ptr, new_elems, elem_size); +} +#endif + +# define JSON_SIZED_REALLOC_N(v, T, m, n) \ + ((v) = (T *)ruby_xrealloc2_sized((void *)(v), (m), sizeof(T), (n))) + +# define JSON_SIZED_FREE(v) ruby_xfree_sized((void *)(v), sizeof(*(v))) +# define JSON_SIZED_FREE_N(v, n) ruby_xfree_sized((void *)(v), sizeof(*(v)) * (n)) + #ifndef HAVE_RB_EXT_RACTOR_SAFE # undef RUBY_TYPED_FROZEN_SHAREABLE # define RUBY_TYPED_FROZEN_SHAREABLE 0 diff --git a/ext/json/parser/extconf.rb b/ext/json/parser/extconf.rb index f12fc2dddce02a..a9d740c7556b5d 100644 --- a/ext/json/parser/extconf.rb +++ b/ext/json/parser/extconf.rb @@ -6,6 +6,7 @@ have_func("rb_str_to_interned_str", "ruby.h") # RUBY_VERSION >= 3.0 have_func("rb_hash_new_capa", "ruby.h") # RUBY_VERSION >= 3.2 have_func("rb_hash_bulk_insert", "ruby.h") # Missing on TruffleRuby +have_func("ruby_xfree_sized", "ruby.h") # RUBY_VERSION >= 4.1 if RUBY_ENGINE == "ruby" have_const("RUBY_TYPED_EMBEDDABLE", "ruby.h") # RUBY_VERSION >= 3.3 diff --git a/ext/json/parser/parser.c b/ext/json/parser/parser.c index 56b214fc42381d..d0482f68611968 100644 --- a/ext/json/parser/parser.c +++ b/ext/json/parser/parser.c @@ -211,7 +211,7 @@ static rvalue_stack *rvalue_stack_grow(rvalue_stack *stack, VALUE *handle, rvalu if (stack->type == RVALUE_STACK_STACK_ALLOCATED) { stack = rvalue_stack_spill(stack, handle, stack_ref); } else { - REALLOC_N(stack->ptr, VALUE, required); + JSON_SIZED_REALLOC_N(stack->ptr, VALUE, required, stack->capa); stack->capa = required; } return stack; @@ -250,7 +250,7 @@ static void rvalue_stack_mark(void *ptr) static void rvalue_stack_free_buffer(rvalue_stack *stack) { - ruby_xfree(stack->ptr); + JSON_SIZED_FREE_N(stack->ptr, stack->capa); stack->ptr = NULL; } @@ -260,7 +260,7 @@ static void rvalue_stack_free(void *ptr) if (stack) { rvalue_stack_free_buffer(stack); #ifndef HAVE_RUBY_TYPED_EMBEDDABLE - ruby_xfree(stack); + JSON_SIZED_FREE(stack); #endif } } From 71515f8bfdd8da23b59742f39abac328913e2868 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Wed, 3 Jun 2026 09:45:45 +0200 Subject: [PATCH 108/188] [ruby/json] Coverage: ignore test/* https://github.com/ruby/json/commit/52c9d9bdf2 --- test/json/test_helper.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/test/json/test_helper.rb b/test/json/test_helper.rb index 24cde4348cdbf7..4c5a91a1926ebe 100644 --- a/test/json/test_helper.rb +++ b/test/json/test_helper.rb @@ -21,6 +21,7 @@ track_files 'lib/**/*.rb' add_filter 'lib/json/truffle_ruby' unless RUBY_ENGINE == 'truffleruby' + add_filter 'test/' end end From 464e5665a4b02760196867406f1291261ea08fbe Mon Sep 17 00:00:00 2001 From: git Date: Wed, 3 Jun 2026 08:30:18 +0000 Subject: [PATCH 109/188] [DOC] Update bundled gems list at 71515f8bfdd8da23b59742f39abac3 --- NEWS.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/NEWS.md b/NEWS.md index b5884b5bdb512d..501b894c67264e 100644 --- a/NEWS.md +++ b/NEWS.md @@ -75,9 +75,9 @@ releases. ### The following default gems are updated. * RubyGems 4.1.0.dev - * 4.0.3 to [v4.0.4][RubyGems-v4.0.4], [v4.0.5][RubyGems-v4.0.5], [v4.0.6][RubyGems-v4.0.6], [v4.0.7][RubyGems-v4.0.7], [v4.0.8][RubyGems-v4.0.8], [v4.0.9][RubyGems-v4.0.9], [v4.0.10][RubyGems-v4.0.10], [v4.0.11][RubyGems-v4.0.11], [v4.0.12][RubyGems-v4.0.12] + * 4.0.3 to [v4.0.4][RubyGems-v4.0.4], [v4.0.5][RubyGems-v4.0.5], [v4.0.6][RubyGems-v4.0.6], [v4.0.7][RubyGems-v4.0.7], [v4.0.8][RubyGems-v4.0.8], [v4.0.9][RubyGems-v4.0.9], [v4.0.10][RubyGems-v4.0.10], [v4.0.11][RubyGems-v4.0.11], [v4.0.12][RubyGems-v4.0.12], [v4.0.13][RubyGems-v4.0.13] * bundler 4.1.0.dev - * 4.0.3 to [v4.0.4][bundler-v4.0.4], [v4.0.5][bundler-v4.0.5], [v4.0.6][bundler-v4.0.6], [v4.0.7][bundler-v4.0.7], [v4.0.8][bundler-v4.0.8], [v4.0.9][bundler-v4.0.9], [v4.0.10][bundler-v4.0.10], [v4.0.11][bundler-v4.0.11], [v4.0.12][bundler-v4.0.12] + * 4.0.3 to [v4.0.4][bundler-v4.0.4], [v4.0.5][bundler-v4.0.5], [v4.0.6][bundler-v4.0.6], [v4.0.7][bundler-v4.0.7], [v4.0.8][bundler-v4.0.8], [v4.0.9][bundler-v4.0.9], [v4.0.10][bundler-v4.0.10], [v4.0.11][bundler-v4.0.11], [v4.0.12][bundler-v4.0.12], [v4.0.13][bundler-v4.0.13] * erb 6.0.4 * 6.0.1 to [v6.0.1.1][erb-v6.0.1.1], [v6.0.2][erb-v6.0.2], [v6.0.3][erb-v6.0.3], [v6.0.4][erb-v6.0.4] * ipaddr 1.2.9 @@ -202,6 +202,7 @@ A lot of work has gone into making Ractors more stable, performant, and usable. [RubyGems-v4.0.10]: https://github.com/rubygems/rubygems/releases/tag/v4.0.10 [RubyGems-v4.0.11]: https://github.com/rubygems/rubygems/releases/tag/v4.0.11 [RubyGems-v4.0.12]: https://github.com/rubygems/rubygems/releases/tag/v4.0.12 +[RubyGems-v4.0.13]: https://github.com/rubygems/rubygems/releases/tag/v4.0.13 [bundler-v4.0.4]: https://github.com/rubygems/rubygems/releases/tag/bundler-v4.0.4 [bundler-v4.0.5]: https://github.com/rubygems/rubygems/releases/tag/bundler-v4.0.5 [bundler-v4.0.6]: https://github.com/rubygems/rubygems/releases/tag/bundler-v4.0.6 @@ -211,6 +212,7 @@ A lot of work has gone into making Ractors more stable, performant, and usable. [bundler-v4.0.10]: https://github.com/rubygems/rubygems/releases/tag/bundler-v4.0.10 [bundler-v4.0.11]: https://github.com/rubygems/rubygems/releases/tag/bundler-v4.0.11 [bundler-v4.0.12]: https://github.com/rubygems/rubygems/releases/tag/bundler-v4.0.12 +[bundler-v4.0.13]: https://github.com/rubygems/rubygems/releases/tag/bundler-v4.0.13 [erb-v6.0.1.1]: https://github.com/ruby/erb/releases/tag/v6.0.1.1 [erb-v6.0.2]: https://github.com/ruby/erb/releases/tag/v6.0.2 [erb-v6.0.3]: https://github.com/ruby/erb/releases/tag/v6.0.3 From f717edca05fa42e122b7fb8148aa6a728b3aa582 Mon Sep 17 00:00:00 2001 From: Nozomi Hijikata <121233810+nozomemein@users.noreply.github.com> Date: Wed, 3 Jun 2026 22:42:20 +0900 Subject: [PATCH 110/188] ZJIT: Implement Polymorphic DefinedIvar (#16981) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ZJIT: Implement Polymorphic Definedivar Closes: https://github.com/Shopify/ruby/issues/980 Build polymorphic shape/type branches for definedivar in HIR. For each profiled T_OBJECT shape, specialize defined?(`@ivar`) to a constant pushval or nil based on the compile-time ivar index check. DefinedIvar already profiles self on monomorphic shape guard failures for recompilation, so the recompiled HIR can use the collected polymorphic shapes while keeping a generic DefinedIvar fallback for misses and unsupported shapes. ## Benchmark ### lobsters Summary: ```diff - dynamic_definedivar_count: 1,294,854 ( 0.7%) + dynamic_definedivar_count: 503,058 ( 0.3%) ratio_in_zjit: 88.4% ``` ``` zjit-master: ruby 4.1.0dev (2026-05-23T00:08:49Z master 5855d61ee4) +ZJIT stats +PRISM [arm64-darwin25] zjit-polymorphic-definedivar: ruby 4.1.0dev (2026-05-23T07:11:22Z zjit-polymorphic-d.. f75a82e7f1) +ZJIT stats +PRISM [arm64-darwin25] last_commit=ZJIT: Implement Polymorphic Definedivar -------- ---------------- --------------------------------- ------------------------------------ ---------------------------------------- bench zjit-master (ms) zjit-polymorphic-definedivar (ms) zjit-polymorphic-definedivar 1st itr zjit-master/zjit-polymorphic-definedivar lobsters 529.9 ± 3.6% 504.2 ± 7.7% 1.148 1.051 -------- ---------------- --------------------------------- ------------------------------------ ---------------------------------------- ``` Full stats details below:
before patch ``` Top-2 not optimized method types for send (100.0% of total 65,120): null: 44,877 (68.9%) optimized: 20,243 (31.1%) Top-3 not optimized method types for send_without_block (100.0% of total 716,678): optimized_send: 711,905 (99.3%) optimized_block_call: 4,299 ( 0.6%) zsuper: 474 ( 0.1%) Top-1 not optimized method types for super (100.0% of total 3,678): attrset: 3,678 (100.0%) Top-1 instructions with uncategorized fallback reason (100.0% of total 65,868): opt_send_without_block: 65,868 (100.0%) Top-20 send fallback reasons (99.5% of total 22,386,632): invokeblock_not_specialized: 5,946,612 (26.6%) one_or_more_complex_arg_pass: 4,376,419 (19.5%) send_without_block_polymorphic: 3,748,855 (16.7%) send_without_block_no_profiles: 2,830,116 (12.6%) send_without_block_megamorphic: 1,108,467 ( 5.0%) sendforward_not_specialized: 971,957 ( 4.3%) send_polymorphic: 806,862 ( 3.6%) send_without_block_not_optimized_method_type_optimized: 716,204 ( 3.2%) super_polymorphic: 337,181 ( 1.5%) too_many_args_for_lir: 333,633 ( 1.5%) send_not_optimized_need_permission: 238,842 ( 1.1%) send_without_block_not_optimized_need_permission: 180,946 ( 0.8%) send_no_profiles: 177,008 ( 0.8%) argc_param_mismatch: 124,061 ( 0.6%) super_complex_args_pass: 89,280 ( 0.4%) invokesuperforward_not_specialized: 80,729 ( 0.4%) uncategorized: 65,868 ( 0.3%) send_not_optimized_method_type: 65,120 ( 0.3%) super_from_block: 43,185 ( 0.2%) obj_to_string_not_string: 42,731 ( 0.2%) Top-4 setivar fallback reasons (100.0% of total 4,182,333): not_monomorphic: 3,969,254 (94.9%) not_t_object: 119,505 ( 2.9%) complex: 93,131 ( 2.2%) new_shape_needs_extension: 443 ( 0.0%) Top-2 getivar fallback reasons (100.0% of total 11,716,846): not_monomorphic: 11,537,525 (98.5%) complex: 179,321 ( 1.5%) Top-3 definedivar fallback reasons (100.0% of total 1,294,854): not_monomorphic: 1,289,423 (99.6%) complex: 5,122 ( 0.4%) not_t_object: 309 ( 0.0%) Top-5 invokeblock handler (100.0% of total 6,968,323): monomorphic_ifunc: 4,143,418 (59.5%) monomorphic_other: 1,444,525 (20.7%) monomorphic_iseq: 865,749 (12.4%) polymorphic: 508,677 ( 7.3%) megamorphic: 5,954 ( 0.1%) Top-6 getblockparamproxy handler (100.0% of total 3,321,873): polymorphic: 2,370,458 (71.4%) nil: 492,185 (14.8%) iseq: 242,627 ( 7.3%) no_profiles: 168,826 ( 5.1%) proc: 40,245 ( 1.2%) megamorphic: 7,532 ( 0.2%) Top-7 popular complex argument-parameter features not optimized (100.0% of total 4,677,994): caller_blockarg: 3,092,361 (66.1%) param_forwardable: 697,044 (14.9%) param_rest: 409,311 ( 8.7%) caller_kwarg: 193,462 ( 4.1%) param_kwrest: 143,181 ( 3.1%) caller_kw_splat: 85,992 ( 1.8%) caller_splat: 56,643 ( 1.2%) Top-3 compile error reasons (100.0% of total 196,397): exception_handler: 192,000 (97.8%) native_stack_too_large: 4,342 ( 2.2%) validation_mismatched_block_arity: 55 ( 0.0%) Top-2 unhandled HIR insns (100.0% of total 229,881): throw: 194,104 (84.4%) invokebuiltin: 35,777 (15.6%) Top-19 side exit reasons (100.0% of total 8,637,525): guard_type_failure: 7,692,359 (89.1%) unhandled_hir_insn: 229,881 ( 2.7%) compile_error: 196,397 ( 2.3%) patchpoint_method_redefined: 118,680 ( 1.4%) block_param_proxy_fallback_miss: 112,551 ( 1.3%) no_profile_send: 93,334 ( 1.1%) block_param_proxy_not_nil: 75,195 ( 0.9%) patchpoint_stable_constant_names: 55,819 ( 0.6%) patchpoint_no_singleton_class: 19,352 ( 0.2%) unhandled_block_arg: 14,541 ( 0.2%) block_param_proxy_not_iseq_or_ifunc: 13,401 ( 0.2%) fixnum_lshift_overflow: 10,165 ( 0.1%) guard_less_failure: 3,120 ( 0.0%) guard_shape_failure: 1,302 ( 0.0%) guard_greater_eq_failure: 941 ( 0.0%) guard_super_method_entry: 407 ( 0.0%) interrupt: 49 ( 0.0%) obj_to_string_fallback: 30 ( 0.0%) guard_not_shared_failure: 1 ( 0.0%) send_count: 182,469,185 dynamic_send_count: 22,386,632 (12.3%) optimized_send_count: 160,082,553 (87.7%) dynamic_setivar_count: 4,182,333 ( 2.3%) dynamic_getivar_count: 11,716,846 ( 6.4%) dynamic_definedivar_count: 1,294,854 ( 0.7%) iseq_optimized_send_count: 54,154,353 (29.7%) inline_cfunc_optimized_send_count: 74,812,159 (41.0%) inline_iseq_optimized_send_count: 6,346,705 ( 3.5%) non_variadic_cfunc_optimized_send_count: 13,789,249 ( 7.6%) variadic_cfunc_optimized_send_count: 10,980,087 ( 6.0%) compiled_iseq_count: 6,166 compiled_side_exit_count: 80,485 failed_iseq_count: 3 compile_time: 2,462ms compile_side_exit_time: 113ms compile_side_exit_time_ratio: 4.6% compile_hir_time: 798ms compile_hir_build_time: 238ms compile_hir_strength_reduce_time: 322ms compile_hir_canonicalize_time: 49ms compile_hir_fold_constants_time: 37ms compile_hir_clean_cfg_time: 26ms compile_hir_eliminate_dead_code_time: 30ms compile_lir_time: 1,525ms profile_time: 39ms gc_time: 33ms invalidation_time: 14ms vm_write_jit_frame_count: 137,433,269 vm_write_sp_count: 137,433,269 vm_write_locals_count: 131,272,119 vm_write_stack_count: 131,272,119 vm_write_to_parent_iseq_local_count: 688,492 guard_type_count: 145,711,992 guard_type_exit_ratio: 5.3% guard_shape_count: 36,890,099 guard_shape_exit_ratio: 0.0% load_field_count: 210,155,590 store_field_count: 15,758,742 side_exit_size: 13,563,708 code_region_bytes: 33,505,280 side_exit_size_ratio: 40.5% zjit_alloc_bytes: 22,783,024 total_mem_bytes: 56,288,304 side_exit_count: 8,637,525 total_insn_count: 995,180,861 vm_insn_count: 115,515,525 zjit_insn_count: 879,665,336 ratio_in_zjit: 88.4% ```
after patch ``` Top-2 not optimized method types for send (100.0% of total 65,119): null: 44,877 (68.9%) optimized: 20,242 (31.1%) Top-3 not optimized method types for send_without_block (100.0% of total 716,636): optimized_send: 711,869 (99.3%) optimized_block_call: 4,293 ( 0.6%) zsuper: 474 ( 0.1%) Top-1 not optimized method types for super (100.0% of total 3,676): attrset: 3,676 (100.0%) Top-1 instructions with uncategorized fallback reason (100.0% of total 65,866): opt_send_without_block: 65,866 (100.0%) Top-20 send fallback reasons (99.5% of total 22,409,237): invokeblock_not_specialized: 5,946,405 (26.5%) one_or_more_complex_arg_pass: 4,376,457 (19.5%) send_without_block_polymorphic: 3,775,975 (16.9%) send_without_block_no_profiles: 2,830,029 (12.6%) send_without_block_megamorphic: 1,104,527 ( 4.9%) sendforward_not_specialized: 971,924 ( 4.3%) send_polymorphic: 806,852 ( 3.6%) send_without_block_not_optimized_method_type_optimized: 716,162 ( 3.2%) super_polymorphic: 337,178 ( 1.5%) too_many_args_for_lir: 333,625 ( 1.5%) send_not_optimized_need_permission: 238,842 ( 1.1%) send_without_block_not_optimized_need_permission: 180,949 ( 0.8%) send_no_profiles: 176,799 ( 0.8%) argc_param_mismatch: 124,064 ( 0.6%) super_complex_args_pass: 89,280 ( 0.4%) invokesuperforward_not_specialized: 80,723 ( 0.4%) uncategorized: 65,866 ( 0.3%) send_not_optimized_method_type: 65,119 ( 0.3%) super_from_block: 43,182 ( 0.2%) obj_to_string_not_string: 42,725 ( 0.2%) Top-4 setivar fallback reasons (100.0% of total 4,182,216): not_monomorphic: 3,969,159 (94.9%) not_t_object: 119,484 ( 2.9%) complex: 93,131 ( 2.2%) new_shape_needs_extension: 442 ( 0.0%) Top-2 getivar fallback reasons (100.0% of total 11,716,471): not_monomorphic: 11,537,150 (98.5%) complex: 179,321 ( 1.5%) Top-3 definedivar fallback reasons (100.0% of total 503,058): not_monomorphic: 498,173 (99.0%) complex: 4,576 ( 0.9%) not_t_object: 309 ( 0.1%) Top-5 invokeblock handler (100.0% of total 6,968,099): monomorphic_ifunc: 4,143,324 (59.5%) monomorphic_other: 1,444,457 (20.7%) monomorphic_iseq: 865,705 (12.4%) polymorphic: 508,659 ( 7.3%) megamorphic: 5,954 ( 0.1%) Top-6 getblockparamproxy handler (100.0% of total 3,321,803): polymorphic: 2,370,425 (71.4%) nil: 492,172 (14.8%) iseq: 242,810 ( 7.3%) no_profiles: 168,625 ( 5.1%) proc: 40,239 ( 1.2%) megamorphic: 7,532 ( 0.2%) Top-7 popular complex argument-parameter features not optimized (100.0% of total 4,678,011): caller_blockarg: 3,092,480 (66.1%) param_forwardable: 697,011 (14.9%) param_rest: 409,285 ( 8.7%) caller_kwarg: 193,450 ( 4.1%) param_kwrest: 143,179 ( 3.1%) caller_kw_splat: 85,971 ( 1.8%) caller_splat: 56,635 ( 1.2%) Top-3 compile error reasons (100.0% of total 196,386): exception_handler: 191,989 (97.8%) native_stack_too_large: 4,342 ( 2.2%) validation_mismatched_block_arity: 55 ( 0.0%) Top-2 unhandled HIR insns (100.0% of total 229,876): throw: 194,101 (84.4%) invokebuiltin: 35,775 (15.6%) Top-19 side exit reasons (100.0% of total 8,638,646): guard_type_failure: 7,693,566 (89.1%) unhandled_hir_insn: 229,876 ( 2.7%) compile_error: 196,386 ( 2.3%) patchpoint_method_redefined: 118,676 ( 1.4%) block_param_proxy_fallback_miss: 112,548 ( 1.3%) no_profile_send: 93,330 ( 1.1%) block_param_proxy_not_nil: 75,195 ( 0.9%) patchpoint_stable_constant_names: 55,792 ( 0.6%) patchpoint_no_singleton_class: 19,326 ( 0.2%) unhandled_block_arg: 14,540 ( 0.2%) block_param_proxy_not_iseq_or_ifunc: 13,401 ( 0.2%) fixnum_lshift_overflow: 10,165 ( 0.1%) guard_less_failure: 3,119 ( 0.0%) guard_shape_failure: 1,302 ( 0.0%) guard_greater_eq_failure: 941 ( 0.0%) guard_super_method_entry: 407 ( 0.0%) interrupt: 45 ( 0.0%) obj_to_string_fallback: 30 ( 0.0%) guard_not_shared_failure: 1 ( 0.0%) send_count: 182,477,537 dynamic_send_count: 22,409,237 (12.3%) optimized_send_count: 160,068,300 (87.7%) dynamic_setivar_count: 4,182,216 ( 2.3%) dynamic_getivar_count: 11,716,471 ( 6.4%) dynamic_definedivar_count: 503,058 ( 0.3%) iseq_optimized_send_count: 54,138,292 (29.7%) inline_cfunc_optimized_send_count: 74,815,196 (41.0%) inline_iseq_optimized_send_count: 6,346,484 ( 3.5%) non_variadic_cfunc_optimized_send_count: 13,788,597 ( 7.6%) variadic_cfunc_optimized_send_count: 10,979,731 ( 6.0%) compiled_iseq_count: 6,167 compiled_side_exit_count: 80,471 failed_iseq_count: 3 compile_time: 2,711ms compile_side_exit_time: 125ms compile_side_exit_time_ratio: 4.6% compile_hir_time: 870ms compile_hir_build_time: 266ms compile_hir_strength_reduce_time: 347ms compile_hir_canonicalize_time: 52ms compile_hir_fold_constants_time: 40ms compile_hir_clean_cfg_time: 27ms compile_hir_eliminate_dead_code_time: 32ms compile_lir_time: 1,680ms profile_time: 48ms gc_time: 32ms invalidation_time: 15ms vm_write_jit_frame_count: 137,425,789 vm_write_sp_count: 137,425,789 vm_write_locals_count: 131,264,963 vm_write_stack_count: 131,264,963 vm_write_to_parent_iseq_local_count: 688,457 guard_type_count: 146,412,455 guard_type_exit_ratio: 5.3% guard_shape_count: 36,937,934 guard_shape_exit_ratio: 0.0% load_field_count: 211,394,883 store_field_count: 15,778,119 side_exit_size: 13,563,624 code_region_bytes: 33,505,280 side_exit_size_ratio: 40.5% zjit_alloc_bytes: 22,870,223 total_mem_bytes: 56,375,503 side_exit_count: 8,638,646 total_insn_count: 995,144,390 vm_insn_count: 115,472,694 zjit_insn_count: 879,671,696 ratio_in_zjit: 88.4% ```
* ZJIT: Add TODO for deduplicating DefinedIvar specialization * ZJIT: Add more DefinedIvar HIR tests Cover unsupported receiver profiles and a mixed polymorphic profile with a generic DefinedIvar fallback. * ZJIT: Add more DefinedIvar polymorphic HIR test Cover polymorphic definedivar profiles with: - complex path - non-T_OBJECT path --- zjit/src/hir.rs | 65 +++++++++- zjit/src/hir/opt_tests.rs | 267 +++++++++++++++++++++++++++++++++++++- 2 files changed, 322 insertions(+), 10 deletions(-) diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index f5363aebe261bb..02e3759cd69d9e 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -4497,8 +4497,6 @@ impl Function { self.push_insn_id(block, insn_id); continue; } if self.policy.no_side_exits { - // TODO: Support polymorphic DefinedIvar shape-specialized paths. - // https://github.com/Shopify/ruby/issues/980 // On the final version, keep the DefinedIvar fallback instead of another shape guard. self.push_insn_id(block, insn_id); continue; } @@ -7155,7 +7153,68 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { // (ID id, IVC ic, VALUE pushval) let id = ID(get_arg(pc, 0).as_u64()); let pushval = get_arg(pc, 2); - state.stack_push(fun.push_insn(block, Insn::DefinedIvar { self_val: self_param, id, pushval, state: exit_id })); + if let Some(summary) = fun.polymorphic_summary(&profiles, self_param, exit_state.insn_idx) { + self_param = fun.push_insn(block, Insn::GuardType { val: self_param, guard_type: types::HeapBasicObject, state: exit_id }); + let rbasic_flags = fun.load_rbasic_flags(block, self_param); + let join_block = insn_idx_to_block.get(&insn_idx).copied().unwrap_or_else(|| fun.new_block(insn_idx)); + let join_param = fun.push_insn(join_block, Insn::Param); + // Dedup by expected shape and type so objects with different classes + // but the same shape can share code. + let mut seen_shape_and_flags = Vec::with_capacity(summary.buckets().len()); + for &profiled_type in summary.buckets() { + // End of the buckets + if profiled_type.is_empty() { break; } + // Runtime immediates cannot pass the HeapBasicObject guard, so don't + // generate unreachable shape branches for profiled immediate buckets. + if profiled_type.flags().is_immediate() { continue; } + // Class/module/T_DATA ivars use different storage rules. + // Let the fallthrough DefinedIvar handle these. + if !profiled_type.flags().is_t_object() { continue; } + let expected_shape = profiled_type.shape(); + let (expected_rbasic_flags, rbasic_flags_mask) = profiled_type.rbasic_flags_and_mask(); + assert!(expected_shape.is_valid()); + // Too-complex shapes use hash tables for ivars; + // rb_shape_get_iv_index doesn't work for them. + // Let the fallthrough DefinedIvar handle these. + if expected_shape.is_complex() { continue; } + if seen_shape_and_flags.contains(&expected_rbasic_flags) { continue; } + seen_shape_and_flags.push(expected_rbasic_flags); + let rbasic_flags_mask = fun.push_insn(block, Insn::Const { val: Const::CUInt64(rbasic_flags_mask) }); + // The expected shape can change over run, so we put it + // as a pointer to keep it stable in snapshot tests. + let expected_rbasic_flags = fun.push_insn(block, Insn::Const { val: Const::CPtr(ptr::without_provenance(expected_rbasic_flags.to_usize())) }); + let expected_rbasic_flags = fun.push_insn(block, Insn::RefineType { val: expected_rbasic_flags, new_type: types::CUInt64 }); + let masked = fun.push_insn(block, Insn::IntAnd { left: rbasic_flags, right: rbasic_flags_mask}); + let has_shape_and_type = fun.push_insn(block, Insn::IsBitEqual { left: masked, right: expected_rbasic_flags }); + let iftrue_block = fun.new_block(insn_idx); + let target = BranchEdge { target: iftrue_block, args: vec![] }; + let fall_through = fun.new_block(insn_idx); + + fun.push_insn(block, Insn::CondBranch { val: has_shape_and_type, + if_true: target, + if_false: BranchEdge { target: fall_through, args: vec![] } + }); + + block = fall_through; + let mut ivar_index: attr_index_t = 0; + let result = if unsafe { rb_shape_get_iv_index(expected_shape.0, id, &mut ivar_index) } { + fun.push_insn(iftrue_block, Insn::Const { val: Const::Value(pushval) }) + } else { + fun.push_insn(iftrue_block, Insn::Const { val: Const::Value(Qnil) }) + }; + fun.push_insn(iftrue_block, Insn::Jump(BranchEdge { target: join_block, args: vec![result] })); + } + // In the fallthrough case, do a generic interpreter definedivar and then join. + let result = fun.push_insn(block, Insn::DefinedIvar { self_val: self_param, id, pushval, state: exit_id }); + fun.push_insn(block, Insn::Jump(BranchEdge { target: join_block, args: vec![result] })); + state.stack_push(join_param); + block = join_block; + } else { + // TODO: Handle monomorphic definedivar specialization here too, including the + // no_side_exits policy, so optimize_getivar doesn't need a separate DefinedIvar + // path. Unlike GetIvar, DefinedIvar isn't emitted by later lowering passes. + state.stack_push(fun.push_insn(block, Insn::DefinedIvar { self_val: self_param, id, pushval, state: exit_id })); + } } YARVINSN_checkkeyword => { // When a keyword is unspecified past index 32, a hash will be used instead. diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index 3212390ecf948f..74b951d2e59410 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -5638,7 +5638,40 @@ mod hir_opt_tests { } #[test] - fn test_dont_specialize_definedivar_with_t_data() { + fn test_dont_specialize_definedivar_with_immediate() { + eval(" + module M + def test = defined?(@a) + end + + class Integer + include M + end + + 1.test + 2.test + TEST = M.instance_method(:test) + "); + assert_snapshot!(hir_string_proc("TEST"), @" + fn test@:3: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb3(v1) + bb2(): + EntryPoint JIT(0) + v4:BasicObject = LoadArg :self@0 + Jump bb3(v4) + bb3(v6:BasicObject): + v10:StringExact|NilClass = DefinedIvar v6, :@a + CheckInterrupts + Return v10 + "); + } + + #[test] + fn test_dont_specialize_definedivar_with_t_struct() { + // Range is T_STRUCT (not T_OBJECT): falls back to DefinedIvar. eval(" class C < Range def test = defined?(@a) @@ -5666,7 +5699,7 @@ mod hir_opt_tests { } #[test] - fn test_dont_specialize_polymorphic_definedivar() { + fn test_optimize_definedivar_polymorphic() { set_call_threshold(3); eval(" class C @@ -5691,9 +5724,206 @@ mod hir_opt_tests { v4:BasicObject = LoadArg :self@0 Jump bb3(v4) bb3(v6:BasicObject): - v10:StringExact|NilClass = DefinedIvar v6, :@a + v10:HeapBasicObject = GuardType v6, HeapBasicObject + v11:CUInt64 = LoadField v10, :RBASIC_FLAGS@0x1000 + v13:CUInt64[0xffffffff0000001f] = Const CUInt64(0xffffffff0000001f) + v14:CPtr[CPtr(0x1001)] = Const CPtr(0x1001) + v15 = RefineType v14, CUInt64 + v16:CInt64 = IntAnd v11, v13 + v17:CBool = IsBitEqual v16, v15 + CondBranch v17, bb5(), bb6() + bb5(): + v19:NilClass = Const Value(nil) + Jump bb4(v19) + bb6(): + v21:CUInt64[0xffffffff0000001f] = Const CUInt64(0xffffffff0000001f) + v22:CPtr[CPtr(0x1002)] = Const CPtr(0x1002) + v23 = RefineType v22, CUInt64 + v24:CInt64 = IntAnd v11, v21 + v25:CBool = IsBitEqual v24, v23 + CondBranch v25, bb7(), bb8() + bb7(): + v27:StringExact[VALUE(0x1008)] = Const Value(VALUE(0x1008)) + Jump bb4(v27) + bb8(): + v29:StringExact|NilClass = DefinedIvar v10, :@a + Jump bb4(v29) + bb4(v12:StringExact|NilClass): CheckInterrupts - Return v10 + Return v12 + "); + } + + #[test] + fn test_optimize_definedivar_polymorphic_with_immediate() { + set_call_threshold(3); + eval(r#" + module M + def test = defined?(@a) + end + + class C + include M + end + + class Integer + include M + end + + obj = C.new + obj.instance_variable_set(:@a, 1) + + obj.test + 1.test + TEST = M.instance_method(:test) + "#); + assert_snapshot!(hir_string_proc("TEST"), @" + fn test@:3: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb3(v1) + bb2(): + EntryPoint JIT(0) + v4:BasicObject = LoadArg :self@0 + Jump bb3(v4) + bb3(v6:BasicObject): + v10:HeapBasicObject = GuardType v6, HeapBasicObject + v11:CUInt64 = LoadField v10, :RBASIC_FLAGS@0x1000 + v13:CUInt64[0xffffffff0000001f] = Const CUInt64(0xffffffff0000001f) + v14:CPtr[CPtr(0x1001)] = Const CPtr(0x1001) + v15 = RefineType v14, CUInt64 + v16:CInt64 = IntAnd v11, v13 + v17:CBool = IsBitEqual v16, v15 + CondBranch v17, bb5(), bb6() + bb5(): + v19:StringExact[VALUE(0x1008)] = Const Value(VALUE(0x1008)) + Jump bb4(v19) + bb6(): + v21:StringExact|NilClass = DefinedIvar v10, :@a + Jump bb4(v21) + bb4(v12:StringExact|NilClass): + CheckInterrupts + Return v12 + "); + } + + #[test] + fn test_optimize_definedivar_polymorphic_with_t_struct() { + set_call_threshold(3); + eval(r#" + module M + def test = defined?(@a) + end + + class C + include M + end + + class D < Range + include M + end + + obj = C.new + obj.instance_variable_set(:@a, 1) + + range = D.new 0, 1 + range.instance_variable_set(:@a, 1) + + obj.test + range.test + TEST = M.instance_method(:test) + "#); + assert_snapshot!(hir_string_proc("TEST"), @" + fn test@:3: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb3(v1) + bb2(): + EntryPoint JIT(0) + v4:BasicObject = LoadArg :self@0 + Jump bb3(v4) + bb3(v6:BasicObject): + v10:HeapBasicObject = GuardType v6, HeapBasicObject + v11:CUInt64 = LoadField v10, :RBASIC_FLAGS@0x1000 + v13:CUInt64[0xffffffff0000001f] = Const CUInt64(0xffffffff0000001f) + v14:CPtr[CPtr(0x1001)] = Const CPtr(0x1001) + v15 = RefineType v14, CUInt64 + v16:CInt64 = IntAnd v11, v13 + v17:CBool = IsBitEqual v16, v15 + CondBranch v17, bb5(), bb6() + bb5(): + v19:StringExact[VALUE(0x1008)] = Const Value(VALUE(0x1008)) + Jump bb4(v19) + bb6(): + v21:StringExact|NilClass = DefinedIvar v10, :@a + Jump bb4(v21) + bb4(v12:StringExact|NilClass): + CheckInterrupts + Return v12 + "); + } + + #[test] + fn test_optimize_definedivar_polymorphic_with_complex_shape() { + set_call_threshold(3); + eval(r#" + module M + def test = defined?(@a) + end + + class C + include M + end + + class D + include M + end + + obj = C.new + obj.instance_variable_set(:@a, 1) + + complex = D.new + (0..1000).each do |i| + complex.instance_variable_set(:"@v#{i}", i) + end + (0..1000).each do |i| + complex.remove_instance_variable(:"@v#{i}") + end + + obj.test + complex.test + TEST = M.instance_method(:test) + "#); + assert_snapshot!(hir_string_proc("TEST"), @" + fn test@:3: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb3(v1) + bb2(): + EntryPoint JIT(0) + v4:BasicObject = LoadArg :self@0 + Jump bb3(v4) + bb3(v6:BasicObject): + v10:HeapBasicObject = GuardType v6, HeapBasicObject + v11:CUInt64 = LoadField v10, :RBASIC_FLAGS@0x1000 + v13:CUInt64[0xffffffff0000001f] = Const CUInt64(0xffffffff0000001f) + v14:CPtr[CPtr(0x1001)] = Const CPtr(0x1001) + v15 = RefineType v14, CUInt64 + v16:CInt64 = IntAnd v11, v13 + v17:CBool = IsBitEqual v16, v15 + CondBranch v17, bb5(), bb6() + bb5(): + v19:StringExact[VALUE(0x1008)] = Const Value(VALUE(0x1008)) + Jump bb4(v19) + bb6(): + v21:StringExact|NilClass = DefinedIvar v10, :@a + Jump bb4(v21) + bb4(v12:StringExact|NilClass): + CheckInterrupts + Return v12 "); } @@ -8010,7 +8240,7 @@ mod hir_opt_tests { fn test_definedivar_shape_guard_recompile() { // Call with one shape to compile, then call with a different shape to // trigger shape guard exits and recompilation. On the recompiled version, - // DefinedIvar stays as a C call fallback to avoid more shape guard exits. + // DefinedIvar uses polymorphic fast paths plus a C call fallback. eval(" class C def initialize(extra = false) @@ -8038,9 +8268,32 @@ mod hir_opt_tests { v4:HeapBasicObject = LoadArg :self@0 Jump bb3(v4) bb3(v6:HeapBasicObject): - v10:StringExact|NilClass = DefinedIvar v6, :@foo + v11:CUInt64 = LoadField v6, :RBASIC_FLAGS@0x1000 + v13:CUInt64[0xffffffff0000001f] = Const CUInt64(0xffffffff0000001f) + v14:CPtr[CPtr(0x1001)] = Const CPtr(0x1001) + v15 = RefineType v14, CUInt64 + v16:CInt64 = IntAnd v11, v13 + v17:CBool = IsBitEqual v16, v15 + CondBranch v17, bb5(), bb6() + bb5(): + v19:StringExact[VALUE(0x1008)] = Const Value(VALUE(0x1008)) + Jump bb4(v19) + bb6(): + v21:CUInt64[0xffffffff0000001f] = Const CUInt64(0xffffffff0000001f) + v22:CPtr[CPtr(0x1010)] = Const CPtr(0x1010) + v23 = RefineType v22, CUInt64 + v24:CInt64 = IntAnd v11, v21 + v25:CBool = IsBitEqual v24, v23 + CondBranch v25, bb7(), bb8() + bb7(): + v27:StringExact[VALUE(0x1008)] = Const Value(VALUE(0x1008)) + Jump bb4(v27) + bb8(): + v29:StringExact|NilClass = DefinedIvar v6, :@foo + Jump bb4(v29) + bb4(v12:StringExact|NilClass): CheckInterrupts - Return v10 + Return v12 "); } From 6da082f6dbc1270893631519c98c18e0deaa208c Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Wed, 3 Jun 2026 12:02:46 -0700 Subject: [PATCH 111/188] ZJIT: Initialize JITFrame on method entry (#17188) --- zjit.h | 6 ----- zjit/bindgen/src/main.rs | 1 - zjit/src/codegen.rs | 41 +++++++++++++++++++--------------- zjit/src/codegen_tests.rs | 15 +++---------- zjit/src/cruby_bindings.inc.rs | 1 - zjit/src/jit_frame.rs | 11 ++++----- 6 files changed, 32 insertions(+), 43 deletions(-) diff --git a/zjit.h b/zjit.h index 5a56297a4e5299..fdece2a6cd33bd 100644 --- a/zjit.h +++ b/zjit.h @@ -57,9 +57,6 @@ void rb_zjit_materialize_frames(const rb_execution_context_t *ec, rb_control_fra // instead of a heap-allocated JITFrame pointer. #define ZJIT_JIT_RETURN_C_FRAME 0x1 -// BADFrame. The high bit is set, so likely SEGV on linux and darwin if dereferenced. -#define ZJIT_JIT_RETURN_POISON 0xbadfbadfbadfbadfULL - static inline const zjit_jit_frame_t * CFP_ZJIT_FRAME(const rb_control_frame_t *cfp) { @@ -67,9 +64,6 @@ CFP_ZJIT_FRAME(const rb_control_frame_t *cfp) return &rb_zjit_c_frame; } else { -#if USE_ZJIT - RUBY_ASSERT((unsigned long long)((VALUE *)cfp->jit_return)[-1] != ZJIT_JIT_RETURN_POISON); -#endif // Read JITFrame from the stack slot. gen_entry_point() writes an initial // frame describing the entry PC + iseq; subsequent gen_save_pc_for_gc() // calls update it with a more accurate PC before any non-leaf C call. diff --git a/zjit/bindgen/src/main.rs b/zjit/bindgen/src/main.rs index 59daf0b92fe3b5..d015f975de626c 100644 --- a/zjit/bindgen/src/main.rs +++ b/zjit/bindgen/src/main.rs @@ -315,7 +315,6 @@ fn main() { .allowlist_function("rb_zjit_insn_leaf") .allowlist_type("jit_bindgen_constants") .allowlist_type("zjit_struct_offsets") - .allowlist_var("ZJIT_JIT_RETURN_POISON") .allowlist_var("ZJIT_JIT_RETURN_C_FRAME") .allowlist_function("rb_assert_holding_vm_lock") .allowlist_function("rb_jit_shape_complex_p") diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index f0ba2fdd8ad7b0..b2edc6e167f5fb 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -47,13 +47,6 @@ const PC_POISON: Option<*const VALUE> = if cfg!(feature = "runtime_checks") { None }; -/// Sentinel jit_return stored on ISEQ frame push when runtime checks are enabled. -const JIT_RETURN_POISON: Option = if cfg!(feature = "runtime_checks") { - Some(ZJIT_JIT_RETURN_POISON as usize) -} else { - None -}; - /// Ephemeral code generation state struct JITState { /// ISEQ version that is being compiled, which will be used by PatchPoint @@ -94,6 +87,11 @@ impl JITState { self.opnds[insn_id.0].unwrap_or_else(|| panic!("Failed to get_opnd({insn_id})")) } + /// Get the ISEQ for the version currently being compiled. + fn iseq(&self) -> IseqPtr { + unsafe { self.version.as_ref().iseq } + } + /// Find or create a label for a given BlockId fn get_label(&mut self, asm: &mut Assembler, lir_block_id: lir::BlockId, hir_block_id: BlockId) -> Target { // Extend labels vector if the requested index is out of bounds @@ -2189,6 +2187,19 @@ fn gen_object_alloc_class(asm: &mut Assembler, class: VALUE, state: &FrameState) } } +/// Map an entry point to the bytecode PC used by its initial JITFrame. +/// JIT call entries use `opt_table[jit_entry_idx]`; the interpreter entry uses +/// `opt_table.last()` for the fall-through path where all optionals are filled. +fn entry_pc(iseq: IseqPtr, jit_entry_idx: Option) -> *const VALUE { + let params = unsafe { iseq.params() }; + let opt_table = params.opt_table_slice(); + let entry_idx = jit_entry_idx.unwrap_or_else(|| opt_table.len() - 1); + let entry_insn_idx = opt_table.get(entry_idx) + .unwrap_or_else(|| panic!("entry_pc: opt_table out of bounds. {params:#?}, entry_idx={entry_idx}")) + .as_u32(); + unsafe { rb_iseq_pc_at_idx(iseq, entry_insn_idx) } +} + /// Compile a frame setup. If jit_entry_idx is Some, remember the address of it as a JIT entry. fn gen_entry_point(jit: &mut JITState, asm: &mut Assembler, jit_entry_idx: Option) { if let Some(jit_entry_idx) = jit_entry_idx { @@ -2200,16 +2211,10 @@ fn gen_entry_point(jit: &mut JITState, asm: &mut Assembler, jit_entry_idx: Optio } asm.frame_setup(&[]); - // Publish the JITFrame slot's location via cfp->jit_return. The slot at - // [NATIVE_BASE_PTR - 8] is left uninitialized here; the JIT design relies on - // gen_save_pc_for_gc() to populate it before any C call, and on cross-ractor - // barriers ensuring that no other ractor scans this CFP before such a call. + // Publish a valid entry JITFrame before setting cfp->jit_return. + let jit_frame = JITFrame::new_iseq(entry_pc(jit.iseq(), jit_entry_idx), jit.iseq()); + asm.mov(Opnd::mem(64, NATIVE_BASE_PTR, -SIZEOF_VALUE_I32), Opnd::const_ptr(jit_frame)); asm.mov(Opnd::mem(64, CFP, RUBY_OFFSET_CFP_JIT_RETURN), NATIVE_BASE_PTR); - - // Poison the JITFrame slot. It should be read only after gen_save_pc_for_gc(). - if let Some(jit_return_poison) = JIT_RETURN_POISON { - asm.mov(Opnd::mem(64, NATIVE_BASE_PTR, -SIZEOF_VALUE_I32), jit_return_poison.into()); - } } /// Compile code that exits from JIT code with a return value @@ -2683,7 +2688,7 @@ fn gen_incr_send_fallback_counter(asm: &mut Assembler, reason: SendFallbackReaso /// (send, sendforward, invokesuper, invokesuperforward, invokeblock). /// These instructions call vm_caller_setup_arg_block which writes to cfp->block_code. #[allow(non_upper_case_globals)] -fn iseq_may_write_block_code(iseq: IseqPtr) -> bool { +pub(crate) fn iseq_may_write_block_code(iseq: IseqPtr) -> bool { let encoded_size = unsafe { rb_iseq_encoded_size(iseq) }; let mut insn_idx: u32 = 0; @@ -2715,7 +2720,7 @@ fn gen_save_pc_for_gc(asm: &mut Assembler, state: &FrameState) { gen_incr_counter(asm, Counter::vm_write_jit_frame_count); asm_comment!(asm, "save JITFrame to CFP"); - let jit_frame = JITFrame::new_iseq(next_pc, state.iseq, !iseq_may_write_block_code(state.iseq)); + let jit_frame = JITFrame::new_iseq(next_pc, state.iseq); asm.mov(Opnd::mem(64, NATIVE_BASE_PTR, -SIZEOF_VALUE_I32), Opnd::const_ptr(jit_frame)); // CFP_PC for a live JIT frame routes through the JITFrame on the native diff --git a/zjit/src/codegen_tests.rs b/zjit/src/codegen_tests.rs index c1eec5b875350a..1ed4a289dfdfd9 100644 --- a/zjit/src/codegen_tests.rs +++ b/zjit/src/codegen_tests.rs @@ -4269,14 +4269,9 @@ fn test_getspecial_multiple_groups() { assert_snapshot!(assert_compiles(r#"test("123-456")"#), @r#""456""#); } -// In a JIT-to-JIT call, gen_push_frame writes JIT_RETURN_POISON to the -// callee's cfp->jit_return (runtime_checks builds). On the *first* such -// call the function stub trampoline clears jit_return to NULL, so the -// crash only manifests on the second JIT-to-JIT hit when the stub has -// been patched to jump directly to the callee's JIT entry. Putting $& as -// the first C call in the callee keeps the poison live until -// gen_getspecial_symbol calls rb_backref_get → rb_vm_svar_lep → CFP_PC → -// CFP_ZJIT_FRAME, which dereferences the poison without the prep fix. +// In a JIT-to-JIT call, the callee's cfp->jit_return is published at entry. +// Putting $& as the first C call in the callee exercises CFP_ZJIT_FRAME before +// gen_save_pc_for_gc has a chance to update the entry JITFrame. #[test] fn test_getspecial_symbol_in_jit_to_jit_callee() { eval(r#" @@ -4287,10 +4282,6 @@ fn test_getspecial_symbol_in_jit_to_jit_callee() { callee callee - # First call to caller_method profiles; second JITs caller_method - # and runs through the function-stub-hit path which clears - # jit_return. The third call goes through the patched stub with - # POISON intact, hitting the bug. caller_method caller_method "#); diff --git a/zjit/src/cruby_bindings.inc.rs b/zjit/src/cruby_bindings.inc.rs index 08c502b0d84e47..cc6a37d827796b 100644 --- a/zjit/src/cruby_bindings.inc.rs +++ b/zjit/src/cruby_bindings.inc.rs @@ -235,7 +235,6 @@ pub const VM_ENV_DATA_INDEX_FLAGS: u32 = 0; pub const VM_BLOCK_HANDLER_NONE: u32 = 0; pub const SHAPE_ID_NUM_BITS: u32 = 32; pub const ZJIT_JIT_RETURN_C_FRAME: u32 = 1; -pub const ZJIT_JIT_RETURN_POISON: i64 = -4981057192772781345; pub type rb_alloc_func_t = ::std::option::Option VALUE>; pub const RUBY_Qfalse: ruby_special_consts = 0; pub const RUBY_Qnil: ruby_special_consts = 4; diff --git a/zjit/src/jit_frame.rs b/zjit/src/jit_frame.rs index b434d0a8ed1dfd..8691833db08032 100644 --- a/zjit/src/jit_frame.rs +++ b/zjit/src/jit_frame.rs @@ -1,5 +1,6 @@ use crate::cruby::{IseqPtr, VALUE, rb_gc_mark_movable, rb_gc_location}; use crate::cruby::zjit_jit_frame; +use crate::codegen::iseq_may_write_block_code; use crate::state::ZJITState; /// JITFrame struct is defined in zjit.h (the single source of truth) and @@ -16,7 +17,8 @@ impl JITFrame { } /// Create a JITFrame for an ISEQ frame. - pub fn new_iseq(pc: *const VALUE, iseq: IseqPtr, materialize_block_code: bool) -> *const Self { + pub fn new_iseq(pc: *const VALUE, iseq: IseqPtr) -> *const Self { + let materialize_block_code = !iseq_may_write_block_code(iseq); Self::alloc(JITFrame { pc, iseq, materialize_block_code }) } @@ -121,11 +123,10 @@ mod tests { "#), @"100"); } - // Side exit at the very start of a method, before any jit_return has been - // written by gen_save_pc_for_gc. The jit_return field should be 0 (from - // vm_push_frame), so materialization should be a no-op for that frame. + // Side exit at the very start of a method, before gen_save_pc_for_gc has + // updated the entry JITFrame. #[test] - fn test_side_exit_before_jit_return_write() { + fn test_side_exit_before_jit_frame_update() { assert_snapshot!(inspect(" def entry(n) = n + 1 entry(1) From 739ec91d5a06395206b103d9d499a3ff9af62203 Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Mon, 16 Mar 2026 20:13:06 -0700 Subject: [PATCH 112/188] Run FREEOBJ hook as separate step --- gc/default/default.c | 66 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 60 insertions(+), 6 deletions(-) diff --git a/gc/default/default.c b/gc/default/default.c index 145140dfbfbc4e..0027f7a13c9796 100644 --- a/gc/default/default.c +++ b/gc/default/default.c @@ -3688,10 +3688,6 @@ gc_sweep_plane(rb_objspace_t *objspace, rb_heap_t *heap, uintptr_t p, bits_t bit #endif if (!rb_gc_obj_needs_cleanup_p(vp)) { - if (RB_UNLIKELY(objspace->hook_events & RUBY_INTERNAL_EVENT_FREEOBJ)) { - rb_gc_event_hook(vp, RUBY_INTERNAL_EVENT_FREEOBJ); - } - (void)VALGRIND_MAKE_MEM_UNDEFINED((void*)p, slot_size); heap_page_add_freeobj(objspace, sweep_page, vp); gc_report(3, objspace, "page_sweep: %s (fast path) added to freelist\n", rb_obj_info(vp)); @@ -3700,8 +3696,6 @@ gc_sweep_plane(rb_objspace_t *objspace, rb_heap_t *heap, uintptr_t p, bits_t bit else { gc_report(2, objspace, "page_sweep: free %p\n", (void *)p); - rb_gc_event_hook(vp, RUBY_INTERNAL_EVENT_FREEOBJ); - rb_gc_obj_free_vm_weak_references(vp); if (rb_gc_obj_free(objspace, vp)) { (void)VALGRIND_MAKE_MEM_UNDEFINED((void*)p, slot_size); @@ -3920,12 +3914,72 @@ gc_ractor_newobj_cache_clear(void *c, void *data) } } +static void +gc_sweep_freeobj_hooks_page(rb_objspace_t *objspace, struct heap_page *page) +{ + bits_t *bits = page->mark_bits; + uintptr_t p = (uintptr_t)page->start; + short slot_size = page->slot_size; + int total_slots = page->total_slots; + int bitmap_plane_count = CEILDIV(total_slots, BITS_BITLENGTH); + + int out_of_range_bits = total_slots % BITS_BITLENGTH; + bits_t last_plane_mask = (out_of_range_bits != 0) + ? ~(((bits_t)1 << out_of_range_bits) - 1) + : 0; + + for (int j = 0; j < bitmap_plane_count; j++) { + bits_t bitset = ~bits[j]; + if (j == bitmap_plane_count - 1) { + bitset &= ~last_plane_mask; + } + + uintptr_t pp = p; + while (bitset) { + if (bitset & 1) { + VALUE vp = (VALUE)pp; + asan_unpoisoning_object(vp) { + switch (BUILTIN_TYPE(vp)) { + case T_NONE: + case T_ZOMBIE: + case T_MOVED: + break; + default: + rb_gc_event_hook(vp, RUBY_INTERNAL_EVENT_FREEOBJ); + break; + } + } + } + pp += slot_size; + bitset >>= 1; + } + p += BITS_BITLENGTH * slot_size; + } +} + +static void +gc_sweep_freeobj_hooks(rb_objspace_t *objspace) +{ + for (int i = 0; i < HEAP_COUNT; i++) { + rb_heap_t *heap = &heaps[i]; + struct heap_page *page = NULL; + + ccan_list_for_each(&heap->pages, page, page_node) { + gc_sweep_freeobj_hooks_page(objspace, page); + } + } +} + static void gc_sweep_start(rb_objspace_t *objspace) { gc_mode_transition(objspace, gc_mode_sweeping); objspace->rincgc.pooled_slots = 0; + if (RB_UNLIKELY(objspace->hook_events & RUBY_INTERNAL_EVENT_FREEOBJ)) { + gc_sweep_freeobj_hooks(objspace); + } + #if GC_CAN_COMPILE_COMPACTION if (objspace->flags.during_compacting) { gc_sort_heap_by_compare_func( From 623fa94e9e85f14feff102d5c4412ee6e52aba1a Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Wed, 3 Jun 2026 20:43:53 +0900 Subject: [PATCH 113/188] [DOC] Improve docs for ObjectSpace.memsize_of --- ext/objspace/objspace.c | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/ext/objspace/objspace.c b/ext/objspace/objspace.c index 112355ad275c6a..20eab820a16a47 100644 --- a/ext/objspace/objspace.c +++ b/ext/objspace/objspace.c @@ -30,19 +30,33 @@ /* * call-seq: - * ObjectSpace.memsize_of(obj) -> Integer + * ObjectSpace.memsize_of(obj) -> integer * - * Return consuming memory size of obj in bytes. + * Returns the amount of memory in bytes consumed by +obj+. * - * Note that the return size is incomplete. You need to deal with this - * information as only a *HINT*. Especially, the size of +T_DATA+ may not be - * correct. + * The returned size includes the slot that +obj+ occupies plus any memory + * that +obj+ allocates outside of that slot, such as the storage backing a + * large String, Array, or Hash: * - * This method is only expected to work with CRuby. + * require 'objspace' + * + * ObjectSpace.memsize_of("small") # => 40 + * ObjectSpace.memsize_of("a" * 1000) # => 1041 + * ObjectSpace.memsize_of([1, 2, 3]) # => 40 + * ObjectSpace.memsize_of(Array.new(100)) # => 840 + * + * Special constants such as +true+, +false+, +nil+, small integers, and some + * symbols do not occupy a slot, so their size is reported as +0+: * - * From Ruby 3.2 with Variable Width Allocation, it returns the actual slot - * size used plus any additional memory allocated outside the slot (such - * as external strings, arrays, or hash tables). + * ObjectSpace.memsize_of(true) # => 0 + * ObjectSpace.memsize_of(42) # => 0 + * + * The returned size is only a hint and may be an underestimate, since it does + * not account for all of the memory that +obj+ references. In particular, the + * size of a +T_DATA+ object (an object implemented in C, such as one defined + * by a C extension) may not be reported correctly. + * + * This method is only expected to work with CRuby. */ static VALUE From 1d104ecf976d3103a66efd1e5eea1e1a1566eec9 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Wed, 3 Jun 2026 20:16:51 -0400 Subject: [PATCH 114/188] ZJIT: Skip HeapBasicObject pointer check if known heap object (#17151) This saves ~115KiB of code on lobsters but mostly it's just a common-sense thing I've been meaning to do for a while. I have also thought about splitting up `GuardType` into different kinds of guards but I like the HIR uniformity. Before: ``` ZJIT stats: code_region_bytes: 15,630,336 zjit_alloc_bytes: 19,423,844 compile_time 1509.19ms profile_time 22.26ms gc_time 19.16ms invalidation_time 6.28ms ``` After: ``` ZJIT stats: code_region_bytes: 15,515,648 zjit_alloc_bytes: 19,333,088 compile_time 1535.77ms profile_time 22.61ms gc_time 16.83ms invalidation_time 6.50ms ``` --- zjit/src/codegen.rs | 60 ++++++++++++++++++++++++++++----------------- 1 file changed, 37 insertions(+), 23 deletions(-) diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index b2edc6e167f5fb..47f4a97ac7caa3 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -696,8 +696,14 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio &Insn::UnboxFixnum { val } => gen_unbox_fixnum(asm, opnd!(val)), Insn::Test { val } => gen_test(asm, opnd!(val)), Insn::RefineType { val, .. } => opnd!(val), - Insn::HasType { val, expected } => gen_has_type(jit, asm, opnd!(val), *expected), - Insn::GuardType { val, guard_type, state } => gen_guard_type(jit, asm, opnd!(val), *guard_type, &function.frame_state(*state)), + Insn::HasType { val, expected } => { + let val_type = function.type_of(*val); + gen_has_type(jit, asm, opnd!(val), val_type, *expected) + } + Insn::GuardType { val, guard_type, state } => { + let val_type = function.type_of(*val); + gen_guard_type(jit, asm, opnd!(val), val_type, *guard_type, &function.frame_state(*state)) + } &Insn::GuardBitEquals { val, expected, reason, state, recompile } => gen_guard_bit_equals(jit, asm, opnd!(val), expected, reason, recompile, &function.frame_state(state)), &Insn::GuardAnyBitSet { val, mask, reason, state, .. } => gen_guard_any_bit_set(jit, asm, opnd!(val), mask, reason, &function.frame_state(state)), &Insn::GuardNoBitsSet { val, mask, reason, state, .. } => gen_guard_no_bits_set(jit, asm, opnd!(val), mask, reason, &function.frame_state(state)), @@ -2452,7 +2458,7 @@ fn gen_test(asm: &mut Assembler, val: lir::Opnd) -> lir::Opnd { asm.csel_e(0.into(), 1.into()) } -fn gen_has_type(jit: &mut JITState, asm: &mut Assembler, val: lir::Opnd, ty: Type) -> lir::Opnd { +fn gen_has_type(jit: &mut JITState, asm: &mut Assembler, val: lir::Opnd, val_type: Type, ty: Type) -> lir::Opnd { if ty.is_subtype(types::Fixnum) { asm.test(val, Opnd::UImm(RUBY_FIXNUM_FLAG as u64)); asm.csel_nz(Opnd::Imm(1), Opnd::Imm(0)) @@ -2495,13 +2501,16 @@ fn gen_has_type(jit: &mut JITState, asm: &mut Assembler, val: lir::Opnd, ty: Typ // TODO: Max thinks codegen should not care about the shapes of the operands except to create them. (Shopify/ruby#685) let val = asm.load_mem(val); - // Immediate -> definitely not the class - asm.test(val, (RUBY_IMMEDIATE_MASK as u64).into()); - asm.jnz(jit, result_edge(Opnd::Imm(0))); + let is_known_heap_basic_object = val_type.is_subtype(types::HeapBasicObject); + if !is_known_heap_basic_object { + // Immediate -> definitely not the class + asm.test(val, (RUBY_IMMEDIATE_MASK as u64).into()); + asm.jnz(jit, result_edge(Opnd::Imm(0))); - // Qfalse -> definitely not the class - asm.cmp(val, Qfalse.into()); - asm.je(jit, result_edge(Opnd::Imm(0))); + // Qfalse -> definitely not the class + asm.cmp(val, Qfalse.into()); + asm.je(jit, result_edge(Opnd::Imm(0))); + } // Heap object -> check klass field let klass = asm.load(Opnd::mem(64, val, RUBY_OFFSET_RBASIC_KLASS)); @@ -2522,7 +2531,8 @@ fn gen_has_type(jit: &mut JITState, asm: &mut Assembler, val: lir::Opnd, ty: Typ } /// Compile a type check with a side exit -fn gen_guard_type(jit: &mut JITState, asm: &mut Assembler, val: lir::Opnd, guard_type: Type, state: &FrameState) -> lir::Opnd { +fn gen_guard_type(jit: &mut JITState, asm: &mut Assembler, val: lir::Opnd, val_type: Type, guard_type: Type, state: &FrameState) -> lir::Opnd { + let is_known_heap_basic_object = val_type.is_subtype(types::HeapBasicObject); gen_incr_counter(asm, Counter::guard_type_count); if guard_type.is_subtype(types::Fixnum) { asm.test(val, Opnd::UImm(RUBY_FIXNUM_FLAG as u64)); @@ -2558,14 +2568,16 @@ fn gen_guard_type(jit: &mut JITState, asm: &mut Assembler, val: lir::Opnd, guard // TODO: Max thinks codegen should not care about the shapes of the operands except to create them. (Shopify/ruby#685) let val = asm.load_mem(val); - // Check if it's a special constant let side_exit = side_exit(jit, state, GuardType(guard_type)); - asm.test(val, (RUBY_IMMEDIATE_MASK as u64).into()); - asm.jnz(jit, side_exit.clone()); - - // Check if it's false - asm.cmp(val, Qfalse.into()); - asm.je(jit, side_exit.clone()); + if !is_known_heap_basic_object { + // Check if it's a special constant + asm.test(val, (RUBY_IMMEDIATE_MASK as u64).into()); + asm.jnz(jit, side_exit.clone()); + + // Check if it's false + asm.cmp(val, Qfalse.into()); + asm.je(jit, side_exit.clone()); + } // Load the class from the object's klass field let klass = asm.load(Opnd::mem(64, val, RUBY_OFFSET_RBASIC_KLASS)); @@ -2575,13 +2587,15 @@ fn gen_guard_type(jit: &mut JITState, asm: &mut Assembler, val: lir::Opnd, guard } else if let Some(builtin_type) = guard_type.builtin_type_equivalent() { let side = side_exit(jit, state, GuardType(guard_type)); - // Check special constant - asm.test(val, Opnd::UImm(RUBY_IMMEDIATE_MASK as u64)); - asm.jnz(jit, side.clone()); + if !is_known_heap_basic_object { + // Check special constant + asm.test(val, Opnd::UImm(RUBY_IMMEDIATE_MASK as u64)); + asm.jnz(jit, side.clone()); - // Check false - asm.cmp(val, Qfalse.into()); - asm.je(jit, side.clone()); + // Check false + asm.cmp(val, Qfalse.into()); + asm.je(jit, side.clone()); + } // Mask and check the builtin type let val = asm.load_mem(val); From a50eaa3dcd6fcb3673d5356db886af26c4e15e52 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Thu, 4 Jun 2026 10:04:20 +0900 Subject: [PATCH 115/188] Bump actions/checkout to v6.0.3 Dependabot left the version comment as v6.0.2 on the four lines that carry a trailing `# zizmor: ignore[artipacked]`, since its comment rewriter only handles a version comment as the last token on the line. zizmor flagged the resulting hash/comment mismatch. Update every checkout pin in .github to the v6.0.3 commit and comment at once. --- .github/actions/setup/directories/action.yml | 2 +- .github/workflows/annocheck.yml | 2 +- .github/workflows/auto_review_pr.yml | 2 +- .github/workflows/baseruby.yml | 2 +- .github/workflows/bundled_gems.yml | 2 +- .github/workflows/check_dependencies.yml | 2 +- .github/workflows/check_misc.yml | 4 +-- .github/workflows/check_sast.yml | 4 +-- .github/workflows/compilers.yml | 26 ++++++++++---------- .github/workflows/crosscompile.yml | 2 +- .github/workflows/cygwin.yml | 2 +- .github/workflows/default_gems_list.yml | 2 +- .github/workflows/macos.yml | 2 +- .github/workflows/mingw.yml | 2 +- .github/workflows/modgc.yml | 2 +- .github/workflows/parse_y.yml | 2 +- .github/workflows/post_push.yml | 2 +- .github/workflows/publish.yml | 2 +- .github/workflows/rust-warnings.yml | 2 +- .github/workflows/scorecards.yml | 2 +- .github/workflows/spec_guards.yml | 2 +- .github/workflows/sync_default_gems.yml | 2 +- .github/workflows/tarball-macos.yml | 2 +- .github/workflows/tarball-test.yml | 2 +- .github/workflows/tarball-ubuntu.yml | 2 +- .github/workflows/tarball-windows.yml | 2 +- .github/workflows/ubuntu.yml | 6 ++--- .github/workflows/wasm.yml | 2 +- .github/workflows/windows.yml | 2 +- .github/workflows/yjit-macos.yml | 4 +-- .github/workflows/yjit-ubuntu.yml | 6 ++--- .github/workflows/zjit-macos.yml | 6 ++--- .github/workflows/zjit-ubuntu.yml | 8 +++--- 33 files changed, 57 insertions(+), 57 deletions(-) diff --git a/.github/actions/setup/directories/action.yml b/.github/actions/setup/directories/action.yml index 589049a4b8b2ac..15dc097b6e66c1 100644 --- a/.github/actions/setup/directories/action.yml +++ b/.github/actions/setup/directories/action.yml @@ -98,7 +98,7 @@ runs: git config --global init.defaultBranch garbage - if: inputs.checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: path: ${{ inputs.srcdir }} fetch-depth: ${{ inputs.fetch-depth }} diff --git a/.github/workflows/annocheck.yml b/.github/workflows/annocheck.yml index 1d23c38fcd024b..5991165d43abf3 100644 --- a/.github/workflows/annocheck.yml +++ b/.github/workflows/annocheck.yml @@ -61,7 +61,7 @@ jobs: - run: id working-directory: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: sparse-checkout-cone-mode: false sparse-checkout: /.github diff --git a/.github/workflows/auto_review_pr.yml b/.github/workflows/auto_review_pr.yml index 24859ad0b09024..bb84a51573814b 100644 --- a/.github/workflows/auto_review_pr.yml +++ b/.github/workflows/auto_review_pr.yml @@ -25,7 +25,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false diff --git a/.github/workflows/baseruby.yml b/.github/workflows/baseruby.yml index 1acfbe19ace9ab..9e7720f659fcff 100644 --- a/.github/workflows/baseruby.yml +++ b/.github/workflows/baseruby.yml @@ -53,7 +53,7 @@ jobs: ruby-version: ${{ matrix.ruby }} bundler: none - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false diff --git a/.github/workflows/bundled_gems.yml b/.github/workflows/bundled_gems.yml index d890ed2495be59..9acec6d41f69ee 100644 --- a/.github/workflows/bundled_gems.yml +++ b/.github/workflows/bundled_gems.yml @@ -34,7 +34,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 # zizmor: ignore[artipacked] + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 # zizmor: ignore[artipacked] with: token: ${{ (github.repository == 'ruby/ruby' && !startsWith(github.event_name, 'pull')) && secrets.MATZBOT_AUTO_UPDATE_TOKEN || secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/check_dependencies.yml b/.github/workflows/check_dependencies.yml index ad399b264fd2ee..a120dde7e5c959 100644 --- a/.github/workflows/check_dependencies.yml +++ b/.github/workflows/check_dependencies.yml @@ -30,7 +30,7 @@ jobs: runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false diff --git a/.github/workflows/check_misc.yml b/.github/workflows/check_misc.yml index f194d007799d11..cb1642b9e2e3b2 100644 --- a/.github/workflows/check_misc.yml +++ b/.github/workflows/check_misc.yml @@ -18,7 +18,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: token: ${{ (github.repository == 'ruby/ruby' && !startsWith(github.event_name, 'pull')) && secrets.MATZBOT_AUTO_UPDATE_TOKEN || secrets.GITHUB_TOKEN }} persist-credentials: false @@ -100,7 +100,7 @@ jobs: { echo version=$2; echo ref=$4; } >> $GITHUB_OUTPUT - name: Checkout rdoc - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: repository: ruby/rdoc ref: ${{ steps.rdoc.outputs.ref }} diff --git a/.github/workflows/check_sast.yml b/.github/workflows/check_sast.yml index 6fd1be6542cf28..58d426fbf2125a 100644 --- a/.github/workflows/check_sast.yml +++ b/.github/workflows/check_sast.yml @@ -40,7 +40,7 @@ jobs: security-events: write steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false @@ -73,7 +73,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false diff --git a/.github/workflows/compilers.yml b/.github/workflows/compilers.yml index 147470f38a9988..f747b7fd033e35 100644 --- a/.github/workflows/compilers.yml +++ b/.github/workflows/compilers.yml @@ -51,7 +51,7 @@ jobs: timeout-minutes: 60 steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: { sparse-checkout-cone-mode: false, sparse-checkout: /.github, persist-credentials: false } # Set fetch-depth: 10 so that Launchable can receive commits information. - { uses: './.github/actions/setup/directories', with: { srcdir: 'src', builddir: 'build', makeup: true, fetch-depth: 10 } } @@ -74,7 +74,7 @@ jobs: timeout-minutes: 60 steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: { sparse-checkout-cone-mode: false, sparse-checkout: /.github, persist-credentials: false } - { uses: './.github/actions/setup/directories', with: { srcdir: 'src', builddir: 'build', makeup: true, fetch-depth: 10 } } - name: 'GCC 15 LTO' @@ -102,7 +102,7 @@ jobs: timeout-minutes: 60 steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: { sparse-checkout-cone-mode: false, sparse-checkout: /.github, persist-credentials: false } - { uses: './.github/actions/setup/directories', with: { srcdir: 'src', builddir: 'build', makeup: true, fetch-depth: 10 } } - { uses: './.github/actions/compilers', name: 'clang 20', with: { tag: 'clang-20' }, timeout-minutes: 5 } @@ -121,7 +121,7 @@ jobs: timeout-minutes: 60 steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: { sparse-checkout-cone-mode: false, sparse-checkout: /.github, persist-credentials: false } - { uses: './.github/actions/setup/directories', with: { srcdir: 'src', builddir: 'build', makeup: true, fetch-depth: 10 } } - { uses: './.github/actions/compilers', name: 'clang 13', with: { tag: 'clang-13' }, timeout-minutes: 5 } @@ -142,7 +142,7 @@ jobs: timeout-minutes: 60 steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: { sparse-checkout-cone-mode: false, sparse-checkout: /.github, persist-credentials: false } - { uses: './.github/actions/setup/directories', with: { srcdir: 'src', builddir: 'build', makeup: true, fetch-depth: 10 } } # -Wno-strict-prototypes is necessary with current clang-15 since @@ -168,7 +168,7 @@ jobs: timeout-minutes: 60 steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: { sparse-checkout-cone-mode: false, sparse-checkout: /.github, persist-credentials: false } - { uses: './.github/actions/setup/directories', with: { srcdir: 'src', builddir: 'build', makeup: true, fetch-depth: 10 } } - { uses: './.github/actions/compilers', name: 'C++20', with: { CXXFLAGS: '-std=c++20 -Werror=pedantic -pedantic-errors -Wno-c++11-long-long' }, timeout-minutes: 5 } @@ -188,7 +188,7 @@ jobs: timeout-minutes: 60 steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: { sparse-checkout-cone-mode: false, sparse-checkout: /.github, persist-credentials: false } - { uses: './.github/actions/setup/directories', with: { srcdir: 'src', builddir: 'build', makeup: true, fetch-depth: 10 } } - { uses: './.github/actions/compilers', name: 'disable-jit', with: { append_configure: '--disable-yjit --disable-zjit' }, timeout-minutes: 5 } @@ -208,7 +208,7 @@ jobs: timeout-minutes: 60 steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: { sparse-checkout-cone-mode: false, sparse-checkout: /.github, persist-credentials: false } - { uses: './.github/actions/setup/directories', with: { srcdir: 'src', builddir: 'build', makeup: true, fetch-depth: 10 } } - { uses: './.github/actions/compilers', name: 'NDEBUG', with: { cppflags: '-DNDEBUG' }, timeout-minutes: 5 } @@ -227,7 +227,7 @@ jobs: timeout-minutes: 60 steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: { sparse-checkout-cone-mode: false, sparse-checkout: /.github, persist-credentials: false } - { uses: './.github/actions/setup/directories', with: { srcdir: 'src', builddir: 'build', makeup: true, fetch-depth: 10 } } - { uses: './.github/actions/compilers', name: 'HASH_DEBUG', with: { cppflags: '-DHASH_DEBUG' }, timeout-minutes: 5 } @@ -247,7 +247,7 @@ jobs: timeout-minutes: 60 steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: { sparse-checkout-cone-mode: false, sparse-checkout: /.github, persist-credentials: false } - { uses: './.github/actions/setup/directories', with: { srcdir: 'src', builddir: 'build', makeup: true, fetch-depth: 10 } } - { uses: './.github/actions/compilers', name: 'USE_LAZY_LOAD', with: { cppflags: '-DUSE_LAZY_LOAD' }, timeout-minutes: 5 } @@ -268,7 +268,7 @@ jobs: timeout-minutes: 60 steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: { sparse-checkout-cone-mode: false, sparse-checkout: /.github, persist-credentials: false } - { uses: './.github/actions/setup/directories', with: { srcdir: 'src', builddir: 'build', makeup: true, fetch-depth: 10 } } - { uses: './.github/actions/compilers', name: 'GC_DEBUG_STRESS_TO_CLASS', with: { cppflags: '-DGC_DEBUG_STRESS_TO_CLASS' }, timeout-minutes: 5 } @@ -287,7 +287,7 @@ jobs: timeout-minutes: 60 steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: { sparse-checkout-cone-mode: false, sparse-checkout: /.github, persist-credentials: false } - { uses: './.github/actions/setup/directories', with: { srcdir: 'src', builddir: 'build', makeup: true, fetch-depth: 10 } } - { uses: './.github/actions/compilers', name: 'VM_DEBUG_BP_CHECK', with: { cppflags: '-DVM_DEBUG_BP_CHECK' }, timeout-minutes: 5 } @@ -317,7 +317,7 @@ jobs: - 'compileB' - 'compileC' steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: { sparse-checkout-cone-mode: false, sparse-checkout: /.github, persist-credentials: false } - uses: ./.github/actions/slack with: diff --git a/.github/workflows/crosscompile.yml b/.github/workflows/crosscompile.yml index 4c28516e25bd2c..3ed6429a1e5ced 100644 --- a/.github/workflows/crosscompile.yml +++ b/.github/workflows/crosscompile.yml @@ -52,7 +52,7 @@ jobs: )}} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: sparse-checkout-cone-mode: false sparse-checkout: /.github diff --git a/.github/workflows/cygwin.yml b/.github/workflows/cygwin.yml index 5ab86c7b19fbf3..f1a6f79587f30f 100644 --- a/.github/workflows/cygwin.yml +++ b/.github/workflows/cygwin.yml @@ -40,7 +40,7 @@ jobs: steps: - run: git config --global core.autocrlf input - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false diff --git a/.github/workflows/default_gems_list.yml b/.github/workflows/default_gems_list.yml index f52b83103ce34c..c4b4cd6d30de5d 100644 --- a/.github/workflows/default_gems_list.yml +++ b/.github/workflows/default_gems_list.yml @@ -23,7 +23,7 @@ jobs: if: ${{ github.repository == 'ruby/ruby' }} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 # zizmor: ignore[artipacked] + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 # zizmor: ignore[artipacked] with: token: ${{ (github.repository == 'ruby/ruby' && !startsWith(github.event_name, 'pull')) && secrets.MATZBOT_AUTO_UPDATE_TOKEN || secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 501d35698b7974..4f1807121f9d96 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -65,7 +65,7 @@ jobs: )}} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: sparse-checkout-cone-mode: false sparse-checkout: /.github diff --git a/.github/workflows/mingw.yml b/.github/workflows/mingw.yml index 1efae8be59e5c5..9a47e70f8cb929 100644 --- a/.github/workflows/mingw.yml +++ b/.github/workflows/mingw.yml @@ -168,7 +168,7 @@ jobs: [ ${#failed[@]} -eq 0 ] shell: sh - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: sparse-checkout-cone-mode: false sparse-checkout: /.github diff --git a/.github/workflows/modgc.yml b/.github/workflows/modgc.yml index bd3c6ab5750e3d..218127aad7c1d5 100644 --- a/.github/workflows/modgc.yml +++ b/.github/workflows/modgc.yml @@ -48,7 +48,7 @@ jobs: )}} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: sparse-checkout-cone-mode: false sparse-checkout: /.github diff --git a/.github/workflows/parse_y.yml b/.github/workflows/parse_y.yml index a0082f71dec0c6..7c26e87e57317f 100644 --- a/.github/workflows/parse_y.yml +++ b/.github/workflows/parse_y.yml @@ -51,7 +51,7 @@ jobs: )}} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: sparse-checkout-cone-mode: false sparse-checkout: /.github diff --git a/.github/workflows/post_push.yml b/.github/workflows/post_push.yml index 237679ebb1a810..eed8d5b42c4b2b 100644 --- a/.github/workflows/post_push.yml +++ b/.github/workflows/post_push.yml @@ -34,7 +34,7 @@ jobs: REDMINE_SYS_API_KEY: ${{ secrets.REDMINE_SYS_API_KEY }} if: ${{ github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/heads/ruby_') }} - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 # zizmor: ignore[artipacked] + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 # zizmor: ignore[artipacked] with: fetch-depth: 500 # for notify-slack-commits token: ${{ secrets.MATZBOT_AUTO_UPDATE_TOKEN }} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 5d16915e10035c..5d4a31d287f03c 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -18,7 +18,7 @@ jobs: release: runs-on: ubuntu-latest steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false diff --git a/.github/workflows/rust-warnings.yml b/.github/workflows/rust-warnings.yml index 23ed16440573ae..7ea7d0c9507fa1 100644 --- a/.github/workflows/rust-warnings.yml +++ b/.github/workflows/rust-warnings.yml @@ -36,7 +36,7 @@ jobs: )}} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index 74c13f98faeb53..71fe3c3c01052e 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -34,7 +34,7 @@ jobs: steps: - name: "Checkout code" - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false diff --git a/.github/workflows/spec_guards.yml b/.github/workflows/spec_guards.yml index 856b6e434e3cd7..39714b13a4304a 100644 --- a/.github/workflows/spec_guards.yml +++ b/.github/workflows/spec_guards.yml @@ -45,7 +45,7 @@ jobs: fail-fast: false steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false diff --git a/.github/workflows/sync_default_gems.yml b/.github/workflows/sync_default_gems.yml index 9fd19454492fbb..7fbddc48a2447f 100644 --- a/.github/workflows/sync_default_gems.yml +++ b/.github/workflows/sync_default_gems.yml @@ -34,7 +34,7 @@ jobs: if: ${{ github.repository == 'ruby/ruby' }} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 # zizmor: ignore[artipacked] + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 # zizmor: ignore[artipacked] name: Check out ruby/ruby with: token: ${{ github.repository == 'ruby/ruby' && secrets.MATZBOT_AUTO_UPDATE_TOKEN || secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/tarball-macos.yml b/.github/workflows/tarball-macos.yml index 9bec94d52804bf..0d02cf6ae17475 100644 --- a/.github/workflows/tarball-macos.yml +++ b/.github/workflows/tarball-macos.yml @@ -76,7 +76,7 @@ jobs: /usr/local/bin/gem -v /usr/local/bin/bundle -v if: matrix.test_task == 'check' - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: sparse-checkout: .github/actions/slack sparse-checkout-cone-mode: false diff --git a/.github/workflows/tarball-test.yml b/.github/workflows/tarball-test.yml index c862ab76cc1886..f75d76761a8cf8 100644 --- a/.github/workflows/tarball-test.yml +++ b/.github/workflows/tarball-test.yml @@ -46,7 +46,7 @@ jobs: || contains(github.event.pull_request.labels.*.name, 'Documentation') || (github.event.pull_request.user.login == 'dependabot[bot]') )}} - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: fetch-depth: 1 # actions/checkout fetches all heads/tags unless > 0 persist-credentials: false diff --git a/.github/workflows/tarball-ubuntu.yml b/.github/workflows/tarball-ubuntu.yml index e227dc29aa59bd..0482db3c7f3181 100644 --- a/.github/workflows/tarball-ubuntu.yml +++ b/.github/workflows/tarball-ubuntu.yml @@ -126,7 +126,7 @@ jobs: if: matrix.test_task == 'check' - name: Show .local run: find $HOME/.local -ls - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: sparse-checkout: .github/actions/slack sparse-checkout-cone-mode: false diff --git a/.github/workflows/tarball-windows.yml b/.github/workflows/tarball-windows.yml index 1ce95de6fcbb87..a66cdf729d0141 100644 --- a/.github/workflows/tarball-windows.yml +++ b/.github/workflows/tarball-windows.yml @@ -138,7 +138,7 @@ jobs: timeout-minutes: 70 continue-on-error: ${{ matrix.continue-on-error || false }} - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: sparse-checkout: .github/actions/slack sparse-checkout-cone-mode: false diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml index 09e96f764c7546..c887ae38118e42 100644 --- a/.github/workflows/ubuntu.yml +++ b/.github/workflows/ubuntu.yml @@ -60,7 +60,7 @@ jobs: )}} steps: &make-steps - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: sparse-checkout-cone-mode: false sparse-checkout: /.github @@ -222,7 +222,7 @@ jobs: )}} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false @@ -240,7 +240,7 @@ jobs: - run: make install - name: Checkout ruby-bench - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: repository: ruby/ruby-bench persist-credentials: false diff --git a/.github/workflows/wasm.yml b/.github/workflows/wasm.yml index edb2c76c48fb27..f0263de5ef15e9 100644 --- a/.github/workflows/wasm.yml +++ b/.github/workflows/wasm.yml @@ -59,7 +59,7 @@ jobs: )}} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: sparse-checkout-cone-mode: false sparse-checkout: /.github diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 13551427bbbf32..b2c84abc6d397e 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -65,7 +65,7 @@ jobs: bundler: none windows-toolchain: none - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false sparse-checkout-cone-mode: false diff --git a/.github/workflows/yjit-macos.yml b/.github/workflows/yjit-macos.yml index b52c6355ada1ea..e11de6bc51c560 100644 --- a/.github/workflows/yjit-macos.yml +++ b/.github/workflows/yjit-macos.yml @@ -41,7 +41,7 @@ jobs: )}} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false @@ -85,7 +85,7 @@ jobs: )}} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: sparse-checkout-cone-mode: false sparse-checkout: /.github diff --git a/.github/workflows/yjit-ubuntu.yml b/.github/workflows/yjit-ubuntu.yml index 934c3f56d4c1ac..ab816940f4bcca 100644 --- a/.github/workflows/yjit-ubuntu.yml +++ b/.github/workflows/yjit-ubuntu.yml @@ -36,7 +36,7 @@ jobs: )}} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false @@ -70,7 +70,7 @@ jobs: )}} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false @@ -125,7 +125,7 @@ jobs: )}} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: sparse-checkout-cone-mode: false sparse-checkout: /.github diff --git a/.github/workflows/zjit-macos.yml b/.github/workflows/zjit-macos.yml index e68bd897fd55d2..7494f77792c79d 100644 --- a/.github/workflows/zjit-macos.yml +++ b/.github/workflows/zjit-macos.yml @@ -69,7 +69,7 @@ jobs: )}} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: sparse-checkout-cone-mode: false sparse-checkout: /.github @@ -192,7 +192,7 @@ jobs: )}} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false @@ -214,7 +214,7 @@ jobs: run: echo "MAKEFLAGS=" >> "$GITHUB_ENV" - name: Checkout ruby-bench - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false repository: ruby/ruby-bench diff --git a/.github/workflows/zjit-ubuntu.yml b/.github/workflows/zjit-ubuntu.yml index 8d146b7bdc8cf5..4ba2542e393d5b 100644 --- a/.github/workflows/zjit-ubuntu.yml +++ b/.github/workflows/zjit-ubuntu.yml @@ -41,7 +41,7 @@ jobs: )}} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false @@ -106,7 +106,7 @@ jobs: )}} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: sparse-checkout-cone-mode: false sparse-checkout: /.github @@ -250,7 +250,7 @@ jobs: )}} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false @@ -268,7 +268,7 @@ jobs: - run: make install - name: Checkout ruby-bench - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: repository: ruby/ruby-bench persist-credentials: false From 97672020e5390f3e15a5219a6983efe98aaa88e0 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Thu, 4 Jun 2026 10:12:47 +0900 Subject: [PATCH 116/188] Move artipacked suppressions to zizmor config The inline `# zizmor: ignore[artipacked]` comment is a second trailing comment on the checkout line, which prevents Dependabot from updating the version comment. Suppress these findings in .github/zizmor.yml instead so the pins stay a single comment and Dependabot keeps them in sync. --- .github/workflows/bundled_gems.yml | 2 +- .github/workflows/default_gems_list.yml | 2 +- .github/workflows/post_push.yml | 2 +- .github/workflows/sync_default_gems.yml | 2 +- .github/zizmor.yml | 7 +++++++ 5 files changed, 11 insertions(+), 4 deletions(-) diff --git a/.github/workflows/bundled_gems.yml b/.github/workflows/bundled_gems.yml index 9acec6d41f69ee..d329ee9b4baccf 100644 --- a/.github/workflows/bundled_gems.yml +++ b/.github/workflows/bundled_gems.yml @@ -34,7 +34,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 # zizmor: ignore[artipacked] + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: token: ${{ (github.repository == 'ruby/ruby' && !startsWith(github.event_name, 'pull')) && secrets.MATZBOT_AUTO_UPDATE_TOKEN || secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/default_gems_list.yml b/.github/workflows/default_gems_list.yml index c4b4cd6d30de5d..68f2d18dd6446a 100644 --- a/.github/workflows/default_gems_list.yml +++ b/.github/workflows/default_gems_list.yml @@ -23,7 +23,7 @@ jobs: if: ${{ github.repository == 'ruby/ruby' }} steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 # zizmor: ignore[artipacked] + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: token: ${{ (github.repository == 'ruby/ruby' && !startsWith(github.event_name, 'pull')) && secrets.MATZBOT_AUTO_UPDATE_TOKEN || secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/post_push.yml b/.github/workflows/post_push.yml index eed8d5b42c4b2b..e351c8c2866712 100644 --- a/.github/workflows/post_push.yml +++ b/.github/workflows/post_push.yml @@ -34,7 +34,7 @@ jobs: REDMINE_SYS_API_KEY: ${{ secrets.REDMINE_SYS_API_KEY }} if: ${{ github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/heads/ruby_') }} - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 # zizmor: ignore[artipacked] + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: fetch-depth: 500 # for notify-slack-commits token: ${{ secrets.MATZBOT_AUTO_UPDATE_TOKEN }} diff --git a/.github/workflows/sync_default_gems.yml b/.github/workflows/sync_default_gems.yml index 7fbddc48a2447f..3aaae5864fcaaf 100644 --- a/.github/workflows/sync_default_gems.yml +++ b/.github/workflows/sync_default_gems.yml @@ -34,7 +34,7 @@ jobs: if: ${{ github.repository == 'ruby/ruby' }} steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 # zizmor: ignore[artipacked] + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 name: Check out ruby/ruby with: token: ${{ github.repository == 'ruby/ruby' && secrets.MATZBOT_AUTO_UPDATE_TOKEN || secrets.GITHUB_TOKEN }} diff --git a/.github/zizmor.yml b/.github/zizmor.yml index 65b67fb6c8eb04..2a8cad1d5ce3de 100644 --- a/.github/zizmor.yml +++ b/.github/zizmor.yml @@ -1,6 +1,13 @@ # Ignore existing findings (baseline) # Composite action findings are suppressed inline with # zizmor: ignore rules: + artipacked: + # These jobs push back to the repo and need persisted credentials. + ignore: + - bundled_gems.yml + - default_gems_list.yml + - post_push.yml + - sync_default_gems.yml dangerous-triggers: ignore: - auto_request_review.yml From b74a5f5ec8e49122a9187611736108caa77d4545 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Wed, 3 Jun 2026 16:10:26 -0400 Subject: [PATCH 117/188] ZJIT: Replace IsNil with existing HasType No need for a separate opcode. --- zjit/src/codegen.rs | 8 -------- zjit/src/hir.rs | 15 ++++----------- zjit/src/hir/opt_tests.rs | 2 ++ zjit/src/hir/tests.rs | 4 ++-- 4 files changed, 8 insertions(+), 21 deletions(-) diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index 47f4a97ac7caa3..2d2ade8d7eebdb 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -687,7 +687,6 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio } &Insn::FixnumMod { left, right, state } => gen_fixnum_mod(jit, asm, opnd!(left), opnd!(right), &function.frame_state(state)), &Insn::FixnumAref { recv, index } => gen_fixnum_aref(asm, opnd!(recv), opnd!(index)), - Insn::IsNil { val } => gen_isnil(asm, opnd!(val)), &Insn::IsMethodCfunc { val, cd, cfunc, state } => gen_is_method_cfunc(asm, opnd!(val), cd, cfunc, &function.frame_state(state)), &Insn::IsBitEqual { left, right } => gen_is_bit_equal(asm, opnd!(left), opnd!(right)), &Insn::IsBitNotEqual { left, right } => gen_is_bit_not_equal(asm, opnd!(left), opnd!(right)), @@ -2404,13 +2403,6 @@ fn gen_fixnum_aref(asm: &mut Assembler, recv: lir::Opnd, index: lir::Opnd) -> li asm_ccall!(asm, rb_fix_aref, recv, index) } -// Compile val == nil -fn gen_isnil(asm: &mut Assembler, val: lir::Opnd) -> lir::Opnd { - asm.cmp(val, Qnil.into()); - // TODO: Implement and use setcc - asm.csel_e(Opnd::Imm(1), Opnd::Imm(0)) -} - fn gen_is_method_cfunc(asm: &mut Assembler, val: lir::Opnd, cd: *const rb_call_data, cfunc: *const u8, state: &FrameState) -> lir::Opnd { unsafe extern "C" { fn rb_vm_method_cfunc_is(iseq: IseqPtr, cd: *const rb_call_data, recv: VALUE, cfunc: *const u8) -> VALUE; diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 02e3759cd69d9e..b247d57ceaafb6 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -926,8 +926,6 @@ pub enum Insn { /// Check if the value is truthy and "return" a C boolean. In reality, we will likely fuse this /// with IfTrue/IfFalse in the backend to generate jcc. Test { val: InsnId }, - /// Return C `true` if `val` is `Qnil`, else `false`. - IsNil { val: InsnId }, /// Return C `true` if `val`'s method on cd resolves to the cfunc. IsMethodCfunc { val: InsnId, cd: *const rb_call_data, cfunc: *const u8, state: InsnId }, /// Return C `true` if left == right @@ -1314,8 +1312,7 @@ macro_rules! for_each_operand_impl { | Insn::Return { val } | Insn::Test { val } | Insn::SetLocal { val, .. } - | Insn::BoxBool { val } - | Insn::IsNil { val } => { + | Insn::BoxBool { val } => { $visit_one!(val); } Insn::SetGlobal { val, state, .. } @@ -1635,7 +1632,6 @@ impl Insn { Insn::ObjectAlloc { .. } => effects::Any, Insn::ObjectAllocClass { .. } => allocates, Insn::Test { .. } => effects::Empty, - Insn::IsNil { .. } => effects::Empty, Insn::IsMethodCfunc { .. } => effects::Any, Insn::IsBitEqual { .. } => effects::Empty, Insn::IsBitNotEqual { .. } => effects::Empty, @@ -2007,7 +2003,6 @@ impl<'a> std::fmt::Display for InsnPrinter<'a> { Ok(()) } Insn::Test { val } => { write!(f, "Test {val}") } - Insn::IsNil { val } => { write!(f, "IsNil {val}") } Insn::IsMethodCfunc { val, cd, .. } => { write!(f, "IsMethodCFunc {val}, :{}", ruby_call_method_name(*cd)) } Insn::IsBitEqual { left, right } => write!(f, "IsBitEqual {left}, {right}"), Insn::IsBitNotEqual { left, right } => write!(f, "IsBitNotEqual {left}, {right}"), @@ -2949,9 +2944,6 @@ impl Function { Insn::Test { val } if self.type_of(*val).is_known_falsy() => Type::from_cbool(false), Insn::Test { val } if self.type_of(*val).is_known_truthy() => Type::from_cbool(true), Insn::Test { .. } => types::CBool, - Insn::IsNil { val } if self.is_a(*val, types::NilClass) => Type::from_cbool(true), - Insn::IsNil { val } if !self.type_of(*val).could_be(types::NilClass) => Type::from_cbool(false), - Insn::IsNil { .. } => types::CBool, Insn::IsMethodCfunc { .. } => types::CBool, Insn::IsBitEqual { .. } => types::CBool, Insn::IsBitNotEqual { .. } => types::CBool, @@ -2990,6 +2982,8 @@ impl Function { Insn::CheckMatch { .. } => types::BasicObject, Insn::GuardType { val, guard_type, .. } => self.type_of(*val).intersection(*guard_type), Insn::RefineType { val, new_type, .. } => self.type_of(*val).intersection(*new_type), + &Insn::HasType { val, expected } if self.is_a(val, expected) => Type::from_cbool(true), + &Insn::HasType { val, expected } if !self.type_of(val).could_be(expected) => Type::from_cbool(false), Insn::HasType { .. } => types::CBool, Insn::GuardBitEquals { val, expected, .. } => self.type_of(*val).intersection(Type::from_const(*expected)), Insn::GuardAnyBitSet { val, .. } => self.type_of(*val), @@ -6025,7 +6019,6 @@ impl Function { } // Instructions with 1 Ruby object operand Insn::Test { val } - | Insn::IsNil { val } | Insn::IsMethodCfunc { val, .. } | Insn::SetGlobal { val, .. } | Insn::SetLocal { val, .. } @@ -7347,7 +7340,7 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { } let offset = get_arg(pc, 0).as_i64(); let val = state.stack_pop()?; - let test_id = fun.push_insn(block, Insn::IsNil { val }); + let test_id = fun.push_insn(block, Insn::HasType { val, expected: types::NilClass }); let target_idx = insn_idx_at_offset(insn_idx, offset); let target = insn_idx_to_block[&target_idx]; let nil = fun.push_insn(block, Insn::Const { val: Const::Value(Qnil) }); diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index 74b951d2e59410..c60b01178f0b41 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -6767,6 +6767,7 @@ mod hir_opt_tests { bb3(v8:BasicObject, v9:NilClass): v13:NilClass = Const Value(nil) CheckInterrupts + v20:CBool[true] = HasType v13, NilClass v21:NilClass = Const Value(nil) Return v21 "); @@ -6796,6 +6797,7 @@ mod hir_opt_tests { bb3(v8:BasicObject, v9:NilClass): v13:Fixnum[1] = Const Value(1) CheckInterrupts + v20:CBool[false] = HasType v13, NilClass PatchPoint MethodRedefined(Integer@0x1000, itself@0x1008, cme:0x1010) Return v13 "); diff --git a/zjit/src/hir/tests.rs b/zjit/src/hir/tests.rs index 7a1cd85c5fb38f..a17a5262f4af07 100644 --- a/zjit/src/hir/tests.rs +++ b/zjit/src/hir/tests.rs @@ -4467,7 +4467,7 @@ pub(crate) mod hir_build_tests { Jump bb3(v6, v7) bb3(v9:BasicObject, v10:BasicObject): CheckInterrupts - v17:CBool = IsNil v10 + v17:CBool = HasType v10, NilClass v18:NilClass = Const Value(nil) CondBranch v17, bb4(v9, v18, v18), bb5() bb5(): @@ -4514,7 +4514,7 @@ pub(crate) mod hir_build_tests { bb6(): v19:Truthy = RefineType v10, Truthy CheckInterrupts - v25:CBool[false] = IsNil v19 + v25:CBool[false] = HasType v19, NilClass v26:NilClass = Const Value(nil) CondBranch v25, bb5(v9, v26, v26), bb7() bb7(): From c8c6de3c9935d6b4762a48be8987f7eb9f3458b1 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Wed, 3 Jun 2026 16:12:24 -0400 Subject: [PATCH 118/188] ZJIT: Remove Control write effect of HasType This was likely a bad copy/paste from GuardType; HasType does not affect control flow at all. --- zjit/src/hir.rs | 2 +- zjit/src/hir/opt_tests.rs | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index b247d57ceaafb6..82e50cdd36c817 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -1747,7 +1747,7 @@ impl Insn { Insn::HasType { expected, .. } => Effect::read_write( if expected.is_subtype(types::Immediate) { abstract_heaps::Empty } else { abstract_heaps::Memory }, - abstract_heaps::Control + abstract_heaps::Empty ), Insn::Entries { .. } => effects::Any, Insn::BreakPoint | Insn::Unreachable => Effect::read_write(abstract_heaps::Empty, abstract_heaps::Control), diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index c60b01178f0b41..74b951d2e59410 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -6767,7 +6767,6 @@ mod hir_opt_tests { bb3(v8:BasicObject, v9:NilClass): v13:NilClass = Const Value(nil) CheckInterrupts - v20:CBool[true] = HasType v13, NilClass v21:NilClass = Const Value(nil) Return v21 "); @@ -6797,7 +6796,6 @@ mod hir_opt_tests { bb3(v8:BasicObject, v9:NilClass): v13:Fixnum[1] = Const Value(1) CheckInterrupts - v20:CBool[false] = HasType v13, NilClass PatchPoint MethodRedefined(Integer@0x1000, itself@0x1008, cme:0x1010) Return v13 "); From 585919619774d0cf08559d85d98ddea591ff6324 Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Wed, 3 Jun 2026 21:41:15 +0900 Subject: [PATCH 119/188] [ruby/mmtk] Use rb_gc_obj_needs_cleanup_p https://github.com/ruby/mmtk/commit/031785b41c --- gc/mmtk/mmtk.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/gc/mmtk/mmtk.c b/gc/mmtk/mmtk.c index 95176b692b4de8..8be69b4fe65dc8 100644 --- a/gc/mmtk/mmtk.c +++ b/gc/mmtk/mmtk.c @@ -345,7 +345,9 @@ rb_mmtk_call_obj_free(MMTk_ObjectReference object) pthread_mutex_unlock(&objspace->event_hook_mutex); } - rb_gc_obj_free(objspace, obj); + if (RB_UNLIKELY(rb_gc_obj_needs_cleanup_p(obj))) { + rb_gc_obj_free(objspace, obj); + } #ifdef MMTK_DEBUG memset((void *)obj, 0, rb_gc_impl_obj_slot_size(obj)); From cac0c38eeaefc4bd516a32d2602ee25312d7a890 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 4 Jun 2026 02:02:49 +0000 Subject: [PATCH 120/188] Bump the github-actions group across 1 directory with 3 updates Bumps the github-actions group with 3 updates in the / directory: [necojackarc/auto-request-review](https://github.com/necojackarc/auto-request-review), [github/codeql-action](https://github.com/github/codeql-action) and [taiki-e/install-action](https://github.com/taiki-e/install-action). Updates `necojackarc/auto-request-review` from 5d3060495e58e9cb41f51de50e808d3135d5374e to 035f049cb68460341ab744f19aa9f31aae685e36 - [Release notes](https://github.com/necojackarc/auto-request-review/releases) - [Commits](https://github.com/necojackarc/auto-request-review/compare/5d3060495e58e9cb41f51de50e808d3135d5374e...035f049cb68460341ab744f19aa9f31aae685e36) Updates `github/codeql-action` from 4.36.0 to 4.36.1 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/7211b7c8077ea37d8641b6271f6a365a22a5fbfa...87557b9c84dde89fdd9b10e88954ac2f4248e463) Updates `taiki-e/install-action` from 2.81.1 to 2.81.3 - [Release notes](https://github.com/taiki-e/install-action/releases) - [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/taiki-e/install-action/compare/e49978b799e49ff429d162b7a30601a569ab6538...25435dc8dd3baed7417e0c96d3fe89013a5b2e09) --- updated-dependencies: - dependency-name: necojackarc/auto-request-review dependency-version: 035f049cb68460341ab744f19aa9f31aae685e36 dependency-type: direct:production dependency-group: github-actions - dependency-name: github/codeql-action dependency-version: 4.36.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions - dependency-name: taiki-e/install-action dependency-version: 2.81.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions ... Signed-off-by: dependabot[bot] --- .github/workflows/auto_request_review.yml | 2 +- .github/workflows/check_sast.yml | 6 +++--- .github/workflows/scorecards.yml | 2 +- .github/workflows/zjit-macos.yml | 2 +- .github/workflows/zjit-ubuntu.yml | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/auto_request_review.yml b/.github/workflows/auto_request_review.yml index 1be2b11b8634e4..80f2517eb5dd35 100644 --- a/.github/workflows/auto_request_review.yml +++ b/.github/workflows/auto_request_review.yml @@ -14,7 +14,7 @@ jobs: if: ${{ github.repository == 'ruby/ruby' && github.base_ref == 'master' }} steps: - name: Request review based on files changes and/or groups the author belongs to - uses: necojackarc/auto-request-review@5d3060495e58e9cb41f51de50e808d3135d5374e # master + uses: necojackarc/auto-request-review@035f049cb68460341ab744f19aa9f31aae685e36 # master with: # scope: public_repo token: ${{ secrets.MATZBOT_AUTO_REQUEST_REVIEW_TOKEN }} diff --git a/.github/workflows/check_sast.yml b/.github/workflows/check_sast.yml index 58d426fbf2125a..0b5d6ad1b69c17 100644 --- a/.github/workflows/check_sast.yml +++ b/.github/workflows/check_sast.yml @@ -78,14 +78,14 @@ jobs: persist-credentials: false - name: Initialize CodeQL - uses: github/codeql-action/init@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0 + uses: github/codeql-action/init@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4.36.1 with: languages: ${{ matrix.language }} build-mode: none config-file: .github/codeql/codeql-config.yml - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0 + uses: github/codeql-action/analyze@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4.36.1 with: category: '/language:${{ matrix.language }}' upload: False @@ -127,7 +127,7 @@ jobs: continue-on-error: true - name: Upload SARIF - uses: github/codeql-action/upload-sarif@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0 + uses: github/codeql-action/upload-sarif@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4.36.1 with: sarif_file: sarif-results/${{ matrix.language }}.sarif continue-on-error: true diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index 71fe3c3c01052e..fdc28c2d09af35 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -73,6 +73,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard (optional). # Commenting out will disable upload of results to your repo's Code Scanning dashboard - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0 + uses: github/codeql-action/upload-sarif@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4.36.1 with: sarif_file: results.sarif diff --git a/.github/workflows/zjit-macos.yml b/.github/workflows/zjit-macos.yml index 7494f77792c79d..09c7c1b6dbe0ef 100644 --- a/.github/workflows/zjit-macos.yml +++ b/.github/workflows/zjit-macos.yml @@ -93,7 +93,7 @@ jobs: rustup install ${{ matrix.rust_version }} --profile minimal rustup default ${{ matrix.rust_version }} - - uses: taiki-e/install-action@e49978b799e49ff429d162b7a30601a569ab6538 # v2.81.1 + - uses: taiki-e/install-action@25435dc8dd3baed7417e0c96d3fe89013a5b2e09 # v2.81.3 with: tool: nextest@0.9 if: ${{ matrix.test_task == 'zjit-check' }} diff --git a/.github/workflows/zjit-ubuntu.yml b/.github/workflows/zjit-ubuntu.yml index 4ba2542e393d5b..7f5ce9322ec9e1 100644 --- a/.github/workflows/zjit-ubuntu.yml +++ b/.github/workflows/zjit-ubuntu.yml @@ -119,7 +119,7 @@ jobs: ruby-version: '3.1' bundler: none - - uses: taiki-e/install-action@e49978b799e49ff429d162b7a30601a569ab6538 # v2.81.1 + - uses: taiki-e/install-action@25435dc8dd3baed7417e0c96d3fe89013a5b2e09 # v2.81.3 with: tool: nextest@0.9 if: ${{ matrix.test_task == 'zjit-check' }} From 4f3cd707ccd97e4fa260c6d42bb48eef27859b51 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Thu, 4 Jun 2026 10:52:25 +0900 Subject: [PATCH 121/188] [ruby/rubygems] Remove external tool version checks from `bundle env` Keeping up with each version manager's invocation convention is not worth the maintenance cost. chruby in particular is wrapped as a shell function and cannot be run as `chruby --version` at all, so the line always reported a missing version. https://github.com/ruby/rubygems/issues/9528 https://github.com/ruby/rubygems/commit/85cb212067 Co-Authored-By: Claude Opus 4.8 (1M context) --- lib/bundler/env.rb | 25 +++---------------------- spec/bundler/bundler/env_spec.rb | 10 +--------- 2 files changed, 4 insertions(+), 31 deletions(-) diff --git a/lib/bundler/env.rb b/lib/bundler/env.rb index 074bef6edcf0c9..2b297050609887 100644 --- a/lib/bundler/env.rb +++ b/lib/bundler/env.rb @@ -78,17 +78,6 @@ def self.git_version "not installed" end - def self.version_of(script) - return "not installed" unless Bundler.which(script) - `#{script} --version`.chomp - end - - def self.chruby_version - return "not installed" unless Bundler.which("chruby-exec") - `chruby-exec -- chruby --version`. - sub(/.*^chruby: (#{Gem::Version::VERSION_PATTERN}).*/m, '\1') - end - def self.environment out = [] @@ -110,16 +99,8 @@ def self.environment out << [" Cert File", OpenSSL::X509::DEFAULT_CERT_FILE] if defined?(OpenSSL::X509::DEFAULT_CERT_FILE) out << [" Cert Dir", OpenSSL::X509::DEFAULT_CERT_DIR] if defined?(OpenSSL::X509::DEFAULT_CERT_DIR) end - out << ["Tools"] - out << [" Git", git_version] - out << [" RVM", ENV.fetch("rvm_version") { version_of("rvm") }] - out << [" rbenv", version_of("rbenv")] - out << [" chruby", chruby_version] - - %w[rubygems-bundler open_gem].each do |name| - specs = Bundler.rubygems.find_name(name) - out << [" #{name}", "(#{specs.map(&:version).join(",")})"] unless specs.empty? - end + out << ["Git", git_version] + if (exe = caller_locations.last.absolute_path)&.match? %r{(exe|bin)/bundler?\z} shebang = File.read(exe).lines.first shebang.sub!(/^#!\s*/, "") @@ -143,6 +124,6 @@ def self.append_formatted_table(title, pairs, out) out << "```\n" end - private_class_method :read_file, :ruby_version, :git_version, :append_formatted_table, :version_of, :chruby_version + private_class_method :read_file, :ruby_version, :git_version, :append_formatted_table end end diff --git a/spec/bundler/bundler/env_spec.rb b/spec/bundler/bundler/env_spec.rb index 259b4ee9dc2f71..320944d4b9d8b7 100644 --- a/spec/bundler/bundler/env_spec.rb +++ b/spec/bundler/bundler/env_spec.rb @@ -222,16 +222,8 @@ def with_clear_paths(env_var, env_value) and_return(["git version 1.2.3 (Apple Git-BS)", "", status]) expect(Bundler::Source::Git::GitProxy).to receive(:new).and_return(git_proxy_stub) - expect(described_class.report).to include("Git 1.2.3 (Apple Git-BS)") + expect(described_class.report).to include("Git 1.2.3 (Apple Git-BS)") end end end - - describe ".version_of" do - let(:parsed_version) { described_class.send(:version_of, "ruby") } - - it "strips version of new line characters" do - expect(parsed_version).to_not end_with("\n") - end - end end From 9f6cb11797881151b155f720e805d1bf4ac4aaa4 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Thu, 4 Jun 2026 12:06:35 +0900 Subject: [PATCH 122/188] [ruby/rubygems] Assert the Tools section is gone from `bundle env` Guards against accidentally reintroducing the removed external tool version output. https://github.com/ruby/rubygems/commit/3060a5498a Co-Authored-By: Claude Opus 4.8 (1M context) --- spec/bundler/bundler/env_spec.rb | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/spec/bundler/bundler/env_spec.rb b/spec/bundler/bundler/env_spec.rb index 320944d4b9d8b7..2b7dbde217d8e2 100644 --- a/spec/bundler/bundler/env_spec.rb +++ b/spec/bundler/bundler/env_spec.rb @@ -225,5 +225,13 @@ def with_clear_paths(env_var, env_value) expect(described_class.report).to include("Git 1.2.3 (Apple Git-BS)") end end + + it "no longer reports the Tools section or external tool versions" do + report = described_class.report + expect(report).not_to include("Tools") + ["rbenv", "RVM", "chruby"].each do |tool| + expect(report).not_to include(tool) + end + end end end From ab2bc7e9e1ca8999f2641032852c706c9d0bc6dd Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Mon, 1 Jun 2026 12:47:06 -0400 Subject: [PATCH 123/188] [ruby/json] Make the JSON parse loop iterative As opposed to a recursive loop. We do this by keeping a stack of frames (very similar to how the stack of values was already stored). Each frame represents the state of a container. Since there are only 2 in JSON, it doesn't have to get too complex. Each frame in the iterative parser now holds an enum describing its "phase", in order to support suspending parsing. For performance to be on par with the previous implementation, we directly jump to the proper handler in most cases. `JSON_ParserState.stack` was renamed into `.value_stack`, as with the introduction of the frames stack, the naming became confusing. https://github.com/ruby/json/commit/a760d9ae9b Co-Authored-By: Jean Boussier --- ext/json/parser/parser.c | 650 ++++++++++++++++++++++++++++----------- 1 file changed, 462 insertions(+), 188 deletions(-) diff --git a/ext/json/parser/parser.c b/ext/json/parser/parser.c index d0482f68611968..0b0bfd713f4061 100644 --- a/ext/json/parser/parser.c +++ b/ext/json/parser/parser.c @@ -309,31 +309,57 @@ static void rvalue_stack_eagerly_release(VALUE handle) } } -static int convert_UTF32_to_UTF8(char *buf, uint32_t ch) -{ - int len = 1; - if (ch <= 0x7F) { - buf[0] = (char) ch; - } else if (ch <= 0x07FF) { - buf[0] = (char) ((ch >> 6) | 0xC0); - buf[1] = (char) ((ch & 0x3F) | 0x80); - len++; - } else if (ch <= 0xFFFF) { - buf[0] = (char) ((ch >> 12) | 0xE0); - buf[1] = (char) (((ch >> 6) & 0x3F) | 0x80); - buf[2] = (char) ((ch & 0x3F) | 0x80); - len += 2; - } else if (ch <= 0x1fffff) { - buf[0] =(char) ((ch >> 18) | 0xF0); - buf[1] =(char) (((ch >> 12) & 0x3F) | 0x80); - buf[2] =(char) (((ch >> 6) & 0x3F) | 0x80); - buf[3] =(char) ((ch & 0x3F) | 0x80); - len += 3; - } else { - buf[0] = '?'; - } - return len; -} +/* frame stack */ + +// Iterative (non-recursive) parsing keeps an explicit stack of the containers +// currently being built, instead of relying on the C call stack. Each frame +// only needs enough bookkeeping to close its container: which kind it is, the +// rvalue_stack position where its children start (so we know how many to pop), +// and the cursor at its opening brace (used to rewind for duplicate key +// errors). Frames hold no VALUEs, so this stack needs no GC marking; it reuses +// the same stack-allocated-with-heap-spill strategy as the rvalue_stack so that +// it's freed even if parsing raises. +// +// The lifecycle helpers below (grow/push/peek/pop/spill/free/eagerly_release +// and the rb_data_type_t) deliberately mirror their rvalue_stack counterparts +// -- the element type and the absence of a mark function are the only real +// differences. Keep the two in sync: a fix to the spill/release or +// HAVE_RUBY_TYPED_EMBEDDABLE handling in one almost certainly belongs in the +// other. +#define JSON_FRAME_STACK_INITIAL_CAPA 32 + +enum json_frame_type { + JSON_FRAME_ROOT, + JSON_FRAME_ARRAY, + JSON_FRAME_OBJECT, +}; + +// Where a frame is within its container's grammar. This is the entirety of the +// parser's "what to do next" state: json_parse_any dispatches on the top +// frame's phase and holds no resume state in C locals, so a parse can stop at +// any value boundary and be resumed purely from the (persistable) frame stack. +enum json_frame_phase { + JSON_PHASE_VALUE, // expecting a value (document root, array element, or object value after ':') + JSON_PHASE_ARRAY_COMMA, // after a value: expecting ',' or the closing ']' + JSON_PHASE_OBJECT_KEY, // expecting a '"' key (after '{' or ',') + JSON_PHASE_OBJECT_COMMA, // after a value: expecting ',' or the closing '}' + JSON_PHASE_OBJECT_COLON, // object only: after a key, expecting ':' + JSON_PHASE_DONE, // root only: the document value has been parsed +}; + +typedef struct json_frame_struct { + enum json_frame_type type; + enum json_frame_phase phase; + long value_stack_head; // rvalue_stack->head when this container opened + const char *start_cursor; // object frames only (the '{'); NULL otherwise +} json_frame; + +typedef struct json_frame_stack_struct { + enum rvalue_stack_type type; // shared with rvalue_stack: is ptr stack- or heap-allocated + long capa; + long head; + json_frame *ptr; +} json_frame_stack; enum duplicate_key_action { JSON_DEPRECATED = 0, @@ -356,17 +382,143 @@ typedef struct JSON_ParserStruct { } JSON_ParserConfig; typedef struct JSON_ParserStateStruct { - VALUE *stack_handle; + VALUE *value_stack_handle; + VALUE *frame_stack_handle; const char *start; const char *cursor; const char *end; - rvalue_stack *stack; + rvalue_stack *value_stack; + json_frame_stack *frames; rvalue_cache name_cache; int in_array; int current_nesting; unsigned int emitted_deprecations; } JSON_ParserState; +static json_frame_stack *json_frame_stack_spill(json_frame_stack *old_stack, VALUE *handle, json_frame_stack **stack_ref); + +static json_frame_stack *json_frame_stack_grow(json_frame_stack *stack, VALUE *handle, json_frame_stack **stack_ref) +{ + long required = stack->capa * 2; + + if (stack->type == RVALUE_STACK_STACK_ALLOCATED) { + stack = json_frame_stack_spill(stack, handle, stack_ref); + } else { + JSON_SIZED_REALLOC_N(stack->ptr, json_frame, required, stack->capa); + stack->capa = required; + } + return stack; +} + +static json_frame *json_frame_stack_push(JSON_ParserState *state, json_frame frame) +{ + json_frame_stack *stack = state->frames; + if (RB_UNLIKELY(stack->head >= stack->capa)) { + stack = json_frame_stack_grow(stack, state->frame_stack_handle, &state->frames); + } + + json_frame *frame_ptr = &stack->ptr[stack->head++]; + *frame_ptr = frame; + return frame_ptr; +} + +static inline json_frame *json_frame_stack_peek(json_frame_stack *stack) +{ + return &stack->ptr[stack->head - 1]; +} + +static inline void json_frame_stack_pop(json_frame_stack *stack) +{ + stack->head--; +} + +static void json_frame_stack_free_buffer(json_frame_stack *stack) +{ + JSON_SIZED_FREE_N(stack->ptr, stack->capa); + stack->ptr = NULL; +} + +static void json_frame_stack_free(void *ptr) +{ + json_frame_stack *stack = (json_frame_stack *)ptr; + if (stack) { + json_frame_stack_free_buffer(stack); +#ifndef HAVE_RUBY_TYPED_EMBEDDABLE + JSON_SIZED_FREE(stack); +#endif + } +} + +static size_t json_frame_stack_memsize(const void *ptr) +{ + const json_frame_stack *stack = (const json_frame_stack *)ptr; + return sizeof(json_frame_stack) + sizeof(json_frame) * stack->capa; +} + +static const rb_data_type_t JSON_Parser_frame_stack_type = { + .wrap_struct_name = "JSON::Ext::Parser/frame_stack", + .function = { + .dmark = NULL, + .dfree = json_frame_stack_free, + .dsize = json_frame_stack_memsize, + }, + .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_EMBEDDABLE, +}; + +static json_frame_stack *json_frame_stack_spill(json_frame_stack *old_stack, VALUE *handle, json_frame_stack **stack_ref) +{ + json_frame_stack *stack; + *handle = TypedData_Make_Struct(0, json_frame_stack, &JSON_Parser_frame_stack_type, stack); + *stack_ref = stack; + MEMCPY(stack, old_stack, json_frame_stack, 1); + + stack->capa = old_stack->capa << 1; + stack->ptr = ALLOC_N(json_frame, stack->capa); + stack->type = RVALUE_STACK_HEAP_ALLOCATED; + MEMCPY(stack->ptr, old_stack->ptr, json_frame, old_stack->head); + return stack; +} + +static void json_frame_stack_eagerly_release(VALUE handle) +{ + if (handle) { + json_frame_stack *stack; + TypedData_Get_Struct(handle, json_frame_stack, &JSON_Parser_frame_stack_type, stack); +#ifdef HAVE_RUBY_TYPED_EMBEDDABLE + json_frame_stack_free_buffer(stack); +#else + json_frame_stack_free(stack); + RTYPEDDATA_DATA(handle) = NULL; +#endif + } +} + +static int convert_UTF32_to_UTF8(char *buf, uint32_t ch) +{ + int len = 1; + if (ch <= 0x7F) { + buf[0] = (char) ch; + } else if (ch <= 0x07FF) { + buf[0] = (char) ((ch >> 6) | 0xC0); + buf[1] = (char) ((ch & 0x3F) | 0x80); + len++; + } else if (ch <= 0xFFFF) { + buf[0] = (char) ((ch >> 12) | 0xE0); + buf[1] = (char) (((ch >> 6) & 0x3F) | 0x80); + buf[2] = (char) ((ch & 0x3F) | 0x80); + len += 2; + } else if (ch <= 0x1fffff) { + buf[0] =(char) ((ch >> 18) | 0xF0); + buf[1] =(char) (((ch >> 12) & 0x3F) | 0x80); + buf[2] =(char) (((ch >> 6) & 0x3F) | 0x80); + buf[3] =(char) ((ch & 0x3F) | 0x80); + len += 3; + } else { + buf[0] = '?'; + } + return len; +} + static inline size_t rest(JSON_ParserState *state) { return state->end - state->cursor; } @@ -890,8 +1042,8 @@ static inline VALUE json_decode_float(JSON_ParserConfig *config, uint64_t mantis static inline VALUE json_decode_array(JSON_ParserState *state, JSON_ParserConfig *config, long count) { - VALUE array = rb_ary_new_from_values(count, rvalue_stack_peek(state->stack, count)); - rvalue_stack_pop(state->stack, count); + VALUE array = rb_ary_new_from_values(count, rvalue_stack_peek(state->value_stack, count)); + rvalue_stack_pop(state->value_stack, count); if (config->freeze) { RB_OBJ_FREEZE(array); @@ -945,7 +1097,7 @@ static inline VALUE json_decode_object(JSON_ParserState *state, JSON_ParserConfi { size_t entries_count = count / 2; VALUE object = rb_hash_new_capa(entries_count); - const VALUE *pairs = rvalue_stack_peek(state->stack, count); + const VALUE *pairs = rvalue_stack_peek(state->value_stack, count); rb_hash_bulk_insert(count, pairs, object); if (RB_UNLIKELY(RHASH_SIZE(object) < entries_count)) { @@ -966,7 +1118,7 @@ static inline VALUE json_decode_object(JSON_ParserState *state, JSON_ParserConfi } } - rvalue_stack_pop(state->stack, count); + rvalue_stack_pop(state->value_stack, count); if (config->freeze) { RB_OBJ_FREEZE(object); @@ -980,7 +1132,7 @@ static inline VALUE json_push_value(JSON_ParserState *state, JSON_ParserConfig * if (RB_UNLIKELY(config->on_load_proc)) { value = rb_proc_call_with_block(config->on_load_proc, 1, &value, Qnil); } - rvalue_stack_push(state->stack, value, state->stack_handle, &state->stack); + rvalue_stack_push(state->value_stack, value, state->value_stack_handle, &state->value_stack); return value; } @@ -1247,92 +1399,241 @@ static inline VALUE json_parse_negative_number(JSON_ParserState *state, JSON_Par return json_parse_number(state, config, true, start); } +// How many values (array elements, or interleaved object keys+values) have been +// pushed onto the rvalue stack since this container opened. Used to size the +// bulk decode on close, and to tell the first key/colon from later ones. +static inline long json_frame_entry_count(const json_frame *frame, const rvalue_stack *value_stack) +{ + return value_stack->head - frame->value_stack_head; +} + +// A complete value now sits on top of the rvalue stack. Advance the frame that +// was waiting for it: the root document is done, or the enclosing container +// moves on to expecting a ',' or its closing bracket. The caller passes the +// frame it already has in hand -- the one that was expecting the value -- which +// after a container close is the freshly re-exposed parent. +static inline void json_value_completed(json_frame *frame) +{ + // TODO: consider a lookup table? + switch (frame->type) { + case JSON_FRAME_ROOT: + frame->phase = JSON_PHASE_DONE; + break; + case JSON_FRAME_ARRAY: + frame->phase = JSON_PHASE_ARRAY_COMMA; + break; + case JSON_FRAME_OBJECT: + frame->phase = JSON_PHASE_OBJECT_COMMA; + break; + } +} + +// Parse an arbitrary JSON value iteratively. This is a state machine driven +// entirely by the top frame's phase so it can stop at any value boundary and +// resume purely from the frame stack. A JSON_FRAME_ROOT frame sits at the +// bottom of the stack, so the stack is never empty mid-parse and the document +// itself is just another frame whose value, once parsed, leaves its phase DONE. static VALUE json_parse_any(JSON_ParserState *state, JSON_ParserConfig *config) { - json_eat_whitespace(state); + while (true) { + json_frame *frame = json_frame_stack_peek(state->frames); - switch (peek(state)) { - case 'n': - if (rest(state) >= 4 && (memcmp(state->cursor, "null", 4) == 0)) { - state->cursor += 4; - return json_push_value(state, config, Qnil); + switch (frame->phase) { + case JSON_PHASE_DONE: { + // The root document value is parsed; it is the lone survivor on + // the rvalue stack. + return *rvalue_stack_peek(state->value_stack, 1); } - raise_parse_error("unexpected token %s", state); - break; - case 't': - if (rest(state) >= 4 && (memcmp(state->cursor, "true", 4) == 0)) { - state->cursor += 4; - return json_push_value(state, config, Qtrue); - } + case JSON_PHASE_VALUE: { + JSON_PHASE_VALUE: + json_eat_whitespace(state); - raise_parse_error("unexpected token %s", state); - break; - case 'f': - // Note: memcmp with a small power of two compile to an integer comparison - if (rest(state) >= 5 && (memcmp(state->cursor + 1, "alse", 4) == 0)) { - state->cursor += 5; - return json_push_value(state, config, Qfalse); - } + switch (peek(state)) { + case 'n': + if (rest(state) >= 4 && (memcmp(state->cursor, "null", 4) == 0)) { + state->cursor += 4; + json_push_value(state, config, Qnil); + json_value_completed(frame); + break; + } - raise_parse_error("unexpected token %s", state); - break; - case 'N': - // Note: memcmp with a small power of two compile to an integer comparison - if (config->allow_nan && rest(state) >= 3 && (memcmp(state->cursor + 1, "aN", 2) == 0)) { - state->cursor += 3; - return json_push_value(state, config, CNaN); - } + raise_parse_error("unexpected token %s", state); + case 't': + if (rest(state) >= 4 && (memcmp(state->cursor, "true", 4) == 0)) { + state->cursor += 4; + json_push_value(state, config, Qtrue); + json_value_completed(frame); + break; + } - raise_parse_error("unexpected token %s", state); - break; - case 'I': - if (config->allow_nan && rest(state) >= 8 && (memcmp(state->cursor, "Infinity", 8) == 0)) { - state->cursor += 8; - return json_push_value(state, config, CInfinity); + raise_parse_error("unexpected token %s", state); + case 'f': + // Note: memcmp with a small power of two compile to an integer comparison + if (rest(state) >= 5 && (memcmp(state->cursor + 1, "alse", 4) == 0)) { + state->cursor += 5; + json_push_value(state, config, Qfalse); + json_value_completed(frame); + break; + } + + raise_parse_error("unexpected token %s", state); + case 'N': + // Note: memcmp with a small power of two compile to an integer comparison + if (config->allow_nan && rest(state) >= 3 && (memcmp(state->cursor + 1, "aN", 2) == 0)) { + state->cursor += 3; + json_push_value(state, config, CNaN); + json_value_completed(frame); + break; + } + + raise_parse_error("unexpected token %s", state); + case 'I': + if (config->allow_nan && rest(state) >= 8 && (memcmp(state->cursor, "Infinity", 8) == 0)) { + state->cursor += 8; + json_push_value(state, config, CInfinity); + json_value_completed(frame); + break; + } + + raise_parse_error("unexpected token %s", state); + case '-': { + // Note: memcmp with a small power of two compile to an integer comparison + if (rest(state) >= 9 && (memcmp(state->cursor + 1, "Infinity", 8) == 0)) { + if (config->allow_nan) { + state->cursor += 9; + json_push_value(state, config, CMinusInfinity); + json_value_completed(frame); + break; + } else { + raise_parse_error("unexpected token %s", state); + } + } + json_push_value(state, config, json_parse_negative_number(state, config)); + json_value_completed(frame); + break; + } + case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': + json_push_value(state, config, json_parse_positive_number(state, config)); + json_value_completed(frame); + break; + case '"': + // %r{\A"[^"\\\t\n\x00]*(?:\\[bfnrtu\\/"][^"\\]*)*"} + json_parse_string(state, config, false); + json_value_completed(frame); + break; + case '[': { + state->cursor++; + json_eat_whitespace(state); + + if (peek(state) == ']') { + state->cursor++; + json_push_value(state, config, json_decode_array(state, config, 0)); + json_value_completed(frame); + break; + } + + state->current_nesting++; + if (RB_UNLIKELY(config->max_nesting && (config->max_nesting < state->current_nesting))) { + rb_raise(eNestingError, "nesting of %d is too deep", state->current_nesting); + } + state->in_array++; + + // Phase stays VALUE: the next iteration reads the first element. + frame = json_frame_stack_push(state, (json_frame){ + .type = JSON_FRAME_ARRAY, + .phase = JSON_PHASE_VALUE, + .value_stack_head = state->value_stack->head, + }); + goto JSON_PHASE_VALUE; + break; + } + case '{': { + const char *object_start_cursor = state->cursor; + + state->cursor++; + json_eat_whitespace(state); + + if (peek(state) == '}') { + state->cursor++; + json_push_value(state, config, json_decode_object(state, config, 0)); + json_value_completed(frame); + break; + } + + state->current_nesting++; + if (RB_UNLIKELY(config->max_nesting && (config->max_nesting < state->current_nesting))) { + rb_raise(eNestingError, "nesting of %d is too deep", state->current_nesting); + } + + // Phase KEY: the next iteration reads the first key. + frame = json_frame_stack_push(state, (json_frame){ + .type = JSON_FRAME_OBJECT, + .phase = JSON_PHASE_OBJECT_KEY, + .value_stack_head = state->value_stack->head, + .start_cursor = object_start_cursor, + }); + goto JSON_PHASE_OBJECT_KEY; + break; + } + + case 0: + raise_parse_error("unexpected end of input", state); + + default: + raise_parse_error("unexpected character: %s", state); + } + break; } - raise_parse_error("unexpected token %s", state); - break; - case '-': { - // Note: memcmp with a small power of two compile to an integer comparison - if (rest(state) >= 9 && (memcmp(state->cursor + 1, "Infinity", 8) == 0)) { - if (config->allow_nan) { - state->cursor += 9; - return json_push_value(state, config, CMinusInfinity); + case JSON_PHASE_OBJECT_KEY: { + JSON_PHASE_OBJECT_KEY: + JSON_ASSERT(frame->type == JSON_FRAME_OBJECT); + + json_eat_whitespace(state); + + if (RB_LIKELY(peek(state) == '"')) { + json_parse_string(state, config, true); + frame->phase = JSON_PHASE_OBJECT_COLON; + goto JSON_PHASE_OBJECT_COLON; } else { - raise_parse_error("unexpected token %s", state); + // The message differs for the first key vs. a key after a + // ',': the first is the only one reached with nothing pushed + // for this object yet. + if (json_frame_entry_count(frame, state->value_stack) == 0) { + raise_parse_error("expected object key, got %s", state); + } else { + raise_parse_error("expected object key, got: %s", state); + } } + break; } - return json_push_value(state, config, json_parse_negative_number(state, config)); - break; - } - case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': - return json_push_value(state, config, json_parse_positive_number(state, config)); - break; - case '"': { - // %r{\A"[^"\\\t\n\x00]*(?:\\[bfnrtu\\/"][^"\\]*)*"} - return json_parse_string(state, config, false); - break; - } - case '[': { - state->cursor++; - json_eat_whitespace(state); - long stack_head = state->stack->head; - if (peek(state) == ']') { - state->cursor++; - return json_push_value(state, config, json_decode_array(state, config, 0)); - } else { - state->current_nesting++; - if (RB_UNLIKELY(config->max_nesting && (config->max_nesting < state->current_nesting))) { - rb_raise(eNestingError, "nesting of %d is too deep", state->current_nesting); + case JSON_PHASE_OBJECT_COLON: { + JSON_PHASE_OBJECT_COLON: + JSON_ASSERT(frame->type == JSON_FRAME_OBJECT); + + json_eat_whitespace(state); + + if (RB_LIKELY(peek(state) == ':')) { + state->cursor++; + frame->phase = JSON_PHASE_VALUE; + goto JSON_PHASE_VALUE; + } else { + // First colon (only the first pair's key is pushed, nothing + // else) vs. a later one. + if (json_frame_entry_count(frame, state->value_stack) == 1) { + raise_parse_error("expected ':' after object key", state); + } else { + raise_parse_error("expected ':' after object key, got: %s", state); + } } - state->in_array++; - json_parse_any(state, config); + break; } - while (true) { + case JSON_PHASE_ARRAY_COMMA: { + JSON_ASSERT(frame->type == JSON_FRAME_ARRAY); + json_eat_whitespace(state); const char next_char = peek(state); @@ -1342,115 +1643,68 @@ static VALUE json_parse_any(JSON_ParserState *state, JSON_ParserConfig *config) if (config->allow_trailing_comma) { json_eat_whitespace(state); if (peek(state) == ']') { - continue; + // Trailing comma: stay in COMMA to close on the next iteration. + break; } } - json_parse_any(state, config); - continue; - } - - if (next_char == ']') { + frame->phase = JSON_PHASE_VALUE; + goto JSON_PHASE_VALUE; + } else if (next_char == ']') { state->cursor++; - long count = state->stack->head - stack_head; + long count = json_frame_entry_count(frame, state->value_stack); state->current_nesting--; state->in_array--; - return json_push_value(state, config, json_decode_array(state, config, count)); + json_frame_stack_pop(state->frames); + json_push_value(state, config, json_decode_array(state, config, count)); + json_value_completed(json_frame_stack_peek(state->frames)); + } else { + raise_parse_error("expected ',' or ']' after array value", state); } - - raise_parse_error("expected ',' or ']' after array value", state); + break; } - break; - } - case '{': { - const char *object_start_cursor = state->cursor; - state->cursor++; - json_eat_whitespace(state); - long stack_head = state->stack->head; + case JSON_PHASE_OBJECT_COMMA: { + JSON_ASSERT(frame->type == JSON_FRAME_OBJECT); - if (peek(state) == '}') { - state->cursor++; - return json_push_value(state, config, json_decode_object(state, config, 0)); - } else { - state->current_nesting++; - if (RB_UNLIKELY(config->max_nesting && (config->max_nesting < state->current_nesting))) { - rb_raise(eNestingError, "nesting of %d is too deep", state->current_nesting); - } - - if (peek(state) != '"') { - raise_parse_error("expected object key, got %s", state); - } - json_parse_string(state, config, true); - - json_eat_whitespace(state); - if (peek(state) != ':') { - raise_parse_error("expected ':' after object key", state); - } - state->cursor++; - - json_parse_any(state, config); - } - - while (true) { json_eat_whitespace(state); - const char next_char = peek(state); - if (next_char == '}') { - state->cursor++; - state->current_nesting--; - size_t count = state->stack->head - stack_head; - // Temporary rewind cursor in case an error is raised - const char *final_cursor = state->cursor; - state->cursor = object_start_cursor; - VALUE object = json_decode_object(state, config, count); - state->cursor = final_cursor; - - return json_push_value(state, config, object); - } - - if (next_char == ',') { + if (RB_LIKELY(next_char == ',')) { state->cursor++; json_eat_whitespace(state); if (config->allow_trailing_comma) { if (peek(state) == '}') { - continue; + // Trailing comma: stay in COMMA to close on the next iteration. + break; } } - if (RB_UNLIKELY(peek(state) != '"')) { - raise_parse_error("expected object key, got: %s", state); - } - json_parse_string(state, config, true); + frame->phase = JSON_PHASE_OBJECT_KEY; + goto JSON_PHASE_OBJECT_KEY; - json_eat_whitespace(state); - if (RB_UNLIKELY(peek(state) != ':')) { - raise_parse_error("expected ':' after object key, got: %s", state); - } + break; + } else if (next_char == '}') { state->cursor++; + state->current_nesting--; + size_t count = json_frame_entry_count(frame, state->value_stack); - json_parse_any(state, config); + // Temporary rewind cursor in case an error is raised + const char *final_cursor = state->cursor; + state->cursor = frame->start_cursor; + VALUE object = json_decode_object(state, config, count); + state->cursor = final_cursor; - continue; + json_frame_stack_pop(state->frames); + json_push_value(state, config, object); + json_value_completed(json_frame_stack_peek(state->frames)); + break; + } else { + raise_parse_error("expected ',' or '}' after object value, got: %s", state); } - - raise_parse_error("expected ',' or '}' after object value, got: %s", state); } - break; } - - case 0: - raise_parse_error("unexpected end of input", state); - break; - - default: - raise_parse_error("unexpected character: %s", state); - break; } - - raise_parse_error("unreachable: %s", state); - return Qundef; } static void json_ensure_eof(JSON_ParserState *state) @@ -1616,24 +1870,42 @@ static VALUE cParser_parse(JSON_ParserConfig *config, VALUE src) } VALUE rvalue_stack_buffer[RVALUE_STACK_INITIAL_CAPA]; - rvalue_stack stack = { + rvalue_stack value_stack = { .type = RVALUE_STACK_STACK_ALLOCATED, .ptr = rvalue_stack_buffer, .capa = RVALUE_STACK_INITIAL_CAPA, }; + // Seed the frame stack with the root frame, establishing the invariant that + // json_parse_any always has a top frame to dispatch on (so the stack is never + // empty mid-parse). + json_frame frame_stack_buffer[JSON_FRAME_STACK_INITIAL_CAPA]; + frame_stack_buffer[0] = (json_frame){ + .type = JSON_FRAME_ROOT, + .phase = JSON_PHASE_VALUE, + }; + json_frame_stack frames = { + .type = RVALUE_STACK_STACK_ALLOCATED, + .ptr = frame_stack_buffer, + .capa = JSON_FRAME_STACK_INITIAL_CAPA, + .head = 1, + }; + long len; const char *start; RSTRING_GETMEM(Vsource, start, len); - VALUE stack_handle = 0; + VALUE value_stack_handle = 0; + VALUE frame_stack_handle = 0; JSON_ParserState _state = { .start = start, .cursor = start, .end = start + len, - .stack = &stack, - .stack_handle = &stack_handle, + .value_stack = &value_stack, + .value_stack_handle = &value_stack_handle, + .frames = &frames, + .frame_stack_handle = &frame_stack_handle, }; JSON_ParserState *state = &_state; @@ -1641,8 +1913,10 @@ static VALUE cParser_parse(JSON_ParserConfig *config, VALUE src) // This may be skipped in case of exception, but // it won't cause a leak. - rvalue_stack_eagerly_release(stack_handle); - RB_GC_GUARD(stack_handle); + rvalue_stack_eagerly_release(value_stack_handle); + json_frame_stack_eagerly_release(frame_stack_handle); + RB_GC_GUARD(value_stack_handle); + RB_GC_GUARD(frame_stack_handle); RB_GC_GUARD(Vsource); json_ensure_eof(state); From 6d5f8aba67d40423c4e467682cc36202ab0c59e7 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Thu, 4 Jun 2026 10:08:07 +0200 Subject: [PATCH 124/188] [ruby/json] parser.c: Mark some paths as unreacheable Generate very sligthly better code (one less instruction...). Also extract `json_on_duplicate_key` as `NOINLINE`. https://github.com/ruby/json/commit/500f0eabf3 --- ext/json/parser/parser.c | 45 +++++++++++++++++++++++----------------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/ext/json/parser/parser.c b/ext/json/parser/parser.c index 0b0bfd713f4061..eb7dc9aa8218cf 100644 --- a/ext/json/parser/parser.c +++ b/ext/json/parser/parser.c @@ -1093,6 +1093,27 @@ NORETURN(static) void raise_duplicate_key_error(JSON_ParserState *state, VALUE d rb_exc_raise(parse_error_new(message, line, column)); } +NOINLINE(static) void json_on_duplicate_key(JSON_ParserState *state, JSON_ParserConfig *config, size_t count, const VALUE *pairs) +{ + switch (config->on_duplicate_key) { + case JSON_IGNORE: + return; + + case JSON_DEPRECATED: + // Only emit the first few deprecations to avoid spamming. + if (state->emitted_deprecations < 5) { + emit_duplicate_key_warning(state, json_find_duplicated_key(count, pairs)); + state->emitted_deprecations++; + } + return; + + case JSON_RAISE: + raise_duplicate_key_error(state, json_find_duplicated_key(count, pairs)); + return; + } + UNREACHABLE; +} + static inline VALUE json_decode_object(JSON_ParserState *state, JSON_ParserConfig *config, size_t count) { size_t entries_count = count / 2; @@ -1101,21 +1122,7 @@ static inline VALUE json_decode_object(JSON_ParserState *state, JSON_ParserConfi rb_hash_bulk_insert(count, pairs, object); if (RB_UNLIKELY(RHASH_SIZE(object) < entries_count)) { - switch (config->on_duplicate_key) { - case JSON_IGNORE: - break; - case JSON_DEPRECATED: - // Only emit the first few deprecations to avoid spamming. - if (state->emitted_deprecations < 5) { - emit_duplicate_key_warning(state, json_find_duplicated_key(count, pairs)); - state->emitted_deprecations++; - } - - break; - case JSON_RAISE: - raise_duplicate_key_error(state, json_find_duplicated_key(count, pairs)); - break; - } + json_on_duplicate_key(state, config, count, pairs); } rvalue_stack_pop(state->value_stack, count); @@ -1414,18 +1421,18 @@ static inline long json_frame_entry_count(const json_frame *frame, const rvalue_ // after a container close is the freshly re-exposed parent. static inline void json_value_completed(json_frame *frame) { - // TODO: consider a lookup table? switch (frame->type) { case JSON_FRAME_ROOT: frame->phase = JSON_PHASE_DONE; - break; + return; case JSON_FRAME_ARRAY: frame->phase = JSON_PHASE_ARRAY_COMMA; - break; + return; case JSON_FRAME_OBJECT: frame->phase = JSON_PHASE_OBJECT_COMMA; - break; + return; } + UNREACHABLE; } // Parse an arbitrary JSON value iteratively. This is a state machine driven From 2c3723df843e66422a84c2442fe96f2e6a805ec0 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Mon, 16 Mar 2026 09:11:28 +0900 Subject: [PATCH 125/188] [ruby/zlib] Drop older rubies than 2.7 This library already uses designated initializers, that is a C99 feature. C99 has been adopted since ruby 2.7. https://github.com/ruby/zlib/commit/cfa8fb2db4 --- ext/zlib/zlib.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/zlib/zlib.gemspec b/ext/zlib/zlib.gemspec index 345dc5f22575e1..ba7114476f2d9e 100644 --- a/ext/zlib/zlib.gemspec +++ b/ext/zlib/zlib.gemspec @@ -27,5 +27,5 @@ Gem::Specification.new do |spec| spec.executables = [] spec.require_paths = ["lib"] spec.extensions = "ext/zlib/extconf.rb" - spec.required_ruby_version = ">= 2.5.0" + spec.required_ruby_version = ">= 2.7.0" end From c976dffd147d9f8871cd122dd8e35561d2b9422d Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 5 Jun 2026 09:36:06 +0900 Subject: [PATCH 126/188] IO::Buffer.map is not shareable across processes on OpenBSD Restore the OpenBSD exclusion dropped when the windows guard was replaced with a fork guard in the spec sync. OpenBSD has fork but MAP_SHARED writes are not reflected through read(2). https://rubyci.s3.amazonaws.com/openbsd-current/ruby-master/log/20260604T223004Z.fail.html.gz Co-Authored-By: Claude Opus 4.8 (1M context) --- spec/ruby/core/io/buffer/map_spec.rb | 46 +++++++++++++++------------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/spec/ruby/core/io/buffer/map_spec.rb b/spec/ruby/core/io/buffer/map_spec.rb index 4b28539ad82762..97764c2dd75429 100644 --- a/spec/ruby/core/io/buffer/map_spec.rb +++ b/spec/ruby/core/io/buffer/map_spec.rb @@ -73,30 +73,34 @@ def open_big_file_fixture @buffer.should.valid? end - guard -> { Process.respond_to?(:fork) } do - it "is shareable across processes" do - file_name = tmp("shared_buffer") - @file = File.open(file_name, "w+") - @file << "I'm private" - @file.rewind - @buffer = IO::Buffer.map(@file) - - IO.popen("-") do |child_pipe| - if child_pipe - # Synchronize on child's output. - child_pipe.readlines.first.chomp.should == @buffer.to_s - @buffer.get_string.should == "I'm shared!" - - @file.read.should == "I'm shared!" - else - @buffer.set_string("I'm shared!") - puts @buffer + # IO::Buffer.map seems not shareable across processes on OpenBSD. + # See https://rubyci.s3.amazonaws.com/openbsd-current/ruby-master/log/20260129T163005Z.fail.html.gz + platform_is_not :openbsd do + guard -> { Process.respond_to?(:fork) } do + it "is shareable across processes" do + file_name = tmp("shared_buffer") + @file = File.open(file_name, "w+") + @file << "I'm private" + @file.rewind + @buffer = IO::Buffer.map(@file) + + IO.popen("-") do |child_pipe| + if child_pipe + # Synchronize on child's output. + child_pipe.readlines.first.chomp.should == @buffer.to_s + @buffer.get_string.should == "I'm shared!" + + @file.read.should == "I'm shared!" + else + @buffer.set_string("I'm shared!") + puts @buffer + end + ensure + child_pipe&.close end ensure - child_pipe&.close + File.unlink(file_name) end - ensure - File.unlink(file_name) end end From fd103929803b3607673bad2956f46295ddf06810 Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Thu, 4 Jun 2026 08:58:42 +0900 Subject: [PATCH 127/188] [DOC] Improve docs for ObjectSpace.undefine_finalizer --- gc.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/gc.c b/gc.c index 2772f956387bfd..0219fa6e78cbee 100644 --- a/gc.c +++ b/gc.c @@ -1856,10 +1856,12 @@ os_each_obj(int argc, VALUE *argv, VALUE os) /* * call-seq: - * ObjectSpace.undefine_finalizer(obj) + * ObjectSpace.undefine_finalizer(obj) -> obj * - * Removes all finalizers for obj. + * Removes all finalizers registered for +obj+ with + * ObjectSpace.define_finalizer, and returns +obj+. * + * Does nothing if +obj+ has no finalizers. */ static VALUE From 13439367d5c89b3161f66adea4578abb307a8890 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Thu, 4 Jun 2026 17:36:46 +0900 Subject: [PATCH 128/188] [ruby/rubygems] Strip C1 control characters in Gem::Text#clean_text Match C1 controls (U+0080-U+009F) as codepoints and only for valid UTF-8 text, so multibyte characters are preserved and other encodings are left unchanged. https://github.com/ruby/rubygems/commit/c272a8b138 Co-Authored-By: Claude Opus 4.8 (1M context) --- lib/rubygems/text.rb | 10 +++++++++- test/rubygems/test_gem_text.rb | 15 +++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/lib/rubygems/text.rb b/lib/rubygems/text.rb index 88d4ce59b4b9ae..8c78304d4ec2f3 100644 --- a/lib/rubygems/text.rb +++ b/lib/rubygems/text.rb @@ -8,7 +8,15 @@ module Gem::Text # Remove any non-printable characters and make the text suitable for # printing. def clean_text(text) - text.gsub(/[\000-\b\v-\f\016-\037\177]/, ".") + text = text.gsub(/[\000-\b\v-\f\016-\037\177]/, ".") + + # C1 control characters (U+0080-U+009F) only occur in UTF-8 text and must + # be matched as codepoints so that multibyte characters are preserved. + if text.encoding == Encoding::UTF_8 && text.valid_encoding? + text = text.gsub(/[\u0080-\u009f]/, ".") + end + + text end def truncate_text(text, description, max_length = 100_000) diff --git a/test/rubygems/test_gem_text.rb b/test/rubygems/test_gem_text.rb index 8e9961094612dd..ad35210c5968a5 100644 --- a/test/rubygems/test_gem_text.rb +++ b/test/rubygems/test_gem_text.rb @@ -100,4 +100,19 @@ def test_truncate_text def test_clean_text assert_equal ".]2;nyan.", clean_text("\e]2;nyan\a") end + + def test_clean_text_strips_c1_control_characters + text = [0x41, 0x9b, 0x42].pack("U*") # "A", CSI (U+009B), "B" + assert_equal "A.B", clean_text(text) + end + + def test_clean_text_preserves_multibyte_characters + text = [0xe9, 0x85].pack("U*") # U+00E9 kept, NEL (U+0085) stripped + assert_equal [0xe9, 0x2e].pack("U*"), clean_text(text) + end + + def test_clean_text_passes_through_non_unicode_encodings + text = "x\x9by".dup.force_encoding("ISO-8859-1") + assert_equal text, clean_text(text) + end end From e8a5722a7fbd6aa5f5090d3ffa3ebcbdbb1e9f3d Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Thu, 4 Jun 2026 17:36:46 +0900 Subject: [PATCH 129/188] [ruby/rubygems] Clean control characters from the post-install message Route the post-install message through Gem::Text#clean_text before printing it so a crafted message cannot emit raw terminal control sequences. https://github.com/ruby/rubygems/commit/cc62ee89ab Co-Authored-By: Claude Opus 4.8 (1M context) --- lib/rubygems/installer.rb | 2 +- test/rubygems/test_gem_installer.rb | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/lib/rubygems/installer.rb b/lib/rubygems/installer.rb index 15d6aac0fd1ba0..5b4504aa96b661 100644 --- a/lib/rubygems/installer.rb +++ b/lib/rubygems/installer.rb @@ -299,7 +299,7 @@ def install File.chmod(dir_mode, gem_dir) if dir_mode - say spec.post_install_message if options[:post_install_message] && !spec.post_install_message.nil? + say clean_text(spec.post_install_message) if options[:post_install_message] && !spec.post_install_message.nil? Gem::Specification.add_spec(spec) unless @install_dir diff --git a/test/rubygems/test_gem_installer.rb b/test/rubygems/test_gem_installer.rb index bf7a4a8dfc8187..44d14e8150c440 100644 --- a/test/rubygems/test_gem_installer.rb +++ b/test/rubygems/test_gem_installer.rb @@ -1481,6 +1481,23 @@ def test_install_with_skipped_message refute_match(/I am a shiny gem!/, @ui.output) end + def test_install_sanitizes_post_install_message + # Use for_spec so the in-memory message reaches the installer verbatim; + # building a gem would escape the control characters during serialization. + @spec = setup_base_spec + @spec.post_install_message = "shiny \e]2;pwn\a gem" + + installer = Gem::Installer.for_spec @spec, post_install_message: true + installer.gem_home = @gemhome + + use_ui @ui do + installer.install + end + + assert_match(/shiny \.\]2;pwn\. gem/, @ui.output) + refute_match(/\e\]2;pwn/, @ui.output) + end + def test_install_extension_dir gemhome2 = "#{@gemhome}2" From 4b4b99deaada687a3e7044c13795207cf4b3d955 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Thu, 4 Jun 2026 18:39:44 +0900 Subject: [PATCH 130/188] [ruby/rubygems] Clarify the C1 comment and strengthen the multibyte test Reword the comment to explain that the UTF-8 guard avoids splitting multibyte sequences, and assert preservation with U+0400, whose continuation byte falls in the C1 byte range. https://github.com/ruby/rubygems/commit/0db489ff2e Co-Authored-By: Claude Opus 4.8 (1M context) --- lib/rubygems/text.rb | 5 +++-- test/rubygems/test_gem_text.rb | 6 ++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/rubygems/text.rb b/lib/rubygems/text.rb index 8c78304d4ec2f3..0550dc473d338c 100644 --- a/lib/rubygems/text.rb +++ b/lib/rubygems/text.rb @@ -10,8 +10,9 @@ module Gem::Text def clean_text(text) text = text.gsub(/[\000-\b\v-\f\016-\037\177]/, ".") - # C1 control characters (U+0080-U+009F) only occur in UTF-8 text and must - # be matched as codepoints so that multibyte characters are preserved. + # Match C1 control characters (U+0080-U+009F) as codepoints. This requires + # a valid UTF-8 string so the regexp does not split a multibyte sequence; + # strings in other encodings are left unchanged. if text.encoding == Encoding::UTF_8 && text.valid_encoding? text = text.gsub(/[\u0080-\u009f]/, ".") end diff --git a/test/rubygems/test_gem_text.rb b/test/rubygems/test_gem_text.rb index ad35210c5968a5..60739e61319892 100644 --- a/test/rubygems/test_gem_text.rb +++ b/test/rubygems/test_gem_text.rb @@ -107,8 +107,10 @@ def test_clean_text_strips_c1_control_characters end def test_clean_text_preserves_multibyte_characters - text = [0xe9, 0x85].pack("U*") # U+00E9 kept, NEL (U+0085) stripped - assert_equal [0xe9, 0x2e].pack("U*"), clean_text(text) + # U+0400 encodes to bytes D0 80, whose 0x80 continuation byte must not be + # mistaken for a C1 control byte. NEL (U+0085) is stripped. + text = [0x400, 0x85].pack("U*") + assert_equal [0x400, 0x2e].pack("U*"), clean_text(text) end def test_clean_text_passes_through_non_unicode_encodings From f2fac353c116e81e74a43158602a1a0f220435b5 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Thu, 4 Jun 2026 18:39:44 +0900 Subject: [PATCH 131/188] [ruby/rubygems] Coerce the post-install message to a String before sanitizing post_install_message may be a non-String such as an array, so call to_s before clean_text to avoid raising during install. https://github.com/ruby/rubygems/commit/95b6bfbd16 Co-Authored-By: Claude Opus 4.8 (1M context) --- lib/rubygems/installer.rb | 2 +- test/rubygems/test_gem_installer.rb | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/lib/rubygems/installer.rb b/lib/rubygems/installer.rb index 5b4504aa96b661..15d241d633d4d9 100644 --- a/lib/rubygems/installer.rb +++ b/lib/rubygems/installer.rb @@ -299,7 +299,7 @@ def install File.chmod(dir_mode, gem_dir) if dir_mode - say clean_text(spec.post_install_message) if options[:post_install_message] && !spec.post_install_message.nil? + say clean_text(spec.post_install_message.to_s) if options[:post_install_message] && !spec.post_install_message.nil? Gem::Specification.add_spec(spec) unless @install_dir diff --git a/test/rubygems/test_gem_installer.rb b/test/rubygems/test_gem_installer.rb index 44d14e8150c440..2f08024ef048a6 100644 --- a/test/rubygems/test_gem_installer.rb +++ b/test/rubygems/test_gem_installer.rb @@ -1498,6 +1498,22 @@ def test_install_sanitizes_post_install_message refute_match(/\e\]2;pwn/, @ui.output) end + def test_install_handles_non_string_post_install_message + # post_install_message may be a non-String (the gemspec schema allows an + # array), so sanitizing must not assume it responds to gsub. + @spec = setup_base_spec + @spec.post_install_message = %w[one two] + + installer = Gem::Installer.for_spec @spec, post_install_message: true + installer.gem_home = @gemhome + + use_ui @ui do + installer.install + end + + assert_match(/one/, @ui.output) + end + def test_install_extension_dir gemhome2 = "#{@gemhome}2" From 4e860ec9e863a6595281a1ef0241fe8d8db76456 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Thu, 4 Jun 2026 16:53:11 +0900 Subject: [PATCH 132/188] [ruby/rubygems] Validate spec.executables in Gem::Installer#verify_spec Reject executables that are not plain basenames during pre-install checks. https://github.com/ruby/rubygems/commit/92198d209d Co-Authored-By: Claude Opus 4.8 (1M context) --- lib/rubygems/installer.rb | 4 ++++ test/rubygems/test_gem_installer.rb | 22 ++++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/lib/rubygems/installer.rb b/lib/rubygems/installer.rb index 15d241d633d4d9..3bc8981c80ca9c 100644 --- a/lib/rubygems/installer.rb +++ b/lib/rubygems/installer.rb @@ -712,6 +712,10 @@ def verify_spec if spec.dependencies.any? {|dep| dep.name =~ /(?:\R|[<>])/ } raise Gem::InstallError, "#{spec} has an invalid dependencies" end + + if spec.executables.any? {|name| name != File.basename(name) || /\A\.\.?\z|\R/.match?(name) } + raise Gem::InstallError, "#{spec} has an invalid executable" + end end ## diff --git a/test/rubygems/test_gem_installer.rb b/test/rubygems/test_gem_installer.rb index 2f08024ef048a6..9b1a1b4d0f745f 100644 --- a/test/rubygems/test_gem_installer.rb +++ b/test/rubygems/test_gem_installer.rb @@ -1954,6 +1954,28 @@ def spec.validate(*args); end end end + def test_pre_install_checks_malicious_executables_before_eval + spec = util_spec "malicious", "1" + def spec.full_name # so the spec is buildable + "malicious-1" + end + + def spec.validate(*args); end + spec.executables = ["../../../tmp/malicious"] + + util_build_gem spec + + gem = File.join(@gemhome, "cache", spec.file_name) + + use_ui @ui do + installer = Gem::Installer.at gem + e = assert_raise Gem::InstallError do + installer.pre_install_checks + end + assert_equal "# has an invalid executable", e.message + end + end + def test_pre_install_checks_malicious_platform_before_eval gem_with_ill_formatted_platform = File.expand_path("packages/ill-formatted-platform-1.0.0.10.gem", __dir__) From 48d8134e4ada96e06b9b17a947eb13597b661826 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Thu, 4 Jun 2026 16:53:32 +0900 Subject: [PATCH 133/188] [ruby/rubygems] Validate spec.bindir in Gem::Installer#verify_spec Reject a bindir that resolves outside the gem directory during pre-install checks. https://github.com/ruby/rubygems/commit/cd61a78b25 Co-Authored-By: Claude Opus 4.8 (1M context) --- lib/rubygems/installer.rb | 6 ++++++ test/rubygems/test_gem_installer.rb | 22 ++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/lib/rubygems/installer.rb b/lib/rubygems/installer.rb index 3bc8981c80ca9c..cea02c9fe9f035 100644 --- a/lib/rubygems/installer.rb +++ b/lib/rubygems/installer.rb @@ -716,6 +716,12 @@ def verify_spec if spec.executables.any? {|name| name != File.basename(name) || /\A\.\.?\z|\R/.match?(name) } raise Gem::InstallError, "#{spec} has an invalid executable" end + + expanded_gem_dir = File.expand_path(gem_dir) + expanded_bindir = File.expand_path(File.join(gem_dir, spec.bindir)) + unless expanded_bindir == expanded_gem_dir || expanded_bindir.start_with?("#{expanded_gem_dir}/") + raise Gem::InstallError, "#{spec} has an invalid bindir" + end end ## diff --git a/test/rubygems/test_gem_installer.rb b/test/rubygems/test_gem_installer.rb index 9b1a1b4d0f745f..1cf1ceeb79c756 100644 --- a/test/rubygems/test_gem_installer.rb +++ b/test/rubygems/test_gem_installer.rb @@ -1976,6 +1976,28 @@ def spec.validate(*args); end end end + def test_pre_install_checks_malicious_bindir_before_eval + spec = util_spec "malicious", "1" + def spec.full_name # so the spec is buildable + "malicious-1" + end + + def spec.validate(*args); end + spec.bindir = "../../../tmp/malicious" + + util_build_gem spec + + gem = File.join(@gemhome, "cache", spec.file_name) + + use_ui @ui do + installer = Gem::Installer.at gem + e = assert_raise Gem::InstallError do + installer.pre_install_checks + end + assert_equal "# has an invalid bindir", e.message + end + end + def test_pre_install_checks_malicious_platform_before_eval gem_with_ill_formatted_platform = File.expand_path("packages/ill-formatted-platform-1.0.0.10.gem", __dir__) From daa3721f02bbd1a70244790e114284da10682498 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Thu, 4 Jun 2026 16:54:39 +0900 Subject: [PATCH 134/188] [ruby/rubygems] Escape executable name when generating the wrapper script Escape the executable name interpolated into the generated wrapper so a name containing quotes cannot change the generated Ruby. https://github.com/ruby/rubygems/commit/9f32631b77 Co-Authored-By: Claude Opus 4.8 (1M context) --- lib/rubygems/installer.rb | 5 +++-- test/rubygems/test_gem_installer.rb | 15 +++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/lib/rubygems/installer.rb b/lib/rubygems/installer.rb index cea02c9fe9f035..7163e356ea2f39 100644 --- a/lib/rubygems/installer.rb +++ b/lib/rubygems/installer.rb @@ -730,6 +730,7 @@ def verify_spec def app_script_text(bin_file_name) # NOTE: that the `load` lines cannot be indented, as old RG versions match # against the beginning of the line + escaped_bin_file_name = bin_file_name.gsub(/[\\']/) {|c| "\\#{c}" } <<~TEXT #{shebang bin_file_name} # @@ -753,9 +754,9 @@ def app_script_text(bin_file_name) end if Gem.respond_to?(:activate_and_load_bin_path) - Gem.activate_and_load_bin_path('#{spec.name}', '#{bin_file_name}', version) + Gem.activate_and_load_bin_path('#{spec.name}', '#{escaped_bin_file_name}', version) else - load Gem.activate_bin_path('#{spec.name}', '#{bin_file_name}', version) + load Gem.activate_bin_path('#{spec.name}', '#{escaped_bin_file_name}', version) end TEXT end diff --git a/test/rubygems/test_gem_installer.rb b/test/rubygems/test_gem_installer.rb index 1cf1ceeb79c756..decd6def175d4e 100644 --- a/test/rubygems/test_gem_installer.rb +++ b/test/rubygems/test_gem_installer.rb @@ -60,6 +60,21 @@ def test_app_script_text end end + def test_app_script_text_escapes_executable_name + installer = setup_base_installer + + malicious = "evil');system('id');#" + @spec.bindir = "bin" + write_file @spec.bin_file(malicious) do |io| + io.puts "#!/usr/bin/ruby" + end + + wrapper = installer.app_script_text malicious + + assert_includes wrapper, %q{Gem.activate_and_load_bin_path('a', 'evil\');system(\'id\');#', version)} + assert_includes wrapper, %q{load Gem.activate_bin_path('a', 'evil\');system(\'id\');#', version)} + end + def test_check_executable_overwrite installer = setup_base_installer From cf90fbfed2b1d345404459b489a9df2115437d58 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Thu, 4 Jun 2026 18:45:18 +0900 Subject: [PATCH 135/188] [ruby/rubygems] Reject non-String executables and bindir with a clear error A non-String executable name or bindir previously raised TypeError from File.basename or File.join. Guard the type so verify_spec raises Gem::InstallError instead of aborting with an unexpected exception. https://github.com/ruby/rubygems/commit/89bf13a11b Co-Authored-By: Claude Opus 4.8 (1M context) --- lib/rubygems/installer.rb | 4 +++- test/rubygems/test_gem_installer.rb | 32 +++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/lib/rubygems/installer.rb b/lib/rubygems/installer.rb index 7163e356ea2f39..a6e1dc4730a617 100644 --- a/lib/rubygems/installer.rb +++ b/lib/rubygems/installer.rb @@ -713,10 +713,12 @@ def verify_spec raise Gem::InstallError, "#{spec} has an invalid dependencies" end - if spec.executables.any? {|name| name != File.basename(name) || /\A\.\.?\z|\R/.match?(name) } + if spec.executables.any? {|name| !name.is_a?(String) || name != File.basename(name) || /\A\.\.?\z|\R/.match?(name) } raise Gem::InstallError, "#{spec} has an invalid executable" end + raise Gem::InstallError, "#{spec} has an invalid bindir" unless spec.bindir.is_a?(String) + expanded_gem_dir = File.expand_path(gem_dir) expanded_bindir = File.expand_path(File.join(gem_dir, spec.bindir)) unless expanded_bindir == expanded_gem_dir || expanded_bindir.start_with?("#{expanded_gem_dir}/") diff --git a/test/rubygems/test_gem_installer.rb b/test/rubygems/test_gem_installer.rb index decd6def175d4e..8947694f53f3bc 100644 --- a/test/rubygems/test_gem_installer.rb +++ b/test/rubygems/test_gem_installer.rb @@ -2013,6 +2013,38 @@ def spec.validate(*args); end end end + def test_pre_install_checks_non_string_executable + spec = util_spec "malicious", "1" + def spec.validate(*args); end + spec.executables = [nil] + + installer = Gem::Installer.for_spec spec + installer.gem_home = @gemhome + + use_ui @ui do + e = assert_raise Gem::InstallError do + installer.pre_install_checks + end + assert_equal "# has an invalid executable", e.message + end + end + + def test_pre_install_checks_non_string_bindir + spec = util_spec "malicious", "1" + def spec.validate(*args); end + spec.bindir = true + + installer = Gem::Installer.for_spec spec + installer.gem_home = @gemhome + + use_ui @ui do + e = assert_raise Gem::InstallError do + installer.pre_install_checks + end + assert_equal "# has an invalid bindir", e.message + end + end + def test_pre_install_checks_malicious_platform_before_eval gem_with_ill_formatted_platform = File.expand_path("packages/ill-formatted-platform-1.0.0.10.gem", __dir__) From a02496ea0ff29f03be2cb136d2f736da619b8ba7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 5 Jun 2026 02:07:37 +0000 Subject: [PATCH 136/188] Bump the github-actions group across 1 directory with 2 updates Bumps the github-actions group with 2 updates in the / directory: [github/codeql-action](https://github.com/github/codeql-action) and [taiki-e/install-action](https://github.com/taiki-e/install-action). Updates `github/codeql-action` from 4.36.1 to 4.36.2 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/87557b9c84dde89fdd9b10e88954ac2f4248e463...8aad20d150bbac5944a9f9d289da16a4b0d87c1e) Updates `taiki-e/install-action` from 2.81.3 to 2.81.5 - [Release notes](https://github.com/taiki-e/install-action/releases) - [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/taiki-e/install-action/compare/25435dc8dd3baed7417e0c96d3fe89013a5b2e09...4bc351f7f2614e48088386e2a0ad917ca3a7e4ba) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 4.36.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions - dependency-name: taiki-e/install-action dependency-version: 2.81.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions ... Signed-off-by: dependabot[bot] --- .github/workflows/check_sast.yml | 6 +++--- .github/workflows/scorecards.yml | 2 +- .github/workflows/zjit-macos.yml | 2 +- .github/workflows/zjit-ubuntu.yml | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/check_sast.yml b/.github/workflows/check_sast.yml index 0b5d6ad1b69c17..c8db1103edd9c1 100644 --- a/.github/workflows/check_sast.yml +++ b/.github/workflows/check_sast.yml @@ -78,14 +78,14 @@ jobs: persist-credentials: false - name: Initialize CodeQL - uses: github/codeql-action/init@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4.36.1 + uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 with: languages: ${{ matrix.language }} build-mode: none config-file: .github/codeql/codeql-config.yml - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4.36.1 + uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 with: category: '/language:${{ matrix.language }}' upload: False @@ -127,7 +127,7 @@ jobs: continue-on-error: true - name: Upload SARIF - uses: github/codeql-action/upload-sarif@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4.36.1 + uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 with: sarif_file: sarif-results/${{ matrix.language }}.sarif continue-on-error: true diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index fdc28c2d09af35..6dc4a7c6ad5a4d 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -73,6 +73,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard (optional). # Commenting out will disable upload of results to your repo's Code Scanning dashboard - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4.36.1 + uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 with: sarif_file: results.sarif diff --git a/.github/workflows/zjit-macos.yml b/.github/workflows/zjit-macos.yml index 09c7c1b6dbe0ef..707e50e36b8028 100644 --- a/.github/workflows/zjit-macos.yml +++ b/.github/workflows/zjit-macos.yml @@ -93,7 +93,7 @@ jobs: rustup install ${{ matrix.rust_version }} --profile minimal rustup default ${{ matrix.rust_version }} - - uses: taiki-e/install-action@25435dc8dd3baed7417e0c96d3fe89013a5b2e09 # v2.81.3 + - uses: taiki-e/install-action@4bc351f7f2614e48088386e2a0ad917ca3a7e4ba # v2.81.5 with: tool: nextest@0.9 if: ${{ matrix.test_task == 'zjit-check' }} diff --git a/.github/workflows/zjit-ubuntu.yml b/.github/workflows/zjit-ubuntu.yml index 7f5ce9322ec9e1..1c3e3f6531633a 100644 --- a/.github/workflows/zjit-ubuntu.yml +++ b/.github/workflows/zjit-ubuntu.yml @@ -119,7 +119,7 @@ jobs: ruby-version: '3.1' bundler: none - - uses: taiki-e/install-action@25435dc8dd3baed7417e0c96d3fe89013a5b2e09 # v2.81.3 + - uses: taiki-e/install-action@4bc351f7f2614e48088386e2a0ad917ca3a7e4ba # v2.81.5 with: tool: nextest@0.9 if: ${{ matrix.test_task == 'zjit-check' }} From b7aa15f594c71a6284f989bd57521dcfba1b7620 Mon Sep 17 00:00:00 2001 From: Mari Imaizumi Date: Thu, 28 May 2026 00:19:59 +0900 Subject: [PATCH 137/188] Improve performance of `String#inspect` with an ASCII bulk-skip fast path Bulk-skip ASCII bytes that need no escaping via a 256-byte lookup table, avoiding per-byte `rb_enc_precise_mbclen` and `rb_enc_mbc_to_codepoint` calls. Eligible for well-formed strings (CR=7BIT, or UTF-8 VALID); other strings fall through to the existing path unchanged. --- benchmark/string_inspect.yml | 13 +++++++++++++ string.c | 27 ++++++++++++++++++++++++++- 2 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 benchmark/string_inspect.yml diff --git a/benchmark/string_inspect.yml b/benchmark/string_inspect.yml new file mode 100644 index 00000000000000..62a884e19d5981 --- /dev/null +++ b/benchmark/string_inspect.yml @@ -0,0 +1,13 @@ +prelude: | + ascii = "Hello, World! This is a benchmark test string." * 100 + utf8 = "こんにちは世界。これはベンチマーク用のテスト文字列です。" * 100 + mixed = ("Hello World! " + "テスト" + " is great! ") * 100 + binary = ("\xE3\x81\x82" * 100).b + escapy = "\n\t\"\\\#" * 100 + +benchmark: + inspect_ascii: ascii.inspect + inspect_utf8: utf8.inspect + inspect_mixed: mixed.inspect + inspect_binary: binary.inspect + inspect_escapy: escapy.inspect diff --git a/string.c b/string.c index 134e1254318f74..63a5114341e754 100644 --- a/string.c +++ b/string.c @@ -7251,6 +7251,21 @@ rb_str_escape(VALUE str) return result; } +/* Lookup table for the inspect fast path. 1 marks bytes that need + * no escaping. 0 marks bytes that need escape inspection: 0x00-0x1F + * (control), 0x22 ("), 0x23 (#), 0x5C (\), 0x7F (DEL), 0x80-0xFF + * (non-ASCII). */ +static const bool inspect_no_escape[256] = { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 0x00-0x0F */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 0x10-0x1F */ + 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, /* 0x20-0x2F */ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, /* 0x30-0x3F */ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, /* 0x40-0x4F */ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, /* 0x50-0x5F */ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, /* 0x60-0x6F */ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, /* 0x70-0x7F */ +}; + /* * call-seq: * inspect -> string @@ -7266,10 +7281,11 @@ rb_str_inspect(VALUE str) rb_encoding *enc = rb_enc_from_index(encidx); const char *p, *pend, *prev; char buf[CHAR_ESC_LEN + 1]; - VALUE result = rb_str_buf_new(0); + VALUE result = rb_str_buf_new(RSTRING_LEN(str) + 2); /* string content + surrounding quotes */ rb_encoding *resenc = rb_default_internal_encoding(); int unicode_p = rb_enc_unicode_p(enc); int asciicompat = rb_enc_asciicompat(enc); + int cr = rb_enc_str_coderange(str); if (resenc == NULL) resenc = rb_default_external_encoding(); if (!rb_enc_asciicompat(resenc)) resenc = rb_usascii_encoding(); @@ -7282,6 +7298,15 @@ rb_str_inspect(VALUE str) unsigned int c, cc; int n; + /* Fast path: bulk-skip runs of safe ASCII bytes via a lookup table. + * Only well-formed strings (CR=7BIT for any encoding, or UTF-8 VALID) + * are eligible. */ + if (cr == ENC_CODERANGE_7BIT || + (encidx == ENCINDEX_UTF_8 && cr == ENC_CODERANGE_VALID)) { + while (p < pend && inspect_no_escape[(unsigned char)*p]) p++; + if (p >= pend) break; + } + n = rb_enc_precise_mbclen(p, pend, enc); if (!MBCLEN_CHARFOUND_P(n)) { if (p > prev) str_buf_cat(result, prev, p - prev); From df002af1040e977f64f25ae3680dab2fe3795bdf Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 5 Jun 2026 06:53:21 +0900 Subject: [PATCH 138/188] [ruby/rubygems] Keep the locked version eligible when it falls inside the cooldown window bundle update and bundle outdated install a >= locked_version prevent-downgrade floor, so resolution never moves a gem backwards. The cooldown filter was excluding that same locked version, making resolution impossible whenever the lockfile was written before cooldown was enabled and still pins an in-cooldown release. Exempt the version sitting exactly at the floor; gems updated explicitly carry an exact = requirement and stay subject to cooldown. https://github.com/ruby/rubygems/issues/9598 https://github.com/ruby/rubygems/commit/b456b2cdf6 Co-Authored-By: Claude Opus 4.8 (1M context) --- lib/bundler/resolver.rb | 16 +++++ spec/bundler/install/cooldown_spec.rb | 85 +++++++++++++++++++++++++++ 2 files changed, 101 insertions(+) diff --git a/lib/bundler/resolver.rb b/lib/bundler/resolver.rb index 422b726980d6fa..753e9987d5b82b 100644 --- a/lib/bundler/resolver.rb +++ b/lib/bundler/resolver.rb @@ -456,11 +456,27 @@ def cooldown_hint(specs) def cooldown_excluded?(spec) return false unless spec.respond_to?(:created_at) && spec.created_at return false unless spec.respond_to?(:remote) && spec.remote + return false if pinned_by_lockfile_floor?(spec) days = spec.remote.effective_cooldown return false if days.nil? || days <= 0 (cooldown_now - spec.created_at) < (days * 86_400) end + # A spec sitting exactly at a `>= locked_version` prevent-downgrade floor is + # the version the lockfile currently pins. `bundle update` and `bundle + # outdated` install that floor so resolution never moves a gem backwards. + # Filtering it out for cooldown would then make resolution impossible + # whenever the locked version is itself inside the cooldown window, which is + # exactly what happens to a lockfile written before cooldown was enabled. + # Keep it eligible; gems being explicitly updated carry an exact `=` + # requirement instead and stay subject to the cooldown filter. + def pinned_by_lockfile_floor?(spec) + return false unless defined?(@base) && @base + requirement = base_requirements[spec.name] + return false unless requirement && !requirement.exact? + requirement.requirements.any? {|op, version| op == ">=" && version == spec.version } + end + def cooldown_now @cooldown_now ||= Time.now end diff --git a/spec/bundler/install/cooldown_spec.rb b/spec/bundler/install/cooldown_spec.rb index b3f57d93ccf5c8..c4bba0fa1d21a5 100644 --- a/spec/bundler/install/cooldown_spec.rb +++ b/spec/bundler/install/cooldown_spec.rb @@ -268,5 +268,90 @@ expect(err).to match(/excluded by the cooldown setting/) expect(err).to match(/--cooldown 0/) end + + it "keeps an in-cooldown locked version on bundle update --all instead of failing" do + # Lockfile written before cooldown was enabled pins the now-in-cooldown + # latest version. A full update must not downgrade below it, and cooldown + # must not filter it out, otherwise resolution becomes impossible (#9598). + gemfile <<-G + source "https://gem.repo3" + gem "ripe_gem" + G + + lockfile <<-L + GEM + remote: https://gem.repo3/ + specs: + ripe_gem (2.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + ripe_gem + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "update --all --cooldown 7", artifice: "compact_index_cooldown" + + expect(the_bundle).to include_gems("ripe_gem 2.0.0") + end + + it "does not fail bundle outdated when the locked version is in cooldown" do + gemfile <<-G + source "https://gem.repo3" + gem "ripe_gem" + G + + lockfile <<-L + GEM + remote: https://gem.repo3/ + specs: + ripe_gem (2.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + ripe_gem + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "outdated --cooldown 7", artifice: "compact_index_cooldown", raise_on_error: false + + # exit 0 means no outdated gems and, crucially, no resolution failure (exit 7) + expect(exitstatus).to eq(0) + end + + it "still applies cooldown and downgrades a gem that is updated explicitly" do + gemfile <<-G + source "https://gem.repo3" + gem "ripe_gem" + G + + lockfile <<-L + GEM + remote: https://gem.repo3/ + specs: + ripe_gem (2.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + ripe_gem + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "update ripe_gem --cooldown 7", artifice: "compact_index_cooldown" + + expect(the_bundle).to include_gems("ripe_gem 1.0.0") + end end end From 8f71ce700b91251b7257051056a958dfdb947cc2 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 5 Jun 2026 09:32:46 +0900 Subject: [PATCH 139/188] [ruby/rubygems] Cover transitive and upgrade paths for in-cooldown locked versions The previous tests only exercised a top-level locked gem. Add a transitive dependency that resolves only through an in-cooldown version, and a case where a cooldown-eligible version above the locked one still gets picked up, so the full update behavior stays pinned down. https://github.com/ruby/rubygems/commit/5deac9f767 Co-Authored-By: Claude Opus 4.8 (1M context) --- spec/bundler/install/cooldown_spec.rb | 76 +++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/spec/bundler/install/cooldown_spec.rb b/spec/bundler/install/cooldown_spec.rb index c4bba0fa1d21a5..bad7b7cf3472d7 100644 --- a/spec/bundler/install/cooldown_spec.rb +++ b/spec/bundler/install/cooldown_spec.rb @@ -87,6 +87,26 @@ build_gem "ripe_gem", "2.0.0" do |s| s.date = now - (1 * 86_400) end + + # parent only resolves with the in-cooldown child 2.0.0 + build_gem "child", "1.0.0" do |s| + s.date = now - (30 * 86_400) + end + build_gem "child", "2.0.0" do |s| + s.date = now - (1 * 86_400) + end + build_gem "parent", "1.0.0" do |s| + s.add_dependency "child", ">= 2.0.0" + s.date = now - (30 * 86_400) + end + + # a cooldown-eligible version exists above the in-cooldown locked one + build_gem "upgradable", "2.0.0" do |s| + s.date = now - (1 * 86_400) + end + build_gem "upgradable", "3.0.0" do |s| + s.date = now - (30 * 86_400) + end end end @@ -353,5 +373,61 @@ expect(the_bundle).to include_gems("ripe_gem 1.0.0") end + + it "keeps an in-cooldown transitive dependency on bundle update --all" do + gemfile <<-G + source "https://gem.repo3" + gem "parent" + G + + lockfile <<-L + GEM + remote: https://gem.repo3/ + specs: + child (2.0.0) + parent (1.0.0) + child (>= 2.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + parent + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "update --all --cooldown 7", artifice: "compact_index_cooldown" + + expect(the_bundle).to include_gems("parent 1.0.0", "child 2.0.0") + end + + it "still upgrades to a cooldown-eligible version above the locked one" do + gemfile <<-G + source "https://gem.repo3" + gem "upgradable" + G + + lockfile <<-L + GEM + remote: https://gem.repo3/ + specs: + upgradable (2.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + upgradable + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "update --all --cooldown 7", artifice: "compact_index_cooldown" + + expect(the_bundle).to include_gems("upgradable 3.0.0") + end end end From b5b951694f96fde490172b4efeef67b5bc928b54 Mon Sep 17 00:00:00 2001 From: BurdetteLamar Date: Thu, 4 Jun 2026 06:54:39 -0500 Subject: [PATCH 140/188] [DOC] Harmonize glob and fnmatch docs --- doc/file/filename_globbing.md | 18 +++++++-------- doc/file/filename_matching.md | 41 ++++++++++++++++------------------- 2 files changed, 27 insertions(+), 32 deletions(-) diff --git a/doc/file/filename_globbing.md b/doc/file/filename_globbing.md index 7981964c5c33c3..ce4549bffee89f 100644 --- a/doc/file/filename_globbing.md +++ b/doc/file/filename_globbing.md @@ -1,22 +1,20 @@ # Filename Globbing -Filename globbing is a pattern-matching feature implemented in certain Ruby methods. - -Filename-globbing methods find filesystem entries (files and directories) -that match certain patterns; -these methods are: +Filename globbing is a pattern-matching feature implemented in certain Ruby methods: - Dir.glob. - [`Dir[]`](https://docs.ruby-lang.org/en/master/Dir.html#method-c-5B-5D). - Pathname.glob. - Pathname#glob. -These methods are quite different from filename-matching methods (not discussed here), -which match patterns against string paths, and do not access the filesystem; -those methods are: +Each `glob` method finds filesystem entries (files and directories) +that match certain patterns. + +These methods are quite different +from [filename-matching](rdoc-ref:filename_matching.md) methods, +which match patterns against string paths, and do not access the filesystem. -- File.fnmatch. -- Pathname#fnmatch. +## Patterns These are the basic elements of filename-globbing patterns; see the sections below for details: diff --git a/doc/file/filename_matching.md b/doc/file/filename_matching.md index 9f13da90126fc6..cf5b60bac29b72 100644 --- a/doc/file/filename_matching.md +++ b/doc/file/filename_matching.md @@ -1,4 +1,4 @@ -## Filename Matching +# Filename Matching Filename matching is a pattern-matching feature implemented in certain Ruby methods: @@ -8,14 +8,11 @@ Filename matching is a pattern-matching feature implemented in certain Ruby meth Each `fnmatch` method matches a pattern against a string _path_; these methods operate only on strings, and do not access the file system. -These are quite different from filename globbing methods (not discussed here), -which match patterns against string paths found in the actual file system: +These methods are quite different +from [filename-globbing](rdoc-ref:filename_globbing.md) methods, +which match patterns against string paths found in the actual file system. -- Dir.glob. -- Pathname.glob. -- Pathname#glob. - -### Patterns +## Patterns These are the basic elements of filename matching patterns; see the sections below for details: @@ -36,7 +33,7 @@ There are two other patterns that are disabled by default: - Alternatives (`'{ , }'`); see [`File::FNM_EXTGLOB`](#constant-filefnmextglob) below. -#### Simple \String +### Simple \String A "simple string" is one that does not contain special filename-matching patterns; see the table above. @@ -78,7 +75,7 @@ File.fnmatch('PROGRAM~1', 'Program Files') # => false It may be enabled by flag [`File::FNM_SHORTNAME`](#constant-filefnmshortname). -#### Any Sequence of Characters (`'*'`) +### Any Sequence of Characters (`'*'`) The asterisk pattern (`'*'`) matches any sequence of characters: @@ -105,7 +102,7 @@ File.fnmatch('*.rb', 'lib/test.rb') # => true That matching may be disabled by flag [`File::FNM_PATHNAME`](#constant-filefnmpathname). -#### Single Character (`'?'`) +### Single Character (`'?'`) The question-mark pattern (`'?'`) matches any single character: @@ -125,7 +122,7 @@ File.fnmatch('foo?boo', 'foo/boo') # => true That matching may be disabled by flag [`File::FNM_PATHNAME`](#constant-filefnmpathname). -#### Single Character from a Set (`'[abc]'`, `'[^abc]'`) +### Single Character from a Set (`'[abc]'`, `'[^abc]'`) Characters enclosed in square brackets define a set of characters, any of which matches a single character: @@ -145,7 +142,7 @@ File.fnmatch('[^ruby]', 'r') # => false File.fnmatch('[^ruby]', 'u') # => false ``` -#### Single Character from a \Range (`'[a-c]'`, `'[^a-c]'`) +### Single Character from a \Range (`'[a-c]'`, `'[^a-c]'`) A range of characters enclosed in square brackets defines a set of characters, any of which matches a single character: @@ -165,7 +162,7 @@ File.fnmatch('[^a-c]', 'b') # => false File.fnmatch('[^a-c]', 'd') # => true ``` -#### Escape (`'\'`) +### Escape (`'\'`) The backslash character (`'\'`) may be used to escape any of the characters that filename matching treats as special: @@ -191,7 +188,7 @@ File.fnmatch('\\\\', '\\') # => true By default escape pattern `'\'` is enabled; it may be disabled by flag [`File::FNM_NOESCAPE`](#constant-filefnmnoescape). -### Flags +## Flags Optional argument `flags` (defaults to `0`) may be the bitwise OR of the constants `File::FNM*`. @@ -210,7 +207,7 @@ see the sections below for details: | [`File::FNM_SYSCASE`](#constant-filefnmsyscase) | Make the pattern use OS's case sensitivity. | -#### Constant File::FNM_CASEFOLD +### Constant File::FNM_CASEFOLD By default, filename matching is case-sensitive; use constant [`File::FNM_CASEFOLD`](#constant-filefnmcasefold) @@ -221,7 +218,7 @@ File.fnmatch('abc', 'ABC') # => false File.fnmatch('abc', 'ABC', File::FNM_CASEFOLD) # => true ``` -#### Constant File::FNM_DOTMATCH +### Constant File::FNM_DOTMATCH By default, filename matching does not allow pattern `'*'` to match a dotfile name (i.e, a filename beginning with a dot); @@ -232,7 +229,7 @@ to enable the match: File.fnmatch('*', '.document') # => false File.fnmatch('*', '.document', File::FNM_DOTMATCH) # => true ``` -#### Constant File::FNM_EXTGLOB +### Constant File::FNM_EXTGLOB By default, filename matching has the alternative notation disabled; use constant [`File::FNM_EXTGLOB`](#constant-filefnmextglob) @@ -261,7 +258,7 @@ File.fnmatch('{*ELLO,?????}', 'hello', File::FNM_EXTGLOB) # => true File.fnmatch('R{ub,foo,bar}y', 'Ruby') # => false ``` -#### Constant File::FNM_NOESCAPE +### Constant File::FNM_NOESCAPE By default filename matching has escaping enabled; use constant [`File::FNM_NOESCAPE`](#constant-filefnmnoescape) @@ -272,7 +269,7 @@ File.fnmatch('\*\?\*\*', '*?**') # => true File.fnmatch('\*\?\*\*', '*?**', File::FNM_NOESCAPE) # => false ``` -#### Constant File::FNM_PATHNAME +### Constant File::FNM_PATHNAME Flag [`File::FNM_PATHNAME`](#constant-filefnmpathname) affects patterns `'**'`, `'*'`, and `'?'`. @@ -316,7 +313,7 @@ File.fnmatch('foo?boo', 'foo/boo') # => true File.fnmatch('foo?boo', 'foo/boo', File::FNM_PATHNAME) # => false ``` -#### Constant File::FNM_SHORTNAME +### Constant File::FNM_SHORTNAME By default, Windows shortname matching is disabled; use constant [`File::FNM_SHORTNAME`](#constant-filefnmshortname) @@ -339,7 +336,7 @@ File.fnmatch('PROGRAM~1', 'Program Files') # => false File.fnmatch('PROGRAM~1', 'Program Files', File::FNM_SHORTNAME) # => true ``` -#### Constant File::FNM_SYSCASE +### Constant File::FNM_SYSCASE By default, filename matching uses Ruby's own case-sensitivity rules; use constant [`File::FNM_SYSCASE`](#constant-filefnmsyscase) From cf37681cd7c77fd4bae9e13f64cbf9a145c900d3 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Thu, 4 Jun 2026 17:08:07 +0200 Subject: [PATCH 141/188] File.expand_path: use `rb_dir_getwd_ospath()` Saves a String allocation and copy, as well as a pair of malloc+free. However it is encoded in ASCII-8BIT, not with FS encoding. Co-Authored-By: John Hawthorn --- file.c | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/file.c b/file.c index c4a531d783ad57..fffd09c22e05a5 100644 --- a/file.c +++ b/file.c @@ -3653,11 +3653,12 @@ has_drive_letter(const char *buf) } #ifndef _WIN32 -static char* +static VALUE getcwdofdrv(int drv) { char drive[4]; - char *drvcwd, *oldcwd; + char *oldcwd; + VALUE drvcwd; drive[0] = drv; drive[1] = ':'; @@ -3669,13 +3670,13 @@ getcwdofdrv(int drv) */ oldcwd = ruby_getcwd(); if (chdir(drive) == 0) { - drvcwd = ruby_getcwd(); + drvcwd = rb_dir_getwd_ospath(); chdir(oldcwd); xfree(oldcwd); } else { /* perhaps the drive is not exist. we return only drive letter */ - drvcwd = strdup(drive); + drvcwd = rb_enc_str_new_cstr(drive, rb_filesystem_encoding()); } return drvcwd; } @@ -4045,16 +4046,19 @@ ospath_new(const char *ptr, long len, rb_encoding *fsenc) } static char * -append_fspath(VALUE result, VALUE fname, char *dir, rb_encoding **enc, rb_encoding *fsenc) +append_fspath(VALUE result, VALUE fname, VALUE dirname, rb_encoding **enc, rb_encoding *fsenc) { - char *buf, *cwdp = dir; - VALUE dirname = Qnil; - size_t dirlen = strlen(dir), buflen = rb_str_capacity(result); + if (RB_UNLIKELY(!rb_enc_asciicompat(fsenc) || rb_enc_str_coderange(dirname) != ENC_CODERANGE_7BIT)) { + dirname = rb_str_new_shared(dirname); + rb_enc_associate(dirname, fsenc); + } + + char *buf, *cwdp; + size_t dirlen = RSTRING_LEN(dirname); + size_t buflen = rb_str_capacity(result); if (NORMALIZE_UTF8PATH || *enc != fsenc) { - dirname = ospath_new(dir, dirlen, fsenc); if (!rb_enc_compatible(fname, dirname)) { - xfree(dir); /* rb_enc_check must raise because the two encodings are not * compatible. */ rb_enc_check(fname, dirname); @@ -4063,19 +4067,15 @@ append_fspath(VALUE result, VALUE fname, char *dir, rb_encoding **enc, rb_encodi rb_encoding *direnc = fs_enc_check(fname, dirname); if (direnc != fsenc) { dirname = rb_str_conv_enc(dirname, fsenc, direnc); - RSTRING_GETMEM(dirname, cwdp, dirlen); - } - else if (NORMALIZE_UTF8PATH) { - RSTRING_GETMEM(dirname, cwdp, dirlen); } *enc = direnc; } + + RSTRING_GETMEM(dirname, cwdp, dirlen); do {buflen *= 2;} while (dirlen > buflen); rb_str_resize(result, buflen); buf = RSTRING_PTR(result); memcpy(buf, cwdp, dirlen); - xfree(dir); - if (!NIL_P(dirname)) rb_str_resize(dirname, 0); rb_enc_associate(result, *enc); return buf + dirlen; } @@ -4177,7 +4177,7 @@ rb_file_expand_path_internal(VALUE fname, VALUE dname, int abs_mode, int long_na p = pend; } else { - char *e = append_fspath(result, fname, ruby_getcwd(), &enc, fsenc); + char *e = append_fspath(result, fname, rb_dir_getwd_ospath(), &enc, fsenc); BUFINIT(); p = e; } From 10f302f5877c53b878ded5698ae18dec8d5d52c4 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Fri, 5 Jun 2026 11:47:05 +0900 Subject: [PATCH 142/188] CI: Fetch vcpkg repository before install for baseline --- .github/workflows/windows.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index b2c84abc6d397e..80a935b30f9ff7 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -96,6 +96,7 @@ jobs: - name: Install libraries with vcpkg run: | + git -C "%VCPKG_INSTALLATION_ROOT%" pull --quiet vcpkg install working-directory: src if: ${{ ! steps.restore-vcpkg.outputs.cache-hit }} From b3257e8732b1f7043dc007b41312d6b4984c3e08 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 5 Jun 2026 02:03:14 +0000 Subject: [PATCH 143/188] Bump github.com/microsoft/vcpkg from master to 2026.06.01 Bumps [github.com/microsoft/vcpkg](https://github.com/microsoft/vcpkg) from master to 2026.06.01. This release includes the previously tagged commit. - [Release notes](https://github.com/microsoft/vcpkg/releases) - [Commits](https://github.com/microsoft/vcpkg/compare/56bb2411609227288b70117ead2c47585ba07713...f3e10653cc27d62a37a3763cd84b38bca07c6075) --- updated-dependencies: - dependency-name: github.com/microsoft/vcpkg dependency-version: 2026.06.01 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- vcpkg.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vcpkg.json b/vcpkg.json index 64d73c40481ccc..c2caad14cddf8a 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -7,5 +7,5 @@ "openssl", "zlib" ], - "builtin-baseline": "56bb2411609227288b70117ead2c47585ba07713" + "builtin-baseline": "f3e10653cc27d62a37a3763cd84b38bca07c6075" } \ No newline at end of file From cfc13d2e89cfebffb60b68eb10a5ffe6172374d3 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Fri, 5 Jun 2026 09:40:39 +0200 Subject: [PATCH 144/188] [ruby/json] Fix memsize function for embedded types We shouldn't report the size of the embedded struct. https://github.com/ruby/json/commit/b1a0891cc3 --- ext/json/generator/generator.c | 4 ++++ ext/json/parser/parser.c | 17 +++++++++++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/ext/json/generator/generator.c b/ext/json/generator/generator.c index 110b5f6b323864..82853633baa9ca 100644 --- a/ext/json/generator/generator.c +++ b/ext/json/generator/generator.c @@ -724,7 +724,11 @@ static void State_compact(void *ptr) static size_t State_memsize(const void *ptr) { +#ifdef HAVE_RUBY_TYPED_EMBEDDABLE + return 0; +#else return sizeof(JSON_Generator_State); +#endif } static const rb_data_type_t JSON_Generator_State_type = { diff --git a/ext/json/parser/parser.c b/ext/json/parser/parser.c index eb7dc9aa8218cf..2d47f31c025332 100644 --- a/ext/json/parser/parser.c +++ b/ext/json/parser/parser.c @@ -268,7 +268,11 @@ static void rvalue_stack_free(void *ptr) static size_t rvalue_stack_memsize(const void *ptr) { const rvalue_stack *stack = (const rvalue_stack *)ptr; - return sizeof(rvalue_stack) + sizeof(VALUE) * stack->capa; + size_t memsize = sizeof(VALUE) * stack->capa; +#ifndef HAVE_RUBY_TYPED_EMBEDDABLE + memsize += sizeof(rvalue_stack); +#endif + return memsize; } static const rb_data_type_t JSON_Parser_rvalue_stack_type = { @@ -452,7 +456,12 @@ static void json_frame_stack_free(void *ptr) static size_t json_frame_stack_memsize(const void *ptr) { const json_frame_stack *stack = (const json_frame_stack *)ptr; - return sizeof(json_frame_stack) + sizeof(json_frame) * stack->capa; + + size_t memsize = sizeof(json_frame) * stack->capa; +#ifndef HAVE_RUBY_TYPED_EMBEDDABLE + memsize += sizeof(json_frame_stack); +#endif + return memsize; } static const rb_data_type_t JSON_Parser_frame_stack_type = { @@ -1961,7 +1970,11 @@ static void JSON_ParserConfig_mark(void *ptr) static size_t JSON_ParserConfig_memsize(const void *ptr) { +#ifdef HAVE_RUBY_TYPED_EMBEDDABLE + return 0; +#else return sizeof(JSON_ParserConfig); +#endif } static const rb_data_type_t JSON_ParserConfig_type = { From 2dd77a0dcd1f634d7787e62d1d735147fd73d99a Mon Sep 17 00:00:00 2001 From: git Date: Fri, 5 Jun 2026 08:09:45 +0000 Subject: [PATCH 145/188] Update bundled gems list as of 2026-06-05 --- NEWS.md | 5 +++-- gems/bundled_gems | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/NEWS.md b/NEWS.md index 501b894c67264e..9ff290ad754c78 100644 --- a/NEWS.md +++ b/NEWS.md @@ -105,8 +105,8 @@ releases. * minitest 6.0.6 * rake 13.4.2 * 13.3.1 to [v13.4.0][rake-v13.4.0], [v13.4.1][rake-v13.4.1], [v13.4.2][rake-v13.4.2] -* test-unit 3.7.7 - * 3.7.5 to [3.7.6][test-unit-3.7.6], [3.7.7][test-unit-3.7.7] +* test-unit 3.7.8 + * 3.7.5 to [3.7.6][test-unit-3.7.6], [3.7.7][test-unit-3.7.7], [3.7.8][test-unit-3.7.8] * net-imap 0.6.4 * 0.6.2 to [v0.6.3][net-imap-v0.6.3], [v0.6.4][net-imap-v0.6.4] * rbs 4.0.2 @@ -242,6 +242,7 @@ A lot of work has gone into making Ractors more stable, performant, and usable. [rake-v13.4.2]: https://github.com/ruby/rake/releases/tag/v13.4.2 [test-unit-3.7.6]: https://github.com/test-unit/test-unit/releases/tag/3.7.6 [test-unit-3.7.7]: https://github.com/test-unit/test-unit/releases/tag/3.7.7 +[test-unit-3.7.8]: https://github.com/test-unit/test-unit/releases/tag/3.7.8 [net-imap-v0.6.3]: https://github.com/ruby/net-imap/releases/tag/v0.6.3 [net-imap-v0.6.4]: https://github.com/ruby/net-imap/releases/tag/v0.6.4 [rbs-v3.10.1]: https://github.com/ruby/rbs/releases/tag/v3.10.1 diff --git a/gems/bundled_gems b/gems/bundled_gems index 8e244af59810c3..14f79ce9ae6f2b 100644 --- a/gems/bundled_gems +++ b/gems/bundled_gems @@ -9,7 +9,7 @@ minitest 6.0.6 https://github.com/minitest/minitest power_assert 3.0.1 https://github.com/ruby/power_assert rake 13.4.2 https://github.com/ruby/rake -test-unit 3.7.7 https://github.com/test-unit/test-unit +test-unit 3.7.8 https://github.com/test-unit/test-unit rexml 3.4.4 https://github.com/ruby/rexml rss 0.3.2 https://github.com/ruby/rss net-imap 0.6.4 https://github.com/ruby/net-imap From 16a08c3b1d4cd5c3e312804e8a557233c6bf7cd0 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Fri, 5 Jun 2026 10:03:44 +0200 Subject: [PATCH 146/188] [ruby/json] Implement GC compaction for JSON_ParserConfig and JSON_Parser_rvalue_stack https://github.com/ruby/json/commit/84fbc08bc9 --- ext/json/parser/parser.c | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/ext/json/parser/parser.c b/ext/json/parser/parser.c index 2d47f31c025332..ca082812e65bd2 100644 --- a/ext/json/parser/parser.c +++ b/ext/json/parser/parser.c @@ -243,7 +243,7 @@ static void rvalue_stack_mark(void *ptr) long index; if (stack && stack->ptr) { for (index = 0; index < stack->head; index++) { - rb_gc_mark(stack->ptr[index]); + rb_gc_mark_movable(stack->ptr[index]); } } } @@ -275,12 +275,24 @@ static size_t rvalue_stack_memsize(const void *ptr) return memsize; } +static void rvalue_stack_compact(void *ptr) +{ + rvalue_stack *stack = (rvalue_stack *)ptr; + long index; + if (stack && stack->ptr) { + for (index = 0; index < stack->head; index++) { + stack->ptr[index] = rb_gc_location(stack->ptr[index]); + } + } +} + static const rb_data_type_t JSON_Parser_rvalue_stack_type = { .wrap_struct_name = "JSON::Ext::Parser/rvalue_stack", .function = { .dmark = rvalue_stack_mark, .dfree = rvalue_stack_free, .dsize = rvalue_stack_memsize, + .dcompact = rvalue_stack_compact, }, .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_EMBEDDABLE, }; @@ -1964,8 +1976,8 @@ static VALUE cParser_m_parse(VALUE klass, VALUE Vsource, VALUE opts) static void JSON_ParserConfig_mark(void *ptr) { JSON_ParserConfig *config = ptr; - rb_gc_mark(config->on_load_proc); - rb_gc_mark(config->decimal_class); + rb_gc_mark_movable(config->on_load_proc); + rb_gc_mark_movable(config->decimal_class); } static size_t JSON_ParserConfig_memsize(const void *ptr) @@ -1977,12 +1989,20 @@ static size_t JSON_ParserConfig_memsize(const void *ptr) #endif } +static void JSON_ParserConfig_compact(void *ptr) +{ + JSON_ParserConfig *config = ptr; + config->on_load_proc = rb_gc_location(config->on_load_proc); + config->decimal_class = rb_gc_location(config->decimal_class); +} + static const rb_data_type_t JSON_ParserConfig_type = { .wrap_struct_name = "JSON::Ext::Parser/ParserConfig", .function = { - JSON_ParserConfig_mark, - RUBY_DEFAULT_FREE, - JSON_ParserConfig_memsize, + .dmark = JSON_ParserConfig_mark, + .dfree = RUBY_DEFAULT_FREE, + .dsize = JSON_ParserConfig_memsize, + .dcompact = JSON_ParserConfig_compact, }, .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED | RUBY_TYPED_FROZEN_SHAREABLE | RUBY_TYPED_EMBEDDABLE, }; From c78418b7a04cf6c9537ef944a3a0150829aba4a1 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Fri, 5 Jun 2026 16:55:57 +0900 Subject: [PATCH 147/188] CI: Allow dependabot to save the built vcpkg cache --- .github/workflows/windows.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 80a935b30f9ff7..03e75ad445ad9c 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -95,6 +95,7 @@ jobs: key: windows-${{ matrix.os }}-vcpkg-${{ hashFiles('src/vcpkg.json') }} - name: Install libraries with vcpkg + id: build-vcpkg run: | git -C "%VCPKG_INSTALLATION_ROOT%" pull --quiet vcpkg install @@ -106,7 +107,13 @@ jobs: with: path: src\vcpkg_installed key: windows-${{ matrix.os }}-vcpkg-${{ hashFiles('src/vcpkg.json') }} - if: ${{ ! steps.restore-vcpkg.outputs.cache-hit && (github.ref_name == 'master' || startsWith(github.ref_name, 'ruby_')) }} + if: >- + steps.build-vcpkg.outcome == 'success' && + ( github.ref_name == 'master' + || startsWith(github.ref_name, 'ruby_') + || ( github.event.pull_request.user.login == 'dependabot[bot]' + && startsWith(github.head_ref || github.ref_name, 'dependabot/vcpkg')) + ) - name: setup env # Available Ruby versions: https://github.com/actions/runner-images/blob/main/images/windows/Windows2019-Readme.md#ruby From 2a5394cee70570f722ed063d5db59f2bf7d4835d Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Sun, 11 Jul 2021 15:19:30 +0900 Subject: [PATCH 148/188] Dump FreeBSD socket credential data wholely --- ext/socket/ancdata.c | 69 ++++++++++++++++++++++--------------- test/socket/test_ancdata.rb | 18 ++++++++++ 2 files changed, 60 insertions(+), 27 deletions(-) diff --git a/ext/socket/ancdata.c b/ext/socket/ancdata.c index f1e9e425248d3e..0e17d9e87383ce 100644 --- a/ext/socket/ancdata.c +++ b/ext/socket/ancdata.c @@ -711,6 +711,9 @@ anc_inspect_passcred_credentials(int level, int type, VALUE data, VALUE ret) static int anc_inspect_socket_creds(int level, int type, VALUE data, VALUE ret) { + long len; + const char *ptr; + if (level != SOL_SOCKET && type != SCM_CREDS) return 0; @@ -725,48 +728,59 @@ anc_inspect_socket_creds(int level, int type, VALUE data, VALUE ret) * This heuristics works well except when sc_ngroups == CMGROUP_MAX. */ + RSTRING_GETMEM(data, ptr, len); #if defined(HAVE_TYPE_STRUCT_CMSGCRED) /* FreeBSD */ - if (RSTRING_LEN(data) == sizeof(struct cmsgcred)) { + if (len == sizeof(struct cmsgcred)) { struct cmsgcred cred; - memcpy(&cred, RSTRING_PTR(data), sizeof(struct cmsgcred)); + int ngroups; + memcpy(&cred, ptr, sizeof(struct cmsgcred)); rb_str_catf(ret, " pid=%u", cred.cmcred_pid); rb_str_catf(ret, " uid=%u", cred.cmcred_uid); rb_str_catf(ret, " euid=%u", cred.cmcred_euid); rb_str_catf(ret, " gid=%u", cred.cmcred_gid); - if (cred.cmcred_ngroups) { + rb_str_catf(ret, " groups[%d]=[", cred.cmcred_ngroups); + ngroups = cred.cmcred_ngroups; + if (ngroups > 0) { int i; - const char *sep = " groups="; - for (i = 0; i < cred.cmcred_ngroups; i++) { - rb_str_catf(ret, "%s%u", sep, cred.cmcred_groups[i]); - sep = ","; + rb_str_catf(ret, "%u", cred.cmcred_groups[0]); + if (ngroups > CMGROUP_MAX) ngroups = CMGROUP_MAX; + for (i = 1; i < ngroups; i++) { + rb_str_catf(ret, ",%u", cred.cmcred_groups[i]); } } - rb_str_cat2(ret, " (cmsgcred)"); + rb_str_cat2(ret, "] (cmsgcred)"); return 1; } #endif #if defined(HAVE_TYPE_STRUCT_SOCKCRED) /* FreeBSD, NetBSD */ - if ((size_t)RSTRING_LEN(data) >= SOCKCREDSIZE(0)) { - struct sockcred cred0, *cred; - memcpy(&cred0, RSTRING_PTR(data), SOCKCREDSIZE(0)); - if ((size_t)RSTRING_LEN(data) == SOCKCREDSIZE(cred0.sc_ngroups)) { - cred = (struct sockcred *)ALLOCA_N(char, SOCKCREDSIZE(cred0.sc_ngroups)); - memcpy(cred, RSTRING_PTR(data), SOCKCREDSIZE(cred0.sc_ngroups)); - rb_str_catf(ret, " uid=%u", cred->sc_uid); - rb_str_catf(ret, " euid=%u", cred->sc_euid); - rb_str_catf(ret, " gid=%u", cred->sc_gid); - rb_str_catf(ret, " egid=%u", cred->sc_egid); - if (cred0.sc_ngroups) { - int i; - const char *sep = " groups="; - for (i = 0; i < cred0.sc_ngroups; i++) { - rb_str_catf(ret, "%s%u", sep, cred->sc_groups[i]); - sep = ","; - } + if ((size_t)len >= SOCKCREDSIZE(0)) { + struct sockcred cred; + int ngroups; + memcpy(&cred, ptr, SOCKCREDSIZE(0)); + rb_str_catf(ret, " uid=%u", cred.sc_uid); + rb_str_catf(ret, " euid=%u", cred.sc_euid); + rb_str_catf(ret, " gid=%u", cred.sc_gid); + rb_str_catf(ret, " egid=%u", cred.sc_egid); + rb_str_catf(ret, " groups[%d]=[", cred.sc_ngroups); + ngroups = cred.sc_ngroups; + if (ngroups <= 0) { + ngroups = 0; + } + else { + size_t max = ((size_t)len - SOCKCREDSIZE(0)) / sizeof(gid_t); + if ((size_t)ngroups > max) ngroups = (int)max; + } + if (ngroups > 0) { + int i; + const void *gp = ptr + offsetof(struct sockcred, sc_groups); + const gid_t *groups = MEMCPY(ALLOCA_N(gid_t, ngroups), gp, gid_t, ngroups); + rb_str_catf(ret, "%u", groups[0]); + for (i = 1; i < ngroups; i++) { + rb_str_catf(ret, ",%u", groups[i]); } - rb_str_cat2(ret, " (sockcred)"); - return 1; } + rb_str_cat2(ret, "] (sockcred)"); + return 1; } #endif return 0; @@ -1000,6 +1014,7 @@ ancillary_inspect(VALUE self) rb_str_catf(ret, " %"PRIsVALUE, rb_sym2str(vtype)); else rb_str_catf(ret, " cmsg_type:%d", type); + RB_GC_GUARD(vtype); } else { rb_str_catf(ret, " cmsg_level:%d", level); diff --git a/test/socket/test_ancdata.rb b/test/socket/test_ancdata.rb index b2f86a0bb1e076..387be6080ff15c 100644 --- a/test/socket/test_ancdata.rb +++ b/test/socket/test_ancdata.rb @@ -65,4 +65,22 @@ def test_unix_rights } end end + + if /freebsd/ =~ RUBY_PLATFORM + def test_cmsgcred_inspect + cred = [0, 0, 0, 0, 9999, *Array.new(16, 0)].pack('L4I!L*') + s = Socket::AncillaryData.new(:UNIX, :SOCKET, :SCM_CREDS, cred).inspect + assert_include(s, 'groups[9999]') + assert_include(s, '(cmsgcred)') + end + end + + if /netbsd|freebsd/ =~ RUBY_PLATFORM + def test_sockcred_inspect + cred = [0, 0, 0, 0, 9999, 0].pack('L4S!L') + s = Socket::AncillaryData.new(:UNIX, :SOCKET, :SCM_CREDS, cred).inspect + assert_include(s, 'groups[9999]') + assert_include(s, '(sockcred)') + end + end end if defined? Socket::AncillaryData From 24ce2563d771301e3b52c82c97f6a76fc6cfa3d4 Mon Sep 17 00:00:00 2001 From: Scott Myron Date: Fri, 5 Jun 2026 09:04:22 -0500 Subject: [PATCH 149/188] [ruby/json] Reorder the json_frame_type and json_frame_phase enum to simplify the transition from a JSON_PHASE_VALUE to the next phase. https://github.com/ruby/json/commit/887274e642 --- ext/json/parser/parser.c | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/ext/json/parser/parser.c b/ext/json/parser/parser.c index ca082812e65bd2..b820066c596492 100644 --- a/ext/json/parser/parser.c +++ b/ext/json/parser/parser.c @@ -345,22 +345,25 @@ static void rvalue_stack_eagerly_release(VALUE handle) #define JSON_FRAME_STACK_INITIAL_CAPA 32 enum json_frame_type { - JSON_FRAME_ROOT, - JSON_FRAME_ARRAY, - JSON_FRAME_OBJECT, + JSON_FRAME_ROOT, // == JSON_PHASE_DONE + JSON_FRAME_ARRAY, // == JSON_PHASE_ARRAY_COMMA + JSON_FRAME_OBJECT, // = JSON_PHASE_OBJECT_COMMA }; // Where a frame is within its container's grammar. This is the entirety of the // parser's "what to do next" state: json_parse_any dispatches on the top // frame's phase and holds no resume state in C locals, so a parse can stop at // any value boundary and be resumed purely from the (persistable) frame stack. +// +// The first three phases are deliberately equal to the corresponding json_frame_type +// to simplify the transition of phase in json_value_completed. enum json_frame_phase { + JSON_PHASE_DONE = JSON_FRAME_ROOT, // root only: the document value has been parsed + JSON_PHASE_ARRAY_COMMA = JSON_FRAME_ARRAY, // after a value: expecting ',' or the closing ']' + JSON_PHASE_OBJECT_COMMA = JSON_FRAME_OBJECT, // after a value: expecting ',' or the closing '}' JSON_PHASE_VALUE, // expecting a value (document root, array element, or object value after ':') - JSON_PHASE_ARRAY_COMMA, // after a value: expecting ',' or the closing ']' JSON_PHASE_OBJECT_KEY, // expecting a '"' key (after '{' or ',') - JSON_PHASE_OBJECT_COMMA, // after a value: expecting ',' or the closing '}' JSON_PHASE_OBJECT_COLON, // object only: after a key, expecting ':' - JSON_PHASE_DONE, // root only: the document value has been parsed }; typedef struct json_frame_struct { @@ -1442,18 +1445,11 @@ static inline long json_frame_entry_count(const json_frame *frame, const rvalue_ // after a container close is the freshly re-exposed parent. static inline void json_value_completed(json_frame *frame) { - switch (frame->type) { - case JSON_FRAME_ROOT: - frame->phase = JSON_PHASE_DONE; - return; - case JSON_FRAME_ARRAY: - frame->phase = JSON_PHASE_ARRAY_COMMA; - return; - case JSON_FRAME_OBJECT: - frame->phase = JSON_PHASE_OBJECT_COMMA; - return; - } - UNREACHABLE; + JSON_ASSERT((int)JSON_PHASE_DONE == (int)JSON_FRAME_ROOT); + JSON_ASSERT((int)JSON_PHASE_ARRAY_COMMA == (int)JSON_FRAME_ARRAY); + JSON_ASSERT((int)JSON_PHASE_OBJECT_COMMA == (int)JSON_FRAME_OBJECT); + + frame->phase = (enum json_frame_phase) frame->type; } // Parse an arbitrary JSON value iteratively. This is a state machine driven From dba570e575ff67096b44b60831a34a7eac79d7c1 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Fri, 5 Jun 2026 18:23:15 +0200 Subject: [PATCH 150/188] [ruby/json] parser.c: Extract json_match_keyword Extracted from: https://github.com/ruby/json/pull/994 Modern compilers shouldn't have problem computing `strlen` at compile time and generating the same code. https://github.com/ruby/json/commit/b07f74bd73 --- ext/json/parser/parser.c | 52 +++++++++++++++++-------------- test/json/json_ext_parser_test.rb | 2 +- 2 files changed, 29 insertions(+), 25 deletions(-) diff --git a/ext/json/parser/parser.c b/ext/json/parser/parser.c index b820066c596492..a61f997a112db1 100644 --- a/ext/json/parser/parser.c +++ b/ext/json/parser/parser.c @@ -1425,9 +1425,7 @@ static inline VALUE json_parse_positive_number(JSON_ParserState *state, JSON_Par static inline VALUE json_parse_negative_number(JSON_ParserState *state, JSON_ParserConfig *config) { - const char *start = state->cursor; - state->cursor++; - return json_parse_number(state, config, true, start); + return json_parse_number(state, config, true, state->cursor - 1); } // How many values (array elements, or interleaved object keys+values) have been @@ -1452,6 +1450,22 @@ static inline void json_value_completed(json_frame *frame) frame->phase = (enum json_frame_phase) frame->type; } +static inline bool json_match_keyword(JSON_ParserState *state, const char *keyword, size_t offset) +{ + // It is assumed that since `keyword` is always a literal, the compiler is able to constantize this + // `strlen` and several other computations in that routine, such as eliminating the `if (resumable)` branch. + + size_t len = strlen(keyword); + + // Note: memcmp with a small power of two and a literal string compile to an integer comparison / + // That's why we sometime compare starting from the first byte and sometimes from the second. + if (rest(state) >= len && (memcmp(state->cursor + offset, keyword + offset, len - offset) == 0)) { + state->cursor += len; + return true; + } + return false; +} + // Parse an arbitrary JSON value iteratively. This is a state machine driven // entirely by the top frame's phase so it can stop at any value boundary and // resume purely from the frame stack. A JSON_FRAME_ROOT frame sits at the @@ -1475,8 +1489,7 @@ static VALUE json_parse_any(JSON_ParserState *state, JSON_ParserConfig *config) switch (peek(state)) { case 'n': - if (rest(state) >= 4 && (memcmp(state->cursor, "null", 4) == 0)) { - state->cursor += 4; + if (json_match_keyword(state, "null", 0)) { json_push_value(state, config, Qnil); json_value_completed(frame); break; @@ -1484,8 +1497,7 @@ static VALUE json_parse_any(JSON_ParserState *state, JSON_ParserConfig *config) raise_parse_error("unexpected token %s", state); case 't': - if (rest(state) >= 4 && (memcmp(state->cursor, "true", 4) == 0)) { - state->cursor += 4; + if (json_match_keyword(state, "true", 0)) { json_push_value(state, config, Qtrue); json_value_completed(frame); break; @@ -1493,9 +1505,7 @@ static VALUE json_parse_any(JSON_ParserState *state, JSON_ParserConfig *config) raise_parse_error("unexpected token %s", state); case 'f': - // Note: memcmp with a small power of two compile to an integer comparison - if (rest(state) >= 5 && (memcmp(state->cursor + 1, "alse", 4) == 0)) { - state->cursor += 5; + if (json_match_keyword(state, "false", 1)) { json_push_value(state, config, Qfalse); json_value_completed(frame); break; @@ -1504,8 +1514,7 @@ static VALUE json_parse_any(JSON_ParserState *state, JSON_ParserConfig *config) raise_parse_error("unexpected token %s", state); case 'N': // Note: memcmp with a small power of two compile to an integer comparison - if (config->allow_nan && rest(state) >= 3 && (memcmp(state->cursor + 1, "aN", 2) == 0)) { - state->cursor += 3; + if (config->allow_nan && json_match_keyword(state, "NaN", 1)) { json_push_value(state, config, CNaN); json_value_completed(frame); break; @@ -1513,8 +1522,7 @@ static VALUE json_parse_any(JSON_ParserState *state, JSON_ParserConfig *config) raise_parse_error("unexpected token %s", state); case 'I': - if (config->allow_nan && rest(state) >= 8 && (memcmp(state->cursor, "Infinity", 8) == 0)) { - state->cursor += 8; + if (config->allow_nan && json_match_keyword(state, "Infinity", 0)) { json_push_value(state, config, CInfinity); json_value_completed(frame); break; @@ -1522,17 +1530,13 @@ static VALUE json_parse_any(JSON_ParserState *state, JSON_ParserConfig *config) raise_parse_error("unexpected token %s", state); case '-': { - // Note: memcmp with a small power of two compile to an integer comparison - if (rest(state) >= 9 && (memcmp(state->cursor + 1, "Infinity", 8) == 0)) { - if (config->allow_nan) { - state->cursor += 9; - json_push_value(state, config, CMinusInfinity); - json_value_completed(frame); - break; - } else { - raise_parse_error("unexpected token %s", state); - } + state->cursor++; + if (config->allow_nan && json_match_keyword(state, "Infinity", 0)) { + json_push_value(state, config, CMinusInfinity); + json_value_completed(frame); + break; } + json_push_value(state, config, json_parse_negative_number(state, config)); json_value_completed(frame); break; diff --git a/test/json/json_ext_parser_test.rb b/test/json/json_ext_parser_test.rb index e610f642f199a6..d585b8d0dcd4ae 100644 --- a/test/json/json_ext_parser_test.rb +++ b/test/json/json_ext_parser_test.rb @@ -26,7 +26,7 @@ def test_error_messages ex = assert_raise(ParserError) { parse('-Infinity something') } unless RUBY_PLATFORM =~ /java/ - assert_equal "unexpected token '-Infinity' at line 1 column 1", ex.message + assert_equal "invalid number: '-Infinity' at line 1 column 1", ex.message end ex = assert_raise(ParserError) { parse('NaN something') } From a2433947b767b7387bd70c71221df4e22f38985e Mon Sep 17 00:00:00 2001 From: Kevin Menard Date: Fri, 5 Jun 2026 12:39:52 -0400 Subject: [PATCH 151/188] ZJIT: Avoid type checker mismatch when forwarding `LoadField` (GH-17185) `LoadField` has an associated `return_type` field that's set depending on the type of load. When an object's shape transitions its storage strategy (embedded->heap), the return type may change (e.g., `BasicObject` to `CPtr`). If that shape layout transition is sandwiched between two `LoadField` inside the same block, it's possible we will see two different return types when reading from the same offset of the same object. Load-store forwarding will rightfully eliminate the second `LoadField`. However, had it not been removed, we'd have two resulting bit-for-bit equal values with two different associated types. That confuses the type checker and so semantically valid code will panic with a `MismatchedOperandType`. I ran across this while working on GH-16966. I've pulled it out here to make review easier and because the fix doesn't touch any inlining code. We haven't run into this thus far because a same-block shape storage transition emits a `SetIvar`, which has a broad enough effect that load-store forwarding reset its cache. If anything along that chain changes then we could get into this state without inlining. It's a lot to keep track of so I think we should tackle this broadly. I chatted with @tekknolagi about the general problem. A simple or obvious solution did not present itself. This PR does not fix the broader problem; it temporarily patches over the `MismatchedOperandType` by preserving duplicate loads if the return value would be different. This is not ideal and will need to be reverted when a comprehensive solution is implemented. But, it's enough to satisfy the type checker. --- zjit/src/hir.rs | 99 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 94 insertions(+), 5 deletions(-) diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 82e50cdd36c817..805503b82bd2d6 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -4987,14 +4987,28 @@ impl Function { compile_time_heap.insert(key, val); insn_id }, - Insn::LoadField { recv, offset, .. } => { + Insn::LoadField { recv, offset, return_type, .. } => { let key = (self.chase_insn(recv), offset); match compile_time_heap.entry(key) { std::collections::hash_map::Entry::Occupied(entry) => { - // If the value is stored already, we should short circuit. - // However, we need to replace insn_id with its representative in the SSA union. - self.make_equal_to(insn_id, *entry.get()); - continue + let cached_insn = *entry.get(); + + // TODO (nirvdrum 2026-06-04): Remove the return type guard and supporting code when the type checker becomes more accurate. + // If there's an an embedded<=>heap shape storage transition, it's possible for this `LoadField` to have a different return + // type than the cached entry (`CPtr` vs `BasicObject`). While the loaded value would be the same in either case, the + // difference in associated type causes type checking to fail. Consequently, we conservatively retain the duplicate `LoadField`. + // The `optimize_load_store_does_not_alias_loads_with_incompatible_return_types` test checks the problematic case. + let can_forward_cached_insn = match self.find(cached_insn) { + Insn::LoadField { return_type : cached_return_type,.. } => cached_return_type.is_subtype(return_type), + _ => true + }; + + if can_forward_cached_insn { + // If the value is stored already, we should short circuit. + // However, we need to replace insn_id with its representative in the SSA union. + self.make_equal_to(insn_id, cached_insn); + continue + } } std::collections::hash_map::Entry::Vacant(_) => { // If the value has not been accessed, cache a copy to optimize future loads or stores. @@ -9307,6 +9321,81 @@ mod validation_tests { function.seal_entries(); assert_matches_err(function.validate(), ValidationError::DuplicateInstruction(exit, val)); } + + // The heap-fields pointer (`as_heap`, a CPtr) and the first embedded + // instance variable both live at ROBJECT_OFFSET_AS_HEAP_FIELDS == + // ROBJECT_OFFSET_AS_ARY == 0x10 on a Ruby object. They are distinct fields + // with incompatible value types that happen to share a base and an offset. + // Since we could end up with two `LoadField` on different shape types + // (e.g., as the result of inlining), `optimize_load_store` must not satisfy + // one load from another cached load with a different return type. The fault + // surfaces here as the forwarded value flowing into a `Return` with the + // wrong type (`CPtr` rather than `BasicObject`). + #[test] + fn optimize_load_store_does_not_alias_loads_with_incompatible_return_types() { + assert_eq!(ROBJECT_OFFSET_AS_HEAP_FIELDS, ROBJECT_OFFSET_AS_ARY, + "Conflicting field offsets changed, rendering the rest of this test incorrect"); + + let mut function = Function::new(std::ptr::null()); + let entry = function.entry_block; + let recv = function.push_insn(entry, Insn::Const { val: Const::Value(Qnil) }); + function.push_insn(entry, Insn::LoadField { + recv, + id: FieldName::as_heap, + offset: ROBJECT_OFFSET_AS_HEAP_FIELDS as i32, + return_type: types::CPtr, + }); + let ivar = function.push_insn(entry, Insn::LoadField { + recv, + id: FieldName::Id(ID(1)), + offset: ROBJECT_OFFSET_AS_ARY as i32, + return_type: types::BasicObject, + }); + function.push_insn(entry, Insn::Return { val: ivar }); + function.seal_entries(); + + function.infer_types(); + function.optimize_load_store(); + + assert!( + function.validate().is_ok(), + "optimize_load_store aliased two loads with different return types: {:?}", + function.validate(), + ); + } + + #[test] + fn optimize_load_store_does_not_alias_loads_with_compatible_return_types() { + assert_eq!(ROBJECT_OFFSET_AS_HEAP_FIELDS, ROBJECT_OFFSET_AS_ARY, + "Conflicting field offsets changed, rendering the rest of this test incorrect"); + + let mut function = Function::new(std::ptr::null()); + let entry = function.entry_block; + let recv = function.push_insn(entry, Insn::Const { val: Const::Value(Qnil) }); + function.push_insn(entry, Insn::LoadField { + recv, + id: FieldName::as_heap, + offset: ROBJECT_OFFSET_AS_HEAP_FIELDS as i32, + return_type: types::BasicObject, + }); + let ivar = function.push_insn(entry, Insn::LoadField { + recv, + id: FieldName::Id(ID(1)), + offset: ROBJECT_OFFSET_AS_ARY as i32, + return_type: types::Array, + }); + function.push_insn(entry, Insn::Return { val: ivar }); + function.seal_entries(); + + function.infer_types(); + function.optimize_load_store(); + + assert!( + function.validate().is_ok(), + "optimize_load_store failed to alias two loads with different, but compatible, return types: {:?}", + function.validate(), + ); + } } #[cfg(test)] From dd9213c41de9447383708aed32d64211f0697506 Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Wed, 3 Jun 2026 17:39:07 -0700 Subject: [PATCH 152/188] Convert object_tracing to use weak references Object tracing listens to the NEWOBJ hook to see all objects allocated while it is active. Previously it also enabled a FREEOBJ tracepoint to drop each object's record as the object was freed. However the FREEOBJ tracepoint only fires while tracing is active. An object recorded during tracing can be freed after tracing stops, and that free was missed, leaving a stale record in object_table keyed by a freed object. These dangling keys are unsafe during compaction, which was previously mitigated with rb_gc_pointer_to_heap_p (which I'd like to stop using). These dangling keys could also become incorrectly associated with a new object allocated in its place. Instead we can declare object_table's keys as weak references. The GC then invokes our weak reference callback on every collection, whether or not tracing is running, letting us reap the records of objects about to be freed. This callback runs before the FREEOBJ hook, which makes the FREEOBJ tracepoint unnecessary. --- ext/objspace/object_tracing.c | 92 ++++++++++++++++------------------ internal/gc.h | 5 +- test/objspace/test_objspace.rb | 15 ++++++ 3 files changed, 60 insertions(+), 52 deletions(-) diff --git a/ext/objspace/object_tracing.c b/ext/objspace/object_tracing.c index 1c18bf02eeaf0b..74d793a6e232c1 100644 --- a/ext/objspace/object_tracing.c +++ b/ext/objspace/object_tracing.c @@ -22,7 +22,6 @@ struct traceobj_arg { int running; int keep_remains; VALUE newobj_trace; - VALUE freeobj_trace; st_table *object_table; /* obj (VALUE) -> allocation_info */ st_table *str_table; /* cstr -> refcount */ struct traceobj_arg *prev_traceobj_arg; @@ -96,13 +95,11 @@ newobj_i(VALUE tpval, void *data) st_data_t v; if (st_lookup(arg->object_table, (st_data_t)obj, &v)) { + /* keep_remains kept this slot's entry after its object was freed. The + * allocator has now reused that address, so recycle the dead entry's + * info. A living entry here would mean two live objects at one address. */ info = (struct allocation_info *)v; - if (arg->keep_remains) { - if (info->living) { - /* do nothing. there is possibility to keep living if FREEOBJ events while suppressing tracing */ - } - } - /* reuse info */ + assert(!info->living); delete_unique_str(arg->str_table, info->path); delete_unique_str(arg->str_table, info->class_path); } @@ -121,37 +118,6 @@ newobj_i(VALUE tpval, void *data) st_insert(arg->object_table, (st_data_t)obj, (st_data_t)info); } -static void -freeobj_i(VALUE tpval, void *data) -{ - struct traceobj_arg *arg = (struct traceobj_arg *)data; - rb_trace_arg_t *tparg = rb_tracearg_from_tracepoint(tpval); - st_data_t obj = (st_data_t)rb_tracearg_object(tparg); - st_data_t v; - struct allocation_info *info; - - /* Modifying the st table can cause allocations, which can trigger GC. - * Since freeobj_i is called during GC, it must not trigger another GC. */ - VALUE gc_disabled = rb_gc_disable_no_rest(); - - if (arg->keep_remains) { - if (st_lookup(arg->object_table, obj, &v)) { - info = (struct allocation_info *)v; - info->living = 0; - } - } - else { - if (st_delete(arg->object_table, &obj, &v)) { - info = (struct allocation_info *)v; - delete_unique_str(arg->str_table, info->path); - delete_unique_str(arg->str_table, info->class_path); - ruby_xfree(info); - } - } - - if (gc_disabled == Qfalse) rb_gc_enable(); -} - static int free_keys_i(st_data_t key, st_data_t value, st_data_t data) { @@ -171,7 +137,6 @@ allocation_info_tracer_mark(void *ptr) { struct traceobj_arg *trace_arg = (struct traceobj_arg *)ptr; rb_gc_mark(trace_arg->newobj_trace); - rb_gc_mark(trace_arg->freeobj_trace); } static void @@ -197,15 +162,47 @@ allocation_info_tracer_memsize(const void *ptr) return size; } +static int +allocation_info_tracer_weak_reference_i(st_data_t key, st_data_t value, st_data_t data) +{ + struct traceobj_arg *arg = (struct traceobj_arg *)data; + struct allocation_info *info = (struct allocation_info *)value; + + if (rb_gc_handle_weak_references_alive_p((VALUE)key)) { + return ST_CONTINUE; + } + + /* Object was collected. keep_remains keeps the dead entry for reporting. */ + if (arg->keep_remains) { + info->living = 0; + return ST_CONTINUE; + } + else { + delete_unique_str(arg->str_table, info->path); + delete_unique_str(arg->str_table, info->class_path); + ruby_xfree(info); + return ST_DELETE; + } +} + +static void +allocation_info_tracer_weak_reference(void *ptr) +{ + struct traceobj_arg *arg = (struct traceobj_arg *)ptr; + + st_foreach(arg->object_table, allocation_info_tracer_weak_reference_i, (st_data_t)arg); +} + static int allocation_info_tracer_compact_update_object_table_i(st_data_t key, st_data_t value, st_data_t data) { st_table *table = (st_table *)data; + struct allocation_info *info = (struct allocation_info *)value; - if (!rb_gc_pointer_to_heap_p(key)) { - struct allocation_info *info = (struct allocation_info *)value; - xfree(info); - return ST_DELETE; + /* In keep_remains mode the table keeps entries for freed objects. Their keys + * are dangling, so skip them instead of passing them to rb_gc_location. */ + if (!info->living) { + return ST_CONTINUE; } if (key != rb_gc_location(key)) { @@ -242,6 +239,7 @@ static const rb_data_type_t allocation_info_tracer_type = { allocation_info_tracer_free, /* Never called because global */ allocation_info_tracer_memsize, allocation_info_tracer_compact, + allocation_info_tracer_weak_reference, }, 0, 0, RUBY_TYPED_FREE_IMMEDIATELY }; @@ -260,9 +258,10 @@ get_traceobj_arg(void) tmp_trace_arg->running = 0; tmp_trace_arg->keep_remains = tmp_keep_remains; tmp_trace_arg->newobj_trace = 0; - tmp_trace_arg->freeobj_trace = 0; tmp_trace_arg->object_table = st_init_numtable(); tmp_trace_arg->str_table = st_init_strtable(); + + rb_gc_declare_weak_references(obj); } return tmp_trace_arg; } @@ -284,10 +283,8 @@ trace_object_allocations_start(VALUE self) else { if (arg->newobj_trace == 0) { arg->newobj_trace = rb_tracepoint_new(0, RUBY_INTERNAL_EVENT_NEWOBJ, newobj_i, arg); - arg->freeobj_trace = rb_tracepoint_new(0, RUBY_INTERNAL_EVENT_FREEOBJ, freeobj_i, arg); } rb_tracepoint_enable(arg->newobj_trace); - rb_tracepoint_enable(arg->freeobj_trace); } return Qnil; @@ -315,9 +312,6 @@ trace_object_allocations_stop(VALUE self) if (arg->newobj_trace != 0) { rb_tracepoint_disable(arg->newobj_trace); } - if (arg->freeobj_trace != 0) { - rb_tracepoint_disable(arg->freeobj_trace); - } } return Qnil; diff --git a/internal/gc.h b/internal/gc.h index 8b136e6572022c..e21fb892676f61 100644 --- a/internal/gc.h +++ b/internal/gc.h @@ -210,9 +210,6 @@ size_t rb_gc_heap_id_for_size(size_t size); void rb_gc_mark_and_move(VALUE *ptr); -void rb_gc_declare_weak_references(VALUE obj); -bool rb_gc_handle_weak_references_alive_p(VALUE obj); - void rb_gc_ref_update_table_values_only(st_table *tbl); void rb_gc_initial_stress_set(VALUE flag); @@ -233,6 +230,8 @@ void rb_objspace_reachable_objects_from_root(void (func)(const char *category, V int rb_objspace_internal_object_p(VALUE obj); int rb_objspace_garbage_object_p(VALUE obj); bool rb_gc_pointer_to_heap_p(VALUE obj); +void rb_gc_declare_weak_references(VALUE obj); +bool rb_gc_handle_weak_references_alive_p(VALUE obj); void rb_objspace_each_objects( int (*callback)(void *start, void *end, size_t stride, void *data), diff --git a/test/objspace/test_objspace.rb b/test/objspace/test_objspace.rb index faa22f1424f90a..a9b902ed458d44 100644 --- a/test/objspace/test_objspace.rb +++ b/test/objspace/test_objspace.rb @@ -350,6 +350,21 @@ def test_trace_object_allocations_compaction_freed_pages RUBY end + def test_trace_object_allocations_does_not_reuse_freed_allocation_info + assert_separately(%w(-robjspace), <<~RUBY) + ObjectSpace.trace_object_allocations do + 1_000_000.times.map { Object.new } + end + + GC.start + + objs = 1_000_000.times.map { Object.new } + + leaked = objs.count { |obj| ObjectSpace.allocation_sourcefile(obj) } + assert_equal 0, leaked + RUBY + end + def test_dump_flags # Ensure that the fstring is promoted to old generation 4.times { GC.start } From cdc31cfc42861daff09634739a919a263c0a323d Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Fri, 5 Jun 2026 18:57:19 +0200 Subject: [PATCH 153/188] [ruby/json] parser.c: refactor json_push_value / json_value_completed Makes each case simpler, but also more consistent. https://github.com/ruby/json/commit/65904e8825 --- ext/json/parser/parser.c | 54 +++++++++++++++++++--------------------- 1 file changed, 25 insertions(+), 29 deletions(-) diff --git a/ext/json/parser/parser.c b/ext/json/parser/parser.c index a61f997a112db1..308b47c37304b7 100644 --- a/ext/json/parser/parser.c +++ b/ext/json/parser/parser.c @@ -1236,7 +1236,7 @@ static VALUE json_parse_escaped_string(JSON_ParserState *state, JSON_ParserConfi case '"': { VALUE string = json_string_unescape(state, config, start, state->cursor, is_name, &positions); state->cursor++; - return json_push_value(state, config, string); + return string; } case '\\': { if (RB_LIKELY(positions.size < JSON_MAX_UNESCAPE_POSITIONS)) { @@ -1271,12 +1271,16 @@ ALWAYS_INLINE(static) VALUE json_parse_string(JSON_ParserState *state, JSON_Pars raise_parse_error("unexpected end of input, expected closing \"", state); } + VALUE string; if (RB_LIKELY(*state->cursor == '"')) { - VALUE string = json_string_fastpath(state, config, start, state->cursor, is_name); + string = json_string_fastpath(state, config, start, state->cursor, is_name); state->cursor++; - return json_push_value(state, config, string); } - return json_parse_escaped_string(state, config, is_name, start); + else { + string = json_parse_escaped_string(state, config, is_name, start); + } + + return string; } #if JSON_CPU_LITTLE_ENDIAN_64BITS @@ -1487,27 +1491,25 @@ static VALUE json_parse_any(JSON_ParserState *state, JSON_ParserConfig *config) JSON_PHASE_VALUE: json_eat_whitespace(state); + VALUE value; switch (peek(state)) { case 'n': if (json_match_keyword(state, "null", 0)) { - json_push_value(state, config, Qnil); - json_value_completed(frame); + value = Qnil; break; } raise_parse_error("unexpected token %s", state); case 't': if (json_match_keyword(state, "true", 0)) { - json_push_value(state, config, Qtrue); - json_value_completed(frame); + value = Qtrue; break; } raise_parse_error("unexpected token %s", state); case 'f': if (json_match_keyword(state, "false", 1)) { - json_push_value(state, config, Qfalse); - json_value_completed(frame); + value = Qfalse; break; } @@ -1515,16 +1517,14 @@ static VALUE json_parse_any(JSON_ParserState *state, JSON_ParserConfig *config) case 'N': // Note: memcmp with a small power of two compile to an integer comparison if (config->allow_nan && json_match_keyword(state, "NaN", 1)) { - json_push_value(state, config, CNaN); - json_value_completed(frame); + value = CNaN; break; } raise_parse_error("unexpected token %s", state); case 'I': if (config->allow_nan && json_match_keyword(state, "Infinity", 0)) { - json_push_value(state, config, CInfinity); - json_value_completed(frame); + value = CInfinity; break; } @@ -1532,23 +1532,18 @@ static VALUE json_parse_any(JSON_ParserState *state, JSON_ParserConfig *config) case '-': { state->cursor++; if (config->allow_nan && json_match_keyword(state, "Infinity", 0)) { - json_push_value(state, config, CMinusInfinity); - json_value_completed(frame); - break; + value = CMinusInfinity; + } else { + value = json_parse_negative_number(state, config); } - - json_push_value(state, config, json_parse_negative_number(state, config)); - json_value_completed(frame); break; } case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': - json_push_value(state, config, json_parse_positive_number(state, config)); - json_value_completed(frame); + value = json_parse_positive_number(state, config); break; case '"': // %r{\A"[^"\\\t\n\x00]*(?:\\[bfnrtu\\/"][^"\\]*)*"} - json_parse_string(state, config, false); - json_value_completed(frame); + value = json_parse_string(state, config, false); break; case '[': { state->cursor++; @@ -1556,8 +1551,7 @@ static VALUE json_parse_any(JSON_ParserState *state, JSON_ParserConfig *config) if (peek(state) == ']') { state->cursor++; - json_push_value(state, config, json_decode_array(state, config, 0)); - json_value_completed(frame); + value = json_decode_array(state, config, 0); break; } @@ -1584,8 +1578,7 @@ static VALUE json_parse_any(JSON_ParserState *state, JSON_ParserConfig *config) if (peek(state) == '}') { state->cursor++; - json_push_value(state, config, json_decode_object(state, config, 0)); - json_value_completed(frame); + value = json_decode_object(state, config, 0); break; } @@ -1611,6 +1604,9 @@ static VALUE json_parse_any(JSON_ParserState *state, JSON_ParserConfig *config) default: raise_parse_error("unexpected character: %s", state); } + + json_push_value(state, config, value); + json_value_completed(frame); break; } @@ -1621,7 +1617,7 @@ static VALUE json_parse_any(JSON_ParserState *state, JSON_ParserConfig *config) json_eat_whitespace(state); if (RB_LIKELY(peek(state) == '"')) { - json_parse_string(state, config, true); + json_push_value(state, config, json_parse_string(state, config, true)); frame->phase = JSON_PHASE_OBJECT_COLON; goto JSON_PHASE_OBJECT_COLON; } else { From e90a7ce80fef1d281ca7ab311809451e88b30004 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Fri, 5 Jun 2026 12:37:12 -0700 Subject: [PATCH 154/188] ZJIT: Fix cases where we need to super to C functions with >6 params (#17186) * ZJIT: Fix cases where we need to super to C functions with >6 params Previously ZJIT was crashing in cases where we need to `super` in to a cfunc. The issue is that we didn't handle C functions with more than the calling convention params. This patch just adds an exit in the case where we need to call in to a cfunc with too many params * Update zjit/src/codegen.rs Co-authored-by: Alan Wu * do what alan asked * Update zjit/src/codegen_tests.rs Co-authored-by: Alan Wu * address more feedback --------- Co-authored-by: Alan Wu --- zjit/src/codegen.rs | 8 ++++--- zjit/src/codegen_tests.rs | 46 +++++++++++++++++++++++++++++++++++++++ zjit/src/cruby.rs | 9 +++++++- 3 files changed, 59 insertions(+), 4 deletions(-) diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index 2d2ade8d7eebdb..108739c3e69445 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -711,9 +711,11 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio Insn::PatchPoint { invariant, state } => no_output!(gen_patch_point(jit, asm, invariant, &function.frame_state(*state))), Insn::CCall { cfunc, recv, args, name, owner: _, return_type: _, elidable: _ } => gen_ccall(asm, *cfunc, *name, opnd!(recv), opnds!(args)), // Give up CCallWithFrame for 7+ args since asm.ccall() supports at most 6 args (recv + args). - // There's no test case for this because no core cfuncs have this many parameters. But C extensions could have such methods. - Insn::CCallWithFrame { cd, state, args, .. } if args.len() + 1 > C_ARG_OPNDS.len() => - gen_send_without_block(jit, asm, *cd, &function.frame_state(*state), SendFallbackReason::CCallWithFrameTooManyArgs), + // We're currently emitting a CCallWithFrame for `super` in to a cfunction. + // We can't lower to `gen_send_without_block` because the + // source opcode isn't necessarily `opt_send_without_block` + // and so the interpreter stack layout may be incompatible. + Insn::CCallWithFrame { cd, state, args, block, .. } if args.len() + 1 > C_ARG_OPNDS.len() => return Err(*state), Insn::CCallWithFrame { cfunc, recv, name, args, cme, state, block, .. } => gen_ccall_with_frame(jit, asm, *cfunc, *name, opnd!(recv), opnds!(args), *cme, *block, &function.frame_state(*state)), Insn::CCallVariadic { cfunc, recv, name, args, cme, state, block, return_type: _, elidable: _ } => { diff --git a/zjit/src/codegen_tests.rs b/zjit/src/codegen_tests.rs index 1ed4a289dfdfd9..9b76690d5be916 100644 --- a/zjit/src/codegen_tests.rs +++ b/zjit/src/codegen_tests.rs @@ -1208,6 +1208,52 @@ fn test_invokesuper_to_cfunc_varargs() { "#), @r#"["MyString", true]"#); } +#[test] +fn test_invokesuper_to_cfunc_with_too_many_args_exits() { + unsafe extern "C" fn test_six_args( + _self: VALUE, + a: VALUE, + b: VALUE, + c: VALUE, + d: VALUE, + e: VALUE, + f: VALUE, + ) -> VALUE { + unsafe { rb_ary_new_from_args(6, a, b, c, d, e, f) } + } + + with_rubyvm(|| { + let superclass = define_class("ZJITSixArgs", unsafe { rb_cObject }); + unsafe { + rb_define_method( + superclass, + c"six".as_ptr(), + Some(std::mem::transmute::< + unsafe extern "C" fn(VALUE, VALUE, VALUE, VALUE, VALUE, VALUE, VALUE) -> VALUE, + unsafe extern "C" fn(VALUE) -> VALUE, + >(test_six_args)), + 6, + ); + } + }); + + assert_snapshot!(assert_compiles_allowing_exits(r#" + class ZJITSixArgsSubclass < ZJITSixArgs + def six(a, b, c, d, e, f) + super + end + end + + def test + ZJITSixArgsSubclass.new.six(1, 2, 3, 4, 5, 6) + end + + test + test + test + "#), @"[1, 2, 3, 4, 5, 6]"); +} + #[test] fn test_string_new_preserves_string_arg() { assert_snapshot!(inspect(r#" diff --git a/zjit/src/cruby.rs b/zjit/src/cruby.rs index 4e4a246b8a1f72..61de3709cbdfcf 100644 --- a/zjit/src/cruby.rs +++ b/zjit/src/cruby.rs @@ -91,7 +91,7 @@ use std::convert::From; use std::ffi::{c_void, CString, CStr}; use std::fmt::{Debug, Display, Formatter}; -use std::os::raw::{c_char, c_int, c_uint}; +use std::os::raw::{c_char, c_int, c_long, c_uint}; use std::panic::{catch_unwind, UnwindSafe}; use crate::cast::IntoUsize as _; @@ -132,6 +132,7 @@ unsafe extern "C" { pub fn rb_float_new(d: f64) -> VALUE; pub fn rb_hash_empty_p(hash: VALUE) -> VALUE; + pub fn rb_ary_new_from_args(n: c_long, ...) -> VALUE; pub fn rb_str_setbyte(str: VALUE, index: VALUE, value: VALUE) -> VALUE; pub fn rb_str_getbyte(str: VALUE, index: VALUE) -> VALUE; pub fn rb_vm_splat_array(flag: VALUE, ary: VALUE) -> VALUE; @@ -165,6 +166,12 @@ unsafe extern "C" { pub fn rb_vm_stack_canary() -> VALUE; pub fn rb_vm_push_cfunc_frame(cme: *const rb_callable_method_entry_t, recv_idx: c_int); pub fn rb_obj_class(klass: VALUE) -> VALUE; + pub fn rb_define_method( + klass: VALUE, + mid: *const c_char, + func: Option VALUE>, + arity: c_int, + ); pub fn rb_vm_objtostring(reg_cfp: CfpPtr, recv: VALUE, cd: *const rb_call_data) -> VALUE; } From 0d632bd429424583e028e797d4ecbf1f6033db31 Mon Sep 17 00:00:00 2001 From: Jacob Date: Fri, 5 Jun 2026 15:39:09 -0400 Subject: [PATCH 155/188] ZJIT: Add recompile support to GuardType (#17133) --- zjit/src/backend/lir.rs | 7 +- zjit/src/codegen.rs | 26 +- zjit/src/hir.rs | 58 ++-- zjit/src/hir/opt_tests.rs | 583 +++++++++++++++++++++++--------------- zjit/src/hir/tests.rs | 4 +- zjit/src/payload.rs | 9 + zjit/src/profile.rs | 10 +- 7 files changed, 427 insertions(+), 270 deletions(-) diff --git a/zjit/src/backend/lir.rs b/zjit/src/backend/lir.rs index a417df300a6885..91a1a3ffcf68f1 100644 --- a/zjit/src/backend/lir.rs +++ b/zjit/src/backend/lir.rs @@ -4,12 +4,12 @@ use std::mem::take; use std::rc::Rc; use crate::bitset::BitSet; use crate::codegen::{local_size_and_idx_to_ep_offset, perf_symbol_range_start, perf_symbol_range_end}; -use crate::cruby::{IseqPtr, RUBY_OFFSET_CFP_ISEQ, RUBY_OFFSET_CFP_JIT_RETURN, RUBY_OFFSET_CFP_PC, RUBY_OFFSET_CFP_SP, SIZEOF_VALUE_I32, vm_stack_canary}; +use crate::cruby::{IseqPtr, RUBY_OFFSET_CFP_ISEQ, RUBY_OFFSET_CFP_JIT_RETURN, RUBY_OFFSET_CFP_PC, RUBY_OFFSET_CFP_SP, SIZEOF_VALUE_I32, vm_stack_canary, YarvInsnIdx }; use crate::hir::{Invariant, SideExitReason}; use crate::hir; use crate::options::{TraceExits, PerfMap, get_option}; use crate::cruby::VALUE; -use crate::payload::IseqVersionRef; +use crate::payload::{IseqVersionRef, get_or_create_iseq_payload}; use crate::stats::{exit_counter_ptr, exit_counter_ptr_for_opcode, side_exit_counter, CompileError}; use crate::virtualmem::CodePtr; use crate::asm::{CodeBlock, Label}; @@ -2402,7 +2402,8 @@ impl Assembler fn compile_exit_recompile(asm: &mut Assembler, exit: &SideExit) { if let Some(recompile) = &exit.recompile { - + let payload = get_or_create_iseq_payload(exit.iseq); + payload.reset_profiles_remaining(recompile.insn_idx as YarvInsnIdx); use crate::codegen::exit_recompile; asm_comment!(asm, "profile and maybe recompile"); asm_ccall!(asm, exit_recompile, diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index 108739c3e69445..ee80ac0db5fb5d 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -699,9 +699,9 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio let val_type = function.type_of(*val); gen_has_type(jit, asm, opnd!(val), val_type, *expected) } - Insn::GuardType { val, guard_type, state } => { - let val_type = function.type_of(*val); - gen_guard_type(jit, asm, opnd!(val), val_type, *guard_type, &function.frame_state(*state)) + &Insn::GuardType { val, guard_type, state, recompile } => { + let val_type = function.type_of(val); + gen_guard_type(jit, asm, opnd!(val), val_type, guard_type, recompile, &function.frame_state(state)) } &Insn::GuardBitEquals { val, expected, reason, state, recompile } => gen_guard_bit_equals(jit, asm, opnd!(val), expected, reason, recompile, &function.frame_state(state)), &Insn::GuardAnyBitSet { val, mask, reason, state, .. } => gen_guard_any_bit_set(jit, asm, opnd!(val), mask, reason, &function.frame_state(state)), @@ -2525,33 +2525,33 @@ fn gen_has_type(jit: &mut JITState, asm: &mut Assembler, val: lir::Opnd, val_typ } /// Compile a type check with a side exit -fn gen_guard_type(jit: &mut JITState, asm: &mut Assembler, val: lir::Opnd, val_type: Type, guard_type: Type, state: &FrameState) -> lir::Opnd { +fn gen_guard_type(jit: &mut JITState, asm: &mut Assembler, val: lir::Opnd, val_type: Type, guard_type: Type, recompile: Option, state: &FrameState) -> lir::Opnd { let is_known_heap_basic_object = val_type.is_subtype(types::HeapBasicObject); gen_incr_counter(asm, Counter::guard_type_count); if guard_type.is_subtype(types::Fixnum) { asm.test(val, Opnd::UImm(RUBY_FIXNUM_FLAG as u64)); - asm.jz(jit, side_exit(jit, state, GuardType(guard_type))); + asm.jz(jit, side_exit_with_recompile(jit, state, GuardType(guard_type), recompile)); } else if guard_type.is_subtype(types::Flonum) { // Flonum: (val & RUBY_FLONUM_MASK) == RUBY_FLONUM_FLAG let masked = asm.and(val, Opnd::UImm(RUBY_FLONUM_MASK as u64)); asm.cmp(masked, Opnd::UImm(RUBY_FLONUM_FLAG as u64)); - asm.jne(jit, side_exit(jit, state, GuardType(guard_type))); + asm.jne(jit, side_exit_with_recompile(jit, state, GuardType(guard_type), recompile)); } else if guard_type.is_subtype(types::StaticSymbol) { // Static symbols have (val & 0xff) == RUBY_SYMBOL_FLAG // Use 8-bit comparison like YJIT does. // If `val` is a constant (rare but possible), put it in a register to allow masking. let val = asm.load_imm(val); asm.cmp(val.with_num_bits(8), Opnd::UImm(RUBY_SYMBOL_FLAG as u64)); - asm.jne(jit, side_exit(jit, state, GuardType(guard_type))); + asm.jne(jit, side_exit_with_recompile(jit, state, GuardType(guard_type), recompile)); } else if guard_type.is_subtype(types::NilClass) { asm.cmp(val, Qnil.into()); - asm.jne(jit, side_exit(jit, state, GuardType(guard_type))); + asm.jne(jit, side_exit_with_recompile(jit, state, GuardType(guard_type), recompile)); } else if guard_type.is_subtype(types::TrueClass) { asm.cmp(val, Qtrue.into()); - asm.jne(jit, side_exit(jit, state, GuardType(guard_type))); + asm.jne(jit, side_exit_with_recompile(jit, state, GuardType(guard_type), recompile)); } else if guard_type.is_subtype(types::FalseClass) { asm.cmp(val, Qfalse.into()); - asm.jne(jit, side_exit(jit, state, GuardType(guard_type))); + asm.jne(jit, side_exit_with_recompile(jit, state, GuardType(guard_type), recompile)); } else if guard_type.is_immediate() { // All immediate types' guard should have been handled above panic!("unexpected immediate guard type: {guard_type}"); @@ -2562,7 +2562,7 @@ fn gen_guard_type(jit: &mut JITState, asm: &mut Assembler, val: lir::Opnd, val_t // TODO: Max thinks codegen should not care about the shapes of the operands except to create them. (Shopify/ruby#685) let val = asm.load_mem(val); - let side_exit = side_exit(jit, state, GuardType(guard_type)); + let side_exit = side_exit_with_recompile(jit, state, GuardType(guard_type), recompile); if !is_known_heap_basic_object { // Check if it's a special constant asm.test(val, (RUBY_IMMEDIATE_MASK as u64).into()); @@ -2579,7 +2579,7 @@ fn gen_guard_type(jit: &mut JITState, asm: &mut Assembler, val: lir::Opnd, val_t asm.cmp(klass, Opnd::Value(expected_class)); asm.jne(jit, side_exit); } else if let Some(builtin_type) = guard_type.builtin_type_equivalent() { - let side = side_exit(jit, state, GuardType(guard_type)); + let side = side_exit_with_recompile(jit, state, GuardType(guard_type), recompile); if !is_known_heap_basic_object { // Check special constant @@ -2598,7 +2598,7 @@ fn gen_guard_type(jit: &mut JITState, asm: &mut Assembler, val: lir::Opnd, val_t asm.cmp(tag, Opnd::UImm(builtin_type as u64)); asm.jne(jit, side); } else if guard_type.bit_equal(types::HeapBasicObject) { - let side_exit = side_exit(jit, state, GuardType(guard_type)); + let side_exit = side_exit_with_recompile(jit, state, GuardType(guard_type), recompile); asm.cmp(val, Opnd::Value(Qfalse)); asm.je(jit, side_exit.clone()); asm.test(val, (RUBY_IMMEDIATE_MASK as u64).into()); diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 805503b82bd2d6..fa035292e42964 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -1168,7 +1168,7 @@ pub enum Insn { HasType { val: InsnId, expected: Type }, /// Side-exit if val doesn't have the expected type. - GuardType { val: InsnId, guard_type: Type, state: InsnId }, + GuardType { val: InsnId, guard_type: Type, state: InsnId, recompile: Option }, /// Side-exit if val is not the expected Const. GuardBitEquals { val: InsnId, expected: Const, reason: SideExitReason, state: InsnId, recompile: Option }, /// Side-exit if (val & mask) == 0 @@ -2125,7 +2125,13 @@ impl<'a> std::fmt::Display for InsnPrinter<'a> { Insn::IntOr { left, right } => { write!(f, "IntOr {left}, {right}") }, Insn::FixnumLShift { left, right, .. } => { write!(f, "FixnumLShift {left}, {right}") }, Insn::FixnumRShift { left, right, .. } => { write!(f, "FixnumRShift {left}, {right}") }, - Insn::GuardType { val, guard_type, .. } => { write!(f, "GuardType {val}, {}", guard_type.print(self.ptr_map)) }, + Insn::GuardType { val, guard_type, recompile, .. } => { + write!(f, "GuardType {val}, {}", guard_type.print(self.ptr_map))?; + if recompile.is_some() { + write!(f, " recompile")?; + } + return Ok(()) + }, Insn::RefineType { val, new_type, .. } => { write!(f, "RefineType {val}, {}", new_type.print(self.ptr_map)) }, Insn::HasType { val, expected, .. } => { write!(f, "HasType {val}, {}", expected.print(self.ptr_map)) }, Insn::GuardBitEquals { val, expected, recompile, .. } => { @@ -3468,7 +3474,7 @@ impl Function { pub fn coerce_to(&mut self, block: BlockId, val: InsnId, guard_type: Type, state: InsnId) -> InsnId { if self.is_a(val, guard_type) { return val; } - self.push_insn(block, Insn::GuardType { val, guard_type, state }) + self.push_insn(block, Insn::GuardType { val, guard_type, state, recompile: None }) } fn count_complex_call_features(&mut self, block: BlockId, ci_flags: c_uint) { @@ -3711,7 +3717,8 @@ impl Function { // Add GuardType for profiled receiver if let Some(profiled_type) = profiled_type { - recv = self.push_insn(block, Insn::GuardType { val: recv, guard_type: Type::from_profiled_type(profiled_type), state }); + let argc = unsafe { vm_ci_argc(ci) } as i32; + recv = self.push_insn(block, Insn::GuardType { val: recv, guard_type: Type::from_profiled_type(profiled_type), state, recompile: Some(Recompile::ProfileSend { argc }) }); } let send_direct = self.push_insn(block, Insn::SendDirect { recv, cd, cme, iseq, args: processed_args, kw_bits, state: send_state, block: send_block }); @@ -3754,7 +3761,8 @@ impl Function { self.push_insn(block, Insn::PatchPoint { invariant: Invariant::MethodRedefined { klass, method: mid, cme }, state }); if let Some(profiled_type) = profiled_type { - recv = self.push_insn(block, Insn::GuardType { val: recv, guard_type: Type::from_profiled_type(profiled_type), state }); + let argc = unsafe { vm_ci_argc(ci) } as i32; + recv = self.push_insn(block, Insn::GuardType { val: recv, guard_type: Type::from_profiled_type(profiled_type), state, recompile: Some(Recompile::ProfileSend{ argc }) }); } let send_direct = self.push_insn(block, Insn::SendDirect { recv, cd, cme, iseq, args: processed_args, kw_bits, state: send_state, block: None }); @@ -3774,7 +3782,8 @@ impl Function { self.push_insn(block, Insn::PatchPoint { invariant: Invariant::MethodRedefined { klass, method: mid, cme }, state }); if let Some(profiled_type) = profiled_type { - recv = self.push_insn(block, Insn::GuardType { val: recv, guard_type: Type::from_profiled_type(profiled_type), state }); + let argc = unsafe { vm_ci_argc(ci) } as i32; + recv = self.push_insn(block, Insn::GuardType { val: recv, guard_type: Type::from_profiled_type(profiled_type), state, recompile: Some(Recompile::ProfileSend{ argc }) }); } let id = unsafe { get_cme_def_body_attr_id(cme) }; @@ -3790,7 +3799,8 @@ impl Function { self.push_insn(block, Insn::PatchPoint { invariant: Invariant::MethodRedefined { klass, method: mid, cme }, state }); if let Some(profiled_type) = profiled_type { - recv = self.push_insn(block, Insn::GuardType { val: recv, guard_type: Type::from_profiled_type(profiled_type), state }); + let argc = unsafe { vm_ci_argc(ci) } as i32; + recv = self.push_insn(block, Insn::GuardType { val: recv, guard_type: Type::from_profiled_type(profiled_type), state, recompile: Some(Recompile::ProfileSend{ argc }) }); } let id = unsafe { get_cme_def_body_attr_id(cme) }; @@ -3812,7 +3822,8 @@ impl Function { } self.push_insn(block, Insn::PatchPoint { invariant: Invariant::MethodRedefined { klass, method: mid, cme }, state }); if let Some(profiled_type) = profiled_type { - recv = self.push_insn(block, Insn::GuardType { val: recv, guard_type: Type::from_profiled_type(profiled_type), state }); + let argc = unsafe { vm_ci_argc(ci) } as i32; + recv = self.push_insn(block, Insn::GuardType { val: recv, guard_type: Type::from_profiled_type(profiled_type), state, recompile: Some(Recompile::ProfileSend{ argc }) }); } let kw_splat = flags & VM_CALL_KW_SPLAT != 0; let invoke_proc = self.push_insn(block, Insn::InvokeProc { recv, args: args.clone(), state, kw_splat }); @@ -3850,7 +3861,8 @@ impl Function { } self.push_insn(block, Insn::PatchPoint { invariant: Invariant::MethodRedefined { klass, method: mid, cme }, state }); if let Some(profiled_type) = profiled_type { - recv = self.push_insn(block, Insn::GuardType { val: recv, guard_type: Type::from_profiled_type(profiled_type), state }); + let argc = unsafe { vm_ci_argc(ci) } as i32; + recv = self.push_insn(block, Insn::GuardType { val: recv, guard_type: Type::from_profiled_type(profiled_type), state, recompile: Some(Recompile::ProfileSend{ argc }) }); } // All structs from the same Struct class should have the same // length. So if our recv is embedded all runtime @@ -3921,12 +3933,12 @@ impl Function { if recv_type.is_string() { self.push_insn(block, Insn::PatchPoint { invariant: Invariant::NoSingletonClass { klass: recv_type.class() }, state }); - let guard = self.push_insn(block, Insn::GuardType { val, guard_type: types::String, state }); + let guard = self.push_insn(block, Insn::GuardType { val, guard_type: types::String, state, recompile: None }); // Infer type so AnyToString can fold off this self.insn_types[guard.0] = self.infer_type(guard); self.make_equal_to(insn_id, guard); } else { - let recv = self.push_insn(block, Insn::GuardType { val, guard_type: Type::from_profiled_type(recv_type), state}); + let recv = self.push_insn(block, Insn::GuardType { val, guard_type: Type::from_profiled_type(recv_type), state, recompile: None }); let send_to_s = self.push_insn(block, Insn::Send { recv, cd, block: None, args: vec![], state, reason: ObjToStringNotString }); self.make_equal_to(insn_id, send_to_s); } @@ -4373,16 +4385,16 @@ impl Function { fn load_ivar_guard_type(&mut self, block: BlockId, recv: InsnId, recv_type: ProfiledType, state: InsnId) -> InsnId { if recv_type.flags().is_t_class() { // Check class first since `Class < Module` - self.push_insn(block, Insn::GuardType { val: recv, guard_type: types::Class, state }) + self.push_insn(block, Insn::GuardType { val: recv, guard_type: types::Class, state, recompile: None }) } else if recv_type.flags().is_t_module() { - self.push_insn(block, Insn::GuardType { val: recv, guard_type: types::Module, state }) + self.push_insn(block, Insn::GuardType { val: recv, guard_type: types::Module, state, recompile: None }) } else if recv_type.flags().is_t_data() { - self.push_insn(block, Insn::GuardType { val: recv, guard_type: types::TData, state }) + self.push_insn(block, Insn::GuardType { val: recv, guard_type: types::TData, state, recompile: None }) } else { // HeapBasicObject is wider than T_OBJECT, but shapes for T_OBJECTs are in a pool of // its own and are guaranteed to be different from shapes of any other T_* types. So // the shape check that follows already covers checking for T_OBJECT. - self.push_insn(block, Insn::GuardType { val: recv, guard_type: types::HeapBasicObject, state }) + self.push_insn(block, Insn::GuardType { val: recv, guard_type: types::HeapBasicObject, state, recompile: None }) } } @@ -4769,7 +4781,8 @@ impl Function { if let Some(profiled_type) = profiled_type { // Guard receiver class - recv = fun.push_insn(block, Insn::GuardType { val: recv, guard_type: Type::from_profiled_type(profiled_type), state }); + let argc = unsafe { vm_ci_argc(call_info) } as i32; + recv = fun.push_insn(block, Insn::GuardType { val: recv, guard_type: Type::from_profiled_type(profiled_type), state, recompile: Some(Recompile::ProfileSend { argc }) }); fun.insn_types[recv.0] = fun.infer_type(recv); } @@ -4835,7 +4848,8 @@ impl Function { if let Some(profiled_type) = profiled_type { // Guard receiver class - recv = fun.push_insn(block, Insn::GuardType { val: recv, guard_type: Type::from_profiled_type(profiled_type), state }); + let argc = unsafe { vm_ci_argc(call_info) } as i32; + recv = fun.push_insn(block, Insn::GuardType { val: recv, guard_type: Type::from_profiled_type(profiled_type), state, recompile: Some(Recompile::ProfileSend { argc }) }); fun.insn_types[recv.0] = fun.infer_type(recv); } @@ -7093,9 +7107,9 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { } let ty = Type::from_profiled_type(summary.bucket(0)); let obj = if ty.is_subtype(types::NilClass) { - fun.push_insn(block, Insn::GuardType { val: hash, guard_type: types::NilClass, state: exit_id }) + fun.push_insn(block, Insn::GuardType { val: hash, guard_type: types::NilClass, state: exit_id, recompile: None }) } else if ty.is_subtype(types::HashExact) { - fun.push_insn(block, Insn::GuardType { val: hash, guard_type: types::HashExact, state: exit_id }) + fun.push_insn(block, Insn::GuardType { val: hash, guard_type: types::HashExact, state: exit_id, recompile: None }) } else { fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::SplatKwNotNilOrHash, recompile: None }); break; // End the block @@ -7161,7 +7175,7 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { let id = ID(get_arg(pc, 0).as_u64()); let pushval = get_arg(pc, 2); if let Some(summary) = fun.polymorphic_summary(&profiles, self_param, exit_state.insn_idx) { - self_param = fun.push_insn(block, Insn::GuardType { val: self_param, guard_type: types::HeapBasicObject, state: exit_id }); + self_param = fun.push_insn(block, Insn::GuardType { val: self_param, guard_type: types::HeapBasicObject, state: exit_id, recompile: None }); let rbasic_flags = fun.load_rbasic_flags(block, self_param); let join_block = insn_idx_to_block.get(&insn_idx).copied().unwrap_or_else(|| fun.new_block(insn_idx)); let join_param = fun.push_insn(join_block, Insn::Param); @@ -8332,7 +8346,7 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { break; // End the block } if let Some(summary) = fun.polymorphic_summary(&profiles, self_param, exit_state.insn_idx) { - self_param = fun.push_insn(block, Insn::GuardType { val: self_param, guard_type: types::HeapBasicObject, state: exit_id }); + self_param = fun.push_insn(block, Insn::GuardType { val: self_param, guard_type: types::HeapBasicObject, state: exit_id, recompile: None }); let rbasic_flags = fun.load_rbasic_flags(block, self_param); let join_block = insn_idx_to_block.get(&insn_idx).copied().unwrap_or_else(|| fun.new_block(insn_idx)); let join_param = fun.push_insn(join_block, Insn::Param); @@ -8540,7 +8554,7 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { break; // End the block } let val = state.stack_pop()?; - let array = fun.push_insn(block, Insn::GuardType { val, guard_type: types::ArrayExact, state: exit_id, }); + let array = fun.push_insn(block, Insn::GuardType { val, guard_type: types::ArrayExact, state: exit_id, recompile: None }); let length = fun.push_insn(block, Insn::ArrayLength { array }); let expected = fun.push_insn(block, Insn::Const { val: Const::CInt64(num as i64) }); fun.push_insn(block, Insn::GuardGreaterEq { left: length, right: expected, reason: SideExitReason::ExpandArray, state: exit_id }); diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index 74b951d2e59410..9a2ff9f03b8ba3 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -225,7 +225,7 @@ mod hir_opt_tests { bb3(v9:BasicObject, v10:BasicObject): v15:Fixnum[0] = Const Value(0) PatchPoint MethodRedefined(Integer@0x1008, -@0x1010, cme:0x1018) - v26:Fixnum = GuardType v10, Fixnum + v26:Fixnum = GuardType v10, Fixnum recompile CheckInterrupts Return v26 "); @@ -455,7 +455,7 @@ mod hir_opt_tests { bb3(v9:BasicObject, v10:BasicObject): v15:Fixnum[1] = Const Value(1) PatchPoint MethodRedefined(Integer@0x1008, /@0x1010, cme:0x1018) - v26:Fixnum = GuardType v10, Fixnum + v26:Fixnum = GuardType v10, Fixnum recompile CheckInterrupts Return v26 "); @@ -1132,7 +1132,7 @@ mod hir_opt_tests { v15:Fixnum[0] = Const Value(0) PatchPoint NoSingletonClass(Array@0x1008) PatchPoint MethodRedefined(Array@0x1008, []@0x1010, cme:0x1018) - v27:ArrayExact = GuardType v10, ArrayExact + v27:ArrayExact = GuardType v10, ArrayExact recompile v35:CInt64[0] = Const CInt64(0) v29:CInt64 = ArrayLength v27 v30:CInt64[0] = GuardLess v35, v29 @@ -1165,7 +1165,7 @@ mod hir_opt_tests { v15:Fixnum[0] = Const Value(0) PatchPoint NoSingletonClass(Array@0x1008) PatchPoint MethodRedefined(Array@0x1008, []@0x1010, cme:0x1018) - v27:ArrayExact = GuardType v10, ArrayExact + v27:ArrayExact = GuardType v10, ArrayExact recompile v35:CInt64[0] = Const CInt64(0) v29:CInt64 = ArrayLength v27 v30:CInt64[0] = GuardLess v35, v29 @@ -1248,7 +1248,7 @@ mod hir_opt_tests { bb3(v9:BasicObject, v10:BasicObject): PatchPoint NoSingletonClass(CustomEq@0x1008) PatchPoint MethodRedefined(CustomEq@0x1008, !=@0x1010, cme:0x1018) - v30:ObjectSubclass[class_exact:CustomEq] = GuardType v10, ObjectSubclass[class_exact:CustomEq] + v30:ObjectSubclass[class_exact:CustomEq] = GuardType v10, ObjectSubclass[class_exact:CustomEq] recompile v31:BoolExact = CCallWithFrame v30, :BasicObject#!=@0x1040, v30 v21:NilClass = Const Value(nil) CheckInterrupts @@ -1280,7 +1280,7 @@ mod hir_opt_tests { bb3(v9:BasicObject, v10:BasicObject): v15:Fixnum[1] = Const Value(1) PatchPoint MethodRedefined(Integer@0x1008, +@0x1010, cme:0x1018) - v26:Fixnum = GuardType v10, Fixnum + v26:Fixnum = GuardType v10, Fixnum recompile v27:Fixnum = FixnumAdd v26, v15 CheckInterrupts Return v27 @@ -1406,7 +1406,7 @@ mod hir_opt_tests { Jump bb3(v4) bb3(v6:BasicObject): PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) - v18:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v18:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile v19:BasicObject = SendDirect v18, 0x1038, :foo (0x1048) CheckInterrupts Return v19 @@ -1434,7 +1434,7 @@ mod hir_opt_tests { Jump bb3(v4) bb3(v6:BasicObject): PatchPoint MethodRedefined(Object@0x1000, baz@0x1008, cme:0x1010) - v18:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v18:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile v20:Fixnum[1] = Const Value(1) CheckInterrupts Return v20 @@ -1461,7 +1461,7 @@ mod hir_opt_tests { Jump bb3(v4) bb3(v6:BasicObject): PatchPoint MethodRedefined(Object@0x1000, baz@0x1008, cme:0x1010) - v19:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v19:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile CheckInterrupts Return v19 "); @@ -1498,7 +1498,7 @@ mod hir_opt_tests { bb3(v6:BasicObject): v11:Fixnum[1] = Const Value(1) PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) - v20:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v20:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile v21:BasicObject = SendDirect v20, 0x1038, :foo (0x1048), v11 CheckInterrupts Return v21 @@ -1530,7 +1530,7 @@ mod hir_opt_tests { bb3(v9:BasicObject, v10:BasicObject): PatchPoint NoSingletonClass(C@0x1008) PatchPoint MethodRedefined(C@0x1008, fun_new_map@0x1010, cme:0x1018) - v27:ArraySubclass[class_exact:C] = GuardType v10, ArraySubclass[class_exact:C] + v27:ArraySubclass[class_exact:C] = GuardType v10, ArraySubclass[class_exact:C] recompile v28:BasicObject = SendDirect v27, 0x1040, :fun_new_map (0x1050) PatchPoint NoEPEscape(test) v18:CPtr = LoadSP @@ -1567,7 +1567,7 @@ mod hir_opt_tests { bb3(v9:BasicObject, v10:BasicObject): PatchPoint NoSingletonClass(C@0x1008) PatchPoint MethodRedefined(C@0x1008, bar@0x1010, cme:0x1018) - v28:ObjectSubclass[class_exact:C] = GuardType v10, ObjectSubclass[class_exact:C] + v28:ObjectSubclass[class_exact:C] = GuardType v10, ObjectSubclass[class_exact:C] recompile v29:BasicObject = CCallWithFrame v28, :Enumerable#bar@0x1040, block=0x1048 PatchPoint NoEPEscape(test) v18:CPtr = LoadSP @@ -1604,7 +1604,7 @@ mod hir_opt_tests { bb3(v9:BasicObject, v10:BasicObject): PatchPoint NoSingletonClass(Array@0x1008) PatchPoint MethodRedefined(Array@0x1008, length@0x1010, cme:0x1018) - v24:ArrayExact = GuardType v10, ArrayExact + v24:ArrayExact = GuardType v10, ArrayExact recompile v25:CInt64 = ArrayLength v24 v26:Fixnum = BoxFixnum v25 CheckInterrupts @@ -1662,7 +1662,7 @@ mod hir_opt_tests { Jump bb3(v4) bb3(v6:BasicObject): PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) - v18:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v18:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile v19:BasicObject = SendDirect v18, 0x1038, :foo (0x1048) CheckInterrupts Return v19 @@ -1690,7 +1690,7 @@ mod hir_opt_tests { bb3(v6:BasicObject): v11:Fixnum[3] = Const Value(3) PatchPoint MethodRedefined(Object@0x1000, Integer@0x1008, cme:0x1010) - v20:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v20:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile v21:BasicObject = SendDirect v20, 0x1038, :Integer (0x1048), v11 CheckInterrupts Return v21 @@ -1720,7 +1720,7 @@ mod hir_opt_tests { v11:Fixnum[1] = Const Value(1) v13:Fixnum[2] = Const Value(2) PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) - v22:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v22:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile v23:BasicObject = SendDirect v22, 0x1038, :foo (0x1048), v11, v13 CheckInterrupts Return v23 @@ -1750,7 +1750,7 @@ mod hir_opt_tests { Jump bb3(v4) bb3(v6:BasicObject): PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) - v23:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v23:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile v24:BasicObject = SendDirect v23, 0x1038, :foo (0x1048) PatchPoint MethodRedefined(Object@0x1000, bar@0x1050, cme:0x1058) v27:BasicObject = SendDirect v23, 0x1038, :bar (0x1048) @@ -1778,7 +1778,7 @@ mod hir_opt_tests { Jump bb3(v4) bb3(v6:BasicObject): PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) - v18:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v18:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile v19:BasicObject = SendDirect v18, 0x1038, :foo (0x1048) CheckInterrupts Return v19 @@ -1805,7 +1805,7 @@ mod hir_opt_tests { bb3(v6:BasicObject): v11:Fixnum[3] = Const Value(3) PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) - v20:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v20:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile v21:BasicObject = SendDirect v20, 0x1038, :foo (0x1048), v11 CheckInterrupts Return v21 @@ -1833,7 +1833,7 @@ mod hir_opt_tests { v11:Fixnum[3] = Const Value(3) v13:Fixnum[4] = Const Value(4) PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) - v22:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v22:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile v23:BasicObject = SendDirect v22, 0x1038, :foo (0x1048), v11, v13 CheckInterrupts Return v23 @@ -1860,7 +1860,7 @@ mod hir_opt_tests { Jump bb3(v4) bb3(v6:BasicObject): PatchPoint MethodRedefined(Object@0x1000, target@0x1008, cme:0x1010) - v44:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v44:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile v45:BasicObject = SendDirect v44, 0x1038, :target (0x1048) v14:Fixnum[10] = Const Value(10) v16:Fixnum[20] = Const Value(20) @@ -1901,7 +1901,7 @@ mod hir_opt_tests { v11:StringExact[VALUE(0x1000)] = Const Value(VALUE(0x1000)) v12:StringExact = StringCopy v11 PatchPoint MethodRedefined(Object@0x1008, puts@0x1010, cme:0x1018) - v22:ObjectSubclass[class_exact*:Object@VALUE(0x1008)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1008)] + v22:ObjectSubclass[class_exact*:Object@VALUE(0x1008)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1008)] recompile v23:BasicObject = CCallVariadic v22, :Kernel#puts@0x1040, v12 CheckInterrupts Return v23 @@ -1936,7 +1936,7 @@ mod hir_opt_tests { Jump bb3(v7, v8, v9) bb3(v11:BasicObject, v12:BasicObject, v13:BasicObject): PatchPoint MethodRedefined(Integer@0x1008, +@0x1010, cme:0x1018) - v27:Fixnum = GuardType v12, Fixnum + v27:Fixnum = GuardType v12, Fixnum recompile v29:Fixnum[100] = Const Value(100) CheckInterrupts Return v29 @@ -1966,7 +1966,7 @@ mod hir_opt_tests { Jump bb3(v7, v8, v9) bb3(v11:BasicObject, v12:BasicObject, v13:BasicObject): PatchPoint MethodRedefined(Integer@0x1008, +@0x1010, cme:0x1018) - v28:Fixnum = GuardType v12, Fixnum + v28:Fixnum = GuardType v12, Fixnum recompile v29:Fixnum = GuardType v13, Fixnum v30:Fixnum = FixnumAdd v28, v29 CheckInterrupts @@ -1996,7 +1996,7 @@ mod hir_opt_tests { bb3(v9:BasicObject, v10:BasicObject): v15:Fixnum[1] = Const Value(1) PatchPoint MethodRedefined(Integer@0x1008, +@0x1010, cme:0x1018) - v26:Fixnum = GuardType v10, Fixnum + v26:Fixnum = GuardType v10, Fixnum recompile v27:Fixnum = FixnumAdd v26, v15 CheckInterrupts Return v27 @@ -2055,7 +2055,7 @@ mod hir_opt_tests { Jump bb3(v7, v8, v9) bb3(v11:BasicObject, v12:BasicObject, v13:BasicObject): PatchPoint MethodRedefined(Integer@0x1008, []@0x1010, cme:0x1018) - v28:Fixnum = GuardType v12, Fixnum + v28:Fixnum = GuardType v12, Fixnum recompile v29:Fixnum = GuardType v13, Fixnum v30:Fixnum = FixnumAref v28, v29 CheckInterrupts @@ -2166,7 +2166,7 @@ mod hir_opt_tests { Jump bb3(v7, v8, v9) bb3(v11:BasicObject, v12:BasicObject, v13:BasicObject): PatchPoint MethodRedefined(Integer@0x1008, <@0x1010, cme:0x1018) - v28:Fixnum = GuardType v12, Fixnum + v28:Fixnum = GuardType v12, Fixnum recompile v29:Fixnum = GuardType v13, Fixnum v30:BoolExact = FixnumLt v28, v29 CheckInterrupts @@ -2196,7 +2196,7 @@ mod hir_opt_tests { bb3(v9:BasicObject, v10:BasicObject): v15:Fixnum[1] = Const Value(1) PatchPoint MethodRedefined(Integer@0x1008, <@0x1010, cme:0x1018) - v26:Fixnum = GuardType v10, Fixnum + v26:Fixnum = GuardType v10, Fixnum recompile v27:BoolExact = FixnumLt v26, v15 CheckInterrupts Return v27 @@ -2484,7 +2484,7 @@ mod hir_opt_tests { v15:Fixnum[0] = Const Value(0) PatchPoint NoSingletonClass(Array@0x1008) PatchPoint MethodRedefined(Array@0x1008, []@0x1010, cme:0x1018) - v27:ArrayExact = GuardType v10, ArrayExact + v27:ArrayExact = GuardType v10, ArrayExact recompile v35:CInt64[0] = Const CInt64(0) v29:CInt64 = ArrayLength v27 v30:CInt64[0] = GuardLess v35, v29 @@ -2519,7 +2519,7 @@ mod hir_opt_tests { v15:Fixnum[0] = Const Value(0) PatchPoint NoSingletonClass(Hash@0x1008) PatchPoint MethodRedefined(Hash@0x1008, []@0x1010, cme:0x1018) - v27:HashExact = GuardType v10, HashExact + v27:HashExact = GuardType v10, HashExact recompile v28:BasicObject = HashAref v27, v15 CheckInterrupts Return v28 @@ -2830,7 +2830,7 @@ mod hir_opt_tests { Jump bb3(v7, v8, v9) bb3(v11:BasicObject, v12:BasicObject, v13:BasicObject): PatchPoint MethodRedefined(Integer@0x1008, +@0x1010, cme:0x1018) - v32:Fixnum = GuardType v12, Fixnum + v32:Fixnum = GuardType v12, Fixnum recompile v33:Fixnum = GuardType v13, Fixnum v24:Fixnum[5] = Const Value(5) CheckInterrupts @@ -2864,7 +2864,7 @@ mod hir_opt_tests { Jump bb3(v7, v8, v9) bb3(v11:BasicObject, v12:BasicObject, v13:BasicObject): PatchPoint MethodRedefined(Integer@0x1008, -@0x1010, cme:0x1018) - v32:Fixnum = GuardType v12, Fixnum + v32:Fixnum = GuardType v12, Fixnum recompile v33:Fixnum = GuardType v13, Fixnum v24:Fixnum[5] = Const Value(5) CheckInterrupts @@ -2898,7 +2898,7 @@ mod hir_opt_tests { Jump bb3(v7, v8, v9) bb3(v11:BasicObject, v12:BasicObject, v13:BasicObject): PatchPoint MethodRedefined(Integer@0x1008, *@0x1010, cme:0x1018) - v32:Fixnum = GuardType v12, Fixnum + v32:Fixnum = GuardType v12, Fixnum recompile v33:Fixnum = GuardType v13, Fixnum v24:Fixnum[5] = Const Value(5) CheckInterrupts @@ -2932,7 +2932,7 @@ mod hir_opt_tests { Jump bb3(v7, v8, v9) bb3(v11:BasicObject, v12:BasicObject, v13:BasicObject): PatchPoint MethodRedefined(Integer@0x1008, /@0x1010, cme:0x1018) - v32:Fixnum = GuardType v12, Fixnum + v32:Fixnum = GuardType v12, Fixnum recompile v33:Fixnum = GuardType v13, Fixnum v34:Integer = FixnumDiv v32, v33 v24:Fixnum[5] = Const Value(5) @@ -2967,7 +2967,7 @@ mod hir_opt_tests { Jump bb3(v7, v8, v9) bb3(v11:BasicObject, v12:BasicObject, v13:BasicObject): PatchPoint MethodRedefined(Integer@0x1008, %@0x1010, cme:0x1018) - v32:Fixnum = GuardType v12, Fixnum + v32:Fixnum = GuardType v12, Fixnum recompile v33:Fixnum = GuardType v13, Fixnum v34:Fixnum = FixnumMod v32, v33 v24:Fixnum[5] = Const Value(5) @@ -3002,7 +3002,7 @@ mod hir_opt_tests { Jump bb3(v7, v8, v9) bb3(v11:BasicObject, v12:BasicObject, v13:BasicObject): PatchPoint MethodRedefined(Integer@0x1008, <@0x1010, cme:0x1018) - v32:Fixnum = GuardType v12, Fixnum + v32:Fixnum = GuardType v12, Fixnum recompile v33:Fixnum = GuardType v13, Fixnum v24:Fixnum[5] = Const Value(5) CheckInterrupts @@ -3036,7 +3036,7 @@ mod hir_opt_tests { Jump bb3(v7, v8, v9) bb3(v11:BasicObject, v12:BasicObject, v13:BasicObject): PatchPoint MethodRedefined(Integer@0x1008, <=@0x1010, cme:0x1018) - v32:Fixnum = GuardType v12, Fixnum + v32:Fixnum = GuardType v12, Fixnum recompile v33:Fixnum = GuardType v13, Fixnum v24:Fixnum[5] = Const Value(5) CheckInterrupts @@ -3070,7 +3070,7 @@ mod hir_opt_tests { Jump bb3(v7, v8, v9) bb3(v11:BasicObject, v12:BasicObject, v13:BasicObject): PatchPoint MethodRedefined(Integer@0x1008, >@0x1010, cme:0x1018) - v32:Fixnum = GuardType v12, Fixnum + v32:Fixnum = GuardType v12, Fixnum recompile v33:Fixnum = GuardType v13, Fixnum v24:Fixnum[5] = Const Value(5) CheckInterrupts @@ -3104,7 +3104,7 @@ mod hir_opt_tests { Jump bb3(v7, v8, v9) bb3(v11:BasicObject, v12:BasicObject, v13:BasicObject): PatchPoint MethodRedefined(Integer@0x1008, >=@0x1010, cme:0x1018) - v32:Fixnum = GuardType v12, Fixnum + v32:Fixnum = GuardType v12, Fixnum recompile v33:Fixnum = GuardType v13, Fixnum v24:Fixnum[5] = Const Value(5) CheckInterrupts @@ -3138,7 +3138,7 @@ mod hir_opt_tests { Jump bb3(v7, v8, v9) bb3(v11:BasicObject, v12:BasicObject, v13:BasicObject): PatchPoint MethodRedefined(Integer@0x1008, ==@0x1010, cme:0x1018) - v32:Fixnum = GuardType v12, Fixnum + v32:Fixnum = GuardType v12, Fixnum recompile v33:Fixnum = GuardType v13, Fixnum v24:Fixnum[5] = Const Value(5) CheckInterrupts @@ -3172,7 +3172,7 @@ mod hir_opt_tests { Jump bb3(v7, v8, v9) bb3(v11:BasicObject, v12:BasicObject, v13:BasicObject): PatchPoint MethodRedefined(Integer@0x1008, !=@0x1010, cme:0x1018) - v32:Fixnum = GuardType v12, Fixnum + v32:Fixnum = GuardType v12, Fixnum recompile PatchPoint BOPRedefined(INTEGER_REDEFINED_OP_FLAG, BOP_EQ) v34:Fixnum = GuardType v13, Fixnum v24:Fixnum[5] = Const Value(5) @@ -3259,7 +3259,7 @@ mod hir_opt_tests { Jump bb3(v6, v7) bb3(v9:BasicObject, v10:BasicObject): PatchPoint MethodRedefined(Integer@0x1008, itself@0x1010, cme:0x1018) - v23:Fixnum = GuardType v10, Fixnum + v23:Fixnum = GuardType v10, Fixnum recompile CheckInterrupts Return v23 "); @@ -3570,7 +3570,7 @@ mod hir_opt_tests { Jump bb3(v4) bb3(v6:BasicObject): PatchPoint MethodRedefined(Object@0x1000, block_given?@0x1008, cme:0x1010) - v19:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v19:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile v20:CPtr = GetEP 0 v21:BoolExact = IsBlockGiven v20 CheckInterrupts @@ -3596,7 +3596,7 @@ mod hir_opt_tests { Jump bb3(v4) bb3(v6:BasicObject): PatchPoint MethodRedefined(Object@0x1000, block_given?@0x1008, cme:0x1010) - v19:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v19:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile v20:FalseClass = Const Value(false) CheckInterrupts Return v20 @@ -3624,7 +3624,7 @@ mod hir_opt_tests { Jump bb3(v4) bb3(v6:BasicObject): PatchPoint MethodRedefined(Object@0x1000, block_given?@0x1008, cme:0x1010) - v23:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v23:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile v15:Fixnum[5] = Const Value(5) CheckInterrupts Return v15 @@ -3751,7 +3751,7 @@ mod hir_opt_tests { bb3(v9:BasicObject, v10:BasicObject): PatchPoint NoSingletonClass(C@0x1008) PatchPoint MethodRedefined(C@0x1008, foo@0x1010, cme:0x1018) - v23:ObjectSubclass[class_exact:C] = GuardType v10, ObjectSubclass[class_exact:C] + v23:ObjectSubclass[class_exact:C] = GuardType v10, ObjectSubclass[class_exact:C] recompile v24:BasicObject = SendDirect v23, 0x1040, :foo (0x1050) CheckInterrupts Return v24 @@ -3781,7 +3781,7 @@ mod hir_opt_tests { v11:Fixnum[1] = Const Value(1) v13:Fixnum[2] = Const Value(2) PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) - v22:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v22:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile v23:BasicObject = SendDirect v22, 0x1038, :foo (0x1048), v11, v13 CheckInterrupts Return v23 @@ -3815,7 +3815,7 @@ mod hir_opt_tests { bb3(v8:BasicObject, v9:NilClass): v13:Fixnum[1] = Const Value(1) PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) - v34:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v8, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v34:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v8, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile v36:Fixnum[1] = Const Value(1) PatchPoint NoEPEscape(test) v21:CPtr = LoadSP @@ -3854,7 +3854,7 @@ mod hir_opt_tests { v13:Fixnum[1] = Const Value(1) SetLocal :a, l0, EP@3, v13 PatchPoint MethodRedefined(Object@0x1000, lambda@0x1008, cme:0x1010) - v45:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v8, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v45:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v8, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile v46:BasicObject = CCallWithFrame v45, :Kernel#lambda@0x1038, block=0x1040 v20:CPtr = GetEP 0 v21:BasicObject = LoadField v20, :a@0x1048 @@ -3920,7 +3920,7 @@ mod hir_opt_tests { bb3(v6:BasicObject): v11:Fixnum[10] = Const Value(10) PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) - v20:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v20:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile v21:BasicObject = SendDirect v20, 0x1038, :foo (0x1048), v11 CheckInterrupts Return v21 @@ -3950,7 +3950,7 @@ mod hir_opt_tests { v11:Fixnum[10] = Const Value(10) v13:Fixnum[20] = Const Value(20) PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) - v22:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v22:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile v23:BasicObject = SendDirect v22, 0x1038, :foo (0x1048), v11, v13 CheckInterrupts Return v23 @@ -3979,7 +3979,7 @@ mod hir_opt_tests { v11:Fixnum[1] = Const Value(1) v13:Fixnum[2] = Const Value(2) PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) - v22:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v22:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile v23:BasicObject = SendDirect v22, 0x1038, :foo (0x1048), v11, v13 CheckInterrupts Return v23 @@ -4009,7 +4009,7 @@ mod hir_opt_tests { v13:Fixnum[1] = Const Value(1) v15:Fixnum[2] = Const Value(2) PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) - v25:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v25:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile v26:BasicObject = SendDirect v25, 0x1038, :foo (0x1048), v13, v15, v11 CheckInterrupts Return v26 @@ -4039,7 +4039,7 @@ mod hir_opt_tests { v13:Fixnum[2] = Const Value(2) v15:Fixnum[1] = Const Value(1) PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) - v25:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v25:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile v26:BasicObject = SendDirect v25, 0x1038, :foo (0x1048), v11, v15, v13 CheckInterrupts Return v26 @@ -4068,7 +4068,7 @@ mod hir_opt_tests { v11:Fixnum[0] = Const Value(0) v13:Fixnum[2] = Const Value(2) PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) - v22:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v22:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile v23:BasicObject = SendDirect v22, 0x1038, :foo (0x1048), v11, v13 CheckInterrupts Return v23 @@ -4098,7 +4098,7 @@ mod hir_opt_tests { v13:Fixnum[3] = Const Value(3) v15:Fixnum[4] = Const Value(4) PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) - v37:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v37:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile v38:BasicObject = SendDirect v37, 0x1038, :foo (0x1048), v11, v13, v15 v20:Fixnum[1] = Const Value(1) v22:Fixnum[2] = Const Value(2) @@ -4135,7 +4135,7 @@ mod hir_opt_tests { v13:Fixnum[3] = Const Value(3) v34:Fixnum[4] = Const Value(4) PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) - v37:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v37:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile v38:BasicObject = SendDirect v37, 0x1038, :foo (0x1048), v11, v13, v34 v18:Fixnum[1] = Const Value(1) v20:Fixnum[2] = Const Value(2) @@ -4170,7 +4170,7 @@ mod hir_opt_tests { bb3(v6:BasicObject): v11:Fixnum[6] = Const Value(6) PatchPoint MethodRedefined(Object@0x1000, target@0x1008, cme:0x1010) - v48:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v48:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile v49:BasicObject = SendDirect v48, 0x1038, :target (0x1048), v11 v16:Fixnum[10] = Const Value(10) v18:Fixnum[20] = Const Value(20) @@ -4212,7 +4212,7 @@ mod hir_opt_tests { bb3(v6:BasicObject): v11:Fixnum[2] = Const Value(2) PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) - v20:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v20:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile v21:BasicObject = SendDirect v20, 0x1038, :foo (0x1048), v11 CheckInterrupts Return v21 @@ -4327,7 +4327,7 @@ mod hir_opt_tests { bb3(v6:BasicObject): v17:Fixnum[1] = Const Value(1) PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) - v20:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v20:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile v21:BasicObject = SendDirect v20, 0x1038, :foo (0x1048), v17 CheckInterrupts Return v21 @@ -4786,7 +4786,7 @@ mod hir_opt_tests { v17:HeapBasicObject = ObjectAlloc v43 PatchPoint NoSingletonClass(Set@0x1008) PatchPoint MethodRedefined(Set@0x1008, initialize@0x1038, cme:0x1040) - v49:SetExact = GuardType v17, SetExact + v49:SetExact = GuardType v17, SetExact recompile v50:BasicObject = CCallVariadic v49, :Set#initialize@0x1068 CheckInterrupts Return v49 @@ -5467,7 +5467,7 @@ mod hir_opt_tests { v15:Fixnum[1] = Const Value(1) PatchPoint NoSingletonClass(Proc@0x1008) PatchPoint MethodRedefined(Proc@0x1008, call@0x1010, cme:0x1018) - v25:ObjectSubclass[class_exact:Proc] = GuardType v10, ObjectSubclass[class_exact:Proc] + v25:ObjectSubclass[class_exact:Proc] = GuardType v10, ObjectSubclass[class_exact:Proc] recompile v26:BasicObject = InvokeProc v25, v15 CheckInterrupts Return v26 @@ -5500,7 +5500,7 @@ mod hir_opt_tests { v15:Fixnum[2] = Const Value(2) PatchPoint NoSingletonClass(Proc@0x1008) PatchPoint MethodRedefined(Proc@0x1008, []@0x1010, cme:0x1018) - v26:ObjectSubclass[class_exact:Proc] = GuardType v10, ObjectSubclass[class_exact:Proc] + v26:ObjectSubclass[class_exact:Proc] = GuardType v10, ObjectSubclass[class_exact:Proc] recompile v27:BasicObject = InvokeProc v26, v15 CheckInterrupts Return v27 @@ -5533,7 +5533,7 @@ mod hir_opt_tests { v15:Fixnum[3] = Const Value(3) PatchPoint NoSingletonClass(Proc@0x1008) PatchPoint MethodRedefined(Proc@0x1008, yield@0x1010, cme:0x1018) - v25:ObjectSubclass[class_exact:Proc] = GuardType v10, ObjectSubclass[class_exact:Proc] + v25:ObjectSubclass[class_exact:Proc] = GuardType v10, ObjectSubclass[class_exact:Proc] recompile v26:BasicObject = InvokeProc v25, v15 CheckInterrupts Return v26 @@ -5566,7 +5566,7 @@ mod hir_opt_tests { v15:Fixnum[1] = Const Value(1) PatchPoint NoSingletonClass(Proc@0x1008) PatchPoint MethodRedefined(Proc@0x1008, ===@0x1010, cme:0x1018) - v25:ObjectSubclass[class_exact:Proc] = GuardType v10, ObjectSubclass[class_exact:Proc] + v25:ObjectSubclass[class_exact:Proc] = GuardType v10, ObjectSubclass[class_exact:Proc] recompile v26:BasicObject = InvokeProc v25, v15 CheckInterrupts Return v26 @@ -7115,7 +7115,7 @@ mod hir_opt_tests { bb3(v6:BasicObject): PatchPoint SingleRactorMode PatchPoint MethodRedefined(Object@0x1000, zero@0x1008, cme:0x1010) - v22:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v22:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile v28:StaticSymbol[:b] = Const Value(VALUE(0x1038)) PatchPoint MethodRedefined(Object@0x1000, one@0x1040, cme:0x1048) CheckInterrupts @@ -7328,7 +7328,7 @@ mod hir_opt_tests { Jump bb3(v6, v7) bb3(v9:BasicObject, v10:BasicObject): PatchPoint MethodRedefined(NilClass@0x1008, nil?@0x1010, cme:0x1018) - v24:NilClass = GuardType v10, NilClass + v24:NilClass = GuardType v10, NilClass recompile v25:TrueClass = Const Value(true) CheckInterrupts Return v25 @@ -7357,7 +7357,7 @@ mod hir_opt_tests { Jump bb3(v6, v7) bb3(v9:BasicObject, v10:BasicObject): PatchPoint MethodRedefined(FalseClass@0x1008, nil?@0x1010, cme:0x1018) - v24:FalseClass = GuardType v10, FalseClass + v24:FalseClass = GuardType v10, FalseClass recompile v25:FalseClass = Const Value(false) CheckInterrupts Return v25 @@ -7386,7 +7386,7 @@ mod hir_opt_tests { Jump bb3(v6, v7) bb3(v9:BasicObject, v10:BasicObject): PatchPoint MethodRedefined(TrueClass@0x1008, nil?@0x1010, cme:0x1018) - v24:TrueClass = GuardType v10, TrueClass + v24:TrueClass = GuardType v10, TrueClass recompile v25:FalseClass = Const Value(false) CheckInterrupts Return v25 @@ -7415,7 +7415,7 @@ mod hir_opt_tests { Jump bb3(v6, v7) bb3(v9:BasicObject, v10:BasicObject): PatchPoint MethodRedefined(Symbol@0x1008, nil?@0x1010, cme:0x1018) - v24:StaticSymbol = GuardType v10, StaticSymbol + v24:StaticSymbol = GuardType v10, StaticSymbol recompile v25:FalseClass = Const Value(false) CheckInterrupts Return v25 @@ -7444,7 +7444,7 @@ mod hir_opt_tests { Jump bb3(v6, v7) bb3(v9:BasicObject, v10:BasicObject): PatchPoint MethodRedefined(Integer@0x1008, nil?@0x1010, cme:0x1018) - v24:Fixnum = GuardType v10, Fixnum + v24:Fixnum = GuardType v10, Fixnum recompile v25:FalseClass = Const Value(false) CheckInterrupts Return v25 @@ -7473,7 +7473,7 @@ mod hir_opt_tests { Jump bb3(v6, v7) bb3(v9:BasicObject, v10:BasicObject): PatchPoint MethodRedefined(Float@0x1008, nil?@0x1010, cme:0x1018) - v24:Flonum = GuardType v10, Flonum + v24:Flonum = GuardType v10, Flonum recompile v25:FalseClass = Const Value(false) CheckInterrupts Return v25 @@ -7503,7 +7503,7 @@ mod hir_opt_tests { bb3(v9:BasicObject, v10:BasicObject): PatchPoint NoSingletonClass(String@0x1008) PatchPoint MethodRedefined(String@0x1008, nil?@0x1010, cme:0x1018) - v25:StringExact = GuardType v10, StringExact + v25:StringExact = GuardType v10, StringExact recompile v26:FalseClass = Const Value(false) CheckInterrupts Return v26 @@ -7533,7 +7533,7 @@ mod hir_opt_tests { bb3(v9:BasicObject, v10:BasicObject): PatchPoint NoSingletonClass(Array@0x1008) PatchPoint MethodRedefined(Array@0x1008, !@0x1010, cme:0x1018) - v25:ArrayExact = GuardType v10, ArrayExact + v25:ArrayExact = GuardType v10, ArrayExact recompile v26:FalseClass = Const Value(false) CheckInterrupts Return v26 @@ -7562,7 +7562,7 @@ mod hir_opt_tests { Jump bb3(v6, v7) bb3(v9:BasicObject, v10:BasicObject): PatchPoint MethodRedefined(FalseClass@0x1008, !@0x1010, cme:0x1018) - v24:FalseClass = GuardType v10, FalseClass + v24:FalseClass = GuardType v10, FalseClass recompile v25:TrueClass = Const Value(true) CheckInterrupts Return v25 @@ -7591,7 +7591,7 @@ mod hir_opt_tests { Jump bb3(v6, v7) bb3(v9:BasicObject, v10:BasicObject): PatchPoint MethodRedefined(NilClass@0x1008, !@0x1010, cme:0x1018) - v24:NilClass = GuardType v10, NilClass + v24:NilClass = GuardType v10, NilClass recompile v25:TrueClass = Const Value(true) CheckInterrupts Return v25 @@ -7635,11 +7635,25 @@ mod hir_opt_tests { v29:NilClass = Const Value(nil) Jump bb5(v25, v26, v29) bb5(v31:BasicObject, v32:BasicObject, v33:Falsy): - PatchPoint MethodRedefined(NilClass@0x1008, !@0x1010, cme:0x1018) - v45:NilClass = GuardType v33, NilClass - v46:TrueClass = Const Value(true) + v37:CBool = HasType v33, FalseClass + CondBranch v37, bb8(v31, v32, v33), bb9() + bb8(v38:BasicObject, v39:BasicObject, v40:Falsy): + PatchPoint MethodRedefined(FalseClass@0x1008, !@0x1010, cme:0x1018) + v68:TrueClass = Const Value(true) + Jump bb7(v38, v39, v68) + bb9(): + v46:CBool = HasType v33, NilClass + CondBranch v46, bb10(v31, v32, v33), bb11() + bb10(v47:BasicObject, v48:BasicObject, v49:Falsy): + PatchPoint MethodRedefined(NilClass@0x1040, !@0x1010, cme:0x1018) + v71:TrueClass = Const Value(true) + Jump bb7(v47, v48, v71) + bb11(): + v55:BasicObject = Send v33, :! # SendFallbackReason: SendWithoutBlock: polymorphic fallback + Jump bb7(v31, v32, v55) + bb7(v57:BasicObject, v58:BasicObject, v59:BasicObject): CheckInterrupts - Return v46 + Return v59 "); } @@ -7666,7 +7680,7 @@ mod hir_opt_tests { bb3(v9:BasicObject, v10:BasicObject): PatchPoint NoSingletonClass(Array@0x1008) PatchPoint MethodRedefined(Array@0x1008, empty?@0x1010, cme:0x1018) - v25:ArrayExact = GuardType v10, ArrayExact + v25:ArrayExact = GuardType v10, ArrayExact recompile v26:CInt64 = ArrayLength v25 v27:CInt64[0] = Const CInt64(0) v28:CBool = IsBitEqual v26, v27 @@ -7699,7 +7713,7 @@ mod hir_opt_tests { bb3(v9:BasicObject, v10:BasicObject): PatchPoint NoSingletonClass(Hash@0x1008) PatchPoint MethodRedefined(Hash@0x1008, empty?@0x1010, cme:0x1018) - v25:HashExact = GuardType v10, HashExact + v25:HashExact = GuardType v10, HashExact recompile v26:BoolExact = CCall v25, :Hash#empty?@0x1040 CheckInterrupts Return v26 @@ -7732,7 +7746,7 @@ mod hir_opt_tests { bb3(v11:BasicObject, v12:BasicObject, v13:BasicObject): PatchPoint NoSingletonClass(C@0x1008) PatchPoint MethodRedefined(C@0x1008, ==@0x1010, cme:0x1018) - v29:ObjectSubclass[class_exact:C] = GuardType v12, ObjectSubclass[class_exact:C] + v29:ObjectSubclass[class_exact:C] = GuardType v12, ObjectSubclass[class_exact:C] recompile v30:CBool = IsBitEqual v29, v13 v31:BoolExact = BoxBool v30 CheckInterrupts @@ -7764,7 +7778,7 @@ mod hir_opt_tests { Jump bb3(v7, v8, v9) bb3(v11:BasicObject, v12:BasicObject, v13:BasicObject): PatchPoint MethodRedefined(Integer@0x1008, &@0x1010, cme:0x1018) - v28:Fixnum = GuardType v12, Fixnum + v28:Fixnum = GuardType v12, Fixnum recompile v29:Fixnum = GuardType v13, Fixnum v30:Fixnum = FixnumAnd v28, v29 CheckInterrupts @@ -7796,7 +7810,7 @@ mod hir_opt_tests { Jump bb3(v7, v8, v9) bb3(v11:BasicObject, v12:BasicObject, v13:BasicObject): PatchPoint MethodRedefined(Integer@0x1008, |@0x1010, cme:0x1018) - v28:Fixnum = GuardType v12, Fixnum + v28:Fixnum = GuardType v12, Fixnum recompile v29:Fixnum = GuardType v13, Fixnum v30:Fixnum = FixnumOr v28, v29 CheckInterrupts @@ -7825,7 +7839,7 @@ mod hir_opt_tests { Jump bb3(v4) bb3(v6:BasicObject): PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) - v18:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v18:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile v20:NilClass = Const Value(nil) CheckInterrupts Return v20 @@ -7863,7 +7877,7 @@ mod hir_opt_tests { bb3(v9:BasicObject, v10:BasicObject): PatchPoint NoSingletonClass(C@0x1008) PatchPoint MethodRedefined(C@0x1008, foo@0x1010, cme:0x1018) - v23:ObjectSubclass[class_exact:C] = GuardType v10, ObjectSubclass[class_exact:C] + v23:ObjectSubclass[class_exact:C] = GuardType v10, ObjectSubclass[class_exact:C] recompile v26:CShape = LoadField v23, :shape_id@0x1040 v27:CShape[0x1041] = GuardBitEquals v26, CShape(0x1041) recompile v28:BasicObject = LoadField v23, :@foo@0x1042 @@ -7906,7 +7920,7 @@ mod hir_opt_tests { bb3(v9:BasicObject, v10:BasicObject): PatchPoint NoSingletonClass(C@0x1008) PatchPoint MethodRedefined(C@0x1008, foo@0x1010, cme:0x1018) - v23:ObjectSubclass[class_exact:C] = GuardType v10, ObjectSubclass[class_exact:C] + v23:ObjectSubclass[class_exact:C] = GuardType v10, ObjectSubclass[class_exact:C] recompile v24:BasicObject = GetIvar v23, :@foo CheckInterrupts Return v24 @@ -8379,7 +8393,7 @@ mod hir_opt_tests { bb3(v9:HeapBasicObject, v10:BasicObject): v17:Fixnum[5] = Const Value(5) PatchPoint MethodRedefined(C@0x1008, foo=@0x1010, cme:0x1018) - v28:ObjectSubclass[class_exact:C] = GuardType v10, ObjectSubclass[class_exact:C] + v28:ObjectSubclass[class_exact:C] = GuardType v10, ObjectSubclass[class_exact:C] recompile v31:CShape = LoadField v28, :shape_id@0x1040 v32:CShape[0x1041] = GuardBitEquals v31, CShape(0x1041) StoreField v28, :@foo@0x1042, v17 @@ -8746,7 +8760,7 @@ mod hir_opt_tests { bb4(v13:BasicObject): v34:Fixnum[1] = Const Value(1) PatchPoint MethodRedefined(Integer@0x1008, +@0x1010, cme:0x1018) - v45:Fixnum = GuardType v13, Fixnum + v45:Fixnum = GuardType v13, Fixnum recompile v46:Fixnum = FixnumAdd v45, v34 CheckInterrupts Return v46 @@ -8828,7 +8842,7 @@ mod hir_opt_tests { bb4(v13:BasicObject): v34:Fixnum[1] = Const Value(1) PatchPoint MethodRedefined(Integer@0x1008, +@0x1010, cme:0x1018) - v50:Fixnum = GuardType v13, Fixnum + v50:Fixnum = GuardType v13, Fixnum recompile v51:Fixnum = FixnumAdd v50, v34 CheckInterrupts Return v51 @@ -8898,7 +8912,7 @@ mod hir_opt_tests { bb4(v13:BasicObject): v33:Fixnum[1] = Const Value(1) PatchPoint MethodRedefined(Integer@0x1008, +@0x1010, cme:0x1018) - v44:Fixnum = GuardType v13, Fixnum + v44:Fixnum = GuardType v13, Fixnum recompile v45:Fixnum = FixnumAdd v44, v33 CheckInterrupts Return v45 @@ -9061,7 +9075,7 @@ mod hir_opt_tests { bb3(v9:BasicObject, v10:BasicObject): PatchPoint NoSingletonClass(C@0x1008) PatchPoint MethodRedefined(C@0x1008, foo@0x1010, cme:0x1018) - v23:ObjectSubclass[class_exact:C] = GuardType v10, ObjectSubclass[class_exact:C] + v23:ObjectSubclass[class_exact:C] = GuardType v10, ObjectSubclass[class_exact:C] recompile v24:BasicObject = GetIvar v23, :@foo CheckInterrupts Return v24 @@ -9283,7 +9297,7 @@ mod hir_opt_tests { Jump bb3(v4) bb3(v6:BasicObject): PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) - v18:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v18:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile v19:BasicObject = SendDirect v18, 0x1038, :foo (0x1048) CheckInterrupts Return v19 @@ -9313,7 +9327,7 @@ mod hir_opt_tests { Jump bb3(v4) bb3(v6:BasicObject): PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) - v18:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v18:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile v19:BasicObject = SendDirect v18, 0x1038, :foo (0x1048) CheckInterrupts Return v19 @@ -9344,7 +9358,7 @@ mod hir_opt_tests { bb3(v6:BasicObject): PatchPoint SingleRactorMode PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) - v19:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v19:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile v20:BasicObject = SendDirect v19, 0x1038, :foo (0x1048) CheckInterrupts Return v20 @@ -9450,7 +9464,7 @@ mod hir_opt_tests { bb3(v9:BasicObject, v10:BasicObject): PatchPoint NoSingletonClass(C@0x1008) PatchPoint MethodRedefined(C@0x1008, foo@0x1010, cme:0x1018) - v23:ObjectSubclass[class_exact:C] = GuardType v10, ObjectSubclass[class_exact:C] + v23:ObjectSubclass[class_exact:C] = GuardType v10, ObjectSubclass[class_exact:C] recompile v26:CShape = LoadField v23, :shape_id@0x1040 v27:CShape[0x1041] = GuardBitEquals v26, CShape(0x1041) recompile v28:NilClass = Const Value(nil) @@ -9486,7 +9500,7 @@ mod hir_opt_tests { bb3(v9:BasicObject, v10:BasicObject): PatchPoint NoSingletonClass(C@0x1008) PatchPoint MethodRedefined(C@0x1008, foo@0x1010, cme:0x1018) - v23:ObjectSubclass[class_exact:C] = GuardType v10, ObjectSubclass[class_exact:C] + v23:ObjectSubclass[class_exact:C] = GuardType v10, ObjectSubclass[class_exact:C] recompile v26:CShape = LoadField v23, :shape_id@0x1040 v27:CShape[0x1041] = GuardBitEquals v26, CShape(0x1041) recompile v28:NilClass = Const Value(nil) @@ -9522,7 +9536,7 @@ mod hir_opt_tests { bb3(v9:BasicObject, v10:BasicObject): v17:Fixnum[5] = Const Value(5) PatchPoint MethodRedefined(C@0x1008, foo=@0x1010, cme:0x1018) - v28:ObjectSubclass[class_exact:C] = GuardType v10, ObjectSubclass[class_exact:C] + v28:ObjectSubclass[class_exact:C] = GuardType v10, ObjectSubclass[class_exact:C] recompile v31:CShape = LoadField v28, :shape_id@0x1040 v32:CShape[0x1041] = GuardBitEquals v31, CShape(0x1041) StoreField v28, :@foo@0x1042, v17 @@ -9561,7 +9575,7 @@ mod hir_opt_tests { bb3(v9:BasicObject, v10:BasicObject): v17:Fixnum[5] = Const Value(5) PatchPoint MethodRedefined(C@0x1008, foo=@0x1010, cme:0x1018) - v28:ObjectSubclass[class_exact:C] = GuardType v10, ObjectSubclass[class_exact:C] + v28:ObjectSubclass[class_exact:C] = GuardType v10, ObjectSubclass[class_exact:C] recompile v31:CShape = LoadField v28, :shape_id@0x1040 v32:CShape[0x1041] = GuardBitEquals v31, CShape(0x1041) StoreField v28, :@foo@0x1042, v17 @@ -9597,7 +9611,7 @@ mod hir_opt_tests { bb3(v9:BasicObject, v10:BasicObject): PatchPoint NoSingletonClass(C@0x1008) PatchPoint MethodRedefined(C@0x1008, foo@0x1010, cme:0x1018) - v23:ObjectSubclass[class_exact:C] = GuardType v10, ObjectSubclass[class_exact:C] + v23:ObjectSubclass[class_exact:C] = GuardType v10, ObjectSubclass[class_exact:C] recompile v24:BasicObject = LoadField v23, :foo@0x1040 CheckInterrupts Return v24 @@ -9628,7 +9642,7 @@ mod hir_opt_tests { bb3(v9:BasicObject, v10:BasicObject): PatchPoint NoSingletonClass(C@0x1008) PatchPoint MethodRedefined(C@0x1008, foo@0x1010, cme:0x1018) - v23:ObjectSubclass[class_exact:C] = GuardType v10, ObjectSubclass[class_exact:C] + v23:ObjectSubclass[class_exact:C] = GuardType v10, ObjectSubclass[class_exact:C] recompile v24:CPtr = LoadField v23, :as_heap@0x1040 v25:BasicObject = LoadField v24, :foo@0x1041 CheckInterrupts @@ -9663,7 +9677,7 @@ mod hir_opt_tests { bb3(v9:BasicObject, v10:BasicObject): PatchPoint NoSingletonClass(C@0x1008) PatchPoint MethodRedefined(C@0x1008, foo@0x1010, cme:0x1018) - v27:ObjectSubclass[class_exact:C] = GuardType v10, ObjectSubclass[class_exact:C] + v27:ObjectSubclass[class_exact:C] = GuardType v10, ObjectSubclass[class_exact:C] recompile v19:Fixnum[5] = Const Value(5) CheckInterrupts Return v19 @@ -9697,7 +9711,7 @@ mod hir_opt_tests { bb3(v11:BasicObject, v12:BasicObject, v13:BasicObject): PatchPoint NoSingletonClass(C@0x1008) PatchPoint MethodRedefined(C@0x1008, foo=@0x1010, cme:0x1018) - v31:ObjectSubclass[class_exact:C] = GuardType v12, ObjectSubclass[class_exact:C] + v31:ObjectSubclass[class_exact:C] = GuardType v12, ObjectSubclass[class_exact:C] recompile v32:CUInt64 = LoadField v31, :RBASIC_FLAGS@0x1040 v33:CUInt64 = GuardNoBitsSet v32, RUBY_FL_FREEZE=CUInt64(2048) StoreField v31, :foo=@0x1041, v13 @@ -9734,7 +9748,7 @@ mod hir_opt_tests { bb3(v11:BasicObject, v12:BasicObject, v13:BasicObject): PatchPoint NoSingletonClass(C@0x1008) PatchPoint MethodRedefined(C@0x1008, foo=@0x1010, cme:0x1018) - v31:ObjectSubclass[class_exact:C] = GuardType v12, ObjectSubclass[class_exact:C] + v31:ObjectSubclass[class_exact:C] = GuardType v12, ObjectSubclass[class_exact:C] recompile v32:CUInt64 = LoadField v31, :RBASIC_FLAGS@0x1040 v33:CUInt64 = GuardNoBitsSet v32, RUBY_FL_FREEZE=CUInt64(2048) v34:CPtr = LoadField v31, :as_heap@0x1041 @@ -9897,7 +9911,7 @@ mod hir_opt_tests { bb3(v9:BasicObject, v10:BasicObject): PatchPoint NoSingletonClass(String@0x1008) PatchPoint MethodRedefined(String@0x1008, to_s@0x1010, cme:0x1018) - v24:StringExact = GuardType v10, StringExact + v24:StringExact = GuardType v10, StringExact recompile CheckInterrupts Return v24 "); @@ -9924,7 +9938,7 @@ mod hir_opt_tests { Jump bb3(v6, v7) bb3(v9:BasicObject, v10:BasicObject): PatchPoint MethodRedefined(Integer@0x1008, to_s@0x1010, cme:0x1018) - v23:Fixnum = GuardType v10, Fixnum + v23:Fixnum = GuardType v10, Fixnum recompile v24:StringExact = CCallVariadic v23, :Integer#to_s@0x1040 CheckInterrupts Return v24 @@ -9952,7 +9966,7 @@ mod hir_opt_tests { Jump bb3(v6, v7) bb3(v9:BasicObject, v10:BasicObject): PatchPoint MethodRedefined(Integer@0x1008, to_s@0x1010, cme:0x1018) - v23:Bignum = GuardType v10, Bignum + v23:Bignum = GuardType v10, Bignum recompile v24:StringExact = CCallVariadic v23, :Integer#to_s@0x1040 CheckInterrupts Return v24 @@ -10050,7 +10064,7 @@ mod hir_opt_tests { bb3(v11:BasicObject, v12:BasicObject, v13:BasicObject): PatchPoint NoSingletonClass(Array@0x1008) PatchPoint MethodRedefined(Array@0x1008, []@0x1010, cme:0x1018) - v29:ArrayExact = GuardType v12, ArrayExact + v29:ArrayExact = GuardType v12, ArrayExact recompile v30:Fixnum = GuardType v13, Fixnum v31:CInt64 = UnboxFixnum v30 v32:CInt64 = ArrayLength v29 @@ -10091,7 +10105,7 @@ mod hir_opt_tests { bb3(v11:BasicObject, v12:BasicObject, v13:BasicObject): PatchPoint NoSingletonClass(C@0x1008) PatchPoint MethodRedefined(C@0x1008, []@0x1010, cme:0x1018) - v29:ArraySubclass[class_exact:C] = GuardType v12, ArraySubclass[class_exact:C] + v29:ArraySubclass[class_exact:C] = GuardType v12, ArraySubclass[class_exact:C] recompile v30:Fixnum = GuardType v13, Fixnum v31:CInt64 = UnboxFixnum v30 v32:CInt64 = ArrayLength v29 @@ -10163,7 +10177,7 @@ mod hir_opt_tests { bb3(v11:BasicObject, v12:BasicObject, v13:BasicObject): PatchPoint NoSingletonClass(Hash@0x1008) PatchPoint MethodRedefined(Hash@0x1008, []@0x1010, cme:0x1018) - v29:HashExact = GuardType v12, HashExact + v29:HashExact = GuardType v12, HashExact recompile v30:BasicObject = HashAref v29, v13 CheckInterrupts Return v30 @@ -10197,7 +10211,7 @@ mod hir_opt_tests { bb3(v11:BasicObject, v12:BasicObject, v13:BasicObject): PatchPoint NoSingletonClass(C@0x1008) PatchPoint MethodRedefined(C@0x1008, []@0x1010, cme:0x1018) - v29:HashSubclass[class_exact:C] = GuardType v12, HashSubclass[class_exact:C] + v29:HashSubclass[class_exact:C] = GuardType v12, HashSubclass[class_exact:C] recompile v30:BasicObject = CCallWithFrame v29, :Hash#[]@0x1040, v13 CheckInterrupts Return v30 @@ -10295,7 +10309,7 @@ mod hir_opt_tests { bb3(v13:BasicObject, v14:BasicObject, v15:BasicObject, v16:BasicObject): PatchPoint NoSingletonClass(Hash@0x1008) PatchPoint MethodRedefined(Hash@0x1008, []=@0x1010, cme:0x1018) - v37:HashExact = GuardType v14, HashExact + v37:HashExact = GuardType v14, HashExact recompile HashAset v37, v15, v16 CheckInterrupts Return v16 @@ -10331,7 +10345,7 @@ mod hir_opt_tests { bb3(v13:BasicObject, v14:BasicObject, v15:BasicObject, v16:BasicObject): PatchPoint NoSingletonClass(C@0x1008) PatchPoint MethodRedefined(C@0x1008, []=@0x1010, cme:0x1018) - v37:HashSubclass[class_exact:C] = GuardType v14, HashSubclass[class_exact:C] + v37:HashSubclass[class_exact:C] = GuardType v14, HashSubclass[class_exact:C] recompile v38:BasicObject = CCallWithFrame v37, :Hash#[]=@0x1040, v15, v16 CheckInterrupts Return v16 @@ -10393,7 +10407,7 @@ mod hir_opt_tests { v19:Fixnum[10] = Const Value(10) PatchPoint NoSingletonClass(Array@0x1008) PatchPoint MethodRedefined(Array@0x1008, []=@0x1010, cme:0x1018) - v33:ArrayExact = GuardType v10, ArrayExact + v33:ArrayExact = GuardType v10, ArrayExact recompile v34:CUInt64 = LoadField v33, :RBASIC_FLAGS@0x1040 v35:CUInt64 = GuardNoBitsSet v34, RUBY_FL_FREEZE=CUInt64(2048) v37:CUInt64 = GuardNoBitsSet v35, RUBY_ELTS_SHARED=CUInt64(4096) @@ -10435,7 +10449,7 @@ mod hir_opt_tests { bb3(v13:BasicObject, v14:BasicObject, v15:BasicObject, v16:BasicObject): PatchPoint NoSingletonClass(Array@0x1008) PatchPoint MethodRedefined(Array@0x1008, []=@0x1010, cme:0x1018) - v37:ArrayExact = GuardType v14, ArrayExact + v37:ArrayExact = GuardType v14, ArrayExact recompile v38:Fixnum = GuardType v15, Fixnum v39:CUInt64 = LoadField v37, :RBASIC_FLAGS@0x1040 v40:CUInt64 = GuardNoBitsSet v39, RUBY_FL_FREEZE=CUInt64(2048) @@ -10483,7 +10497,7 @@ mod hir_opt_tests { bb3(v13:BasicObject, v14:BasicObject, v15:BasicObject, v16:BasicObject): PatchPoint NoSingletonClass(MyArray@0x1008) PatchPoint MethodRedefined(MyArray@0x1008, []=@0x1010, cme:0x1018) - v37:ArraySubclass[class_exact:MyArray] = GuardType v14, ArraySubclass[class_exact:MyArray] + v37:ArraySubclass[class_exact:MyArray] = GuardType v14, ArraySubclass[class_exact:MyArray] recompile v38:BasicObject = CCallVariadic v37, :Array#[]=@0x1040, v15, v16 CheckInterrupts Return v16 @@ -10515,7 +10529,7 @@ mod hir_opt_tests { v15:Fixnum[1] = Const Value(1) PatchPoint NoSingletonClass(Array@0x1008) PatchPoint MethodRedefined(Array@0x1008, <<@0x1010, cme:0x1018) - v27:ArrayExact = GuardType v10, ArrayExact + v27:ArrayExact = GuardType v10, ArrayExact recompile v28:CUInt64 = LoadField v27, :RBASIC_FLAGS@0x1040 v29:CUInt64 = GuardNoBitsSet v28, RUBY_FL_FREEZE=CUInt64(2048) ArrayPush v27, v15 @@ -10549,7 +10563,7 @@ mod hir_opt_tests { v15:Fixnum[1] = Const Value(1) PatchPoint NoSingletonClass(Array@0x1008) PatchPoint MethodRedefined(Array@0x1008, push@0x1010, cme:0x1018) - v26:ArrayExact = GuardType v10, ArrayExact + v26:ArrayExact = GuardType v10, ArrayExact recompile v27:CUInt64 = LoadField v26, :RBASIC_FLAGS@0x1040 v28:CUInt64 = GuardNoBitsSet v27, RUBY_FL_FREEZE=CUInt64(2048) ArrayPush v26, v15 @@ -10585,7 +10599,7 @@ mod hir_opt_tests { v19:Fixnum[3] = Const Value(3) PatchPoint NoSingletonClass(Array@0x1008) PatchPoint MethodRedefined(Array@0x1008, push@0x1010, cme:0x1018) - v30:ArrayExact = GuardType v10, ArrayExact + v30:ArrayExact = GuardType v10, ArrayExact recompile v31:BasicObject = CCallVariadic v30, :Array#push@0x1040, v15, v17, v19 CheckInterrupts Return v31 @@ -10767,7 +10781,7 @@ mod hir_opt_tests { bb3(v9:BasicObject, v10:BasicObject): PatchPoint NoSingletonClass(Array@0x1008) PatchPoint MethodRedefined(Array@0x1008, length@0x1010, cme:0x1018) - v25:ArrayExact = GuardType v10, ArrayExact + v25:ArrayExact = GuardType v10, ArrayExact recompile v26:CInt64 = ArrayLength v25 v27:Fixnum = BoxFixnum v26 CheckInterrupts @@ -10798,7 +10812,7 @@ mod hir_opt_tests { bb3(v9:BasicObject, v10:BasicObject): PatchPoint NoSingletonClass(Array@0x1008) PatchPoint MethodRedefined(Array@0x1008, size@0x1010, cme:0x1018) - v25:ArrayExact = GuardType v10, ArrayExact + v25:ArrayExact = GuardType v10, ArrayExact recompile v26:CInt64 = ArrayLength v25 v27:Fixnum = BoxFixnum v26 CheckInterrupts @@ -10830,7 +10844,7 @@ mod hir_opt_tests { v15:RegexpExact[VALUE(0x1008)] = Const Value(VALUE(0x1008)) PatchPoint NoSingletonClass(String@0x1010) PatchPoint MethodRedefined(String@0x1010, =~@0x1018, cme:0x1020) - v27:StringExact = GuardType v10, StringExact + v27:StringExact = GuardType v10, StringExact recompile v28:BasicObject = CCallWithFrame v27, :String#=~@0x1048, v15 CheckInterrupts Return v28 @@ -10861,7 +10875,7 @@ mod hir_opt_tests { bb3(v11:BasicObject, v12:BasicObject, v13:BasicObject): PatchPoint NoSingletonClass(String@0x1008) PatchPoint MethodRedefined(String@0x1008, getbyte@0x1010, cme:0x1018) - v28:StringExact = GuardType v12, StringExact + v28:StringExact = GuardType v12, StringExact recompile v29:Fixnum = GuardType v13, Fixnum v30:CInt64 = UnboxFixnum v29 v31:CInt64 = LoadField v28, :len@0x1040 @@ -10902,7 +10916,7 @@ mod hir_opt_tests { bb3(v11:BasicObject, v12:BasicObject, v13:BasicObject): PatchPoint NoSingletonClass(String@0x1008) PatchPoint MethodRedefined(String@0x1008, getbyte@0x1010, cme:0x1018) - v32:StringExact = GuardType v12, StringExact + v32:StringExact = GuardType v12, StringExact recompile v33:Fixnum = GuardType v13, Fixnum v34:CInt64 = UnboxFixnum v33 v35:CInt64 = LoadField v32, :len@0x1040 @@ -10944,7 +10958,7 @@ mod hir_opt_tests { bb3(v13:BasicObject, v14:BasicObject, v15:BasicObject, v16:BasicObject): PatchPoint NoSingletonClass(String@0x1008) PatchPoint MethodRedefined(String@0x1008, setbyte@0x1010, cme:0x1018) - v32:StringExact = GuardType v14, StringExact + v32:StringExact = GuardType v14, StringExact recompile v33:Fixnum = GuardType v15, Fixnum v34:Fixnum = GuardType v16, Fixnum v35:CInt64 = UnboxFixnum v33 @@ -10991,7 +11005,7 @@ mod hir_opt_tests { bb3(v13:BasicObject, v14:BasicObject, v15:BasicObject, v16:BasicObject): PatchPoint NoSingletonClass(MyString@0x1008) PatchPoint MethodRedefined(MyString@0x1008, setbyte@0x1010, cme:0x1018) - v32:StringSubclass[class_exact:MyString] = GuardType v14, StringSubclass[class_exact:MyString] + v32:StringSubclass[class_exact:MyString] = GuardType v14, StringSubclass[class_exact:MyString] recompile v33:Fixnum = GuardType v15, Fixnum v34:Fixnum = GuardType v16, Fixnum v35:CInt64 = UnboxFixnum v33 @@ -11036,7 +11050,7 @@ mod hir_opt_tests { bb3(v13:BasicObject, v14:BasicObject, v15:BasicObject, v16:BasicObject): PatchPoint NoSingletonClass(String@0x1008) PatchPoint MethodRedefined(String@0x1008, setbyte@0x1010, cme:0x1018) - v32:StringExact = GuardType v14, StringExact + v32:StringExact = GuardType v14, StringExact recompile v33:BasicObject = CCallWithFrame v32, :String#setbyte@0x1040, v15, v16 CheckInterrupts Return v33 @@ -11067,7 +11081,7 @@ mod hir_opt_tests { bb3(v9:BasicObject, v10:BasicObject): PatchPoint NoSingletonClass(String@0x1008) PatchPoint MethodRedefined(String@0x1008, empty?@0x1010, cme:0x1018) - v25:StringExact = GuardType v10, StringExact + v25:StringExact = GuardType v10, StringExact recompile v26:CInt64 = LoadField v25, :len@0x1040 v27:CInt64[0] = Const CInt64(0) v28:CBool = IsBitEqual v26, v27 @@ -11102,7 +11116,7 @@ mod hir_opt_tests { bb3(v9:BasicObject, v10:BasicObject): PatchPoint NoSingletonClass(String@0x1008) PatchPoint MethodRedefined(String@0x1008, empty?@0x1010, cme:0x1018) - v29:StringExact = GuardType v10, StringExact + v29:StringExact = GuardType v10, StringExact recompile v20:Fixnum[4] = Const Value(4) CheckInterrupts Return v20 @@ -11131,7 +11145,7 @@ mod hir_opt_tests { Jump bb3(v6, v7) bb3(v9:BasicObject, v10:BasicObject): PatchPoint MethodRedefined(Integer@0x1008, succ@0x1010, cme:0x1018) - v24:Fixnum = GuardType v10, Fixnum + v24:Fixnum = GuardType v10, Fixnum recompile v25:Fixnum[1] = Const Value(1) v26:Fixnum = FixnumAdd v24, v25 CheckInterrupts @@ -11161,7 +11175,7 @@ mod hir_opt_tests { Jump bb3(v6, v7) bb3(v9:BasicObject, v10:BasicObject): PatchPoint MethodRedefined(Integer@0x1008, succ@0x1010, cme:0x1018) - v24:Bignum = GuardType v10, Bignum + v24:Bignum = GuardType v10, Bignum recompile v25:BasicObject = CCallWithFrame v24, :Integer#succ@0x1040 CheckInterrupts Return v25 @@ -11191,7 +11205,7 @@ mod hir_opt_tests { bb3(v9:BasicObject, v10:BasicObject): v15:Fixnum[5] = Const Value(5) PatchPoint MethodRedefined(Integer@0x1008, <<@0x1010, cme:0x1018) - v26:Fixnum = GuardType v10, Fixnum + v26:Fixnum = GuardType v10, Fixnum recompile v27:Fixnum = FixnumLShift v26, v15 CheckInterrupts Return v27 @@ -11221,7 +11235,7 @@ mod hir_opt_tests { bb3(v9:BasicObject, v10:BasicObject): v15:Fixnum[-5] = Const Value(-5) PatchPoint MethodRedefined(Integer@0x1008, <<@0x1010, cme:0x1018) - v26:Fixnum = GuardType v10, Fixnum + v26:Fixnum = GuardType v10, Fixnum recompile v27:BasicObject = CCallWithFrame v26, :Integer#<<@0x1040, v15 CheckInterrupts Return v27 @@ -11251,7 +11265,7 @@ mod hir_opt_tests { bb3(v9:BasicObject, v10:BasicObject): v15:Fixnum[64] = Const Value(64) PatchPoint MethodRedefined(Integer@0x1008, <<@0x1010, cme:0x1018) - v26:Fixnum = GuardType v10, Fixnum + v26:Fixnum = GuardType v10, Fixnum recompile v27:BasicObject = CCallWithFrame v26, :Integer#<<@0x1040, v15 CheckInterrupts Return v27 @@ -11282,7 +11296,7 @@ mod hir_opt_tests { Jump bb3(v7, v8, v9) bb3(v11:BasicObject, v12:BasicObject, v13:BasicObject): PatchPoint MethodRedefined(Integer@0x1008, <<@0x1010, cme:0x1018) - v28:Fixnum = GuardType v12, Fixnum + v28:Fixnum = GuardType v12, Fixnum recompile v29:BasicObject = CCallWithFrame v28, :Integer#<<@0x1040, v13 CheckInterrupts Return v29 @@ -11311,7 +11325,7 @@ mod hir_opt_tests { bb3(v9:BasicObject, v10:BasicObject): v15:Fixnum[5] = Const Value(5) PatchPoint MethodRedefined(Integer@0x1008, >>@0x1010, cme:0x1018) - v25:Fixnum = GuardType v10, Fixnum + v25:Fixnum = GuardType v10, Fixnum recompile v26:Fixnum = FixnumRShift v25, v15 CheckInterrupts Return v26 @@ -11340,7 +11354,7 @@ mod hir_opt_tests { bb3(v9:BasicObject, v10:BasicObject): v15:Fixnum[-5] = Const Value(-5) PatchPoint MethodRedefined(Integer@0x1008, >>@0x1010, cme:0x1018) - v25:Fixnum = GuardType v10, Fixnum + v25:Fixnum = GuardType v10, Fixnum recompile v26:BasicObject = CCallWithFrame v25, :Integer#>>@0x1040, v15 CheckInterrupts Return v26 @@ -11369,7 +11383,7 @@ mod hir_opt_tests { bb3(v9:BasicObject, v10:BasicObject): v15:Fixnum[64] = Const Value(64) PatchPoint MethodRedefined(Integer@0x1008, >>@0x1010, cme:0x1018) - v25:Fixnum = GuardType v10, Fixnum + v25:Fixnum = GuardType v10, Fixnum recompile v26:BasicObject = CCallWithFrame v25, :Integer#>>@0x1040, v15 CheckInterrupts Return v26 @@ -11399,7 +11413,7 @@ mod hir_opt_tests { Jump bb3(v7, v8, v9) bb3(v11:BasicObject, v12:BasicObject, v13:BasicObject): PatchPoint MethodRedefined(Integer@0x1008, >>@0x1010, cme:0x1018) - v27:Fixnum = GuardType v12, Fixnum + v27:Fixnum = GuardType v12, Fixnum recompile v28:BasicObject = CCallWithFrame v27, :Integer#>>@0x1040, v13 CheckInterrupts Return v28 @@ -11430,7 +11444,7 @@ mod hir_opt_tests { bb3(v11:BasicObject, v12:BasicObject, v13:BasicObject): PatchPoint NoSingletonClass(String@0x1008) PatchPoint MethodRedefined(String@0x1008, <<@0x1010, cme:0x1018) - v29:StringExact = GuardType v12, StringExact + v29:StringExact = GuardType v12, StringExact recompile v30:String = GuardType v13, String v31:StringExact = StringAppend v29, v30 CheckInterrupts @@ -11462,7 +11476,7 @@ mod hir_opt_tests { bb3(v11:BasicObject, v12:BasicObject, v13:BasicObject): PatchPoint NoSingletonClass(String@0x1008) PatchPoint MethodRedefined(String@0x1008, <<@0x1010, cme:0x1018) - v29:StringExact = GuardType v12, StringExact + v29:StringExact = GuardType v12, StringExact recompile v30:Fixnum = GuardType v13, Fixnum v31:StringExact = StringAppendCodepoint v29, v30 CheckInterrupts @@ -11496,7 +11510,7 @@ mod hir_opt_tests { bb3(v11:BasicObject, v12:BasicObject, v13:BasicObject): PatchPoint NoSingletonClass(String@0x1008) PatchPoint MethodRedefined(String@0x1008, <<@0x1010, cme:0x1018) - v29:StringExact = GuardType v12, StringExact + v29:StringExact = GuardType v12, StringExact recompile v30:String = GuardType v13, String v31:StringExact = StringAppend v29, v30 CheckInterrupts @@ -11530,7 +11544,7 @@ mod hir_opt_tests { bb3(v11:BasicObject, v12:BasicObject, v13:BasicObject): PatchPoint NoSingletonClass(MyString@0x1008) PatchPoint MethodRedefined(MyString@0x1008, <<@0x1010, cme:0x1018) - v29:StringSubclass[class_exact:MyString] = GuardType v12, StringSubclass[class_exact:MyString] + v29:StringSubclass[class_exact:MyString] = GuardType v12, StringSubclass[class_exact:MyString] recompile v30:BasicObject = CCallWithFrame v29, :String#<<@0x1040, v13 CheckInterrupts Return v30 @@ -11613,7 +11627,7 @@ mod hir_opt_tests { bb3(v9:BasicObject, v10:BasicObject): PatchPoint NoSingletonClass(String@0x1008) PatchPoint MethodRedefined(String@0x1008, ascii_only?@0x1010, cme:0x1018) - v24:StringExact = GuardType v10, StringExact + v24:StringExact = GuardType v10, StringExact recompile v25:BoolExact = CCall v24, :String#ascii_only?@0x1040 CheckInterrupts Return v25 @@ -11691,7 +11705,7 @@ mod hir_opt_tests { Jump bb3(v7, v8, v9) bb3(v11:BasicObject, v12:BasicObject, v13:BasicObject): PatchPoint MethodRedefined(Integer@0x1008, ^@0x1010, cme:0x1018) - v27:Fixnum = GuardType v12, Fixnum + v27:Fixnum = GuardType v12, Fixnum recompile v28:Fixnum = GuardType v13, Fixnum v29:Fixnum = FixnumXor v27, v28 CheckInterrupts @@ -11725,7 +11739,7 @@ mod hir_opt_tests { Jump bb3(v7, v8, v9) bb3(v11:BasicObject, v12:BasicObject, v13:BasicObject): PatchPoint MethodRedefined(Integer@0x1008, ^@0x1010, cme:0x1018) - v31:Fixnum = GuardType v12, Fixnum + v31:Fixnum = GuardType v12, Fixnum recompile v32:Fixnum = GuardType v13, Fixnum v23:Fixnum[42] = Const Value(42) CheckInterrupts @@ -11756,7 +11770,7 @@ mod hir_opt_tests { Jump bb3(v7, v8, v9) bb3(v11:BasicObject, v12:BasicObject, v13:BasicObject): PatchPoint MethodRedefined(Integer@0x1008, ^@0x1010, cme:0x1018) - v27:Bignum = GuardType v12, Bignum + v27:Bignum = GuardType v12, Bignum recompile v28:BasicObject = CCallWithFrame v27, :Integer#^@0x1040, v13 CheckInterrupts Return v28 @@ -11786,7 +11800,7 @@ mod hir_opt_tests { Jump bb3(v7, v8, v9) bb3(v11:BasicObject, v12:BasicObject, v13:BasicObject): PatchPoint MethodRedefined(Integer@0x1008, ^@0x1010, cme:0x1018) - v27:Fixnum = GuardType v12, Fixnum + v27:Fixnum = GuardType v12, Fixnum recompile v28:BasicObject = CCallWithFrame v27, :Integer#^@0x1040, v13 CheckInterrupts Return v28 @@ -11816,7 +11830,7 @@ mod hir_opt_tests { Jump bb3(v7, v8, v9) bb3(v11:BasicObject, v12:BasicObject, v13:BasicObject): PatchPoint MethodRedefined(TrueClass@0x1008, ^@0x1010, cme:0x1018) - v27:TrueClass = GuardType v12, TrueClass + v27:TrueClass = GuardType v12, TrueClass recompile v28:BasicObject = CCallWithFrame v27, :TrueClass#^@0x1040, v13 CheckInterrupts Return v28 @@ -11870,7 +11884,7 @@ mod hir_opt_tests { bb3(v9:BasicObject, v10:BasicObject): PatchPoint NoSingletonClass(Hash@0x1008) PatchPoint MethodRedefined(Hash@0x1008, size@0x1010, cme:0x1018) - v25:HashExact = GuardType v10, HashExact + v25:HashExact = GuardType v10, HashExact recompile v26:Fixnum = CCall v25, :Hash#size@0x1040 CheckInterrupts Return v26 @@ -11902,7 +11916,7 @@ mod hir_opt_tests { bb3(v9:BasicObject, v10:BasicObject): PatchPoint NoSingletonClass(Hash@0x1008) PatchPoint MethodRedefined(Hash@0x1008, size@0x1010, cme:0x1018) - v29:HashExact = GuardType v10, HashExact + v29:HashExact = GuardType v10, HashExact recompile v20:Fixnum[5] = Const Value(5) CheckInterrupts Return v20 @@ -11935,7 +11949,7 @@ mod hir_opt_tests { v15:StaticSymbol[:foo] = Const Value(VALUE(0x1008)) PatchPoint NoSingletonClass(C@0x1010) PatchPoint MethodRedefined(C@0x1010, respond_to?@0x1018, cme:0x1020) - v26:ObjectSubclass[class_exact:C] = GuardType v10, ObjectSubclass[class_exact:C] + v26:ObjectSubclass[class_exact:C] = GuardType v10, ObjectSubclass[class_exact:C] recompile PatchPoint MethodRedefined(C@0x1010, foo@0x1048, cme:0x1050) v30:TrueClass = Const Value(true) CheckInterrupts @@ -11968,7 +11982,7 @@ mod hir_opt_tests { v15:StaticSymbol[:foo] = Const Value(VALUE(0x1008)) PatchPoint NoSingletonClass(C@0x1010) PatchPoint MethodRedefined(C@0x1010, respond_to?@0x1018, cme:0x1020) - v26:ObjectSubclass[class_exact:C] = GuardType v10, ObjectSubclass[class_exact:C] + v26:ObjectSubclass[class_exact:C] = GuardType v10, ObjectSubclass[class_exact:C] recompile PatchPoint MethodRedefined(C@0x1010, respond_to_missing?@0x1048, cme:0x1050) PatchPoint MethodRedefined(C@0x1010, foo@0x1078, cme:0x1080) v32:FalseClass = Const Value(false) @@ -12004,7 +12018,7 @@ mod hir_opt_tests { v15:StaticSymbol[:foo] = Const Value(VALUE(0x1008)) PatchPoint NoSingletonClass(C@0x1010) PatchPoint MethodRedefined(C@0x1010, respond_to?@0x1018, cme:0x1020) - v26:ObjectSubclass[class_exact:C] = GuardType v10, ObjectSubclass[class_exact:C] + v26:ObjectSubclass[class_exact:C] = GuardType v10, ObjectSubclass[class_exact:C] recompile PatchPoint MethodRedefined(C@0x1010, foo@0x1048, cme:0x1050) v30:FalseClass = Const Value(false) CheckInterrupts @@ -12040,7 +12054,7 @@ mod hir_opt_tests { v17:FalseClass = Const Value(false) PatchPoint NoSingletonClass(C@0x1010) PatchPoint MethodRedefined(C@0x1010, respond_to?@0x1018, cme:0x1020) - v28:ObjectSubclass[class_exact:C] = GuardType v10, ObjectSubclass[class_exact:C] + v28:ObjectSubclass[class_exact:C] = GuardType v10, ObjectSubclass[class_exact:C] recompile PatchPoint MethodRedefined(C@0x1010, foo@0x1048, cme:0x1050) v32:FalseClass = Const Value(false) CheckInterrupts @@ -12076,7 +12090,7 @@ mod hir_opt_tests { v17:NilClass = Const Value(nil) PatchPoint NoSingletonClass(C@0x1010) PatchPoint MethodRedefined(C@0x1010, respond_to?@0x1018, cme:0x1020) - v28:ObjectSubclass[class_exact:C] = GuardType v10, ObjectSubclass[class_exact:C] + v28:ObjectSubclass[class_exact:C] = GuardType v10, ObjectSubclass[class_exact:C] recompile PatchPoint MethodRedefined(C@0x1010, foo@0x1048, cme:0x1050) v32:FalseClass = Const Value(false) CheckInterrupts @@ -12112,7 +12126,7 @@ mod hir_opt_tests { v17:TrueClass = Const Value(true) PatchPoint NoSingletonClass(C@0x1010) PatchPoint MethodRedefined(C@0x1010, respond_to?@0x1018, cme:0x1020) - v28:ObjectSubclass[class_exact:C] = GuardType v10, ObjectSubclass[class_exact:C] + v28:ObjectSubclass[class_exact:C] = GuardType v10, ObjectSubclass[class_exact:C] recompile PatchPoint MethodRedefined(C@0x1010, foo@0x1048, cme:0x1050) v32:TrueClass = Const Value(true) CheckInterrupts @@ -12147,7 +12161,7 @@ mod hir_opt_tests { v17:Fixnum[4] = Const Value(4) PatchPoint NoSingletonClass(C@0x1010) PatchPoint MethodRedefined(C@0x1010, respond_to?@0x1018, cme:0x1020) - v28:ObjectSubclass[class_exact:C] = GuardType v10, ObjectSubclass[class_exact:C] + v28:ObjectSubclass[class_exact:C] = GuardType v10, ObjectSubclass[class_exact:C] recompile PatchPoint MethodRedefined(C@0x1010, foo@0x1048, cme:0x1050) v32:TrueClass = Const Value(true) CheckInterrupts @@ -12182,7 +12196,7 @@ mod hir_opt_tests { v17:NilClass = Const Value(nil) PatchPoint NoSingletonClass(C@0x1010) PatchPoint MethodRedefined(C@0x1010, respond_to?@0x1018, cme:0x1020) - v28:ObjectSubclass[class_exact:C] = GuardType v10, ObjectSubclass[class_exact:C] + v28:ObjectSubclass[class_exact:C] = GuardType v10, ObjectSubclass[class_exact:C] recompile PatchPoint MethodRedefined(C@0x1010, foo@0x1048, cme:0x1050) v32:TrueClass = Const Value(true) CheckInterrupts @@ -12215,7 +12229,7 @@ mod hir_opt_tests { v15:StaticSymbol[:foo] = Const Value(VALUE(0x1008)) PatchPoint NoSingletonClass(C@0x1010) PatchPoint MethodRedefined(C@0x1010, respond_to?@0x1018, cme:0x1020) - v26:ObjectSubclass[class_exact:C] = GuardType v10, ObjectSubclass[class_exact:C] + v26:ObjectSubclass[class_exact:C] = GuardType v10, ObjectSubclass[class_exact:C] recompile PatchPoint MethodRedefined(C@0x1010, respond_to_missing?@0x1048, cme:0x1050) PatchPoint MethodRedefined(C@0x1010, foo@0x1078, cme:0x1080) v32:FalseClass = Const Value(false) @@ -12252,7 +12266,7 @@ mod hir_opt_tests { v15:StaticSymbol[:foo] = Const Value(VALUE(0x1008)) PatchPoint NoSingletonClass(C@0x1010) PatchPoint MethodRedefined(C@0x1010, respond_to?@0x1018, cme:0x1020) - v26:ObjectSubclass[class_exact:C] = GuardType v10, ObjectSubclass[class_exact:C] + v26:ObjectSubclass[class_exact:C] = GuardType v10, ObjectSubclass[class_exact:C] recompile v27:BasicObject = CCallVariadic v26, :Kernel#respond_to?@0x1048, v15 CheckInterrupts Return v27 @@ -12278,7 +12292,7 @@ mod hir_opt_tests { Jump bb3(v4) bb3(v6:BasicObject): PatchPoint MethodRedefined(Object@0x1000, callee@0x1008, cme:0x1010) - v18:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v18:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile CheckInterrupts Return v18 "); @@ -12304,7 +12318,7 @@ mod hir_opt_tests { Jump bb3(v4) bb3(v6:BasicObject): PatchPoint MethodRedefined(Object@0x1000, callee@0x1008, cme:0x1010) - v18:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v18:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile v20:StringExact[VALUE(0x1038)] = Const Value(VALUE(0x1038)) CheckInterrupts Return v20 @@ -12330,7 +12344,7 @@ mod hir_opt_tests { Jump bb3(v4) bb3(v6:BasicObject): PatchPoint MethodRedefined(Object@0x1000, callee@0x1008, cme:0x1010) - v18:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v18:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile v20:NilClass = Const Value(nil) CheckInterrupts Return v20 @@ -12356,7 +12370,7 @@ mod hir_opt_tests { Jump bb3(v4) bb3(v6:BasicObject): PatchPoint MethodRedefined(Object@0x1000, callee@0x1008, cme:0x1010) - v18:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v18:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile v20:TrueClass = Const Value(true) CheckInterrupts Return v20 @@ -12382,7 +12396,7 @@ mod hir_opt_tests { Jump bb3(v4) bb3(v6:BasicObject): PatchPoint MethodRedefined(Object@0x1000, callee@0x1008, cme:0x1010) - v18:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v18:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile v20:FalseClass = Const Value(false) CheckInterrupts Return v20 @@ -12408,7 +12422,7 @@ mod hir_opt_tests { Jump bb3(v4) bb3(v6:BasicObject): PatchPoint MethodRedefined(Object@0x1000, callee@0x1008, cme:0x1010) - v18:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v18:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile v20:Fixnum[0] = Const Value(0) CheckInterrupts Return v20 @@ -12434,7 +12448,7 @@ mod hir_opt_tests { Jump bb3(v4) bb3(v6:BasicObject): PatchPoint MethodRedefined(Object@0x1000, callee@0x1008, cme:0x1010) - v18:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v18:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile v20:Fixnum[1] = Const Value(1) CheckInterrupts Return v20 @@ -12461,7 +12475,7 @@ mod hir_opt_tests { bb3(v6:BasicObject): v11:Fixnum[3] = Const Value(3) PatchPoint MethodRedefined(Object@0x1000, callee@0x1008, cme:0x1010) - v20:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v20:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile CheckInterrupts Return v11 "); @@ -12489,7 +12503,7 @@ mod hir_opt_tests { v13:Fixnum[2] = Const Value(2) v15:Fixnum[3] = Const Value(3) PatchPoint MethodRedefined(Object@0x1000, callee@0x1008, cme:0x1010) - v24:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v24:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile CheckInterrupts Return v15 "); @@ -12559,7 +12573,7 @@ mod hir_opt_tests { Jump bb3(v6, v7) bb3(v9:BasicObject, v10:BasicObject): PatchPoint MethodRedefined(Symbol@0x1008, to_sym@0x1010, cme:0x1018) - v22:StaticSymbol = GuardType v10, StaticSymbol + v22:StaticSymbol = GuardType v10, StaticSymbol recompile CheckInterrupts Return v22 "); @@ -12586,7 +12600,7 @@ mod hir_opt_tests { Jump bb3(v6, v7) bb3(v9:BasicObject, v10:BasicObject): PatchPoint MethodRedefined(Integer@0x1008, to_i@0x1010, cme:0x1018) - v22:Fixnum = GuardType v10, Fixnum + v22:Fixnum = GuardType v10, Fixnum recompile CheckInterrupts Return v22 "); @@ -12776,7 +12790,7 @@ mod hir_opt_tests { bb3(v11:BasicObject, v12:BasicObject, v13:BasicObject): PatchPoint NoSingletonClass(String@0x1008) PatchPoint MethodRedefined(String@0x1008, ==@0x1010, cme:0x1018) - v29:StringExact = GuardType v12, StringExact + v29:StringExact = GuardType v12, StringExact recompile v30:String = GuardType v13, String v31:BoolExact = StringEqual v29, v30 CheckInterrupts @@ -12810,7 +12824,7 @@ mod hir_opt_tests { bb3(v11:BasicObject, v12:BasicObject, v13:BasicObject): PatchPoint NoSingletonClass(C@0x1008) PatchPoint MethodRedefined(C@0x1008, ==@0x1010, cme:0x1018) - v29:StringSubclass[class_exact:C] = GuardType v12, StringSubclass[class_exact:C] + v29:StringSubclass[class_exact:C] = GuardType v12, StringSubclass[class_exact:C] recompile v30:String = GuardType v13, String v31:BoolExact = StringEqual v29, v30 CheckInterrupts @@ -12844,7 +12858,7 @@ mod hir_opt_tests { bb3(v11:BasicObject, v12:BasicObject, v13:BasicObject): PatchPoint NoSingletonClass(String@0x1008) PatchPoint MethodRedefined(String@0x1008, ==@0x1010, cme:0x1018) - v29:StringExact = GuardType v12, StringExact + v29:StringExact = GuardType v12, StringExact recompile v30:String = GuardType v13, String v31:BoolExact = StringEqual v29, v30 CheckInterrupts @@ -12876,7 +12890,7 @@ mod hir_opt_tests { bb3(v11:BasicObject, v12:BasicObject, v13:BasicObject): PatchPoint NoSingletonClass(String@0x1008) PatchPoint MethodRedefined(String@0x1008, ===@0x1010, cme:0x1018) - v28:StringExact = GuardType v12, StringExact + v28:StringExact = GuardType v12, StringExact recompile v29:String = GuardType v13, String v30:BoolExact = StringEqual v28, v29 CheckInterrupts @@ -12910,7 +12924,7 @@ mod hir_opt_tests { bb3(v11:BasicObject, v12:BasicObject, v13:BasicObject): PatchPoint NoSingletonClass(C@0x1008) PatchPoint MethodRedefined(C@0x1008, ===@0x1010, cme:0x1018) - v28:StringSubclass[class_exact:C] = GuardType v12, StringSubclass[class_exact:C] + v28:StringSubclass[class_exact:C] = GuardType v12, StringSubclass[class_exact:C] recompile v29:String = GuardType v13, String v30:BoolExact = StringEqual v28, v29 CheckInterrupts @@ -12944,7 +12958,7 @@ mod hir_opt_tests { bb3(v11:BasicObject, v12:BasicObject, v13:BasicObject): PatchPoint NoSingletonClass(String@0x1008) PatchPoint MethodRedefined(String@0x1008, ===@0x1010, cme:0x1018) - v28:StringExact = GuardType v12, StringExact + v28:StringExact = GuardType v12, StringExact recompile v29:String = GuardType v13, String v30:BoolExact = StringEqual v28, v29 CheckInterrupts @@ -12974,7 +12988,7 @@ mod hir_opt_tests { bb3(v9:BasicObject, v10:BasicObject): PatchPoint NoSingletonClass(String@0x1008) PatchPoint MethodRedefined(String@0x1008, ==@0x1010, cme:0x1018) - v26:StringExact = GuardType v10, StringExact + v26:StringExact = GuardType v10, StringExact recompile v29:TrueClass = Const Value(true) CheckInterrupts Return v29 @@ -13003,7 +13017,7 @@ mod hir_opt_tests { bb3(v9:BasicObject, v10:BasicObject): PatchPoint NoSingletonClass(String@0x1008) PatchPoint MethodRedefined(String@0x1008, ===@0x1010, cme:0x1018) - v25:StringExact = GuardType v10, StringExact + v25:StringExact = GuardType v10, StringExact recompile v28:TrueClass = Const Value(true) CheckInterrupts Return v28 @@ -13277,7 +13291,7 @@ mod hir_opt_tests { bb3(v11:BasicObject, v12:BasicObject, v13:BasicObject): PatchPoint NoSingletonClass(String@0x1008) PatchPoint MethodRedefined(String@0x1008, ==@0x1010, cme:0x1018) - v29:StringExact = GuardType v12, StringExact + v29:StringExact = GuardType v12, StringExact recompile v30:String = GuardType v13, String v31:BoolExact = StringEqual v29, v30 CheckInterrupts @@ -13344,7 +13358,7 @@ mod hir_opt_tests { v15:NilClass = Const Value(nil) PatchPoint NoSingletonClass(String@0x1008) PatchPoint MethodRedefined(String@0x1008, !=@0x1010, cme:0x1018) - v27:StringExact = GuardType v10, StringExact + v27:StringExact = GuardType v10, StringExact recompile v28:BoolExact = CCallWithFrame v27, :BasicObject#!=@0x1040, v15 CheckInterrupts Return v28 @@ -13377,7 +13391,7 @@ mod hir_opt_tests { bb3(v11:BasicObject, v12:BasicObject, v13:BasicObject): PatchPoint NoSingletonClass(String@0x1008) PatchPoint MethodRedefined(String@0x1008, !=@0x1010, cme:0x1018) - v29:StringExact = GuardType v12, StringExact + v29:StringExact = GuardType v12, StringExact recompile PatchPoint MethodRedefined(String@0x1008, ==@0x1040, cme:0x1048) v33:String = GuardType v13, String v34:BoolExact = StringEqual v29, v33 @@ -13413,7 +13427,7 @@ mod hir_opt_tests { bb3(v9:BasicObject, v10:BasicObject): PatchPoint NoSingletonClass(String@0x1008) PatchPoint MethodRedefined(String@0x1008, !=@0x1010, cme:0x1018) - v26:StringExact = GuardType v10, StringExact + v26:StringExact = GuardType v10, StringExact recompile PatchPoint MethodRedefined(String@0x1008, ==@0x1040, cme:0x1048) v35:TrueClass = Const Value(true) v32:TrueClass = Const Value(true) @@ -13448,7 +13462,7 @@ mod hir_opt_tests { bb3(v9:BasicObject, v10:BasicObject): PatchPoint NoSingletonClass(String@0x1008) PatchPoint MethodRedefined(String@0x1008, size@0x1010, cme:0x1018) - v25:StringExact = GuardType v10, StringExact + v25:StringExact = GuardType v10, StringExact recompile v26:Fixnum = CCall v25, :String#size@0x1040 CheckInterrupts Return v26 @@ -13480,7 +13494,7 @@ mod hir_opt_tests { bb3(v9:BasicObject, v10:BasicObject): PatchPoint NoSingletonClass(String@0x1008) PatchPoint MethodRedefined(String@0x1008, size@0x1010, cme:0x1018) - v29:StringExact = GuardType v10, StringExact + v29:StringExact = GuardType v10, StringExact recompile v20:Fixnum[5] = Const Value(5) CheckInterrupts Return v20 @@ -13511,7 +13525,7 @@ mod hir_opt_tests { bb3(v9:BasicObject, v10:BasicObject): PatchPoint NoSingletonClass(String@0x1008) PatchPoint MethodRedefined(String@0x1008, bytesize@0x1010, cme:0x1018) - v24:StringExact = GuardType v10, StringExact + v24:StringExact = GuardType v10, StringExact recompile v25:CInt64 = LoadField v24, :len@0x1040 v26:Fixnum = BoxFixnum v25 CheckInterrupts @@ -13544,7 +13558,7 @@ mod hir_opt_tests { bb3(v9:BasicObject, v10:BasicObject): PatchPoint NoSingletonClass(String@0x1008) PatchPoint MethodRedefined(String@0x1008, bytesize@0x1010, cme:0x1018) - v28:StringExact = GuardType v10, StringExact + v28:StringExact = GuardType v10, StringExact recompile v19:Fixnum[5] = Const Value(5) CheckInterrupts Return v19 @@ -13575,7 +13589,7 @@ mod hir_opt_tests { bb3(v9:BasicObject, v10:BasicObject): PatchPoint NoSingletonClass(String@0x1008) PatchPoint MethodRedefined(String@0x1008, length@0x1010, cme:0x1018) - v25:StringExact = GuardType v10, StringExact + v25:StringExact = GuardType v10, StringExact recompile v26:Fixnum = CCall v25, :String#length@0x1040 CheckInterrupts Return v26 @@ -13669,7 +13683,7 @@ mod hir_opt_tests { v25:ClassSubclass[String@0x1010] = Const Value(VALUE(0x1010)) PatchPoint NoSingletonClass(String@0x1010) PatchPoint MethodRedefined(String@0x1010, is_a?@0x1011, cme:0x1018) - v29:StringExact = GuardType v10, StringExact + v29:StringExact = GuardType v10, StringExact recompile v30:BoolExact = IsA v29, v25 CheckInterrupts Return v30 @@ -13701,7 +13715,7 @@ mod hir_opt_tests { v25:ModuleSubclass[Kernel@0x1010] = Const Value(VALUE(0x1010)) PatchPoint NoSingletonClass(String@0x1018) PatchPoint MethodRedefined(String@0x1018, is_a?@0x1020, cme:0x1028) - v29:StringExact = GuardType v10, StringExact + v29:StringExact = GuardType v10, StringExact recompile v30:BasicObject = CCallWithFrame v29, :Kernel#is_a?@0x1050, v25 CheckInterrupts Return v30 @@ -13736,7 +13750,7 @@ mod hir_opt_tests { v29:ClassSubclass[Integer@0x1010] = Const Value(VALUE(0x1010)) PatchPoint NoSingletonClass(String@0x1018) PatchPoint MethodRedefined(String@0x1018, is_a?@0x1020, cme:0x1028) - v33:StringExact = GuardType v10, StringExact + v33:StringExact = GuardType v10, StringExact recompile v21:Fixnum[5] = Const Value(5) CheckInterrupts Return v21 @@ -13802,7 +13816,7 @@ mod hir_opt_tests { v25:ClassSubclass[String@0x1010] = Const Value(VALUE(0x1010)) PatchPoint NoSingletonClass(String@0x1010) PatchPoint MethodRedefined(String@0x1010, kind_of?@0x1011, cme:0x1018) - v29:StringExact = GuardType v10, StringExact + v29:StringExact = GuardType v10, StringExact recompile v30:BoolExact = IsA v29, v25 CheckInterrupts Return v30 @@ -13834,7 +13848,7 @@ mod hir_opt_tests { v25:ModuleSubclass[Kernel@0x1010] = Const Value(VALUE(0x1010)) PatchPoint NoSingletonClass(String@0x1018) PatchPoint MethodRedefined(String@0x1018, kind_of?@0x1020, cme:0x1028) - v29:StringExact = GuardType v10, StringExact + v29:StringExact = GuardType v10, StringExact recompile v30:BasicObject = CCallWithFrame v29, :Kernel#kind_of?@0x1050, v25 CheckInterrupts Return v30 @@ -13869,7 +13883,7 @@ mod hir_opt_tests { v29:ClassSubclass[Integer@0x1010] = Const Value(VALUE(0x1010)) PatchPoint NoSingletonClass(String@0x1018) PatchPoint MethodRedefined(String@0x1018, kind_of?@0x1020, cme:0x1028) - v33:StringExact = GuardType v10, StringExact + v33:StringExact = GuardType v10, StringExact recompile v21:Fixnum[5] = Const Value(5) CheckInterrupts Return v21 @@ -14100,7 +14114,7 @@ mod hir_opt_tests { bb3(v9:BasicObject, v10:BasicObject): PatchPoint NoSingletonClass(String@0x1008) PatchPoint MethodRedefined(String@0x1008, length@0x1010, cme:0x1018) - v29:StringExact = GuardType v10, StringExact + v29:StringExact = GuardType v10, StringExact recompile v20:Fixnum[4] = Const Value(4) CheckInterrupts Return v20 @@ -14140,7 +14154,7 @@ mod hir_opt_tests { bb3(v6:BasicObject): PatchPoint NoSingletonClass(C@0x1000) PatchPoint MethodRedefined(C@0x1000, class@0x1008, cme:0x1010) - v43:ObjectSubclass[class_exact:C] = GuardType v6, ObjectSubclass[class_exact:C] + v43:ObjectSubclass[class_exact:C] = GuardType v6, ObjectSubclass[class_exact:C] recompile v46:ClassSubclass[C@0x1000] = Const Value(VALUE(0x1000)) v13:StaticSymbol[:_lex_actions] = Const Value(VALUE(0x1038)) v15:TrueClass = Const Value(true) @@ -14176,7 +14190,7 @@ mod hir_opt_tests { bb3(v9:BasicObject, v10:BasicObject): PatchPoint NoSingletonClass(C@0x1008) PatchPoint MethodRedefined(C@0x1008, class@0x1010, cme:0x1018) - v25:ObjectSubclass[class_exact:C] = GuardType v10, ObjectSubclass[class_exact:C] + v25:ObjectSubclass[class_exact:C] = GuardType v10, ObjectSubclass[class_exact:C] recompile v28:ClassSubclass[C@0x1008] = Const Value(VALUE(0x1008)) PatchPoint MethodRedefined(Class@0x1040, name@0x1048, cme:0x1050) v32:StringExact|NilClass = CCall v28, :Module#name@0x1078 @@ -14208,7 +14222,7 @@ mod hir_opt_tests { bb3(v9:BasicObject, v10:BasicObject): PatchPoint NoSingletonClass(C@0x1008) PatchPoint MethodRedefined(C@0x1008, class@0x1010, cme:0x1018) - v23:ObjectSubclass[class_exact:C] = GuardType v10, ObjectSubclass[class_exact:C] + v23:ObjectSubclass[class_exact:C] = GuardType v10, ObjectSubclass[class_exact:C] recompile v26:ClassSubclass[C@0x1008] = Const Value(VALUE(0x1008)) CheckInterrupts Return v26 @@ -14258,7 +14272,7 @@ mod hir_opt_tests { Jump bb3(v4) bb3(v6:BasicObject): PatchPoint MethodRedefined(Object@0x1000, class@0x1008, cme:0x1010) - v18:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] + v18:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile v21:ClassSubclass[Object@0x1038] = Const Value(VALUE(0x1038)) CheckInterrupts Return v21 @@ -14699,7 +14713,7 @@ mod hir_opt_tests { bb3(v9:BasicObject, v10:BasicObject): PatchPoint NoSingletonClass(TestDynamic@0x1008) PatchPoint MethodRedefined(TestDynamic@0x1008, val@0x1010, cme:0x1018) - v23:ObjectSubclass[class_exact:TestDynamic] = GuardType v10, ObjectSubclass[class_exact:TestDynamic] + v23:ObjectSubclass[class_exact:TestDynamic] = GuardType v10, ObjectSubclass[class_exact:TestDynamic] recompile v26:CShape = LoadField v23, :shape_id@0x1040 v27:CShape[0x1041] = GuardBitEquals v26, CShape(0x1041) recompile v28:BasicObject = LoadField v23, :@val@0x1042 @@ -14806,7 +14820,7 @@ mod hir_opt_tests { bb3(v6:BasicObject): PatchPoint NoSingletonClass(C@0x1000) PatchPoint MethodRedefined(C@0x1000, secret@0x1008, cme:0x1010) - v19:ObjectSubclass[class_exact:C] = GuardType v6, ObjectSubclass[class_exact:C] + v19:ObjectSubclass[class_exact:C] = GuardType v6, ObjectSubclass[class_exact:C] recompile v21:Fixnum[42] = Const Value(42) CheckInterrupts Return v21 @@ -14864,7 +14878,7 @@ mod hir_opt_tests { bb3(v6:BasicObject): PatchPoint NoSingletonClass(BasicObject@0x1000) PatchPoint MethodRedefined(BasicObject@0x1000, initialize@0x1008, cme:0x1010) - v21:BasicObjectExact = GuardType v6, BasicObjectExact + v21:BasicObjectExact = GuardType v6, BasicObjectExact recompile v22:NilClass = Const Value(nil) CheckInterrupts Return v22 @@ -14948,7 +14962,7 @@ mod hir_opt_tests { bb3(v6:BasicObject): PatchPoint NoSingletonClass(C@0x1000) PatchPoint MethodRedefined(C@0x1000, secret@0x1008, cme:0x1010) - v19:ObjectSubclass[class_exact:C] = GuardType v6, ObjectSubclass[class_exact:C] + v19:ObjectSubclass[class_exact:C] = GuardType v6, ObjectSubclass[class_exact:C] recompile v21:Fixnum[42] = Const Value(42) CheckInterrupts Return v21 @@ -15031,7 +15045,7 @@ mod hir_opt_tests { v19:BasicObject = Send v12, :length # SendFallbackReason: Singleton class previously created for receiver class PatchPoint NoSingletonClass(Proc@0x1008) PatchPoint MethodRedefined(Proc@0x1008, call@0x1010, cme:0x1018) - v40:ObjectSubclass[class_exact:Proc] = GuardType v13, ObjectSubclass[class_exact:Proc] + v40:ObjectSubclass[class_exact:Proc] = GuardType v13, ObjectSubclass[class_exact:Proc] recompile v41:BasicObject = InvokeProc v40 PatchPoint NoEPEscape(test) v32:BasicObject = Send v12, :length # SendFallbackReason: Singleton class previously created for receiver class @@ -15187,7 +15201,7 @@ mod hir_opt_tests { v33:BasicObject = SendDirect v9, 0x1058, :foo (0x1068), v10 v18:Fixnum[1] = Const Value(1) PatchPoint MethodRedefined(Integer@0x1070, +@0x1078, cme:0x1080) - v36:Fixnum = GuardType v33, Fixnum + v36:Fixnum = GuardType v33, Fixnum recompile v37:Fixnum = FixnumAdd v36, v18 CheckInterrupts Return v37 @@ -15448,7 +15462,7 @@ mod hir_opt_tests { bb3(v11:HeapBasicObject, v12:BasicObject, v13:NilClass): PatchPoint NoSingletonClass(B@0x1008) PatchPoint MethodRedefined(B@0x1008, proc@0x1010, cme:0x1018) - v39:ObjectSubclass[class_exact:B] = GuardType v11, ObjectSubclass[class_exact:B] + v39:ObjectSubclass[class_exact:B] = GuardType v11, ObjectSubclass[class_exact:B] recompile v40:BasicObject = CCallWithFrame v39, :Kernel#proc@0x1040, block=0x1048 v19:CPtr = GetEP 0 v20:BasicObject = LoadField v19, :blk@0x1050 @@ -15661,7 +15675,7 @@ mod hir_opt_tests { bb4(v35:BasicObject, v36:BasicObject, v37:BasicObject): v40:Fixnum[2] = Const Value(2) PatchPoint MethodRedefined(Integer@0x1070, +@0x1078, cme:0x1080) - v59:Fixnum = GuardType v37, Fixnum + v59:Fixnum = GuardType v37, Fixnum recompile v60:Fixnum = FixnumAdd v59, v40 CheckInterrupts Return v60 @@ -16078,7 +16092,7 @@ mod hir_opt_tests { bb11(): v71:Falsy = RefineType v60, Falsy PatchPoint MethodRedefined(Object@0x1010, lambda@0x1018, cme:0x1020) - v118:ObjectSubclass[class_exact*:Object@VALUE(0x1010)] = GuardType v58, ObjectSubclass[class_exact*:Object@VALUE(0x1010)] + v118:ObjectSubclass[class_exact*:Object@VALUE(0x1010)] = GuardType v58, ObjectSubclass[class_exact*:Object@VALUE(0x1010)] recompile v119:BasicObject = CCallWithFrame v118, :Kernel#lambda@0x1048, block=0x1050 v75:CPtr = GetEP 0 v76:BasicObject = LoadField v75, :list@0x1001 @@ -16387,7 +16401,7 @@ mod hir_opt_tests { v19:Truthy = RefineType v10, Truthy v23:Fixnum[42] = Const Value(42) PatchPoint MethodRedefined(Object@0x1008, greet_recompile@0x1010, cme:0x1018) - v43:ObjectSubclass[class_exact*:Object@VALUE(0x1008)] = GuardType v9, ObjectSubclass[class_exact*:Object@VALUE(0x1008)] + v43:ObjectSubclass[class_exact*:Object@VALUE(0x1008)] = GuardType v9, ObjectSubclass[class_exact*:Object@VALUE(0x1008)] recompile v44:BasicObject = SendDirect v43, 0x1040, :greet_recompile (0x1050), v23 CheckInterrupts Return v44 @@ -16541,7 +16555,7 @@ mod hir_opt_tests { bb3(v9:BasicObject, v10:BasicObject): v15:Fixnum[1] = Const Value(1) PatchPoint MethodRedefined(Integer@0x1008, -@0x1010, cme:0x1018) - v35:Fixnum = GuardType v10, Fixnum + v35:Fixnum = GuardType v10, Fixnum recompile v36:Fixnum = FixnumSub v35, v15 v21:Fixnum[2] = Const Value(2) v40:Fixnum = FixnumSub v35, v21 @@ -16731,7 +16745,7 @@ mod hir_opt_tests { Jump bb3(v6, v7) bb3(v9:BasicObject, v10:BasicObject): PatchPoint MethodRedefined(Float@0x1008, nan?@0x1010, cme:0x1018) - v23:Flonum = GuardType v10, Flonum + v23:Flonum = GuardType v10, Flonum recompile v24:BoolExact = CCall v23, :Float#nan?@0x1040 CheckInterrupts Return v24 @@ -16759,7 +16773,7 @@ mod hir_opt_tests { Jump bb3(v6, v7) bb3(v9:BasicObject, v10:BasicObject): PatchPoint MethodRedefined(Float@0x1008, finite?@0x1010, cme:0x1018) - v23:Flonum = GuardType v10, Flonum + v23:Flonum = GuardType v10, Flonum recompile v24:BoolExact = CCall v23, :Float#finite?@0x1040 CheckInterrupts Return v24 @@ -16787,7 +16801,7 @@ mod hir_opt_tests { Jump bb3(v6, v7) bb3(v9:BasicObject, v10:BasicObject): PatchPoint MethodRedefined(Float@0x1008, infinite?@0x1010, cme:0x1018) - v23:Flonum = GuardType v10, Flonum + v23:Flonum = GuardType v10, Flonum recompile v24:NilClass|Fixnum = CCall v23, :Float#infinite?@0x1040 CheckInterrupts Return v24 @@ -16815,7 +16829,7 @@ mod hir_opt_tests { Jump bb3(v6, v7) bb3(v9:BasicObject, v10:BasicObject): PatchPoint MethodRedefined(Integer@0x1008, even?@0x1010, cme:0x1018) - v22:Fixnum = GuardType v10, Fixnum + v22:Fixnum = GuardType v10, Fixnum recompile v24:BoolExact = InvokeBuiltin leaf , v22 CheckInterrupts Return v24 @@ -16843,7 +16857,7 @@ mod hir_opt_tests { Jump bb3(v6, v7) bb3(v9:BasicObject, v10:BasicObject): PatchPoint MethodRedefined(Integer@0x1008, odd?@0x1010, cme:0x1018) - v22:Fixnum = GuardType v10, Fixnum + v22:Fixnum = GuardType v10, Fixnum recompile v24:BoolExact = InvokeBuiltin leaf , v22 CheckInterrupts Return v24 @@ -16871,7 +16885,7 @@ mod hir_opt_tests { Jump bb3(v6, v7) bb3(v9:BasicObject, v10:BasicObject): PatchPoint MethodRedefined(Float@0x1008, zero?@0x1010, cme:0x1018) - v22:Flonum = GuardType v10, Flonum + v22:Flonum = GuardType v10, Flonum recompile v24:BoolExact = InvokeBuiltin leaf , v22 CheckInterrupts Return v24 @@ -16899,7 +16913,7 @@ mod hir_opt_tests { Jump bb3(v6, v7) bb3(v9:BasicObject, v10:BasicObject): PatchPoint MethodRedefined(Float@0x1008, positive?@0x1010, cme:0x1018) - v22:Flonum = GuardType v10, Flonum + v22:Flonum = GuardType v10, Flonum recompile v24:BoolExact = InvokeBuiltin leaf , v22 CheckInterrupts Return v24 @@ -16927,7 +16941,7 @@ mod hir_opt_tests { Jump bb3(v6, v7) bb3(v9:BasicObject, v10:BasicObject): PatchPoint MethodRedefined(Float@0x1008, negative?@0x1010, cme:0x1018) - v22:Flonum = GuardType v10, Flonum + v22:Flonum = GuardType v10, Flonum recompile v24:BoolExact = InvokeBuiltin leaf , v22 CheckInterrupts Return v24 @@ -16956,7 +16970,7 @@ mod hir_opt_tests { Jump bb3(v7, v8, v9) bb3(v11:BasicObject, v12:BasicObject, v13:BasicObject): PatchPoint MethodRedefined(Float@0x1008, +@0x1010, cme:0x1018) - v28:Flonum = GuardType v12, Flonum + v28:Flonum = GuardType v12, Flonum recompile v29:Flonum = GuardType v13, Flonum v30:Float = FloatAdd v28, v29 CheckInterrupts @@ -16987,7 +17001,7 @@ mod hir_opt_tests { Jump bb3(v7, v8, v9) bb3(v11:BasicObject, v12:BasicObject, v13:BasicObject): PatchPoint MethodRedefined(Float@0x1008, *@0x1010, cme:0x1018) - v28:Flonum = GuardType v12, Flonum + v28:Flonum = GuardType v12, Flonum recompile v29:Flonum = GuardType v13, Flonum v30:Float = FloatMul v28, v29 CheckInterrupts @@ -17018,7 +17032,7 @@ mod hir_opt_tests { Jump bb3(v7, v8, v9) bb3(v11:BasicObject, v12:BasicObject, v13:BasicObject): PatchPoint MethodRedefined(Float@0x1008, -@0x1010, cme:0x1018) - v28:Flonum = GuardType v12, Flonum + v28:Flonum = GuardType v12, Flonum recompile v29:Flonum = GuardType v13, Flonum v30:Float = FloatSub v28, v29 CheckInterrupts @@ -17049,7 +17063,7 @@ mod hir_opt_tests { Jump bb3(v7, v8, v9) bb3(v11:BasicObject, v12:BasicObject, v13:BasicObject): PatchPoint MethodRedefined(Float@0x1008, /@0x1010, cme:0x1018) - v28:Flonum = GuardType v12, Flonum + v28:Flonum = GuardType v12, Flonum recompile v29:Flonum = GuardType v13, Flonum v30:Float = FloatDiv v28, v29 CheckInterrupts @@ -17078,7 +17092,7 @@ mod hir_opt_tests { Jump bb3(v6, v7) bb3(v9:BasicObject, v10:BasicObject): PatchPoint MethodRedefined(Float@0x1008, to_i@0x1010, cme:0x1018) - v23:Flonum = GuardType v10, Flonum + v23:Flonum = GuardType v10, Flonum recompile v24:Integer = FloatToInt v23 CheckInterrupts Return v24 @@ -17108,7 +17122,7 @@ mod hir_opt_tests { Jump bb3(v7, v8, v9) bb3(v11:BasicObject, v12:BasicObject, v13:BasicObject): PatchPoint MethodRedefined(Float@0x1008, *@0x1010, cme:0x1018) - v28:Flonum = GuardType v12, Flonum + v28:Flonum = GuardType v12, Flonum recompile v29:Fixnum = GuardType v13, Fixnum v30:Float = FloatMul v28, v29 CheckInterrupts @@ -17155,7 +17169,7 @@ mod hir_opt_tests { v17:Fixnum[0] = Const Value(0) PatchPoint NoSingletonClass(C@0x1008) PatchPoint MethodRedefined(C@0x1008, var@0x1010, cme:0x1018) - v138:ObjectSubclass[class_exact:C] = GuardType v12, ObjectSubclass[class_exact:C] + v138:ObjectSubclass[class_exact:C] = GuardType v12, ObjectSubclass[class_exact:C] recompile v139:BasicObject = LoadField v138, :var@0x1040 PatchPoint MethodRedefined(Integer@0x1048, +@0x1050, cme:0x1058) v179:Fixnum = GuardType v139, Fixnum @@ -17263,7 +17277,7 @@ mod hir_opt_tests { Jump bb3(v7, v8, v9) bb3(v11:BasicObject, v12:BasicObject, v13:BasicObject): PatchPoint MethodRedefined(Symbol@0x1008, ==@0x1010, cme:0x1018) - v48:StaticSymbol = GuardType v12, StaticSymbol + v48:StaticSymbol = GuardType v12, StaticSymbol recompile v49:CBool = IsBitEqual v48, v13 v50:BoolExact = BoxBool v49 CheckInterrupts @@ -17278,4 +17292,117 @@ mod hir_opt_tests { Return v40 "); } + + #[test] + fn test_trigger_guard_type_recompilation() { + eval(" + class C + def f(x) + @a = 1 + y = x + 1 + @a = y + end + end + + # As of 06/04/2026, zjit/src/options.rs uses 5 as the default number of profiles + # Let's pick a number that is reasonably larger to ensure compilation, even if + # the default value changes a bit + num_to_compile = 30 + + c = C.new + + # Repeatedly call an integer until this fast path gets JITed + num_to_compile.times { c.f(1) } + + "); + + let intermediate_hir = hir_string_proc("C.new.method(:f)"); + + eval(" + # Supposed to be the same as the earlier Ruby method in this test + num_to_compile = 30 + c = C.new + # Call this with a float in order to trigger a guard failure + # Do this enough times to cause a recompilation + num_to_compile.times { c.f(1.5) } + "); + + let final_hir = hir_string_proc("C.new.method(:f)"); + + assert_snapshot!(format!("{intermediate_hir}\n{final_hir}"), @" + fn f@:4: + bb1(): + EntryPoint interpreter + v1:HeapBasicObject = LoadSelf + v2:CPtr = LoadSP + v3:BasicObject = LoadField v2, :x@0x1000 + v4:NilClass = Const Value(nil) + Jump bb3(v1, v3, v4) + bb2(): + EntryPoint JIT(0) + v7:HeapBasicObject = LoadArg :self@0 + v8:BasicObject = LoadArg :x@1 + v9:NilClass = Const Value(nil) + Jump bb3(v7, v8, v9) + bb3(v11:HeapBasicObject, v12:BasicObject, v13:NilClass): + v17:Fixnum[1] = Const Value(1) + PatchPoint SingleRactorMode + SetIvar v11, :@a, v17 + PatchPoint NoEPEscape(f) + v27:Fixnum[1] = Const Value(1) + PatchPoint MethodRedefined(Integer@0x1008, +@0x1010, cme:0x1018) + v46:Fixnum = GuardType v12, Fixnum recompile + v47:Fixnum = FixnumAdd v46, v27 + PatchPoint SingleRactorMode + SetIvar v11, :@a, v47 + CheckInterrupts + Return v47 + + fn f@:4: + bb1(): + EntryPoint interpreter + v1:HeapBasicObject = LoadSelf + v2:CPtr = LoadSP + v3:BasicObject = LoadField v2, :x@0x1000 + v4:NilClass = Const Value(nil) + Jump bb3(v1, v3, v4) + bb2(): + EntryPoint JIT(0) + v7:HeapBasicObject = LoadArg :self@0 + v8:BasicObject = LoadArg :x@1 + v9:NilClass = Const Value(nil) + Jump bb3(v7, v8, v9) + bb3(v11:HeapBasicObject, v12:BasicObject, v13:NilClass): + v17:Fixnum[1] = Const Value(1) + PatchPoint SingleRactorMode + SetIvar v11, :@a, v17 + PatchPoint NoEPEscape(f) + v27:Fixnum[1] = Const Value(1) + v30:CBool = HasType v12, Flonum + CondBranch v30, bb5(v11, v12, v13, v12, v27), bb6() + bb5(v31:HeapBasicObject, v32:BasicObject, v33:NilClass, v34:BasicObject, v35:Fixnum[1]): + v37:Flonum = RefineType v34, Flonum + PatchPoint MethodRedefined(Float@0x1008, +@0x1010, cme:0x1018) + v74:Float = FloatAdd v37, v35 + Jump bb4(v31, v32, v33, v74) + bb6(): + v41:CBool = HasType v12, Fixnum + CondBranch v41, bb7(v11, v12, v13, v12, v27), bb8() + bb7(v42:HeapBasicObject, v43:BasicObject, v44:NilClass, v45:BasicObject, v46:Fixnum[1]): + v48:Fixnum = RefineType v45, Fixnum + PatchPoint MethodRedefined(Integer@0x1040, +@0x1010, cme:0x1048) + v77:Fixnum = FixnumAdd v48, v46 + Jump bb4(v42, v43, v44, v77) + bb8(): + PatchPoint MethodRedefined(Float@0x1008, +@0x1010, cme:0x1018) + v80:Flonum = GuardType v12, Flonum recompile + v81:Float = FloatAdd v80, v27 + Jump bb4(v11, v80, v13, v81) + bb4(v54:HeapBasicObject, v55:BasicObject, v56:NilClass, v57:Float|Fixnum): + PatchPoint SingleRactorMode + SetIvar v54, :@a, v57 + CheckInterrupts + Return v57 + "); + } } diff --git a/zjit/src/hir/tests.rs b/zjit/src/hir/tests.rs index a17a5262f4af07..09ca3687e230d5 100644 --- a/zjit/src/hir/tests.rs +++ b/zjit/src/hir/tests.rs @@ -120,7 +120,7 @@ mod snapshot_tests { v16:Any = Snapshot FrameState { pc: 0x1008, stack: [v6, v11, v13, v15], locals: [] } v23:Any = Snapshot FrameState { pc: 0x1008, stack: [v6, v13, v15, v11], locals: [] } PatchPoint MethodRedefined(Object@0x1010, foo@0x1018, cme:0x1020) - v25:ObjectSubclass[class_exact*:Object@VALUE(0x1010)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1010)] + v25:ObjectSubclass[class_exact*:Object@VALUE(0x1010)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1010)] recompile v26:BasicObject = SendDirect v25, 0x1048, :foo (0x1058), v13, v15, v11 v18:Any = Snapshot FrameState { pc: 0x1060, stack: [v26], locals: [] } PatchPoint NoTracePoint @@ -156,7 +156,7 @@ mod snapshot_tests { v13:Fixnum[2] = Const Value(2) v14:Any = Snapshot FrameState { pc: 0x1008, stack: [v6, v11, v13], locals: [] } PatchPoint MethodRedefined(Object@0x1010, foo@0x1018, cme:0x1020) - v22:ObjectSubclass[class_exact*:Object@VALUE(0x1010)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1010)] + v22:ObjectSubclass[class_exact*:Object@VALUE(0x1010)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1010)] recompile v23:BasicObject = SendDirect v22, 0x1048, :foo (0x1058), v11, v13 v16:Any = Snapshot FrameState { pc: 0x1060, stack: [v23], locals: [] } PatchPoint NoTracePoint diff --git a/zjit/src/payload.rs b/zjit/src/payload.rs index 807c33b8b76fe2..51b6f4721bf40e 100644 --- a/zjit/src/payload.rs +++ b/zjit/src/payload.rs @@ -3,6 +3,7 @@ use std::ptr::NonNull; use crate::codegen::IseqCallRef; use crate::stats::CompileError; use crate::{cruby::*, profile::IseqProfile, virtualmem::CodePtr}; +use crate::options::get_option; pub use crate::jit_frame::JITFrame; @@ -35,6 +36,14 @@ impl IseqPayload { self_is_heap_object: false, } } + + /// Profile counts are used for compilation policy. + /// When we deoptimize a method that can be recompiled, we need to update the count to collect more profiles. + /// Otherwise, we will generate the same code that was just deoptimized. + pub fn reset_profiles_remaining(&mut self, insn_idx: YarvInsnIdx) { + let num_profiles = get_option!(num_profiles); + self.profile.entry_mut(insn_idx).set_profiles_remaining(num_profiles); + } } /// JIT code version. When the same ISEQ is compiled with a different assumption, a new version is created. diff --git a/zjit/src/profile.rs b/zjit/src/profile.rs index 000c424da48ca0..56a0f9bc3d1152 100644 --- a/zjit/src/profile.rs +++ b/zjit/src/profile.rs @@ -417,7 +417,7 @@ impl ProfiledType { /// Per-instruction profile entry, stored sparsely in a sorted Vec. #[derive(Debug)] -struct ProfileEntry { +pub struct ProfileEntry { /// YARV instruction index insn_idx: u32, /// Type information of YARV instruction operands @@ -426,6 +426,12 @@ struct ProfileEntry { profiles_remaining: NumProfiles, } +impl ProfileEntry { + pub fn set_profiles_remaining(&mut self, num_profiles: NumProfiles) { + self.profiles_remaining = num_profiles; + } +} + #[derive(Debug)] pub struct IseqProfile { /// Sparse storage of per-instruction profile data, sorted by instruction index. @@ -445,7 +451,7 @@ impl IseqProfile { } /// Get or create a mutable profile entry for the given instruction index. - fn entry_mut(&mut self, insn_idx: YarvInsnIdx) -> &mut ProfileEntry { + pub fn entry_mut(&mut self, insn_idx: YarvInsnIdx) -> &mut ProfileEntry { let idx = insn_idx as u32; match self.entries.binary_search_by_key(&idx, |e| e.insn_idx) { Ok(i) => &mut self.entries[i], From 08efe07a7b9881d54b32c9ed8f098859840aac4a Mon Sep 17 00:00:00 2001 From: Benoit Daloze Date: Fri, 5 Jun 2026 23:13:47 +0200 Subject: [PATCH 156/188] [ruby/json] ALWAYS_INLINE for json_match_keyword() https://github.com/ruby/json/commit/7b284d4c1d --- ext/json/parser/parser.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/json/parser/parser.c b/ext/json/parser/parser.c index 308b47c37304b7..fbfbfac9dae3ea 100644 --- a/ext/json/parser/parser.c +++ b/ext/json/parser/parser.c @@ -1454,7 +1454,7 @@ static inline void json_value_completed(json_frame *frame) frame->phase = (enum json_frame_phase) frame->type; } -static inline bool json_match_keyword(JSON_ParserState *state, const char *keyword, size_t offset) +ALWAYS_INLINE(static) bool json_match_keyword(JSON_ParserState *state, const char *keyword, size_t offset) { // It is assumed that since `keyword` is always a literal, the compiler is able to constantize this // `strlen` and several other computations in that routine, such as eliminating the `if (resumable)` branch. From 954f4dc4c82712d19f9f2a392ae262654cdc4a68 Mon Sep 17 00:00:00 2001 From: Satoshi Tagomori Date: Sat, 6 Jun 2026 13:38:45 +0900 Subject: [PATCH 157/188] [Box] make the tmp array safe * Using rb_ary_hidden_new() to hide this array from ObjectSpace.each_object * Call RB_GC_GUARD to not collect this value in GC --- vm_method.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/vm_method.c b/vm_method.c index fb34426bf269a7..59906f9b164a0f 100644 --- a/vm_method.c +++ b/vm_method.c @@ -522,7 +522,7 @@ clear_method_cache_by_id_in_class(VALUE klass, ID mid) // not see these origins via RCLASS_ORIGIN(owner), so we find them by // iterating all of owner's classexts and checking their origin_ fields. { - VALUE origins = rb_ary_new(); + VALUE origins = rb_ary_hidden_new(1); struct collect_per_box_origins_arg origins_arg = { .owner = owner, .klass_housing_cme = klass_housing_cme, @@ -532,6 +532,7 @@ clear_method_cache_by_id_in_class(VALUE klass, ID mid) for (long i = 0; i < RARRAY_LEN(origins); i++) { invalidate_callable_method_entry_in_every_m_table(RARRAY_AREF(origins, i), mid, cme); } + RB_GC_GUARD(origins); } } From 61e15ee5f5ab3478fb811eb5d3f029472ebc7cec Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Sat, 6 Jun 2026 08:52:23 +0200 Subject: [PATCH 158/188] [ruby/json] json_parse_any: eliminate the loop and switch Each phase now takes care of dispatching to the next one. https://github.com/ruby/json/commit/ad6b9cff9e --- ext/json/parser/parser.c | 457 +++++++++++++++++++++------------------ 1 file changed, 245 insertions(+), 212 deletions(-) diff --git a/ext/json/parser/parser.c b/ext/json/parser/parser.c index fbfbfac9dae3ea..e5f702612fcec4 100644 --- a/ext/json/parser/parser.c +++ b/ext/json/parser/parser.c @@ -1477,258 +1477,291 @@ ALWAYS_INLINE(static) bool json_match_keyword(JSON_ParserState *state, const cha // itself is just another frame whose value, once parsed, leaves its phase DONE. static VALUE json_parse_any(JSON_ParserState *state, JSON_ParserConfig *config) { - while (true) { - json_frame *frame = json_frame_stack_peek(state->frames); - - switch (frame->phase) { - case JSON_PHASE_DONE: { - // The root document value is parsed; it is the lone survivor on - // the rvalue stack. - return *rvalue_stack_peek(state->value_stack, 1); - } - - case JSON_PHASE_VALUE: { - JSON_PHASE_VALUE: - json_eat_whitespace(state); - - VALUE value; - switch (peek(state)) { - case 'n': - if (json_match_keyword(state, "null", 0)) { - value = Qnil; - break; - } - - raise_parse_error("unexpected token %s", state); - case 't': - if (json_match_keyword(state, "true", 0)) { - value = Qtrue; - break; - } - - raise_parse_error("unexpected token %s", state); - case 'f': - if (json_match_keyword(state, "false", 1)) { - value = Qfalse; - break; - } - - raise_parse_error("unexpected token %s", state); - case 'N': - // Note: memcmp with a small power of two compile to an integer comparison - if (config->allow_nan && json_match_keyword(state, "NaN", 1)) { - value = CNaN; - break; - } - - raise_parse_error("unexpected token %s", state); - case 'I': - if (config->allow_nan && json_match_keyword(state, "Infinity", 0)) { - value = CInfinity; - break; - } + json_frame *frame = json_frame_stack_peek(state->frames); - raise_parse_error("unexpected token %s", state); - case '-': { - state->cursor++; - if (config->allow_nan && json_match_keyword(state, "Infinity", 0)) { - value = CMinusInfinity; - } else { - value = json_parse_negative_number(state, config); - } - break; - } - case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': - value = json_parse_positive_number(state, config); - break; - case '"': - // %r{\A"[^"\\\t\n\x00]*(?:\\[bfnrtu\\/"][^"\\]*)*"} - value = json_parse_string(state, config, false); - break; - case '[': { - state->cursor++; - json_eat_whitespace(state); - - if (peek(state) == ']') { - state->cursor++; - value = json_decode_array(state, config, 0); - break; - } + switch (frame->phase) { + case JSON_PHASE_DONE: goto JSON_PHASE_DONE; + case JSON_PHASE_ARRAY_COMMA: goto JSON_PHASE_ARRAY_COMMA; + case JSON_PHASE_OBJECT_COMMA: goto JSON_PHASE_OBJECT_COMMA; + case JSON_PHASE_VALUE: goto JSON_PHASE_VALUE; + case JSON_PHASE_OBJECT_KEY: goto JSON_PHASE_OBJECT_KEY; + case JSON_PHASE_OBJECT_COLON: goto JSON_PHASE_OBJECT_COLON; + } + UNREACHABLE_RETURN(Qundef); - state->current_nesting++; - if (RB_UNLIKELY(config->max_nesting && (config->max_nesting < state->current_nesting))) { - rb_raise(eNestingError, "nesting of %d is too deep", state->current_nesting); - } - state->in_array++; - - // Phase stays VALUE: the next iteration reads the first element. - frame = json_frame_stack_push(state, (json_frame){ - .type = JSON_FRAME_ARRAY, - .phase = JSON_PHASE_VALUE, - .value_stack_head = state->value_stack->head, - }); - goto JSON_PHASE_VALUE; - break; - } - case '{': { - const char *object_start_cursor = state->cursor; + JSON_PHASE_DONE: { + // The root document value is parsed; it is the lone survivor on + // the rvalue stack. + return *rvalue_stack_peek(state->value_stack, 1); + } - state->cursor++; - json_eat_whitespace(state); + JSON_PHASE_VALUE: { + json_eat_whitespace(state); - if (peek(state) == '}') { - state->cursor++; - value = json_decode_object(state, config, 0); - break; - } + VALUE value; + switch (peek(state)) { + case 'n': + if (json_match_keyword(state, "null", 0)) { + value = Qnil; + break; + } + raise_parse_error("unexpected token %s", state); - state->current_nesting++; - if (RB_UNLIKELY(config->max_nesting && (config->max_nesting < state->current_nesting))) { - rb_raise(eNestingError, "nesting of %d is too deep", state->current_nesting); - } + case 't': + if (json_match_keyword(state, "true", 0)) { + value = Qtrue; + break; + } + raise_parse_error("unexpected token %s", state); - // Phase KEY: the next iteration reads the first key. - frame = json_frame_stack_push(state, (json_frame){ - .type = JSON_FRAME_OBJECT, - .phase = JSON_PHASE_OBJECT_KEY, - .value_stack_head = state->value_stack->head, - .start_cursor = object_start_cursor, - }); - goto JSON_PHASE_OBJECT_KEY; - break; - } + case 'f': + if (json_match_keyword(state, "false", 1)) { + value = Qfalse; + break; + } + raise_parse_error("unexpected token %s", state); - case 0: - raise_parse_error("unexpected end of input", state); + case 'N': + // Note: memcmp with a small power of two compile to an integer comparison + if (config->allow_nan && json_match_keyword(state, "NaN", 1)) { + value = CNaN; + break; + } + raise_parse_error("unexpected token %s", state); - default: - raise_parse_error("unexpected character: %s", state); + case 'I': + if (config->allow_nan && json_match_keyword(state, "Infinity", 0)) { + value = CInfinity; + break; } + raise_parse_error("unexpected token %s", state); - json_push_value(state, config, value); - json_value_completed(frame); + case '-': { + state->cursor++; + if (config->allow_nan && json_match_keyword(state, "Infinity", 0)) { + value = CMinusInfinity; + } else { + value = json_parse_negative_number(state, config); + } break; } - case JSON_PHASE_OBJECT_KEY: { - JSON_PHASE_OBJECT_KEY: - JSON_ASSERT(frame->type == JSON_FRAME_OBJECT); + case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': + value = json_parse_positive_number(state, config); + break; + case '"': + // %r{\A"[^"\\\t\n\x00]*(?:\\[bfnrtu\\/"][^"\\]*)*"} + value = json_parse_string(state, config, false); + break; + + case '[': { + state->cursor++; json_eat_whitespace(state); - if (RB_LIKELY(peek(state) == '"')) { - json_push_value(state, config, json_parse_string(state, config, true)); - frame->phase = JSON_PHASE_OBJECT_COLON; - goto JSON_PHASE_OBJECT_COLON; - } else { - // The message differs for the first key vs. a key after a - // ',': the first is the only one reached with nothing pushed - // for this object yet. - if (json_frame_entry_count(frame, state->value_stack) == 0) { - raise_parse_error("expected object key, got %s", state); - } else { - raise_parse_error("expected object key, got: %s", state); - } + if (peek(state) == ']') { + state->cursor++; + value = json_decode_array(state, config, 0); + break; } - break; - } - case JSON_PHASE_OBJECT_COLON: { - JSON_PHASE_OBJECT_COLON: - JSON_ASSERT(frame->type == JSON_FRAME_OBJECT); + state->current_nesting++; + if (RB_UNLIKELY(config->max_nesting && (config->max_nesting < state->current_nesting))) { + rb_raise(eNestingError, "nesting of %d is too deep", state->current_nesting); + } + state->in_array++; + + // Phase stays VALUE: the next iteration reads the first element. + frame = json_frame_stack_push(state, (json_frame){ + .type = JSON_FRAME_ARRAY, + .phase = JSON_PHASE_VALUE, + .value_stack_head = state->value_stack->head, + }); + goto JSON_PHASE_VALUE; + } + case '{': { + const char *object_start_cursor = state->cursor; + state->cursor++; json_eat_whitespace(state); - if (RB_LIKELY(peek(state) == ':')) { + if (peek(state) == '}') { state->cursor++; - frame->phase = JSON_PHASE_VALUE; - goto JSON_PHASE_VALUE; - } else { - // First colon (only the first pair's key is pushed, nothing - // else) vs. a later one. - if (json_frame_entry_count(frame, state->value_stack) == 1) { - raise_parse_error("expected ':' after object key", state); - } else { - raise_parse_error("expected ':' after object key, got: %s", state); - } + value = json_decode_object(state, config, 0); + break; } - break; + + state->current_nesting++; + if (RB_UNLIKELY(config->max_nesting && (config->max_nesting < state->current_nesting))) { + rb_raise(eNestingError, "nesting of %d is too deep", state->current_nesting); + } + + // Phase KEY: the next iteration reads the first key. + frame = json_frame_stack_push(state, (json_frame){ + .type = JSON_FRAME_OBJECT, + .phase = JSON_PHASE_OBJECT_KEY, + .value_stack_head = state->value_stack->head, + .start_cursor = object_start_cursor, + }); + goto JSON_PHASE_OBJECT_KEY; } - case JSON_PHASE_ARRAY_COMMA: { - JSON_ASSERT(frame->type == JSON_FRAME_ARRAY); + case 0: + raise_parse_error("unexpected end of input", state); - json_eat_whitespace(state); + default: + raise_parse_error("unexpected character: %s", state); + } - const char next_char = peek(state); + json_push_value(state, config, value); + json_value_completed(frame); - if (RB_LIKELY(next_char == ',')) { - state->cursor++; - if (config->allow_trailing_comma) { - json_eat_whitespace(state); - if (peek(state) == ']') { - // Trailing comma: stay in COMMA to close on the next iteration. - break; - } - } - frame->phase = JSON_PHASE_VALUE; - goto JSON_PHASE_VALUE; - } else if (next_char == ']') { - state->cursor++; - long count = json_frame_entry_count(frame, state->value_stack); - state->current_nesting--; - state->in_array--; - json_frame_stack_pop(state->frames); - json_push_value(state, config, json_decode_array(state, config, count)); - json_value_completed(json_frame_stack_peek(state->frames)); - } else { - raise_parse_error("expected ',' or ']' after array value", state); - } - break; + switch (frame->phase) { + case JSON_PHASE_DONE: goto JSON_PHASE_DONE; + case JSON_PHASE_ARRAY_COMMA: goto JSON_PHASE_ARRAY_COMMA; + case JSON_PHASE_OBJECT_COMMA: goto JSON_PHASE_OBJECT_COMMA; + case JSON_PHASE_VALUE: goto JSON_PHASE_VALUE; + case JSON_PHASE_OBJECT_KEY: UNREACHABLE_RETURN(Qundef); + case JSON_PHASE_OBJECT_COLON: goto JSON_PHASE_OBJECT_COLON; + } + UNREACHABLE_RETURN(Qundef); + } + + JSON_PHASE_OBJECT_KEY: { + JSON_ASSERT(frame->type == JSON_FRAME_OBJECT); + + json_eat_whitespace(state); + + if (RB_LIKELY(peek(state) == '"')) { + json_push_value(state, config, json_parse_string(state, config, true)); + frame->phase = JSON_PHASE_OBJECT_COLON; + goto JSON_PHASE_OBJECT_COLON; + } else { + // The message differs for the first key vs. a key after a + // ',': the first is the only one reached with nothing pushed + // for this object yet. + if (json_frame_entry_count(frame, state->value_stack) == 0) { + raise_parse_error("expected object key, got %s", state); + } else { + raise_parse_error("expected object key, got: %s", state); + } + } + UNREACHABLE_RETURN(Qundef); + } + + JSON_PHASE_OBJECT_COLON: { + JSON_ASSERT(frame->type == JSON_FRAME_OBJECT); + + json_eat_whitespace(state); + + if (RB_LIKELY(peek(state) == ':')) { + state->cursor++; + frame->phase = JSON_PHASE_VALUE; + goto JSON_PHASE_VALUE; + } else { + // First colon (only the first pair's key is pushed, nothing + // else) vs. a later one. + if (json_frame_entry_count(frame, state->value_stack) == 1) { + raise_parse_error("expected ':' after object key", state); + } else { + raise_parse_error("expected ':' after object key, got: %s", state); } + } + UNREACHABLE_RETURN(Qundef); + } + + JSON_PHASE_ARRAY_COMMA: { + JSON_ASSERT(frame->type == JSON_FRAME_ARRAY); - case JSON_PHASE_OBJECT_COMMA: { - JSON_ASSERT(frame->type == JSON_FRAME_OBJECT); + json_eat_whitespace(state); + + const char next_char = peek(state); + if (RB_LIKELY(next_char == ',')) { + state->cursor++; + if (config->allow_trailing_comma) { json_eat_whitespace(state); - const char next_char = peek(state); + if (peek(state) == ']') { + // Trailing comma: stay in COMMA to close on the next iteration. + goto JSON_PHASE_ARRAY_COMMA; + } + } + frame->phase = JSON_PHASE_VALUE; + goto JSON_PHASE_VALUE; + } else if (next_char == ']') { + state->cursor++; + long count = json_frame_entry_count(frame, state->value_stack); + state->current_nesting--; + state->in_array--; + json_frame_stack_pop(state->frames); + json_push_value(state, config, json_decode_array(state, config, count)); + frame = json_frame_stack_peek(state->frames); + json_value_completed(frame); + + switch (frame->phase) { + case JSON_PHASE_DONE: goto JSON_PHASE_DONE; + case JSON_PHASE_ARRAY_COMMA: goto JSON_PHASE_ARRAY_COMMA; + case JSON_PHASE_OBJECT_COMMA: goto JSON_PHASE_OBJECT_COMMA; + case JSON_PHASE_VALUE: goto JSON_PHASE_VALUE; + case JSON_PHASE_OBJECT_KEY: UNREACHABLE_RETURN(Qundef); + case JSON_PHASE_OBJECT_COLON: goto JSON_PHASE_OBJECT_COLON; + } + } else { + raise_parse_error("expected ',' or ']' after array value", state); + } + UNREACHABLE_RETURN(Qundef); + } - if (RB_LIKELY(next_char == ',')) { - state->cursor++; - json_eat_whitespace(state); + JSON_PHASE_OBJECT_COMMA: { + JSON_ASSERT(frame->type == JSON_FRAME_OBJECT); - if (config->allow_trailing_comma) { - if (peek(state) == '}') { - // Trailing comma: stay in COMMA to close on the next iteration. - break; - } - } + json_eat_whitespace(state); + const char next_char = peek(state); - frame->phase = JSON_PHASE_OBJECT_KEY; - goto JSON_PHASE_OBJECT_KEY; + if (RB_LIKELY(next_char == ',')) { + state->cursor++; - break; - } else if (next_char == '}') { - state->cursor++; - state->current_nesting--; - size_t count = json_frame_entry_count(frame, state->value_stack); - - // Temporary rewind cursor in case an error is raised - const char *final_cursor = state->cursor; - state->cursor = frame->start_cursor; - VALUE object = json_decode_object(state, config, count); - state->cursor = final_cursor; - - json_frame_stack_pop(state->frames); - json_push_value(state, config, object); - json_value_completed(json_frame_stack_peek(state->frames)); - break; - } else { - raise_parse_error("expected ',' or '}' after object value, got: %s", state); + if (config->allow_trailing_comma) { + json_eat_whitespace(state); + if (peek(state) == '}') { + // Trailing comma: stay in COMMA to close on the next iteration. + goto JSON_PHASE_OBJECT_COMMA; } } + + frame->phase = JSON_PHASE_OBJECT_KEY; + goto JSON_PHASE_OBJECT_KEY; + } else if (next_char == '}') { + state->cursor++; + state->current_nesting--; + size_t count = json_frame_entry_count(frame, state->value_stack); + + // Temporary rewind cursor in case an error is raised + const char *final_cursor = state->cursor; + state->cursor = frame->start_cursor; + VALUE object = json_decode_object(state, config, count); + state->cursor = final_cursor; + + json_push_value(state, config, object); + json_frame_stack_pop(state->frames); + frame = json_frame_stack_peek(state->frames); + json_value_completed(frame); + + switch (frame->phase) { + case JSON_PHASE_DONE: goto JSON_PHASE_DONE; + case JSON_PHASE_ARRAY_COMMA: goto JSON_PHASE_ARRAY_COMMA; + case JSON_PHASE_OBJECT_COMMA: goto JSON_PHASE_OBJECT_COMMA; + case JSON_PHASE_VALUE: goto JSON_PHASE_VALUE; + case JSON_PHASE_OBJECT_KEY: UNREACHABLE_RETURN(Qundef); + case JSON_PHASE_OBJECT_COLON: goto JSON_PHASE_OBJECT_COLON; + } + } else { + raise_parse_error("expected ',' or '}' after object value, got: %s", state); } + UNREACHABLE_RETURN(Qundef); } + + UNREACHABLE_RETURN(Qundef); } static void json_ensure_eof(JSON_ParserState *state) From 7cce6d23c4731ae29c48825a25e18d9452776ff8 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Sat, 6 Jun 2026 10:45:11 +0200 Subject: [PATCH 159/188] [ruby/json] Add note explaining why rvalue_stack is not WB_PROTECTED https://github.com/ruby/json/commit/6444417670 --- ext/json/parser/parser.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ext/json/parser/parser.c b/ext/json/parser/parser.c index e5f702612fcec4..c0631728c38c80 100644 --- a/ext/json/parser/parser.c +++ b/ext/json/parser/parser.c @@ -294,6 +294,8 @@ static const rb_data_type_t JSON_Parser_rvalue_stack_type = { .dsize = rvalue_stack_memsize, .dcompact = rvalue_stack_compact, }, + // We deliberately don't declare rvalue_stack as RUBY_TYPED_WB_PROTECTED + // because it churns a lot of values so trigering write barriers every time is very costly. .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_EMBEDDABLE, }; From e98f95b4fd830c5e89941702e7b216e3212ac778 Mon Sep 17 00:00:00 2001 From: thekuwayama Date: Sat, 6 Jun 2026 19:09:12 +0900 Subject: [PATCH 160/188] Use nprocessors as default_max_cpu for M:N scheduler (#17100) Resolves the "TODO: CPU num?". Uses `sysconf(_SC_NPROCESSORS_ONLN)` with a fallback to 8, guarded by `#if defined(HAVE_SYSCONF) && defined(_SC_NPROCESSORS_ONLN)`. --------- Co-authored-by: Claude Sonnet 4.6 --- test/ruby/test_ractor.rb | 11 +++++++++++ thread_pthread.c | 7 ++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/test/ruby/test_ractor.rb b/test/ruby/test_ractor.rb index 611b3b771584bb..e7eb0cd4b34fe7 100644 --- a/test/ruby/test_ractor.rb +++ b/test/ruby/test_ractor.rb @@ -321,6 +321,17 @@ def test_max_cpu_1 RUBY end + def test_mn_threads + # Ideally, we would assert that vm->ractor.sched.max_cpu equals sysconf(_SC_NPROCESSORS_ONLN) + # when RUBY_MAX_CPU is not set. + assert_ractor(<<~'RUBY', args: [{ "RUBY_MN_THREADS" => "1" }]) + require "etc" + n = Etc.respond_to?(:nprocessors) ? Etc.nprocessors : 8 + rs = n.times.map { Ractor.new { :ok } } + assert_equal [:ok] * n, rs.map(&:value) + RUBY + end + def test_symbol_proc_is_shareable pr = :symbol.to_proc assert_make_shareable(pr) diff --git a/thread_pthread.c b/thread_pthread.c index 6e3ce8ef871944..5b5329fd217916 100644 --- a/thread_pthread.c +++ b/thread_pthread.c @@ -1799,7 +1799,12 @@ ruby_mn_threads_params(void) main_ractor->threads.sched.enable_mn_threads = enable_mn_threads; const char *max_cpu_cstr = getenv("RUBY_MAX_CPU"); - const int default_max_cpu = 8; // TODO: CPU num? +#if defined(HAVE_SYSCONF) && defined(_SC_NPROCESSORS_ONLN) + long nprocessors = sysconf(_SC_NPROCESSORS_ONLN); + const int default_max_cpu = (nprocessors > 0) ? (int)nprocessors : 8; +#else + const int default_max_cpu = 8; +#endif int max_cpu = default_max_cpu; if (USE_MN_THREADS && max_cpu_cstr) { From 5f1288759ff40023cb455251b5857d6d74a4b8d3 Mon Sep 17 00:00:00 2001 From: Benoit Daloze Date: Sat, 6 Jun 2026 12:53:15 +0200 Subject: [PATCH 161/188] Update to ruby/mspec@82868a2 --- spec/mspec/Gemfile | 2 +- spec/mspec/Gemfile.lock | 4 ++-- spec/mspec/lib/mspec/matchers/base.rb | 2 ++ spec/mspec/lib/mspec/matchers/be_an_instance_of.rb | 1 + spec/mspec/lib/mspec/matchers/be_ancestor_of.rb | 1 + spec/mspec/lib/mspec/matchers/be_empty.rb | 1 + spec/mspec/lib/mspec/matchers/be_false.rb | 1 + spec/mspec/lib/mspec/matchers/be_kind_of.rb | 1 + spec/mspec/lib/mspec/matchers/be_nan.rb | 1 + spec/mspec/lib/mspec/matchers/be_nil.rb | 1 + spec/mspec/lib/mspec/matchers/be_true.rb | 1 + spec/mspec/lib/mspec/matchers/eql.rb | 1 + spec/mspec/lib/mspec/matchers/equal.rb | 1 + spec/mspec/lib/mspec/matchers/have_class_variable.rb | 1 + spec/mspec/lib/mspec/matchers/have_constant.rb | 1 + spec/mspec/lib/mspec/matchers/have_instance_method.rb | 1 + spec/mspec/lib/mspec/matchers/have_instance_variable.rb | 1 + spec/mspec/lib/mspec/matchers/have_method.rb | 1 + .../mspec/lib/mspec/matchers/have_private_instance_method.rb | 1 + spec/mspec/lib/mspec/matchers/have_private_method.rb | 1 + .../lib/mspec/matchers/have_protected_instance_method.rb | 1 + spec/mspec/lib/mspec/matchers/have_public_instance_method.rb | 1 + spec/mspec/lib/mspec/matchers/have_singleton_method.rb | 1 + spec/mspec/lib/mspec/matchers/include.rb | 1 + spec/mspec/lib/mspec/matchers/infinity.rb | 2 ++ spec/mspec/lib/mspec/matchers/raise_error.rb | 1 + spec/mspec/lib/mspec/matchers/respond_to.rb | 1 + spec/mspec/spec/fixtures/should.rb | 4 ++-- spec/mspec/spec/matchers/raise_error_spec.rb | 4 ++-- spec/mspec/spec/spec_helper.rb | 5 ----- 30 files changed, 34 insertions(+), 12 deletions(-) diff --git a/spec/mspec/Gemfile b/spec/mspec/Gemfile index 617a995cad1366..e57acd6435365c 100644 --- a/spec/mspec/Gemfile +++ b/spec/mspec/Gemfile @@ -1,4 +1,4 @@ source 'https://rubygems.org' -gem "rake", "~> 12.3" +gem "rake" gem "rspec", "~> 3.0" diff --git a/spec/mspec/Gemfile.lock b/spec/mspec/Gemfile.lock index cd39906044ef92..075337d6711c49 100644 --- a/spec/mspec/Gemfile.lock +++ b/spec/mspec/Gemfile.lock @@ -2,7 +2,7 @@ GEM remote: https://rubygems.org/ specs: diff-lcs (1.4.4) - rake (12.3.3) + rake (13.4.2) rspec (3.10.0) rspec-core (~> 3.10.0) rspec-expectations (~> 3.10.0) @@ -22,5 +22,5 @@ PLATFORMS ruby DEPENDENCIES - rake (~> 12.3) + rake rspec (~> 3.0) diff --git a/spec/mspec/lib/mspec/matchers/base.rb b/spec/mspec/lib/mspec/matchers/base.rb index 3534520d88c438..4056e73a811775 100644 --- a/spec/mspec/lib/mspec/matchers/base.rb +++ b/spec/mspec/lib/mspec/matchers/base.rb @@ -1,3 +1,5 @@ +require 'mspec/utils/deprecate' + module MSpecMatchers end diff --git a/spec/mspec/lib/mspec/matchers/be_an_instance_of.rb b/spec/mspec/lib/mspec/matchers/be_an_instance_of.rb index fdf3736ac2979a..230cea2e9b9ec0 100644 --- a/spec/mspec/lib/mspec/matchers/be_an_instance_of.rb +++ b/spec/mspec/lib/mspec/matchers/be_an_instance_of.rb @@ -21,6 +21,7 @@ def negative_failure_message module MSpecMatchers private def be_an_instance_of(expected) + MSpec.deprecate __method__, '.should.instance_of?' BeAnInstanceOfMatcher.new(expected) end end diff --git a/spec/mspec/lib/mspec/matchers/be_ancestor_of.rb b/spec/mspec/lib/mspec/matchers/be_ancestor_of.rb index 05f72099e424d1..b428a153bf9514 100644 --- a/spec/mspec/lib/mspec/matchers/be_ancestor_of.rb +++ b/spec/mspec/lib/mspec/matchers/be_ancestor_of.rb @@ -19,6 +19,7 @@ def negative_failure_message module MSpecMatchers private def be_ancestor_of(expected) + MSpec.deprecate __method__, '.ancestors.should.include?' BeAncestorOfMatcher.new(expected) end end diff --git a/spec/mspec/lib/mspec/matchers/be_empty.rb b/spec/mspec/lib/mspec/matchers/be_empty.rb index 5abd5c9485e6f3..aac175ffb4f397 100644 --- a/spec/mspec/lib/mspec/matchers/be_empty.rb +++ b/spec/mspec/lib/mspec/matchers/be_empty.rb @@ -15,6 +15,7 @@ def negative_failure_message module MSpecMatchers private def be_empty + MSpec.deprecate __method__, '.should.empty?' BeEmptyMatcher.new end end diff --git a/spec/mspec/lib/mspec/matchers/be_false.rb b/spec/mspec/lib/mspec/matchers/be_false.rb index 9e9a2608e17c6e..4fa0bba8a31b70 100644 --- a/spec/mspec/lib/mspec/matchers/be_false.rb +++ b/spec/mspec/lib/mspec/matchers/be_false.rb @@ -15,6 +15,7 @@ def negative_failure_message module MSpecMatchers private def be_false + MSpec.deprecate __method__, '.should == false' BeFalseMatcher.new end end diff --git a/spec/mspec/lib/mspec/matchers/be_kind_of.rb b/spec/mspec/lib/mspec/matchers/be_kind_of.rb index a69906f210e36e..d0b23086aa5e98 100644 --- a/spec/mspec/lib/mspec/matchers/be_kind_of.rb +++ b/spec/mspec/lib/mspec/matchers/be_kind_of.rb @@ -19,6 +19,7 @@ def negative_failure_message module MSpecMatchers private def be_kind_of(expected) + MSpec.deprecate __method__, '.should.is_a?' BeKindOfMatcher.new(expected) end end diff --git a/spec/mspec/lib/mspec/matchers/be_nan.rb b/spec/mspec/lib/mspec/matchers/be_nan.rb index b279d8f1cfd52e..8ba2a3cafea2d4 100644 --- a/spec/mspec/lib/mspec/matchers/be_nan.rb +++ b/spec/mspec/lib/mspec/matchers/be_nan.rb @@ -15,6 +15,7 @@ def negative_failure_message module MSpecMatchers private def be_nan + MSpec.deprecate __method__, '.should.nan?' BeNaNMatcher.new end end diff --git a/spec/mspec/lib/mspec/matchers/be_nil.rb b/spec/mspec/lib/mspec/matchers/be_nil.rb index 049b1e3a53d279..c79a50a3f9f4fa 100644 --- a/spec/mspec/lib/mspec/matchers/be_nil.rb +++ b/spec/mspec/lib/mspec/matchers/be_nil.rb @@ -15,6 +15,7 @@ def negative_failure_message module MSpecMatchers private def be_nil + MSpec.deprecate __method__, '.should == nil' BeNilMatcher.new end end diff --git a/spec/mspec/lib/mspec/matchers/be_true.rb b/spec/mspec/lib/mspec/matchers/be_true.rb index 52f50137525c0d..91020cc3fe37d3 100644 --- a/spec/mspec/lib/mspec/matchers/be_true.rb +++ b/spec/mspec/lib/mspec/matchers/be_true.rb @@ -15,6 +15,7 @@ def negative_failure_message module MSpecMatchers private def be_true + MSpec.deprecate __method__, '.should == true' BeTrueMatcher.new end end diff --git a/spec/mspec/lib/mspec/matchers/eql.rb b/spec/mspec/lib/mspec/matchers/eql.rb index bcab88ebeee443..3b132f9ae39e7c 100644 --- a/spec/mspec/lib/mspec/matchers/eql.rb +++ b/spec/mspec/lib/mspec/matchers/eql.rb @@ -21,6 +21,7 @@ def negative_failure_message module MSpecMatchers private def eql(expected) + MSpec.deprecate __method__, '.should.eql?' EqlMatcher.new(expected) end end diff --git a/spec/mspec/lib/mspec/matchers/equal.rb b/spec/mspec/lib/mspec/matchers/equal.rb index 5ba4856d8274ef..1ca5172e0b55f4 100644 --- a/spec/mspec/lib/mspec/matchers/equal.rb +++ b/spec/mspec/lib/mspec/matchers/equal.rb @@ -21,6 +21,7 @@ def negative_failure_message module MSpecMatchers private def equal(expected) + MSpec.deprecate __method__, '.should.equal?' EqualMatcher.new(expected) end end diff --git a/spec/mspec/lib/mspec/matchers/have_class_variable.rb b/spec/mspec/lib/mspec/matchers/have_class_variable.rb index dd43ced621bd4e..576f43793b7656 100644 --- a/spec/mspec/lib/mspec/matchers/have_class_variable.rb +++ b/spec/mspec/lib/mspec/matchers/have_class_variable.rb @@ -7,6 +7,7 @@ class HaveClassVariableMatcher < VariableMatcher module MSpecMatchers private def have_class_variable(variable) + MSpec.deprecate __method__, '.should.class_variable_defined?' HaveClassVariableMatcher.new(variable) end end diff --git a/spec/mspec/lib/mspec/matchers/have_constant.rb b/spec/mspec/lib/mspec/matchers/have_constant.rb index 6ec7c75b8581a6..c796d742e0e7f1 100644 --- a/spec/mspec/lib/mspec/matchers/have_constant.rb +++ b/spec/mspec/lib/mspec/matchers/have_constant.rb @@ -7,6 +7,7 @@ class HaveConstantMatcher < VariableMatcher module MSpecMatchers private def have_constant(variable) + MSpec.deprecate __method__, '.should.const_defined?' HaveConstantMatcher.new(variable) end end diff --git a/spec/mspec/lib/mspec/matchers/have_instance_method.rb b/spec/mspec/lib/mspec/matchers/have_instance_method.rb index 9a5a31aa0f6f43..76dde482d5d160 100644 --- a/spec/mspec/lib/mspec/matchers/have_instance_method.rb +++ b/spec/mspec/lib/mspec/matchers/have_instance_method.rb @@ -19,6 +19,7 @@ def negative_failure_message module MSpecMatchers private def have_instance_method(method, include_super = true) + MSpec.deprecate __method__, '.should.method_defined?' HaveInstanceMethodMatcher.new method, include_super end end diff --git a/spec/mspec/lib/mspec/matchers/have_instance_variable.rb b/spec/mspec/lib/mspec/matchers/have_instance_variable.rb index de51b3209d7fe4..f49595ff7ea98e 100644 --- a/spec/mspec/lib/mspec/matchers/have_instance_variable.rb +++ b/spec/mspec/lib/mspec/matchers/have_instance_variable.rb @@ -7,6 +7,7 @@ class HaveInstanceVariableMatcher < VariableMatcher module MSpecMatchers private def have_instance_variable(variable) + MSpec.deprecate __method__, '.should.instance_variable_defined?' HaveInstanceVariableMatcher.new(variable) end end diff --git a/spec/mspec/lib/mspec/matchers/have_method.rb b/spec/mspec/lib/mspec/matchers/have_method.rb index e962e69e0a354f..3db01a723589bb 100644 --- a/spec/mspec/lib/mspec/matchers/have_method.rb +++ b/spec/mspec/lib/mspec/matchers/have_method.rb @@ -19,6 +19,7 @@ def negative_failure_message module MSpecMatchers private def have_method(method, include_super = true) + MSpec.deprecate __method__, '.should.respond_to?' HaveMethodMatcher.new method, include_super end end diff --git a/spec/mspec/lib/mspec/matchers/have_private_instance_method.rb b/spec/mspec/lib/mspec/matchers/have_private_instance_method.rb index d32db76c6af578..bae01e846ee0d0 100644 --- a/spec/mspec/lib/mspec/matchers/have_private_instance_method.rb +++ b/spec/mspec/lib/mspec/matchers/have_private_instance_method.rb @@ -19,6 +19,7 @@ def negative_failure_message module MSpecMatchers private def have_private_instance_method(method, include_super = true) + MSpec.deprecate __method__, '.private_instance_methods(false).should.include?' HavePrivateInstanceMethodMatcher.new method, include_super end end diff --git a/spec/mspec/lib/mspec/matchers/have_private_method.rb b/spec/mspec/lib/mspec/matchers/have_private_method.rb index c74165cfc743d0..32efd5a1558bc5 100644 --- a/spec/mspec/lib/mspec/matchers/have_private_method.rb +++ b/spec/mspec/lib/mspec/matchers/have_private_method.rb @@ -19,6 +19,7 @@ def negative_failure_message module MSpecMatchers private def have_private_method(method, include_super = true) + MSpec.deprecate __method__, '.private_methods(false).should.include?' HavePrivateMethodMatcher.new method, include_super end end diff --git a/spec/mspec/lib/mspec/matchers/have_protected_instance_method.rb b/spec/mspec/lib/mspec/matchers/have_protected_instance_method.rb index 1deb2f995d1605..9f09e8b529fd6e 100644 --- a/spec/mspec/lib/mspec/matchers/have_protected_instance_method.rb +++ b/spec/mspec/lib/mspec/matchers/have_protected_instance_method.rb @@ -19,6 +19,7 @@ def negative_failure_message module MSpecMatchers private def have_protected_instance_method(method, include_super = true) + MSpec.deprecate __method__, '.protected_instance_methods(false).should.include?' HaveProtectedInstanceMethodMatcher.new method, include_super end end diff --git a/spec/mspec/lib/mspec/matchers/have_public_instance_method.rb b/spec/mspec/lib/mspec/matchers/have_public_instance_method.rb index 0e620532c0a118..69abadfafad143 100644 --- a/spec/mspec/lib/mspec/matchers/have_public_instance_method.rb +++ b/spec/mspec/lib/mspec/matchers/have_public_instance_method.rb @@ -19,6 +19,7 @@ def negative_failure_message module MSpecMatchers private def have_public_instance_method(method, include_super = true) + MSpec.deprecate __method__, '.public_instance_methods(false).should.include?' HavePublicInstanceMethodMatcher.new method, include_super end end diff --git a/spec/mspec/lib/mspec/matchers/have_singleton_method.rb b/spec/mspec/lib/mspec/matchers/have_singleton_method.rb index b60dd2536bc616..2d2707c528376a 100644 --- a/spec/mspec/lib/mspec/matchers/have_singleton_method.rb +++ b/spec/mspec/lib/mspec/matchers/have_singleton_method.rb @@ -19,6 +19,7 @@ def negative_failure_message module MSpecMatchers private def have_singleton_method(method, include_super = true) + MSpec.deprecate __method__, '.singleton_methods(false).should.include?' HaveSingletonMethodMatcher.new method, include_super end end diff --git a/spec/mspec/lib/mspec/matchers/include.rb b/spec/mspec/lib/mspec/matchers/include.rb index 3f07f355482dbd..05d5079f134f87 100644 --- a/spec/mspec/lib/mspec/matchers/include.rb +++ b/spec/mspec/lib/mspec/matchers/include.rb @@ -26,6 +26,7 @@ def negative_failure_message # Cannot override #include at the toplevel in MRI module MSpecMatchers private def include(*expected) + MSpec.deprecate __method__, '.should.include?' IncludeMatcher.new(*expected) end end diff --git a/spec/mspec/lib/mspec/matchers/infinity.rb b/spec/mspec/lib/mspec/matchers/infinity.rb index 8bfa6dbd10d18d..0a4c95c7cc6249 100644 --- a/spec/mspec/lib/mspec/matchers/infinity.rb +++ b/spec/mspec/lib/mspec/matchers/infinity.rb @@ -19,10 +19,12 @@ def negative_failure_message module MSpecMatchers private def be_positive_infinity + MSpec.deprecate __method__, '.should.infinite? == 1' InfinityMatcher.new(1) end private def be_negative_infinity + MSpec.deprecate __method__, '.should.infinite? == -1' InfinityMatcher.new(-1) end end diff --git a/spec/mspec/lib/mspec/matchers/raise_error.rb b/spec/mspec/lib/mspec/matchers/raise_error.rb index 8cba842ce3ffe8..17ea47148b3ab6 100644 --- a/spec/mspec/lib/mspec/matchers/raise_error.rb +++ b/spec/mspec/lib/mspec/matchers/raise_error.rb @@ -116,6 +116,7 @@ def negative_failure_message module MSpecMatchers private def raise_error(exception = Exception, message = nil, options = nil, &block) + MSpec.deprecate __method__, '.should.raise' RaiseErrorMatcher.new(exception, message, options, &block) end diff --git a/spec/mspec/lib/mspec/matchers/respond_to.rb b/spec/mspec/lib/mspec/matchers/respond_to.rb index 6b35ae2d3caa4c..a36bf8aee2cd7c 100644 --- a/spec/mspec/lib/mspec/matchers/respond_to.rb +++ b/spec/mspec/lib/mspec/matchers/respond_to.rb @@ -19,6 +19,7 @@ def negative_failure_message module MSpecMatchers private def respond_to(expected) + MSpec.deprecate __method__, '.should.respond_to?' RespondToMatcher.new(expected) end end diff --git a/spec/mspec/spec/fixtures/should.rb b/spec/mspec/spec/fixtures/should.rb index f494775c5fdb33..6eb156da254067 100644 --- a/spec/mspec/spec/fixtures/should.rb +++ b/spec/mspec/spec/fixtures/should.rb @@ -40,7 +40,7 @@ def finish # Specs describe "MSpec expectation method #should" do it "accepts a matcher" do - :sym.should be_kind_of(Symbol) + 0.4.should be_close(0.5, 0.2) end it "causes a failure to be recorded" do @@ -59,7 +59,7 @@ def finish describe "MSpec expectation method #should_not" do it "accepts a matcher" do - "sym".should_not be_kind_of(Symbol) + 0.1.should_not be_close(0.5, 0.2) end it "causes a failure to be recorded" do diff --git a/spec/mspec/spec/matchers/raise_error_spec.rb b/spec/mspec/spec/matchers/raise_error_spec.rb index 3849c7dd2a3bfb..957baa087d4db4 100644 --- a/spec/mspec/spec/matchers/raise_error_spec.rb +++ b/spec/mspec/spec/matchers/raise_error_spec.rb @@ -19,7 +19,7 @@ class UnexpectedException < Exception; end ensure_mspec_method(-> {}.method(:should)) run = false - -> { raise ExpectedException }.should PublicMSpecMatchers.raise_error { |error| + -> { raise ExpectedException }.should.raise { |error| expect(error.class).to eq(ExpectedException) run = true } @@ -30,7 +30,7 @@ class UnexpectedException < Exception; end ensure_mspec_method(-> {}.method(:should)) run = false - -> { raise ExpectedException }.should PublicMSpecMatchers.raise_error do |error| + -> { raise ExpectedException }.should.raise do |error| expect(error.class).to eq(ExpectedException) run = true end diff --git a/spec/mspec/spec/spec_helper.rb b/spec/mspec/spec/spec_helper.rb index 5cabfe5626448b..8ea38b644f2c66 100644 --- a/spec/mspec/spec/spec_helper.rb +++ b/spec/mspec/spec/spec_helper.rb @@ -62,9 +62,4 @@ def ensure_mspec_method(method) expect(file).to start_with(File.expand_path('../../lib/mspec', __FILE__ )) end -PublicMSpecMatchers = Class.new { - include MSpecMatchers - public :raise_error -}.new - BACKTRACE_QUOTE = RUBY_VERSION >= "3.4" ? "'" : "`" From 937a1d9a6fcf163b5859ad607ef0065c6a188489 Mon Sep 17 00:00:00 2001 From: Benoit Daloze Date: Sat, 6 Jun 2026 12:53:17 +0200 Subject: [PATCH 162/188] Update to ruby/spec@e695ce6 --- spec/ruby/core/array/collect_spec.rb | 138 +- spec/ruby/core/array/length_spec.rb | 11 +- spec/ruby/core/array/map_spec.rb | 138 +- spec/ruby/core/array/size_spec.rb | 11 +- spec/ruby/core/basicobject/equal_spec.rb | 51 +- .../ruby/core/basicobject/equal_value_spec.rb | 44 + spec/ruby/core/complex/rect_spec.rb | 2 +- spec/ruby/core/data/inspect_spec.rb | 59 +- spec/ruby/core/data/to_s_spec.rb | 58 +- spec/ruby/core/dir/delete_spec.rb | 53 +- spec/ruby/core/dir/path_spec.rb | 34 +- spec/ruby/core/dir/pos_spec.rb | 36 +- spec/ruby/core/dir/rmdir_spec.rb | 12 +- spec/ruby/core/dir/shared/delete.rb | 53 - spec/ruby/core/dir/tell_spec.rb | 36 +- spec/ruby/core/dir/to_path_spec.rb | 34 +- spec/ruby/core/dir/unlink_spec.rb | 12 +- .../converter/asciicompat_encoding_spec.rb | 10 +- .../core/enumerable/collect_concat_spec.rb | 6 +- spec/ruby/core/enumerable/collect_spec.rb | 6 +- spec/ruby/core/enumerable/detect_spec.rb | 6 +- spec/ruby/core/enumerable/entries_spec.rb | 6 +- spec/ruby/core/enumerable/filter_spec.rb | 6 +- spec/ruby/core/enumerable/find_all_spec.rb | 6 +- spec/ruby/core/enumerable/find_spec.rb | 75 +- spec/ruby/core/enumerable/flat_map_spec.rb | 53 +- spec/ruby/core/enumerable/include_spec.rb | 33 +- spec/ruby/core/enumerable/inject_spec.rb | 141 +- spec/ruby/core/enumerable/map_spec.rb | 106 +- spec/ruby/core/enumerable/member_spec.rb | 6 +- spec/ruby/core/enumerable/reduce_spec.rb | 6 +- spec/ruby/core/enumerable/select_spec.rb | 30 +- spec/ruby/core/enumerable/shared/collect.rb | 107 -- .../core/enumerable/shared/collect_concat.rb | 54 - spec/ruby/core/enumerable/shared/entries.rb | 16 - spec/ruby/core/enumerable/shared/find.rb | 77 - spec/ruby/core/enumerable/shared/find_all.rb | 31 - spec/ruby/core/enumerable/shared/include.rb | 34 - spec/ruby/core/enumerable/shared/inject.rb | 142 -- spec/ruby/core/enumerable/to_a_spec.rb | 16 +- spec/ruby/core/enumerator/each_spec.rb | 15 + .../core/enumerator/each_with_object_spec.rb | 40 +- spec/ruby/core/enumerator/enum_for_spec.rb | 6 - .../enumerator/lazy/collect_concat_spec.rb | 8 +- .../ruby/core/enumerator/lazy/collect_spec.rb | 8 +- .../core/enumerator/lazy/enum_for_spec.rb | 8 +- spec/ruby/core/enumerator/lazy/filter_spec.rb | 6 +- .../core/enumerator/lazy/find_all_spec.rb | 8 +- .../core/enumerator/lazy/flat_map_spec.rb | 76 +- spec/ruby/core/enumerator/lazy/map_spec.rb | 60 +- spec/ruby/core/enumerator/lazy/select_spec.rb | 64 +- .../core/enumerator/lazy/shared/collect.rb | 62 - .../enumerator/lazy/shared/collect_concat.rb | 78 - .../core/enumerator/lazy/shared/select.rb | 66 - .../core/enumerator/lazy/shared/to_enum.rb | 55 - .../ruby/core/enumerator/lazy/to_enum_spec.rb | 54 +- spec/ruby/core/enumerator/shared/each.rb | 46 + spec/ruby/core/enumerator/shared/enum_for.rb | 57 - .../core/enumerator/shared/with_object.rb | 42 - spec/ruby/core/enumerator/to_enum_spec.rb | 6 - spec/ruby/core/enumerator/with_object_spec.rb | 5 +- spec/ruby/core/env/each_pair_spec.rb | 61 +- spec/ruby/core/env/each_spec.rb | 5 +- spec/ruby/core/env/element_set_spec.rb | 60 +- spec/ruby/core/env/filter_spec.rb | 12 +- spec/ruby/core/env/has_key_spec.rb | 5 +- spec/ruby/core/env/has_value_spec.rb | 5 +- spec/ruby/core/env/include_spec.rb | 30 +- spec/ruby/core/env/key_spec.rb | 5 +- spec/ruby/core/env/length_spec.rb | 5 +- spec/ruby/core/env/member_spec.rb | 5 +- spec/ruby/core/env/merge_spec.rb | 104 +- spec/ruby/core/env/select_spec.rb | 61 +- spec/ruby/core/env/shared/each.rb | 65 - spec/ruby/core/env/shared/include.rb | 30 - spec/ruby/core/env/shared/length.rb | 13 - spec/ruby/core/env/shared/select.rb | 61 - spec/ruby/core/env/shared/store.rb | 60 - spec/ruby/core/env/shared/update.rb | 104 -- spec/ruby/core/env/shared/value.rb | 29 - spec/ruby/core/env/size_spec.rb | 13 +- spec/ruby/core/env/store_spec.rb | 5 +- spec/ruby/core/env/update_spec.rb | 5 +- spec/ruby/core/env/value_spec.rb | 29 +- spec/ruby/core/false/inspect_spec.rb | 4 +- spec/ruby/core/false/xor_spec.rb | 8 +- spec/ruby/core/fiber/scheduler_spec.rb | 5 +- spec/ruby/core/fiber/set_scheduler_spec.rb | 51 +- spec/ruby/core/fiber/shared/scheduler.rb | 51 - spec/ruby/core/file/delete_spec.rb | 61 +- spec/ruby/core/file/fnmatch_spec.rb | 298 +++- spec/ruby/core/file/path_spec.rb | 5 +- spec/ruby/core/file/shared/fnmatch.rb | 294 ---- spec/ruby/core/file/shared/path.rb | 82 -- spec/ruby/core/file/shared/unlink.rb | 61 - spec/ruby/core/file/sticky_spec.rb | 2 +- spec/ruby/core/file/to_path_spec.rb | 82 +- spec/ruby/core/file/unlink_spec.rb | 5 +- spec/ruby/core/file/zero_spec.rb | 6 +- spec/ruby/core/filetest/empty_spec.rb | 7 + spec/ruby/core/filetest/zero_spec.rb | 6 +- spec/ruby/core/float/angle_spec.rb | 5 +- spec/ruby/core/float/arg_spec.rb | 36 +- spec/ruby/core/float/case_compare_spec.rb | 5 +- spec/ruby/core/float/equal_value_spec.rb | 38 +- spec/ruby/core/float/fdiv_spec.rb | 59 +- spec/ruby/core/float/inspect_spec.rb | 5 +- spec/ruby/core/float/magnitude_spec.rb | 10 +- spec/ruby/core/float/modulo_spec.rb | 52 +- spec/ruby/core/float/phase_spec.rb | 5 +- spec/ruby/core/float/quo_spec.rb | 5 +- spec/ruby/core/float/shared/arg.rb | 36 - spec/ruby/core/float/shared/equal.rb | 38 - spec/ruby/core/float/shared/modulo.rb | 48 - spec/ruby/core/float/shared/quo.rb | 59 - spec/ruby/core/float/shared/to_s.rb | 308 ---- spec/ruby/core/float/to_int_spec.rb | 5 +- spec/ruby/core/float/to_s_spec.rb | 308 +++- spec/ruby/core/hash/has_value_spec.rb | 13 +- spec/ruby/core/hash/merge_spec.rb | 75 +- spec/ruby/core/hash/update_spec.rb | 76 +- spec/ruby/core/hash/value_spec.rb | 13 +- spec/ruby/core/integer/abs_spec.rb | 18 +- spec/ruby/core/integer/case_compare_spec.rb | 5 +- spec/ruby/core/integer/equal_value_spec.rb | 63 +- spec/ruby/core/integer/inspect_spec.rb | 7 + spec/ruby/core/integer/magnitude_spec.rb | 5 +- spec/ruby/core/integer/modulo_spec.rb | 118 +- spec/ruby/core/integer/next_spec.rb | 5 +- spec/ruby/core/integer/shared/abs.rb | 18 - spec/ruby/core/integer/shared/equal.rb | 63 - spec/ruby/core/integer/shared/modulo.rb | 114 -- spec/ruby/core/integer/shared/next.rb | 25 - spec/ruby/core/integer/succ_spec.rb | 25 +- spec/ruby/core/io/each_char_spec.rb | 71 +- spec/ruby/core/io/each_codepoint_spec.rb | 51 +- spec/ruby/core/io/each_line_spec.rb | 246 +++- spec/ruby/core/io/each_spec.rb | 10 +- spec/ruby/core/io/eof_spec.rb | 6 + spec/ruby/core/io/foreach_spec.rb | 2 +- spec/ruby/core/io/isatty_spec.rb | 5 +- spec/ruby/core/io/pos_spec.rb | 32 +- spec/ruby/core/io/read_spec.rb | 2 +- spec/ruby/core/io/readlines_spec.rb | 2 +- spec/ruby/core/io/shared/chars.rb | 73 - spec/ruby/core/io/shared/codepoints.rb | 54 - spec/ruby/core/io/shared/each.rb | 251 ---- spec/ruby/core/io/shared/pos.rb | 34 - spec/ruby/core/io/shared/tty.rb | 24 - spec/ruby/core/io/tell_spec.rb | 6 +- spec/ruby/core/io/to_i_spec.rb | 9 +- spec/ruby/core/io/to_path_spec.rb | 7 + spec/ruby/core/io/tty_spec.rb | 23 +- spec/ruby/core/kernel/clone_spec.rb | 6 - spec/ruby/core/kernel/dup_spec.rb | 6 - spec/ruby/core/kernel/enum_for_spec.rb | 4 +- spec/ruby/core/kernel/fail_spec.rb | 39 +- spec/ruby/core/kernel/format_spec.rb | 42 +- spec/ruby/core/kernel/is_a_spec.rb | 54 +- spec/ruby/core/kernel/kind_of_spec.rb | 5 +- .../ruby/core/kernel/require_relative_spec.rb | 8 +- spec/ruby/core/kernel/shared/kind_of.rb | 55 - spec/ruby/core/kernel/shared/sprintf.rb | 23 + spec/ruby/core/kernel/then_spec.rb | 10 +- spec/ruby/core/kernel/to_enum_spec.rb | 56 +- spec/ruby/core/marshal/load_spec.rb | 1289 +++++++++++++++- spec/ruby/core/marshal/restore_spec.rb | 5 +- spec/ruby/core/marshal/shared/load.rb | 1291 ----------------- spec/ruby/core/matchdata/captures_spec.rb | 11 +- spec/ruby/core/matchdata/deconstruct_spec.rb | 5 +- spec/ruby/core/matchdata/eql_spec.rb | 5 +- spec/ruby/core/matchdata/equal_value_spec.rb | 24 +- spec/ruby/core/matchdata/length_spec.rb | 5 +- spec/ruby/core/matchdata/shared/captures.rb | 13 - spec/ruby/core/matchdata/shared/eql.rb | 26 - spec/ruby/core/matchdata/shared/length.rb | 5 - spec/ruby/core/matchdata/size_spec.rb | 5 +- spec/ruby/core/method/call_spec.rb | 51 +- spec/ruby/core/method/case_compare_spec.rb | 6 +- .../core/method/element_reference_spec.rb | 6 +- spec/ruby/core/method/eql_spec.rb | 5 +- spec/ruby/core/method/equal_value_spec.rb | 92 +- spec/ruby/core/method/inspect_spec.rb | 7 +- spec/ruby/core/method/shared/call.rb | 51 - spec/ruby/core/method/shared/eql.rb | 94 -- spec/ruby/core/module/class_eval_spec.rb | 6 +- spec/ruby/core/module/class_exec_spec.rb | 6 +- spec/ruby/core/module/inspect_spec.rb | 7 + spec/ruby/core/module/module_eval_spec.rb | 172 ++- spec/ruby/core/module/module_exec_spec.rb | 35 +- spec/ruby/core/module/shared/class_eval.rb | 172 --- spec/ruby/core/module/shared/class_exec.rb | 35 - spec/ruby/core/nil/xor_spec.rb | 8 +- spec/ruby/core/numeric/abs_spec.rb | 17 +- spec/ruby/core/numeric/angle_spec.rb | 5 +- spec/ruby/core/numeric/arg_spec.rb | 36 +- spec/ruby/core/numeric/conj_spec.rb | 5 +- spec/ruby/core/numeric/conjugate_spec.rb | 18 +- spec/ruby/core/numeric/imag_spec.rb | 5 +- spec/ruby/core/numeric/imaginary_spec.rb | 24 +- spec/ruby/core/numeric/magnitude_spec.rb | 5 +- spec/ruby/core/numeric/modulo_spec.rb | 19 +- spec/ruby/core/numeric/phase_spec.rb | 5 +- spec/ruby/core/numeric/rect_spec.rb | 5 +- spec/ruby/core/numeric/rectangular_spec.rb | 46 +- spec/ruby/core/numeric/shared/abs.rb | 19 - spec/ruby/core/numeric/shared/arg.rb | 38 - spec/ruby/core/numeric/shared/conj.rb | 20 - spec/ruby/core/numeric/shared/imag.rb | 26 - spec/ruby/core/numeric/shared/rect.rb | 48 - .../objectspace/weakmap/each_pair_spec.rb | 11 +- .../core/objectspace/weakmap/each_spec.rb | 2 +- .../objectspace/weakmap/each_value_spec.rb | 2 +- .../core/objectspace/weakmap/include_spec.rb | 30 +- .../ruby/core/objectspace/weakmap/key_spec.rb | 6 +- .../core/objectspace/weakmap/length_spec.rb | 6 +- .../core/objectspace/weakmap/member_spec.rb | 6 +- .../objectspace/weakmap/shared/include.rb | 30 - .../core/objectspace/weakmap/shared/size.rb | 14 - .../core/objectspace/weakmap/size_spec.rb | 14 +- spec/ruby/core/proc/call_spec.rb | 142 +- spec/ruby/core/proc/case_compare_spec.rb | 15 +- spec/ruby/core/proc/element_reference_spec.rb | 24 +- spec/ruby/core/proc/eql_spec.rb | 5 +- spec/ruby/core/proc/equal_value_spec.rb | 81 +- spec/ruby/core/proc/fixtures/proc_aref.rb | 10 - .../core/proc/fixtures/proc_aref_frozen.rb | 10 - spec/ruby/core/proc/fixtures/proc_call.rb | 10 + .../core/proc/fixtures/proc_call_frozen.rb | 10 + spec/ruby/core/proc/inspect_spec.rb | 5 +- spec/ruby/core/proc/shared/call.rb | 99 -- spec/ruby/core/proc/shared/call_arguments.rb | 29 - spec/ruby/core/proc/shared/equal.rb | 83 -- spec/ruby/core/proc/shared/to_s.rb | 60 - spec/ruby/core/proc/to_s_spec.rb | 60 +- spec/ruby/core/proc/yield_spec.rb | 15 +- spec/ruby/core/process/daemon_spec.rb | 15 +- spec/ruby/core/process/detach_spec.rb | 119 +- spec/ruby/core/process/setpgid_spec.rb | 2 +- spec/ruby/core/process/setpgrp_spec.rb | 2 +- spec/ruby/core/process/status/wait_spec.rb | 36 +- spec/ruby/core/process/wait2_spec.rb | 19 +- spec/ruby/core/process/wait_spec.rb | 37 +- spec/ruby/core/process/waitall_spec.rb | 20 +- spec/ruby/core/process/waitpid2_spec.rb | 4 +- spec/ruby/core/process/waitpid_spec.rb | 11 +- spec/ruby/core/queue/deq_spec.rb | 10 +- spec/ruby/core/queue/enq_spec.rb | 5 +- spec/ruby/core/queue/length_spec.rb | 5 +- spec/ruby/core/queue/push_spec.rb | 5 +- spec/ruby/core/queue/shift_spec.rb | 10 +- spec/ruby/core/range/entries_spec.rb | 7 + spec/ruby/core/range/include_spec.rb | 90 +- spec/ruby/core/range/member_spec.rb | 9 +- spec/ruby/core/range/shared/include.rb | 91 -- spec/ruby/core/rational/abs_spec.rb | 9 +- spec/ruby/core/rational/magnitude_spec.rb | 7 +- spec/ruby/core/rational/quo_spec.rb | 22 +- spec/ruby/core/rational/shared/abs.rb | 11 - .../core/refinement/refined_class_spec.rb | 1 - spec/ruby/core/refinement/shared/target.rb | 13 - spec/ruby/core/refinement/target_spec.rb | 13 +- spec/ruby/core/regexp/eql_spec.rb | 5 +- spec/ruby/core/regexp/equal_value_spec.rb | 31 +- spec/ruby/core/regexp/escape_spec.rb | 5 +- spec/ruby/core/regexp/quote_spec.rb | 41 +- spec/ruby/core/regexp/shared/equal_value.rb | 31 - spec/ruby/core/regexp/shared/quote.rb | 41 - spec/ruby/core/set/add_spec.rb | 14 +- spec/ruby/core/set/append_spec.rb | 5 +- spec/ruby/core/set/case_compare_spec.rb | 8 +- spec/ruby/core/set/case_equality_spec.rb | 6 - spec/ruby/core/set/collect_spec.rb | 5 +- spec/ruby/core/set/difference_spec.rb | 5 +- spec/ruby/core/set/eql_spec.rb | 24 +- spec/ruby/core/set/filter_spec.rb | 5 +- spec/ruby/core/set/gt_spec.rb | 7 + spec/ruby/core/set/gte_spec.rb | 7 + spec/ruby/core/set/include_spec.rb | 29 +- spec/ruby/core/set/inspect_spec.rb | 5 +- spec/ruby/core/set/intersection_spec.rb | 19 +- spec/ruby/core/set/length_spec.rb | 5 +- spec/ruby/core/set/lt_spec.rb | 7 + spec/ruby/core/set/lte_spec.rb | 7 + spec/ruby/core/set/map_spec.rb | 20 +- spec/ruby/core/set/member_spec.rb | 5 +- spec/ruby/core/set/minus_spec.rb | 15 +- spec/ruby/core/set/plus_spec.rb | 5 +- spec/ruby/core/set/select_spec.rb | 39 +- spec/ruby/core/set/shared/add.rb | 14 - spec/ruby/core/set/shared/collect.rb | 20 - spec/ruby/core/set/shared/difference.rb | 15 - spec/ruby/core/set/shared/include.rb | 29 - spec/ruby/core/set/shared/inspect.rb | 45 - spec/ruby/core/set/shared/intersection.rb | 15 - spec/ruby/core/set/shared/length.rb | 6 - spec/ruby/core/set/shared/select.rb | 41 - spec/ruby/core/set/shared/union.rb | 15 - spec/ruby/core/set/size_spec.rb | 6 +- spec/ruby/core/set/to_s_spec.rb | 46 +- spec/ruby/core/set/union_spec.rb | 19 +- spec/ruby/core/sizedqueue/deq_spec.rb | 10 +- spec/ruby/core/sizedqueue/enq_spec.rb | 15 +- spec/ruby/core/sizedqueue/length_spec.rb | 5 +- spec/ruby/core/sizedqueue/push_spec.rb | 15 +- spec/ruby/core/sizedqueue/shift_spec.rb | 10 +- spec/ruby/core/string/case_compare_spec.rb | 7 +- spec/ruby/core/string/codepoints_spec.rb | 1 - spec/ruby/core/string/dedup_spec.rb | 5 +- spec/ruby/core/string/each_codepoint_spec.rb | 34 +- spec/ruby/core/string/equal_value_spec.rb | 26 +- spec/ruby/core/string/intern_spec.rb | 6 +- spec/ruby/core/string/length_spec.rb | 53 +- spec/ruby/core/string/next_spec.rb | 10 +- spec/ruby/core/string/shared/dedup.rb | 51 - .../shared/each_codepoint_without_block.rb | 33 - spec/ruby/core/string/shared/equal_value.rb | 29 - spec/ruby/core/string/shared/length.rb | 55 - spec/ruby/core/string/shared/succ.rb | 87 -- spec/ruby/core/string/shared/to_s.rb | 13 - spec/ruby/core/string/shared/to_sym.rb | 72 - spec/ruby/core/string/size_spec.rb | 6 +- spec/ruby/core/string/slice_spec.rb | 33 +- spec/ruby/core/string/succ_spec.rb | 85 +- spec/ruby/core/string/to_s_spec.rb | 13 +- spec/ruby/core/string/to_str_spec.rb | 6 +- spec/ruby/core/string/to_sym_spec.rb | 73 +- spec/ruby/core/string/uminus_spec.rb | 51 +- spec/ruby/core/struct/deconstruct_spec.rb | 7 +- spec/ruby/core/struct/filter_spec.rb | 10 +- spec/ruby/core/struct/inspect_spec.rb | 5 +- spec/ruby/core/struct/length_spec.rb | 8 +- spec/ruby/core/struct/select_spec.rb | 25 +- spec/ruby/core/struct/shared/inspect.rb | 40 - spec/ruby/core/struct/shared/select.rb | 26 - spec/ruby/core/struct/size_spec.rb | 5 +- spec/ruby/core/struct/to_s_spec.rb | 39 +- spec/ruby/core/struct/values_spec.rb | 7 +- spec/ruby/core/symbol/case_compare_spec.rb | 8 +- .../core/symbol/element_reference_spec.rb | 261 +++- spec/ruby/core/symbol/id2name_spec.rb | 5 +- spec/ruby/core/symbol/intern_spec.rb | 8 +- spec/ruby/core/symbol/length_spec.rb | 21 +- spec/ruby/core/symbol/next_spec.rb | 5 +- spec/ruby/core/symbol/shared/id2name.rb | 30 - spec/ruby/core/symbol/shared/length.rb | 23 - spec/ruby/core/symbol/shared/slice.rb | 262 ---- spec/ruby/core/symbol/shared/succ.rb | 18 - spec/ruby/core/symbol/size_spec.rb | 5 +- spec/ruby/core/symbol/slice_spec.rb | 5 +- spec/ruby/core/symbol/succ_spec.rb | 16 +- spec/ruby/core/symbol/to_s_spec.rb | 30 +- spec/ruby/core/symbol/to_sym_spec.rb | 2 +- spec/ruby/core/thread/exit_spec.rb | 7 +- spec/ruby/core/thread/fork_spec.rb | 6 +- spec/ruby/core/thread/inspect_spec.rb | 5 +- spec/ruby/core/thread/kill_spec.rb | 213 ++- spec/ruby/core/thread/shared/exit.rb | 219 --- spec/ruby/core/thread/shared/start.rb | 41 - spec/ruby/core/thread/shared/to_s.rb | 53 - spec/ruby/core/thread/start_spec.rb | 42 +- spec/ruby/core/thread/terminate_spec.rb | 6 +- spec/ruby/core/thread/to_s_spec.rb | 52 +- spec/ruby/core/time/asctime_spec.rb | 6 +- spec/ruby/core/time/ctime_spec.rb | 5 +- spec/ruby/core/time/day_spec.rb | 15 +- spec/ruby/core/time/dst_spec.rb | 8 +- spec/ruby/core/time/getgm_spec.rb | 5 +- spec/ruby/core/time/getutc_spec.rb | 9 +- spec/ruby/core/time/gm_spec.rb | 9 +- spec/ruby/core/time/gmt_offset_spec.rb | 5 +- spec/ruby/core/time/gmt_spec.rb | 5 +- spec/ruby/core/time/gmtime_spec.rb | 5 +- spec/ruby/core/time/gmtoff_spec.rb | 5 +- spec/ruby/core/time/isdst_spec.rb | 5 +- spec/ruby/core/time/iso8601_spec.rb | 31 +- spec/ruby/core/time/mday_spec.rb | 5 +- spec/ruby/core/time/mktime_spec.rb | 10 +- spec/ruby/core/time/mon_spec.rb | 5 +- spec/ruby/core/time/month_spec.rb | 15 +- spec/ruby/core/time/shared/asctime.rb | 6 - spec/ruby/core/time/shared/day.rb | 15 - spec/ruby/core/time/shared/getgm.rb | 9 - spec/ruby/core/time/shared/gm.rb | 70 - spec/ruby/core/time/shared/gmt_offset.rb | 59 - spec/ruby/core/time/shared/gmtime.rb | 40 - spec/ruby/core/time/shared/isdst.rb | 8 - spec/ruby/core/time/shared/month.rb | 15 - spec/ruby/core/time/shared/to_i.rb | 16 - spec/ruby/core/time/shared/xmlschema.rb | 31 - spec/ruby/core/time/to_i_spec.rb | 16 +- spec/ruby/core/time/tv_nsec_spec.rb | 4 +- spec/ruby/core/time/tv_sec_spec.rb | 5 +- spec/ruby/core/time/tv_usec_spec.rb | 4 +- spec/ruby/core/time/utc_offset_spec.rb | 59 +- spec/ruby/core/time/utc_spec.rb | 113 +- spec/ruby/core/time/xmlschema_spec.rb | 7 +- spec/ruby/core/true/inspect_spec.rb | 4 +- spec/ruby/core/unboundmethod/eql_spec.rb | 4 +- spec/ruby/core/unboundmethod/inspect_spec.rb | 8 +- spec/ruby/core/unboundmethod/shared/to_s.rb | 33 - spec/ruby/core/unboundmethod/to_s_spec.rb | 31 +- spec/ruby/language/def_spec.rb | 33 +- spec/ruby/language/delegation_spec.rb | 14 + spec/ruby/language/fixtures/delegation.rb | 4 + spec/ruby/language/predefined_spec.rb | 2 +- .../library/bigdecimal/case_compare_spec.rb | 6 +- spec/ruby/library/bigdecimal/clone_spec.rb | 8 +- spec/ruby/library/bigdecimal/dup_spec.rb | 12 +- spec/ruby/library/bigdecimal/eql_spec.rb | 6 +- .../library/bigdecimal/equal_value_spec.rb | 60 +- spec/ruby/library/bigdecimal/modulo_spec.rb | 15 +- spec/ruby/library/bigdecimal/shared/clone.rb | 13 - spec/ruby/library/bigdecimal/shared/eql.rb | 61 - spec/ruby/library/bigdecimal/shared/modulo.rb | 10 - spec/ruby/library/bigdecimal/shared/to_int.rb | 16 - spec/ruby/library/bigdecimal/to_i_spec.rb | 14 +- spec/ruby/library/bigdecimal/to_int_spec.rb | 6 +- spec/ruby/library/date/asctime_spec.rb | 5 +- spec/ruby/library/date/commercial_spec.rb | 46 +- spec/ruby/library/date/ctime_spec.rb | 4 +- spec/ruby/library/date/jd_spec.rb | 15 +- spec/ruby/library/date/mday_spec.rb | 4 +- spec/ruby/library/date/mon_spec.rb | 5 +- spec/ruby/library/date/month_spec.rb | 6 +- spec/ruby/library/date/ordinal_spec.rb | 14 +- spec/ruby/library/date/shared/commercial.rb | 39 - spec/ruby/library/date/shared/jd.rb | 14 - spec/ruby/library/date/shared/month.rb | 6 - spec/ruby/library/date/shared/ordinal.rb | 22 - spec/ruby/library/date/shared/valid_civil.rb | 36 - .../library/date/shared/valid_commercial.rb | 34 - spec/ruby/library/date/shared/valid_jd.rb | 20 - .../ruby/library/date/shared/valid_ordinal.rb | 26 - spec/ruby/library/date/succ_spec.rb | 4 +- spec/ruby/library/date/valid_civil_spec.rb | 9 +- .../library/date/valid_commercial_spec.rb | 33 +- spec/ruby/library/date/valid_date_spec.rb | 35 +- spec/ruby/library/date/valid_jd_spec.rb | 18 +- spec/ruby/library/date/valid_ordinal_spec.rb | 26 +- spec/ruby/library/datetime/hour_spec.rb | 3 +- spec/ruby/library/datetime/iso8601_spec.rb | 4 +- spec/ruby/library/datetime/min_spec.rb | 8 +- spec/ruby/library/datetime/minute_spec.rb | 40 +- spec/ruby/library/datetime/sec_spec.rb | 8 +- .../library/datetime/second_fraction_spec.rb | 4 +- spec/ruby/library/datetime/second_spec.rb | 43 +- spec/ruby/library/datetime/shared/min.rb | 40 - spec/ruby/library/datetime/shared/sec.rb | 45 - .../library/digest/instance/append_spec.rb | 8 +- .../library/digest/instance/shared/update.rb | 8 - .../library/digest/instance/update_spec.rb | 5 +- spec/ruby/library/digest/md5/append_spec.rb | 7 +- spec/ruby/library/digest/md5/length_spec.rb | 8 +- spec/ruby/library/digest/md5/shared/length.rb | 8 - spec/ruby/library/digest/md5/shared/update.rb | 7 - spec/ruby/library/digest/md5/size_spec.rb | 5 +- spec/ruby/library/digest/md5/update_spec.rb | 7 +- .../ruby/library/digest/sha256/append_spec.rb | 7 +- .../ruby/library/digest/sha256/length_spec.rb | 8 +- .../library/digest/sha256/shared/length.rb | 8 - .../library/digest/sha256/shared/update.rb | 7 - spec/ruby/library/digest/sha256/size_spec.rb | 5 +- .../ruby/library/digest/sha256/update_spec.rb | 7 +- .../ruby/library/digest/sha384/append_spec.rb | 7 +- .../ruby/library/digest/sha384/length_spec.rb | 8 +- .../library/digest/sha384/shared/length.rb | 8 - .../library/digest/sha384/shared/update.rb | 7 - spec/ruby/library/digest/sha384/size_spec.rb | 7 +- .../ruby/library/digest/sha384/update_spec.rb | 7 +- .../ruby/library/digest/sha512/append_spec.rb | 7 +- .../ruby/library/digest/sha512/length_spec.rb | 8 +- .../library/digest/sha512/shared/length.rb | 8 - .../library/digest/sha512/shared/update.rb | 7 - spec/ruby/library/digest/sha512/size_spec.rb | 7 +- .../ruby/library/digest/sha512/update_spec.rb | 7 +- .../library/getoptlong/each_option_spec.rb | 18 +- spec/ruby/library/getoptlong/each_spec.rb | 5 +- .../library/getoptlong/get_option_spec.rb | 5 +- spec/ruby/library/getoptlong/get_spec.rb | 62 +- spec/ruby/library/getoptlong/shared/each.rb | 18 - spec/ruby/library/getoptlong/shared/get.rb | 62 - spec/ruby/library/matrix/I_spec.rb | 6 +- spec/ruby/library/matrix/collect_spec.rb | 6 +- spec/ruby/library/matrix/conj_spec.rb | 6 +- spec/ruby/library/matrix/conjugate_spec.rb | 18 +- spec/ruby/library/matrix/det_spec.rb | 5 +- spec/ruby/library/matrix/determinant_spec.rb | 36 +- spec/ruby/library/matrix/identity_spec.rb | 18 +- spec/ruby/library/matrix/imag_spec.rb | 6 +- spec/ruby/library/matrix/imaginary_spec.rb | 19 +- spec/ruby/library/matrix/inv_spec.rb | 7 +- spec/ruby/library/matrix/inverse_spec.rb | 36 +- spec/ruby/library/matrix/map_spec.rb | 24 +- spec/ruby/library/matrix/rect_spec.rb | 6 +- spec/ruby/library/matrix/rectangular_spec.rb | 17 +- spec/ruby/library/matrix/shared/collect.rb | 26 - spec/ruby/library/matrix/shared/conjugate.rb | 20 - .../ruby/library/matrix/shared/determinant.rb | 38 - spec/ruby/library/matrix/shared/identity.rb | 19 - spec/ruby/library/matrix/shared/imaginary.rb | 20 - spec/ruby/library/matrix/shared/inverse.rb | 38 - .../ruby/library/matrix/shared/rectangular.rb | 18 - spec/ruby/library/matrix/shared/trace.rb | 12 - spec/ruby/library/matrix/shared/transpose.rb | 19 - spec/ruby/library/matrix/t_spec.rb | 8 +- spec/ruby/library/matrix/tr_spec.rb | 5 +- spec/ruby/library/matrix/trace_spec.rb | 9 +- spec/ruby/library/matrix/transpose_spec.rb | 17 +- spec/ruby/library/matrix/unit_spec.rb | 6 +- .../ruby/library/net-http/http/active_spec.rb | 6 +- spec/ruby/library/net-http/http/get2_spec.rb | 6 +- spec/ruby/library/net-http/http/head2_spec.rb | 6 +- .../net-http/http/is_version_1_1_spec.rb | 5 +- .../net-http/http/is_version_1_2_spec.rb | 5 +- spec/ruby/library/net-http/http/post2_spec.rb | 6 +- spec/ruby/library/net-http/http/put2_spec.rb | 6 +- .../library/net-http/http/request_get_spec.rb | 41 +- .../net-http/http/request_head_spec.rb | 41 +- .../net-http/http/request_post_spec.rb | 41 +- .../library/net-http/http/request_put_spec.rb | 41 +- .../net-http/http/shared/request_get.rb | 41 - .../net-http/http/shared/request_head.rb | 41 - .../net-http/http/shared/request_post.rb | 41 - .../net-http/http/shared/request_put.rb | 41 - .../library/net-http/http/shared/started.rb | 26 - .../net-http/http/shared/version_1_1.rb | 6 - .../net-http/http/shared/version_1_2.rb | 6 - .../library/net-http/http/started_spec.rb | 26 +- .../library/net-http/http/version_1_1_spec.rb | 6 +- .../library/net-http/http/version_1_2_spec.rb | 6 +- .../httpheader/canonical_each_spec.rb | 7 +- .../net-http/httpheader/content_type_spec.rb | 6 +- .../httpheader/each_capitalized_spec.rb | 31 +- .../net-http/httpheader/each_header_spec.rb | 31 +- .../net-http/httpheader/each_key_spec.rb | 31 +- .../net-http/httpheader/each_name_spec.rb | 6 +- .../library/net-http/httpheader/each_spec.rb | 7 +- .../net-http/httpheader/form_data_spec.rb | 6 +- .../net-http/httpheader/length_spec.rb | 7 +- .../library/net-http/httpheader/range_spec.rb | 6 +- .../httpheader/set_content_type_spec.rb | 18 +- .../net-http/httpheader/set_form_data_spec.rb | 27 +- .../net-http/httpheader/set_range_spec.rb | 89 +- .../httpheader/shared/each_capitalized.rb | 31 - .../net-http/httpheader/shared/each_header.rb | 31 - .../net-http/httpheader/shared/each_name.rb | 31 - .../httpheader/shared/set_content_type.rb | 18 - .../httpheader/shared/set_form_data.rb | 27 - .../net-http/httpheader/shared/set_range.rb | 89 -- .../net-http/httpheader/shared/size.rb | 18 - .../library/net-http/httpheader/size_spec.rb | 18 +- .../net-http/httpresponse/body_spec.rb | 19 +- .../net-http/httpresponse/entity_spec.rb | 6 +- .../net-http/httpresponse/shared/body.rb | 20 - .../library/openstruct/equal_value_spec.rb | 2 +- spec/ruby/library/openstruct/inspect_spec.rb | 6 +- .../ruby/library/openstruct/shared/inspect.rb | 20 - spec/ruby/library/openstruct/to_s_spec.rb | 20 +- .../library/pathname/case_compare_spec.rb | 8 + spec/ruby/library/pathname/divide_spec.rb | 6 +- spec/ruby/library/pathname/plus_spec.rb | 7 +- spec/ruby/library/pathname/shared/plus.rb | 8 - spec/ruby/library/prime/next_spec.rb | 8 +- spec/ruby/library/prime/shared/next.rb | 8 - spec/ruby/library/prime/succ_spec.rb | 6 +- .../socket/addrinfo/shared/to_sockaddr.rb | 47 - .../ruby/library/socket/addrinfo/to_s_spec.rb | 5 +- .../socket/addrinfo/to_sockaddr_spec.rb | 47 +- .../library/socket/shared/pack_sockaddr.rb | 92 -- spec/ruby/library/socket/shared/socketpair.rb | 138 -- .../socket/socket/pack_sockaddr_in_spec.rb | 6 +- .../socket/socket/pack_sockaddr_un_spec.rb | 8 +- spec/ruby/library/socket/socket/pair_spec.rb | 138 +- .../library/socket/socket/sockaddr_in_spec.rb | 48 +- .../library/socket/socket/sockaddr_un_spec.rb | 46 +- .../library/socket/socket/socketpair_spec.rb | 6 +- .../library/socket/unixsocket/pair_spec.rb | 45 +- .../library/socket/unixsocket/shared/pair.rb | 47 - .../socket/unixsocket/socketpair_spec.rb | 15 +- spec/ruby/library/stringio/each_byte_spec.rb | 46 +- spec/ruby/library/stringio/each_char_spec.rb | 33 +- .../library/stringio/each_codepoint_spec.rb | 46 +- spec/ruby/library/stringio/each_line_spec.rb | 199 ++- spec/ruby/library/stringio/each_spec.rb | 33 +- spec/ruby/library/stringio/eof_spec.rb | 30 +- spec/ruby/library/stringio/isatty_spec.rb | 7 +- spec/ruby/library/stringio/length_spec.rb | 7 +- spec/ruby/library/stringio/pos_spec.rb | 12 +- .../library/stringio/shared/codepoints.rb | 45 - spec/ruby/library/stringio/shared/each.rb | 209 --- .../ruby/library/stringio/shared/each_byte.rb | 48 - .../ruby/library/stringio/shared/each_char.rb | 36 - spec/ruby/library/stringio/shared/eof.rb | 24 - spec/ruby/library/stringio/shared/isatty.rb | 5 - spec/ruby/library/stringio/shared/length.rb | 5 - spec/ruby/library/stringio/shared/tell.rb | 12 - spec/ruby/library/stringio/size_spec.rb | 7 +- spec/ruby/library/stringio/tell_spec.rb | 7 +- spec/ruby/library/stringio/tty_spec.rb | 7 +- .../ruby/library/stringscanner/append_spec.rb | 28 +- .../stringscanner/beginning_of_line_spec.rb | 25 +- spec/ruby/library/stringscanner/bol_spec.rb | 5 +- .../ruby/library/stringscanner/concat_spec.rb | 9 +- .../library/stringscanner/pointer_spec.rb | 9 +- spec/ruby/library/stringscanner/pos_spec.rb | 57 +- spec/ruby/library/stringscanner/shared/bol.rb | 25 - .../library/stringscanner/shared/concat.rb | 30 - spec/ruby/library/stringscanner/shared/pos.rb | 59 - spec/ruby/library/syslog/open_spec.rb | 38 +- spec/ruby/library/syslog/reopen_spec.rb | 5 +- spec/ruby/library/syslog/shared/reopen.rb | 40 - spec/ruby/library/tempfile/delete_spec.rb | 12 +- spec/ruby/library/tempfile/length_spec.rb | 5 +- spec/ruby/library/tempfile/shared/length.rb | 21 - spec/ruby/library/tempfile/shared/unlink.rb | 12 - spec/ruby/library/tempfile/size_spec.rb | 21 +- spec/ruby/library/tempfile/unlink_spec.rb | 5 +- spec/ruby/library/time/iso8601_spec.rb | 5 +- spec/ruby/library/time/rfc2822_spec.rb | 65 +- spec/ruby/library/time/rfc822_spec.rb | 5 +- spec/ruby/library/time/shared/rfc2822.rb | 65 - spec/ruby/library/time/shared/xmlschema.rb | 53 - spec/ruby/library/time/xmlschema_spec.rb | 53 +- spec/ruby/library/uri/parser/extract_spec.rb | 87 +- spec/ruby/library/uri/parser/join_spec.rb | 59 +- spec/ruby/library/uri/parser/parse_spec.rb | 210 ++- spec/ruby/library/uri/shared/extract.rb | 83 -- spec/ruby/library/uri/shared/join.rb | 56 - spec/ruby/library/uri/shared/parse.rb | 206 --- spec/ruby/library/yaml/load_stream_spec.rb | 20 +- .../ruby/library/yaml/shared/each_document.rb | 19 - .../library/zlib/gzipreader/each_line_spec.rb | 7 +- .../ruby/library/zlib/gzipreader/each_spec.rb | 47 +- spec/ruby/library/zlib/gzipreader/eof_spec.rb | 7 + .../library/zlib/gzipreader/shared/each.rb | 49 - .../ruby/library/zlib/gzipreader/tell_spec.rb | 9 + spec/ruby/optional/capi/io_spec.rb | 6 +- spec/ruby/shared/kernel/raise.rb | 15 + spec/ruby/shared/process/fork.rb | 4 +- 640 files changed, 10544 insertions(+), 11372 deletions(-) delete mode 100644 spec/ruby/core/dir/shared/delete.rb delete mode 100644 spec/ruby/core/enumerable/shared/collect.rb delete mode 100644 spec/ruby/core/enumerable/shared/collect_concat.rb delete mode 100644 spec/ruby/core/enumerable/shared/entries.rb delete mode 100644 spec/ruby/core/enumerable/shared/find.rb delete mode 100644 spec/ruby/core/enumerable/shared/find_all.rb delete mode 100644 spec/ruby/core/enumerable/shared/include.rb delete mode 100644 spec/ruby/core/enumerable/shared/inject.rb delete mode 100644 spec/ruby/core/enumerator/enum_for_spec.rb delete mode 100644 spec/ruby/core/enumerator/lazy/shared/collect.rb delete mode 100644 spec/ruby/core/enumerator/lazy/shared/collect_concat.rb delete mode 100644 spec/ruby/core/enumerator/lazy/shared/select.rb delete mode 100644 spec/ruby/core/enumerator/lazy/shared/to_enum.rb create mode 100644 spec/ruby/core/enumerator/shared/each.rb delete mode 100644 spec/ruby/core/enumerator/shared/enum_for.rb delete mode 100644 spec/ruby/core/enumerator/shared/with_object.rb delete mode 100644 spec/ruby/core/enumerator/to_enum_spec.rb delete mode 100644 spec/ruby/core/env/shared/each.rb delete mode 100644 spec/ruby/core/env/shared/include.rb delete mode 100644 spec/ruby/core/env/shared/length.rb delete mode 100644 spec/ruby/core/env/shared/select.rb delete mode 100644 spec/ruby/core/env/shared/store.rb delete mode 100644 spec/ruby/core/env/shared/update.rb delete mode 100644 spec/ruby/core/env/shared/value.rb delete mode 100644 spec/ruby/core/fiber/shared/scheduler.rb delete mode 100644 spec/ruby/core/file/shared/fnmatch.rb delete mode 100644 spec/ruby/core/file/shared/path.rb delete mode 100644 spec/ruby/core/file/shared/unlink.rb create mode 100644 spec/ruby/core/filetest/empty_spec.rb delete mode 100644 spec/ruby/core/float/shared/arg.rb delete mode 100644 spec/ruby/core/float/shared/equal.rb delete mode 100644 spec/ruby/core/float/shared/modulo.rb delete mode 100644 spec/ruby/core/float/shared/quo.rb delete mode 100644 spec/ruby/core/float/shared/to_s.rb create mode 100644 spec/ruby/core/integer/inspect_spec.rb delete mode 100644 spec/ruby/core/integer/shared/abs.rb delete mode 100644 spec/ruby/core/integer/shared/equal.rb delete mode 100644 spec/ruby/core/integer/shared/modulo.rb delete mode 100644 spec/ruby/core/integer/shared/next.rb delete mode 100644 spec/ruby/core/io/shared/chars.rb delete mode 100644 spec/ruby/core/io/shared/codepoints.rb delete mode 100644 spec/ruby/core/io/shared/each.rb delete mode 100644 spec/ruby/core/io/shared/tty.rb create mode 100644 spec/ruby/core/io/to_path_spec.rb delete mode 100644 spec/ruby/core/kernel/shared/kind_of.rb delete mode 100644 spec/ruby/core/marshal/shared/load.rb delete mode 100644 spec/ruby/core/matchdata/shared/captures.rb delete mode 100644 spec/ruby/core/matchdata/shared/eql.rb delete mode 100644 spec/ruby/core/matchdata/shared/length.rb delete mode 100644 spec/ruby/core/method/shared/call.rb delete mode 100644 spec/ruby/core/method/shared/eql.rb create mode 100644 spec/ruby/core/module/inspect_spec.rb delete mode 100644 spec/ruby/core/module/shared/class_eval.rb delete mode 100644 spec/ruby/core/module/shared/class_exec.rb delete mode 100644 spec/ruby/core/numeric/shared/abs.rb delete mode 100644 spec/ruby/core/numeric/shared/arg.rb delete mode 100644 spec/ruby/core/numeric/shared/conj.rb delete mode 100644 spec/ruby/core/numeric/shared/imag.rb delete mode 100644 spec/ruby/core/numeric/shared/rect.rb delete mode 100644 spec/ruby/core/objectspace/weakmap/shared/include.rb delete mode 100644 spec/ruby/core/objectspace/weakmap/shared/size.rb delete mode 100644 spec/ruby/core/proc/fixtures/proc_aref.rb delete mode 100644 spec/ruby/core/proc/fixtures/proc_aref_frozen.rb create mode 100644 spec/ruby/core/proc/fixtures/proc_call.rb create mode 100644 spec/ruby/core/proc/fixtures/proc_call_frozen.rb delete mode 100644 spec/ruby/core/proc/shared/call.rb delete mode 100644 spec/ruby/core/proc/shared/call_arguments.rb delete mode 100644 spec/ruby/core/proc/shared/equal.rb delete mode 100644 spec/ruby/core/proc/shared/to_s.rb create mode 100644 spec/ruby/core/range/entries_spec.rb delete mode 100644 spec/ruby/core/range/shared/include.rb delete mode 100644 spec/ruby/core/rational/shared/abs.rb delete mode 100644 spec/ruby/core/refinement/shared/target.rb delete mode 100644 spec/ruby/core/regexp/shared/equal_value.rb delete mode 100644 spec/ruby/core/regexp/shared/quote.rb delete mode 100644 spec/ruby/core/set/case_equality_spec.rb create mode 100644 spec/ruby/core/set/gt_spec.rb create mode 100644 spec/ruby/core/set/gte_spec.rb create mode 100644 spec/ruby/core/set/lt_spec.rb create mode 100644 spec/ruby/core/set/lte_spec.rb delete mode 100644 spec/ruby/core/set/shared/add.rb delete mode 100644 spec/ruby/core/set/shared/collect.rb delete mode 100644 spec/ruby/core/set/shared/difference.rb delete mode 100644 spec/ruby/core/set/shared/include.rb delete mode 100644 spec/ruby/core/set/shared/inspect.rb delete mode 100644 spec/ruby/core/set/shared/intersection.rb delete mode 100644 spec/ruby/core/set/shared/length.rb delete mode 100644 spec/ruby/core/set/shared/select.rb delete mode 100644 spec/ruby/core/set/shared/union.rb delete mode 100644 spec/ruby/core/string/shared/dedup.rb delete mode 100644 spec/ruby/core/string/shared/each_codepoint_without_block.rb delete mode 100644 spec/ruby/core/string/shared/equal_value.rb delete mode 100644 spec/ruby/core/string/shared/length.rb delete mode 100644 spec/ruby/core/string/shared/succ.rb delete mode 100644 spec/ruby/core/string/shared/to_s.rb delete mode 100644 spec/ruby/core/string/shared/to_sym.rb delete mode 100644 spec/ruby/core/struct/shared/inspect.rb delete mode 100644 spec/ruby/core/struct/shared/select.rb delete mode 100644 spec/ruby/core/symbol/shared/id2name.rb delete mode 100644 spec/ruby/core/symbol/shared/length.rb delete mode 100644 spec/ruby/core/symbol/shared/slice.rb delete mode 100644 spec/ruby/core/symbol/shared/succ.rb delete mode 100644 spec/ruby/core/thread/shared/exit.rb delete mode 100644 spec/ruby/core/thread/shared/start.rb delete mode 100644 spec/ruby/core/thread/shared/to_s.rb delete mode 100644 spec/ruby/core/time/shared/asctime.rb delete mode 100644 spec/ruby/core/time/shared/day.rb delete mode 100644 spec/ruby/core/time/shared/getgm.rb delete mode 100644 spec/ruby/core/time/shared/gm.rb delete mode 100644 spec/ruby/core/time/shared/gmt_offset.rb delete mode 100644 spec/ruby/core/time/shared/gmtime.rb delete mode 100644 spec/ruby/core/time/shared/isdst.rb delete mode 100644 spec/ruby/core/time/shared/month.rb delete mode 100644 spec/ruby/core/time/shared/to_i.rb delete mode 100644 spec/ruby/core/time/shared/xmlschema.rb delete mode 100644 spec/ruby/core/unboundmethod/shared/to_s.rb delete mode 100644 spec/ruby/library/bigdecimal/shared/clone.rb delete mode 100644 spec/ruby/library/bigdecimal/shared/eql.rb delete mode 100644 spec/ruby/library/bigdecimal/shared/to_int.rb delete mode 100644 spec/ruby/library/date/shared/commercial.rb delete mode 100644 spec/ruby/library/date/shared/jd.rb delete mode 100644 spec/ruby/library/date/shared/month.rb delete mode 100644 spec/ruby/library/date/shared/ordinal.rb delete mode 100644 spec/ruby/library/date/shared/valid_civil.rb delete mode 100644 spec/ruby/library/date/shared/valid_commercial.rb delete mode 100644 spec/ruby/library/date/shared/valid_jd.rb delete mode 100644 spec/ruby/library/date/shared/valid_ordinal.rb delete mode 100644 spec/ruby/library/datetime/shared/min.rb delete mode 100644 spec/ruby/library/datetime/shared/sec.rb delete mode 100644 spec/ruby/library/digest/instance/shared/update.rb delete mode 100644 spec/ruby/library/digest/md5/shared/length.rb delete mode 100644 spec/ruby/library/digest/md5/shared/update.rb delete mode 100644 spec/ruby/library/digest/sha256/shared/length.rb delete mode 100644 spec/ruby/library/digest/sha256/shared/update.rb delete mode 100644 spec/ruby/library/digest/sha384/shared/length.rb delete mode 100644 spec/ruby/library/digest/sha384/shared/update.rb delete mode 100644 spec/ruby/library/digest/sha512/shared/length.rb delete mode 100644 spec/ruby/library/digest/sha512/shared/update.rb delete mode 100644 spec/ruby/library/getoptlong/shared/each.rb delete mode 100644 spec/ruby/library/getoptlong/shared/get.rb delete mode 100644 spec/ruby/library/matrix/shared/collect.rb delete mode 100644 spec/ruby/library/matrix/shared/conjugate.rb delete mode 100644 spec/ruby/library/matrix/shared/determinant.rb delete mode 100644 spec/ruby/library/matrix/shared/identity.rb delete mode 100644 spec/ruby/library/matrix/shared/imaginary.rb delete mode 100644 spec/ruby/library/matrix/shared/inverse.rb delete mode 100644 spec/ruby/library/matrix/shared/rectangular.rb delete mode 100644 spec/ruby/library/matrix/shared/trace.rb delete mode 100644 spec/ruby/library/matrix/shared/transpose.rb delete mode 100644 spec/ruby/library/net-http/http/shared/request_get.rb delete mode 100644 spec/ruby/library/net-http/http/shared/request_head.rb delete mode 100644 spec/ruby/library/net-http/http/shared/request_post.rb delete mode 100644 spec/ruby/library/net-http/http/shared/request_put.rb delete mode 100644 spec/ruby/library/net-http/http/shared/started.rb delete mode 100644 spec/ruby/library/net-http/http/shared/version_1_1.rb delete mode 100644 spec/ruby/library/net-http/http/shared/version_1_2.rb delete mode 100644 spec/ruby/library/net-http/httpheader/shared/each_capitalized.rb delete mode 100644 spec/ruby/library/net-http/httpheader/shared/each_header.rb delete mode 100644 spec/ruby/library/net-http/httpheader/shared/each_name.rb delete mode 100644 spec/ruby/library/net-http/httpheader/shared/set_content_type.rb delete mode 100644 spec/ruby/library/net-http/httpheader/shared/set_form_data.rb delete mode 100644 spec/ruby/library/net-http/httpheader/shared/set_range.rb delete mode 100644 spec/ruby/library/net-http/httpheader/shared/size.rb delete mode 100644 spec/ruby/library/net-http/httpresponse/shared/body.rb delete mode 100644 spec/ruby/library/openstruct/shared/inspect.rb create mode 100644 spec/ruby/library/pathname/case_compare_spec.rb delete mode 100644 spec/ruby/library/pathname/shared/plus.rb delete mode 100644 spec/ruby/library/prime/shared/next.rb delete mode 100644 spec/ruby/library/socket/addrinfo/shared/to_sockaddr.rb delete mode 100644 spec/ruby/library/socket/shared/pack_sockaddr.rb delete mode 100644 spec/ruby/library/socket/shared/socketpair.rb delete mode 100644 spec/ruby/library/socket/unixsocket/shared/pair.rb delete mode 100644 spec/ruby/library/stringio/shared/codepoints.rb delete mode 100644 spec/ruby/library/stringio/shared/each.rb delete mode 100644 spec/ruby/library/stringio/shared/each_byte.rb delete mode 100644 spec/ruby/library/stringio/shared/each_char.rb delete mode 100644 spec/ruby/library/stringio/shared/eof.rb delete mode 100644 spec/ruby/library/stringio/shared/isatty.rb delete mode 100644 spec/ruby/library/stringio/shared/length.rb delete mode 100644 spec/ruby/library/stringio/shared/tell.rb delete mode 100644 spec/ruby/library/stringscanner/shared/bol.rb delete mode 100644 spec/ruby/library/stringscanner/shared/concat.rb delete mode 100644 spec/ruby/library/stringscanner/shared/pos.rb delete mode 100644 spec/ruby/library/syslog/shared/reopen.rb delete mode 100644 spec/ruby/library/tempfile/shared/length.rb delete mode 100644 spec/ruby/library/tempfile/shared/unlink.rb delete mode 100644 spec/ruby/library/time/shared/rfc2822.rb delete mode 100644 spec/ruby/library/time/shared/xmlschema.rb delete mode 100644 spec/ruby/library/uri/shared/extract.rb delete mode 100644 spec/ruby/library/uri/shared/join.rb delete mode 100644 spec/ruby/library/uri/shared/parse.rb delete mode 100644 spec/ruby/library/yaml/shared/each_document.rb delete mode 100644 spec/ruby/library/zlib/gzipreader/shared/each.rb create mode 100644 spec/ruby/library/zlib/gzipreader/tell_spec.rb diff --git a/spec/ruby/core/array/collect_spec.rb b/spec/ruby/core/array/collect_spec.rb index 43a539f805668a..bdee5c240a7e64 100644 --- a/spec/ruby/core/array/collect_spec.rb +++ b/spec/ruby/core/array/collect_spec.rb @@ -1,143 +1,13 @@ require_relative '../../spec_helper' -require_relative 'fixtures/classes' -require_relative '../enumerable/shared/enumeratorized' -require_relative 'shared/iterable_and_tolerating_size_increasing' describe "Array#collect" do - it "returns a copy of array with each element replaced by the value returned by block" do - a = ['a', 'b', 'c', 'd'] - b = a.collect { |i| i + '!' } - b.should == ["a!", "b!", "c!", "d!"] - b.should_not.equal? a + it "is an alias of Array#map" do + Array.instance_method(:collect).should == Array.instance_method(:map) end - - it "does not return subclass instance" do - ArraySpecs::MyArray[1, 2, 3].collect { |x| x + 1 }.should.instance_of?(Array) - end - - it "does not change self" do - a = ['a', 'b', 'c', 'd'] - a.collect { |i| i + '!' } - a.should == ['a', 'b', 'c', 'd'] - end - - it "returns the evaluated value of block if it broke in the block" do - a = ['a', 'b', 'c', 'd'] - b = a.collect {|i| - if i == 'c' - break 0 - else - i + '!' - end - } - b.should == 0 - end - - it "returns an Enumerator when no block given" do - a = [1, 2, 3] - a.collect.should.instance_of?(Enumerator) - end - - it "raises an ArgumentError when no block and with arguments" do - a = [1, 2, 3] - -> { - a.collect(:foo) - }.should.raise(ArgumentError) - end - - before :each do - @object = [1, 2, 3, 4] - end - it_behaves_like :enumeratorized_with_origin_size, :collect - - it_behaves_like :array_iterable_and_tolerating_size_increasing, :collect end describe "Array#collect!" do - it "replaces each element with the value returned by block" do - a = [7, 9, 3, 5] - a.collect! { |i| i - 1 }.should.equal?(a) - a.should == [6, 8, 2, 4] - end - - it "returns self" do - a = [1, 2, 3, 4, 5] - b = a.collect! {|i| i+1 } - a.should.equal? b - end - - it "returns the evaluated value of block but its contents is partially modified, if it broke in the block" do - a = ['a', 'b', 'c', 'd'] - b = a.collect! {|i| - if i == 'c' - break 0 - else - i + '!' - end - } - b.should == 0 - a.should == ['a!', 'b!', 'c', 'd'] + it "is an alias of Array#map!" do + Array.instance_method(:collect!).should == Array.instance_method(:map!) end - - it "returns an Enumerator when no block given, and the enumerator can modify the original array" do - a = [1, 2, 3] - enum = a.collect! - enum.should.instance_of?(Enumerator) - enum.each{|i| "#{i}!" } - a.should == ["1!", "2!", "3!"] - end - - describe "when frozen" do - it "raises a FrozenError" do - -> { ArraySpecs.frozen_array.collect! {} }.should.raise(FrozenError) - end - - it "raises a FrozenError when empty" do - -> { ArraySpecs.empty_frozen_array.collect! {} }.should.raise(FrozenError) - end - - it "raises a FrozenError when calling #each on the returned Enumerator" do - enumerator = ArraySpecs.frozen_array.collect! - -> { enumerator.each {|x| x } }.should.raise(FrozenError) - end - - it "raises a FrozenError when calling #each on the returned Enumerator when empty" do - enumerator = ArraySpecs.empty_frozen_array.collect! - -> { enumerator.each {|x| x } }.should.raise(FrozenError) - end - end - - it "does not truncate the array is the block raises an exception" do - a = [1, 2, 3] - begin - a.collect! { raise StandardError, 'Oops' } - rescue - end - - a.should == [1, 2, 3] - end - - it "only changes elements before error is raised, keeping the element which raised an error." do - a = [1, 2, 3, 4] - begin - a.collect! do |e| - case e - when 1 then -1 - when 2 then -2 - when 3 then raise StandardError, 'Oops' - else 0 - end - end - rescue StandardError - end - - a.should == [-1, -2, 3, 4] - end - - before :each do - @object = [1, 2, 3, 4] - end - it_behaves_like :enumeratorized_with_origin_size, :collect! - - it_behaves_like :array_iterable_and_tolerating_size_increasing, :collect! end diff --git a/spec/ruby/core/array/length_spec.rb b/spec/ruby/core/array/length_spec.rb index 74b2eb3a08cc9b..e45abb2138009b 100644 --- a/spec/ruby/core/array/length_spec.rb +++ b/spec/ruby/core/array/length_spec.rb @@ -1,14 +1,7 @@ require_relative '../../spec_helper' -require_relative 'fixtures/classes' describe "Array#length" do - it "returns the number of elements" do - [].length.should == 0 - [1, 2, 3].length.should == 3 - end - - it "properly handles recursive arrays" do - ArraySpecs.empty_recursive_array.length.should == 1 - ArraySpecs.recursive_array.length.should == 8 + it "is an alias of Array#size" do + Array.instance_method(:length).should == Array.instance_method(:size) end end diff --git a/spec/ruby/core/array/map_spec.rb b/spec/ruby/core/array/map_spec.rb index f5e88c86245767..0cc394663a42b7 100644 --- a/spec/ruby/core/array/map_spec.rb +++ b/spec/ruby/core/array/map_spec.rb @@ -1,13 +1,143 @@ require_relative '../../spec_helper' +require_relative 'fixtures/classes' +require_relative '../enumerable/shared/enumeratorized' +require_relative 'shared/iterable_and_tolerating_size_increasing' describe "Array#map" do - it "is an alias of Array#collect" do - Array.instance_method(:map).should == Array.instance_method(:collect) + it "returns a copy of array with each element replaced by the value returned by block" do + a = ['a', 'b', 'c', 'd'] + b = a.map { |i| i + '!' } + b.should == ["a!", "b!", "c!", "d!"] + b.should_not.equal? a end + + it "does not return subclass instance" do + ArraySpecs::MyArray[1, 2, 3].map { |x| x + 1 }.should.instance_of?(Array) + end + + it "does not change self" do + a = ['a', 'b', 'c', 'd'] + a.map { |i| i + '!' } + a.should == ['a', 'b', 'c', 'd'] + end + + it "returns the evaluated value of block if it broke in the block" do + a = ['a', 'b', 'c', 'd'] + b = a.map {|i| + if i == 'c' + break 0 + else + i + '!' + end + } + b.should == 0 + end + + it "returns an Enumerator when no block given" do + a = [1, 2, 3] + a.map.should.instance_of?(Enumerator) + end + + it "raises an ArgumentError when no block and with arguments" do + a = [1, 2, 3] + -> { + a.map(:foo) + }.should.raise(ArgumentError) + end + + before :each do + @object = [1, 2, 3, 4] + end + it_behaves_like :enumeratorized_with_origin_size, :map + + it_behaves_like :array_iterable_and_tolerating_size_increasing, :map end describe "Array#map!" do - it "is an alias of Array#collect!" do - Array.instance_method(:map!).should == Array.instance_method(:collect!) + it "replaces each element with the value returned by block" do + a = [7, 9, 3, 5] + a.map! { |i| i - 1 }.should.equal?(a) + a.should == [6, 8, 2, 4] + end + + it "returns self" do + a = [1, 2, 3, 4, 5] + b = a.map! {|i| i+1 } + a.should.equal? b + end + + it "returns the evaluated value of block but its contents is partially modified, if it broke in the block" do + a = ['a', 'b', 'c', 'd'] + b = a.map! {|i| + if i == 'c' + break 0 + else + i + '!' + end + } + b.should == 0 + a.should == ['a!', 'b!', 'c', 'd'] end + + it "returns an Enumerator when no block given, and the enumerator can modify the original array" do + a = [1, 2, 3] + enum = a.map! + enum.should.instance_of?(Enumerator) + enum.each{|i| "#{i}!" } + a.should == ["1!", "2!", "3!"] + end + + describe "when frozen" do + it "raises a FrozenError" do + -> { ArraySpecs.frozen_array.map! {} }.should.raise(FrozenError) + end + + it "raises a FrozenError when empty" do + -> { ArraySpecs.empty_frozen_array.map! {} }.should.raise(FrozenError) + end + + it "raises a FrozenError when calling #each on the returned Enumerator" do + enumerator = ArraySpecs.frozen_array.map! + -> { enumerator.each {|x| x } }.should.raise(FrozenError) + end + + it "raises a FrozenError when calling #each on the returned Enumerator when empty" do + enumerator = ArraySpecs.empty_frozen_array.map! + -> { enumerator.each {|x| x } }.should.raise(FrozenError) + end + end + + it "does not truncate the array is the block raises an exception" do + a = [1, 2, 3] + begin + a.map! { raise StandardError, 'Oops' } + rescue + end + + a.should == [1, 2, 3] + end + + it "only changes elements before error is raised, keeping the element which raised an error." do + a = [1, 2, 3, 4] + begin + a.map! do |e| + case e + when 1 then -1 + when 2 then -2 + when 3 then raise StandardError, 'Oops' + else 0 + end + end + rescue StandardError + end + + a.should == [-1, -2, 3, 4] + end + + before :each do + @object = [1, 2, 3, 4] + end + it_behaves_like :enumeratorized_with_origin_size, :map! + + it_behaves_like :array_iterable_and_tolerating_size_increasing, :map! end diff --git a/spec/ruby/core/array/size_spec.rb b/spec/ruby/core/array/size_spec.rb index 83e89690124143..710754ed62b9e8 100644 --- a/spec/ruby/core/array/size_spec.rb +++ b/spec/ruby/core/array/size_spec.rb @@ -1,7 +1,14 @@ require_relative '../../spec_helper' +require_relative 'fixtures/classes' describe "Array#size" do - it "is an alias of Array#length" do - Array.instance_method(:size).should == Array.instance_method(:length) + it "returns the number of elements" do + [].size.should == 0 + [1, 2, 3].size.should == 3 + end + + it "properly handles recursive arrays" do + ArraySpecs.empty_recursive_array.size.should == 1 + ArraySpecs.recursive_array.size.should == 8 end end diff --git a/spec/ruby/core/basicobject/equal_spec.rb b/spec/ruby/core/basicobject/equal_spec.rb index c0f41dc0c0acb4..f27f0d7aca191d 100644 --- a/spec/ruby/core/basicobject/equal_spec.rb +++ b/spec/ruby/core/basicobject/equal_spec.rb @@ -1,54 +1,7 @@ require_relative '../../spec_helper' -require_relative '../../shared/kernel/equal' describe "BasicObject#equal?" do - it "is a public instance method" do - BasicObject.public_instance_methods(false).should.include?(:equal?) - end - - it_behaves_like :object_equal, :equal? - - it "is unaffected by overriding __id__" do - o1 = mock("object") - o2 = mock("object") - suppress_warning { - def o1.__id__; 10; end - def o2.__id__; 10; end - } - o1.equal?(o2).should == false - end - - it "is unaffected by overriding object_id" do - o1 = mock("object") - o1.stub!(:object_id).and_return(10) - o2 = mock("object") - o2.stub!(:object_id).and_return(10) - o1.equal?(o2).should == false - end - - it "is unaffected by overriding ==" do - # different objects, overriding == to return true - o1 = mock("object") - o1.stub!(:==).and_return(true) - o2 = mock("object") - o1.equal?(o2).should == false - - # same objects, overriding == to return false - o3 = mock("object") - o3.stub!(:==).and_return(false) - o3.equal?(o3).should == true - end - - it "is unaffected by overriding eql?" do - # different objects, overriding eql? to return true - o1 = mock("object") - o1.stub!(:eql?).and_return(true) - o2 = mock("object") - o1.equal?(o2).should == false - - # same objects, overriding eql? to return false - o3 = mock("object") - o3.stub!(:eql?).and_return(false) - o3.equal?(o3).should == true + it "is an alias of BasicObject#==" do + BasicObject.instance_method(:equal?).should == BasicObject.instance_method(:==) end end diff --git a/spec/ruby/core/basicobject/equal_value_spec.rb b/spec/ruby/core/basicobject/equal_value_spec.rb index eb951a8305f6b1..15755a44a279a7 100644 --- a/spec/ruby/core/basicobject/equal_value_spec.rb +++ b/spec/ruby/core/basicobject/equal_value_spec.rb @@ -7,4 +7,48 @@ end it_behaves_like :object_equal, :== + + it "is unaffected by overriding __id__" do + o1 = mock("object") + o2 = mock("object") + suppress_warning { + def o1.__id__; 10; end + def o2.__id__; 10; end + } + (o1 == o2).should == false + end + + it "is unaffected by overriding object_id" do + o1 = mock("object") + o1.stub!(:object_id).and_return(10) + o2 = mock("object") + o2.stub!(:object_id).and_return(10) + (o1 == o2).should == false + end + + it "is unaffected by overriding equal?" do + # different objects, overriding equal? to return true + o1 = mock("object") + o1.stub!(:equal?).and_return(true) + o2 = mock("object") + (o1 == o2).should == false + + # same objects, overriding equal? to return false + o3 = mock("object") + o3.stub!(:equal?).and_return(false) + (o3 == o3).should == true + end + + it "is unaffected by overriding eql?" do + # different objects, overriding eql? to return true + o1 = mock("object") + o1.stub!(:eql?).and_return(true) + o2 = mock("object") + (o1 == o2).should == false + + # same objects, overriding eql? to return false + o3 = mock("object") + o3.stub!(:eql?).and_return(false) + (o3 == o3).should == true + end end diff --git a/spec/ruby/core/complex/rect_spec.rb b/spec/ruby/core/complex/rect_spec.rb index 72f2bf9930bf67..bf3467c6115ca4 100644 --- a/spec/ruby/core/complex/rect_spec.rb +++ b/spec/ruby/core/complex/rect_spec.rb @@ -7,7 +7,7 @@ end describe "Complex.rect" do - it "is an alias of Complex#rectangular" do + it "is an alias of Complex.rectangular" do Complex.method(:rect).should == Complex.method(:rectangular) end end diff --git a/spec/ruby/core/data/inspect_spec.rb b/spec/ruby/core/data/inspect_spec.rb index 6c97a719e3c51d..e5b9689ca522f2 100644 --- a/spec/ruby/core/data/inspect_spec.rb +++ b/spec/ruby/core/data/inspect_spec.rb @@ -2,62 +2,7 @@ require_relative 'fixtures/classes' describe "Data#inspect" do - it "returns a string representation showing members and values" do - a = DataSpecs::Measure.new(42, "km") - a.inspect.should == '#' - end - - it "returns a string representation without the class name for anonymous structs" do - Data.define(:a).new("").inspect.should == '#' - end - - it "returns a string representation without the class name for structs nested in anonymous classes" do - c = Class.new - c.class_eval <<~DOC - Foo = Data.define(:a) - DOC - - c::Foo.new("").inspect.should == '#' - end - - it "returns a string representation without the class name for structs nested in anonymous modules" do - m = Module.new - m.class_eval <<~DOC - Foo = Data.define(:a) - DOC - - m::Foo.new("").inspect.should == '#' - end - - it "does not call #name method" do - struct = DataSpecs::MeasureWithOverriddenName.new(42, "km") - struct.inspect.should == '#' - end - - it "does not call #name method when struct is anonymous" do - klass = Class.new(DataSpecs::Measure) do - def self.name - "A" - end - end - struct = klass.new(42, "km") - struct.inspect.should == '#' - end - - context "recursive structure" do - it "returns string representation with recursive attribute replaced with ..." do - a = DataSpecs::Measure.allocate - a.send(:initialize, amount: 42, unit: a) - - a.inspect.should == "#>" - end - - it "returns string representation with recursive attribute replaced with ... when an anonymous class" do - klass = Class.new(DataSpecs::Measure) - a = klass.allocate - a.send(:initialize, amount: 42, unit: a) - - a.inspect.should =~ /#:\.\.\.>>/ - end + it "is an alias of Data#to_s" do + DataSpecs::Measure.instance_method(:inspect).should == DataSpecs::Measure.instance_method(:to_s) end end diff --git a/spec/ruby/core/data/to_s_spec.rb b/spec/ruby/core/data/to_s_spec.rb index a552e4659b8833..e436c211091f32 100644 --- a/spec/ruby/core/data/to_s_spec.rb +++ b/spec/ruby/core/data/to_s_spec.rb @@ -2,8 +2,62 @@ require_relative 'fixtures/classes' describe "Data#to_s" do - it "is an alias of Data#inspect" do + it "returns a string representation showing members and values" do a = DataSpecs::Measure.new(42, "km") - a.method(:to_s).should == a.method(:inspect) + a.to_s.should == '#' + end + + it "returns a string representation without the class name for anonymous structs" do + Data.define(:a).new("").to_s.should == '#' + end + + it "returns a string representation without the class name for structs nested in anonymous classes" do + c = Class.new + c.class_eval <<~DOC + Foo = Data.define(:a) + DOC + + c::Foo.new("").to_s.should == '#' + end + + it "returns a string representation without the class name for structs nested in anonymous modules" do + m = Module.new + m.class_eval <<~DOC + Foo = Data.define(:a) + DOC + + m::Foo.new("").to_s.should == '#' + end + + it "does not call #name method" do + struct = DataSpecs::MeasureWithOverriddenName.new(42, "km") + struct.to_s.should == '#' + end + + it "does not call #name method when struct is anonymous" do + klass = Class.new(DataSpecs::Measure) do + def self.name + "A" + end + end + struct = klass.new(42, "km") + struct.to_s.should == '#' + end + + context "recursive structure" do + it "returns string representation with recursive attribute replaced with ..." do + a = DataSpecs::Measure.allocate + a.send(:initialize, amount: 42, unit: a) + + a.to_s.should == "#>" + end + + it "returns string representation with recursive attribute replaced with ... when an anonymous class" do + klass = Class.new(DataSpecs::Measure) + a = klass.allocate + a.send(:initialize, amount: 42, unit: a) + + a.to_s.should =~ /#:\.\.\.>>/ + end end end diff --git a/spec/ruby/core/dir/delete_spec.rb b/spec/ruby/core/dir/delete_spec.rb index a0020788caec25..2dbd461c945d41 100644 --- a/spec/ruby/core/dir/delete_spec.rb +++ b/spec/ruby/core/dir/delete_spec.rb @@ -1,6 +1,5 @@ require_relative '../../spec_helper' require_relative 'fixtures/common' -require_relative 'shared/delete' describe "Dir.delete" do before :all do @@ -11,5 +10,55 @@ DirSpecs.delete_mock_dirs end - it_behaves_like :dir_delete, :delete + before :each do + DirSpecs.rmdir_dirs true + end + + after :each do + DirSpecs.rmdir_dirs false + end + + it "removes empty directories" do + Dir.delete(DirSpecs.mock_rmdir("empty")).should == 0 + end + + it "calls #to_path on non-String arguments" do + p = mock('path') + p.should_receive(:to_path).and_return(DirSpecs.mock_rmdir("empty")) + Dir.delete(p) + end + + it "raises an Errno::ENOTEMPTY when trying to remove a nonempty directory" do + -> do + Dir.delete DirSpecs.mock_rmdir("nonempty") + end.should.raise(Errno::ENOTEMPTY) + end + + it "raises an Errno::ENOENT when trying to remove a non-existing directory" do + -> do + Dir.delete DirSpecs.nonexistent + end.should.raise(Errno::ENOENT) + end + + it "raises an Errno::ENOTDIR when trying to remove a non-directory" do + file = DirSpecs.mock_rmdir("nonempty/regular") + touch(file) + -> do + Dir.delete file + end.should.raise(Errno::ENOTDIR) + end + + # this won't work on Windows, since chmod(0000) does not remove all permissions + platform_is_not :windows do + as_user do + it "raises an Errno::EACCES if lacking adequate permissions to remove the directory" do + parent = DirSpecs.mock_rmdir("noperm") + child = DirSpecs.mock_rmdir("noperm", "child") + File.chmod(0000, parent) + -> do + Dir.delete child + end.should.raise(Errno::EACCES) + end + end + end end diff --git a/spec/ruby/core/dir/path_spec.rb b/spec/ruby/core/dir/path_spec.rb index 02ddd2cd4257cd..e506db1222c557 100644 --- a/spec/ruby/core/dir/path_spec.rb +++ b/spec/ruby/core/dir/path_spec.rb @@ -1,37 +1,7 @@ require_relative '../../spec_helper' -require_relative 'fixtures/common' describe "Dir#path" do - before :all do - DirSpecs.create_mock_dirs - end - - after :all do - DirSpecs.delete_mock_dirs - end - - it "returns the path that was supplied to .new or .open" do - dir = Dir.open DirSpecs.mock_dir - begin - dir.path.should == DirSpecs.mock_dir - ensure - dir.close rescue nil - end - end - - it "returns the path even when called on a closed Dir instance" do - dir = Dir.open DirSpecs.mock_dir - dir.close - dir.path.should == DirSpecs.mock_dir - end - - it "returns a String with the same encoding as the argument to .open" do - path = DirSpecs.mock_dir.force_encoding Encoding::IBM866 - dir = Dir.open path - begin - dir.path.encoding.should.equal?(Encoding::IBM866) - ensure - dir.close - end + it "is an alias of Dir#to_path" do + Dir.instance_method(:path).should == Dir.instance_method(:to_path) end end diff --git a/spec/ruby/core/dir/pos_spec.rb b/spec/ruby/core/dir/pos_spec.rb index f8ca06d7780124..1e364fbe3c8373 100644 --- a/spec/ruby/core/dir/pos_spec.rb +++ b/spec/ruby/core/dir/pos_spec.rb @@ -1,10 +1,42 @@ require_relative '../../spec_helper' require_relative 'fixtures/common' +require_relative 'shared/closed' require_relative 'shared/pos' describe "Dir#pos" do - it "is an alias of Dir#tell" do - Dir.instance_method(:pos).should == Dir.instance_method(:tell) + before :all do + DirSpecs.create_mock_dirs + end + + after :all do + DirSpecs.delete_mock_dirs + end + + it_behaves_like :dir_closed, :pos + + before :each do + @dir = Dir.open DirSpecs.mock_dir + end + + after :each do + @dir.close rescue nil + end + + it "returns an Integer representing the current position in the directory" do + @dir.pos.should.is_a?(Integer) + @dir.pos.should.is_a?(Integer) + @dir.pos.should.is_a?(Integer) + end + + it "returns a different Integer if moved from previous position" do + a = @dir.pos + @dir.read + b = @dir.pos + + a.should.is_a?(Integer) + b.should.is_a?(Integer) + + a.should_not == b end end diff --git a/spec/ruby/core/dir/rmdir_spec.rb b/spec/ruby/core/dir/rmdir_spec.rb index 08cd1a5bc67f84..c31067ca296388 100644 --- a/spec/ruby/core/dir/rmdir_spec.rb +++ b/spec/ruby/core/dir/rmdir_spec.rb @@ -1,15 +1,7 @@ require_relative '../../spec_helper' -require_relative 'fixtures/common' -require_relative 'shared/delete' describe "Dir.rmdir" do - before :all do - DirSpecs.create_mock_dirs + it "is an alias of Dir.delete" do + Dir.method(:rmdir).should == Dir.method(:delete) end - - after :all do - DirSpecs.delete_mock_dirs - end - - it_behaves_like :dir_delete, :rmdir end diff --git a/spec/ruby/core/dir/shared/delete.rb b/spec/ruby/core/dir/shared/delete.rb deleted file mode 100644 index ba013e8615af16..00000000000000 --- a/spec/ruby/core/dir/shared/delete.rb +++ /dev/null @@ -1,53 +0,0 @@ -describe :dir_delete, shared: true do - before :each do - DirSpecs.rmdir_dirs true - end - - after :each do - DirSpecs.rmdir_dirs false - end - - it "removes empty directories" do - Dir.send(@method, DirSpecs.mock_rmdir("empty")).should == 0 - end - - it "calls #to_path on non-String arguments" do - p = mock('path') - p.should_receive(:to_path).and_return(DirSpecs.mock_rmdir("empty")) - Dir.send(@method, p) - end - - it "raises an Errno::ENOTEMPTY when trying to remove a nonempty directory" do - -> do - Dir.send @method, DirSpecs.mock_rmdir("nonempty") - end.should.raise(Errno::ENOTEMPTY) - end - - it "raises an Errno::ENOENT when trying to remove a non-existing directory" do - -> do - Dir.send @method, DirSpecs.nonexistent - end.should.raise(Errno::ENOENT) - end - - it "raises an Errno::ENOTDIR when trying to remove a non-directory" do - file = DirSpecs.mock_rmdir("nonempty/regular") - touch(file) - -> do - Dir.send @method, file - end.should.raise(Errno::ENOTDIR) - end - - # this won't work on Windows, since chmod(0000) does not remove all permissions - platform_is_not :windows do - as_user do - it "raises an Errno::EACCES if lacking adequate permissions to remove the directory" do - parent = DirSpecs.mock_rmdir("noperm") - child = DirSpecs.mock_rmdir("noperm", "child") - File.chmod(0000, parent) - -> do - Dir.send @method, child - end.should.raise(Errno::EACCES) - end - end - end -end diff --git a/spec/ruby/core/dir/tell_spec.rb b/spec/ruby/core/dir/tell_spec.rb index dcbb40438f2b40..04f92a8adeeb80 100644 --- a/spec/ruby/core/dir/tell_spec.rb +++ b/spec/ruby/core/dir/tell_spec.rb @@ -1,41 +1,9 @@ require_relative '../../spec_helper' require_relative 'fixtures/common' -require_relative 'shared/closed' require_relative 'shared/pos' describe "Dir#tell" do - before :all do - DirSpecs.create_mock_dirs - end - - after :all do - DirSpecs.delete_mock_dirs - end - - it_behaves_like :dir_closed, :tell - - before :each do - @dir = Dir.open DirSpecs.mock_dir - end - - after :each do - @dir.close rescue nil - end - - it "returns an Integer representing the current position in the directory" do - @dir.tell.should.is_a?(Integer) - @dir.tell.should.is_a?(Integer) - @dir.tell.should.is_a?(Integer) - end - - it "returns a different Integer if moved from previous position" do - a = @dir.tell - @dir.read - b = @dir.tell - - a.should.is_a?(Integer) - b.should.is_a?(Integer) - - a.should_not == b + it "is an alias of Dir#pos" do + Dir.instance_method(:tell).should == Dir.instance_method(:pos) end end diff --git a/spec/ruby/core/dir/to_path_spec.rb b/spec/ruby/core/dir/to_path_spec.rb index 2ed533e757df68..43e349c50e34e1 100644 --- a/spec/ruby/core/dir/to_path_spec.rb +++ b/spec/ruby/core/dir/to_path_spec.rb @@ -1,7 +1,37 @@ require_relative '../../spec_helper' +require_relative 'fixtures/common' describe "Dir#to_path" do - it "is an alias of Dir#path" do - Dir.instance_method(:to_path).should == Dir.instance_method(:path) + before :all do + DirSpecs.create_mock_dirs + end + + after :all do + DirSpecs.delete_mock_dirs + end + + it "returns the to_path that was supplied to .new or .open" do + dir = Dir.open DirSpecs.mock_dir + begin + dir.to_path.should == DirSpecs.mock_dir + ensure + dir.close rescue nil + end + end + + it "returns the to_path even when called on a closed Dir instance" do + dir = Dir.open DirSpecs.mock_dir + dir.close + dir.to_path.should == DirSpecs.mock_dir + end + + it "returns a String with the same encoding as the argument to .open" do + to_path = DirSpecs.mock_dir.force_encoding Encoding::IBM866 + dir = Dir.open to_path + begin + dir.to_path.encoding.should.equal?(Encoding::IBM866) + ensure + dir.close + end end end diff --git a/spec/ruby/core/dir/unlink_spec.rb b/spec/ruby/core/dir/unlink_spec.rb index 79027e020ce27b..d9cd1b1a8751e6 100644 --- a/spec/ruby/core/dir/unlink_spec.rb +++ b/spec/ruby/core/dir/unlink_spec.rb @@ -1,15 +1,7 @@ require_relative '../../spec_helper' -require_relative 'fixtures/common' -require_relative 'shared/delete' describe "Dir.unlink" do - before :all do - DirSpecs.create_mock_dirs + it "is an alias of Dir.delete" do + Dir.method(:unlink).should == Dir.method(:delete) end - - after :all do - DirSpecs.delete_mock_dirs - end - - it_behaves_like :dir_delete, :unlink end diff --git a/spec/ruby/core/encoding/converter/asciicompat_encoding_spec.rb b/spec/ruby/core/encoding/converter/asciicompat_encoding_spec.rb index 07c7a883562a81..7fa867a57e9334 100644 --- a/spec/ruby/core/encoding/converter/asciicompat_encoding_spec.rb +++ b/spec/ruby/core/encoding/converter/asciicompat_encoding_spec.rb @@ -1,11 +1,6 @@ require_relative '../../../spec_helper' describe "Encoding::Converter.asciicompat_encoding" do - it "accepts an encoding name as a String argument" do - -> { Encoding::Converter.asciicompat_encoding('UTF-8') }. - should_not raise_error - end - it "coerces non-String/Encoding objects with #to_str" do str = mock('string') str.should_receive(:to_str).at_least(1).times.and_return('string') @@ -13,9 +8,8 @@ end it "accepts an Encoding object as an argument" do - Encoding::Converter. - asciicompat_encoding(Encoding.find("ISO-2022-JP")). - should == Encoding::Converter.asciicompat_encoding("ISO-2022-JP") + Encoding::Converter.asciicompat_encoding(Encoding.find("ISO-2022-JP")).should == + Encoding::Converter.asciicompat_encoding("ISO-2022-JP") end it "returns a corresponding ASCII compatible encoding for ASCII-incompatible encodings" do diff --git a/spec/ruby/core/enumerable/collect_concat_spec.rb b/spec/ruby/core/enumerable/collect_concat_spec.rb index 59317cfe341185..5024aaddab2d01 100644 --- a/spec/ruby/core/enumerable/collect_concat_spec.rb +++ b/spec/ruby/core/enumerable/collect_concat_spec.rb @@ -1,7 +1,7 @@ require_relative '../../spec_helper' -require_relative 'fixtures/classes' -require_relative 'shared/collect_concat' describe "Enumerable#collect_concat" do - it_behaves_like :enumerable_collect_concat, :collect_concat + it "is an alias of Enumerable#flat_map" do + Enumerable.instance_method(:collect_concat).should == Enumerable.instance_method(:flat_map) + end end diff --git a/spec/ruby/core/enumerable/collect_spec.rb b/spec/ruby/core/enumerable/collect_spec.rb index cfa2895cce21ce..319b1b263dabb0 100644 --- a/spec/ruby/core/enumerable/collect_spec.rb +++ b/spec/ruby/core/enumerable/collect_spec.rb @@ -1,7 +1,7 @@ require_relative '../../spec_helper' -require_relative 'fixtures/classes' -require_relative 'shared/collect' describe "Enumerable#collect" do - it_behaves_like :enumerable_collect, :collect + it "is an alias of Enumerable#map" do + Enumerable.instance_method(:collect).should == Enumerable.instance_method(:map) + end end diff --git a/spec/ruby/core/enumerable/detect_spec.rb b/spec/ruby/core/enumerable/detect_spec.rb index 6959aadc44b7db..0669c50c586186 100644 --- a/spec/ruby/core/enumerable/detect_spec.rb +++ b/spec/ruby/core/enumerable/detect_spec.rb @@ -1,7 +1,7 @@ require_relative '../../spec_helper' -require_relative 'fixtures/classes' -require_relative 'shared/find' describe "Enumerable#detect" do - it_behaves_like :enumerable_find, :detect + it "is an alias of Enumerable#find" do + Enumerable.instance_method(:detect).should == Enumerable.instance_method(:find) + end end diff --git a/spec/ruby/core/enumerable/entries_spec.rb b/spec/ruby/core/enumerable/entries_spec.rb index 2de4fc756a23a8..8cb29b7b470be4 100644 --- a/spec/ruby/core/enumerable/entries_spec.rb +++ b/spec/ruby/core/enumerable/entries_spec.rb @@ -1,7 +1,7 @@ require_relative '../../spec_helper' -require_relative 'fixtures/classes' -require_relative 'shared/entries' describe "Enumerable#entries" do - it_behaves_like :enumerable_entries, :entries + it "is an alias of Enumerable#to_a" do + Enumerable.instance_method(:entries).should == Enumerable.instance_method(:to_a) + end end diff --git a/spec/ruby/core/enumerable/filter_spec.rb b/spec/ruby/core/enumerable/filter_spec.rb index 1c3a7e9ff59c0c..d075b393968bdb 100644 --- a/spec/ruby/core/enumerable/filter_spec.rb +++ b/spec/ruby/core/enumerable/filter_spec.rb @@ -1,7 +1,7 @@ require_relative '../../spec_helper' -require_relative 'fixtures/classes' -require_relative 'shared/find_all' describe "Enumerable#filter" do - it_behaves_like :enumerable_find_all, :filter + it "is an alias of Enumerable#select" do + Enumerable.instance_method(:filter).should == Enumerable.instance_method(:select) + end end diff --git a/spec/ruby/core/enumerable/find_all_spec.rb b/spec/ruby/core/enumerable/find_all_spec.rb index 9cd635f2470bb2..1095a195699f05 100644 --- a/spec/ruby/core/enumerable/find_all_spec.rb +++ b/spec/ruby/core/enumerable/find_all_spec.rb @@ -1,7 +1,7 @@ require_relative '../../spec_helper' -require_relative 'fixtures/classes' -require_relative 'shared/find_all' describe "Enumerable#find_all" do - it_behaves_like :enumerable_find_all, :find_all + it "is an alias of Enumerable#select" do + Enumerable.instance_method(:find_all).should == Enumerable.instance_method(:select) + end end diff --git a/spec/ruby/core/enumerable/find_spec.rb b/spec/ruby/core/enumerable/find_spec.rb index 5ddebc05f837ba..4ac4b75c4735de 100644 --- a/spec/ruby/core/enumerable/find_spec.rb +++ b/spec/ruby/core/enumerable/find_spec.rb @@ -1,7 +1,78 @@ require_relative '../../spec_helper' require_relative 'fixtures/classes' -require_relative 'shared/find' +require_relative 'shared/enumerable_enumeratorized' describe "Enumerable#find" do - it_behaves_like :enumerable_find, :find + before :each do + ScratchPad.record [] + @elements = [2, 4, 6, 8, 10] + @numerous = EnumerableSpecs::Numerous.new(*@elements) + @empty = [] + end + + it "passes each entry in enum to block while block when block is false" do + visited_elements = [] + @numerous.find do |element| + visited_elements << element + false + end + visited_elements.should == @elements + end + + it "returns nil when the block is false and there is no ifnone proc given" do + @numerous.find {|e| false }.should == nil + end + + it "returns the first element for which the block is not false" do + @elements.each do |element| + @numerous.find {|e| e > element - 1 }.should == element + end + end + + it "returns the value of the ifnone proc if the block is false" do + fail_proc = -> { "cheeseburgers" } + @numerous.find(fail_proc) {|e| false }.should == "cheeseburgers" + end + + it "doesn't call the ifnone proc if an element is found" do + fail_proc = -> { raise "This shouldn't have been called" } + @numerous.find(fail_proc) {|e| e == @elements.first }.should == 2 + end + + it "calls the ifnone proc only once when the block is false" do + times = 0 + fail_proc = -> { times += 1; raise if times > 1; "cheeseburgers" } + @numerous.find(fail_proc) {|e| false }.should == "cheeseburgers" + end + + it "calls the ifnone proc when there are no elements" do + fail_proc = -> { "yay" } + @empty.find(fail_proc) {|e| true}.should == "yay" + end + + it "ignores the ifnone argument when nil" do + @numerous.find(nil) {|e| false }.should == nil + end + + it "passes through the values yielded by #each_with_index" do + [:a, :b].each_with_index.find { |x, i| ScratchPad << [x, i]; nil } + ScratchPad.recorded.should == [[:a, 0], [:b, 1]] + end + + it "returns an enumerator when no block given" do + @numerous.find.should.instance_of?(Enumerator) + end + + it "passes the ifnone proc to the enumerator" do + times = 0 + fail_proc = -> { times += 1; raise if times > 1; "cheeseburgers" } + @numerous.find(fail_proc).each {|e| false }.should == "cheeseburgers" + end + + it "gathers whole arrays as elements when each yields multiple" do + multi = EnumerableSpecs::YieldsMulti.new + multi.find {|e| e == [1, 2] }.should == [1, 2] + end + + it_behaves_like :enumerable_enumeratorized_with_unknown_size, :find end diff --git a/spec/ruby/core/enumerable/flat_map_spec.rb b/spec/ruby/core/enumerable/flat_map_spec.rb index bd07eab6c5c132..ef50cb26963d1a 100644 --- a/spec/ruby/core/enumerable/flat_map_spec.rb +++ b/spec/ruby/core/enumerable/flat_map_spec.rb @@ -1,7 +1,56 @@ require_relative '../../spec_helper' require_relative 'fixtures/classes' -require_relative 'shared/collect_concat' +require_relative 'shared/enumerable_enumeratorized' describe "Enumerable#flat_map" do - it_behaves_like :enumerable_collect_concat, :flat_map + it "yields elements to the block and flattens one level" do + numerous = EnumerableSpecs::Numerous.new(1, [2, 3], [4, [5, 6]], {foo: :bar}) + numerous.flat_map { |i| i }.should == [1, 2, 3, 4, [5, 6], {foo: :bar}] + end + + it "appends non-Array elements that do not define #to_ary" do + obj = mock("to_ary undefined") + + numerous = EnumerableSpecs::Numerous.new(1, obj, 2) + numerous.flat_map { |i| i }.should == [1, obj, 2] + end + + it "concatenates the result of calling #to_ary if it returns an Array" do + obj = mock("to_ary defined") + obj.should_receive(:to_ary).and_return([:a, :b]) + + numerous = EnumerableSpecs::Numerous.new(1, obj, 2) + numerous.flat_map { |i| i }.should == [1, :a, :b, 2] + end + + it "does not call #to_a" do + obj = mock("to_ary undefined") + obj.should_not_receive(:to_a) + + numerous = EnumerableSpecs::Numerous.new(1, obj, 2) + numerous.flat_map { |i| i }.should == [1, obj, 2] + end + + it "appends an element that defines #to_ary that returns nil" do + obj = mock("to_ary defined") + obj.should_receive(:to_ary).and_return(nil) + + numerous = EnumerableSpecs::Numerous.new(1, obj, 2) + numerous.flat_map { |i| i }.should == [1, obj, 2] + end + + it "raises a TypeError if an element defining #to_ary does not return an Array or nil" do + obj = mock("to_ary defined") + obj.should_receive(:to_ary).and_return("array") + + -> { [1, obj, 3].flat_map { |i| i } }.should.raise(TypeError) + end + + it "returns an enumerator when no block given" do + enum = EnumerableSpecs::Numerous.new(1, 2).flat_map + enum.should.instance_of?(Enumerator) + enum.each{ |i| [i] * i }.should == [1, 2, 2] + end + + it_behaves_like :enumerable_enumeratorized_with_origin_size, :flat_map end diff --git a/spec/ruby/core/enumerable/include_spec.rb b/spec/ruby/core/enumerable/include_spec.rb index dab1b04451e56d..d59b35148695c2 100644 --- a/spec/ruby/core/enumerable/include_spec.rb +++ b/spec/ruby/core/enumerable/include_spec.rb @@ -1,7 +1,36 @@ require_relative '../../spec_helper' require_relative 'fixtures/classes' -require_relative 'shared/include' describe "Enumerable#include?" do - it_behaves_like :enumerable_include, :include? + it "returns true if any element == argument for numbers" do + class EnumerableSpecIncludeP; def ==(obj) obj == 5; end; end + + elements = (0..5).to_a + EnumerableSpecs::Numerous.new(*elements).include?(5).should == true + EnumerableSpecs::Numerous.new(*elements).include?(10).should == false + EnumerableSpecs::Numerous.new(*elements).include?(EnumerableSpecIncludeP.new).should == true + end + + it "returns true if any element == argument for other objects" do + class EnumerableSpecIncludeP11; def ==(obj); obj == '11'; end; end + + elements = ('0'..'5').to_a + [EnumerableSpecIncludeP11.new] + EnumerableSpecs::Numerous.new(*elements).include?('5').should == true + EnumerableSpecs::Numerous.new(*elements).include?('10').should == false + EnumerableSpecs::Numerous.new(*elements).include?(EnumerableSpecIncludeP11.new).should == true + EnumerableSpecs::Numerous.new(*elements).include?('11').should == true + end + + + it "returns true if any member of enum equals obj when == compare different classes (legacy rubycon)" do + # equality is tested with == + EnumerableSpecs::Numerous.new(2,4,6,8,10).include?(2.0).should == true + EnumerableSpecs::Numerous.new(2,4,[6,8],10).include?([6, 8]).should == true + EnumerableSpecs::Numerous.new(2,4,[6,8],10).include?([6.0, 8.0]).should == true + end + + it "gathers whole arrays as elements when each yields multiple" do + multi = EnumerableSpecs::YieldsMulti.new + multi.include?([1,2]).should == true + end end diff --git a/spec/ruby/core/enumerable/inject_spec.rb b/spec/ruby/core/enumerable/inject_spec.rb index e1fe216144011a..10de321395f57e 100644 --- a/spec/ruby/core/enumerable/inject_spec.rb +++ b/spec/ruby/core/enumerable/inject_spec.rb @@ -1,7 +1,144 @@ require_relative '../../spec_helper' +require_relative '../array/shared/iterable_and_tolerating_size_increasing' require_relative 'fixtures/classes' -require_relative 'shared/inject' describe "Enumerable#inject" do - it_behaves_like :enumerable_inject, :inject + it "with argument takes a block with an accumulator (with argument as initial value) and the current element. Value of block becomes new accumulator" do + a = [] + EnumerableSpecs::Numerous.new.inject(0) { |memo, i| a << [memo, i]; i } + a.should == [[0, 2], [2, 5], [5, 3], [3, 6], [6, 1], [1, 4]] + EnumerableSpecs::EachDefiner.new(true, true, true).inject(nil) {|result, i| i && result}.should == nil + end + + it "produces an array of the accumulator and the argument when given a block with a *arg" do + a = [] + [1,2].inject(0) {|*args| a << args; args[0] + args[1]} + a.should == [[0, 1], [1, 2]] + end + + it "can take two argument" do + EnumerableSpecs::Numerous.new(1, 2, 3).inject(10, :-).should == 4 + EnumerableSpecs::Numerous.new(1, 2, 3).inject(10, "-").should == 4 + + [1, 2, 3].inject(10, :-).should == 4 + [1, 2, 3].inject(10, "-").should == 4 + end + + it "converts non-Symbol method name argument to String with #to_str if two arguments" do + name = Object.new + def name.to_str; "-"; end + + EnumerableSpecs::Numerous.new(1, 2, 3).inject(10, name).should == 4 + [1, 2, 3].inject(10, name).should == 4 + end + + it "raises TypeError when the second argument is not Symbol or String and it cannot be converted to String if two arguments" do + -> { EnumerableSpecs::Numerous.new(1, 2, 3).inject(10, Object.new) }.should.raise(TypeError, /is not a symbol nor a string/) + -> { [1, 2, 3].inject(10, Object.new) }.should.raise(TypeError, /is not a symbol nor a string/) + end + + it "ignores the block if two arguments" do + -> { + EnumerableSpecs::Numerous.new(1, 2, 3).inject(10, :-) { raise "we never get here"}.should == 4 + }.should complain(/#{__FILE__}:#{__LINE__-1}: warning: given block not used/, verbose: true) + + -> { + [1, 2, 3].inject(10, :-) { raise "we never get here"}.should == 4 + }.should complain(/#{__FILE__}:#{__LINE__-1}: warning: given block not used/, verbose: true) + end + + it "does not warn when given a Symbol with $VERBOSE true" do + -> { + [1, 2].inject(0, :+) + [1, 2].inject(:+) + EnumerableSpecs::Numerous.new(1, 2).inject(0, :+) + EnumerableSpecs::Numerous.new(1, 2).inject(:+) + }.should_not complain(verbose: true) + end + + it "can take a symbol argument" do + EnumerableSpecs::Numerous.new(10, 1, 2, 3).inject(:-).should == 4 + [10, 1, 2, 3].inject(:-).should == 4 + end + + it "can take a String argument" do + EnumerableSpecs::Numerous.new(10, 1, 2, 3).inject("-").should == 4 + [10, 1, 2, 3].inject("-").should == 4 + end + + it "converts non-Symbol method name argument to String with #to_str" do + name = Object.new + def name.to_str; "-"; end + + EnumerableSpecs::Numerous.new(10, 1, 2, 3).inject(name).should == 4 + [10, 1, 2, 3].inject(name).should == 4 + end + + it "raises TypeError when passed not Symbol or String method name argument and it cannot be converted to String" do + -> { EnumerableSpecs::Numerous.new(10, 1, 2, 3).inject(Object.new) }.should.raise(TypeError, /is not a symbol nor a string/) + -> { [10, 1, 2, 3].inject(Object.new) }.should.raise(TypeError, /is not a symbol nor a string/) + end + + it "without argument takes a block with an accumulator (with first element as initial value) and the current element. Value of block becomes new accumulator" do + a = [] + EnumerableSpecs::Numerous.new.inject { |memo, i| a << [memo, i]; i } + a.should == [[2, 5], [5, 3], [3, 6], [6, 1], [1, 4]] + end + + it "gathers whole arrays as elements when each yields multiple" do + multi = EnumerableSpecs::YieldsMulti.new + multi.inject([]) {|acc, e| acc << e }.should == [[1, 2], [3, 4, 5], [6, 7, 8, 9]] + end + + it "with inject arguments(legacy rubycon)" do + # with inject argument + EnumerableSpecs::EachDefiner.new().inject(1) {|acc,x| 999 }.should == 1 + EnumerableSpecs::EachDefiner.new(2).inject(1) {|acc,x| 999 }.should == 999 + EnumerableSpecs::EachDefiner.new(2).inject(1) {|acc,x| acc }.should == 1 + EnumerableSpecs::EachDefiner.new(2).inject(1) {|acc,x| x }.should == 2 + + EnumerableSpecs::EachDefiner.new(1,2,3,4).inject(100) {|acc,x| acc + x }.should == 110 + EnumerableSpecs::EachDefiner.new(1,2,3,4).inject(100) {|acc,x| acc * x }.should == 2400 + + EnumerableSpecs::EachDefiner.new('a','b','c').inject("z") {|result, i| i+result}.should == "cbaz" + end + + it "without inject arguments(legacy rubycon)" do + # no inject argument + EnumerableSpecs::EachDefiner.new(2).inject {|acc,x| 999 }.should == 2 + EnumerableSpecs::EachDefiner.new(2).inject {|acc,x| acc }.should == 2 + EnumerableSpecs::EachDefiner.new(2).inject {|acc,x| x }.should == 2 + + EnumerableSpecs::EachDefiner.new(1,2,3,4).inject {|acc,x| acc + x }.should == 10 + EnumerableSpecs::EachDefiner.new(1,2,3,4).inject {|acc,x| acc * x }.should == 24 + + EnumerableSpecs::EachDefiner.new('a','b','c').inject {|result, i| i+result}.should == "cba" + EnumerableSpecs::EachDefiner.new(3, 4, 5).inject {|result, i| result*i}.should == 60 + EnumerableSpecs::EachDefiner.new([1], 2, 'a','b').inject {|r,i| r< { [1,2].inject }.should.raise(ArgumentError) + -> { {one: 1, two: 2}.inject }.should.raise(ArgumentError) + end end diff --git a/spec/ruby/core/enumerable/map_spec.rb b/spec/ruby/core/enumerable/map_spec.rb index 98a70781cfc7f0..e6447f5c23f05e 100644 --- a/spec/ruby/core/enumerable/map_spec.rb +++ b/spec/ruby/core/enumerable/map_spec.rb @@ -1,7 +1,109 @@ require_relative '../../spec_helper' require_relative 'fixtures/classes' -require_relative 'shared/collect' +require_relative 'shared/enumerable_enumeratorized' describe "Enumerable#map" do - it_behaves_like :enumerable_collect, :map + before :each do + ScratchPad.record [] + end + + it "returns a new array with the results of passing each element to block" do + entries = [0, 1, 3, 4, 5, 6] + numerous = EnumerableSpecs::Numerous.new(*entries) + numerous.map { |i| i % 2 }.should == [0, 1, 1, 0, 1, 0] + numerous.map { |i| i }.should == entries + end + + it "passes through the values yielded by #each_with_index" do + [:a, :b].each_with_index.map { |x, i| ScratchPad << [x, i]; nil } + ScratchPad.recorded.should == [[:a, 0], [:b, 1]] + end + + it "gathers initial args as elements when each yields multiple" do + multi = EnumerableSpecs::YieldsMulti.new + multi.map {|e| e}.should == [1,3,6] + end + + it "only yields increasing values for a Range" do + (1..0).map { |x| x }.should == [] + (1..1).map { |x| x }.should == [1] + (1..2).map { |x| x }.should == [1, 2] + end + + it "returns an enumerator when no block given" do + enum = EnumerableSpecs::Numerous.new.map + enum.should.instance_of?(Enumerator) + enum.each { |i| -i }.should == [-2, -5, -3, -6, -1, -4] + end + + it "reports the same arity as the given block" do + entries = [0, 1, 3, 4, 5, 6] + numerous = EnumerableSpecs::Numerous.new(*entries) + + def numerous.each(&block) + ScratchPad << block.arity + super + end + + numerous.map { |a, b| a % 2 }.should == [0, 1, 1, 0, 1, 0] + ScratchPad.recorded.should == [2] + ScratchPad.clear + ScratchPad.record [] + numerous.map { |i| i }.should == entries + ScratchPad.recorded.should == [1] + end + + it "yields an Array of 2 elements for a Hash when block arity is 1" do + c = Class.new do + def register(a) + ScratchPad << a + end + end + m = c.new.method(:register) + + ScratchPad.record [] + { 1 => 'a', 2 => 'b' }.map(&m) + ScratchPad.recorded.should == [[1, 'a'], [2, 'b']] + end + + it "yields 2 arguments for a Hash when block arity is 2" do + c = Class.new do + def register(a, b) + ScratchPad << [a, b] + end + end + m = c.new.method(:register) + + ScratchPad.record [] + { 1 => 'a', 2 => 'b' }.map(&m) + ScratchPad.recorded.should == [[1, 'a'], [2, 'b']] + end + + it "raises an error for a Hash when an arity enforcing block of arity >2 is passed in" do + c = Class.new do + def register(a, b, c) + end + end + m = c.new.method(:register) + + -> do + { 1 => 'a', 2 => 'b' }.map(&m) + end.should.raise(ArgumentError) + end + + it "calls the each method on sub-classes" do + c = Class.new(Hash) do + def each + ScratchPad << 'in each' + super + end + end + h = c.new + h[1] = 'a' + ScratchPad.record [] + h.map { |k,v| v } + ScratchPad.recorded.should == ['in each'] + end + + it_behaves_like :enumerable_enumeratorized_with_origin_size, :map end diff --git a/spec/ruby/core/enumerable/member_spec.rb b/spec/ruby/core/enumerable/member_spec.rb index 1fe3cebd281d26..be06880ebb6484 100644 --- a/spec/ruby/core/enumerable/member_spec.rb +++ b/spec/ruby/core/enumerable/member_spec.rb @@ -1,7 +1,7 @@ require_relative '../../spec_helper' -require_relative 'fixtures/classes' -require_relative 'shared/include' describe "Enumerable#member?" do - it_behaves_like :enumerable_include, :member? + it "is an alias of Enumerable#include?" do + Enumerable.instance_method(:member?).should == Enumerable.instance_method(:include?) + end end diff --git a/spec/ruby/core/enumerable/reduce_spec.rb b/spec/ruby/core/enumerable/reduce_spec.rb index bc8691c1b0b3bb..40452b66a1510b 100644 --- a/spec/ruby/core/enumerable/reduce_spec.rb +++ b/spec/ruby/core/enumerable/reduce_spec.rb @@ -1,7 +1,7 @@ require_relative '../../spec_helper' -require_relative 'fixtures/classes' -require_relative 'shared/inject' describe "Enumerable#reduce" do - it_behaves_like :enumerable_inject, :reduce + it "is an alias of Enumerable#inject" do + Enumerable.instance_method(:reduce).should == Enumerable.instance_method(:inject) + end end diff --git a/spec/ruby/core/enumerable/select_spec.rb b/spec/ruby/core/enumerable/select_spec.rb index 7fc61926f9008a..a53c228a447420 100644 --- a/spec/ruby/core/enumerable/select_spec.rb +++ b/spec/ruby/core/enumerable/select_spec.rb @@ -1,7 +1,33 @@ require_relative '../../spec_helper' require_relative 'fixtures/classes' -require_relative 'shared/find_all' +require_relative 'shared/enumerable_enumeratorized' describe "Enumerable#select" do - it_behaves_like :enumerable_find_all, :select + before :each do + ScratchPad.record [] + @elements = (1..10).to_a + @numerous = EnumerableSpecs::Numerous.new(*@elements) + end + + it "returns all elements for which the block is not false" do + @numerous.select {|i| i % 3 == 0 }.should == [3, 6, 9] + @numerous.select {|i| true }.should == @elements + @numerous.select {|i| false }.should == [] + end + + it "returns an enumerator when no block given" do + @numerous.select.should.instance_of?(Enumerator) + end + + it "passes through the values yielded by #each_with_index" do + [:a, :b].each_with_index.select { |x, i| ScratchPad << [x, i] } + ScratchPad.recorded.should == [[:a, 0], [:b, 1]] + end + + it "gathers whole arrays as elements when each yields multiple" do + multi = EnumerableSpecs::YieldsMulti.new + multi.select {|e| e == [3, 4, 5] }.should == [[3, 4, 5]] + end + + it_behaves_like :enumerable_enumeratorized_with_origin_size, :select end diff --git a/spec/ruby/core/enumerable/shared/collect.rb b/spec/ruby/core/enumerable/shared/collect.rb deleted file mode 100644 index 4696d324544e90..00000000000000 --- a/spec/ruby/core/enumerable/shared/collect.rb +++ /dev/null @@ -1,107 +0,0 @@ -require_relative 'enumerable_enumeratorized' - -describe :enumerable_collect, shared: true do - before :each do - ScratchPad.record [] - end - - it "returns a new array with the results of passing each element to block" do - entries = [0, 1, 3, 4, 5, 6] - numerous = EnumerableSpecs::Numerous.new(*entries) - numerous.send(@method) { |i| i % 2 }.should == [0, 1, 1, 0, 1, 0] - numerous.send(@method) { |i| i }.should == entries - end - - it "passes through the values yielded by #each_with_index" do - [:a, :b].each_with_index.send(@method) { |x, i| ScratchPad << [x, i]; nil } - ScratchPad.recorded.should == [[:a, 0], [:b, 1]] - end - - it "gathers initial args as elements when each yields multiple" do - multi = EnumerableSpecs::YieldsMulti.new - multi.send(@method) {|e| e}.should == [1,3,6] - end - - it "only yields increasing values for a Range" do - (1..0).send(@method) { |x| x }.should == [] - (1..1).send(@method) { |x| x }.should == [1] - (1..2).send(@method) { |x| x }.should == [1, 2] - end - - it "returns an enumerator when no block given" do - enum = EnumerableSpecs::Numerous.new.send(@method) - enum.should.instance_of?(Enumerator) - enum.each { |i| -i }.should == [-2, -5, -3, -6, -1, -4] - end - - it "reports the same arity as the given block" do - entries = [0, 1, 3, 4, 5, 6] - numerous = EnumerableSpecs::Numerous.new(*entries) - - def numerous.each(&block) - ScratchPad << block.arity - super - end - - numerous.send(@method) { |a, b| a % 2 }.should == [0, 1, 1, 0, 1, 0] - ScratchPad.recorded.should == [2] - ScratchPad.clear - ScratchPad.record [] - numerous.send(@method) { |i| i }.should == entries - ScratchPad.recorded.should == [1] - end - - it "yields an Array of 2 elements for a Hash when block arity is 1" do - c = Class.new do - def register(a) - ScratchPad << a - end - end - m = c.new.method(:register) - - ScratchPad.record [] - { 1 => 'a', 2 => 'b' }.map(&m) - ScratchPad.recorded.should == [[1, 'a'], [2, 'b']] - end - - it "yields 2 arguments for a Hash when block arity is 2" do - c = Class.new do - def register(a, b) - ScratchPad << [a, b] - end - end - m = c.new.method(:register) - - ScratchPad.record [] - { 1 => 'a', 2 => 'b' }.map(&m) - ScratchPad.recorded.should == [[1, 'a'], [2, 'b']] - end - - it "raises an error for a Hash when an arity enforcing block of arity >2 is passed in" do - c = Class.new do - def register(a, b, c) - end - end - m = c.new.method(:register) - - -> do - { 1 => 'a', 2 => 'b' }.map(&m) - end.should.raise(ArgumentError) - end - - it "calls the each method on sub-classes" do - c = Class.new(Hash) do - def each - ScratchPad << 'in each' - super - end - end - h = c.new - h[1] = 'a' - ScratchPad.record [] - h.send(@method) { |k,v| v } - ScratchPad.recorded.should == ['in each'] - end - - it_should_behave_like :enumerable_enumeratorized_with_origin_size -end diff --git a/spec/ruby/core/enumerable/shared/collect_concat.rb b/spec/ruby/core/enumerable/shared/collect_concat.rb deleted file mode 100644 index 1694e3fdce6f31..00000000000000 --- a/spec/ruby/core/enumerable/shared/collect_concat.rb +++ /dev/null @@ -1,54 +0,0 @@ -require_relative 'enumerable_enumeratorized' - -describe :enumerable_collect_concat, shared: true do - it "yields elements to the block and flattens one level" do - numerous = EnumerableSpecs::Numerous.new(1, [2, 3], [4, [5, 6]], {foo: :bar}) - numerous.send(@method) { |i| i }.should == [1, 2, 3, 4, [5, 6], {foo: :bar}] - end - - it "appends non-Array elements that do not define #to_ary" do - obj = mock("to_ary undefined") - - numerous = EnumerableSpecs::Numerous.new(1, obj, 2) - numerous.send(@method) { |i| i }.should == [1, obj, 2] - end - - it "concatenates the result of calling #to_ary if it returns an Array" do - obj = mock("to_ary defined") - obj.should_receive(:to_ary).and_return([:a, :b]) - - numerous = EnumerableSpecs::Numerous.new(1, obj, 2) - numerous.send(@method) { |i| i }.should == [1, :a, :b, 2] - end - - it "does not call #to_a" do - obj = mock("to_ary undefined") - obj.should_not_receive(:to_a) - - numerous = EnumerableSpecs::Numerous.new(1, obj, 2) - numerous.send(@method) { |i| i }.should == [1, obj, 2] - end - - it "appends an element that defines #to_ary that returns nil" do - obj = mock("to_ary defined") - obj.should_receive(:to_ary).and_return(nil) - - numerous = EnumerableSpecs::Numerous.new(1, obj, 2) - numerous.send(@method) { |i| i }.should == [1, obj, 2] - end - - it "raises a TypeError if an element defining #to_ary does not return an Array or nil" do - obj = mock("to_ary defined") - obj.should_receive(:to_ary).and_return("array") - - -> { [1, obj, 3].send(@method) { |i| i } }.should.raise(TypeError) - end - - it "returns an enumerator when no block given" do - enum = EnumerableSpecs::Numerous.new(1, 2).send(@method) - enum.should.instance_of?(Enumerator) - enum.each{ |i| [i] * i }.should == [1, 2, 2] - end - - it_should_behave_like :enumerable_enumeratorized_with_origin_size -end diff --git a/spec/ruby/core/enumerable/shared/entries.rb b/spec/ruby/core/enumerable/shared/entries.rb deleted file mode 100644 index e32eb23d2a9a71..00000000000000 --- a/spec/ruby/core/enumerable/shared/entries.rb +++ /dev/null @@ -1,16 +0,0 @@ -describe :enumerable_entries, shared: true do - it "returns an array containing the elements" do - numerous = EnumerableSpecs::Numerous.new(1, nil, 'a', 2, false, true) - numerous.send(@method).should == [1, nil, "a", 2, false, true] - end - - it "passes through the values yielded by #each_with_index" do - [:a, :b].each_with_index.send(@method).should == [[:a, 0], [:b, 1]] - end - - it "passes arguments to each" do - count = EnumerableSpecs::EachCounter.new(1, 2, 3) - count.send(@method, :hello, "world").should == [1, 2, 3] - count.arguments_passed.should == [:hello, "world"] - end -end diff --git a/spec/ruby/core/enumerable/shared/find.rb b/spec/ruby/core/enumerable/shared/find.rb deleted file mode 100644 index cdff6404151b74..00000000000000 --- a/spec/ruby/core/enumerable/shared/find.rb +++ /dev/null @@ -1,77 +0,0 @@ -require_relative 'enumerable_enumeratorized' - -describe :enumerable_find, shared: true do - # #detect and #find are aliases, so we only need one function - before :each do - ScratchPad.record [] - @elements = [2, 4, 6, 8, 10] - @numerous = EnumerableSpecs::Numerous.new(*@elements) - @empty = [] - end - - it "passes each entry in enum to block while block when block is false" do - visited_elements = [] - @numerous.send(@method) do |element| - visited_elements << element - false - end - visited_elements.should == @elements - end - - it "returns nil when the block is false and there is no ifnone proc given" do - @numerous.send(@method) {|e| false }.should == nil - end - - it "returns the first element for which the block is not false" do - @elements.each do |element| - @numerous.send(@method) {|e| e > element - 1 }.should == element - end - end - - it "returns the value of the ifnone proc if the block is false" do - fail_proc = -> { "cheeseburgers" } - @numerous.send(@method, fail_proc) {|e| false }.should == "cheeseburgers" - end - - it "doesn't call the ifnone proc if an element is found" do - fail_proc = -> { raise "This shouldn't have been called" } - @numerous.send(@method, fail_proc) {|e| e == @elements.first }.should == 2 - end - - it "calls the ifnone proc only once when the block is false" do - times = 0 - fail_proc = -> { times += 1; raise if times > 1; "cheeseburgers" } - @numerous.send(@method, fail_proc) {|e| false }.should == "cheeseburgers" - end - - it "calls the ifnone proc when there are no elements" do - fail_proc = -> { "yay" } - @empty.send(@method, fail_proc) {|e| true}.should == "yay" - end - - it "ignores the ifnone argument when nil" do - @numerous.send(@method, nil) {|e| false }.should == nil - end - - it "passes through the values yielded by #each_with_index" do - [:a, :b].each_with_index.send(@method) { |x, i| ScratchPad << [x, i]; nil } - ScratchPad.recorded.should == [[:a, 0], [:b, 1]] - end - - it "returns an enumerator when no block given" do - @numerous.send(@method).should.instance_of?(Enumerator) - end - - it "passes the ifnone proc to the enumerator" do - times = 0 - fail_proc = -> { times += 1; raise if times > 1; "cheeseburgers" } - @numerous.send(@method, fail_proc).each {|e| false }.should == "cheeseburgers" - end - - it "gathers whole arrays as elements when each yields multiple" do - multi = EnumerableSpecs::YieldsMulti.new - multi.send(@method) {|e| e == [1, 2] }.should == [1, 2] - end - - it_should_behave_like :enumerable_enumeratorized_with_unknown_size -end diff --git a/spec/ruby/core/enumerable/shared/find_all.rb b/spec/ruby/core/enumerable/shared/find_all.rb deleted file mode 100644 index 27f01de6e01d53..00000000000000 --- a/spec/ruby/core/enumerable/shared/find_all.rb +++ /dev/null @@ -1,31 +0,0 @@ -require_relative 'enumerable_enumeratorized' - -describe :enumerable_find_all, shared: true do - before :each do - ScratchPad.record [] - @elements = (1..10).to_a - @numerous = EnumerableSpecs::Numerous.new(*@elements) - end - - it "returns all elements for which the block is not false" do - @numerous.send(@method) {|i| i % 3 == 0 }.should == [3, 6, 9] - @numerous.send(@method) {|i| true }.should == @elements - @numerous.send(@method) {|i| false }.should == [] - end - - it "returns an enumerator when no block given" do - @numerous.send(@method).should.instance_of?(Enumerator) - end - - it "passes through the values yielded by #each_with_index" do - [:a, :b].each_with_index.send(@method) { |x, i| ScratchPad << [x, i] } - ScratchPad.recorded.should == [[:a, 0], [:b, 1]] - end - - it "gathers whole arrays as elements when each yields multiple" do - multi = EnumerableSpecs::YieldsMulti.new - multi.send(@method) {|e| e == [3, 4, 5] }.should == [[3, 4, 5]] - end - - it_should_behave_like :enumerable_enumeratorized_with_origin_size -end diff --git a/spec/ruby/core/enumerable/shared/include.rb b/spec/ruby/core/enumerable/shared/include.rb deleted file mode 100644 index ea250f032bb55a..00000000000000 --- a/spec/ruby/core/enumerable/shared/include.rb +++ /dev/null @@ -1,34 +0,0 @@ -describe :enumerable_include, shared: true do - it "returns true if any element == argument for numbers" do - class EnumerableSpecIncludeP; def ==(obj) obj == 5; end; end - - elements = (0..5).to_a - EnumerableSpecs::Numerous.new(*elements).send(@method,5).should == true - EnumerableSpecs::Numerous.new(*elements).send(@method,10).should == false - EnumerableSpecs::Numerous.new(*elements).send(@method,EnumerableSpecIncludeP.new).should == true - end - - it "returns true if any element == argument for other objects" do - class EnumerableSpecIncludeP11; def ==(obj); obj == '11'; end; end - - elements = ('0'..'5').to_a + [EnumerableSpecIncludeP11.new] - EnumerableSpecs::Numerous.new(*elements).send(@method,'5').should == true - EnumerableSpecs::Numerous.new(*elements).send(@method,'10').should == false - EnumerableSpecs::Numerous.new(*elements).send(@method,EnumerableSpecIncludeP11.new).should == true - EnumerableSpecs::Numerous.new(*elements).send(@method,'11').should == true - end - - - it "returns true if any member of enum equals obj when == compare different classes (legacy rubycon)" do - # equality is tested with == - EnumerableSpecs::Numerous.new(2,4,6,8,10).send(@method, 2.0).should == true - EnumerableSpecs::Numerous.new(2,4,[6,8],10).send(@method, [6, 8]).should == true - EnumerableSpecs::Numerous.new(2,4,[6,8],10).send(@method, [6.0, 8.0]).should == true - end - - it "gathers whole arrays as elements when each yields multiple" do - multi = EnumerableSpecs::YieldsMulti.new - multi.send(@method, [1,2]).should == true - end - -end diff --git a/spec/ruby/core/enumerable/shared/inject.rb b/spec/ruby/core/enumerable/shared/inject.rb deleted file mode 100644 index 7da4f0ca9938b2..00000000000000 --- a/spec/ruby/core/enumerable/shared/inject.rb +++ /dev/null @@ -1,142 +0,0 @@ -require_relative '../../array/shared/iterable_and_tolerating_size_increasing' - -describe :enumerable_inject, shared: true do - it "with argument takes a block with an accumulator (with argument as initial value) and the current element. Value of block becomes new accumulator" do - a = [] - EnumerableSpecs::Numerous.new.send(@method, 0) { |memo, i| a << [memo, i]; i } - a.should == [[0, 2], [2, 5], [5, 3], [3, 6], [6, 1], [1, 4]] - EnumerableSpecs::EachDefiner.new(true, true, true).send(@method, nil) {|result, i| i && result}.should == nil - end - - it "produces an array of the accumulator and the argument when given a block with a *arg" do - a = [] - [1,2].send(@method, 0) {|*args| a << args; args[0] + args[1]} - a.should == [[0, 1], [1, 2]] - end - - it "can take two argument" do - EnumerableSpecs::Numerous.new(1, 2, 3).send(@method, 10, :-).should == 4 - EnumerableSpecs::Numerous.new(1, 2, 3).send(@method, 10, "-").should == 4 - - [1, 2, 3].send(@method, 10, :-).should == 4 - [1, 2, 3].send(@method, 10, "-").should == 4 - end - - it "converts non-Symbol method name argument to String with #to_str if two arguments" do - name = Object.new - def name.to_str; "-"; end - - EnumerableSpecs::Numerous.new(1, 2, 3).send(@method, 10, name).should == 4 - [1, 2, 3].send(@method, 10, name).should == 4 - end - - it "raises TypeError when the second argument is not Symbol or String and it cannot be converted to String if two arguments" do - -> { EnumerableSpecs::Numerous.new(1, 2, 3).send(@method, 10, Object.new) }.should.raise(TypeError, /is not a symbol nor a string/) - -> { [1, 2, 3].send(@method, 10, Object.new) }.should.raise(TypeError, /is not a symbol nor a string/) - end - - it "ignores the block if two arguments" do - -> { - EnumerableSpecs::Numerous.new(1, 2, 3).send(@method, 10, :-) { raise "we never get here"}.should == 4 - }.should complain(/#{__FILE__}:#{__LINE__-1}: warning: given block not used/, verbose: true) - - -> { - [1, 2, 3].send(@method, 10, :-) { raise "we never get here"}.should == 4 - }.should complain(/#{__FILE__}:#{__LINE__-1}: warning: given block not used/, verbose: true) - end - - it "does not warn when given a Symbol with $VERBOSE true" do - -> { - [1, 2].send(@method, 0, :+) - [1, 2].send(@method, :+) - EnumerableSpecs::Numerous.new(1, 2).send(@method, 0, :+) - EnumerableSpecs::Numerous.new(1, 2).send(@method, :+) - }.should_not complain(verbose: true) - end - - it "can take a symbol argument" do - EnumerableSpecs::Numerous.new(10, 1, 2, 3).send(@method, :-).should == 4 - [10, 1, 2, 3].send(@method, :-).should == 4 - end - - it "can take a String argument" do - EnumerableSpecs::Numerous.new(10, 1, 2, 3).send(@method, "-").should == 4 - [10, 1, 2, 3].send(@method, "-").should == 4 - end - - it "converts non-Symbol method name argument to String with #to_str" do - name = Object.new - def name.to_str; "-"; end - - EnumerableSpecs::Numerous.new(10, 1, 2, 3).send(@method, name).should == 4 - [10, 1, 2, 3].send(@method, name).should == 4 - end - - it "raises TypeError when passed not Symbol or String method name argument and it cannot be converted to String" do - -> { EnumerableSpecs::Numerous.new(10, 1, 2, 3).send(@method, Object.new) }.should.raise(TypeError, /is not a symbol nor a string/) - -> { [10, 1, 2, 3].send(@method, Object.new) }.should.raise(TypeError, /is not a symbol nor a string/) - end - - it "without argument takes a block with an accumulator (with first element as initial value) and the current element. Value of block becomes new accumulator" do - a = [] - EnumerableSpecs::Numerous.new.send(@method) { |memo, i| a << [memo, i]; i } - a.should == [[2, 5], [5, 3], [3, 6], [6, 1], [1, 4]] - end - - it "gathers whole arrays as elements when each yields multiple" do - multi = EnumerableSpecs::YieldsMulti.new - multi.send(@method, []) {|acc, e| acc << e }.should == [[1, 2], [3, 4, 5], [6, 7, 8, 9]] - end - - it "with inject arguments(legacy rubycon)" do - # with inject argument - EnumerableSpecs::EachDefiner.new().send(@method, 1) {|acc,x| 999 }.should == 1 - EnumerableSpecs::EachDefiner.new(2).send(@method, 1) {|acc,x| 999 }.should == 999 - EnumerableSpecs::EachDefiner.new(2).send(@method, 1) {|acc,x| acc }.should == 1 - EnumerableSpecs::EachDefiner.new(2).send(@method, 1) {|acc,x| x }.should == 2 - - EnumerableSpecs::EachDefiner.new(1,2,3,4).send(@method, 100) {|acc,x| acc + x }.should == 110 - EnumerableSpecs::EachDefiner.new(1,2,3,4).send(@method, 100) {|acc,x| acc * x }.should == 2400 - - EnumerableSpecs::EachDefiner.new('a','b','c').send(@method, "z") {|result, i| i+result}.should == "cbaz" - end - - it "without inject arguments(legacy rubycon)" do - # no inject argument - EnumerableSpecs::EachDefiner.new(2).send(@method) {|acc,x| 999 }.should == 2 - EnumerableSpecs::EachDefiner.new(2).send(@method) {|acc,x| acc }.should == 2 - EnumerableSpecs::EachDefiner.new(2).send(@method) {|acc,x| x }.should == 2 - - EnumerableSpecs::EachDefiner.new(1,2,3,4).send(@method) {|acc,x| acc + x }.should == 10 - EnumerableSpecs::EachDefiner.new(1,2,3,4).send(@method) {|acc,x| acc * x }.should == 24 - - EnumerableSpecs::EachDefiner.new('a','b','c').send(@method) {|result, i| i+result}.should == "cba" - EnumerableSpecs::EachDefiner.new(3, 4, 5).send(@method) {|result, i| result*i}.should == 60 - EnumerableSpecs::EachDefiner.new([1], 2, 'a','b').send(@method){|r,i| r< { [1,2].send(@method) }.should.raise(ArgumentError) - -> { {one: 1, two: 2}.send(@method) }.should.raise(ArgumentError) - end -end diff --git a/spec/ruby/core/enumerable/to_a_spec.rb b/spec/ruby/core/enumerable/to_a_spec.rb index 723f9225740c8e..f1796070f01db3 100644 --- a/spec/ruby/core/enumerable/to_a_spec.rb +++ b/spec/ruby/core/enumerable/to_a_spec.rb @@ -1,7 +1,19 @@ require_relative '../../spec_helper' require_relative 'fixtures/classes' -require_relative 'shared/entries' describe "Enumerable#to_a" do - it_behaves_like :enumerable_entries, :to_a + it "returns an array containing the elements" do + numerous = EnumerableSpecs::Numerous.new(1, nil, 'a', 2, false, true) + numerous.to_a.should == [1, nil, "a", 2, false, true] + end + + it "passes through the values yielded by #each_with_index" do + [:a, :b].each_with_index.to_a.should == [[:a, 0], [:b, 1]] + end + + it "passes arguments to each" do + count = EnumerableSpecs::EachCounter.new(1, 2, 3) + count.to_a(:hello, "world").should == [1, 2, 3] + count.arguments_passed.should == [:hello, "world"] + end end diff --git a/spec/ruby/core/enumerator/each_spec.rb b/spec/ruby/core/enumerator/each_spec.rb index 03be53fe055a8a..64912b98b6ea09 100644 --- a/spec/ruby/core/enumerator/each_spec.rb +++ b/spec/ruby/core/enumerator/each_spec.rb @@ -1,6 +1,21 @@ require_relative '../../spec_helper' +require_relative 'shared/each' describe "Enumerator#each" do + describe "passing source-yielded arguments to the block" do + before :each do + @object = -> e { e } + end + it_behaves_like :enum_each, nil + end + + describe "passing source-yielded arguments to the block (lazy)" do + before :each do + @object = -> e { e.lazy } + end + it_behaves_like :enum_each, nil + end + before :each do object_each_with_arguments = Object.new def object_each_with_arguments.each_with_arguments(arg, *args) diff --git a/spec/ruby/core/enumerator/each_with_object_spec.rb b/spec/ruby/core/enumerator/each_with_object_spec.rb index 84a45ae89dfc50..0e0a4496f401b9 100644 --- a/spec/ruby/core/enumerator/each_with_object_spec.rb +++ b/spec/ruby/core/enumerator/each_with_object_spec.rb @@ -1,6 +1,42 @@ require_relative '../../spec_helper' -require_relative 'shared/with_object' describe "Enumerator#each_with_object" do - it_behaves_like :enum_with_object, :each_with_object + before :each do + @enum = [:a, :b].to_enum + @memo = '' + @block_params = @enum.each_with_object(@memo).to_a + end + + it "receives an argument" do + @enum.method(:each_with_object).arity.should == 1 + end + + context "with block" do + it "returns the given object" do + ret = @enum.each_with_object(@memo) do |elm, memo| + # nothing + end + ret.should.equal?(@memo) + end + + context "the block parameter" do + it "passes each element to first parameter" do + @block_params[0][0].should.equal?(:a) + @block_params[1][0].should.equal?(:b) + end + + it "passes the given object to last parameter" do + @block_params[0][1].should.equal?(@memo) + @block_params[1][1].should.equal?(@memo) + end + end + end + + context "without block" do + it "returns new Enumerator" do + ret = @enum.each_with_object(@memo) + ret.should.instance_of?(Enumerator) + ret.should_not.equal?(@enum) + end + end end diff --git a/spec/ruby/core/enumerator/enum_for_spec.rb b/spec/ruby/core/enumerator/enum_for_spec.rb deleted file mode 100644 index fbdf98545a964d..00000000000000 --- a/spec/ruby/core/enumerator/enum_for_spec.rb +++ /dev/null @@ -1,6 +0,0 @@ -require_relative '../../spec_helper' -require_relative 'shared/enum_for' - -describe "Enumerator#enum_for" do - it_behaves_like :enum_for, :enum_for -end diff --git a/spec/ruby/core/enumerator/lazy/collect_concat_spec.rb b/spec/ruby/core/enumerator/lazy/collect_concat_spec.rb index 8765bb219065cf..d9fd576e4332db 100644 --- a/spec/ruby/core/enumerator/lazy/collect_concat_spec.rb +++ b/spec/ruby/core/enumerator/lazy/collect_concat_spec.rb @@ -1,8 +1,8 @@ -# -*- encoding: us-ascii -*- - require_relative '../../../spec_helper' -require_relative 'shared/collect_concat' describe "Enumerator::Lazy#collect_concat" do - it_behaves_like :enumerator_lazy_collect_concat, :collect_concat + it "is an alias of Enumerator::Lazy#flat_map" do + Enumerator::Lazy.instance_method(:collect_concat).should == + Enumerator::Lazy.instance_method(:flat_map) + end end diff --git a/spec/ruby/core/enumerator/lazy/collect_spec.rb b/spec/ruby/core/enumerator/lazy/collect_spec.rb index 14b79ce16d18ab..53a477e053d17b 100644 --- a/spec/ruby/core/enumerator/lazy/collect_spec.rb +++ b/spec/ruby/core/enumerator/lazy/collect_spec.rb @@ -1,8 +1,8 @@ -# -*- encoding: us-ascii -*- - require_relative '../../../spec_helper' -require_relative 'shared/collect' describe "Enumerator::Lazy#collect" do - it_behaves_like :enumerator_lazy_collect, :collect + it "is an alias of Enumerator::Lazy#map" do + Enumerator::Lazy.instance_method(:collect).should == + Enumerator::Lazy.instance_method(:map) + end end diff --git a/spec/ruby/core/enumerator/lazy/enum_for_spec.rb b/spec/ruby/core/enumerator/lazy/enum_for_spec.rb index 7e7783f6f12c91..b40c5d991596ba 100644 --- a/spec/ruby/core/enumerator/lazy/enum_for_spec.rb +++ b/spec/ruby/core/enumerator/lazy/enum_for_spec.rb @@ -1,8 +1,8 @@ -# -*- encoding: us-ascii -*- - require_relative '../../../spec_helper' -require_relative 'shared/to_enum' describe "Enumerator::Lazy#enum_for" do - it_behaves_like :enumerator_lazy_to_enum, :enum_for + it "is an alias of Enumerator::Lazy#to_enum" do + Enumerator::Lazy.instance_method(:enum_for).should == + Enumerator::Lazy.instance_method(:to_enum) + end end diff --git a/spec/ruby/core/enumerator/lazy/filter_spec.rb b/spec/ruby/core/enumerator/lazy/filter_spec.rb index 43128241e02eb5..3ca5376faa69a4 100644 --- a/spec/ruby/core/enumerator/lazy/filter_spec.rb +++ b/spec/ruby/core/enumerator/lazy/filter_spec.rb @@ -1,6 +1,8 @@ require_relative '../../../spec_helper' -require_relative 'shared/select' describe "Enumerator::Lazy#filter" do - it_behaves_like :enumerator_lazy_select, :filter + it "is an alias of Enumerator::Lazy#select" do + Enumerator::Lazy.instance_method(:filter).should == + Enumerator::Lazy.instance_method(:select) + end end diff --git a/spec/ruby/core/enumerator/lazy/find_all_spec.rb b/spec/ruby/core/enumerator/lazy/find_all_spec.rb index 8b05c53803946a..64930dc61bdeab 100644 --- a/spec/ruby/core/enumerator/lazy/find_all_spec.rb +++ b/spec/ruby/core/enumerator/lazy/find_all_spec.rb @@ -1,8 +1,8 @@ -# -*- encoding: us-ascii -*- - require_relative '../../../spec_helper' -require_relative 'shared/select' describe "Enumerator::Lazy#find_all" do - it_behaves_like :enumerator_lazy_select, :find_all + it "is an alias of Enumerator::Lazy#select" do + Enumerator::Lazy.instance_method(:find_all).should == + Enumerator::Lazy.instance_method(:select) + end end diff --git a/spec/ruby/core/enumerator/lazy/flat_map_spec.rb b/spec/ruby/core/enumerator/lazy/flat_map_spec.rb index 5dcaa8bfa1ab0c..609bf95b9efbc4 100644 --- a/spec/ruby/core/enumerator/lazy/flat_map_spec.rb +++ b/spec/ruby/core/enumerator/lazy/flat_map_spec.rb @@ -1,10 +1,78 @@ -# -*- encoding: us-ascii -*- - require_relative '../../../spec_helper' -require_relative 'shared/collect_concat' +require_relative 'fixtures/classes' describe "Enumerator::Lazy#flat_map" do - it_behaves_like :enumerator_lazy_collect_concat, :flat_map + before :each do + @yieldsmixed = EnumeratorLazySpecs::YieldsMixed.new.to_enum.lazy + @eventsmixed = EnumeratorLazySpecs::EventsMixed.new.to_enum.lazy + ScratchPad.record [] + end + + after :each do + ScratchPad.clear + end + + it "returns a new instance of Enumerator::Lazy" do + ret = @yieldsmixed.flat_map {} + ret.should.instance_of?(Enumerator::Lazy) + ret.should_not.equal?(@yieldsmixed) + end + + it "sets #size to nil" do + Enumerator::Lazy.new(Object.new, 100) {}.flat_map { true }.size.should == nil + end + + describe "when the returned lazy enumerator is evaluated by Enumerable#first" do + it "stops after specified times" do + (0..Float::INFINITY).lazy.flat_map { |n| (n * 10).to_s }.first(6).should == %w[0 10 20 30 40 50] + + @eventsmixed.flat_map {}.first(1) + ScratchPad.recorded.should == [:before_yield] + end + + it "flattens elements when the given block returned an array or responding to .each and .force" do + (0..Float::INFINITY).lazy.flat_map { |n| (n * 10).to_s.chars }.first(6).should == %w[0 1 0 2 0 3] + (0..Float::INFINITY).lazy.flat_map { |n| (n * 10).to_s.each_char }.first(6).all? { |o| o.instance_of? Enumerator }.should == true + (0..Float::INFINITY).lazy.flat_map { |n| (n * 10).to_s.each_char.lazy }.first(6).should == %w[0 1 0 2 0 3] + end + end + + it "calls the block with initial values when yield with multiple arguments" do + yields = [] + @yieldsmixed.flat_map { |v| yields << v }.force + yields.should == EnumeratorLazySpecs::YieldsMixed.initial_yields + end + + it "raises an ArgumentError when not given a block" do + -> { @yieldsmixed.flat_map }.should.raise(ArgumentError) + end + + describe "on a nested Lazy" do + it "sets #size to nil" do + Enumerator::Lazy.new(Object.new, 100) {}.take(50) {}.flat_map {}.size.should == nil + end + + describe "when the returned lazy enumerator is evaluated by Enumerable#first" do + it "stops after specified times" do + (0..Float::INFINITY).lazy.map {|n| n * 10 }.flat_map { |n| n.to_s }.first(6).should == %w[0 10 20 30 40 50] + + @eventsmixed.flat_map {}.flat_map {}.first(1) + ScratchPad.recorded.should == [:before_yield] + end + + it "flattens elements when the given block returned an array or responding to .each and .force" do + (0..Float::INFINITY).lazy.map {|n| n * 10 }.flat_map { |n| n.to_s.chars }.first(6).should == %w[0 1 0 2 0 3] + (0..Float::INFINITY).lazy.map {|n| n * 10 }.flat_map { |n| n.to_s.each_char }.first(6).all? { |o| o.instance_of? Enumerator }.should == true + (0..Float::INFINITY).lazy.map {|n| n * 10 }.flat_map { |n| n.to_s.each_char.lazy }.first(6).should == %w[0 1 0 2 0 3] + end + end + end + + it "works with an infinite enumerable" do + s = 0..Float::INFINITY + s.lazy.flat_map { |n| [-n, +n] }.first(200).should == + s.first(100).flat_map { |n| [-n, +n] }.to_a + end it "properly unwraps nested yields" do s = Enumerator.new do |y| loop do y << [1, 2] end end diff --git a/spec/ruby/core/enumerator/lazy/map_spec.rb b/spec/ruby/core/enumerator/lazy/map_spec.rb index 5cb998f5f7c6e7..2c7f8efab897cf 100644 --- a/spec/ruby/core/enumerator/lazy/map_spec.rb +++ b/spec/ruby/core/enumerator/lazy/map_spec.rb @@ -1,10 +1,62 @@ -# -*- encoding: us-ascii -*- - require_relative '../../../spec_helper' -require_relative 'shared/collect' +require_relative 'fixtures/classes' describe "Enumerator::Lazy#map" do - it_behaves_like :enumerator_lazy_collect, :map + before :each do + @yieldsmixed = EnumeratorLazySpecs::YieldsMixed.new.to_enum.lazy + @eventsmixed = EnumeratorLazySpecs::EventsMixed.new.to_enum.lazy + ScratchPad.record [] + end + + after :each do + ScratchPad.clear + end + + it "returns a new instance of Enumerator::Lazy" do + ret = @yieldsmixed.map {} + ret.should.instance_of?(Enumerator::Lazy) + ret.should_not.equal?(@yieldsmixed) + end + + it "keeps size" do + Enumerator::Lazy.new(Object.new, 100) {}.map {}.size.should == 100 + end + + describe "when the returned lazy enumerator is evaluated by Enumerable#first" do + it "stops after specified times" do + (0..Float::INFINITY).lazy.map(&:succ).first(3).should == [1, 2, 3] + + @eventsmixed.map {}.first(1) + ScratchPad.recorded.should == [:before_yield] + end + end + + it "calls the block with initial values when yield with multiple arguments" do + yields = [] + @yieldsmixed.map { |v| yields << v }.force + yields.should == EnumeratorLazySpecs::YieldsMixed.initial_yields + end + + describe "on a nested Lazy" do + it "keeps size" do + Enumerator::Lazy.new(Object.new, 100) {}.map {}.map {}.size.should == 100 + end + + describe "when the returned lazy enumerator is evaluated by Enumerable#first" do + it "stops after specified times" do + (0..Float::INFINITY).lazy.map(&:succ).map(&:succ).first(3).should == [2, 3, 4] + + @eventsmixed.map {}.map {}.first(1) + ScratchPad.recorded.should == [:before_yield] + end + end + end + + it "works with an infinite enumerable" do + s = 0..Float::INFINITY + s.lazy.map { |n| n }.first(100).should == + s.first(100).map { |n| n }.to_a + end it "doesn't unwrap Arrays" do Enumerator.new {|y| y.yield([1])}.lazy.to_a.should == [[1]] diff --git a/spec/ruby/core/enumerator/lazy/select_spec.rb b/spec/ruby/core/enumerator/lazy/select_spec.rb index 3773d8f0a8187c..29c8f1bd8096f8 100644 --- a/spec/ruby/core/enumerator/lazy/select_spec.rb +++ b/spec/ruby/core/enumerator/lazy/select_spec.rb @@ -1,10 +1,66 @@ -# -*- encoding: us-ascii -*- - require_relative '../../../spec_helper' -require_relative 'shared/select' +require_relative 'fixtures/classes' describe "Enumerator::Lazy#select" do - it_behaves_like :enumerator_lazy_select, :select + before :each do + @yieldsmixed = EnumeratorLazySpecs::YieldsMixed.new.to_enum.lazy + @eventsmixed = EnumeratorLazySpecs::EventsMixed.new.to_enum.lazy + ScratchPad.record [] + end + + after :each do + ScratchPad.clear + end + + it "returns a new instance of Enumerator::Lazy" do + ret = @yieldsmixed.select {} + ret.should.instance_of?(Enumerator::Lazy) + ret.should_not.equal?(@yieldsmixed) + end + + it "sets #size to nil" do + Enumerator::Lazy.new(Object.new, 100) {}.select { true }.size.should == nil + end + + describe "when the returned lazy enumerator is evaluated by Enumerable#first" do + it "stops after specified times" do + (0..Float::INFINITY).lazy.select(&:even?).first(3).should == [0, 2, 4] + + @eventsmixed.select { true }.first(1) + ScratchPad.recorded.should == [:before_yield] + end + end + + it "calls the block with a gathered array when yield with multiple arguments" do + yields = [] + @yieldsmixed.select { |v| yields << v }.force + yields.should == EnumeratorLazySpecs::YieldsMixed.gathered_yields + end + + it "raises an ArgumentError when not given a block" do + -> { @yieldsmixed.select }.should.raise(ArgumentError) + end + + describe "on a nested Lazy" do + it "sets #size to nil" do + Enumerator::Lazy.new(Object.new, 100) {}.take(50) {}.select { true }.size.should == nil + end + + describe "when the returned lazy enumerator is evaluated by Enumerable#first" do + it "stops after specified times" do + (0..Float::INFINITY).lazy.select { |n| n > 5 }.select(&:even?).first(3).should == [6, 8, 10] + + @eventsmixed.select { true }.select { true }.first(1) + ScratchPad.recorded.should == [:before_yield] + end + end + end + + it "works with an infinite enumerable" do + s = 0..Float::INFINITY + s.lazy.select { |n| true }.first(100).should == + s.first(100).select { |n| true } + end it "doesn't pre-evaluate the next element" do eval_count = 0 diff --git a/spec/ruby/core/enumerator/lazy/shared/collect.rb b/spec/ruby/core/enumerator/lazy/shared/collect.rb deleted file mode 100644 index 0ed04c8e02dd4c..00000000000000 --- a/spec/ruby/core/enumerator/lazy/shared/collect.rb +++ /dev/null @@ -1,62 +0,0 @@ -# -*- encoding: us-ascii -*- - -require_relative '../../../../spec_helper' -require_relative '../fixtures/classes' - -describe :enumerator_lazy_collect, shared: true do - before :each do - @yieldsmixed = EnumeratorLazySpecs::YieldsMixed.new.to_enum.lazy - @eventsmixed = EnumeratorLazySpecs::EventsMixed.new.to_enum.lazy - ScratchPad.record [] - end - - after :each do - ScratchPad.clear - end - - it "returns a new instance of Enumerator::Lazy" do - ret = @yieldsmixed.send(@method) {} - ret.should.instance_of?(Enumerator::Lazy) - ret.should_not.equal?(@yieldsmixed) - end - - it "keeps size" do - Enumerator::Lazy.new(Object.new, 100) {}.send(@method) {}.size.should == 100 - end - - describe "when the returned lazy enumerator is evaluated by Enumerable#first" do - it "stops after specified times" do - (0..Float::INFINITY).lazy.send(@method, &:succ).first(3).should == [1, 2, 3] - - @eventsmixed.send(@method) {}.first(1) - ScratchPad.recorded.should == [:before_yield] - end - end - - it "calls the block with initial values when yield with multiple arguments" do - yields = [] - @yieldsmixed.send(@method) { |v| yields << v }.force - yields.should == EnumeratorLazySpecs::YieldsMixed.initial_yields - end - - describe "on a nested Lazy" do - it "keeps size" do - Enumerator::Lazy.new(Object.new, 100) {}.send(@method) {}.send(@method) {}.size.should == 100 - end - - describe "when the returned lazy enumerator is evaluated by Enumerable#first" do - it "stops after specified times" do - (0..Float::INFINITY).lazy.send(@method, &:succ).send(@method, &:succ).first(3).should == [2, 3, 4] - - @eventsmixed.send(@method) {}.send(@method) {}.first(1) - ScratchPad.recorded.should == [:before_yield] - end - end - end - - it "works with an infinite enumerable" do - s = 0..Float::INFINITY - s.lazy.send(@method) { |n| n }.first(100).should == - s.first(100).send(@method) { |n| n }.to_a - end -end diff --git a/spec/ruby/core/enumerator/lazy/shared/collect_concat.rb b/spec/ruby/core/enumerator/lazy/shared/collect_concat.rb deleted file mode 100644 index 685e6d05948648..00000000000000 --- a/spec/ruby/core/enumerator/lazy/shared/collect_concat.rb +++ /dev/null @@ -1,78 +0,0 @@ -# -*- encoding: us-ascii -*- - -require_relative '../../../../spec_helper' -require_relative '../fixtures/classes' - -describe :enumerator_lazy_collect_concat, shared: true do - before :each do - @yieldsmixed = EnumeratorLazySpecs::YieldsMixed.new.to_enum.lazy - @eventsmixed = EnumeratorLazySpecs::EventsMixed.new.to_enum.lazy - ScratchPad.record [] - end - - after :each do - ScratchPad.clear - end - - it "returns a new instance of Enumerator::Lazy" do - ret = @yieldsmixed.send(@method) {} - ret.should.instance_of?(Enumerator::Lazy) - ret.should_not.equal?(@yieldsmixed) - end - - it "sets #size to nil" do - Enumerator::Lazy.new(Object.new, 100) {}.send(@method) { true }.size.should == nil - end - - describe "when the returned lazy enumerator is evaluated by Enumerable#first" do - it "stops after specified times" do - (0..Float::INFINITY).lazy.send(@method) { |n| (n * 10).to_s }.first(6).should == %w[0 10 20 30 40 50] - - @eventsmixed.send(@method) {}.first(1) - ScratchPad.recorded.should == [:before_yield] - end - - it "flattens elements when the given block returned an array or responding to .each and .force" do - (0..Float::INFINITY).lazy.send(@method) { |n| (n * 10).to_s.chars }.first(6).should == %w[0 1 0 2 0 3] - (0..Float::INFINITY).lazy.send(@method) { |n| (n * 10).to_s.each_char }.first(6).all? { |o| o.instance_of? Enumerator }.should == true - (0..Float::INFINITY).lazy.send(@method) { |n| (n * 10).to_s.each_char.lazy }.first(6).should == %w[0 1 0 2 0 3] - end - end - - it "calls the block with initial values when yield with multiple arguments" do - yields = [] - @yieldsmixed.send(@method) { |v| yields << v }.force - yields.should == EnumeratorLazySpecs::YieldsMixed.initial_yields - end - - it "raises an ArgumentError when not given a block" do - -> { @yieldsmixed.send(@method) }.should.raise(ArgumentError) - end - - describe "on a nested Lazy" do - it "sets #size to nil" do - Enumerator::Lazy.new(Object.new, 100) {}.take(50) {}.send(@method) {}.size.should == nil - end - - describe "when the returned lazy enumerator is evaluated by Enumerable#first" do - it "stops after specified times" do - (0..Float::INFINITY).lazy.map {|n| n * 10 }.send(@method) { |n| n.to_s }.first(6).should == %w[0 10 20 30 40 50] - - @eventsmixed.send(@method) {}.send(@method) {}.first(1) - ScratchPad.recorded.should == [:before_yield] - end - - it "flattens elements when the given block returned an array or responding to .each and .force" do - (0..Float::INFINITY).lazy.map {|n| n * 10 }.send(@method) { |n| n.to_s.chars }.first(6).should == %w[0 1 0 2 0 3] - (0..Float::INFINITY).lazy.map {|n| n * 10 }.send(@method) { |n| n.to_s.each_char }.first(6).all? { |o| o.instance_of? Enumerator }.should == true - (0..Float::INFINITY).lazy.map {|n| n * 10 }.send(@method) { |n| n.to_s.each_char.lazy }.first(6).should == %w[0 1 0 2 0 3] - end - end - end - - it "works with an infinite enumerable" do - s = 0..Float::INFINITY - s.lazy.send(@method) { |n| [-n, +n] }.first(200).should == - s.first(100).send(@method) { |n| [-n, +n] }.to_a - end -end diff --git a/spec/ruby/core/enumerator/lazy/shared/select.rb b/spec/ruby/core/enumerator/lazy/shared/select.rb deleted file mode 100644 index 2d151766201110..00000000000000 --- a/spec/ruby/core/enumerator/lazy/shared/select.rb +++ /dev/null @@ -1,66 +0,0 @@ -# -*- encoding: us-ascii -*- - -require_relative '../../../../spec_helper' -require_relative '../fixtures/classes' - -describe :enumerator_lazy_select, shared: true do - before :each do - @yieldsmixed = EnumeratorLazySpecs::YieldsMixed.new.to_enum.lazy - @eventsmixed = EnumeratorLazySpecs::EventsMixed.new.to_enum.lazy - ScratchPad.record [] - end - - after :each do - ScratchPad.clear - end - - it "returns a new instance of Enumerator::Lazy" do - ret = @yieldsmixed.send(@method) {} - ret.should.instance_of?(Enumerator::Lazy) - ret.should_not.equal?(@yieldsmixed) - end - - it "sets #size to nil" do - Enumerator::Lazy.new(Object.new, 100) {}.send(@method) { true }.size.should == nil - end - - describe "when the returned lazy enumerator is evaluated by Enumerable#first" do - it "stops after specified times" do - (0..Float::INFINITY).lazy.send(@method, &:even?).first(3).should == [0, 2, 4] - - @eventsmixed.send(@method) { true }.first(1) - ScratchPad.recorded.should == [:before_yield] - end - end - - it "calls the block with a gathered array when yield with multiple arguments" do - yields = [] - @yieldsmixed.send(@method) { |v| yields << v }.force - yields.should == EnumeratorLazySpecs::YieldsMixed.gathered_yields - end - - it "raises an ArgumentError when not given a block" do - -> { @yieldsmixed.send(@method) }.should.raise(ArgumentError) - end - - describe "on a nested Lazy" do - it "sets #size to nil" do - Enumerator::Lazy.new(Object.new, 100) {}.take(50) {}.send(@method) { true }.size.should == nil - end - - describe "when the returned lazy enumerator is evaluated by Enumerable#first" do - it "stops after specified times" do - (0..Float::INFINITY).lazy.send(@method) { |n| n > 5 }.send(@method, &:even?).first(3).should == [6, 8, 10] - - @eventsmixed.send(@method) { true }.send(@method) { true }.first(1) - ScratchPad.recorded.should == [:before_yield] - end - end - end - - it "works with an infinite enumerable" do - s = 0..Float::INFINITY - s.lazy.send(@method) { |n| true }.first(100).should == - s.first(100).send(@method) { |n| true } - end -end diff --git a/spec/ruby/core/enumerator/lazy/shared/to_enum.rb b/spec/ruby/core/enumerator/lazy/shared/to_enum.rb deleted file mode 100644 index 75b9aefe8c1e78..00000000000000 --- a/spec/ruby/core/enumerator/lazy/shared/to_enum.rb +++ /dev/null @@ -1,55 +0,0 @@ -# -*- encoding: us-ascii -*- - -require_relative '../../../../spec_helper' - -describe :enumerator_lazy_to_enum, shared: true do - before :each do - @infinite = (0..Float::INFINITY).lazy - end - - it "requires multiple arguments" do - Enumerator::Lazy.instance_method(@method).arity.should < 0 - end - - it "returns a new instance of Enumerator::Lazy" do - ret = @infinite.send @method - ret.should.instance_of?(Enumerator::Lazy) - ret.should_not.equal?(@infinite) - end - - it "sets #size to nil when not given a block" do - Enumerator::Lazy.new(Object.new, 100) {}.send(@method).size.should == nil - end - - it "sets given block to size when given a block" do - Enumerator::Lazy.new(Object.new, 100) {}.send(@method) { 30 }.size.should == 30 - end - - it "generates a lazy enumerator from the given name" do - @infinite.send(@method, :with_index, 10).first(3).should == [[0, 10], [1, 11], [2, 12]] - end - - it "passes given arguments to wrapped method" do - @infinite.send(@method, :each_slice, 2).map { |assoc| assoc.first * assoc.last }.first(4).should == [0, 6, 20, 42] - end - - it "used by some parent's methods though returning Lazy" do - { each_with_index: [], - with_index: [], - cycle: [1], - each_with_object: [Object.new], - with_object: [Object.new], - each_slice: [2], - each_entry: [], - each_cons: [2] - }.each_pair do |method, args| - @infinite.send(method, *args).should.instance_of?(Enumerator::Lazy) - end - end - - it "works with an infinite enumerable" do - s = 0..Float::INFINITY - s.lazy.send(@method, :with_index).first(100).should == - s.first(100).to_enum.send(@method, :with_index).to_a - end -end diff --git a/spec/ruby/core/enumerator/lazy/to_enum_spec.rb b/spec/ruby/core/enumerator/lazy/to_enum_spec.rb index 210e5294b7bfc1..c0233d60facdc7 100644 --- a/spec/ruby/core/enumerator/lazy/to_enum_spec.rb +++ b/spec/ruby/core/enumerator/lazy/to_enum_spec.rb @@ -1,8 +1,54 @@ -# -*- encoding: us-ascii -*- - require_relative '../../../spec_helper' -require_relative 'shared/to_enum' +require_relative 'fixtures/classes' describe "Enumerator::Lazy#to_enum" do - it_behaves_like :enumerator_lazy_to_enum, :to_enum + before :each do + @infinite = (0..Float::INFINITY).lazy + end + + it "requires multiple arguments" do + Enumerator::Lazy.instance_method(:to_enum).arity.should < 0 + end + + it "returns a new instance of Enumerator::Lazy" do + ret = @infinite.to_enum + ret.should.instance_of?(Enumerator::Lazy) + ret.should_not.equal?(@infinite) + end + + it "sets #size to nil when not given a block" do + Enumerator::Lazy.new(Object.new, 100) {}.to_enum.size.should == nil + end + + it "sets given block to size when given a block" do + Enumerator::Lazy.new(Object.new, 100) {}.to_enum { 30 }.size.should == 30 + end + + it "generates a lazy enumerator from the given name" do + @infinite.to_enum(:with_index, 10).first(3).should == [[0, 10], [1, 11], [2, 12]] + end + + it "passes given arguments to wrapped method" do + @infinite.to_enum(:each_slice, 2).map { |assoc| assoc.first * assoc.last }.first(4).should == [0, 6, 20, 42] + end + + it "used by some parent's methods though returning Lazy" do + { each_with_index: [], + with_index: [], + cycle: [1], + each_with_object: [Object.new], + with_object: [Object.new], + each_slice: [2], + each_entry: [], + each_cons: [2] + }.each_pair do |method, args| + @infinite.send(method, *args).should.instance_of?(Enumerator::Lazy) + end + end + + it "works with an infinite enumerable" do + s = 0..Float::INFINITY + s.lazy.to_enum(:with_index).first(100).should == + s.first(100).to_enum.to_enum(:with_index).to_a + end end diff --git a/spec/ruby/core/enumerator/shared/each.rb b/spec/ruby/core/enumerator/shared/each.rb new file mode 100644 index 00000000000000..18ca773207f3d5 --- /dev/null +++ b/spec/ruby/core/enumerator/shared/each.rb @@ -0,0 +1,46 @@ +# #each passes source-yielded values to the block by ordinary block arity +# (rb_yield_values2 semantics in CRuby), unlike the Enumerable collection methods +# which pack them via rb_enum_values_pack() (see enumerable/shared/value_packing.rb). +describe :enum_each, shared: true do + # @object must be set to a Proc that wraps an Enumerator into the receiver + # under test (e.g. -> e { e } for Enumerator#each, -> e { e.lazy } for Lazy#each). + describe "with a source that yields multiple values" do + before :each do + @enum = @object.call(Enumerator.new { |y| y.yield 1, 2; y.yield 3, 4 }) + end + + it "yields the first value to a single-argument block" do + collected = [] + @enum.each { |x| collected << x } + collected.should == [1, 3] + end + + it "yields each value to a multi-argument block" do + collected = [] + @enum.each { |x, y| collected << [x, y] } + collected.should == [[1, 2], [3, 4]] + end + + it "gathers the values for a splat block" do + collected = [] + @enum.each { |*args| collected << args } + collected.should == [[1, 2], [3, 4]] + end + end + + describe "with a source that yields a single value" do + it "yields the value to a single-argument block" do + collected = [] + @object.call(Enumerator.new { |y| y.yield 7; y.yield 8 }).each { |x| collected << x } + collected.should == [7, 8] + end + end + + describe "with a source that yields no value" do + it "yields nil to a single-argument block" do + collected = [] + @object.call(Enumerator.new { |y| y.yield; y.yield }).each { |x| collected << x } + collected.should == [nil, nil] + end + end +end diff --git a/spec/ruby/core/enumerator/shared/enum_for.rb b/spec/ruby/core/enumerator/shared/enum_for.rb deleted file mode 100644 index 4388103ecf409d..00000000000000 --- a/spec/ruby/core/enumerator/shared/enum_for.rb +++ /dev/null @@ -1,57 +0,0 @@ -describe :enum_for, shared: true do - it "is defined in Kernel" do - Kernel.method_defined?(@method).should == true - end - - it "returns a new enumerator" do - "abc".send(@method).should.instance_of?(Enumerator) - end - - it "defaults the first argument to :each" do - enum = [1,2].send(@method) - enum.map { |v| v }.should == [1,2].each { |v| v } - end - - it "sets regexp matches in the caller" do - "wawa".send(@method, :scan, /./).map {|o| $& }.should == ["w", "a", "w", "a"] - a = [] - "wawa".send(@method, :scan, /./).each {|o| a << $& } - a.should == ["w", "a", "w", "a"] - end - - it "exposes multi-arg yields as an array" do - o = Object.new - def o.each - yield :a - yield :b1, :b2 - yield [:c] - yield :d1, :d2 - yield :e1, :e2, :e3 - end - - enum = o.send(@method) - enum.next.should == :a - enum.next.should == [:b1, :b2] - enum.next.should == [:c] - enum.next.should == [:d1, :d2] - enum.next.should == [:e1, :e2, :e3] - end - - it "uses the passed block's value to calculate the size of the enumerator" do - Object.new.enum_for { 100 }.size.should == 100 - end - - it "defers the evaluation of the passed block until #size is called" do - ScratchPad.record [] - - enum = Object.new.enum_for do - ScratchPad << :called - 100 - end - - ScratchPad.recorded.should.empty? - - enum.size - ScratchPad.recorded.should == [:called] - end -end diff --git a/spec/ruby/core/enumerator/shared/with_object.rb b/spec/ruby/core/enumerator/shared/with_object.rb deleted file mode 100644 index 50d4f24eb34f63..00000000000000 --- a/spec/ruby/core/enumerator/shared/with_object.rb +++ /dev/null @@ -1,42 +0,0 @@ -require_relative '../../../spec_helper' - -describe :enum_with_object, shared: true do - before :each do - @enum = [:a, :b].to_enum - @memo = '' - @block_params = @enum.send(@method, @memo).to_a - end - - it "receives an argument" do - @enum.method(@method).arity.should == 1 - end - - context "with block" do - it "returns the given object" do - ret = @enum.send(@method, @memo) do |elm, memo| - # nothing - end - ret.should.equal?(@memo) - end - - context "the block parameter" do - it "passes each element to first parameter" do - @block_params[0][0].should.equal?(:a) - @block_params[1][0].should.equal?(:b) - end - - it "passes the given object to last parameter" do - @block_params[0][1].should.equal?(@memo) - @block_params[1][1].should.equal?(@memo) - end - end - end - - context "without block" do - it "returns new Enumerator" do - ret = @enum.send(@method, @memo) - ret.should.instance_of?(Enumerator) - ret.should_not.equal?(@enum) - end - end -end diff --git a/spec/ruby/core/enumerator/to_enum_spec.rb b/spec/ruby/core/enumerator/to_enum_spec.rb deleted file mode 100644 index 7fb73d0c3cdd6c..00000000000000 --- a/spec/ruby/core/enumerator/to_enum_spec.rb +++ /dev/null @@ -1,6 +0,0 @@ -require_relative '../../spec_helper' -require_relative 'shared/enum_for' - -describe "Enumerator#to_enum" do - it_behaves_like :enum_for, :to_enum -end diff --git a/spec/ruby/core/enumerator/with_object_spec.rb b/spec/ruby/core/enumerator/with_object_spec.rb index 58031fd765d5b1..790be66a11a1cd 100644 --- a/spec/ruby/core/enumerator/with_object_spec.rb +++ b/spec/ruby/core/enumerator/with_object_spec.rb @@ -1,6 +1,7 @@ require_relative '../../spec_helper' -require_relative 'shared/with_object' describe "Enumerator#with_object" do - it_behaves_like :enum_with_object, :with_object + it "is an alias of Enumerator#each_with_object" do + Enumerator.instance_method(:with_object).should == Enumerator.instance_method(:each_with_object) + end end diff --git a/spec/ruby/core/env/each_pair_spec.rb b/spec/ruby/core/env/each_pair_spec.rb index 2d7ed5faa064be..1acd2fbb00b2eb 100644 --- a/spec/ruby/core/env/each_pair_spec.rb +++ b/spec/ruby/core/env/each_pair_spec.rb @@ -1,6 +1,63 @@ require_relative 'spec_helper' -require_relative 'shared/each' +require_relative '../enumerable/shared/enumeratorized' describe "ENV.each_pair" do - it_behaves_like :env_each, :each_pair + it "returns each pair" do + orig = ENV.to_hash + e = [] + begin + ENV.clear + ENV["foo"] = "bar" + ENV["baz"] = "boo" + ENV.each_pair { |k, v| e << [k, v] }.should.equal?(ENV) + e.should.include?(["foo", "bar"]) + e.should.include?(["baz", "boo"]) + ensure + ENV.replace orig + end + end + + it "returns an Enumerator if called without a block" do + enum = ENV.each_pair + enum.should.instance_of?(Enumerator) + enum.each do |name, value| + ENV[name].should == value + end + end + + it_behaves_like :enumeratorized_with_origin_size, :each_pair, ENV + + describe "with encoding" do + before :each do + @external = Encoding.default_external + @internal = Encoding.default_internal + + Encoding.default_external = Encoding::BINARY + end + + after :each do + Encoding.default_external = @external + Encoding.default_internal = @internal + end + + it "uses the locale encoding when Encoding.default_internal is nil" do + Encoding.default_internal = nil + + ENV.each_pair do |key, value| + key.should.be_locale_env + value.should.be_locale_env + end + end + + it "transcodes from the locale encoding to Encoding.default_internal if set" do + Encoding.default_internal = internal = Encoding::IBM437 + + ENV.each_pair do |key, value| + key.encoding.should.equal?(internal) + if value.ascii_only? + value.encoding.should.equal?(internal) + end + end + end + end end diff --git a/spec/ruby/core/env/each_spec.rb b/spec/ruby/core/env/each_spec.rb index d1e06f55b6c33e..166a0b4fc87eaa 100644 --- a/spec/ruby/core/env/each_spec.rb +++ b/spec/ruby/core/env/each_spec.rb @@ -1,6 +1,7 @@ require_relative 'spec_helper' -require_relative 'shared/each' describe "ENV.each" do - it_behaves_like :env_each, :each + it "is an alias of ENV.each_pair" do + ENV.method(:each).should == ENV.method(:each_pair) + end end diff --git a/spec/ruby/core/env/element_set_spec.rb b/spec/ruby/core/env/element_set_spec.rb index 26dfee1ade7f64..ca5d468009a6f6 100644 --- a/spec/ruby/core/env/element_set_spec.rb +++ b/spec/ruby/core/env/element_set_spec.rb @@ -1,6 +1,62 @@ require_relative '../../spec_helper' -require_relative 'shared/store' describe "ENV.[]=" do - it_behaves_like :env_store, :[]= + before :each do + @saved_foo = ENV["foo"] + end + + after :each do + ENV["foo"] = @saved_foo + end + + it "sets the environment variable to the given value" do + ENV["foo"] = "bar" + ENV["foo"].should == "bar" + end + + it "returns the value" do + value = "bar" + ENV.send(:[]=, "foo", value).should.equal?(value) + end + + it "deletes the environment variable when the value is nil" do + ENV["foo"] = "bar" + ENV["foo"] = nil + ENV.key?("foo").should == false + end + + it "coerces the key argument with #to_str" do + k = mock("key") + k.should_receive(:to_str).and_return("foo") + ENV[k] = "bar" + ENV["foo"].should == "bar" + end + + it "coerces the value argument with #to_str" do + v = mock("value") + v.should_receive(:to_str).and_return("bar") + ENV["foo"] = v + ENV["foo"].should == "bar" + end + + it "raises TypeError when the key is not coercible to String" do + -> { ENV[Object.new] = "bar" }.should.raise(TypeError, "no implicit conversion of Object into String") + end + + it "raises TypeError when the value is not coercible to String" do + -> { ENV["foo"] = Object.new }.should.raise(TypeError, "no implicit conversion of Object into String") + end + + it "raises Errno::EINVAL when the key contains the '=' character" do + -> { ENV["foo="] = "bar" }.should.raise(Errno::EINVAL) + end + + it "raises Errno::EINVAL when the key is an empty string" do + -> { ENV[""] = "bar" }.should.raise(Errno::EINVAL) + end + + it "does nothing when the key is not a valid environment variable key and the value is nil" do + ENV["foo="] = nil + ENV.key?("foo=").should == false + end end diff --git a/spec/ruby/core/env/filter_spec.rb b/spec/ruby/core/env/filter_spec.rb index 52f8b79a0b51db..54997bfda152bf 100644 --- a/spec/ruby/core/env/filter_spec.rb +++ b/spec/ruby/core/env/filter_spec.rb @@ -1,13 +1,13 @@ require_relative '../../spec_helper' -require_relative '../enumerable/shared/enumeratorized' -require_relative 'shared/select' describe "ENV.filter!" do - it_behaves_like :env_select!, :filter! - it_behaves_like :enumeratorized_with_origin_size, :filter!, ENV + it "is an alias of ENV.select!" do + ENV.method(:filter!).should == ENV.method(:select!) + end end describe "ENV.filter" do - it_behaves_like :env_select, :filter - it_behaves_like :enumeratorized_with_origin_size, :filter, ENV + it "is an alias of ENV.select" do + ENV.method(:filter).should == ENV.method(:select) + end end diff --git a/spec/ruby/core/env/has_key_spec.rb b/spec/ruby/core/env/has_key_spec.rb index 798668105d0d90..7feeec8dfa6335 100644 --- a/spec/ruby/core/env/has_key_spec.rb +++ b/spec/ruby/core/env/has_key_spec.rb @@ -1,6 +1,7 @@ require_relative '../../spec_helper' -require_relative 'shared/include' describe "ENV.has_key?" do - it_behaves_like :env_include, :has_key? + it "is an alias of ENV.include?" do + ENV.method(:has_key?).should == ENV.method(:include?) + end end diff --git a/spec/ruby/core/env/has_value_spec.rb b/spec/ruby/core/env/has_value_spec.rb index a2bf3eb8779e42..b76ec0dc5db0a1 100644 --- a/spec/ruby/core/env/has_value_spec.rb +++ b/spec/ruby/core/env/has_value_spec.rb @@ -1,6 +1,7 @@ require_relative '../../spec_helper' -require_relative 'shared/value' describe "ENV.has_value?" do - it_behaves_like :env_value, :has_value? + it "is an alias of ENV.value?" do + ENV.method(:has_value?).should == ENV.method(:value?) + end end diff --git a/spec/ruby/core/env/include_spec.rb b/spec/ruby/core/env/include_spec.rb index 3975f095ac7532..8e68aa3bfd00ae 100644 --- a/spec/ruby/core/env/include_spec.rb +++ b/spec/ruby/core/env/include_spec.rb @@ -1,6 +1,32 @@ require_relative '../../spec_helper' -require_relative 'shared/include' describe "ENV.include?" do - it_behaves_like :env_include, :include? + before :each do + @saved_foo = ENV["foo"] + end + + after :each do + ENV["foo"] = @saved_foo + end + + it "returns true if ENV has the key" do + ENV["foo"] = "bar" + ENV.include?( "foo").should == true + end + + it "returns false if ENV doesn't include the key" do + ENV.delete("foo") + ENV.include?( "foo").should == false + end + + it "coerces the key with #to_str" do + ENV["foo"] = "bar" + k = mock('key') + k.should_receive(:to_str).and_return("foo") + ENV.include?( k).should == true + end + + it "raises TypeError if the argument is not a String and does not respond to #to_str" do + -> { ENV.include?( Object.new) }.should.raise(TypeError, "no implicit conversion of Object into String") + end end diff --git a/spec/ruby/core/env/key_spec.rb b/spec/ruby/core/env/key_spec.rb index 677cf35216ce52..56f5f609a73d82 100644 --- a/spec/ruby/core/env/key_spec.rb +++ b/spec/ruby/core/env/key_spec.rb @@ -1,8 +1,9 @@ require_relative '../../spec_helper' -require_relative 'shared/include' describe "ENV.key?" do - it_behaves_like :env_include, :key? + it "is an alias of ENV.include?" do + ENV.method(:key?).should == ENV.method(:include?) + end end describe "ENV.key" do diff --git a/spec/ruby/core/env/length_spec.rb b/spec/ruby/core/env/length_spec.rb index c6f90628921b0a..6baac6f7a4c9d6 100644 --- a/spec/ruby/core/env/length_spec.rb +++ b/spec/ruby/core/env/length_spec.rb @@ -1,6 +1,7 @@ require_relative '../../spec_helper' -require_relative 'shared/length' describe "ENV.length" do - it_behaves_like :env_length, :length + it "is an alias of ENV.size" do + ENV.method(:length).should == ENV.method(:size) + end end diff --git a/spec/ruby/core/env/member_spec.rb b/spec/ruby/core/env/member_spec.rb index 9119022ae5f536..f062d411901ebd 100644 --- a/spec/ruby/core/env/member_spec.rb +++ b/spec/ruby/core/env/member_spec.rb @@ -1,6 +1,7 @@ require_relative '../../spec_helper' -require_relative 'shared/include' describe "ENV.member?" do - it_behaves_like :env_include, :member? + it "is an alias of ENV.include?" do + ENV.method(:member?).should == ENV.method(:include?) + end end diff --git a/spec/ruby/core/env/merge_spec.rb b/spec/ruby/core/env/merge_spec.rb index f10662cf796b06..78231afbb2737b 100644 --- a/spec/ruby/core/env/merge_spec.rb +++ b/spec/ruby/core/env/merge_spec.rb @@ -1,6 +1,106 @@ require_relative '../../spec_helper' -require_relative 'shared/update' describe "ENV.merge!" do - it_behaves_like :env_update, :merge! + before :each do + @saved_foo = ENV["foo"] + @saved_bar = ENV["bar"] + end + + after :each do + ENV["foo"] = @saved_foo + ENV["bar"] = @saved_bar + end + + it "adds the parameter hash to ENV, returning ENV" do + ENV.merge!("foo" => "0", "bar" => "1").should.equal?(ENV) + ENV["foo"].should == "0" + ENV["bar"].should == "1" + end + + it "adds the multiple parameter hashes to ENV, returning ENV" do + ENV.merge!({"foo" => "multi1"}, {"bar" => "multi2"}).should.equal?(ENV) + ENV["foo"].should == "multi1" + ENV["bar"].should == "multi2" + end + + it "returns ENV when no block given" do + ENV.merge!({"foo" => "0", "bar" => "1"}).should.equal?(ENV) + end + + it "yields key, the old value and the new value when replacing an entry" do + ENV.merge!({"foo" => "0", "bar" => "3"}) + a = [] + ENV.merge!({"foo" => "1", "bar" => "4"}) do |key, old, new| + a << [key, old, new] + new + end + a[0].should == ["foo", "0", "1"] + a[1].should == ["bar", "3", "4"] + end + + it "yields key, the old value and the new value when replacing an entry" do + ENV.merge!({"foo" => "0", "bar" => "3"}) + ENV.merge!({"foo" => "1", "bar" => "4"}) do |key, old, new| + (new.to_i + 1).to_s + end + ENV["foo"].should == "2" + ENV["bar"].should == "5" + end + + # BUG: https://bugs.ruby-lang.org/issues/16192 + it "does not evaluate the block when the name is new" do + ENV.delete("bar") + ENV.merge!({"foo" => "0"}) + ENV.merge!("bar" => "1") { |key, old, new| fail "Should not get here" } + ENV["bar"].should == "1" + end + + # BUG: https://bugs.ruby-lang.org/issues/16192 + it "does not use the block's return value as the value when the name is new" do + ENV.delete("bar") + ENV.merge!({"foo" => "0"}) + ENV.merge!("bar" => "1") { |key, old, new| "Should not use this value" } + ENV["foo"].should == "0" + ENV["bar"].should == "1" + end + + it "returns ENV when block given" do + ENV.merge!({"foo" => "0", "bar" => "1"}){}.should.equal?(ENV) + end + + it "raises TypeError when a name is not coercible to String" do + -> { ENV.merge!(Object.new => "0") }.should.raise(TypeError, "no implicit conversion of Object into String") + end + + it "raises TypeError when a value is not coercible to String" do + -> { ENV.merge!("foo" => Object.new) }.should.raise(TypeError, "no implicit conversion of Object into String") + end + + it "raises Errno::EINVAL when a name contains the '=' character" do + -> { ENV.merge!("foo=" => "bar") }.should.raise(Errno::EINVAL) + end + + it "raises Errno::EINVAL when a name is an empty string" do + -> { ENV.merge!("" => "bar") }.should.raise(Errno::EINVAL) + end + + it "updates good data preceding an error" do + ENV["foo"] = "0" + begin + ENV.merge!({"foo" => "2", Object.new => "1"}) + rescue TypeError + ensure + ENV["foo"].should == "2" + end + end + + it "does not update good data following an error" do + ENV["foo"] = "0" + begin + ENV.merge!({Object.new => "1", "foo" => "2"}) + rescue TypeError + ensure + ENV["foo"].should == "0" + end + end end diff --git a/spec/ruby/core/env/select_spec.rb b/spec/ruby/core/env/select_spec.rb index c3a76f4434f23b..2b92d61551d590 100644 --- a/spec/ruby/core/env/select_spec.rb +++ b/spec/ruby/core/env/select_spec.rb @@ -1,13 +1,68 @@ require_relative '../../spec_helper' require_relative '../enumerable/shared/enumeratorized' -require_relative 'shared/select' describe "ENV.select!" do - it_behaves_like :env_select!, :select! it_behaves_like :enumeratorized_with_origin_size, :select!, ENV + + before :each do + @saved_foo = ENV["foo"] + end + + after :each do + ENV["foo"] = @saved_foo + end + + it "removes environment variables for which the block returns false" do + ENV["foo"] = "bar" + ENV.select! { |k, v| k != "foo" } + ENV["foo"].should == nil + end + + it "returns self if any changes were made" do + ENV["foo"] = "bar" + (ENV.select! { |k, v| k != "foo" }).should == ENV + end + + it "returns nil if no changes were made" do + (ENV.select! { true }).should == nil + end + + it "returns an Enumerator if called without a block" do + ENV.select!.should.instance_of?(Enumerator) + end + + it "selects via the enumerator" do + enum = ENV.select! + ENV["foo"] = "bar" + enum.each { |k, v| k != "foo" } + ENV["foo"].should == nil + end end describe "ENV.select" do - it_behaves_like :env_select, :select it_behaves_like :enumeratorized_with_origin_size, :select, ENV + + before :each do + @saved_foo = ENV["foo"] + end + + after :each do + ENV["foo"] = @saved_foo + end + + it "returns a Hash of names and values for which block returns true" do + ENV["foo"] = "bar" + (ENV.select { |k, v| k == "foo" }).should == { "foo" => "bar" } + end + + it "returns an Enumerator when no block is given" do + enum = ENV.select + enum.should.instance_of?(Enumerator) + end + + it "selects via the enumerator" do + enum = ENV.select + ENV["foo"] = "bar" + enum.each { |k, v| k == "foo" }.should == { "foo" => "bar"} + end end diff --git a/spec/ruby/core/env/shared/each.rb b/spec/ruby/core/env/shared/each.rb deleted file mode 100644 index 0661ca924cee22..00000000000000 --- a/spec/ruby/core/env/shared/each.rb +++ /dev/null @@ -1,65 +0,0 @@ -require_relative '../../enumerable/shared/enumeratorized' - -describe :env_each, shared: true do - it "returns each pair" do - orig = ENV.to_hash - e = [] - begin - ENV.clear - ENV["foo"] = "bar" - ENV["baz"] = "boo" - ENV.send(@method) { |k, v| e << [k, v] }.should.equal?(ENV) - e.should.include?(["foo", "bar"]) - e.should.include?(["baz", "boo"]) - ensure - ENV.replace orig - end - end - - it "returns an Enumerator if called without a block" do - enum = ENV.send(@method) - enum.should.instance_of?(Enumerator) - enum.each do |name, value| - ENV[name].should == value - end - end - - before :all do - @object = ENV - end - it_should_behave_like :enumeratorized_with_origin_size - - describe "with encoding" do - before :each do - @external = Encoding.default_external - @internal = Encoding.default_internal - - Encoding.default_external = Encoding::BINARY - end - - after :each do - Encoding.default_external = @external - Encoding.default_internal = @internal - end - - it "uses the locale encoding when Encoding.default_internal is nil" do - Encoding.default_internal = nil - - ENV.send(@method) do |key, value| - key.should.be_locale_env - value.should.be_locale_env - end - end - - it "transcodes from the locale encoding to Encoding.default_internal if set" do - Encoding.default_internal = internal = Encoding::IBM437 - - ENV.send(@method) do |key, value| - key.encoding.should.equal?(internal) - if value.ascii_only? - value.encoding.should.equal?(internal) - end - end - end - end -end diff --git a/spec/ruby/core/env/shared/include.rb b/spec/ruby/core/env/shared/include.rb deleted file mode 100644 index ceca02e3ebbc47..00000000000000 --- a/spec/ruby/core/env/shared/include.rb +++ /dev/null @@ -1,30 +0,0 @@ -describe :env_include, shared: true do - before :each do - @saved_foo = ENV["foo"] - end - - after :each do - ENV["foo"] = @saved_foo - end - - it "returns true if ENV has the key" do - ENV["foo"] = "bar" - ENV.send(@method, "foo").should == true - end - - it "returns false if ENV doesn't include the key" do - ENV.delete("foo") - ENV.send(@method, "foo").should == false - end - - it "coerces the key with #to_str" do - ENV["foo"] = "bar" - k = mock('key') - k.should_receive(:to_str).and_return("foo") - ENV.send(@method, k).should == true - end - - it "raises TypeError if the argument is not a String and does not respond to #to_str" do - -> { ENV.send(@method, Object.new) }.should.raise(TypeError, "no implicit conversion of Object into String") - end -end diff --git a/spec/ruby/core/env/shared/length.rb b/spec/ruby/core/env/shared/length.rb deleted file mode 100644 index 6d788a3f4a1288..00000000000000 --- a/spec/ruby/core/env/shared/length.rb +++ /dev/null @@ -1,13 +0,0 @@ -describe :env_length, shared: true do - it "returns the number of ENV entries" do - orig = ENV.to_hash - begin - ENV.clear - ENV["foo"] = "bar" - ENV["baz"] = "boo" - ENV.send(@method).should == 2 - ensure - ENV.replace orig - end - end -end diff --git a/spec/ruby/core/env/shared/select.rb b/spec/ruby/core/env/shared/select.rb deleted file mode 100644 index 8ec648a637f044..00000000000000 --- a/spec/ruby/core/env/shared/select.rb +++ /dev/null @@ -1,61 +0,0 @@ -describe :env_select, shared: true do - before :each do - @saved_foo = ENV["foo"] - end - - after :each do - ENV["foo"] = @saved_foo - end - - it "returns a Hash of names and values for which block return true" do - ENV["foo"] = "bar" - (ENV.send(@method) { |k, v| k == "foo" }).should == { "foo" => "bar" } - end - - it "returns an Enumerator when no block is given" do - enum = ENV.send(@method) - enum.should.instance_of?(Enumerator) - end - - it "selects via the enumerator" do - enum = ENV.send(@method) - ENV["foo"] = "bar" - enum.each { |k, v| k == "foo" }.should == { "foo" => "bar"} - end -end - -describe :env_select!, shared: true do - before :each do - @saved_foo = ENV["foo"] - end - - after :each do - ENV["foo"] = @saved_foo - end - - it "removes environment variables for which the block returns true" do - ENV["foo"] = "bar" - ENV.send(@method) { |k, v| k != "foo" } - ENV["foo"].should == nil - end - - it "returns self if any changes were made" do - ENV["foo"] = "bar" - (ENV.send(@method) { |k, v| k != "foo" }).should == ENV - end - - it "returns nil if no changes were made" do - (ENV.send(@method) { true }).should == nil - end - - it "returns an Enumerator if called without a block" do - ENV.send(@method).should.instance_of?(Enumerator) - end - - it "selects via the enumerator" do - enum = ENV.send(@method) - ENV["foo"] = "bar" - enum.each { |k, v| k != "foo" } - ENV["foo"].should == nil - end -end diff --git a/spec/ruby/core/env/shared/store.rb b/spec/ruby/core/env/shared/store.rb deleted file mode 100644 index 388208a8ac16a1..00000000000000 --- a/spec/ruby/core/env/shared/store.rb +++ /dev/null @@ -1,60 +0,0 @@ -describe :env_store, shared: true do - before :each do - @saved_foo = ENV["foo"] - end - - after :each do - ENV["foo"] = @saved_foo - end - - it "sets the environment variable to the given value" do - ENV.send(@method, "foo", "bar") - ENV["foo"].should == "bar" - end - - it "returns the value" do - value = "bar" - ENV.send(@method, "foo", value).should.equal?(value) - end - - it "deletes the environment variable when the value is nil" do - ENV["foo"] = "bar" - ENV.send(@method, "foo", nil) - ENV.key?("foo").should == false - end - - it "coerces the key argument with #to_str" do - k = mock("key") - k.should_receive(:to_str).and_return("foo") - ENV.send(@method, k, "bar") - ENV["foo"].should == "bar" - end - - it "coerces the value argument with #to_str" do - v = mock("value") - v.should_receive(:to_str).and_return("bar") - ENV.send(@method, "foo", v) - ENV["foo"].should == "bar" - end - - it "raises TypeError when the key is not coercible to String" do - -> { ENV.send(@method, Object.new, "bar") }.should.raise(TypeError, "no implicit conversion of Object into String") - end - - it "raises TypeError when the value is not coercible to String" do - -> { ENV.send(@method, "foo", Object.new) }.should.raise(TypeError, "no implicit conversion of Object into String") - end - - it "raises Errno::EINVAL when the key contains the '=' character" do - -> { ENV.send(@method, "foo=", "bar") }.should.raise(Errno::EINVAL) - end - - it "raises Errno::EINVAL when the key is an empty string" do - -> { ENV.send(@method, "", "bar") }.should.raise(Errno::EINVAL) - end - - it "does nothing when the key is not a valid environment variable key and the value is nil" do - ENV.send(@method, "foo=", nil) - ENV.key?("foo=").should == false - end -end diff --git a/spec/ruby/core/env/shared/update.rb b/spec/ruby/core/env/shared/update.rb deleted file mode 100644 index 112cc2505d095e..00000000000000 --- a/spec/ruby/core/env/shared/update.rb +++ /dev/null @@ -1,104 +0,0 @@ -describe :env_update, shared: true do - before :each do - @saved_foo = ENV["foo"] - @saved_bar = ENV["bar"] - end - - after :each do - ENV["foo"] = @saved_foo - ENV["bar"] = @saved_bar - end - - it "adds the parameter hash to ENV, returning ENV" do - ENV.send(@method, "foo" => "0", "bar" => "1").should.equal?(ENV) - ENV["foo"].should == "0" - ENV["bar"].should == "1" - end - - it "adds the multiple parameter hashes to ENV, returning ENV" do - ENV.send(@method, {"foo" => "multi1"}, {"bar" => "multi2"}).should.equal?(ENV) - ENV["foo"].should == "multi1" - ENV["bar"].should == "multi2" - end - - it "returns ENV when no block given" do - ENV.send(@method, {"foo" => "0", "bar" => "1"}).should.equal?(ENV) - end - - it "yields key, the old value and the new value when replacing an entry" do - ENV.send @method, {"foo" => "0", "bar" => "3"} - a = [] - ENV.send @method, {"foo" => "1", "bar" => "4"} do |key, old, new| - a << [key, old, new] - new - end - a[0].should == ["foo", "0", "1"] - a[1].should == ["bar", "3", "4"] - end - - it "yields key, the old value and the new value when replacing an entry" do - ENV.send @method, {"foo" => "0", "bar" => "3"} - ENV.send @method, {"foo" => "1", "bar" => "4"} do |key, old, new| - (new.to_i + 1).to_s - end - ENV["foo"].should == "2" - ENV["bar"].should == "5" - end - - # BUG: https://bugs.ruby-lang.org/issues/16192 - it "does not evaluate the block when the name is new" do - ENV.delete("bar") - ENV.send @method, {"foo" => "0"} - ENV.send(@method, "bar" => "1") { |key, old, new| fail "Should not get here" } - ENV["bar"].should == "1" - end - - # BUG: https://bugs.ruby-lang.org/issues/16192 - it "does not use the block's return value as the value when the name is new" do - ENV.delete("bar") - ENV.send @method, {"foo" => "0"} - ENV.send(@method, "bar" => "1") { |key, old, new| "Should not use this value" } - ENV["foo"].should == "0" - ENV["bar"].should == "1" - end - - it "returns ENV when block given" do - ENV.send(@method, {"foo" => "0", "bar" => "1"}){}.should.equal?(ENV) - end - - it "raises TypeError when a name is not coercible to String" do - -> { ENV.send @method, Object.new => "0" }.should.raise(TypeError, "no implicit conversion of Object into String") - end - - it "raises TypeError when a value is not coercible to String" do - -> { ENV.send @method, "foo" => Object.new }.should.raise(TypeError, "no implicit conversion of Object into String") - end - - it "raises Errno::EINVAL when a name contains the '=' character" do - -> { ENV.send(@method, "foo=" => "bar") }.should.raise(Errno::EINVAL) - end - - it "raises Errno::EINVAL when a name is an empty string" do - -> { ENV.send(@method, "" => "bar") }.should.raise(Errno::EINVAL) - end - - it "updates good data preceding an error" do - ENV["foo"] = "0" - begin - ENV.send @method, {"foo" => "2", Object.new => "1"} - rescue TypeError - ensure - ENV["foo"].should == "2" - end - end - - it "does not update good data following an error" do - ENV["foo"] = "0" - begin - ENV.send @method, {Object.new => "1", "foo" => "2"} - rescue TypeError - ensure - ENV["foo"].should == "0" - end - end -end diff --git a/spec/ruby/core/env/shared/value.rb b/spec/ruby/core/env/shared/value.rb deleted file mode 100644 index c2b5025465e258..00000000000000 --- a/spec/ruby/core/env/shared/value.rb +++ /dev/null @@ -1,29 +0,0 @@ -describe :env_value, shared: true do - before :each do - @saved_foo = ENV["foo"] - end - - after :each do - ENV["foo"] = @saved_foo - end - - it "returns true if ENV has the value" do - ENV["foo"] = "bar" - ENV.send(@method, "bar").should == true - end - - it "returns false if ENV doesn't have the value" do - ENV.send(@method, "foo").should == false - end - - it "coerces the value element with #to_str" do - ENV["foo"] = "bar" - v = mock('value') - v.should_receive(:to_str).and_return("bar") - ENV.send(@method, v).should == true - end - - it "returns nil if the argument is not a String and does not respond to #to_str" do - ENV.send(@method, Object.new).should == nil - end -end diff --git a/spec/ruby/core/env/size_spec.rb b/spec/ruby/core/env/size_spec.rb index 7c8072481ed96c..9c6de20df69ca3 100644 --- a/spec/ruby/core/env/size_spec.rb +++ b/spec/ruby/core/env/size_spec.rb @@ -1,6 +1,15 @@ require_relative '../../spec_helper' -require_relative 'shared/length' describe "ENV.size" do - it_behaves_like :env_length, :size + it "returns the number of ENV entries" do + orig = ENV.to_hash + begin + ENV.clear + ENV["foo"] = "bar" + ENV["baz"] = "boo" + ENV.size.should == 2 + ensure + ENV.replace orig + end + end end diff --git a/spec/ruby/core/env/store_spec.rb b/spec/ruby/core/env/store_spec.rb index b4700e0a965ea8..a5fd7e1e26f275 100644 --- a/spec/ruby/core/env/store_spec.rb +++ b/spec/ruby/core/env/store_spec.rb @@ -1,6 +1,7 @@ require_relative '../../spec_helper' -require_relative 'shared/store' describe "ENV.store" do - it_behaves_like :env_store, :store + it "is an alias of ENV.[]=" do + ENV.method(:store).should == ENV.method(:[]=) + end end diff --git a/spec/ruby/core/env/update_spec.rb b/spec/ruby/core/env/update_spec.rb index 95a8a2eb4954e1..44d05d617f496f 100644 --- a/spec/ruby/core/env/update_spec.rb +++ b/spec/ruby/core/env/update_spec.rb @@ -1,6 +1,7 @@ require_relative '../../spec_helper' -require_relative 'shared/update' describe "ENV.update" do - it_behaves_like :env_update, :update + it "is an alias of ENV.merge!" do + ENV.method(:update).should == ENV.method(:merge!) + end end diff --git a/spec/ruby/core/env/value_spec.rb b/spec/ruby/core/env/value_spec.rb index 906e86ab39a58a..c732cfbd15268f 100644 --- a/spec/ruby/core/env/value_spec.rb +++ b/spec/ruby/core/env/value_spec.rb @@ -1,6 +1,31 @@ require_relative '../../spec_helper' -require_relative 'shared/value' describe "ENV.value?" do - it_behaves_like :env_value, :value? + before :each do + @saved_foo = ENV["foo"] + end + + after :each do + ENV["foo"] = @saved_foo + end + + it "returns true if ENV has the value" do + ENV["foo"] = "bar" + ENV.value?("bar").should == true + end + + it "returns false if ENV doesn't have the value" do + ENV.value?("foo").should == false + end + + it "coerces the value element with #to_str" do + ENV["foo"] = "bar" + v = mock('value') + v.should_receive(:to_str).and_return("bar") + ENV.value?(v).should == true + end + + it "returns nil if the argument is not a String and does not respond to #to_str" do + ENV.value?(Object.new).should == nil + end end diff --git a/spec/ruby/core/false/inspect_spec.rb b/spec/ruby/core/false/inspect_spec.rb index 4cbb55d434296e..70f4aa0159be1f 100644 --- a/spec/ruby/core/false/inspect_spec.rb +++ b/spec/ruby/core/false/inspect_spec.rb @@ -1,7 +1,7 @@ require_relative '../../spec_helper' describe "FalseClass#inspect" do - it "returns the string 'false'" do - false.inspect.should == "false" + it "is an alias of FalseClass#to_s" do + false.method(:inspect).should == false.method(:to_s) end end diff --git a/spec/ruby/core/false/xor_spec.rb b/spec/ruby/core/false/xor_spec.rb index 1b87b9f412a0cc..d8432ca32620c9 100644 --- a/spec/ruby/core/false/xor_spec.rb +++ b/spec/ruby/core/false/xor_spec.rb @@ -1,11 +1,7 @@ require_relative '../../spec_helper' describe "FalseClass#^" do - it "returns false if other is nil or false, otherwise true" do - (false ^ false).should == false - (false ^ true).should == true - (false ^ nil).should == false - (false ^ "").should == true - (false ^ mock('x')).should == true + it "is an alias of FalseClass#|" do + false.method(:^).should == false.method(:|) end end diff --git a/spec/ruby/core/fiber/scheduler_spec.rb b/spec/ruby/core/fiber/scheduler_spec.rb index 15a03c147921aa..2a517ba93b270b 100644 --- a/spec/ruby/core/fiber/scheduler_spec.rb +++ b/spec/ruby/core/fiber/scheduler_spec.rb @@ -1,8 +1,5 @@ require_relative '../../spec_helper' -require_relative 'shared/scheduler' - -require "fiber" describe "Fiber.scheduler" do - it_behaves_like :scheduler, :scheduler + it "is already tested in Fiber.set_scheduler" end diff --git a/spec/ruby/core/fiber/set_scheduler_spec.rb b/spec/ruby/core/fiber/set_scheduler_spec.rb index 82f6acbe866f19..b34aff87343c82 100644 --- a/spec/ruby/core/fiber/set_scheduler_spec.rb +++ b/spec/ruby/core/fiber/set_scheduler_spec.rb @@ -1,8 +1,55 @@ require_relative '../../spec_helper' -require_relative 'shared/scheduler' require "fiber" describe "Fiber.scheduler" do - it_behaves_like :scheduler, :set_scheduler + it "validates the scheduler for required methods" do + required_methods = [:block, :unblock, :kernel_sleep, :io_wait] + required_methods.each do |missing_method| + scheduler = Object.new + required_methods.difference([missing_method]).each do |method| + scheduler.define_singleton_method(method) {} + end + -> { + suppress_warning { Fiber.set_scheduler(scheduler) } + }.should.raise(ArgumentError, /Scheduler must implement ##{missing_method}/) + end + end + + it "can set and get the scheduler" do + required_methods = [:block, :unblock, :kernel_sleep, :io_wait] + scheduler = Object.new + required_methods.each do |method| + scheduler.define_singleton_method(method) {} + end + suppress_warning { Fiber.set_scheduler(scheduler) } + Fiber.scheduler.should == scheduler + end + + it "returns the scheduler after setting it" do + required_methods = [:block, :unblock, :kernel_sleep, :io_wait] + scheduler = Object.new + required_methods.each do |method| + scheduler.define_singleton_method(method) {} + end + result = suppress_warning { Fiber.set_scheduler(scheduler) } + result.should == scheduler + end + + it "can remove the scheduler" do + required_methods = [:block, :unblock, :kernel_sleep, :io_wait] + scheduler = Object.new + required_methods.each do |method| + scheduler.define_singleton_method(method) {} + end + suppress_warning { Fiber.set_scheduler(scheduler) } + Fiber.set_scheduler(nil) + Fiber.scheduler.should == nil + end + + it "can assign a nil scheduler multiple times" do + Fiber.set_scheduler(nil) + Fiber.set_scheduler(nil) + Fiber.scheduler.should == nil + end end diff --git a/spec/ruby/core/fiber/shared/scheduler.rb b/spec/ruby/core/fiber/shared/scheduler.rb deleted file mode 100644 index 04bdded53a04bb..00000000000000 --- a/spec/ruby/core/fiber/shared/scheduler.rb +++ /dev/null @@ -1,51 +0,0 @@ -describe :scheduler, shared: true do - it "validates the scheduler for required methods" do - required_methods = [:block, :unblock, :kernel_sleep, :io_wait] - required_methods.each do |missing_method| - scheduler = Object.new - required_methods.difference([missing_method]).each do |method| - scheduler.define_singleton_method(method) {} - end - -> { - suppress_warning { Fiber.set_scheduler(scheduler) } - }.should.raise(ArgumentError, /Scheduler must implement ##{missing_method}/) - end - end - - it "can set and get the scheduler" do - required_methods = [:block, :unblock, :kernel_sleep, :io_wait] - scheduler = Object.new - required_methods.each do |method| - scheduler.define_singleton_method(method) {} - end - suppress_warning { Fiber.set_scheduler(scheduler) } - Fiber.scheduler.should == scheduler - end - - it "returns the scheduler after setting it" do - required_methods = [:block, :unblock, :kernel_sleep, :io_wait] - scheduler = Object.new - required_methods.each do |method| - scheduler.define_singleton_method(method) {} - end - result = suppress_warning { Fiber.set_scheduler(scheduler) } - result.should == scheduler - end - - it "can remove the scheduler" do - required_methods = [:block, :unblock, :kernel_sleep, :io_wait] - scheduler = Object.new - required_methods.each do |method| - scheduler.define_singleton_method(method) {} - end - suppress_warning { Fiber.set_scheduler(scheduler) } - Fiber.set_scheduler(nil) - Fiber.scheduler.should == nil - end - - it "can assign a nil scheduler multiple times" do - Fiber.set_scheduler(nil) - Fiber.set_scheduler(nil) - Fiber.scheduler.should == nil - end -end diff --git a/spec/ruby/core/file/delete_spec.rb b/spec/ruby/core/file/delete_spec.rb index 4098499942b2a5..7149b8a37d9250 100644 --- a/spec/ruby/core/file/delete_spec.rb +++ b/spec/ruby/core/file/delete_spec.rb @@ -1,6 +1,63 @@ require_relative '../../spec_helper' -require_relative 'shared/unlink' describe "File.delete" do - it_behaves_like :file_unlink, :delete + before :each do + @file1 = tmp('test.txt') + @file2 = tmp('test2.txt') + + touch @file1 + touch @file2 + end + + after :each do + File.delete(@file1) if File.exist?(@file1) + File.delete(@file2) if File.exist?(@file2) + + @file1 = nil + @file2 = nil + end + + it "returns 0 when called without arguments" do + File.delete.should == 0 + end + + it "deletes a single file" do + File.delete(@file1).should == 1 + File.should_not.exist?(@file1) + end + + it "deletes multiple files" do + File.delete(@file1, @file2).should == 2 + File.should_not.exist?(@file1) + File.should_not.exist?(@file2) + end + + it "raises a TypeError if not passed a String type" do + -> { File.delete(1) }.should.raise(TypeError) + end + + it "raises an Errno::ENOENT when the given file doesn't exist" do + -> { File.delete('bogus') }.should.raise(Errno::ENOENT) + end + + it "coerces a given parameter into a string if possible" do + mock = mock("to_str") + mock.should_receive(:to_str).and_return(@file1) + File.delete(mock).should == 1 + end + + it "accepts an object that has a #to_path method" do + File.delete(mock_to_path(@file1)).should == 1 + end + + platform_is :windows do + it "allows deleting an open file with File::SHARE_DELETE" do + path = tmp("share_delete.txt") + File.open(path, mode: File::CREAT | File::WRONLY | File::BINARY | File::SHARE_DELETE) do |f| + File.should.exist?(path) + File.delete(path) + end + File.should_not.exist?(path) + end + end end diff --git a/spec/ruby/core/file/fnmatch_spec.rb b/spec/ruby/core/file/fnmatch_spec.rb index a1b7fa12b3955c..44a143bddcd4d9 100644 --- a/spec/ruby/core/file/fnmatch_spec.rb +++ b/spec/ruby/core/file/fnmatch_spec.rb @@ -1,10 +1,302 @@ require_relative '../../spec_helper' -require_relative 'shared/fnmatch' describe "File.fnmatch" do - it_behaves_like :file_fnmatch, :fnmatch + it "matches entire strings" do + File.fnmatch('cat', 'cat').should == true + end + + it "does not match partial strings" do + File.fnmatch('cat', 'category').should == false + end + + it "does not support { } patterns by default" do + File.fnmatch('c{at,ub}s', 'cats').should == false + File.fnmatch('c{at,ub}s', 'c{at,ub}s').should == true + end + + it "supports some { } patterns when File::FNM_EXTGLOB is passed" do + File.fnmatch("{a,b}", "a", File::FNM_EXTGLOB).should == true + File.fnmatch("{a,b}", "b", File::FNM_EXTGLOB).should == true + File.fnmatch("c{at,ub}s", "cats", File::FNM_EXTGLOB).should == true + File.fnmatch("c{at,ub}s", "cubs", File::FNM_EXTGLOB).should == true + File.fnmatch("-c{at,ub}s-", "-cats-", File::FNM_EXTGLOB).should == true + File.fnmatch("-c{at,ub}s-", "-cubs-", File::FNM_EXTGLOB).should == true + File.fnmatch("{a,b,c}{d,e,f}{g,h}", "adg", File::FNM_EXTGLOB).should == true + File.fnmatch("{a,b,c}{d,e,f}{g,h}", "bdg", File::FNM_EXTGLOB).should == true + File.fnmatch("{a,b,c}{d,e,f}{g,h}", "ceh", File::FNM_EXTGLOB).should == true + File.fnmatch("{aa,bb,cc,dd}", "aa", File::FNM_EXTGLOB).should == true + File.fnmatch("{aa,bb,cc,dd}", "bb", File::FNM_EXTGLOB).should == true + File.fnmatch("{aa,bb,cc,dd}", "cc", File::FNM_EXTGLOB).should == true + File.fnmatch("{aa,bb,cc,dd}", "dd", File::FNM_EXTGLOB).should == true + File.fnmatch("{1,5{a,b{c,d}}}", "1", File::FNM_EXTGLOB).should == true + File.fnmatch("{1,5{a,b{c,d}}}", "5a", File::FNM_EXTGLOB).should == true + File.fnmatch("{1,5{a,b{c,d}}}", "5bc", File::FNM_EXTGLOB).should == true + File.fnmatch("{1,5{a,b{c,d}}}", "5bd", File::FNM_EXTGLOB).should == true + File.fnmatch("\\\\{a\\,b,b\\}c}", "\\a,b", File::FNM_EXTGLOB).should == true + File.fnmatch("\\\\{a\\,b,b\\}c}", "\\b}c", File::FNM_EXTGLOB).should == true + end + + it "doesn't support some { } patterns even when File::FNM_EXTGLOB is passed" do + File.fnmatch("a{0..3}b", "a0b", File::FNM_EXTGLOB).should == false + File.fnmatch("a{0..3}b", "a1b", File::FNM_EXTGLOB).should == false + File.fnmatch("a{0..3}b", "a2b", File::FNM_EXTGLOB).should == false + File.fnmatch("a{0..3}b", "a3b", File::FNM_EXTGLOB).should == false + File.fnmatch("{0..12}", "0", File::FNM_EXTGLOB).should == false + File.fnmatch("{0..12}", "6", File::FNM_EXTGLOB).should == false + File.fnmatch("{0..12}", "12", File::FNM_EXTGLOB).should == false + File.fnmatch("{3..-2}", "3", File::FNM_EXTGLOB).should == false + File.fnmatch("{3..-2}", "0", File::FNM_EXTGLOB).should == false + File.fnmatch("{3..-2}", "-2", File::FNM_EXTGLOB).should == false + File.fnmatch("{a..g}", "a", File::FNM_EXTGLOB).should == false + File.fnmatch("{a..g}", "d", File::FNM_EXTGLOB).should == false + File.fnmatch("{a..g}", "g", File::FNM_EXTGLOB).should == false + File.fnmatch("{g..a}", "a", File::FNM_EXTGLOB).should == false + File.fnmatch("{g..a}", "d", File::FNM_EXTGLOB).should == false + File.fnmatch("{g..a}", "g", File::FNM_EXTGLOB).should == false + File.fnmatch("escaping: {{,\\,,\\},\\{}", "escaping: {", File::FNM_EXTGLOB).should == false + File.fnmatch("escaping: {{,\\,,\\},\\{}", "escaping: ,", File::FNM_EXTGLOB).should == false + File.fnmatch("escaping: {{,\\,,\\},\\{}", "escaping: }", File::FNM_EXTGLOB).should == false + File.fnmatch("escaping: {{,\\,,\\},\\{}", "escaping: {", File::FNM_EXTGLOB).should == false + end + + it "doesn't match an extra } when File::FNM_EXTGLOB is passed" do + File.fnmatch('c{at,ub}}s', 'cats', File::FNM_EXTGLOB).should == false + end + + it "matches when both FNM_EXTGLOB and FNM_PATHNAME are passed" do + File.fnmatch("?.md", "a.md", File::FNM_EXTGLOB | File::FNM_PATHNAME).should == true + end + + it "matches a single character for each ? character" do + File.fnmatch('c?t', 'cat').should == true + File.fnmatch('c??t', 'cat').should == false + end + + it "matches zero or more characters for each * character" do + File.fnmatch('c*', 'cats').should == true + File.fnmatch('c*t', 'c/a/b/t').should == true + end + + it "does not match unterminated range of characters" do + File.fnmatch('abc[de', 'abcd').should == false + end + + it "does not match unterminated range of characters as a literal" do + File.fnmatch('abc[de', 'abc[de').should == false + end + + it "matches ranges of characters using bracket expression (e.g. [a-z])" do + File.fnmatch('ca[a-z]', 'cat').should == true + end + + it "matches ranges of characters using bracket expression, taking case into account" do + File.fnmatch('[a-z]', 'D').should == false + File.fnmatch('[^a-z]', 'D').should == true + File.fnmatch('[A-Z]', 'd').should == false + File.fnmatch('[^A-Z]', 'd').should == true + File.fnmatch('[a-z]', 'D', File::FNM_CASEFOLD).should == true + end + + it "does not match characters outside of the range of the bracket expression" do + File.fnmatch('ca[x-z]', 'cat').should == false + File.fnmatch('/ca[s][s-t]/rul[a-b]/[z]he/[x-Z]orld', '/cats/rule/the/World').should == false + end + + it "matches ranges of characters using exclusive bracket expression (e.g. [^t] or [!t])" do + File.fnmatch('ca[^t]', 'cat').should == false + File.fnmatch('ca[^t]', 'cas').should == true + File.fnmatch('ca[!t]', 'cat').should == false + end + + it "matches characters with a case sensitive comparison" do + File.fnmatch('cat', 'CAT').should == false + end + + it "matches characters with case insensitive comparison when flags includes FNM_CASEFOLD" do + File.fnmatch('cat', 'CAT', File::FNM_CASEFOLD).should == true + end + + platform_is_not :windows do + it "doesn't match case sensitive characters on platforms with case sensitive paths, when flags include FNM_SYSCASE" do + File.fnmatch('cat', 'CAT', File::FNM_SYSCASE).should == false + end + end + + platform_is :windows do + it "matches case sensitive characters on platforms with case insensitive paths, when flags include FNM_SYSCASE" do + File.fnmatch('cat', 'CAT', File::FNM_SYSCASE).should == true + end + end + + it "matches wildcard with characters when flags includes FNM_PATHNAME" do + File.fnmatch('*a', 'aa', File::FNM_PATHNAME).should == true + File.fnmatch('a*', 'aa', File::FNM_PATHNAME).should == true + File.fnmatch('a*', 'aaa', File::FNM_PATHNAME).should == true + File.fnmatch('*a', 'aaa', File::FNM_PATHNAME).should == true + end + + it "does not match '/' characters with ? or * when flags includes FNM_PATHNAME" do + File.fnmatch('?', '/', File::FNM_PATHNAME).should == false + File.fnmatch('*', '/', File::FNM_PATHNAME).should == false + end + + it "does not match '/' characters inside bracket expressions when flags includes FNM_PATHNAME" do + File.fnmatch('[/]', '/', File::FNM_PATHNAME).should == false + end + + it "matches literal ? or * in path when pattern includes \\? or \\*" do + File.fnmatch('\?', '?').should == true + File.fnmatch('\?', 'a').should == false + + File.fnmatch('\*', '*').should == true + File.fnmatch('\*', 'a').should == false + end + + it "matches literal character (e.g. 'a') in path when pattern includes escaped character (e.g. \\a)" do + File.fnmatch('\a', 'a').should == true + File.fnmatch('this\b', 'thisb').should == true + end + + it "matches '\\' characters in path when flags includes FNM_NOESCAPE" do + File.fnmatch('\a', '\a', File::FNM_NOESCAPE).should == true + File.fnmatch('\a', 'a', File::FNM_NOESCAPE).should == false + File.fnmatch('\[foo\]\[bar\]', '[foo][bar]', File::FNM_NOESCAPE).should == false + end + + it "escapes special characters inside bracket expression" do + File.fnmatch('[\?]', '?').should == true + File.fnmatch('[\*]', '*').should == true + end + + it "does not match leading periods in filenames with wildcards by default" do + File.should_not.fnmatch('*', '.profile') + File.should.fnmatch('*', 'home/.profile') + File.should.fnmatch('*/*', 'home/.profile') + File.should_not.fnmatch('*/*', 'dave/.profile', File::FNM_PATHNAME) + end + + it "matches patterns with leading periods to dotfiles" do + File.fnmatch('.*', '.profile').should == true + File.fnmatch('.*', '.profile', File::FNM_PATHNAME).should == true + File.fnmatch(".*file", "nondotfile").should == false + File.fnmatch(".*file", "nondotfile", File::FNM_PATHNAME).should == false + end + + it "does not match directories with leading periods by default with FNM_PATHNAME" do + File.fnmatch('.*', '.directory/nondotfile', File::FNM_PATHNAME).should == false + File.fnmatch('.*', '.directory/.profile', File::FNM_PATHNAME).should == false + File.fnmatch('.*', 'foo/.directory/nondotfile', File::FNM_PATHNAME).should == false + File.fnmatch('.*', 'foo/.directory/.profile', File::FNM_PATHNAME).should == false + File.fnmatch('**/.dotfile', '.dotsubdir/.dotfile', File::FNM_PATHNAME).should == false + end + + it "matches leading periods in filenames when flags includes FNM_DOTMATCH" do + File.fnmatch('*', '.profile', File::FNM_DOTMATCH).should == true + File.fnmatch('*', 'home/.profile', File::FNM_DOTMATCH).should == true + end + + it "matches multiple directories with ** and *" do + files = '**/*.rb' + File.fnmatch(files, 'main.rb').should == false + File.fnmatch(files, './main.rb').should == false + File.fnmatch(files, 'lib/song.rb').should == true + File.fnmatch('**.rb', 'main.rb').should == true + File.fnmatch('**.rb', './main.rb').should == false + File.fnmatch('**.rb', 'lib/song.rb').should == true + File.fnmatch('*', 'dave/.profile').should == true + end + + it "matches multiple directories with ** when flags includes File::FNM_PATHNAME" do + files = '**/*.rb' + flags = File::FNM_PATHNAME + + File.fnmatch(files, 'main.rb', flags).should == true + File.fnmatch(files, 'one/two/three/main.rb', flags).should == true + File.fnmatch(files, './main.rb', flags).should == false + + flags = File::FNM_PATHNAME | File::FNM_DOTMATCH + + File.fnmatch(files, './main.rb', flags).should == true + File.fnmatch(files, 'one/two/.main.rb', flags).should == true + + File.fnmatch("**/best/*", 'lib/my/best/song.rb').should == true + end + + it "returns false if '/' in pattern do not match '/' in path when flags includes FNM_PATHNAME" do + pattern = '*/*' + File.fnmatch(pattern, 'dave/.profile', File::FNM_PATHNAME).should == false + + pattern = '**/foo' + File.fnmatch(pattern, 'a/.b/c/foo', File::FNM_PATHNAME).should == false + end + + it "returns true if '/' in pattern match '/' in path when flags includes FNM_PATHNAME" do + pattern = '*/*' + File.fnmatch(pattern, 'dave/.profile', File::FNM_PATHNAME | File::FNM_DOTMATCH).should == true + + pattern = '**/foo' + File.fnmatch(pattern, 'a/b/c/foo', File::FNM_PATHNAME).should == true + File.fnmatch(pattern, '/a/b/c/foo', File::FNM_PATHNAME).should == true + File.fnmatch(pattern, 'c:/a/b/c/foo', File::FNM_PATHNAME).should == true + File.fnmatch(pattern, 'a/.b/c/foo', File::FNM_PATHNAME | File::FNM_DOTMATCH).should == true + end + + it "has special handling for ./ when using * and FNM_PATHNAME" do + File.fnmatch('./*', '.', File::FNM_PATHNAME).should == false + File.fnmatch('./*', './', File::FNM_PATHNAME).should == true + File.fnmatch('./*/', './', File::FNM_PATHNAME).should == false + File.fnmatch('./**', './', File::FNM_PATHNAME).should == true + File.fnmatch('./**/', './', File::FNM_PATHNAME).should == true + File.fnmatch('./*', '.', File::FNM_PATHNAME | File::FNM_DOTMATCH).should == false + File.fnmatch('./*', './', File::FNM_PATHNAME | File::FNM_DOTMATCH).should == true + File.fnmatch('./*/', './', File::FNM_PATHNAME | File::FNM_DOTMATCH).should == false + File.fnmatch('./**', './', File::FNM_PATHNAME | File::FNM_DOTMATCH).should == true + File.fnmatch('./**/', './', File::FNM_PATHNAME | File::FNM_DOTMATCH).should == true + end + + it "matches **/* with FNM_PATHNAME to recurse directories" do + File.fnmatch('nested/**/*', 'nested/subdir', File::FNM_PATHNAME).should == true + File.fnmatch('nested/**/*', 'nested/subdir/file', File::FNM_PATHNAME).should == true + File.fnmatch('nested/**/*', 'nested/.dotsubdir', File::FNM_PATHNAME | File::FNM_DOTMATCH).should == true + File.fnmatch('nested/**/*', 'nested/.dotsubir/.dotfile', File::FNM_PATHNAME | File::FNM_DOTMATCH).should == true + end + + it "matches ** with FNM_PATHNAME only in current directory" do + File.fnmatch('nested/**', 'nested/subdir', File::FNM_PATHNAME).should == true + File.fnmatch('nested/**', 'nested/subdir/file', File::FNM_PATHNAME).should == false + File.fnmatch('nested/**', 'nested/.dotsubdir', File::FNM_PATHNAME | File::FNM_DOTMATCH).should == true + File.fnmatch('nested/**', 'nested/.dotsubir/.dotfile', File::FNM_PATHNAME | File::FNM_DOTMATCH).should == false + end + + it "accepts an object that has a #to_path method" do + File.fnmatch('\*', mock_to_path('a')).should == false + end + + it "raises a TypeError if the first and second arguments are not string-like" do + -> { File.fnmatch(nil, nil, 0, 0) }.should.raise(ArgumentError) + -> { File.fnmatch(1, 'some/thing') }.should.raise(TypeError) + -> { File.fnmatch('some/thing', 1) }.should.raise(TypeError) + -> { File.fnmatch(1, 1) }.should.raise(TypeError) + end + + it "raises a TypeError if the third argument is not an Integer" do + -> { File.fnmatch("*/place", "path/to/file", "flags") }.should.raise(TypeError) + -> { File.fnmatch("*/place", "path/to/file", nil) }.should.raise(TypeError) + end + + it "does not raise a TypeError if the third argument can be coerced to an Integer" do + flags = mock("flags") + flags.should_receive(:to_int).and_return(10) + -> { File.fnmatch("*/place", "path/to/file", flags) }.should_not.raise + end + + it "matches multibyte characters" do + File.fnmatch("*/ä/ø/ñ", "a/ä/ø/ñ").should == true + end end describe "File.fnmatch?" do - it_behaves_like :file_fnmatch, :fnmatch? + it "is an alias of File.fnmatch" do + File.method(:fnmatch?).should == File.method(:fnmatch) + end end diff --git a/spec/ruby/core/file/path_spec.rb b/spec/ruby/core/file/path_spec.rb index ce78dbeede3d9b..f3b9b56dbe1bfa 100644 --- a/spec/ruby/core/file/path_spec.rb +++ b/spec/ruby/core/file/path_spec.rb @@ -1,8 +1,9 @@ require_relative '../../spec_helper' -require_relative 'shared/path' describe "File#path" do - it_behaves_like :file_path, :path + it "is an alias of File#to_path" do + File.instance_method(:path).should == File.instance_method(:to_path) + end end describe "File.path" do diff --git a/spec/ruby/core/file/shared/fnmatch.rb b/spec/ruby/core/file/shared/fnmatch.rb deleted file mode 100644 index b9140d027d2320..00000000000000 --- a/spec/ruby/core/file/shared/fnmatch.rb +++ /dev/null @@ -1,294 +0,0 @@ -describe :file_fnmatch, shared: true do - it "matches entire strings" do - File.send(@method, 'cat', 'cat').should == true - end - - it "does not match partial strings" do - File.send(@method, 'cat', 'category').should == false - end - - it "does not support { } patterns by default" do - File.send(@method, 'c{at,ub}s', 'cats').should == false - File.send(@method, 'c{at,ub}s', 'c{at,ub}s').should == true - end - - it "supports some { } patterns when File::FNM_EXTGLOB is passed" do - File.send(@method, "{a,b}", "a", File::FNM_EXTGLOB).should == true - File.send(@method, "{a,b}", "b", File::FNM_EXTGLOB).should == true - File.send(@method, "c{at,ub}s", "cats", File::FNM_EXTGLOB).should == true - File.send(@method, "c{at,ub}s", "cubs", File::FNM_EXTGLOB).should == true - File.send(@method, "-c{at,ub}s-", "-cats-", File::FNM_EXTGLOB).should == true - File.send(@method, "-c{at,ub}s-", "-cubs-", File::FNM_EXTGLOB).should == true - File.send(@method, "{a,b,c}{d,e,f}{g,h}", "adg", File::FNM_EXTGLOB).should == true - File.send(@method, "{a,b,c}{d,e,f}{g,h}", "bdg", File::FNM_EXTGLOB).should == true - File.send(@method, "{a,b,c}{d,e,f}{g,h}", "ceh", File::FNM_EXTGLOB).should == true - File.send(@method, "{aa,bb,cc,dd}", "aa", File::FNM_EXTGLOB).should == true - File.send(@method, "{aa,bb,cc,dd}", "bb", File::FNM_EXTGLOB).should == true - File.send(@method, "{aa,bb,cc,dd}", "cc", File::FNM_EXTGLOB).should == true - File.send(@method, "{aa,bb,cc,dd}", "dd", File::FNM_EXTGLOB).should == true - File.send(@method, "{1,5{a,b{c,d}}}", "1", File::FNM_EXTGLOB).should == true - File.send(@method, "{1,5{a,b{c,d}}}", "5a", File::FNM_EXTGLOB).should == true - File.send(@method, "{1,5{a,b{c,d}}}", "5bc", File::FNM_EXTGLOB).should == true - File.send(@method, "{1,5{a,b{c,d}}}", "5bd", File::FNM_EXTGLOB).should == true - File.send(@method, "\\\\{a\\,b,b\\}c}", "\\a,b", File::FNM_EXTGLOB).should == true - File.send(@method, "\\\\{a\\,b,b\\}c}", "\\b}c", File::FNM_EXTGLOB).should == true - end - - it "doesn't support some { } patterns even when File::FNM_EXTGLOB is passed" do - File.send(@method, "a{0..3}b", "a0b", File::FNM_EXTGLOB).should == false - File.send(@method, "a{0..3}b", "a1b", File::FNM_EXTGLOB).should == false - File.send(@method, "a{0..3}b", "a2b", File::FNM_EXTGLOB).should == false - File.send(@method, "a{0..3}b", "a3b", File::FNM_EXTGLOB).should == false - File.send(@method, "{0..12}", "0", File::FNM_EXTGLOB).should == false - File.send(@method, "{0..12}", "6", File::FNM_EXTGLOB).should == false - File.send(@method, "{0..12}", "12", File::FNM_EXTGLOB).should == false - File.send(@method, "{3..-2}", "3", File::FNM_EXTGLOB).should == false - File.send(@method, "{3..-2}", "0", File::FNM_EXTGLOB).should == false - File.send(@method, "{3..-2}", "-2", File::FNM_EXTGLOB).should == false - File.send(@method, "{a..g}", "a", File::FNM_EXTGLOB).should == false - File.send(@method, "{a..g}", "d", File::FNM_EXTGLOB).should == false - File.send(@method, "{a..g}", "g", File::FNM_EXTGLOB).should == false - File.send(@method, "{g..a}", "a", File::FNM_EXTGLOB).should == false - File.send(@method, "{g..a}", "d", File::FNM_EXTGLOB).should == false - File.send(@method, "{g..a}", "g", File::FNM_EXTGLOB).should == false - File.send(@method, "escaping: {{,\\,,\\},\\{}", "escaping: {", File::FNM_EXTGLOB).should == false - File.send(@method, "escaping: {{,\\,,\\},\\{}", "escaping: ,", File::FNM_EXTGLOB).should == false - File.send(@method, "escaping: {{,\\,,\\},\\{}", "escaping: }", File::FNM_EXTGLOB).should == false - File.send(@method, "escaping: {{,\\,,\\},\\{}", "escaping: {", File::FNM_EXTGLOB).should == false - end - - it "doesn't match an extra } when File::FNM_EXTGLOB is passed" do - File.send(@method, 'c{at,ub}}s', 'cats', File::FNM_EXTGLOB).should == false - end - - it "matches when both FNM_EXTGLOB and FNM_PATHNAME are passed" do - File.send(@method, "?.md", "a.md", File::FNM_EXTGLOB | File::FNM_PATHNAME).should == true - end - - it "matches a single character for each ? character" do - File.send(@method, 'c?t', 'cat').should == true - File.send(@method, 'c??t', 'cat').should == false - end - - it "matches zero or more characters for each * character" do - File.send(@method, 'c*', 'cats').should == true - File.send(@method, 'c*t', 'c/a/b/t').should == true - end - - it "does not match unterminated range of characters" do - File.send(@method, 'abc[de', 'abcd').should == false - end - - it "does not match unterminated range of characters as a literal" do - File.send(@method, 'abc[de', 'abc[de').should == false - end - - it "matches ranges of characters using bracket expression (e.g. [a-z])" do - File.send(@method, 'ca[a-z]', 'cat').should == true - end - - it "matches ranges of characters using bracket expression, taking case into account" do - File.send(@method, '[a-z]', 'D').should == false - File.send(@method, '[^a-z]', 'D').should == true - File.send(@method, '[A-Z]', 'd').should == false - File.send(@method, '[^A-Z]', 'd').should == true - File.send(@method, '[a-z]', 'D', File::FNM_CASEFOLD).should == true - end - - it "does not match characters outside of the range of the bracket expression" do - File.send(@method, 'ca[x-z]', 'cat').should == false - File.send(@method, '/ca[s][s-t]/rul[a-b]/[z]he/[x-Z]orld', '/cats/rule/the/World').should == false - end - - it "matches ranges of characters using exclusive bracket expression (e.g. [^t] or [!t])" do - File.send(@method, 'ca[^t]', 'cat').should == false - File.send(@method, 'ca[^t]', 'cas').should == true - File.send(@method, 'ca[!t]', 'cat').should == false - end - - it "matches characters with a case sensitive comparison" do - File.send(@method, 'cat', 'CAT').should == false - end - - it "matches characters with case insensitive comparison when flags includes FNM_CASEFOLD" do - File.send(@method, 'cat', 'CAT', File::FNM_CASEFOLD).should == true - end - - platform_is_not :windows do - it "doesn't match case sensitive characters on platforms with case sensitive paths, when flags include FNM_SYSCASE" do - File.send(@method, 'cat', 'CAT', File::FNM_SYSCASE).should == false - end - end - - platform_is :windows do - it "matches case sensitive characters on platforms with case insensitive paths, when flags include FNM_SYSCASE" do - File.send(@method, 'cat', 'CAT', File::FNM_SYSCASE).should == true - end - end - - it "matches wildcard with characters when flags includes FNM_PATHNAME" do - File.send(@method, '*a', 'aa', File::FNM_PATHNAME).should == true - File.send(@method, 'a*', 'aa', File::FNM_PATHNAME).should == true - File.send(@method, 'a*', 'aaa', File::FNM_PATHNAME).should == true - File.send(@method, '*a', 'aaa', File::FNM_PATHNAME).should == true - end - - it "does not match '/' characters with ? or * when flags includes FNM_PATHNAME" do - File.send(@method, '?', '/', File::FNM_PATHNAME).should == false - File.send(@method, '*', '/', File::FNM_PATHNAME).should == false - end - - it "does not match '/' characters inside bracket expressions when flags includes FNM_PATHNAME" do - File.send(@method, '[/]', '/', File::FNM_PATHNAME).should == false - end - - it "matches literal ? or * in path when pattern includes \\? or \\*" do - File.send(@method, '\?', '?').should == true - File.send(@method, '\?', 'a').should == false - - File.send(@method, '\*', '*').should == true - File.send(@method, '\*', 'a').should == false - end - - it "matches literal character (e.g. 'a') in path when pattern includes escaped character (e.g. \\a)" do - File.send(@method, '\a', 'a').should == true - File.send(@method, 'this\b', 'thisb').should == true - end - - it "matches '\\' characters in path when flags includes FNM_NOESACPE" do - File.send(@method, '\a', '\a', File::FNM_NOESCAPE).should == true - File.send(@method, '\a', 'a', File::FNM_NOESCAPE).should == false - File.send(@method, '\[foo\]\[bar\]', '[foo][bar]', File::FNM_NOESCAPE).should == false - end - - it "escapes special characters inside bracket expression" do - File.send(@method, '[\?]', '?').should == true - File.send(@method, '[\*]', '*').should == true - end - - it "does not match leading periods in filenames with wildcards by default" do - File.should_not.send(@method, '*', '.profile') - File.should.send(@method, '*', 'home/.profile') - File.should.send(@method, '*/*', 'home/.profile') - File.should_not.send(@method, '*/*', 'dave/.profile', File::FNM_PATHNAME) - end - - it "matches patterns with leading periods to dotfiles" do - File.send(@method, '.*', '.profile').should == true - File.send(@method, '.*', '.profile', File::FNM_PATHNAME).should == true - File.send(@method, ".*file", "nondotfile").should == false - File.send(@method, ".*file", "nondotfile", File::FNM_PATHNAME).should == false - end - - it "does not match directories with leading periods by default with FNM_PATHNAME" do - File.send(@method, '.*', '.directory/nondotfile', File::FNM_PATHNAME).should == false - File.send(@method, '.*', '.directory/.profile', File::FNM_PATHNAME).should == false - File.send(@method, '.*', 'foo/.directory/nondotfile', File::FNM_PATHNAME).should == false - File.send(@method, '.*', 'foo/.directory/.profile', File::FNM_PATHNAME).should == false - File.send(@method, '**/.dotfile', '.dotsubdir/.dotfile', File::FNM_PATHNAME).should == false - end - - it "matches leading periods in filenames when flags includes FNM_DOTMATCH" do - File.send(@method, '*', '.profile', File::FNM_DOTMATCH).should == true - File.send(@method, '*', 'home/.profile', File::FNM_DOTMATCH).should == true - end - - it "matches multiple directories with ** and *" do - files = '**/*.rb' - File.send(@method, files, 'main.rb').should == false - File.send(@method, files, './main.rb').should == false - File.send(@method, files, 'lib/song.rb').should == true - File.send(@method, '**.rb', 'main.rb').should == true - File.send(@method, '**.rb', './main.rb').should == false - File.send(@method, '**.rb', 'lib/song.rb').should == true - File.send(@method, '*', 'dave/.profile').should == true - end - - it "matches multiple directories with ** when flags includes File::FNM_PATHNAME" do - files = '**/*.rb' - flags = File::FNM_PATHNAME - - File.send(@method, files, 'main.rb', flags).should == true - File.send(@method, files, 'one/two/three/main.rb', flags).should == true - File.send(@method, files, './main.rb', flags).should == false - - flags = File::FNM_PATHNAME | File::FNM_DOTMATCH - - File.send(@method, files, './main.rb', flags).should == true - File.send(@method, files, 'one/two/.main.rb', flags).should == true - - File.send(@method, "**/best/*", 'lib/my/best/song.rb').should == true - end - - it "returns false if '/' in pattern do not match '/' in path when flags includes FNM_PATHNAME" do - pattern = '*/*' - File.send(@method, pattern, 'dave/.profile', File::FNM_PATHNAME).should == false - - pattern = '**/foo' - File.send(@method, pattern, 'a/.b/c/foo', File::FNM_PATHNAME).should == false - end - - it "returns true if '/' in pattern match '/' in path when flags includes FNM_PATHNAME" do - pattern = '*/*' - File.send(@method, pattern, 'dave/.profile', File::FNM_PATHNAME | File::FNM_DOTMATCH).should == true - - pattern = '**/foo' - File.send(@method, pattern, 'a/b/c/foo', File::FNM_PATHNAME).should == true - File.send(@method, pattern, '/a/b/c/foo', File::FNM_PATHNAME).should == true - File.send(@method, pattern, 'c:/a/b/c/foo', File::FNM_PATHNAME).should == true - File.send(@method, pattern, 'a/.b/c/foo', File::FNM_PATHNAME | File::FNM_DOTMATCH).should == true - end - - it "has special handling for ./ when using * and FNM_PATHNAME" do - File.send(@method, './*', '.', File::FNM_PATHNAME).should == false - File.send(@method, './*', './', File::FNM_PATHNAME).should == true - File.send(@method, './*/', './', File::FNM_PATHNAME).should == false - File.send(@method, './**', './', File::FNM_PATHNAME).should == true - File.send(@method, './**/', './', File::FNM_PATHNAME).should == true - File.send(@method, './*', '.', File::FNM_PATHNAME | File::FNM_DOTMATCH).should == false - File.send(@method, './*', './', File::FNM_PATHNAME | File::FNM_DOTMATCH).should == true - File.send(@method, './*/', './', File::FNM_PATHNAME | File::FNM_DOTMATCH).should == false - File.send(@method, './**', './', File::FNM_PATHNAME | File::FNM_DOTMATCH).should == true - File.send(@method, './**/', './', File::FNM_PATHNAME | File::FNM_DOTMATCH).should == true - end - - it "matches **/* with FNM_PATHNAME to recurse directories" do - File.send(@method, 'nested/**/*', 'nested/subdir', File::FNM_PATHNAME).should == true - File.send(@method, 'nested/**/*', 'nested/subdir/file', File::FNM_PATHNAME).should == true - File.send(@method, 'nested/**/*', 'nested/.dotsubdir', File::FNM_PATHNAME | File::FNM_DOTMATCH).should == true - File.send(@method, 'nested/**/*', 'nested/.dotsubir/.dotfile', File::FNM_PATHNAME | File::FNM_DOTMATCH).should == true - end - - it "matches ** with FNM_PATHNAME only in current directory" do - File.send(@method, 'nested/**', 'nested/subdir', File::FNM_PATHNAME).should == true - File.send(@method, 'nested/**', 'nested/subdir/file', File::FNM_PATHNAME).should == false - File.send(@method, 'nested/**', 'nested/.dotsubdir', File::FNM_PATHNAME | File::FNM_DOTMATCH).should == true - File.send(@method, 'nested/**', 'nested/.dotsubir/.dotfile', File::FNM_PATHNAME | File::FNM_DOTMATCH).should == false - end - - it "accepts an object that has a #to_path method" do - File.send(@method, '\*', mock_to_path('a')).should == false - end - - it "raises a TypeError if the first and second arguments are not string-like" do - -> { File.send(@method, nil, nil, 0, 0) }.should.raise(ArgumentError) - -> { File.send(@method, 1, 'some/thing') }.should.raise(TypeError) - -> { File.send(@method, 'some/thing', 1) }.should.raise(TypeError) - -> { File.send(@method, 1, 1) }.should.raise(TypeError) - end - - it "raises a TypeError if the third argument is not an Integer" do - -> { File.send(@method, "*/place", "path/to/file", "flags") }.should.raise(TypeError) - -> { File.send(@method, "*/place", "path/to/file", nil) }.should.raise(TypeError) - end - - it "does not raise a TypeError if the third argument can be coerced to an Integer" do - flags = mock("flags") - flags.should_receive(:to_int).and_return(10) - -> { File.send(@method, "*/place", "path/to/file", flags) }.should_not.raise - end - - it "matches multibyte characters" do - File.fnmatch("*/ä/ø/ñ", "a/ä/ø/ñ").should == true - end -end diff --git a/spec/ruby/core/file/shared/path.rb b/spec/ruby/core/file/shared/path.rb deleted file mode 100644 index 6c6f7d4234ab4f..00000000000000 --- a/spec/ruby/core/file/shared/path.rb +++ /dev/null @@ -1,82 +0,0 @@ -describe :file_path, shared: true do - before :each do - @path = tmp("file_to_path") - @name = File.basename(@path) - touch @path - end - - after :each do - @file.close if @file and !@file.closed? - rm_r @path - end - - it "returns a String" do - @file = File.new @path - @file.send(@method).should.instance_of?(String) - end - - it "returns a different String on every call" do - @file = File.new @path - path1 = @file.send(@method) - path2 = @file.send(@method) - path1.should == path2 - path1.should_not.equal?(path2) - end - - it "returns a mutable String" do - @file = File.new @path.dup.freeze - path = @file.send(@method) - path.should == @path - path.should_not.frozen? - path << "test" - @file.send(@method).should == @path - end - - it "calls to_str on argument and returns exact value" do - path = mock('path') - path.should_receive(:to_str).and_return(@path) - @file = File.new path - @file.send(@method).should == @path - end - - it "does not normalise the path it returns" do - Dir.chdir(tmp("")) do - unorm = "./#{@name}" - @file = File.new unorm - @file.send(@method).should == unorm - end - end - - it "does not canonicalize the path it returns" do - dir = File.basename tmp("") - path = "#{tmp("")}../#{dir}/#{@name}" - @file = File.new path - @file.send(@method).should == path - end - - it "does not absolute-ise the path it returns" do - Dir.chdir(tmp("")) do - @file = File.new @name - @file.send(@method).should == @name - end - end - - it "preserves the encoding of the path" do - path = @path.force_encoding("euc-jp") - @file = File.new path - @file.send(@method).encoding.should == Encoding.find("euc-jp") - end - - platform_is :linux do - guard -> { defined?(File::TMPFILE) } do - before :each do - @dir = tmp("tmpfilespec") - mkdir_p @dir - end - - after :each do - rm_r @dir - end - end - end -end diff --git a/spec/ruby/core/file/shared/unlink.rb b/spec/ruby/core/file/shared/unlink.rb deleted file mode 100644 index 0032907ba2624d..00000000000000 --- a/spec/ruby/core/file/shared/unlink.rb +++ /dev/null @@ -1,61 +0,0 @@ -describe :file_unlink, shared: true do - before :each do - @file1 = tmp('test.txt') - @file2 = tmp('test2.txt') - - touch @file1 - touch @file2 - end - - after :each do - File.send(@method, @file1) if File.exist?(@file1) - File.send(@method, @file2) if File.exist?(@file2) - - @file1 = nil - @file2 = nil - end - - it "returns 0 when called without arguments" do - File.send(@method).should == 0 - end - - it "deletes a single file" do - File.send(@method, @file1).should == 1 - File.should_not.exist?(@file1) - end - - it "deletes multiple files" do - File.send(@method, @file1, @file2).should == 2 - File.should_not.exist?(@file1) - File.should_not.exist?(@file2) - end - - it "raises a TypeError if not passed a String type" do - -> { File.send(@method, 1) }.should.raise(TypeError) - end - - it "raises an Errno::ENOENT when the given file doesn't exist" do - -> { File.send(@method, 'bogus') }.should.raise(Errno::ENOENT) - end - - it "coerces a given parameter into a string if possible" do - mock = mock("to_str") - mock.should_receive(:to_str).and_return(@file1) - File.send(@method, mock).should == 1 - end - - it "accepts an object that has a #to_path method" do - File.send(@method, mock_to_path(@file1)).should == 1 - end - - platform_is :windows do - it "allows deleting an open file with File::SHARE_DELETE" do - path = tmp("share_delete.txt") - File.open(path, mode: File::CREAT | File::WRONLY | File::BINARY | File::SHARE_DELETE) do |f| - File.should.exist?(path) - File.send(@method, path) - end - File.should_not.exist?(path) - end - end -end diff --git a/spec/ruby/core/file/sticky_spec.rb b/spec/ruby/core/file/sticky_spec.rb index 5f7b2d93eb5eec..782541abf3ce79 100644 --- a/spec/ruby/core/file/sticky_spec.rb +++ b/spec/ruby/core/file/sticky_spec.rb @@ -41,7 +41,7 @@ touch(filename) stat = File.stat(filename) mode = stat.mode - raise_error(Errno::EFTYPE){File.chmod(mode|01000, filename)} + -> { File.chmod(mode|01000, filename) }.should.raise(Errno::EFTYPE) File.sticky?(filename).should == false rm_r filename diff --git a/spec/ruby/core/file/to_path_spec.rb b/spec/ruby/core/file/to_path_spec.rb index 6d168a065c96ad..b968d3b2b965bb 100644 --- a/spec/ruby/core/file/to_path_spec.rb +++ b/spec/ruby/core/file/to_path_spec.rb @@ -1,6 +1,84 @@ require_relative '../../spec_helper' -require_relative 'shared/path' describe "File#to_path" do - it_behaves_like :file_path, :to_path + before :each do + @path = tmp("file_to_path") + @name = File.basename(@path) + touch @path + end + + after :each do + @file.close if @file and !@file.closed? + rm_r @path + end + + it "returns a String" do + @file = File.new @path + @file.to_path.should.instance_of?(String) + end + + it "returns a different String on every call" do + @file = File.new @path + path1 = @file.to_path + path2 = @file.to_path + path1.should == path2 + path1.should_not.equal?(path2) + end + + it "returns a mutable String" do + @file = File.new @path.dup.freeze + path = @file.to_path + path.should == @path + path.should_not.frozen? + path << "test" + @file.to_path.should == @path + end + + it "calls to_str on argument and returns exact value" do + path = mock('path') + path.should_receive(:to_str).and_return(@path) + @file = File.new path + @file.to_path.should == @path + end + + it "does not normalise the path it returns" do + Dir.chdir(tmp("")) do + unorm = "./#{@name}" + @file = File.new unorm + @file.to_path.should == unorm + end + end + + it "does not canonicalize the path it returns" do + dir = File.basename tmp("") + path = "#{tmp("")}../#{dir}/#{@name}" + @file = File.new path + @file.to_path.should == path + end + + it "does not absolute-ise the path it returns" do + Dir.chdir(tmp("")) do + @file = File.new @name + @file.to_path.should == @name + end + end + + it "preserves the encoding of the path" do + path = @path.force_encoding("euc-jp") + @file = File.new path + @file.to_path.encoding.should == Encoding.find("euc-jp") + end + + platform_is :linux do + guard -> { defined?(File::TMPFILE) } do + before :each do + @dir = tmp("tmpfilespec") + mkdir_p @dir + end + + after :each do + rm_r @dir + end + end + end end diff --git a/spec/ruby/core/file/unlink_spec.rb b/spec/ruby/core/file/unlink_spec.rb index 28872d55ed61d0..db0c08f3aed59f 100644 --- a/spec/ruby/core/file/unlink_spec.rb +++ b/spec/ruby/core/file/unlink_spec.rb @@ -1,6 +1,7 @@ require_relative '../../spec_helper' -require_relative 'shared/unlink' describe "File.unlink" do - it_behaves_like :file_unlink, :unlink + it "is an alias of File.delete" do + File.method(:unlink).should == File.method(:delete) + end end diff --git a/spec/ruby/core/file/zero_spec.rb b/spec/ruby/core/file/zero_spec.rb index 01c7505ef226bc..09b0decf48812b 100644 --- a/spec/ruby/core/file/zero_spec.rb +++ b/spec/ruby/core/file/zero_spec.rb @@ -1,7 +1,7 @@ require_relative '../../spec_helper' -require_relative '../../shared/file/zero' describe "File.zero?" do - it_behaves_like :file_zero, :zero?, File - it_behaves_like :file_zero_missing, :zero?, File + it "is an alias of File.empty?" do + File.method(:zero?).should == File.method(:empty?) + end end diff --git a/spec/ruby/core/filetest/empty_spec.rb b/spec/ruby/core/filetest/empty_spec.rb new file mode 100644 index 00000000000000..2c7fbe0dcd48f9 --- /dev/null +++ b/spec/ruby/core/filetest/empty_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' +require_relative '../../shared/file/zero' + +describe "FileTest.empty?" do + it_behaves_like :file_zero, :empty?, FileTest + it_behaves_like :file_zero_missing, :empty?, FileTest +end diff --git a/spec/ruby/core/filetest/zero_spec.rb b/spec/ruby/core/filetest/zero_spec.rb index 92cab67f1b4ff7..de024c25ff4c0a 100644 --- a/spec/ruby/core/filetest/zero_spec.rb +++ b/spec/ruby/core/filetest/zero_spec.rb @@ -1,7 +1,7 @@ require_relative '../../spec_helper' -require_relative '../../shared/file/zero' describe "FileTest.zero?" do - it_behaves_like :file_zero, :zero?, FileTest - it_behaves_like :file_zero_missing, :zero?, FileTest + it "is an alias of FileTest.empty?" do + FileTest.method(:zero?).should == FileTest.method(:empty?) + end end diff --git a/spec/ruby/core/float/angle_spec.rb b/spec/ruby/core/float/angle_spec.rb index c07249aa976e28..ac182c5b73802a 100644 --- a/spec/ruby/core/float/angle_spec.rb +++ b/spec/ruby/core/float/angle_spec.rb @@ -1,6 +1,7 @@ require_relative '../../spec_helper' -require_relative 'shared/arg' describe "Float#angle" do - it_behaves_like :float_arg, :angle + it "is an alias of Float#arg" do + Float.instance_method(:angle).should == Float.instance_method(:arg) + end end diff --git a/spec/ruby/core/float/arg_spec.rb b/spec/ruby/core/float/arg_spec.rb index d3a50668f82f7a..c9c602bbf6c893 100644 --- a/spec/ruby/core/float/arg_spec.rb +++ b/spec/ruby/core/float/arg_spec.rb @@ -1,6 +1,38 @@ require_relative '../../spec_helper' -require_relative 'shared/arg' describe "Float#arg" do - it_behaves_like :float_arg, :arg + it "returns NaN if NaN" do + f = nan_value + f.arg.nan?.should == true + end + + it "returns self if NaN" do + f = nan_value + f.arg.should.equal?(f) + end + + it "returns 0 if positive" do + 1.0.arg.should == 0 + end + + it "returns 0 if +0.0" do + 0.0.arg.should == 0 + end + + it "returns 0 if +Infinity" do + infinity_value.arg.should == 0 + end + + it "returns Pi if negative" do + (-1.0).arg.should == Math::PI + end + + # This was established in r23960 + it "returns Pi if -0.0" do + (-0.0).arg.should == Math::PI + end + + it "returns Pi if -Infinity" do + (-infinity_value).arg.should == Math::PI + end end diff --git a/spec/ruby/core/float/case_compare_spec.rb b/spec/ruby/core/float/case_compare_spec.rb index b902fbea18780f..c82803642d7f7f 100644 --- a/spec/ruby/core/float/case_compare_spec.rb +++ b/spec/ruby/core/float/case_compare_spec.rb @@ -1,6 +1,7 @@ require_relative '../../spec_helper' -require_relative 'shared/equal' describe "Float#===" do - it_behaves_like :float_equal, :=== + it "is an alias of Float#==" do + Float.instance_method(:===).should == Float.instance_method(:==) + end end diff --git a/spec/ruby/core/float/equal_value_spec.rb b/spec/ruby/core/float/equal_value_spec.rb index 03eea5108e450a..37d0e162d356b4 100644 --- a/spec/ruby/core/float/equal_value_spec.rb +++ b/spec/ruby/core/float/equal_value_spec.rb @@ -1,6 +1,40 @@ require_relative '../../spec_helper' -require_relative 'shared/equal' describe "Float#==" do - it_behaves_like :float_equal, :== + it "returns true if self has the same value as other" do + (1.0 == 1).should == true + (2.71828 == 1.428).should == false + (-4.2 == 4.2).should == false + end + + it "calls 'other == self' if coercion fails" do + x = mock('other') + def x.==(other) + 2.0 == other + end + + (1.0 == x).should == false + (2.0 == x).should == true + end + + it "returns false if one side is NaN" do + [1.0, 42, bignum_value].each { |n| + (nan_value == n).should == false + (n == nan_value).should == false + } + end + + it "handles positive infinity" do + [1.0, 42, bignum_value].each { |n| + (infinity_value == n).should == false + (n == infinity_value).should == false + } + end + + it "handles negative infinity" do + [1.0, 42, bignum_value].each { |n| + ((-infinity_value) == n).should == false + (n == -infinity_value).should == false + } + end end diff --git a/spec/ruby/core/float/fdiv_spec.rb b/spec/ruby/core/float/fdiv_spec.rb index be25ee283bacfc..8a3ead488080b7 100644 --- a/spec/ruby/core/float/fdiv_spec.rb +++ b/spec/ruby/core/float/fdiv_spec.rb @@ -1,6 +1,61 @@ require_relative '../../spec_helper' -require_relative 'shared/quo' describe "Float#fdiv" do - it_behaves_like :float_quo, :fdiv + it "performs floating-point division between self and an Integer" do + 8.9.fdiv(7).should == 1.2714285714285716 + end + + it "performs floating-point division between self and an Integer" do + 8.9.fdiv(9999999999999**9).should == 8.900000000008011e-117 + end + + it "performs floating-point division between self and a Float" do + 2827.22.fdiv(872.111111).should == 3.2418116961704433 + end + + it "returns NaN when the argument is NaN" do + -1819.999999.fdiv(nan_value).nan?.should == true + 11109.1981271.fdiv(nan_value).nan?.should == true + end + + it "returns Infinity when the argument is 0.0" do + 2827.22.fdiv(0.0).infinite?.should == 1 + end + + it "returns -Infinity when the argument is 0.0 and self is negative" do + -48229.282.fdiv(0.0).infinite?.should == -1 + end + + it "returns Infinity when the argument is 0" do + 2827.22.fdiv(0).infinite?.should == 1 + end + + it "returns -Infinity when the argument is 0 and self is negative" do + -48229.282.fdiv(0).infinite?.should == -1 + end + + it "returns 0.0 when the argument is Infinity" do + 47292.2821.fdiv(infinity_value).should == 0.0 + end + + it "returns -0.0 when the argument is -Infinity" do + 1.9999918.fdiv(-infinity_value).should == -0.0 + end + + it "performs floating-point division between self and a Rational" do + 74620.09.fdiv(Rational(2,3)).should == 111930.135 + end + + it "performs floating-point division between self and a Complex" do + 74620.09.fdiv(Complex(8,2)).should == Complex( + 8778.834117647059, -2194.7085294117646) + end + + it "raises a TypeError when argument isn't numeric" do + -> { 27292.2.fdiv(mock('non-numeric')) }.should.raise(TypeError) + end + + it "raises an ArgumentError when passed multiple arguments" do + -> { 272.221.fdiv(6,0.2) }.should.raise(ArgumentError) + end end diff --git a/spec/ruby/core/float/inspect_spec.rb b/spec/ruby/core/float/inspect_spec.rb index 4be1927d847c56..3827167c174254 100644 --- a/spec/ruby/core/float/inspect_spec.rb +++ b/spec/ruby/core/float/inspect_spec.rb @@ -1,6 +1,7 @@ require_relative '../../spec_helper' -require_relative 'shared/to_s' describe "Float#inspect" do - it_behaves_like :float_to_s, :inspect + it "is an alias of Float#to_s" do + Float.instance_method(:inspect).should == Float.instance_method(:to_s) + end end diff --git a/spec/ruby/core/float/magnitude_spec.rb b/spec/ruby/core/float/magnitude_spec.rb index 7cdd8ef28a76fd..4a753267e0ef53 100644 --- a/spec/ruby/core/float/magnitude_spec.rb +++ b/spec/ruby/core/float/magnitude_spec.rb @@ -2,5 +2,13 @@ require_relative 'shared/abs' describe "Float#magnitude" do - it_behaves_like :float_abs, :magnitude + ruby_version_is ""..."3.4" do + it_behaves_like :float_abs, :magnitude + end + + ruby_version_is "3.4" do + it "is an alias of Float#abs" do + Float.instance_method(:magnitude).should == Float.instance_method(:abs) + end + end end diff --git a/spec/ruby/core/float/modulo_spec.rb b/spec/ruby/core/float/modulo_spec.rb index 8ae80a0b052150..8b7aedf822502a 100644 --- a/spec/ruby/core/float/modulo_spec.rb +++ b/spec/ruby/core/float/modulo_spec.rb @@ -1,10 +1,56 @@ require_relative '../../spec_helper' -require_relative 'shared/modulo' describe "Float#%" do - it_behaves_like :float_modulo, :% + it "returns self modulo other" do + (6543.21 % 137).should be_close(104.21, TOLERANCE) + (5667.19 % bignum_value).should be_close(5667.19, TOLERANCE) + (6543.21 % 137.24).should be_close(92.9299999999996, TOLERANCE) + + (-1.0 % 1).should == 0 + end + + it "returns self when modulus is +Infinity" do + (4.2 % Float::INFINITY).should == 4.2 + end + + it "returns -Infinity when modulus is -Infinity" do + (4.2 % -Float::INFINITY).should == -Float::INFINITY + end + + it "returns NaN when called on NaN or Infinities" do + (Float::NAN % 42).should.nan? + (Float::INFINITY % 42).should.nan? + (-Float::INFINITY % 42).should.nan? + end + + it "returns NaN when modulus is NaN" do + (4.2 % Float::NAN).should.nan? + end + + it "returns -0.0 when called on -0.0 with a non zero modulus" do + r = -0.0 % 42 + r.should == 0 + (1/r).should < 0 + + r = -0.0 % Float::INFINITY + r.should == 0 + (1/r).should < 0 + end + + it "tries to coerce the modulus" do + obj = mock("modulus") + obj.should_receive(:coerce).with(1.25).and_return([1.25, 0.5]) + (1.25 % obj).should == 0.25 + end + + it "raises a ZeroDivisionError if other is zero" do + -> { 1.0 % 0 }.should.raise(ZeroDivisionError) + -> { 1.0 % 0.0 }.should.raise(ZeroDivisionError) + end end describe "Float#modulo" do - it_behaves_like :float_modulo, :modulo + it "is an alias of Float#%" do + Float.instance_method(:modulo).should == Float.instance_method(:%) + end end diff --git a/spec/ruby/core/float/phase_spec.rb b/spec/ruby/core/float/phase_spec.rb index 2aa84024b4d201..ad112ac9fe1ea4 100644 --- a/spec/ruby/core/float/phase_spec.rb +++ b/spec/ruby/core/float/phase_spec.rb @@ -1,6 +1,7 @@ require_relative '../../spec_helper' -require_relative 'shared/arg' describe "Float#phase" do - it_behaves_like :float_arg, :phase + it "is an alias of Float#arg" do + Float.instance_method(:phase).should == Float.instance_method(:arg) + end end diff --git a/spec/ruby/core/float/quo_spec.rb b/spec/ruby/core/float/quo_spec.rb index b5c64f9d8742f1..0e9a7a0a0c9233 100644 --- a/spec/ruby/core/float/quo_spec.rb +++ b/spec/ruby/core/float/quo_spec.rb @@ -1,6 +1,7 @@ require_relative '../../spec_helper' -require_relative 'shared/quo' describe "Float#quo" do - it_behaves_like :float_quo, :quo + it "is an alias of Float#fdiv" do + Float.instance_method(:quo).should == Float.instance_method(:fdiv) + end end diff --git a/spec/ruby/core/float/shared/arg.rb b/spec/ruby/core/float/shared/arg.rb deleted file mode 100644 index de0024313da8c8..00000000000000 --- a/spec/ruby/core/float/shared/arg.rb +++ /dev/null @@ -1,36 +0,0 @@ -describe :float_arg, shared: true do - it "returns NaN if NaN" do - f = nan_value - f.send(@method).nan?.should == true - end - - it "returns self if NaN" do - f = nan_value - f.send(@method).should.equal?(f) - end - - it "returns 0 if positive" do - 1.0.send(@method).should == 0 - end - - it "returns 0 if +0.0" do - 0.0.send(@method).should == 0 - end - - it "returns 0 if +Infinity" do - infinity_value.send(@method).should == 0 - end - - it "returns Pi if negative" do - (-1.0).send(@method).should == Math::PI - end - - # This was established in r23960 - it "returns Pi if -0.0" do - (-0.0).send(@method).should == Math::PI - end - - it "returns Pi if -Infinity" do - (-infinity_value).send(@method).should == Math::PI - end -end diff --git a/spec/ruby/core/float/shared/equal.rb b/spec/ruby/core/float/shared/equal.rb deleted file mode 100644 index 4d524e1cf238e5..00000000000000 --- a/spec/ruby/core/float/shared/equal.rb +++ /dev/null @@ -1,38 +0,0 @@ -describe :float_equal, shared: true do - it "returns true if self has the same value as other" do - 1.0.send(@method, 1).should == true - 2.71828.send(@method, 1.428).should == false - -4.2.send(@method, 4.2).should == false - end - - it "calls 'other == self' if coercion fails" do - x = mock('other') - def x.==(other) - 2.0 == other - end - - 1.0.send(@method, x).should == false - 2.0.send(@method, x).should == true - end - - it "returns false if one side is NaN" do - [1.0, 42, bignum_value].each { |n| - (nan_value.send(@method, n)).should == false - (n.send(@method, nan_value)).should == false - } - end - - it "handles positive infinity" do - [1.0, 42, bignum_value].each { |n| - (infinity_value.send(@method, n)).should == false - (n.send(@method, infinity_value)).should == false - } - end - - it "handles negative infinity" do - [1.0, 42, bignum_value].each { |n| - ((-infinity_value).send(@method, n)).should == false - (n.send(@method, -infinity_value)).should == false - } - end -end diff --git a/spec/ruby/core/float/shared/modulo.rb b/spec/ruby/core/float/shared/modulo.rb deleted file mode 100644 index 1efee1476d3c69..00000000000000 --- a/spec/ruby/core/float/shared/modulo.rb +++ /dev/null @@ -1,48 +0,0 @@ -describe :float_modulo, shared: true do - it "returns self modulo other" do - 6543.21.send(@method, 137).should be_close(104.21, TOLERANCE) - 5667.19.send(@method, bignum_value).should be_close(5667.19, TOLERANCE) - 6543.21.send(@method, 137.24).should be_close(92.9299999999996, TOLERANCE) - - -1.0.send(@method, 1).should == 0 - end - - it "returns self when modulus is +Infinity" do - 4.2.send(@method, Float::INFINITY).should == 4.2 - end - - it "returns -Infinity when modulus is -Infinity" do - 4.2.send(@method, -Float::INFINITY).should == -Float::INFINITY - end - - it "returns NaN when called on NaN or Infinities" do - Float::NAN.send(@method, 42).should.nan? - Float::INFINITY.send(@method, 42).should.nan? - (-Float::INFINITY).send(@method, 42).should.nan? - end - - it "returns NaN when modulus is NaN" do - 4.2.send(@method, Float::NAN).should.nan? - end - - it "returns -0.0 when called on -0.0 with a non zero modulus" do - r = (-0.0).send(@method, 42) - r.should == 0 - (1/r).should < 0 - - r = (-0.0).send(@method, Float::INFINITY) - r.should == 0 - (1/r).should < 0 - end - - it "tries to coerce the modulus" do - obj = mock("modulus") - obj.should_receive(:coerce).with(1.25).and_return([1.25, 0.5]) - (1.25 % obj).should == 0.25 - end - - it "raises a ZeroDivisionError if other is zero" do - -> { 1.0.send(@method, 0) }.should.raise(ZeroDivisionError) - -> { 1.0.send(@method, 0.0) }.should.raise(ZeroDivisionError) - end -end diff --git a/spec/ruby/core/float/shared/quo.rb b/spec/ruby/core/float/shared/quo.rb deleted file mode 100644 index 930187aaf7b40b..00000000000000 --- a/spec/ruby/core/float/shared/quo.rb +++ /dev/null @@ -1,59 +0,0 @@ -describe :float_quo, shared: true do - it "performs floating-point division between self and an Integer" do - 8.9.send(@method, 7).should == 1.2714285714285716 - end - - it "performs floating-point division between self and an Integer" do - 8.9.send(@method, 9999999999999**9).should == 8.900000000008011e-117 - end - - it "performs floating-point division between self and a Float" do - 2827.22.send(@method, 872.111111).should == 3.2418116961704433 - end - - it "returns NaN when the argument is NaN" do - -1819.999999.send(@method, nan_value).nan?.should == true - 11109.1981271.send(@method, nan_value).nan?.should == true - end - - it "returns Infinity when the argument is 0.0" do - 2827.22.send(@method, 0.0).infinite?.should == 1 - end - - it "returns -Infinity when the argument is 0.0 and self is negative" do - -48229.282.send(@method, 0.0).infinite?.should == -1 - end - - it "returns Infinity when the argument is 0" do - 2827.22.send(@method, 0).infinite?.should == 1 - end - - it "returns -Infinity when the argument is 0 and self is negative" do - -48229.282.send(@method, 0).infinite?.should == -1 - end - - it "returns 0.0 when the argument is Infinity" do - 47292.2821.send(@method, infinity_value).should == 0.0 - end - - it "returns -0.0 when the argument is -Infinity" do - 1.9999918.send(@method, -infinity_value).should == -0.0 - end - - it "performs floating-point division between self and a Rational" do - 74620.09.send(@method, Rational(2,3)).should == 111930.135 - end - - it "performs floating-point division between self and a Complex" do - 74620.09.send(@method, Complex(8,2)).should == Complex( - 8778.834117647059, -2194.7085294117646) - end - - it "raises a TypeError when argument isn't numeric" do - -> { 27292.2.send(@method, mock('non-numeric')) }.should.raise(TypeError) - end - - it "raises an ArgumentError when passed multiple arguments" do - -> { 272.221.send(@method, 6,0.2) }.should.raise(ArgumentError) - end -end diff --git a/spec/ruby/core/float/shared/to_s.rb b/spec/ruby/core/float/shared/to_s.rb deleted file mode 100644 index 81ffdd9e81464d..00000000000000 --- a/spec/ruby/core/float/shared/to_s.rb +++ /dev/null @@ -1,308 +0,0 @@ -describe :float_to_s, shared: true do - it "returns 'NaN' for NaN" do - nan_value().send(@method).should == 'NaN' - end - - it "returns 'Infinity' for positive infinity" do - infinity_value().send(@method).should == 'Infinity' - end - - it "returns '-Infinity' for negative infinity" do - (-infinity_value()).send(@method).should == '-Infinity' - end - - it "returns '0.0' for 0.0" do - 0.0.send(@method).should == "0.0" - end - - platform_is_not :openbsd do - it "emits '-' for -0.0" do - -0.0.send(@method).should == "-0.0" - end - end - - it "emits a '-' for negative values" do - -3.14.send(@method).should == "-3.14" - end - - it "emits a trailing '.0' for a whole number" do - 50.0.send(@method).should == "50.0" - end - - it "emits a trailing '.0' for the mantissa in e format" do - 1.0e20.send(@method).should == "1.0e+20" - end - - it "uses non-e format for a positive value with fractional part having 5 significant figures" do - 0.0001.send(@method).should == "0.0001" - end - - it "uses non-e format for a negative value with fractional part having 5 significant figures" do - -0.0001.send(@method).should == "-0.0001" - end - - it "uses e format for a positive value with fractional part having 6 significant figures" do - 0.00001.send(@method).should == "1.0e-05" - end - - it "uses e format for a negative value with fractional part having 6 significant figures" do - -0.00001.send(@method).should == "-1.0e-05" - end - - it "uses non-e format for a positive value with whole part having 15 significant figures" do - 10000000000000.0.send(@method).should == "10000000000000.0" - end - - it "uses non-e format for a negative value with whole part having 15 significant figures" do - -10000000000000.0.send(@method).should == "-10000000000000.0" - end - - it "uses non-e format for a positive value with whole part having 16 significant figures" do - 100000000000000.0.send(@method).should == "100000000000000.0" - end - - it "uses non-e format for a negative value with whole part having 16 significant figures" do - -100000000000000.0.send(@method).should == "-100000000000000.0" - end - - it "uses e format for a positive value with whole part having 18 significant figures" do - 10000000000000000.0.send(@method).should == "1.0e+16" - end - - it "uses e format for a negative value with whole part having 18 significant figures" do - -10000000000000000.0.send(@method).should == "-1.0e+16" - end - - it "uses e format for a positive value with whole part having 17 significant figures" do - 1000000000000000.0.send(@method).should == "1.0e+15" - end - - it "uses e format for a negative value with whole part having 17 significant figures" do - -1000000000000000.0.send(@method).should == "-1.0e+15" - end - - # #3273 - it "outputs the minimal, unique form necessary to recreate the value" do - value = 0.21611564636388508 - string = "0.21611564636388508" - - value.send(@method).should == string - string.to_f.should == value - end - - it "outputs the minimal, unique form to represent the value" do - 0.56.send(@method).should == "0.56" - end - - describe "matches" do - it "random examples in all ranges" do - # 50.times do - # bytes = (0...8).map { rand(256) } - # string = bytes.pack('C8') - # float = string.unpack('D').first - # puts "#{'%.20g' % float}.send(@method).should == #{float.send(@method).inspect}" - # end - - 2.5540217314354050325e+163.send(@method).should == "2.554021731435405e+163" - 2.5492588360356597544e-172.send(@method).should == "2.5492588360356598e-172" - 1.742770260934704852e-82.send(@method).should == "1.7427702609347049e-82" - 6.2108093676180883209e-104.send(@method).should == "6.210809367618088e-104" - -3.3448803488331067402e-143.send(@method).should == "-3.3448803488331067e-143" - -2.2740074343500832557e-168.send(@method).should == "-2.2740074343500833e-168" - 7.0587971678048535732e+191.send(@method).should == "7.058797167804854e+191" - -284438.88327586348169.send(@method).should == "-284438.8832758635" - 3.953272468476091301e+105.send(@method).should == "3.9532724684760913e+105" - -3.6361359552959847853e+100.send(@method).should == "-3.636135955295985e+100" - -1.3222325865575206185e-31.send(@method).should == "-1.3222325865575206e-31" - 1.1440138916932761366e+130.send(@method).should == "1.1440138916932761e+130" - 4.8750891560387561157e-286.send(@method).should == "4.875089156038756e-286" - 5.6101113356591453525e-257.send(@method).should == "5.610111335659145e-257" - -3.829644279545809575e-100.send(@method).should == "-3.8296442795458096e-100" - 1.5342839401396406117e-194.send(@method).should == "1.5342839401396406e-194" - 2.2284972755169921402e-144.send(@method).should == "2.228497275516992e-144" - 2.1825655917065601737e-61.send(@method).should == "2.1825655917065602e-61" - -2.6672271363524338322e-62.send(@method).should == "-2.667227136352434e-62" - -1.9257995160119059415e+21.send(@method).should == "-1.925799516011906e+21" - -8.9096732962887121718e-198.send(@method).should == "-8.909673296288712e-198" - 2.0202075376548644959e-90.send(@method).should == "2.0202075376548645e-90" - -7.7341602581786258961e-266.send(@method).should == "-7.734160258178626e-266" - 3.5134482598733635046e+98.send(@method).should == "3.5134482598733635e+98" - -2.124411722371029134e+154.send(@method).should == "-2.124411722371029e+154" - -4.573908787355718687e+110.send(@method).should == "-4.573908787355719e+110" - -1.9344425934170969879e-232.send(@method).should == "-1.934442593417097e-232" - -1.3274227399979271095e+171.send(@method).should == "-1.3274227399979271e+171" - 9.3495270482104442383e-283.send(@method).should == "9.349527048210444e-283" - -4.2046059371986483233e+307.send(@method).should == "-4.2046059371986483e+307" - 3.6133547278583543004e-117.send(@method).should == "3.613354727858354e-117" - 4.9247416523566613499e-08.send(@method).should == "4.9247416523566613e-08" - 1.6936145488250064007e-71.send(@method).should == "1.6936145488250064e-71" - 2.4455483206829433098e+96.send(@method).should == "2.4455483206829433e+96" - 7.9797449851436455384e+124.send(@method).should == "7.979744985143646e+124" - -1.3873689634457876774e-129.send(@method).should == "-1.3873689634457877e-129" - 3.9761102037533483075e+284.send(@method).should == "3.976110203753348e+284" - -4.2819791952139402486e-303.send(@method).should == "-4.28197919521394e-303" - -5.7981017546689831298e-116.send(@method).should == "-5.798101754668983e-116" - -3.953266497860534199e-28.send(@method).should == "-3.953266497860534e-28" - -2.0659852720290440959e-243.send(@method).should == "-2.065985272029044e-243" - 8.9670488995878688018e-05.send(@method).should == "8.967048899587869e-05" - -1.2317943708113061768e-98.send(@method).should == "-1.2317943708113062e-98" - -3.8930768307633080463e+248.send(@method).should == "-3.893076830763308e+248" - 6.5854032671803925627e-239.send(@method).should == "6.5854032671803926e-239" - 4.6257022188980878952e+177.send(@method).should == "4.625702218898088e+177" - -1.9397155125507235603e-187.send(@method).should == "-1.9397155125507236e-187" - 8.5752156951245705056e+117.send(@method).should == "8.57521569512457e+117" - -2.4784875958162501671e-132.send(@method).should == "-2.4784875958162502e-132" - -4.4125691841230058457e-203.send(@method).should == "-4.412569184123006e-203" - end - - it "random examples in human ranges" do - # 50.times do - # formatted = '' - # rand(1..3).times do - # formatted << rand(10).to_s - # end - # formatted << '.' - # rand(1..9).times do - # formatted << rand(10).to_s - # end - # float = formatted.to_f - # puts "#{'%.20f' % float}.send(@method).should == #{float.send(@method).inspect}" - # end - - 5.17869899999999994122.send(@method).should == "5.178699" - 905.62695729999995819526.send(@method).should == "905.6269573" - 62.75999999999999801048.send(@method).should == "62.76" - 6.93856795800000014651.send(@method).should == "6.938567958" - 4.95999999999999996447.send(@method).should == "4.96" - 32.77993899999999882766.send(@method).should == "32.779939" - 544.12756779999995160324.send(@method).should == "544.1275678" - 66.25801119999999855281.send(@method).should == "66.2580112" - 7.90000000000000035527.send(@method).should == "7.9" - 5.93100000000000004974.send(@method).should == "5.931" - 5.21229313600000043749.send(@method).should == "5.212293136" - 503.44173809000000119340.send(@method).should == "503.44173809" - 79.26000000000000511591.send(@method).should == "79.26" - 8.51524999999999998579.send(@method).should == "8.51525" - 174.00000000000000000000.send(@method).should == "174.0" - 50.39580000000000126192.send(@method).should == "50.3958" - 35.28999999999999914735.send(@method).should == "35.29" - 5.43136675399999990788.send(@method).should == "5.431366754" - 654.07680000000004838512.send(@method).should == "654.0768" - 6.07423700000000010846.send(@method).should == "6.074237" - 102.25779799999999397642.send(@method).should == "102.257798" - 5.08129999999999970584.send(@method).should == "5.0813" - 6.00000000000000000000.send(@method).should == "6.0" - 8.30000000000000071054.send(@method).should == "8.3" - 32.68345999999999662577.send(@method).should == "32.68346" - 581.11170000000004165486.send(@method).should == "581.1117" - 76.31342999999999676675.send(@method).should == "76.31343" - 438.30826000000001840817.send(@method).should == "438.30826" - 482.06631994000002805478.send(@method).should == "482.06631994" - 55.92721026899999969828.send(@method).should == "55.927210269" - 4.00000000000000000000.send(@method).should == "4.0" - 55.86693999999999959982.send(@method).should == "55.86694" - 787.98299999999994724931.send(@method).should == "787.983" - 5.73810511000000023074.send(@method).should == "5.73810511" - 74.51926810000000500622.send(@method).should == "74.5192681" - 892.89999999999997726263.send(@method).should == "892.9" - 68.27299999999999613465.send(@method).should == "68.273" - 904.10000000000002273737.send(@method).should == "904.1" - 5.23200000000000020606.send(@method).should == "5.232" - 4.09628000000000014325.send(@method).should == "4.09628" - 46.05152633699999853434.send(@method).should == "46.051526337" - 142.12884990599999923688.send(@method).should == "142.128849906" - 3.83057023500000015659.send(@method).should == "3.830570235" - 11.81684594699999912848.send(@method).should == "11.816845947" - 80.50000000000000000000.send(@method).should == "80.5" - 382.18215010000000120272.send(@method).should == "382.1821501" - 55.38444606899999911320.send(@method).should == "55.384446069" - 5.78000000000000024869.send(@method).should == "5.78" - 2.88244999999999995666.send(@method).should == "2.88245" - 43.27709999999999723741.send(@method).should == "43.2771" - end - - it "random values from divisions" do - (1.0 / 7).send(@method).should == "0.14285714285714285" - - # 50.times do - # a = rand(10) - # b = rand(10) - # c = rand(10) - # d = rand(10) - # expression = "#{a}.#{b} / #{c}.#{d}" - # puts " (#{expression}).send(@method).should == #{eval(expression).send(@method).inspect}" - # end - - (1.1 / 7.1).send(@method).should == "0.15492957746478875" - (6.5 / 8.8).send(@method).should == "0.7386363636363635" - (4.8 / 4.3).send(@method).should == "1.1162790697674418" - (4.0 / 1.9).send(@method).should == "2.1052631578947367" - (9.1 / 0.8).send(@method).should == "11.374999999999998" - (5.3 / 7.5).send(@method).should == "0.7066666666666667" - (2.8 / 1.8).send(@method).should == "1.5555555555555554" - (2.1 / 2.5).send(@method).should == "0.8400000000000001" - (3.5 / 6.0).send(@method).should == "0.5833333333333334" - (4.6 / 0.3).send(@method).should == "15.333333333333332" - (0.6 / 2.4).send(@method).should == "0.25" - (1.3 / 9.1).send(@method).should == "0.14285714285714288" - (0.3 / 5.0).send(@method).should == "0.06" - (5.0 / 4.2).send(@method).should == "1.1904761904761905" - (3.0 / 2.0).send(@method).should == "1.5" - (6.3 / 2.0).send(@method).should == "3.15" - (5.4 / 6.0).send(@method).should == "0.9" - (9.6 / 8.1).send(@method).should == "1.1851851851851851" - (8.7 / 1.6).send(@method).should == "5.437499999999999" - (1.9 / 7.8).send(@method).should == "0.24358974358974358" - (0.5 / 2.1).send(@method).should == "0.23809523809523808" - (9.3 / 5.8).send(@method).should == "1.6034482758620692" - (2.7 / 8.0).send(@method).should == "0.3375" - (9.7 / 7.8).send(@method).should == "1.2435897435897436" - (8.1 / 2.4).send(@method).should == "3.375" - (7.7 / 2.7).send(@method).should == "2.8518518518518516" - (7.9 / 1.7).send(@method).should == "4.647058823529412" - (6.5 / 8.2).send(@method).should == "0.7926829268292683" - (7.8 / 9.6).send(@method).should == "0.8125" - (2.2 / 4.6).send(@method).should == "0.47826086956521746" - (0.0 / 1.0).send(@method).should == "0.0" - (8.3 / 2.9).send(@method).should == "2.8620689655172415" - (3.1 / 6.1).send(@method).should == "0.5081967213114754" - (2.8 / 7.8).send(@method).should == "0.358974358974359" - (8.0 / 0.1).send(@method).should == "80.0" - (1.7 / 6.4).send(@method).should == "0.265625" - (1.8 / 5.4).send(@method).should == "0.3333333333333333" - (8.0 / 5.8).send(@method).should == "1.3793103448275863" - (5.2 / 4.1).send(@method).should == "1.2682926829268295" - (9.8 / 5.8).send(@method).should == "1.6896551724137934" - (5.4 / 9.5).send(@method).should == "0.5684210526315789" - (8.4 / 4.9).send(@method).should == "1.7142857142857142" - (1.7 / 3.5).send(@method).should == "0.4857142857142857" - (1.2 / 5.1).send(@method).should == "0.23529411764705882" - (1.4 / 2.0).send(@method).should == "0.7" - (4.8 / 8.0).send(@method).should == "0.6" - (9.0 / 2.5).send(@method).should == "3.6" - (0.2 / 0.6).send(@method).should == "0.33333333333333337" - (7.8 / 5.2).send(@method).should == "1.5" - (9.5 / 5.5).send(@method).should == "1.7272727272727273" - end - end - - describe 'encoding' do - before :each do - @internal = Encoding.default_internal - end - - after :each do - Encoding.default_internal = @internal - end - - it "returns a String in US-ASCII encoding when Encoding.default_internal is nil" do - Encoding.default_internal = nil - 1.23.send(@method).encoding.should.equal?(Encoding::US_ASCII) - end - - it "returns a String in US-ASCII encoding when Encoding.default_internal is not nil" do - Encoding.default_internal = Encoding::IBM437 - 5.47.send(@method).encoding.should.equal?(Encoding::US_ASCII) - end - end -end diff --git a/spec/ruby/core/float/to_int_spec.rb b/spec/ruby/core/float/to_int_spec.rb index 084a58b4319884..ff70d508ffc979 100644 --- a/spec/ruby/core/float/to_int_spec.rb +++ b/spec/ruby/core/float/to_int_spec.rb @@ -1,6 +1,7 @@ require_relative '../../spec_helper' -require_relative 'shared/to_i' describe "Float#to_int" do - it_behaves_like :float_to_i, :to_int + it "is an alias of Float#to_i" do + Float.instance_method(:to_int).should == Float.instance_method(:to_i) + end end diff --git a/spec/ruby/core/float/to_s_spec.rb b/spec/ruby/core/float/to_s_spec.rb index 6727a883f86013..3fd64581c21e6a 100644 --- a/spec/ruby/core/float/to_s_spec.rb +++ b/spec/ruby/core/float/to_s_spec.rb @@ -1,6 +1,310 @@ require_relative '../../spec_helper' -require_relative 'shared/to_s' describe "Float#to_s" do - it_behaves_like :float_to_s, :to_s + it "returns 'NaN' for NaN" do + nan_value().to_s.should == 'NaN' + end + + it "returns 'Infinity' for positive infinity" do + infinity_value().to_s.should == 'Infinity' + end + + it "returns '-Infinity' for negative infinity" do + (-infinity_value()).to_s.should == '-Infinity' + end + + it "returns '0.0' for 0.0" do + 0.0.to_s.should == "0.0" + end + + platform_is_not :openbsd do + it "emits '-' for -0.0" do + -0.0.to_s.should == "-0.0" + end + end + + it "emits a '-' for negative values" do + -3.14.to_s.should == "-3.14" + end + + it "emits a trailing '.0' for a whole number" do + 50.0.to_s.should == "50.0" + end + + it "emits a trailing '.0' for the mantissa in e format" do + 1.0e20.to_s.should == "1.0e+20" + end + + it "uses non-e format for a positive value with fractional part having 5 significant figures" do + 0.0001.to_s.should == "0.0001" + end + + it "uses non-e format for a negative value with fractional part having 5 significant figures" do + -0.0001.to_s.should == "-0.0001" + end + + it "uses e format for a positive value with fractional part having 6 significant figures" do + 0.00001.to_s.should == "1.0e-05" + end + + it "uses e format for a negative value with fractional part having 6 significant figures" do + -0.00001.to_s.should == "-1.0e-05" + end + + it "uses non-e format for a positive value with whole part having 15 significant figures" do + 10000000000000.0.to_s.should == "10000000000000.0" + end + + it "uses non-e format for a negative value with whole part having 15 significant figures" do + -10000000000000.0.to_s.should == "-10000000000000.0" + end + + it "uses non-e format for a positive value with whole part having 16 significant figures" do + 100000000000000.0.to_s.should == "100000000000000.0" + end + + it "uses non-e format for a negative value with whole part having 16 significant figures" do + -100000000000000.0.to_s.should == "-100000000000000.0" + end + + it "uses e format for a positive value with whole part having 18 significant figures" do + 10000000000000000.0.to_s.should == "1.0e+16" + end + + it "uses e format for a negative value with whole part having 18 significant figures" do + -10000000000000000.0.to_s.should == "-1.0e+16" + end + + it "uses e format for a positive value with whole part having 17 significant figures" do + 1000000000000000.0.to_s.should == "1.0e+15" + end + + it "uses e format for a negative value with whole part having 17 significant figures" do + -1000000000000000.0.to_s.should == "-1.0e+15" + end + + # #3273 + it "outputs the minimal, unique form necessary to recreate the value" do + value = 0.21611564636388508 + string = "0.21611564636388508" + + value.to_s.should == string + string.to_f.should == value + end + + it "outputs the minimal, unique form to represent the value" do + 0.56.to_s.should == "0.56" + end + + describe "matches" do + it "random examples in all ranges" do + # 50.times do + # bytes = (0...8).map { rand(256) } + # string = bytes.pack('C8') + # float = string.unpack('D').first + # puts "#{'%.20g' % float}.to_s.should == #{float.to_s.inspect}" + # end + + 2.5540217314354050325e+163.to_s.should == "2.554021731435405e+163" + 2.5492588360356597544e-172.to_s.should == "2.5492588360356598e-172" + 1.742770260934704852e-82.to_s.should == "1.7427702609347049e-82" + 6.2108093676180883209e-104.to_s.should == "6.210809367618088e-104" + -3.3448803488331067402e-143.to_s.should == "-3.3448803488331067e-143" + -2.2740074343500832557e-168.to_s.should == "-2.2740074343500833e-168" + 7.0587971678048535732e+191.to_s.should == "7.058797167804854e+191" + -284438.88327586348169.to_s.should == "-284438.8832758635" + 3.953272468476091301e+105.to_s.should == "3.9532724684760913e+105" + -3.6361359552959847853e+100.to_s.should == "-3.636135955295985e+100" + -1.3222325865575206185e-31.to_s.should == "-1.3222325865575206e-31" + 1.1440138916932761366e+130.to_s.should == "1.1440138916932761e+130" + 4.8750891560387561157e-286.to_s.should == "4.875089156038756e-286" + 5.6101113356591453525e-257.to_s.should == "5.610111335659145e-257" + -3.829644279545809575e-100.to_s.should == "-3.8296442795458096e-100" + 1.5342839401396406117e-194.to_s.should == "1.5342839401396406e-194" + 2.2284972755169921402e-144.to_s.should == "2.228497275516992e-144" + 2.1825655917065601737e-61.to_s.should == "2.1825655917065602e-61" + -2.6672271363524338322e-62.to_s.should == "-2.667227136352434e-62" + -1.9257995160119059415e+21.to_s.should == "-1.925799516011906e+21" + -8.9096732962887121718e-198.to_s.should == "-8.909673296288712e-198" + 2.0202075376548644959e-90.to_s.should == "2.0202075376548645e-90" + -7.7341602581786258961e-266.to_s.should == "-7.734160258178626e-266" + 3.5134482598733635046e+98.to_s.should == "3.5134482598733635e+98" + -2.124411722371029134e+154.to_s.should == "-2.124411722371029e+154" + -4.573908787355718687e+110.to_s.should == "-4.573908787355719e+110" + -1.9344425934170969879e-232.to_s.should == "-1.934442593417097e-232" + -1.3274227399979271095e+171.to_s.should == "-1.3274227399979271e+171" + 9.3495270482104442383e-283.to_s.should == "9.349527048210444e-283" + -4.2046059371986483233e+307.to_s.should == "-4.2046059371986483e+307" + 3.6133547278583543004e-117.to_s.should == "3.613354727858354e-117" + 4.9247416523566613499e-08.to_s.should == "4.9247416523566613e-08" + 1.6936145488250064007e-71.to_s.should == "1.6936145488250064e-71" + 2.4455483206829433098e+96.to_s.should == "2.4455483206829433e+96" + 7.9797449851436455384e+124.to_s.should == "7.979744985143646e+124" + -1.3873689634457876774e-129.to_s.should == "-1.3873689634457877e-129" + 3.9761102037533483075e+284.to_s.should == "3.976110203753348e+284" + -4.2819791952139402486e-303.to_s.should == "-4.28197919521394e-303" + -5.7981017546689831298e-116.to_s.should == "-5.798101754668983e-116" + -3.953266497860534199e-28.to_s.should == "-3.953266497860534e-28" + -2.0659852720290440959e-243.to_s.should == "-2.065985272029044e-243" + 8.9670488995878688018e-05.to_s.should == "8.967048899587869e-05" + -1.2317943708113061768e-98.to_s.should == "-1.2317943708113062e-98" + -3.8930768307633080463e+248.to_s.should == "-3.893076830763308e+248" + 6.5854032671803925627e-239.to_s.should == "6.5854032671803926e-239" + 4.6257022188980878952e+177.to_s.should == "4.625702218898088e+177" + -1.9397155125507235603e-187.to_s.should == "-1.9397155125507236e-187" + 8.5752156951245705056e+117.to_s.should == "8.57521569512457e+117" + -2.4784875958162501671e-132.to_s.should == "-2.4784875958162502e-132" + -4.4125691841230058457e-203.to_s.should == "-4.412569184123006e-203" + end + + it "random examples in human ranges" do + # 50.times do + # formatted = '' + # rand(1..3).times do + # formatted << rand(10).to_s + # end + # formatted << '.' + # rand(1..9).times do + # formatted << rand(10).to_s + # end + # float = formatted.to_f + # puts "#{'%.20f' % float}.to_s.should == #{float.to_s.inspect}" + # end + + 5.17869899999999994122.to_s.should == "5.178699" + 905.62695729999995819526.to_s.should == "905.6269573" + 62.75999999999999801048.to_s.should == "62.76" + 6.93856795800000014651.to_s.should == "6.938567958" + 4.95999999999999996447.to_s.should == "4.96" + 32.77993899999999882766.to_s.should == "32.779939" + 544.12756779999995160324.to_s.should == "544.1275678" + 66.25801119999999855281.to_s.should == "66.2580112" + 7.90000000000000035527.to_s.should == "7.9" + 5.93100000000000004974.to_s.should == "5.931" + 5.21229313600000043749.to_s.should == "5.212293136" + 503.44173809000000119340.to_s.should == "503.44173809" + 79.26000000000000511591.to_s.should == "79.26" + 8.51524999999999998579.to_s.should == "8.51525" + 174.00000000000000000000.to_s.should == "174.0" + 50.39580000000000126192.to_s.should == "50.3958" + 35.28999999999999914735.to_s.should == "35.29" + 5.43136675399999990788.to_s.should == "5.431366754" + 654.07680000000004838512.to_s.should == "654.0768" + 6.07423700000000010846.to_s.should == "6.074237" + 102.25779799999999397642.to_s.should == "102.257798" + 5.08129999999999970584.to_s.should == "5.0813" + 6.00000000000000000000.to_s.should == "6.0" + 8.30000000000000071054.to_s.should == "8.3" + 32.68345999999999662577.to_s.should == "32.68346" + 581.11170000000004165486.to_s.should == "581.1117" + 76.31342999999999676675.to_s.should == "76.31343" + 438.30826000000001840817.to_s.should == "438.30826" + 482.06631994000002805478.to_s.should == "482.06631994" + 55.92721026899999969828.to_s.should == "55.927210269" + 4.00000000000000000000.to_s.should == "4.0" + 55.86693999999999959982.to_s.should == "55.86694" + 787.98299999999994724931.to_s.should == "787.983" + 5.73810511000000023074.to_s.should == "5.73810511" + 74.51926810000000500622.to_s.should == "74.5192681" + 892.89999999999997726263.to_s.should == "892.9" + 68.27299999999999613465.to_s.should == "68.273" + 904.10000000000002273737.to_s.should == "904.1" + 5.23200000000000020606.to_s.should == "5.232" + 4.09628000000000014325.to_s.should == "4.09628" + 46.05152633699999853434.to_s.should == "46.051526337" + 142.12884990599999923688.to_s.should == "142.128849906" + 3.83057023500000015659.to_s.should == "3.830570235" + 11.81684594699999912848.to_s.should == "11.816845947" + 80.50000000000000000000.to_s.should == "80.5" + 382.18215010000000120272.to_s.should == "382.1821501" + 55.38444606899999911320.to_s.should == "55.384446069" + 5.78000000000000024869.to_s.should == "5.78" + 2.88244999999999995666.to_s.should == "2.88245" + 43.27709999999999723741.to_s.should == "43.2771" + end + + it "random values from divisions" do + (1.0 / 7).to_s.should == "0.14285714285714285" + + # 50.times do + # a = rand(10) + # b = rand(10) + # c = rand(10) + # d = rand(10) + # expression = "#{a}.#{b} / #{c}.#{d}" + # puts " (#{expression}).to_s.should == #{eval(expression).to_s.inspect}" + # end + + (1.1 / 7.1).to_s.should == "0.15492957746478875" + (6.5 / 8.8).to_s.should == "0.7386363636363635" + (4.8 / 4.3).to_s.should == "1.1162790697674418" + (4.0 / 1.9).to_s.should == "2.1052631578947367" + (9.1 / 0.8).to_s.should == "11.374999999999998" + (5.3 / 7.5).to_s.should == "0.7066666666666667" + (2.8 / 1.8).to_s.should == "1.5555555555555554" + (2.1 / 2.5).to_s.should == "0.8400000000000001" + (3.5 / 6.0).to_s.should == "0.5833333333333334" + (4.6 / 0.3).to_s.should == "15.333333333333332" + (0.6 / 2.4).to_s.should == "0.25" + (1.3 / 9.1).to_s.should == "0.14285714285714288" + (0.3 / 5.0).to_s.should == "0.06" + (5.0 / 4.2).to_s.should == "1.1904761904761905" + (3.0 / 2.0).to_s.should == "1.5" + (6.3 / 2.0).to_s.should == "3.15" + (5.4 / 6.0).to_s.should == "0.9" + (9.6 / 8.1).to_s.should == "1.1851851851851851" + (8.7 / 1.6).to_s.should == "5.437499999999999" + (1.9 / 7.8).to_s.should == "0.24358974358974358" + (0.5 / 2.1).to_s.should == "0.23809523809523808" + (9.3 / 5.8).to_s.should == "1.6034482758620692" + (2.7 / 8.0).to_s.should == "0.3375" + (9.7 / 7.8).to_s.should == "1.2435897435897436" + (8.1 / 2.4).to_s.should == "3.375" + (7.7 / 2.7).to_s.should == "2.8518518518518516" + (7.9 / 1.7).to_s.should == "4.647058823529412" + (6.5 / 8.2).to_s.should == "0.7926829268292683" + (7.8 / 9.6).to_s.should == "0.8125" + (2.2 / 4.6).to_s.should == "0.47826086956521746" + (0.0 / 1.0).to_s.should == "0.0" + (8.3 / 2.9).to_s.should == "2.8620689655172415" + (3.1 / 6.1).to_s.should == "0.5081967213114754" + (2.8 / 7.8).to_s.should == "0.358974358974359" + (8.0 / 0.1).to_s.should == "80.0" + (1.7 / 6.4).to_s.should == "0.265625" + (1.8 / 5.4).to_s.should == "0.3333333333333333" + (8.0 / 5.8).to_s.should == "1.3793103448275863" + (5.2 / 4.1).to_s.should == "1.2682926829268295" + (9.8 / 5.8).to_s.should == "1.6896551724137934" + (5.4 / 9.5).to_s.should == "0.5684210526315789" + (8.4 / 4.9).to_s.should == "1.7142857142857142" + (1.7 / 3.5).to_s.should == "0.4857142857142857" + (1.2 / 5.1).to_s.should == "0.23529411764705882" + (1.4 / 2.0).to_s.should == "0.7" + (4.8 / 8.0).to_s.should == "0.6" + (9.0 / 2.5).to_s.should == "3.6" + (0.2 / 0.6).to_s.should == "0.33333333333333337" + (7.8 / 5.2).to_s.should == "1.5" + (9.5 / 5.5).to_s.should == "1.7272727272727273" + end + end + + describe 'encoding' do + before :each do + @internal = Encoding.default_internal + end + + after :each do + Encoding.default_internal = @internal + end + + it "returns a String in US-ASCII encoding when Encoding.default_internal is nil" do + Encoding.default_internal = nil + 1.23.to_s.encoding.should.equal?(Encoding::US_ASCII) + end + + it "returns a String in US-ASCII encoding when Encoding.default_internal is not nil" do + Encoding.default_internal = Encoding::IBM437 + 5.47.to_s.encoding.should.equal?(Encoding::US_ASCII) + end + end end diff --git a/spec/ruby/core/hash/has_value_spec.rb b/spec/ruby/core/hash/has_value_spec.rb index d40e52ebfd78f0..95b8390a66ec67 100644 --- a/spec/ruby/core/hash/has_value_spec.rb +++ b/spec/ruby/core/hash/has_value_spec.rb @@ -1,16 +1,7 @@ require_relative '../../spec_helper' describe "Hash#has_value?" do - it "returns true if the value exists in the hash" do - { a: :b }.has_value?(:a).should == false - { 1 => 2 }.has_value?(2).should == true - h = Hash.new(5) - h.has_value?(5).should == false - h = Hash.new { 5 } - h.has_value?(5).should == false - end - - it "uses == semantics for comparing values" do - { 5 => 2.0 }.has_value?(2).should == true + it "is an alias of Hash#value?" do + Hash.instance_method(:has_value?).should == Hash.instance_method(:value?) end end diff --git a/spec/ruby/core/hash/merge_spec.rb b/spec/ruby/core/hash/merge_spec.rb index 9e566fcee92b11..7a444f7f259e72 100644 --- a/spec/ruby/core/hash/merge_spec.rb +++ b/spec/ruby/core/hash/merge_spec.rb @@ -117,7 +117,78 @@ end describe "Hash#merge!" do - it "is an alias of Hash#update" do - Hash.instance_method(:merge!).should == Hash.instance_method(:update) + it "adds the entries from other, overwriting duplicate keys. Returns self" do + h = { _1: 'a', _2: '3' } + h.merge!(_1: '9', _9: 2).should.equal?(h) + h.should == { _1: "9", _2: "3", _9: 2 } + end + + it "sets any duplicate key to the value of block if passed a block" do + h1 = { a: 2, b: -1 } + h2 = { a: -2, c: 1 } + h1.merge!(h2) { |k,x,y| 3.14 }.should.equal?(h1) + h1.should == { c: 1, b: -1, a: 3.14 } + + h1.merge!(h1) { nil } + h1.should == { a: nil, b: nil, c: nil } + end + + it "tries to convert the passed argument to a hash using #to_hash" do + obj = mock('{1=>2}') + obj.should_receive(:to_hash).and_return({ 1 => 2 }) + { 3 => 4 }.merge!(obj).should == { 1 => 2, 3 => 4 } + end + + it "does not call to_hash on hash subclasses" do + { 3 => 4 }.merge!(HashSpecs::ToHashHash[1 => 2]).should == { 1 => 2, 3 => 4 } + end + + it "processes entries with same order as merge()" do + h = { 1 => 2, 3 => 4, 5 => 6, "x" => nil, nil => 5, [] => [] } + merge_bang_pairs = [] + merge_pairs = [] + h.merge(h) { |*arg| merge_pairs << arg } + h.merge!(h) { |*arg| merge_bang_pairs << arg } + merge_bang_pairs.should == merge_pairs + end + + it "raises a FrozenError on a frozen instance that is modified" do + -> do + HashSpecs.frozen_hash.merge!(1 => 2) + end.should.raise(FrozenError) + end + + it "checks frozen status before coercing an object with #to_hash" do + obj = mock("to_hash frozen") + # This is necessary because mock cleanup code cannot run on the frozen + # object. + def obj.to_hash() raise Exception, "should not receive #to_hash" end + obj.freeze + + -> { HashSpecs.frozen_hash.merge!(obj) }.should.raise(FrozenError) + end + + # see redmine #1571 + it "raises a FrozenError on a frozen instance that would not be modified" do + -> do + HashSpecs.frozen_hash.merge!(HashSpecs.empty_frozen_hash) + end.should.raise(FrozenError) + end + + it "does not raise an exception if changing the value of an existing key during iteration" do + hash = {1 => 2, 3 => 4, 5 => 6} + hash2 = {1 => :foo, 3 => :bar} + hash.each { hash.merge!(hash2) } + hash.should == {1 => :foo, 3 => :bar, 5 => 6} + end + + it "accepts multiple hashes" do + result = { a: 1 }.merge!({ b: 2 }, { c: 3 }, { d: 4 }) + result.should == { a: 1, b: 2, c: 3, d: 4 } + end + + it "accepts zero arguments" do + hash = { a: 1 } + hash.merge!.should.eql?(hash) end end diff --git a/spec/ruby/core/hash/update_spec.rb b/spec/ruby/core/hash/update_spec.rb index f3a3e6b4db2931..04070baad84ddc 100644 --- a/spec/ruby/core/hash/update_spec.rb +++ b/spec/ruby/core/hash/update_spec.rb @@ -1,79 +1,7 @@ require_relative '../../spec_helper' -require_relative 'fixtures/classes' describe "Hash#update" do - it "adds the entries from other, overwriting duplicate keys. Returns self" do - h = { _1: 'a', _2: '3' } - h.update(_1: '9', _9: 2).should.equal?(h) - h.should == { _1: "9", _2: "3", _9: 2 } - end - - it "sets any duplicate key to the value of block if passed a block" do - h1 = { a: 2, b: -1 } - h2 = { a: -2, c: 1 } - h1.update(h2) { |k,x,y| 3.14 }.should.equal?(h1) - h1.should == { c: 1, b: -1, a: 3.14 } - - h1.update(h1) { nil } - h1.should == { a: nil, b: nil, c: nil } - end - - it "tries to convert the passed argument to a hash using #to_hash" do - obj = mock('{1=>2}') - obj.should_receive(:to_hash).and_return({ 1 => 2 }) - { 3 => 4 }.update(obj).should == { 1 => 2, 3 => 4 } - end - - it "does not call to_hash on hash subclasses" do - { 3 => 4 }.update(HashSpecs::ToHashHash[1 => 2]).should == { 1 => 2, 3 => 4 } - end - - it "processes entries with same order as merge()" do - h = { 1 => 2, 3 => 4, 5 => 6, "x" => nil, nil => 5, [] => [] } - merge_bang_pairs = [] - merge_pairs = [] - h.merge(h) { |*arg| merge_pairs << arg } - h.update(h) { |*arg| merge_bang_pairs << arg } - merge_bang_pairs.should == merge_pairs - end - - it "raises a FrozenError on a frozen instance that is modified" do - -> do - HashSpecs.frozen_hash.update(1 => 2) - end.should.raise(FrozenError) - end - - it "checks frozen status before coercing an object with #to_hash" do - obj = mock("to_hash frozen") - # This is necessary because mock cleanup code cannot run on the frozen - # object. - def obj.to_hash() raise Exception, "should not receive #to_hash" end - obj.freeze - - -> { HashSpecs.frozen_hash.update(obj) }.should.raise(FrozenError) - end - - # see redmine #1571 - it "raises a FrozenError on a frozen instance that would not be modified" do - -> do - HashSpecs.frozen_hash.update(HashSpecs.empty_frozen_hash) - end.should.raise(FrozenError) - end - - it "does not raise an exception if changing the value of an existing key during iteration" do - hash = {1 => 2, 3 => 4, 5 => 6} - hash2 = {1 => :foo, 3 => :bar} - hash.each { hash.update(hash2) } - hash.should == {1 => :foo, 3 => :bar, 5 => 6} - end - - it "accepts multiple hashes" do - result = { a: 1 }.update({ b: 2 }, { c: 3 }, { d: 4 }) - result.should == { a: 1, b: 2, c: 3, d: 4 } - end - - it "accepts zero arguments" do - hash = { a: 1 } - hash.update.should.eql?(hash) + it "is an alias of Hash#merge!" do + Hash.instance_method(:update).should == Hash.instance_method(:merge!) end end diff --git a/spec/ruby/core/hash/value_spec.rb b/spec/ruby/core/hash/value_spec.rb index 9cfbe576d26a40..8e4732480fa7ff 100644 --- a/spec/ruby/core/hash/value_spec.rb +++ b/spec/ruby/core/hash/value_spec.rb @@ -1,7 +1,16 @@ require_relative '../../spec_helper' describe "Hash#value?" do - it "is an alias of Hash#has_value?" do - Hash.instance_method(:value?).should == Hash.instance_method(:has_value?) + it "returns true if the value exists in the hash" do + { a: :b }.value?(:a).should == false + { 1 => 2 }.value?(2).should == true + h = Hash.new(5) + h.value?(5).should == false + h = Hash.new { 5 } + h.value?(5).should == false + end + + it "uses == semantics for comparing values" do + { 5 => 2.0 }.value?(2).should == true end end diff --git a/spec/ruby/core/integer/abs_spec.rb b/spec/ruby/core/integer/abs_spec.rb index c40356db125335..768eebdce21cac 100644 --- a/spec/ruby/core/integer/abs_spec.rb +++ b/spec/ruby/core/integer/abs_spec.rb @@ -1,6 +1,20 @@ require_relative '../../spec_helper' -require_relative 'shared/abs' describe "Integer#abs" do - it_behaves_like :integer_abs, :abs + context "fixnum" do + it "returns self's absolute fixnum value" do + { 0 => [0, -0, +0], 2 => [2, -2, +2], 100 => [100, -100, +100] }.each do |key, values| + values.each do |value| + value.abs.should == key + end + end + end + end + + context "bignum" do + it "returns the absolute bignum value" do + bignum_value(39).abs.should == 18446744073709551655 + (-bignum_value(18)).abs.should == 18446744073709551634 + end + end end diff --git a/spec/ruby/core/integer/case_compare_spec.rb b/spec/ruby/core/integer/case_compare_spec.rb index e5dde2c64ab347..1e0c6cb411ee19 100644 --- a/spec/ruby/core/integer/case_compare_spec.rb +++ b/spec/ruby/core/integer/case_compare_spec.rb @@ -1,6 +1,7 @@ require_relative '../../spec_helper' -require_relative 'shared/equal' describe "Integer#===" do - it_behaves_like :integer_equal, :=== + it "is an alias of Integer#==" do + Integer.instance_method(:===).should == Integer.instance_method(:==) + end end diff --git a/spec/ruby/core/integer/equal_value_spec.rb b/spec/ruby/core/integer/equal_value_spec.rb index 67a73713aff244..dc7355226744cc 100644 --- a/spec/ruby/core/integer/equal_value_spec.rb +++ b/spec/ruby/core/integer/equal_value_spec.rb @@ -1,6 +1,65 @@ require_relative '../../spec_helper' -require_relative 'shared/equal' describe "Integer#==" do - it_behaves_like :integer_equal, :== + context "fixnum" do + it "returns true if self has the same value as other" do + (1 == 1).should == true + (9 == 5).should == false + + # Actually, these call Float#==, Integer#== etc. + (9 == 9.0).should == true + (9 == 9.01).should == false + + (10 == bignum_value).should == false + end + + it "calls 'other == self' if the given argument is not an Integer" do + (1 == '*').should == false + + obj = mock('one other') + obj.should_receive(:==).any_number_of_times.and_return(false) + (1 == obj).should == false + + obj = mock('another') + obj.should_receive(:==).any_number_of_times.and_return(true) + (2 == obj).should == true + end + end + + context "bignum" do + before :each do + @bignum = bignum_value + end + + it "returns true if self has the same value as the given argument" do + (@bignum == @bignum).should == true + (@bignum == @bignum.to_f).should == true + + (@bignum == @bignum + 1).should == false + ((@bignum + 1) == @bignum).should == false + + (@bignum == 9).should == false + (@bignum == 9.01).should == false + + (@bignum == bignum_value(10)).should == false + end + + it "calls 'other == self' if the given argument is not an Integer" do + obj = mock('not integer') + obj.should_receive(:==).and_return(true) + (@bignum == obj).should == true + end + + it "returns the result of 'other == self' as a boolean" do + obj = mock('not integer') + obj.should_receive(:==).exactly(2).times.and_return("woot", nil) + (@bignum == obj).should == true + (@bignum == obj).should == false + end + + it "does not lose precision when comparing with a Float" do + ((bignum_value(1) == bignum_value.to_f)).should == false + ((bignum_value == bignum_value.to_f)).should == true + end + end end diff --git a/spec/ruby/core/integer/inspect_spec.rb b/spec/ruby/core/integer/inspect_spec.rb new file mode 100644 index 00000000000000..0b0d5cc7a95e5e --- /dev/null +++ b/spec/ruby/core/integer/inspect_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' + +describe "Integer#inspect" do + it "is an alias of Integer#to_s" do + Integer.instance_method(:inspect).should == Integer.instance_method(:to_s) + end +end diff --git a/spec/ruby/core/integer/magnitude_spec.rb b/spec/ruby/core/integer/magnitude_spec.rb index 48cf1a85342072..000e5be7f754d3 100644 --- a/spec/ruby/core/integer/magnitude_spec.rb +++ b/spec/ruby/core/integer/magnitude_spec.rb @@ -1,6 +1,7 @@ require_relative '../../spec_helper' -require_relative 'shared/abs' describe "Integer#magnitude" do - it_behaves_like :integer_abs, :magnitude + it "is an alias of Integer#abs" do + Integer.instance_method(:magnitude).should == Integer.instance_method(:abs) + end end diff --git a/spec/ruby/core/integer/modulo_spec.rb b/spec/ruby/core/integer/modulo_spec.rb index e263338e381ec1..2680f49510e260 100644 --- a/spec/ruby/core/integer/modulo_spec.rb +++ b/spec/ruby/core/integer/modulo_spec.rb @@ -1,10 +1,122 @@ require_relative '../../spec_helper' -require_relative 'shared/modulo' describe "Integer#%" do - it_behaves_like :integer_modulo, :% + context "fixnum" do + it "returns the modulus obtained from dividing self by the given argument" do + # test all possible combinations: + # - integer/double/bignum argument + # - positive/negative argument + # - positive/negative self + # - self greater/smaller than argument + + (13 % 4).should == 1 + (4 % 13).should == 4 + + (13 % 4.0).should == 1 + (4 % 13.0).should == 4 + + (-200 % 256).should == 56 + (-1000 % 512).should == 24 + + (-200 % -256).should == -200 + (-1000 % -512).should == -488 + + (200 % -256).should == -56 + (1000 % -512).should == -24 + + (13 % -4.0).should == -3.0 + (4 % -13.0).should == -9.0 + + (-13 % -4.0).should == -1.0 + (-4 % -13.0).should == -4.0 + + (-13 % 4.0).should == 3.0 + (-4 % 13.0).should == 9.0 + + (1 % 2.0).should == 1.0 + (200 % bignum_value).should == 200 + + (4 % bignum_value(10)).should == 4 + (4 % -bignum_value(10)).should == -18446744073709551622 + (-4 % bignum_value(10)).should == 18446744073709551622 + (-4 % -bignum_value(10)).should == -4 + end + + it "raises a ZeroDivisionError when the given argument is 0" do + -> { 13 % 0 }.should.raise(ZeroDivisionError) + -> { 0 % 0 }.should.raise(ZeroDivisionError) + -> { -10 % 0 }.should.raise(ZeroDivisionError) + end + + it "raises a ZeroDivisionError when the given argument is 0 and a Float" do + -> { 0 % 0.0 }.should.raise(ZeroDivisionError) + -> { 10 % 0.0 }.should.raise(ZeroDivisionError) + -> { -10 % 0.0 }.should.raise(ZeroDivisionError) + end + + it "raises a TypeError when given a non-Integer" do + -> { + (obj = mock('10')).should_receive(:to_int).any_number_of_times.and_return(10) + 13 % obj + }.should.raise(TypeError) + -> { 13 % "10" }.should.raise(TypeError) + -> { 13 % :symbol }.should.raise(TypeError) + end + end + + context "bignum" do + before :each do + @bignum = bignum_value(10) + end + + it "returns the modulus obtained from dividing self by the given argument" do + # test all possible combinations: + # - integer/double/bignum argument + # - positive/negative argument + # - positive/negative self + # - self greater/smaller than argument + + (@bignum % 5).should == 1 + (@bignum % -5).should == -4 + (-@bignum % 5).should == 4 + (-@bignum % -5).should == -1 + + (@bignum % 2.22).should be_close(1.5603603603605034, TOLERANCE) + (@bignum % -2.22).should be_close(-0.6596396396394968, TOLERANCE) + (-@bignum % 2.22).should be_close(0.6596396396394968, TOLERANCE) + (-@bignum % -2.22).should be_close(-1.5603603603605034, TOLERANCE) + + (@bignum % (@bignum + 10)).should == 18446744073709551626 + (@bignum % -(@bignum + 10)).should == -10 + (-@bignum % (@bignum + 10)).should == 10 + (-@bignum % -(@bignum + 10)).should == -18446744073709551626 + + ((@bignum + 10) % @bignum).should == 10 + ((@bignum + 10) % -@bignum).should == -18446744073709551616 + (-(@bignum + 10) % @bignum).should == 18446744073709551616 + (-(@bignum + 10) % -@bignum).should == -10 + end + + it "raises a ZeroDivisionError when the given argument is 0" do + -> { @bignum % 0 }.should.raise(ZeroDivisionError) + -> { -@bignum % 0 }.should.raise(ZeroDivisionError) + end + + it "raises a ZeroDivisionError when the given argument is 0 and a Float" do + -> { @bignum % 0.0 }.should.raise(ZeroDivisionError) + -> { -@bignum % 0.0 }.should.raise(ZeroDivisionError) + end + + it "raises a TypeError when given a non-Integer" do + -> { @bignum % mock('10') }.should.raise(TypeError) + -> { @bignum % "10" }.should.raise(TypeError) + -> { @bignum % :symbol }.should.raise(TypeError) + end + end end describe "Integer#modulo" do - it_behaves_like :integer_modulo, :modulo + it "is an alias of Integer#%" do + Integer.instance_method(:modulo).should == Integer.instance_method(:%) + end end diff --git a/spec/ruby/core/integer/next_spec.rb b/spec/ruby/core/integer/next_spec.rb index 63c4e678936ee3..da54dec454b872 100644 --- a/spec/ruby/core/integer/next_spec.rb +++ b/spec/ruby/core/integer/next_spec.rb @@ -1,6 +1,7 @@ require_relative '../../spec_helper' -require_relative 'shared/next' describe "Integer#next" do - it_behaves_like :integer_next, :next + it "is an alias of Integer#succ" do + Integer.instance_method(:next).should == Integer.instance_method(:succ) + end end diff --git a/spec/ruby/core/integer/shared/abs.rb b/spec/ruby/core/integer/shared/abs.rb deleted file mode 100644 index 43844c9065e8fb..00000000000000 --- a/spec/ruby/core/integer/shared/abs.rb +++ /dev/null @@ -1,18 +0,0 @@ -describe :integer_abs, shared: true do - context "fixnum" do - it "returns self's absolute fixnum value" do - { 0 => [0, -0, +0], 2 => [2, -2, +2], 100 => [100, -100, +100] }.each do |key, values| - values.each do |value| - value.send(@method).should == key - end - end - end - end - - context "bignum" do - it "returns the absolute bignum value" do - bignum_value(39).send(@method).should == 18446744073709551655 - (-bignum_value(18)).send(@method).should == 18446744073709551634 - end - end -end diff --git a/spec/ruby/core/integer/shared/equal.rb b/spec/ruby/core/integer/shared/equal.rb deleted file mode 100644 index c621ba3f81a0ca..00000000000000 --- a/spec/ruby/core/integer/shared/equal.rb +++ /dev/null @@ -1,63 +0,0 @@ -describe :integer_equal, shared: true do - context "fixnum" do - it "returns true if self has the same value as other" do - 1.send(@method, 1).should == true - 9.send(@method, 5).should == false - - # Actually, these call Float#==, Integer#== etc. - 9.send(@method, 9.0).should == true - 9.send(@method, 9.01).should == false - - 10.send(@method, bignum_value).should == false - end - - it "calls 'other == self' if the given argument is not an Integer" do - 1.send(@method, '*').should == false - - obj = mock('one other') - obj.should_receive(:==).any_number_of_times.and_return(false) - 1.send(@method, obj).should == false - - obj = mock('another') - obj.should_receive(:==).any_number_of_times.and_return(true) - 2.send(@method, obj).should == true - end - end - - context "bignum" do - before :each do - @bignum = bignum_value - end - - it "returns true if self has the same value as the given argument" do - @bignum.send(@method, @bignum).should == true - @bignum.send(@method, @bignum.to_f).should == true - - @bignum.send(@method, @bignum + 1).should == false - (@bignum + 1).send(@method, @bignum).should == false - - @bignum.send(@method, 9).should == false - @bignum.send(@method, 9.01).should == false - - @bignum.send(@method, bignum_value(10)).should == false - end - - it "calls 'other == self' if the given argument is not an Integer" do - obj = mock('not integer') - obj.should_receive(:==).and_return(true) - @bignum.send(@method, obj).should == true - end - - it "returns the result of 'other == self' as a boolean" do - obj = mock('not integer') - obj.should_receive(:==).exactly(2).times.and_return("woot", nil) - @bignum.send(@method, obj).should == true - @bignum.send(@method, obj).should == false - end - - it "does not lose precision when comparing with a Float" do - (bignum_value(1).send(@method, bignum_value.to_f)).should == false - (bignum_value.send(@method, bignum_value.to_f)).should == true - end - end -end diff --git a/spec/ruby/core/integer/shared/modulo.rb b/spec/ruby/core/integer/shared/modulo.rb deleted file mode 100644 index d0b5e26ed53600..00000000000000 --- a/spec/ruby/core/integer/shared/modulo.rb +++ /dev/null @@ -1,114 +0,0 @@ -describe :integer_modulo, shared: true do - context "fixnum" do - it "returns the modulus obtained from dividing self by the given argument" do - # test all possible combinations: - # - integer/double/bignum argument - # - positive/negative argument - # - positive/negative self - # - self greater/smaller than argument - - 13.send(@method, 4).should == 1 - 4.send(@method, 13).should == 4 - - 13.send(@method, 4.0).should == 1 - 4.send(@method, 13.0).should == 4 - - (-200).send(@method, 256).should == 56 - (-1000).send(@method, 512).should == 24 - - (-200).send(@method, -256).should == -200 - (-1000).send(@method, -512).should == -488 - - (200).send(@method, -256).should == -56 - (1000).send(@method, -512).should == -24 - - 13.send(@method, -4.0).should == -3.0 - 4.send(@method, -13.0).should == -9.0 - - -13.send(@method, -4.0).should == -1.0 - -4.send(@method, -13.0).should == -4.0 - - -13.send(@method, 4.0).should == 3.0 - -4.send(@method, 13.0).should == 9.0 - - 1.send(@method, 2.0).should == 1.0 - 200.send(@method, bignum_value).should == 200 - - 4.send(@method, bignum_value(10)).should == 4 - 4.send(@method, -bignum_value(10)).should == -18446744073709551622 - -4.send(@method, bignum_value(10)).should == 18446744073709551622 - -4.send(@method, -bignum_value(10)).should == -4 - end - - it "raises a ZeroDivisionError when the given argument is 0" do - -> { 13.send(@method, 0) }.should.raise(ZeroDivisionError) - -> { 0.send(@method, 0) }.should.raise(ZeroDivisionError) - -> { -10.send(@method, 0) }.should.raise(ZeroDivisionError) - end - - it "raises a ZeroDivisionError when the given argument is 0 and a Float" do - -> { 0.send(@method, 0.0) }.should.raise(ZeroDivisionError) - -> { 10.send(@method, 0.0) }.should.raise(ZeroDivisionError) - -> { -10.send(@method, 0.0) }.should.raise(ZeroDivisionError) - end - - it "raises a TypeError when given a non-Integer" do - -> { - (obj = mock('10')).should_receive(:to_int).any_number_of_times.and_return(10) - 13.send(@method, obj) - }.should.raise(TypeError) - -> { 13.send(@method, "10") }.should.raise(TypeError) - -> { 13.send(@method, :symbol) }.should.raise(TypeError) - end - end - - context "bignum" do - before :each do - @bignum = bignum_value(10) - end - - it "returns the modulus obtained from dividing self by the given argument" do - # test all possible combinations: - # - integer/double/bignum argument - # - positive/negative argument - # - positive/negative self - # - self greater/smaller than argument - - @bignum.send(@method, 5).should == 1 - @bignum.send(@method, -5).should == -4 - (-@bignum).send(@method, 5).should == 4 - (-@bignum).send(@method, -5).should == -1 - - @bignum.send(@method, 2.22).should be_close(1.5603603603605034, TOLERANCE) - @bignum.send(@method, -2.22).should be_close(-0.6596396396394968, TOLERANCE) - (-@bignum).send(@method, 2.22).should be_close(0.6596396396394968, TOLERANCE) - (-@bignum).send(@method, -2.22).should be_close(-1.5603603603605034, TOLERANCE) - - @bignum.send(@method, @bignum + 10).should == 18446744073709551626 - @bignum.send(@method, -(@bignum + 10)).should == -10 - (-@bignum).send(@method, @bignum + 10).should == 10 - (-@bignum).send(@method, -(@bignum + 10)).should == -18446744073709551626 - - (@bignum + 10).send(@method, @bignum).should == 10 - (@bignum + 10).send(@method, -@bignum).should == -18446744073709551616 - (-(@bignum + 10)).send(@method, @bignum).should == 18446744073709551616 - (-(@bignum + 10)).send(@method, -@bignum).should == -10 - end - - it "raises a ZeroDivisionError when the given argument is 0" do - -> { @bignum.send(@method, 0) }.should.raise(ZeroDivisionError) - -> { (-@bignum).send(@method, 0) }.should.raise(ZeroDivisionError) - end - - it "raises a ZeroDivisionError when the given argument is 0 and a Float" do - -> { @bignum.send(@method, 0.0) }.should.raise(ZeroDivisionError) - -> { -@bignum.send(@method, 0.0) }.should.raise(ZeroDivisionError) - end - - it "raises a TypeError when given a non-Integer" do - -> { @bignum.send(@method, mock('10')) }.should.raise(TypeError) - -> { @bignum.send(@method, "10") }.should.raise(TypeError) - -> { @bignum.send(@method, :symbol) }.should.raise(TypeError) - end - end -end diff --git a/spec/ruby/core/integer/shared/next.rb b/spec/ruby/core/integer/shared/next.rb deleted file mode 100644 index 85b83d69654718..00000000000000 --- a/spec/ruby/core/integer/shared/next.rb +++ /dev/null @@ -1,25 +0,0 @@ -describe :integer_next, shared: true do - it "returns the next larger positive Fixnum" do - 2.send(@method).should == 3 - end - - it "returns the next larger negative Fixnum" do - (-2).send(@method).should == -1 - end - - it "returns the next larger positive Bignum" do - bignum_value.send(@method).should == bignum_value(1) - end - - it "returns the next larger negative Bignum" do - (-bignum_value(1)).send(@method).should == -bignum_value - end - - it "overflows a Fixnum to a Bignum" do - fixnum_max.send(@method).should == fixnum_max + 1 - end - - it "underflows a Bignum to a Fixnum" do - (fixnum_min - 1).send(@method).should == fixnum_min - end -end diff --git a/spec/ruby/core/integer/succ_spec.rb b/spec/ruby/core/integer/succ_spec.rb index 9ae9a14fe79a60..2201a4c76df508 100644 --- a/spec/ruby/core/integer/succ_spec.rb +++ b/spec/ruby/core/integer/succ_spec.rb @@ -1,6 +1,27 @@ require_relative '../../spec_helper' -require_relative 'shared/next' describe "Integer#succ" do - it_behaves_like :integer_next, :succ + it "returns the next larger positive Fixnum" do + 2.succ.should == 3 + end + + it "returns the next larger negative Fixnum" do + (-2).succ.should == -1 + end + + it "returns the next larger positive Bignum" do + bignum_value.succ.should == bignum_value(1) + end + + it "returns the next larger negative Bignum" do + (-bignum_value(1)).succ.should == -bignum_value + end + + it "overflows a Fixnum to a Bignum" do + fixnum_max.succ.should == fixnum_max + 1 + end + + it "underflows a Bignum to a Fixnum" do + (fixnum_min - 1).succ.should == fixnum_min + end end diff --git a/spec/ruby/core/io/each_char_spec.rb b/spec/ruby/core/io/each_char_spec.rb index 5d460a1e7cf86d..7c1ca4f0694e1e 100644 --- a/spec/ruby/core/io/each_char_spec.rb +++ b/spec/ruby/core/io/each_char_spec.rb @@ -1,12 +1,75 @@ -# -*- encoding: utf-8 -*- require_relative '../../spec_helper' require_relative 'fixtures/classes' -require_relative 'shared/chars' describe "IO#each_char" do - it_behaves_like :io_chars, :each_char + before :each do + @io = IOSpecs.io_fixture "lines.txt" + ScratchPad.record [] + end + + after :each do + @io.close unless @io.closed? + end + + it "yields each character" do + @io.readline.should == "Voici la ligne une.\n" + + count = 0 + @io.each_char do |c| + ScratchPad << c + break if 4 < count += 1 + end + + ScratchPad.recorded.should == ["Q", "u", "i", " ", "è"] + end + + describe "when no block is given" do + it "returns an Enumerator" do + enum = @io.each_char + enum.should.instance_of?(Enumerator) + enum.first(5).should == ["V", "o", "i", "c", "i"] + end + + describe "returned Enumerator" do + describe "size" do + it "should return nil" do + @io.each_char.size.should == nil + end + end + end + end + + it "returns itself" do + @io.each_char { |c| }.should.equal?(@io) + end + + it "returns an enumerator for a closed stream" do + IOSpecs.closed_io.each_char.should.instance_of?(Enumerator) + end + + it "raises an IOError when an enumerator created on a closed stream is accessed" do + -> { IOSpecs.closed_io.each_char.first }.should.raise(IOError) + end + + it "raises IOError on closed stream" do + -> { IOSpecs.closed_io.each_char {} }.should.raise(IOError) + end end describe "IO#each_char" do - it_behaves_like :io_chars_empty, :each_char + before :each do + @name = tmp("io_each_char") + @io = new_io @name, "w+:utf-8" + ScratchPad.record [] + end + + after :each do + @io.close unless @io.closed? + rm_r @name + end + + it "does not yield any characters on an empty stream" do + @io.each_char { |c| ScratchPad << c } + ScratchPad.recorded.should == [] + end end diff --git a/spec/ruby/core/io/each_codepoint_spec.rb b/spec/ruby/core/io/each_codepoint_spec.rb index 26cc87fc0e49a9..758a524986b610 100644 --- a/spec/ruby/core/io/each_codepoint_spec.rb +++ b/spec/ruby/core/io/each_codepoint_spec.rb @@ -1,10 +1,57 @@ require_relative '../../spec_helper' require_relative 'fixtures/classes' -require_relative 'shared/codepoints' # See redmine #1667 describe "IO#each_codepoint" do - it_behaves_like :io_codepoints, :each_codepoint + before :each do + @io = IOSpecs.io_fixture "lines.txt" + @enum = @io.each_codepoint + end + + after :each do + @io.close + end + + describe "when no block is given" do + it "returns an Enumerator" do + @enum.should.instance_of?(Enumerator) + end + + describe "returned Enumerator" do + describe "size" do + it "should return nil" do + @enum.size.should == nil + end + end + end + end + + it "yields each codepoint" do + @enum.first(25).should == [ + 86, 111, 105, 99, 105, 32, 108, 97, 32, 108, 105, 103, 110, + 101, 32, 117, 110, 101, 46, 10, 81, 117, 105, 32, 232 + ] + end + + it "yields each codepoint starting from the current position" do + @io.pos = 130 + @enum.to_a.should == [101, 32, 115, 105, 120, 46, 10] + end + + it "raises an error if reading invalid sequence" do + @io.pos = 60 # inside of a multibyte sequence + -> { @enum.first }.should.raise(ArgumentError) + end + + it "does not change $_" do + $_ = "test" + @enum.to_a + $_.should == "test" + end + + it "raises an IOError when self is not readable" do + -> { IOSpecs.closed_io.each_codepoint.to_a }.should.raise(IOError) + end end describe "IO#each_codepoint" do diff --git a/spec/ruby/core/io/each_line_spec.rb b/spec/ruby/core/io/each_line_spec.rb index 58d26b325d3639..bcda4040b881d5 100644 --- a/spec/ruby/core/io/each_line_spec.rb +++ b/spec/ruby/core/io/each_line_spec.rb @@ -1,11 +1,251 @@ require_relative '../../spec_helper' require_relative 'fixtures/classes' -require_relative 'shared/each' describe "IO#each_line" do - it_behaves_like :io_each, :each_line + before :each do + @io = IOSpecs.io_fixture "lines.txt" + ScratchPad.record [] + end + + after :each do + @io.close if @io + end + + describe "with no separator" do + it "yields each line to the passed block" do + @io.each_line { |s| ScratchPad << s } + ScratchPad.recorded.should == IOSpecs.lines + end + + it "yields each line starting from the current position" do + @io.pos = 41 + @io.each_line { |s| ScratchPad << s } + ScratchPad.recorded.should == IOSpecs.lines[2..-1] + end + + it "returns self" do + @io.each_line { |l| l }.should.equal?(@io) + end + + it "does not change $_" do + $_ = "test" + @io.each_line { |s| s } + $_.should == "test" + end + + it "raises an IOError when self is not readable" do + -> { IOSpecs.closed_io.each_line {} }.should.raise(IOError) + end + + it "makes line count accessible via lineno" do + @io.each_line { ScratchPad << @io.lineno } + ScratchPad.recorded.should == [ 1,2,3,4,5,6,7,8,9 ] + end + + it "makes line count accessible via $." do + @io.each_line { ScratchPad << $. } + ScratchPad.recorded.should == [ 1,2,3,4,5,6,7,8,9 ] + end + + describe "when no block is given" do + it "returns an Enumerator" do + enum = @io.each_line + enum.should.instance_of?(Enumerator) + + enum.each { |l| ScratchPad << l } + ScratchPad.recorded.should == IOSpecs.lines + end + + describe "returned Enumerator" do + describe "size" do + it "should return nil" do + @io.each_line.size.should == nil + end + end + end + end + end + + describe "with limit" do + describe "when limit is 0" do + it "raises an ArgumentError" do + # must pass block so Enumerator is evaluated and raises + -> { @io.each_line(0){} }.should.raise(ArgumentError) + end + end + + it "does not accept Integers that don't fit in a C off_t" do + -> { @io.each_line(2**128){} }.should.raise(RangeError) + end + end + + describe "when passed a String containing one space as a separator" do + it "uses the passed argument as the line separator" do + @io.each_line(" ") { |s| ScratchPad << s } + ScratchPad.recorded.should == IOSpecs.lines_space_separator + end + + it "does not change $_" do + $_ = "test" + @io.each_line(" ") { |s| } + $_.should == "test" + end + + it "tries to convert the passed separator to a String using #to_str" do + obj = mock("to_str") + obj.stub!(:to_str).and_return(" ") + + @io.each_line(obj) { |l| ScratchPad << l } + ScratchPad.recorded.should == IOSpecs.lines_space_separator + end + end + + describe "when passed nil as a separator" do + it "yields self's content starting from the current position when the passed separator is nil" do + @io.pos = 100 + @io.each_line(nil) { |s| ScratchPad << s } + ScratchPad.recorded.should == ["qui a linha cinco.\nHere is line six.\n"] + end + end + + describe "when passed an empty String as a separator" do + it "yields each paragraph" do + @io.each_line("") { |s| ScratchPad << s } + ScratchPad.recorded.should == IOSpecs.paragraphs + end + + it "discards leading newlines" do + @io.readline + @io.readline + @io.each_line("") { |s| ScratchPad << s } + ScratchPad.recorded.should == IOSpecs.paragraphs[1..-1] + end + end + + describe "with both separator and limit" do + describe "when no block is given" do + it "returns an Enumerator" do + enum = @io.each_line(nil, 1024) + enum.should.instance_of?(Enumerator) + + enum.each { |l| ScratchPad << l } + ScratchPad.recorded.should == [IOSpecs.lines.join] + end + + describe "returned Enumerator" do + describe "size" do + it "should return nil" do + @io.each_line(nil, 1024).size.should == nil + end + end + end + end + + describe "when a block is given" do + it "accepts an empty block" do + @io.each_line(nil, 1024) {}.should.equal?(@io) + end + + describe "when passed nil as a separator" do + it "yields self's content starting from the current position when the passed separator is nil" do + @io.pos = 100 + @io.each_line(nil, 1024) { |s| ScratchPad << s } + ScratchPad.recorded.should == ["qui a linha cinco.\nHere is line six.\n"] + end + end + + describe "when passed an empty String as a separator" do + it "yields each paragraph" do + @io.each_line("", 1024) { |s| ScratchPad << s } + ScratchPad.recorded.should == IOSpecs.paragraphs + end + + it "discards leading newlines" do + @io.readline + @io.readline + @io.each_line("", 1024) { |s| ScratchPad << s } + ScratchPad.recorded.should == IOSpecs.paragraphs[1..-1] + end + end + end + end + + describe "when passed chomp" do + it "yields each line without trailing newline characters to the passed block" do + @io.each_line(chomp: true) { |s| ScratchPad << s } + ScratchPad.recorded.should == IOSpecs.lines_without_newline_characters + end + + it "raises exception when options passed as Hash" do + -> { + @io.each_line({ chomp: true }) { |s| } + }.should.raise(TypeError) + + -> { + @io.each_line("\n", 1, { chomp: true }) { |s| } + }.should.raise(ArgumentError, "wrong number of arguments (given 3, expected 0..2)") + end + end + + describe "when passed chomp and a separator" do + it "yields each line without separator to the passed block" do + @io.each_line(" ", chomp: true) { |s| ScratchPad << s } + ScratchPad.recorded.should == IOSpecs.lines_space_separator_without_trailing_spaces + end + end + + describe "when passed chomp and empty line as a separator" do + it "yields each paragraph without trailing new line characters" do + @io.each_line("", 1024, chomp: true) { |s| ScratchPad << s } + ScratchPad.recorded.should == IOSpecs.paragraphs_without_trailing_new_line_characters + end + end + + describe "when passed chomp and nil as a separator" do + it "yields self's content" do + @io.pos = 100 + @io.each_line(nil, chomp: true) { |s| ScratchPad << s } + ScratchPad.recorded.should == ["qui a linha cinco.\nHere is line six.\n"] + end + end + + describe "when passed chomp, nil as a separator, and a limit" do + it "yields each line of limit size without truncating trailing new line character" do + # 43 - is a size of the 1st paragraph in the file + @io.each_line(nil, 43, chomp: true) { |s| ScratchPad << s } + + ScratchPad.recorded.should == [ + "Voici la ligne une.\nQui è la linea due.\n\n\n", + "Aquí está la línea tres.\n" + "Hier ist Zeile ", + "vier.\n\nEstá aqui a linha cinco.\nHere is li", + "ne six.\n" + ] + end + end + + describe "when passed too many arguments" do + it "raises ArgumentError" do + -> { + @io.each_line("", 1, "excess argument", chomp: true) {} + }.should.raise(ArgumentError) + end + end end describe "IO#each_line" do - it_behaves_like :io_each_default_separator, :each_line + before :each do + @io = IOSpecs.io_fixture "lines.txt" + ScratchPad.record [] + suppress_warning {@sep, $/ = $/, " "} + end + + after :each do + @io.close if @io + suppress_warning {$/ = @sep} + end + + it "uses $/ as the default line separator" do + @io.each_line { |s| ScratchPad << s } + ScratchPad.recorded.should == IOSpecs.lines_space_separator + end end diff --git a/spec/ruby/core/io/each_spec.rb b/spec/ruby/core/io/each_spec.rb index 91ecbd19c8ecd2..594052256ea887 100644 --- a/spec/ruby/core/io/each_spec.rb +++ b/spec/ruby/core/io/each_spec.rb @@ -1,11 +1,7 @@ require_relative '../../spec_helper' -require_relative 'fixtures/classes' -require_relative 'shared/each' describe "IO#each" do - it_behaves_like :io_each, :each -end - -describe "IO#each" do - it_behaves_like :io_each_default_separator, :each + it "is an alias of IO#each_line" do + IO.instance_method(:each).should == IO.instance_method(:each_line) + end end diff --git a/spec/ruby/core/io/eof_spec.rb b/spec/ruby/core/io/eof_spec.rb index c8955abde0ebd4..561daa4ec3b1aa 100644 --- a/spec/ruby/core/io/eof_spec.rb +++ b/spec/ruby/core/io/eof_spec.rb @@ -105,3 +105,9 @@ @r.should.eof? end end + +describe "IO#eof" do + it "is an alias of IO#eof?" do + IO.instance_method(:eof).should == IO.instance_method(:eof?) + end +end diff --git a/spec/ruby/core/io/foreach_spec.rb b/spec/ruby/core/io/foreach_spec.rb index 015988f9fb1d19..ccd2f255170485 100644 --- a/spec/ruby/core/io/foreach_spec.rb +++ b/spec/ruby/core/io/foreach_spec.rb @@ -28,7 +28,7 @@ ScratchPad.recorded.should == ["hello\n", "line2\n"] end - platform_is_not :windows do + guard -> { Process.respond_to?(:fork) } do it "gets data from a fork when passed -" do parent_pid = $$ diff --git a/spec/ruby/core/io/isatty_spec.rb b/spec/ruby/core/io/isatty_spec.rb index 3b6c69b1906739..60b97d21be63a9 100644 --- a/spec/ruby/core/io/isatty_spec.rb +++ b/spec/ruby/core/io/isatty_spec.rb @@ -1,6 +1,7 @@ require_relative '../../spec_helper' -require_relative 'shared/tty' describe "IO#isatty" do - it_behaves_like :io_tty, :isatty + it "is an alias of IO#tty?" do + IO.instance_method(:isatty).should == IO.instance_method(:tty?) + end end diff --git a/spec/ruby/core/io/pos_spec.rb b/spec/ruby/core/io/pos_spec.rb index e6cda2643dbfaf..bbe25ce97b88d8 100644 --- a/spec/ruby/core/io/pos_spec.rb +++ b/spec/ruby/core/io/pos_spec.rb @@ -3,7 +3,37 @@ require_relative 'shared/pos' describe "IO#pos" do - it_behaves_like :io_pos, :pos + before :each do + @fname = tmp('test.txt') + File.open(@fname, 'w') { |f| f.write "123" } + end + + after :each do + rm_r @fname + end + + it "gets the offset" do + File.open @fname do |f| + f.pos.should == 0 + f.read 1 + f.pos.should == 1 + f.read 2 + f.pos.should == 3 + end + end + + it "raises IOError on closed stream" do + -> { IOSpecs.closed_io.pos }.should.raise(IOError) + end + + it "resets #eof?" do + open @fname do |io| + io.read 1 + io.read 1 + io.pos + io.should_not.eof? + end + end end describe "IO#pos=" do diff --git a/spec/ruby/core/io/read_spec.rb b/spec/ruby/core/io/read_spec.rb index dd787c9b60d85b..5be969e2801248 100644 --- a/spec/ruby/core/io/read_spec.rb +++ b/spec/ruby/core/io/read_spec.rb @@ -163,7 +163,7 @@ end end - platform_is_not :windows do + guard -> { Process.respond_to?(:fork) } do it "opens a pipe to a fork if the rest is -" do str = nil suppress_warning do # https://bugs.ruby-lang.org/issues/19630 diff --git a/spec/ruby/core/io/readlines_spec.rb b/spec/ruby/core/io/readlines_spec.rb index 640e2532004c6e..d41d24d7d17b95 100644 --- a/spec/ruby/core/io/readlines_spec.rb +++ b/spec/ruby/core/io/readlines_spec.rb @@ -189,7 +189,7 @@ lines.should == ["hello\n", "line2\n"] end - platform_is_not :windows do + guard -> { Process.respond_to?(:fork) } do it "gets data from a fork when passed -" do lines = nil suppress_warning do # https://bugs.ruby-lang.org/issues/19630 diff --git a/spec/ruby/core/io/shared/chars.rb b/spec/ruby/core/io/shared/chars.rb deleted file mode 100644 index efd4d5ee104097..00000000000000 --- a/spec/ruby/core/io/shared/chars.rb +++ /dev/null @@ -1,73 +0,0 @@ -# -*- encoding: utf-8 -*- -describe :io_chars, shared: true do - before :each do - @io = IOSpecs.io_fixture "lines.txt" - ScratchPad.record [] - end - - after :each do - @io.close unless @io.closed? - end - - it "yields each character" do - @io.readline.should == "Voici la ligne une.\n" - - count = 0 - @io.send(@method) do |c| - ScratchPad << c - break if 4 < count += 1 - end - - ScratchPad.recorded.should == ["Q", "u", "i", " ", "è"] - end - - describe "when no block is given" do - it "returns an Enumerator" do - enum = @io.send(@method) - enum.should.instance_of?(Enumerator) - enum.first(5).should == ["V", "o", "i", "c", "i"] - end - - describe "returned Enumerator" do - describe "size" do - it "should return nil" do - @io.send(@method).size.should == nil - end - end - end - end - - it "returns itself" do - @io.send(@method) { |c| }.should.equal?(@io) - end - - it "returns an enumerator for a closed stream" do - IOSpecs.closed_io.send(@method).should.instance_of?(Enumerator) - end - - it "raises an IOError when an enumerator created on a closed stream is accessed" do - -> { IOSpecs.closed_io.send(@method).first }.should.raise(IOError) - end - - it "raises IOError on closed stream" do - -> { IOSpecs.closed_io.send(@method) {} }.should.raise(IOError) - end -end - -describe :io_chars_empty, shared: true do - before :each do - @name = tmp("io_each_char") - @io = new_io @name, "w+:utf-8" - ScratchPad.record [] - end - - after :each do - @io.close unless @io.closed? - rm_r @name - end - - it "does not yield any characters on an empty stream" do - @io.send(@method) { |c| ScratchPad << c } - ScratchPad.recorded.should == [] - end -end diff --git a/spec/ruby/core/io/shared/codepoints.rb b/spec/ruby/core/io/shared/codepoints.rb deleted file mode 100644 index 21c756986f6830..00000000000000 --- a/spec/ruby/core/io/shared/codepoints.rb +++ /dev/null @@ -1,54 +0,0 @@ -# -*- encoding: utf-8 -*- -require_relative '../fixtures/classes' - -describe :io_codepoints, shared: true do - before :each do - @io = IOSpecs.io_fixture "lines.txt" - @enum = @io.send(@method) - end - - after :each do - @io.close - end - - describe "when no block is given" do - it "returns an Enumerator" do - @enum.should.instance_of?(Enumerator) - end - - describe "returned Enumerator" do - describe "size" do - it "should return nil" do - @enum.size.should == nil - end - end - end - end - - it "yields each codepoint" do - @enum.first(25).should == [ - 86, 111, 105, 99, 105, 32, 108, 97, 32, 108, 105, 103, 110, - 101, 32, 117, 110, 101, 46, 10, 81, 117, 105, 32, 232 - ] - end - - it "yields each codepoint starting from the current position" do - @io.pos = 130 - @enum.to_a.should == [101, 32, 115, 105, 120, 46, 10] - end - - it "raises an error if reading invalid sequence" do - @io.pos = 60 # inside of a multibyte sequence - -> { @enum.first }.should.raise(ArgumentError) - end - - it "does not change $_" do - $_ = "test" - @enum.to_a - $_.should == "test" - end - - it "raises an IOError when self is not readable" do - -> { IOSpecs.closed_io.send(@method).to_a }.should.raise(IOError) - end -end diff --git a/spec/ruby/core/io/shared/each.rb b/spec/ruby/core/io/shared/each.rb deleted file mode 100644 index ae60c3506aae8f..00000000000000 --- a/spec/ruby/core/io/shared/each.rb +++ /dev/null @@ -1,251 +0,0 @@ -# -*- encoding: utf-8 -*- -require_relative '../fixtures/classes' - -describe :io_each, shared: true do - before :each do - @io = IOSpecs.io_fixture "lines.txt" - ScratchPad.record [] - end - - after :each do - @io.close if @io - end - - describe "with no separator" do - it "yields each line to the passed block" do - @io.send(@method) { |s| ScratchPad << s } - ScratchPad.recorded.should == IOSpecs.lines - end - - it "yields each line starting from the current position" do - @io.pos = 41 - @io.send(@method) { |s| ScratchPad << s } - ScratchPad.recorded.should == IOSpecs.lines[2..-1] - end - - it "returns self" do - @io.send(@method) { |l| l }.should.equal?(@io) - end - - it "does not change $_" do - $_ = "test" - @io.send(@method) { |s| s } - $_.should == "test" - end - - it "raises an IOError when self is not readable" do - -> { IOSpecs.closed_io.send(@method) {} }.should.raise(IOError) - end - - it "makes line count accessible via lineno" do - @io.send(@method) { ScratchPad << @io.lineno } - ScratchPad.recorded.should == [ 1,2,3,4,5,6,7,8,9 ] - end - - it "makes line count accessible via $." do - @io.send(@method) { ScratchPad << $. } - ScratchPad.recorded.should == [ 1,2,3,4,5,6,7,8,9 ] - end - - describe "when no block is given" do - it "returns an Enumerator" do - enum = @io.send(@method) - enum.should.instance_of?(Enumerator) - - enum.each { |l| ScratchPad << l } - ScratchPad.recorded.should == IOSpecs.lines - end - - describe "returned Enumerator" do - describe "size" do - it "should return nil" do - @io.send(@method).size.should == nil - end - end - end - end - end - - describe "with limit" do - describe "when limit is 0" do - it "raises an ArgumentError" do - # must pass block so Enumerator is evaluated and raises - -> { @io.send(@method, 0){} }.should.raise(ArgumentError) - end - end - - it "does not accept Integers that don't fit in a C off_t" do - -> { @io.send(@method, 2**128){} }.should.raise(RangeError) - end - end - - describe "when passed a String containing one space as a separator" do - it "uses the passed argument as the line separator" do - @io.send(@method, " ") { |s| ScratchPad << s } - ScratchPad.recorded.should == IOSpecs.lines_space_separator - end - - it "does not change $_" do - $_ = "test" - @io.send(@method, " ") { |s| } - $_.should == "test" - end - - it "tries to convert the passed separator to a String using #to_str" do - obj = mock("to_str") - obj.stub!(:to_str).and_return(" ") - - @io.send(@method, obj) { |l| ScratchPad << l } - ScratchPad.recorded.should == IOSpecs.lines_space_separator - end - end - - describe "when passed nil as a separator" do - it "yields self's content starting from the current position when the passed separator is nil" do - @io.pos = 100 - @io.send(@method, nil) { |s| ScratchPad << s } - ScratchPad.recorded.should == ["qui a linha cinco.\nHere is line six.\n"] - end - end - - describe "when passed an empty String as a separator" do - it "yields each paragraph" do - @io.send(@method, "") { |s| ScratchPad << s } - ScratchPad.recorded.should == IOSpecs.paragraphs - end - - it "discards leading newlines" do - @io.readline - @io.readline - @io.send(@method, "") { |s| ScratchPad << s } - ScratchPad.recorded.should == IOSpecs.paragraphs[1..-1] - end - end - - describe "with both separator and limit" do - describe "when no block is given" do - it "returns an Enumerator" do - enum = @io.send(@method, nil, 1024) - enum.should.instance_of?(Enumerator) - - enum.each { |l| ScratchPad << l } - ScratchPad.recorded.should == [IOSpecs.lines.join] - end - - describe "returned Enumerator" do - describe "size" do - it "should return nil" do - @io.send(@method, nil, 1024).size.should == nil - end - end - end - end - - describe "when a block is given" do - it "accepts an empty block" do - @io.send(@method, nil, 1024) {}.should.equal?(@io) - end - - describe "when passed nil as a separator" do - it "yields self's content starting from the current position when the passed separator is nil" do - @io.pos = 100 - @io.send(@method, nil, 1024) { |s| ScratchPad << s } - ScratchPad.recorded.should == ["qui a linha cinco.\nHere is line six.\n"] - end - end - - describe "when passed an empty String as a separator" do - it "yields each paragraph" do - @io.send(@method, "", 1024) { |s| ScratchPad << s } - ScratchPad.recorded.should == IOSpecs.paragraphs - end - - it "discards leading newlines" do - @io.readline - @io.readline - @io.send(@method, "", 1024) { |s| ScratchPad << s } - ScratchPad.recorded.should == IOSpecs.paragraphs[1..-1] - end - end - end - end - - describe "when passed chomp" do - it "yields each line without trailing newline characters to the passed block" do - @io.send(@method, chomp: true) { |s| ScratchPad << s } - ScratchPad.recorded.should == IOSpecs.lines_without_newline_characters - end - - it "raises exception when options passed as Hash" do - -> { - @io.send(@method, { chomp: true }) { |s| } - }.should.raise(TypeError) - - -> { - @io.send(@method, "\n", 1, { chomp: true }) { |s| } - }.should.raise(ArgumentError, "wrong number of arguments (given 3, expected 0..2)") - end - end - - describe "when passed chomp and a separator" do - it "yields each line without separator to the passed block" do - @io.send(@method, " ", chomp: true) { |s| ScratchPad << s } - ScratchPad.recorded.should == IOSpecs.lines_space_separator_without_trailing_spaces - end - end - - describe "when passed chomp and empty line as a separator" do - it "yields each paragraph without trailing new line characters" do - @io.send(@method, "", 1024, chomp: true) { |s| ScratchPad << s } - ScratchPad.recorded.should == IOSpecs.paragraphs_without_trailing_new_line_characters - end - end - - describe "when passed chomp and nil as a separator" do - it "yields self's content" do - @io.pos = 100 - @io.send(@method, nil, chomp: true) { |s| ScratchPad << s } - ScratchPad.recorded.should == ["qui a linha cinco.\nHere is line six.\n"] - end - end - - describe "when passed chomp, nil as a separator, and a limit" do - it "yields each line of limit size without truncating trailing new line character" do - # 43 - is a size of the 1st paragraph in the file - @io.send(@method, nil, 43, chomp: true) { |s| ScratchPad << s } - - ScratchPad.recorded.should == [ - "Voici la ligne une.\nQui è la linea due.\n\n\n", - "Aquí está la línea tres.\n" + "Hier ist Zeile ", - "vier.\n\nEstá aqui a linha cinco.\nHere is li", - "ne six.\n" - ] - end - end - - describe "when passed too many arguments" do - it "raises ArgumentError" do - -> { - @io.send(@method, "", 1, "excess argument", chomp: true) {} - }.should.raise(ArgumentError) - end - end -end - -describe :io_each_default_separator, shared: true do - before :each do - @io = IOSpecs.io_fixture "lines.txt" - ScratchPad.record [] - suppress_warning {@sep, $/ = $/, " "} - end - - after :each do - @io.close if @io - suppress_warning {$/ = @sep} - end - - it "uses $/ as the default line separator" do - @io.send(@method) { |s| ScratchPad << s } - ScratchPad.recorded.should == IOSpecs.lines_space_separator - end -end diff --git a/spec/ruby/core/io/shared/pos.rb b/spec/ruby/core/io/shared/pos.rb index f4d040586378fa..450058e15947b3 100644 --- a/spec/ruby/core/io/shared/pos.rb +++ b/spec/ruby/core/io/shared/pos.rb @@ -1,37 +1,3 @@ -describe :io_pos, shared: true do - before :each do - @fname = tmp('test.txt') - File.open(@fname, 'w') { |f| f.write "123" } - end - - after :each do - rm_r @fname - end - - it "gets the offset" do - File.open @fname do |f| - f.send(@method).should == 0 - f.read 1 - f.send(@method).should == 1 - f.read 2 - f.send(@method).should == 3 - end - end - - it "raises IOError on closed stream" do - -> { IOSpecs.closed_io.send(@method) }.should.raise(IOError) - end - - it "resets #eof?" do - open @fname do |io| - io.read 1 - io.read 1 - io.send(@method) - io.should_not.eof? - end - end -end - describe :io_set_pos, shared: true do before :each do @fname = tmp('test.txt') diff --git a/spec/ruby/core/io/shared/tty.rb b/spec/ruby/core/io/shared/tty.rb deleted file mode 100644 index 1dc0e95739f70e..00000000000000 --- a/spec/ruby/core/io/shared/tty.rb +++ /dev/null @@ -1,24 +0,0 @@ -require_relative '../fixtures/classes' - -describe :io_tty, shared: true do - platform_is_not :windows do - it "returns true if this stream is a terminal device (TTY)" do - begin - # check to enabled tty - File.open('/dev/tty') {} - rescue Errno::ENXIO - skip "workaround for not configured environment like OS X" - else - File.open('/dev/tty') { |f| f.send(@method) }.should == true - end - end - end - - it "returns false if this stream is not a terminal device (TTY)" do - File.open(__FILE__) { |f| f.send(@method) }.should == false - end - - it "raises IOError on closed stream" do - -> { IOSpecs.closed_io.send @method }.should.raise(IOError) - end -end diff --git a/spec/ruby/core/io/tell_spec.rb b/spec/ruby/core/io/tell_spec.rb index 0d6c6b02d34d76..a6b51adc17c6bb 100644 --- a/spec/ruby/core/io/tell_spec.rb +++ b/spec/ruby/core/io/tell_spec.rb @@ -1,7 +1,7 @@ require_relative '../../spec_helper' -require_relative 'fixtures/classes' -require_relative 'shared/pos' describe "IO#tell" do - it_behaves_like :io_pos, :tell + it "is an alias of IO#pos" do + IO.instance_method(:tell).should == IO.instance_method(:pos) + end end diff --git a/spec/ruby/core/io/to_i_spec.rb b/spec/ruby/core/io/to_i_spec.rb index 1d0cf2a1f6b8b2..b271112a81e7e4 100644 --- a/spec/ruby/core/io/to_i_spec.rb +++ b/spec/ruby/core/io/to_i_spec.rb @@ -1,12 +1,7 @@ require_relative '../../spec_helper' -require_relative 'fixtures/classes' describe "IO#to_i" do - it "returns the numeric file descriptor of the given IO object" do - $stdout.to_i.should == 1 - end - - it "raises IOError on closed stream" do - -> { IOSpecs.closed_io.to_i }.should.raise(IOError) + it "is an alias of IO#fileno" do + IO.instance_method(:to_i).should == IO.instance_method(:fileno) end end diff --git a/spec/ruby/core/io/to_path_spec.rb b/spec/ruby/core/io/to_path_spec.rb new file mode 100644 index 00000000000000..ec6dffc115b3d4 --- /dev/null +++ b/spec/ruby/core/io/to_path_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' + +describe "IO#to_path" do + it "is an alias of IO#path" do + IO.instance_method(:to_path).should == IO.instance_method(:path) + end +end diff --git a/spec/ruby/core/io/tty_spec.rb b/spec/ruby/core/io/tty_spec.rb index 3b76c6d2b8e1e0..e1848a1760a09c 100644 --- a/spec/ruby/core/io/tty_spec.rb +++ b/spec/ruby/core/io/tty_spec.rb @@ -1,6 +1,25 @@ require_relative '../../spec_helper' -require_relative 'shared/tty' +require_relative 'fixtures/classes' describe "IO#tty?" do - it_behaves_like :io_tty, :tty? + platform_is_not :windows do + it "returns true if this stream is a terminal device (TTY)" do + begin + # check to enabled tty + File.open('/dev/tty') {} + rescue Errno::ENXIO + skip "workaround for not configured environment like OS X" + else + File.open('/dev/tty') { |f| f.tty? }.should == true + end + end + end + + it "returns false if this stream is not a terminal device (TTY)" do + File.open(__FILE__) { |f| f.tty? }.should == false + end + + it "raises IOError on closed stream" do + -> { IOSpecs.closed_io.tty? }.should.raise(IOError) + end end diff --git a/spec/ruby/core/kernel/clone_spec.rb b/spec/ruby/core/kernel/clone_spec.rb index 4ddb23d6e6ed84..80e7a78abbd5ed 100644 --- a/spec/ruby/core/kernel/clone_spec.rb +++ b/spec/ruby/core/kernel/clone_spec.rb @@ -10,12 +10,6 @@ @obj = KernelSpecs::Duplicate.new 1, :a end - it "calls #initialize_copy on the new instance" do - clone = @obj.clone - ScratchPad.recorded.should_not == @obj.object_id - ScratchPad.recorded.should == clone.object_id - end - it "uses the internal allocator and does not call #allocate" do klass = Class.new instance = klass.new diff --git a/spec/ruby/core/kernel/dup_spec.rb b/spec/ruby/core/kernel/dup_spec.rb index 99de5e37327c08..fa4ca714767dfe 100644 --- a/spec/ruby/core/kernel/dup_spec.rb +++ b/spec/ruby/core/kernel/dup_spec.rb @@ -10,12 +10,6 @@ @obj = KernelSpecs::Duplicate.new 1, :a end - it "calls #initialize_copy on the new instance" do - dup = @obj.dup - ScratchPad.recorded.should_not == @obj.object_id - ScratchPad.recorded.should == dup.object_id - end - it "uses the internal allocator and does not call #allocate" do klass = Class.new instance = klass.new diff --git a/spec/ruby/core/kernel/enum_for_spec.rb b/spec/ruby/core/kernel/enum_for_spec.rb index 0092e204683881..ef0fb64e635c26 100644 --- a/spec/ruby/core/kernel/enum_for_spec.rb +++ b/spec/ruby/core/kernel/enum_for_spec.rb @@ -1,5 +1,7 @@ require_relative '../../spec_helper' describe "Kernel#enum_for" do - it "needs to be reviewed for spec completeness" + it "is an alias of Kernel#to_enum" do + Kernel.instance_method(:enum_for).should == Kernel.instance_method(:to_enum) + end end diff --git a/spec/ruby/core/kernel/fail_spec.rb b/spec/ruby/core/kernel/fail_spec.rb index ac379b67d516f1..2d117d26cd678c 100644 --- a/spec/ruby/core/kernel/fail_spec.rb +++ b/spec/ruby/core/kernel/fail_spec.rb @@ -1,42 +1,13 @@ require_relative '../../spec_helper' -require_relative 'fixtures/classes' describe "Kernel#fail" do - it "is a private method" do - Kernel.private_instance_methods(false).should.include?(:fail) - end - - it "raises a RuntimeError" do - -> { fail }.should.raise(RuntimeError) - end - - it "accepts an Object with an exception method returning an Exception" do - obj = Object.new - def obj.exception(msg) - StandardError.new msg - end - -> { fail obj, "..." }.should.raise(StandardError, "...") - end - - it "instantiates the specified exception class" do - error_class = Class.new(RuntimeError) - -> { fail error_class }.should.raise(error_class) - end - - it "uses the specified message" do - -> { - begin - fail "the duck is not irish." - rescue => e - e.message.should == "the duck is not irish." - raise - else - raise Exception - end - }.should.raise(RuntimeError) + it "is an alias of Kernel#raise" do + Kernel.instance_method(:fail).should == Kernel.instance_method(:raise) end end describe "Kernel.fail" do - it "needs to be reviewed for spec completeness" + it "is an alias of Kernel.raise" do + Kernel.method(:fail).should == Kernel.method(:raise) + end end diff --git a/spec/ruby/core/kernel/format_spec.rb b/spec/ruby/core/kernel/format_spec.rb index 35c752b1abd9c0..a311f3c3d7b565 100644 --- a/spec/ruby/core/kernel/format_spec.rb +++ b/spec/ruby/core/kernel/format_spec.rb @@ -1,47 +1,13 @@ require_relative '../../spec_helper' -require_relative 'fixtures/classes' -# NOTE: most specs are in sprintf_spec.rb, this is just an alias describe "Kernel#format" do - it "is a private method" do - Kernel.private_instance_methods(false).should.include?(:format) + it "is an alias of Kernel#sprintf" do + Kernel.instance_method(:format).should == Kernel.instance_method(:sprintf) end end describe "Kernel.format" do - it "is accessible as a module function" do - Kernel.format("%s", "hello").should == "hello" - end - - describe "when $VERBOSE is true" do - it "warns if too many arguments are passed" do - code = <<~RUBY - $VERBOSE = true - format("test", 1) - RUBY - - ruby_exe(code, args: "2>&1").should.include?("warning: too many arguments for format string") - end - - it "does not warns if too many keyword arguments are passed" do - code = <<~RUBY - $VERBOSE = true - format("test %{test}", test: 1, unused: 2) - RUBY - - ruby_exe(code, args: "2>&1").should_not.include?("warning") - end - - ruby_bug "#20593", ""..."3.4" do - it "doesn't warns if keyword arguments are passed and none are used" do - code = <<~RUBY - $VERBOSE = true - format("test", test: 1) - format("test", {}) - RUBY - - ruby_exe(code, args: "2>&1").should_not.include?("warning") - end - end + it "is an alias of Kernel.sprintf" do + Kernel.method(:format).should == Kernel.method(:sprintf) end end diff --git a/spec/ruby/core/kernel/is_a_spec.rb b/spec/ruby/core/kernel/is_a_spec.rb index bd8c96529a84a6..ff36a769c70ab4 100644 --- a/spec/ruby/core/kernel/is_a_spec.rb +++ b/spec/ruby/core/kernel/is_a_spec.rb @@ -1,6 +1,56 @@ require_relative '../../spec_helper' -require_relative 'shared/kind_of' +require_relative 'fixtures/classes' describe "Kernel#is_a?" do - it_behaves_like :kernel_kind_of, :is_a? + before :each do + @o = KernelSpecs::KindaClass.new + end + + it "returns true if given class is the object's class" do + @o.is_a?(KernelSpecs::KindaClass).should == true + end + + it "returns true if given class is an ancestor of the object's class" do + @o.is_a?(KernelSpecs::AncestorClass).should == true + @o.is_a?(String).should == true + @o.is_a?(Object).should == true + end + + it "returns false if the given class is not object's class nor an ancestor" do + @o.is_a?(Array).should == false + end + + it "returns true if given a Module that is included in object's class" do + @o.is_a?(KernelSpecs::MyModule).should == true + end + + it "returns true if given a Module that is included one of object's ancestors only" do + @o.is_a?(KernelSpecs::AncestorModule).should == true + end + + it "returns true if given a Module that object has been extended with" do + @o.is_a?(KernelSpecs::MyExtensionModule).should == true + end + + it "returns true if given a Module that object has been prepended with" do + @o.is_a?(KernelSpecs::MyPrependedModule).should == true + end + + it "returns false if given a Module not included nor prepended in object's class nor ancestors" do + @o.is_a?(KernelSpecs::SomeOtherModule).should == false + end + + it "raises a TypeError if given an object that is not a Class nor a Module" do + -> { @o.is_a?(1) }.should.raise(TypeError) + -> { @o.is_a?('KindaClass') }.should.raise(TypeError) + -> { @o.is_a?(:KindaClass) }.should.raise(TypeError) + -> { @o.is_a?(Object.new) }.should.raise(TypeError) + end + + it "does not take into account `class` method overriding" do + def @o.class; Integer; end + + @o.is_a?(Integer).should == false + @o.is_a?(KernelSpecs::KindaClass).should == true + end end diff --git a/spec/ruby/core/kernel/kind_of_spec.rb b/spec/ruby/core/kernel/kind_of_spec.rb index c988edccb5bbd2..7fcc72543d1a81 100644 --- a/spec/ruby/core/kernel/kind_of_spec.rb +++ b/spec/ruby/core/kernel/kind_of_spec.rb @@ -1,6 +1,7 @@ require_relative '../../spec_helper' -require_relative 'shared/kind_of' describe "Kernel#kind_of?" do - it_behaves_like :kernel_kind_of, :kind_of? + it "is an alias of Kernel#is_a?" do + Kernel.instance_method(:kind_of?).should == Kernel.instance_method(:is_a?) + end end diff --git a/spec/ruby/core/kernel/require_relative_spec.rb b/spec/ruby/core/kernel/require_relative_spec.rb index 332045b2006f37..b3ac1fc64c5eff 100644 --- a/spec/ruby/core/kernel/require_relative_spec.rb +++ b/spec/ruby/core/kernel/require_relative_spec.rb @@ -109,9 +109,9 @@ -> { require_relative(path) - }.should(raise_error(LoadError) { |e| + }.should.raise(LoadError) { |e| e.path.should == File.expand_path(path, @abs_dir) - }) + } end it "calls #to_str on non-String objects" do @@ -311,9 +311,9 @@ -> { require_relative(path) - }.should(raise_error(LoadError) { |e| + }.should.raise(LoadError) { |e| e.path.should == File.expand_path(path, @abs_dir) - }) + } end it "calls #to_str on non-String objects" do diff --git a/spec/ruby/core/kernel/shared/kind_of.rb b/spec/ruby/core/kernel/shared/kind_of.rb deleted file mode 100644 index a5e0eab08f8fe1..00000000000000 --- a/spec/ruby/core/kernel/shared/kind_of.rb +++ /dev/null @@ -1,55 +0,0 @@ -require_relative '../fixtures/classes' - -describe :kernel_kind_of, shared: true do - before :each do - @o = KernelSpecs::KindaClass.new - end - - it "returns true if given class is the object's class" do - @o.send(@method, KernelSpecs::KindaClass).should == true - end - - it "returns true if given class is an ancestor of the object's class" do - @o.send(@method, KernelSpecs::AncestorClass).should == true - @o.send(@method, String).should == true - @o.send(@method, Object).should == true - end - - it "returns false if the given class is not object's class nor an ancestor" do - @o.send(@method, Array).should == false - end - - it "returns true if given a Module that is included in object's class" do - @o.send(@method, KernelSpecs::MyModule).should == true - end - - it "returns true if given a Module that is included one of object's ancestors only" do - @o.send(@method, KernelSpecs::AncestorModule).should == true - end - - it "returns true if given a Module that object has been extended with" do - @o.send(@method, KernelSpecs::MyExtensionModule).should == true - end - - it "returns true if given a Module that object has been prepended with" do - @o.send(@method, KernelSpecs::MyPrependedModule).should == true - end - - it "returns false if given a Module not included nor prepended in object's class nor ancestors" do - @o.send(@method, KernelSpecs::SomeOtherModule).should == false - end - - it "raises a TypeError if given an object that is not a Class nor a Module" do - -> { @o.send(@method, 1) }.should.raise(TypeError) - -> { @o.send(@method, 'KindaClass') }.should.raise(TypeError) - -> { @o.send(@method, :KindaClass) }.should.raise(TypeError) - -> { @o.send(@method, Object.new) }.should.raise(TypeError) - end - - it "does not take into account `class` method overriding" do - def @o.class; Integer; end - - @o.send(@method, Integer).should == false - @o.send(@method, KernelSpecs::KindaClass).should == true - end -end diff --git a/spec/ruby/core/kernel/shared/sprintf.rb b/spec/ruby/core/kernel/shared/sprintf.rb index b40bd95f594673..e57334c5abb4fd 100644 --- a/spec/ruby/core/kernel/shared/sprintf.rb +++ b/spec/ruby/core/kernel/shared/sprintf.rb @@ -1031,4 +1031,27 @@ def obj.to_str; end it "does not raise error when passed more arguments than needed" do sprintf("%s %d %c", "string", 2, "c", []).should == "string 2 c" end + + describe "when $VERBOSE is true" do + it "warns if too many arguments are passed" do + -> { + format("test", 1) + }.should complain(/too many arguments for format string/, verbose: true) + end + + it "does not warns if too many keyword arguments are passed" do + -> { + format("test %{test}", test: 1, unused: 2) + }.should_not complain(verbose: true) + end + + ruby_bug "#20593", ""..."3.4" do + it "doesn't warns if keyword arguments are passed and none are used" do + -> { + format("test", test: 1) + format("test", {}) + }.should_not complain(verbose: true) + end + end + end end diff --git a/spec/ruby/core/kernel/then_spec.rb b/spec/ruby/core/kernel/then_spec.rb index 8109a2960a22a5..bda5a696622742 100644 --- a/spec/ruby/core/kernel/then_spec.rb +++ b/spec/ruby/core/kernel/then_spec.rb @@ -2,5 +2,13 @@ require_relative 'shared/then' describe "Kernel#then" do - it_behaves_like :kernel_then, :then + ruby_version_is ""..."3.4" do + it_behaves_like :kernel_then, :then + end + + ruby_version_is "3.4" do + it "is an alias of Kernel#yield_self" do + Kernel.instance_method(:then).should == Kernel.instance_method(:yield_self) + end + end end diff --git a/spec/ruby/core/kernel/to_enum_spec.rb b/spec/ruby/core/kernel/to_enum_spec.rb index 9d9945450f3817..1cee26b694befc 100644 --- a/spec/ruby/core/kernel/to_enum_spec.rb +++ b/spec/ruby/core/kernel/to_enum_spec.rb @@ -1,5 +1,59 @@ require_relative '../../spec_helper' describe "Kernel#to_enum" do - it "needs to be reviewed for spec completeness" + it "is defined in Kernel" do + Kernel.method_defined?(:to_enum).should == true + end + + it "returns a new enumerator" do + "abc".to_enum.should.instance_of?(Enumerator) + end + + it "defaults the first argument to :each" do + enum = [1,2].to_enum + enum.map { |v| v }.should == [1,2].each { |v| v } + end + + it "sets regexp matches in the caller" do + "wawa".to_enum(:scan, /./).map {|o| $& }.should == ["w", "a", "w", "a"] + a = [] + "wawa".to_enum(:scan, /./).each {|o| a << $& } + a.should == ["w", "a", "w", "a"] + end + + it "exposes multi-arg yields as an array" do + o = Object.new + def o.each + yield :a + yield :b1, :b2 + yield [:c] + yield :d1, :d2 + yield :e1, :e2, :e3 + end + + enum = o.to_enum + enum.next.should == :a + enum.next.should == [:b1, :b2] + enum.next.should == [:c] + enum.next.should == [:d1, :d2] + enum.next.should == [:e1, :e2, :e3] + end + + it "uses the passed block's value to calculate the size of the enumerator" do + Object.new.to_enum { 100 }.size.should == 100 + end + + it "defers the evaluation of the passed block until #size is called" do + ScratchPad.record [] + + enum = Object.new.to_enum do + ScratchPad << :called + 100 + end + + ScratchPad.recorded.should.empty? + + enum.size + ScratchPad.recorded.should == [:called] + end end diff --git a/spec/ruby/core/marshal/load_spec.rb b/spec/ruby/core/marshal/load_spec.rb index a5bdfbf520fdcb..f5a05f8e527406 100644 --- a/spec/ruby/core/marshal/load_spec.rb +++ b/spec/ruby/core/marshal/load_spec.rb @@ -1,6 +1,1291 @@ +# encoding: binary require_relative '../../spec_helper' -require_relative 'shared/load' +require_relative 'fixtures/marshal_data' describe "Marshal.load" do - it_behaves_like :marshal_load, :load + before :all do + @num_self_class = 1 + end + + it "raises an ArgumentError when the dumped data is truncated" do + obj = {first: 1, second: 2, third: 3} + -> { Marshal.load(Marshal.dump(obj)[0, 5]) }.should.raise(ArgumentError, "marshal data too short") + end + + it "raises an ArgumentError when the argument is empty String" do + -> { Marshal.load("") }.should.raise(ArgumentError, "marshal data too short") + end + + it "raises an ArgumentError when the dumped class is missing" do + Object.send(:const_set, :KaBoom, Class.new) + kaboom = Marshal.dump(KaBoom.new) + Object.send(:remove_const, :KaBoom) + + -> { Marshal.load(kaboom) }.should.raise(ArgumentError) + end + + describe "when called with freeze: true" do + it "returns frozen strings" do + string = Marshal.load(Marshal.dump("foo"), freeze: true) + string.should == "foo" + string.should.frozen? + + utf8_string = "foo".encode(Encoding::UTF_8) + string = Marshal.load(Marshal.dump(utf8_string), freeze: true) + string.should == utf8_string + string.should.frozen? + end + + it "returns frozen arrays" do + array = Marshal.load(Marshal.dump([1, 2, 3]), freeze: true) + array.should == [1, 2, 3] + array.should.frozen? + end + + it "returns frozen hashes" do + hash = Marshal.load(Marshal.dump({foo: 42}), freeze: true) + hash.should == {foo: 42} + hash.should.frozen? + end + + it "returns frozen regexps" do + regexp = Marshal.load(Marshal.dump(/foo/), freeze: true) + regexp.should == /foo/ + regexp.should.frozen? + end + + it "returns frozen structs" do + struct = Marshal.load(Marshal.dump(MarshalSpec::StructToDump.new(1, 2)), freeze: true) + struct.should == MarshalSpec::StructToDump.new(1, 2) + struct.should.frozen? + end + + it "returns frozen objects" do + source_object = Object.new + + object = Marshal.load(Marshal.dump(source_object), freeze: true) + object.should.frozen? + end + + describe "deep freezing" do + it "returns hashes with frozen keys and values" do + key = Object.new + value = Object.new + source_object = {key => value} + + hash = Marshal.load(Marshal.dump(source_object), freeze: true) + hash.size.should == 1 + hash.keys[0].should.frozen? + hash.values[0].should.frozen? + end + + it "returns arrays with frozen elements" do + object = Object.new + source_object = [object] + + array = Marshal.load(Marshal.dump(source_object), freeze: true) + array.size.should == 1 + array[0].should.frozen? + end + + it "returns structs with frozen members" do + object1 = Object.new + object2 = Object.new + source_object = MarshalSpec::StructToDump.new(object1, object2) + + struct = Marshal.load(Marshal.dump(source_object), freeze: true) + struct.a.should.frozen? + struct.b.should.frozen? + end + + it "returns objects with frozen instance variables" do + source_object = Object.new + instance_variable = Object.new + source_object.instance_variable_set(:@a, instance_variable) + + object = Marshal.load(Marshal.dump(source_object), freeze: true) + object.instance_variable_get(:@a).should != nil + object.instance_variable_get(:@a).should.frozen? + end + + it "deduplicates frozen strings" do + source_object = ["foo" + "bar", "foobar"] + object = Marshal.load(Marshal.dump(source_object), freeze: true) + + object[0].should.equal?(object[1]) + end + end + + it "does not freeze modules" do + object = Marshal.load(Marshal.dump(Kernel), freeze: true) + object.should_not.frozen? + Kernel.should_not.frozen? + end + + it "does not freeze classes" do + object = Marshal.load(Marshal.dump(Object), freeze: true) + object.should_not.frozen? + Object.should_not.frozen? + end + + it "does freeze extended objects" do + object = Marshal.load("\x04\be:\x0FEnumerableo:\vObject\x00", freeze: true) + object.should.frozen? + end + + it "does freeze extended objects with instance variables" do + object = Marshal.load("\x04\be:\x0FEnumerableo:\vObject\x06:\n@ivarT", freeze: true) + object.should.frozen? + end + + it "returns frozen object having #_dump method" do + object = Marshal.load(Marshal.dump(UserDefined.new), freeze: true) + object.should.frozen? + end + + it "returns frozen object responding to #marshal_dump and #marshal_load" do + object = Marshal.load(Marshal.dump(UserMarshal.new), freeze: true) + object.should.frozen? + end + + it "returns frozen object extended by a module" do + object = Object.new + object.extend(MarshalSpec::ModuleToExtendBy) + + object = Marshal.load(Marshal.dump(object), freeze: true) + object.should.frozen? + end + + it "does not call freeze method" do + object = MarshalSpec::ObjectWithFreezeRaisingException.new + object = Marshal.load(Marshal.dump(object), freeze: true) + object.should.frozen? + end + + it "returns frozen object even if object does not respond to freeze method" do + object = MarshalSpec::ObjectWithoutFreeze.new + object = Marshal.load(Marshal.dump(object), freeze: true) + object.should.frozen? + end + + it "returns a frozen object when is an instance of String/Array/Regexp/Hash subclass and has instance variables" do + source_object = UserString.new + source_object.instance_variable_set(:@foo, "bar") + + object = Marshal.load(Marshal.dump(source_object), freeze: true) + object.should.frozen? + end + + describe "when called with a proc" do + it "call the proc with frozen objects" do + arr = [] + s = +'hi' + s.instance_variable_set(:@foo, 5) + st = Struct.new("Brittle", :a).new + st.instance_variable_set(:@clue, 'none') + st.a = 0.0 + h = Hash.new('def') + h['nine'] = 9 + a = [:a, :b, :c] + a.instance_variable_set(:@two, 2) + obj = [s, 10, s, s, st, a] + obj.instance_variable_set(:@zoo, 'ant') + proc = Proc.new { |o| arr << o; o} + + Marshal.load( + "\x04\bI[\vI\"\ahi\a:\x06EF:\t@fooi\ni\x0F@\x06@\x06IS:\x14Struct::Brittle\x06:\x06af\x060\x06:\n@clueI\"\tnone\x06;\x00FI[\b;\b:\x06b:\x06c\x06:\t@twoi\a\x06:\t@zooI\"\bant\x06;\x00F", + proc, + freeze: true, + ) + + arr.should == [ + false, 5, "hi", 10, "hi", "hi", 0.0, false, "none", st, + :b, :c, 2, a, false, "ant", ["hi", 10, "hi", "hi", st, [:a, :b, :c]], + ] + + arr.each do |v| + v.should.frozen? + end + + Struct.send(:remove_const, :Brittle) + end + + it "does not freeze the object returned by the proc" do + string = Marshal.load(Marshal.dump("foo"), proc { |o| o.upcase }, freeze: true) + string.should == "FOO" + string.should_not.frozen? + end + end + end + + describe "when called with a proc" do + it "call the proc with fully initialized strings" do + utf8_string = "foo".encode(Encoding::UTF_8) + Marshal.load(Marshal.dump(utf8_string), proc { |arg| + if arg.is_a?(String) + arg.should == utf8_string + arg.encoding.should == Encoding::UTF_8 + end + arg + }) + end + + it "no longer mutate the object after it was passed to the proc" do + string = Marshal.load(Marshal.dump("foo"), :freeze.to_proc) + string.should.frozen? + end + + it "call the proc with extended objects" do + objs = [] + obj = Marshal.load("\x04\be:\x0FEnumerableo:\vObject\x00", Proc.new { |o| objs << o; o }) + objs.should == [obj] + end + + it "returns the value of the proc" do + Marshal.load(Marshal.dump([1,2]), proc { [3,4] }).should == [3,4] + end + + it "calls the proc for recursively visited data" do + a = [1] + a << a + ret = [] + Marshal.load(Marshal.dump(a), proc { |arg| ret << arg.inspect; arg }) + ret[0].should == 1.inspect + ret[1].should == a.inspect + ret.size.should == 2 + end + + it "loads an Array with proc" do + arr = [] + s = +'hi' + s.instance_variable_set(:@foo, 5) + st = Struct.new("Brittle", :a).new + st.instance_variable_set(:@clue, 'none') + st.a = 0.0 + h = Hash.new('def') + h['nine'] = 9 + a = [:a, :b, :c] + a.instance_variable_set(:@two, 2) + obj = [s, 10, s, s, st, a] + obj.instance_variable_set(:@zoo, 'ant') + proc = Proc.new { |o| arr << o.dup; o} + + Marshal.load("\x04\bI[\vI\"\ahi\a:\x06EF:\t@fooi\ni\x0F@\x06@\x06IS:\x14Struct::Brittle\x06:\x06af\x060\x06:\n@clueI\"\tnone\x06;\x00FI[\b;\b:\x06b:\x06c\x06:\t@twoi\a\x06:\t@zooI\"\bant\x06;\x00F", proc) + + arr.should == [ + false, 5, "hi", 10, "hi", "hi", 0.0, false, "none", st, + :b, :c, 2, a, false, "ant", ["hi", 10, "hi", "hi", st, [:a, :b, :c]], + ] + Struct.send(:remove_const, :Brittle) + end + end + + describe "when called with nil for the proc argument" do + it "behaves as if no proc argument was passed" do + a = [1] + a << a + b = Marshal.load(Marshal.dump(a), nil) + b.should == a + end + end + + describe "when called on objects with custom _dump methods" do + it "does not set instance variables of an object with user-defined _dump/_load" do + # this string represents: <#UserPreviouslyDefinedWithInitializedIvar @field2=7 @field1=6> + dump_str = "\004\bu:-UserPreviouslyDefinedWithInitializedIvar\a:\f@field2i\f:\f@field1i\v" + + UserPreviouslyDefinedWithInitializedIvar.should_receive(:_load).and_return(UserPreviouslyDefinedWithInitializedIvar.new) + marshaled_obj = Marshal.load(dump_str) + + marshaled_obj.should.instance_of?(UserPreviouslyDefinedWithInitializedIvar) + marshaled_obj.field1.should == nil + marshaled_obj.field2.should == nil + end + + it "loads the String in non US-ASCII and non UTF-8 encoding" do + source_object = UserDefinedString.new("a".encode("windows-1251")) + object = Marshal.load(Marshal.dump(source_object)) + object.string.should == "a".encode("windows-1251") + end + + it "loads the String in multibyte encoding" do + source_object = UserDefinedString.new("a".encode("utf-32le")) + object = Marshal.load(Marshal.dump(source_object)) + object.string.should == "a".encode("utf-32le") + end + + describe "that returns an immediate value" do + it "loads an array containing an instance of the object, followed by multiple instances of another object" do + str = "string" + + # this string represents: [<#UserDefinedImmediate A>, <#String "string">, <#String "string">] + marshaled_obj = Marshal.load("\004\b[\bu:\031UserDefinedImmediate\000\"\vstring@\a") + + marshaled_obj.should == [nil, str, str] + end + + it "loads any structure with multiple references to the same object, followed by multiple instances of another object" do + str = "string" + + # this string represents: {a: <#UserDefinedImmediate A>, b: <#UserDefinedImmediate A>, c: <#String "string">, d: <#String "string">} + hash_dump = "\x04\b{\t:\x06aIu:\x19UserDefinedImmediate\x00\x06:\x06ET:\x06b@\x06:\x06cI\"\vstring\x06;\aT:\x06d@\a" + + marshaled_obj = Marshal.load(hash_dump) + marshaled_obj.should == {a: nil, b: nil, c: str, d: str} + + # this string represents: [<#UserDefinedImmediate A>, <#UserDefinedImmediate A>, <#String "string">, <#String "string">] + array_dump = "\x04\b[\tIu:\x19UserDefinedImmediate\x00\x06:\x06ET@\x06I\"\vstring\x06;\x06T@\a" + + marshaled_obj = Marshal.load(array_dump) + marshaled_obj.should == [nil, nil, str, str] + end + + it "loads an array containing references to multiple instances of the object, followed by multiple instances of another object" do + str = "string" + + # this string represents: [<#UserDefinedImmediate A>, <#UserDefinedImmediate B>, <#String "string">, <#String "string">] + array_dump = "\x04\b[\tIu:\x19UserDefinedImmediate\x00\x06:\x06ETIu;\x00\x00\x06;\x06TI\"\vstring\x06;\x06T@\b" + + marshaled_obj = Marshal.load(array_dump) + marshaled_obj.should == [nil, nil, str, str] + end + end + end + + it "loads an array containing objects having _dump method, and with proc" do + arr = [] + myproc = Proc.new { |o| arr << o.dup; o } + o1 = UserDefined.new; + o2 = UserDefinedWithIvar.new + obj = [o1, o2, o1, o2] + + Marshal.load("\x04\b[\tu:\x10UserDefined\x18\x04\b[\aI\"\nstuff\x06:\x06EF@\x06u:\x18UserDefinedWithIvar>\x04\b[\bI\"\nstuff\a:\x06EF:\t@foo:\x18UserDefinedWithIvarI\"\tmore\x06;\x00F@\a@\x06@\a", myproc) + + arr[0].should == o1 + arr[1].should == o2 + arr[2].should == obj + arr.size.should == 3 + end + + it "loads an array containing objects having marshal_dump method, and with proc" do + arr = [] + proc = Proc.new { |o| arr << o.dup; o } + o1 = UserMarshal.new + o2 = UserMarshalWithIvar.new + + Marshal.load("\004\b[\tU:\020UserMarshal\"\nstuffU:\030UserMarshalWithIvar[\006\"\fmy data@\006@\b", proc) + + arr[0].should == 'stuff' + arr[1].should == o1 + arr[2].should == 'my data' + arr[3].should == ['my data'] + arr[4].should == o2 + arr[5].should == [o1, o2, o1, o2] + + arr.size.should == 6 + end + + it "assigns classes to nested subclasses of Array correctly" do + arr = ArraySub.new(ArraySub.new) + arr_dump = Marshal.dump(arr) + Marshal.load(arr_dump).class.should == ArraySub + end + + it "loads subclasses of Array with overridden << and push correctly" do + arr = ArraySubPush.new + arr[0] = '1' + arr_dump = Marshal.dump(arr) + Marshal.load(arr_dump).should == arr + end + + it "raises a TypeError with bad Marshal version" do + marshal_data = +'\xff\xff' + marshal_data[0] = (Marshal::MAJOR_VERSION).chr + marshal_data[1] = (Marshal::MINOR_VERSION + 1).chr + + -> { Marshal.load(marshal_data) }.should.raise(TypeError) + + marshal_data = +'\xff\xff' + marshal_data[0] = (Marshal::MAJOR_VERSION - 1).chr + marshal_data[1] = (Marshal::MINOR_VERSION).chr + + -> { Marshal.load(marshal_data) }.should.raise(TypeError) + end + + it "raises EOFError on loading an empty file" do + temp_file = tmp("marshal.rubyspec.tmp.#{Process.pid}") + file = File.new(temp_file, "w+") + begin + -> { Marshal.load(file) }.should.raise(EOFError) + ensure + file.close + rm_r temp_file + end + end + + # Note: Ruby 1.9 should be compatible with older marshal format + MarshalSpec::DATA.each do |description, (object, marshal, attributes)| + it "loads a #{description}" do + Marshal.load(marshal).should == object + end + end + + MarshalSpec::DATA_19.each do |description, (object, marshal, attributes)| + it "loads a #{description}" do + Marshal.load(marshal).should == object + end + end + + describe "for an Array" do + it "loads an array containing the same objects" do + s = 'oh' + b = 'hi' + r = // + d = [b, :no, s, :go] + c = String + f = 1.0 + + o1 = UserMarshalWithIvar.new; o2 = UserMarshal.new + + obj = [:so, 'hello', 100, :so, :so, d, :so, o2, :so, :no, o2, + :go, c, nil, Struct::Pyramid.new, f, :go, :no, s, b, r, + :so, 'huh', o1, true, b, b, 99, r, b, s, :so, f, c, :no, o1, d] + + Marshal.load("\004\b[*:\aso\"\nhelloii;\000;\000[\t\"\ahi:\ano\"\aoh:\ago;\000U:\020UserMarshal\"\nstuff;\000;\006@\n;\ac\vString0S:\024Struct::Pyramid\000f\0061;\a;\006@\t@\b/\000\000;\000\"\bhuhU:\030UserMarshalWithIvar[\006\"\fmy dataT@\b@\bih@\017@\b@\t;\000@\016@\f;\006@\021@\a").should == + obj + end + + it "loads an array having ivar" do + s = +'well' + s.instance_variable_set(:@foo, 10) + obj = ['5', s, 'hi'].extend(Meths, MethsMore) + obj.instance_variable_set(:@mix, s) + new_obj = Marshal.load("\004\bI[\b\"\0065I\"\twell\006:\t@fooi\017\"\ahi\006:\t@mix@\a") + new_obj.should == obj + new_obj.instance_variable_get(:@mix).should.equal? new_obj[1] + new_obj[1].instance_variable_get(:@foo).should == 10 + end + + it "loads an extended Array object containing a user-marshaled object" do + obj = [UserMarshal.new, UserMarshal.new].extend(Meths) + dump = "\x04\be:\nMeths[\ao:\x10UserMarshal\x06:\n@dataI\"\nstuff\x06:\x06ETo;\x06\x06;\aI\"\nstuff\x06;\bT" + new_obj = Marshal.load(dump) + + new_obj.should == obj + obj_ancestors = class << obj; ancestors[1..-1]; end + new_obj_ancestors = class << new_obj; ancestors[1..-1]; end + obj_ancestors.should == new_obj_ancestors + end + end + + describe "for a Hash" do + it "loads an extended_user_hash with a parameter to initialize" do + obj = UserHashInitParams.new(:abc).extend(Meths) + + new_obj = Marshal.load("\004\bIe:\nMethsC:\027UserHashInitParams{\000\006:\a@a:\babc") + + new_obj.should == obj + new_obj_metaclass_ancestors = class << new_obj; ancestors; end + new_obj_metaclass_ancestors[@num_self_class].should == Meths + new_obj_metaclass_ancestors[@num_self_class+1].should == UserHashInitParams + end + + it "loads an extended hash object containing a user-marshaled object" do + obj = {a: UserMarshal.new}.extend(Meths) + + new_obj = Marshal.load("\004\be:\nMeths{\006:\006aU:\020UserMarshal\"\nstuff") + + new_obj.should == obj + new_obj_metaclass_ancestors = class << new_obj; ancestors; end + new_obj_metaclass_ancestors[@num_self_class].should == Meths + new_obj_metaclass_ancestors[@num_self_class+1].should == Hash + end + + it "preserves hash ivars when hash contains a string having ivar" do + s = +'string' + s.instance_variable_set :@string_ivar, 'string ivar' + h = { key: s } + h.instance_variable_set :@hash_ivar, 'hash ivar' + + unmarshalled = Marshal.load(Marshal.dump(h)) + unmarshalled.instance_variable_get(:@hash_ivar).should == 'hash ivar' + unmarshalled[:key].instance_variable_get(:@string_ivar).should == 'string ivar' + end + + it "preserves compare_by_identity behaviour" do + h = { a: 1 } + h.compare_by_identity + unmarshalled = Marshal.load(Marshal.dump(h)) + unmarshalled.should.compare_by_identity? + + h = { a: 1 } + unmarshalled = Marshal.load(Marshal.dump(h)) + unmarshalled.should_not.compare_by_identity? + end + + it "preserves compare_by_identity behaviour for a Hash subclass" do + h = UserHash.new({ a: 1 }) + h.compare_by_identity + unmarshalled = Marshal.load(Marshal.dump(h)) + unmarshalled.should.compare_by_identity? + + h = UserHash.new({ a: 1 }) + unmarshalled = Marshal.load(Marshal.dump(h)) + unmarshalled.should_not.compare_by_identity? + end + + it "allocates an instance of the proper class when Hash subclass with compare_by_identity behaviour" do + h = UserHash.new({ a: 1 }) + h.compare_by_identity + + unmarshalled = Marshal.load(Marshal.dump(h)) + unmarshalled.should.kind_of?(UserHash) + end + end + + describe "for a Symbol" do + it "loads a Symbol" do + sym = Marshal.load("\004\b:\vsymbol") + sym.should == :symbol + sym.encoding.should == Encoding::US_ASCII + end + + it "loads a big Symbol" do + sym = ('big' * 100).to_sym + Marshal.load("\004\b:\002,\001#{'big' * 100}").should == sym + end + + it "loads an encoded Symbol" do + s = "\u2192" + + sym = Marshal.load("\x04\bI:\b\xE2\x86\x92\x06:\x06ET") + sym.should == s.encode("utf-8").to_sym + sym.encoding.should == Encoding::UTF_8 + + sym = Marshal.load("\x04\bI:\t\xFE\xFF!\x92\x06:\rencoding\"\vUTF-16") + sym.should == s.encode("utf-16").to_sym + sym.encoding.should == Encoding::UTF_16 + + sym = Marshal.load("\x04\bI:\a\x92!\x06:\rencoding\"\rUTF-16LE") + sym.should == s.encode("utf-16le").to_sym + sym.encoding.should == Encoding::UTF_16LE + + sym = Marshal.load("\x04\bI:\a!\x92\x06:\rencoding\"\rUTF-16BE") + sym.should == s.encode("utf-16be").to_sym + sym.encoding.should == Encoding::UTF_16BE + + sym = Marshal.load("\x04\bI:\a\xA2\xAA\x06:\rencoding\"\vEUC-JP") + sym.should == s.encode("euc-jp").to_sym + sym.encoding.should == Encoding::EUC_JP + + sym = Marshal.load("\x04\bI:\a\x81\xA8\x06:\rencoding\"\x10Windows-31J") + sym.should == s.encode("sjis").to_sym + sym.encoding.should == Encoding::SJIS + end + + it "loads a binary encoded Symbol" do + s = "\u2192".dup.force_encoding("binary").to_sym + sym = Marshal.load("\x04\b:\b\xE2\x86\x92") + sym.should == s + sym.encoding.should == Encoding::BINARY + end + + it "loads multiple Symbols sharing the same encoding" do + # Note that the encoding is a link for the second Symbol + symbol1 = "I:\t\xE2\x82\xACa\x06:\x06ET" + symbol2 = "I:\t\xE2\x82\xACb\x06;\x06T" + dump = "\x04\b[\a#{symbol1}#{symbol2}" + value = Marshal.load(dump) + value.map(&:encoding).should == [Encoding::UTF_8, Encoding::UTF_8] + expected = [ + "€a".dup.force_encoding(Encoding::UTF_8).to_sym, + "€b".dup.force_encoding(Encoding::UTF_8).to_sym + ] + value.should == expected + + value = Marshal.load("\x04\b[\b#{symbol1}#{symbol2};\x00") + value.map(&:encoding).should == [Encoding::UTF_8, Encoding::UTF_8, Encoding::UTF_8] + value.should == [*expected, expected[0]] + end + + it "raises ArgumentError when end of byte sequence reached before symbol characters end" do + Marshal.dump(:hello).should == "\x04\b:\nhello" + + -> { + Marshal.load("\x04\b:\nhel") + }.should.raise(ArgumentError, "marshal data too short") + end + end + + describe "for a String" do + it "loads a string having ivar with ref to self" do + obj = +'hi' + obj.instance_variable_set(:@self, obj) + Marshal.load("\004\bI\"\ahi\006:\n@self@\000").should == obj + end + + it "loads a string through StringIO stream" do + require 'stringio' + obj = "This is a string which should be unmarshalled through StringIO stream!" + Marshal.load(StringIO.new(Marshal.dump(obj))).should == obj + end + + it "sets binmode if it is loading through StringIO stream" do + io = StringIO.new("\004\b:\vsymbol") + def io.binmode; raise "binmode"; end + -> { Marshal.load(io) }.should.raise(RuntimeError, "binmode") + end + + it "loads a string with an ivar" do + str = Marshal.load("\x04\bI\"\x00\x06:\t@fooI\"\bbar\x06:\x06EF") + str.instance_variable_get("@foo").should == "bar" + end + + it "loads a String subclass with custom constructor" do + str = Marshal.load("\x04\bC: UserCustomConstructorString\"\x00") + str.should.instance_of?(UserCustomConstructorString) + end + + it "loads a US-ASCII String" do + str = "abc".dup.force_encoding("us-ascii") + data = "\x04\bI\"\babc\x06:\x06EF" + result = Marshal.load(data) + result.should == str + result.encoding.should.equal?(Encoding::US_ASCII) + end + + it "loads a UTF-8 String" do + str = "\x6d\xc3\xb6\x68\x72\x65".dup.force_encoding("utf-8") + data = "\x04\bI\"\vm\xC3\xB6hre\x06:\x06ET" + result = Marshal.load(data) + result.should == str + result.encoding.should.equal?(Encoding::UTF_8) + end + + it "loads a String in another encoding" do + str = "\x6d\x00\xf6\x00\x68\x00\x72\x00\x65\x00".dup.force_encoding("utf-16le") + data = "\x04\bI\"\x0Fm\x00\xF6\x00h\x00r\x00e\x00\x06:\rencoding\"\rUTF-16LE" + result = Marshal.load(data) + result.should == str + result.encoding.should.equal?(Encoding::UTF_16LE) + end + + it "loads a String as BINARY if no encoding is specified at the end" do + str = "\xC3\xB8".dup.force_encoding("BINARY") + data = "\x04\b\"\a\xC3\xB8".dup.force_encoding("UTF-8") + result = Marshal.load(data) + result.encoding.should == Encoding::BINARY + result.should == str + end + + it "raises ArgumentError when end of byte sequence reached before string characters end" do + Marshal.dump("hello").should == "\x04\b\"\nhello" + + -> { + Marshal.load("\x04\b\"\nhel") + }.should.raise(ArgumentError, "marshal data too short") + end + end + + describe "for a Struct" do + it "loads a extended_struct having fields with same objects" do + s = 'hi' + obj = Struct.new("Extended", :a, :b).new.extend(Meths) + dump = "\004\be:\nMethsS:\025Struct::Extended\a:\006a0:\006b0" + Marshal.load(dump).should == obj + + obj.a = [:a, s] + obj.b = [:Meths, s] + dump = "\004\be:\nMethsS:\025Struct::Extended\a:\006a[\a;\a\"\ahi:\006b[\a;\000@\a" + Marshal.load(dump).should == obj + Struct.send(:remove_const, :Extended) + end + + it "loads a struct having ivar" do + obj = Struct.new("Thick").new + obj.instance_variable_set(:@foo, 5) + reloaded = Marshal.load("\004\bIS:\022Struct::Thick\000\006:\t@fooi\n") + reloaded.should == obj + reloaded.instance_variable_get(:@foo).should == 5 + Struct.send(:remove_const, :Thick) + end + + it "loads a struct having fields" do + obj = Struct.new("Ure1", :a, :b).new + Marshal.load("\004\bS:\021Struct::Ure1\a:\006a0:\006b0").should == obj + Struct.send(:remove_const, :Ure1) + end + + it "does not call initialize on the unmarshaled struct" do + threadlocal_key = MarshalSpec::StructWithUserInitialize::THREADLOCAL_KEY + + s = MarshalSpec::StructWithUserInitialize.new('foo') + Thread.current[threadlocal_key].should == ['foo'] + s.a.should == 'foo' + + Thread.current[threadlocal_key] = nil + + dumped = Marshal.dump(s) + loaded = Marshal.load(dumped) + + Thread.current[threadlocal_key].should == nil + loaded.a.should == 'foo' + end + end + + describe "for a Data" do + it "loads a Data" do + obj = MarshalSpec::DataSpec::Measure.new(100, 'km') + dumped = "\x04\bS:#MarshalSpec::DataSpec::Measure\a:\vamountii:\tunit\"\akm" + Marshal.dump(obj).should == dumped + + Marshal.load(dumped).should == obj + end + + it "loads an extended Data" do + obj = MarshalSpec::DataSpec::MeasureExtended.new(100, "km") + dumped = "\x04\bS:+MarshalSpec::DataSpec::MeasureExtended\a:\vamountii:\tunit\"\akm" + Marshal.dump(obj).should == dumped + + Marshal.load(dumped).should == obj + end + + it "returns a frozen object" do + obj = MarshalSpec::DataSpec::Measure.new(100, 'km') + dumped = "\x04\bS:#MarshalSpec::DataSpec::Measure\a:\vamountii:\tunit\"\akm" + Marshal.dump(obj).should == dumped + + Marshal.load(dumped).should.frozen? + end + end + + describe "for an Exception" do + it "loads a marshalled exception with no message" do + obj = Exception.new + loaded = Marshal.load("\004\bo:\016Exception\a:\abt0:\tmesg0") + loaded.message.should == obj.message + loaded.backtrace.should == obj.backtrace + loaded = Marshal.load("\x04\bo:\x0EException\a:\tmesg0:\abt0") + loaded.message.should == obj.message + loaded.backtrace.should == obj.backtrace + end + + it "loads a marshalled exception with a message" do + obj = Exception.new("foo") + loaded = Marshal.load("\004\bo:\016Exception\a:\abt0:\tmesg\"\bfoo") + loaded.message.should == obj.message + loaded.backtrace.should == obj.backtrace + loaded = Marshal.load("\x04\bo:\x0EException\a:\tmesgI\"\bfoo\x06:\x06EF:\abt0") + loaded.message.should == obj.message + loaded.backtrace.should == obj.backtrace + end + + it "loads a marshalled exception with a backtrace" do + obj = Exception.new("foo") + obj.set_backtrace(["foo/bar.rb:10"]) + loaded = Marshal.load("\004\bo:\016Exception\a:\abt[\006\"\022foo/bar.rb:10:\tmesg\"\bfoo") + loaded.message.should == obj.message + loaded.backtrace.should == obj.backtrace + loaded = Marshal.load("\x04\bo:\x0EException\a:\tmesgI\"\bfoo\x06:\x06EF:\abt[\x06I\"\x12foo/bar.rb:10\x06;\aF") + loaded.message.should == obj.message + loaded.backtrace.should == obj.backtrace + end + + it "loads an marshalled exception with ivars" do + s = 'hi' + arr = [:so, :so, s, s] + obj = Exception.new("foo") + obj.instance_variable_set :@arr, arr + + loaded = Marshal.load("\x04\bo:\x0EException\b:\tmesg\"\bfoo:\abt0:\t@arr[\t:\aso;\t\"\ahi@\b") + new_arr = loaded.instance_variable_get :@arr + + loaded.message.should == obj.message + new_arr.should == arr + end + end + + describe "for an Object" do + it "loads an object" do + Marshal.load("\004\bo:\vObject\000").should.is_a?(Object) + end + + it "loads an extended Object" do + obj = Object.new.extend(Meths) + + new_obj = Marshal.load("\004\be:\nMethso:\vObject\000") + + new_obj.class.should == obj.class + new_obj_metaclass_ancestors = class << new_obj; ancestors; end + new_obj_metaclass_ancestors[@num_self_class, 2].should == [Meths, Object] + end + + it "loads an object having ivar" do + s = 'hi' + arr = [:so, :so, s, s] + obj = Object.new + obj.instance_variable_set :@str, arr + + new_obj = Marshal.load("\004\bo:\vObject\006:\t@str[\t:\aso;\a\"\ahi@\a") + new_str = new_obj.instance_variable_get :@str + + new_str.should == arr + end + + it "loads an Object with a non-US-ASCII instance variable" do + ivar = "@é".dup.force_encoding(Encoding::UTF_8).to_sym + obj = Marshal.load("\x04\bo:\vObject\x06I:\b@\xC3\xA9\x06:\x06ETi\x06") + obj.instance_variables.should == [ivar] + obj.instance_variables[0].encoding.should == Encoding::UTF_8 + obj.instance_variable_get(ivar).should == 1 + end + + it "raises ArgumentError if the object from an 'o' stream is not dumpable as 'o' type user class" do + -> do + Marshal.load("\x04\bo:\tFile\001\001:\001\005@path\"\x10/etc/passwd") + end.should.raise(ArgumentError) + end + + it "raises ArgumentError when end of byte sequence reached before class name end" do + Marshal.dump(Object.new).should == "\x04\bo:\vObject\x00" + + -> { + Marshal.load("\x04\bo:\vObj") + }.should.raise(ArgumentError, "marshal data too short") + end + end + + describe "for an object responding to #marshal_dump and #marshal_load" do + it "loads a user-marshaled object" do + obj = UserMarshal.new + obj.data = :data + value = [obj, :data] + dump = Marshal.dump(value) + dump.should == "\x04\b[\aU:\x10UserMarshal:\tdata;\x06" + reloaded = Marshal.load(dump) + reloaded.should == value + end + end + + describe "for a user object" do + it "loads a user-marshaled extended object" do + obj = UserMarshal.new.extend(Meths) + + new_obj = Marshal.load("\004\bU:\020UserMarshal\"\nstuff") + + new_obj.should == obj + new_obj_metaclass_ancestors = class << new_obj; ancestors; end + new_obj_metaclass_ancestors[@num_self_class].should == UserMarshal + end + + it "loads a UserObject" do + Marshal.load("\004\bo:\017UserObject\000").should.is_a?(UserObject) + end + + describe "that extends a core type other than Object or BasicObject" do + after :each do + MarshalSpec.reset_swapped_class + end + + it "raises ArgumentError if the resulting class does not extend the same type" do + MarshalSpec.set_swapped_class(Class.new(Hash)) + data = Marshal.dump(MarshalSpec::SwappedClass.new) + + MarshalSpec.set_swapped_class(Class.new(Array)) + -> { Marshal.load(data) }.should.raise(ArgumentError) + + MarshalSpec.set_swapped_class(Class.new) + -> { Marshal.load(data) }.should.raise(ArgumentError) + end + end + end + + describe "for a Regexp" do + ruby_version_is "4.1" do + it "raises FrozenError for an extended Regexp" do + -> { + Marshal.load("\004\be:\nMethse:\016MethsMore/\n[a-z]\000") + }.should.raise(FrozenError) + end + + it "raises FrozenError when regexp has instance variables" do + -> { + Marshal.load("\x04\bI/\nhello\x00\a:\x06EF:\x11@regexp_ivar[\x06i/") + }.should.raise(FrozenError) + end + end + + ruby_version_is ""..."4.1" do + it "loads an extended Regexp" do + obj = /[a-z]/.dup.extend(Meths, MethsMore) + new_obj = Marshal.load("\004\be:\nMethse:\016MethsMore/\n[a-z]\000") + + new_obj.should == obj + new_obj_metaclass_ancestors = class << new_obj; ancestors; end + new_obj_metaclass_ancestors[@num_self_class, 3].should == + [Meths, MethsMore, Regexp] + end + + it "restore the regexp instance variables" do + obj = Regexp.new("hello") + obj.instance_variable_set(:@regexp_ivar, [42]) + + new_obj = Marshal.load("\x04\bI/\nhello\x00\a:\x06EF:\x11@regexp_ivar[\x06i/") + new_obj.instance_variables.should == [:@regexp_ivar] + new_obj.instance_variable_get(:@regexp_ivar).should == [42] + end + end + + it "loads a Regexp subclass instance variables" do + obj = UserRegexp.new('abc') + obj.instance_variable_set(:@noise, 'much') + + new_obj = Marshal.load(Marshal.dump(obj)) + + new_obj.should == obj + new_obj.instance_variable_get(:@noise).should == 'much' + new_obj_metaclass_ancestors = class << new_obj; ancestors; end + new_obj_metaclass_ancestors[@num_self_class, 2].should == + [UserRegexp, Regexp] + end + + it "loads a Regexp subclass instance variables when it is extended with a module" do + obj = UserRegexp.new('').extend(Meths) + obj.instance_variable_set(:@noise, 'much') + + new_obj = Marshal.load("\004\bIe:\nMethsC:\017UserRegexp/\000\000\006:\v@noise\"\tmuch") + + new_obj.should == obj + new_obj.instance_variable_get(:@noise).should == 'much' + new_obj_metaclass_ancestors = class << new_obj; ancestors; end + new_obj_metaclass_ancestors[@num_self_class, 3].should == + [Meths, UserRegexp, Regexp] + end + + it "preserves Regexp encoding" do + source_object = Regexp.new("a".encode("utf-32le")) + regexp = Marshal.load(Marshal.dump(source_object)) + + regexp.encoding.should == Encoding::UTF_32LE + regexp.source.should == "a".encode("utf-32le") + end + + it "raises ArgumentError when end of byte sequence reached before source string end" do + Marshal.dump(/hello world/).should == "\x04\bI/\x10hello world\x00\x06:\x06EF" + + -> { + Marshal.load("\x04\bI/\x10hel") + }.should.raise(ArgumentError, "marshal data too short") + end + end + + describe "for a Float" do + it "loads a Float NaN" do + obj = 0.0 / 0.0 + Marshal.load("\004\bf\bnan").to_s.should == obj.to_s + end + + it "loads a Float 1.3" do + Marshal.load("\004\bf\v1.3\000\314\315").should == 1.3 + end + + it "loads a Float -5.1867345e-22" do + obj = -5.1867345e-22 + Marshal.load("\004\bf\037-5.1867345000000008e-22\000\203_").should be_close(obj, 1e-30) + end + + it "loads a Float 1.1867345e+22" do + obj = 1.1867345e+22 + Marshal.load("\004\bf\0361.1867344999999999e+22\000\344@").should == obj + end + + it "raises ArgumentError when end of byte sequence reached before float string representation end" do + Marshal.dump(1.3).should == "\x04\bf\b1.3" + + -> { + Marshal.load("\004\bf\v1") + }.should.raise(ArgumentError, "marshal data too short") + end + end + + describe "for an Integer" do + it "loads 0" do + Marshal.load("\004\bi\000").should == 0 + Marshal.load("\004\bi\005").should == 0 + end + + it "loads an Integer 8" do + Marshal.load("\004\bi\r" ).should == 8 + end + + it "loads and Integer -8" do + Marshal.load("\004\bi\363" ).should == -8 + end + + it "loads an Integer 1234" do + Marshal.load("\004\bi\002\322\004").should == 1234 + end + + it "loads an Integer -1234" do + Marshal.load("\004\bi\376.\373").should == -1234 + end + + it "loads an Integer 4611686018427387903" do + Marshal.load("\004\bl+\t\377\377\377\377\377\377\377?").should == 4611686018427387903 + end + + it "loads an Integer -4611686018427387903" do + Marshal.load("\004\bl-\t\377\377\377\377\377\377\377?").should == -4611686018427387903 + end + + it "loads an Integer 2361183241434822606847" do + Marshal.load("\004\bl+\n\377\377\377\377\377\377\377\377\177\000").should == 2361183241434822606847 + end + + it "loads an Integer -2361183241434822606847" do + Marshal.load("\004\bl-\n\377\377\377\377\377\377\377\377\177\000").should == -2361183241434822606847 + end + + it "raises ArgumentError if the input is too short" do + ["\004\bi", + "\004\bi\001", + "\004\bi\002", + "\004\bi\002\0", + "\004\bi\003", + "\004\bi\003\0", + "\004\bi\003\0\0", + "\004\bi\004", + "\004\bi\004\0", + "\004\bi\004\0\0", + "\004\bi\004\0\0\0"].each do |invalid| + -> { Marshal.load(invalid) }.should.raise(ArgumentError) + end + end + + if 0.size == 8 # for platforms like x86_64 + it "roundtrips 4611686018427387903 from dump/load correctly" do + Marshal.load(Marshal.dump(4611686018427387903)).should == 4611686018427387903 + end + end + end + + describe "for a Rational" do + it "loads" do + r = Marshal.load(Marshal.dump(Rational(1, 3))) + r.should == Rational(1, 3) + r.should.frozen? + end + end + + describe "for a Complex" do + it "loads" do + c = Marshal.load(Marshal.dump(Complex(4, 3))) + c.should == Complex(4, 3) + c.should.frozen? + end + end + + describe "for a Bignum" do + platform_is c_long_size: 64 do + context "that is Bignum on 32-bit platforms but Fixnum on 64-bit" do + it "dumps a Fixnum" do + val = Marshal.load("\004\bl+\ab:wU") + val.should == 1433877090 + val.class.should == Integer + end + + it "dumps an array containing multiple references to the Bignum as an array of Fixnum" do + arr = Marshal.load("\004\b[\al+\a\223BwU@\006") + arr.should == [1433879187, 1433879187] + arr.each { |v| v.class.should == Integer } + end + end + end + end + + describe "for a Time" do + it "loads" do + Marshal.load(Marshal.dump(Time.at(1))).should == Time.at(1) + end + + it "loads serialized instance variables" do + t = Time.new + t.instance_variable_set(:@foo, 'bar') + + Marshal.load(Marshal.dump(t)).instance_variable_get(:@foo).should == 'bar' + end + + it "loads Time objects stored as links" do + t = Time.new + + t1, t2 = Marshal.load(Marshal.dump([t, t])) + t1.should.equal? t2 + end + + it "keeps the local zone" do + with_timezone 'AST', 3 do + t = Time.local(2012, 1, 1) + Marshal.load(Marshal.dump(t)).zone.should == t.zone + end + end + + it "keeps UTC zone" do + t = Time.now.utc + t2 = Marshal.load(Marshal.dump(t)) + t2.should.utc? + end + + it "keeps the zone" do + t = nil + + with_timezone 'AST', 4 do + t = Time.local(2012, 1, 1) + end + + with_timezone 'EET', -2 do + Marshal.load(Marshal.dump(t)).zone.should == 'AST' + end + end + + it "keeps utc offset" do + t = Time.new(2007,11,1,15,25,0, "+09:00") + t2 = Marshal.load(Marshal.dump(t)) + t2.utc_offset.should == 32400 + end + + it "keeps nanoseconds" do + t = Time.now + Marshal.load(Marshal.dump(t)).nsec.should == t.nsec + end + + it "does not add any additional instance variable" do + t = Time.now + t2 = Marshal.load(Marshal.dump(t)) + t2.instance_variables.should.empty? + end + end + + describe "for nil" do + it "loads" do + Marshal.load("\x04\b0").should == nil + end + end + + describe "for true" do + it "loads" do + Marshal.load("\x04\bT").should == true + end + end + + describe "for false" do + it "loads" do + Marshal.load("\x04\bF").should == false + end + end + + describe "for a Class" do + it "loads" do + Marshal.load("\x04\bc\vString").should == String + end + + it "raises ArgumentError if given the name of a non-Module" do + -> { Marshal.load("\x04\bc\vKernel") }.should.raise(ArgumentError) + end + + it "raises ArgumentError if given a nonexistent class" do + -> { Marshal.load("\x04\bc\vStrung") }.should.raise(ArgumentError) + end + + it "raises ArgumentError when end of byte sequence reached before class name end" do + Marshal.dump(String).should == "\x04\bc\vString" + + -> { + Marshal.load("\x04\bc\vStr") + }.should.raise(ArgumentError, "marshal data too short") + end + end + + describe "for a Module" do + it "loads a module" do + Marshal.load("\x04\bm\vKernel").should == Kernel + end + + it "raises ArgumentError if given the name of a non-Class" do + -> { Marshal.load("\x04\bm\vString") }.should.raise(ArgumentError) + end + + it "loads an old module" do + Marshal.load("\x04\bM\vKernel").should == Kernel + end + + it "raises ArgumentError when end of byte sequence reached before module name end" do + Marshal.dump(Kernel).should == "\x04\bm\vKernel" + + -> { + Marshal.load("\x04\bm\vKer") + }.should.raise(ArgumentError, "marshal data too short") + end + end + + describe "for a wrapped C pointer" do + it "loads" do + class DumpableDir < Dir + def _dump_data + path + end + def _load_data path + initialize(path) + end + end + + data = "\x04\bd:\x10DumpableDirI\"\x06.\x06:\x06ET" + + dir = Marshal.load(data) + begin + dir.path.should == '.' + ensure + dir.close + end + end + + it "raises TypeError when the local class is missing _load_data" do + class UnloadableDumpableDir < Dir + def _dump_data + path + end + # no _load_data + end + + data = "\x04\bd:\x1AUnloadableDumpableDirI\"\x06.\x06:\x06ET" + + -> { Marshal.load(data) }.should.raise(TypeError) + end + + it "raises ArgumentError when the local class is a regular object" do + data = "\004\bd:\020UserDefined\0" + + -> { Marshal.load(data) }.should.raise(ArgumentError) + end + end + + describe "when a class does not exist in the namespace" do + before :each do + NamespaceTest.send(:const_set, :SameName, Class.new) + @data = Marshal.dump(NamespaceTest::SameName.new) + NamespaceTest.send(:remove_const, :SameName) + end + + it "raises an ArgumentError" do + message = "undefined class/module NamespaceTest::SameName" + -> { Marshal.load(@data) }.should.raise(ArgumentError, message) + end + end + + it "raises an ArgumentError with full constant name when the dumped constant is missing" do + NamespaceTest.send(:const_set, :KaBoom, Class.new) + @data = Marshal.dump(NamespaceTest::KaBoom.new) + NamespaceTest.send(:remove_const, :KaBoom) + + -> { Marshal.load(@data) }.should.raise(ArgumentError, /NamespaceTest::KaBoom/) + end end diff --git a/spec/ruby/core/marshal/restore_spec.rb b/spec/ruby/core/marshal/restore_spec.rb index 7e75d7dea64478..3135f881d43ec1 100644 --- a/spec/ruby/core/marshal/restore_spec.rb +++ b/spec/ruby/core/marshal/restore_spec.rb @@ -1,6 +1,7 @@ require_relative '../../spec_helper' -require_relative 'shared/load' describe "Marshal.restore" do - it_behaves_like :marshal_load, :restore + it "is an alias of Marshal.load" do + Marshal.method(:restore).should == Marshal.method(:load) + end end diff --git a/spec/ruby/core/marshal/shared/load.rb b/spec/ruby/core/marshal/shared/load.rb deleted file mode 100644 index 02c8e7f0b17d31..00000000000000 --- a/spec/ruby/core/marshal/shared/load.rb +++ /dev/null @@ -1,1291 +0,0 @@ -# encoding: binary -require_relative '../fixtures/marshal_data' - -describe :marshal_load, shared: true do - before :all do - @num_self_class = 1 - end - - it "raises an ArgumentError when the dumped data is truncated" do - obj = {first: 1, second: 2, third: 3} - -> { Marshal.send(@method, Marshal.dump(obj)[0, 5]) }.should.raise(ArgumentError, "marshal data too short") - end - - it "raises an ArgumentError when the argument is empty String" do - -> { Marshal.send(@method, "") }.should.raise(ArgumentError, "marshal data too short") - end - - it "raises an ArgumentError when the dumped class is missing" do - Object.send(:const_set, :KaBoom, Class.new) - kaboom = Marshal.dump(KaBoom.new) - Object.send(:remove_const, :KaBoom) - - -> { Marshal.send(@method, kaboom) }.should.raise(ArgumentError) - end - - describe "when called with freeze: true" do - it "returns frozen strings" do - string = Marshal.send(@method, Marshal.dump("foo"), freeze: true) - string.should == "foo" - string.should.frozen? - - utf8_string = "foo".encode(Encoding::UTF_8) - string = Marshal.send(@method, Marshal.dump(utf8_string), freeze: true) - string.should == utf8_string - string.should.frozen? - end - - it "returns frozen arrays" do - array = Marshal.send(@method, Marshal.dump([1, 2, 3]), freeze: true) - array.should == [1, 2, 3] - array.should.frozen? - end - - it "returns frozen hashes" do - hash = Marshal.send(@method, Marshal.dump({foo: 42}), freeze: true) - hash.should == {foo: 42} - hash.should.frozen? - end - - it "returns frozen regexps" do - regexp = Marshal.send(@method, Marshal.dump(/foo/), freeze: true) - regexp.should == /foo/ - regexp.should.frozen? - end - - it "returns frozen structs" do - struct = Marshal.send(@method, Marshal.dump(MarshalSpec::StructToDump.new(1, 2)), freeze: true) - struct.should == MarshalSpec::StructToDump.new(1, 2) - struct.should.frozen? - end - - it "returns frozen objects" do - source_object = Object.new - - object = Marshal.send(@method, Marshal.dump(source_object), freeze: true) - object.should.frozen? - end - - describe "deep freezing" do - it "returns hashes with frozen keys and values" do - key = Object.new - value = Object.new - source_object = {key => value} - - hash = Marshal.send(@method, Marshal.dump(source_object), freeze: true) - hash.size.should == 1 - hash.keys[0].should.frozen? - hash.values[0].should.frozen? - end - - it "returns arrays with frozen elements" do - object = Object.new - source_object = [object] - - array = Marshal.send(@method, Marshal.dump(source_object), freeze: true) - array.size.should == 1 - array[0].should.frozen? - end - - it "returns structs with frozen members" do - object1 = Object.new - object2 = Object.new - source_object = MarshalSpec::StructToDump.new(object1, object2) - - struct = Marshal.send(@method, Marshal.dump(source_object), freeze: true) - struct.a.should.frozen? - struct.b.should.frozen? - end - - it "returns objects with frozen instance variables" do - source_object = Object.new - instance_variable = Object.new - source_object.instance_variable_set(:@a, instance_variable) - - object = Marshal.send(@method, Marshal.dump(source_object), freeze: true) - object.instance_variable_get(:@a).should != nil - object.instance_variable_get(:@a).should.frozen? - end - - it "deduplicates frozen strings" do - source_object = ["foo" + "bar", "foobar"] - object = Marshal.send(@method, Marshal.dump(source_object), freeze: true) - - object[0].should.equal?(object[1]) - end - end - - it "does not freeze modules" do - object = Marshal.send(@method, Marshal.dump(Kernel), freeze: true) - object.should_not.frozen? - Kernel.should_not.frozen? - end - - it "does not freeze classes" do - object = Marshal.send(@method, Marshal.dump(Object), freeze: true) - object.should_not.frozen? - Object.should_not.frozen? - end - - it "does freeze extended objects" do - object = Marshal.load("\x04\be:\x0FEnumerableo:\vObject\x00", freeze: true) - object.should.frozen? - end - - it "does freeze extended objects with instance variables" do - object = Marshal.load("\x04\be:\x0FEnumerableo:\vObject\x06:\n@ivarT", freeze: true) - object.should.frozen? - end - - it "returns frozen object having #_dump method" do - object = Marshal.send(@method, Marshal.dump(UserDefined.new), freeze: true) - object.should.frozen? - end - - it "returns frozen object responding to #marshal_dump and #marshal_load" do - object = Marshal.send(@method, Marshal.dump(UserMarshal.new), freeze: true) - object.should.frozen? - end - - it "returns frozen object extended by a module" do - object = Object.new - object.extend(MarshalSpec::ModuleToExtendBy) - - object = Marshal.send(@method, Marshal.dump(object), freeze: true) - object.should.frozen? - end - - it "does not call freeze method" do - object = MarshalSpec::ObjectWithFreezeRaisingException.new - object = Marshal.send(@method, Marshal.dump(object), freeze: true) - object.should.frozen? - end - - it "returns frozen object even if object does not respond to freeze method" do - object = MarshalSpec::ObjectWithoutFreeze.new - object = Marshal.send(@method, Marshal.dump(object), freeze: true) - object.should.frozen? - end - - it "returns a frozen object when is an instance of String/Array/Regexp/Hash subclass and has instance variables" do - source_object = UserString.new - source_object.instance_variable_set(:@foo, "bar") - - object = Marshal.send(@method, Marshal.dump(source_object), freeze: true) - object.should.frozen? - end - - describe "when called with a proc" do - it "call the proc with frozen objects" do - arr = [] - s = +'hi' - s.instance_variable_set(:@foo, 5) - st = Struct.new("Brittle", :a).new - st.instance_variable_set(:@clue, 'none') - st.a = 0.0 - h = Hash.new('def') - h['nine'] = 9 - a = [:a, :b, :c] - a.instance_variable_set(:@two, 2) - obj = [s, 10, s, s, st, a] - obj.instance_variable_set(:@zoo, 'ant') - proc = Proc.new { |o| arr << o; o} - - Marshal.send( - @method, - "\x04\bI[\vI\"\ahi\a:\x06EF:\t@fooi\ni\x0F@\x06@\x06IS:\x14Struct::Brittle\x06:\x06af\x060\x06:\n@clueI\"\tnone\x06;\x00FI[\b;\b:\x06b:\x06c\x06:\t@twoi\a\x06:\t@zooI\"\bant\x06;\x00F", - proc, - freeze: true, - ) - - arr.should == [ - false, 5, "hi", 10, "hi", "hi", 0.0, false, "none", st, - :b, :c, 2, a, false, "ant", ["hi", 10, "hi", "hi", st, [:a, :b, :c]], - ] - - arr.each do |v| - v.should.frozen? - end - - Struct.send(:remove_const, :Brittle) - end - - it "does not freeze the object returned by the proc" do - string = Marshal.send(@method, Marshal.dump("foo"), proc { |o| o.upcase }, freeze: true) - string.should == "FOO" - string.should_not.frozen? - end - end - end - - describe "when called with a proc" do - it "call the proc with fully initialized strings" do - utf8_string = "foo".encode(Encoding::UTF_8) - Marshal.send(@method, Marshal.dump(utf8_string), proc { |arg| - if arg.is_a?(String) - arg.should == utf8_string - arg.encoding.should == Encoding::UTF_8 - end - arg - }) - end - - it "no longer mutate the object after it was passed to the proc" do - string = Marshal.load(Marshal.dump("foo"), :freeze.to_proc) - string.should.frozen? - end - - it "call the proc with extended objects" do - objs = [] - obj = Marshal.load("\x04\be:\x0FEnumerableo:\vObject\x00", Proc.new { |o| objs << o; o }) - objs.should == [obj] - end - - it "returns the value of the proc" do - Marshal.send(@method, Marshal.dump([1,2]), proc { [3,4] }).should == [3,4] - end - - it "calls the proc for recursively visited data" do - a = [1] - a << a - ret = [] - Marshal.send(@method, Marshal.dump(a), proc { |arg| ret << arg.inspect; arg }) - ret[0].should == 1.inspect - ret[1].should == a.inspect - ret.size.should == 2 - end - - it "loads an Array with proc" do - arr = [] - s = +'hi' - s.instance_variable_set(:@foo, 5) - st = Struct.new("Brittle", :a).new - st.instance_variable_set(:@clue, 'none') - st.a = 0.0 - h = Hash.new('def') - h['nine'] = 9 - a = [:a, :b, :c] - a.instance_variable_set(:@two, 2) - obj = [s, 10, s, s, st, a] - obj.instance_variable_set(:@zoo, 'ant') - proc = Proc.new { |o| arr << o.dup; o} - - Marshal.send(@method, "\x04\bI[\vI\"\ahi\a:\x06EF:\t@fooi\ni\x0F@\x06@\x06IS:\x14Struct::Brittle\x06:\x06af\x060\x06:\n@clueI\"\tnone\x06;\x00FI[\b;\b:\x06b:\x06c\x06:\t@twoi\a\x06:\t@zooI\"\bant\x06;\x00F", proc) - - arr.should == [ - false, 5, "hi", 10, "hi", "hi", 0.0, false, "none", st, - :b, :c, 2, a, false, "ant", ["hi", 10, "hi", "hi", st, [:a, :b, :c]], - ] - Struct.send(:remove_const, :Brittle) - end - end - - describe "when called with nil for the proc argument" do - it "behaves as if no proc argument was passed" do - a = [1] - a << a - b = Marshal.send(@method, Marshal.dump(a), nil) - b.should == a - end - end - - describe "when called on objects with custom _dump methods" do - it "does not set instance variables of an object with user-defined _dump/_load" do - # this string represents: <#UserPreviouslyDefinedWithInitializedIvar @field2=7 @field1=6> - dump_str = "\004\bu:-UserPreviouslyDefinedWithInitializedIvar\a:\f@field2i\f:\f@field1i\v" - - UserPreviouslyDefinedWithInitializedIvar.should_receive(:_load).and_return(UserPreviouslyDefinedWithInitializedIvar.new) - marshaled_obj = Marshal.send(@method, dump_str) - - marshaled_obj.should.instance_of?(UserPreviouslyDefinedWithInitializedIvar) - marshaled_obj.field1.should == nil - marshaled_obj.field2.should == nil - end - - it "loads the String in non US-ASCII and non UTF-8 encoding" do - source_object = UserDefinedString.new("a".encode("windows-1251")) - object = Marshal.send(@method, Marshal.dump(source_object)) - object.string.should == "a".encode("windows-1251") - end - - it "loads the String in multibyte encoding" do - source_object = UserDefinedString.new("a".encode("utf-32le")) - object = Marshal.send(@method, Marshal.dump(source_object)) - object.string.should == "a".encode("utf-32le") - end - - describe "that returns an immediate value" do - it "loads an array containing an instance of the object, followed by multiple instances of another object" do - str = "string" - - # this string represents: [<#UserDefinedImmediate A>, <#String "string">, <#String "string">] - marshaled_obj = Marshal.send(@method, "\004\b[\bu:\031UserDefinedImmediate\000\"\vstring@\a") - - marshaled_obj.should == [nil, str, str] - end - - it "loads any structure with multiple references to the same object, followed by multiple instances of another object" do - str = "string" - - # this string represents: {a: <#UserDefinedImmediate A>, b: <#UserDefinedImmediate A>, c: <#String "string">, d: <#String "string">} - hash_dump = "\x04\b{\t:\x06aIu:\x19UserDefinedImmediate\x00\x06:\x06ET:\x06b@\x06:\x06cI\"\vstring\x06;\aT:\x06d@\a" - - marshaled_obj = Marshal.send(@method, hash_dump) - marshaled_obj.should == {a: nil, b: nil, c: str, d: str} - - # this string represents: [<#UserDefinedImmediate A>, <#UserDefinedImmediate A>, <#String "string">, <#String "string">] - array_dump = "\x04\b[\tIu:\x19UserDefinedImmediate\x00\x06:\x06ET@\x06I\"\vstring\x06;\x06T@\a" - - marshaled_obj = Marshal.send(@method, array_dump) - marshaled_obj.should == [nil, nil, str, str] - end - - it "loads an array containing references to multiple instances of the object, followed by multiple instances of another object" do - str = "string" - - # this string represents: [<#UserDefinedImmediate A>, <#UserDefinedImmediate B>, <#String "string">, <#String "string">] - array_dump = "\x04\b[\tIu:\x19UserDefinedImmediate\x00\x06:\x06ETIu;\x00\x00\x06;\x06TI\"\vstring\x06;\x06T@\b" - - marshaled_obj = Marshal.send(@method, array_dump) - marshaled_obj.should == [nil, nil, str, str] - end - end - end - - it "loads an array containing objects having _dump method, and with proc" do - arr = [] - myproc = Proc.new { |o| arr << o.dup; o } - o1 = UserDefined.new; - o2 = UserDefinedWithIvar.new - obj = [o1, o2, o1, o2] - - Marshal.send(@method, "\x04\b[\tu:\x10UserDefined\x18\x04\b[\aI\"\nstuff\x06:\x06EF@\x06u:\x18UserDefinedWithIvar>\x04\b[\bI\"\nstuff\a:\x06EF:\t@foo:\x18UserDefinedWithIvarI\"\tmore\x06;\x00F@\a@\x06@\a", myproc) - - arr[0].should == o1 - arr[1].should == o2 - arr[2].should == obj - arr.size.should == 3 - end - - it "loads an array containing objects having marshal_dump method, and with proc" do - arr = [] - proc = Proc.new { |o| arr << o.dup; o } - o1 = UserMarshal.new - o2 = UserMarshalWithIvar.new - - Marshal.send(@method, "\004\b[\tU:\020UserMarshal\"\nstuffU:\030UserMarshalWithIvar[\006\"\fmy data@\006@\b", proc) - - arr[0].should == 'stuff' - arr[1].should == o1 - arr[2].should == 'my data' - arr[3].should == ['my data'] - arr[4].should == o2 - arr[5].should == [o1, o2, o1, o2] - - arr.size.should == 6 - end - - it "assigns classes to nested subclasses of Array correctly" do - arr = ArraySub.new(ArraySub.new) - arr_dump = Marshal.dump(arr) - Marshal.send(@method, arr_dump).class.should == ArraySub - end - - it "loads subclasses of Array with overridden << and push correctly" do - arr = ArraySubPush.new - arr[0] = '1' - arr_dump = Marshal.dump(arr) - Marshal.send(@method, arr_dump).should == arr - end - - it "raises a TypeError with bad Marshal version" do - marshal_data = +'\xff\xff' - marshal_data[0] = (Marshal::MAJOR_VERSION).chr - marshal_data[1] = (Marshal::MINOR_VERSION + 1).chr - - -> { Marshal.send(@method, marshal_data) }.should.raise(TypeError) - - marshal_data = +'\xff\xff' - marshal_data[0] = (Marshal::MAJOR_VERSION - 1).chr - marshal_data[1] = (Marshal::MINOR_VERSION).chr - - -> { Marshal.send(@method, marshal_data) }.should.raise(TypeError) - end - - it "raises EOFError on loading an empty file" do - temp_file = tmp("marshal.rubyspec.tmp.#{Process.pid}") - file = File.new(temp_file, "w+") - begin - -> { Marshal.send(@method, file) }.should.raise(EOFError) - ensure - file.close - rm_r temp_file - end - end - - # Note: Ruby 1.9 should be compatible with older marshal format - MarshalSpec::DATA.each do |description, (object, marshal, attributes)| - it "loads a #{description}" do - Marshal.send(@method, marshal).should == object - end - end - - MarshalSpec::DATA_19.each do |description, (object, marshal, attributes)| - it "loads a #{description}" do - Marshal.send(@method, marshal).should == object - end - end - - describe "for an Array" do - it "loads an array containing the same objects" do - s = 'oh' - b = 'hi' - r = // - d = [b, :no, s, :go] - c = String - f = 1.0 - - o1 = UserMarshalWithIvar.new; o2 = UserMarshal.new - - obj = [:so, 'hello', 100, :so, :so, d, :so, o2, :so, :no, o2, - :go, c, nil, Struct::Pyramid.new, f, :go, :no, s, b, r, - :so, 'huh', o1, true, b, b, 99, r, b, s, :so, f, c, :no, o1, d] - - Marshal.send(@method, "\004\b[*:\aso\"\nhelloii;\000;\000[\t\"\ahi:\ano\"\aoh:\ago;\000U:\020UserMarshal\"\nstuff;\000;\006@\n;\ac\vString0S:\024Struct::Pyramid\000f\0061;\a;\006@\t@\b/\000\000;\000\"\bhuhU:\030UserMarshalWithIvar[\006\"\fmy dataT@\b@\bih@\017@\b@\t;\000@\016@\f;\006@\021@\a").should == - obj - end - - it "loads an array having ivar" do - s = +'well' - s.instance_variable_set(:@foo, 10) - obj = ['5', s, 'hi'].extend(Meths, MethsMore) - obj.instance_variable_set(:@mix, s) - new_obj = Marshal.send(@method, "\004\bI[\b\"\0065I\"\twell\006:\t@fooi\017\"\ahi\006:\t@mix@\a") - new_obj.should == obj - new_obj.instance_variable_get(:@mix).should.equal? new_obj[1] - new_obj[1].instance_variable_get(:@foo).should == 10 - end - - it "loads an extended Array object containing a user-marshaled object" do - obj = [UserMarshal.new, UserMarshal.new].extend(Meths) - dump = "\x04\be:\nMeths[\ao:\x10UserMarshal\x06:\n@dataI\"\nstuff\x06:\x06ETo;\x06\x06;\aI\"\nstuff\x06;\bT" - new_obj = Marshal.send(@method, dump) - - new_obj.should == obj - obj_ancestors = class << obj; ancestors[1..-1]; end - new_obj_ancestors = class << new_obj; ancestors[1..-1]; end - obj_ancestors.should == new_obj_ancestors - end - end - - describe "for a Hash" do - it "loads an extended_user_hash with a parameter to initialize" do - obj = UserHashInitParams.new(:abc).extend(Meths) - - new_obj = Marshal.send(@method, "\004\bIe:\nMethsC:\027UserHashInitParams{\000\006:\a@a:\babc") - - new_obj.should == obj - new_obj_metaclass_ancestors = class << new_obj; ancestors; end - new_obj_metaclass_ancestors[@num_self_class].should == Meths - new_obj_metaclass_ancestors[@num_self_class+1].should == UserHashInitParams - end - - it "loads an extended hash object containing a user-marshaled object" do - obj = {a: UserMarshal.new}.extend(Meths) - - new_obj = Marshal.send(@method, "\004\be:\nMeths{\006:\006aU:\020UserMarshal\"\nstuff") - - new_obj.should == obj - new_obj_metaclass_ancestors = class << new_obj; ancestors; end - new_obj_metaclass_ancestors[@num_self_class].should == Meths - new_obj_metaclass_ancestors[@num_self_class+1].should == Hash - end - - it "preserves hash ivars when hash contains a string having ivar" do - s = +'string' - s.instance_variable_set :@string_ivar, 'string ivar' - h = { key: s } - h.instance_variable_set :@hash_ivar, 'hash ivar' - - unmarshalled = Marshal.send(@method, Marshal.dump(h)) - unmarshalled.instance_variable_get(:@hash_ivar).should == 'hash ivar' - unmarshalled[:key].instance_variable_get(:@string_ivar).should == 'string ivar' - end - - it "preserves compare_by_identity behaviour" do - h = { a: 1 } - h.compare_by_identity - unmarshalled = Marshal.send(@method, Marshal.dump(h)) - unmarshalled.should.compare_by_identity? - - h = { a: 1 } - unmarshalled = Marshal.send(@method, Marshal.dump(h)) - unmarshalled.should_not.compare_by_identity? - end - - it "preserves compare_by_identity behaviour for a Hash subclass" do - h = UserHash.new({ a: 1 }) - h.compare_by_identity - unmarshalled = Marshal.send(@method, Marshal.dump(h)) - unmarshalled.should.compare_by_identity? - - h = UserHash.new({ a: 1 }) - unmarshalled = Marshal.send(@method, Marshal.dump(h)) - unmarshalled.should_not.compare_by_identity? - end - - it "allocates an instance of the proper class when Hash subclass with compare_by_identity behaviour" do - h = UserHash.new({ a: 1 }) - h.compare_by_identity - - unmarshalled = Marshal.send(@method, Marshal.dump(h)) - unmarshalled.should.kind_of?(UserHash) - end - end - - describe "for a Symbol" do - it "loads a Symbol" do - sym = Marshal.send(@method, "\004\b:\vsymbol") - sym.should == :symbol - sym.encoding.should == Encoding::US_ASCII - end - - it "loads a big Symbol" do - sym = ('big' * 100).to_sym - Marshal.send(@method, "\004\b:\002,\001#{'big' * 100}").should == sym - end - - it "loads an encoded Symbol" do - s = "\u2192" - - sym = Marshal.send(@method, "\x04\bI:\b\xE2\x86\x92\x06:\x06ET") - sym.should == s.encode("utf-8").to_sym - sym.encoding.should == Encoding::UTF_8 - - sym = Marshal.send(@method, "\x04\bI:\t\xFE\xFF!\x92\x06:\rencoding\"\vUTF-16") - sym.should == s.encode("utf-16").to_sym - sym.encoding.should == Encoding::UTF_16 - - sym = Marshal.send(@method, "\x04\bI:\a\x92!\x06:\rencoding\"\rUTF-16LE") - sym.should == s.encode("utf-16le").to_sym - sym.encoding.should == Encoding::UTF_16LE - - sym = Marshal.send(@method, "\x04\bI:\a!\x92\x06:\rencoding\"\rUTF-16BE") - sym.should == s.encode("utf-16be").to_sym - sym.encoding.should == Encoding::UTF_16BE - - sym = Marshal.send(@method, "\x04\bI:\a\xA2\xAA\x06:\rencoding\"\vEUC-JP") - sym.should == s.encode("euc-jp").to_sym - sym.encoding.should == Encoding::EUC_JP - - sym = Marshal.send(@method, "\x04\bI:\a\x81\xA8\x06:\rencoding\"\x10Windows-31J") - sym.should == s.encode("sjis").to_sym - sym.encoding.should == Encoding::SJIS - end - - it "loads a binary encoded Symbol" do - s = "\u2192".dup.force_encoding("binary").to_sym - sym = Marshal.send(@method, "\x04\b:\b\xE2\x86\x92") - sym.should == s - sym.encoding.should == Encoding::BINARY - end - - it "loads multiple Symbols sharing the same encoding" do - # Note that the encoding is a link for the second Symbol - symbol1 = "I:\t\xE2\x82\xACa\x06:\x06ET" - symbol2 = "I:\t\xE2\x82\xACb\x06;\x06T" - dump = "\x04\b[\a#{symbol1}#{symbol2}" - value = Marshal.send(@method, dump) - value.map(&:encoding).should == [Encoding::UTF_8, Encoding::UTF_8] - expected = [ - "€a".dup.force_encoding(Encoding::UTF_8).to_sym, - "€b".dup.force_encoding(Encoding::UTF_8).to_sym - ] - value.should == expected - - value = Marshal.send(@method, "\x04\b[\b#{symbol1}#{symbol2};\x00") - value.map(&:encoding).should == [Encoding::UTF_8, Encoding::UTF_8, Encoding::UTF_8] - value.should == [*expected, expected[0]] - end - - it "raises ArgumentError when end of byte sequence reached before symbol characters end" do - Marshal.dump(:hello).should == "\x04\b:\nhello" - - -> { - Marshal.send(@method, "\x04\b:\nhel") - }.should.raise(ArgumentError, "marshal data too short") - end - end - - describe "for a String" do - it "loads a string having ivar with ref to self" do - obj = +'hi' - obj.instance_variable_set(:@self, obj) - Marshal.send(@method, "\004\bI\"\ahi\006:\n@self@\000").should == obj - end - - it "loads a string through StringIO stream" do - require 'stringio' - obj = "This is a string which should be unmarshalled through StringIO stream!" - Marshal.send(@method, StringIO.new(Marshal.dump(obj))).should == obj - end - - it "sets binmode if it is loading through StringIO stream" do - io = StringIO.new("\004\b:\vsymbol") - def io.binmode; raise "binmode"; end - -> { Marshal.load(io) }.should.raise(RuntimeError, "binmode") - end - - it "loads a string with an ivar" do - str = Marshal.send(@method, "\x04\bI\"\x00\x06:\t@fooI\"\bbar\x06:\x06EF") - str.instance_variable_get("@foo").should == "bar" - end - - it "loads a String subclass with custom constructor" do - str = Marshal.send(@method, "\x04\bC: UserCustomConstructorString\"\x00") - str.should.instance_of?(UserCustomConstructorString) - end - - it "loads a US-ASCII String" do - str = "abc".dup.force_encoding("us-ascii") - data = "\x04\bI\"\babc\x06:\x06EF" - result = Marshal.send(@method, data) - result.should == str - result.encoding.should.equal?(Encoding::US_ASCII) - end - - it "loads a UTF-8 String" do - str = "\x6d\xc3\xb6\x68\x72\x65".dup.force_encoding("utf-8") - data = "\x04\bI\"\vm\xC3\xB6hre\x06:\x06ET" - result = Marshal.send(@method, data) - result.should == str - result.encoding.should.equal?(Encoding::UTF_8) - end - - it "loads a String in another encoding" do - str = "\x6d\x00\xf6\x00\x68\x00\x72\x00\x65\x00".dup.force_encoding("utf-16le") - data = "\x04\bI\"\x0Fm\x00\xF6\x00h\x00r\x00e\x00\x06:\rencoding\"\rUTF-16LE" - result = Marshal.send(@method, data) - result.should == str - result.encoding.should.equal?(Encoding::UTF_16LE) - end - - it "loads a String as BINARY if no encoding is specified at the end" do - str = "\xC3\xB8".dup.force_encoding("BINARY") - data = "\x04\b\"\a\xC3\xB8".dup.force_encoding("UTF-8") - result = Marshal.send(@method, data) - result.encoding.should == Encoding::BINARY - result.should == str - end - - it "raises ArgumentError when end of byte sequence reached before string characters end" do - Marshal.dump("hello").should == "\x04\b\"\nhello" - - -> { - Marshal.send(@method, "\x04\b\"\nhel") - }.should.raise(ArgumentError, "marshal data too short") - end - end - - describe "for a Struct" do - it "loads a extended_struct having fields with same objects" do - s = 'hi' - obj = Struct.new("Extended", :a, :b).new.extend(Meths) - dump = "\004\be:\nMethsS:\025Struct::Extended\a:\006a0:\006b0" - Marshal.send(@method, dump).should == obj - - obj.a = [:a, s] - obj.b = [:Meths, s] - dump = "\004\be:\nMethsS:\025Struct::Extended\a:\006a[\a;\a\"\ahi:\006b[\a;\000@\a" - Marshal.send(@method, dump).should == obj - Struct.send(:remove_const, :Extended) - end - - it "loads a struct having ivar" do - obj = Struct.new("Thick").new - obj.instance_variable_set(:@foo, 5) - reloaded = Marshal.send(@method, "\004\bIS:\022Struct::Thick\000\006:\t@fooi\n") - reloaded.should == obj - reloaded.instance_variable_get(:@foo).should == 5 - Struct.send(:remove_const, :Thick) - end - - it "loads a struct having fields" do - obj = Struct.new("Ure1", :a, :b).new - Marshal.send(@method, "\004\bS:\021Struct::Ure1\a:\006a0:\006b0").should == obj - Struct.send(:remove_const, :Ure1) - end - - it "does not call initialize on the unmarshaled struct" do - threadlocal_key = MarshalSpec::StructWithUserInitialize::THREADLOCAL_KEY - - s = MarshalSpec::StructWithUserInitialize.new('foo') - Thread.current[threadlocal_key].should == ['foo'] - s.a.should == 'foo' - - Thread.current[threadlocal_key] = nil - - dumped = Marshal.dump(s) - loaded = Marshal.send(@method, dumped) - - Thread.current[threadlocal_key].should == nil - loaded.a.should == 'foo' - end - end - - describe "for a Data" do - it "loads a Data" do - obj = MarshalSpec::DataSpec::Measure.new(100, 'km') - dumped = "\x04\bS:#MarshalSpec::DataSpec::Measure\a:\vamountii:\tunit\"\akm" - Marshal.dump(obj).should == dumped - - Marshal.send(@method, dumped).should == obj - end - - it "loads an extended Data" do - obj = MarshalSpec::DataSpec::MeasureExtended.new(100, "km") - dumped = "\x04\bS:+MarshalSpec::DataSpec::MeasureExtended\a:\vamountii:\tunit\"\akm" - Marshal.dump(obj).should == dumped - - Marshal.send(@method, dumped).should == obj - end - - it "returns a frozen object" do - obj = MarshalSpec::DataSpec::Measure.new(100, 'km') - dumped = "\x04\bS:#MarshalSpec::DataSpec::Measure\a:\vamountii:\tunit\"\akm" - Marshal.dump(obj).should == dumped - - Marshal.send(@method, dumped).should.frozen? - end - end - - describe "for an Exception" do - it "loads a marshalled exception with no message" do - obj = Exception.new - loaded = Marshal.send(@method, "\004\bo:\016Exception\a:\abt0:\tmesg0") - loaded.message.should == obj.message - loaded.backtrace.should == obj.backtrace - loaded = Marshal.send(@method, "\x04\bo:\x0EException\a:\tmesg0:\abt0") - loaded.message.should == obj.message - loaded.backtrace.should == obj.backtrace - end - - it "loads a marshalled exception with a message" do - obj = Exception.new("foo") - loaded = Marshal.send(@method, "\004\bo:\016Exception\a:\abt0:\tmesg\"\bfoo") - loaded.message.should == obj.message - loaded.backtrace.should == obj.backtrace - loaded = Marshal.send(@method, "\x04\bo:\x0EException\a:\tmesgI\"\bfoo\x06:\x06EF:\abt0") - loaded.message.should == obj.message - loaded.backtrace.should == obj.backtrace - end - - it "loads a marshalled exception with a backtrace" do - obj = Exception.new("foo") - obj.set_backtrace(["foo/bar.rb:10"]) - loaded = Marshal.send(@method, "\004\bo:\016Exception\a:\abt[\006\"\022foo/bar.rb:10:\tmesg\"\bfoo") - loaded.message.should == obj.message - loaded.backtrace.should == obj.backtrace - loaded = Marshal.send(@method, "\x04\bo:\x0EException\a:\tmesgI\"\bfoo\x06:\x06EF:\abt[\x06I\"\x12foo/bar.rb:10\x06;\aF") - loaded.message.should == obj.message - loaded.backtrace.should == obj.backtrace - end - - it "loads an marshalled exception with ivars" do - s = 'hi' - arr = [:so, :so, s, s] - obj = Exception.new("foo") - obj.instance_variable_set :@arr, arr - - loaded = Marshal.send(@method, "\x04\bo:\x0EException\b:\tmesg\"\bfoo:\abt0:\t@arr[\t:\aso;\t\"\ahi@\b") - new_arr = loaded.instance_variable_get :@arr - - loaded.message.should == obj.message - new_arr.should == arr - end - end - - describe "for an Object" do - it "loads an object" do - Marshal.send(@method, "\004\bo:\vObject\000").should.is_a?(Object) - end - - it "loads an extended Object" do - obj = Object.new.extend(Meths) - - new_obj = Marshal.send(@method, "\004\be:\nMethso:\vObject\000") - - new_obj.class.should == obj.class - new_obj_metaclass_ancestors = class << new_obj; ancestors; end - new_obj_metaclass_ancestors[@num_self_class, 2].should == [Meths, Object] - end - - it "loads an object having ivar" do - s = 'hi' - arr = [:so, :so, s, s] - obj = Object.new - obj.instance_variable_set :@str, arr - - new_obj = Marshal.send(@method, "\004\bo:\vObject\006:\t@str[\t:\aso;\a\"\ahi@\a") - new_str = new_obj.instance_variable_get :@str - - new_str.should == arr - end - - it "loads an Object with a non-US-ASCII instance variable" do - ivar = "@é".dup.force_encoding(Encoding::UTF_8).to_sym - obj = Marshal.send(@method, "\x04\bo:\vObject\x06I:\b@\xC3\xA9\x06:\x06ETi\x06") - obj.instance_variables.should == [ivar] - obj.instance_variables[0].encoding.should == Encoding::UTF_8 - obj.instance_variable_get(ivar).should == 1 - end - - it "raises ArgumentError if the object from an 'o' stream is not dumpable as 'o' type user class" do - -> do - Marshal.send(@method, "\x04\bo:\tFile\001\001:\001\005@path\"\x10/etc/passwd") - end.should.raise(ArgumentError) - end - - it "raises ArgumentError when end of byte sequence reached before class name end" do - Marshal.dump(Object.new).should == "\x04\bo:\vObject\x00" - - -> { - Marshal.send(@method, "\x04\bo:\vObj") - }.should.raise(ArgumentError, "marshal data too short") - end - end - - describe "for an object responding to #marshal_dump and #marshal_load" do - it "loads a user-marshaled object" do - obj = UserMarshal.new - obj.data = :data - value = [obj, :data] - dump = Marshal.dump(value) - dump.should == "\x04\b[\aU:\x10UserMarshal:\tdata;\x06" - reloaded = Marshal.load(dump) - reloaded.should == value - end - end - - describe "for a user object" do - it "loads a user-marshaled extended object" do - obj = UserMarshal.new.extend(Meths) - - new_obj = Marshal.send(@method, "\004\bU:\020UserMarshal\"\nstuff") - - new_obj.should == obj - new_obj_metaclass_ancestors = class << new_obj; ancestors; end - new_obj_metaclass_ancestors[@num_self_class].should == UserMarshal - end - - it "loads a UserObject" do - Marshal.send(@method, "\004\bo:\017UserObject\000").should.is_a?(UserObject) - end - - describe "that extends a core type other than Object or BasicObject" do - after :each do - MarshalSpec.reset_swapped_class - end - - it "raises ArgumentError if the resulting class does not extend the same type" do - MarshalSpec.set_swapped_class(Class.new(Hash)) - data = Marshal.dump(MarshalSpec::SwappedClass.new) - - MarshalSpec.set_swapped_class(Class.new(Array)) - -> { Marshal.send(@method, data) }.should.raise(ArgumentError) - - MarshalSpec.set_swapped_class(Class.new) - -> { Marshal.send(@method, data) }.should.raise(ArgumentError) - end - end - end - - describe "for a Regexp" do - ruby_version_is "4.1" do - it "raises FrozenError for an extended Regexp" do - -> { - Marshal.send(@method, "\004\be:\nMethse:\016MethsMore/\n[a-z]\000") - }.should.raise(FrozenError) - end - - it "raises FrozenError when regexp has instance variables" do - -> { - Marshal.send(@method, "\x04\bI/\nhello\x00\a:\x06EF:\x11@regexp_ivar[\x06i/") - }.should.raise(FrozenError) - end - end - - ruby_version_is ""..."4.1" do - it "loads an extended Regexp" do - obj = /[a-z]/.dup.extend(Meths, MethsMore) - new_obj = Marshal.send(@method, "\004\be:\nMethse:\016MethsMore/\n[a-z]\000") - - new_obj.should == obj - new_obj_metaclass_ancestors = class << new_obj; ancestors; end - new_obj_metaclass_ancestors[@num_self_class, 3].should == - [Meths, MethsMore, Regexp] - end - - it "restore the regexp instance variables" do - obj = Regexp.new("hello") - obj.instance_variable_set(:@regexp_ivar, [42]) - - new_obj = Marshal.send(@method, "\x04\bI/\nhello\x00\a:\x06EF:\x11@regexp_ivar[\x06i/") - new_obj.instance_variables.should == [:@regexp_ivar] - new_obj.instance_variable_get(:@regexp_ivar).should == [42] - end - end - - it "loads a Regexp subclass instance variables" do - obj = UserRegexp.new('abc') - obj.instance_variable_set(:@noise, 'much') - - new_obj = Marshal.send(@method, Marshal.dump(obj)) - - new_obj.should == obj - new_obj.instance_variable_get(:@noise).should == 'much' - new_obj_metaclass_ancestors = class << new_obj; ancestors; end - new_obj_metaclass_ancestors[@num_self_class, 2].should == - [UserRegexp, Regexp] - end - - it "loads a Regexp subclass instance variables when it is extended with a module" do - obj = UserRegexp.new('').extend(Meths) - obj.instance_variable_set(:@noise, 'much') - - new_obj = Marshal.send(@method, "\004\bIe:\nMethsC:\017UserRegexp/\000\000\006:\v@noise\"\tmuch") - - new_obj.should == obj - new_obj.instance_variable_get(:@noise).should == 'much' - new_obj_metaclass_ancestors = class << new_obj; ancestors; end - new_obj_metaclass_ancestors[@num_self_class, 3].should == - [Meths, UserRegexp, Regexp] - end - - it "preserves Regexp encoding" do - source_object = Regexp.new("a".encode("utf-32le")) - regexp = Marshal.send(@method, Marshal.dump(source_object)) - - regexp.encoding.should == Encoding::UTF_32LE - regexp.source.should == "a".encode("utf-32le") - end - - it "raises ArgumentError when end of byte sequence reached before source string end" do - Marshal.dump(/hello world/).should == "\x04\bI/\x10hello world\x00\x06:\x06EF" - - -> { - Marshal.send(@method, "\x04\bI/\x10hel") - }.should.raise(ArgumentError, "marshal data too short") - end - end - - describe "for a Float" do - it "loads a Float NaN" do - obj = 0.0 / 0.0 - Marshal.send(@method, "\004\bf\bnan").to_s.should == obj.to_s - end - - it "loads a Float 1.3" do - Marshal.send(@method, "\004\bf\v1.3\000\314\315").should == 1.3 - end - - it "loads a Float -5.1867345e-22" do - obj = -5.1867345e-22 - Marshal.send(@method, "\004\bf\037-5.1867345000000008e-22\000\203_").should be_close(obj, 1e-30) - end - - it "loads a Float 1.1867345e+22" do - obj = 1.1867345e+22 - Marshal.send(@method, "\004\bf\0361.1867344999999999e+22\000\344@").should == obj - end - - it "raises ArgumentError when end of byte sequence reached before float string representation end" do - Marshal.dump(1.3).should == "\x04\bf\b1.3" - - -> { - Marshal.send(@method, "\004\bf\v1") - }.should.raise(ArgumentError, "marshal data too short") - end - end - - describe "for an Integer" do - it "loads 0" do - Marshal.send(@method, "\004\bi\000").should == 0 - Marshal.send(@method, "\004\bi\005").should == 0 - end - - it "loads an Integer 8" do - Marshal.send(@method, "\004\bi\r" ).should == 8 - end - - it "loads and Integer -8" do - Marshal.send(@method, "\004\bi\363" ).should == -8 - end - - it "loads an Integer 1234" do - Marshal.send(@method, "\004\bi\002\322\004").should == 1234 - end - - it "loads an Integer -1234" do - Marshal.send(@method, "\004\bi\376.\373").should == -1234 - end - - it "loads an Integer 4611686018427387903" do - Marshal.send(@method, "\004\bl+\t\377\377\377\377\377\377\377?").should == 4611686018427387903 - end - - it "loads an Integer -4611686018427387903" do - Marshal.send(@method, "\004\bl-\t\377\377\377\377\377\377\377?").should == -4611686018427387903 - end - - it "loads an Integer 2361183241434822606847" do - Marshal.send(@method, "\004\bl+\n\377\377\377\377\377\377\377\377\177\000").should == 2361183241434822606847 - end - - it "loads an Integer -2361183241434822606847" do - Marshal.send(@method, "\004\bl-\n\377\377\377\377\377\377\377\377\177\000").should == -2361183241434822606847 - end - - it "raises ArgumentError if the input is too short" do - ["\004\bi", - "\004\bi\001", - "\004\bi\002", - "\004\bi\002\0", - "\004\bi\003", - "\004\bi\003\0", - "\004\bi\003\0\0", - "\004\bi\004", - "\004\bi\004\0", - "\004\bi\004\0\0", - "\004\bi\004\0\0\0"].each do |invalid| - -> { Marshal.send(@method, invalid) }.should.raise(ArgumentError) - end - end - - if 0.size == 8 # for platforms like x86_64 - it "roundtrips 4611686018427387903 from dump/load correctly" do - Marshal.send(@method, Marshal.dump(4611686018427387903)).should == 4611686018427387903 - end - end - end - - describe "for a Rational" do - it "loads" do - r = Marshal.send(@method, Marshal.dump(Rational(1, 3))) - r.should == Rational(1, 3) - r.should.frozen? - end - end - - describe "for a Complex" do - it "loads" do - c = Marshal.send(@method, Marshal.dump(Complex(4, 3))) - c.should == Complex(4, 3) - c.should.frozen? - end - end - - describe "for a Bignum" do - platform_is c_long_size: 64 do - context "that is Bignum on 32-bit platforms but Fixnum on 64-bit" do - it "dumps a Fixnum" do - val = Marshal.send(@method, "\004\bl+\ab:wU") - val.should == 1433877090 - val.class.should == Integer - end - - it "dumps an array containing multiple references to the Bignum as an array of Fixnum" do - arr = Marshal.send(@method, "\004\b[\al+\a\223BwU@\006") - arr.should == [1433879187, 1433879187] - arr.each { |v| v.class.should == Integer } - end - end - end - end - - describe "for a Time" do - it "loads" do - Marshal.send(@method, Marshal.dump(Time.at(1))).should == Time.at(1) - end - - it "loads serialized instance variables" do - t = Time.new - t.instance_variable_set(:@foo, 'bar') - - Marshal.send(@method, Marshal.dump(t)).instance_variable_get(:@foo).should == 'bar' - end - - it "loads Time objects stored as links" do - t = Time.new - - t1, t2 = Marshal.send(@method, Marshal.dump([t, t])) - t1.should.equal? t2 - end - - it "keeps the local zone" do - with_timezone 'AST', 3 do - t = Time.local(2012, 1, 1) - Marshal.send(@method, Marshal.dump(t)).zone.should == t.zone - end - end - - it "keeps UTC zone" do - t = Time.now.utc - t2 = Marshal.send(@method, Marshal.dump(t)) - t2.should.utc? - end - - it "keeps the zone" do - t = nil - - with_timezone 'AST', 4 do - t = Time.local(2012, 1, 1) - end - - with_timezone 'EET', -2 do - Marshal.send(@method, Marshal.dump(t)).zone.should == 'AST' - end - end - - it "keeps utc offset" do - t = Time.new(2007,11,1,15,25,0, "+09:00") - t2 = Marshal.send(@method, Marshal.dump(t)) - t2.utc_offset.should == 32400 - end - - it "keeps nanoseconds" do - t = Time.now - Marshal.send(@method, Marshal.dump(t)).nsec.should == t.nsec - end - - it "does not add any additional instance variable" do - t = Time.now - t2 = Marshal.send(@method, Marshal.dump(t)) - t2.instance_variables.should.empty? - end - end - - describe "for nil" do - it "loads" do - Marshal.send(@method, "\x04\b0").should == nil - end - end - - describe "for true" do - it "loads" do - Marshal.send(@method, "\x04\bT").should == true - end - end - - describe "for false" do - it "loads" do - Marshal.send(@method, "\x04\bF").should == false - end - end - - describe "for a Class" do - it "loads" do - Marshal.send(@method, "\x04\bc\vString").should == String - end - - it "raises ArgumentError if given the name of a non-Module" do - -> { Marshal.send(@method, "\x04\bc\vKernel") }.should.raise(ArgumentError) - end - - it "raises ArgumentError if given a nonexistent class" do - -> { Marshal.send(@method, "\x04\bc\vStrung") }.should.raise(ArgumentError) - end - - it "raises ArgumentError when end of byte sequence reached before class name end" do - Marshal.dump(String).should == "\x04\bc\vString" - - -> { - Marshal.send(@method, "\x04\bc\vStr") - }.should.raise(ArgumentError, "marshal data too short") - end - end - - describe "for a Module" do - it "loads a module" do - Marshal.send(@method, "\x04\bm\vKernel").should == Kernel - end - - it "raises ArgumentError if given the name of a non-Class" do - -> { Marshal.send(@method, "\x04\bm\vString") }.should.raise(ArgumentError) - end - - it "loads an old module" do - Marshal.send(@method, "\x04\bM\vKernel").should == Kernel - end - - it "raises ArgumentError when end of byte sequence reached before module name end" do - Marshal.dump(Kernel).should == "\x04\bm\vKernel" - - -> { - Marshal.send(@method, "\x04\bm\vKer") - }.should.raise(ArgumentError, "marshal data too short") - end - end - - describe "for a wrapped C pointer" do - it "loads" do - class DumpableDir < Dir - def _dump_data - path - end - def _load_data path - initialize(path) - end - end - - data = "\x04\bd:\x10DumpableDirI\"\x06.\x06:\x06ET" - - dir = Marshal.send(@method, data) - begin - dir.path.should == '.' - ensure - dir.close - end - end - - it "raises TypeError when the local class is missing _load_data" do - class UnloadableDumpableDir < Dir - def _dump_data - path - end - # no _load_data - end - - data = "\x04\bd:\x1AUnloadableDumpableDirI\"\x06.\x06:\x06ET" - - -> { Marshal.send(@method, data) }.should.raise(TypeError) - end - - it "raises ArgumentError when the local class is a regular object" do - data = "\004\bd:\020UserDefined\0" - - -> { Marshal.send(@method, data) }.should.raise(ArgumentError) - end - end - - describe "when a class does not exist in the namespace" do - before :each do - NamespaceTest.send(:const_set, :SameName, Class.new) - @data = Marshal.dump(NamespaceTest::SameName.new) - NamespaceTest.send(:remove_const, :SameName) - end - - it "raises an ArgumentError" do - message = "undefined class/module NamespaceTest::SameName" - -> { Marshal.send(@method, @data) }.should.raise(ArgumentError, message) - end - end - - it "raises an ArgumentError with full constant name when the dumped constant is missing" do - NamespaceTest.send(:const_set, :KaBoom, Class.new) - @data = Marshal.dump(NamespaceTest::KaBoom.new) - NamespaceTest.send(:remove_const, :KaBoom) - - -> { Marshal.send(@method, @data) }.should.raise(ArgumentError, /NamespaceTest::KaBoom/) - end -end diff --git a/spec/ruby/core/matchdata/captures_spec.rb b/spec/ruby/core/matchdata/captures_spec.rb index f829a254811de2..fdbc1c434609b8 100644 --- a/spec/ruby/core/matchdata/captures_spec.rb +++ b/spec/ruby/core/matchdata/captures_spec.rb @@ -1,6 +1,13 @@ require_relative '../../spec_helper' -require_relative 'shared/captures' +require_relative 'fixtures/classes' describe "MatchData#captures" do - it_behaves_like :matchdata_captures, :captures + it "returns an array of the match captures" do + /(.)(.)(\d+)(\d)/.match("THX1138.").captures.should == ["H","X","113","8"] + end + + it "returns instances of String when given a String subclass" do + str = MatchDataSpecs::MyString.new("THX1138: The Movie") + /(.)(.)(\d+)(\d)/.match(str).captures.each { |c| c.should.instance_of?(String) } + end end diff --git a/spec/ruby/core/matchdata/deconstruct_spec.rb b/spec/ruby/core/matchdata/deconstruct_spec.rb index c55095665df940..13ebd1848f1b77 100644 --- a/spec/ruby/core/matchdata/deconstruct_spec.rb +++ b/spec/ruby/core/matchdata/deconstruct_spec.rb @@ -1,6 +1,7 @@ require_relative '../../spec_helper' -require_relative 'shared/captures' describe "MatchData#deconstruct" do - it_behaves_like :matchdata_captures, :deconstruct + it "is an alias of MatchData#captures" do + MatchData.instance_method(:deconstruct).should == MatchData.instance_method(:captures) + end end diff --git a/spec/ruby/core/matchdata/eql_spec.rb b/spec/ruby/core/matchdata/eql_spec.rb index 1d9666ebe12265..937c4424aa59a6 100644 --- a/spec/ruby/core/matchdata/eql_spec.rb +++ b/spec/ruby/core/matchdata/eql_spec.rb @@ -1,6 +1,7 @@ require_relative '../../spec_helper' -require_relative 'shared/eql' describe "MatchData#eql?" do - it_behaves_like :matchdata_eql, :eql? + it "is an alias of MatchData#==" do + MatchData.instance_method(:eql?).should == MatchData.instance_method(:==) + end end diff --git a/spec/ruby/core/matchdata/equal_value_spec.rb b/spec/ruby/core/matchdata/equal_value_spec.rb index a58f1277e466c8..74d3a5adcde289 100644 --- a/spec/ruby/core/matchdata/equal_value_spec.rb +++ b/spec/ruby/core/matchdata/equal_value_spec.rb @@ -1,6 +1,26 @@ require_relative '../../spec_helper' -require_relative 'shared/eql' describe "MatchData#==" do - it_behaves_like :matchdata_eql, :== + it "returns true if both operands have equal target strings, patterns, and match positions" do + a = 'haystack'.match(/hay/) + b = 'haystack'.match(/hay/) + a.==(b).should == true + end + + it "returns false if the operands have different target strings" do + a = 'hay'.match(/hay/) + b = 'haystack'.match(/hay/) + a.==(b).should == false + end + + it "returns false if the operands have different patterns" do + a = 'haystack'.match(/h.y/) + b = 'haystack'.match(/hay/) + a.==(b).should == false + end + + it "returns false if the argument is not a MatchData object" do + a = 'haystack'.match(/hay/) + a.==(Object.new).should == false + end end diff --git a/spec/ruby/core/matchdata/length_spec.rb b/spec/ruby/core/matchdata/length_spec.rb index 39df36df4b8a63..72d4d80cec2b5f 100644 --- a/spec/ruby/core/matchdata/length_spec.rb +++ b/spec/ruby/core/matchdata/length_spec.rb @@ -1,6 +1,7 @@ require_relative '../../spec_helper' -require_relative 'shared/length' describe "MatchData#length" do - it_behaves_like :matchdata_length, :length + it "is an alias of MatchData#size" do + MatchData.instance_method(:length).should == MatchData.instance_method(:size) + end end diff --git a/spec/ruby/core/matchdata/shared/captures.rb b/spec/ruby/core/matchdata/shared/captures.rb deleted file mode 100644 index de5870f543ab51..00000000000000 --- a/spec/ruby/core/matchdata/shared/captures.rb +++ /dev/null @@ -1,13 +0,0 @@ -require_relative '../../../spec_helper' -require_relative '../fixtures/classes' - -describe :matchdata_captures, shared: true do - it "returns an array of the match captures" do - /(.)(.)(\d+)(\d)/.match("THX1138.").send(@method).should == ["H","X","113","8"] - end - - it "returns instances of String when given a String subclass" do - str = MatchDataSpecs::MyString.new("THX1138: The Movie") - /(.)(.)(\d+)(\d)/.match(str).send(@method).each { |c| c.should.instance_of?(String) } - end -end diff --git a/spec/ruby/core/matchdata/shared/eql.rb b/spec/ruby/core/matchdata/shared/eql.rb deleted file mode 100644 index e4bb8797b7d637..00000000000000 --- a/spec/ruby/core/matchdata/shared/eql.rb +++ /dev/null @@ -1,26 +0,0 @@ -require_relative '../../../spec_helper' - -describe :matchdata_eql, shared: true do - it "returns true if both operands have equal target strings, patterns, and match positions" do - a = 'haystack'.match(/hay/) - b = 'haystack'.match(/hay/) - a.send(@method, b).should == true - end - - it "returns false if the operands have different target strings" do - a = 'hay'.match(/hay/) - b = 'haystack'.match(/hay/) - a.send(@method, b).should == false - end - - it "returns false if the operands have different patterns" do - a = 'haystack'.match(/h.y/) - b = 'haystack'.match(/hay/) - a.send(@method, b).should == false - end - - it "returns false if the argument is not a MatchData object" do - a = 'haystack'.match(/hay/) - a.send(@method, Object.new).should == false - end -end diff --git a/spec/ruby/core/matchdata/shared/length.rb b/spec/ruby/core/matchdata/shared/length.rb deleted file mode 100644 index 6312a7ed4ccc94..00000000000000 --- a/spec/ruby/core/matchdata/shared/length.rb +++ /dev/null @@ -1,5 +0,0 @@ -describe :matchdata_length, shared: true do - it "length should return the number of elements in the match array" do - /(.)(.)(\d+)(\d)/.match("THX1138.").send(@method).should == 5 - end -end diff --git a/spec/ruby/core/matchdata/size_spec.rb b/spec/ruby/core/matchdata/size_spec.rb index b4965db3b8d6e8..f93ded72cba695 100644 --- a/spec/ruby/core/matchdata/size_spec.rb +++ b/spec/ruby/core/matchdata/size_spec.rb @@ -1,6 +1,7 @@ require_relative '../../spec_helper' -require_relative 'shared/length' describe "MatchData#size" do - it_behaves_like :matchdata_length, :size + it "should return the number of elements in the match array" do + /(.)(.)(\d+)(\d)/.match("THX1138.").size.should == 5 + end end diff --git a/spec/ruby/core/method/call_spec.rb b/spec/ruby/core/method/call_spec.rb index 6d997325fad39e..cb11545e91c081 100644 --- a/spec/ruby/core/method/call_spec.rb +++ b/spec/ruby/core/method/call_spec.rb @@ -1,7 +1,54 @@ require_relative '../../spec_helper' require_relative 'fixtures/classes' -require_relative 'shared/call' describe "Method#call" do - it_behaves_like :method_call, :call + it "invokes the method with the specified arguments, returning the method's return value" do + m = 12.method("+") + m.call(3).should == 15 + m.call(20).should == 32 + + m = MethodSpecs::Methods.new.method(:attr=) + m.call(42).should == 42 + end + + it "raises an ArgumentError when given incorrect number of arguments" do + -> { + MethodSpecs::Methods.new.method(:two_req).call(1, 2, 3) + }.should.raise(ArgumentError) + -> { + MethodSpecs::Methods.new.method(:two_req).call(1) + }.should.raise(ArgumentError) + end + + describe "for a Method generated by respond_to_missing?" do + it "invokes method_missing with the specified arguments and returns the result" do + @m = MethodSpecs::Methods.new + meth = @m.method(:handled_via_method_missing) + meth.call(:argument).should == [:argument] + end + + it "invokes method_missing with the method name and the specified arguments" do + @m = MethodSpecs::Methods.new + meth = @m.method(:handled_via_method_missing) + + @m.should_receive(:method_missing).with(:handled_via_method_missing, :argument) + meth.call(:argument) + end + + it "invokes method_missing dynamically" do + @m = MethodSpecs::Methods.new + meth = @m.method(:handled_via_method_missing) + + def @m.method_missing(*); :changed; end + meth.call(:argument).should == :changed + end + + it "does not call the original method name even if it now exists" do + @m = MethodSpecs::Methods.new + meth = @m.method(:handled_via_method_missing) + + def @m.handled_via_method_missing(*); :not_called; end + meth.call(:argument).should == [:argument] + end + end end diff --git a/spec/ruby/core/method/case_compare_spec.rb b/spec/ruby/core/method/case_compare_spec.rb index a78953e8ad831f..771fea1ce56c8f 100644 --- a/spec/ruby/core/method/case_compare_spec.rb +++ b/spec/ruby/core/method/case_compare_spec.rb @@ -1,7 +1,7 @@ require_relative '../../spec_helper' -require_relative 'fixtures/classes' -require_relative 'shared/call' describe "Method#===" do - it_behaves_like :method_call, :=== + it "is an alias of Method#call" do + Method.instance_method(:===).should == Method.instance_method(:call) + end end diff --git a/spec/ruby/core/method/element_reference_spec.rb b/spec/ruby/core/method/element_reference_spec.rb index aa6c54d1cbf524..65c13cf32b3bf4 100644 --- a/spec/ruby/core/method/element_reference_spec.rb +++ b/spec/ruby/core/method/element_reference_spec.rb @@ -1,7 +1,7 @@ require_relative '../../spec_helper' -require_relative 'fixtures/classes' -require_relative 'shared/call' describe "Method#[]" do - it_behaves_like :method_call, :[] + it "is an alias of Method#call" do + Method.instance_method(:[]).should == Method.instance_method(:call) + end end diff --git a/spec/ruby/core/method/eql_spec.rb b/spec/ruby/core/method/eql_spec.rb index b97c9e4db030a4..81fd086bd5e3e1 100644 --- a/spec/ruby/core/method/eql_spec.rb +++ b/spec/ruby/core/method/eql_spec.rb @@ -1,6 +1,7 @@ require_relative '../../spec_helper' -require_relative 'shared/eql' describe "Method#eql?" do - it_behaves_like :method_equal, :eql? + it "is an alias of Method#==" do + Method.instance_method(:eql?).should == Method.instance_method(:==) + end end diff --git a/spec/ruby/core/method/equal_value_spec.rb b/spec/ruby/core/method/equal_value_spec.rb index 0431d0c5f6c8c7..ca9ef9f10807f0 100644 --- a/spec/ruby/core/method/equal_value_spec.rb +++ b/spec/ruby/core/method/equal_value_spec.rb @@ -1,6 +1,94 @@ require_relative '../../spec_helper' -require_relative 'shared/eql' +require_relative 'fixtures/classes' describe "Method#==" do - it_behaves_like :method_equal, :== + before :each do + @m = MethodSpecs::Methods.new + @m_foo = @m.method(:foo) + @m2 = MethodSpecs::Methods.new + @a = MethodSpecs::A.new + end + + it "returns true if methods are the same" do + m2 = @m.method(:foo) + + (@m_foo == @m_foo).should == true + (@m_foo == m2).should == true + end + + it "returns true on aliased methods" do + m_bar = @m.method(:bar) + + (m_bar == @m_foo).should == true + end + + it "returns true if the two core methods are aliases" do + s = "hello" + a = s.method(:size) + b = s.method(:length) + (a == b).should == true + end + + it "returns false on a method which is neither aliased nor the same method" do + m2 = @m.method(:zero) + + (@m_foo == m2).should == false + end + + it "returns false for a method which is not bound to the same object" do + m2_foo = @m2.method(:foo) + a_baz = @a.method(:baz) + + (@m_foo == m2_foo).should == false + (@m_foo == a_baz).should == false + end + + it "returns false if the two methods are bound to the same object but were defined independently" do + m2 = @m.method(:same_as_foo) + (@m_foo == m2).should == false + end + + it "returns true if a method was defined using the other one" do + MethodSpecs::Methods.send :define_method, :defined_foo, MethodSpecs::Methods.instance_method(:foo) + m2 = @m.method(:defined_foo) + (@m_foo == m2).should == true + end + + it "returns false if comparing a method defined via define_method and def" do + defn = @m.method(:zero) + defined = @m.method(:zero_defined_method) + + (defn == defined).should == false + (defined == defn).should == false + end + + describe 'missing methods' do + it "returns true for the same method missing" do + miss1 = @m.method(:handled_via_method_missing) + miss1bis = @m.method(:handled_via_method_missing) + miss2 = @m.method(:also_handled) + + (miss1 == miss1bis).should == true + (miss1 == miss2).should == false + end + + it 'calls respond_to_missing? with true to include private methods' do + @m.should_receive(:respond_to_missing?).with(:some_missing_method, true).and_return(true) + @m.method(:some_missing_method) + end + end + + it "returns false if the two methods are bound to different objects, have the same names, and identical bodies" do + a = MethodSpecs::Eql.instance_method(:same_body) + b = MethodSpecs::Eql2.instance_method(:same_body) + (a == b).should == false + end + + it "returns false if the argument is not a Method object" do + (String.instance_method(:size) == 7).should == false + end + + it "returns false if the argument is an unbound version of self" do + (method(:load) == method(:load).unbind).should == false + end end diff --git a/spec/ruby/core/method/inspect_spec.rb b/spec/ruby/core/method/inspect_spec.rb index 97ff2d8c11de63..5eb8850ff33332 100644 --- a/spec/ruby/core/method/inspect_spec.rb +++ b/spec/ruby/core/method/inspect_spec.rb @@ -1,8 +1,7 @@ require_relative '../../spec_helper' -require_relative 'shared/to_s' -require_relative 'shared/aliased_inspect' describe "Method#inspect" do - it_behaves_like :method_to_s, :inspect - it_behaves_like :method_to_s_aliased, :inspect, -> meth { meth } + it "is an alias of Method#to_s" do + Method.instance_method(:inspect).should == Method.instance_method(:to_s) + end end diff --git a/spec/ruby/core/method/shared/call.rb b/spec/ruby/core/method/shared/call.rb deleted file mode 100644 index 41ee2b06cb99c4..00000000000000 --- a/spec/ruby/core/method/shared/call.rb +++ /dev/null @@ -1,51 +0,0 @@ -describe :method_call, shared: true do - it "invokes the method with the specified arguments, returning the method's return value" do - m = 12.method("+") - m.send(@method, 3).should == 15 - m.send(@method, 20).should == 32 - - m = MethodSpecs::Methods.new.method(:attr=) - m.send(@method, 42).should == 42 - end - - it "raises an ArgumentError when given incorrect number of arguments" do - -> { - MethodSpecs::Methods.new.method(:two_req).send(@method, 1, 2, 3) - }.should.raise(ArgumentError) - -> { - MethodSpecs::Methods.new.method(:two_req).send(@method, 1) - }.should.raise(ArgumentError) - end - - describe "for a Method generated by respond_to_missing?" do - it "invokes method_missing with the specified arguments and returns the result" do - @m = MethodSpecs::Methods.new - meth = @m.method(:handled_via_method_missing) - meth.send(@method, :argument).should == [:argument] - end - - it "invokes method_missing with the method name and the specified arguments" do - @m = MethodSpecs::Methods.new - meth = @m.method(:handled_via_method_missing) - - @m.should_receive(:method_missing).with(:handled_via_method_missing, :argument) - meth.send(@method, :argument) - end - - it "invokes method_missing dynamically" do - @m = MethodSpecs::Methods.new - meth = @m.method(:handled_via_method_missing) - - def @m.method_missing(*); :changed; end - meth.send(@method, :argument).should == :changed - end - - it "does not call the original method name even if it now exists" do - @m = MethodSpecs::Methods.new - meth = @m.method(:handled_via_method_missing) - - def @m.handled_via_method_missing(*); :not_called; end - meth.send(@method, :argument).should == [:argument] - end - end -end diff --git a/spec/ruby/core/method/shared/eql.rb b/spec/ruby/core/method/shared/eql.rb deleted file mode 100644 index 3c340202b7536b..00000000000000 --- a/spec/ruby/core/method/shared/eql.rb +++ /dev/null @@ -1,94 +0,0 @@ -require_relative '../../../spec_helper' -require_relative '../fixtures/classes' - -describe :method_equal, shared: true do - before :each do - @m = MethodSpecs::Methods.new - @m_foo = @m.method(:foo) - @m2 = MethodSpecs::Methods.new - @a = MethodSpecs::A.new - end - - it "returns true if methods are the same" do - m2 = @m.method(:foo) - - @m_foo.send(@method, @m_foo).should == true - @m_foo.send(@method, m2).should == true - end - - it "returns true on aliased methods" do - m_bar = @m.method(:bar) - - m_bar.send(@method, @m_foo).should == true - end - - it "returns true if the two core methods are aliases" do - s = "hello" - a = s.method(:size) - b = s.method(:length) - a.send(@method, b).should == true - end - - it "returns false on a method which is neither aliased nor the same method" do - m2 = @m.method(:zero) - - @m_foo.send(@method, m2).should == false - end - - it "returns false for a method which is not bound to the same object" do - m2_foo = @m2.method(:foo) - a_baz = @a.method(:baz) - - @m_foo.send(@method, m2_foo).should == false - @m_foo.send(@method, a_baz).should == false - end - - it "returns false if the two methods are bound to the same object but were defined independently" do - m2 = @m.method(:same_as_foo) - @m_foo.send(@method, m2).should == false - end - - it "returns true if a method was defined using the other one" do - MethodSpecs::Methods.send :define_method, :defined_foo, MethodSpecs::Methods.instance_method(:foo) - m2 = @m.method(:defined_foo) - @m_foo.send(@method, m2).should == true - end - - it "returns false if comparing a method defined via define_method and def" do - defn = @m.method(:zero) - defined = @m.method(:zero_defined_method) - - defn.send(@method, defined).should == false - defined.send(@method, defn).should == false - end - - describe 'missing methods' do - it "returns true for the same method missing" do - miss1 = @m.method(:handled_via_method_missing) - miss1bis = @m.method(:handled_via_method_missing) - miss2 = @m.method(:also_handled) - - miss1.send(@method, miss1bis).should == true - miss1.send(@method, miss2).should == false - end - - it 'calls respond_to_missing? with true to include private methods' do - @m.should_receive(:respond_to_missing?).with(:some_missing_method, true).and_return(true) - @m.method(:some_missing_method) - end - end - - it "returns false if the two methods are bound to different objects, have the same names, and identical bodies" do - a = MethodSpecs::Eql.instance_method(:same_body) - b = MethodSpecs::Eql2.instance_method(:same_body) - a.send(@method, b).should == false - end - - it "returns false if the argument is not a Method object" do - String.instance_method(:size).send(@method, 7).should == false - end - - it "returns false if the argument is an unbound version of self" do - method(:load).send(@method, method(:load).unbind).should == false - end -end diff --git a/spec/ruby/core/module/class_eval_spec.rb b/spec/ruby/core/module/class_eval_spec.rb index c6665d5aff7a14..0c190ceaff8f96 100644 --- a/spec/ruby/core/module/class_eval_spec.rb +++ b/spec/ruby/core/module/class_eval_spec.rb @@ -1,7 +1,7 @@ require_relative '../../spec_helper' -require_relative 'fixtures/classes' -require_relative 'shared/class_eval' describe "Module#class_eval" do - it_behaves_like :module_class_eval, :class_eval + it "is an alias of Module#module_eval" do + Module.instance_method(:class_eval).should == Module.instance_method(:module_eval) + end end diff --git a/spec/ruby/core/module/class_exec_spec.rb b/spec/ruby/core/module/class_exec_spec.rb index 4acd0169ad8128..d47a6ba982ae62 100644 --- a/spec/ruby/core/module/class_exec_spec.rb +++ b/spec/ruby/core/module/class_exec_spec.rb @@ -1,7 +1,7 @@ require_relative '../../spec_helper' -require_relative 'fixtures/classes' -require_relative 'shared/class_exec' describe "Module#class_exec" do - it_behaves_like :module_class_exec, :class_exec + it "is an alias of Module#module_exec" do + Module.instance_method(:class_exec).should == Module.instance_method(:module_exec) + end end diff --git a/spec/ruby/core/module/inspect_spec.rb b/spec/ruby/core/module/inspect_spec.rb new file mode 100644 index 00000000000000..68c8494c964e62 --- /dev/null +++ b/spec/ruby/core/module/inspect_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' + +describe "Module#inspect" do + it "is an alias of Module#to_s" do + Module.instance_method(:inspect).should == Module.instance_method(:to_s) + end +end diff --git a/spec/ruby/core/module/module_eval_spec.rb b/spec/ruby/core/module/module_eval_spec.rb index e9e9fda28ddf0d..bcd51ca19d4664 100644 --- a/spec/ruby/core/module/module_eval_spec.rb +++ b/spec/ruby/core/module/module_eval_spec.rb @@ -1,7 +1,175 @@ require_relative '../../spec_helper' require_relative 'fixtures/classes' -require_relative 'shared/class_eval' describe "Module#module_eval" do - it_behaves_like :module_class_eval, :module_eval + # TODO: This should probably be replaced with a "should behave like" that uses + # the many scoping/binding specs from kernel/eval_spec, since most of those + # behaviors are the same for instance_eval. See also module_eval/class_eval. + + it "evaluates a given string in the context of self" do + ModuleSpecs.module_eval("self").should == ModuleSpecs + ModuleSpecs.module_eval("1 + 1").should == 2 + end + + it "does not add defined methods to other classes" do + FalseClass.module_eval do + def foo + 'foo' + end + end + -> {42.foo}.should.raise(NoMethodError) + end + + it "resolves constants in the caller scope" do + ModuleSpecs::ClassEvalTest.get_constant_from_scope.should == ModuleSpecs::Lookup + end + + it "resolves constants in the caller scope ignoring send" do + ModuleSpecs::ClassEvalTest.get_constant_from_scope_with_send(:module_eval).should == ModuleSpecs::Lookup + end + + it "resolves constants in the receiver's scope" do + ModuleSpecs.module_eval("Lookup").should == ModuleSpecs::Lookup + ModuleSpecs.module_eval("Lookup::LOOKIE").should == ModuleSpecs::Lookup::LOOKIE + end + + it "defines constants in the receiver's scope" do + ModuleSpecs.module_eval("module NewEvaluatedModule;end") + ModuleSpecs.const_defined?(:NewEvaluatedModule, false).should == true + end + + it "evaluates a given block in the context of self" do + ModuleSpecs.module_eval { self }.should == ModuleSpecs + ModuleSpecs.module_eval { 1 + 1 }.should == 2 + end + + it "passes the module as the first argument of the block" do + given = nil + ModuleSpecs.module_eval do |block_parameter| + given = block_parameter + end + given.should.equal? ModuleSpecs + end + + it "uses the optional filename and lineno parameters for error messages" do + ModuleSpecs.module_eval("[__FILE__, __LINE__]", "test", 102).should == ["test", 102] + end + + it "uses the caller location as default filename" do + ModuleSpecs.module_eval("[__FILE__, __LINE__]").should == ["(eval at #{__FILE__}:#{__LINE__})", 1] + end + + it "converts a non-string filename to a string using to_str" do + (file = mock(__FILE__)).should_receive(:to_str).and_return(__FILE__) + ModuleSpecs.module_eval("1+1", file) + + (file = mock(__FILE__)).should_receive(:to_str).and_return(__FILE__) + ModuleSpecs.module_eval("1+1", file, 15) + end + + it "raises a TypeError when the given filename can't be converted to string using to_str" do + (file = mock('123')).should_receive(:to_str).and_return(123) + -> { ModuleSpecs.module_eval("1+1", file) }.should raise_consistent_error(TypeError, /can't convert MockObject into String/) + end + + it "converts non string eval-string to string using to_str" do + (o = mock('1 + 1')).should_receive(:to_str).and_return("1 + 1") + ModuleSpecs.module_eval(o).should == 2 + + (o = mock('1 + 1')).should_receive(:to_str).and_return("1 + 1") + ModuleSpecs.module_eval(o, "file.rb").should == 2 + + (o = mock('1 + 1')).should_receive(:to_str).and_return("1 + 1") + ModuleSpecs.module_eval(o, "file.rb", 15).should == 2 + end + + it "raises a TypeError when the given eval-string can't be converted to string using to_str" do + o = mock('x') + -> { ModuleSpecs.module_eval(o) }.should.raise(TypeError, "no implicit conversion of MockObject into String") + + (o = mock('123')).should_receive(:to_str).and_return(123) + -> { ModuleSpecs.module_eval(o) }.should raise_consistent_error(TypeError, /can't convert MockObject into String/) + end + + it "raises an ArgumentError when no arguments and no block are given" do + -> { ModuleSpecs.module_eval }.should.raise(ArgumentError, "wrong number of arguments (given 0, expected 1..3)") + end + + it "raises an ArgumentError when more than 3 arguments are given" do + -> { + ModuleSpecs.module_eval("1 + 1", "some file", 0, "bogus") + }.should.raise(ArgumentError, "wrong number of arguments (given 4, expected 1..3)") + end + + it "raises an ArgumentError when a block and normal arguments are given" do + -> { + ModuleSpecs.module_eval("1 + 1") { 1 + 1 } + }.should.raise(ArgumentError, "wrong number of arguments (given 1, expected 0)") + end + + # This case was found because Rubinius was caching the compiled + # version of the string and not duping the methods within the + # eval, causing the method addition to change the static scope + # of the shared CompiledCode. + it "adds methods respecting the lexical constant scope" do + code = "def self.attribute; C; end" + + a = Class.new do + self::C = "A" + end + + b = Class.new do + self::C = "B" + end + + a.module_eval(code) + b.module_eval(code) + + a.attribute.should == "A" + b.attribute.should == "B" + end + + it "activates refinements from the eval scope" do + refinery = Module.new do + refine ModuleSpecs::NamedClass do + def foo + "bar" + end + end + end + + mid = :module_eval + result = nil + + Class.new do + using refinery + + result = send(mid, "ModuleSpecs::NamedClass.new.foo") + end + + result.should == "bar" + end + + it "activates refinements from the eval scope with block" do + refinery = Module.new do + refine ModuleSpecs::NamedClass do + def foo + "bar" + end + end + end + + mid = :module_eval + result = nil + + Class.new do + using refinery + + result = send(mid) do + ModuleSpecs::NamedClass.new.foo + end + end + + result.should == "bar" + end end diff --git a/spec/ruby/core/module/module_exec_spec.rb b/spec/ruby/core/module/module_exec_spec.rb index 47cdf7ef526778..6b36e8a02f94c2 100644 --- a/spec/ruby/core/module/module_exec_spec.rb +++ b/spec/ruby/core/module/module_exec_spec.rb @@ -1,7 +1,38 @@ require_relative '../../spec_helper' require_relative 'fixtures/classes' -require_relative 'shared/class_exec' describe "Module#module_exec" do - it_behaves_like :module_class_exec, :module_exec + it "does not add defined methods to other classes" do + FalseClass.module_exec do + def foo + 'foo' + end + end + -> {42.foo}.should.raise(NoMethodError) + end + + it "defines method in the receiver's scope" do + ModuleSpecs::Subclass.module_exec { def foo; end } + ModuleSpecs::Subclass.new.respond_to?(:foo).should == true + end + + it "evaluates a given block in the context of self" do + ModuleSpecs::Subclass.module_exec { self }.should == ModuleSpecs::Subclass + ModuleSpecs::Subclass.new.module_exec { 1 + 1 }.should == 2 + end + + it "raises a LocalJumpError when no block is given" do + -> { ModuleSpecs::Subclass.module_exec }.should.raise(LocalJumpError) + end + + it "passes arguments to the block" do + a = ModuleSpecs::Subclass + a.module_exec(1) { |b| b }.should.equal?(1) + end + + describe "with optional argument" do + it "does not destructure a single array argument" do + ModuleSpecs::Subclass.module_exec([1, 2, 3]) { |a = 99| a }.should == [1, 2, 3] + end + end end diff --git a/spec/ruby/core/module/shared/class_eval.rb b/spec/ruby/core/module/shared/class_eval.rb deleted file mode 100644 index ee2860449ae8d2..00000000000000 --- a/spec/ruby/core/module/shared/class_eval.rb +++ /dev/null @@ -1,172 +0,0 @@ -describe :module_class_eval, shared: true do - # TODO: This should probably be replaced with a "should behave like" that uses - # the many scoping/binding specs from kernel/eval_spec, since most of those - # behaviors are the same for instance_eval. See also module_eval/class_eval. - - it "evaluates a given string in the context of self" do - ModuleSpecs.send(@method, "self").should == ModuleSpecs - ModuleSpecs.send(@method, "1 + 1").should == 2 - end - - it "does not add defined methods to other classes" do - FalseClass.send(@method) do - def foo - 'foo' - end - end - -> {42.foo}.should.raise(NoMethodError) - end - - it "resolves constants in the caller scope" do - ModuleSpecs::ClassEvalTest.get_constant_from_scope.should == ModuleSpecs::Lookup - end - - it "resolves constants in the caller scope ignoring send" do - ModuleSpecs::ClassEvalTest.get_constant_from_scope_with_send(@method).should == ModuleSpecs::Lookup - end - - it "resolves constants in the receiver's scope" do - ModuleSpecs.send(@method, "Lookup").should == ModuleSpecs::Lookup - ModuleSpecs.send(@method, "Lookup::LOOKIE").should == ModuleSpecs::Lookup::LOOKIE - end - - it "defines constants in the receiver's scope" do - ModuleSpecs.send(@method, "module NewEvaluatedModule;end") - ModuleSpecs.const_defined?(:NewEvaluatedModule, false).should == true - end - - it "evaluates a given block in the context of self" do - ModuleSpecs.send(@method) { self }.should == ModuleSpecs - ModuleSpecs.send(@method) { 1 + 1 }.should == 2 - end - - it "passes the module as the first argument of the block" do - given = nil - ModuleSpecs.send(@method) do |block_parameter| - given = block_parameter - end - given.should.equal? ModuleSpecs - end - - it "uses the optional filename and lineno parameters for error messages" do - ModuleSpecs.send(@method, "[__FILE__, __LINE__]", "test", 102).should == ["test", 102] - end - - it "uses the caller location as default filename" do - ModuleSpecs.send(@method, "[__FILE__, __LINE__]").should == ["(eval at #{__FILE__}:#{__LINE__})", 1] - end - - it "converts a non-string filename to a string using to_str" do - (file = mock(__FILE__)).should_receive(:to_str).and_return(__FILE__) - ModuleSpecs.send(@method, "1+1", file) - - (file = mock(__FILE__)).should_receive(:to_str).and_return(__FILE__) - ModuleSpecs.send(@method, "1+1", file, 15) - end - - it "raises a TypeError when the given filename can't be converted to string using to_str" do - (file = mock('123')).should_receive(:to_str).and_return(123) - -> { ModuleSpecs.send(@method, "1+1", file) }.should raise_consistent_error(TypeError, /can't convert MockObject into String/) - end - - it "converts non string eval-string to string using to_str" do - (o = mock('1 + 1')).should_receive(:to_str).and_return("1 + 1") - ModuleSpecs.send(@method, o).should == 2 - - (o = mock('1 + 1')).should_receive(:to_str).and_return("1 + 1") - ModuleSpecs.send(@method, o, "file.rb").should == 2 - - (o = mock('1 + 1')).should_receive(:to_str).and_return("1 + 1") - ModuleSpecs.send(@method, o, "file.rb", 15).should == 2 - end - - it "raises a TypeError when the given eval-string can't be converted to string using to_str" do - o = mock('x') - -> { ModuleSpecs.send(@method, o) }.should.raise(TypeError, "no implicit conversion of MockObject into String") - - (o = mock('123')).should_receive(:to_str).and_return(123) - -> { ModuleSpecs.send(@method, o) }.should raise_consistent_error(TypeError, /can't convert MockObject into String/) - end - - it "raises an ArgumentError when no arguments and no block are given" do - -> { ModuleSpecs.send(@method) }.should.raise(ArgumentError, "wrong number of arguments (given 0, expected 1..3)") - end - - it "raises an ArgumentError when more than 3 arguments are given" do - -> { - ModuleSpecs.send(@method, "1 + 1", "some file", 0, "bogus") - }.should.raise(ArgumentError, "wrong number of arguments (given 4, expected 1..3)") - end - - it "raises an ArgumentError when a block and normal arguments are given" do - -> { - ModuleSpecs.send(@method, "1 + 1") { 1 + 1 } - }.should.raise(ArgumentError, "wrong number of arguments (given 1, expected 0)") - end - - # This case was found because Rubinius was caching the compiled - # version of the string and not duping the methods within the - # eval, causing the method addition to change the static scope - # of the shared CompiledCode. - it "adds methods respecting the lexical constant scope" do - code = "def self.attribute; C; end" - - a = Class.new do - self::C = "A" - end - - b = Class.new do - self::C = "B" - end - - a.send @method, code - b.send @method, code - - a.attribute.should == "A" - b.attribute.should == "B" - end - - it "activates refinements from the eval scope" do - refinery = Module.new do - refine ModuleSpecs::NamedClass do - def foo - "bar" - end - end - end - - mid = @method - result = nil - - Class.new do - using refinery - - result = send(mid, "ModuleSpecs::NamedClass.new.foo") - end - - result.should == "bar" - end - - it "activates refinements from the eval scope with block" do - refinery = Module.new do - refine ModuleSpecs::NamedClass do - def foo - "bar" - end - end - end - - mid = @method - result = nil - - Class.new do - using refinery - - result = send(mid) do - ModuleSpecs::NamedClass.new.foo - end - end - - result.should == "bar" - end -end diff --git a/spec/ruby/core/module/shared/class_exec.rb b/spec/ruby/core/module/shared/class_exec.rb deleted file mode 100644 index e51af1966dd328..00000000000000 --- a/spec/ruby/core/module/shared/class_exec.rb +++ /dev/null @@ -1,35 +0,0 @@ -describe :module_class_exec, shared: true do - it "does not add defined methods to other classes" do - FalseClass.send(@method) do - def foo - 'foo' - end - end - -> {42.foo}.should.raise(NoMethodError) - end - - it "defines method in the receiver's scope" do - ModuleSpecs::Subclass.send(@method) { def foo; end } - ModuleSpecs::Subclass.new.respond_to?(:foo).should == true - end - - it "evaluates a given block in the context of self" do - ModuleSpecs::Subclass.send(@method) { self }.should == ModuleSpecs::Subclass - ModuleSpecs::Subclass.new.send(@method) { 1 + 1 }.should == 2 - end - - it "raises a LocalJumpError when no block is given" do - -> { ModuleSpecs::Subclass.send(@method) }.should.raise(LocalJumpError) - end - - it "passes arguments to the block" do - a = ModuleSpecs::Subclass - a.send(@method, 1) { |b| b }.should.equal?(1) - end - - describe "with optional argument" do - it "does not destructure a single array argument" do - ModuleSpecs::Subclass.send(@method, [1, 2, 3]) { |a = 99| a }.should == [1, 2, 3] - end - end -end diff --git a/spec/ruby/core/nil/xor_spec.rb b/spec/ruby/core/nil/xor_spec.rb index b45da9d443803c..31ce33e9716643 100644 --- a/spec/ruby/core/nil/xor_spec.rb +++ b/spec/ruby/core/nil/xor_spec.rb @@ -1,11 +1,7 @@ require_relative '../../spec_helper' describe "NilClass#^" do - it "returns false if other is nil or false, otherwise true" do - (nil ^ nil).should == false - (nil ^ true).should == true - (nil ^ false).should == false - (nil ^ "").should == true - (nil ^ mock('x')).should == true + it "is an alias of NilClass#|" do + nil.method(:^).should == nil.method(:|) end end diff --git a/spec/ruby/core/numeric/abs_spec.rb b/spec/ruby/core/numeric/abs_spec.rb index 8bec50e337846c..4b16e06c978f71 100644 --- a/spec/ruby/core/numeric/abs_spec.rb +++ b/spec/ruby/core/numeric/abs_spec.rb @@ -1,6 +1,19 @@ require_relative '../../spec_helper' -require_relative 'shared/abs' +require_relative 'fixtures/classes' describe "Numeric#abs" do - it_behaves_like :numeric_abs, :abs + before :each do + @obj = NumericSpecs::Subclass.new + end + + it "returns self when self is greater than 0" do + @obj.should_receive(:<).with(0).and_return(false) + @obj.abs.should == @obj + end + + it "returns self\#@- when self is less than 0" do + @obj.should_receive(:<).with(0).and_return(true) + @obj.should_receive(:-@).and_return(:absolute_value) + @obj.abs.should == :absolute_value + end end diff --git a/spec/ruby/core/numeric/angle_spec.rb b/spec/ruby/core/numeric/angle_spec.rb index bb3816577775d6..25d2834a52e06b 100644 --- a/spec/ruby/core/numeric/angle_spec.rb +++ b/spec/ruby/core/numeric/angle_spec.rb @@ -1,6 +1,7 @@ require_relative '../../spec_helper' -require_relative 'shared/arg' describe "Numeric#angle" do - it_behaves_like :numeric_arg, :angle + it "is an alias of Numeric#arg" do + Numeric.instance_method(:angle).should == Numeric.instance_method(:arg) + end end diff --git a/spec/ruby/core/numeric/arg_spec.rb b/spec/ruby/core/numeric/arg_spec.rb index ba3b57c6878648..4fd059d7fca214 100644 --- a/spec/ruby/core/numeric/arg_spec.rb +++ b/spec/ruby/core/numeric/arg_spec.rb @@ -1,6 +1,38 @@ require_relative '../../spec_helper' -require_relative 'shared/arg' describe "Numeric#arg" do - it_behaves_like :numeric_arg, :arg + before :each do + @numbers = [ + 20, + Rational(3, 4), + bignum_value, + infinity_value + ] + end + + it "returns 0 if positive" do + @numbers.each do |number| + number.arg.should == 0 + end + end + + it "returns Pi if negative" do + @numbers.each do |number| + (0-number).arg.should == Math::PI + end + end + + describe "with a Numeric subclass" do + it "returns 0 if self#<(0) returns false" do + numeric = mock_numeric('positive') + numeric.should_receive(:<).with(0).and_return(false) + numeric.arg.should == 0 + end + + it "returns Pi if self#<(0) returns true" do + numeric = mock_numeric('positive') + numeric.should_receive(:<).with(0).and_return(true) + numeric.arg.should == Math::PI + end + end end diff --git a/spec/ruby/core/numeric/conj_spec.rb b/spec/ruby/core/numeric/conj_spec.rb index 7d4777ca605784..f376a0d4b1e048 100644 --- a/spec/ruby/core/numeric/conj_spec.rb +++ b/spec/ruby/core/numeric/conj_spec.rb @@ -1,6 +1,7 @@ require_relative '../../spec_helper' -require_relative 'shared/conj' describe "Numeric#conj" do - it_behaves_like :numeric_conj, :conj + it "is an alias of Numeric#conjugate" do + Numeric.instance_method(:conj).should == Numeric.instance_method(:conjugate) + end end diff --git a/spec/ruby/core/numeric/conjugate_spec.rb b/spec/ruby/core/numeric/conjugate_spec.rb index 99854766e7e7d3..ea4731991db901 100644 --- a/spec/ruby/core/numeric/conjugate_spec.rb +++ b/spec/ruby/core/numeric/conjugate_spec.rb @@ -1,6 +1,20 @@ require_relative '../../spec_helper' -require_relative 'shared/conj' describe "Numeric#conjugate" do - it_behaves_like :numeric_conj, :conjugate + before :each do + @numbers = [ + 20, # Integer + 398.72, # Float + Rational(3, 4), # Rational + bignum_value, + infinity_value, + nan_value + ] + end + + it "returns self" do + @numbers.each do |number| + number.conjugate.should.equal?(number) + end + end end diff --git a/spec/ruby/core/numeric/imag_spec.rb b/spec/ruby/core/numeric/imag_spec.rb index b9e343cee9c1e1..761d6b0dbed324 100644 --- a/spec/ruby/core/numeric/imag_spec.rb +++ b/spec/ruby/core/numeric/imag_spec.rb @@ -1,6 +1,7 @@ require_relative '../../spec_helper' -require_relative 'shared/imag' describe "Numeric#imag" do - it_behaves_like :numeric_imag, :imag + it "is an alias of Numeric#imaginary" do + Numeric.instance_method(:imag).should == Numeric.instance_method(:imaginary) + end end diff --git a/spec/ruby/core/numeric/imaginary_spec.rb b/spec/ruby/core/numeric/imaginary_spec.rb index ec708cb505b0f8..7b5d94cc754359 100644 --- a/spec/ruby/core/numeric/imaginary_spec.rb +++ b/spec/ruby/core/numeric/imaginary_spec.rb @@ -1,6 +1,26 @@ require_relative '../../spec_helper' -require_relative 'shared/imag' describe "Numeric#imaginary" do - it_behaves_like :numeric_imag, :imaginary + before :each do + @numbers = [ + 20, # Integer + 398.72, # Float + Rational(3, 4), # Rational + bignum_value, # Bignum + infinity_value, + nan_value + ].map{|n| [n,-n]}.flatten + end + + it "returns 0" do + @numbers.each do |number| + number.imaginary.should == 0 + end + end + + it "raises an ArgumentError if given any arguments" do + @numbers.each do |number| + -> { number.imaginary(number) }.should.raise(ArgumentError) + end + end end diff --git a/spec/ruby/core/numeric/magnitude_spec.rb b/spec/ruby/core/numeric/magnitude_spec.rb index 1371dff21f400c..ea4dbd166f293d 100644 --- a/spec/ruby/core/numeric/magnitude_spec.rb +++ b/spec/ruby/core/numeric/magnitude_spec.rb @@ -1,6 +1,7 @@ require_relative "../../spec_helper" -require_relative 'shared/abs' describe "Numeric#magnitude" do - it_behaves_like :numeric_abs, :magnitude + it "is an alias of Numeric#abs" do + Numeric.instance_method(:magnitude).should == Numeric.instance_method(:abs) + end end diff --git a/spec/ruby/core/numeric/modulo_spec.rb b/spec/ruby/core/numeric/modulo_spec.rb index e3dc7e56f3d702..0baf96dc18cc95 100644 --- a/spec/ruby/core/numeric/modulo_spec.rb +++ b/spec/ruby/core/numeric/modulo_spec.rb @@ -1,7 +1,12 @@ require_relative '../../spec_helper' -require_relative 'fixtures/classes' -describe :numeric_modulo_19, shared: true do +describe "Numeric#modulo" do + it "is an alias of Numeric#%" do + Numeric.instance_method(:modulo).should == Numeric.instance_method(:%) + end +end + +describe "Numeric#%" do it "returns self - other * self.div(other)" do s = mock_numeric('self') o = mock_numeric('other') @@ -11,14 +16,6 @@ s.should_receive(:div).with(o).and_return(n3) o.should_receive(:*).with(n3).and_return(n4) s.should_receive(:-).with(n4).and_return(n5) - s.send(@method, o).should == n5 + (s % o).should == n5 end end - -describe "Numeric#modulo" do - it_behaves_like :numeric_modulo_19, :modulo -end - -describe "Numeric#%" do - it_behaves_like :numeric_modulo_19, :% -end diff --git a/spec/ruby/core/numeric/phase_spec.rb b/spec/ruby/core/numeric/phase_spec.rb index bc1995303f5b93..3abe8f2e02e4fe 100644 --- a/spec/ruby/core/numeric/phase_spec.rb +++ b/spec/ruby/core/numeric/phase_spec.rb @@ -1,6 +1,7 @@ require_relative '../../spec_helper' -require_relative 'shared/arg' describe "Numeric#phase" do - it_behaves_like :numeric_arg, :phase + it "is an alias of Numeric#arg" do + Numeric.instance_method(:phase).should == Numeric.instance_method(:arg) + end end diff --git a/spec/ruby/core/numeric/rect_spec.rb b/spec/ruby/core/numeric/rect_spec.rb index 79a144c5a4aab1..65cdcc52296be4 100644 --- a/spec/ruby/core/numeric/rect_spec.rb +++ b/spec/ruby/core/numeric/rect_spec.rb @@ -1,6 +1,7 @@ require_relative '../../spec_helper' -require_relative 'shared/rect' describe "Numeric#rect" do - it_behaves_like :numeric_rect, :rect + it "is an alias of Numeric#rectangular" do + Numeric.instance_method(:rect).should == Numeric.instance_method(:rectangular) + end end diff --git a/spec/ruby/core/numeric/rectangular_spec.rb b/spec/ruby/core/numeric/rectangular_spec.rb index 2c68985a163a9b..81afccc12dca1a 100644 --- a/spec/ruby/core/numeric/rectangular_spec.rb +++ b/spec/ruby/core/numeric/rectangular_spec.rb @@ -1,6 +1,48 @@ require_relative '../../spec_helper' -require_relative 'shared/rect' describe "Numeric#rectangular" do - it_behaves_like :numeric_rect, :rectangular + before :each do + @numbers = [ + 20, # Integer + 398.72, # Float + Rational(3, 4), # Rational + 99999999**99, # Bignum + infinity_value, + nan_value + ] + end + + it "returns an Array" do + @numbers.each do |number| + number.rectangular.should.instance_of?(Array) + end + end + + it "returns a two-element Array" do + @numbers.each do |number| + number.rectangular.size.should == 2 + end + end + + it "returns self as the first element" do + @numbers.each do |number| + if Float === number and number.nan? + number.rectangular.first.nan?.should == true + else + number.rectangular.first.should == number + end + end + end + + it "returns 0 as the last element" do + @numbers.each do |number| + number.rectangular.last.should == 0 + end + end + + it "raises an ArgumentError if given any arguments" do + @numbers.each do |number| + -> { number.rectangular(number) }.should.raise(ArgumentError) + end + end end diff --git a/spec/ruby/core/numeric/shared/abs.rb b/spec/ruby/core/numeric/shared/abs.rb deleted file mode 100644 index c3dadccfd6c0e9..00000000000000 --- a/spec/ruby/core/numeric/shared/abs.rb +++ /dev/null @@ -1,19 +0,0 @@ -require_relative '../../../spec_helper' -require_relative '../fixtures/classes' - -describe :numeric_abs, shared: true do - before :each do - @obj = NumericSpecs::Subclass.new - end - - it "returns self when self is greater than 0" do - @obj.should_receive(:<).with(0).and_return(false) - @obj.send(@method).should == @obj - end - - it "returns self\#@- when self is less than 0" do - @obj.should_receive(:<).with(0).and_return(true) - @obj.should_receive(:-@).and_return(:absolute_value) - @obj.send(@method).should == :absolute_value - end -end diff --git a/spec/ruby/core/numeric/shared/arg.rb b/spec/ruby/core/numeric/shared/arg.rb deleted file mode 100644 index c8e7ad8333ac4f..00000000000000 --- a/spec/ruby/core/numeric/shared/arg.rb +++ /dev/null @@ -1,38 +0,0 @@ -require_relative '../../../spec_helper' - -describe :numeric_arg, shared: true do - before :each do - @numbers = [ - 20, - Rational(3, 4), - bignum_value, - infinity_value - ] - end - - it "returns 0 if positive" do - @numbers.each do |number| - number.send(@method).should == 0 - end - end - - it "returns Pi if negative" do - @numbers.each do |number| - (0-number).send(@method).should == Math::PI - end - end - - describe "with a Numeric subclass" do - it "returns 0 if self#<(0) returns false" do - numeric = mock_numeric('positive') - numeric.should_receive(:<).with(0).and_return(false) - numeric.send(@method).should == 0 - end - - it "returns Pi if self#<(0) returns true" do - numeric = mock_numeric('positive') - numeric.should_receive(:<).with(0).and_return(true) - numeric.send(@method).should == Math::PI - end - end -end diff --git a/spec/ruby/core/numeric/shared/conj.rb b/spec/ruby/core/numeric/shared/conj.rb deleted file mode 100644 index 1a661fbbe86a31..00000000000000 --- a/spec/ruby/core/numeric/shared/conj.rb +++ /dev/null @@ -1,20 +0,0 @@ -require_relative '../../../spec_helper' - -describe :numeric_conj, shared: true do - before :each do - @numbers = [ - 20, # Integer - 398.72, # Float - Rational(3, 4), # Rational - bignum_value, - infinity_value, - nan_value - ] - end - - it "returns self" do - @numbers.each do |number| - number.send(@method).should.equal?(number) - end - end -end diff --git a/spec/ruby/core/numeric/shared/imag.rb b/spec/ruby/core/numeric/shared/imag.rb deleted file mode 100644 index 605a23d8c64f60..00000000000000 --- a/spec/ruby/core/numeric/shared/imag.rb +++ /dev/null @@ -1,26 +0,0 @@ -require_relative '../../../spec_helper' - -describe :numeric_imag, shared: true do - before :each do - @numbers = [ - 20, # Integer - 398.72, # Float - Rational(3, 4), # Rational - bignum_value, # Bignum - infinity_value, - nan_value - ].map{|n| [n,-n]}.flatten - end - - it "returns 0" do - @numbers.each do |number| - number.send(@method).should == 0 - end - end - - it "raises an ArgumentError if given any arguments" do - @numbers.each do |number| - -> { number.send(@method, number) }.should.raise(ArgumentError) - end - end -end diff --git a/spec/ruby/core/numeric/shared/rect.rb b/spec/ruby/core/numeric/shared/rect.rb deleted file mode 100644 index 32d03e45d2e026..00000000000000 --- a/spec/ruby/core/numeric/shared/rect.rb +++ /dev/null @@ -1,48 +0,0 @@ -require_relative '../../../spec_helper' - -describe :numeric_rect, shared: true do - before :each do - @numbers = [ - 20, # Integer - 398.72, # Float - Rational(3, 4), # Rational - 99999999**99, # Bignum - infinity_value, - nan_value - ] - end - - it "returns an Array" do - @numbers.each do |number| - number.send(@method).should.instance_of?(Array) - end - end - - it "returns a two-element Array" do - @numbers.each do |number| - number.send(@method).size.should == 2 - end - end - - it "returns self as the first element" do - @numbers.each do |number| - if Float === number and number.nan? - number.send(@method).first.nan?.should == true - else - number.send(@method).first.should == number - end - end - end - - it "returns 0 as the last element" do - @numbers.each do |number| - number.send(@method).last.should == 0 - end - end - - it "raises an ArgumentError if given any arguments" do - @numbers.each do |number| - -> { number.send(@method, number) }.should.raise(ArgumentError) - end - end -end diff --git a/spec/ruby/core/objectspace/weakmap/each_pair_spec.rb b/spec/ruby/core/objectspace/weakmap/each_pair_spec.rb index ea29edbd2fee92..272669ad0ad06e 100644 --- a/spec/ruby/core/objectspace/weakmap/each_pair_spec.rb +++ b/spec/ruby/core/objectspace/weakmap/each_pair_spec.rb @@ -1,11 +1,8 @@ require_relative '../../../spec_helper' -require_relative 'shared/members' -require_relative 'shared/each' describe "ObjectSpace::WeakMap#each_pair" do - it_behaves_like :weakmap_members, -> map { a = []; map.each_pair{ |k,v| a << "#{k}#{v}" }; a }, %w[Ax By] -end - -describe "ObjectSpace::WeakMap#each_key" do - it_behaves_like :weakmap_each, :each_pair + it "is an alias of ObjectSpace::WeakMap#each" do + ObjectSpace::WeakMap.instance_method(:each_pair).should == + ObjectSpace::WeakMap.instance_method(:each) + end end diff --git a/spec/ruby/core/objectspace/weakmap/each_spec.rb b/spec/ruby/core/objectspace/weakmap/each_spec.rb index 46fcb66a6f5b42..8493a361583fa8 100644 --- a/spec/ruby/core/objectspace/weakmap/each_spec.rb +++ b/spec/ruby/core/objectspace/weakmap/each_spec.rb @@ -6,6 +6,6 @@ it_behaves_like :weakmap_members, -> map { a = []; map.each{ |k,v| a << "#{k}#{v}" }; a }, %w[Ax By] end -describe "ObjectSpace::WeakMap#each_key" do +describe "ObjectSpace::WeakMap#each" do it_behaves_like :weakmap_each, :each end diff --git a/spec/ruby/core/objectspace/weakmap/each_value_spec.rb b/spec/ruby/core/objectspace/weakmap/each_value_spec.rb index 65a1a7f6fe6283..89f2f6ae98ab83 100644 --- a/spec/ruby/core/objectspace/weakmap/each_value_spec.rb +++ b/spec/ruby/core/objectspace/weakmap/each_value_spec.rb @@ -6,6 +6,6 @@ it_behaves_like :weakmap_members, -> map { a = []; map.each_value{ |k| a << k }; a }, %w[x y] end -describe "ObjectSpace::WeakMap#each_key" do +describe "ObjectSpace::WeakMap#each_value" do it_behaves_like :weakmap_each, :each_value end diff --git a/spec/ruby/core/objectspace/weakmap/include_spec.rb b/spec/ruby/core/objectspace/weakmap/include_spec.rb index 54ca6b303080ad..1affaef907424b 100644 --- a/spec/ruby/core/objectspace/weakmap/include_spec.rb +++ b/spec/ruby/core/objectspace/weakmap/include_spec.rb @@ -1,6 +1,32 @@ require_relative '../../../spec_helper' -require_relative 'shared/include' describe "ObjectSpace::WeakMap#include?" do - it_behaves_like :weakmap_include?, :include? + it "recognizes keys in use" do + map = ObjectSpace::WeakMap.new + key1, key2 = %w[a b].map(&:upcase) + ref1, ref2 = %w[x y] + + map[key1] = ref1 + map.include?(key1).should == true + map[key1] = ref1 + map.include?(key1).should == true + map[key2] = ref2 + map.include?(key2).should == true + end + + it "matches using identity semantics" do + map = ObjectSpace::WeakMap.new + key1, key2 = %w[a a].map(&:upcase) + ref = "x" + map[key1] = ref + map.include?(key2).should == false + end + + it "reports true if the pair exists and the value is nil" do + map = ObjectSpace::WeakMap.new + key = Object.new + map[key] = nil + map.size.should == 1 + map.include?(key).should == true + end end diff --git a/spec/ruby/core/objectspace/weakmap/key_spec.rb b/spec/ruby/core/objectspace/weakmap/key_spec.rb index 999685ff951cab..5d38f1fa5fb7df 100644 --- a/spec/ruby/core/objectspace/weakmap/key_spec.rb +++ b/spec/ruby/core/objectspace/weakmap/key_spec.rb @@ -1,6 +1,8 @@ require_relative '../../../spec_helper' -require_relative 'shared/include' describe "ObjectSpace::WeakMap#key?" do - it_behaves_like :weakmap_include?, :key? + it "is an alias of ObjectSpace::WeakMap#include?" do + ObjectSpace::WeakMap.instance_method(:key?).should == + ObjectSpace::WeakMap.instance_method(:include?) + end end diff --git a/spec/ruby/core/objectspace/weakmap/length_spec.rb b/spec/ruby/core/objectspace/weakmap/length_spec.rb index 3a935648b14306..8ad47aa9d6e29d 100644 --- a/spec/ruby/core/objectspace/weakmap/length_spec.rb +++ b/spec/ruby/core/objectspace/weakmap/length_spec.rb @@ -1,6 +1,8 @@ require_relative '../../../spec_helper' -require_relative 'shared/size' describe "ObjectSpace::WeakMap#length" do - it_behaves_like :weakmap_size, :length + it "is an alias of ObjectSpace::WeakMap#size" do + ObjectSpace::WeakMap.instance_method(:length).should == + ObjectSpace::WeakMap.instance_method(:size) + end end diff --git a/spec/ruby/core/objectspace/weakmap/member_spec.rb b/spec/ruby/core/objectspace/weakmap/member_spec.rb index cefb190ce7cfd7..eaf9a7628536ab 100644 --- a/spec/ruby/core/objectspace/weakmap/member_spec.rb +++ b/spec/ruby/core/objectspace/weakmap/member_spec.rb @@ -1,6 +1,8 @@ require_relative '../../../spec_helper' -require_relative 'shared/include' describe "ObjectSpace::WeakMap#member?" do - it_behaves_like :weakmap_include?, :member? + it "is an alias of ObjectSpace::WeakMap#include?" do + ObjectSpace::WeakMap.instance_method(:member?).should == + ObjectSpace::WeakMap.instance_method(:include?) + end end diff --git a/spec/ruby/core/objectspace/weakmap/shared/include.rb b/spec/ruby/core/objectspace/weakmap/shared/include.rb deleted file mode 100644 index 1770eeac8b12cb..00000000000000 --- a/spec/ruby/core/objectspace/weakmap/shared/include.rb +++ /dev/null @@ -1,30 +0,0 @@ -describe :weakmap_include?, shared: true do - it "recognizes keys in use" do - map = ObjectSpace::WeakMap.new - key1, key2 = %w[a b].map(&:upcase) - ref1, ref2 = %w[x y] - - map[key1] = ref1 - map.send(@method, key1).should == true - map[key1] = ref1 - map.send(@method, key1).should == true - map[key2] = ref2 - map.send(@method, key2).should == true - end - - it "matches using identity semantics" do - map = ObjectSpace::WeakMap.new - key1, key2 = %w[a a].map(&:upcase) - ref = "x" - map[key1] = ref - map.send(@method, key2).should == false - end - - it "reports true if the pair exists and the value is nil" do - map = ObjectSpace::WeakMap.new - key = Object.new - map[key] = nil - map.size.should == 1 - map.send(@method, key).should == true - end -end diff --git a/spec/ruby/core/objectspace/weakmap/shared/size.rb b/spec/ruby/core/objectspace/weakmap/shared/size.rb deleted file mode 100644 index 1064f99d1b987b..00000000000000 --- a/spec/ruby/core/objectspace/weakmap/shared/size.rb +++ /dev/null @@ -1,14 +0,0 @@ -describe :weakmap_size, shared: true do - it "is correct" do - map = ObjectSpace::WeakMap.new - key1, key2 = %w[a b].map(&:upcase) - ref1, ref2 = %w[x y] - map.send(@method).should == 0 - map[key1] = ref1 - map.send(@method).should == 1 - map[key1] = ref1 - map.send(@method).should == 1 - map[key2] = ref2 - map.send(@method).should == 2 - end -end diff --git a/spec/ruby/core/objectspace/weakmap/size_spec.rb b/spec/ruby/core/objectspace/weakmap/size_spec.rb index 1446abaa24d661..d301750c624138 100644 --- a/spec/ruby/core/objectspace/weakmap/size_spec.rb +++ b/spec/ruby/core/objectspace/weakmap/size_spec.rb @@ -1,6 +1,16 @@ require_relative '../../../spec_helper' -require_relative 'shared/size' describe "ObjectSpace::WeakMap#size" do - it_behaves_like :weakmap_size, :size + it "is correct" do + map = ObjectSpace::WeakMap.new + key1, key2 = %w[a b].map(&:upcase) + ref1, ref2 = %w[x y] + map.size.should == 0 + map[key1] = ref1 + map.size.should == 1 + map[key1] = ref1 + map.size.should == 1 + map[key2] = ref2 + map.size.should == 2 + end end diff --git a/spec/ruby/core/proc/call_spec.rb b/spec/ruby/core/proc/call_spec.rb index 6ec2fc86821b4c..8b65be97c9bfdc 100644 --- a/spec/ruby/core/proc/call_spec.rb +++ b/spec/ruby/core/proc/call_spec.rb @@ -1,16 +1,138 @@ require_relative '../../spec_helper' -require_relative 'shared/call' -require_relative 'shared/call_arguments' +require_relative 'fixtures/common' +require_relative 'fixtures/proc_call' +require_relative 'fixtures/proc_call_frozen' describe "Proc#call" do - it_behaves_like :proc_call, :call - it_behaves_like :proc_call_block_args, :call -end + it "invokes self" do + Proc.new { "test!" }.call.should == "test!" + -> { "test!" }.call.should == "test!" + proc { "test!" }.call.should == "test!" + end -describe "Proc#call on a Proc created with Proc.new" do - it_behaves_like :proc_call_on_proc_new, :call -end + it "sets self's parameters to the given values" do + Proc.new { |a, b| a + b }.call(1, 2).should == 3 + Proc.new { |*args| args }.call(1, 2, 3, 4).should == [1, 2, 3, 4] + Proc.new { |_, *args| args }.call(1, 2, 3).should == [2, 3] + + -> a, b { a + b }.call(1, 2).should == 3 + -> *args { args }.call(1, 2, 3, 4).should == [1, 2, 3, 4] + -> _, *args { args }.call(1, 2, 3).should == [2, 3] + + proc { |a, b| a + b }.call(1, 2).should == 3 + proc { |*args| args }.call(1, 2, 3, 4).should == [1, 2, 3, 4] + proc { |_, *args| args }.call(1, 2, 3).should == [2, 3] + end + + it "can receive block arguments" do + Proc.new {|&b| b.call}.call {1 + 1}.should == 2 + -> &b { b.call}.call {1 + 1}.should == 2 + proc {|&b| b.call}.call {1 + 1}.should == 2 + end + + it "yields to the block given at declaration and not to the block argument" do + proc_creator = Object.new + def proc_creator.create + Proc.new do |&b| + yield + end + end + a_proc = proc_creator.create { 7 } + a_proc.call { 3 }.should == 7 + end + + it "can call its block argument declared with a block argument" do + proc_creator = Object.new + def proc_creator.create(method_name) + Proc.new do |&b| + yield + b.send(method_name) + end + end + a_proc = proc_creator.create(:call) { 7 } + a_proc.call { 3 }.should == 10 + end + + describe "on a Proc created with frozen_string_literal: true/false" do + it "doesn't duplicate frozen strings" do + ProcCallSpecs.call.frozen?.should == false + ProcCallSpecs.call_freeze.frozen?.should == true + ProcCallFrozenSpecs.call.frozen?.should == true + ProcCallFrozenSpecs.call_freeze.frozen?.should == true + end + end + + context "on a Proc created with Proc.new" do + it "replaces missing arguments with nil" do + Proc.new { |a, b| [a, b] }.call.should == [nil, nil] + Proc.new { |a, b| [a, b] }.call(1).should == [1, nil] + end + + it "silently ignores extra arguments" do + Proc.new { |a, b| a + b }.call(1, 2, 5).should == 3 + end + + it "auto-explodes a single Array argument" do + p = Proc.new { |a, b| [a, b] } + p.call(1, 2).should == [1, 2] + p.call([1, 2]).should == [1, 2] + p.call([1, 2, 3]).should == [1, 2] + p.call([1, 2, 3], 4).should == [[1, 2, 3], 4] + end + end + + context "on a Proc created with Kernel#lambda or Kernel#proc" do + it "ignores excess arguments when self is a proc" do + a = proc {|x| x}.call(1, 2) + a.should == 1 + + a = proc {|x| x}.call(1, 2, 3) + a.should == 1 + + a = proc {|x:| x}.call(2, x: 1) + a.should == 1 + end + + it "will call #to_ary on argument and return self if return is nil" do + argument = ProcSpecs::ToAryAsNil.new + result = proc { |x, _| x }.call(argument) + result.should == argument + end + + it "substitutes nil for missing arguments when self is a proc" do + proc {|x,y| [x,y]}.call.should == [nil,nil] + + a = proc {|x,y| [x, y]}.call(1) + a.should == [1,nil] + end + + it "raises an ArgumentError on excess arguments when self is a lambda" do + -> { + -> x { x }.call(1, 2) + }.should.raise(ArgumentError) + + -> { + -> x { x }.call(1, 2, 3) + }.should.raise(ArgumentError) + end + + it "raises an ArgumentError on missing arguments when self is a lambda" do + -> { + -> x { x }.call + }.should.raise(ArgumentError) + + -> { + -> x, y { [x,y] }.call(1) + }.should.raise(ArgumentError) + end + + it "treats a single Array argument as a single argument when self is a lambda" do + -> a { a }.call([1, 2]).should == [1, 2] + -> a, b { [a, b] }.call([1, 2], 3).should == [[1,2], 3] + end -describe "Proc#call on a Proc created with Kernel#lambda or Kernel#proc" do - it_behaves_like :proc_call_on_proc_or_lambda, :call + it "treats a single Array argument as a single argument when self is a proc" do + proc { |a| a }.call([1, 2]).should == [1, 2] + proc { |a, b| [a, b] }.call([1, 2], 3).should == [[1,2], 3] + end + end end diff --git a/spec/ruby/core/proc/case_compare_spec.rb b/spec/ruby/core/proc/case_compare_spec.rb index f11513cdb94e91..421afb24f008d7 100644 --- a/spec/ruby/core/proc/case_compare_spec.rb +++ b/spec/ruby/core/proc/case_compare_spec.rb @@ -1,16 +1,7 @@ require_relative '../../spec_helper' -require_relative 'shared/call' -require_relative 'shared/call_arguments' describe "Proc#===" do - it_behaves_like :proc_call, :=== - it_behaves_like :proc_call_block_args, :=== -end - -describe "Proc#=== on a Proc created with Proc.new" do - it_behaves_like :proc_call_on_proc_new, :=== -end - -describe "Proc#=== on a Proc created with Kernel#lambda or Kernel#proc" do - it_behaves_like :proc_call_on_proc_or_lambda, :=== + it "is an alias of Proc#call" do + Proc.instance_method(:===).should == Proc.instance_method(:call) + end end diff --git a/spec/ruby/core/proc/element_reference_spec.rb b/spec/ruby/core/proc/element_reference_spec.rb index ea3a915a11257e..ac142924644558 100644 --- a/spec/ruby/core/proc/element_reference_spec.rb +++ b/spec/ruby/core/proc/element_reference_spec.rb @@ -1,27 +1,7 @@ require_relative '../../spec_helper' -require_relative 'shared/call' -require_relative 'shared/call_arguments' -require_relative 'fixtures/proc_aref' -require_relative 'fixtures/proc_aref_frozen' describe "Proc#[]" do - it_behaves_like :proc_call, :[] - it_behaves_like :proc_call_block_args, :[] -end - -describe "Proc#call on a Proc created with Proc.new" do - it_behaves_like :proc_call_on_proc_new, :call -end - -describe "Proc#call on a Proc created with Kernel#lambda or Kernel#proc" do - it_behaves_like :proc_call_on_proc_or_lambda, :call -end - -describe "Proc#[] with frozen_string_literal: true/false" do - it "doesn't duplicate frozen strings" do - ProcArefSpecs.aref.frozen?.should == false - ProcArefSpecs.aref_freeze.frozen?.should == true - ProcArefFrozenSpecs.aref.frozen?.should == true - ProcArefFrozenSpecs.aref_freeze.frozen?.should == true + it "is an alias of Proc#call" do + Proc.instance_method(:[]).should == Proc.instance_method(:call) end end diff --git a/spec/ruby/core/proc/eql_spec.rb b/spec/ruby/core/proc/eql_spec.rb index ad8f6749fcb711..1a5fb42a0e3fe1 100644 --- a/spec/ruby/core/proc/eql_spec.rb +++ b/spec/ruby/core/proc/eql_spec.rb @@ -1,6 +1,7 @@ require_relative '../../spec_helper' -require_relative 'shared/equal' describe "Proc#eql?" do - it_behaves_like :proc_equal, :eql? + it "is an alias of Proc#==" do + Proc.instance_method(:eql?).should == Proc.instance_method(:==) + end end diff --git a/spec/ruby/core/proc/equal_value_spec.rb b/spec/ruby/core/proc/equal_value_spec.rb index ec7f27473237e7..92e462152e6af2 100644 --- a/spec/ruby/core/proc/equal_value_spec.rb +++ b/spec/ruby/core/proc/equal_value_spec.rb @@ -1,6 +1,83 @@ require_relative '../../spec_helper' -require_relative 'shared/equal' +require_relative 'fixtures/common' describe "Proc#==" do - it_behaves_like :proc_equal, :== + it "is a public method" do + Proc.public_instance_methods(false).should.include?(:==) + end + + it "returns true if self and other are the same object" do + p = proc { :foo } + (p == p).should == true + + p = Proc.new { :foo } + (p == p).should == true + + p = -> { :foo } + (p == p).should == true + end + + it "returns true if other is a dup of the original" do + p = proc { :foo } + (p == p.dup).should == true + + p = Proc.new { :foo } + (p == p.dup).should == true + + p = -> { :foo } + (p == p.dup).should == true + end + + # identical here means the same method invocation. + it "returns false when bodies are the same but capture env is not identical" do + a = ProcSpecs.proc_for_1 + b = ProcSpecs.proc_for_1 + + (a == b).should == false + end + + it "returns false if procs are distinct but have the same body and environment" do + p = proc { :foo } + p2 = proc { :foo } + (p == p2).should == false + end + + it "returns false if lambdas are distinct but have same body and environment" do + x = -> { :foo } + x2 = -> { :foo } + (x == x2).should == false + end + + it "returns false if using comparing lambda to proc, even with the same body and env" do + p = -> { :foo } + p2 = proc { :foo } + (p == p2).should == false + + x = proc { :bar } + x2 = -> { :bar } + (x == x2).should == false + end + + it "returns false if other is not a Proc" do + p = proc { :foo } + (p == []).should == false + + p = Proc.new { :foo } + (p == Object.new).should == false + + p = -> { :foo } + (p == :foo).should == false + end + + it "returns false if self and other are both procs but have different bodies" do + p = proc { :bar } + p2 = proc { :foo } + (p == p2).should == false + end + + it "returns false if self and other are both lambdas but have different bodies" do + p = -> { :foo } + p2 = -> { :bar } + (p == p2).should == false + end end diff --git a/spec/ruby/core/proc/fixtures/proc_aref.rb b/spec/ruby/core/proc/fixtures/proc_aref.rb deleted file mode 100644 index 8ee355b14ca6df..00000000000000 --- a/spec/ruby/core/proc/fixtures/proc_aref.rb +++ /dev/null @@ -1,10 +0,0 @@ -# frozen_string_literal: false -module ProcArefSpecs - def self.aref - proc {|a| a }["sometext"] - end - - def self.aref_freeze - proc {|a| a }["sometext".freeze] - end -end diff --git a/spec/ruby/core/proc/fixtures/proc_aref_frozen.rb b/spec/ruby/core/proc/fixtures/proc_aref_frozen.rb deleted file mode 100644 index 50a330ba4f827b..00000000000000 --- a/spec/ruby/core/proc/fixtures/proc_aref_frozen.rb +++ /dev/null @@ -1,10 +0,0 @@ -# frozen_string_literal: true -module ProcArefFrozenSpecs - def self.aref - proc {|a| a }["sometext"] - end - - def self.aref_freeze - proc {|a| a }["sometext".freeze] - end -end diff --git a/spec/ruby/core/proc/fixtures/proc_call.rb b/spec/ruby/core/proc/fixtures/proc_call.rb new file mode 100644 index 00000000000000..32048f531930c9 --- /dev/null +++ b/spec/ruby/core/proc/fixtures/proc_call.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: false +module ProcCallSpecs + def self.call + proc {|a| a }.call("sometext") + end + + def self.call_freeze + proc {|a| a }.call("sometext".freeze) + end +end diff --git a/spec/ruby/core/proc/fixtures/proc_call_frozen.rb b/spec/ruby/core/proc/fixtures/proc_call_frozen.rb new file mode 100644 index 00000000000000..29ffc3c4c95a9c --- /dev/null +++ b/spec/ruby/core/proc/fixtures/proc_call_frozen.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true +module ProcCallFrozenSpecs + def self.call + proc {|a| a }.call("sometext") + end + + def self.call_freeze + proc {|a| a }.call("sometext".freeze) + end +end diff --git a/spec/ruby/core/proc/inspect_spec.rb b/spec/ruby/core/proc/inspect_spec.rb index f53d34116f93b0..96995ec410e48b 100644 --- a/spec/ruby/core/proc/inspect_spec.rb +++ b/spec/ruby/core/proc/inspect_spec.rb @@ -1,6 +1,7 @@ require_relative '../../spec_helper' -require_relative 'shared/to_s' describe "Proc#inspect" do - it_behaves_like :proc_to_s, :inspect + it "is an alias of Proc#to_s" do + Proc.instance_method(:inspect).should == Proc.instance_method(:to_s) + end end diff --git a/spec/ruby/core/proc/shared/call.rb b/spec/ruby/core/proc/shared/call.rb deleted file mode 100644 index fae2331b68ed20..00000000000000 --- a/spec/ruby/core/proc/shared/call.rb +++ /dev/null @@ -1,99 +0,0 @@ -require_relative '../fixtures/common' - -describe :proc_call, shared: true do - it "invokes self" do - Proc.new { "test!" }.send(@method).should == "test!" - -> { "test!" }.send(@method).should == "test!" - proc { "test!" }.send(@method).should == "test!" - end - - it "sets self's parameters to the given values" do - Proc.new { |a, b| a + b }.send(@method, 1, 2).should == 3 - Proc.new { |*args| args }.send(@method, 1, 2, 3, 4).should == [1, 2, 3, 4] - Proc.new { |_, *args| args }.send(@method, 1, 2, 3).should == [2, 3] - - -> a, b { a + b }.send(@method, 1, 2).should == 3 - -> *args { args }.send(@method, 1, 2, 3, 4).should == [1, 2, 3, 4] - -> _, *args { args }.send(@method, 1, 2, 3).should == [2, 3] - - proc { |a, b| a + b }.send(@method, 1, 2).should == 3 - proc { |*args| args }.send(@method, 1, 2, 3, 4).should == [1, 2, 3, 4] - proc { |_, *args| args }.send(@method, 1, 2, 3).should == [2, 3] - end -end - - -describe :proc_call_on_proc_new, shared: true do - it "replaces missing arguments with nil" do - Proc.new { |a, b| [a, b] }.send(@method).should == [nil, nil] - Proc.new { |a, b| [a, b] }.send(@method, 1).should == [1, nil] - end - - it "silently ignores extra arguments" do - Proc.new { |a, b| a + b }.send(@method, 1, 2, 5).should == 3 - end - - it "auto-explodes a single Array argument" do - p = Proc.new { |a, b| [a, b] } - p.send(@method, 1, 2).should == [1, 2] - p.send(@method, [1, 2]).should == [1, 2] - p.send(@method, [1, 2, 3]).should == [1, 2] - p.send(@method, [1, 2, 3], 4).should == [[1, 2, 3], 4] - end -end - -describe :proc_call_on_proc_or_lambda, shared: true do - it "ignores excess arguments when self is a proc" do - a = proc {|x| x}.send(@method, 1, 2) - a.should == 1 - - a = proc {|x| x}.send(@method, 1, 2, 3) - a.should == 1 - - a = proc {|x:| x}.send(@method, 2, x: 1) - a.should == 1 - end - - it "will call #to_ary on argument and return self if return is nil" do - argument = ProcSpecs::ToAryAsNil.new - result = proc { |x, _| x }.send(@method, argument) - result.should == argument - end - - it "substitutes nil for missing arguments when self is a proc" do - proc {|x,y| [x,y]}.send(@method).should == [nil,nil] - - a = proc {|x,y| [x, y]}.send(@method, 1) - a.should == [1,nil] - end - - it "raises an ArgumentError on excess arguments when self is a lambda" do - -> { - -> x { x }.send(@method, 1, 2) - }.should.raise(ArgumentError) - - -> { - -> x { x }.send(@method, 1, 2, 3) - }.should.raise(ArgumentError) - end - - it "raises an ArgumentError on missing arguments when self is a lambda" do - -> { - -> x { x }.send(@method) - }.should.raise(ArgumentError) - - -> { - -> x, y { [x,y] }.send(@method, 1) - }.should.raise(ArgumentError) - end - - it "treats a single Array argument as a single argument when self is a lambda" do - -> a { a }.send(@method, [1, 2]).should == [1, 2] - -> a, b { [a, b] }.send(@method, [1, 2], 3).should == [[1,2], 3] - end - - it "treats a single Array argument as a single argument when self is a proc" do - proc { |a| a }.send(@method, [1, 2]).should == [1, 2] - proc { |a, b| [a, b] }.send(@method, [1, 2], 3).should == [[1,2], 3] - end -end diff --git a/spec/ruby/core/proc/shared/call_arguments.rb b/spec/ruby/core/proc/shared/call_arguments.rb deleted file mode 100644 index 91ada3439e45b8..00000000000000 --- a/spec/ruby/core/proc/shared/call_arguments.rb +++ /dev/null @@ -1,29 +0,0 @@ -describe :proc_call_block_args, shared: true do - it "can receive block arguments" do - Proc.new {|&b| b.send(@method)}.send(@method) {1 + 1}.should == 2 - -> &b { b.send(@method)}.send(@method) {1 + 1}.should == 2 - proc {|&b| b.send(@method)}.send(@method) {1 + 1}.should == 2 - end - - it "yields to the block given at declaration and not to the block argument" do - proc_creator = Object.new - def proc_creator.create - Proc.new do |&b| - yield - end - end - a_proc = proc_creator.create { 7 } - a_proc.send(@method) { 3 }.should == 7 - end - - it "can call its block argument declared with a block argument" do - proc_creator = Object.new - def proc_creator.create(method_name) - Proc.new do |&b| - yield + b.send(method_name) - end - end - a_proc = proc_creator.create(@method) { 7 } - a_proc.call { 3 }.should == 10 - end -end diff --git a/spec/ruby/core/proc/shared/equal.rb b/spec/ruby/core/proc/shared/equal.rb deleted file mode 100644 index 4f6f6c41be04ec..00000000000000 --- a/spec/ruby/core/proc/shared/equal.rb +++ /dev/null @@ -1,83 +0,0 @@ -require_relative '../../../spec_helper' -require_relative '../fixtures/common' - -describe :proc_equal, shared: true do - it "is a public method" do - Proc.public_instance_methods(false).should.include?(@method) - end - - it "returns true if self and other are the same object" do - p = proc { :foo } - p.send(@method, p).should == true - - p = Proc.new { :foo } - p.send(@method, p).should == true - - p = -> { :foo } - p.send(@method, p).should == true - end - - it "returns true if other is a dup of the original" do - p = proc { :foo } - p.send(@method, p.dup).should == true - - p = Proc.new { :foo } - p.send(@method, p.dup).should == true - - p = -> { :foo } - p.send(@method, p.dup).should == true - end - - # identical here means the same method invocation. - it "returns false when bodies are the same but capture env is not identical" do - a = ProcSpecs.proc_for_1 - b = ProcSpecs.proc_for_1 - - a.send(@method, b).should == false - end - - it "returns false if procs are distinct but have the same body and environment" do - p = proc { :foo } - p2 = proc { :foo } - p.send(@method, p2).should == false - end - - it "returns false if lambdas are distinct but have same body and environment" do - x = -> { :foo } - x2 = -> { :foo } - x.send(@method, x2).should == false - end - - it "returns false if using comparing lambda to proc, even with the same body and env" do - p = -> { :foo } - p2 = proc { :foo } - p.send(@method, p2).should == false - - x = proc { :bar } - x2 = -> { :bar } - x.send(@method, x2).should == false - end - - it "returns false if other is not a Proc" do - p = proc { :foo } - p.send(@method, []).should == false - - p = Proc.new { :foo } - p.send(@method, Object.new).should == false - - p = -> { :foo } - p.send(@method, :foo).should == false - end - - it "returns false if self and other are both procs but have different bodies" do - p = proc { :bar } - p2 = proc { :foo } - p.send(@method, p2).should == false - end - - it "returns false if self and other are both lambdas but have different bodies" do - p = -> { :foo } - p2 = -> { :bar } - p.send(@method, p2).should == false - end -end diff --git a/spec/ruby/core/proc/shared/to_s.rb b/spec/ruby/core/proc/shared/to_s.rb deleted file mode 100644 index a52688a89f6294..00000000000000 --- a/spec/ruby/core/proc/shared/to_s.rb +++ /dev/null @@ -1,60 +0,0 @@ -describe :proc_to_s, shared: true do - describe "for a proc created with Proc.new" do - it "returns a description including file and line number" do - Proc.new { "hello" }.send(@method).should =~ /^#$/ - end - - it "has a binary encoding" do - Proc.new { "hello" }.send(@method).encoding.should == Encoding::BINARY - end - end - - describe "for a proc created with lambda" do - it "returns a description including '(lambda)' and including file and line number" do - -> { "hello" }.send(@method).should =~ /^#$/ - end - - it "has a binary encoding" do - -> { "hello" }.send(@method).encoding.should == Encoding::BINARY - end - end - - describe "for a proc created with proc" do - it "returns a description including file and line number" do - proc { "hello" }.send(@method).should =~ /^#$/ - end - - it "has a binary encoding" do - proc { "hello" }.send(@method).encoding.should == Encoding::BINARY - end - end - - describe "for a proc created with UnboundMethod#to_proc" do - it "returns a description including '(lambda)' and optionally including file and line number" do - def hello; end - s = method("hello").to_proc.send(@method) - if s.include? __FILE__ - s.should =~ /^#$/ - else - s.should =~ /^#$/ - end - end - - it "has a binary encoding" do - def hello; end - method("hello").to_proc.send(@method).encoding.should == Encoding::BINARY - end - end - - describe "for a proc created with Symbol#to_proc" do - it "returns a description including '(&:symbol)'" do - proc = :foobar.to_proc - proc.send(@method).should.include?('(&:foobar)') - end - - it "has a binary encoding" do - proc = :foobar.to_proc - proc.send(@method).encoding.should == Encoding::BINARY - end - end -end diff --git a/spec/ruby/core/proc/to_s_spec.rb b/spec/ruby/core/proc/to_s_spec.rb index 5e9c46b6b8c4f8..58a9aa76fb43aa 100644 --- a/spec/ruby/core/proc/to_s_spec.rb +++ b/spec/ruby/core/proc/to_s_spec.rb @@ -1,6 +1,62 @@ require_relative '../../spec_helper' -require_relative 'shared/to_s' describe "Proc#to_s" do - it_behaves_like :proc_to_s, :to_s + describe "for a proc created with Proc.new" do + it "returns a description including file and line number" do + Proc.new { "hello" }.to_s.should =~ /^#$/ + end + + it "has a binary encoding" do + Proc.new { "hello" }.to_s.encoding.should == Encoding::BINARY + end + end + + describe "for a proc created with lambda" do + it "returns a description including '(lambda)' and including file and line number" do + -> { "hello" }.to_s.should =~ /^#$/ + end + + it "has a binary encoding" do + -> { "hello" }.to_s.encoding.should == Encoding::BINARY + end + end + + describe "for a proc created with proc" do + it "returns a description including file and line number" do + proc { "hello" }.to_s.should =~ /^#$/ + end + + it "has a binary encoding" do + proc { "hello" }.to_s.encoding.should == Encoding::BINARY + end + end + + describe "for a proc created with UnboundMethod#to_proc" do + it "returns a description including '(lambda)' and optionally including file and line number" do + def hello; end + s = method("hello").to_proc.to_s + if s.include? __FILE__ + s.should =~ /^#$/ + else + s.should =~ /^#$/ + end + end + + it "has a binary encoding" do + def hello; end + method("hello").to_proc.to_s.encoding.should == Encoding::BINARY + end + end + + describe "for a proc created with Symbol#to_proc" do + it "returns a description including '(&:symbol)'" do + proc = :foobar.to_proc + proc.to_s.should.include?('(&:foobar)') + end + + it "has a binary encoding" do + proc = :foobar.to_proc + proc.to_s.encoding.should == Encoding::BINARY + end + end end diff --git a/spec/ruby/core/proc/yield_spec.rb b/spec/ruby/core/proc/yield_spec.rb index 365d5b04bd2139..e6ee2d5eff873e 100644 --- a/spec/ruby/core/proc/yield_spec.rb +++ b/spec/ruby/core/proc/yield_spec.rb @@ -1,16 +1,7 @@ require_relative '../../spec_helper' -require_relative 'shared/call' -require_relative 'shared/call_arguments' describe "Proc#yield" do - it_behaves_like :proc_call, :yield - it_behaves_like :proc_call_block_args, :yield -end - -describe "Proc#yield on a Proc created with Proc.new" do - it_behaves_like :proc_call_on_proc_new, :yield -end - -describe "Proc#yield on a Proc created with Kernel#lambda or Kernel#proc" do - it_behaves_like :proc_call_on_proc_or_lambda, :yield + it "is an alias of Proc#call" do + Proc.instance_method(:yield).should == Proc.instance_method(:call) + end end diff --git a/spec/ruby/core/process/daemon_spec.rb b/spec/ruby/core/process/daemon_spec.rb index 9b7eba14118096..7198dfa6eead66 100644 --- a/spec/ruby/core/process/daemon_spec.rb +++ b/spec/ruby/core/process/daemon_spec.rb @@ -1,10 +1,11 @@ require_relative '../../spec_helper' require_relative 'fixtures/common' -platform_is_not :windows do - # macOS 15 is not working this examples - return if /darwin/ =~ RUBY_PLATFORM && /15/ =~ `sw_vers -productVersion` - +guard -> { + Process.respond_to?(:fork) and + # macOS 15 is not working for these examples + !(/darwin/ =~ RUBY_PLATFORM && /15/ =~ `sw_vers -productVersion`) +} do describe :process_daemon_keep_stdio_open_false, shared: true do it "redirects stdout to /dev/null" do @daemon.invoke("keep_stdio_open_false_stdout", @object).should == "" @@ -107,8 +108,12 @@ end end -platform_is :windows do +guard_not -> { Process.respond_to?(:fork) } do describe "Process.daemon" do + it "returns false from #respond_to?" do + Process.respond_to?(:daemon).should == false + end + it "raises a NotImplementedError" do -> { Process.daemon diff --git a/spec/ruby/core/process/detach_spec.rb b/spec/ruby/core/process/detach_spec.rb index 33c674394e2bee..862768a9096f1a 100644 --- a/spec/ruby/core/process/detach_spec.rb +++ b/spec/ruby/core/process/detach_spec.rb @@ -1,81 +1,82 @@ require_relative '../../spec_helper' +require_relative 'fixtures/common' describe "Process.detach" do - platform_is_not :windows do - it "returns a thread" do - pid = Process.fork { Process.exit! } - thr = Process.detach(pid) - thr.should.is_a?(Thread) - thr.join - end + ProcessSpecs.use_system_ruby(self) - it "produces the exit Process::Status as the thread value" do - pid = Process.fork { Process.exit! } - thr = Process.detach(pid) - thr.join + it "returns a thread" do + pid = Process.spawn(*ruby_exe, "-e", "exit") + thr = Process.detach(pid) + thr.should.is_a?(Thread) + thr.join + end - status = thr.value - status.should.is_a?(Process::Status) - status.pid.should == pid - end + it "produces the exit Process::Status as the thread value" do + pid = Process.spawn(*ruby_exe, "-e", "exit") + thr = Process.detach(pid) + thr.join + + status = thr.value + status.should.is_a?(Process::Status) + status.pid.should == pid + end - platform_is_not :openbsd do - it "reaps the child process's status automatically" do - pid = Process.fork { Process.exit! } - Process.detach(pid).join - -> { Process.waitpid(pid) }.should.raise(Errno::ECHILD) - end + platform_is_not :openbsd do + it "reaps the child process's status automatically" do + pid = Process.spawn(*ruby_exe, "-e", "exit") + Process.detach(pid).join + -> { Process.waitpid(pid) }.should.raise(Errno::ECHILD) end + end - it "sets the :pid thread-local to the PID" do - pid = Process.fork { Process.exit! } - thr = Process.detach(pid) - thr.join + it "sets the :pid thread-local to the PID" do + pid = Process.spawn(*ruby_exe, "-e", "exit") + thr = Process.detach(pid) + thr.join - thr[:pid].should == pid - end + thr[:pid].should == pid + end - it "provides a #pid method on the returned thread which returns the PID" do - pid = Process.fork { Process.exit! } - thr = Process.detach(pid) - thr.join + it "provides a #pid method on the returned thread which returns the PID" do + pid = Process.spawn(*ruby_exe, "-e", "exit") + thr = Process.detach(pid) + thr.join - thr.pid.should == pid - end + thr.pid.should == pid + end - it "tolerates not existing child process pid" do - # Use a value that is close to the INT_MAX (pid usually is signed int). - # It should (at least) be greater than allowed pid limit value that depends on OS. - pid_not_existing = 2.pow(30) + it "tolerates not existing child process pid" do + # Use a value that is close to the INT_MAX (pid usually is signed int). + # It should (at least) be greater than allowed pid limit value that depends on OS. + pid_not_existing = 2.pow(30) - # Check that there is no a child process with this hardcoded pid. - # Command `kill 0 pid`: - # - returns "1" if a process exists and - # - raises Errno::ESRCH otherwise - -> { Process.kill(0, pid_not_existing) }.should.raise(Errno::ESRCH) + # Check that there is no a child process with this hardcoded pid. + # Command `kill 0 pid`: + # - returns "1" if a process exists and + # - raises Errno::ESRCH otherwise + -> { Process.kill(0, pid_not_existing) }.should.raise(Errno::ESRCH) - thr = Process.detach(pid_not_existing) - thr.join + thr = Process.detach(pid_not_existing) + thr.join - thr.should.is_a?(Thread) - end + thr.should.is_a?(Thread) + end - it "calls #to_int to implicitly convert non-Integer pid to Integer" do - pid = MockObject.new('mock-enumerable') - pid.should_receive(:to_int).and_return(100500) + it "calls #to_int to implicitly convert non-Integer pid to Integer" do + pid = MockObject.new('mock-enumerable') + pid.should_receive(:to_int).and_return(100500) - Process.detach(pid).join - end + Process.detach(pid).join + end - it "raises TypeError when pid argument does not have #to_int method" do - -> { Process.detach(Object.new) }.should.raise(TypeError, "no implicit conversion of Object into Integer") - end + it "raises TypeError when pid argument does not have #to_int method" do + -> { Process.detach(Object.new) }.should.raise(TypeError, "no implicit conversion of Object into Integer") + end - it "raises TypeError when #to_int returns non-Integer value" do - pid = MockObject.new('mock-enumerable') - pid.should_receive(:to_int).and_return(:symbol) + it "raises TypeError when #to_int returns non-Integer value" do + pid = MockObject.new('mock-enumerable') + pid.should_receive(:to_int).and_return(:symbol) - -> { Process.detach(pid) }.should raise_consistent_error(TypeError, "can't convert MockObject into Integer (MockObject#to_int gives Symbol)") - end + -> { Process.detach(pid) }.should raise_consistent_error(TypeError, "can't convert MockObject into Integer (MockObject#to_int gives Symbol)") end end diff --git a/spec/ruby/core/process/setpgid_spec.rb b/spec/ruby/core/process/setpgid_spec.rb index be724e9007fc1e..1442d7f99ce39e 100644 --- a/spec/ruby/core/process/setpgid_spec.rb +++ b/spec/ruby/core/process/setpgid_spec.rb @@ -1,7 +1,7 @@ require_relative '../../spec_helper' describe "Process.setpgid" do - platform_is_not :windows do + guard -> { Process.respond_to?(:fork) } do # Must use fork as setpgid(2) gives EACCESS after execve() it "sets the process group id of the specified process" do rd, wr = IO.pipe diff --git a/spec/ruby/core/process/setpgrp_spec.rb b/spec/ruby/core/process/setpgrp_spec.rb index 800668008d4e55..7c4344f115fab9 100644 --- a/spec/ruby/core/process/setpgrp_spec.rb +++ b/spec/ruby/core/process/setpgrp_spec.rb @@ -2,7 +2,7 @@ # TODO: put these in the right files. describe "Process.setpgrp and Process.getpgrp" do - platform_is_not :windows do + guard -> { Process.respond_to?(:fork) } do it "sets and gets the process group ID of the calling process" do # there are two synchronization points here: # One for the child to let the parent know that it has finished diff --git a/spec/ruby/core/process/status/wait_spec.rb b/spec/ruby/core/process/status/wait_spec.rb index 18ecc14f6fac86..8bd7fc6b435146 100644 --- a/spec/ruby/core/process/status/wait_spec.rb +++ b/spec/ruby/core/process/status/wait_spec.rb @@ -70,25 +70,27 @@ end # This spec is probably system-dependent. - it "doesn't block if no child is available when WNOHANG is used" do - read, write = IO.pipe - pid = Process.fork do - read.close - Signal.trap("TERM") { Process.exit! } - write << 1 + guard -> { Process.respond_to?(:fork) } do + it "doesn't block if no child is available when WNOHANG is used" do + read, write = IO.pipe + pid = Process.fork do + read.close + Signal.trap("TERM") { Process.exit! } + write << 1 + write.close + sleep + end + + Process::Status.wait(pid, Process::WNOHANG).should == nil + + # wait for the child to setup its TERM handler write.close - sleep - end - - Process::Status.wait(pid, Process::WNOHANG).should == nil - - # wait for the child to setup its TERM handler - write.close - read.read(1) - read.close + read.read(1) + read.close - Process.kill("TERM", pid) - Process::Status.wait.pid.should == pid + Process.kill("TERM", pid) + Process::Status.wait.pid.should == pid + end end it "always accepts flags=0" do diff --git a/spec/ruby/core/process/wait2_spec.rb b/spec/ruby/core/process/wait2_spec.rb index 5c57dd40fb79f2..1fa6f761517172 100644 --- a/spec/ruby/core/process/wait2_spec.rb +++ b/spec/ruby/core/process/wait2_spec.rb @@ -1,6 +1,9 @@ require_relative '../../spec_helper' +require_relative 'fixtures/common' describe "Process.wait2" do + ProcessSpecs.use_system_ruby(self) + before :all do # HACK: this kludge is temporarily necessary because some # misbehaving spec somewhere else does not clear processes @@ -18,15 +21,13 @@ end end - platform_is_not :windows do - it "returns the pid and status of child process" do - pidf = Process.fork { Process.exit! 99 } - results = Process.wait2 - results.size.should == 2 - pidw, status = results - pidf.should == pidw - status.exitstatus.should == 99 - end + it "returns the pid and status of child process" do + pidf = Process.spawn(*ruby_exe, "-e", "exit 99") + results = Process.wait2 + results.size.should == 2 + pidw, status = results + pidf.should == pidw + status.exitstatus.should == 99 end it "raises a StandardError if no child processes exist" do diff --git a/spec/ruby/core/process/wait_spec.rb b/spec/ruby/core/process/wait_spec.rb index 0b2e715f65d075..5a1889487cf265 100644 --- a/spec/ruby/core/process/wait_spec.rb +++ b/spec/ruby/core/process/wait_spec.rb @@ -17,19 +17,30 @@ -> { Process.wait }.should.raise(Errno::ECHILD) end - platform_is_not :windows do - it "returns its child pid" do - pid = Process.spawn(ruby_cmd('exit')) - Process.wait.should == pid - end + it "returns its child pid" do + pid = Process.spawn(ruby_cmd('exit')) + Process.wait.should == pid + end - it "sets $? to a Process::Status" do - pid = Process.spawn(ruby_cmd('exit')) - Process.wait - $?.should.is_a?(Process::Status) - $?.pid.should == pid + it "returns nil when the process has not yet completed and WNOHANG is specified" do + cmd = platform_is(:windows) ? "timeout" : "sleep" + pid = spawn("#{cmd} 5") + begin + Process.wait(pid, Process::WNOHANG).should == nil + Process.kill("KILL", pid) + ensure + Process.wait(pid) end + end + + it "sets $? to a Process::Status" do + pid = Process.spawn(ruby_cmd('exit')) + Process.wait + $?.should.is_a?(Process::Status) + $?.pid.should == pid + end + platform_is_not :windows do it "waits for any child process if no pid is given" do pid = Process.spawn(ruby_cmd('exit')) Process.wait.should == pid @@ -59,8 +70,10 @@ Process.wait(0).should == pid2 Process.wait.should == pid1 end + end - # This spec is probably system-dependent. + # This spec is probably system-dependent. + guard -> { Process.respond_to?(:fork) } do it "doesn't block if no child is available when WNOHANG is used" do read, write = IO.pipe pid = Process.fork do @@ -81,7 +94,9 @@ Process.kill("TERM", pid) Process.wait.should == pid end + end + platform_is_not :windows do it "always accepts flags=0" do pid = Process.spawn(ruby_cmd('exit')) Process.wait(-1, 0).should == pid diff --git a/spec/ruby/core/process/waitall_spec.rb b/spec/ruby/core/process/waitall_spec.rb index a77873f553ce6b..f5fbce71c1c1ec 100644 --- a/spec/ruby/core/process/waitall_spec.rb +++ b/spec/ruby/core/process/waitall_spec.rb @@ -1,6 +1,9 @@ require_relative '../../spec_helper' +require_relative 'fixtures/common' describe "Process.waitall" do + ProcessSpecs.use_system_ruby(self) + before :all do begin Process.waitall @@ -17,23 +20,16 @@ end platform_is_not :windows do - it "waits for all children" do + it "waits for all children and returns an array of pid/status pairs" do pids = [] - pids << Process.fork { Process.exit! 2 } - pids << Process.fork { Process.exit! 1 } - pids << Process.fork { Process.exit! 0 } - Process.waitall + pids << Process.spawn(ruby_cmd('exit 2')) + pids << Process.spawn(ruby_cmd('exit 1')) + pids << Process.spawn(ruby_cmd('exit 0')) + a = Process.waitall pids.each { |pid| -> { Process.kill(0, pid) }.should.raise(Errno::ESRCH) } - end - it "returns an array of pid/status pairs" do - pids = [] - pids << Process.fork { Process.exit! 2 } - pids << Process.fork { Process.exit! 1 } - pids << Process.fork { Process.exit! 0 } - a = Process.waitall a.should.is_a?(Array) a.size.should == 3 pids.each { |pid| diff --git a/spec/ruby/core/process/waitpid2_spec.rb b/spec/ruby/core/process/waitpid2_spec.rb index 45513af667bd2c..70fe0fbeee7d7f 100644 --- a/spec/ruby/core/process/waitpid2_spec.rb +++ b/spec/ruby/core/process/waitpid2_spec.rb @@ -1,5 +1,7 @@ require_relative '../../spec_helper' describe "Process.waitpid2" do - it "needs to be reviewed for spec completeness" + it "is an alias of Process.wait2" do + Process.method(:waitpid2).should == Process.method(:wait2) + end end diff --git a/spec/ruby/core/process/waitpid_spec.rb b/spec/ruby/core/process/waitpid_spec.rb index a02147b663c364..9b2f49e7cfab6c 100644 --- a/spec/ruby/core/process/waitpid_spec.rb +++ b/spec/ruby/core/process/waitpid_spec.rb @@ -1,14 +1,7 @@ require_relative '../../spec_helper' describe "Process.waitpid" do - it "returns nil when the process has not yet completed and WNOHANG is specified" do - cmd = platform_is(:windows) ? "timeout" : "sleep" - pid = spawn("#{cmd} 5") - begin - Process.waitpid(pid, Process::WNOHANG).should == nil - Process.kill("KILL", pid) - ensure - Process.wait(pid) - end + it "is an alias of Process.wait" do + Process.method(:waitpid).should == Process.method(:wait) end end diff --git a/spec/ruby/core/queue/deq_spec.rb b/spec/ruby/core/queue/deq_spec.rb index a2784e6a63f5ca..374611366e3755 100644 --- a/spec/ruby/core/queue/deq_spec.rb +++ b/spec/ruby/core/queue/deq_spec.rb @@ -1,11 +1,7 @@ require_relative '../../spec_helper' -require_relative '../../shared/queue/deque' -require_relative '../../shared/types/rb_num2dbl_fails' describe "Queue#deq" do - it_behaves_like :queue_deq, :deq, -> { Queue.new } -end - -describe "Queue operations with timeout" do - it_behaves_like :rb_num2dbl_fails, nil, -> v { q = Queue.new; q.push(1); q.deq(timeout: v) } + it "is an alias of Queue#pop" do + Queue.instance_method(:deq).should == Queue.instance_method(:pop) + end end diff --git a/spec/ruby/core/queue/enq_spec.rb b/spec/ruby/core/queue/enq_spec.rb index c69c496fbcbb43..76ecf0ca5f3b8d 100644 --- a/spec/ruby/core/queue/enq_spec.rb +++ b/spec/ruby/core/queue/enq_spec.rb @@ -1,6 +1,7 @@ require_relative '../../spec_helper' -require_relative '../../shared/queue/enque' describe "Queue#enq" do - it_behaves_like :queue_enq, :enq, -> { Queue.new } + it "is an alias of Queue#<<" do + Queue.instance_method(:enq).should == Queue.instance_method(:<<) + end end diff --git a/spec/ruby/core/queue/length_spec.rb b/spec/ruby/core/queue/length_spec.rb index 25399b2b76e662..0566b1d547b170 100644 --- a/spec/ruby/core/queue/length_spec.rb +++ b/spec/ruby/core/queue/length_spec.rb @@ -1,6 +1,7 @@ require_relative '../../spec_helper' -require_relative '../../shared/queue/length' describe "Queue#length" do - it_behaves_like :queue_length, :length, -> { Queue.new } + it "is an alias of Queue#size" do + Queue.instance_method(:length).should == Queue.instance_method(:size) + end end diff --git a/spec/ruby/core/queue/push_spec.rb b/spec/ruby/core/queue/push_spec.rb index e936f9d2822a51..ef622ac89d74a7 100644 --- a/spec/ruby/core/queue/push_spec.rb +++ b/spec/ruby/core/queue/push_spec.rb @@ -1,6 +1,7 @@ require_relative '../../spec_helper' -require_relative '../../shared/queue/enque' describe "Queue#push" do - it_behaves_like :queue_enq, :push, -> { Queue.new } + it "is an alias of Queue#<<" do + Queue.instance_method(:push).should == Queue.instance_method(:<<) + end end diff --git a/spec/ruby/core/queue/shift_spec.rb b/spec/ruby/core/queue/shift_spec.rb index c105da74b2ec4f..074332359c39ff 100644 --- a/spec/ruby/core/queue/shift_spec.rb +++ b/spec/ruby/core/queue/shift_spec.rb @@ -1,11 +1,7 @@ require_relative '../../spec_helper' -require_relative '../../shared/queue/deque' -require_relative '../../shared/types/rb_num2dbl_fails' describe "Queue#shift" do - it_behaves_like :queue_deq, :shift, -> { Queue.new } -end - -describe "Queue operations with timeout" do - it_behaves_like :rb_num2dbl_fails, nil, -> v { q = Queue.new; q.push(1); q.shift(timeout: v) } + it "is an alias of Queue#pop" do + Queue.instance_method(:shift).should == Queue.instance_method(:pop) + end end diff --git a/spec/ruby/core/range/entries_spec.rb b/spec/ruby/core/range/entries_spec.rb new file mode 100644 index 00000000000000..29296711dc68ee --- /dev/null +++ b/spec/ruby/core/range/entries_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' + +describe "Range#entries" do + it "is an alias of Range#to_a" do + Range.instance_method(:entries).should == Range.instance_method(:to_a) + end +end diff --git a/spec/ruby/core/range/include_spec.rb b/spec/ruby/core/range/include_spec.rb index 66a049a90db510..e5cc0dc234f395 100644 --- a/spec/ruby/core/range/include_spec.rb +++ b/spec/ruby/core/range/include_spec.rb @@ -1,12 +1,96 @@ # encoding: binary require_relative '../../spec_helper' +require_relative 'fixtures/classes' require_relative 'shared/cover_and_include' -require_relative 'shared/include' -require_relative 'shared/cover' describe "Range#include?" do it_behaves_like :range_cover_and_include, :include? - it_behaves_like :range_include, :include? + + describe "on string elements" do + it "returns true if other is matched by element.succ" do + ('a'..'c').include?('b').should == true + ('a'...'c').include?('b').should == true + end + + it "returns false if other is not matched by element.succ" do + ('a'..'c').include?('bc').should == false + ('a'...'c').include?('bc').should == false + end + end + + describe "with weird succ" do + describe "when included end value" do + before :each do + @range = RangeSpecs::TenfoldSucc.new(1)..RangeSpecs::TenfoldSucc.new(99) + end + + it "returns false if other is less than first element" do + @range.include?(RangeSpecs::TenfoldSucc.new(0)).should == false + end + + it "returns true if other is equal as first element" do + @range.include?(RangeSpecs::TenfoldSucc.new(1)).should == true + end + + it "returns true if other is matched by element.succ" do + @range.include?(RangeSpecs::TenfoldSucc.new(10)).should == true + end + + it "returns false if other is not matched by element.succ" do + @range.include?(RangeSpecs::TenfoldSucc.new(2)).should == false + end + + it "returns false if other is equal as last element but not matched by element.succ" do + @range.include?(RangeSpecs::TenfoldSucc.new(99)).should == false + end + + it "returns false if other is greater than last element but matched by element.succ" do + @range.include?(RangeSpecs::TenfoldSucc.new(100)).should == false + end + end + + describe "when excluded end value" do + before :each do + @range = RangeSpecs::TenfoldSucc.new(1)...RangeSpecs::TenfoldSucc.new(99) + end + + it "returns false if other is less than first element" do + @range.include?(RangeSpecs::TenfoldSucc.new(0)).should == false + end + + it "returns true if other is equal as first element" do + @range.include?(RangeSpecs::TenfoldSucc.new(1)).should == true + end + + it "returns true if other is matched by element.succ" do + @range.include?(RangeSpecs::TenfoldSucc.new(10)).should == true + end + + it "returns false if other is not matched by element.succ" do + @range.include?(RangeSpecs::TenfoldSucc.new(2)).should == false + end + + it "returns false if other is equal as last element but not matched by element.succ" do + @range.include?(RangeSpecs::TenfoldSucc.new(99)).should == false + end + + it "returns false if other is greater than last element but matched by element.succ" do + @range.include?(RangeSpecs::TenfoldSucc.new(100)).should == false + end + end + end + + describe "with Time endpoints" do + it "uses cover? logic" do + now = Time.now + range = (now..(now + 60)) + + range.include?(now).should == true + range.include?(now - 1).should == false + range.include?(now + 60).should == true + range.include?(now + 61).should == false + end + end it "does not include U+9995 in the range U+0999..U+9999" do ("\u{999}".."\u{9999}").include?("\u{9995}").should == false diff --git a/spec/ruby/core/range/member_spec.rb b/spec/ruby/core/range/member_spec.rb index 78299ae9e5ea3b..98835e4cf312f2 100644 --- a/spec/ruby/core/range/member_spec.rb +++ b/spec/ruby/core/range/member_spec.rb @@ -1,10 +1,7 @@ -# encoding: binary require_relative '../../spec_helper' -require_relative 'shared/cover_and_include' -require_relative 'shared/include' -require_relative 'shared/cover' describe "Range#member?" do - it_behaves_like :range_cover_and_include, :member? - it_behaves_like :range_include, :member? + it "is an alias of Range#include?" do + Range.instance_method(:member?).should == Range.instance_method(:include?) + end end diff --git a/spec/ruby/core/range/shared/include.rb b/spec/ruby/core/range/shared/include.rb deleted file mode 100644 index 5f0db480081ab4..00000000000000 --- a/spec/ruby/core/range/shared/include.rb +++ /dev/null @@ -1,91 +0,0 @@ -# encoding: binary -require_relative '../../../spec_helper' -require_relative '../fixtures/classes' - -describe :range_include, shared: true do - describe "on string elements" do - it "returns true if other is matched by element.succ" do - ('a'..'c').send(@method, 'b').should == true - ('a'...'c').send(@method, 'b').should == true - end - - it "returns false if other is not matched by element.succ" do - ('a'..'c').send(@method, 'bc').should == false - ('a'...'c').send(@method, 'bc').should == false - end - end - - describe "with weird succ" do - describe "when included end value" do - before :each do - @range = RangeSpecs::TenfoldSucc.new(1)..RangeSpecs::TenfoldSucc.new(99) - end - - it "returns false if other is less than first element" do - @range.send(@method, RangeSpecs::TenfoldSucc.new(0)).should == false - end - - it "returns true if other is equal as first element" do - @range.send(@method, RangeSpecs::TenfoldSucc.new(1)).should == true - end - - it "returns true if other is matched by element.succ" do - @range.send(@method, RangeSpecs::TenfoldSucc.new(10)).should == true - end - - it "returns false if other is not matched by element.succ" do - @range.send(@method, RangeSpecs::TenfoldSucc.new(2)).should == false - end - - it "returns false if other is equal as last element but not matched by element.succ" do - @range.send(@method, RangeSpecs::TenfoldSucc.new(99)).should == false - end - - it "returns false if other is greater than last element but matched by element.succ" do - @range.send(@method, RangeSpecs::TenfoldSucc.new(100)).should == false - end - end - - describe "when excluded end value" do - before :each do - @range = RangeSpecs::TenfoldSucc.new(1)...RangeSpecs::TenfoldSucc.new(99) - end - - it "returns false if other is less than first element" do - @range.send(@method, RangeSpecs::TenfoldSucc.new(0)).should == false - end - - it "returns true if other is equal as first element" do - @range.send(@method, RangeSpecs::TenfoldSucc.new(1)).should == true - end - - it "returns true if other is matched by element.succ" do - @range.send(@method, RangeSpecs::TenfoldSucc.new(10)).should == true - end - - it "returns false if other is not matched by element.succ" do - @range.send(@method, RangeSpecs::TenfoldSucc.new(2)).should == false - end - - it "returns false if other is equal as last element but not matched by element.succ" do - @range.send(@method, RangeSpecs::TenfoldSucc.new(99)).should == false - end - - it "returns false if other is greater than last element but matched by element.succ" do - @range.send(@method, RangeSpecs::TenfoldSucc.new(100)).should == false - end - end - end - - describe "with Time endpoints" do - it "uses cover? logic" do - now = Time.now - range = (now..(now + 60)) - - range.include?(now).should == true - range.include?(now - 1).should == false - range.include?(now + 60).should == true - range.include?(now + 61).should == false - end - end -end diff --git a/spec/ruby/core/rational/abs_spec.rb b/spec/ruby/core/rational/abs_spec.rb index 54099aa14d0ce0..6bb4a0fbefcc4f 100644 --- a/spec/ruby/core/rational/abs_spec.rb +++ b/spec/ruby/core/rational/abs_spec.rb @@ -1,6 +1,11 @@ require_relative "../../spec_helper" -require_relative 'shared/abs' describe "Rational#abs" do - it_behaves_like :rational_abs, :abs + it "returns self's absolute value" do + Rational(3, 4).abs.should == Rational(3, 4) + Rational(-3, 4).abs.should == Rational(3, 4) + Rational(3, -4).abs.should == Rational(3, 4) + + Rational(bignum_value, -bignum_value).abs.should == Rational(bignum_value, bignum_value) + end end diff --git a/spec/ruby/core/rational/magnitude_spec.rb b/spec/ruby/core/rational/magnitude_spec.rb index f5f667edb1b4ac..0df637df7a2a17 100644 --- a/spec/ruby/core/rational/magnitude_spec.rb +++ b/spec/ruby/core/rational/magnitude_spec.rb @@ -1,6 +1,7 @@ require_relative "../../spec_helper" -require_relative 'shared/abs' -describe "Rational#abs" do - it_behaves_like :rational_abs, :magnitude +describe "Rational#magnitude" do + it "is an alias of Rational#abs" do + Rational.instance_method(:magnitude).should == Rational.instance_method(:abs) + end end diff --git a/spec/ruby/core/rational/quo_spec.rb b/spec/ruby/core/rational/quo_spec.rb index 907898ad34920a..62178f403b6d11 100644 --- a/spec/ruby/core/rational/quo_spec.rb +++ b/spec/ruby/core/rational/quo_spec.rb @@ -1,25 +1,7 @@ require_relative "../../spec_helper" describe "Rational#quo" do - it "calls #coerce on the passed argument with self" do - rational = Rational(3, 4) - obj = mock("Object") - obj.should_receive(:coerce).with(rational).and_return([1, 2]) - - rational.quo(obj) - end - - it "calls #/ on the coerced Rational with the coerced Object" do - rational = Rational(3, 4) - - coerced_rational = mock("Coerced Rational") - coerced_rational.should_receive(:/).and_return(:result) - - coerced_obj = mock("Coerced Object") - - obj = mock("Object") - obj.should_receive(:coerce).and_return([coerced_rational, coerced_obj]) - - rational.quo(obj).should == :result + it "is an alias of Rational#/" do + Rational.instance_method(:quo).should == Rational.instance_method(:/) end end diff --git a/spec/ruby/core/rational/shared/abs.rb b/spec/ruby/core/rational/shared/abs.rb deleted file mode 100644 index 3d64bcc1a0e94f..00000000000000 --- a/spec/ruby/core/rational/shared/abs.rb +++ /dev/null @@ -1,11 +0,0 @@ -require_relative '../../../spec_helper' - -describe :rational_abs, shared: true do - it "returns self's absolute value" do - Rational(3, 4).send(@method).should == Rational(3, 4) - Rational(-3, 4).send(@method).should == Rational(3, 4) - Rational(3, -4).send(@method).should == Rational(3, 4) - - Rational(bignum_value, -bignum_value).send(@method).should == Rational(bignum_value, bignum_value) - end -end diff --git a/spec/ruby/core/refinement/refined_class_spec.rb b/spec/ruby/core/refinement/refined_class_spec.rb index b532d9a7738cca..90f8d963d8a0f7 100644 --- a/spec/ruby/core/refinement/refined_class_spec.rb +++ b/spec/ruby/core/refinement/refined_class_spec.rb @@ -1,5 +1,4 @@ require_relative "../../spec_helper" -require_relative 'shared/target' describe "Refinement#refined_class" do ruby_version_is ""..."3.4" do diff --git a/spec/ruby/core/refinement/shared/target.rb b/spec/ruby/core/refinement/shared/target.rb deleted file mode 100644 index 79557bea0b9679..00000000000000 --- a/spec/ruby/core/refinement/shared/target.rb +++ /dev/null @@ -1,13 +0,0 @@ -describe :refinement_target, shared: true do - it "returns the class refined by the receiver" do - refinement_int = nil - - Module.new do - refine Integer do - refinement_int = self - end - end - - refinement_int.send(@method).should == Integer - end -end diff --git a/spec/ruby/core/refinement/target_spec.rb b/spec/ruby/core/refinement/target_spec.rb index 8bd816aea622dd..eaee71e8c6d0e6 100644 --- a/spec/ruby/core/refinement/target_spec.rb +++ b/spec/ruby/core/refinement/target_spec.rb @@ -1,6 +1,15 @@ require_relative "../../spec_helper" -require_relative 'shared/target' describe "Refinement#target" do - it_behaves_like :refinement_target, :target + it "returns the class refined by the receiver" do + refinement_int = nil + + Module.new do + refine Integer do + refinement_int = self + end + end + + refinement_int.target.should == Integer + end end diff --git a/spec/ruby/core/regexp/eql_spec.rb b/spec/ruby/core/regexp/eql_spec.rb index bd5ae43eb2c81e..5924333fbd2ff7 100644 --- a/spec/ruby/core/regexp/eql_spec.rb +++ b/spec/ruby/core/regexp/eql_spec.rb @@ -1,6 +1,7 @@ require_relative '../../spec_helper' -require_relative 'shared/equal_value' describe "Regexp#eql?" do - it_behaves_like :regexp_eql, :eql? + it "is an alias of Regexp#==" do + Regexp.instance_method(:eql?).should == Regexp.instance_method(:==) + end end diff --git a/spec/ruby/core/regexp/equal_value_spec.rb b/spec/ruby/core/regexp/equal_value_spec.rb index 5455a30598fd26..ad8dc332229565 100644 --- a/spec/ruby/core/regexp/equal_value_spec.rb +++ b/spec/ruby/core/regexp/equal_value_spec.rb @@ -1,6 +1,33 @@ require_relative '../../spec_helper' -require_relative 'shared/equal_value' describe "Regexp#==" do - it_behaves_like :regexp_eql, :== + it "is true if self and other have the same pattern" do + (/abc/ == /abc/).should == true + (/abc/ == /abd/).should == false + end + + not_supported_on :opal do + it "is true if self and other have the same character set code" do + (/abc/ == /abc/x).should == false + (/abc/x == /abc/x).should == true + (/abc/u == /abc/n).should == false + (/abc/u == /abc/u).should == true + (/abc/n == /abc/n).should == true + end + end + + it "is true if other has the same #casefold? values" do + (/abc/ == /abc/i).should == false + (/abc/i == /abc/i).should == true + end + + not_supported_on :opal do + it "is true if self does not specify /n option and other does" do + (// == //n).should == true + end + + it "is true if self specifies /n option and other does not" do + (//n == //).should == true + end + end end diff --git a/spec/ruby/core/regexp/escape_spec.rb b/spec/ruby/core/regexp/escape_spec.rb index 6b06ab1cbcbcd4..99e5eb71d66a99 100644 --- a/spec/ruby/core/regexp/escape_spec.rb +++ b/spec/ruby/core/regexp/escape_spec.rb @@ -1,6 +1,7 @@ require_relative '../../spec_helper' -require_relative 'shared/quote' describe "Regexp.escape" do - it_behaves_like :regexp_quote, :escape + it "is an alias of Regexp.quote" do + Regexp.method(:escape).should == Regexp.method(:quote) + end end diff --git a/spec/ruby/core/regexp/quote_spec.rb b/spec/ruby/core/regexp/quote_spec.rb index 370ab13e301d09..27fa0c06693b42 100644 --- a/spec/ruby/core/regexp/quote_spec.rb +++ b/spec/ruby/core/regexp/quote_spec.rb @@ -1,6 +1,43 @@ +# encoding: binary + require_relative '../../spec_helper' -require_relative 'shared/quote' describe "Regexp.quote" do - it_behaves_like :regexp_quote, :quote + it "escapes any characters with special meaning in a regular expression" do + Regexp.quote('\*?{}.+^$[]()- ').should == '\\\\\*\?\{\}\.\+\^\$\[\]\(\)\-\\ ' + Regexp.quote("\*?{}.+^$[]()- ").should == '\\*\\?\\{\\}\\.\\+\\^\\$\\[\\]\\(\\)\\-\\ ' + Regexp.quote('\n\r\f\t').should == '\\\\n\\\\r\\\\f\\\\t' + Regexp.quote("\n\r\f\t").should == '\\n\\r\\f\\t' + end + + it "works with symbols" do + Regexp.quote(:symbol).should == 'symbol' + end + + it "works with substrings" do + str = ".+[]()"[1...-1] + Regexp.quote(str).should == '\+\[\]\(' + end + + it "works for broken strings" do + Regexp.quote("a.\x85b.".dup.force_encoding("US-ASCII")).should =="a\\.\x85b\\.".dup.force_encoding("US-ASCII") + Regexp.quote("a.\x80".dup.force_encoding("UTF-8")).should == "a\\.\x80".dup.force_encoding("UTF-8") + end + + it "sets the encoding of the result to US-ASCII if there are only US-ASCII characters present in the input String" do + str = "abc".dup.force_encoding("euc-jp") + Regexp.quote(str).encoding.should == Encoding::US_ASCII + end + + it "sets the encoding of the result to the encoding of the String if any non-US-ASCII characters are present in an input String with valid encoding" do + str = "ありがとう".dup.force_encoding("utf-8") + str.valid_encoding?.should == true + Regexp.quote(str).encoding.should == Encoding::UTF_8 + end + + it "sets the encoding of the result to BINARY if any non-US-ASCII characters are present in an input String with invalid encoding" do + str = "\xff".dup.force_encoding "us-ascii" + str.valid_encoding?.should == false + Regexp.quote("\xff").encoding.should == Encoding::BINARY + end end diff --git a/spec/ruby/core/regexp/shared/equal_value.rb b/spec/ruby/core/regexp/shared/equal_value.rb deleted file mode 100644 index 803988de9e83c3..00000000000000 --- a/spec/ruby/core/regexp/shared/equal_value.rb +++ /dev/null @@ -1,31 +0,0 @@ -describe :regexp_eql, shared: true do - it "is true if self and other have the same pattern" do - /abc/.send(@method, /abc/).should == true - /abc/.send(@method, /abd/).should == false - end - - not_supported_on :opal do - it "is true if self and other have the same character set code" do - /abc/.send(@method, /abc/x).should == false - /abc/x.send(@method, /abc/x).should == true - /abc/u.send(@method, /abc/n).should == false - /abc/u.send(@method, /abc/u).should == true - /abc/n.send(@method, /abc/n).should == true - end - end - - it "is true if other has the same #casefold? values" do - /abc/.send(@method, /abc/i).should == false - /abc/i.send(@method, /abc/i).should == true - end - - not_supported_on :opal do - it "is true if self does not specify /n option and other does" do - //.send(@method, //n).should == true - end - - it "is true if self specifies /n option and other does not" do - //n.send(@method, //).should == true - end - end -end diff --git a/spec/ruby/core/regexp/shared/quote.rb b/spec/ruby/core/regexp/shared/quote.rb deleted file mode 100644 index 083f12d78c1266..00000000000000 --- a/spec/ruby/core/regexp/shared/quote.rb +++ /dev/null @@ -1,41 +0,0 @@ -# encoding: binary - -describe :regexp_quote, shared: true do - it "escapes any characters with special meaning in a regular expression" do - Regexp.send(@method, '\*?{}.+^$[]()- ').should == '\\\\\*\?\{\}\.\+\^\$\[\]\(\)\-\\ ' - Regexp.send(@method, "\*?{}.+^$[]()- ").should == '\\*\\?\\{\\}\\.\\+\\^\\$\\[\\]\\(\\)\\-\\ ' - Regexp.send(@method, '\n\r\f\t').should == '\\\\n\\\\r\\\\f\\\\t' - Regexp.send(@method, "\n\r\f\t").should == '\\n\\r\\f\\t' - end - - it "works with symbols" do - Regexp.send(@method, :symbol).should == 'symbol' - end - - it "works with substrings" do - str = ".+[]()"[1...-1] - Regexp.send(@method, str).should == '\+\[\]\(' - end - - it "works for broken strings" do - Regexp.send(@method, "a.\x85b.".dup.force_encoding("US-ASCII")).should =="a\\.\x85b\\.".dup.force_encoding("US-ASCII") - Regexp.send(@method, "a.\x80".dup.force_encoding("UTF-8")).should == "a\\.\x80".dup.force_encoding("UTF-8") - end - - it "sets the encoding of the result to US-ASCII if there are only US-ASCII characters present in the input String" do - str = "abc".dup.force_encoding("euc-jp") - Regexp.send(@method, str).encoding.should == Encoding::US_ASCII - end - - it "sets the encoding of the result to the encoding of the String if any non-US-ASCII characters are present in an input String with valid encoding" do - str = "ありがとう".dup.force_encoding("utf-8") - str.valid_encoding?.should == true - Regexp.send(@method, str).encoding.should == Encoding::UTF_8 - end - - it "sets the encoding of the result to BINARY if any non-US-ASCII characters are present in an input String with invalid encoding" do - str = "\xff".dup.force_encoding "us-ascii" - str.valid_encoding?.should == false - Regexp.send(@method, "\xff").encoding.should == Encoding::BINARY - end -end diff --git a/spec/ruby/core/set/add_spec.rb b/spec/ruby/core/set/add_spec.rb index 1ce03b1eabb543..1a018b186a0a8c 100644 --- a/spec/ruby/core/set/add_spec.rb +++ b/spec/ruby/core/set/add_spec.rb @@ -1,8 +1,18 @@ require_relative '../../spec_helper' -require_relative 'shared/add' describe "Set#add" do - it_behaves_like :set_add, :add + before :each do + @set = Set.new + end + + it "adds the passed Object to self" do + @set.add("dog") + @set.should.include?("dog") + end + + it "returns self" do + @set.add("dog").should.equal?(@set) + end end describe "Set#add?" do diff --git a/spec/ruby/core/set/append_spec.rb b/spec/ruby/core/set/append_spec.rb index 82d34d9130ce8c..4f4e2351e2c362 100644 --- a/spec/ruby/core/set/append_spec.rb +++ b/spec/ruby/core/set/append_spec.rb @@ -1,6 +1,7 @@ require_relative '../../spec_helper' -require_relative 'shared/add' describe "Set#<<" do - it_behaves_like :set_add, :<< + it "is an alias of Set#add" do + Set.instance_method(:<<).should == Set.instance_method(:add) + end end diff --git a/spec/ruby/core/set/case_compare_spec.rb b/spec/ruby/core/set/case_compare_spec.rb index 3781b1b96349df..6fe749c79bcc43 100644 --- a/spec/ruby/core/set/case_compare_spec.rb +++ b/spec/ruby/core/set/case_compare_spec.rb @@ -1,11 +1,7 @@ require_relative '../../spec_helper' -require_relative 'shared/include' describe "Set#===" do - it_behaves_like :set_include, :=== - - it "is an alias for include?" do - set = Set.new - set.method(:===).should == set.method(:include?) + it "is an alias of Set#include?" do + Set.instance_method(:===).should == Set.instance_method(:include?) end end diff --git a/spec/ruby/core/set/case_equality_spec.rb b/spec/ruby/core/set/case_equality_spec.rb deleted file mode 100644 index 19c1fb6b9c51b0..00000000000000 --- a/spec/ruby/core/set/case_equality_spec.rb +++ /dev/null @@ -1,6 +0,0 @@ -require_relative '../../spec_helper' -require_relative 'shared/include' - -describe "Set#===" do - it_behaves_like :set_include, :=== -end diff --git a/spec/ruby/core/set/collect_spec.rb b/spec/ruby/core/set/collect_spec.rb index d186f1a0d97dc2..b78ee493d493c1 100644 --- a/spec/ruby/core/set/collect_spec.rb +++ b/spec/ruby/core/set/collect_spec.rb @@ -1,6 +1,7 @@ require_relative '../../spec_helper' -require_relative 'shared/collect' describe "Set#collect!" do - it_behaves_like :set_collect_bang, :collect! + it "is an alias of Set#map!" do + Set.instance_method(:collect!).should == Set.instance_method(:map!) + end end diff --git a/spec/ruby/core/set/difference_spec.rb b/spec/ruby/core/set/difference_spec.rb index 149f946592b46f..22d89973a818ad 100644 --- a/spec/ruby/core/set/difference_spec.rb +++ b/spec/ruby/core/set/difference_spec.rb @@ -1,6 +1,7 @@ require_relative '../../spec_helper' -require_relative 'shared/difference' describe "Set#difference" do - it_behaves_like :set_difference, :difference + it "is an alias of Set#-" do + Set.instance_method(:difference).should == Set.instance_method(:-) + end end diff --git a/spec/ruby/core/set/eql_spec.rb b/spec/ruby/core/set/eql_spec.rb index 6862ed4edad592..e7eacf29999884 100644 --- a/spec/ruby/core/set/eql_spec.rb +++ b/spec/ruby/core/set/eql_spec.rb @@ -1,14 +1,22 @@ require_relative '../../spec_helper' describe "Set#eql?" do - it "returns true when the passed argument is a Set and contains the same elements" do - Set[].should.eql?(Set[]) - Set[1, 2, 3].should.eql?(Set[1, 2, 3]) - Set[1, 2, 3].should.eql?(Set[3, 2, 1]) - Set["a", :b, ?c].should.eql?(Set[?c, :b, "a"]) + ruby_version_is ""..."4.0" do + it "returns true when the passed argument is a Set and contains the same elements" do + Set[].should.eql?(Set[]) + Set[1, 2, 3].should.eql?(Set[1, 2, 3]) + Set[1, 2, 3].should.eql?(Set[3, 2, 1]) + Set["a", :b, ?c].should.eql?(Set[?c, :b, "a"]) - Set[1, 2, 3].should_not.eql?(Set[1.0, 2, 3]) - Set[1, 2, 3].should_not.eql?(Set[2, 3]) - Set[1, 2, 3].should_not.eql?(Set[]) + Set[1, 2, 3].should_not.eql?(Set[1.0, 2, 3]) + Set[1, 2, 3].should_not.eql?(Set[2, 3]) + Set[1, 2, 3].should_not.eql?(Set[]) + end + end + + ruby_version_is "4.0" do + it "is an alias of Set#==" do + Set.instance_method(:eql?).should == Set.instance_method(:==) + end end end diff --git a/spec/ruby/core/set/filter_spec.rb b/spec/ruby/core/set/filter_spec.rb index 779254ad680add..d0c294c27fef78 100644 --- a/spec/ruby/core/set/filter_spec.rb +++ b/spec/ruby/core/set/filter_spec.rb @@ -1,6 +1,7 @@ require_relative '../../spec_helper' -require_relative 'shared/select' describe "Set#filter!" do - it_behaves_like :set_select_bang, :filter! + it "is an alias of Set#select!" do + Set.instance_method(:filter!).should == Set.instance_method(:select!) + end end diff --git a/spec/ruby/core/set/gt_spec.rb b/spec/ruby/core/set/gt_spec.rb new file mode 100644 index 00000000000000..8a7e421e4014cb --- /dev/null +++ b/spec/ruby/core/set/gt_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' + +describe "Set#>" do + it "is an alias of Set#proper_superset?" do + Set.instance_method(:>).should == Set.instance_method(:proper_superset?) + end +end diff --git a/spec/ruby/core/set/gte_spec.rb b/spec/ruby/core/set/gte_spec.rb new file mode 100644 index 00000000000000..e98c3cb1e20e87 --- /dev/null +++ b/spec/ruby/core/set/gte_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' + +describe "Set#>=" do + it "is an alias of Set#superset?" do + Set.instance_method(:>=).should == Set.instance_method(:superset?) + end +end diff --git a/spec/ruby/core/set/include_spec.rb b/spec/ruby/core/set/include_spec.rb index dd33bbc3bd5332..92a6ca04e6df08 100644 --- a/spec/ruby/core/set/include_spec.rb +++ b/spec/ruby/core/set/include_spec.rb @@ -1,6 +1,31 @@ require_relative '../../spec_helper' -require_relative 'shared/include' describe "Set#include?" do - it_behaves_like :set_include, :include? + it "returns true when self contains the passed Object" do + set = Set[:a, :b, :c] + set.include?(:a).should == true + set.include?(:e).should == false + end + + describe "member equality" do + it "is checked using both #hash and #eql?" do + obj = Object.new + obj_another = Object.new + + def obj.hash; 42 end + def obj_another.hash; 42 end + def obj_another.eql?(o) hash == o.hash end + + set = Set["a", "b", "c", obj] + set.include?(obj_another).should == true + end + + it "is not checked using #==" do + obj = Object.new + set = Set["a", "b", "c"] + + obj.should_not_receive(:==) + set.include?(obj) + end + end end diff --git a/spec/ruby/core/set/inspect_spec.rb b/spec/ruby/core/set/inspect_spec.rb index 0dcce83eb63dfe..45aeed280ece4f 100644 --- a/spec/ruby/core/set/inspect_spec.rb +++ b/spec/ruby/core/set/inspect_spec.rb @@ -1,6 +1,7 @@ require_relative '../../spec_helper' -require_relative 'shared/inspect' describe "Set#inspect" do - it_behaves_like :set_inspect, :inspect + it "is an alias of Set#to_s" do + Set.instance_method(:inspect).should == Set.instance_method(:to_s) + end end diff --git a/spec/ruby/core/set/intersection_spec.rb b/spec/ruby/core/set/intersection_spec.rb index 136b886775864b..c14e1f62ad30f7 100644 --- a/spec/ruby/core/set/intersection_spec.rb +++ b/spec/ruby/core/set/intersection_spec.rb @@ -1,10 +1,23 @@ require_relative '../../spec_helper' -require_relative 'shared/intersection' describe "Set#intersection" do - it_behaves_like :set_intersection, :intersection + it "is an alias of Set#&" do + Set.instance_method(:intersection).should == Set.instance_method(:&) + end end describe "Set#&" do - it_behaves_like :set_intersection, :& + before :each do + @set = Set[:a, :b, :c] + end + + it "returns a new Set containing only elements shared by self and the passed Enumerable" do + (@set & Set[:b, :c, :d, :e]).should == Set[:b, :c] + (@set & [:b, :c, :d]).should == Set[:b, :c] + end + + it "raises an ArgumentError when passed a non-Enumerable" do + -> { @set & 1 }.should.raise(ArgumentError) + -> { @set & Object.new }.should.raise(ArgumentError) + end end diff --git a/spec/ruby/core/set/length_spec.rb b/spec/ruby/core/set/length_spec.rb index 6bb697b4caab9a..9b0d3622b8f52c 100644 --- a/spec/ruby/core/set/length_spec.rb +++ b/spec/ruby/core/set/length_spec.rb @@ -1,6 +1,7 @@ require_relative '../../spec_helper' -require_relative 'shared/length' describe "Set#length" do - it_behaves_like :set_length, :length + it "is an alias of Set#size" do + Set.instance_method(:length).should == Set.instance_method(:size) + end end diff --git a/spec/ruby/core/set/lt_spec.rb b/spec/ruby/core/set/lt_spec.rb new file mode 100644 index 00000000000000..0f5bc9c64275e9 --- /dev/null +++ b/spec/ruby/core/set/lt_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' + +describe "Set#<" do + it "is an alias of Set#proper_subset?" do + Set.instance_method(:<).should == Set.instance_method(:proper_subset?) + end +end diff --git a/spec/ruby/core/set/lte_spec.rb b/spec/ruby/core/set/lte_spec.rb new file mode 100644 index 00000000000000..291d582240a164 --- /dev/null +++ b/spec/ruby/core/set/lte_spec.rb @@ -0,0 +1,7 @@ +require_relative '../../spec_helper' + +describe "Set#<=" do + it "is an alias of Set#subset?" do + Set.instance_method(:<=).should == Set.instance_method(:subset?) + end +end diff --git a/spec/ruby/core/set/map_spec.rb b/spec/ruby/core/set/map_spec.rb index 996191b0a8bc26..fd04a8bde17af2 100644 --- a/spec/ruby/core/set/map_spec.rb +++ b/spec/ruby/core/set/map_spec.rb @@ -1,6 +1,22 @@ require_relative '../../spec_helper' -require_relative 'shared/collect' describe "Set#map!" do - it_behaves_like :set_collect_bang, :map! + before :each do + @set = Set[1, 2, 3, 4, 5] + end + + it "yields each Object in self" do + res = [] + @set.map! { |x| res << x } + res.sort.should == [1, 2, 3, 4, 5].sort + end + + it "returns self" do + @set.map! { |x| x }.should.equal?(@set) + end + + it "replaces self with the return values of the block" do + @set.map! { |x| x * 2 } + @set.should == Set[2, 4, 6, 8, 10] + end end diff --git a/spec/ruby/core/set/member_spec.rb b/spec/ruby/core/set/member_spec.rb index 5c82e8f826b553..a36308eec7cf33 100644 --- a/spec/ruby/core/set/member_spec.rb +++ b/spec/ruby/core/set/member_spec.rb @@ -1,6 +1,7 @@ require_relative '../../spec_helper' -require_relative 'shared/include' describe "Set#member?" do - it_behaves_like :set_include, :member? + it "is an alias of Set#include?" do + Set.instance_method(:member?).should == Set.instance_method(:include?) + end end diff --git a/spec/ruby/core/set/minus_spec.rb b/spec/ruby/core/set/minus_spec.rb index 72f98f985ee3cd..8574708559ad96 100644 --- a/spec/ruby/core/set/minus_spec.rb +++ b/spec/ruby/core/set/minus_spec.rb @@ -1,6 +1,17 @@ require_relative '../../spec_helper' -require_relative 'shared/difference' describe "Set#-" do - it_behaves_like :set_difference, :- + before :each do + @set = Set[:a, :b, :c] + end + + it "returns a new Set containing self's elements excluding the elements in the passed Enumerable" do + (@set - Set[:a, :b]).should == Set[:c] + (@set - [:b, :c]).should == Set[:a] + end + + it "raises an ArgumentError when passed a non-Enumerable" do + -> { @set - 1 }.should.raise(ArgumentError) + -> { @set - Object.new }.should.raise(ArgumentError) + end end diff --git a/spec/ruby/core/set/plus_spec.rb b/spec/ruby/core/set/plus_spec.rb index 7e44ff0b7e19b6..839f77fc399350 100644 --- a/spec/ruby/core/set/plus_spec.rb +++ b/spec/ruby/core/set/plus_spec.rb @@ -1,6 +1,7 @@ require_relative '../../spec_helper' -require_relative 'shared/union' describe "Set#+" do - it_behaves_like :set_union, :+ + it "is an alias of Set#|" do + Set.instance_method(:+).should == Set.instance_method(:|) + end end diff --git a/spec/ruby/core/set/select_spec.rb b/spec/ruby/core/set/select_spec.rb index b458ffacaa63d2..619194605b9cb0 100644 --- a/spec/ruby/core/set/select_spec.rb +++ b/spec/ruby/core/set/select_spec.rb @@ -1,6 +1,41 @@ require_relative '../../spec_helper' -require_relative 'shared/select' describe "Set#select!" do - it_behaves_like :set_select_bang, :select! + before :each do + @set = Set["one", "two", "three"] + end + + it "yields every element of self" do + ret = [] + @set.select! { |x| ret << x } + ret.sort.should == ["one", "two", "three"].sort + end + + it "keeps every element from self for which the passed block returns true" do + @set.select! { |x| x.size != 3 } + @set.size.should.eql?(1) + + @set.should_not.include?("one") + @set.should_not.include?("two") + @set.should.include?("three") + end + + it "returns self when self was modified" do + @set.select! { false }.should.equal?(@set) + end + + it "returns nil when self was not modified" do + @set.select! { true }.should == nil + end + + it "returns an Enumerator when passed no block" do + enum = @set.select! + enum.should.instance_of?(Enumerator) + + enum.each { |x| x.size != 3 } + + @set.should_not.include?("one") + @set.should_not.include?("two") + @set.should.include?("three") + end end diff --git a/spec/ruby/core/set/shared/add.rb b/spec/ruby/core/set/shared/add.rb deleted file mode 100644 index 8d6d83434f52e4..00000000000000 --- a/spec/ruby/core/set/shared/add.rb +++ /dev/null @@ -1,14 +0,0 @@ -describe :set_add, shared: true do - before :each do - @set = Set.new - end - - it "adds the passed Object to self" do - @set.send(@method, "dog") - @set.should.include?("dog") - end - - it "returns self" do - @set.send(@method, "dog").should.equal?(@set) - end -end diff --git a/spec/ruby/core/set/shared/collect.rb b/spec/ruby/core/set/shared/collect.rb deleted file mode 100644 index ad5c5afa590b04..00000000000000 --- a/spec/ruby/core/set/shared/collect.rb +++ /dev/null @@ -1,20 +0,0 @@ -describe :set_collect_bang, shared: true do - before :each do - @set = Set[1, 2, 3, 4, 5] - end - - it "yields each Object in self" do - res = [] - @set.send(@method) { |x| res << x } - res.sort.should == [1, 2, 3, 4, 5].sort - end - - it "returns self" do - @set.send(@method) { |x| x }.should.equal?(@set) - end - - it "replaces self with the return values of the block" do - @set.send(@method) { |x| x * 2 } - @set.should == Set[2, 4, 6, 8, 10] - end -end diff --git a/spec/ruby/core/set/shared/difference.rb b/spec/ruby/core/set/shared/difference.rb deleted file mode 100644 index 8a17056a823862..00000000000000 --- a/spec/ruby/core/set/shared/difference.rb +++ /dev/null @@ -1,15 +0,0 @@ -describe :set_difference, shared: true do - before :each do - @set = Set[:a, :b, :c] - end - - it "returns a new Set containing self's elements excluding the elements in the passed Enumerable" do - @set.send(@method, Set[:a, :b]).should == Set[:c] - @set.send(@method, [:b, :c]).should == Set[:a] - end - - it "raises an ArgumentError when passed a non-Enumerable" do - -> { @set.send(@method, 1) }.should.raise(ArgumentError) - -> { @set.send(@method, Object.new) }.should.raise(ArgumentError) - end -end diff --git a/spec/ruby/core/set/shared/include.rb b/spec/ruby/core/set/shared/include.rb deleted file mode 100644 index 82755ccf593313..00000000000000 --- a/spec/ruby/core/set/shared/include.rb +++ /dev/null @@ -1,29 +0,0 @@ -describe :set_include, shared: true do - it "returns true when self contains the passed Object" do - set = Set[:a, :b, :c] - set.send(@method, :a).should == true - set.send(@method, :e).should == false - end - - describe "member equality" do - it "is checked using both #hash and #eql?" do - obj = Object.new - obj_another = Object.new - - def obj.hash; 42 end - def obj_another.hash; 42 end - def obj_another.eql?(o) hash == o.hash end - - set = Set["a", "b", "c", obj] - set.send(@method, obj_another).should == true - end - - it "is not checked using #==" do - obj = Object.new - set = Set["a", "b", "c"] - - obj.should_not_receive(:==) - set.send(@method, obj) - end - end -end diff --git a/spec/ruby/core/set/shared/inspect.rb b/spec/ruby/core/set/shared/inspect.rb deleted file mode 100644 index 31bd8accfd0ec7..00000000000000 --- a/spec/ruby/core/set/shared/inspect.rb +++ /dev/null @@ -1,45 +0,0 @@ -describe :set_inspect, shared: true do - it "returns a String representation of self" do - Set[].send(@method).should.is_a?(String) - Set[nil, false, true].send(@method).should.is_a?(String) - Set[1, 2, 3].send(@method).should.is_a?(String) - Set["1", "2", "3"].send(@method).should.is_a?(String) - Set[:a, "b", Set[?c]].send(@method).should.is_a?(String) - end - - ruby_version_is "4.0" do - it "does include the elements of the set" do - Set["1"].send(@method).should == 'Set["1"]' - end - end - - ruby_version_is ""..."4.0" do - it "does include the elements of the set" do - Set["1"].send(@method).should == '#' - end - end - - it "puts spaces between the elements" do - Set["1", "2"].send(@method).should.include?('", "') - end - - ruby_version_is "4.0" do - it "correctly handles cyclic-references" do - set1 = Set[] - set2 = Set[set1] - set1 << set2 - set1.send(@method).should.is_a?(String) - set1.send(@method).should.include?("Set[...]") - end - end - - ruby_version_is ""..."4.0" do - it "correctly handles cyclic-references" do - set1 = Set[] - set2 = Set[set1] - set1 << set2 - set1.send(@method).should.is_a?(String) - set1.send(@method).should.include?("#") - end - end -end diff --git a/spec/ruby/core/set/shared/intersection.rb b/spec/ruby/core/set/shared/intersection.rb deleted file mode 100644 index 978a4924ef86d2..00000000000000 --- a/spec/ruby/core/set/shared/intersection.rb +++ /dev/null @@ -1,15 +0,0 @@ -describe :set_intersection, shared: true do - before :each do - @set = Set[:a, :b, :c] - end - - it "returns a new Set containing only elements shared by self and the passed Enumerable" do - @set.send(@method, Set[:b, :c, :d, :e]).should == Set[:b, :c] - @set.send(@method, [:b, :c, :d]).should == Set[:b, :c] - end - - it "raises an ArgumentError when passed a non-Enumerable" do - -> { @set.send(@method, 1) }.should.raise(ArgumentError) - -> { @set.send(@method, Object.new) }.should.raise(ArgumentError) - end -end diff --git a/spec/ruby/core/set/shared/length.rb b/spec/ruby/core/set/shared/length.rb deleted file mode 100644 index a8fcee9f397dda..00000000000000 --- a/spec/ruby/core/set/shared/length.rb +++ /dev/null @@ -1,6 +0,0 @@ -describe :set_length, shared: true do - it "returns the number of elements in the set" do - set = Set[:a, :b, :c] - set.send(@method).should == 3 - end -end diff --git a/spec/ruby/core/set/shared/select.rb b/spec/ruby/core/set/shared/select.rb deleted file mode 100644 index 0d4a53fffd1c61..00000000000000 --- a/spec/ruby/core/set/shared/select.rb +++ /dev/null @@ -1,41 +0,0 @@ -require_relative '../../../spec_helper' - -describe :set_select_bang, shared: true do - before :each do - @set = Set["one", "two", "three"] - end - - it "yields every element of self" do - ret = [] - @set.send(@method) { |x| ret << x } - ret.sort.should == ["one", "two", "three"].sort - end - - it "keeps every element from self for which the passed block returns true" do - @set.send(@method) { |x| x.size != 3 } - @set.size.should.eql?(1) - - @set.should_not.include?("one") - @set.should_not.include?("two") - @set.should.include?("three") - end - - it "returns self when self was modified" do - @set.send(@method) { false }.should.equal?(@set) - end - - it "returns nil when self was not modified" do - @set.send(@method) { true }.should == nil - end - - it "returns an Enumerator when passed no block" do - enum = @set.send(@method) - enum.should.instance_of?(Enumerator) - - enum.each { |x| x.size != 3 } - - @set.should_not.include?("one") - @set.should_not.include?("two") - @set.should.include?("three") - end -end diff --git a/spec/ruby/core/set/shared/union.rb b/spec/ruby/core/set/shared/union.rb deleted file mode 100644 index dddf1716e5e3f3..00000000000000 --- a/spec/ruby/core/set/shared/union.rb +++ /dev/null @@ -1,15 +0,0 @@ -describe :set_union, shared: true do - before :each do - @set = Set[:a, :b, :c] - end - - it "returns a new Set containing all elements of self and the passed Enumerable" do - @set.send(@method, Set[:b, :d, :e]).should == Set[:a, :b, :c, :d, :e] - @set.send(@method, [:b, :e]).should == Set[:a, :b, :c, :e] - end - - it "raises an ArgumentError when passed a non-Enumerable" do - -> { @set.send(@method, 1) }.should.raise(ArgumentError) - -> { @set.send(@method, Object.new) }.should.raise(ArgumentError) - end -end diff --git a/spec/ruby/core/set/size_spec.rb b/spec/ruby/core/set/size_spec.rb index 4ae22c5f0a0991..c57272a2353aeb 100644 --- a/spec/ruby/core/set/size_spec.rb +++ b/spec/ruby/core/set/size_spec.rb @@ -1,6 +1,8 @@ require_relative '../../spec_helper' -require_relative 'shared/length' describe "Set#size" do - it_behaves_like :set_length, :size + it "returns the number of elements in the set" do + set = Set[:a, :b, :c] + set.size.should == 3 + end end diff --git a/spec/ruby/core/set/to_s_spec.rb b/spec/ruby/core/set/to_s_spec.rb index 55b8bfd9b20c6e..7f768bdcbf3b7e 100644 --- a/spec/ruby/core/set/to_s_spec.rb +++ b/spec/ruby/core/set/to_s_spec.rb @@ -1,11 +1,47 @@ require_relative "../../spec_helper" -require_relative 'shared/inspect' describe "Set#to_s" do - it_behaves_like :set_inspect, :to_s + it "returns a String representation of self" do + Set[].to_s.should.is_a?(String) + Set[nil, false, true].to_s.should.is_a?(String) + Set[1, 2, 3].to_s.should.is_a?(String) + Set["1", "2", "3"].to_s.should.is_a?(String) + Set[:a, "b", Set[?c]].to_s.should.is_a?(String) + end + + ruby_version_is "4.0" do + it "does include the elements of the set" do + Set["1"].to_s.should == 'Set["1"]' + end + end + + ruby_version_is ""..."4.0" do + it "does include the elements of the set" do + Set["1"].to_s.should == '#' + end + end + + it "puts spaces between the elements" do + Set["1", "2"].to_s.should.include?('", "') + end + + ruby_version_is "4.0" do + it "correctly handles cyclic-references" do + set1 = Set[] + set2 = Set[set1] + set1 << set2 + set1.to_s.should.is_a?(String) + set1.to_s.should.include?("Set[...]") + end + end - it "is an alias of inspect" do - set = Set.new - set.method(:to_s).should == set.method(:inspect) + ruby_version_is ""..."4.0" do + it "correctly handles cyclic-references" do + set1 = Set[] + set2 = Set[set1] + set1 << set2 + set1.to_s.should.is_a?(String) + set1.to_s.should.include?("#") + end end end diff --git a/spec/ruby/core/set/union_spec.rb b/spec/ruby/core/set/union_spec.rb index 3e77022d4b79cf..206535aae21265 100644 --- a/spec/ruby/core/set/union_spec.rb +++ b/spec/ruby/core/set/union_spec.rb @@ -1,10 +1,23 @@ require_relative '../../spec_helper' -require_relative 'shared/union' describe "Set#union" do - it_behaves_like :set_union, :union + it "is an alias of Set#|" do + Set.instance_method(:union).should == Set.instance_method(:|) + end end describe "Set#|" do - it_behaves_like :set_union, :| + before :each do + @set = Set[:a, :b, :c] + end + + it "returns a new Set containing all elements of self and the passed Enumerable" do + (@set | Set[:b, :d, :e]).should == Set[:a, :b, :c, :d, :e] + (@set | [:b, :e]).should == Set[:a, :b, :c, :e] + end + + it "raises an ArgumentError when passed a non-Enumerable" do + -> { @set | 1 }.should.raise(ArgumentError) + -> { @set | Object.new }.should.raise(ArgumentError) + end end diff --git a/spec/ruby/core/sizedqueue/deq_spec.rb b/spec/ruby/core/sizedqueue/deq_spec.rb index 2aeb52f8a64b62..51ff70655724cd 100644 --- a/spec/ruby/core/sizedqueue/deq_spec.rb +++ b/spec/ruby/core/sizedqueue/deq_spec.rb @@ -1,11 +1,7 @@ require_relative '../../spec_helper' -require_relative '../../shared/queue/deque' -require_relative '../../shared/types/rb_num2dbl_fails' describe "SizedQueue#deq" do - it_behaves_like :queue_deq, :deq, -> { SizedQueue.new(10) } -end - -describe "SizedQueue operations with timeout" do - it_behaves_like :rb_num2dbl_fails, nil, -> v { q = SizedQueue.new(10); q.push(1); q.deq(timeout: v) } + it "is an alias of SizedQueue#pop" do + SizedQueue.instance_method(:deq).should == SizedQueue.instance_method(:pop) + end end diff --git a/spec/ruby/core/sizedqueue/enq_spec.rb b/spec/ruby/core/sizedqueue/enq_spec.rb index b955909475b4ff..94697cd247e657 100644 --- a/spec/ruby/core/sizedqueue/enq_spec.rb +++ b/spec/ruby/core/sizedqueue/enq_spec.rb @@ -1,16 +1,7 @@ require_relative '../../spec_helper' -require_relative '../../shared/queue/enque' -require_relative '../../shared/sizedqueue/enque' -require_relative '../../shared/types/rb_num2dbl_fails' describe "SizedQueue#enq" do - it_behaves_like :queue_enq, :enq, -> { SizedQueue.new(10) } -end - -describe "SizedQueue#enq" do - it_behaves_like :sizedqueue_enq, :enq, -> n { SizedQueue.new(n) } -end - -describe "SizedQueue operations with timeout" do - it_behaves_like :rb_num2dbl_fails, nil, -> v { q = SizedQueue.new(1); q.enq(1, timeout: v) } + it "is an alias of SizedQueue#<<" do + SizedQueue.instance_method(:enq).should == SizedQueue.instance_method(:<<) + end end diff --git a/spec/ruby/core/sizedqueue/length_spec.rb b/spec/ruby/core/sizedqueue/length_spec.rb index b93e7f8997f22e..b9d16d89322afa 100644 --- a/spec/ruby/core/sizedqueue/length_spec.rb +++ b/spec/ruby/core/sizedqueue/length_spec.rb @@ -1,6 +1,7 @@ require_relative '../../spec_helper' -require_relative '../../shared/queue/length' describe "SizedQueue#length" do - it_behaves_like :queue_length, :length, -> { SizedQueue.new(10) } + it "is an alias of SizedQueue#size" do + SizedQueue.instance_method(:length).should == SizedQueue.instance_method(:size) + end end diff --git a/spec/ruby/core/sizedqueue/push_spec.rb b/spec/ruby/core/sizedqueue/push_spec.rb index 9eaa6beca089ba..943d0fcfb49752 100644 --- a/spec/ruby/core/sizedqueue/push_spec.rb +++ b/spec/ruby/core/sizedqueue/push_spec.rb @@ -1,16 +1,7 @@ require_relative '../../spec_helper' -require_relative '../../shared/queue/enque' -require_relative '../../shared/sizedqueue/enque' -require_relative '../../shared/types/rb_num2dbl_fails' describe "SizedQueue#push" do - it_behaves_like :queue_enq, :push, -> { SizedQueue.new(10) } -end - -describe "SizedQueue#push" do - it_behaves_like :sizedqueue_enq, :push, -> n { SizedQueue.new(n) } -end - -describe "SizedQueue operations with timeout" do - it_behaves_like :rb_num2dbl_fails, nil, -> v { q = SizedQueue.new(1); q.push(1, timeout: v) } + it "is an alias of SizedQueue#<<" do + SizedQueue.instance_method(:push).should == SizedQueue.instance_method(:<<) + end end diff --git a/spec/ruby/core/sizedqueue/shift_spec.rb b/spec/ruby/core/sizedqueue/shift_spec.rb index 52974c1d995379..f410f3f80d649a 100644 --- a/spec/ruby/core/sizedqueue/shift_spec.rb +++ b/spec/ruby/core/sizedqueue/shift_spec.rb @@ -1,11 +1,7 @@ require_relative '../../spec_helper' -require_relative '../../shared/queue/deque' -require_relative '../../shared/types/rb_num2dbl_fails' describe "SizedQueue#shift" do - it_behaves_like :queue_deq, :shift, -> { SizedQueue.new(10) } -end - -describe "SizedQueue operations with timeout" do - it_behaves_like :rb_num2dbl_fails, nil, -> v { q = SizedQueue.new(10); q.push(1); q.shift(timeout: v) } + it "is an alias of SizedQueue#pop" do + SizedQueue.instance_method(:shift).should == SizedQueue.instance_method(:pop) + end end diff --git a/spec/ruby/core/string/case_compare_spec.rb b/spec/ruby/core/string/case_compare_spec.rb index b83d1adb91438b..f98ec003bed0f0 100644 --- a/spec/ruby/core/string/case_compare_spec.rb +++ b/spec/ruby/core/string/case_compare_spec.rb @@ -1,8 +1,7 @@ require_relative '../../spec_helper' -require_relative 'shared/eql' -require_relative 'shared/equal_value' describe "String#===" do - it_behaves_like :string_eql_value, :=== - it_behaves_like :string_equal_value, :=== + it "is an alias of String#==" do + String.instance_method(:===).should == String.instance_method(:==) + end end diff --git a/spec/ruby/core/string/codepoints_spec.rb b/spec/ruby/core/string/codepoints_spec.rb index 51bd57d127badd..4434eb66a55126 100644 --- a/spec/ruby/core/string/codepoints_spec.rb +++ b/spec/ruby/core/string/codepoints_spec.rb @@ -1,7 +1,6 @@ # encoding: binary require_relative '../../spec_helper' require_relative 'shared/codepoints' -require_relative 'shared/each_codepoint_without_block' describe "String#codepoints" do it_behaves_like :string_codepoints, :codepoints diff --git a/spec/ruby/core/string/dedup_spec.rb b/spec/ruby/core/string/dedup_spec.rb index 2b31d80708276e..940f07668ec220 100644 --- a/spec/ruby/core/string/dedup_spec.rb +++ b/spec/ruby/core/string/dedup_spec.rb @@ -1,6 +1,7 @@ require_relative '../../spec_helper' -require_relative 'shared/dedup' describe 'String#dedup' do - it_behaves_like :string_dedup, :dedup + it "is an alias of String#-@" do + String.instance_method(:dedup).should == String.instance_method(:-@) + end end diff --git a/spec/ruby/core/string/each_codepoint_spec.rb b/spec/ruby/core/string/each_codepoint_spec.rb index c11cb1beaeb650..08d4292371b05b 100644 --- a/spec/ruby/core/string/each_codepoint_spec.rb +++ b/spec/ruby/core/string/each_codepoint_spec.rb @@ -1,8 +1,38 @@ +# encoding: binary require_relative '../../spec_helper' require_relative 'shared/codepoints' -require_relative 'shared/each_codepoint_without_block' describe "String#each_codepoint" do it_behaves_like :string_codepoints, :each_codepoint - it_behaves_like :string_each_codepoint_without_block, :each_codepoint + + describe "when no block is given" do + it "returns an Enumerator" do + "".each_codepoint.should.instance_of?(Enumerator) + end + + it "returns an Enumerator even when self has an invalid encoding" do + s = "\xDF".dup.force_encoding(Encoding::UTF_8) + s.valid_encoding?.should == false + s.each_codepoint.should.instance_of?(Enumerator) + end + + describe "returned Enumerator" do + describe "size" do + it "should return the size of the string" do + str = "hello" + str.each_codepoint.size.should == str.size + str = "ola" + str.each_codepoint.size.should == str.size + str = "\303\207\342\210\202\303\251\306\222g" + str.each_codepoint.size.should == str.size + end + + it "should return the size of the string even when the string has an invalid encoding" do + s = "\xDF".dup.force_encoding(Encoding::UTF_8) + s.valid_encoding?.should == false + s.each_codepoint.size.should == 1 + end + end + end + end end diff --git a/spec/ruby/core/string/equal_value_spec.rb b/spec/ruby/core/string/equal_value_spec.rb index b9c9c372f8413a..e8b706c5243db8 100644 --- a/spec/ruby/core/string/equal_value_spec.rb +++ b/spec/ruby/core/string/equal_value_spec.rb @@ -1,8 +1,30 @@ require_relative '../../spec_helper' require_relative 'shared/eql' -require_relative 'shared/equal_value' describe "String#==" do it_behaves_like :string_eql_value, :== - it_behaves_like :string_equal_value, :== + + it "returns false if obj does not respond to to_str" do + ('hello' == 5).should == false + not_supported_on :opal do + ('hello' == :hello).should == false + end + ('hello' == mock('x')).should == false + end + + it "returns obj == self if obj responds to to_str" do + obj = Object.new + + # String#== merely checks if #to_str is defined. It does + # not call it. + obj.stub!(:to_str) + + obj.should_receive(:==).and_return(true) + + ('hello' == obj).should == true + end + + it "is not fooled by NUL characters" do + ("abc\0def" == "abc\0xyz").should == false + end end diff --git a/spec/ruby/core/string/intern_spec.rb b/spec/ruby/core/string/intern_spec.rb index cd7dad435994f4..af85e56dba0de0 100644 --- a/spec/ruby/core/string/intern_spec.rb +++ b/spec/ruby/core/string/intern_spec.rb @@ -1,7 +1,7 @@ require_relative '../../spec_helper' -require_relative 'fixtures/classes' -require_relative 'shared/to_sym' describe "String#intern" do - it_behaves_like :string_to_sym, :intern + it "is an alias of String#to_sym" do + String.instance_method(:intern).should == String.instance_method(:to_sym) + end end diff --git a/spec/ruby/core/string/length_spec.rb b/spec/ruby/core/string/length_spec.rb index 98cee1f03d7e00..a723babdbc0e41 100644 --- a/spec/ruby/core/string/length_spec.rb +++ b/spec/ruby/core/string/length_spec.rb @@ -1,7 +1,56 @@ require_relative '../../spec_helper' require_relative 'fixtures/classes' -require_relative 'shared/length' describe "String#length" do - it_behaves_like :string_length, :length + it "returns the length of self" do + "".length.should == 0 + "\x00".length.should == 1 + "one".length.should == 3 + "two".length.should == 3 + "three".length.should == 5 + "four".length.should == 4 + end + + it "returns the length of a string in different encodings" do + utf8_str = 'こにちわ' * 100 + utf8_str.length.should == 400 + utf8_str.encode(Encoding::UTF_32BE).length.should == 400 + utf8_str.encode(Encoding::SHIFT_JIS).length.should == 400 + end + + it "returns the length of the new self after encoding is changed" do + str = +'こにちわ' + str.length + + str.force_encoding('BINARY').length.should == 12 + end + + it "returns the correct length after force_encoding(BINARY)" do + utf8 = "あ" + ascii = "a" + concat = utf8 + ascii + + concat.encoding.should == Encoding::UTF_8 + concat.bytesize.should == 4 + + concat.length.should == 2 + concat.force_encoding(Encoding::ASCII_8BIT) + concat.length.should == 4 + end + + it "adds 1 for every invalid byte in UTF-8" do + "\xF4\x90\x80\x80".length.should == 4 + "a\xF4\x90\x80\x80b".length.should == 6 + "é\xF4\x90\x80\x80è".length.should == 6 + end + + it "adds 1 (and not 2) for a incomplete surrogate in UTF-16" do + "\x00\xd8".dup.force_encoding("UTF-16LE").length.should == 1 + "\xd8\x00".dup.force_encoding("UTF-16BE").length.should == 1 + end + + it "adds 1 for a broken sequence in UTF-32" do + "\x04\x03\x02\x01".dup.force_encoding("UTF-32LE").length.should == 1 + "\x01\x02\x03\x04".dup.force_encoding("UTF-32BE").length.should == 1 + end end diff --git a/spec/ruby/core/string/next_spec.rb b/spec/ruby/core/string/next_spec.rb index fcd3e5ef90587d..2ab121a909cbdc 100644 --- a/spec/ruby/core/string/next_spec.rb +++ b/spec/ruby/core/string/next_spec.rb @@ -1,11 +1,13 @@ require_relative '../../spec_helper' -require_relative 'fixtures/classes' -require_relative 'shared/succ' describe "String#next" do - it_behaves_like :string_succ, :next + it "is an alias of String#succ" do + String.instance_method(:next).should == String.instance_method(:succ) + end end describe "String#next!" do - it_behaves_like :string_succ_bang, :"next!" + it "is an alias of String#succ!" do + String.instance_method(:next!).should == String.instance_method(:succ!) + end end diff --git a/spec/ruby/core/string/shared/dedup.rb b/spec/ruby/core/string/shared/dedup.rb deleted file mode 100644 index 59506c290144e2..00000000000000 --- a/spec/ruby/core/string/shared/dedup.rb +++ /dev/null @@ -1,51 +0,0 @@ -# frozen_string_literal: false -describe :string_dedup, shared: true do - it 'returns self if the String is frozen' do - input = 'foo'.freeze - output = input.send(@method) - - output.should.equal?(input) - output.should.frozen? - end - - it 'returns a frozen copy if the String is not frozen' do - input = 'foo' - output = input.send(@method) - - output.should.frozen? - output.should_not.equal?(input) - output.should == 'foo' - end - - it "returns the same object for equal unfrozen strings" do - origin = "this is a string" - dynamic = %w(this is a string).join(' ') - - origin.should_not.equal?(dynamic) - origin.send(@method).should.equal?(dynamic.send(@method)) - end - - it "returns the same object when it's called on the same String literal" do - "unfrozen string".send(@method).should.equal?("unfrozen string".send(@method)) - "unfrozen string".send(@method).should_not.equal?("another unfrozen string".send(@method)) - end - - it "deduplicates frozen strings" do - dynamic = %w(this string is frozen).join(' ').freeze - - dynamic.should_not.equal?("this string is frozen".freeze) - - dynamic.send(@method).should.equal?("this string is frozen".freeze) - dynamic.send(@method).should.equal?("this string is frozen".send(@method).freeze) - end - - it "does not deduplicate a frozen string when it has instance variables" do - dynamic = %w(this string is frozen).join(' ') - dynamic.instance_variable_set(:@a, 1) - dynamic.freeze - - dynamic.send(@method).should_not.equal?("this string is frozen".freeze) - dynamic.send(@method).should_not.equal?("this string is frozen".send(@method).freeze) - dynamic.send(@method).should.equal?(dynamic) - end -end diff --git a/spec/ruby/core/string/shared/each_codepoint_without_block.rb b/spec/ruby/core/string/shared/each_codepoint_without_block.rb deleted file mode 100644 index 60d603954c3e34..00000000000000 --- a/spec/ruby/core/string/shared/each_codepoint_without_block.rb +++ /dev/null @@ -1,33 +0,0 @@ -# encoding: binary -describe :string_each_codepoint_without_block, shared: true do - describe "when no block is given" do - it "returns an Enumerator" do - "".send(@method).should.instance_of?(Enumerator) - end - - it "returns an Enumerator even when self has an invalid encoding" do - s = "\xDF".dup.force_encoding(Encoding::UTF_8) - s.valid_encoding?.should == false - s.send(@method).should.instance_of?(Enumerator) - end - - describe "returned Enumerator" do - describe "size" do - it "should return the size of the string" do - str = "hello" - str.send(@method).size.should == str.size - str = "ola" - str.send(@method).size.should == str.size - str = "\303\207\342\210\202\303\251\306\222g" - str.send(@method).size.should == str.size - end - - it "should return the size of the string even when the string has an invalid encoding" do - s = "\xDF".dup.force_encoding(Encoding::UTF_8) - s.valid_encoding?.should == false - s.send(@method).size.should == 1 - end - end - end - end -end diff --git a/spec/ruby/core/string/shared/equal_value.rb b/spec/ruby/core/string/shared/equal_value.rb deleted file mode 100644 index dfc5c7cd29e66f..00000000000000 --- a/spec/ruby/core/string/shared/equal_value.rb +++ /dev/null @@ -1,29 +0,0 @@ -require_relative '../../../spec_helper' -require_relative '../fixtures/classes' - -describe :string_equal_value, shared: true do - it "returns false if obj does not respond to to_str" do - 'hello'.send(@method, 5).should == false - not_supported_on :opal do - 'hello'.send(@method, :hello).should == false - end - 'hello'.send(@method, mock('x')).should == false - end - - it "returns obj == self if obj responds to to_str" do - obj = Object.new - - # String#== merely checks if #to_str is defined. It does - # not call it. - obj.stub!(:to_str) - - # Don't use @method for :== in `obj.should_receive(:==)` - obj.should_receive(:==).and_return(true) - - 'hello'.send(@method, obj).should == true - end - - it "is not fooled by NUL characters" do - "abc\0def".send(@method, "abc\0xyz").should == false - end -end diff --git a/spec/ruby/core/string/shared/length.rb b/spec/ruby/core/string/shared/length.rb deleted file mode 100644 index ae572ba75562ee..00000000000000 --- a/spec/ruby/core/string/shared/length.rb +++ /dev/null @@ -1,55 +0,0 @@ -# encoding: utf-8 - -describe :string_length, shared: true do - it "returns the length of self" do - "".send(@method).should == 0 - "\x00".send(@method).should == 1 - "one".send(@method).should == 3 - "two".send(@method).should == 3 - "three".send(@method).should == 5 - "four".send(@method).should == 4 - end - - it "returns the length of a string in different encodings" do - utf8_str = 'こにちわ' * 100 - utf8_str.send(@method).should == 400 - utf8_str.encode(Encoding::UTF_32BE).send(@method).should == 400 - utf8_str.encode(Encoding::SHIFT_JIS).send(@method).should == 400 - end - - it "returns the length of the new self after encoding is changed" do - str = +'こにちわ' - str.send(@method) - - str.force_encoding('BINARY').send(@method).should == 12 - end - - it "returns the correct length after force_encoding(BINARY)" do - utf8 = "あ" - ascii = "a" - concat = utf8 + ascii - - concat.encoding.should == Encoding::UTF_8 - concat.bytesize.should == 4 - - concat.send(@method).should == 2 - concat.force_encoding(Encoding::ASCII_8BIT) - concat.send(@method).should == 4 - end - - it "adds 1 for every invalid byte in UTF-8" do - "\xF4\x90\x80\x80".send(@method).should == 4 - "a\xF4\x90\x80\x80b".send(@method).should == 6 - "é\xF4\x90\x80\x80è".send(@method).should == 6 - end - - it "adds 1 (and not 2) for a incomplete surrogate in UTF-16" do - "\x00\xd8".dup.force_encoding("UTF-16LE").send(@method).should == 1 - "\xd8\x00".dup.force_encoding("UTF-16BE").send(@method).should == 1 - end - - it "adds 1 for a broken sequence in UTF-32" do - "\x04\x03\x02\x01".dup.force_encoding("UTF-32LE").send(@method).should == 1 - "\x01\x02\x03\x04".dup.force_encoding("UTF-32BE").send(@method).should == 1 - end -end diff --git a/spec/ruby/core/string/shared/succ.rb b/spec/ruby/core/string/shared/succ.rb deleted file mode 100644 index 8f1d327741f7f4..00000000000000 --- a/spec/ruby/core/string/shared/succ.rb +++ /dev/null @@ -1,87 +0,0 @@ -# encoding: binary -describe :string_succ, shared: true do - it "returns an empty string for empty strings" do - "".send(@method).should == "" - end - - it "returns the successor by increasing the rightmost alphanumeric (digit => digit, letter => letter with same case)" do - "abcd".send(@method).should == "abce" - "THX1138".send(@method).should == "THX1139" - - "<>".send(@method).should == "<>" - "==A??".send(@method).should == "==B??" - end - - it "increases non-alphanumerics (via ascii rules) if there are no alphanumerics" do - "***".send(@method).should == "**+" - "**`".send(@method).should == "**a" - end - - it "increases the next best alphanumeric (jumping over non-alphanumerics) if there is a carry" do - "dz".send(@method).should == "ea" - "HZ".send(@method).should == "IA" - "49".send(@method).should == "50" - - "izz".send(@method).should == "jaa" - "IZZ".send(@method).should == "JAA" - "699".send(@method).should == "700" - - "6Z99z99Z".send(@method).should == "7A00a00A" - - "1999zzz".send(@method).should == "2000aaa" - "NZ/[]ZZZ9999".send(@method).should == "OA/[]AAA0000" - end - - it "increases the next best character if there is a carry for non-alphanumerics" do - "(\xFF".send(@method).should == ")\x00" - "`\xFF".send(@method).should == "a\x00" - "<\xFF\xFF".send(@method).should == "=\x00\x00" - end - - it "adds an additional character (just left to the last increased one) if there is a carry and no character left to increase" do - "z".send(@method).should == "aa" - "Z".send(@method).should == "AA" - "9".send(@method).should == "10" - - "zz".send(@method).should == "aaa" - "ZZ".send(@method).should == "AAA" - "99".send(@method).should == "100" - - "9Z99z99Z".send(@method).should == "10A00a00A" - - "ZZZ9999".send(@method).should == "AAAA0000" - "/[]9999".send(@method).should == "/[]10000" - "/[]ZZZ9999".send(@method).should == "/[]AAAA0000" - "Z/[]ZZZ9999".send(@method).should == "AA/[]AAA0000" - - # non-alphanumeric cases - "\xFF".send(@method).should == "\x01\x00" - "\xFF\xFF".send(@method).should == "\x01\x00\x00" - end - - it "returns String instances when called on a subclass" do - StringSpecs::MyString.new("").send(@method).should.instance_of?(String) - StringSpecs::MyString.new("a").send(@method).should.instance_of?(String) - StringSpecs::MyString.new("z").send(@method).should.instance_of?(String) - end - - it "returns a String in the same encoding as self" do - "z".encode("US-ASCII").send(@method).encoding.should == Encoding::US_ASCII - end -end - -describe :string_succ_bang, shared: true do - it "is equivalent to succ, but modifies self in place (still returns self)" do - ["", "abcd", "THX1138"].each do |s| - s = +s - r = s.dup.send(@method) - s.send(@method).should.equal?(s) - s.should == r - end - end - - it "raises a FrozenError if self is frozen" do - -> { "".freeze.send(@method) }.should.raise(FrozenError) - -> { "abcd".freeze.send(@method) }.should.raise(FrozenError) - end -end diff --git a/spec/ruby/core/string/shared/to_s.rb b/spec/ruby/core/string/shared/to_s.rb deleted file mode 100644 index 96c59470d6060e..00000000000000 --- a/spec/ruby/core/string/shared/to_s.rb +++ /dev/null @@ -1,13 +0,0 @@ -describe :string_to_s, shared: true do - it "returns self when self.class == String" do - a = "a string" - a.should.equal?(a.send(@method)) - end - - it "returns a new instance of String when called on a subclass" do - a = StringSpecs::MyString.new("a string") - s = a.send(@method) - s.should == "a string" - s.should.instance_of?(String) - end -end diff --git a/spec/ruby/core/string/shared/to_sym.rb b/spec/ruby/core/string/shared/to_sym.rb deleted file mode 100644 index 2a8a2e3182166d..00000000000000 --- a/spec/ruby/core/string/shared/to_sym.rb +++ /dev/null @@ -1,72 +0,0 @@ -describe :string_to_sym, shared: true do - it "returns the symbol corresponding to self" do - "Koala".send(@method).should.equal? :Koala - 'cat'.send(@method).should.equal? :cat - '@cat'.send(@method).should.equal? :@cat - 'cat and dog'.send(@method).should.equal? :"cat and dog" - "abc=".send(@method).should.equal? :abc= - end - - it "does not special case +(binary) and -(binary)" do - "+(binary)".send(@method).should.equal? :"+(binary)" - "-(binary)".send(@method).should.equal? :"-(binary)" - end - - it "does not special case certain operators" do - "!@".send(@method).should.equal? :"!@" - "~@".send(@method).should.equal? :"~@" - "!(unary)".send(@method).should.equal? :"!(unary)" - "~(unary)".send(@method).should.equal? :"~(unary)" - "+(unary)".send(@method).should.equal? :"+(unary)" - "-(unary)".send(@method).should.equal? :"-(unary)" - end - - it "returns a US-ASCII Symbol for a UTF-8 String containing only US-ASCII characters" do - sym = "foobar".send(@method) - sym.encoding.should == Encoding::US_ASCII - sym.should.equal? :"foobar" - end - - it "returns a US-ASCII Symbol for a binary String containing only US-ASCII characters" do - sym = "foobar".b.send(@method) - sym.encoding.should == Encoding::US_ASCII - sym.should.equal? :"foobar" - end - - it "returns a UTF-8 Symbol for a UTF-8 String containing non US-ASCII characters" do - sym = "il était une fois".send(@method) - sym.encoding.should == Encoding::UTF_8 - sym.should.equal? :"il était une #{'fois'}" - end - - it "returns a UTF-16LE Symbol for a UTF-16LE String containing non US-ASCII characters" do - utf16_str = "UtéF16".encode(Encoding::UTF_16LE) - sym = utf16_str.send(@method) - sym.encoding.should == Encoding::UTF_16LE - sym.to_s.should == utf16_str - end - - it "returns a binary Symbol for a binary String containing non US-ASCII characters" do - binary_string = "binarí".b - sym = binary_string.send(@method) - sym.encoding.should == Encoding::BINARY - sym.to_s.should == binary_string - end - - it "ignores existing symbols with different encoding" do - source = "fée" - - iso_symbol = source.dup.force_encoding(Encoding::ISO_8859_1).send(@method) - iso_symbol.encoding.should == Encoding::ISO_8859_1 - binary_symbol = source.dup.force_encoding(Encoding::BINARY).send(@method) - binary_symbol.encoding.should == Encoding::BINARY - end - - it "raises an EncodingError for UTF-8 String containing invalid bytes" do - invalid_utf8 = "\xC3" - invalid_utf8.should_not.valid_encoding? - -> { - invalid_utf8.send(@method) - }.should.raise(EncodingError, 'invalid symbol in encoding UTF-8 :"\xC3"') - end -end diff --git a/spec/ruby/core/string/size_spec.rb b/spec/ruby/core/string/size_spec.rb index 9e1f40c5ae9a1d..6fc81480c4906f 100644 --- a/spec/ruby/core/string/size_spec.rb +++ b/spec/ruby/core/string/size_spec.rb @@ -1,7 +1,7 @@ require_relative '../../spec_helper' -require_relative 'fixtures/classes' -require_relative 'shared/length' describe "String#size" do - it_behaves_like :string_length, :size + it "is an alias of String#length" do + String.instance_method(:size).should == String.instance_method(:length) + end end diff --git a/spec/ruby/core/string/slice_spec.rb b/spec/ruby/core/string/slice_spec.rb index 14e2251b3f7bff..16d7665bbf24c8 100644 --- a/spec/ruby/core/string/slice_spec.rb +++ b/spec/ruby/core/string/slice_spec.rb @@ -1,39 +1,12 @@ -# -*- encoding: utf-8 -*- # frozen_string_literal: false require_relative '../../spec_helper' require_relative 'fixtures/classes' require_relative 'shared/slice' describe "String#slice" do - it_behaves_like :string_slice, :slice -end - -describe "String#slice with index, length" do - it_behaves_like :string_slice_index_length, :slice -end - -describe "String#slice with Range" do - it_behaves_like :string_slice_range, :slice -end - -describe "String#slice with Regexp" do - it_behaves_like :string_slice_regexp, :slice -end - -describe "String#slice with Regexp, index" do - it_behaves_like :string_slice_regexp_index, :slice -end - -describe "String#slice with Regexp, group" do - it_behaves_like :string_slice_regexp_group, :slice -end - -describe "String#slice with String" do - it_behaves_like :string_slice_string, :slice -end - -describe "String#slice with Symbol" do - it_behaves_like :string_slice_symbol, :slice + it "is an alias of String#[]" do + String.instance_method(:slice).should == String.instance_method(:[]) + end end describe "String#slice! with index" do diff --git a/spec/ruby/core/string/succ_spec.rb b/spec/ruby/core/string/succ_spec.rb index 65047e0aa2204e..87beca8b09aa2f 100644 --- a/spec/ruby/core/string/succ_spec.rb +++ b/spec/ruby/core/string/succ_spec.rb @@ -1,11 +1,90 @@ +# encoding: binary require_relative '../../spec_helper' require_relative 'fixtures/classes' -require_relative 'shared/succ' describe "String#succ" do - it_behaves_like :string_succ, :succ + it "returns an empty string for empty strings" do + "".succ.should == "" + end + + it "returns the successor by increasing the rightmost alphanumeric (digit => digit, letter => letter with same case)" do + "abcd".succ.should == "abce" + "THX1138".succ.should == "THX1139" + + "<>".succ.should == "<>" + "==A??".succ.should == "==B??" + end + + it "increases non-alphanumerics (via ascii rules) if there are no alphanumerics" do + "***".succ.should == "**+" + "**`".succ.should == "**a" + end + + it "increases the next best alphanumeric (jumping over non-alphanumerics) if there is a carry" do + "dz".succ.should == "ea" + "HZ".succ.should == "IA" + "49".succ.should == "50" + + "izz".succ.should == "jaa" + "IZZ".succ.should == "JAA" + "699".succ.should == "700" + + "6Z99z99Z".succ.should == "7A00a00A" + + "1999zzz".succ.should == "2000aaa" + "NZ/[]ZZZ9999".succ.should == "OA/[]AAA0000" + end + + it "increases the next best character if there is a carry for non-alphanumerics" do + "(\xFF".succ.should == ")\x00" + "`\xFF".succ.should == "a\x00" + "<\xFF\xFF".succ.should == "=\x00\x00" + end + + it "adds an additional character (just left to the last increased one) if there is a carry and no character left to increase" do + "z".succ.should == "aa" + "Z".succ.should == "AA" + "9".succ.should == "10" + + "zz".succ.should == "aaa" + "ZZ".succ.should == "AAA" + "99".succ.should == "100" + + "9Z99z99Z".succ.should == "10A00a00A" + + "ZZZ9999".succ.should == "AAAA0000" + "/[]9999".succ.should == "/[]10000" + "/[]ZZZ9999".succ.should == "/[]AAAA0000" + "Z/[]ZZZ9999".succ.should == "AA/[]AAA0000" + + # non-alphanumeric cases + "\xFF".succ.should == "\x01\x00" + "\xFF\xFF".succ.should == "\x01\x00\x00" + end + + it "returns String instances when called on a subclass" do + StringSpecs::MyString.new("").succ.should.instance_of?(String) + StringSpecs::MyString.new("a").succ.should.instance_of?(String) + StringSpecs::MyString.new("z").succ.should.instance_of?(String) + end + + it "returns a String in the same encoding as self" do + "z".encode("US-ASCII").succ.encoding.should == Encoding::US_ASCII + end end describe "String#succ!" do - it_behaves_like :string_succ_bang, :"succ!" + it "is equivalent to succ, but modifies self in place (still returns self)" do + ["", "abcd", "THX1138"].each do |s| + s = +s + r = s.dup.succ! + s.succ!.should.equal?(s) + s.should == r + end + end + + it "raises a FrozenError if self is frozen" do + -> { "".freeze.succ! }.should.raise(FrozenError) + -> { "abcd".freeze.succ! }.should.raise(FrozenError) + end end diff --git a/spec/ruby/core/string/to_s_spec.rb b/spec/ruby/core/string/to_s_spec.rb index e5872745a86441..c48c7f89acae59 100644 --- a/spec/ruby/core/string/to_s_spec.rb +++ b/spec/ruby/core/string/to_s_spec.rb @@ -1,7 +1,16 @@ require_relative '../../spec_helper' require_relative 'fixtures/classes' -require_relative 'shared/to_s' describe "String#to_s" do - it_behaves_like :string_to_s, :to_s + it "returns self when self.class == String" do + a = "a string" + a.should.equal?(a.to_s) + end + + it "returns a new instance of String when called on a subclass" do + a = StringSpecs::MyString.new("a string") + s = a.to_s + s.should == "a string" + s.should.instance_of?(String) + end end diff --git a/spec/ruby/core/string/to_str_spec.rb b/spec/ruby/core/string/to_str_spec.rb index e24262a7ae4f9b..8253b3d8a30146 100644 --- a/spec/ruby/core/string/to_str_spec.rb +++ b/spec/ruby/core/string/to_str_spec.rb @@ -1,7 +1,7 @@ require_relative '../../spec_helper' -require_relative 'fixtures/classes' -require_relative 'shared/to_s' describe "String#to_str" do - it_behaves_like :string_to_s, :to_str + it "is an alias of String#to_s" do + String.instance_method(:to_str).should == String.instance_method(:to_s) + end end diff --git a/spec/ruby/core/string/to_sym_spec.rb b/spec/ruby/core/string/to_sym_spec.rb index f9135211cec2cc..f0ffe586749bc7 100644 --- a/spec/ruby/core/string/to_sym_spec.rb +++ b/spec/ruby/core/string/to_sym_spec.rb @@ -1,7 +1,74 @@ require_relative '../../spec_helper' -require_relative 'fixtures/classes' -require_relative 'shared/to_sym' describe "String#to_sym" do - it_behaves_like :string_to_sym, :to_sym + it "returns the symbol corresponding to self" do + "Koala".to_sym.should.equal? :Koala + 'cat'.to_sym.should.equal? :cat + '@cat'.to_sym.should.equal? :@cat + 'cat and dog'.to_sym.should.equal? :"cat and dog" + "abc=".to_sym.should.equal? :abc= + end + + it "does not special case +(binary) and -(binary)" do + "+(binary)".to_sym.should.equal? :"+(binary)" + "-(binary)".to_sym.should.equal? :"-(binary)" + end + + it "does not special case certain operators" do + "!@".to_sym.should.equal? :"!@" + "~@".to_sym.should.equal? :"~@" + "!(unary)".to_sym.should.equal? :"!(unary)" + "~(unary)".to_sym.should.equal? :"~(unary)" + "+(unary)".to_sym.should.equal? :"+(unary)" + "-(unary)".to_sym.should.equal? :"-(unary)" + end + + it "returns a US-ASCII Symbol for a UTF-8 String containing only US-ASCII characters" do + sym = "foobar".to_sym + sym.encoding.should == Encoding::US_ASCII + sym.should.equal? :"foobar" + end + + it "returns a US-ASCII Symbol for a binary String containing only US-ASCII characters" do + sym = "foobar".b.to_sym + sym.encoding.should == Encoding::US_ASCII + sym.should.equal? :"foobar" + end + + it "returns a UTF-8 Symbol for a UTF-8 String containing non US-ASCII characters" do + sym = "il était une fois".to_sym + sym.encoding.should == Encoding::UTF_8 + sym.should.equal? :"il était une fois" + end + + it "returns a UTF-16LE Symbol for a UTF-16LE String containing non US-ASCII characters" do + utf16_str = "UtéF16".encode(Encoding::UTF_16LE) + sym = utf16_str.to_sym + sym.encoding.should == Encoding::UTF_16LE + sym.to_s.should == utf16_str + end + + it "returns a binary Symbol for a binary String containing non US-ASCII characters" do + binary_string = "binarí".b + sym = binary_string.to_sym + sym.encoding.should == Encoding::BINARY + sym.to_s.should == binary_string + end + + it "ignores existing symbols with different encoding" do + source = "fée" + + iso_symbol = source.dup.force_encoding(Encoding::ISO_8859_1).to_sym + iso_symbol.encoding.should == Encoding::ISO_8859_1 + binary_symbol = source.dup.force_encoding(Encoding::BINARY).to_sym + binary_symbol.encoding.should == Encoding::BINARY + end + + it "raises an EncodingError for UTF-8 String containing invalid bytes" do + invalid_utf8 = "\xC3" + invalid_utf8.should_not.valid_encoding? + -> { + invalid_utf8.to_sym + }.should.raise(EncodingError, 'invalid symbol in encoding UTF-8 :"\xC3"') + end end diff --git a/spec/ruby/core/string/uminus_spec.rb b/spec/ruby/core/string/uminus_spec.rb index 46d88f6704eed3..43abf71d50a8d4 100644 --- a/spec/ruby/core/string/uminus_spec.rb +++ b/spec/ruby/core/string/uminus_spec.rb @@ -1,6 +1,53 @@ +# frozen_string_literal: false require_relative '../../spec_helper' -require_relative 'shared/dedup' describe 'String#-@' do - it_behaves_like :string_dedup, :-@ + it 'returns self if the String is frozen' do + input = 'foo'.freeze + output = -input + + output.should.equal?(input) + output.should.frozen? + end + + it 'returns a frozen copy if the String is not frozen' do + input = 'foo' + output = -input + + output.should.frozen? + output.should_not.equal?(input) + output.should == 'foo' + end + + it "returns the same object for equal unfrozen strings" do + origin = "this is a string" + dynamic = %w(this is a string).join(' ') + + origin.should_not.equal?(dynamic) + (-origin).should.equal?(-dynamic) + end + + it "returns the same object when it's called on the same String literal" do + (-"unfrozen string").should.equal?(-"unfrozen string") + (-"unfrozen string").should_not.equal?(-"another unfrozen string") + end + + it "deduplicates frozen strings" do + dynamic = %w(this string is frozen).join(' ').freeze + + dynamic.should_not.equal?("this string is frozen".freeze) + + (-dynamic).should.equal?("this string is frozen".freeze) + (-dynamic).should.equal?((-"this string is frozen").freeze) + end + + it "does not deduplicate a frozen string when it has instance variables" do + dynamic = %w(this string is frozen).join(' ') + dynamic.instance_variable_set(:@a, 1) + dynamic.freeze + + (-dynamic).should_not.equal?("this string is frozen".freeze) + (-dynamic).should_not.equal?((-"this string is frozen").freeze) + (-dynamic).should.equal?(-dynamic) + end end diff --git a/spec/ruby/core/struct/deconstruct_spec.rb b/spec/ruby/core/struct/deconstruct_spec.rb index 32d4f6bac45e4f..aad82ac2ba95a3 100644 --- a/spec/ruby/core/struct/deconstruct_spec.rb +++ b/spec/ruby/core/struct/deconstruct_spec.rb @@ -1,10 +1,7 @@ require_relative '../../spec_helper' describe "Struct#deconstruct" do - it "returns an array of attribute values" do - struct = Struct.new(:x, :y) - s = struct.new(1, 2) - - s.deconstruct.should == [1, 2] + it "is an alias of Struct#to_a" do + Struct.instance_method(:deconstruct).should == Struct.instance_method(:to_a) end end diff --git a/spec/ruby/core/struct/filter_spec.rb b/spec/ruby/core/struct/filter_spec.rb index 0ccd8ad6b244e5..5d11f47e6b63b7 100644 --- a/spec/ruby/core/struct/filter_spec.rb +++ b/spec/ruby/core/struct/filter_spec.rb @@ -1,10 +1,8 @@ require_relative '../../spec_helper' -require_relative 'shared/select' -require_relative 'shared/accessor' -require_relative '../enumerable/shared/enumeratorized' +require_relative 'fixtures/classes' describe "Struct#filter" do - it_behaves_like :struct_select, :filter - it_behaves_like :struct_accessor, :filter - it_behaves_like :enumeratorized_with_origin_size, :filter, Struct.new(:foo).new + it "is an alias of Struct#select" do + StructClasses::Car.instance_method(:filter).should == StructClasses::Car.instance_method(:select) + end end diff --git a/spec/ruby/core/struct/inspect_spec.rb b/spec/ruby/core/struct/inspect_spec.rb index 657b06abc12f9f..13fd0bef410b5b 100644 --- a/spec/ruby/core/struct/inspect_spec.rb +++ b/spec/ruby/core/struct/inspect_spec.rb @@ -1,7 +1,8 @@ require_relative '../../spec_helper' require_relative 'fixtures/classes' -require_relative 'shared/inspect' describe "Struct#inspect" do - it_behaves_like :struct_inspect, :inspect + it "is an alias of Struct#to_s" do + StructClasses::Car.instance_method(:inspect).should == StructClasses::Car.instance_method(:to_s) + end end diff --git a/spec/ruby/core/struct/length_spec.rb b/spec/ruby/core/struct/length_spec.rb index 1143676122ebdd..8d48f4118d3510 100644 --- a/spec/ruby/core/struct/length_spec.rb +++ b/spec/ruby/core/struct/length_spec.rb @@ -1,12 +1,8 @@ require_relative '../../spec_helper' require_relative 'fixtures/classes' -require_relative 'shared/accessor' describe "Struct#length" do - it "returns the number of attributes" do - StructClasses::Car.new('Cadillac', 'DeVille').length.should == 3 - StructClasses::Car.new.length.should == 3 + it "is an alias of Struct#size" do + StructClasses::Car.instance_method(:length).should == StructClasses::Car.instance_method(:size) end - - it_behaves_like :struct_accessor, :length end diff --git a/spec/ruby/core/struct/select_spec.rb b/spec/ruby/core/struct/select_spec.rb index ee846ec45f4c19..5f26a177eb5ac1 100644 --- a/spec/ruby/core/struct/select_spec.rb +++ b/spec/ruby/core/struct/select_spec.rb @@ -1,10 +1,31 @@ require_relative '../../spec_helper' -require_relative 'shared/select' +require_relative 'fixtures/classes' require_relative 'shared/accessor' require_relative '../enumerable/shared/enumeratorized' describe "Struct#select" do - it_behaves_like :struct_select, :select it_behaves_like :struct_accessor, :select it_behaves_like :enumeratorized_with_origin_size, :select, Struct.new(:foo).new + + it "raises an ArgumentError if given any non-block arguments" do + struct = StructClasses::Car.new + -> { struct.select(1) { } }.should.raise(ArgumentError) + end + + it "returns a new array of elements for which block is true" do + struct = StructClasses::Car.new("Toyota", "Tercel", "2000") + struct.select { |i| i == "2000" }.should == [ "2000" ] + end + + it "returns an instance of Array" do + struct = StructClasses::Car.new("Ford", "Escort", "1995") + struct.select { true }.should.instance_of?(Array) + end + + describe "without block" do + it "returns an instance of Enumerator" do + struct = Struct.new(:foo).new + struct.select.should.instance_of?(Enumerator) + end + end end diff --git a/spec/ruby/core/struct/shared/inspect.rb b/spec/ruby/core/struct/shared/inspect.rb deleted file mode 100644 index 1a0fb6a6b2d829..00000000000000 --- a/spec/ruby/core/struct/shared/inspect.rb +++ /dev/null @@ -1,40 +0,0 @@ -describe :struct_inspect, shared: true do - it "returns a string representation showing members and values" do - car = StructClasses::Car.new('Ford', 'Ranger') - car.send(@method).should == '#' - end - - it "returns a string representation without the class name for anonymous structs" do - Struct.new(:a).new("").send(@method).should == '#' - end - - it "returns a string representation without the class name for structs nested in anonymous classes" do - c = Class.new - c.class_eval <<~DOC - class Foo < Struct.new(:a); end - DOC - - c::Foo.new("").send(@method).should == '#' - end - - it "returns a string representation without the class name for structs nested in anonymous modules" do - m = Module.new - m.module_eval <<~DOC - class Foo < Struct.new(:a); end - DOC - - m::Foo.new("").send(@method).should == '#' - end - - it "does not call #name method" do - struct = StructClasses::StructWithOverriddenName.new("") - struct.send(@method).should == '#' - end - - it "does not call #name method when struct is anonymous" do - struct = Struct.new(:a) - def struct.name; "A"; end - - struct.new("").send(@method).should == '#' - end -end diff --git a/spec/ruby/core/struct/shared/select.rb b/spec/ruby/core/struct/shared/select.rb deleted file mode 100644 index dfa1a809fe774c..00000000000000 --- a/spec/ruby/core/struct/shared/select.rb +++ /dev/null @@ -1,26 +0,0 @@ -require_relative '../../../spec_helper' -require_relative '../fixtures/classes' - -describe :struct_select, shared: true do - it "raises an ArgumentError if given any non-block arguments" do - struct = StructClasses::Car.new - -> { struct.send(@method, 1) { } }.should.raise(ArgumentError) - end - - it "returns a new array of elements for which block is true" do - struct = StructClasses::Car.new("Toyota", "Tercel", "2000") - struct.send(@method) { |i| i == "2000" }.should == [ "2000" ] - end - - it "returns an instance of Array" do - struct = StructClasses::Car.new("Ford", "Escort", "1995") - struct.send(@method) { true }.should.instance_of?(Array) - end - - describe "without block" do - it "returns an instance of Enumerator" do - struct = Struct.new(:foo).new - struct.send(@method).should.instance_of?(Enumerator) - end - end -end diff --git a/spec/ruby/core/struct/size_spec.rb b/spec/ruby/core/struct/size_spec.rb index 09f260cf2061e3..5f07320bb9426c 100644 --- a/spec/ruby/core/struct/size_spec.rb +++ b/spec/ruby/core/struct/size_spec.rb @@ -3,8 +3,9 @@ require_relative 'shared/accessor' describe "Struct#size" do - it "is a synonym for length" do - StructClasses::Car.new.size.should == StructClasses::Car.new.length + it "returns the number of attributes" do + StructClasses::Car.new('Cadillac', 'DeVille').length.should == 3 + StructClasses::Car.new.length.should == 3 end it_behaves_like :struct_accessor, :size diff --git a/spec/ruby/core/struct/to_s_spec.rb b/spec/ruby/core/struct/to_s_spec.rb index 94c672d3d5b78d..9648b8af9bafb9 100644 --- a/spec/ruby/core/struct/to_s_spec.rb +++ b/spec/ruby/core/struct/to_s_spec.rb @@ -1,12 +1,43 @@ require_relative '../../spec_helper' require_relative 'fixtures/classes' -require_relative 'shared/inspect' describe "Struct#to_s" do - it "is a synonym for inspect" do + it "returns a string representation showing members and values" do car = StructClasses::Car.new('Ford', 'Ranger') - car.inspect.should == car.to_s + car.to_s.should == '#' end - it_behaves_like :struct_inspect, :to_s + it "returns a string representation without the class name for anonymous structs" do + Struct.new(:a).new("").to_s.should == '#' + end + + it "returns a string representation without the class name for structs nested in anonymous classes" do + c = Class.new + c.class_eval <<~DOC + class Foo < Struct.new(:a); end + DOC + + c::Foo.new("").to_s.should == '#' + end + + it "returns a string representation without the class name for structs nested in anonymous modules" do + m = Module.new + m.module_eval <<~DOC + class Foo < Struct.new(:a); end + DOC + + m::Foo.new("").to_s.should == '#' + end + + it "does not call #name method" do + struct = StructClasses::StructWithOverriddenName.new("") + struct.to_s.should == '#' + end + + it "does not call #name method when struct is anonymous" do + struct = Struct.new(:a) + def struct.name; "A"; end + + struct.new("").to_s.should == '#' + end end diff --git a/spec/ruby/core/struct/values_spec.rb b/spec/ruby/core/struct/values_spec.rb index b2d11725b9d5c6..16583253d7cc65 100644 --- a/spec/ruby/core/struct/values_spec.rb +++ b/spec/ruby/core/struct/values_spec.rb @@ -2,10 +2,7 @@ require_relative 'fixtures/classes' describe "Struct#values" do - it "is a synonym for to_a" do - car = StructClasses::Car.new('Nissan', 'Maxima') - car.values.should == car.to_a - - StructClasses::Car.new.values.should == StructClasses::Car.new.to_a + it "is an alias of Struct#to_a" do + StructClasses::Car.instance_method(:values).should == StructClasses::Car.instance_method(:to_a) end end diff --git a/spec/ruby/core/symbol/case_compare_spec.rb b/spec/ruby/core/symbol/case_compare_spec.rb index 0c6bc1eda5874a..a296132c04cfab 100644 --- a/spec/ruby/core/symbol/case_compare_spec.rb +++ b/spec/ruby/core/symbol/case_compare_spec.rb @@ -1,11 +1,7 @@ require_relative '../../spec_helper' describe "Symbol#===" do - it "returns true when the argument is a Symbol" do - (Symbol === :ruby).should == true - end - - it "returns false when the argument is a String" do - (Symbol === 'ruby').should == false + it "is an alias of Symbol#==" do + Symbol.instance_method(:===).should == Symbol.instance_method(:==) end end diff --git a/spec/ruby/core/symbol/element_reference_spec.rb b/spec/ruby/core/symbol/element_reference_spec.rb index df6bc15ddb6e7b..360a661891b6ed 100644 --- a/spec/ruby/core/symbol/element_reference_spec.rb +++ b/spec/ruby/core/symbol/element_reference_spec.rb @@ -1,6 +1,263 @@ require_relative '../../spec_helper' -require_relative 'shared/slice' +require_relative 'fixtures/classes' describe "Symbol#[]" do - it_behaves_like :symbol_slice, :[] + describe "with an Integer index" do + it "returns the character code of the element at the index" do + :symbol[1].should == ?y + end + + it "returns nil if the index starts from the end and is greater than the length" do + :symbol[-10].should == nil + end + + it "returns nil if the index is greater than the length" do + :symbol[42].should == nil + end + end + + describe "with an Integer index and length" do + describe "and a positive index and length" do + it "returns a slice" do + :symbol[1, 3].should == "ymb" + end + + it "returns a blank slice if the length is 0" do + :symbol[0, 0].should == "" + :symbol[1, 0].should == "" + end + + it "returns a slice of all remaining characters if the given length is greater than the actual length" do + :symbol[1, 100].should == "ymbol" + end + + it "returns nil if the index is greater than the length" do + :symbol[10, 1].should == nil + end + end + + describe "and a positive index and negative length" do + it "returns nil" do + :symbol[0, -1].should == nil + :symbol[1, -1].should == nil + end + end + + describe "and a negative index and positive length" do + it "returns a slice starting from the end upto the length" do + :symbol[-3, 2].should == "bo" + end + + it "returns a blank slice if the length is 0" do + :symbol[-1, 0].should == "" + end + + it "returns a slice of all remaining characters if the given length is larger than the actual length" do + :symbol[-4, 100].should == "mbol" + end + + it "returns nil if the index is past the start" do + :symbol[-10, 1].should == nil + end + end + + describe "and a negative index and negative length" do + it "returns nil" do + :symbol[-1, -1].should == nil + end + end + + describe "and a Float length" do + it "converts the length to an Integer" do + :symbol[2, 2.5].should == "mb" + end + end + + describe "and a nil length" do + it "raises a TypeError" do + -> { :symbol[1, nil] }.should.raise(TypeError) + end + end + + describe "and a length that cannot be converted into an Integer" do + it "raises a TypeError when given an Array" do + -> { :symbol[1, Array.new] }.should.raise(TypeError) + end + + it "raises a TypeError when given an Hash" do + -> { :symbol[1, Hash.new] }.should.raise(TypeError) + end + + it "raises a TypeError when given an Object" do + -> { :symbol[1, Object.new] }.should.raise(TypeError) + end + end + end + + describe "with a Float index" do + it "converts the index to an Integer" do + :symbol[1.5].should == ?y + end + end + + describe "with a nil index" do + it "raises a TypeError" do + -> { :symbol[nil] }.should.raise(TypeError) + end + end + + describe "with an index that cannot be converted into an Integer" do + it "raises a TypeError when given an Array" do + -> { :symbol[Array.new] }.should.raise(TypeError) + end + + it "raises a TypeError when given an Hash" do + -> { :symbol[Hash.new] }.should.raise(TypeError) + end + + it "raises a TypeError when given an Object" do + -> { :symbol[Object.new] }.should.raise(TypeError) + end + end + + describe "with a Range slice" do + describe "that is within bounds" do + it "returns a slice if both range values begin at the start and are within bounds" do + :symbol[1..4].should == "ymbo" + end + + it "returns a slice if the first range value begins at the start and the last begins at the end" do + :symbol[1..-1].should == "ymbol" + end + + it "returns a slice if the first range value begins at the end and the last begins at the end" do + :symbol[-4..-1].should == "mbol" + end + end + + describe "that is out of bounds" do + it "returns nil if the first range value begins past the end" do + :symbol[10..12].should == nil + end + + it "returns a blank string if the first range value is within bounds and the last range value is not" do + :symbol[-2..-10].should == "" + :symbol[2..-10].should == "" + end + + it "returns nil if the first range value starts from the end and is within bounds and the last value starts from the end and is greater than the length" do + :symbol[-10..-12].should == nil + end + + it "returns nil if the first range value starts from the end and is out of bounds and the last value starts from the end and is less than the length" do + :symbol[-10..-2].should == nil + end + end + + describe "with Float values" do + it "converts the first value to an Integer" do + :symbol[0.5..2].should == "sym" + end + + it "converts the last value to an Integer" do + :symbol[0..2.5].should == "sym" + end + end + end + + describe "with a Range subclass slice" do + it "returns a slice" do + range = SymbolSpecs::MyRange.new(1, 4) + :symbol[range].should == "ymbo" + end + end + + describe "with a Regex slice" do + describe "without a capture index" do + it "returns a string of the match" do + :symbol[/[^bol]+/].should == "sym" + end + + it "returns nil if the expression does not match" do + :symbol[/0-9/].should == nil + end + + it "sets $~ to the MatchData if there is a match" do + :symbol[/[^bol]+/] + $~[0].should == "sym" + end + + it "does not set $~ if there if there is not a match" do + :symbol[/[0-9]+/] + $~.should == nil + end + end + + describe "with a capture index" do + it "returns a string of the complete match if the capture index is 0" do + :symbol[/(sy)(mb)(ol)/, 0].should == "symbol" + end + + it "returns a string for the matched capture at the given index" do + :symbol[/(sy)(mb)(ol)/, 1].should == "sy" + :symbol[/(sy)(mb)(ol)/, -1].should == "ol" + end + + it "returns nil if there is no capture for the index" do + :symbol[/(sy)(mb)(ol)/, 4].should == nil + :symbol[/(sy)(mb)(ol)/, -4].should == nil + end + + it "converts the index to an Integer" do + :symbol[/(sy)(mb)(ol)/, 1.5].should == "sy" + end + + describe "and an index that cannot be converted to an Integer" do + it "raises a TypeError when given an Hash" do + -> { :symbol[/(sy)(mb)(ol)/, Hash.new] }.should.raise(TypeError) + end + + it "raises a TypeError when given an Array" do + -> { :symbol[/(sy)(mb)(ol)/, Array.new] }.should.raise(TypeError) + end + + it "raises a TypeError when given an Object" do + -> { :symbol[/(sy)(mb)(ol)/, Object.new] }.should.raise(TypeError) + end + end + + it "raises a TypeError if the index is nil" do + -> { :symbol[/(sy)(mb)(ol)/, nil] }.should.raise(TypeError) + end + + it "sets $~ to the MatchData if there is a match" do + :symbol[/(sy)(mb)(ol)/, 0] + $~[0].should == "symbol" + $~[1].should == "sy" + $~[2].should == "mb" + $~[3].should == "ol" + end + + it "does not set $~ to the MatchData if there is not a match" do + :symbol[/0-9/, 0] + $~.should == nil + end + end + end + + describe "with a String slice" do + it "does not set $~" do + $~ = nil + :symbol["sym"] + $~.should == nil + end + + it "returns a string if there is match" do + :symbol["ymb"].should == "ymb" + end + + it "returns nil if there is not a match" do + :symbol["foo"].should == nil + end + end end diff --git a/spec/ruby/core/symbol/id2name_spec.rb b/spec/ruby/core/symbol/id2name_spec.rb index 2caa89fc37882f..abcbff65f46305 100644 --- a/spec/ruby/core/symbol/id2name_spec.rb +++ b/spec/ruby/core/symbol/id2name_spec.rb @@ -1,6 +1,7 @@ require_relative '../../spec_helper' -require_relative 'shared/id2name' describe "Symbol#id2name" do - it_behaves_like :symbol_id2name, :id2name + it "is an alias of Symbol#to_s" do + Symbol.instance_method(:id2name).should == Symbol.instance_method(:to_s) + end end diff --git a/spec/ruby/core/symbol/intern_spec.rb b/spec/ruby/core/symbol/intern_spec.rb index 9d0914c7fbe802..746d313d4001ee 100644 --- a/spec/ruby/core/symbol/intern_spec.rb +++ b/spec/ruby/core/symbol/intern_spec.rb @@ -1,11 +1,7 @@ require_relative '../../spec_helper' describe "Symbol#intern" do - it "returns self" do - :foo.intern.should == :foo - end - - it "returns a Symbol" do - :foo.intern.should.is_a?(Symbol) + it "is an alias of Symbol#to_sym" do + Symbol.instance_method(:intern).should == Symbol.instance_method(:to_sym) end end diff --git a/spec/ruby/core/symbol/length_spec.rb b/spec/ruby/core/symbol/length_spec.rb index 27bee575ef3412..29dd4ad46b4919 100644 --- a/spec/ruby/core/symbol/length_spec.rb +++ b/spec/ruby/core/symbol/length_spec.rb @@ -1,6 +1,23 @@ require_relative '../../spec_helper' -require_relative 'shared/length' describe "Symbol#length" do - it_behaves_like :symbol_length, :length + it "returns 0 for empty name" do + :''.length.should == 0 + end + + it "returns 1 for name formed by a NUL character" do + :"\x00".length.should == 1 + end + + it "returns 3 for name formed by 3 ASCII characters" do + :one.length.should == 3 + end + + it "returns 4 for name formed by 4 ASCII characters" do + :four.length.should == 4 + end + + it "returns 4 for name formed by 1 multibyte and 3 ASCII characters" do + :"\xC3\x9Cber".length.should == 4 + end end diff --git a/spec/ruby/core/symbol/next_spec.rb b/spec/ruby/core/symbol/next_spec.rb index 97fe913739f0a1..e80bbaa508139e 100644 --- a/spec/ruby/core/symbol/next_spec.rb +++ b/spec/ruby/core/symbol/next_spec.rb @@ -1,6 +1,7 @@ require_relative '../../spec_helper' -require_relative 'shared/succ' describe "Symbol#next" do - it_behaves_like :symbol_succ, :next + it "is an alias of Symbol#succ" do + Symbol.instance_method(:next).should == Symbol.instance_method(:succ) + end end diff --git a/spec/ruby/core/symbol/shared/id2name.rb b/spec/ruby/core/symbol/shared/id2name.rb deleted file mode 100644 index 00a9c7d7dc69a6..00000000000000 --- a/spec/ruby/core/symbol/shared/id2name.rb +++ /dev/null @@ -1,30 +0,0 @@ -describe :symbol_id2name, shared: true do - it "returns the string corresponding to self" do - :rubinius.send(@method).should == "rubinius" - :squash.send(@method).should == "squash" - :[].send(@method).should == "[]" - :@ruby.send(@method).should == "@ruby" - :@@ruby.send(@method).should == "@@ruby" - end - - it "returns a String in the same encoding as self" do - string = "ruby".encode("US-ASCII") - symbol = string.to_sym - - symbol.send(@method).encoding.should == Encoding::US_ASCII - end - - ruby_version_is "3.4" do - it "warns about mutating returned string" do - -> { :bad!.send(@method).upcase! }.should complain(/warning: string returned by :bad!.to_s will be frozen in the future/) - end - - it "does not warn about mutation when Warning[:deprecated] is false" do - deprecated = Warning[:deprecated] - Warning[:deprecated] = false - -> { :bad!.send(@method).upcase! }.should_not complain - ensure - Warning[:deprecated] = deprecated - end - end -end diff --git a/spec/ruby/core/symbol/shared/length.rb b/spec/ruby/core/symbol/shared/length.rb deleted file mode 100644 index 692e8c57e3426d..00000000000000 --- a/spec/ruby/core/symbol/shared/length.rb +++ /dev/null @@ -1,23 +0,0 @@ -# -*- encoding: utf-8 -*- - -describe :symbol_length, shared: true do - it "returns 0 for empty name" do - :''.send(@method).should == 0 - end - - it "returns 1 for name formed by a NUL character" do - :"\x00".send(@method).should == 1 - end - - it "returns 3 for name formed by 3 ASCII characters" do - :one.send(@method).should == 3 - end - - it "returns 4 for name formed by 4 ASCII characters" do - :four.send(@method).should == 4 - end - - it "returns 4 for name formed by 1 multibyte and 3 ASCII characters" do - :"\xC3\x9Cber".send(@method).should == 4 - end -end diff --git a/spec/ruby/core/symbol/shared/slice.rb b/spec/ruby/core/symbol/shared/slice.rb deleted file mode 100644 index 4e3a35240cc4d0..00000000000000 --- a/spec/ruby/core/symbol/shared/slice.rb +++ /dev/null @@ -1,262 +0,0 @@ -require_relative '../fixtures/classes' - -describe :symbol_slice, shared: true do - describe "with an Integer index" do - it "returns the character code of the element at the index" do - :symbol.send(@method, 1).should == ?y - end - - it "returns nil if the index starts from the end and is greater than the length" do - :symbol.send(@method, -10).should == nil - end - - it "returns nil if the index is greater than the length" do - :symbol.send(@method, 42).should == nil - end - end - - describe "with an Integer index and length" do - describe "and a positive index and length" do - it "returns a slice" do - :symbol.send(@method, 1,3).should == "ymb" - end - - it "returns a blank slice if the length is 0" do - :symbol.send(@method, 0,0).should == "" - :symbol.send(@method, 1,0).should == "" - end - - it "returns a slice of all remaining characters if the given length is greater than the actual length" do - :symbol.send(@method, 1,100).should == "ymbol" - end - - it "returns nil if the index is greater than the length" do - :symbol.send(@method, 10,1).should == nil - end - end - - describe "and a positive index and negative length" do - it "returns nil" do - :symbol.send(@method, 0,-1).should == nil - :symbol.send(@method, 1,-1).should == nil - end - end - - describe "and a negative index and positive length" do - it "returns a slice starting from the end upto the length" do - :symbol.send(@method, -3,2).should == "bo" - end - - it "returns a blank slice if the length is 0" do - :symbol.send(@method, -1,0).should == "" - end - - it "returns a slice of all remaining characters if the given length is larger than the actual length" do - :symbol.send(@method, -4,100).should == "mbol" - end - - it "returns nil if the index is past the start" do - :symbol.send(@method, -10,1).should == nil - end - end - - describe "and a negative index and negative length" do - it "returns nil" do - :symbol.send(@method, -1,-1).should == nil - end - end - - describe "and a Float length" do - it "converts the length to an Integer" do - :symbol.send(@method, 2,2.5).should == "mb" - end - end - - describe "and a nil length" do - it "raises a TypeError" do - -> { :symbol.send(@method, 1,nil) }.should.raise(TypeError) - end - end - - describe "and a length that cannot be converted into an Integer" do - it "raises a TypeError when given an Array" do - -> { :symbol.send(@method, 1,Array.new) }.should.raise(TypeError) - end - - it "raises a TypeError when given an Hash" do - -> { :symbol.send(@method, 1,Hash.new) }.should.raise(TypeError) - end - - it "raises a TypeError when given an Object" do - -> { :symbol.send(@method, 1,Object.new) }.should.raise(TypeError) - end - end - end - - describe "with a Float index" do - it "converts the index to an Integer" do - :symbol.send(@method, 1.5).should == ?y - end - end - - describe "with a nil index" do - it "raises a TypeError" do - -> { :symbol.send(@method, nil) }.should.raise(TypeError) - end - end - - describe "with an index that cannot be converted into an Integer" do - it "raises a TypeError when given an Array" do - -> { :symbol.send(@method, Array.new) }.should.raise(TypeError) - end - - it "raises a TypeError when given an Hash" do - -> { :symbol.send(@method, Hash.new) }.should.raise(TypeError) - end - - it "raises a TypeError when given an Object" do - -> { :symbol.send(@method, Object.new) }.should.raise(TypeError) - end - end - - describe "with a Range slice" do - describe "that is within bounds" do - it "returns a slice if both range values begin at the start and are within bounds" do - :symbol.send(@method, 1..4).should == "ymbo" - end - - it "returns a slice if the first range value begins at the start and the last begins at the end" do - :symbol.send(@method, 1..-1).should == "ymbol" - end - - it "returns a slice if the first range value begins at the end and the last begins at the end" do - :symbol.send(@method, -4..-1).should == "mbol" - end - end - - describe "that is out of bounds" do - it "returns nil if the first range value begins past the end" do - :symbol.send(@method, 10..12).should == nil - end - - it "returns a blank string if the first range value is within bounds and the last range value is not" do - :symbol.send(@method, -2..-10).should == "" - :symbol.send(@method, 2..-10).should == "" - end - - it "returns nil if the first range value starts from the end and is within bounds and the last value starts from the end and is greater than the length" do - :symbol.send(@method, -10..-12).should == nil - end - - it "returns nil if the first range value starts from the end and is out of bounds and the last value starts from the end and is less than the length" do - :symbol.send(@method, -10..-2).should == nil - end - end - - describe "with Float values" do - it "converts the first value to an Integer" do - :symbol.send(@method, 0.5..2).should == "sym" - end - - it "converts the last value to an Integer" do - :symbol.send(@method, 0..2.5).should == "sym" - end - end - end - - describe "with a Range subclass slice" do - it "returns a slice" do - range = SymbolSpecs::MyRange.new(1, 4) - :symbol.send(@method, range).should == "ymbo" - end - end - - describe "with a Regex slice" do - describe "without a capture index" do - it "returns a string of the match" do - :symbol.send(@method, /[^bol]+/).should == "sym" - end - - it "returns nil if the expression does not match" do - :symbol.send(@method, /0-9/).should == nil - end - - it "sets $~ to the MatchData if there is a match" do - :symbol.send(@method, /[^bol]+/) - $~[0].should == "sym" - end - - it "does not set $~ if there if there is not a match" do - :symbol.send(@method, /[0-9]+/) - $~.should == nil - end - end - - describe "with a capture index" do - it "returns a string of the complete match if the capture index is 0" do - :symbol.send(@method, /(sy)(mb)(ol)/, 0).should == "symbol" - end - - it "returns a string for the matched capture at the given index" do - :symbol.send(@method, /(sy)(mb)(ol)/, 1).should == "sy" - :symbol.send(@method, /(sy)(mb)(ol)/, -1).should == "ol" - end - - it "returns nil if there is no capture for the index" do - :symbol.send(@method, /(sy)(mb)(ol)/, 4).should == nil - :symbol.send(@method, /(sy)(mb)(ol)/, -4).should == nil - end - - it "converts the index to an Integer" do - :symbol.send(@method, /(sy)(mb)(ol)/, 1.5).should == "sy" - end - - describe "and an index that cannot be converted to an Integer" do - it "raises a TypeError when given an Hash" do - -> { :symbol.send(@method, /(sy)(mb)(ol)/, Hash.new) }.should.raise(TypeError) - end - - it "raises a TypeError when given an Array" do - -> { :symbol.send(@method, /(sy)(mb)(ol)/, Array.new) }.should.raise(TypeError) - end - - it "raises a TypeError when given an Object" do - -> { :symbol.send(@method, /(sy)(mb)(ol)/, Object.new) }.should.raise(TypeError) - end - end - - it "raises a TypeError if the index is nil" do - -> { :symbol.send(@method, /(sy)(mb)(ol)/, nil) }.should.raise(TypeError) - end - - it "sets $~ to the MatchData if there is a match" do - :symbol.send(@method, /(sy)(mb)(ol)/, 0) - $~[0].should == "symbol" - $~[1].should == "sy" - $~[2].should == "mb" - $~[3].should == "ol" - end - - it "does not set $~ to the MatchData if there is not a match" do - :symbol.send(@method, /0-9/, 0) - $~.should == nil - end - end - end - - describe "with a String slice" do - it "does not set $~" do - $~ = nil - :symbol.send(@method, "sym") - $~.should == nil - end - - it "returns a string if there is match" do - :symbol.send(@method, "ymb").should == "ymb" - end - - it "returns nil if there is not a match" do - :symbol.send(@method, "foo").should == nil - end - end -end diff --git a/spec/ruby/core/symbol/shared/succ.rb b/spec/ruby/core/symbol/shared/succ.rb deleted file mode 100644 index dde298207ecb2a..00000000000000 --- a/spec/ruby/core/symbol/shared/succ.rb +++ /dev/null @@ -1,18 +0,0 @@ -require_relative '../../../spec_helper' - -describe :symbol_succ, shared: true do - it "returns a successor" do - :abcd.send(@method).should == :abce - :THX1138.send(@method).should == :THX1139 - end - - it "propagates a 'carry'" do - :"1999zzz".send(@method).should == :"2000aaa" - :ZZZ9999.send(@method).should == :AAAA0000 - end - - it "increments non-alphanumeric characters when no alphanumeric characters are present" do - :"<>".send(@method).should == :"<>" - :"***".send(@method).should == :"**+" - end -end diff --git a/spec/ruby/core/symbol/size_spec.rb b/spec/ruby/core/symbol/size_spec.rb index 5e2aa8d4d2125c..b5d375b3aaaa75 100644 --- a/spec/ruby/core/symbol/size_spec.rb +++ b/spec/ruby/core/symbol/size_spec.rb @@ -1,6 +1,7 @@ require_relative '../../spec_helper' -require_relative 'shared/length' describe "Symbol#size" do - it_behaves_like :symbol_length, :size + it "is an alias of Symbol#length" do + Symbol.instance_method(:size).should == Symbol.instance_method(:length) + end end diff --git a/spec/ruby/core/symbol/slice_spec.rb b/spec/ruby/core/symbol/slice_spec.rb index d2421c474cb57a..050a2cf38c1368 100644 --- a/spec/ruby/core/symbol/slice_spec.rb +++ b/spec/ruby/core/symbol/slice_spec.rb @@ -1,6 +1,7 @@ require_relative '../../spec_helper' -require_relative 'shared/slice' describe "Symbol#slice" do - it_behaves_like :symbol_slice, :slice + it "is an alias of Symbol#[]" do + Symbol.instance_method(:slice).should == Symbol.instance_method(:[]) + end end diff --git a/spec/ruby/core/symbol/succ_spec.rb b/spec/ruby/core/symbol/succ_spec.rb index 694bfff862fc3e..893164a2a614b6 100644 --- a/spec/ruby/core/symbol/succ_spec.rb +++ b/spec/ruby/core/symbol/succ_spec.rb @@ -1,6 +1,18 @@ require_relative '../../spec_helper' -require_relative 'shared/succ' describe "Symbol#succ" do - it_behaves_like :symbol_succ, :succ + it "returns a successor" do + :abcd.succ.should == :abce + :THX1138.succ.should == :THX1139 + end + + it "propagates a 'carry'" do + :"1999zzz".succ.should == :"2000aaa" + :ZZZ9999.succ.should == :AAAA0000 + end + + it "increments non-alphanumeric characters when no alphanumeric characters are present" do + :"<>".succ.should == :"<>" + :"***".succ.should == :"**+" + end end diff --git a/spec/ruby/core/symbol/to_s_spec.rb b/spec/ruby/core/symbol/to_s_spec.rb index cd963faa287baa..2cb57c4cbcf499 100644 --- a/spec/ruby/core/symbol/to_s_spec.rb +++ b/spec/ruby/core/symbol/to_s_spec.rb @@ -1,6 +1,32 @@ require_relative '../../spec_helper' -require_relative 'shared/id2name' describe "Symbol#to_s" do - it_behaves_like :symbol_id2name, :to_s + it "returns the string corresponding to self" do + :rubinius.to_s.should == "rubinius" + :squash.to_s.should == "squash" + :[].to_s.should == "[]" + :@ruby.to_s.should == "@ruby" + :@@ruby.to_s.should == "@@ruby" + end + + it "returns a String in the same encoding as self" do + string = "ruby".encode("US-ASCII") + symbol = string.to_sym + + symbol.to_s.encoding.should == Encoding::US_ASCII + end + + ruby_version_is "3.4" do + it "warns about mutating returned string" do + -> { :bad!.to_s.upcase! }.should complain(/warning: string returned by :bad!.to_s will be frozen in the future/) + end + + it "does not warn about mutation when Warning[:deprecated] is false" do + deprecated = Warning[:deprecated] + Warning[:deprecated] = false + -> { :bad!.to_s.upcase! }.should_not complain + ensure + Warning[:deprecated] = deprecated + end + end end diff --git a/spec/ruby/core/symbol/to_sym_spec.rb b/spec/ruby/core/symbol/to_sym_spec.rb index e75f3d48a85958..062daa3fc72793 100644 --- a/spec/ruby/core/symbol/to_sym_spec.rb +++ b/spec/ruby/core/symbol/to_sym_spec.rb @@ -3,7 +3,7 @@ describe "Symbol#to_sym" do it "returns self" do [:rubinius, :squash, :[], :@ruby, :@@ruby].each do |sym| - sym.to_sym.should == sym + sym.to_sym.should.equal?(sym) end end end diff --git a/spec/ruby/core/thread/exit_spec.rb b/spec/ruby/core/thread/exit_spec.rb index b2e923c680c10a..dbcd889898953e 100644 --- a/spec/ruby/core/thread/exit_spec.rb +++ b/spec/ruby/core/thread/exit_spec.rb @@ -1,6 +1,11 @@ require_relative '../../spec_helper' require_relative 'fixtures/classes' -require_relative 'shared/exit' + +describe "Thread#exit" do + it "is an alias of Thread#kill" do + Thread.instance_method(:exit).should == Thread.instance_method(:kill) + end +end describe "Thread#exit!" do it "needs to be reviewed for spec completeness" diff --git a/spec/ruby/core/thread/fork_spec.rb b/spec/ruby/core/thread/fork_spec.rb index a2f41812989900..60d574a1853716 100644 --- a/spec/ruby/core/thread/fork_spec.rb +++ b/spec/ruby/core/thread/fork_spec.rb @@ -1,9 +1,7 @@ require_relative '../../spec_helper' -require_relative 'fixtures/classes' -require_relative 'shared/start' describe "Thread.fork" do - describe "Thread.start" do - it_behaves_like :thread_start, :fork + it "is an alias of Thread.start" do + Thread.method(:fork).should == Thread.method(:start) end end diff --git a/spec/ruby/core/thread/inspect_spec.rb b/spec/ruby/core/thread/inspect_spec.rb index bd6e0c31fcc8f0..fee401830af26e 100644 --- a/spec/ruby/core/thread/inspect_spec.rb +++ b/spec/ruby/core/thread/inspect_spec.rb @@ -1,6 +1,7 @@ require_relative '../../spec_helper' -require_relative 'shared/to_s' describe "Thread#inspect" do - it_behaves_like :thread_to_s, :inspect + it "is an alias of Thread#to_s" do + Thread.instance_method(:inspect).should == Thread.instance_method(:to_s) + end end diff --git a/spec/ruby/core/thread/kill_spec.rb b/spec/ruby/core/thread/kill_spec.rb index f9f1f467444400..907cc5c4d80087 100644 --- a/spec/ruby/core/thread/kill_spec.rb +++ b/spec/ruby/core/thread/kill_spec.rb @@ -1,12 +1,221 @@ require_relative '../../spec_helper' require_relative 'fixtures/classes' -require_relative 'shared/exit' # This spec randomly kills mspec worker like: https://ci.appveyor.com/project/ruby/ruby/builds/19473223/job/f69derxnlo09xhuj # TODO: Investigate the cause or at least print helpful logs, and remove this `platform_is_not` guard. platform_is_not :mingw do describe "Thread#kill" do - it_behaves_like :thread_exit, :kill + before :each do + ScratchPad.clear + end + + it "kills sleeping thread" do + sleeping_thread = Thread.new do + sleep + ScratchPad.record :after_sleep + end + Thread.pass while sleeping_thread.status and sleeping_thread.status != "sleep" + sleeping_thread.kill + sleeping_thread.join + ScratchPad.recorded.should == nil + end + + it "kills current thread" do + thread = Thread.new do + Thread.current.kill + ScratchPad.record :after_sleep + end + thread.join + ScratchPad.recorded.should == nil + end + + it "runs ensure clause" do + thread = ThreadSpecs.dying_thread_ensures(:kill) { ScratchPad.record :in_ensure_clause } + thread.join + ScratchPad.recorded.should == :in_ensure_clause + end + + it "runs nested ensure clauses" do + ScratchPad.record [] + @outer = Thread.new do + begin + @inner = Thread.new do + begin + sleep + ensure + ScratchPad << :inner_ensure_clause + end + end + sleep + ensure + ScratchPad << :outer_ensure_clause + @inner.kill + @inner.join + end + end + Thread.pass while @outer.status and @outer.status != "sleep" + Thread.pass until @inner + Thread.pass while @inner.status and @inner.status != "sleep" + @outer.kill + @outer.join + ScratchPad.recorded.should.include?(:inner_ensure_clause) + ScratchPad.recorded.should.include?(:outer_ensure_clause) + end + + it "does not set $!" do + thread = ThreadSpecs.dying_thread_ensures(:kill) { ScratchPad.record $! } + thread.join + ScratchPad.recorded.should == nil + end + + it "does not reset $!" do + ScratchPad.record [] + + exc = RuntimeError.new("foo") + thread = Thread.new do + begin + raise exc + ensure + ScratchPad << $! + begin + Thread.current.kill + ensure + ScratchPad << $! + end + end + end + thread.join + ScratchPad.recorded.should == [exc, exc] + end + + it "cannot be rescued" do + thread = Thread.new do + begin + Thread.current.kill + rescue Exception + ScratchPad.record :in_rescue + end + ScratchPad.record :end_of_thread_block + end + + thread.join + ScratchPad.recorded.should == nil + end + + it "kills the entire thread when a fiber is active" do + t = Thread.new do + Fiber.new do + sleep + end.resume + ScratchPad.record :fiber_resumed + end + Thread.pass while t.status and t.status != "sleep" + t.kill + t.join + ScratchPad.recorded.should == nil + end + + it "kills other fibers of that thread without running their ensure clauses" do + t = Thread.new do + f = Fiber.new do + ScratchPad.record :fiber_resumed + begin + Fiber.yield + ensure + ScratchPad.record :fiber_ensure + end + end + f.resume + sleep + end + Thread.pass until t.stop? + t.kill + t.join + ScratchPad.recorded.should == :fiber_resumed + end + + # This spec is a mess. It fails randomly, it hangs on MRI, it needs to be removed + quarantine! do + it "killing dying running does nothing" do + in_ensure_clause = false + exit_loop = true + t = ThreadSpecs.dying_thread_ensures do + in_ensure_clause = true + loop { if exit_loop then break end } + ScratchPad.record :after_stop + end + + Thread.pass until in_ensure_clause == true + 10.times { t.kill; Thread.pass } + exit_loop = true + t.join + ScratchPad.recorded.should == :after_stop + end + end + + quarantine! do + + it "propagates inner exception to Thread.join if there is an outer ensure clause" do + thread = ThreadSpecs.dying_thread_with_outer_ensure(:kill) { } + -> { thread.join }.should.raise(RuntimeError, "In dying thread") + end + + it "runs all outer ensure clauses even if inner ensure clause raises exception" do + ThreadSpecs.join_dying_thread_with_outer_ensure(:kill) { ScratchPad.record :in_outer_ensure_clause } + ScratchPad.recorded.should == :in_outer_ensure_clause + end + + it "sets $! in outer ensure clause if inner ensure clause raises exception" do + ThreadSpecs.join_dying_thread_with_outer_ensure(:kill) { ScratchPad.record $! } + ScratchPad.recorded.to_s.should == "In dying thread" + end + end + + it "can be rescued by outer rescue clause when inner ensure clause raises exception" do + thread = Thread.new do + begin + begin + Thread.current.kill + ensure + raise "In dying thread" + end + rescue Exception + ScratchPad.record $! + end + :end_of_thread_block + end + + thread.value.should == :end_of_thread_block + ScratchPad.recorded.to_s.should == "In dying thread" + end + + it "is deferred if ensure clause does Thread.stop" do + ThreadSpecs.wakeup_dying_sleeping_thread(:kill) { Thread.stop; ScratchPad.record :after_sleep } + ScratchPad.recorded.should == :after_sleep + end + + # Hangs on 1.8.6.114 OS X, possibly also on Linux + quarantine! do + it "is deferred if ensure clause sleeps" do + ThreadSpecs.wakeup_dying_sleeping_thread(:kill) { sleep; ScratchPad.record :after_sleep } + ScratchPad.recorded.should == :after_sleep + end + end + + # This case occurred in JRuby where native threads are used to provide + # the same behavior as MRI green threads. Key to this issue was the fact + # that the thread which called #exit in its block was also being explicitly + # sent #join from outside the thread. The 100.times provides a certain + # probability that the deadlock will occur. It was sufficient to reliably + # reproduce the deadlock in JRuby. + it "does not deadlock when called from within the thread while being joined from without" do + 100.times do + t = Thread.new { Thread.stop; Thread.current.kill } + Thread.pass while t.status and t.status != "sleep" + t.wakeup.should == t + t.join.should == t + end + end end describe "Thread.kill" do diff --git a/spec/ruby/core/thread/shared/exit.rb b/spec/ruby/core/thread/shared/exit.rb deleted file mode 100644 index a294c3a4d63ff4..00000000000000 --- a/spec/ruby/core/thread/shared/exit.rb +++ /dev/null @@ -1,219 +0,0 @@ -describe :thread_exit, shared: true do - before :each do - ScratchPad.clear - end - - # This spec randomly kills mspec worker like: https://ci.appveyor.com/project/ruby/ruby/builds/19390874/job/wv1bsm8skd4e1pxl - # TODO: Investigate the cause or at least print helpful logs, and remove this `platform_is_not` guard. - platform_is_not :mingw do - - it "kills sleeping thread" do - sleeping_thread = Thread.new do - sleep - ScratchPad.record :after_sleep - end - Thread.pass while sleeping_thread.status and sleeping_thread.status != "sleep" - sleeping_thread.send(@method) - sleeping_thread.join - ScratchPad.recorded.should == nil - end - - it "kills current thread" do - thread = Thread.new do - Thread.current.send(@method) - ScratchPad.record :after_sleep - end - thread.join - ScratchPad.recorded.should == nil - end - - it "runs ensure clause" do - thread = ThreadSpecs.dying_thread_ensures(@method) { ScratchPad.record :in_ensure_clause } - thread.join - ScratchPad.recorded.should == :in_ensure_clause - end - - it "runs nested ensure clauses" do - ScratchPad.record [] - @outer = Thread.new do - begin - @inner = Thread.new do - begin - sleep - ensure - ScratchPad << :inner_ensure_clause - end - end - sleep - ensure - ScratchPad << :outer_ensure_clause - @inner.send(@method) - @inner.join - end - end - Thread.pass while @outer.status and @outer.status != "sleep" - Thread.pass until @inner - Thread.pass while @inner.status and @inner.status != "sleep" - @outer.send(@method) - @outer.join - ScratchPad.recorded.should.include?(:inner_ensure_clause) - ScratchPad.recorded.should.include?(:outer_ensure_clause) - end - - it "does not set $!" do - thread = ThreadSpecs.dying_thread_ensures(@method) { ScratchPad.record $! } - thread.join - ScratchPad.recorded.should == nil - end - - it "does not reset $!" do - ScratchPad.record [] - - exc = RuntimeError.new("foo") - thread = Thread.new do - begin - raise exc - ensure - ScratchPad << $! - begin - Thread.current.send(@method) - ensure - ScratchPad << $! - end - end - end - thread.join - ScratchPad.recorded.should == [exc, exc] - end - - it "cannot be rescued" do - thread = Thread.new do - begin - Thread.current.send(@method) - rescue Exception - ScratchPad.record :in_rescue - end - ScratchPad.record :end_of_thread_block - end - - thread.join - ScratchPad.recorded.should == nil - end - - it "kills the entire thread when a fiber is active" do - t = Thread.new do - Fiber.new do - sleep - end.resume - ScratchPad.record :fiber_resumed - end - Thread.pass while t.status and t.status != "sleep" - t.send(@method) - t.join - ScratchPad.recorded.should == nil - end - - it "kills other fibers of that thread without running their ensure clauses" do - t = Thread.new do - f = Fiber.new do - ScratchPad.record :fiber_resumed - begin - Fiber.yield - ensure - ScratchPad.record :fiber_ensure - end - end - f.resume - sleep - end - Thread.pass until t.stop? - t.send(@method) - t.join - ScratchPad.recorded.should == :fiber_resumed - end - - # This spec is a mess. It fails randomly, it hangs on MRI, it needs to be removed - quarantine! do - it "killing dying running does nothing" do - in_ensure_clause = false - exit_loop = true - t = ThreadSpecs.dying_thread_ensures do - in_ensure_clause = true - loop { if exit_loop then break end } - ScratchPad.record :after_stop - end - - Thread.pass until in_ensure_clause == true - 10.times { t.send(@method); Thread.pass } - exit_loop = true - t.join - ScratchPad.recorded.should == :after_stop - end - end - - quarantine! do - - it "propagates inner exception to Thread.join if there is an outer ensure clause" do - thread = ThreadSpecs.dying_thread_with_outer_ensure(@method) { } - -> { thread.join }.should.raise(RuntimeError, "In dying thread") - end - - it "runs all outer ensure clauses even if inner ensure clause raises exception" do - ThreadSpecs.join_dying_thread_with_outer_ensure(@method) { ScratchPad.record :in_outer_ensure_clause } - ScratchPad.recorded.should == :in_outer_ensure_clause - end - - it "sets $! in outer ensure clause if inner ensure clause raises exception" do - ThreadSpecs.join_dying_thread_with_outer_ensure(@method) { ScratchPad.record $! } - ScratchPad.recorded.to_s.should == "In dying thread" - end - end - - it "can be rescued by outer rescue clause when inner ensure clause raises exception" do - thread = Thread.new do - begin - begin - Thread.current.send(@method) - ensure - raise "In dying thread" - end - rescue Exception - ScratchPad.record $! - end - :end_of_thread_block - end - - thread.value.should == :end_of_thread_block - ScratchPad.recorded.to_s.should == "In dying thread" - end - - it "is deferred if ensure clause does Thread.stop" do - ThreadSpecs.wakeup_dying_sleeping_thread(@method) { Thread.stop; ScratchPad.record :after_sleep } - ScratchPad.recorded.should == :after_sleep - end - - # Hangs on 1.8.6.114 OS X, possibly also on Linux - quarantine! do - it "is deferred if ensure clause sleeps" do - ThreadSpecs.wakeup_dying_sleeping_thread(@method) { sleep; ScratchPad.record :after_sleep } - ScratchPad.recorded.should == :after_sleep - end - end - - # This case occurred in JRuby where native threads are used to provide - # the same behavior as MRI green threads. Key to this issue was the fact - # that the thread which called #exit in its block was also being explicitly - # sent #join from outside the thread. The 100.times provides a certain - # probability that the deadlock will occur. It was sufficient to reliably - # reproduce the deadlock in JRuby. - it "does not deadlock when called from within the thread while being joined from without" do - 100.times do - t = Thread.new { Thread.stop; Thread.current.send(@method) } - Thread.pass while t.status and t.status != "sleep" - t.wakeup.should == t - t.join.should == t - end - end - - end # platform_is_not :mingw -end diff --git a/spec/ruby/core/thread/shared/start.rb b/spec/ruby/core/thread/shared/start.rb deleted file mode 100644 index c5d62ab0984a57..00000000000000 --- a/spec/ruby/core/thread/shared/start.rb +++ /dev/null @@ -1,41 +0,0 @@ -describe :thread_start, shared: true do - before :each do - ScratchPad.clear - end - - it "raises an ArgumentError if not passed a block" do - -> { - Thread.send(@method) - }.should.raise(ArgumentError) - end - - it "spawns a new Thread running the block" do - run = false - t = Thread.send(@method) { run = true } - t.should.is_a?(Thread) - t.join - - run.should == true - end - - it "respects Thread subclasses" do - c = Class.new(Thread) - t = c.send(@method) { } - t.should.is_a?(c) - - t.join - end - - it "does not call #initialize" do - c = Class.new(Thread) do - def initialize - ScratchPad.record :bad - end - end - - t = c.send(@method) { } - t.join - - ScratchPad.recorded.should == nil - end -end diff --git a/spec/ruby/core/thread/shared/to_s.rb b/spec/ruby/core/thread/shared/to_s.rb deleted file mode 100644 index 27e53ba4b83fa8..00000000000000 --- a/spec/ruby/core/thread/shared/to_s.rb +++ /dev/null @@ -1,53 +0,0 @@ -require_relative '../fixtures/classes' - -describe :thread_to_s, shared: true do - it "returns a description including file and line number" do - thread, line = Thread.new { "hello" }, __LINE__ - thread.join - thread.send(@method).should =~ /^#$/ - end - - it "has a binary encoding" do - ThreadSpecs.status_of_current_thread.send(@method).encoding.should == Encoding::BINARY - end - - it "can check it's own status" do - ThreadSpecs.status_of_current_thread.send(@method).should.include?('run') - end - - it "describes a running thread" do - ThreadSpecs.status_of_running_thread.send(@method).should.include?('run') - end - - it "describes a sleeping thread" do - ThreadSpecs.status_of_sleeping_thread.send(@method).should.include?('sleep') - end - - it "describes a blocked thread" do - ThreadSpecs.status_of_blocked_thread.send(@method).should.include?('sleep') - end - - it "describes a completed thread" do - ThreadSpecs.status_of_completed_thread.send(@method).should.include?('dead') - end - - it "describes a killed thread" do - ThreadSpecs.status_of_killed_thread.send(@method).should.include?('dead') - end - - it "describes a thread with an uncaught exception" do - ThreadSpecs.status_of_thread_with_uncaught_exception.send(@method).should.include?('dead') - end - - it "describes a dying sleeping thread" do - ThreadSpecs.status_of_dying_sleeping_thread.send(@method).should.include?('sleep') - end - - it "reports aborting on a killed thread" do - ThreadSpecs.status_of_dying_running_thread.send(@method).should.include?('aborting') - end - - it "reports aborting on a killed thread after sleep" do - ThreadSpecs.status_of_dying_thread_after_sleep.send(@method).should.include?('aborting') - end -end diff --git a/spec/ruby/core/thread/start_spec.rb b/spec/ruby/core/thread/start_spec.rb index 3dd040f98b413a..a2ee52180bb1f5 100644 --- a/spec/ruby/core/thread/start_spec.rb +++ b/spec/ruby/core/thread/start_spec.rb @@ -1,9 +1,43 @@ require_relative '../../spec_helper' -require_relative 'fixtures/classes' -require_relative 'shared/start' describe "Thread.start" do - describe "Thread.start" do - it_behaves_like :thread_start, :start + before :each do + ScratchPad.clear + end + + it "raises an ArgumentError if not passed a block" do + -> { + Thread.start + }.should.raise(ArgumentError) + end + + it "spawns a new Thread running the block" do + run = false + t = Thread.start { run = true } + t.should.is_a?(Thread) + t.join + + run.should == true + end + + it "respects Thread subclasses" do + c = Class.new(Thread) + t = c.start { } + t.should.is_a?(c) + + t.join + end + + it "does not call #initialize" do + c = Class.new(Thread) do + def initialize + ScratchPad.record :bad + end + end + + t = c.start { } + t.join + + ScratchPad.recorded.should == nil end end diff --git a/spec/ruby/core/thread/terminate_spec.rb b/spec/ruby/core/thread/terminate_spec.rb index cf6cab472b33ea..68c431a0c08937 100644 --- a/spec/ruby/core/thread/terminate_spec.rb +++ b/spec/ruby/core/thread/terminate_spec.rb @@ -1,7 +1,7 @@ require_relative '../../spec_helper' -require_relative 'fixtures/classes' -require_relative 'shared/exit' describe "Thread#terminate" do - it_behaves_like :thread_exit, :terminate + it "is an alias of Thread#kill" do + Thread.instance_method(:terminate).should == Thread.instance_method(:kill) + end end diff --git a/spec/ruby/core/thread/to_s_spec.rb b/spec/ruby/core/thread/to_s_spec.rb index cb182a017ff257..2aef426de86d2c 100644 --- a/spec/ruby/core/thread/to_s_spec.rb +++ b/spec/ruby/core/thread/to_s_spec.rb @@ -1,6 +1,54 @@ require_relative '../../spec_helper' -require_relative 'shared/to_s' +require_relative 'fixtures/classes' describe "Thread#to_s" do - it_behaves_like :thread_to_s, :to_s + it "returns a description including file and line number" do + thread, line = Thread.new { "hello" }, __LINE__ + thread.join + thread.to_s.should =~ /^#$/ + end + + it "has a binary encoding" do + ThreadSpecs.status_of_current_thread.to_s.encoding.should == Encoding::BINARY + end + + it "can check it's own status" do + ThreadSpecs.status_of_current_thread.to_s.should.include?('run') + end + + it "describes a running thread" do + ThreadSpecs.status_of_running_thread.to_s.should.include?('run') + end + + it "describes a sleeping thread" do + ThreadSpecs.status_of_sleeping_thread.to_s.should.include?('sleep') + end + + it "describes a blocked thread" do + ThreadSpecs.status_of_blocked_thread.to_s.should.include?('sleep') + end + + it "describes a completed thread" do + ThreadSpecs.status_of_completed_thread.to_s.should.include?('dead') + end + + it "describes a killed thread" do + ThreadSpecs.status_of_killed_thread.to_s.should.include?('dead') + end + + it "describes a thread with an uncaught exception" do + ThreadSpecs.status_of_thread_with_uncaught_exception.to_s.should.include?('dead') + end + + it "describes a dying sleeping thread" do + ThreadSpecs.status_of_dying_sleeping_thread.to_s.should.include?('sleep') + end + + it "reports aborting on a killed thread" do + ThreadSpecs.status_of_dying_running_thread.to_s.should.include?('aborting') + end + + it "reports aborting on a killed thread after sleep" do + ThreadSpecs.status_of_dying_thread_after_sleep.to_s.should.include?('aborting') + end end diff --git a/spec/ruby/core/time/asctime_spec.rb b/spec/ruby/core/time/asctime_spec.rb index a41ee531e4e8eb..17e155787a0e5c 100644 --- a/spec/ruby/core/time/asctime_spec.rb +++ b/spec/ruby/core/time/asctime_spec.rb @@ -1,6 +1,8 @@ require_relative '../../spec_helper' -require_relative 'shared/asctime' describe "Time#asctime" do - it_behaves_like :time_asctime, :asctime + it "returns a canonical string representation of time" do + t = Time.now + t.asctime.should == t.strftime("%a %b %e %H:%M:%S %Y") + end end diff --git a/spec/ruby/core/time/ctime_spec.rb b/spec/ruby/core/time/ctime_spec.rb index 57e7cfd9ced1a5..b609b03974b78c 100644 --- a/spec/ruby/core/time/ctime_spec.rb +++ b/spec/ruby/core/time/ctime_spec.rb @@ -1,6 +1,7 @@ require_relative '../../spec_helper' -require_relative 'shared/asctime' describe "Time#ctime" do - it_behaves_like :time_asctime, :ctime + it "is an alias of Time#asctime" do + Time.instance_method(:ctime).should == Time.instance_method(:asctime) + end end diff --git a/spec/ruby/core/time/day_spec.rb b/spec/ruby/core/time/day_spec.rb index 895bcd7a86fbfd..3dec17644c4a6e 100644 --- a/spec/ruby/core/time/day_spec.rb +++ b/spec/ruby/core/time/day_spec.rb @@ -1,6 +1,17 @@ require_relative '../../spec_helper' -require_relative 'shared/day' describe "Time#day" do - it_behaves_like :time_day, :day + it "returns the day of the month (1..n) for a local Time" do + with_timezone("CET", 1) do + Time.local(1970, 1, 1).day.should == 1 + end + end + + it "returns the day of the month for a UTC Time" do + Time.utc(1970, 1, 1).day.should == 1 + end + + it "returns the day of the month for a Time with a fixed offset" do + Time.new(2012, 1, 1, 0, 0, 0, -3600).day.should == 1 + end end diff --git a/spec/ruby/core/time/dst_spec.rb b/spec/ruby/core/time/dst_spec.rb index 436240aae59302..42daf86875d9c6 100644 --- a/spec/ruby/core/time/dst_spec.rb +++ b/spec/ruby/core/time/dst_spec.rb @@ -1,6 +1,10 @@ require_relative '../../spec_helper' -require_relative 'shared/isdst' describe "Time#dst?" do - it_behaves_like :time_isdst, :dst? + it "returns whether time is during daylight saving time" do + with_timezone("America/Los_Angeles") do + Time.local(2007, 9, 9, 0, 0, 0).dst?.should == true + Time.local(2007, 1, 9, 0, 0, 0).dst?.should == false + end + end end diff --git a/spec/ruby/core/time/getgm_spec.rb b/spec/ruby/core/time/getgm_spec.rb index b5d45b1d9f07ee..7698156c8ccfdb 100644 --- a/spec/ruby/core/time/getgm_spec.rb +++ b/spec/ruby/core/time/getgm_spec.rb @@ -1,6 +1,7 @@ require_relative '../../spec_helper' -require_relative 'shared/getgm' describe "Time#getgm" do - it_behaves_like :time_getgm, :getgm + it "is an alias of Time#getutc" do + Time.instance_method(:getgm).should == Time.instance_method(:getutc) + end end diff --git a/spec/ruby/core/time/getutc_spec.rb b/spec/ruby/core/time/getutc_spec.rb index 0cd9c17b00cbab..1d49059a71d194 100644 --- a/spec/ruby/core/time/getutc_spec.rb +++ b/spec/ruby/core/time/getutc_spec.rb @@ -1,6 +1,11 @@ require_relative '../../spec_helper' -require_relative 'shared/getgm' describe "Time#getutc" do - it_behaves_like :time_getgm, :getutc + it "returns a new time which is the utc representation of time" do + # Testing with America/Regina here because it doesn't have DST. + with_timezone("CST", -6) do + t = Time.local(2007, 1, 9, 6, 0, 0) + t.getutc.should == Time.gm(2007, 1, 9, 12, 0, 0) + end + end end diff --git a/spec/ruby/core/time/gm_spec.rb b/spec/ruby/core/time/gm_spec.rb index 26dffbcedc0797..fbabede6ba4ed6 100644 --- a/spec/ruby/core/time/gm_spec.rb +++ b/spec/ruby/core/time/gm_spec.rb @@ -1,10 +1,7 @@ require_relative '../../spec_helper' -require_relative 'shared/gm' -require_relative 'shared/time_params' describe "Time.gm" do - it_behaves_like :time_gm, :gm - it_behaves_like :time_params, :gm - it_behaves_like :time_params_10_arg, :gm - it_behaves_like :time_params_microseconds, :gm + it "is an alias of Time.utc" do + Time.method(:gm).should == Time.method(:utc) + end end diff --git a/spec/ruby/core/time/gmt_offset_spec.rb b/spec/ruby/core/time/gmt_offset_spec.rb index df417e6e4e0d0e..176998175301fe 100644 --- a/spec/ruby/core/time/gmt_offset_spec.rb +++ b/spec/ruby/core/time/gmt_offset_spec.rb @@ -1,6 +1,7 @@ require_relative '../../spec_helper' -require_relative 'shared/gmt_offset' describe "Time#gmt_offset" do - it_behaves_like :time_gmt_offset, :gmt_offset + it "is an alias of Time#utc_offset" do + Time.instance_method(:gmt_offset).should == Time.instance_method(:utc_offset) + end end diff --git a/spec/ruby/core/time/gmt_spec.rb b/spec/ruby/core/time/gmt_spec.rb index 840f59e0e84abc..38e98cc43ceac8 100644 --- a/spec/ruby/core/time/gmt_spec.rb +++ b/spec/ruby/core/time/gmt_spec.rb @@ -1,8 +1,7 @@ require_relative '../../spec_helper' describe "Time#gmt?" do - it "returns true if time represents a time in UTC (GMT)" do - Time.now.should_not.gmt? - Time.now.gmtime.should.gmt? + it "is an alias of Time#utc?" do + Time.instance_method(:gmt?).should == Time.instance_method(:utc?) end end diff --git a/spec/ruby/core/time/gmtime_spec.rb b/spec/ruby/core/time/gmtime_spec.rb index d965cd541d24c2..e7e1d4ffb28c47 100644 --- a/spec/ruby/core/time/gmtime_spec.rb +++ b/spec/ruby/core/time/gmtime_spec.rb @@ -1,6 +1,7 @@ require_relative '../../spec_helper' -require_relative 'shared/gmtime' describe "Time#gmtime" do - it_behaves_like :time_gmtime, :gmtime + it "is an alias of Time#utc" do + Time.instance_method(:gmtime).should == Time.instance_method(:utc) + end end diff --git a/spec/ruby/core/time/gmtoff_spec.rb b/spec/ruby/core/time/gmtoff_spec.rb index fa28520e9e1a1a..c7d801a681dbc0 100644 --- a/spec/ruby/core/time/gmtoff_spec.rb +++ b/spec/ruby/core/time/gmtoff_spec.rb @@ -1,6 +1,7 @@ require_relative '../../spec_helper' -require_relative 'shared/gmt_offset' describe "Time#gmtoff" do - it_behaves_like :time_gmt_offset, :gmtoff + it "is an alias of Time#utc_offset" do + Time.instance_method(:gmtoff).should == Time.instance_method(:utc_offset) + end end diff --git a/spec/ruby/core/time/isdst_spec.rb b/spec/ruby/core/time/isdst_spec.rb index 173230ca07a73d..759953cca71c5e 100644 --- a/spec/ruby/core/time/isdst_spec.rb +++ b/spec/ruby/core/time/isdst_spec.rb @@ -1,6 +1,7 @@ require_relative '../../spec_helper' -require_relative 'shared/isdst' describe "Time#isdst" do - it_behaves_like :time_isdst, :isdst + it "is an alias of Time#dst?" do + Time.instance_method(:isdst).should == Time.instance_method(:dst?) + end end diff --git a/spec/ruby/core/time/iso8601_spec.rb b/spec/ruby/core/time/iso8601_spec.rb index ad60c3bb322100..a6efc57b2864ce 100644 --- a/spec/ruby/core/time/iso8601_spec.rb +++ b/spec/ruby/core/time/iso8601_spec.rb @@ -1,6 +1,33 @@ require_relative '../../spec_helper' -require_relative 'shared/xmlschema' describe "Time#iso8601" do - it_behaves_like :time_xmlschema, :iso8601 + ruby_version_is "3.4" do + it "generates ISO-8601 strings in Z for UTC times" do + t = Time.utc(1985, 4, 12, 23, 20, 50, 521245) + t.iso8601.should == "1985-04-12T23:20:50Z" + t.iso8601(2).should == "1985-04-12T23:20:50.52Z" + t.iso8601(9).should == "1985-04-12T23:20:50.521245000Z" + end + + it "generates ISO-8601 string with timezone offset for non-UTC times" do + t = Time.new(1985, 4, 12, 23, 20, 50, "+02:00") + t.iso8601.should == "1985-04-12T23:20:50+02:00" + t.iso8601(2).should == "1985-04-12T23:20:50.00+02:00" + end + + it "year is always at least 4 digits" do + t = Time.utc(12, 4, 12) + t.iso8601.should == "0012-04-12T00:00:00Z" + end + + it "year can be more than 4 digits" do + t = Time.utc(40_000, 4, 12) + t.iso8601.should == "40000-04-12T00:00:00Z" + end + + it "year can be negative" do + t = Time.utc(-2000, 4, 12) + t.iso8601.should == "-2000-04-12T00:00:00Z" + end + end end diff --git a/spec/ruby/core/time/mday_spec.rb b/spec/ruby/core/time/mday_spec.rb index 3c2193989087e3..78021785f96135 100644 --- a/spec/ruby/core/time/mday_spec.rb +++ b/spec/ruby/core/time/mday_spec.rb @@ -1,6 +1,7 @@ require_relative '../../spec_helper' -require_relative 'shared/day' describe "Time#mday" do - it_behaves_like :time_day, :mday + it "is an alias of Time#day" do + Time.instance_method(:mday).should == Time.instance_method(:day) + end end diff --git a/spec/ruby/core/time/mktime_spec.rb b/spec/ruby/core/time/mktime_spec.rb index 78a6a6e772cbd3..83bf12293a146e 100644 --- a/spec/ruby/core/time/mktime_spec.rb +++ b/spec/ruby/core/time/mktime_spec.rb @@ -1,11 +1,7 @@ require_relative '../../spec_helper' -require_relative 'shared/local' -require_relative 'shared/time_params' describe "Time.mktime" do - it_behaves_like :time_local, :mktime - it_behaves_like :time_local_10_arg, :mktime - it_behaves_like :time_params, :mktime - it_behaves_like :time_params_10_arg, :mktime - it_behaves_like :time_params_microseconds, :mktime + it "is an alias of Time.local" do + Time.method(:mktime).should == Time.method(:local) + end end diff --git a/spec/ruby/core/time/mon_spec.rb b/spec/ruby/core/time/mon_spec.rb index f41b39648bf1e8..d57549dadddeea 100644 --- a/spec/ruby/core/time/mon_spec.rb +++ b/spec/ruby/core/time/mon_spec.rb @@ -1,6 +1,7 @@ require_relative '../../spec_helper' -require_relative 'shared/month' describe "Time#mon" do - it_behaves_like :time_month, :mon + it "is an alias of Time#month" do + Time.instance_method(:mon).should == Time.instance_method(:month) + end end diff --git a/spec/ruby/core/time/month_spec.rb b/spec/ruby/core/time/month_spec.rb index 81e20384abb1bd..eae0e85acd8298 100644 --- a/spec/ruby/core/time/month_spec.rb +++ b/spec/ruby/core/time/month_spec.rb @@ -1,6 +1,17 @@ require_relative '../../spec_helper' -require_relative 'shared/month' describe "Time#month" do - it_behaves_like :time_month, :month + it "returns the month of the year for a local Time" do + with_timezone("CET", 1) do + Time.local(1970, 1).month.should == 1 + end + end + + it "returns the month of the year for a UTC Time" do + Time.utc(1970, 1).month.should == 1 + end + + it "returns the four digit year for a Time with a fixed offset" do + Time.new(2012, 1, 1, 0, 0, 0, -3600).month.should == 1 + end end diff --git a/spec/ruby/core/time/shared/asctime.rb b/spec/ruby/core/time/shared/asctime.rb deleted file mode 100644 index d0966668636980..00000000000000 --- a/spec/ruby/core/time/shared/asctime.rb +++ /dev/null @@ -1,6 +0,0 @@ -describe :time_asctime, shared: true do - it "returns a canonical string representation of time" do - t = Time.now - t.send(@method).should == t.strftime("%a %b %e %H:%M:%S %Y") - end -end diff --git a/spec/ruby/core/time/shared/day.rb b/spec/ruby/core/time/shared/day.rb deleted file mode 100644 index 472dc959c172f9..00000000000000 --- a/spec/ruby/core/time/shared/day.rb +++ /dev/null @@ -1,15 +0,0 @@ -describe :time_day, shared: true do - it "returns the day of the month (1..n) for a local Time" do - with_timezone("CET", 1) do - Time.local(1970, 1, 1).send(@method).should == 1 - end - end - - it "returns the day of the month for a UTC Time" do - Time.utc(1970, 1, 1).send(@method).should == 1 - end - - it "returns the day of the month for a Time with a fixed offset" do - Time.new(2012, 1, 1, 0, 0, 0, -3600).send(@method).should == 1 - end -end diff --git a/spec/ruby/core/time/shared/getgm.rb b/spec/ruby/core/time/shared/getgm.rb deleted file mode 100644 index 3576365772bd7f..00000000000000 --- a/spec/ruby/core/time/shared/getgm.rb +++ /dev/null @@ -1,9 +0,0 @@ -describe :time_getgm, shared: true do - it "returns a new time which is the utc representation of time" do - # Testing with America/Regina here because it doesn't have DST. - with_timezone("CST", -6) do - t = Time.local(2007, 1, 9, 6, 0, 0) - t.send(@method).should == Time.gm(2007, 1, 9, 12, 0, 0) - end - end -end diff --git a/spec/ruby/core/time/shared/gm.rb b/spec/ruby/core/time/shared/gm.rb deleted file mode 100644 index 0ee602c837ec01..00000000000000 --- a/spec/ruby/core/time/shared/gm.rb +++ /dev/null @@ -1,70 +0,0 @@ -describe :time_gm, shared: true do - it "creates a time based on given values, interpreted as UTC (GMT)" do - Time.send(@method, 2000,"jan",1,20,15,1).inspect.should == "2000-01-01 20:15:01 UTC" - end - - it "creates a time based on given C-style gmtime arguments, interpreted as UTC (GMT)" do - time = Time.send(@method, 1, 15, 20, 1, 1, 2000, :ignored, :ignored, :ignored, :ignored) - time.inspect.should == "2000-01-01 20:15:01 UTC" - end - - it "interprets pre-Gregorian reform dates using Gregorian proleptic calendar" do - Time.send(@method, 1582, 10, 4, 12).to_i.should == -12220200000 # 2299150j - end - - it "interprets Julian-Gregorian gap dates using Gregorian proleptic calendar" do - Time.send(@method, 1582, 10, 14, 12).to_i.should == -12219336000 # 2299160j - end - - it "interprets post-Gregorian reform dates using Gregorian calendar" do - Time.send(@method, 1582, 10, 15, 12).to_i.should == -12219249600 # 2299161j - end - - it "handles fractional usec close to rounding limit" do - time = Time.send(@method, 2000, 1, 1, 12, 30, 0, 9999r/10000) - - time.usec.should == 0 - time.nsec.should == 999 - end - - guard -> { - with_timezone 'right/UTC' do - (Time.gm(1972, 6, 30, 23, 59, 59) + 1).sec == 60 - end - } do - it "handles real leap seconds in zone 'right/UTC'" do - with_timezone 'right/UTC' do - time = Time.send(@method, 1972, 6, 30, 23, 59, 60) - - time.sec.should == 60 - time.min.should == 59 - time.hour.should == 23 - time.day.should == 30 - time.month.should == 6 - end - end - end - - it "handles bad leap seconds by carrying values forward" do - with_timezone 'UTC' do - time = Time.send(@method, 2017, 7, 5, 23, 59, 60) - time.sec.should == 0 - time.min.should == 0 - time.hour.should == 0 - time.day.should == 6 - time.month.should == 7 - end - end - - it "handles a value of 60 for seconds by carrying values forward in zone 'UTC'" do - with_timezone 'UTC' do - time = Time.send(@method, 1972, 6, 30, 23, 59, 60) - - time.sec.should == 0 - time.min.should == 0 - time.hour.should == 0 - time.day.should == 1 - time.month.should == 7 - end - end -end diff --git a/spec/ruby/core/time/shared/gmt_offset.rb b/spec/ruby/core/time/shared/gmt_offset.rb deleted file mode 100644 index 839566c249c361..00000000000000 --- a/spec/ruby/core/time/shared/gmt_offset.rb +++ /dev/null @@ -1,59 +0,0 @@ -describe :time_gmt_offset, shared: true do - it "returns the offset in seconds between the timezone of time and UTC" do - with_timezone("AST", 3) do - Time.new.send(@method).should == 10800 - end - end - - it "returns 0 when the date is UTC" do - with_timezone("AST", 3) do - Time.new.utc.send(@method).should == 0 - end - end - - platform_is_not :windows do - it "returns the correct offset for US Eastern time zone around daylight savings time change" do - # "2010-03-14 01:59:59 -0500" + 1 ==> "2010-03-14 03:00:00 -0400" - with_timezone("EST5EDT") do - t = Time.local(2010,3,14,1,59,59) - t.send(@method).should == -5*60*60 - (t + 1).send(@method).should == -4*60*60 - end - end - - it "returns the correct offset for Hawaii around daylight savings time change" do - # "2010-03-14 01:59:59 -1000" + 1 ==> "2010-03-14 02:00:00 -1000" - with_timezone("Pacific/Honolulu") do - t = Time.local(2010,3,14,1,59,59) - t.send(@method).should == -10*60*60 - (t + 1).send(@method).should == -10*60*60 - end - end - - it "returns the correct offset for New Zealand around daylight savings time change" do - # "2010-04-04 02:59:59 +1300" + 1 ==> "2010-04-04 02:00:00 +1200" - with_timezone("Pacific/Auckland") do - t = Time.local(2010,4,4,1,59,59) + (60 * 60) - t.send(@method).should == 13*60*60 - (t + 1).send(@method).should == 12*60*60 - end - end - end - - it "returns offset as Rational" do - Time.new(2010,4,4,1,59,59,7245).send(@method).should == 7245 - Time.new(2010,4,4,1,59,59,7245.5).send(@method).should == Rational(14491,2) - end - - context 'given positive offset' do - it 'returns a positive offset' do - Time.new(2013,3,17,nil,nil,nil,"+03:00").send(@method).should == 10800 - end - end - - context 'given negative offset' do - it 'returns a negative offset' do - Time.new(2013,3,17,nil,nil,nil,"-03:00").send(@method).should == -10800 - end - end -end diff --git a/spec/ruby/core/time/shared/gmtime.rb b/spec/ruby/core/time/shared/gmtime.rb deleted file mode 100644 index aa76b436ccca45..00000000000000 --- a/spec/ruby/core/time/shared/gmtime.rb +++ /dev/null @@ -1,40 +0,0 @@ -describe :time_gmtime, shared: true do - it "converts self to UTC, modifying the receiver" do - # Testing with America/Regina here because it doesn't have DST. - with_timezone("CST", -6) do - t = Time.local(2007, 1, 9, 6, 0, 0) - t.send(@method) - # Time#== compensates for time zones, so check all parts separately - t.year.should == 2007 - t.month.should == 1 - t.mday.should == 9 - t.hour.should == 12 - t.min.should == 0 - t.sec.should == 0 - t.zone.should == "UTC" - end - end - - it "returns self" do - with_timezone("CST", -6) do - t = Time.local(2007, 1, 9, 12, 0, 0) - t.send(@method).should.equal?(t) - end - end - - describe "on a frozen time" do - it "does not raise an error if already in UTC" do - time = Time.gm(2007, 1, 9, 12, 0, 0) - time.freeze - time.send(@method).should.equal?(time) - end - - it "raises a FrozenError if the time is not UTC" do - with_timezone("CST", -6) do - time = Time.now - time.freeze - -> { time.send(@method) }.should.raise(FrozenError) - end - end - end -end diff --git a/spec/ruby/core/time/shared/isdst.rb b/spec/ruby/core/time/shared/isdst.rb deleted file mode 100644 index bc6d1392304b49..00000000000000 --- a/spec/ruby/core/time/shared/isdst.rb +++ /dev/null @@ -1,8 +0,0 @@ -describe :time_isdst, shared: true do - it "dst? returns whether time is during daylight saving time" do - with_timezone("America/Los_Angeles") do - Time.local(2007, 9, 9, 0, 0, 0).send(@method).should == true - Time.local(2007, 1, 9, 0, 0, 0).send(@method).should == false - end - end -end diff --git a/spec/ruby/core/time/shared/month.rb b/spec/ruby/core/time/shared/month.rb deleted file mode 100644 index 31ca679557d7c2..00000000000000 --- a/spec/ruby/core/time/shared/month.rb +++ /dev/null @@ -1,15 +0,0 @@ -describe :time_month, shared: true do - it "returns the month of the year for a local Time" do - with_timezone("CET", 1) do - Time.local(1970, 1).send(@method).should == 1 - end - end - - it "returns the month of the year for a UTC Time" do - Time.utc(1970, 1).send(@method).should == 1 - end - - it "returns the four digit year for a Time with a fixed offset" do - Time.new(2012, 1, 1, 0, 0, 0, -3600).send(@method).should == 1 - end -end diff --git a/spec/ruby/core/time/shared/to_i.rb b/spec/ruby/core/time/shared/to_i.rb deleted file mode 100644 index 06c966b7085b1e..00000000000000 --- a/spec/ruby/core/time/shared/to_i.rb +++ /dev/null @@ -1,16 +0,0 @@ -describe :time_to_i, shared: true do - it "returns the value of time as an integer number of seconds since epoch" do - Time.at(0).send(@method).should == 0 - end - - it "doesn't return an actual number of seconds in time" do - Time.at(65.5).send(@method).should == 65 - end - - it "rounds fractional seconds toward zero" do - t = Time.utc(1960, 1, 1, 0, 0, 0, 999_999) - - t.to_f.to_i.should == -315619199 - t.to_i.should == -315619200 - end -end diff --git a/spec/ruby/core/time/shared/xmlschema.rb b/spec/ruby/core/time/shared/xmlschema.rb deleted file mode 100644 index d68c18df364b86..00000000000000 --- a/spec/ruby/core/time/shared/xmlschema.rb +++ /dev/null @@ -1,31 +0,0 @@ -describe :time_xmlschema, shared: true do - ruby_version_is "3.4" do - it "generates ISO-8601 strings in Z for UTC times" do - t = Time.utc(1985, 4, 12, 23, 20, 50, 521245) - t.send(@method).should == "1985-04-12T23:20:50Z" - t.send(@method, 2).should == "1985-04-12T23:20:50.52Z" - t.send(@method, 9).should == "1985-04-12T23:20:50.521245000Z" - end - - it "generates ISO-8601 string with timeone offset for non-UTC times" do - t = Time.new(1985, 4, 12, 23, 20, 50, "+02:00") - t.send(@method).should == "1985-04-12T23:20:50+02:00" - t.send(@method, 2).should == "1985-04-12T23:20:50.00+02:00" - end - - it "year is always at least 4 digits" do - t = Time.utc(12, 4, 12) - t.send(@method).should == "0012-04-12T00:00:00Z" - end - - it "year can be more than 4 digits" do - t = Time.utc(40_000, 4, 12) - t.send(@method).should == "40000-04-12T00:00:00Z" - end - - it "year can be negative" do - t = Time.utc(-2000, 4, 12) - t.send(@method).should == "-2000-04-12T00:00:00Z" - end - end -end diff --git a/spec/ruby/core/time/to_i_spec.rb b/spec/ruby/core/time/to_i_spec.rb index 54929d1e1884b0..00c4215d31563d 100644 --- a/spec/ruby/core/time/to_i_spec.rb +++ b/spec/ruby/core/time/to_i_spec.rb @@ -1,6 +1,18 @@ require_relative '../../spec_helper' -require_relative 'shared/to_i' describe "Time#to_i" do - it_behaves_like :time_to_i, :to_i + it "returns the value of time as an integer number of seconds since epoch" do + Time.at(0).to_i.should == 0 + end + + it "doesn't return an actual number of seconds in time" do + Time.at(65.5).to_i.should == 65 + end + + it "rounds fractional seconds toward zero" do + t = Time.utc(1960, 1, 1, 0, 0, 0, 999_999) + + t.to_f.to_i.should == -315619199 + t.to_i.should == -315619200 + end end diff --git a/spec/ruby/core/time/tv_nsec_spec.rb b/spec/ruby/core/time/tv_nsec_spec.rb index feb7998b169236..802138bf3af6cf 100644 --- a/spec/ruby/core/time/tv_nsec_spec.rb +++ b/spec/ruby/core/time/tv_nsec_spec.rb @@ -1,5 +1,7 @@ require_relative '../../spec_helper' describe "Time#tv_nsec" do - it "needs to be reviewed for spec completeness" + it "is an alias of Time#nsec" do + Time.instance_method(:tv_nsec).should == Time.instance_method(:nsec) + end end diff --git a/spec/ruby/core/time/tv_sec_spec.rb b/spec/ruby/core/time/tv_sec_spec.rb index f83e1fbfdde50e..21bdb53ee65923 100644 --- a/spec/ruby/core/time/tv_sec_spec.rb +++ b/spec/ruby/core/time/tv_sec_spec.rb @@ -1,6 +1,7 @@ require_relative '../../spec_helper' -require_relative 'shared/to_i' describe "Time#tv_sec" do - it_behaves_like :time_to_i, :tv_sec + it "is an alias of Time#to_i" do + Time.instance_method(:tv_sec).should == Time.instance_method(:to_i) + end end diff --git a/spec/ruby/core/time/tv_usec_spec.rb b/spec/ruby/core/time/tv_usec_spec.rb index f0de4f4a9c9f77..e922ee5625473f 100644 --- a/spec/ruby/core/time/tv_usec_spec.rb +++ b/spec/ruby/core/time/tv_usec_spec.rb @@ -1,5 +1,7 @@ require_relative '../../spec_helper' describe "Time#tv_usec" do - it "needs to be reviewed for spec completeness" + it "is an alias of Time#usec" do + Time.instance_method(:tv_usec).should == Time.instance_method(:usec) + end end diff --git a/spec/ruby/core/time/utc_offset_spec.rb b/spec/ruby/core/time/utc_offset_spec.rb index 17c031b8c68013..8d2fff20123609 100644 --- a/spec/ruby/core/time/utc_offset_spec.rb +++ b/spec/ruby/core/time/utc_offset_spec.rb @@ -1,6 +1,61 @@ require_relative '../../spec_helper' -require_relative 'shared/gmt_offset' describe "Time#utc_offset" do - it_behaves_like :time_gmt_offset, :utc_offset + it "returns the offset in seconds between the timezone of time and UTC" do + with_timezone("AST", 3) do + Time.new.utc_offset.should == 10800 + end + end + + it "returns 0 when the date is UTC" do + with_timezone("AST", 3) do + Time.new.utc.utc_offset.should == 0 + end + end + + platform_is_not :windows do + it "returns the correct offset for US Eastern time zone around daylight savings time change" do + # "2010-03-14 01:59:59 -0500" + 1 ==> "2010-03-14 03:00:00 -0400" + with_timezone("EST5EDT") do + t = Time.local(2010,3,14,1,59,59) + t.utc_offset.should == -5*60*60 + (t + 1).utc_offset.should == -4*60*60 + end + end + + it "returns the correct offset for Hawaii around daylight savings time change" do + # "2010-03-14 01:59:59 -1000" + 1 ==> "2010-03-14 02:00:00 -1000" + with_timezone("Pacific/Honolulu") do + t = Time.local(2010,3,14,1,59,59) + t.utc_offset.should == -10*60*60 + (t + 1).utc_offset.should == -10*60*60 + end + end + + it "returns the correct offset for New Zealand around daylight savings time change" do + # "2010-04-04 02:59:59 +1300" + 1 ==> "2010-04-04 02:00:00 +1200" + with_timezone("Pacific/Auckland") do + t = Time.local(2010,4,4,1,59,59) + (60 * 60) + t.utc_offset.should == 13*60*60 + (t + 1).utc_offset.should == 12*60*60 + end + end + end + + it "returns offset as Rational" do + Time.new(2010,4,4,1,59,59,7245).utc_offset.should == 7245 + Time.new(2010,4,4,1,59,59,7245.5).utc_offset.should == Rational(14491,2) + end + + context 'given positive offset' do + it 'returns a positive offset' do + Time.new(2013,3,17,nil,nil,nil,"+03:00").utc_offset.should == 10800 + end + end + + context 'given negative offset' do + it 'returns a negative offset' do + Time.new(2013,3,17,nil,nil,nil,"-03:00").utc_offset.should == -10800 + end + end end diff --git a/spec/ruby/core/time/utc_spec.rb b/spec/ruby/core/time/utc_spec.rb index ab3c0df657e045..35c1daa9e52f7c 100644 --- a/spec/ruby/core/time/utc_spec.rb +++ b/spec/ruby/core/time/utc_spec.rb @@ -1,6 +1,4 @@ require_relative '../../spec_helper' -require_relative 'shared/gm' -require_relative 'shared/gmtime' require_relative 'shared/time_params' describe "Time#utc?" do @@ -11,7 +9,7 @@ it "treats time as UTC what was created in different ways" do Time.now.utc.utc?.should == true - Time.now.gmtime.utc?.should == true + Time.now.utc.utc?.should == true Time.now.getgm.utc?.should == true Time.now.getutc.utc?.should == true Time.utc(2022).utc?.should == true @@ -55,12 +53,117 @@ end describe "Time.utc" do - it_behaves_like :time_gm, :utc it_behaves_like :time_params, :utc it_behaves_like :time_params_10_arg, :utc it_behaves_like :time_params_microseconds, :utc + + it "creates a time based on given values, interpreted as UTC (GMT)" do + Time.utc(2000,"jan",1,20,15,1).inspect.should == "2000-01-01 20:15:01 UTC" + end + + it "creates a time based on given C-style gmtime arguments, interpreted as UTC (GMT)" do + time = Time.utc(1, 15, 20, 1, 1, 2000, :ignored, :ignored, :ignored, :ignored) + time.inspect.should == "2000-01-01 20:15:01 UTC" + end + + it "interprets pre-Gregorian reform dates using Gregorian proleptic calendar" do + Time.utc(1582, 10, 4, 12).to_i.should == -12220200000 # 2299150j + end + + it "interprets Julian-Gregorian gap dates using Gregorian proleptic calendar" do + Time.utc(1582, 10, 14, 12).to_i.should == -12219336000 # 2299160j + end + + it "interprets post-Gregorian reform dates using Gregorian calendar" do + Time.utc(1582, 10, 15, 12).to_i.should == -12219249600 # 2299161j + end + + it "handles fractional usec close to rounding limit" do + time = Time.utc(2000, 1, 1, 12, 30, 0, 9999r/10000) + + time.usec.should == 0 + time.nsec.should == 999 + end + + guard -> { + with_timezone 'right/UTC' do + (Time.utc(1972, 6, 30, 23, 59, 59) + 1).sec == 60 + end + } do + it "handles real leap seconds in zone 'right/UTC'" do + with_timezone 'right/UTC' do + time = Time.utc(1972, 6, 30, 23, 59, 60) + + time.sec.should == 60 + time.min.should == 59 + time.hour.should == 23 + time.day.should == 30 + time.month.should == 6 + end + end + end + + it "handles bad leap seconds by carrying values forward" do + with_timezone 'UTC' do + time = Time.utc(2017, 7, 5, 23, 59, 60) + time.sec.should == 0 + time.min.should == 0 + time.hour.should == 0 + time.day.should == 6 + time.month.should == 7 + end + end + + it "handles a value of 60 for seconds by carrying values forward in zone 'UTC'" do + with_timezone 'UTC' do + time = Time.utc(1972, 6, 30, 23, 59, 60) + + time.sec.should == 0 + time.min.should == 0 + time.hour.should == 0 + time.day.should == 1 + time.month.should == 7 + end + end end describe "Time#utc" do - it_behaves_like :time_gmtime, :utc + it "converts self to UTC, modifying the receiver" do + # Testing with America/Regina here because it doesn't have DST. + with_timezone("CST", -6) do + t = Time.local(2007, 1, 9, 6, 0, 0) + t.utc + # Time#== compensates for time zones, so check all parts separately + t.year.should == 2007 + t.month.should == 1 + t.mday.should == 9 + t.hour.should == 12 + t.min.should == 0 + t.sec.should == 0 + t.zone.should == "UTC" + end + end + + it "returns self" do + with_timezone("CST", -6) do + t = Time.local(2007, 1, 9, 12, 0, 0) + t.utc.should.equal?(t) + end + end + + describe "on a frozen time" do + it "does not raise an error if already in UTC" do + time = Time.gm(2007, 1, 9, 12, 0, 0) + time.freeze + time.utc.should.equal?(time) + end + + it "raises a FrozenError if the time is not UTC" do + with_timezone("CST", -6) do + time = Time.now + time.freeze + -> { time.utc }.should.raise(FrozenError) + end + end + end end diff --git a/spec/ruby/core/time/xmlschema_spec.rb b/spec/ruby/core/time/xmlschema_spec.rb index bdf1dc7923eeaf..6a26861a456f94 100644 --- a/spec/ruby/core/time/xmlschema_spec.rb +++ b/spec/ruby/core/time/xmlschema_spec.rb @@ -1,6 +1,9 @@ require_relative '../../spec_helper' -require_relative 'shared/xmlschema' describe "Time#xmlschema" do - it_behaves_like :time_xmlschema, :xmlschema + ruby_version_is "3.4" do + it "is an alias of Time#iso8601" do + Time.instance_method(:xmlschema).should == Time.instance_method(:iso8601) + end + end end diff --git a/spec/ruby/core/true/inspect_spec.rb b/spec/ruby/core/true/inspect_spec.rb index 09d1914856a8fa..b9f5390b5c36e6 100644 --- a/spec/ruby/core/true/inspect_spec.rb +++ b/spec/ruby/core/true/inspect_spec.rb @@ -1,7 +1,7 @@ require_relative '../../spec_helper' describe "TrueClass#inspect" do - it "returns the string 'true'" do - true.inspect.should == "true" + it "is an alias of TrueClass#to_s" do + true.method(:inspect).should == true.method(:to_s) end end diff --git a/spec/ruby/core/unboundmethod/eql_spec.rb b/spec/ruby/core/unboundmethod/eql_spec.rb index cf2c6b5aae9ee9..3b299d047ab22f 100644 --- a/spec/ruby/core/unboundmethod/eql_spec.rb +++ b/spec/ruby/core/unboundmethod/eql_spec.rb @@ -1,5 +1,7 @@ require_relative '../../spec_helper' describe "UnboundMethod#eql?" do - it "needs to be reviewed for spec completeness" + it "is an alias of UnboundMethod#==" do + UnboundMethod.instance_method(:eql?).should == UnboundMethod.instance_method(:==) + end end diff --git a/spec/ruby/core/unboundmethod/inspect_spec.rb b/spec/ruby/core/unboundmethod/inspect_spec.rb index 3abed94f7f82fc..b0fcfd00ea9f4d 100644 --- a/spec/ruby/core/unboundmethod/inspect_spec.rb +++ b/spec/ruby/core/unboundmethod/inspect_spec.rb @@ -1,9 +1,7 @@ require_relative '../../spec_helper' -require_relative 'fixtures/classes' -require_relative 'shared/to_s' -require_relative '../method/shared/aliased_inspect' describe "UnboundMethod#inspect" do - it_behaves_like :unboundmethod_to_s, :inspect - it_behaves_like :method_to_s_aliased, :inspect, -> meth { meth.unbind } + it "is an alias of UnboundMethod#to_s" do + UnboundMethod.instance_method(:inspect).should == UnboundMethod.instance_method(:to_s) + end end diff --git a/spec/ruby/core/unboundmethod/shared/to_s.rb b/spec/ruby/core/unboundmethod/shared/to_s.rb deleted file mode 100644 index 848c1eba2e30ce..00000000000000 --- a/spec/ruby/core/unboundmethod/shared/to_s.rb +++ /dev/null @@ -1,33 +0,0 @@ -require_relative '../../../spec_helper' -require_relative '../fixtures/classes' - -describe :unboundmethod_to_s, shared: true do - before :each do - @from_module = UnboundMethodSpecs::Methods.instance_method(:from_mod) - @from_method = UnboundMethodSpecs::Methods.new.method(:from_mod).unbind - end - - it "returns a String" do - @from_module.send(@method).should.is_a?(String) - @from_method.send(@method).should.is_a?(String) - end - - it "the String reflects that this is an UnboundMethod object" do - @from_module.send(@method).should =~ /\bUnboundMethod\b/ - @from_method.send(@method).should =~ /\bUnboundMethod\b/ - end - - it "the String shows the method name, Module defined in and Module extracted from" do - @from_module.send(@method).should =~ /\bfrom_mod\b/ - @from_module.send(@method).should =~ /\bUnboundMethodSpecs::Mod\b/ - end - - it "returns a String including all details" do - @from_module.send(@method).should.start_with? "# meth { meth.unbind } + + before :each do + @from_module = UnboundMethodSpecs::Methods.instance_method(:from_mod) + @from_method = UnboundMethodSpecs::Methods.new.method(:from_mod).unbind + end + + it "returns a String" do + @from_module.to_s.should.is_a?(String) + @from_method.to_s.should.is_a?(String) + end + + it "the String reflects that this is an UnboundMethod object" do + @from_module.to_s.should =~ /\bUnboundMethod\b/ + @from_method.to_s.should =~ /\bUnboundMethod\b/ + end + + it "the String shows the method name, Module defined in and Module extracted from" do + @from_module.to_s.should =~ /\bfrom_mod\b/ + @from_module.to_s.should =~ /\bUnboundMethodSpecs::Mod\b/ + end + + it "returns a String including all details" do + @from_module.to_s.should.start_with? "# { bd5667 % 0 }.should.raise(ZeroDivisionError) + -> { bd5667 % BigDecimal("0") }.should.raise(ZeroDivisionError) + -> { @zero % @zero }.should.raise(ZeroDivisionError) + end end describe "BigDecimal#modulo" do - it_behaves_like :bigdecimal_modulo, :modulo - it_behaves_like :bigdecimal_modulo_zerodivisionerror, :modulo + it "is an alias of BigDecimal#%" do + BigDecimal.instance_method(:modulo).should == BigDecimal.instance_method(:%) + end end diff --git a/spec/ruby/library/bigdecimal/shared/clone.rb b/spec/ruby/library/bigdecimal/shared/clone.rb deleted file mode 100644 index 03de6d0fc3afb2..00000000000000 --- a/spec/ruby/library/bigdecimal/shared/clone.rb +++ /dev/null @@ -1,13 +0,0 @@ -require 'bigdecimal' - -describe :bigdecimal_clone, shared: true do - before :each do - @obj = BigDecimal("1.2345") - end - - it "returns self" do - copy = @obj.public_send(@method) - - copy.should.equal?(@obj) - end -end diff --git a/spec/ruby/library/bigdecimal/shared/eql.rb b/spec/ruby/library/bigdecimal/shared/eql.rb deleted file mode 100644 index 8e3e388bab2244..00000000000000 --- a/spec/ruby/library/bigdecimal/shared/eql.rb +++ /dev/null @@ -1,61 +0,0 @@ -require 'bigdecimal' - -describe :bigdecimal_eql, shared: true do - before :each do - @bg6543_21 = BigDecimal("6543.21") - @bg5667_19 = BigDecimal("5667.19") - @a = BigDecimal("1.0000000000000000000000000000000000000000005") - @b = BigDecimal("1.00000000000000000000000000000000000000000005") - @bigint = BigDecimal("1000.0") - @nan = BigDecimal("NaN") - @infinity = BigDecimal("Infinity") - @infinity_minus = BigDecimal("-Infinity") - end - - it "tests for equality" do - @bg6543_21.send(@method, @bg6543_21).should == true - @a.send(@method, @a).should == true - @a.send(@method, @b).should == false - @bg6543_21.send(@method, @a).should == false - @bigint.send(@method, 1000).should == true - end - - it "returns false for NaN as it is never equal to any number" do - @nan.send(@method, @nan).should == false - @a.send(@method, @nan).should == false - @nan.send(@method, @a).should == false - @nan.send(@method, @infinity).should == false - @nan.send(@method, @infinity_minus).should == false - @infinity.send(@method, @nan).should == false - @infinity_minus.send(@method, @nan).should == false - end - - it "returns true for infinity values with the same sign" do - @infinity.send(@method, @infinity).should == true - @infinity.send(@method, BigDecimal("Infinity")).should == true - BigDecimal("Infinity").send(@method, @infinity).should == true - - @infinity_minus.send(@method, @infinity_minus).should == true - @infinity_minus.send(@method, BigDecimal("-Infinity")).should == true - BigDecimal("-Infinity").send(@method, @infinity_minus).should == true - end - - it "returns false for infinity values with different signs" do - @infinity.send(@method, @infinity_minus).should == false - @infinity_minus.send(@method, @infinity).should == false - end - - it "returns false when infinite value compared to finite one" do - @infinity.send(@method, @a).should == false - @infinity_minus.send(@method, @a).should == false - - @a.send(@method, @infinity).should == false - @a.send(@method, @infinity_minus).should == false - end - - it "returns false when compared objects that can not be coerced into BigDecimal" do - @infinity.send(@method, nil).should == false - @bigint.send(@method, nil).should == false - @nan.send(@method, nil).should == false - end -end diff --git a/spec/ruby/library/bigdecimal/shared/modulo.rb b/spec/ruby/library/bigdecimal/shared/modulo.rb index 63470d0977e52b..5b5e3503c46c53 100644 --- a/spec/ruby/library/bigdecimal/shared/modulo.rb +++ b/spec/ruby/library/bigdecimal/shared/modulo.rb @@ -119,13 +119,3 @@ }.should.raise(TypeError) end end - -describe :bigdecimal_modulo_zerodivisionerror, shared: true do - it "raises ZeroDivisionError if other is zero" do - bd5667 = BigDecimal("5667.19") - - -> { bd5667.send(@method, 0) }.should.raise(ZeroDivisionError) - -> { bd5667.send(@method, BigDecimal("0")) }.should.raise(ZeroDivisionError) - -> { @zero.send(@method, @zero) }.should.raise(ZeroDivisionError) - end -end diff --git a/spec/ruby/library/bigdecimal/shared/to_int.rb b/spec/ruby/library/bigdecimal/shared/to_int.rb deleted file mode 100644 index 3c9f3b4f97ae31..00000000000000 --- a/spec/ruby/library/bigdecimal/shared/to_int.rb +++ /dev/null @@ -1,16 +0,0 @@ -require 'bigdecimal' - -describe :bigdecimal_to_int, shared: true do - it "raises FloatDomainError if BigDecimal is infinity or NaN" do - -> { BigDecimal("Infinity").send(@method) }.should.raise(FloatDomainError) - -> { BigDecimal("NaN").send(@method) }.should.raise(FloatDomainError) - end - - it "returns Integer otherwise" do - BigDecimal("3E-20001").send(@method).should == 0 - BigDecimal("2E4000").send(@method).should == 2 * 10 ** 4000 - BigDecimal("2").send(@method).should == 2 - BigDecimal("2E10").send(@method).should == 20000000000 - BigDecimal("3.14159").send(@method).should == 3 - end -end diff --git a/spec/ruby/library/bigdecimal/to_i_spec.rb b/spec/ruby/library/bigdecimal/to_i_spec.rb index e5e65c562e3618..b6d9e43e5710fb 100644 --- a/spec/ruby/library/bigdecimal/to_i_spec.rb +++ b/spec/ruby/library/bigdecimal/to_i_spec.rb @@ -1,7 +1,17 @@ require_relative '../../spec_helper' -require_relative 'shared/to_int' require 'bigdecimal' describe "BigDecimal#to_i" do - it_behaves_like :bigdecimal_to_int, :to_i + it "raises FloatDomainError if BigDecimal is infinity or NaN" do + -> { BigDecimal("Infinity").to_i }.should.raise(FloatDomainError) + -> { BigDecimal("NaN").to_i }.should.raise(FloatDomainError) + end + + it "returns Integer otherwise" do + BigDecimal("3E-20001").to_i.should == 0 + BigDecimal("2E4000").to_i.should == 2 * 10 ** 4000 + BigDecimal("2").to_i.should == 2 + BigDecimal("2E10").to_i.should == 20000000000 + BigDecimal("3.14159").to_i.should == 3 + end end diff --git a/spec/ruby/library/bigdecimal/to_int_spec.rb b/spec/ruby/library/bigdecimal/to_int_spec.rb index 4df674984525ff..18f3620f5e5f9f 100644 --- a/spec/ruby/library/bigdecimal/to_int_spec.rb +++ b/spec/ruby/library/bigdecimal/to_int_spec.rb @@ -1,8 +1,8 @@ require_relative '../../spec_helper' -require_relative 'shared/to_int' require 'bigdecimal' - describe "BigDecimal#to_int" do - it_behaves_like :bigdecimal_to_int, :to_int + it "is an alias of BigDecimal#to_i" do + BigDecimal.instance_method(:to_int).should == BigDecimal.instance_method(:to_i) + end end diff --git a/spec/ruby/library/date/asctime_spec.rb b/spec/ruby/library/date/asctime_spec.rb index 67d158cc018539..e268ffe09811c9 100644 --- a/spec/ruby/library/date/asctime_spec.rb +++ b/spec/ruby/library/date/asctime_spec.rb @@ -2,5 +2,8 @@ require 'date' describe "Date#asctime" do - it "needs to be reviewed for spec completeness" + it "returns a canonical string representation of date" do + d = Date.today + d.asctime.should == d.strftime("%a %b %e %H:%M:%S %Y") + end end diff --git a/spec/ruby/library/date/commercial_spec.rb b/spec/ruby/library/date/commercial_spec.rb index d7fc34d74a6357..8e2df79b90ae92 100644 --- a/spec/ruby/library/date/commercial_spec.rb +++ b/spec/ruby/library/date/commercial_spec.rb @@ -1,12 +1,5 @@ require 'date' require_relative '../../spec_helper' -require_relative 'shared/commercial' - -describe "Date#commercial" do - - it_behaves_like :date_commercial, :commercial - -end # reference: # October 1582 (the Gregorian calendar, Civil Date) @@ -15,3 +8,42 @@ # 17 18 19 20 21 22 23 # 24 25 26 27 28 29 30 # 31 +describe "Date.commercial" do + it "creates a Date for Julian Day Number day 0 by default" do + d = Date.commercial + d.year.should == -4712 + d.month.should == 1 + d.day.should == 1 + end + + it "creates a Date for the monday in the year and week given" do + d = Date.commercial(2000, 1) + d.year.should == 2000 + d.month.should == 1 + d.day.should == 3 + d.cwday.should == 1 + end + + it "creates a Date for the correct day given the year, week and day number" do + d = Date.commercial(2004, 1, 1) + d.year.should == 2003 + d.month.should == 12 + d.day.should == 29 + d.cwday.should == 1 + d.cweek.should == 1 + d.cwyear.should == 2004 + end + + it "creates only Date objects for valid weeks" do + -> { Date.commercial(2004, 53, 1) }.should_not.raise(ArgumentError) + -> { Date.commercial(2004, 53, 0) }.should.raise(ArgumentError) + -> { Date.commercial(2004, 53, 8) }.should.raise(ArgumentError) + -> { Date.commercial(2004, 54, 1) }.should.raise(ArgumentError) + -> { Date.commercial(2004, 0, 1) }.should.raise(ArgumentError) + + -> { Date.commercial(2003, 52, 1) }.should_not.raise(ArgumentError) + -> { Date.commercial(2003, 53, 1) }.should.raise(ArgumentError) + -> { Date.commercial(2003, 52, 0) }.should.raise(ArgumentError) + -> { Date.commercial(2003, 52, 8) }.should.raise(ArgumentError) + end +end diff --git a/spec/ruby/library/date/ctime_spec.rb b/spec/ruby/library/date/ctime_spec.rb index 3faa7c63805126..330076a735f848 100644 --- a/spec/ruby/library/date/ctime_spec.rb +++ b/spec/ruby/library/date/ctime_spec.rb @@ -2,5 +2,7 @@ require 'date' describe "Date#ctime" do - it "needs to be reviewed for spec completeness" + it "is an alias of Date#asctime" do + Date.instance_method(:ctime).should == Date.instance_method(:asctime) + end end diff --git a/spec/ruby/library/date/jd_spec.rb b/spec/ruby/library/date/jd_spec.rb index 336b783e8da0ae..e5cfe10eff4c94 100644 --- a/spec/ruby/library/date/jd_spec.rb +++ b/spec/ruby/library/date/jd_spec.rb @@ -1,15 +1,22 @@ require_relative '../../spec_helper' -require_relative 'shared/jd' require 'date' describe "Date#jd" do - it "determines the Julian day for a Date object" do Date.civil(2008, 1, 16).jd.should == 2454482 end - end describe "Date.jd" do - it_behaves_like :date_jd, :jd + it "constructs a Date object if passed a Julian day" do + Date.jd(2454482).should == Date.civil(2008, 1, 16) + end + + it "returns a Date object representing Julian day 0 (-4712-01-01) if no arguments passed" do + Date.jd.should == Date.civil(-4712, 1, 1) + end + + it "constructs a Date object if passed a negative number" do + Date.jd(-1).should == Date.civil(-4713, 12, 31) + end end diff --git a/spec/ruby/library/date/mday_spec.rb b/spec/ruby/library/date/mday_spec.rb index 53f6f981693141..32fd8fc7f181da 100644 --- a/spec/ruby/library/date/mday_spec.rb +++ b/spec/ruby/library/date/mday_spec.rb @@ -2,5 +2,7 @@ require 'date' describe "Date#mday" do - it "needs to be reviewed for spec completeness" + it "is an alias of Date#day" do + Date.instance_method(:mday).should == Date.instance_method(:day) + end end diff --git a/spec/ruby/library/date/mon_spec.rb b/spec/ruby/library/date/mon_spec.rb index 616d72cf882492..15754ffb1ffae4 100644 --- a/spec/ruby/library/date/mon_spec.rb +++ b/spec/ruby/library/date/mon_spec.rb @@ -1,7 +1,8 @@ require_relative '../../spec_helper' -require_relative 'shared/month' require 'date' describe "Date#mon" do - it_behaves_like :date_month, :mon + it "is an alias of Date#month" do + Date.instance_method(:mon).should == Date.instance_method(:month) + end end diff --git a/spec/ruby/library/date/month_spec.rb b/spec/ruby/library/date/month_spec.rb index f493ec81192fff..e040f9a94c9fe0 100644 --- a/spec/ruby/library/date/month_spec.rb +++ b/spec/ruby/library/date/month_spec.rb @@ -1,7 +1,9 @@ require_relative '../../spec_helper' -require_relative 'shared/month' require 'date' describe "Date#month" do - it_behaves_like :date_month, :month + it "returns the month" do + m = Date.new(2000, 7, 1).month + m.should == 7 + end end diff --git a/spec/ruby/library/date/ordinal_spec.rb b/spec/ruby/library/date/ordinal_spec.rb index ec490fd49c7d13..c8bf715163aa92 100644 --- a/spec/ruby/library/date/ordinal_spec.rb +++ b/spec/ruby/library/date/ordinal_spec.rb @@ -1,7 +1,17 @@ require 'date' require_relative '../../spec_helper' -require_relative 'shared/ordinal' describe "Date.ordinal" do - it_behaves_like :date_ordinal, :ordinal + it "constructs a Date object from an ordinal date" do + # October 1582 (the Gregorian calendar, Ordinal Date) + # S M Tu W Th F S + # 274 275 276 277 278 279 + # 280 281 282 283 284 285 286 + # 287 288 289 290 291 292 293 + # 294 + Date.ordinal(1582, 274).should == Date.civil(1582, 10, 1) + Date.ordinal(1582, 277).should == Date.civil(1582, 10, 4) + Date.ordinal(1582, 278).should == Date.civil(1582, 10, 15) + Date.ordinal(1582, 287, Date::ENGLAND).should == Date.civil(1582, 10, 14, Date::ENGLAND) + end end diff --git a/spec/ruby/library/date/shared/commercial.rb b/spec/ruby/library/date/shared/commercial.rb deleted file mode 100644 index f53d83235aa0b6..00000000000000 --- a/spec/ruby/library/date/shared/commercial.rb +++ /dev/null @@ -1,39 +0,0 @@ -describe :date_commercial, shared: true do - it "creates a Date for Julian Day Number day 0 by default" do - d = Date.send(@method) - d.year.should == -4712 - d.month.should == 1 - d.day.should == 1 - end - - it "creates a Date for the monday in the year and week given" do - d = Date.send(@method, 2000, 1) - d.year.should == 2000 - d.month.should == 1 - d.day.should == 3 - d.cwday.should == 1 - end - - it "creates a Date for the correct day given the year, week and day number" do - d = Date.send(@method, 2004, 1, 1) - d.year.should == 2003 - d.month.should == 12 - d.day.should == 29 - d.cwday.should == 1 - d.cweek.should == 1 - d.cwyear.should == 2004 - end - - it "creates only Date objects for valid weeks" do - -> { Date.send(@method, 2004, 53, 1) }.should_not.raise(ArgumentError) - -> { Date.send(@method, 2004, 53, 0) }.should.raise(ArgumentError) - -> { Date.send(@method, 2004, 53, 8) }.should.raise(ArgumentError) - -> { Date.send(@method, 2004, 54, 1) }.should.raise(ArgumentError) - -> { Date.send(@method, 2004, 0, 1) }.should.raise(ArgumentError) - - -> { Date.send(@method, 2003, 52, 1) }.should_not.raise(ArgumentError) - -> { Date.send(@method, 2003, 53, 1) }.should.raise(ArgumentError) - -> { Date.send(@method, 2003, 52, 0) }.should.raise(ArgumentError) - -> { Date.send(@method, 2003, 52, 8) }.should.raise(ArgumentError) - end -end diff --git a/spec/ruby/library/date/shared/jd.rb b/spec/ruby/library/date/shared/jd.rb deleted file mode 100644 index 511557b4f7e30b..00000000000000 --- a/spec/ruby/library/date/shared/jd.rb +++ /dev/null @@ -1,14 +0,0 @@ -describe :date_jd, shared: true do - it "constructs a Date object if passed a Julian day" do - Date.send(@method, 2454482).should == Date.civil(2008, 1, 16) - end - - it "returns a Date object representing Julian day 0 (-4712-01-01) if no arguments passed" do - Date.send(@method).should == Date.civil(-4712, 1, 1) - end - - it "constructs a Date object if passed a negative number" do - Date.send(@method, -1).should == Date.civil(-4713, 12, 31) - end - -end diff --git a/spec/ruby/library/date/shared/month.rb b/spec/ruby/library/date/shared/month.rb deleted file mode 100644 index 5fcb2cbeb0cead..00000000000000 --- a/spec/ruby/library/date/shared/month.rb +++ /dev/null @@ -1,6 +0,0 @@ -describe :date_month, shared: true do - it "returns the month" do - m = Date.new(2000, 7, 1).send(@method) - m.should == 7 - end -end diff --git a/spec/ruby/library/date/shared/ordinal.rb b/spec/ruby/library/date/shared/ordinal.rb deleted file mode 100644 index 4b182d5a25302a..00000000000000 --- a/spec/ruby/library/date/shared/ordinal.rb +++ /dev/null @@ -1,22 +0,0 @@ -# reference: -# October 1582 (the Gregorian calendar, Civil Date) -# S M Tu W Th F S -# 1 2 3 4 15 16 -# 17 18 19 20 21 22 23 -# 24 25 26 27 28 29 30 -# 31 - -describe :date_ordinal, shared: true do - it "constructs a Date object from an ordinal date" do - # October 1582 (the Gregorian calendar, Ordinal Date) - # S M Tu W Th F S - # 274 275 276 277 278 279 - # 280 281 282 283 284 285 286 - # 287 288 289 290 291 292 293 - # 294 - Date.send(@method, 1582, 274).should == Date.civil(1582, 10, 1) - Date.send(@method, 1582, 277).should == Date.civil(1582, 10, 4) - Date.send(@method, 1582, 278).should == Date.civil(1582, 10, 15) - Date.send(@method, 1582, 287, Date::ENGLAND).should == Date.civil(1582, 10, 14, Date::ENGLAND) - end -end diff --git a/spec/ruby/library/date/shared/valid_civil.rb b/spec/ruby/library/date/shared/valid_civil.rb deleted file mode 100644 index 425fee4d2d7fb0..00000000000000 --- a/spec/ruby/library/date/shared/valid_civil.rb +++ /dev/null @@ -1,36 +0,0 @@ -describe :date_valid_civil?, shared: true do - - # reference: - # October 1582 (the Gregorian calendar, Civil Date) - # S M Tu W Th F S - # 1 2 3 4 15 16 - # 17 18 19 20 21 22 23 - # 24 25 26 27 28 29 30 - # 31 - - it "returns true if it is a valid civil date" do - Date.send(@method, 1582, 10, 15).should == true - Date.send(@method, 1582, 10, 14, Date::ENGLAND).should == true - end - - it "returns false if it is not a valid civil date" do - Date.send(@method, 1582, 10, 14).should == false - end - - it "handles negative months and days" do - # October 1582 (the Gregorian calendar, Civil Date) - # S M Tu W Th F S - # -21 -20 -19 -18 -17 -16 - # -15 -14 -13 -12 -11 -10 -9 - # -8 -7 -6 -5 -4 -3 -2 - # -1 - Date.send(@method, 1582, -3, -22).should == false - Date.send(@method, 1582, -3, -21).should == true - Date.send(@method, 1582, -3, -18).should == true - Date.send(@method, 1582, -3, -17).should == true - - Date.send(@method, 2007, -11, -10).should == true - Date.send(@method, 2008, -11, -10).should == true - end - -end diff --git a/spec/ruby/library/date/shared/valid_commercial.rb b/spec/ruby/library/date/shared/valid_commercial.rb deleted file mode 100644 index 573b851fdd4c88..00000000000000 --- a/spec/ruby/library/date/shared/valid_commercial.rb +++ /dev/null @@ -1,34 +0,0 @@ -describe :date_valid_commercial?, shared: true do - - it "returns true if it is a valid commercial date" do - # October 1582 (the Gregorian calendar, Commercial Date) - # M Tu W Th F Sa Su - # 39: 1 2 3 4 5 6 7 - # 40: 1 2 3 4 5 6 7 - # 41: 1 2 3 4 5 6 7 - Date.send(@method, 1582, 39, 4).should == true - Date.send(@method, 1582, 39, 5).should == true - Date.send(@method, 1582, 41, 4).should == true - Date.send(@method, 1582, 41, 5).should == true - Date.send(@method, 1582, 41, 4, Date::ENGLAND).should == true - Date.send(@method, 1752, 37, 4, Date::ENGLAND).should == true - end - - it "returns false it is not a valid commercial date" do - Date.send(@method, 1999, 53, 1).should == false - end - - it "handles negative week and day numbers" do - # October 1582 (the Gregorian calendar, Commercial Date) - # M Tu W Th F Sa Su - # -12: -7 -6 -5 -4 -3 -2 -1 - # -11: -7 -6 -5 -4 -3 -2 -1 - # -10: -7 -6 -5 -4 -3 -2 -1 - Date.send(@method, 1582, -12, -4).should == true - Date.send(@method, 1582, -12, -3).should == true - Date.send(@method, 2007, -44, -2).should == true - Date.send(@method, 2008, -44, -2).should == true - Date.send(@method, 1999, -53, -1).should == false - end - -end diff --git a/spec/ruby/library/date/shared/valid_jd.rb b/spec/ruby/library/date/shared/valid_jd.rb deleted file mode 100644 index 0c01710208efe8..00000000000000 --- a/spec/ruby/library/date/shared/valid_jd.rb +++ /dev/null @@ -1,20 +0,0 @@ -describe :date_valid_jd?, shared: true do - it "returns true if passed a number value" do - Date.send(@method, -100).should == true - Date.send(@method, 100.0).should == true - Date.send(@method, 2**100).should == true - Date.send(@method, Rational(1,2)).should == true - end - - it "returns false if passed nil" do - Date.send(@method, nil).should == false - end - - it "returns false if passed symbol" do - Date.send(@method, :number).should == false - end - - it "returns false if passed false" do - Date.send(@method, false).should == false - end -end diff --git a/spec/ruby/library/date/shared/valid_ordinal.rb b/spec/ruby/library/date/shared/valid_ordinal.rb deleted file mode 100644 index 1ed961be231d53..00000000000000 --- a/spec/ruby/library/date/shared/valid_ordinal.rb +++ /dev/null @@ -1,26 +0,0 @@ -describe :date_valid_ordinal?, shared: true do - it "determines if the date is a valid ordinal date" do - # October 1582 (the Gregorian calendar, Ordinal Date) - # S M Tu W Th F S - # 274 275 276 277 278 279 - # 280 281 282 283 284 285 286 - # 287 288 289 290 291 292 293 - # 294 - Date.send(@method, 1582, 277).should == true - Date.send(@method, 1582, 278).should == true - Date.send(@method, 1582, 287).should == true - Date.send(@method, 1582, 288).should == true - end - - it "handles negative day numbers" do - # October 1582 (the Gregorian calendar, Ordinal Date) - # S M Tu W Th F S - # -82 -81 -80 -79 -78 -77 - # -76 -75 -74 -73 -72 -71 -70 - # -69 -68 -67 -66 -65 -64 -63 - # -62 - Date.send(@method, 1582, -79).should == true - Date.send(@method, 1582, -78).should == true - Date.send(@method, 2007, -100).should == true - end -end diff --git a/spec/ruby/library/date/succ_spec.rb b/spec/ruby/library/date/succ_spec.rb index c4a902aa63c419..0b14d3bb73f130 100644 --- a/spec/ruby/library/date/succ_spec.rb +++ b/spec/ruby/library/date/succ_spec.rb @@ -2,5 +2,7 @@ require 'date' describe "Date#succ" do - it "needs to be reviewed for spec completeness" + it "is an alias of Date#next" do + Date.instance_method(:succ).should == Date.instance_method(:next) + end end diff --git a/spec/ruby/library/date/valid_civil_spec.rb b/spec/ruby/library/date/valid_civil_spec.rb index 00f2c57205d04f..8cffc80310e704 100644 --- a/spec/ruby/library/date/valid_civil_spec.rb +++ b/spec/ruby/library/date/valid_civil_spec.rb @@ -1,9 +1,8 @@ require_relative '../../spec_helper' -require_relative 'shared/valid_civil' require 'date' -describe "Date#valid_civil?" do - - it_behaves_like :date_valid_civil?, :valid_civil? - +describe "Date.valid_civil?" do + it "is an alias of Date.valid_date?" do + Date.method(:valid_civil?).should == Date.method(:valid_date?) + end end diff --git a/spec/ruby/library/date/valid_commercial_spec.rb b/spec/ruby/library/date/valid_commercial_spec.rb index 7e96782b6b8fa9..21a91ad867094e 100644 --- a/spec/ruby/library/date/valid_commercial_spec.rb +++ b/spec/ruby/library/date/valid_commercial_spec.rb @@ -1,8 +1,35 @@ require_relative '../../spec_helper' -require_relative 'shared/valid_commercial' require 'date' -describe "Date#valid_commercial?" do +describe "Date.valid_commercial?" do + it "returns true if it is a valid commercial date" do + # October 1582 (the Gregorian calendar, Commercial Date) + # M Tu W Th F Sa Su + # 39: 1 2 3 4 5 6 7 + # 40: 1 2 3 4 5 6 7 + # 41: 1 2 3 4 5 6 7 + Date.valid_commercial?(1582, 39, 4).should == true + Date.valid_commercial?(1582, 39, 5).should == true + Date.valid_commercial?(1582, 41, 4).should == true + Date.valid_commercial?(1582, 41, 5).should == true + Date.valid_commercial?(1582, 41, 4, Date::ENGLAND).should == true + Date.valid_commercial?(1752, 37, 4, Date::ENGLAND).should == true + end - it_behaves_like :date_valid_commercial?, :valid_commercial? + it "returns false it is not a valid commercial date" do + Date.valid_commercial?(1999, 53, 1).should == false + end + + it "handles negative week and day numbers" do + # October 1582 (the Gregorian calendar, Commercial Date) + # M Tu W Th F Sa Su + # -12: -7 -6 -5 -4 -3 -2 -1 + # -11: -7 -6 -5 -4 -3 -2 -1 + # -10: -7 -6 -5 -4 -3 -2 -1 + Date.valid_commercial?(1582, -12, -4).should == true + Date.valid_commercial?(1582, -12, -3).should == true + Date.valid_commercial?(2007, -44, -2).should == true + Date.valid_commercial?(2008, -44, -2).should == true + Date.valid_commercial?(1999, -53, -1).should == false + end end diff --git a/spec/ruby/library/date/valid_date_spec.rb b/spec/ruby/library/date/valid_date_spec.rb index f12a71d9665e7a..f0d5ec7b4d6999 100644 --- a/spec/ruby/library/date/valid_date_spec.rb +++ b/spec/ruby/library/date/valid_date_spec.rb @@ -1,7 +1,36 @@ require_relative '../../spec_helper' -require_relative 'shared/valid_civil' require 'date' -describe "Date#valid_date?" do - it_behaves_like :date_valid_civil?, :valid_date? +describe "Date.valid_date?" do + # reference: + # October 1582 (the Gregorian calendar, Civil Date) + # S M Tu W Th F S + # 1 2 3 4 15 16 + # 17 18 19 20 21 22 23 + # 24 25 26 27 28 29 30 + # 31 + it "returns true if it is a valid civil date" do + Date.valid_date?(1582, 10, 15).should == true + Date.valid_date?(1582, 10, 14, Date::ENGLAND).should == true + end + + it "returns false if it is not a valid civil date" do + Date.valid_date?(1582, 10, 14).should == false + end + + it "handles negative months and days" do + # October 1582 (the Gregorian calendar, Civil Date) + # S M Tu W Th F S + # -21 -20 -19 -18 -17 -16 + # -15 -14 -13 -12 -11 -10 -9 + # -8 -7 -6 -5 -4 -3 -2 + # -1 + Date.valid_date?(1582, -3, -22).should == false + Date.valid_date?(1582, -3, -21).should == true + Date.valid_date?(1582, -3, -18).should == true + Date.valid_date?(1582, -3, -17).should == true + + Date.valid_date?(2007, -11, -10).should == true + Date.valid_date?(2008, -11, -10).should == true + end end diff --git a/spec/ruby/library/date/valid_jd_spec.rb b/spec/ruby/library/date/valid_jd_spec.rb index aecaaabcf47993..46f22de4972514 100644 --- a/spec/ruby/library/date/valid_jd_spec.rb +++ b/spec/ruby/library/date/valid_jd_spec.rb @@ -1,9 +1,23 @@ require_relative '../../spec_helper' -require_relative 'shared/valid_jd' require 'date' describe "Date.valid_jd?" do + it "returns true if passed a number value" do + Date.valid_jd?(-100).should == true + Date.valid_jd?(100.0).should == true + Date.valid_jd?(2**100).should == true + Date.valid_jd?(Rational(1,2)).should == true + end - it_behaves_like :date_valid_jd?, :valid_jd? + it "returns false if passed nil" do + Date.valid_jd?(nil).should == false + end + it "returns false if passed symbol" do + Date.valid_jd?(:number).should == false + end + + it "returns false if passed false" do + Date.valid_jd?(false).should == false + end end diff --git a/spec/ruby/library/date/valid_ordinal_spec.rb b/spec/ruby/library/date/valid_ordinal_spec.rb index 58d548c7043cee..bb5c259606b056 100644 --- a/spec/ruby/library/date/valid_ordinal_spec.rb +++ b/spec/ruby/library/date/valid_ordinal_spec.rb @@ -1,9 +1,29 @@ require_relative '../../spec_helper' -require_relative 'shared/valid_ordinal' require 'date' describe "Date.valid_ordinal?" do + it "determines if the date is a valid ordinal date" do + # October 1582 (the Gregorian calendar, Ordinal Date) + # S M Tu W Th F S + # 274 275 276 277 278 279 + # 280 281 282 283 284 285 286 + # 287 288 289 290 291 292 293 + # 294 + Date.valid_ordinal?(1582, 277).should == true + Date.valid_ordinal?(1582, 278).should == true + Date.valid_ordinal?(1582, 287).should == true + Date.valid_ordinal?(1582, 288).should == true + end - it_behaves_like :date_valid_ordinal?, :valid_ordinal? - + it "handles negative day numbers" do + # October 1582 (the Gregorian calendar, Ordinal Date) + # S M Tu W Th F S + # -82 -81 -80 -79 -78 -77 + # -76 -75 -74 -73 -72 -71 -70 + # -69 -68 -67 -66 -65 -64 -63 + # -62 + Date.valid_ordinal?(1582, -79).should == true + Date.valid_ordinal?(1582, -78).should == true + Date.valid_ordinal?(2007, -100).should == true + end end diff --git a/spec/ruby/library/datetime/hour_spec.rb b/spec/ruby/library/datetime/hour_spec.rb index 383a85fe6027b5..5d8e75edcb3dca 100644 --- a/spec/ruby/library/datetime/hour_spec.rb +++ b/spec/ruby/library/datetime/hour_spec.rb @@ -35,8 +35,7 @@ end it "raises an error for hour fractions smaller than -24" do - -> { new_datetime(hour: -24 - Rational(1,2)) }.should( - raise_error(ArgumentError)) + -> { new_datetime(hour: -24 - Rational(1,2)) }.should.raise(ArgumentError) end it "adds 1 to day, when 24 hours given" do diff --git a/spec/ruby/library/datetime/iso8601_spec.rb b/spec/ruby/library/datetime/iso8601_spec.rb index 457881277a7c25..4368300fd5f2b0 100644 --- a/spec/ruby/library/datetime/iso8601_spec.rb +++ b/spec/ruby/library/datetime/iso8601_spec.rb @@ -6,5 +6,7 @@ end describe "DateTime#iso8601" do - it "needs to be reviewed for spec completeness" + it "is an alias of DateTime#isoxmlschema8601" do + DateTime.instance_method(:iso8601).should == DateTime.instance_method(:xmlschema) + end end diff --git a/spec/ruby/library/datetime/min_spec.rb b/spec/ruby/library/datetime/min_spec.rb index a1eaa214cb95a4..ca995a7eede89c 100644 --- a/spec/ruby/library/datetime/min_spec.rb +++ b/spec/ruby/library/datetime/min_spec.rb @@ -1,6 +1,8 @@ require_relative '../../spec_helper' -require_relative 'shared/min' +require 'date' -describe "DateTime.min" do - it_behaves_like :datetime_min, :min +describe "DateTime#min" do + it "is an alias of DateTime#minute" do + DateTime.instance_method(:min).should == DateTime.instance_method(:minute) + end end diff --git a/spec/ruby/library/datetime/minute_spec.rb b/spec/ruby/library/datetime/minute_spec.rb index acdfeda345f0ac..6e99752de71cdf 100644 --- a/spec/ruby/library/datetime/minute_spec.rb +++ b/spec/ruby/library/datetime/minute_spec.rb @@ -1,6 +1,40 @@ require_relative '../../spec_helper' -require_relative 'shared/min' +require 'date' -describe "DateTime.minute" do - it_behaves_like :datetime_min, :minute +describe "DateTime#minute" do + it "returns 0 if no argument is passed" do + DateTime.new.minute.should == 0 + end + + it "returns the minute passed as argument" do + new_datetime(minute: 5).minute.should == 5 + end + + it "adds 60 to negative minutes" do + new_datetime(minute: -20).minute.should == 40 + end + + it "raises an error for Rational" do + -> { new_datetime minute: 5 + Rational(1,2) }.should.raise(ArgumentError) + end + + it "raises an error for Float" do + -> { new_datetime minute: 5.5 }.should.raise(ArgumentError) + end + + it "raises an error for Rational" do + -> { new_datetime(hour: 2 + Rational(1,2)) }.should.raise(ArgumentError) + end + + it "raises an error, when the minute is smaller than -60" do + -> { new_datetime(minute: -61) }.should.raise(ArgumentError) + end + + it "raises an error, when the minute is greater or equal than 60" do + -> { new_datetime(minute: 60) }.should.raise(ArgumentError) + end + + it "raises an error for minute fractions smaller than -60" do + -> { new_datetime(minute: -60 - Rational(1,2))}.should.raise(ArgumentError) + end end diff --git a/spec/ruby/library/datetime/sec_spec.rb b/spec/ruby/library/datetime/sec_spec.rb index f681283c8e44c1..f8a8b4646e4505 100644 --- a/spec/ruby/library/datetime/sec_spec.rb +++ b/spec/ruby/library/datetime/sec_spec.rb @@ -1,6 +1,8 @@ require_relative '../../spec_helper' -require_relative 'shared/sec' +require 'date' -describe "DateTime.sec" do - it_behaves_like :datetime_sec, :sec +describe "DateTime#sec" do + it "is an alias of DateTime#second" do + DateTime.instance_method(:sec).should == DateTime.instance_method(:second) + end end diff --git a/spec/ruby/library/datetime/second_fraction_spec.rb b/spec/ruby/library/datetime/second_fraction_spec.rb index d5393149ba4b85..70f5abf56015dd 100644 --- a/spec/ruby/library/datetime/second_fraction_spec.rb +++ b/spec/ruby/library/datetime/second_fraction_spec.rb @@ -2,5 +2,7 @@ require 'date' describe "DateTime#second_fraction" do - it "needs to be reviewed for spec completeness" + it "is an alias of DateTime#sec_fraction" do + DateTime.instance_method(:second_fraction).should == DateTime.instance_method(:sec_fraction) + end end diff --git a/spec/ruby/library/datetime/second_spec.rb b/spec/ruby/library/datetime/second_spec.rb index 545c3f91090cd8..9fb1965b73aa19 100644 --- a/spec/ruby/library/datetime/second_spec.rb +++ b/spec/ruby/library/datetime/second_spec.rb @@ -1,6 +1,45 @@ require_relative '../../spec_helper' -require_relative 'shared/sec' +require 'date' describe "DateTime#second" do - it_behaves_like :datetime_sec, :second + it "returns 0 seconds if passed no arguments" do + d = DateTime.new + d.second.should == 0 + end + + it "returns the seconds passed in the arguments" do + new_datetime(second: 5).second.should == 5 + end + + it "adds 60 to negative values" do + new_datetime(second: -20).second.should == 40 + end + + it "returns the absolute value of a Rational" do + new_datetime(second: 5 + Rational(1,2)).second.should == 5 + end + + it "returns the absolute value of a float" do + new_datetime(second: 5.5).second.should == 5 + end + + it "raises an error when minute is given as a rational" do + -> { new_datetime(minute: 5 + Rational(1,2)) }.should.raise(ArgumentError) + end + + it "raises an error, when the second is smaller than -60" do + -> { new_datetime(second: -61) }.should.raise(ArgumentError) + end + + it "raises an error, when the second is greater or equal than 60" do + -> { new_datetime(second: 60) }.should.raise(ArgumentError) + end + + it "raises an error for second fractions smaller than -60" do + -> { new_datetime(second: -60 - Rational(1,2))}.should.raise(ArgumentError) + end + + it "takes a second fraction near 60" do + new_datetime(second: 59 + Rational(1,2)).second.should == 59 + end end diff --git a/spec/ruby/library/datetime/shared/min.rb b/spec/ruby/library/datetime/shared/min.rb deleted file mode 100644 index 04e5f3457a7397..00000000000000 --- a/spec/ruby/library/datetime/shared/min.rb +++ /dev/null @@ -1,40 +0,0 @@ -require 'date' - -describe :datetime_min, shared: true do - it "returns 0 if no argument is passed" do - DateTime.new.send(@method).should == 0 - end - - it "returns the minute passed as argument" do - new_datetime(minute: 5).send(@method).should == 5 - end - - it "adds 60 to negative minutes" do - new_datetime(minute: -20).send(@method).should == 40 - end - - it "raises an error for Rational" do - -> { new_datetime minute: 5 + Rational(1,2) }.should.raise(ArgumentError) - end - - it "raises an error for Float" do - -> { new_datetime minute: 5.5 }.should.raise(ArgumentError) - end - - it "raises an error for Rational" do - -> { new_datetime(hour: 2 + Rational(1,2)) }.should.raise(ArgumentError) - end - - it "raises an error, when the minute is smaller than -60" do - -> { new_datetime(minute: -61) }.should.raise(ArgumentError) - end - - it "raises an error, when the minute is greater or equal than 60" do - -> { new_datetime(minute: 60) }.should.raise(ArgumentError) - end - - it "raises an error for minute fractions smaller than -60" do - -> { new_datetime(minute: -60 - Rational(1,2))}.should( - raise_error(ArgumentError)) - end -end diff --git a/spec/ruby/library/datetime/shared/sec.rb b/spec/ruby/library/datetime/shared/sec.rb deleted file mode 100644 index 5af5db4fb2cd8d..00000000000000 --- a/spec/ruby/library/datetime/shared/sec.rb +++ /dev/null @@ -1,45 +0,0 @@ -require 'date' - -describe :datetime_sec, shared: true do - it "returns 0 seconds if passed no arguments" do - d = DateTime.new - d.send(@method).should == 0 - end - - it "returns the seconds passed in the arguments" do - new_datetime(second: 5).send(@method).should == 5 - end - - it "adds 60 to negative values" do - new_datetime(second: -20).send(@method).should == 40 - end - - it "returns the absolute value of a Rational" do - new_datetime(second: 5 + Rational(1,2)).send(@method).should == 5 - end - - it "returns the absolute value of a float" do - new_datetime(second: 5.5).send(@method).should == 5 - end - - it "raises an error when minute is given as a rational" do - -> { new_datetime(minute: 5 + Rational(1,2)) }.should.raise(ArgumentError) - end - - it "raises an error, when the second is smaller than -60" do - -> { new_datetime(second: -61) }.should.raise(ArgumentError) - end - - it "raises an error, when the second is greater or equal than 60" do - -> { new_datetime(second: 60) }.should.raise(ArgumentError) - end - - it "raises an error for second fractions smaller than -60" do - -> { new_datetime(second: -60 - Rational(1,2))}.should( - raise_error(ArgumentError)) - end - - it "takes a second fraction near 60" do - new_datetime(second: 59 + Rational(1,2)).send(@method).should == 59 - end -end diff --git a/spec/ruby/library/digest/instance/append_spec.rb b/spec/ruby/library/digest/instance/append_spec.rb index 2499579298eb60..7f4ce3d12165cc 100644 --- a/spec/ruby/library/digest/instance/append_spec.rb +++ b/spec/ruby/library/digest/instance/append_spec.rb @@ -1,7 +1,11 @@ require_relative '../../../spec_helper' require 'digest' -require_relative 'shared/update' describe "Digest::Instance#<<" do - it_behaves_like :digest_instance_update, :<< + it "raises a RuntimeError if called" do + c = Class.new do + include Digest::Instance + end + -> { c.new << "test" }.should.raise(RuntimeError) + end end diff --git a/spec/ruby/library/digest/instance/shared/update.rb b/spec/ruby/library/digest/instance/shared/update.rb deleted file mode 100644 index e064a90087e59d..00000000000000 --- a/spec/ruby/library/digest/instance/shared/update.rb +++ /dev/null @@ -1,8 +0,0 @@ -describe :digest_instance_update, shared: true do - it "raises a RuntimeError if called" do - c = Class.new do - include Digest::Instance - end - -> { c.new.send(@method, "test") }.should.raise(RuntimeError) - end -end diff --git a/spec/ruby/library/digest/instance/update_spec.rb b/spec/ruby/library/digest/instance/update_spec.rb index 3bb4dd7f1bafe6..d15b9762133e70 100644 --- a/spec/ruby/library/digest/instance/update_spec.rb +++ b/spec/ruby/library/digest/instance/update_spec.rb @@ -1,7 +1,8 @@ require_relative '../../../spec_helper' require 'digest' -require_relative 'shared/update' describe "Digest::Instance#update" do - it_behaves_like :digest_instance_update, :update + it "is an alias of Digest::Instance#<<" do + Digest::Instance.instance_method(:update).should == Digest::Instance.instance_method(:<<) + end end diff --git a/spec/ruby/library/digest/md5/append_spec.rb b/spec/ruby/library/digest/md5/append_spec.rb index 0abdc074a14c03..6f42e4f28675b7 100644 --- a/spec/ruby/library/digest/md5/append_spec.rb +++ b/spec/ruby/library/digest/md5/append_spec.rb @@ -1,7 +1,10 @@ require_relative '../../../spec_helper' require_relative 'shared/constants' -require_relative 'shared/update' describe "Digest::MD5#<<" do - it_behaves_like :md5_update, :<< + it "can update" do + cur_digest = Digest::MD5.new + cur_digest << MD5Constants::Contents + cur_digest.digest.should == MD5Constants::Digest + end end diff --git a/spec/ruby/library/digest/md5/length_spec.rb b/spec/ruby/library/digest/md5/length_spec.rb index b05b2a20fdc2af..18bda51129f440 100644 --- a/spec/ruby/library/digest/md5/length_spec.rb +++ b/spec/ruby/library/digest/md5/length_spec.rb @@ -1,7 +1,11 @@ require_relative '../../../spec_helper' require_relative 'shared/constants' -require_relative 'shared/length' describe "Digest::MD5#length" do - it_behaves_like :md5_length, :length + it "returns the length of the digest" do + cur_digest = Digest::MD5.new + cur_digest.length.should == MD5Constants::BlankDigest.size + cur_digest << MD5Constants::Contents + cur_digest.length.should == MD5Constants::Digest.size + end end diff --git a/spec/ruby/library/digest/md5/shared/length.rb b/spec/ruby/library/digest/md5/shared/length.rb deleted file mode 100644 index c5b2b97b5832ec..00000000000000 --- a/spec/ruby/library/digest/md5/shared/length.rb +++ /dev/null @@ -1,8 +0,0 @@ -describe :md5_length, shared: true do - it "returns the length of the digest" do - cur_digest = Digest::MD5.new - cur_digest.send(@method).should == MD5Constants::BlankDigest.size - cur_digest << MD5Constants::Contents - cur_digest.send(@method).should == MD5Constants::Digest.size - end -end diff --git a/spec/ruby/library/digest/md5/shared/update.rb b/spec/ruby/library/digest/md5/shared/update.rb deleted file mode 100644 index be8622aed5a06b..00000000000000 --- a/spec/ruby/library/digest/md5/shared/update.rb +++ /dev/null @@ -1,7 +0,0 @@ -describe :md5_update, shared: true do - it "can update" do - cur_digest = Digest::MD5.new - cur_digest.send @method, MD5Constants::Contents - cur_digest.digest.should == MD5Constants::Digest - end -end diff --git a/spec/ruby/library/digest/md5/size_spec.rb b/spec/ruby/library/digest/md5/size_spec.rb index 22e3272d366c5c..54709234dea03a 100644 --- a/spec/ruby/library/digest/md5/size_spec.rb +++ b/spec/ruby/library/digest/md5/size_spec.rb @@ -1,7 +1,8 @@ require_relative '../../../spec_helper' require_relative 'shared/constants' -require_relative 'shared/length' describe "Digest::MD5#size" do - it_behaves_like :md5_length, :size + it "is an alias of Digest::MD5#length" do + Digest::MD5.instance_method(:size).should == Digest::MD5.instance_method(:length) + end end diff --git a/spec/ruby/library/digest/md5/update_spec.rb b/spec/ruby/library/digest/md5/update_spec.rb index 4773db308c2012..830ccfead61445 100644 --- a/spec/ruby/library/digest/md5/update_spec.rb +++ b/spec/ruby/library/digest/md5/update_spec.rb @@ -1,7 +1,8 @@ require_relative '../../../spec_helper' -require_relative 'shared/constants' -require_relative 'shared/update' +require 'digest' describe "Digest::MD5#update" do - it_behaves_like :md5_update, :update + it "is an alias of Digest::MD5#<<" do + Digest::MD5.instance_method(:update).should == Digest::MD5.instance_method(:<<) + end end diff --git a/spec/ruby/library/digest/sha256/append_spec.rb b/spec/ruby/library/digest/sha256/append_spec.rb index ab594c105f3b8c..f18b06c2a1916c 100644 --- a/spec/ruby/library/digest/sha256/append_spec.rb +++ b/spec/ruby/library/digest/sha256/append_spec.rb @@ -1,7 +1,10 @@ require_relative '../../../spec_helper' require_relative 'shared/constants' -require_relative 'shared/update' describe "Digest::SHA256#<<" do - it_behaves_like :sha256_update, :<< + it "can update" do + cur_digest = Digest::SHA256.new + cur_digest << SHA256Constants::Contents + cur_digest.digest.should == SHA256Constants::Digest + end end diff --git a/spec/ruby/library/digest/sha256/length_spec.rb b/spec/ruby/library/digest/sha256/length_spec.rb index 181ac564ad89ff..fc3db6548ed078 100644 --- a/spec/ruby/library/digest/sha256/length_spec.rb +++ b/spec/ruby/library/digest/sha256/length_spec.rb @@ -1,7 +1,11 @@ require_relative '../../../spec_helper' require_relative 'shared/constants' -require_relative 'shared/length' describe "Digest::SHA256#length" do - it_behaves_like :sha256_length, :length + it "returns the length of the digest" do + cur_digest = Digest::SHA256.new + cur_digest.length.should == SHA256Constants::BlankDigest.size + cur_digest << SHA256Constants::Contents + cur_digest.length.should == SHA256Constants::Digest.size + end end diff --git a/spec/ruby/library/digest/sha256/shared/length.rb b/spec/ruby/library/digest/sha256/shared/length.rb deleted file mode 100644 index 996673a5bd41ba..00000000000000 --- a/spec/ruby/library/digest/sha256/shared/length.rb +++ /dev/null @@ -1,8 +0,0 @@ -describe :sha256_length, shared: true do - it "returns the length of the digest" do - cur_digest = Digest::SHA256.new - cur_digest.send(@method).should == SHA256Constants::BlankDigest.size - cur_digest << SHA256Constants::Contents - cur_digest.send(@method).should == SHA256Constants::Digest.size - end -end diff --git a/spec/ruby/library/digest/sha256/shared/update.rb b/spec/ruby/library/digest/sha256/shared/update.rb deleted file mode 100644 index 0edc07935b803b..00000000000000 --- a/spec/ruby/library/digest/sha256/shared/update.rb +++ /dev/null @@ -1,7 +0,0 @@ -describe :sha256_update, shared: true do - it "can update" do - cur_digest = Digest::SHA256.new - cur_digest.send @method, SHA256Constants::Contents - cur_digest.digest.should == SHA256Constants::Digest - end -end diff --git a/spec/ruby/library/digest/sha256/size_spec.rb b/spec/ruby/library/digest/sha256/size_spec.rb index 102826334235b4..6102e1c8aa4d1b 100644 --- a/spec/ruby/library/digest/sha256/size_spec.rb +++ b/spec/ruby/library/digest/sha256/size_spec.rb @@ -1,7 +1,8 @@ require_relative '../../../spec_helper' require_relative 'shared/constants' -require_relative 'shared/length' describe "Digest::SHA256#size" do - it_behaves_like :sha256_length, :size + it "is an alias of Digest::SHA256#length" do + Digest::SHA256.instance_method(:size).should == Digest::SHA256.instance_method(:length) + end end diff --git a/spec/ruby/library/digest/sha256/update_spec.rb b/spec/ruby/library/digest/sha256/update_spec.rb index 92316eb7523afe..d6724936f138e1 100644 --- a/spec/ruby/library/digest/sha256/update_spec.rb +++ b/spec/ruby/library/digest/sha256/update_spec.rb @@ -1,7 +1,8 @@ require_relative '../../../spec_helper' -require_relative 'shared/constants' -require_relative 'shared/update' +require 'digest' describe "Digest::SHA256#update" do - it_behaves_like :sha256_update, :update + it "is an alias of Digest::SHA256#<<" do + Digest::SHA256.instance_method(:update).should == Digest::SHA256.instance_method(:<<) + end end diff --git a/spec/ruby/library/digest/sha384/append_spec.rb b/spec/ruby/library/digest/sha384/append_spec.rb index 94c036cc3ffcb1..b9a862f1c280bf 100644 --- a/spec/ruby/library/digest/sha384/append_spec.rb +++ b/spec/ruby/library/digest/sha384/append_spec.rb @@ -1,7 +1,10 @@ require_relative '../../../spec_helper' require_relative 'shared/constants' -require_relative 'shared/update' describe "Digest::SHA384#<<" do - it_behaves_like :sha384_update, :<< + it "can update" do + cur_digest = Digest::SHA384.new + cur_digest << SHA384Constants::Contents + cur_digest.digest.should == SHA384Constants::Digest + end end diff --git a/spec/ruby/library/digest/sha384/length_spec.rb b/spec/ruby/library/digest/sha384/length_spec.rb index 33fed492efda39..e5cd6131fd81d8 100644 --- a/spec/ruby/library/digest/sha384/length_spec.rb +++ b/spec/ruby/library/digest/sha384/length_spec.rb @@ -1,7 +1,11 @@ require_relative '../../../spec_helper' require_relative 'shared/constants' -require_relative 'shared/length' describe "Digest::SHA384#length" do - it_behaves_like :sha384_length, :length + it "returns the length of the digest" do + cur_digest = Digest::SHA384.new + cur_digest.length.should == SHA384Constants::BlankDigest.size + cur_digest << SHA384Constants::Contents + cur_digest.length.should == SHA384Constants::Digest.size + end end diff --git a/spec/ruby/library/digest/sha384/shared/length.rb b/spec/ruby/library/digest/sha384/shared/length.rb deleted file mode 100644 index 0c88288bcfd72b..00000000000000 --- a/spec/ruby/library/digest/sha384/shared/length.rb +++ /dev/null @@ -1,8 +0,0 @@ -describe :sha384_length, shared: true do - it "returns the length of the digest" do - cur_digest = Digest::SHA384.new - cur_digest.send(@method).should == SHA384Constants::BlankDigest.size - cur_digest << SHA384Constants::Contents - cur_digest.send(@method).should == SHA384Constants::Digest.size - end -end diff --git a/spec/ruby/library/digest/sha384/shared/update.rb b/spec/ruby/library/digest/sha384/shared/update.rb deleted file mode 100644 index 1c6e31cf6a54b4..00000000000000 --- a/spec/ruby/library/digest/sha384/shared/update.rb +++ /dev/null @@ -1,7 +0,0 @@ -describe :sha384_update, shared: true do - it "can update" do - cur_digest = Digest::SHA384.new - cur_digest.send @method, SHA384Constants::Contents - cur_digest.digest.should == SHA384Constants::Digest - end -end diff --git a/spec/ruby/library/digest/sha384/size_spec.rb b/spec/ruby/library/digest/sha384/size_spec.rb index 4c3b14f7a03c23..40c291c6235bb8 100644 --- a/spec/ruby/library/digest/sha384/size_spec.rb +++ b/spec/ruby/library/digest/sha384/size_spec.rb @@ -1,7 +1,8 @@ require_relative '../../../spec_helper' -require_relative 'shared/constants' -require_relative 'shared/length' +require 'digest' describe "Digest::SHA384#size" do - it_behaves_like :sha384_length, :size + it "is an alias of Digest::SHA384#length" do + Digest::SHA384.instance_method(:size).should == Digest::SHA384.instance_method(:length) + end end diff --git a/spec/ruby/library/digest/sha384/update_spec.rb b/spec/ruby/library/digest/sha384/update_spec.rb index a1d0dd6068c443..561dcad3ec6681 100644 --- a/spec/ruby/library/digest/sha384/update_spec.rb +++ b/spec/ruby/library/digest/sha384/update_spec.rb @@ -1,7 +1,8 @@ require_relative '../../../spec_helper' -require_relative 'shared/constants' -require_relative 'shared/update' +require 'digest' describe "Digest::SHA384#update" do - it_behaves_like :sha384_update, :update + it "is an alias of Digest::SHA384#<<" do + Digest::SHA384.instance_method(:update).should == Digest::SHA384.instance_method(:<<) + end end diff --git a/spec/ruby/library/digest/sha512/append_spec.rb b/spec/ruby/library/digest/sha512/append_spec.rb index 9106e9685da3fc..f2970054036e7d 100644 --- a/spec/ruby/library/digest/sha512/append_spec.rb +++ b/spec/ruby/library/digest/sha512/append_spec.rb @@ -1,7 +1,10 @@ require_relative '../../../spec_helper' require_relative 'shared/constants' -require_relative 'shared/update' describe "Digest::SHA512#<<" do - it_behaves_like :sha512_update, :<< + it "can update" do + cur_digest = Digest::SHA512.new + cur_digest << SHA512Constants::Contents + cur_digest.digest.should == SHA512Constants::Digest + end end diff --git a/spec/ruby/library/digest/sha512/length_spec.rb b/spec/ruby/library/digest/sha512/length_spec.rb index e9fde9057766d3..8e909482c51281 100644 --- a/spec/ruby/library/digest/sha512/length_spec.rb +++ b/spec/ruby/library/digest/sha512/length_spec.rb @@ -1,7 +1,11 @@ require_relative '../../../spec_helper' require_relative 'shared/constants' -require_relative 'shared/length' describe "Digest::SHA512#length" do - it_behaves_like :sha512_length, :length + it "returns the length of the digest" do + cur_digest = Digest::SHA512.new + cur_digest.length.should == SHA512Constants::BlankDigest.size + cur_digest << SHA512Constants::Contents + cur_digest.length.should == SHA512Constants::Digest.size + end end diff --git a/spec/ruby/library/digest/sha512/shared/length.rb b/spec/ruby/library/digest/sha512/shared/length.rb deleted file mode 100644 index c0609d538627dc..00000000000000 --- a/spec/ruby/library/digest/sha512/shared/length.rb +++ /dev/null @@ -1,8 +0,0 @@ -describe :sha512_length, shared: true do - it "returns the length of the digest" do - cur_digest = Digest::SHA512.new - cur_digest.send(@method).should == SHA512Constants::BlankDigest.size - cur_digest << SHA512Constants::Contents - cur_digest.send(@method).should == SHA512Constants::Digest.size - end -end diff --git a/spec/ruby/library/digest/sha512/shared/update.rb b/spec/ruby/library/digest/sha512/shared/update.rb deleted file mode 100644 index ca74dbf4dfb9ed..00000000000000 --- a/spec/ruby/library/digest/sha512/shared/update.rb +++ /dev/null @@ -1,7 +0,0 @@ -describe :sha512_update, shared: true do - it "can update" do - cur_digest = Digest::SHA512.new - cur_digest.send @method, SHA512Constants::Contents - cur_digest.digest.should == SHA512Constants::Digest - end -end diff --git a/spec/ruby/library/digest/sha512/size_spec.rb b/spec/ruby/library/digest/sha512/size_spec.rb index 6d0acdabdb92bc..498d6868026d0b 100644 --- a/spec/ruby/library/digest/sha512/size_spec.rb +++ b/spec/ruby/library/digest/sha512/size_spec.rb @@ -1,7 +1,8 @@ require_relative '../../../spec_helper' -require_relative 'shared/constants' -require_relative 'shared/length' +require 'digest' describe "Digest::SHA512#size" do - it_behaves_like :sha512_length, :size + it "is an alias of Digest::SHA512#length" do + Digest::SHA512.instance_method(:size).should == Digest::SHA512.instance_method(:length) + end end diff --git a/spec/ruby/library/digest/sha512/update_spec.rb b/spec/ruby/library/digest/sha512/update_spec.rb index 682d3a19bb2d9e..33edf216ac7954 100644 --- a/spec/ruby/library/digest/sha512/update_spec.rb +++ b/spec/ruby/library/digest/sha512/update_spec.rb @@ -1,7 +1,8 @@ require_relative '../../../spec_helper' -require_relative 'shared/constants' -require_relative 'shared/update' +require 'digest' describe "Digest::SHA512#update" do - it_behaves_like :sha512_update, :update + it "is an alias of Digest::SHA512#<<" do + Digest::SHA512.instance_method(:update).should == Digest::SHA512.instance_method(:<<) + end end diff --git a/spec/ruby/library/getoptlong/each_option_spec.rb b/spec/ruby/library/getoptlong/each_option_spec.rb index c6d82af86d4107..3349554aaa787c 100644 --- a/spec/ruby/library/getoptlong/each_option_spec.rb +++ b/spec/ruby/library/getoptlong/each_option_spec.rb @@ -1,7 +1,21 @@ require_relative '../../spec_helper' require 'getoptlong' -require_relative 'shared/each' describe "GetoptLong#each_option" do - it_behaves_like :getoptlong_each, :each_option + before :each do + @opts = GetoptLong.new( + [ '--size', '-s', GetoptLong::REQUIRED_ARGUMENT ], + [ '--verbose', '-v', GetoptLong::NO_ARGUMENT ], + [ '--query', '-q', GetoptLong::NO_ARGUMENT ], + [ '--check', '--valid', '-c', GetoptLong::NO_ARGUMENT ] + ) + end + + it "passes each_option argument/value pair to the block" do + argv [ "--size", "10k", "-v", "-q", "a.txt", "b.txt" ] do + pairs = [] + @opts.each_option { |arg, val| pairs << [ arg, val ] } + pairs.should == [ [ "--size", "10k" ], [ "--verbose", "" ], [ "--query", ""] ] + end + end end diff --git a/spec/ruby/library/getoptlong/each_spec.rb b/spec/ruby/library/getoptlong/each_spec.rb index d9022f02af2ace..646c3297b5e91a 100644 --- a/spec/ruby/library/getoptlong/each_spec.rb +++ b/spec/ruby/library/getoptlong/each_spec.rb @@ -1,7 +1,8 @@ require_relative '../../spec_helper' require 'getoptlong' -require_relative 'shared/each' describe "GetoptLong#each" do - it_behaves_like :getoptlong_each, :each + it "is an alias of GetoptLong#each_option" do + GetoptLong.instance_method(:each).should == GetoptLong.instance_method(:each_option) + end end diff --git a/spec/ruby/library/getoptlong/get_option_spec.rb b/spec/ruby/library/getoptlong/get_option_spec.rb index 3cb20443796f0e..1d80e3622eebb8 100644 --- a/spec/ruby/library/getoptlong/get_option_spec.rb +++ b/spec/ruby/library/getoptlong/get_option_spec.rb @@ -1,7 +1,8 @@ require_relative '../../spec_helper' require 'getoptlong' -require_relative 'shared/get' describe "GetoptLong#get_option" do - it_behaves_like :getoptlong_get, :get_option + it "is an alias of GetoptLong#get" do + GetoptLong.instance_method(:get_option).should == GetoptLong.instance_method(:get) + end end diff --git a/spec/ruby/library/getoptlong/get_spec.rb b/spec/ruby/library/getoptlong/get_spec.rb index a8ec586fc9d115..bfc6697a5adbe2 100644 --- a/spec/ruby/library/getoptlong/get_spec.rb +++ b/spec/ruby/library/getoptlong/get_spec.rb @@ -1,7 +1,65 @@ require_relative '../../spec_helper' require 'getoptlong' -require_relative 'shared/get' describe "GetoptLong#get" do - it_behaves_like :getoptlong_get, :get + before :each do + @opts = GetoptLong.new( + [ '--size', '-s', GetoptLong::REQUIRED_ARGUMENT ], + [ '--verbose', '-v', GetoptLong::NO_ARGUMENT ], + [ '--query', '-q', GetoptLong::NO_ARGUMENT ], + [ '--check', '--valid', '-c', GetoptLong::NO_ARGUMENT ] + ) + @opts.quiet = true # silence using $deferr + end + + it "returns the next option name and its argument as an Array" do + argv [ "--size", "10k", "-v", "-q", "a.txt", "b.txt" ] do + @opts.get.should == [ "--size", "10k" ] + @opts.get.should == [ "--verbose", "" ] + @opts.get.should == [ "--query", ""] + @opts.get.should == nil + end + end + + it "shifts ARGV on each call" do + argv [ "--size", "10k", "-v", "-q", "a.txt", "b.txt" ] do + @opts.get + ARGV.should == [ "-v", "-q", "a.txt", "b.txt" ] + + @opts.get + ARGV.should == [ "-q", "a.txt", "b.txt" ] + + @opts.get + ARGV.should == [ "a.txt", "b.txt" ] + + @opts.get + ARGV.should == [ "a.txt", "b.txt" ] + end + end + + it "terminates processing when encountering '--'" do + argv [ "--size", "10k", "--", "-v", "-q", "a.txt", "b.txt" ] do + @opts.get + ARGV.should == ["--", "-v", "-q", "a.txt", "b.txt"] + + @opts.get + ARGV.should == ["-v", "-q", "a.txt", "b.txt"] + + @opts.get + ARGV.should == ["-v", "-q", "a.txt", "b.txt"] + end + end + + it "raises a if an argument was required, but none given" do + argv [ "--size" ] do + -> { @opts.get }.should.raise(GetoptLong::MissingArgument) + end + end + + # https://bugs.ruby-lang.org/issues/13858 + it "returns multiline argument" do + argv [ "--size=\n10k\n" ] do + @opts.get.should == [ "--size", "\n10k\n" ] + end + end end diff --git a/spec/ruby/library/getoptlong/shared/each.rb b/spec/ruby/library/getoptlong/shared/each.rb deleted file mode 100644 index b534e24c0f2d16..00000000000000 --- a/spec/ruby/library/getoptlong/shared/each.rb +++ /dev/null @@ -1,18 +0,0 @@ -describe :getoptlong_each, shared: true do - before :each do - @opts = GetoptLong.new( - [ '--size', '-s', GetoptLong::REQUIRED_ARGUMENT ], - [ '--verbose', '-v', GetoptLong::NO_ARGUMENT ], - [ '--query', '-q', GetoptLong::NO_ARGUMENT ], - [ '--check', '--valid', '-c', GetoptLong::NO_ARGUMENT ] - ) - end - - it "passes each argument/value pair to the block" do - argv [ "--size", "10k", "-v", "-q", "a.txt", "b.txt" ] do - pairs = [] - @opts.send(@method) { |arg, val| pairs << [ arg, val ] } - pairs.should == [ [ "--size", "10k" ], [ "--verbose", "" ], [ "--query", ""] ] - end - end -end diff --git a/spec/ruby/library/getoptlong/shared/get.rb b/spec/ruby/library/getoptlong/shared/get.rb deleted file mode 100644 index 8d24c4c25523f1..00000000000000 --- a/spec/ruby/library/getoptlong/shared/get.rb +++ /dev/null @@ -1,62 +0,0 @@ -describe :getoptlong_get, shared: true do - before :each do - @opts = GetoptLong.new( - [ '--size', '-s', GetoptLong::REQUIRED_ARGUMENT ], - [ '--verbose', '-v', GetoptLong::NO_ARGUMENT ], - [ '--query', '-q', GetoptLong::NO_ARGUMENT ], - [ '--check', '--valid', '-c', GetoptLong::NO_ARGUMENT ] - ) - @opts.quiet = true # silence using $deferr - end - - it "returns the next option name and its argument as an Array" do - argv [ "--size", "10k", "-v", "-q", "a.txt", "b.txt" ] do - @opts.send(@method).should == [ "--size", "10k" ] - @opts.send(@method).should == [ "--verbose", "" ] - @opts.send(@method).should == [ "--query", ""] - @opts.send(@method).should == nil - end - end - - it "shifts ARGV on each call" do - argv [ "--size", "10k", "-v", "-q", "a.txt", "b.txt" ] do - @opts.send(@method) - ARGV.should == [ "-v", "-q", "a.txt", "b.txt" ] - - @opts.send(@method) - ARGV.should == [ "-q", "a.txt", "b.txt" ] - - @opts.send(@method) - ARGV.should == [ "a.txt", "b.txt" ] - - @opts.send(@method) - ARGV.should == [ "a.txt", "b.txt" ] - end - end - - it "terminates processing when encountering '--'" do - argv [ "--size", "10k", "--", "-v", "-q", "a.txt", "b.txt" ] do - @opts.send(@method) - ARGV.should == ["--", "-v", "-q", "a.txt", "b.txt"] - - @opts.send(@method) - ARGV.should == ["-v", "-q", "a.txt", "b.txt"] - - @opts.send(@method) - ARGV.should == ["-v", "-q", "a.txt", "b.txt"] - end - end - - it "raises a if an argument was required, but none given" do - argv [ "--size" ] do - -> { @opts.send(@method) }.should.raise(GetoptLong::MissingArgument) - end - end - - # https://bugs.ruby-lang.org/issues/13858 - it "returns multiline argument" do - argv [ "--size=\n10k\n" ] do - @opts.send(@method).should == [ "--size", "\n10k\n" ] - end - end -end diff --git a/spec/ruby/library/matrix/I_spec.rb b/spec/ruby/library/matrix/I_spec.rb index 6eeffe8e98cc90..ca5e79279a7b92 100644 --- a/spec/ruby/library/matrix/I_spec.rb +++ b/spec/ruby/library/matrix/I_spec.rb @@ -1,6 +1,8 @@ require_relative '../../spec_helper' -require_relative 'shared/identity' +require 'matrix' describe "Matrix.I" do - it_behaves_like :matrix_identity, :I + it "is an alias of Matrix.identity" do + Matrix.method(:I).should == Matrix.method(:identity) + end end diff --git a/spec/ruby/library/matrix/collect_spec.rb b/spec/ruby/library/matrix/collect_spec.rb index bba640213bd8d2..664c3f303892be 100644 --- a/spec/ruby/library/matrix/collect_spec.rb +++ b/spec/ruby/library/matrix/collect_spec.rb @@ -1,6 +1,8 @@ require_relative '../../spec_helper' -require_relative 'shared/collect' +require 'matrix' describe "Matrix#collect" do - it_behaves_like :collect, :collect + it "is an alias of Matrix#map" do + Matrix.instance_method(:collect).should == Matrix.instance_method(:map) + end end diff --git a/spec/ruby/library/matrix/conj_spec.rb b/spec/ruby/library/matrix/conj_spec.rb index ecee95c255b6bb..eff5986fc40c01 100644 --- a/spec/ruby/library/matrix/conj_spec.rb +++ b/spec/ruby/library/matrix/conj_spec.rb @@ -1,6 +1,8 @@ require_relative '../../spec_helper' -require_relative 'shared/conjugate' +require 'matrix' describe "Matrix#conj" do - it_behaves_like :matrix_conjugate, :conj + it "is an alias of Matrix#conjugate" do + Matrix.instance_method(:conj).should == Matrix.instance_method(:conjugate) + end end diff --git a/spec/ruby/library/matrix/conjugate_spec.rb b/spec/ruby/library/matrix/conjugate_spec.rb index 682bd41d9445df..46077d4fa961b5 100644 --- a/spec/ruby/library/matrix/conjugate_spec.rb +++ b/spec/ruby/library/matrix/conjugate_spec.rb @@ -1,6 +1,20 @@ require_relative '../../spec_helper' -require_relative 'shared/conjugate' +require_relative 'fixtures/classes' describe "Matrix#conjugate" do - it_behaves_like :matrix_conjugate, :conjugate + it "returns a matrix with all entries 'conjugated'" do + Matrix[ [1, 2], [3, 4] ].conjugate.should == Matrix[ [1, 2], [3, 4] ] + Matrix[ [1.9, Complex(1,1)], [3, 4] ].conjugate.should == Matrix[ [1.9, Complex(1,-1)], [3, 4] ] + end + + it "returns empty matrices on the same size if empty" do + Matrix.empty(0, 3).conjugate.should == Matrix.empty(0, 3) + Matrix.empty(3, 0).conjugate.should == Matrix.empty(3, 0) + end + + describe "for a subclass of Matrix" do + it "returns an instance of that subclass" do + MatrixSub.ins.conjugate.should.instance_of?(MatrixSub) + end + end end diff --git a/spec/ruby/library/matrix/det_spec.rb b/spec/ruby/library/matrix/det_spec.rb index aa7086cacf2041..fc4b1c252ad957 100644 --- a/spec/ruby/library/matrix/det_spec.rb +++ b/spec/ruby/library/matrix/det_spec.rb @@ -1,7 +1,8 @@ require_relative '../../spec_helper' -require_relative 'shared/determinant' require 'matrix' describe "Matrix#det" do - it_behaves_like :determinant, :det + it "is an alias of Matrix#determinant" do + Matrix.instance_method(:det).should == Matrix.instance_method(:determinant) + end end diff --git a/spec/ruby/library/matrix/determinant_spec.rb b/spec/ruby/library/matrix/determinant_spec.rb index 825c9907b11b5a..603e13ba288b6e 100644 --- a/spec/ruby/library/matrix/determinant_spec.rb +++ b/spec/ruby/library/matrix/determinant_spec.rb @@ -1,7 +1,39 @@ require_relative '../../spec_helper' -require_relative 'shared/determinant' require 'matrix' describe "Matrix#determinant" do - it_behaves_like :determinant, :determinant + it "returns the determinant of a square Matrix" do + m = Matrix[ [7,6], [3,9] ] + m.determinant.should == 45 + + m = Matrix[ [9, 8], [6,5] ] + m.determinant.should == -3 + + m = Matrix[ [9,8,3], [4,20,5], [1,1,1] ] + m.determinant.should == 95 + end + + it "returns the determinant of a single-element Matrix" do + m = Matrix[ [2] ] + m.determinant.should == 2 + end + + it "returns 1 for an empty Matrix" do + m = Matrix[ ] + m.determinant.should == 1 + end + + it "returns the determinant even for Matrices containing 0 as first entry" do + Matrix[[0,1],[1,0]].determinant.should == -1 + end + + it "raises an error for rectangular matrices" do + -> { + Matrix[[1], [2], [3]].determinant + }.should.raise(Matrix::ErrDimensionMismatch) + + -> { + Matrix.empty(3,0).determinant + }.should.raise(Matrix::ErrDimensionMismatch) + end end diff --git a/spec/ruby/library/matrix/identity_spec.rb b/spec/ruby/library/matrix/identity_spec.rb index 646462bc477c8d..afefd27565cbe1 100644 --- a/spec/ruby/library/matrix/identity_spec.rb +++ b/spec/ruby/library/matrix/identity_spec.rb @@ -1,6 +1,20 @@ require_relative '../../spec_helper' -require_relative 'shared/identity' +require_relative 'fixtures/classes' +require 'matrix' describe "Matrix.identity" do - it_behaves_like :matrix_identity, :identity + it "returns a Matrix" do + Matrix.identity(2).should.is_a?(Matrix) + end + + it "returns a n x n identity matrix" do + Matrix.identity(3).should == Matrix.scalar(3, 1) + Matrix.identity(100).should == Matrix.scalar(100, 1) + end + + describe "for a subclass of Matrix" do + it "returns an instance of that subclass" do + MatrixSub.identity(2).should.instance_of?(MatrixSub) + end + end end diff --git a/spec/ruby/library/matrix/imag_spec.rb b/spec/ruby/library/matrix/imag_spec.rb index 1c988753d87c06..9d6cc2e9533126 100644 --- a/spec/ruby/library/matrix/imag_spec.rb +++ b/spec/ruby/library/matrix/imag_spec.rb @@ -1,6 +1,8 @@ require_relative '../../spec_helper' -require_relative 'shared/imaginary' +require 'matrix' describe "Matrix#imag" do - it_behaves_like :matrix_imaginary, :imag + it "is an alias of Matrix#imaginary" do + Matrix.instance_method(:imag).should == Matrix.instance_method(:imaginary) + end end diff --git a/spec/ruby/library/matrix/imaginary_spec.rb b/spec/ruby/library/matrix/imaginary_spec.rb index ceae4bbe8d0895..bbd06677b798b3 100644 --- a/spec/ruby/library/matrix/imaginary_spec.rb +++ b/spec/ruby/library/matrix/imaginary_spec.rb @@ -1,6 +1,21 @@ require_relative '../../spec_helper' -require_relative 'shared/imaginary' +require_relative 'fixtures/classes' +require 'matrix' describe "Matrix#imaginary" do - it_behaves_like :matrix_imaginary, :imaginary + it "returns a matrix with the imaginary part of the elements of the receiver" do + Matrix[ [1, 2], [3, 4] ].imaginary.should == Matrix[ [0, 0], [0, 0] ] + Matrix[ [1.9, Complex(1,1)], [Complex(-2,0.42), 4] ].imaginary.should == Matrix[ [0, 1], [0.42, 0] ] + end + + it "returns empty matrices on the same size if empty" do + Matrix.empty(0, 3).imaginary.should == Matrix.empty(0, 3) + Matrix.empty(3, 0).imaginary.should == Matrix.empty(3, 0) + end + + describe "for a subclass of Matrix" do + it "returns an instance of that subclass" do + MatrixSub.ins.imaginary.should.instance_of?(MatrixSub) + end + end end diff --git a/spec/ruby/library/matrix/inv_spec.rb b/spec/ruby/library/matrix/inv_spec.rb index 82879a6d82d07b..02684030d296f0 100644 --- a/spec/ruby/library/matrix/inv_spec.rb +++ b/spec/ruby/library/matrix/inv_spec.rb @@ -1,7 +1,8 @@ require_relative '../../spec_helper' -require_relative 'spec_helper' -require_relative 'shared/inverse' +require 'matrix' describe "Matrix#inv" do - it_behaves_like :inverse, :inv + it "is an alias of Matrix#inverse" do + Matrix.instance_method(:inv).should == Matrix.instance_method(:inverse) + end end diff --git a/spec/ruby/library/matrix/inverse_spec.rb b/spec/ruby/library/matrix/inverse_spec.rb index fa3fa7de8a7ee4..38b01b28fb3be0 100644 --- a/spec/ruby/library/matrix/inverse_spec.rb +++ b/spec/ruby/library/matrix/inverse_spec.rb @@ -1,7 +1,39 @@ require_relative '../../spec_helper' require_relative 'spec_helper' -require_relative 'shared/inverse' +require_relative 'fixtures/classes' +require 'matrix' describe "Matrix#inverse" do - it_behaves_like :inverse, :inverse + it "returns a Matrix" do + Matrix[ [1,2], [2,1] ].inverse.should.instance_of?(Matrix) + end + + it "returns the inverse of the Matrix" do + Matrix[ + [1, 3, 3], [1, 4, 3], [1, 3, 4] + ].inverse.should == + Matrix[ + [7, -3, -3], [-1, 1, 0], [-1, 0, 1] + ] + end + + it "returns the inverse of the Matrix (other case)" do + Matrix[ + [1, 2, 3], [0, 1, 4], [5, 6, 0] + ].inverse.should be_close_to_matrix([ + [-24, 18, 5], [20, -15, -4], [-5, 4, 1] + ]) + end + + it "raises a ErrDimensionMismatch if the Matrix is not square" do + ->{ + Matrix[ [1,2,3], [1,2,3] ].inverse + }.should.raise(Matrix::ErrDimensionMismatch) + end + + describe "for a subclass of Matrix" do + it "returns an instance of that subclass" do + MatrixSub.ins.inverse.should.instance_of?(MatrixSub) + end + end end diff --git a/spec/ruby/library/matrix/map_spec.rb b/spec/ruby/library/matrix/map_spec.rb index bc07c48cda9a98..bae96db381fc9b 100644 --- a/spec/ruby/library/matrix/map_spec.rb +++ b/spec/ruby/library/matrix/map_spec.rb @@ -1,6 +1,26 @@ require_relative '../../spec_helper' -require_relative 'shared/collect' +require_relative 'fixtures/classes' describe "Matrix#map" do - it_behaves_like :collect, :map + before :all do + @m = Matrix[ [1, 2], [1, 2] ] + end + + it "returns an instance of Matrix" do + @m.map{|n| n * 2 }.should.is_a?(Matrix) + end + + it "returns a Matrix where each element is the result of the block" do + @m.map { |n| n * 2 }.should == Matrix[ [2, 4], [2, 4] ] + end + + it "returns an enumerator if no block is given" do + @m.map.should.instance_of?(Enumerator) + end + + describe "for a subclass of Matrix" do + it "returns an instance of that subclass" do + MatrixSub.ins.map{1}.should.instance_of?(MatrixSub) + end + end end diff --git a/spec/ruby/library/matrix/rect_spec.rb b/spec/ruby/library/matrix/rect_spec.rb index 83a0404e47d18d..b0ca3f04211d30 100644 --- a/spec/ruby/library/matrix/rect_spec.rb +++ b/spec/ruby/library/matrix/rect_spec.rb @@ -1,6 +1,8 @@ require_relative '../../spec_helper' -require_relative 'shared/rectangular' +require 'matrix' describe "Matrix#rect" do - it_behaves_like :matrix_rectangular, :rect + it "is an alias of Matrix#rectangular" do + Matrix.instance_method(:rect).should == Matrix.instance_method(:rectangular) + end end diff --git a/spec/ruby/library/matrix/rectangular_spec.rb b/spec/ruby/library/matrix/rectangular_spec.rb index a235fac640c3fa..c0732f96bc366f 100644 --- a/spec/ruby/library/matrix/rectangular_spec.rb +++ b/spec/ruby/library/matrix/rectangular_spec.rb @@ -1,6 +1,19 @@ require_relative '../../spec_helper' -require_relative 'shared/rectangular' +require_relative 'fixtures/classes' +require 'matrix' describe "Matrix#rectangular" do - it_behaves_like :matrix_rectangular, :rectangular + it "returns [receiver.real, receiver.imag]" do + m = Matrix[ [1.2, Complex(1,2)], [Complex(-2,0.42), 4] ] + m.rectangular.should == [m.real, m.imag] + + m = Matrix.empty(3, 0) + m.rectangular.should == [m.real, m.imag] + end + + describe "for a subclass of Matrix" do + it "returns instances of that subclass" do + MatrixSub.ins.rectangular.each{|m| m.should.instance_of?(MatrixSub) } + end + end end diff --git a/spec/ruby/library/matrix/shared/collect.rb b/spec/ruby/library/matrix/shared/collect.rb deleted file mode 100644 index 3a4dbe3a366c81..00000000000000 --- a/spec/ruby/library/matrix/shared/collect.rb +++ /dev/null @@ -1,26 +0,0 @@ -require_relative '../fixtures/classes' -require 'matrix' - -describe :collect, shared: true do - before :all do - @m = Matrix[ [1, 2], [1, 2] ] - end - - it "returns an instance of Matrix" do - @m.send(@method){|n| n * 2 }.should.is_a?(Matrix) - end - - it "returns a Matrix where each element is the result of the block" do - @m.send(@method) { |n| n * 2 }.should == Matrix[ [2, 4], [2, 4] ] - end - - it "returns an enumerator if no block is given" do - @m.send(@method).should.instance_of?(Enumerator) - end - - describe "for a subclass of Matrix" do - it "returns an instance of that subclass" do - MatrixSub.ins.send(@method){1}.should.instance_of?(MatrixSub) - end - end -end diff --git a/spec/ruby/library/matrix/shared/conjugate.rb b/spec/ruby/library/matrix/shared/conjugate.rb deleted file mode 100644 index ffdf5ebca1713d..00000000000000 --- a/spec/ruby/library/matrix/shared/conjugate.rb +++ /dev/null @@ -1,20 +0,0 @@ -require_relative '../fixtures/classes' -require 'matrix' - -describe :matrix_conjugate, shared: true do - it "returns a matrix with all entries 'conjugated'" do - Matrix[ [1, 2], [3, 4] ].send(@method).should == Matrix[ [1, 2], [3, 4] ] - Matrix[ [1.9, Complex(1,1)], [3, 4] ].send(@method).should == Matrix[ [1.9, Complex(1,-1)], [3, 4] ] - end - - it "returns empty matrices on the same size if empty" do - Matrix.empty(0, 3).send(@method).should == Matrix.empty(0, 3) - Matrix.empty(3, 0).send(@method).should == Matrix.empty(3, 0) - end - - describe "for a subclass of Matrix" do - it "returns an instance of that subclass" do - MatrixSub.ins.send(@method).should.instance_of?(MatrixSub) - end - end -end diff --git a/spec/ruby/library/matrix/shared/determinant.rb b/spec/ruby/library/matrix/shared/determinant.rb deleted file mode 100644 index b7c86393baf1e3..00000000000000 --- a/spec/ruby/library/matrix/shared/determinant.rb +++ /dev/null @@ -1,38 +0,0 @@ -require 'matrix' - -describe :determinant, shared: true do - it "returns the determinant of a square Matrix" do - m = Matrix[ [7,6], [3,9] ] - m.send(@method).should == 45 - - m = Matrix[ [9, 8], [6,5] ] - m.send(@method).should == -3 - - m = Matrix[ [9,8,3], [4,20,5], [1,1,1] ] - m.send(@method).should == 95 - end - - it "returns the determinant of a single-element Matrix" do - m = Matrix[ [2] ] - m.send(@method).should == 2 - end - - it "returns 1 for an empty Matrix" do - m = Matrix[ ] - m.send(@method).should == 1 - end - - it "returns the determinant even for Matrices containing 0 as first entry" do - Matrix[[0,1],[1,0]].send(@method).should == -1 - end - - it "raises an error for rectangular matrices" do - -> { - Matrix[[1], [2], [3]].send(@method) - }.should.raise(Matrix::ErrDimensionMismatch) - - -> { - Matrix.empty(3,0).send(@method) - }.should.raise(Matrix::ErrDimensionMismatch) - end -end diff --git a/spec/ruby/library/matrix/shared/identity.rb b/spec/ruby/library/matrix/shared/identity.rb deleted file mode 100644 index df957b5a750306..00000000000000 --- a/spec/ruby/library/matrix/shared/identity.rb +++ /dev/null @@ -1,19 +0,0 @@ -require_relative '../fixtures/classes' -require 'matrix' - -describe :matrix_identity, shared: true do - it "returns a Matrix" do - Matrix.send(@method, 2).should.is_a?(Matrix) - end - - it "returns a n x n identity matrix" do - Matrix.send(@method, 3).should == Matrix.scalar(3, 1) - Matrix.send(@method, 100).should == Matrix.scalar(100, 1) - end - - describe "for a subclass of Matrix" do - it "returns an instance of that subclass" do - MatrixSub.send(@method, 2).should.instance_of?(MatrixSub) - end - end -end diff --git a/spec/ruby/library/matrix/shared/imaginary.rb b/spec/ruby/library/matrix/shared/imaginary.rb deleted file mode 100644 index 16615213a245a6..00000000000000 --- a/spec/ruby/library/matrix/shared/imaginary.rb +++ /dev/null @@ -1,20 +0,0 @@ -require_relative '../fixtures/classes' -require 'matrix' - -describe :matrix_imaginary, shared: true do - it "returns a matrix with the imaginary part of the elements of the receiver" do - Matrix[ [1, 2], [3, 4] ].send(@method).should == Matrix[ [0, 0], [0, 0] ] - Matrix[ [1.9, Complex(1,1)], [Complex(-2,0.42), 4] ].send(@method).should == Matrix[ [0, 1], [0.42, 0] ] - end - - it "returns empty matrices on the same size if empty" do - Matrix.empty(0, 3).send(@method).should == Matrix.empty(0, 3) - Matrix.empty(3, 0).send(@method).should == Matrix.empty(3, 0) - end - - describe "for a subclass of Matrix" do - it "returns an instance of that subclass" do - MatrixSub.ins.send(@method).should.instance_of?(MatrixSub) - end - end -end diff --git a/spec/ruby/library/matrix/shared/inverse.rb b/spec/ruby/library/matrix/shared/inverse.rb deleted file mode 100644 index ac463cf680828b..00000000000000 --- a/spec/ruby/library/matrix/shared/inverse.rb +++ /dev/null @@ -1,38 +0,0 @@ -require_relative '../fixtures/classes' -require 'matrix' - -describe :inverse, shared: true do - - it "returns a Matrix" do - Matrix[ [1,2], [2,1] ].send(@method).should.instance_of?(Matrix) - end - - it "returns the inverse of the Matrix" do - Matrix[ - [1, 3, 3], [1, 4, 3], [1, 3, 4] - ].send(@method).should == - Matrix[ - [7, -3, -3], [-1, 1, 0], [-1, 0, 1] - ] - end - - it "returns the inverse of the Matrix (other case)" do - Matrix[ - [1, 2, 3], [0, 1, 4], [5, 6, 0] - ].send(@method).should be_close_to_matrix([ - [-24, 18, 5], [20, -15, -4], [-5, 4, 1] - ]) - end - - it "raises a ErrDimensionMismatch if the Matrix is not square" do - ->{ - Matrix[ [1,2,3], [1,2,3] ].send(@method) - }.should.raise(Matrix::ErrDimensionMismatch) - end - - describe "for a subclass of Matrix" do - it "returns an instance of that subclass" do - MatrixSub.ins.send(@method).should.instance_of?(MatrixSub) - end - end -end diff --git a/spec/ruby/library/matrix/shared/rectangular.rb b/spec/ruby/library/matrix/shared/rectangular.rb deleted file mode 100644 index 0229e614c64557..00000000000000 --- a/spec/ruby/library/matrix/shared/rectangular.rb +++ /dev/null @@ -1,18 +0,0 @@ -require_relative '../fixtures/classes' -require 'matrix' - -describe :matrix_rectangular, shared: true do - it "returns [receiver.real, receiver.imag]" do - m = Matrix[ [1.2, Complex(1,2)], [Complex(-2,0.42), 4] ] - m.send(@method).should == [m.real, m.imag] - - m = Matrix.empty(3, 0) - m.send(@method).should == [m.real, m.imag] - end - - describe "for a subclass of Matrix" do - it "returns instances of that subclass" do - MatrixSub.ins.send(@method).each{|m| m.should.instance_of?(MatrixSub) } - end - end -end diff --git a/spec/ruby/library/matrix/shared/trace.rb b/spec/ruby/library/matrix/shared/trace.rb deleted file mode 100644 index c4a5491b226077..00000000000000 --- a/spec/ruby/library/matrix/shared/trace.rb +++ /dev/null @@ -1,12 +0,0 @@ -require 'matrix' - -describe :trace, shared: true do - it "returns the sum of diagonal elements in a square Matrix" do - Matrix[[7,6], [3,9]].trace.should == 16 - end - - it "returns the sum of diagonal elements in a rectangular Matrix" do - ->{ Matrix[[1,2,3], [4,5,6]].trace}.should.raise(Matrix::ErrDimensionMismatch) - end - -end diff --git a/spec/ruby/library/matrix/shared/transpose.rb b/spec/ruby/library/matrix/shared/transpose.rb deleted file mode 100644 index a0b495359bb782..00000000000000 --- a/spec/ruby/library/matrix/shared/transpose.rb +++ /dev/null @@ -1,19 +0,0 @@ -require_relative '../fixtures/classes' -require 'matrix' - -describe :matrix_transpose, shared: true do - it "returns a transposed matrix" do - Matrix[[1, 2], [3, 4], [5, 6]].send(@method).should == Matrix[[1, 3, 5], [2, 4, 6]] - end - - it "can transpose empty matrices" do - m = Matrix[[], [], []] - m.send(@method).send(@method).should == m - end - - describe "for a subclass of Matrix" do - it "returns an instance of that subclass" do - MatrixSub.ins.send(@method).should.instance_of?(MatrixSub) - end - end -end diff --git a/spec/ruby/library/matrix/t_spec.rb b/spec/ruby/library/matrix/t_spec.rb index 6f1a5178e0a5a1..9411597e7c0c0e 100644 --- a/spec/ruby/library/matrix/t_spec.rb +++ b/spec/ruby/library/matrix/t_spec.rb @@ -1,6 +1,8 @@ require_relative '../../spec_helper' -require_relative 'shared/transpose' +require 'matrix' -describe "Matrix#transpose" do - it_behaves_like :matrix_transpose, :t +describe "Matrix#t" do + it "is an alias of Matrix#transpose" do + Matrix.instance_method(:t).should == Matrix.instance_method(:transpose) + end end diff --git a/spec/ruby/library/matrix/tr_spec.rb b/spec/ruby/library/matrix/tr_spec.rb index e17bd790d7412a..04d237d4836f13 100644 --- a/spec/ruby/library/matrix/tr_spec.rb +++ b/spec/ruby/library/matrix/tr_spec.rb @@ -1,7 +1,8 @@ require_relative '../../spec_helper' -require_relative 'shared/trace' require 'matrix' describe "Matrix#tr" do - it_behaves_like :trace, :tr + it "is an alias of Matrix#trace" do + Matrix.instance_method(:tr).should == Matrix.instance_method(:trace) + end end diff --git a/spec/ruby/library/matrix/trace_spec.rb b/spec/ruby/library/matrix/trace_spec.rb index 290e7cb1f71d64..831278c8383b66 100644 --- a/spec/ruby/library/matrix/trace_spec.rb +++ b/spec/ruby/library/matrix/trace_spec.rb @@ -1,7 +1,12 @@ require_relative '../../spec_helper' -require_relative 'shared/trace' require 'matrix' describe "Matrix#trace" do - it_behaves_like :trace, :trace + it "returns the sum of diagonal elements in a square Matrix" do + Matrix[[7,6], [3,9]].trace.should == 16 + end + + it "returns the sum of diagonal elements in a rectangular Matrix" do + ->{ Matrix[[1,2,3], [4,5,6]].trace}.should.raise(Matrix::ErrDimensionMismatch) + end end diff --git a/spec/ruby/library/matrix/transpose_spec.rb b/spec/ruby/library/matrix/transpose_spec.rb index 79600dd439ec10..0b24ab32a7df11 100644 --- a/spec/ruby/library/matrix/transpose_spec.rb +++ b/spec/ruby/library/matrix/transpose_spec.rb @@ -1,6 +1,19 @@ require_relative '../../spec_helper' -require_relative 'shared/transpose' +require_relative 'fixtures/classes' describe "Matrix#transpose" do - it_behaves_like :matrix_transpose, :transpose + it "returns a transposed matrix" do + Matrix[[1, 2], [3, 4], [5, 6]].transpose.should == Matrix[[1, 3, 5], [2, 4, 6]] + end + + it "can transpose empty matrices" do + m = Matrix[[], [], []] + m.transpose.transpose.should == m + end + + describe "for a subclass of Matrix" do + it "returns an instance of that subclass" do + MatrixSub.ins.transpose.should.instance_of?(MatrixSub) + end + end end diff --git a/spec/ruby/library/matrix/unit_spec.rb b/spec/ruby/library/matrix/unit_spec.rb index 6a41d729c72349..112199612256a9 100644 --- a/spec/ruby/library/matrix/unit_spec.rb +++ b/spec/ruby/library/matrix/unit_spec.rb @@ -1,6 +1,8 @@ require_relative '../../spec_helper' -require_relative 'shared/identity' +require 'matrix' describe "Matrix.unit" do - it_behaves_like :matrix_identity, :unit + it "is an alias of Matrix.identity" do + Matrix.method(:unit).should == Matrix.method(:identity) + end end diff --git a/spec/ruby/library/net-http/http/active_spec.rb b/spec/ruby/library/net-http/http/active_spec.rb index c2602745949ce8..ba870b39d23fc0 100644 --- a/spec/ruby/library/net-http/http/active_spec.rb +++ b/spec/ruby/library/net-http/http/active_spec.rb @@ -1,8 +1,8 @@ require_relative '../../../spec_helper' require 'net/http' -require_relative 'fixtures/http_server' -require_relative 'shared/started' describe "Net::HTTP#active?" do - it_behaves_like :net_http_started_p, :active? + it "is an alias of Net::HTTP#started?" do + Net::HTTP.instance_method(:active?).should == Net::HTTP.instance_method(:started?) + end end diff --git a/spec/ruby/library/net-http/http/get2_spec.rb b/spec/ruby/library/net-http/http/get2_spec.rb index 57c05ec64b818c..046443d73e1424 100644 --- a/spec/ruby/library/net-http/http/get2_spec.rb +++ b/spec/ruby/library/net-http/http/get2_spec.rb @@ -1,8 +1,8 @@ require_relative '../../../spec_helper' require 'net/http' -require_relative 'fixtures/http_server' -require_relative 'shared/request_get' describe "Net::HTTP#get2" do - it_behaves_like :net_http_request_get, :get2 + it "is an alias of Net::HTTP#request_get" do + Net::HTTP.instance_method(:get2).should == Net::HTTP.instance_method(:request_get) + end end diff --git a/spec/ruby/library/net-http/http/head2_spec.rb b/spec/ruby/library/net-http/http/head2_spec.rb index 84cfff33d7cdad..19c0cede9f92f9 100644 --- a/spec/ruby/library/net-http/http/head2_spec.rb +++ b/spec/ruby/library/net-http/http/head2_spec.rb @@ -1,8 +1,8 @@ require_relative '../../../spec_helper' require 'net/http' -require_relative 'fixtures/http_server' -require_relative 'shared/request_head' describe "Net::HTTP#head2" do - it_behaves_like :net_http_request_head, :head2 + it "is an alias of Net::HTTP#request_head" do + Net::HTTP.instance_method(:head2).should == Net::HTTP.instance_method(:request_head) + end end diff --git a/spec/ruby/library/net-http/http/is_version_1_1_spec.rb b/spec/ruby/library/net-http/http/is_version_1_1_spec.rb index bdb343f9e0e2a9..f4910ef1e430d9 100644 --- a/spec/ruby/library/net-http/http/is_version_1_1_spec.rb +++ b/spec/ruby/library/net-http/http/is_version_1_1_spec.rb @@ -1,7 +1,8 @@ require_relative '../../../spec_helper' require 'net/http' -require_relative 'shared/version_1_1' describe "Net::HTTP.is_version_1_1?" do - it_behaves_like :net_http_version_1_1_p, :is_version_1_1? + it "is an alias of Net::HTTP.version_1_1?" do + Net::HTTP.method(:is_version_1_1?).should == Net::HTTP.method(:version_1_1?) + end end diff --git a/spec/ruby/library/net-http/http/is_version_1_2_spec.rb b/spec/ruby/library/net-http/http/is_version_1_2_spec.rb index 555bb205dd8379..555724babe8c71 100644 --- a/spec/ruby/library/net-http/http/is_version_1_2_spec.rb +++ b/spec/ruby/library/net-http/http/is_version_1_2_spec.rb @@ -1,7 +1,8 @@ require_relative '../../../spec_helper' require 'net/http' -require_relative 'shared/version_1_2' describe "Net::HTTP.is_version_1_2?" do - it_behaves_like :net_http_version_1_2_p, :is_version_1_2? + it "is an alias of Net::HTTP.version_1_2?" do + Net::HTTP.method(:is_version_1_2?).should == Net::HTTP.method(:version_1_2?) + end end diff --git a/spec/ruby/library/net-http/http/post2_spec.rb b/spec/ruby/library/net-http/http/post2_spec.rb index abc998709fe0c4..68c2a9ea064ac4 100644 --- a/spec/ruby/library/net-http/http/post2_spec.rb +++ b/spec/ruby/library/net-http/http/post2_spec.rb @@ -1,8 +1,8 @@ require_relative '../../../spec_helper' require 'net/http' -require_relative 'fixtures/http_server' -require_relative 'shared/request_post' describe "Net::HTTP#post2" do - it_behaves_like :net_http_request_post, :post2 + it "is an alias of Net::HTTP#request_post" do + Net::HTTP.instance_method(:post2).should == Net::HTTP.instance_method(:request_post) + end end diff --git a/spec/ruby/library/net-http/http/put2_spec.rb b/spec/ruby/library/net-http/http/put2_spec.rb index 7b03a39d0b828d..237df67e82a87a 100644 --- a/spec/ruby/library/net-http/http/put2_spec.rb +++ b/spec/ruby/library/net-http/http/put2_spec.rb @@ -1,8 +1,8 @@ require_relative '../../../spec_helper' require 'net/http' -require_relative 'fixtures/http_server' -require_relative 'shared/request_put' describe "Net::HTTP#put2" do - it_behaves_like :net_http_request_put, :put2 + it "is an alias of Net::HTTP#request_put" do + Net::HTTP.instance_method(:put2).should == Net::HTTP.instance_method(:request_put) + end end diff --git a/spec/ruby/library/net-http/http/request_get_spec.rb b/spec/ruby/library/net-http/http/request_get_spec.rb index 98025a14a18a7f..1737e62439eb9e 100644 --- a/spec/ruby/library/net-http/http/request_get_spec.rb +++ b/spec/ruby/library/net-http/http/request_get_spec.rb @@ -1,8 +1,45 @@ require_relative '../../../spec_helper' require 'net/http' require_relative 'fixtures/http_server' -require_relative 'shared/request_get' describe "Net::HTTP#request_get" do - it_behaves_like :net_http_request_get, :get2 + before :each do + NetHTTPSpecs.start_server + @http = Net::HTTP.start("localhost", NetHTTPSpecs.port) + end + + after :each do + @http.finish if @http.started? + NetHTTPSpecs.stop_server + end + + describe "when passed no block" do + it "sends a GET request to the passed path and returns the response" do + response = @http.request_get("/request") + response.body.should == "Request type: GET" + end + + it "returns a Net::HTTPResponse object" do + response = @http.request_get("/request") + response.should.is_a?(Net::HTTPResponse) + end + end + + describe "when passed a block" do + it "sends a GET request to the passed path and returns the response" do + response = @http.request_get("/request") {} + response.body.should == "Request type: GET" + end + + it "yields the response to the passed block" do + @http.request_get("/request") do |response| + response.body.should == "Request type: GET" + end + end + + it "returns a Net::HTTPResponse object" do + response = @http.request_get("/request") {} + response.should.is_a?(Net::HTTPResponse) + end + end end diff --git a/spec/ruby/library/net-http/http/request_head_spec.rb b/spec/ruby/library/net-http/http/request_head_spec.rb index 8f514d4eeeaa29..7c46ebfc539cd3 100644 --- a/spec/ruby/library/net-http/http/request_head_spec.rb +++ b/spec/ruby/library/net-http/http/request_head_spec.rb @@ -1,8 +1,45 @@ require_relative '../../../spec_helper' require 'net/http' require_relative 'fixtures/http_server' -require_relative 'shared/request_head' describe "Net::HTTP#request_head" do - it_behaves_like :net_http_request_head, :request_head + before :each do + NetHTTPSpecs.start_server + @http = Net::HTTP.start("localhost", NetHTTPSpecs.port) + end + + after :each do + @http.finish if @http.started? + NetHTTPSpecs.stop_server + end + + describe "when passed no block" do + it "sends a head request to the passed path and returns the response" do + response = @http.request_head("/request") + response.body.should == nil + end + + it "returns a Net::HTTPResponse object" do + response = @http.request_head("/request") + response.should.is_a?(Net::HTTPResponse) + end + end + + describe "when passed a block" do + it "sends a head request to the passed path and returns the response" do + response = @http.request_head("/request") {} + response.body.should == nil + end + + it "yields the response to the passed block" do + @http.request_head("/request") do |response| + response.body.should == nil + end + end + + it "returns a Net::HTTPResponse object" do + response = @http.request_head("/request") {} + response.should.is_a?(Net::HTTPResponse) + end + end end diff --git a/spec/ruby/library/net-http/http/request_post_spec.rb b/spec/ruby/library/net-http/http/request_post_spec.rb index 719bd5a7eeca29..8cfdd3469ea61f 100644 --- a/spec/ruby/library/net-http/http/request_post_spec.rb +++ b/spec/ruby/library/net-http/http/request_post_spec.rb @@ -1,8 +1,45 @@ require_relative '../../../spec_helper' require 'net/http' require_relative 'fixtures/http_server' -require_relative 'shared/request_post' describe "Net::HTTP#request_post" do - it_behaves_like :net_http_request_post, :request_post + before :each do + NetHTTPSpecs.start_server + @http = Net::HTTP.start("localhost", NetHTTPSpecs.port) + end + + after :each do + @http.finish if @http.started? + NetHTTPSpecs.stop_server + end + + describe "when passed no block" do + it "sends a post request to the passed path and returns the response" do + response = @http.request_post("/request", "test=test") + response.body.should == "Request type: POST" + end + + it "returns a Net::HTTPResponse object" do + response = @http.request_post("/request", "test=test") + response.should.is_a?(Net::HTTPResponse) + end + end + + describe "when passed a block" do + it "sends a post request to the passed path and returns the response" do + response = @http.request_post("/request", "test=test") {} + response.body.should == "Request type: POST" + end + + it "yields the response to the passed block" do + @http.request_post("/request", "test=test") do |response| + response.body.should == "Request type: POST" + end + end + + it "returns a Net::HTTPResponse object" do + response = @http.request_post("/request", "test=test") {} + response.should.is_a?(Net::HTTPResponse) + end + end end diff --git a/spec/ruby/library/net-http/http/request_put_spec.rb b/spec/ruby/library/net-http/http/request_put_spec.rb index 9fcf3a98d68a9c..b7388a21c83f68 100644 --- a/spec/ruby/library/net-http/http/request_put_spec.rb +++ b/spec/ruby/library/net-http/http/request_put_spec.rb @@ -1,8 +1,45 @@ require_relative '../../../spec_helper' require 'net/http' require_relative 'fixtures/http_server' -require_relative 'shared/request_put' describe "Net::HTTP#request_put" do - it_behaves_like :net_http_request_put, :request_put + before :each do + NetHTTPSpecs.start_server + @http = Net::HTTP.start("localhost", NetHTTPSpecs.port) + end + + after :each do + @http.finish if @http.started? + NetHTTPSpecs.stop_server + end + + describe "when passed no block" do + it "sends a put request to the passed path and returns the response" do + response = @http.request_put("/request", "test=test") + response.body.should == "Request type: PUT" + end + + it "returns a Net::HTTPResponse object" do + response = @http.request_put("/request", "test=test") + response.should.is_a?(Net::HTTPResponse) + end + end + + describe "when passed a block" do + it "sends a put request to the passed path and returns the response" do + response = @http.request_put("/request", "test=test") {} + response.body.should == "Request type: PUT" + end + + it "yields the response to the passed block" do + @http.request_put("/request", "test=test") do |response| + response.body.should == "Request type: PUT" + end + end + + it "returns a Net::HTTPResponse object" do + response = @http.request_put("/request", "test=test") {} + response.should.is_a?(Net::HTTPResponse) + end + end end diff --git a/spec/ruby/library/net-http/http/shared/request_get.rb b/spec/ruby/library/net-http/http/shared/request_get.rb deleted file mode 100644 index 4f2f152ea4d7a9..00000000000000 --- a/spec/ruby/library/net-http/http/shared/request_get.rb +++ /dev/null @@ -1,41 +0,0 @@ -describe :net_http_request_get, shared: true do - before :each do - NetHTTPSpecs.start_server - @http = Net::HTTP.start("localhost", NetHTTPSpecs.port) - end - - after :each do - @http.finish if @http.started? - NetHTTPSpecs.stop_server - end - - describe "when passed no block" do - it "sends a GET request to the passed path and returns the response" do - response = @http.send(@method, "/request") - response.body.should == "Request type: GET" - end - - it "returns a Net::HTTPResponse object" do - response = @http.send(@method, "/request") - response.should.is_a?(Net::HTTPResponse) - end - end - - describe "when passed a block" do - it "sends a GET request to the passed path and returns the response" do - response = @http.send(@method, "/request") {} - response.body.should == "Request type: GET" - end - - it "yields the response to the passed block" do - @http.send(@method, "/request") do |response| - response.body.should == "Request type: GET" - end - end - - it "returns a Net::HTTPResponse object" do - response = @http.send(@method, "/request") {} - response.should.is_a?(Net::HTTPResponse) - end - end -end diff --git a/spec/ruby/library/net-http/http/shared/request_head.rb b/spec/ruby/library/net-http/http/shared/request_head.rb deleted file mode 100644 index a5fe787d327f64..00000000000000 --- a/spec/ruby/library/net-http/http/shared/request_head.rb +++ /dev/null @@ -1,41 +0,0 @@ -describe :net_http_request_head, shared: true do - before :each do - NetHTTPSpecs.start_server - @http = Net::HTTP.start("localhost", NetHTTPSpecs.port) - end - - after :each do - @http.finish if @http.started? - NetHTTPSpecs.stop_server - end - - describe "when passed no block" do - it "sends a head request to the passed path and returns the response" do - response = @http.send(@method, "/request") - response.body.should == nil - end - - it "returns a Net::HTTPResponse object" do - response = @http.send(@method, "/request") - response.should.is_a?(Net::HTTPResponse) - end - end - - describe "when passed a block" do - it "sends a head request to the passed path and returns the response" do - response = @http.send(@method, "/request") {} - response.body.should == nil - end - - it "yields the response to the passed block" do - @http.send(@method, "/request") do |response| - response.body.should == nil - end - end - - it "returns a Net::HTTPResponse object" do - response = @http.send(@method, "/request") {} - response.should.is_a?(Net::HTTPResponse) - end - end -end diff --git a/spec/ruby/library/net-http/http/shared/request_post.rb b/spec/ruby/library/net-http/http/shared/request_post.rb deleted file mode 100644 index 73cfd577cff321..00000000000000 --- a/spec/ruby/library/net-http/http/shared/request_post.rb +++ /dev/null @@ -1,41 +0,0 @@ -describe :net_http_request_post, shared: true do - before :each do - NetHTTPSpecs.start_server - @http = Net::HTTP.start("localhost", NetHTTPSpecs.port) - end - - after :each do - @http.finish if @http.started? - NetHTTPSpecs.stop_server - end - - describe "when passed no block" do - it "sends a post request to the passed path and returns the response" do - response = @http.send(@method, "/request", "test=test") - response.body.should == "Request type: POST" - end - - it "returns a Net::HTTPResponse object" do - response = @http.send(@method, "/request", "test=test") - response.should.is_a?(Net::HTTPResponse) - end - end - - describe "when passed a block" do - it "sends a post request to the passed path and returns the response" do - response = @http.send(@method, "/request", "test=test") {} - response.body.should == "Request type: POST" - end - - it "yields the response to the passed block" do - @http.send(@method, "/request", "test=test") do |response| - response.body.should == "Request type: POST" - end - end - - it "returns a Net::HTTPResponse object" do - response = @http.send(@method, "/request", "test=test") {} - response.should.is_a?(Net::HTTPResponse) - end - end -end diff --git a/spec/ruby/library/net-http/http/shared/request_put.rb b/spec/ruby/library/net-http/http/shared/request_put.rb deleted file mode 100644 index 3b64d7e055777c..00000000000000 --- a/spec/ruby/library/net-http/http/shared/request_put.rb +++ /dev/null @@ -1,41 +0,0 @@ -describe :net_http_request_put, shared: true do - before :each do - NetHTTPSpecs.start_server - @http = Net::HTTP.start("localhost", NetHTTPSpecs.port) - end - - after :each do - @http.finish if @http.started? - NetHTTPSpecs.stop_server - end - - describe "when passed no block" do - it "sends a put request to the passed path and returns the response" do - response = @http.send(@method, "/request", "test=test") - response.body.should == "Request type: PUT" - end - - it "returns a Net::HTTPResponse object" do - response = @http.send(@method, "/request", "test=test") - response.should.is_a?(Net::HTTPResponse) - end - end - - describe "when passed a block" do - it "sends a put request to the passed path and returns the response" do - response = @http.send(@method, "/request", "test=test") {} - response.body.should == "Request type: PUT" - end - - it "yields the response to the passed block" do - @http.send(@method, "/request", "test=test") do |response| - response.body.should == "Request type: PUT" - end - end - - it "returns a Net::HTTPResponse object" do - response = @http.send(@method, "/request", "test=test") {} - response.should.is_a?(Net::HTTPResponse) - end - end -end diff --git a/spec/ruby/library/net-http/http/shared/started.rb b/spec/ruby/library/net-http/http/shared/started.rb deleted file mode 100644 index 0ab18a4e83f3f0..00000000000000 --- a/spec/ruby/library/net-http/http/shared/started.rb +++ /dev/null @@ -1,26 +0,0 @@ -describe :net_http_started_p, shared: true do - before :each do - NetHTTPSpecs.start_server - @http = Net::HTTP.new("localhost", NetHTTPSpecs.port) - end - - after :each do - @http.finish if @http.started? - NetHTTPSpecs.stop_server - end - - it "returns true when self has been started" do - @http.start - @http.send(@method).should == true - end - - it "returns false when self has not been started yet" do - @http.send(@method).should == false - end - - it "returns false when self has been stopped again" do - @http.start - @http.finish - @http.send(@method).should == false - end -end diff --git a/spec/ruby/library/net-http/http/shared/version_1_1.rb b/spec/ruby/library/net-http/http/shared/version_1_1.rb deleted file mode 100644 index 84e7264eabfec0..00000000000000 --- a/spec/ruby/library/net-http/http/shared/version_1_1.rb +++ /dev/null @@ -1,6 +0,0 @@ -describe :net_http_version_1_1_p, shared: true do - it "returns the state of net/http 1.1 features" do - Net::HTTP.version_1_2 - Net::HTTP.send(@method).should == false - end -end diff --git a/spec/ruby/library/net-http/http/shared/version_1_2.rb b/spec/ruby/library/net-http/http/shared/version_1_2.rb deleted file mode 100644 index dcf541970453ef..00000000000000 --- a/spec/ruby/library/net-http/http/shared/version_1_2.rb +++ /dev/null @@ -1,6 +0,0 @@ -describe :net_http_version_1_2_p, shared: true do - it "returns the state of net/http 1.2 features" do - Net::HTTP.version_1_2 - Net::HTTP.send(@method).should == true - end -end diff --git a/spec/ruby/library/net-http/http/started_spec.rb b/spec/ruby/library/net-http/http/started_spec.rb index cbb82ceefa8687..a0b46fcbd22220 100644 --- a/spec/ruby/library/net-http/http/started_spec.rb +++ b/spec/ruby/library/net-http/http/started_spec.rb @@ -1,8 +1,30 @@ require_relative '../../../spec_helper' require 'net/http' require_relative 'fixtures/http_server' -require_relative 'shared/started' describe "Net::HTTP#started?" do - it_behaves_like :net_http_started_p, :started? + before :each do + NetHTTPSpecs.start_server + @http = Net::HTTP.new("localhost", NetHTTPSpecs.port) + end + + after :each do + @http.finish if @http.started? + NetHTTPSpecs.stop_server + end + + it "returns true when self has been started" do + @http.start + @http.started?.should == true + end + + it "returns false when self has not been started yet" do + @http.started?.should == false + end + + it "returns false when self has been stopped again" do + @http.start + @http.finish + @http.started?.should == false + end end diff --git a/spec/ruby/library/net-http/http/version_1_1_spec.rb b/spec/ruby/library/net-http/http/version_1_1_spec.rb index 34a4ac8a6bb4f6..7f87bf30f95226 100644 --- a/spec/ruby/library/net-http/http/version_1_1_spec.rb +++ b/spec/ruby/library/net-http/http/version_1_1_spec.rb @@ -1,7 +1,9 @@ require_relative '../../../spec_helper' require 'net/http' -require_relative 'shared/version_1_1' describe "Net::HTTP.version_1_1?" do - it_behaves_like :net_http_version_1_1_p, :version_1_1? + it "returns the state of net/http 1.1 features" do + Net::HTTP.version_1_2 + Net::HTTP.version_1_1?.should == false + end end diff --git a/spec/ruby/library/net-http/http/version_1_2_spec.rb b/spec/ruby/library/net-http/http/version_1_2_spec.rb index 4918597234f4cc..73ca70ac7b3bec 100644 --- a/spec/ruby/library/net-http/http/version_1_2_spec.rb +++ b/spec/ruby/library/net-http/http/version_1_2_spec.rb @@ -1,6 +1,5 @@ require_relative '../../../spec_helper' require 'net/http' -require_relative 'shared/version_1_2' describe "Net::HTTP.version_1_2" do it "turns on net/http 1.2 features" do @@ -16,5 +15,8 @@ end describe "Net::HTTP.version_1_2?" do - it_behaves_like :net_http_version_1_2_p, :version_1_2? + it "returns the state of net/http 1.2 features" do + Net::HTTP.version_1_2 + Net::HTTP.version_1_2?.should == true + end end diff --git a/spec/ruby/library/net-http/httpheader/canonical_each_spec.rb b/spec/ruby/library/net-http/httpheader/canonical_each_spec.rb index 64a5cae89e2dc0..c009e9f7ea910f 100644 --- a/spec/ruby/library/net-http/httpheader/canonical_each_spec.rb +++ b/spec/ruby/library/net-http/httpheader/canonical_each_spec.rb @@ -1,8 +1,9 @@ require_relative '../../../spec_helper' require 'net/http' -require_relative 'fixtures/classes' -require_relative 'shared/each_capitalized' describe "Net::HTTPHeader#canonical_each" do - it_behaves_like :net_httpheader_each_capitalized, :canonical_each + it "is an alias of Net::HTTPHeader#each_capitalized" do + Net::HTTPHeader.instance_method(:canonical_each).should == + Net::HTTPHeader.instance_method(:each_capitalized) + end end diff --git a/spec/ruby/library/net-http/httpheader/content_type_spec.rb b/spec/ruby/library/net-http/httpheader/content_type_spec.rb index c9c936ba0f224d..0ee43a6942a217 100644 --- a/spec/ruby/library/net-http/httpheader/content_type_spec.rb +++ b/spec/ruby/library/net-http/httpheader/content_type_spec.rb @@ -1,7 +1,6 @@ require_relative '../../../spec_helper' require 'net/http' require_relative 'fixtures/classes' -require_relative 'shared/set_content_type' describe "Net::HTTPHeader#content_type" do before :each do @@ -22,5 +21,8 @@ end describe "Net::HTTPHeader#content_type=" do - it_behaves_like :net_httpheader_set_content_type, :content_type= + it "is an alias of Net::HTTPHeader#set_content_type" do + Net::HTTPHeader.instance_method(:content_type=).should == + Net::HTTPHeader.instance_method(:set_content_type) + end end diff --git a/spec/ruby/library/net-http/httpheader/each_capitalized_spec.rb b/spec/ruby/library/net-http/httpheader/each_capitalized_spec.rb index 1e853995ea660a..e24e77823820b9 100644 --- a/spec/ruby/library/net-http/httpheader/each_capitalized_spec.rb +++ b/spec/ruby/library/net-http/httpheader/each_capitalized_spec.rb @@ -1,8 +1,35 @@ require_relative '../../../spec_helper' require 'net/http' require_relative 'fixtures/classes' -require_relative 'shared/each_capitalized' describe "Net::HTTPHeader#each_capitalized" do - it_behaves_like :net_httpheader_each_capitalized, :each_capitalized + before :each do + @headers = NetHTTPHeaderSpecs::Example.new + @headers["my-header"] = "test" + @headers.add_field("my-Other-Header", "a") + @headers.add_field("My-Other-header", "b") + end + + describe "when passed a block" do + it "yields each header entry to the passed block (capitalized keys, values joined)" do + res = [] + @headers.each_capitalized do |key, value| + res << [key, value] + end + res.sort.should == [["My-Header", "test"], ["My-Other-Header", "a, b"]] + end + end + + describe "when passed no block" do + it "returns an Enumerator" do + enumerator = @headers.each_capitalized + enumerator.should.instance_of?(Enumerator) + + res = [] + enumerator.each do |*key| + res << key + end + res.sort.should == [["My-Header", "test"], ["My-Other-Header", "a, b"]] + end + end end diff --git a/spec/ruby/library/net-http/httpheader/each_header_spec.rb b/spec/ruby/library/net-http/httpheader/each_header_spec.rb index 869feebacfe9e9..63c1106d18dd5a 100644 --- a/spec/ruby/library/net-http/httpheader/each_header_spec.rb +++ b/spec/ruby/library/net-http/httpheader/each_header_spec.rb @@ -1,8 +1,35 @@ require_relative '../../../spec_helper' require 'net/http' require_relative 'fixtures/classes' -require_relative 'shared/each_header' describe "Net::HTTPHeader#each_header" do - it_behaves_like :net_httpheader_each_header, :each_header + before :each do + @headers = NetHTTPHeaderSpecs::Example.new + @headers["My-Header"] = "test" + @headers.add_field("My-Other-Header", "a") + @headers.add_field("My-Other-Header", "b") + end + + describe "when passed a block" do + it "yields each header entry to the passed block (keys in lower case, values joined)" do + res = [] + @headers.each_header do |key, value| + res << [key, value] + end + res.sort.should == [["my-header", "test"], ["my-other-header", "a, b"]] + end + end + + describe "when passed no block" do + it "returns an Enumerator" do + enumerator = @headers.each_header + enumerator.should.instance_of?(Enumerator) + + res = [] + enumerator.each do |*key| + res << key + end + res.sort.should == [["my-header", "test"], ["my-other-header", "a, b"]] + end + end end diff --git a/spec/ruby/library/net-http/httpheader/each_key_spec.rb b/spec/ruby/library/net-http/httpheader/each_key_spec.rb index 1ad145629fd0e2..a5635da5dba2d5 100644 --- a/spec/ruby/library/net-http/httpheader/each_key_spec.rb +++ b/spec/ruby/library/net-http/httpheader/each_key_spec.rb @@ -1,8 +1,35 @@ require_relative '../../../spec_helper' require 'net/http' require_relative 'fixtures/classes' -require_relative 'shared/each_name' describe "Net::HTTPHeader#each_key" do - it_behaves_like :net_httpheader_each_name, :each_key + before :each do + @headers = NetHTTPHeaderSpecs::Example.new + @headers["My-Header"] = "test" + @headers.add_field("My-Other-Header", "a") + @headers.add_field("My-Other-Header", "b") + end + + describe "when passed a block" do + it "yields each header key to the passed block (keys in lower case)" do + res = [] + @headers.each_key do |key| + res << key + end + res.sort.should == ["my-header", "my-other-header"] + end + end + + describe "when passed no block" do + it "returns an Enumerator" do + enumerator = @headers.each_key + enumerator.should.instance_of?(Enumerator) + + res = [] + enumerator.each do |key| + res << key + end + res.sort.should == ["my-header", "my-other-header"] + end + end end diff --git a/spec/ruby/library/net-http/httpheader/each_name_spec.rb b/spec/ruby/library/net-http/httpheader/each_name_spec.rb index f819bd989d43b7..02f9761f80d942 100644 --- a/spec/ruby/library/net-http/httpheader/each_name_spec.rb +++ b/spec/ruby/library/net-http/httpheader/each_name_spec.rb @@ -1,8 +1,10 @@ require_relative '../../../spec_helper' require 'net/http' require_relative 'fixtures/classes' -require_relative 'shared/each_name' describe "Net::HTTPHeader#each_name" do - it_behaves_like :net_httpheader_each_name, :each_name + it "is an alias of Net::HTTPHeader#each_key" do + Net::HTTPHeader.instance_method(:each_name).should == + Net::HTTPHeader.instance_method(:each_key) + end end diff --git a/spec/ruby/library/net-http/httpheader/each_spec.rb b/spec/ruby/library/net-http/httpheader/each_spec.rb index ff37249d0ac66f..e219609b67ad32 100644 --- a/spec/ruby/library/net-http/httpheader/each_spec.rb +++ b/spec/ruby/library/net-http/httpheader/each_spec.rb @@ -1,8 +1,9 @@ require_relative '../../../spec_helper' require 'net/http' -require_relative 'fixtures/classes' -require_relative 'shared/each_header' describe "Net::HTTPHeader#each" do - it_behaves_like :net_httpheader_each_header, :each + it "is an alias of Net::HTTPHeader#each_header" do + Net::HTTPHeader.instance_method(:each).should == + Net::HTTPHeader.instance_method(:each_header) + end end diff --git a/spec/ruby/library/net-http/httpheader/form_data_spec.rb b/spec/ruby/library/net-http/httpheader/form_data_spec.rb index acd913f53a97a5..8d4974cde3ddac 100644 --- a/spec/ruby/library/net-http/httpheader/form_data_spec.rb +++ b/spec/ruby/library/net-http/httpheader/form_data_spec.rb @@ -1,8 +1,10 @@ require_relative '../../../spec_helper' require 'net/http' require_relative 'fixtures/classes' -require_relative 'shared/set_form_data' describe "Net::HTTPHeader#form_data=" do - it_behaves_like :net_httpheader_set_form_data, :form_data= + it "is an alias of Net::HTTPHeader#set_form_data" do + Net::HTTPHeader.instance_method(:form_data=).should == + Net::HTTPHeader.instance_method(:set_form_data) + end end diff --git a/spec/ruby/library/net-http/httpheader/length_spec.rb b/spec/ruby/library/net-http/httpheader/length_spec.rb index 57e32742e478e0..1f059719e91312 100644 --- a/spec/ruby/library/net-http/httpheader/length_spec.rb +++ b/spec/ruby/library/net-http/httpheader/length_spec.rb @@ -1,8 +1,9 @@ require_relative '../../../spec_helper' require 'net/http' -require_relative 'fixtures/classes' -require_relative 'shared/size' describe "Net::HTTPHeader#length" do - it_behaves_like :net_httpheader_size, :length + it "is an alias of Net::HTTPHeader#size" do + Net::HTTPHeader.instance_method(:length).should == + Net::HTTPHeader.instance_method(:size) + end end diff --git a/spec/ruby/library/net-http/httpheader/range_spec.rb b/spec/ruby/library/net-http/httpheader/range_spec.rb index 0fc0feb5c9b8be..8944e2d5f2a339 100644 --- a/spec/ruby/library/net-http/httpheader/range_spec.rb +++ b/spec/ruby/library/net-http/httpheader/range_spec.rb @@ -1,7 +1,6 @@ require_relative '../../../spec_helper' require 'net/http' require_relative 'fixtures/classes' -require_relative 'shared/set_range' describe "Net::HTTPHeader#range" do before :each do @@ -44,5 +43,8 @@ end describe "Net::HTTPHeader#range=" do - it_behaves_like :net_httpheader_set_range, :range= + it "is an alias of Net::HTTPHeader#set_range" do + Net::HTTPHeader.instance_method(:range=).should == + Net::HTTPHeader.instance_method(:set_range) + end end diff --git a/spec/ruby/library/net-http/httpheader/set_content_type_spec.rb b/spec/ruby/library/net-http/httpheader/set_content_type_spec.rb index 7ec4f90b8e5274..367406162671f5 100644 --- a/spec/ruby/library/net-http/httpheader/set_content_type_spec.rb +++ b/spec/ruby/library/net-http/httpheader/set_content_type_spec.rb @@ -1,8 +1,22 @@ require_relative '../../../spec_helper' require 'net/http' require_relative 'fixtures/classes' -require_relative 'shared/set_content_type' describe "Net::HTTPHeader#set_content_type" do - it_behaves_like :net_httpheader_set_content_type, :set_content_type + describe "when passed type, params" do + before :each do + @headers = NetHTTPHeaderSpecs::Example.new + end + + it "sets the 'Content-Type' header entry based on the passed type and params" do + @headers.set_content_type("text/html") + @headers["Content-Type"].should == "text/html" + + @headers.set_content_type("text/html", "charset" => "utf-8") + @headers["Content-Type"].should == "text/html; charset=utf-8" + + @headers.set_content_type("text/html", "charset" => "utf-8", "rubyspec" => "rocks") + @headers["Content-Type"].split(/; /).sort.should == %w[charset=utf-8 rubyspec=rocks text/html] + end + end end diff --git a/spec/ruby/library/net-http/httpheader/set_form_data_spec.rb b/spec/ruby/library/net-http/httpheader/set_form_data_spec.rb index 7aac19f04510e7..093dc100d5b5a4 100644 --- a/spec/ruby/library/net-http/httpheader/set_form_data_spec.rb +++ b/spec/ruby/library/net-http/httpheader/set_form_data_spec.rb @@ -1,8 +1,31 @@ require_relative '../../../spec_helper' require 'net/http' require_relative 'fixtures/classes' -require_relative 'shared/set_form_data' describe "Net::HTTPHeader#set_form_data" do - it_behaves_like :net_httpheader_set_form_data, :set_form_data + before :each do + @headers = NetHTTPHeaderSpecs::Example.new + end + + describe "when passed params" do + it "automatically set the 'Content-Type' to 'application/x-www-form-urlencoded'" do + @headers.set_form_data("cmd" => "search", "q" => "ruby", "max" => "50") + @headers["Content-Type"].should == "application/x-www-form-urlencoded" + end + + it "sets self's body based on the passed form parameters" do + @headers.set_form_data("cmd" => "search", "q" => "ruby", "max" => "50") + @headers.body.split("&").sort.should == ["cmd=search", "max=50", "q=ruby"] + end + end + + describe "when passed params, separator" do + it "sets self's body based on the passed form parameters and the passed separator" do + @headers.set_form_data({"cmd" => "search", "q" => "ruby", "max" => "50"}, "&") + @headers.body.split("&").sort.should == ["cmd=search", "max=50", "q=ruby"] + + @headers.set_form_data({"cmd" => "search", "q" => "ruby", "max" => "50"}, ";") + @headers.body.split(";").sort.should == ["cmd=search", "max=50", "q=ruby"] + end + end end diff --git a/spec/ruby/library/net-http/httpheader/set_range_spec.rb b/spec/ruby/library/net-http/httpheader/set_range_spec.rb index 0f98de55e62d86..d48ed1897a6628 100644 --- a/spec/ruby/library/net-http/httpheader/set_range_spec.rb +++ b/spec/ruby/library/net-http/httpheader/set_range_spec.rb @@ -1,8 +1,93 @@ require_relative '../../../spec_helper' require 'net/http' require_relative 'fixtures/classes' -require_relative 'shared/set_range' describe "Net::HTTPHeader#set_range" do - it_behaves_like :net_httpheader_set_range, :set_range + before :each do + @headers = NetHTTPHeaderSpecs::Example.new + end + + describe "when passed nil" do + it "returns nil" do + @headers.set_range(nil).should == nil + end + + it "deletes the 'Range' header entry" do + @headers["Range"] = "bytes 0-499/1234" + @headers.set_range(nil) + @headers["Range"].should == nil + end + end + + describe "when passed Numeric" do + it "sets the 'Range' header entry based on the passed Numeric" do + @headers.set_range(10) + @headers["Range"].should == "bytes=0-9" + + @headers.set_range(-10) + @headers["Range"].should == "bytes=-10" + + @headers.set_range(10.9) + @headers["Range"].should == "bytes=0-9" + end + end + + describe "when passed Range" do + it "sets the 'Range' header entry based on the passed Range" do + @headers.set_range(10..200) + @headers["Range"].should == "bytes=10-200" + + @headers.set_range(1..5) + @headers["Range"].should == "bytes=1-5" + + @headers.set_range(1...5) + @headers["Range"].should == "bytes=1-4" + + @headers.set_range(234..567) + @headers["Range"].should == "bytes=234-567" + + @headers.set_range(-5..-1) + @headers["Range"].should == "bytes=-5" + + @headers.set_range(1..-1) + @headers["Range"].should == "bytes=1-" + end + + it "raises a Net::HTTPHeaderSyntaxError when the first Range element is negative" do + -> { @headers.set_range(-10..5) }.should.raise(Net::HTTPHeaderSyntaxError) + end + + it "raises a Net::HTTPHeaderSyntaxError when the last Range element is negative" do + -> { @headers.set_range(10..-5) }.should.raise(Net::HTTPHeaderSyntaxError) + end + + it "raises a Net::HTTPHeaderSyntaxError when the last Range element is smaller than the first" do + -> { @headers.set_range(10..5) }.should.raise(Net::HTTPHeaderSyntaxError) + end + end + + describe "when passed start, end" do + it "sets the 'Range' header entry based on the passed start and length values" do + @headers.set_range(10, 200) + @headers["Range"].should == "bytes=10-209" + + @headers.set_range(1, 5) + @headers["Range"].should == "bytes=1-5" + + @headers.set_range(234, 567) + @headers["Range"].should == "bytes=234-800" + end + + it "raises a Net::HTTPHeaderSyntaxError when start is negative" do + -> { @headers.set_range(-10, 5) }.should.raise(Net::HTTPHeaderSyntaxError) + end + + it "raises a Net::HTTPHeaderSyntaxError when start + length is negative" do + -> { @headers.set_range(10, -15) }.should.raise(Net::HTTPHeaderSyntaxError) + end + + it "raises a Net::HTTPHeaderSyntaxError when length is negative" do + -> { @headers.set_range(10, -4) }.should.raise(Net::HTTPHeaderSyntaxError) + end + end end diff --git a/spec/ruby/library/net-http/httpheader/shared/each_capitalized.rb b/spec/ruby/library/net-http/httpheader/shared/each_capitalized.rb deleted file mode 100644 index c12df62787e43e..00000000000000 --- a/spec/ruby/library/net-http/httpheader/shared/each_capitalized.rb +++ /dev/null @@ -1,31 +0,0 @@ -describe :net_httpheader_each_capitalized, shared: true do - before :each do - @headers = NetHTTPHeaderSpecs::Example.new - @headers["my-header"] = "test" - @headers.add_field("my-Other-Header", "a") - @headers.add_field("My-Other-header", "b") - end - - describe "when passed a block" do - it "yields each header entry to the passed block (capitalized keys, values joined)" do - res = [] - @headers.send(@method) do |key, value| - res << [key, value] - end - res.sort.should == [["My-Header", "test"], ["My-Other-Header", "a, b"]] - end - end - - describe "when passed no block" do - it "returns an Enumerator" do - enumerator = @headers.send(@method) - enumerator.should.instance_of?(Enumerator) - - res = [] - enumerator.each do |*key| - res << key - end - res.sort.should == [["My-Header", "test"], ["My-Other-Header", "a, b"]] - end - end -end diff --git a/spec/ruby/library/net-http/httpheader/shared/each_header.rb b/spec/ruby/library/net-http/httpheader/shared/each_header.rb deleted file mode 100644 index 5913665a4de203..00000000000000 --- a/spec/ruby/library/net-http/httpheader/shared/each_header.rb +++ /dev/null @@ -1,31 +0,0 @@ -describe :net_httpheader_each_header, shared: true do - before :each do - @headers = NetHTTPHeaderSpecs::Example.new - @headers["My-Header"] = "test" - @headers.add_field("My-Other-Header", "a") - @headers.add_field("My-Other-Header", "b") - end - - describe "when passed a block" do - it "yields each header entry to the passed block (keys in lower case, values joined)" do - res = [] - @headers.send(@method) do |key, value| - res << [key, value] - end - res.sort.should == [["my-header", "test"], ["my-other-header", "a, b"]] - end - end - - describe "when passed no block" do - it "returns an Enumerator" do - enumerator = @headers.send(@method) - enumerator.should.instance_of?(Enumerator) - - res = [] - enumerator.each do |*key| - res << key - end - res.sort.should == [["my-header", "test"], ["my-other-header", "a, b"]] - end - end -end diff --git a/spec/ruby/library/net-http/httpheader/shared/each_name.rb b/spec/ruby/library/net-http/httpheader/shared/each_name.rb deleted file mode 100644 index 29c9400fef161f..00000000000000 --- a/spec/ruby/library/net-http/httpheader/shared/each_name.rb +++ /dev/null @@ -1,31 +0,0 @@ -describe :net_httpheader_each_name, shared: true do - before :each do - @headers = NetHTTPHeaderSpecs::Example.new - @headers["My-Header"] = "test" - @headers.add_field("My-Other-Header", "a") - @headers.add_field("My-Other-Header", "b") - end - - describe "when passed a block" do - it "yields each header key to the passed block (keys in lower case)" do - res = [] - @headers.send(@method) do |key| - res << key - end - res.sort.should == ["my-header", "my-other-header"] - end - end - - describe "when passed no block" do - it "returns an Enumerator" do - enumerator = @headers.send(@method) - enumerator.should.instance_of?(Enumerator) - - res = [] - enumerator.each do |key| - res << key - end - res.sort.should == ["my-header", "my-other-header"] - end - end -end diff --git a/spec/ruby/library/net-http/httpheader/shared/set_content_type.rb b/spec/ruby/library/net-http/httpheader/shared/set_content_type.rb deleted file mode 100644 index b7359bdca65ba8..00000000000000 --- a/spec/ruby/library/net-http/httpheader/shared/set_content_type.rb +++ /dev/null @@ -1,18 +0,0 @@ -describe :net_httpheader_set_content_type, shared: true do - describe "when passed type, params" do - before :each do - @headers = NetHTTPHeaderSpecs::Example.new - end - - it "sets the 'Content-Type' header entry based on the passed type and params" do - @headers.send(@method, "text/html") - @headers["Content-Type"].should == "text/html" - - @headers.send(@method, "text/html", "charset" => "utf-8") - @headers["Content-Type"].should == "text/html; charset=utf-8" - - @headers.send(@method, "text/html", "charset" => "utf-8", "rubyspec" => "rocks") - @headers["Content-Type"].split(/; /).sort.should == %w[charset=utf-8 rubyspec=rocks text/html] - end - end -end diff --git a/spec/ruby/library/net-http/httpheader/shared/set_form_data.rb b/spec/ruby/library/net-http/httpheader/shared/set_form_data.rb deleted file mode 100644 index db20b18803dcc3..00000000000000 --- a/spec/ruby/library/net-http/httpheader/shared/set_form_data.rb +++ /dev/null @@ -1,27 +0,0 @@ -describe :net_httpheader_set_form_data, shared: true do - before :each do - @headers = NetHTTPHeaderSpecs::Example.new - end - - describe "when passed params" do - it "automatically set the 'Content-Type' to 'application/x-www-form-urlencoded'" do - @headers.send(@method, "cmd" => "search", "q" => "ruby", "max" => "50") - @headers["Content-Type"].should == "application/x-www-form-urlencoded" - end - - it "sets self's body based on the passed form parameters" do - @headers.send(@method, "cmd" => "search", "q" => "ruby", "max" => "50") - @headers.body.split("&").sort.should == ["cmd=search", "max=50", "q=ruby"] - end - end - - describe "when passed params, separator" do - it "sets self's body based on the passed form parameters and the passed separator" do - @headers.send(@method, {"cmd" => "search", "q" => "ruby", "max" => "50"}, "&") - @headers.body.split("&").sort.should == ["cmd=search", "max=50", "q=ruby"] - - @headers.send(@method, {"cmd" => "search", "q" => "ruby", "max" => "50"}, ";") - @headers.body.split(";").sort.should == ["cmd=search", "max=50", "q=ruby"] - end - end -end diff --git a/spec/ruby/library/net-http/httpheader/shared/set_range.rb b/spec/ruby/library/net-http/httpheader/shared/set_range.rb deleted file mode 100644 index 9ab50a075e8388..00000000000000 --- a/spec/ruby/library/net-http/httpheader/shared/set_range.rb +++ /dev/null @@ -1,89 +0,0 @@ -describe :net_httpheader_set_range, shared: true do - before :each do - @headers = NetHTTPHeaderSpecs::Example.new - end - - describe "when passed nil" do - it "returns nil" do - @headers.send(@method, nil).should == nil - end - - it "deletes the 'Range' header entry" do - @headers["Range"] = "bytes 0-499/1234" - @headers.send(@method, nil) - @headers["Range"].should == nil - end - end - - describe "when passed Numeric" do - it "sets the 'Range' header entry based on the passed Numeric" do - @headers.send(@method, 10) - @headers["Range"].should == "bytes=0-9" - - @headers.send(@method, -10) - @headers["Range"].should == "bytes=-10" - - @headers.send(@method, 10.9) - @headers["Range"].should == "bytes=0-9" - end - end - - describe "when passed Range" do - it "sets the 'Range' header entry based on the passed Range" do - @headers.send(@method, 10..200) - @headers["Range"].should == "bytes=10-200" - - @headers.send(@method, 1..5) - @headers["Range"].should == "bytes=1-5" - - @headers.send(@method, 1...5) - @headers["Range"].should == "bytes=1-4" - - @headers.send(@method, 234..567) - @headers["Range"].should == "bytes=234-567" - - @headers.send(@method, -5..-1) - @headers["Range"].should == "bytes=-5" - - @headers.send(@method, 1..-1) - @headers["Range"].should == "bytes=1-" - end - - it "raises a Net::HTTPHeaderSyntaxError when the first Range element is negative" do - -> { @headers.send(@method, -10..5) }.should.raise(Net::HTTPHeaderSyntaxError) - end - - it "raises a Net::HTTPHeaderSyntaxError when the last Range element is negative" do - -> { @headers.send(@method, 10..-5) }.should.raise(Net::HTTPHeaderSyntaxError) - end - - it "raises a Net::HTTPHeaderSyntaxError when the last Range element is smaller than the first" do - -> { @headers.send(@method, 10..5) }.should.raise(Net::HTTPHeaderSyntaxError) - end - end - - describe "when passed start, end" do - it "sets the 'Range' header entry based on the passed start and length values" do - @headers.send(@method, 10, 200) - @headers["Range"].should == "bytes=10-209" - - @headers.send(@method, 1, 5) - @headers["Range"].should == "bytes=1-5" - - @headers.send(@method, 234, 567) - @headers["Range"].should == "bytes=234-800" - end - - it "raises a Net::HTTPHeaderSyntaxError when start is negative" do - -> { @headers.send(@method, -10, 5) }.should.raise(Net::HTTPHeaderSyntaxError) - end - - it "raises a Net::HTTPHeaderSyntaxError when start + length is negative" do - -> { @headers.send(@method, 10, -15) }.should.raise(Net::HTTPHeaderSyntaxError) - end - - it "raises a Net::HTTPHeaderSyntaxError when length is negative" do - -> { @headers.send(@method, 10, -4) }.should.raise(Net::HTTPHeaderSyntaxError) - end - end -end diff --git a/spec/ruby/library/net-http/httpheader/shared/size.rb b/spec/ruby/library/net-http/httpheader/shared/size.rb deleted file mode 100644 index b38310a940776e..00000000000000 --- a/spec/ruby/library/net-http/httpheader/shared/size.rb +++ /dev/null @@ -1,18 +0,0 @@ -describe :net_httpheader_size, shared: true do - before :each do - @headers = NetHTTPHeaderSpecs::Example.new - end - - it "returns the number of header entries in self" do - @headers.send(@method).should.eql?(0) - - @headers["a"] = "b" - @headers.send(@method).should.eql?(1) - - @headers["b"] = "b" - @headers.send(@method).should.eql?(2) - - @headers["c"] = "c" - @headers.send(@method).should.eql?(3) - end -end diff --git a/spec/ruby/library/net-http/httpheader/size_spec.rb b/spec/ruby/library/net-http/httpheader/size_spec.rb index 210060ce210fa5..f84a0fb5ab8732 100644 --- a/spec/ruby/library/net-http/httpheader/size_spec.rb +++ b/spec/ruby/library/net-http/httpheader/size_spec.rb @@ -1,8 +1,22 @@ require_relative '../../../spec_helper' require 'net/http' require_relative 'fixtures/classes' -require_relative 'shared/size' describe "Net::HTTPHeader#size" do - it_behaves_like :net_httpheader_size, :size + before :each do + @headers = NetHTTPHeaderSpecs::Example.new + end + + it "returns the number of header entries in self" do + @headers.size.should.eql?(0) + + @headers["a"] = "b" + @headers.size.should.eql?(1) + + @headers["b"] = "b" + @headers.size.should.eql?(2) + + @headers["c"] = "c" + @headers.size.should.eql?(3) + end end diff --git a/spec/ruby/library/net-http/httpresponse/body_spec.rb b/spec/ruby/library/net-http/httpresponse/body_spec.rb index ddfcd834c49de2..5b00913687f8ba 100644 --- a/spec/ruby/library/net-http/httpresponse/body_spec.rb +++ b/spec/ruby/library/net-http/httpresponse/body_spec.rb @@ -1,7 +1,22 @@ require_relative '../../../spec_helper' require 'net/http' -require_relative 'shared/body' +require 'stringio' describe "Net::HTTPResponse#body" do - it_behaves_like :net_httpresponse_body, :body + before :each do + @res = Net::HTTPUnknownResponse.new("1.0", "???", "test response") + @socket = Net::BufferedIO.new(StringIO.new("test body")) + end + + it "returns the read body" do + @res.reading_body(@socket, true) do + @res.body.should == "test body" + end + end + + it "returns the previously read body if called a second time" do + @res.reading_body(@socket, true) do + @res.body.should.equal?(@res.body) + end + end end diff --git a/spec/ruby/library/net-http/httpresponse/entity_spec.rb b/spec/ruby/library/net-http/httpresponse/entity_spec.rb index ca8c4b29c09cfd..d2201db37b2735 100644 --- a/spec/ruby/library/net-http/httpresponse/entity_spec.rb +++ b/spec/ruby/library/net-http/httpresponse/entity_spec.rb @@ -1,7 +1,9 @@ require_relative '../../../spec_helper' require 'net/http' -require_relative 'shared/body' describe "Net::HTTPResponse#entity" do - it_behaves_like :net_httpresponse_body, :entity + it "is an alias of Net::HTTPResponse#body" do + Net::HTTPResponse.instance_method(:entity).should == + Net::HTTPResponse.instance_method(:body) + end end diff --git a/spec/ruby/library/net-http/httpresponse/shared/body.rb b/spec/ruby/library/net-http/httpresponse/shared/body.rb deleted file mode 100644 index 368774fb52836f..00000000000000 --- a/spec/ruby/library/net-http/httpresponse/shared/body.rb +++ /dev/null @@ -1,20 +0,0 @@ -require 'stringio' - -describe :net_httpresponse_body, shared: true do - before :each do - @res = Net::HTTPUnknownResponse.new("1.0", "???", "test response") - @socket = Net::BufferedIO.new(StringIO.new("test body")) - end - - it "returns the read body" do - @res.reading_body(@socket, true) do - @res.send(@method).should == "test body" - end - end - - it "returns the previously read body if called a second time" do - @res.reading_body(@socket, true) do - @res.send(@method).should.equal?(@res.send(@method)) - end - end -end diff --git a/spec/ruby/library/openstruct/equal_value_spec.rb b/spec/ruby/library/openstruct/equal_value_spec.rb index c72c09ce14a25f..ec30214fd37d62 100644 --- a/spec/ruby/library/openstruct/equal_value_spec.rb +++ b/spec/ruby/library/openstruct/equal_value_spec.rb @@ -1,5 +1,5 @@ require_relative '../../spec_helper' -require "ostruct" +require 'ostruct' require_relative 'fixtures/classes' describe "OpenStruct#==" do diff --git a/spec/ruby/library/openstruct/inspect_spec.rb b/spec/ruby/library/openstruct/inspect_spec.rb index e2fed415285926..81da96d6bfd959 100644 --- a/spec/ruby/library/openstruct/inspect_spec.rb +++ b/spec/ruby/library/openstruct/inspect_spec.rb @@ -1,8 +1,8 @@ require_relative '../../spec_helper' require 'ostruct' -require_relative 'fixtures/classes' -require_relative 'shared/inspect' describe "OpenStruct#inspect" do - it_behaves_like :ostruct_inspect, :inspect + it "is an alias of OpenStruct#to_s" do + OpenStruct.instance_method(:inspect).should == OpenStruct.instance_method(:to_s) + end end diff --git a/spec/ruby/library/openstruct/shared/inspect.rb b/spec/ruby/library/openstruct/shared/inspect.rb deleted file mode 100644 index d5fffa0e2e378e..00000000000000 --- a/spec/ruby/library/openstruct/shared/inspect.rb +++ /dev/null @@ -1,20 +0,0 @@ -describe :ostruct_inspect, shared: true do - it "returns a String representation of self" do - os = OpenStruct.new(name: "John Smith") - os.send(@method).should == "#" - - os = OpenStruct.new(age: 20, name: "John Smith") - os.send(@method).should.is_a?(String) - end - - it "correctly handles self-referential OpenStructs" do - os = OpenStruct.new - os.self = os - os.send(@method).should == "#>" - end - - it "correctly handles OpenStruct subclasses" do - os = OpenStructSpecs::OpenStructSub.new(name: "John Smith") - os.send(@method).should == "#" - end -end diff --git a/spec/ruby/library/openstruct/to_s_spec.rb b/spec/ruby/library/openstruct/to_s_spec.rb index 73d91bf981f6df..9131cd4897fbea 100644 --- a/spec/ruby/library/openstruct/to_s_spec.rb +++ b/spec/ruby/library/openstruct/to_s_spec.rb @@ -1,8 +1,24 @@ require_relative '../../spec_helper' require 'ostruct' require_relative 'fixtures/classes' -require_relative 'shared/inspect' describe "OpenStruct#to_s" do - it_behaves_like :ostruct_inspect, :to_s + it "returns a String representation of self" do + os = OpenStruct.new(name: "John Smith") + os.to_s.should == "#" + + os = OpenStruct.new(age: 20, name: "John Smith") + os.to_s.should.is_a?(String) + end + + it "correctly handles self-referential OpenStructs" do + os = OpenStruct.new + os.self = os + os.to_s.should == "#>" + end + + it "correctly handles OpenStruct subclasses" do + os = OpenStructSpecs::OpenStructSub.new(name: "John Smith") + os.to_s.should == "#" + end end diff --git a/spec/ruby/library/pathname/case_compare_spec.rb b/spec/ruby/library/pathname/case_compare_spec.rb new file mode 100644 index 00000000000000..0cf799dd235e3d --- /dev/null +++ b/spec/ruby/library/pathname/case_compare_spec.rb @@ -0,0 +1,8 @@ +require_relative '../../spec_helper' +require 'pathname' + +describe "Pathname#===" do + it "is an alias of Pathname#==" do + Pathname.instance_method(:===).should == Pathname.instance_method(:==) + end +end diff --git a/spec/ruby/library/pathname/divide_spec.rb b/spec/ruby/library/pathname/divide_spec.rb index 8af79d0c8fee04..e5afc9f864b222 100644 --- a/spec/ruby/library/pathname/divide_spec.rb +++ b/spec/ruby/library/pathname/divide_spec.rb @@ -1,6 +1,8 @@ require_relative '../../spec_helper' -require_relative 'shared/plus' +require 'pathname' describe "Pathname#/" do - it_behaves_like :pathname_plus, :/ + it "is an alias of Pathname#+" do + Pathname.instance_method(:/).should == Pathname.instance_method(:+) + end end diff --git a/spec/ruby/library/pathname/plus_spec.rb b/spec/ruby/library/pathname/plus_spec.rb index 57e472c2661a7b..76316df9d24586 100644 --- a/spec/ruby/library/pathname/plus_spec.rb +++ b/spec/ruby/library/pathname/plus_spec.rb @@ -1,6 +1,9 @@ require_relative '../../spec_helper' -require_relative 'shared/plus' +require 'pathname' describe "Pathname#+" do - it_behaves_like :pathname_plus, :+ + it "appends a pathname to self" do + p = Pathname.new("/usr") + (p + "bin/ruby").should == Pathname.new("/usr/bin/ruby") + end end diff --git a/spec/ruby/library/pathname/shared/plus.rb b/spec/ruby/library/pathname/shared/plus.rb deleted file mode 100644 index b3b896ea43ce52..00000000000000 --- a/spec/ruby/library/pathname/shared/plus.rb +++ /dev/null @@ -1,8 +0,0 @@ -require 'pathname' - -describe :pathname_plus, shared: true do - it "appends a pathname to self" do - p = Pathname.new("/usr") - p.send(@method, "bin/ruby").should == Pathname.new("/usr/bin/ruby") - end -end diff --git a/spec/ruby/library/prime/next_spec.rb b/spec/ruby/library/prime/next_spec.rb index 39c4ae16ae8565..07e80ab3a53b4d 100644 --- a/spec/ruby/library/prime/next_spec.rb +++ b/spec/ruby/library/prime/next_spec.rb @@ -1,7 +1,11 @@ require_relative '../../spec_helper' -require_relative 'shared/next' require 'prime' describe "Prime#next" do - it_behaves_like :prime_next, :next + it "returns the element at the current position and moves forward" do + p = Prime.instance.each + p.next.should == 2 + p.next.should == 3 + p.next.next.should == 6 + end end diff --git a/spec/ruby/library/prime/shared/next.rb b/spec/ruby/library/prime/shared/next.rb deleted file mode 100644 index f79b2c051ece31..00000000000000 --- a/spec/ruby/library/prime/shared/next.rb +++ /dev/null @@ -1,8 +0,0 @@ -describe :prime_next, shared: true do - it "returns the element at the current position and moves forward" do - p = Prime.instance.each - p.next.should == 2 - p.next.should == 3 - p.next.next.should == 6 - end -end diff --git a/spec/ruby/library/prime/succ_spec.rb b/spec/ruby/library/prime/succ_spec.rb index 34c18d2ba0645b..86f76c2513ede4 100644 --- a/spec/ruby/library/prime/succ_spec.rb +++ b/spec/ruby/library/prime/succ_spec.rb @@ -1,7 +1,9 @@ require_relative '../../spec_helper' -require_relative 'shared/next' require 'prime' describe "Prime#succ" do - it_behaves_like :prime_next, :succ + it "is an alias of Prime#next" do + p = Prime.instance.each + p.method(:succ).should == p.method(:next) + end end diff --git a/spec/ruby/library/socket/addrinfo/shared/to_sockaddr.rb b/spec/ruby/library/socket/addrinfo/shared/to_sockaddr.rb deleted file mode 100644 index 70d6bfbbfed7a7..00000000000000 --- a/spec/ruby/library/socket/addrinfo/shared/to_sockaddr.rb +++ /dev/null @@ -1,47 +0,0 @@ -describe :socket_addrinfo_to_sockaddr, shared: true do - describe "for an ipv4 socket" do - before :each do - @addrinfo = Addrinfo.tcp("127.0.0.1", 80) - end - - it "returns a sockaddr packed structure" do - @addrinfo.send(@method).should == Socket.sockaddr_in(80, '127.0.0.1') - end - end - - describe "for an ipv6 socket" do - before :each do - @addrinfo = Addrinfo.tcp("::1", 80) - end - - it "returns a sockaddr packed structure" do - @addrinfo.send(@method).should == Socket.sockaddr_in(80, '::1') - end - end - - describe "for a unix socket" do - before :each do - @addrinfo = Addrinfo.unix("/tmp/sock") - end - - it "returns a sockaddr packed structure" do - @addrinfo.send(@method).should == Socket.sockaddr_un('/tmp/sock') - end - end - - describe 'using a Addrinfo with just an IP address' do - it 'returns a String' do - addr = Addrinfo.ip('127.0.0.1') - - addr.send(@method).should == Socket.sockaddr_in(0, '127.0.0.1') - end - end - - describe 'using a Addrinfo without an IP and port' do - it 'returns a String' do - addr = Addrinfo.new(['AF_INET', 0, '', '']) - - addr.send(@method).should == Socket.sockaddr_in(0, '') - end - end -end diff --git a/spec/ruby/library/socket/addrinfo/to_s_spec.rb b/spec/ruby/library/socket/addrinfo/to_s_spec.rb index ddf994e051b28f..5c1c82793c378d 100644 --- a/spec/ruby/library/socket/addrinfo/to_s_spec.rb +++ b/spec/ruby/library/socket/addrinfo/to_s_spec.rb @@ -1,6 +1,7 @@ require_relative '../spec_helper' -require_relative 'shared/to_sockaddr' describe "Addrinfo#to_s" do - it_behaves_like :socket_addrinfo_to_sockaddr, :to_s + it "is an alias of Addrinfo#to_sockaddr" do + Addrinfo.instance_method(:to_s).should == Addrinfo.instance_method(:to_sockaddr) + end end diff --git a/spec/ruby/library/socket/addrinfo/to_sockaddr_spec.rb b/spec/ruby/library/socket/addrinfo/to_sockaddr_spec.rb index b9f75454bd1f7f..c703c7b28f20c5 100644 --- a/spec/ruby/library/socket/addrinfo/to_sockaddr_spec.rb +++ b/spec/ruby/library/socket/addrinfo/to_sockaddr_spec.rb @@ -1,6 +1,49 @@ require_relative '../spec_helper' -require_relative 'shared/to_sockaddr' describe "Addrinfo#to_sockaddr" do - it_behaves_like :socket_addrinfo_to_sockaddr, :to_sockaddr + describe "for an ipv4 socket" do + before :each do + @addrinfo = Addrinfo.tcp("127.0.0.1", 80) + end + + it "returns a sockaddr packed structure" do + @addrinfo.to_sockaddr.should == Socket.sockaddr_in(80, '127.0.0.1') + end + end + + describe "for an ipv6 socket" do + before :each do + @addrinfo = Addrinfo.tcp("::1", 80) + end + + it "returns a sockaddr packed structure" do + @addrinfo.to_sockaddr.should == Socket.sockaddr_in(80, '::1') + end + end + + describe "for a unix socket" do + before :each do + @addrinfo = Addrinfo.unix("/tmp/sock") + end + + it "returns a sockaddr packed structure" do + @addrinfo.to_sockaddr.should == Socket.sockaddr_un('/tmp/sock') + end + end + + describe 'using a Addrinfo with just an IP address' do + it 'returns a String' do + addr = Addrinfo.ip('127.0.0.1') + + addr.to_sockaddr.should == Socket.sockaddr_in(0, '127.0.0.1') + end + end + + describe 'using a Addrinfo without an IP and port' do + it 'returns a String' do + addr = Addrinfo.new(['AF_INET', 0, '', '']) + + addr.to_sockaddr.should == Socket.sockaddr_in(0, '') + end + end end diff --git a/spec/ruby/library/socket/shared/pack_sockaddr.rb b/spec/ruby/library/socket/shared/pack_sockaddr.rb deleted file mode 100644 index db6f39612d08aa..00000000000000 --- a/spec/ruby/library/socket/shared/pack_sockaddr.rb +++ /dev/null @@ -1,92 +0,0 @@ -# coding: utf-8 -describe :socket_pack_sockaddr_in, shared: true do - it "packs and unpacks" do - sockaddr_in = Socket.public_send(@method, 0, nil) - port, addr = Socket.unpack_sockaddr_in(sockaddr_in) - ["127.0.0.1", "::1"].include?(addr).should == true - port.should == 0 - - sockaddr_in = Socket.public_send(@method, 0, '') - Socket.unpack_sockaddr_in(sockaddr_in).should == [0, '0.0.0.0'] - - sockaddr_in = Socket.public_send(@method, 80, '127.0.0.1') - Socket.unpack_sockaddr_in(sockaddr_in).should == [80, '127.0.0.1'] - - sockaddr_in = Socket.public_send(@method, '80', '127.0.0.1') - Socket.unpack_sockaddr_in(sockaddr_in).should == [80, '127.0.0.1'] - - sockaddr_in = Socket.public_send(@method, nil, '127.0.0.1') - Socket.unpack_sockaddr_in(sockaddr_in).should == [0, '127.0.0.1'] - - sockaddr_in = Socket.public_send(@method, 80, Socket::INADDR_ANY) - Socket.unpack_sockaddr_in(sockaddr_in).should == [80, '0.0.0.0'] - end - - it 'resolves the service name to a port' do - sockaddr_in = Socket.public_send(@method, 'http', '127.0.0.1') - Socket.unpack_sockaddr_in(sockaddr_in).should == [80, '127.0.0.1'] - end - - describe 'using an IPv4 address' do - it 'returns a String of 16 bytes' do - str = Socket.public_send(@method, 80, '127.0.0.1') - - str.should.instance_of?(String) - str.bytesize.should == 16 - end - end - - describe 'using an IPv6 address' do - it 'returns a String of 28 bytes' do - str = Socket.public_send(@method, 80, '::1') - - str.should.instance_of?(String) - str.bytesize.should == 28 - end - end -end - -describe :socket_pack_sockaddr_un, shared: true do - it 'should be idempotent' do - bytes = Socket.public_send(@method, '/tmp/foo').bytes - bytes[2..9].should == [47, 116, 109, 112, 47, 102, 111, 111] - bytes[10..-1].all?(&:zero?).should == true - end - - it "packs and unpacks" do - sockaddr_un = Socket.public_send(@method, '/tmp/s') - Socket.unpack_sockaddr_un(sockaddr_un).should == '/tmp/s' - end - - it "handles correctly paths with multibyte chars" do - sockaddr_un = Socket.public_send(@method, '/home/вася/sock') - path = Socket.unpack_sockaddr_un(sockaddr_un).encode('UTF-8', 'UTF-8') - path.should == '/home/вася/sock' - end - - platform_is :linux do - it 'returns a String of 110 bytes' do - str = Socket.public_send(@method, '/tmp/test.sock') - - str.should.instance_of?(String) - str.bytesize.should == 110 - end - end - - platform_is :bsd do - it 'returns a String of 106 bytes' do - str = Socket.public_send(@method, '/tmp/test.sock') - - str.should.instance_of?(String) - str.bytesize.should == 106 - end - end - - platform_is_not :aix do - it "raises ArgumentError for paths that are too long" do - # AIX doesn't raise error - long_path = 'a' * 110 - -> { Socket.public_send(@method, long_path) }.should.raise(ArgumentError) - end - end -end diff --git a/spec/ruby/library/socket/shared/socketpair.rb b/spec/ruby/library/socket/shared/socketpair.rb deleted file mode 100644 index 7fcd4d6b46b95b..00000000000000 --- a/spec/ruby/library/socket/shared/socketpair.rb +++ /dev/null @@ -1,138 +0,0 @@ -describe :socket_socketpair, shared: true do - platform_is_not :windows do - it "ensures the returned sockets are connected" do - s1, s2 = Socket.public_send(@method, Socket::AF_UNIX, 1, 0) - s1.puts("test") - s2.gets.should == "test\n" - s1.close - s2.close - end - - it "responses with array of two sockets" do - begin - s1, s2 = Socket.public_send(@method, :UNIX, :STREAM) - - s1.should.instance_of?(Socket) - s2.should.instance_of?(Socket) - ensure - s1.close - s2.close - end - end - - describe 'using an Integer as the 1st and 2nd argument' do - it 'returns two Socket objects' do - s1, s2 = Socket.public_send(@method, Socket::AF_UNIX, Socket::SOCK_STREAM) - - s1.should.instance_of?(Socket) - s2.should.instance_of?(Socket) - s1.close - s2.close - end - end - - describe 'using a Symbol as the 1st and 2nd argument' do - it 'returns two Socket objects' do - s1, s2 = Socket.public_send(@method, :UNIX, :STREAM) - - s1.should.instance_of?(Socket) - s2.should.instance_of?(Socket) - s1.close - s2.close - end - - it 'raises SocketError for an unknown address family' do - -> { Socket.public_send(@method, :CATS, :STREAM) }.should.raise(SocketError) - end - - it 'raises SocketError for an unknown socket type' do - -> { Socket.public_send(@method, :UNIX, :CATS) }.should.raise(SocketError) - end - end - - describe 'using a String as the 1st and 2nd argument' do - it 'returns two Socket objects' do - s1, s2 = Socket.public_send(@method, 'UNIX', 'STREAM') - - s1.should.instance_of?(Socket) - s2.should.instance_of?(Socket) - s1.close - s2.close - end - - it 'raises SocketError for an unknown address family' do - -> { Socket.public_send(@method, 'CATS', 'STREAM') }.should.raise(SocketError) - end - - it 'raises SocketError for an unknown socket type' do - -> { Socket.public_send(@method, 'UNIX', 'CATS') }.should.raise(SocketError) - end - end - - describe 'using an object that responds to #to_str as the 1st and 2nd argument' do - it 'returns two Socket objects' do - family = mock(:family) - type = mock(:type) - - family.stub!(:to_str).and_return('UNIX') - type.stub!(:to_str).and_return('STREAM') - - s1, s2 = Socket.public_send(@method, family, type) - - s1.should.instance_of?(Socket) - s2.should.instance_of?(Socket) - s1.close - s2.close - end - - it 'raises TypeError when #to_str does not return a String' do - family = mock(:family) - type = mock(:type) - - family.stub!(:to_str).and_return(Socket::AF_UNIX) - type.stub!(:to_str).and_return(Socket::SOCK_STREAM) - - -> { Socket.public_send(@method, family, type) }.should.raise(TypeError) - end - - it 'raises SocketError for an unknown address family' do - family = mock(:family) - type = mock(:type) - - family.stub!(:to_str).and_return('CATS') - type.stub!(:to_str).and_return('STREAM') - - -> { Socket.public_send(@method, family, type) }.should.raise(SocketError) - end - - it 'raises SocketError for an unknown socket type' do - family = mock(:family) - type = mock(:type) - - family.stub!(:to_str).and_return('UNIX') - type.stub!(:to_str).and_return('CATS') - - -> { Socket.public_send(@method, family, type) }.should.raise(SocketError) - end - end - - it 'accepts a custom protocol as an Integer as the 3rd argument' do - s1, s2 = Socket.public_send(@method, :UNIX, :STREAM, Socket::IPPROTO_IP) - s1.should.instance_of?(Socket) - s2.should.instance_of?(Socket) - s1.close - s2.close - end - - it 'connects the returned Socket objects' do - s1, s2 = Socket.public_send(@method, :UNIX, :STREAM) - begin - s1.write('hello') - s2.recv(5).should == 'hello' - ensure - s1.close - s2.close - end - end - end -end diff --git a/spec/ruby/library/socket/socket/pack_sockaddr_in_spec.rb b/spec/ruby/library/socket/socket/pack_sockaddr_in_spec.rb index ef2a2d4ba92212..17a737cacd6174 100644 --- a/spec/ruby/library/socket/socket/pack_sockaddr_in_spec.rb +++ b/spec/ruby/library/socket/socket/pack_sockaddr_in_spec.rb @@ -1,7 +1,7 @@ require_relative '../spec_helper' -require_relative '../fixtures/classes' -require_relative '../shared/pack_sockaddr' describe "Socket.pack_sockaddr_in" do - it_behaves_like :socket_pack_sockaddr_in, :pack_sockaddr_in + it "is an alias of Socket.sockaddr_in" do + Socket.method(:pack_sockaddr_in).should == Socket.method(:sockaddr_in) + end end diff --git a/spec/ruby/library/socket/socket/pack_sockaddr_un_spec.rb b/spec/ruby/library/socket/socket/pack_sockaddr_un_spec.rb index 1ee0bc6157f1a2..34d4fc1f51b387 100644 --- a/spec/ruby/library/socket/socket/pack_sockaddr_un_spec.rb +++ b/spec/ruby/library/socket/socket/pack_sockaddr_un_spec.rb @@ -1,7 +1,7 @@ require_relative '../spec_helper' -require_relative '../fixtures/classes' -require_relative '../shared/pack_sockaddr' -describe "Socket#pack_sockaddr_un" do - it_behaves_like :socket_pack_sockaddr_un, :pack_sockaddr_un +describe "Socket.pack_sockaddr_un" do + it "is an alias of Socket.sockaddr_un" do + Socket.method(:pack_sockaddr_un).should == Socket.method(:sockaddr_un) + end end diff --git a/spec/ruby/library/socket/socket/pair_spec.rb b/spec/ruby/library/socket/socket/pair_spec.rb index 8dd470a95e2daf..91317a8d07de32 100644 --- a/spec/ruby/library/socket/socket/pair_spec.rb +++ b/spec/ruby/library/socket/socket/pair_spec.rb @@ -1,7 +1,141 @@ require_relative '../spec_helper' require_relative '../fixtures/classes' -require_relative '../shared/socketpair' describe "Socket.pair" do - it_behaves_like :socket_socketpair, :pair + platform_is_not :windows do + it "ensures the returned sockets are connected" do + s1, s2 = Socket.pair(Socket::AF_UNIX, 1, 0) + s1.puts("test") + s2.gets.should == "test\n" + s1.close + s2.close + end + + it "returns an array of two sockets" do + begin + s1, s2 = Socket.pair(:UNIX, :STREAM) + + s1.should.instance_of?(Socket) + s2.should.instance_of?(Socket) + ensure + s1.close + s2.close + end + end + + describe 'using an Integer as the 1st and 2nd argument' do + it 'returns two Socket objects' do + s1, s2 = Socket.pair(Socket::AF_UNIX, Socket::SOCK_STREAM) + + s1.should.instance_of?(Socket) + s2.should.instance_of?(Socket) + s1.close + s2.close + end + end + + describe 'using a Symbol as the 1st and 2nd argument' do + it 'returns two Socket objects' do + s1, s2 = Socket.pair(:UNIX, :STREAM) + + s1.should.instance_of?(Socket) + s2.should.instance_of?(Socket) + s1.close + s2.close + end + + it 'raises SocketError for an unknown address family' do + -> { Socket.pair(:CATS, :STREAM) }.should.raise(SocketError) + end + + it 'raises SocketError for an unknown socket type' do + -> { Socket.pair(:UNIX, :CATS) }.should.raise(SocketError) + end + end + + describe 'using a String as the 1st and 2nd argument' do + it 'returns two Socket objects' do + s1, s2 = Socket.pair('UNIX', 'STREAM') + + s1.should.instance_of?(Socket) + s2.should.instance_of?(Socket) + s1.close + s2.close + end + + it 'raises SocketError for an unknown address family' do + -> { Socket.pair('CATS', 'STREAM') }.should.raise(SocketError) + end + + it 'raises SocketError for an unknown socket type' do + -> { Socket.pair('UNIX', 'CATS') }.should.raise(SocketError) + end + end + + describe 'using an object that responds to #to_str as the 1st and 2nd argument' do + it 'returns two Socket objects' do + family = mock(:family) + type = mock(:type) + + family.stub!(:to_str).and_return('UNIX') + type.stub!(:to_str).and_return('STREAM') + + s1, s2 = Socket.pair(family, type) + + s1.should.instance_of?(Socket) + s2.should.instance_of?(Socket) + s1.close + s2.close + end + + it 'raises TypeError when #to_str does not return a String' do + family = mock(:family) + type = mock(:type) + + family.stub!(:to_str).and_return(Socket::AF_UNIX) + type.stub!(:to_str).and_return(Socket::SOCK_STREAM) + + -> { Socket.pair(family, type) }.should.raise(TypeError) + end + + it 'raises SocketError for an unknown address family' do + family = mock(:family) + type = mock(:type) + + family.stub!(:to_str).and_return('CATS') + type.stub!(:to_str).and_return('STREAM') + + -> { Socket.pair(family, type) }.should.raise(SocketError) + end + + it 'raises SocketError for an unknown socket type' do + family = mock(:family) + type = mock(:type) + + family.stub!(:to_str).and_return('UNIX') + type.stub!(:to_str).and_return('CATS') + + -> { Socket.pair(family, type) }.should.raise(SocketError) + end + end + + it 'accepts a custom protocol as an Integer as the 3rd argument' do + s1, s2 = Socket.pair(:UNIX, :STREAM, Socket::IPPROTO_IP) + s1.should.instance_of?(Socket) + s2.should.instance_of?(Socket) + s1.close + s2.close + end + + it 'connects the returned Socket objects' do + s1, s2 = Socket.pair(:UNIX, :STREAM) + begin + s1.write('hello') + s2.recv(5).should == 'hello' + ensure + s1.close + s2.close + end + end + end end diff --git a/spec/ruby/library/socket/socket/sockaddr_in_spec.rb b/spec/ruby/library/socket/socket/sockaddr_in_spec.rb index 8ee956ac26d4e5..9d3367cd690e20 100644 --- a/spec/ruby/library/socket/socket/sockaddr_in_spec.rb +++ b/spec/ruby/library/socket/socket/sockaddr_in_spec.rb @@ -1,7 +1,49 @@ require_relative '../spec_helper' require_relative '../fixtures/classes' -require_relative '../shared/pack_sockaddr' -describe "Socket#sockaddr_in" do - it_behaves_like :socket_pack_sockaddr_in, :sockaddr_in +describe "Socket.sockaddr_in" do + it "packs and unpacks" do + sockaddr_in = Socket.sockaddr_in(0, nil) + port, addr = Socket.unpack_sockaddr_in(sockaddr_in) + ["127.0.0.1", "::1"].include?(addr).should == true + port.should == 0 + + sockaddr_in = Socket.sockaddr_in(0, '') + Socket.unpack_sockaddr_in(sockaddr_in).should == [0, '0.0.0.0'] + + sockaddr_in = Socket.sockaddr_in(80, '127.0.0.1') + Socket.unpack_sockaddr_in(sockaddr_in).should == [80, '127.0.0.1'] + + sockaddr_in = Socket.sockaddr_in('80', '127.0.0.1') + Socket.unpack_sockaddr_in(sockaddr_in).should == [80, '127.0.0.1'] + + sockaddr_in = Socket.sockaddr_in(nil, '127.0.0.1') + Socket.unpack_sockaddr_in(sockaddr_in).should == [0, '127.0.0.1'] + + sockaddr_in = Socket.sockaddr_in(80, Socket::INADDR_ANY) + Socket.unpack_sockaddr_in(sockaddr_in).should == [80, '0.0.0.0'] + end + + it 'resolves the service name to a port' do + sockaddr_in = Socket.sockaddr_in('http', '127.0.0.1') + Socket.unpack_sockaddr_in(sockaddr_in).should == [80, '127.0.0.1'] + end + + describe 'using an IPv4 address' do + it 'returns a String of 16 bytes' do + str = Socket.sockaddr_in(80, '127.0.0.1') + + str.should.instance_of?(String) + str.bytesize.should == 16 + end + end + + describe 'using an IPv6 address' do + it 'returns a String of 28 bytes' do + str = Socket.sockaddr_in(80, '::1') + + str.should.instance_of?(String) + str.bytesize.should == 28 + end + end end diff --git a/spec/ruby/library/socket/socket/sockaddr_un_spec.rb b/spec/ruby/library/socket/socket/sockaddr_un_spec.rb index 8922ff4d6da41e..548dc526ff4d20 100644 --- a/spec/ruby/library/socket/socket/sockaddr_un_spec.rb +++ b/spec/ruby/library/socket/socket/sockaddr_un_spec.rb @@ -1,7 +1,47 @@ require_relative '../spec_helper' require_relative '../fixtures/classes' -require_relative '../shared/pack_sockaddr' -describe "Socket#sockaddr_un" do - it_behaves_like :socket_pack_sockaddr_un, :sockaddr_un +describe "Socket.sockaddr_un" do + it 'should be idempotent' do + bytes = Socket.sockaddr_un('/tmp/foo').bytes + bytes[2..9].should == [47, 116, 109, 112, 47, 102, 111, 111] + bytes[10..-1].all?(&:zero?).should == true + end + + it "packs and unpacks" do + sockaddr_un = Socket.sockaddr_un('/tmp/s') + Socket.unpack_sockaddr_un(sockaddr_un).should == '/tmp/s' + end + + it "handles correctly paths with multibyte chars" do + sockaddr_un = Socket.sockaddr_un('/home/вася/sock') + path = Socket.unpack_sockaddr_un(sockaddr_un).encode('UTF-8', 'UTF-8') + path.should == '/home/вася/sock' + end + + platform_is :linux do + it 'returns a String of 110 bytes' do + str = Socket.sockaddr_un('/tmp/test.sock') + + str.should.instance_of?(String) + str.bytesize.should == 110 + end + end + + platform_is :bsd do + it 'returns a String of 106 bytes' do + str = Socket.sockaddr_un('/tmp/test.sock') + + str.should.instance_of?(String) + str.bytesize.should == 106 + end + end + + platform_is_not :aix do + it "raises ArgumentError for paths that are too long" do + # AIX doesn't raise error + long_path = 'a' * 110 + -> { Socket.sockaddr_un(long_path) }.should.raise(ArgumentError) + end + end end diff --git a/spec/ruby/library/socket/socket/socketpair_spec.rb b/spec/ruby/library/socket/socket/socketpair_spec.rb index 551c376d49fab6..191fb358cff83f 100644 --- a/spec/ruby/library/socket/socket/socketpair_spec.rb +++ b/spec/ruby/library/socket/socket/socketpair_spec.rb @@ -1,7 +1,7 @@ require_relative '../spec_helper' -require_relative '../fixtures/classes' -require_relative '../shared/socketpair' describe "Socket.socketpair" do - it_behaves_like :socket_socketpair, :socketpair + it "is an alias of Socket.pair" do + Socket.method(:socketpair).should == Socket.method(:pair) + end end diff --git a/spec/ruby/library/socket/unixsocket/pair_spec.rb b/spec/ruby/library/socket/unixsocket/pair_spec.rb index 9690142668e498..9f04f568fa17e3 100644 --- a/spec/ruby/library/socket/unixsocket/pair_spec.rb +++ b/spec/ruby/library/socket/unixsocket/pair_spec.rb @@ -1,10 +1,8 @@ require_relative '../spec_helper' require_relative '../fixtures/classes' require_relative '../shared/partially_closable_sockets' -require_relative 'shared/pair' describe "UNIXSocket.pair" do - it_should_behave_like :unixsocket_pair it_should_behave_like :partially_closable_sockets before :each do @@ -15,4 +13,47 @@ @s1.close @s2.close end + + it "returns two UNIXSockets" do + @s1.should.instance_of?(UNIXSocket) + @s2.should.instance_of?(UNIXSocket) + end + + it "returns a pair of connected sockets" do + @s1.puts "foo" + @s2.gets.should == "foo\n" + end + + platform_is_not :windows do + it "sets the socket paths to empty Strings" do + @s1.path.should == "" + @s2.path.should == "" + end + + it "sets the socket addresses to empty Strings" do + @s1.addr.should == ["AF_UNIX", ""] + @s2.addr.should == ["AF_UNIX", ""] + end + + it "sets the socket peer addresses to empty Strings" do + @s1.peeraddr.should == ["AF_UNIX", ""] + @s2.peeraddr.should == ["AF_UNIX", ""] + end + end + + platform_is :windows do + it "emulates unnamed sockets with a temporary file with a path" do + @s1.addr.should == ["AF_UNIX", @s1.path] + @s2.peeraddr.should == ["AF_UNIX", @s1.path] + end + + it "sets the peer address of first socket to an empty string" do + @s1.peeraddr.should == ["AF_UNIX", ""] + end + + it "sets the address and path of second socket to an empty string" do + @s2.addr.should == ["AF_UNIX", ""] + @s2.path.should == "" + end + end end diff --git a/spec/ruby/library/socket/unixsocket/shared/pair.rb b/spec/ruby/library/socket/unixsocket/shared/pair.rb deleted file mode 100644 index 49b6a6a4137808..00000000000000 --- a/spec/ruby/library/socket/unixsocket/shared/pair.rb +++ /dev/null @@ -1,47 +0,0 @@ -require_relative '../../spec_helper' -require_relative '../../fixtures/classes' - -describe :unixsocket_pair, shared: true do - it "returns two UNIXSockets" do - @s1.should.instance_of?(UNIXSocket) - @s2.should.instance_of?(UNIXSocket) - end - - it "returns a pair of connected sockets" do - @s1.puts "foo" - @s2.gets.should == "foo\n" - end - - platform_is_not :windows do - it "sets the socket paths to empty Strings" do - @s1.path.should == "" - @s2.path.should == "" - end - - it "sets the socket addresses to empty Strings" do - @s1.addr.should == ["AF_UNIX", ""] - @s2.addr.should == ["AF_UNIX", ""] - end - - it "sets the socket peer addresses to empty Strings" do - @s1.peeraddr.should == ["AF_UNIX", ""] - @s2.peeraddr.should == ["AF_UNIX", ""] - end - end - - platform_is :windows do - it "emulates unnamed sockets with a temporary file with a path" do - @s1.addr.should == ["AF_UNIX", @s1.path] - @s2.peeraddr.should == ["AF_UNIX", @s1.path] - end - - it "sets the peer address of first socket to an empty string" do - @s1.peeraddr.should == ["AF_UNIX", ""] - end - - it "sets the address and path of second socket to an empty string" do - @s2.addr.should == ["AF_UNIX", ""] - @s2.path.should == "" - end - end -end diff --git a/spec/ruby/library/socket/unixsocket/socketpair_spec.rb b/spec/ruby/library/socket/unixsocket/socketpair_spec.rb index c61fc00be4cce7..a8bfb412e546a5 100644 --- a/spec/ruby/library/socket/unixsocket/socketpair_spec.rb +++ b/spec/ruby/library/socket/unixsocket/socketpair_spec.rb @@ -1,18 +1,7 @@ require_relative '../spec_helper' -require_relative '../fixtures/classes' -require_relative '../shared/partially_closable_sockets' -require_relative 'shared/pair' describe "UNIXSocket.socketpair" do - it_should_behave_like :unixsocket_pair - it_should_behave_like :partially_closable_sockets - - before :each do - @s1, @s2 = UNIXSocket.socketpair - end - - after :each do - @s1.close - @s2.close + it "is an alias of UNIXSocket.pair" do + UNIXSocket.method(:socketpair).should == UNIXSocket.method(:pair) end end diff --git a/spec/ruby/library/stringio/each_byte_spec.rb b/spec/ruby/library/stringio/each_byte_spec.rb index 6f82a32441064d..1be0081c1efa0f 100644 --- a/spec/ruby/library/stringio/each_byte_spec.rb +++ b/spec/ruby/library/stringio/each_byte_spec.rb @@ -1,11 +1,51 @@ require_relative '../../spec_helper' require 'stringio' -require_relative 'shared/each_byte' describe "StringIO#each_byte" do - it_behaves_like :stringio_each_byte, :each_byte + before :each do + @io = StringIO.new("xyz") + end + + it "yields each character code in turn" do + seen = [] + @io.each_byte { |b| seen << b } + seen.should == [120, 121, 122] + end + + it "updates the position before each yield" do + seen = [] + @io.each_byte { |b| seen << @io.pos } + seen.should == [1, 2, 3] + end + + it "does not yield if the current position is out of bounds" do + @io.pos = 1000 + seen = nil + @io.each_byte { |b| seen = b } + seen.should == nil + end + + it "returns self" do + @io.each_byte {}.should.equal?(@io) + end + + it "returns an Enumerator when passed no block" do + enum = @io.each_byte + enum.instance_of?(Enumerator).should == true + + seen = [] + enum.each { |b| seen << b } + seen.should == [120, 121, 122] + end end describe "StringIO#each_byte when self is not readable" do - it_behaves_like :stringio_each_byte_not_readable, :each_byte + it "raises an IOError" do + io = StringIO.new(+"xyz", "w") + -> { io.each_byte { |b| b } }.should.raise(IOError) + + io = StringIO.new("xyz") + io.close_read + -> { io.each_byte { |b| b } }.should.raise(IOError) + end end diff --git a/spec/ruby/library/stringio/each_char_spec.rb b/spec/ruby/library/stringio/each_char_spec.rb index 14b2f09a177dc7..1db80c7d07af88 100644 --- a/spec/ruby/library/stringio/each_char_spec.rb +++ b/spec/ruby/library/stringio/each_char_spec.rb @@ -1,11 +1,38 @@ require_relative '../../spec_helper' require 'stringio' -require_relative 'shared/each_char' describe "StringIO#each_char" do - it_behaves_like :stringio_each_char, :each_char + before :each do + @io = StringIO.new("xyz äöü") + end + + it "yields each character code in turn" do + seen = [] + @io.each_char { |c| seen << c } + seen.should == ["x", "y", "z", " ", "ä", "ö", "ü"] + end + + it "returns self" do + @io.each_char {}.should.equal?(@io) + end + + it "returns an Enumerator when passed no block" do + enum = @io.each_char + enum.instance_of?(Enumerator).should == true + + seen = [] + enum.each { |c| seen << c } + seen.should == ["x", "y", "z", " ", "ä", "ö", "ü"] + end end describe "StringIO#each_char when self is not readable" do - it_behaves_like :stringio_each_char_not_readable, :each_char + it "raises an IOError" do + io = StringIO.new(+"xyz", "w") + -> { io.each_char { |b| b } }.should.raise(IOError) + + io = StringIO.new("xyz") + io.close_read + -> { io.each_char { |b| b } }.should.raise(IOError) + end end diff --git a/spec/ruby/library/stringio/each_codepoint_spec.rb b/spec/ruby/library/stringio/each_codepoint_spec.rb index f18de22aad49e2..d4f461db90b2e5 100644 --- a/spec/ruby/library/stringio/each_codepoint_spec.rb +++ b/spec/ruby/library/stringio/each_codepoint_spec.rb @@ -1,9 +1,47 @@ -# -*- encoding: utf-8 -*- require_relative '../../spec_helper' -require_relative 'fixtures/classes' -require_relative 'shared/codepoints' +require 'stringio' # See redmine #1667 describe "StringIO#each_codepoint" do - it_behaves_like :stringio_codepoints, :each_codepoint + before :each do + @io = StringIO.new("∂φ/∂x = gaîté") + @enum = @io.each_codepoint + end + + it "returns an Enumerator" do + @enum.should.instance_of?(Enumerator) + end + + it "yields each codepoint code in turn" do + @enum.to_a.should == [8706, 966, 47, 8706, 120, 32, 61, 32, 103, 97, 238, 116, 233] + end + + it "yields each codepoint starting from the current position" do + @io.pos = 15 + @enum.to_a.should == [238, 116, 233] + end + + it "raises an error if reading invalid sequence" do + @io.pos = 1 # inside of a multibyte sequence + -> { @enum.first }.should.raise(ArgumentError) + end + + it "raises an IOError if not readable" do + @io.close_read + -> { @enum.to_a }.should.raise(IOError) + + io = StringIO.new(+"xyz", "w") + -> { io.each_codepoint.to_a }.should.raise(IOError) + end + + + it "calls the given block" do + r = [] + @io.each_codepoint{|c| r << c } + r.should == [8706, 966, 47, 8706, 120, 32, 61, 32, 103, 97, 238, 116, 233] + end + + it "returns self" do + @io.each_codepoint {|l| l }.should.equal?(@io) + end end diff --git a/spec/ruby/library/stringio/each_line_spec.rb b/spec/ruby/library/stringio/each_line_spec.rb index 4ac0db7c45908f..4abecbf026feeb 100644 --- a/spec/ruby/library/stringio/each_line_spec.rb +++ b/spec/ruby/library/stringio/each_line_spec.rb @@ -1,27 +1,212 @@ require_relative '../../spec_helper' require_relative 'fixtures/classes' -require_relative 'shared/each' describe "StringIO#each_line when passed a separator" do - it_behaves_like :stringio_each_separator, :each_line + before :each do + @io = StringIO.new("a b c d e\n1 2 3 4 5") + end + + it "uses the passed argument as the line separator" do + seen = [] + @io.each_line(" ") {|s| seen << s} + seen.should == ["a ", "b ", "c ", "d ", "e\n1 ", "2 ", "3 ", "4 ", "5"] + end + + it "does not change $_" do + $_ = "test" + @io.each_line(" ") { |s| s} + $_.should == "test" + end + + it "returns self" do + @io.each_line {|l| l }.should.equal?(@io) + end + + it "tries to convert the passed separator to a String using #to_str" do + obj = mock("to_str") + obj.stub!(:to_str).and_return(" ") + + seen = [] + @io.each_line(obj) { |l| seen << l } + seen.should == ["a ", "b ", "c ", "d ", "e\n1 ", "2 ", "3 ", "4 ", "5"] + end + + it "yields self's content starting from the current position when the passed separator is nil" do + seen = [] + io = StringIO.new("1 2 1 2 1 2") + io.pos = 2 + io.each_line(nil) {|s| seen << s} + seen.should == ["2 1 2 1 2"] + end + + it "yields each paragraph with all separation characters when passed an empty String as separator" do + seen = [] + io = StringIO.new("para1\n\npara2\n\n\npara3") + io.each_line("") {|s| seen << s} + seen.should == ["para1\n\n", "para2\n\n\n", "para3"] + end end describe "StringIO#each_line when passed no arguments" do - it_behaves_like :stringio_each_no_arguments, :each_line + before :each do + @io = StringIO.new("a b c d e\n1 2 3 4 5") + end + + it "yields each line to the passed block" do + seen = [] + @io.each_line {|s| seen << s } + seen.should == ["a b c d e\n", "1 2 3 4 5"] + end + + it "yields each line starting from the current position" do + seen = [] + @io.pos = 4 + @io.each_line {|s| seen << s } + seen.should == ["c d e\n", "1 2 3 4 5"] + end + + it "does not change $_" do + $_ = "test" + @io.each_line { |s| s} + $_.should == "test" + end + + it "uses $/ as the default line separator" do + seen = [] + begin + old_rs = $/ + suppress_warning {$/ = " "} + @io.each_line {|s| seen << s } + seen.should.eql?(["a ", "b ", "c ", "d ", "e\n1 ", "2 ", "3 ", "4 ", "5"]) + ensure + suppress_warning {$/ = old_rs} + end + end + + it "returns self" do + @io.each_line {|l| l }.should.equal?(@io) + end + + it "returns an Enumerator when passed no block" do + enum = @io.each_line + enum.instance_of?(Enumerator).should == true + + seen = [] + enum.each { |b| seen << b } + seen.should == ["a b c d e\n", "1 2 3 4 5"] + end end describe "StringIO#each_line when self is not readable" do - it_behaves_like :stringio_each_not_readable, :each_line + it "raises an IOError" do + io = StringIO.new(+"a b c d e", "w") + -> { io.each_line { |b| b } }.should.raise(IOError) + + io = StringIO.new("a b c d e") + io.close_read + -> { io.each_line { |b| b } }.should.raise(IOError) + end +end + +describe "StringIO#each_line when passed chomp" do + it "yields each line with removed newline characters to the passed block" do + seen = [] + io = StringIO.new("a b \rc d e\n1 2 3 4 5\r\nthe end") + io.each_line(chomp: true) {|s| seen << s } + seen.should == ["a b \rc d e", "1 2 3 4 5", "the end"] + end + + it "returns each line with removed newline characters when called without block" do + seen = [] + io = StringIO.new("a b \rc d e\n1 2 3 4 5\r\nthe end") + enum = io.each_line(chomp: true) + enum.each {|s| seen << s } + seen.should == ["a b \rc d e", "1 2 3 4 5", "the end"] + end end describe "StringIO#each_line when passed chomp" do - it_behaves_like :stringio_each_chomp, :each_line + it "yields each line with removed separator to the passed block" do + seen = [] + io = StringIO.new("a b \nc d e|1 2 3 4 5\n|the end") + io.each_line("|", chomp: true) {|s| seen << s } + seen.should == ["a b \nc d e", "1 2 3 4 5\n", "the end"] + end + + it "returns each line with removed separator when called without block" do + seen = [] + io = StringIO.new("a b \nc d e|1 2 3 4 5\n|the end") + enum = io.each_line("|", chomp: true) + enum.each {|s| seen << s } + seen.should == ["a b \nc d e", "1 2 3 4 5\n", "the end"] + end end describe "StringIO#each_line when passed limit" do - it_behaves_like :stringio_each_limit, :each_line + before :each do + @io = StringIO.new("a b c d e\n1 2 3 4 5") + end + + it "returns the data read until the limit is met" do + seen = [] + @io.each_line(4) { |s| seen << s } + seen.should == ["a b ", "c d ", "e\n", "1 2 ", "3 4 ", "5"] + end end describe "StringIO#each when passed separator and limit" do - it_behaves_like :stringio_each_separator_and_limit, :each_line + before :each do + @io = StringIO.new("this>is>an>example") + end + + it "returns the data read until the limit is consumed or the separator is met" do + @io.each_line('>', 8) { |s| break s }.should == "this>" + @io.each_line('>', 2) { |s| break s }.should == "is" + @io.each_line('>', 10) { |s| break s }.should == ">" + @io.each_line('>', 6) { |s| break s }.should == "an>" + @io.each_line('>', 5) { |s| break s }.should == "examp" + end + + it "truncates the multi-character separator at the end to meet the limit" do + @io.each_line("is>an", 7) { |s| break s }.should == "this>is" + end + + it "does not change $_" do + $_ = "test" + @io.each_line('>', 8) { |s| s } + $_.should == "test" + end + + it "updates self's lineno by one" do + @io.each_line('>', 3) { |s| break s } + @io.lineno.should.eql?(1) + + @io.each_line('>', 3) { |s| break s } + @io.lineno.should.eql?(2) + + @io.each_line('>', 3) { |s| break s } + @io.lineno.should.eql?(3) + end + + it "tries to convert the passed separator to a String using #to_str" do # TODO + obj = mock('to_str') + obj.should_receive(:to_str).and_return('>') + + seen = [] + @io.each_line(obj, 5) { |s| seen << s } + seen.should == ["this>", "is>", "an>", "examp", "le"] + end + + it "does not raise TypeError if passed separator is nil" do + @io.each_line(nil, 5) { |s| break s }.should == "this>" + end + + it "tries to convert the passed limit to an Integer using #to_int" do # TODO + obj = mock('to_int') + obj.should_receive(:to_int).and_return(5) + + seen = [] + @io.each_line('>', obj) { |s| seen << s } + seen.should == ["this>", "is>", "an>", "examp", "le"] + end end diff --git a/spec/ruby/library/stringio/each_spec.rb b/spec/ruby/library/stringio/each_spec.rb index 7eb322f3ffd9c3..f3785bc18fdcd2 100644 --- a/spec/ruby/library/stringio/each_spec.rb +++ b/spec/ruby/library/stringio/each_spec.rb @@ -1,31 +1,8 @@ require_relative '../../spec_helper' -require_relative 'fixtures/classes' -require_relative 'shared/each' +require 'stringio' -describe "StringIO#each when passed a separator" do - it_behaves_like :stringio_each_separator, :each -end - -describe "StringIO#each when passed no arguments" do - it_behaves_like :stringio_each_no_arguments, :each -end - -describe "StringIO#each when self is not readable" do - it_behaves_like :stringio_each_not_readable, :each -end - -describe "StringIO#each when passed chomp" do - it_behaves_like :stringio_each_chomp, :each -end - -describe "StringIO#each when passed chomp" do - it_behaves_like :stringio_each_separator_and_chomp, :each -end - -describe "StringIO#each when passed limit" do - it_behaves_like :stringio_each_limit, :each -end - -describe "StringIO#each when passed separator and limit" do - it_behaves_like :stringio_each_separator_and_limit, :each +describe "StringIO#each" do + it "is an alias of StringIO#each_line" do + StringIO.instance_method(:each).should == StringIO.instance_method(:each_line) + end end diff --git a/spec/ruby/library/stringio/eof_spec.rb b/spec/ruby/library/stringio/eof_spec.rb index af0170977ce0f5..acc49305f57a1c 100644 --- a/spec/ruby/library/stringio/eof_spec.rb +++ b/spec/ruby/library/stringio/eof_spec.rb @@ -1,11 +1,33 @@ require_relative '../../spec_helper' -require_relative 'fixtures/classes' -require_relative 'shared/eof' +require 'stringio' describe "StringIO#eof?" do - it_behaves_like :stringio_eof, :eof? + before :each do + @io = StringIO.new("eof") + end + + it "returns true when self's position is greater than or equal to self's size" do + @io.pos = 3 + @io.eof?.should == true + + @io.pos = 6 + @io.eof?.should == true + end + + it "returns false when self's position is less than self's size" do + @io.pos = 0 + @io.eof?.should == false + + @io.pos = 1 + @io.eof?.should == false + + @io.pos = 2 + @io.eof?.should == false + end end describe "StringIO#eof" do - it_behaves_like :stringio_eof, :eof + it "is an alias of StringIO#eof?" do + StringIO.instance_method(:eof).should == StringIO.instance_method(:eof?) + end end diff --git a/spec/ruby/library/stringio/isatty_spec.rb b/spec/ruby/library/stringio/isatty_spec.rb index 1ef33978b56742..07743acc1268a5 100644 --- a/spec/ruby/library/stringio/isatty_spec.rb +++ b/spec/ruby/library/stringio/isatty_spec.rb @@ -1,7 +1,8 @@ require_relative '../../spec_helper' -require_relative 'fixtures/classes' -require_relative 'shared/isatty' +require 'stringio' describe "StringIO#isatty" do - it_behaves_like :stringio_isatty, :isatty + it "is an alias of StringIO#tty?" do + StringIO.instance_method(:isatty).should == StringIO.instance_method(:tty?) + end end diff --git a/spec/ruby/library/stringio/length_spec.rb b/spec/ruby/library/stringio/length_spec.rb index d3070f50a7e1f2..a83be6256ac397 100644 --- a/spec/ruby/library/stringio/length_spec.rb +++ b/spec/ruby/library/stringio/length_spec.rb @@ -1,7 +1,8 @@ require_relative '../../spec_helper' -require_relative 'fixtures/classes' -require_relative 'shared/length' +require 'stringio' describe "StringIO#length" do - it_behaves_like :stringio_length, :length + it "returns the length of the wrapped string" do + StringIO.new("example").length.should == 7 + end end diff --git a/spec/ruby/library/stringio/pos_spec.rb b/spec/ruby/library/stringio/pos_spec.rb index ba640f8c18e022..16f068b04993e0 100644 --- a/spec/ruby/library/stringio/pos_spec.rb +++ b/spec/ruby/library/stringio/pos_spec.rb @@ -1,9 +1,17 @@ require_relative '../../spec_helper' require_relative 'fixtures/classes' -require_relative 'shared/tell' describe "StringIO#pos" do - it_behaves_like :stringio_tell, :pos + before :each do + @io = StringIOSpecs.build + end + + it "returns the current byte offset" do + @io.getc + @io.pos.should == 1 + @io.read(7) + @io.pos.should == 8 + end end describe "StringIO#pos=" do diff --git a/spec/ruby/library/stringio/shared/codepoints.rb b/spec/ruby/library/stringio/shared/codepoints.rb deleted file mode 100644 index e35a02ccb48cc2..00000000000000 --- a/spec/ruby/library/stringio/shared/codepoints.rb +++ /dev/null @@ -1,45 +0,0 @@ -# -*- encoding: utf-8 -*- -describe :stringio_codepoints, shared: true do - before :each do - @io = StringIO.new("∂φ/∂x = gaîté") - @enum = @io.send(@method) - end - - it "returns an Enumerator" do - @enum.should.instance_of?(Enumerator) - end - - it "yields each codepoint code in turn" do - @enum.to_a.should == [8706, 966, 47, 8706, 120, 32, 61, 32, 103, 97, 238, 116, 233] - end - - it "yields each codepoint starting from the current position" do - @io.pos = 15 - @enum.to_a.should == [238, 116, 233] - end - - it "raises an error if reading invalid sequence" do - @io.pos = 1 # inside of a multibyte sequence - -> { @enum.first }.should.raise(ArgumentError) - end - - it "raises an IOError if not readable" do - @io.close_read - -> { @enum.to_a }.should.raise(IOError) - - io = StringIO.new(+"xyz", "w") - -> { io.send(@method).to_a }.should.raise(IOError) - end - - - it "calls the given block" do - r = [] - @io.send(@method){|c| r << c } - r.should == [8706, 966, 47, 8706, 120, 32, 61, 32, 103, 97, 238, 116, 233] - end - - it "returns self" do - @io.send(@method) {|l| l }.should.equal?(@io) - end - -end diff --git a/spec/ruby/library/stringio/shared/each.rb b/spec/ruby/library/stringio/shared/each.rb deleted file mode 100644 index 04e40b6b2a7650..00000000000000 --- a/spec/ruby/library/stringio/shared/each.rb +++ /dev/null @@ -1,209 +0,0 @@ -describe :stringio_each_separator, shared: true do - before :each do - @io = StringIO.new("a b c d e\n1 2 3 4 5") - end - - it "uses the passed argument as the line separator" do - seen = [] - @io.send(@method, " ") {|s| seen << s} - seen.should == ["a ", "b ", "c ", "d ", "e\n1 ", "2 ", "3 ", "4 ", "5"] - end - - it "does not change $_" do - $_ = "test" - @io.send(@method, " ") { |s| s} - $_.should == "test" - end - - it "returns self" do - @io.send(@method) {|l| l }.should.equal?(@io) - end - - it "tries to convert the passed separator to a String using #to_str" do - obj = mock("to_str") - obj.stub!(:to_str).and_return(" ") - - seen = [] - @io.send(@method, obj) { |l| seen << l } - seen.should == ["a ", "b ", "c ", "d ", "e\n1 ", "2 ", "3 ", "4 ", "5"] - end - - it "yields self's content starting from the current position when the passed separator is nil" do - seen = [] - io = StringIO.new("1 2 1 2 1 2") - io.pos = 2 - io.send(@method, nil) {|s| seen << s} - seen.should == ["2 1 2 1 2"] - end - - it "yields each paragraph with all separation characters when passed an empty String as separator" do - seen = [] - io = StringIO.new("para1\n\npara2\n\n\npara3") - io.send(@method, "") {|s| seen << s} - seen.should == ["para1\n\n", "para2\n\n\n", "para3"] - end -end - -describe :stringio_each_no_arguments, shared: true do - before :each do - @io = StringIO.new("a b c d e\n1 2 3 4 5") - end - - it "yields each line to the passed block" do - seen = [] - @io.send(@method) {|s| seen << s } - seen.should == ["a b c d e\n", "1 2 3 4 5"] - end - - it "yields each line starting from the current position" do - seen = [] - @io.pos = 4 - @io.send(@method) {|s| seen << s } - seen.should == ["c d e\n", "1 2 3 4 5"] - end - - it "does not change $_" do - $_ = "test" - @io.send(@method) { |s| s} - $_.should == "test" - end - - it "uses $/ as the default line separator" do - seen = [] - begin - old_rs = $/ - suppress_warning {$/ = " "} - @io.send(@method) {|s| seen << s } - seen.should.eql?(["a ", "b ", "c ", "d ", "e\n1 ", "2 ", "3 ", "4 ", "5"]) - ensure - suppress_warning {$/ = old_rs} - end - end - - it "returns self" do - @io.send(@method) {|l| l }.should.equal?(@io) - end - - it "returns an Enumerator when passed no block" do - enum = @io.send(@method) - enum.instance_of?(Enumerator).should == true - - seen = [] - enum.each { |b| seen << b } - seen.should == ["a b c d e\n", "1 2 3 4 5"] - end -end - -describe :stringio_each_not_readable, shared: true do - it "raises an IOError" do - io = StringIO.new(+"a b c d e", "w") - -> { io.send(@method) { |b| b } }.should.raise(IOError) - - io = StringIO.new("a b c d e") - io.close_read - -> { io.send(@method) { |b| b } }.should.raise(IOError) - end -end - -describe :stringio_each_chomp, shared: true do - it "yields each line with removed newline characters to the passed block" do - seen = [] - io = StringIO.new("a b \rc d e\n1 2 3 4 5\r\nthe end") - io.send(@method, chomp: true) {|s| seen << s } - seen.should == ["a b \rc d e", "1 2 3 4 5", "the end"] - end - - it "returns each line with removed newline characters when called without block" do - seen = [] - io = StringIO.new("a b \rc d e\n1 2 3 4 5\r\nthe end") - enum = io.send(@method, chomp: true) - enum.each {|s| seen << s } - seen.should == ["a b \rc d e", "1 2 3 4 5", "the end"] - end -end - -describe :stringio_each_separator_and_chomp, shared: true do - it "yields each line with removed separator to the passed block" do - seen = [] - io = StringIO.new("a b \nc d e|1 2 3 4 5\n|the end") - io.send(@method, "|", chomp: true) {|s| seen << s } - seen.should == ["a b \nc d e", "1 2 3 4 5\n", "the end"] - end - - it "returns each line with removed separator when called without block" do - seen = [] - io = StringIO.new("a b \nc d e|1 2 3 4 5\n|the end") - enum = io.send(@method, "|", chomp: true) - enum.each {|s| seen << s } - seen.should == ["a b \nc d e", "1 2 3 4 5\n", "the end"] - end -end - -describe :stringio_each_limit, shared: true do - before :each do - @io = StringIO.new("a b c d e\n1 2 3 4 5") - end - - it "returns the data read until the limit is met" do - seen = [] - @io.send(@method, 4) { |s| seen << s } - seen.should == ["a b ", "c d ", "e\n", "1 2 ", "3 4 ", "5"] - end -end - -describe :stringio_each_separator_and_limit, shared: true do - before :each do - @io = StringIO.new("this>is>an>example") - end - - it "returns the data read until the limit is consumed or the separator is met" do - @io.send(@method, '>', 8) { |s| break s }.should == "this>" - @io.send(@method, '>', 2) { |s| break s }.should == "is" - @io.send(@method, '>', 10) { |s| break s }.should == ">" - @io.send(@method, '>', 6) { |s| break s }.should == "an>" - @io.send(@method, '>', 5) { |s| break s }.should == "examp" - end - - it "truncates the multi-character separator at the end to meet the limit" do - @io.send(@method, "is>an", 7) { |s| break s }.should == "this>is" - end - - it "does not change $_" do - $_ = "test" - @io.send(@method, '>', 8) { |s| s } - $_.should == "test" - end - - it "updates self's lineno by one" do - @io.send(@method, '>', 3) { |s| break s } - @io.lineno.should.eql?(1) - - @io.send(@method, '>', 3) { |s| break s } - @io.lineno.should.eql?(2) - - @io.send(@method, '>', 3) { |s| break s } - @io.lineno.should.eql?(3) - end - - it "tries to convert the passed separator to a String using #to_str" do # TODO - obj = mock('to_str') - obj.should_receive(:to_str).and_return('>') - - seen = [] - @io.send(@method, obj, 5) { |s| seen << s } - seen.should == ["this>", "is>", "an>", "examp", "le"] - end - - it "does not raise TypeError if passed separator is nil" do - @io.send(@method, nil, 5) { |s| break s }.should == "this>" - end - - it "tries to convert the passed limit to an Integer using #to_int" do # TODO - obj = mock('to_int') - obj.should_receive(:to_int).and_return(5) - - seen = [] - @io.send(@method, '>', obj) { |s| seen << s } - seen.should == ["this>", "is>", "an>", "examp", "le"] - end -end diff --git a/spec/ruby/library/stringio/shared/each_byte.rb b/spec/ruby/library/stringio/shared/each_byte.rb deleted file mode 100644 index b3939c26de4984..00000000000000 --- a/spec/ruby/library/stringio/shared/each_byte.rb +++ /dev/null @@ -1,48 +0,0 @@ -describe :stringio_each_byte, shared: true do - before :each do - @io = StringIO.new("xyz") - end - - it "yields each character code in turn" do - seen = [] - @io.send(@method) { |b| seen << b } - seen.should == [120, 121, 122] - end - - it "updates the position before each yield" do - seen = [] - @io.send(@method) { |b| seen << @io.pos } - seen.should == [1, 2, 3] - end - - it "does not yield if the current position is out of bounds" do - @io.pos = 1000 - seen = nil - @io.send(@method) { |b| seen = b } - seen.should == nil - end - - it "returns self" do - @io.send(@method) {}.should.equal?(@io) - end - - it "returns an Enumerator when passed no block" do - enum = @io.send(@method) - enum.instance_of?(Enumerator).should == true - - seen = [] - enum.each { |b| seen << b } - seen.should == [120, 121, 122] - end -end - -describe :stringio_each_byte_not_readable, shared: true do - it "raises an IOError" do - io = StringIO.new(+"xyz", "w") - -> { io.send(@method) { |b| b } }.should.raise(IOError) - - io = StringIO.new("xyz") - io.close_read - -> { io.send(@method) { |b| b } }.should.raise(IOError) - end -end diff --git a/spec/ruby/library/stringio/shared/each_char.rb b/spec/ruby/library/stringio/shared/each_char.rb deleted file mode 100644 index 4215a9952b8d6f..00000000000000 --- a/spec/ruby/library/stringio/shared/each_char.rb +++ /dev/null @@ -1,36 +0,0 @@ -# -*- encoding: utf-8 -*- -describe :stringio_each_char, shared: true do - before :each do - @io = StringIO.new("xyz äöü") - end - - it "yields each character code in turn" do - seen = [] - @io.send(@method) { |c| seen << c } - seen.should == ["x", "y", "z", " ", "ä", "ö", "ü"] - end - - it "returns self" do - @io.send(@method) {}.should.equal?(@io) - end - - it "returns an Enumerator when passed no block" do - enum = @io.send(@method) - enum.instance_of?(Enumerator).should == true - - seen = [] - enum.each { |c| seen << c } - seen.should == ["x", "y", "z", " ", "ä", "ö", "ü"] - end -end - -describe :stringio_each_char_not_readable, shared: true do - it "raises an IOError" do - io = StringIO.new(+"xyz", "w") - -> { io.send(@method) { |b| b } }.should.raise(IOError) - - io = StringIO.new("xyz") - io.close_read - -> { io.send(@method) { |b| b } }.should.raise(IOError) - end -end diff --git a/spec/ruby/library/stringio/shared/eof.rb b/spec/ruby/library/stringio/shared/eof.rb deleted file mode 100644 index a9489581fcca57..00000000000000 --- a/spec/ruby/library/stringio/shared/eof.rb +++ /dev/null @@ -1,24 +0,0 @@ -describe :stringio_eof, shared: true do - before :each do - @io = StringIO.new("eof") - end - - it "returns true when self's position is greater than or equal to self's size" do - @io.pos = 3 - @io.send(@method).should == true - - @io.pos = 6 - @io.send(@method).should == true - end - - it "returns false when self's position is less than self's size" do - @io.pos = 0 - @io.send(@method).should == false - - @io.pos = 1 - @io.send(@method).should == false - - @io.pos = 2 - @io.send(@method).should == false - end -end diff --git a/spec/ruby/library/stringio/shared/isatty.rb b/spec/ruby/library/stringio/shared/isatty.rb deleted file mode 100644 index 2b92e8d6563517..00000000000000 --- a/spec/ruby/library/stringio/shared/isatty.rb +++ /dev/null @@ -1,5 +0,0 @@ -describe :stringio_isatty, shared: true do - it "returns false" do - StringIO.new("tty").send(@method).should == false - end -end diff --git a/spec/ruby/library/stringio/shared/length.rb b/spec/ruby/library/stringio/shared/length.rb deleted file mode 100644 index 60a4eb1bdd3b7a..00000000000000 --- a/spec/ruby/library/stringio/shared/length.rb +++ /dev/null @@ -1,5 +0,0 @@ -describe :stringio_length, shared: true do - it "returns the length of the wrapped string" do - StringIO.new("example").send(@method).should == 7 - end -end diff --git a/spec/ruby/library/stringio/shared/tell.rb b/spec/ruby/library/stringio/shared/tell.rb deleted file mode 100644 index 852c51c19239b3..00000000000000 --- a/spec/ruby/library/stringio/shared/tell.rb +++ /dev/null @@ -1,12 +0,0 @@ -describe :stringio_tell, shared: true do - before :each do - @io = StringIOSpecs.build - end - - it "returns the current byte offset" do - @io.getc - @io.send(@method).should == 1 - @io.read(7) - @io.send(@method).should == 8 - end -end diff --git a/spec/ruby/library/stringio/size_spec.rb b/spec/ruby/library/stringio/size_spec.rb index f674d22db955fe..33e574ddaec713 100644 --- a/spec/ruby/library/stringio/size_spec.rb +++ b/spec/ruby/library/stringio/size_spec.rb @@ -1,7 +1,8 @@ require_relative '../../spec_helper' -require_relative 'fixtures/classes' -require_relative 'shared/length' +require 'stringio' describe "StringIO#size" do - it_behaves_like :stringio_length, :size + it "is an alias of StringIO#length" do + StringIO.instance_method(:size).should == StringIO.instance_method(:length) + end end diff --git a/spec/ruby/library/stringio/tell_spec.rb b/spec/ruby/library/stringio/tell_spec.rb index 8350ee6f4d5643..80095999e93950 100644 --- a/spec/ruby/library/stringio/tell_spec.rb +++ b/spec/ruby/library/stringio/tell_spec.rb @@ -1,7 +1,8 @@ require_relative '../../spec_helper' -require_relative 'fixtures/classes' -require_relative 'shared/tell' +require 'stringio' describe "StringIO#tell" do - it_behaves_like :stringio_tell, :tell + it "is an alias of StringIO#pos" do + StringIO.instance_method(:tell).should == StringIO.instance_method(:pos) + end end diff --git a/spec/ruby/library/stringio/tty_spec.rb b/spec/ruby/library/stringio/tty_spec.rb index c6293dcbd7a579..87e22d49a5e95e 100644 --- a/spec/ruby/library/stringio/tty_spec.rb +++ b/spec/ruby/library/stringio/tty_spec.rb @@ -1,7 +1,8 @@ require_relative '../../spec_helper' -require_relative 'fixtures/classes' -require_relative 'shared/isatty' +require 'stringio' describe "StringIO#tty?" do - it_behaves_like :stringio_isatty, :tty? + it "returns false" do + StringIO.new("tty").tty?.should == false + end end diff --git a/spec/ruby/library/stringscanner/append_spec.rb b/spec/ruby/library/stringscanner/append_spec.rb index fef5dcf2bd0bda..68747d52d71964 100644 --- a/spec/ruby/library/stringscanner/append_spec.rb +++ b/spec/ruby/library/stringscanner/append_spec.rb @@ -1,11 +1,33 @@ require_relative '../../spec_helper' -require_relative 'shared/concat' require 'strscan' describe "StringScanner#<<" do - it_behaves_like :strscan_concat, :<< + it "concatenates the given argument to self and returns self" do + s = StringScanner.new(+"hello ") + (s. << 'world').should == s + s.string.should == "hello world" + s.eos?.should == false + end + + it "raises a TypeError if the given argument can't be converted to a String" do + -> { StringScanner.new('hello') << :world }.should.raise(TypeError) + -> { StringScanner.new('hello') << mock('x') }.should.raise(TypeError) + end end describe "StringScanner#<< when passed an Integer" do - it_behaves_like :strscan_concat_fixnum, :<< + it "raises a TypeError" do + a = StringScanner.new("hello world") + -> { a << 333 }.should.raise(TypeError) + b = StringScanner.new("") + -> { b << (256 * 3 + 64) }.should.raise(TypeError) + -> { b << -200 }.should.raise(TypeError) + end + + it "doesn't call to_int on the argument" do + x = mock('x') + x.should_not_receive(:to_int) + + -> { StringScanner.new("") << x }.should.raise(TypeError) + end end diff --git a/spec/ruby/library/stringscanner/beginning_of_line_spec.rb b/spec/ruby/library/stringscanner/beginning_of_line_spec.rb index 3f6f0da75f6d76..ae97f52fe08d26 100644 --- a/spec/ruby/library/stringscanner/beginning_of_line_spec.rb +++ b/spec/ruby/library/stringscanner/beginning_of_line_spec.rb @@ -1,7 +1,28 @@ require_relative '../../spec_helper' -require_relative 'shared/bol' require 'strscan' describe "StringScanner#beginning_of_line?" do - it_behaves_like :strscan_bol, :beginning_of_line? + it "returns true if the scan pointer is at the beginning of the line, false otherwise" do + s = StringScanner.new("This is a test") + s.beginning_of_line?.should == true + s.scan(/This/) + s.beginning_of_line?.should == false + s.terminate + s.beginning_of_line?.should == false + + s = StringScanner.new("hello\nworld") + s.beginning_of_line?.should == true + s.scan(/\w+/) + s.beginning_of_line?.should == false + s.scan(/\n/) + s.beginning_of_line?.should == true + s.unscan + s.beginning_of_line?.should == false + end + + it "returns true if the scan pointer is at the end of the line of an empty string." do + s = StringScanner.new('') + s.terminate + s.beginning_of_line?.should == true + end end diff --git a/spec/ruby/library/stringscanner/bol_spec.rb b/spec/ruby/library/stringscanner/bol_spec.rb index d31766e0e2e533..1d10c8f7c0cc66 100644 --- a/spec/ruby/library/stringscanner/bol_spec.rb +++ b/spec/ruby/library/stringscanner/bol_spec.rb @@ -1,7 +1,8 @@ require_relative '../../spec_helper' -require_relative 'shared/bol' require 'strscan' describe "StringScanner#bol?" do - it_behaves_like :strscan_bol, :bol? + it "is an alias of StringScanner#beginning_of_line?" do + StringScanner.instance_method(:bol?).should == StringScanner.instance_method(:beginning_of_line?) + end end diff --git a/spec/ruby/library/stringscanner/concat_spec.rb b/spec/ruby/library/stringscanner/concat_spec.rb index 4f790e250584c9..716268c956503b 100644 --- a/spec/ruby/library/stringscanner/concat_spec.rb +++ b/spec/ruby/library/stringscanner/concat_spec.rb @@ -1,11 +1,8 @@ require_relative '../../spec_helper' -require_relative 'shared/concat' require 'strscan' describe "StringScanner#concat" do - it_behaves_like :strscan_concat, :concat -end - -describe "StringScanner#concat when passed an Integer" do - it_behaves_like :strscan_concat_fixnum, :concat + it "is an alias of StringScanner#<<" do + StringScanner.instance_method(:concat).should == StringScanner.instance_method(:<<) + end end diff --git a/spec/ruby/library/stringscanner/pointer_spec.rb b/spec/ruby/library/stringscanner/pointer_spec.rb index bc0c0c50b7dbb7..5fc6c8cdf3ea5d 100644 --- a/spec/ruby/library/stringscanner/pointer_spec.rb +++ b/spec/ruby/library/stringscanner/pointer_spec.rb @@ -1,11 +1,14 @@ require_relative '../../spec_helper' -require_relative 'shared/pos' require 'strscan' describe "StringScanner#pointer" do - it_behaves_like :strscan_pos, :pointer + it "is an alias of StringScanner#pos" do + StringScanner.instance_method(:pointer).should == StringScanner.instance_method(:pos) + end end describe "StringScanner#pointer=" do - it_behaves_like :strscan_pos_set, :pointer= + it "is an alias of StringScanner#pos=" do + StringScanner.instance_method(:pointer=).should == StringScanner.instance_method(:pos=) + end end diff --git a/spec/ruby/library/stringscanner/pos_spec.rb b/spec/ruby/library/stringscanner/pos_spec.rb index 275fecf0f3cef3..bc3003ebdff4c7 100644 --- a/spec/ruby/library/stringscanner/pos_spec.rb +++ b/spec/ruby/library/stringscanner/pos_spec.rb @@ -1,11 +1,62 @@ require_relative '../../spec_helper' -require_relative 'shared/pos' require 'strscan' describe "StringScanner#pos" do - it_behaves_like :strscan_pos, :pos + before :each do + @s = StringScanner.new("This is a test") + end + + it "returns the position of the scan pointer" do + @s.pos.should == 0 + @s.scan_until(/This is/) + @s.pos.should == 7 + @s.get_byte + @s.pos.should == 8 + @s.terminate + @s.pos.should == 14 + end + + it "returns 0 in the reset position" do + @s.reset + @s.pos.should == 0 + end + + it "returns the length of the string in the terminate position" do + @s.terminate + @s.pos.should == @s.string.length + end + + it "is not multi-byte character sensitive" do + s = StringScanner.new("abcädeföghi") + + s.scan_until(/ö/) + s.pos.should == 10 + end end describe "StringScanner#pos=" do - it_behaves_like :strscan_pos_set, :pos= + before :each do + @s = StringScanner.new("This is a test") + end + + it "modify the scan pointer" do + @s.pos = 5 + @s.rest.should == "is a test" + end + + it "positions from the end if the argument is negative" do + @s.pos = -2 + @s.rest.should == "st" + @s.pos.should == 12 + end + + it "raises a RangeError if position too far backward" do + -> { + @s.pos = -20 + }.should.raise(RangeError) + end + + it "raises a RangeError when the passed argument is out of range" do + -> { @s.pos = 20 }.should.raise(RangeError) + end end diff --git a/spec/ruby/library/stringscanner/shared/bol.rb b/spec/ruby/library/stringscanner/shared/bol.rb deleted file mode 100644 index ec5c2051b51f83..00000000000000 --- a/spec/ruby/library/stringscanner/shared/bol.rb +++ /dev/null @@ -1,25 +0,0 @@ -describe :strscan_bol, shared: true do - it "returns true if the scan pointer is at the beginning of the line, false otherwise" do - s = StringScanner.new("This is a test") - s.send(@method).should == true - s.scan(/This/) - s.send(@method).should == false - s.terminate - s.send(@method).should == false - - s = StringScanner.new("hello\nworld") - s.bol?.should == true - s.scan(/\w+/) - s.bol?.should == false - s.scan(/\n/) - s.bol?.should == true - s.unscan - s.bol?.should == false - end - - it "returns true if the scan pointer is at the end of the line of an empty string." do - s = StringScanner.new('') - s.terminate - s.send(@method).should == true - end -end diff --git a/spec/ruby/library/stringscanner/shared/concat.rb b/spec/ruby/library/stringscanner/shared/concat.rb deleted file mode 100644 index 8138b0f8dcd9f9..00000000000000 --- a/spec/ruby/library/stringscanner/shared/concat.rb +++ /dev/null @@ -1,30 +0,0 @@ -describe :strscan_concat, shared: true do - it "concatenates the given argument to self and returns self" do - s = StringScanner.new(+"hello ") - s.send(@method, 'world').should == s - s.string.should == "hello world" - s.eos?.should == false - end - - it "raises a TypeError if the given argument can't be converted to a String" do - -> { StringScanner.new('hello').send(@method, :world) }.should.raise(TypeError) - -> { StringScanner.new('hello').send(@method, mock('x')) }.should.raise(TypeError) - end -end - -describe :strscan_concat_fixnum, shared: true do - it "raises a TypeError" do - a = StringScanner.new("hello world") - -> { a.send(@method, 333) }.should.raise(TypeError) - b = StringScanner.new("") - -> { b.send(@method, (256 * 3 + 64)) }.should.raise(TypeError) - -> { b.send(@method, -200) }.should.raise(TypeError) - end - - it "doesn't call to_int on the argument" do - x = mock('x') - x.should_not_receive(:to_int) - - -> { StringScanner.new("").send(@method, x) }.should.raise(TypeError) - end -end diff --git a/spec/ruby/library/stringscanner/shared/pos.rb b/spec/ruby/library/stringscanner/shared/pos.rb deleted file mode 100644 index 91f80fdf0842f0..00000000000000 --- a/spec/ruby/library/stringscanner/shared/pos.rb +++ /dev/null @@ -1,59 +0,0 @@ -describe :strscan_pos, shared: true do - before :each do - @s = StringScanner.new("This is a test") - end - - it "returns the position of the scan pointer" do - @s.send(@method).should == 0 - @s.scan_until(/This is/) - @s.send(@method).should == 7 - @s.get_byte - @s.send(@method).should == 8 - @s.terminate - @s.send(@method).should == 14 - end - - it "returns 0 in the reset position" do - @s.reset - @s.send(@method).should == 0 - end - - it "returns the length of the string in the terminate position" do - @s.terminate - @s.send(@method).should == @s.string.length - end - - it "is not multi-byte character sensitive" do - s = StringScanner.new("abcädeföghi") - - s.scan_until(/ö/) - s.pos.should == 10 - end -end - -describe :strscan_pos_set, shared: true do - before :each do - @s = StringScanner.new("This is a test") - end - - it "modify the scan pointer" do - @s.send(@method, 5) - @s.rest.should == "is a test" - end - - it "positions from the end if the argument is negative" do - @s.send(@method, -2) - @s.rest.should == "st" - @s.pos.should == 12 - end - - it "raises a RangeError if position too far backward" do - -> { - @s.send(@method, -20) - }.should.raise(RangeError) - end - - it "raises a RangeError when the passed argument is out of range" do - -> { @s.send(@method, 20) }.should.raise(RangeError) - end -end diff --git a/spec/ruby/library/syslog/open_spec.rb b/spec/ruby/library/syslog/open_spec.rb index 73e3780d785f5d..3aceea007d3b58 100644 --- a/spec/ruby/library/syslog/open_spec.rb +++ b/spec/ruby/library/syslog/open_spec.rb @@ -1,7 +1,6 @@ require_relative '../../spec_helper' platform_is_not :windows do - require_relative 'shared/reopen' require 'syslog' describe "Syslog.open" do @@ -87,6 +86,41 @@ end describe "Syslog.open!" do - it_behaves_like :syslog_reopen, :open! + before :each do + Syslog.opened?.should == false + end + + after :each do + Syslog.opened?.should == false + end + + it "reopens the log" do + Syslog.open + -> { Syslog.open! }.should_not.raise + Syslog.opened?.should == true + Syslog.close + end + + it "fails with RuntimeError if the log is closed" do + -> { Syslog.open! }.should.raise(RuntimeError) + end + + it "receives the same parameters as Syslog.open" do + Syslog.open + Syslog.open!("rubyspec", 3, 8) do |s| + s.should == Syslog + s.ident.should == "rubyspec" + s.options.should == 3 + s.facility.should == Syslog::LOG_USER + s.opened?.should == true + end + Syslog.opened?.should == false + end + + it "returns the module" do + Syslog.open + Syslog.open!.should == Syslog + Syslog.close + end end end diff --git a/spec/ruby/library/syslog/reopen_spec.rb b/spec/ruby/library/syslog/reopen_spec.rb index a78529fa1fe083..ef32d13a876bea 100644 --- a/spec/ruby/library/syslog/reopen_spec.rb +++ b/spec/ruby/library/syslog/reopen_spec.rb @@ -1,10 +1,11 @@ require_relative '../../spec_helper' platform_is_not :windows do - require_relative 'shared/reopen' require 'syslog' describe "Syslog.reopen" do - it_behaves_like :syslog_reopen, :reopen + it "is an alias of Syslog.open!" do + Syslog.method(:reopen).should == Syslog.method(:open!) + end end end diff --git a/spec/ruby/library/syslog/shared/reopen.rb b/spec/ruby/library/syslog/shared/reopen.rb deleted file mode 100644 index f04408e8078c8e..00000000000000 --- a/spec/ruby/library/syslog/shared/reopen.rb +++ /dev/null @@ -1,40 +0,0 @@ -describe :syslog_reopen, shared: true do - platform_is_not :windows do - before :each do - Syslog.opened?.should == false - end - - after :each do - Syslog.opened?.should == false - end - - it "reopens the log" do - Syslog.open - -> { Syslog.send(@method)}.should_not.raise - Syslog.opened?.should == true - Syslog.close - end - - it "fails with RuntimeError if the log is closed" do - -> { Syslog.send(@method)}.should.raise(RuntimeError) - end - - it "receives the same parameters as Syslog.open" do - Syslog.open - Syslog.send(@method, "rubyspec", 3, 8) do |s| - s.should == Syslog - s.ident.should == "rubyspec" - s.options.should == 3 - s.facility.should == Syslog::LOG_USER - s.opened?.should == true - end - Syslog.opened?.should == false - end - - it "returns the module" do - Syslog.open - Syslog.send(@method).should == Syslog - Syslog.close - end - end -end diff --git a/spec/ruby/library/tempfile/delete_spec.rb b/spec/ruby/library/tempfile/delete_spec.rb index 0332b44dde29e2..b126ceae6af64f 100644 --- a/spec/ruby/library/tempfile/delete_spec.rb +++ b/spec/ruby/library/tempfile/delete_spec.rb @@ -1,7 +1,15 @@ require_relative '../../spec_helper' -require_relative 'shared/unlink' require 'tempfile' describe "Tempfile#delete" do - it_behaves_like :tempfile_unlink, :delete + before :each do + @tempfile = Tempfile.new("specs") + end + + it "unlinks self" do + @tempfile.close + path = @tempfile.path + @tempfile.delete + File.should_not.exist?(path) + end end diff --git a/spec/ruby/library/tempfile/length_spec.rb b/spec/ruby/library/tempfile/length_spec.rb index bc622b9a706b0d..924c12942bf49c 100644 --- a/spec/ruby/library/tempfile/length_spec.rb +++ b/spec/ruby/library/tempfile/length_spec.rb @@ -1,7 +1,8 @@ require_relative '../../spec_helper' -require_relative 'shared/length' require 'tempfile' describe "Tempfile#length" do - it_behaves_like :tempfile_length, :length + it "is an alias of Tempfile#size" do + Tempfile.instance_method(:length).should == Tempfile.instance_method(:size) + end end diff --git a/spec/ruby/library/tempfile/shared/length.rb b/spec/ruby/library/tempfile/shared/length.rb deleted file mode 100644 index 1a89ff7b4d070d..00000000000000 --- a/spec/ruby/library/tempfile/shared/length.rb +++ /dev/null @@ -1,21 +0,0 @@ -describe :tempfile_length, shared: true do - before :each do - @tempfile = Tempfile.new("specs") - end - - after :each do - @tempfile.close! - end - - it "returns the size of self" do - @tempfile.send(@method).should.eql?(0) - @tempfile.print("Test!") - @tempfile.send(@method).should.eql?(5) - end - - it "returns the size of self even if self is closed" do - @tempfile.print("Test!") - @tempfile.close - @tempfile.send(@method).should.eql?(5) - end -end diff --git a/spec/ruby/library/tempfile/shared/unlink.rb b/spec/ruby/library/tempfile/shared/unlink.rb deleted file mode 100644 index e821228d7073a4..00000000000000 --- a/spec/ruby/library/tempfile/shared/unlink.rb +++ /dev/null @@ -1,12 +0,0 @@ -describe :tempfile_unlink, shared: true do - before :each do - @tempfile = Tempfile.new("specs") - end - - it "unlinks self" do - @tempfile.close - path = @tempfile.path - @tempfile.send(@method) - File.should_not.exist?(path) - end -end diff --git a/spec/ruby/library/tempfile/size_spec.rb b/spec/ruby/library/tempfile/size_spec.rb index f4824601c7d2e5..5a7edf8e4b197c 100644 --- a/spec/ruby/library/tempfile/size_spec.rb +++ b/spec/ruby/library/tempfile/size_spec.rb @@ -1,7 +1,24 @@ require_relative '../../spec_helper' -require_relative 'shared/length' require 'tempfile' describe "Tempfile#size" do - it_behaves_like :tempfile_length, :size + before :each do + @tempfile = Tempfile.new("specs") + end + + after :each do + @tempfile.close! + end + + it "returns the size of self" do + @tempfile.size.should.eql?(0) + @tempfile.print("Test!") + @tempfile.size.should.eql?(5) + end + + it "returns the size of self even if self is closed" do + @tempfile.print("Test!") + @tempfile.close + @tempfile.size.should.eql?(5) + end end diff --git a/spec/ruby/library/tempfile/unlink_spec.rb b/spec/ruby/library/tempfile/unlink_spec.rb index eac7df84720766..c03fc34a541b11 100644 --- a/spec/ruby/library/tempfile/unlink_spec.rb +++ b/spec/ruby/library/tempfile/unlink_spec.rb @@ -1,7 +1,8 @@ require_relative '../../spec_helper' -require_relative 'shared/unlink' require 'tempfile' describe "Tempfile#unlink" do - it_behaves_like :tempfile_unlink, :unlink + it "is an alias of Tempfile#delete" do + Tempfile.instance_method(:unlink).should == Tempfile.instance_method(:delete) + end end diff --git a/spec/ruby/library/time/iso8601_spec.rb b/spec/ruby/library/time/iso8601_spec.rb index ab35ab25d65356..d78de767922c01 100644 --- a/spec/ruby/library/time/iso8601_spec.rb +++ b/spec/ruby/library/time/iso8601_spec.rb @@ -1,7 +1,8 @@ require_relative '../../spec_helper' -require_relative 'shared/xmlschema' require 'time' describe "Time.iso8601" do - it_behaves_like :time_library_xmlschema, :iso8601 + it "is an alias of Time.xmlschema" do + Time.method(:iso8601).should == Time.method(:xmlschema) + end end diff --git a/spec/ruby/library/time/rfc2822_spec.rb b/spec/ruby/library/time/rfc2822_spec.rb index 7fc5e9a64bb65d..14824e2396ee8b 100644 --- a/spec/ruby/library/time/rfc2822_spec.rb +++ b/spec/ruby/library/time/rfc2822_spec.rb @@ -1,7 +1,68 @@ require_relative '../../spec_helper' -require_relative 'shared/rfc2822' require 'time' describe "Time.rfc2822" do - it_behaves_like :time_rfc2822, :rfc2822 + it "parses RFC-822 strings" do + t1 = (Time.utc(1976, 8, 26, 14, 30) + 4 * 3600) + t2 = Time.rfc2822("26 Aug 76 14:30 EDT") + t1.should == t2 + + t3 = Time.utc(1976, 8, 27, 9, 32) + 7 * 3600 + t4 = Time.rfc2822("27 Aug 76 09:32 PDT") + t3.should == t4 + end + + it "parses RFC-2822 strings" do + t1 = Time.utc(1997, 11, 21, 9, 55, 6) + 6 * 3600 + t2 = Time.rfc2822("Fri, 21 Nov 1997 09:55:06 -0600") + t1.should == t2 + + t3 = Time.utc(2003, 7, 1, 10, 52, 37) - 2 * 3600 + t4 = Time.rfc2822("Tue, 1 Jul 2003 10:52:37 +0200") + t3.should == t4 + + t5 = Time.utc(1997, 11, 21, 10, 1, 10) + 6 * 3600 + t6 = Time.rfc2822("Fri, 21 Nov 1997 10:01:10 -0600") + t5.should == t6 + + t7 = Time.utc(1997, 11, 21, 11, 0, 0) + 6 * 3600 + t8 = Time.rfc2822("Fri, 21 Nov 1997 11:00:00 -0600") + t7.should == t8 + + t9 = Time.utc(1997, 11, 24, 14, 22, 1) + 8 * 3600 + t10 = Time.rfc2822("Mon, 24 Nov 1997 14:22:01 -0800") + t9.should == t10 + + begin + Time.at(-1) + rescue ArgumentError + # ignore + else + t11 = Time.utc(1969, 2, 13, 23, 32, 54) + 3 * 3600 + 30 * 60 + t12 = Time.rfc2822("Thu, 13 Feb 1969 23:32:54 -0330") + t11.should == t12 + + t13 = Time.utc(1969, 2, 13, 23, 32, 0) + 3 * 3600 + 30 * 60 + t14 = Time.rfc2822(" Thu, + 13 + Feb + 1969 + 23:32 + -0330 (Newfoundland Time)") + t13.should == t14 + end + + t15 = Time.utc(1997, 11, 21, 9, 55, 6) + t16 = Time.rfc2822("21 Nov 97 09:55:06 GMT") + t15.should == t16 + + t17 = Time.utc(1997, 11, 21, 9, 55, 6) + 6 * 3600 + t18 = Time.rfc2822("Fri, 21 Nov 1997 09 : 55 : 06 -0600") + t17.should == t18 + + -> { + # inner comment is not supported. + Time.rfc2822("Fri, 21 Nov 1997 09(comment): 55 : 06 -0600") + }.should.raise(ArgumentError) + end end diff --git a/spec/ruby/library/time/rfc822_spec.rb b/spec/ruby/library/time/rfc822_spec.rb index da77e6ee7725b8..e32e9becae0dfe 100644 --- a/spec/ruby/library/time/rfc822_spec.rb +++ b/spec/ruby/library/time/rfc822_spec.rb @@ -1,7 +1,8 @@ require_relative '../../spec_helper' -require_relative 'shared/rfc2822' require 'time' describe "Time.rfc822" do - it_behaves_like :time_rfc2822, :rfc822 + it "is an alias of Time.rfc2822" do + Time.method(:rfc822).should == Time.method(:rfc2822) + end end diff --git a/spec/ruby/library/time/shared/rfc2822.rb b/spec/ruby/library/time/shared/rfc2822.rb deleted file mode 100644 index 49ef76db47f9a5..00000000000000 --- a/spec/ruby/library/time/shared/rfc2822.rb +++ /dev/null @@ -1,65 +0,0 @@ -describe :time_rfc2822, shared: true do - it "parses RFC-822 strings" do - t1 = (Time.utc(1976, 8, 26, 14, 30) + 4 * 3600) - t2 = Time.send(@method, "26 Aug 76 14:30 EDT") - t1.should == t2 - - t3 = Time.utc(1976, 8, 27, 9, 32) + 7 * 3600 - t4 = Time.send(@method, "27 Aug 76 09:32 PDT") - t3.should == t4 - end - - it "parses RFC-2822 strings" do - t1 = Time.utc(1997, 11, 21, 9, 55, 6) + 6 * 3600 - t2 = Time.send(@method, "Fri, 21 Nov 1997 09:55:06 -0600") - t1.should == t2 - - t3 = Time.utc(2003, 7, 1, 10, 52, 37) - 2 * 3600 - t4 = Time.send(@method, "Tue, 1 Jul 2003 10:52:37 +0200") - t3.should == t4 - - t5 = Time.utc(1997, 11, 21, 10, 1, 10) + 6 * 3600 - t6 = Time.send(@method, "Fri, 21 Nov 1997 10:01:10 -0600") - t5.should == t6 - - t7 = Time.utc(1997, 11, 21, 11, 0, 0) + 6 * 3600 - t8 = Time.send(@method, "Fri, 21 Nov 1997 11:00:00 -0600") - t7.should == t8 - - t9 = Time.utc(1997, 11, 24, 14, 22, 1) + 8 * 3600 - t10 = Time.send(@method, "Mon, 24 Nov 1997 14:22:01 -0800") - t9.should == t10 - - begin - Time.at(-1) - rescue ArgumentError - # ignore - else - t11 = Time.utc(1969, 2, 13, 23, 32, 54) + 3 * 3600 + 30 * 60 - t12 = Time.send(@method, "Thu, 13 Feb 1969 23:32:54 -0330") - t11.should == t12 - - t13 = Time.utc(1969, 2, 13, 23, 32, 0) + 3 * 3600 + 30 * 60 - t14 = Time.send(@method, " Thu, - 13 - Feb - 1969 - 23:32 - -0330 (Newfoundland Time)") - t13.should == t14 - end - - t15 = Time.utc(1997, 11, 21, 9, 55, 6) - t16 = Time.send(@method, "21 Nov 97 09:55:06 GMT") - t15.should == t16 - - t17 = Time.utc(1997, 11, 21, 9, 55, 6) + 6 * 3600 - t18 = Time.send(@method, "Fri, 21 Nov 1997 09 : 55 : 06 -0600") - t17.should == t18 - - -> { - # inner comment is not supported. - Time.send(@method, "Fri, 21 Nov 1997 09(comment): 55 : 06 -0600") - }.should.raise(ArgumentError) - end -end diff --git a/spec/ruby/library/time/shared/xmlschema.rb b/spec/ruby/library/time/shared/xmlschema.rb deleted file mode 100644 index 0002886ca5b24f..00000000000000 --- a/spec/ruby/library/time/shared/xmlschema.rb +++ /dev/null @@ -1,53 +0,0 @@ -describe :time_library_xmlschema, shared: true do - it "parses ISO-8601 strings" do - t = Time.utc(1985, 4, 12, 23, 20, 50, 520000) - s = "1985-04-12T23:20:50.52Z" - t.should == Time.send(@method, s) - #s.should == t.send(@method, 2) - - t = Time.utc(1996, 12, 20, 0, 39, 57) - s = "1996-12-19T16:39:57-08:00" - t.should == Time.send(@method, s) - # There is no way to generate time string with arbitrary timezone. - s = "1996-12-20T00:39:57Z" - t.should == Time.send(@method, s) - #assert_equal(s, t.send(@method)) - - t = Time.utc(1990, 12, 31, 23, 59, 60) - s = "1990-12-31T23:59:60Z" - t.should == Time.send(@method, s) - # leap second is representable only if timezone file has it. - s = "1990-12-31T15:59:60-08:00" - t.should == Time.send(@method, s) - - begin - Time.at(-1) - rescue ArgumentError - # ignore - else - t = Time.utc(1937, 1, 1, 11, 40, 27, 870000) - s = "1937-01-01T12:00:27.87+00:20" - t.should == Time.send(@method, s) - end - - # more - - # (Time.utc(1999, 5, 31, 13, 20, 0) + 5 * 3600).should == Time.send(@method, "1999-05-31T13:20:00-05:00") - # (Time.local(2000, 1, 20, 12, 0, 0)).should == Time.send(@method, "2000-01-20T12:00:00") - # (Time.utc(2000, 1, 20, 12, 0, 0)).should == Time.send(@method, "2000-01-20T12:00:00Z") - # (Time.utc(2000, 1, 20, 12, 0, 0) - 12 * 3600).should == Time.send(@method, "2000-01-20T12:00:00+12:00") - # (Time.utc(2000, 1, 20, 12, 0, 0) + 13 * 3600).should == Time.send(@method, "2000-01-20T12:00:00-13:00") - # (Time.utc(2000, 3, 4, 23, 0, 0) - 3 * 3600).should == Time.send(@method, "2000-03-04T23:00:00+03:00") - # (Time.utc(2000, 3, 4, 20, 0, 0)).should == Time.send(@method, "2000-03-04T20:00:00Z") - # (Time.local(2000, 1, 15, 0, 0, 0)).should == Time.send(@method, "2000-01-15T00:00:00") - # (Time.local(2000, 2, 15, 0, 0, 0)).should == Time.send(@method, "2000-02-15T00:00:00") - # (Time.local(2000, 1, 15, 12, 0, 0)).should == Time.send(@method, "2000-01-15T12:00:00") - # (Time.utc(2000, 1, 16, 12, 0, 0)).should == Time.send(@method, "2000-01-16T12:00:00Z") - # (Time.local(2000, 1, 1, 12, 0, 0)).should == Time.send(@method, "2000-01-01T12:00:00") - # (Time.utc(1999, 12, 31, 23, 0, 0)).should == Time.send(@method, "1999-12-31T23:00:00Z") - # (Time.local(2000, 1, 16, 12, 0, 0)).should == Time.send(@method, "2000-01-16T12:00:00") - # (Time.local(2000, 1, 16, 0, 0, 0)).should == Time.send(@method, "2000-01-16T00:00:00") - # (Time.utc(2000, 1, 12, 12, 13, 14)).should == Time.send(@method, "2000-01-12T12:13:14Z") - # (Time.utc(2001, 4, 17, 19, 23, 17, 300000)).should == Time.send(@method, "2001-04-17T19:23:17.3Z") - end -end diff --git a/spec/ruby/library/time/xmlschema_spec.rb b/spec/ruby/library/time/xmlschema_spec.rb index ff3c864a02e85b..1f7d63979a1861 100644 --- a/spec/ruby/library/time/xmlschema_spec.rb +++ b/spec/ruby/library/time/xmlschema_spec.rb @@ -1,7 +1,56 @@ require_relative '../../spec_helper' -require_relative 'shared/xmlschema' require 'time' describe "Time.xmlschema" do - it_behaves_like :time_library_xmlschema, :xmlschema + it "parses ISO-8601 strings" do + t = Time.utc(1985, 4, 12, 23, 20, 50, 520000) + s = "1985-04-12T23:20:50.52Z" + t.should == Time.xmlschema(s) + #s.should == t.xmlschema(2) + + t = Time.utc(1996, 12, 20, 0, 39, 57) + s = "1996-12-19T16:39:57-08:00" + t.should == Time.xmlschema(s) + # There is no way to generate time string with arbitrary timezone. + s = "1996-12-20T00:39:57Z" + t.should == Time.xmlschema(s) + #assert_equal(s, t.xmlschema) + + t = Time.utc(1990, 12, 31, 23, 59, 60) + s = "1990-12-31T23:59:60Z" + t.should == Time.xmlschema(s) + # leap second is representable only if timezone file has it. + s = "1990-12-31T15:59:60-08:00" + t.should == Time.xmlschema(s) + + begin + Time.at(-1) + rescue ArgumentError + # ignore + else + t = Time.utc(1937, 1, 1, 11, 40, 27, 870000) + s = "1937-01-01T12:00:27.87+00:20" + t.should == Time.xmlschema(s) + end + + # more + + # (Time.utc(1999, 5, 31, 13, 20, 0) + 5 * 3600).should == Time.xmlschema("1999-05-31T13:20:00-05:00") + # (Time.local(2000, 1, 20, 12, 0, 0)).should == Time.xmlschema("2000-01-20T12:00:00") + # (Time.utc(2000, 1, 20, 12, 0, 0)).should == Time.xmlschema("2000-01-20T12:00:00Z") + # (Time.utc(2000, 1, 20, 12, 0, 0) - 12 * 3600).should == Time.xmlschema("2000-01-20T12:00:00+12:00") + # (Time.utc(2000, 1, 20, 12, 0, 0) + 13 * 3600).should == Time.xmlschema("2000-01-20T12:00:00-13:00") + # (Time.utc(2000, 3, 4, 23, 0, 0) - 3 * 3600).should == Time.xmlschema("2000-03-04T23:00:00+03:00") + # (Time.utc(2000, 3, 4, 20, 0, 0)).should == Time.xmlschema("2000-03-04T20:00:00Z") + # (Time.local(2000, 1, 15, 0, 0, 0)).should == Time.xmlschema("2000-01-15T00:00:00") + # (Time.local(2000, 2, 15, 0, 0, 0)).should == Time.xmlschema("2000-02-15T00:00:00") + # (Time.local(2000, 1, 15, 12, 0, 0)).should == Time.xmlschema("2000-01-15T12:00:00") + # (Time.utc(2000, 1, 16, 12, 0, 0)).should == Time.xmlschema("2000-01-16T12:00:00Z") + # (Time.local(2000, 1, 1, 12, 0, 0)).should == Time.xmlschema("2000-01-01T12:00:00") + # (Time.utc(1999, 12, 31, 23, 0, 0)).should == Time.xmlschema("1999-12-31T23:00:00Z") + # (Time.local(2000, 1, 16, 12, 0, 0)).should == Time.xmlschema("2000-01-16T12:00:00") + # (Time.local(2000, 1, 16, 0, 0, 0)).should == Time.xmlschema("2000-01-16T00:00:00") + # (Time.utc(2000, 1, 12, 12, 13, 14)).should == Time.xmlschema("2000-01-12T12:13:14Z") + # (Time.utc(2001, 4, 17, 19, 23, 17, 300000)).should == Time.xmlschema("2001-04-17T19:23:17.3Z") + end end diff --git a/spec/ruby/library/uri/parser/extract_spec.rb b/spec/ruby/library/uri/parser/extract_spec.rb index 20d4565b08a870..f5ecd6ec8ed1c8 100644 --- a/spec/ruby/library/uri/parser/extract_spec.rb +++ b/spec/ruby/library/uri/parser/extract_spec.rb @@ -1,7 +1,90 @@ require_relative '../../../spec_helper' -require_relative '../shared/extract' require 'uri' describe "URI::Parser#extract" do - it_behaves_like :uri_extract, :extract, URI::Parser.new + before :all do + @parser = URI::Parser.new + end + + it "behaves according to its documentation" do + @parser.extract("text here http://foo.example.org/bla and here mailto:test@example.com and here also.").should == ["http://foo.example.org/bla", "mailto:test@example.com"] + end + + it "treats contiguous URIs as a single URI" do + @parser.extract('http://example.jphttp://example.jp').should == ['http://example.jphttp://example.jp'] + end + + it "treats pretty much anything with a colon as a URI" do + @parser.extract('From: XXX [mailto:xxx@xxx.xxx.xxx]').should == ['From:', 'mailto:xxx@xxx.xxx.xxx]'] + end + + it "wraps a URI string in an array" do + @parser.extract("http://github.com/brixen/rubyspec/tree/master").should == ["http://github.com/brixen/rubyspec/tree/master"] + end + + it "pulls a variety of protocol URIs from a string" do + @parser.extract("this is a string, it has http://rubini.us/ in it").should == ["http://rubini.us/"] + @parser.extract("mailto:spambait@example.com").should == ["mailto:spambait@example.com"] + @parser.extract("ftp://ruby-lang.org/").should == ["ftp://ruby-lang.org/"] + @parser.extract("https://mail.google.com").should == ["https://mail.google.com"] + @parser.extract("anything://example.com/").should == ["anything://example.com/"] + end + + it "pulls all URIs within a string in order into an array when a block is not given" do + @parser.extract("1.3. Example URI + + The following examples illustrate URI that are in common use. + + ftp://ftp.is.co.za/rfc/rfc1808.txt + -- ftp scheme for File Transfer Protocol services + + gopher://spinaltap.micro.umn.edu/00/Weather/California/Los%20Angeles + -- gopher scheme for Gopher and Gopher+ Protocol services + + http://www.math.uio.no/faq/compression-faq/part1.html + -- http scheme for Hypertext Transfer Protocol services + + mailto:mduerst@ifi.unizh.ch + -- mailto scheme for electronic mail addresses + + news:comp.infosystems.www.servers.unix + -- news scheme for USENET news groups and articles + + telnet://melvyl.ucop.edu/ + -- telnet scheme for interactive services via the TELNET Protocol + ").should == ["ftp://ftp.is.co.za/rfc/rfc1808.txt","gopher://spinaltap.micro.umn.edu/00/Weather/California/Los%20Angeles","http://www.math.uio.no/faq/compression-faq/part1.html","mailto:mduerst@ifi.unizh.ch","news:comp.infosystems.www.servers.unix","telnet://melvyl.ucop.edu/"] + end + + it "yields each URI in the given string in order to a block, if given, and returns nil" do + results = ["http://foo.example.org/bla", "mailto:test@example.com"] + @parser.extract("text here http://foo.example.org/bla and here mailto:test@example.com and here also.") {|uri| + uri.should == results.shift + }.should == nil + results.should == [] + end + + it "allows the user to specify a list of acceptable protocols of URIs to scan for" do + @parser.extract("1.3. Example URI + + The following examples illustrate URI that are in common use. + + ftp://ftp.is.co.za/rfc/rfc1808.txt + -- ftp scheme for File Transfer Protocol services + + gopher://spinaltap.micro.umn.edu/00/Weather/California/Los%20Angeles + -- gopher scheme for Gopher and Gopher+ Protocol services + + http://www.math.uio.no/faq/compression-faq/part1.html + -- http scheme for Hypertext Transfer Protocol services + + mailto:mduerst@ifi.unizh.ch + -- mailto scheme for electronic mail addresses + + news:comp.infosystems.www.servers.unix + -- news scheme for USENET news groups and articles + + telnet://melvyl.ucop.edu/ + -- telnet scheme for interactive services via the TELNET Protocol + ", ["http","ftp","mailto"]).should == ["ftp://ftp.is.co.za/rfc/rfc1808.txt","http://www.math.uio.no/faq/compression-faq/part1.html","mailto:mduerst@ifi.unizh.ch"] + end end diff --git a/spec/ruby/library/uri/parser/join_spec.rb b/spec/ruby/library/uri/parser/join_spec.rb index 0c9230be76a7a3..0fb29cf00aff17 100644 --- a/spec/ruby/library/uri/parser/join_spec.rb +++ b/spec/ruby/library/uri/parser/join_spec.rb @@ -1,7 +1,62 @@ require_relative '../../../spec_helper' -require_relative '../shared/join' require 'uri' describe "URI::Parser#join" do - it_behaves_like :uri_join, :join, URI::Parser.new + before :all do + @parser = URI::Parser.new + end + + it "returns a URI object of the concatenation of a protocol and domain, and a path" do + @parser.join("http://localhost/","main.rbx").should == URI.parse("http://localhost/main.rbx") + end + + it "accepts URI objects" do + @parser.join(URI("http://localhost/"),"main.rbx").should == URI.parse("http://localhost/main.rbx") + @parser.join("http://localhost/",URI("main.rbx")).should == URI.parse("http://localhost/main.rbx") + @parser.join(URI("http://localhost/"),URI("main.rbx")).should == URI.parse("http://localhost/main.rbx") + end + + it "accepts string-like arguments with to_str" do + str = mock('string-like') + str.should_receive(:to_str).and_return("http://ruby-lang.org") + str2 = mock('string-like also') + str2.should_receive(:to_str).and_return("foo/bar") + @parser.join(str, str2).should == URI.parse("http://ruby-lang.org/foo/bar") + end + + it "raises an error if given no argument" do + -> { + @parser.join + }.should.raise(ArgumentError) + end + + it "doesn't create redundant '/'s" do + @parser.join("http://localhost/", "/main.rbx").should == URI.parse("http://localhost/main.rbx") + end + + it "discards arguments given before an absolute uri" do + @parser.join("http://localhost/a/b/c/d", "http://ruby-lang.com/foo", "bar").should == URI.parse("http://ruby-lang.com/bar") + end + + it "resolves .. in paths" do + @parser.join("http://localhost/a/b/c/d", "../../e/f", "g/h/../i").to_s.should == "http://localhost/a/e/g/i" + end end + +# assert_equal(URI.parse('http://foo/bar'), URI.join('http://foo/bar')) +# assert_equal(URI.parse('http://foo/bar'), URI.join('http://foo', 'bar')) +# assert_equal(URI.parse('http://foo/bar/'), URI.join('http://foo', 'bar/')) +# +# assert_equal(URI.parse('http://foo/baz'), URI.join('http://foo', 'bar', 'baz')) +# assert_equal(URI.parse('http://foo/baz'), URI.join('http://foo', 'bar', '/baz')) +# assert_equal(URI.parse('http://foo/baz/'), URI.join('http://foo', 'bar', '/baz/')) +# assert_equal(URI.parse('http://foo/bar/baz'), URI.join('http://foo', 'bar/', 'baz')) +# assert_equal(URI.parse('http://foo/hoge'), URI.join('http://foo', 'bar', 'baz', 'hoge')) +# +# assert_equal(URI.parse('http://foo/bar/baz'), URI.join('http://foo', 'bar/baz')) +# assert_equal(URI.parse('http://foo/bar/hoge'), URI.join('http://foo', 'bar/baz', 'hoge')) +# assert_equal(URI.parse('http://foo/bar/baz/hoge'), URI.join('http://foo', 'bar/baz/', 'hoge')) +# assert_equal(URI.parse('http://foo/hoge'), URI.join('http://foo', 'bar/baz', '/hoge')) +# assert_equal(URI.parse('http://foo/bar/hoge'), URI.join('http://foo', 'bar/baz', 'hoge')) +# assert_equal(URI.parse('http://foo/bar/baz/hoge'), URI.join('http://foo', 'bar/baz/', 'hoge')) +# assert_equal(URI.parse('http://foo/hoge'), URI.join('http://foo', 'bar/baz', '/hoge')) diff --git a/spec/ruby/library/uri/parser/parse_spec.rb b/spec/ruby/library/uri/parser/parse_spec.rb index df126eab6d7851..0e6a06ebe53a32 100644 --- a/spec/ruby/library/uri/parser/parse_spec.rb +++ b/spec/ruby/library/uri/parser/parse_spec.rb @@ -1,7 +1,213 @@ require_relative '../../../spec_helper' require_relative '../fixtures/classes' -require_relative '../shared/parse' describe "URI::Parser#parse" do - it_behaves_like :uri_parse, :parse, URI::Parser.new + before :all do + @parser = URI::Parser.new + end + + it "returns a URI::HTTP object when parsing an HTTP URI" do + @parser.parse("http://www.example.com/").should.is_a?(URI::HTTP) + end + + it "populates the components of a parsed URI::HTTP, setting the port to 80 by default" do + # general case + URISpec.components(@parser.parse("http://user:pass@example.com/path/?query=val&q2=val2#fragment")).should == { + scheme: "http", + userinfo: "user:pass", + host: "example.com", + port: 80, + path: "/path/", + query: "query=val&q2=val2", + fragment: "fragment" + } + + # multiple paths + URISpec.components(@parser.parse("http://a/b/c/d;p?q")).should == { + scheme: "http", + userinfo: nil, + host: "a", + port: 80, + path: "/b/c/d;p", + query: "q", + fragment: nil + } + + # multi-level domain + URISpec.components(@parser.parse('http://www.math.uio.no/faq/compression-faq/part1.html')).should == { + scheme: "http", + userinfo: nil, + host: "www.math.uio.no", + port: 80, + path: "/faq/compression-faq/part1.html", + query: nil, + fragment: nil + } + end + + it "parses out the port number of a URI, when given" do + @parser.parse("http://example.com:8080/").port.should == 8080 + end + + it "returns a URI::HTTPS object when parsing an HTTPS URI" do + @parser.parse("https://important-intern-net.net").should.is_a?(URI::HTTPS) + end + + it "sets the port of a parsed https URI to 443 by default" do + @parser.parse("https://example.com/").port.should == 443 + end + + it "populates the components of a parsed URI::FTP object" do + # generic, empty password. + url = @parser.parse("ftp://anonymous@ruby-lang.org/pub/ruby/1.8/ruby-1.8.6.tar.bz2;type=i") + url.should.is_a?(URI::FTP) + URISpec.components(url).should == { + scheme: "ftp", + userinfo: "anonymous", + host: "ruby-lang.org", + port: 21, + path: "pub/ruby/1.8/ruby-1.8.6.tar.bz2", + typecode: "i" + } + + # multidomain, no user or password + url = @parser.parse('ftp://ftp.is.co.za/rfc/rfc1808.txt') + url.should.is_a?(URI::FTP) + URISpec.components(url).should == { + scheme: "ftp", + userinfo: nil, + host: "ftp.is.co.za", + port: 21, + path: "rfc/rfc1808.txt", + typecode: nil + } + + # empty user + url = @parser.parse('ftp://:pass@localhost/') + url.should.is_a?(URI::FTP) + URISpec.components(url).should == { + scheme: "ftp", + userinfo: ":pass", + host: "localhost", + port: 21, + path: "", + typecode: nil + } + url.password.should == "pass" + end + + it "returns a URI::LDAP object when parsing an LDAP URI" do + #taken from http://www.faqs.org/rfcs/rfc2255.html 'cause I don't really know what an LDAP url looks like + ldap_uris = %w{ ldap:///o=University%20of%20Michigan,c=US ldap://ldap.itd.umich.edu/o=University%20of%20Michigan,c=US ldap://ldap.itd.umich.edu/o=University%20of%20Michigan,c=US?postalAddress ldap://host.com:6666/o=University%20of%20Michigan,c=US??sub?(cn=Babs%20Jensen) ldap://ldap.itd.umich.edu/c=GB?objectClass?one ldap://ldap.question.com/o=Question%3f,c=US?mail ldap://ldap.netscape.com/o=Babsco,c=US??(int=%5c00%5c00%5c00%5c04) ldap:///??sub??bindname=cn=Manager%2co=Foo ldap:///??sub??!bindname=cn=Manager%2co=Foo } + ldap_uris.each do |ldap_uri| + @parser.parse(ldap_uri).should.is_a?(URI::LDAP) + end + end + + it "populates the components of a parsed URI::LDAP object" do + URISpec.components(@parser.parse("ldap://ldap.itd.umich.edu/o=University%20of%20Michigan,c=US?postalAddress?scope?filter?extensions")).should == { + scheme: "ldap", + host: "ldap.itd.umich.edu", + port: 389, + dn: "o=University%20of%20Michigan,c=US", + attributes: "postalAddress", + scope: "scope", + filter: "filter", + extensions: "extensions" + } + end + + it "returns a URI::MailTo object when passed a mailto URI" do + @parser.parse("mailto:spam@mailinator.com").should.is_a?(URI::MailTo) + end + + it "populates the components of a parsed URI::MailTo object" do + URISpec.components(@parser.parse("mailto:spam@mailinator.com?subject=Discounts%20On%20Imported%20methods!!!&body=Exciting%20offer")).should == { + scheme: "mailto", + to: "spam@mailinator.com", + headers: [["subject","Discounts%20On%20Imported%20methods!!!"], + ["body", "Exciting%20offer"]] + } + end + + # TODO + # Test registry + it "does its best to extract components from URI::Generic objects" do + # generic + URISpec.components(URI("scheme://userinfo@host/path?query#fragment")).should == { + scheme: "scheme", + userinfo: "userinfo", + host: "host", + port: nil, + path: "/path", + query: "query", + fragment: "fragment", + registry: nil, + opaque: nil + } + + # gopher + gopher = @parser.parse('gopher://spinaltap.micro.umn.edu/00/Weather/California/Los%20Angeles') + gopher.should.is_a?(URI::Generic) + + URISpec.components(gopher).should == { + scheme: "gopher", + userinfo: nil, + host: "spinaltap.micro.umn.edu", + port: nil, + path: "/00/Weather/California/Los%20Angeles", + query: nil, + fragment: nil, + registry: nil, + opaque: nil + } + + # news + news = @parser.parse('news:comp.infosystems.www.servers.unix') + news.should.is_a?(URI::Generic) + URISpec.components(news).should == { + scheme: "news", + userinfo: nil, + host: nil, + port: nil, + path: nil, + query: nil, + fragment: nil, + registry: nil, + opaque: "comp.infosystems.www.servers.unix" + } + + # telnet + telnet = @parser.parse('telnet://melvyl.ucop.edu/') + telnet.should.is_a?(URI::Generic) + URISpec.components(telnet).should == { + scheme: "telnet", + userinfo: nil, + host: "melvyl.ucop.edu", + port: nil, + path: "/", + query: nil, + fragment: nil, + registry: nil, + opaque: nil + } + + # files + file_l = @parser.parse('file:///foo/bar.txt') + file_l.should.is_a?(URI::Generic) + file = @parser.parse('file:/foo/bar.txt') + file.should.is_a?(URI::Generic) + end + + if URI::DEFAULT_PARSER == URI::RFC2396_Parser + it "raises errors on malformed URIs" do + -> { @parser.parse('http://a_b:80/') }.should.raise(URI::InvalidURIError) + -> { @parser.parse('http://a_b/') }.should.raise(URI::InvalidURIError) + end + elsif URI::DEFAULT_PARSER == URI::RFC3986_Parser + it "does not raise errors on URIs contained underscore" do + -> { @parser.parse('http://a_b:80/') }.should_not.raise(URI::InvalidURIError) + -> { @parser.parse('http://a_b/') }.should_not.raise(URI::InvalidURIError) + end + end end diff --git a/spec/ruby/library/uri/shared/extract.rb b/spec/ruby/library/uri/shared/extract.rb deleted file mode 100644 index efe60ae4b91864..00000000000000 --- a/spec/ruby/library/uri/shared/extract.rb +++ /dev/null @@ -1,83 +0,0 @@ -describe :uri_extract, shared: true do - it "behaves according to its documentation" do - @object.extract("text here http://foo.example.org/bla and here mailto:test@example.com and here also.").should == ["http://foo.example.org/bla", "mailto:test@example.com"] - end - - it "treats contiguous URIs as a single URI" do - @object.extract('http://example.jphttp://example.jp').should == ['http://example.jphttp://example.jp'] - end - - it "treats pretty much anything with a colon as a URI" do - @object.extract('From: XXX [mailto:xxx@xxx.xxx.xxx]').should == ['From:', 'mailto:xxx@xxx.xxx.xxx]'] - end - - it "wraps a URI string in an array" do - @object.extract("http://github.com/brixen/rubyspec/tree/master").should == ["http://github.com/brixen/rubyspec/tree/master"] - end - - it "pulls a variety of protocol URIs from a string" do - @object.extract("this is a string, it has http://rubini.us/ in it").should == ["http://rubini.us/"] - @object.extract("mailto:spambait@example.com").should == ["mailto:spambait@example.com"] - @object.extract("ftp://ruby-lang.org/").should == ["ftp://ruby-lang.org/"] - @object.extract("https://mail.google.com").should == ["https://mail.google.com"] - @object.extract("anything://example.com/").should == ["anything://example.com/"] - end - - it "pulls all URIs within a string in order into an array when a block is not given" do - @object.extract("1.3. Example URI - - The following examples illustrate URI that are in common use. - - ftp://ftp.is.co.za/rfc/rfc1808.txt - -- ftp scheme for File Transfer Protocol services - - gopher://spinaltap.micro.umn.edu/00/Weather/California/Los%20Angeles - -- gopher scheme for Gopher and Gopher+ Protocol services - - http://www.math.uio.no/faq/compression-faq/part1.html - -- http scheme for Hypertext Transfer Protocol services - - mailto:mduerst@ifi.unizh.ch - -- mailto scheme for electronic mail addresses - - news:comp.infosystems.www.servers.unix - -- news scheme for USENET news groups and articles - - telnet://melvyl.ucop.edu/ - -- telnet scheme for interactive services via the TELNET Protocol - ").should == ["ftp://ftp.is.co.za/rfc/rfc1808.txt","gopher://spinaltap.micro.umn.edu/00/Weather/California/Los%20Angeles","http://www.math.uio.no/faq/compression-faq/part1.html","mailto:mduerst@ifi.unizh.ch","news:comp.infosystems.www.servers.unix","telnet://melvyl.ucop.edu/"] - end - - it "yields each URI in the given string in order to a block, if given, and returns nil" do - results = ["http://foo.example.org/bla", "mailto:test@example.com"] - @object.extract("text here http://foo.example.org/bla and here mailto:test@example.com and here also.") {|uri| - uri.should == results.shift - }.should == nil - results.should == [] - end - - it "allows the user to specify a list of acceptable protocols of URIs to scan for" do - @object.extract("1.3. Example URI - - The following examples illustrate URI that are in common use. - - ftp://ftp.is.co.za/rfc/rfc1808.txt - -- ftp scheme for File Transfer Protocol services - - gopher://spinaltap.micro.umn.edu/00/Weather/California/Los%20Angeles - -- gopher scheme for Gopher and Gopher+ Protocol services - - http://www.math.uio.no/faq/compression-faq/part1.html - -- http scheme for Hypertext Transfer Protocol services - - mailto:mduerst@ifi.unizh.ch - -- mailto scheme for electronic mail addresses - - news:comp.infosystems.www.servers.unix - -- news scheme for USENET news groups and articles - - telnet://melvyl.ucop.edu/ - -- telnet scheme for interactive services via the TELNET Protocol - ", ["http","ftp","mailto"]).should == ["ftp://ftp.is.co.za/rfc/rfc1808.txt","http://www.math.uio.no/faq/compression-faq/part1.html","mailto:mduerst@ifi.unizh.ch"] - end -end diff --git a/spec/ruby/library/uri/shared/join.rb b/spec/ruby/library/uri/shared/join.rb deleted file mode 100644 index b1f5f1c72b3706..00000000000000 --- a/spec/ruby/library/uri/shared/join.rb +++ /dev/null @@ -1,56 +0,0 @@ -describe :uri_join, shared: true do - it "returns a URI object of the concatenation of a protocol and domain, and a path" do - @object.join("http://localhost/","main.rbx").should == URI.parse("http://localhost/main.rbx") - end - - it "accepts URI objects" do - @object.join(URI("http://localhost/"),"main.rbx").should == URI.parse("http://localhost/main.rbx") - @object.join("http://localhost/",URI("main.rbx")).should == URI.parse("http://localhost/main.rbx") - @object.join(URI("http://localhost/"),URI("main.rbx")).should == URI.parse("http://localhost/main.rbx") - end - - it "accepts string-like arguments with to_str" do - str = mock('string-like') - str.should_receive(:to_str).and_return("http://ruby-lang.org") - str2 = mock('string-like also') - str2.should_receive(:to_str).and_return("foo/bar") - @object.join(str, str2).should == URI.parse("http://ruby-lang.org/foo/bar") - end - - it "raises an error if given no argument" do - -> { - @object.join - }.should.raise(ArgumentError) - end - - it "doesn't create redundant '/'s" do - @object.join("http://localhost/", "/main.rbx").should == URI.parse("http://localhost/main.rbx") - end - - it "discards arguments given before an absolute uri" do - @object.join("http://localhost/a/b/c/d", "http://ruby-lang.com/foo", "bar").should == URI.parse("http://ruby-lang.com/bar") - end - - it "resolves .. in paths" do - @object.join("http://localhost/a/b/c/d", "../../e/f", "g/h/../i").to_s.should == "http://localhost/a/e/g/i" - end -end - - -# assert_equal(URI.parse('http://foo/bar'), URI.join('http://foo/bar')) -# assert_equal(URI.parse('http://foo/bar'), URI.join('http://foo', 'bar')) -# assert_equal(URI.parse('http://foo/bar/'), URI.join('http://foo', 'bar/')) -# -# assert_equal(URI.parse('http://foo/baz'), URI.join('http://foo', 'bar', 'baz')) -# assert_equal(URI.parse('http://foo/baz'), URI.join('http://foo', 'bar', '/baz')) -# assert_equal(URI.parse('http://foo/baz/'), URI.join('http://foo', 'bar', '/baz/')) -# assert_equal(URI.parse('http://foo/bar/baz'), URI.join('http://foo', 'bar/', 'baz')) -# assert_equal(URI.parse('http://foo/hoge'), URI.join('http://foo', 'bar', 'baz', 'hoge')) -# -# assert_equal(URI.parse('http://foo/bar/baz'), URI.join('http://foo', 'bar/baz')) -# assert_equal(URI.parse('http://foo/bar/hoge'), URI.join('http://foo', 'bar/baz', 'hoge')) -# assert_equal(URI.parse('http://foo/bar/baz/hoge'), URI.join('http://foo', 'bar/baz/', 'hoge')) -# assert_equal(URI.parse('http://foo/hoge'), URI.join('http://foo', 'bar/baz', '/hoge')) -# assert_equal(URI.parse('http://foo/bar/hoge'), URI.join('http://foo', 'bar/baz', 'hoge')) -# assert_equal(URI.parse('http://foo/bar/baz/hoge'), URI.join('http://foo', 'bar/baz/', 'hoge')) -# assert_equal(URI.parse('http://foo/hoge'), URI.join('http://foo', 'bar/baz', '/hoge')) diff --git a/spec/ruby/library/uri/shared/parse.rb b/spec/ruby/library/uri/shared/parse.rb deleted file mode 100644 index 7ec71795264677..00000000000000 --- a/spec/ruby/library/uri/shared/parse.rb +++ /dev/null @@ -1,206 +0,0 @@ -describe :uri_parse, shared: true do - it "returns a URI::HTTP object when parsing an HTTP URI" do - @object.parse("http://www.example.com/").should.is_a?(URI::HTTP) - end - - it "populates the components of a parsed URI::HTTP, setting the port to 80 by default" do - # general case - URISpec.components(@object.parse("http://user:pass@example.com/path/?query=val&q2=val2#fragment")).should == { - scheme: "http", - userinfo: "user:pass", - host: "example.com", - port: 80, - path: "/path/", - query: "query=val&q2=val2", - fragment: "fragment" - } - - # multiple paths - URISpec.components(@object.parse("http://a/b/c/d;p?q")).should == { - scheme: "http", - userinfo: nil, - host: "a", - port: 80, - path: "/b/c/d;p", - query: "q", - fragment: nil - } - - # multi-level domain - URISpec.components(@object.parse('http://www.math.uio.no/faq/compression-faq/part1.html')).should == { - scheme: "http", - userinfo: nil, - host: "www.math.uio.no", - port: 80, - path: "/faq/compression-faq/part1.html", - query: nil, - fragment: nil - } - end - - it "parses out the port number of a URI, when given" do - @object.parse("http://example.com:8080/").port.should == 8080 - end - - it "returns a URI::HTTPS object when parsing an HTTPS URI" do - @object.parse("https://important-intern-net.net").should.is_a?(URI::HTTPS) - end - - it "sets the port of a parsed https URI to 443 by default" do - @object.parse("https://example.com/").port.should == 443 - end - - it "populates the components of a parsed URI::FTP object" do - # generic, empty password. - url = @object.parse("ftp://anonymous@ruby-lang.org/pub/ruby/1.8/ruby-1.8.6.tar.bz2;type=i") - url.should.is_a?(URI::FTP) - URISpec.components(url).should == { - scheme: "ftp", - userinfo: "anonymous", - host: "ruby-lang.org", - port: 21, - path: "pub/ruby/1.8/ruby-1.8.6.tar.bz2", - typecode: "i" - } - - # multidomain, no user or password - url = @object.parse('ftp://ftp.is.co.za/rfc/rfc1808.txt') - url.should.is_a?(URI::FTP) - URISpec.components(url).should == { - scheme: "ftp", - userinfo: nil, - host: "ftp.is.co.za", - port: 21, - path: "rfc/rfc1808.txt", - typecode: nil - } - - # empty user - url = @object.parse('ftp://:pass@localhost/') - url.should.is_a?(URI::FTP) - URISpec.components(url).should == { - scheme: "ftp", - userinfo: ":pass", - host: "localhost", - port: 21, - path: "", - typecode: nil - } - url.password.should == "pass" - end - - it "returns a URI::LDAP object when parsing an LDAP URI" do - #taken from http://www.faqs.org/rfcs/rfc2255.html 'cause I don't really know what an LDAP url looks like - ldap_uris = %w{ ldap:///o=University%20of%20Michigan,c=US ldap://ldap.itd.umich.edu/o=University%20of%20Michigan,c=US ldap://ldap.itd.umich.edu/o=University%20of%20Michigan,c=US?postalAddress ldap://host.com:6666/o=University%20of%20Michigan,c=US??sub?(cn=Babs%20Jensen) ldap://ldap.itd.umich.edu/c=GB?objectClass?one ldap://ldap.question.com/o=Question%3f,c=US?mail ldap://ldap.netscape.com/o=Babsco,c=US??(int=%5c00%5c00%5c00%5c04) ldap:///??sub??bindname=cn=Manager%2co=Foo ldap:///??sub??!bindname=cn=Manager%2co=Foo } - ldap_uris.each do |ldap_uri| - @object.parse(ldap_uri).should.is_a?(URI::LDAP) - end - end - - it "populates the components of a parsed URI::LDAP object" do - URISpec.components(@object.parse("ldap://ldap.itd.umich.edu/o=University%20of%20Michigan,c=US?postalAddress?scope?filter?extensions")).should == { - scheme: "ldap", - host: "ldap.itd.umich.edu", - port: 389, - dn: "o=University%20of%20Michigan,c=US", - attributes: "postalAddress", - scope: "scope", - filter: "filter", - extensions: "extensions" - } - end - - it "returns a URI::MailTo object when passed a mailto URI" do - @object.parse("mailto:spam@mailinator.com").should.is_a?(URI::MailTo) - end - - it "populates the components of a parsed URI::MailTo object" do - URISpec.components(@object.parse("mailto:spam@mailinator.com?subject=Discounts%20On%20Imported%20methods!!!&body=Exciting%20offer")).should == { - scheme: "mailto", - to: "spam@mailinator.com", - headers: [["subject","Discounts%20On%20Imported%20methods!!!"], - ["body", "Exciting%20offer"]] - } - end - - # TODO - # Test registry - it "does its best to extract components from URI::Generic objects" do - # generic - URISpec.components(URI("scheme://userinfo@host/path?query#fragment")).should == { - scheme: "scheme", - userinfo: "userinfo", - host: "host", - port: nil, - path: "/path", - query: "query", - fragment: "fragment", - registry: nil, - opaque: nil - } - - # gopher - gopher = @object.parse('gopher://spinaltap.micro.umn.edu/00/Weather/California/Los%20Angeles') - gopher.should.is_a?(URI::Generic) - - URISpec.components(gopher).should == { - scheme: "gopher", - userinfo: nil, - host: "spinaltap.micro.umn.edu", - port: nil, - path: "/00/Weather/California/Los%20Angeles", - query: nil, - fragment: nil, - registry: nil, - opaque: nil - } - - # news - news = @object.parse('news:comp.infosystems.www.servers.unix') - news.should.is_a?(URI::Generic) - URISpec.components(news).should == { - scheme: "news", - userinfo: nil, - host: nil, - port: nil, - path: nil, - query: nil, - fragment: nil, - registry: nil, - opaque: "comp.infosystems.www.servers.unix" - } - - # telnet - telnet = @object.parse('telnet://melvyl.ucop.edu/') - telnet.should.is_a?(URI::Generic) - URISpec.components(telnet).should == { - scheme: "telnet", - userinfo: nil, - host: "melvyl.ucop.edu", - port: nil, - path: "/", - query: nil, - fragment: nil, - registry: nil, - opaque: nil - } - - # files - file_l = @object.parse('file:///foo/bar.txt') - file_l.should.is_a?(URI::Generic) - file = @object.parse('file:/foo/bar.txt') - file.should.is_a?(URI::Generic) - end - - if URI::DEFAULT_PARSER == URI::RFC2396_Parser - it "raises errors on malformed URIs" do - -> { @object.parse('http://a_b:80/') }.should.raise(URI::InvalidURIError) - -> { @object.parse('http://a_b/') }.should.raise(URI::InvalidURIError) - end - elsif URI::DEFAULT_PARSER == URI::RFC3986_Parser - it "does not raise errors on URIs contained underscore" do - -> { @object.parse('http://a_b:80/') }.should_not.raise(URI::InvalidURIError) - -> { @object.parse('http://a_b/') }.should_not.raise(URI::InvalidURIError) - end - end -end diff --git a/spec/ruby/library/yaml/load_stream_spec.rb b/spec/ruby/library/yaml/load_stream_spec.rb index 31bc862f5e7479..5f5d4c73376a28 100644 --- a/spec/ruby/library/yaml/load_stream_spec.rb +++ b/spec/ruby/library/yaml/load_stream_spec.rb @@ -1,9 +1,23 @@ require_relative '../../spec_helper' require_relative 'fixtures/strings' -require_relative 'shared/each_document' - require 'yaml' describe "YAML.load_stream" do - it_behaves_like :yaml_each_document, :load_stream + it "calls the block on each successive document" do + documents = [] + YAML.load_stream(YAMLSpecs::MULTIDOCUMENT) do |doc| + documents << doc + end + documents.should == [["Mark McGwire", "Sammy Sosa", "Ken Griffey"], + ["Chicago Cubs", "St Louis Cardinals"]] + end + + it "works on files" do + test_parse_file = fixture __FILE__, "test_yaml.yml" + File.open(test_parse_file, "r") do |file| + YAML.load_stream(file) do |doc| + doc.should == {"project"=>{"name"=>"RubySpec"}} + end + end + end end diff --git a/spec/ruby/library/yaml/shared/each_document.rb b/spec/ruby/library/yaml/shared/each_document.rb deleted file mode 100644 index 6f00aee297e6c9..00000000000000 --- a/spec/ruby/library/yaml/shared/each_document.rb +++ /dev/null @@ -1,19 +0,0 @@ -describe :yaml_each_document, shared: true do - it "calls the block on each successive document" do - documents = [] - YAML.send(@method, YAMLSpecs::MULTIDOCUMENT) do |doc| - documents << doc - end - documents.should == [["Mark McGwire", "Sammy Sosa", "Ken Griffey"], - ["Chicago Cubs", "St Louis Cardinals"]] - end - - it "works on files" do - test_parse_file = fixture __FILE__, "test_yaml.yml" - File.open(test_parse_file, "r") do |file| - YAML.send(@method, file) do |doc| - doc.should == {"project"=>{"name"=>"RubySpec"}} - end - end - end -end diff --git a/spec/ruby/library/zlib/gzipreader/each_line_spec.rb b/spec/ruby/library/zlib/gzipreader/each_line_spec.rb index 6f173658799382..97f24d410f4136 100644 --- a/spec/ruby/library/zlib/gzipreader/each_line_spec.rb +++ b/spec/ruby/library/zlib/gzipreader/each_line_spec.rb @@ -1,6 +1,9 @@ require_relative "../../../spec_helper" -require_relative 'shared/each' +require 'zlib' describe "Zlib::GzipReader#each_line" do - it_behaves_like :gzipreader_each, :each_line + it "is an alias of Zlib::GzipReader#each" do + Zlib::GzipReader.instance_method(:each_line).should == + Zlib::GzipReader.instance_method(:each) + end end diff --git a/spec/ruby/library/zlib/gzipreader/each_spec.rb b/spec/ruby/library/zlib/gzipreader/each_spec.rb index 3b98391a87fd84..75fd7e6bae6f12 100644 --- a/spec/ruby/library/zlib/gzipreader/each_spec.rb +++ b/spec/ruby/library/zlib/gzipreader/each_spec.rb @@ -1,6 +1,49 @@ require_relative "../../../spec_helper" -require_relative 'shared/each' +require 'stringio' +require 'zlib' describe "Zlib::GzipReader#each" do - it_behaves_like :gzipreader_each, :each + before :each do + @data = "firstline\nsecondline\n\nforthline" + @zip = [31, 139, 8, 0, 244, 125, 128, 88, 2, 255, 75, 203, 44, 42, 46, 201, + 201, 204, 75, 229, 42, 78, 77, 206, 207, 75, 1, 51, 185, 210,242, + 139, 74, 50, 64, 76, 0, 180, 54, 61, 111, 31, 0, 0, 0].pack('C*') + + @io = StringIO.new @zip + @gzreader = Zlib::GzipReader.new @io + end + + after :each do + ScratchPad.clear + end + + it "calls the given block for each line in the stream, passing the line as an argument" do + ScratchPad.record [] + @gzreader.each { |b| ScratchPad << b } + + ScratchPad.recorded.should == ["firstline\n", "secondline\n", "\n", "forthline"] + end + + it "returns an enumerator, which yields each byte in the stream, when no block is passed" do + enum = @gzreader.each + + ScratchPad.record [] + while true + begin + ScratchPad << enum.next + rescue StopIteration + break + end + end + + ScratchPad.recorded.should == ["firstline\n", "secondline\n", "\n", "forthline"] + end + + it "increments position before calling the block" do + i = 0 + @gzreader.each do |line| + i += line.length + @gzreader.pos.should == i + end + end end diff --git a/spec/ruby/library/zlib/gzipreader/eof_spec.rb b/spec/ruby/library/zlib/gzipreader/eof_spec.rb index a38e144c7231a2..434e716b649ee0 100644 --- a/spec/ruby/library/zlib/gzipreader/eof_spec.rb +++ b/spec/ruby/library/zlib/gzipreader/eof_spec.rb @@ -52,3 +52,10 @@ gz.eof?.should == true end end + +describe "Zlib::GzipReader#eof" do + it "is an alias of Zlib::GzipReader#eof?" do + Zlib::GzipReader.instance_method(:eof).should == + Zlib::GzipReader.instance_method(:eof?) + end +end diff --git a/spec/ruby/library/zlib/gzipreader/shared/each.rb b/spec/ruby/library/zlib/gzipreader/shared/each.rb deleted file mode 100644 index 71608e04abd36a..00000000000000 --- a/spec/ruby/library/zlib/gzipreader/shared/each.rb +++ /dev/null @@ -1,49 +0,0 @@ -require_relative '../../../../spec_helper' -require 'stringio' -require 'zlib' - -describe :gzipreader_each, shared: true do - before :each do - @data = "firstline\nsecondline\n\nforthline" - @zip = [31, 139, 8, 0, 244, 125, 128, 88, 2, 255, 75, 203, 44, 42, 46, 201, - 201, 204, 75, 229, 42, 78, 77, 206, 207, 75, 1, 51, 185, 210,242, - 139, 74, 50, 64, 76, 0, 180, 54, 61, 111, 31, 0, 0, 0].pack('C*') - - @io = StringIO.new @zip - @gzreader = Zlib::GzipReader.new @io - end - - after :each do - ScratchPad.clear - end - - it "calls the given block for each line in the stream, passing the line as an argument" do - ScratchPad.record [] - @gzreader.send(@method) { |b| ScratchPad << b } - - ScratchPad.recorded.should == ["firstline\n", "secondline\n", "\n", "forthline"] - end - - it "returns an enumerator, which yields each byte in the stream, when no block is passed" do - enum = @gzreader.send(@method) - - ScratchPad.record [] - while true - begin - ScratchPad << enum.next - rescue StopIteration - break - end - end - - ScratchPad.recorded.should == ["firstline\n", "secondline\n", "\n", "forthline"] - end - - it "increments position before calling the block" do - i = 0 - @gzreader.send(@method) do |line| - i += line.length - @gzreader.pos.should == i - end - end -end diff --git a/spec/ruby/library/zlib/gzipreader/tell_spec.rb b/spec/ruby/library/zlib/gzipreader/tell_spec.rb new file mode 100644 index 00000000000000..cc103e57b4b4d5 --- /dev/null +++ b/spec/ruby/library/zlib/gzipreader/tell_spec.rb @@ -0,0 +1,9 @@ +require_relative "../../../spec_helper" +require 'zlib' + +describe "Zlib::GzipReader#tell" do + it "is an alias of Zlib::GzipReader#pos" do + Zlib::GzipReader.instance_method(:tell).should == + Zlib::GzipReader.instance_method(:pos) + end +end diff --git a/spec/ruby/optional/capi/io_spec.rb b/spec/ruby/optional/capi/io_spec.rb index 35bd856e001471..459a32d954b3f2 100644 --- a/spec/ruby/optional/capi/io_spec.rb +++ b/spec/ruby/optional/capi/io_spec.rb @@ -142,7 +142,7 @@ describe "rb_io_check_closed" do it "does not raise an exception if the IO is not closed" do - # The MRI function is void, so we use should_not raise_error + # The MRI function is void, so we use should_not.raise -> { @o.rb_io_check_closed(@io) }.should_not.raise end @@ -221,7 +221,7 @@ describe "rb_io_check_readable" do it "does not raise an exception if the IO is opened for reading" do - # The MRI function is void, so we use should_not raise_error + # The MRI function is void, so we use should_not.raise -> { @o.rb_io_check_readable(@r_io) }.should_not.raise end @@ -237,7 +237,7 @@ describe "rb_io_check_writable" do it "does not raise an exception if the IO is opened for writing" do - # The MRI function is void, so we use should_not raise_error + # The MRI function is void, so we use should_not.raise -> { @o.rb_io_check_writable(@w_io) }.should_not.raise end diff --git a/spec/ruby/shared/kernel/raise.rb b/spec/ruby/shared/kernel/raise.rb index 84591669060c37..04eaa94e6aa476 100644 --- a/spec/ruby/shared/kernel/raise.rb +++ b/spec/ruby/shared/kernel/raise.rb @@ -92,6 +92,21 @@ def initialize -> { @object.raise("message", {cause: RuntimeError.new()}) }.should.raise(TypeError, "exception class/object expected") end + it "raises result from #exception when passed a non-Exception object" do + e = Object.new + def e.exception = StandardError.new + + -> { @object.raise e }.should.raise(StandardError) + end + + it "raises result from #exception with given arguments when passed a non-Exception object" do + e = Object.new + def e.exception(msg) = StandardError.new(msg) + + -> { @object.raise e, "foo" }.should.raise(StandardError, "foo") + -> { @object.raise e }.should.raise(ArgumentError, "wrong number of arguments (given 0, expected 1)") + end + it "raises TypeError when passed a non-Exception object but it responds to #exception method that doesn't return an instance of Exception class" do e = Object.new def e.exception diff --git a/spec/ruby/shared/process/fork.rb b/spec/ruby/shared/process/fork.rb index dd595cd93ed13c..6c7ea759809416 100644 --- a/spec/ruby/shared/process/fork.rb +++ b/spec/ruby/shared/process/fork.rb @@ -1,5 +1,5 @@ describe :process_fork, shared: true do - platform_is :windows do + guard_not -> { Process.respond_to?(:fork) } do it "returns false from #respond_to?" do # Workaround for Kernel::Method being public and losing the "non-respond_to? magic" mod = @object.class.name == "KernelSpecs::Method" ? Object.new : @object @@ -12,7 +12,7 @@ end end - platform_is_not :windows do + guard -> { Process.respond_to?(:fork) } do before :each do @file = tmp('i_exist') rm_r @file From b08d3f8f11b4956cfce8bd86b55be1a262121898 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Sat, 6 Jun 2026 17:57:15 +0900 Subject: [PATCH 163/188] Extract str_to_cstr --- string.c | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/string.c b/string.c index 63a5114341e754..283f992ff9cabd 100644 --- a/string.c +++ b/string.c @@ -2912,6 +2912,8 @@ str_null_check(VALUE str, int *w) return s; } +static char *str_to_cstr(VALUE str); + const char * rb_str_null_check(VALUE str) { @@ -2927,14 +2929,7 @@ rb_str_null_check(VALUE str) } } else { - int w; - const char *s = str_null_check(str, &w); - if (!s) { - if (w) { - rb_raise(rb_eArgError, "string contains null char"); - } - rb_raise(rb_eArgError, "string contains null byte"); - } + str_to_cstr(str); } return s; @@ -2951,6 +2946,12 @@ char * rb_string_value_cstr(volatile VALUE *ptr) { VALUE str = rb_string_value(ptr); + return str_to_cstr(str); +} + +static char * +str_to_cstr(VALUE str) +{ int w; char *s = str_null_check(str, &w); if (!s) { From 2d9827db8b9fffe08a2f7dfb64ce5629a186bc93 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Sat, 6 Jun 2026 18:00:42 +0900 Subject: [PATCH 164/188] Constify local pointer variables in string.c --- string.c | 77 +++++++++++++++++++++++++++----------------------------- 1 file changed, 37 insertions(+), 40 deletions(-) diff --git a/string.c b/string.c index 283f992ff9cabd..51f1a255b9f52d 100644 --- a/string.c +++ b/string.c @@ -2486,7 +2486,8 @@ rb_str_plus(VALUE str1, VALUE str2) { VALUE str3; rb_encoding *enc; - char *ptr1, *ptr2, *ptr3; + const char *ptr1, *ptr2; + char *ptr3; long len1, len2; int termlen; @@ -2919,7 +2920,7 @@ rb_str_null_check(VALUE str) { RUBY_ASSERT(RB_TYPE_P(str, T_STRING)); - char *s; + const char *s; long len; RSTRING_GETMEM(str, s, len); @@ -3128,7 +3129,7 @@ rb_str_sublen(VALUE str, long pos) if (single_byte_optimizable(str) || pos < 0) return pos; else { - char *p = RSTRING_PTR(str); + const char *p = RSTRING_PTR(str); return enc_strlen(p, p + pos, STR_ENC_GET(str), ENC_CODERANGE(str)); } } @@ -3197,7 +3198,7 @@ rb_str_subpos(VALUE str, long beg, long *lenp) long slen = -1L; const long blen = RSTRING_LEN(str); rb_encoding *enc = STR_ENC_GET(str); - char *p, *s = RSTRING_PTR(str), *e = s + blen; + const char *p, *s = RSTRING_PTR(str), *e = s + blen; if (len < 0) return 0; if (beg < 0 && -beg < 0) return 0; @@ -3274,7 +3275,7 @@ rb_str_subpos(VALUE str, long beg, long *lenp) end: *lenp = len; RB_GC_GUARD(str); - return p; + return (char *)p; } static VALUE str_substr(VALUE str, long beg, long len, int empty); @@ -3294,7 +3295,7 @@ rb_str_substr_two_fixnums(VALUE str, VALUE beg, VALUE len, int empty) static VALUE str_substr(VALUE str, long beg, long len, int empty) { - char *p = rb_str_subpos(str, beg, &len); + const char *p = rb_str_subpos(str, beg, &len); if (!p) return Qnil; if (!len && !empty) return Qnil; @@ -4768,10 +4769,9 @@ memrchr(const char *search_str, int chr, long search_len) static long str_rindex(VALUE str, VALUE sub, const char *s, rb_encoding *enc) { - char *hit, *adjusted; + const char *hit, *adjusted, *sbeg, *e, *t; int c; long slen, searchlen; - char *sbeg, *e, *t; sbeg = RSTRING_PTR(str); slen = RSTRING_LEN(sub); @@ -4806,7 +4806,7 @@ static long rb_str_rindex(VALUE str, VALUE sub, long pos) { long len, slen; - char *sbeg, *s; + const char *sbeg, *s; rb_encoding *enc; int singlebyte; @@ -4890,7 +4890,7 @@ static long rb_str_byterindex(VALUE str, VALUE sub, long pos) { long len, slen; - char *sbeg, *s; + const char *sbeg, *s; rb_encoding *enc; enc = rb_enc_check(str, sub); @@ -8283,7 +8283,7 @@ typedef unsigned char *USTR; struct tr { int gen; unsigned int now, max; - char *p, *pend; + const char *p, *pend; }; static unsigned int @@ -9013,7 +9013,7 @@ rb_str_count(int argc, VALUE *argv, VALUE str) char table[TR_TABLE_SIZE]; rb_encoding *enc = 0; VALUE del = 0, nodel = 0, tstr; - char *s, *send; + const char *s, *send; int i; int ascompat; size_t n = 0; @@ -9244,12 +9244,12 @@ rb_str_split_m(int argc, VALUE *argv, VALUE str) str_mod_check(str, str_start, str_len)) beg = 0; - char *ptr = RSTRING_PTR(str); - char *const str_start = ptr; + const char *ptr = RSTRING_PTR(str); + const char *const str_start = ptr; const long str_len = RSTRING_LEN(str); - char *const eptr = str_start + str_len; + const char *const eptr = str_start + str_len; if (split_type == SPLIT_TYPE_AWK) { - char *bptr = ptr; + const char *bptr = ptr; int skip = 1; unsigned int c; @@ -9308,8 +9308,8 @@ rb_str_split_m(int argc, VALUE *argv, VALUE str) } } else if (split_type == SPLIT_TYPE_STRING) { - char *substr_start = ptr; - char *sptr = RSTRING_PTR(spat); + const char *substr_start = ptr; + const char *sptr = RSTRING_PTR(spat); long slen = RSTRING_LEN(spat); if (result) result = rb_ary_new(); @@ -9318,7 +9318,7 @@ rb_str_split_m(int argc, VALUE *argv, VALUE str) while (ptr < eptr && (end = rb_memsearch(sptr, slen, ptr, eptr - ptr, enc)) >= 0) { /* Check we are at the start of a char */ - char *t = rb_enc_right_char_head(ptr, ptr + end, eptr, enc); + const char *t = rb_enc_right_char_head(ptr, ptr + end, eptr, enc); if (t != ptr + end) { ptr = t; continue; @@ -9457,8 +9457,8 @@ rb_str_enumerate_lines(int argc, VALUE *argv, VALUE str, VALUE ary) { rb_encoding *enc; VALUE line, rs, orig = str, opts = Qnil, chomp = Qfalse; - const char *ptr, *pend, *subptr, *subend, *rsptr, *hit, *adjusted; - long pos, len, rslen; + const char *pend, *subptr, *subend, *rsptr, *hit, *adjusted; + long pos, rslen; int rsnewline = 0; if (rb_scan_args(argc, argv, "01:", &rs, &opts) == 0) @@ -9483,9 +9483,9 @@ rb_str_enumerate_lines(int argc, VALUE *argv, VALUE str, VALUE ary) if (!RSTRING_LEN(str)) goto end; str = rb_str_new_frozen(str); - ptr = subptr = RSTRING_PTR(str); + const char *const ptr = subptr = RSTRING_PTR(str); + const long len = RSTRING_LEN(str); pend = RSTRING_END(str); - len = RSTRING_LEN(str); StringValue(rs); rslen = RSTRING_LEN(rs); @@ -10122,9 +10122,9 @@ chompped_length(VALUE str, VALUE rs) { rb_encoding *enc; int newline; - char *pp, *e, *rsptr; + const char *pp, *e, *rsptr; long rslen; - char *const p = RSTRING_PTR(str); + const char *const p = RSTRING_PTR(str); long len = RSTRING_LEN(str); if (len == 0) return 0; @@ -10337,7 +10337,7 @@ static VALUE rb_str_lstrip_bang(int argc, VALUE *argv, VALUE str) { rb_encoding *enc; - char *start, *s; + char *start; long olen, loffset; str_modify_keep_cr(str); @@ -10356,8 +10356,7 @@ rb_str_lstrip_bang(int argc, VALUE *argv, VALUE str) if (loffset > 0) { long len = olen-loffset; - s = start + loffset; - memmove(start, s, len); + memmove(start, start + loffset, len); STR_SET_LEN(str, len); TERM_FILL(start+len, rb_enc_mbminlen(enc)); return str; @@ -10396,7 +10395,7 @@ rb_str_lstrip_bang(int argc, VALUE *argv, VALUE str) static VALUE rb_str_lstrip(int argc, VALUE *argv, VALUE str) { - char *start; + const char *start; long len, loffset; RSTRING_GETMEM(str, start, len); @@ -10432,7 +10431,7 @@ rstrip_offset(VALUE str, const char *s, const char *e, rb_encoding *enc) while (s < t && ((c = *(t-1)) == '\0' || ascii_isspace(c))) t--; } else { - char *tp; + const char *tp; while ((tp = rb_enc_prev_char(s, t, e, enc)) != NULL) { unsigned int c = rb_enc_codepoint(tp, e, enc); @@ -10447,8 +10446,7 @@ static long rstrip_offset_table(VALUE str, const char *s, const char *e, rb_encoding *enc, char table[TR_TABLE_SIZE], VALUE del, VALUE nodel) { - const char *t; - char *tp; + const char *t, *tp; rb_str_check_dummy_enc(enc); if (rb_enc_str_coderange(str) == ENC_CODERANGE_BROKEN) { @@ -10540,7 +10538,7 @@ static VALUE rb_str_rstrip(int argc, VALUE *argv, VALUE str) { rb_encoding *enc; - char *start; + const char *start; long olen, roffset; enc = STR_ENC_GET(str); @@ -10640,7 +10638,7 @@ rb_str_strip_bang(int argc, VALUE *argv, VALUE str) static VALUE rb_str_strip(int argc, VALUE *argv, VALUE str) { - char *start; + const char *start; long olen, loffset, roffset; rb_encoding *enc = STR_ENC_GET(str); @@ -10733,7 +10731,8 @@ rb_str_scan(VALUE str, VALUE pat) VALUE result; long start = 0; long last = -1, prev = 0; - char *p = RSTRING_PTR(str); long len = RSTRING_LEN(str); + const char *p = RSTRING_PTR(str); + long len = RSTRING_LEN(str); pat = get_pat_quoted(pat, 1); mustnot_broken(str); @@ -10981,8 +10980,7 @@ rb_str_crypt(VALUE str, VALUE salt) # define CRYPT_END() rb_nativethread_lock_unlock(&crypt_mutex.lock) #endif VALUE result; - const char *s, *saltp; - char *res; + const char *s, *saltp, *res; #ifdef BROKEN_CRYPT char salt_8bit_clean[3]; #endif @@ -11027,12 +11025,11 @@ rb_str_crypt(VALUE str, VALUE salt) // before allocating a new object (the string to be returned). If we allocate while // holding the lock, we could run GC which fires the VM barrier and causes a deadlock // if other ractors are waiting on this lock. - size_t res_size = strlen(res)+1; + size_t res_size = strlen(res); tmp_buf = ALLOCA_N(char, res_size); // should be small enough to alloca memcpy(tmp_buf, res, res_size); - res = tmp_buf; CRYPT_END(); - result = rb_str_new_cstr(res); + result = rb_str_new(tmp_buf, res_size); #endif return result; } From 925fa468111d5e0115237807b2ef9505c311a389 Mon Sep 17 00:00:00 2001 From: Burdette Lamar Date: Sun, 7 Jun 2026 00:39:34 -0500 Subject: [PATCH 165/188] [DOC] Fnmatch examples doc --- doc/file/filename_matching.md | 208 ++++++++++++++++++++++++++-------- 1 file changed, 163 insertions(+), 45 deletions(-) diff --git a/doc/file/filename_matching.md b/doc/file/filename_matching.md index cf5b60bac29b72..fca02f1d83a824 100644 --- a/doc/file/filename_matching.md +++ b/doc/file/filename_matching.md @@ -41,17 +41,30 @@ see the table above. A simple string matches itself: ```ruby -File.fnmatch('xyzzy', 'xyzzy') # => true -File.fnmatch('one_two_three', 'one_two_three') # => true -File.fnmatch('123', '123') # => true -File.fnmatch('Form 27B/6', 'Form 27B/6') # => true -File.fnmatch('bcd', 'abcde') # => false # Must be exact. +File.fnmatch('xyzzy', 'xyzzy') # => true +File.fnmatch('one_two_three', 'one_two_three') # => true +File.fnmatch('123', '123') # => true +File.fnmatch('Form 27B/6', 'Form 27B/6') # => true + +Pathname('xyzzy').fnmatch('xyzzy') # => true +Pathname('one_two_three').fnmatch('one_two_three') # => true +Pathname('123').fnmatch('123') # => true +Pathname('Form 27B/6').fnmatch('Form 27B/6') # => true + +# Must be exact. +pattern = 'abcde' +path = 'abc' +File.fnmatch(pattern, path) # => false +Pathname(path).fnmatch(pattern) # => false ``` By default, the matching is case-sensitive: ```ruby -File.fnmatch('abc', 'ABC') # => false +pattern = 'abc' +path = 'ABC' +File.fnmatch(pattern, path) # => false +Pathname(path).fnmatch(pattern) # => false ``` Case-sensitivity may be modified by flags: @@ -61,8 +74,11 @@ Case-sensitivity may be modified by flags: By default, the alternatives pattern is disabled: -```rutby -File.fnmatch('R{ub,foo}y', 'Ruby') # => false +```ruby +pattern = 'R{ub,foo}y' +path = 'Ruby' +File.fnmatch(pattern, path) # => false +Pathname(path).fnmatch(pattern) # => false ``` It may be enabled by flag [`File::FNM_EXTGLOB`](#constant-filefnmextglob). @@ -70,7 +86,10 @@ It may be enabled by flag [`File::FNM_EXTGLOB`](#constant-filefnmextglob). By default, the Windows short name pattern is disabled: ```ruby -File.fnmatch('PROGRAM~1', 'Program Files') # => false +pattern ='PROGRAM~1' +path = 'Program Files' +File.fnmatch(pattern, path) # => false +Pathname(path).fnmatch(pattern) # => false ``` It may be enabled by flag [`File::FNM_SHORTNAME`](#constant-filefnmshortname). @@ -80,16 +99,31 @@ It may be enabled by flag [`File::FNM_SHORTNAME`](#constant-filefnmshortname). The asterisk pattern (`'*'`) matches any sequence of characters: ```ruby -File.fnmatch('*', 'foo') # => true -File.fnmatch('*', '') # => true -File.fnmatch('*', '*') # => true -File.fnmatch('\*', 'foo') # => false # Escaped. +pattern = '*' +File.fnmatch(pattern, 'foo') # => true +File.fnmatch(pattern, '') # => true +File.fnmatch(pattern, 'foo') # => true + +Pathname('foo').fnmatch(pattern) # => true +Pathname('').fnmatch(pattern) # => true +Pathname('*').fnmatch(pattern) # => true +``` + +The pattern may be escaped: + +```ruby +pattern = '\*' +File.fnmatch(pattern, 'foo') # => false +Pathname('foo').fnmatch(pattern) # => false ``` By default, the asterisk pattern does not match a leading period (as in a dot-file): ```ruby -File.fnmatch('*', '.document') # => false +pattern = '*' +path = '.document' +File.fnmatch(pattern, path) # => false +Pathname(path).fnmatch(pattern) # => false ``` That matching may be enabled by flag [`File::FNM_DOTMATCH`](#constant-filefnmdotmatch). @@ -97,7 +131,10 @@ That matching may be enabled by flag [`File::FNM_DOTMATCH`](#constant-filefnmdot By default, the asterisk pattern matches across file separators: ```ruby -File.fnmatch('*.rb', 'lib/test.rb') # => true +pattern = '*.rb' +path = 'lib/test.rb' +File.fnmatch(pattern, path) # => true +Pathname(path).fnmatch(pattern) # => true ``` That matching may be disabled by flag [`File::FNM_PATHNAME`](#constant-filefnmpathname). @@ -107,17 +144,37 @@ That matching may be disabled by flag [`File::FNM_PATHNAME`](#constant-filefnmpa The question-mark pattern (`'?'`) matches any single character: ```ruby -File.fnmatch('?', 'f') # => true -File.fnmatch("foo-?.txt", "foo-1.txt") # => true -File.fnmatch('?', 'foo') # => false -File.fnmatch('?', '') # => false -File.fnmatch('\?', 'f') # => false # Escaped. +pattern = '?' +File.fnmatch(pattern, 'f') # => true +File.fnmatch(pattern, '') # => false +File.fnmatch(pattern, 'foo') # => false + +Pathname('f').fnmatch(pattern) # => true +Pathname('').fnmatch(pattern) # => false +Pathname('foo').fnmatch(pattern) # => false + +pattern = 'foo-?.txt' +path = 'foo-1.txt' +File.fnmatch(pattern, path) # => true +Pathname(path).fnmatch(pattern) # => true +``` + +The pattern may be escaped: + +```ruby +pattern = '\?' +path = 'f' +File.fnmatch(pattern, path) # => false +Pathname(path).fnmatch(pattern) # => false ``` By default, pattern `'?'` matches the file separator: ```ruby -File.fnmatch('foo?boo', 'foo/boo') # => true +pattern = 'foo?bar' +path = 'foo/bar' +File.fnmatch(pattern, path) # => true +Pathname(path).fnmatch(pattern) # => true ``` That matching may be disabled by flag [`File::FNM_PATHNAME`](#constant-filefnmpathname). @@ -128,18 +185,40 @@ Characters enclosed in square brackets define a set of characters, any of which matches a single character: ```ruby -File.fnmatch('[ruby]', 'r') # => true -File.fnmatch('[ruby]', 'u') # => true -File.fnmatch('[ruby]', 'y') # => true -File.fnmatch('[ruby]', 'ruby') # => false -File.fnmatch('\[ruby]', 'r') # => false # Escaped. +pattern = '[ruby]' +File.fnmatch(pattern, 'r') # => true +File.fnmatch(pattern, 'u') # => true +File.fnmatch(pattern, 'y') # => true + +Pathname('r').fnmatch(pattern) # => true +Pathname('u').fnmatch(pattern) # => true +Pathname('y').fnmatch(pattern) # => true + +# Matches a single character. +pattern = '[ruby]' +path = 'ruby' +File.fnmatch(pattern, path) # => false +Pathname(path).fnmatch(pattern) # => false +``` + +The pattern may be escaped: + +```ruby +pattern = '\[ruby]' +path = 'r' +File.fnmatch(pattern, path) # => false +Pathname(path).fnmatch(pattern) # => false ``` The character set may be negated: ```ruby -File.fnmatch('[^ruby]', 'r') # => false -File.fnmatch('[^ruby]', 'u') # => false +pattern = '[^ruby]' +File.fnmatch(pattern, 'r') # => false +File.fnmatch(pattern, 'u') # => false + +Pathname('r').fnmatch(pattern) # => false +Pathname('u').fnmatch(pattern) # => false ``` ### Single Character from a \Range (`'[a-c]'`, `'[^a-c]'`) @@ -148,18 +227,42 @@ A range of characters enclosed in square brackets defines a set of characters, any of which matches a single character: ```ruby -File.fnmatch('[a-c]', 'b') # => true -File.fnmatch('[a-c]', 'd') # => false -File.fnmatch('[a-c]', 'abc') # => false -File.fnmatch('R[t-v][a-c]y', 'Ruby') # => true # Multiple ranges allowed. -File.fnmatch('\[a-c]', 'b') # => false # Escaped. +pattern = '[a-c]' +File.fnmatch(pattern, 'b') # => true +File.fnmatch(pattern, 'd') # => false +File.fnmatch(pattern, 'abc') # => false + +Pathname('b').fnmatch(pattern) # => true +Pathname('d').fnmatch(pattern) # => false +Pathname('abc').fnmatch(pattern) # => false +``` + +The pattern may be escaped: + +```ruby +pattern = '\[a-c]' +path = 'b' +File.fnmatch(pattern, path) # => false +Pathname(path).fnmatch(pattern) # => false + +``` + +Multiple ranges are allowed: + +```ruby +pattern = 'R[t-v][a-c]y' +path = 'Ruby' +File.fnmatch(pattern, path) # => true +Pathname(path).fnmatch(pattern) # => true ``` The range may be negated: ```ruby -File.fnmatch('[^a-c]', 'b') # => false -File.fnmatch('[^a-c]', 'd') # => true +pattern = '[^a-c]' +path = 'b' +File.fnmatch(pattern, path) # => false +Pathname(path).fnmatch(pattern) # => false ``` ### Escape (`'\'`) @@ -168,21 +271,36 @@ The backslash character (`'\'`) may be used to escape any of the characters that filename matching treats as special: ```ruby -File.fnmatch('[a-c]', 'b') # => true -File.fnmatch('\[a-c]', 'b') # => false -File.fnmatch('[a-c\]', 'b') # => false -File.fnmatch('[a\-c]', 'b') # => false - -File.fnmatch('{a,b}', 'b', File::FNM_EXTGLOB) # => true -File.fnmatch('\{a,b}', 'b', File::FNM_EXTGLOB) # => false -File.fnmatch('{a\,b}', 'b', File::FNM_EXTGLOB) # => false -File.fnmatch('{a,b\}', 'b', File::FNM_EXTGLOB) # => false +path = 'b' +File.fnmatch('[a-c]', path) # => true +File.fnmatch('\[a-c]', path) # => false +File.fnmatch('[a-c\]', path) # => false +File.fnmatch('[a\-c]', path) # => false + +Pathname(path).fnmatch('[a-c]') # => true +Pathname(path).fnmatch('\[a-c]') # => false +Pathname(path).fnmatch('[a-c\]') # => false +Pathname(path).fnmatch('[a\-c]') # => false + +File.fnmatch('{a,b}', path, File::FNM_EXTGLOB) # => true +File.fnmatch('\{a,b}', path, File::FNM_EXTGLOB) # => false +File.fnmatch('{a\,b}', path, File::FNM_EXTGLOB) # => false +File.fnmatch('{a,b\}', path, File::FNM_EXTGLOB) # => false + +Pathname(path).fnmatch('{a,b}', File::FNM_EXTGLOB) # => true +Pathname(path).fnmatch('\{a,b}', File::FNM_EXTGLOB) # => false +Pathname(path).fnmatch('{a,b\}', File::FNM_EXTGLOB) # => false +Pathname(path).fnmatch('{a\,b}', File::FNM_EXTGLOB) # => false + ``` Use a double-backslash to represent an ordinary backslash: ```ruby -File.fnmatch('\\\\', '\\') # => true +pattern = '\\\\' +path = '\\' +File.fnmatch(pattern, path) # => true +Pathname(path).fnmatch(pattern) # => true ``` By default escape pattern `'\'` is enabled; From 99d06012e41ae320c9b1276d27a7ac09a3e059ec Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Wed, 25 Mar 2026 14:06:14 +0900 Subject: [PATCH 166/188] sprintf.c: Refactor CHECK macro --- sprintf.c | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/sprintf.c b/sprintf.c index 234aff76f5d3a1..4788e1872889e3 100644 --- a/sprintf.c +++ b/sprintf.c @@ -65,15 +65,22 @@ sign_bits(int base, const char *p) #define FPREC 64 #define FPREC0 128 +static long +expand_result(VALUE result, long bsiz, long blen, long l) +{ + int cr = ENC_CODERANGE(result); + RUBY_ASSERT(bsiz >= blen); + while (l > bsiz - blen) { + bsiz *= 2; + if (bsiz < 0) rb_raise(rb_eArgError, "too big specifier"); + } + rb_str_resize(result, bsiz); + ENC_CODERANGE_SET(result, cr); + return bsiz; +} + #define CHECK(l) do {\ - int cr = ENC_CODERANGE(result);\ - RUBY_ASSERT(bsiz >= blen); \ - while ((l) > bsiz - blen) {\ - bsiz*=2;\ - if (bsiz<0) rb_raise(rb_eArgError, "too big specifier");\ - }\ - rb_str_resize(result, bsiz);\ - ENC_CODERANGE_SET(result, cr);\ + bsiz = expand_result(result, bsiz, blen, l);\ buf = RSTRING_PTR(result);\ } while (0) From e39475a544f08ce5f140213c740f2464933a2bae Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Wed, 25 Mar 2026 14:00:02 +0900 Subject: [PATCH 167/188] sprintf.c: Fix width overflow --- sprintf.c | 21 ++++++++++----------- test/ruby/test_sprintf.rb | 6 ++++++ 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/sprintf.c b/sprintf.c index 4788e1872889e3..b91d7589572969 100644 --- a/sprintf.c +++ b/sprintf.c @@ -84,6 +84,11 @@ expand_result(VALUE result, long bsiz, long blen, long l) buf = RSTRING_PTR(result);\ } while (0) +#define CHECK_WIDTH(l, w) do { \ + if ((l) > INT_MAX - (w)) rb_raise(rb_eArgError, "width too big");\ + CHECK((l)+(w));\ +} while (0) + #define PUSH(s, l) do { \ CHECK(l);\ PUSH_(s, l);\ @@ -476,19 +481,13 @@ rb_str_format(int argc, const VALUE *argv, VALUE fmt) rb_enc_mbcput(c, &buf[blen], enc); blen += n; } - else if ((flags & FMINUS)) { - --width; - CHECK(n + (width > 0 ? width : 0)); - rb_enc_mbcput(c, &buf[blen], enc); - blen += n; - if (width > 0) FILL_(' ', width); - } else { --width; - CHECK(n + (width > 0 ? width : 0)); - if (width > 0) FILL_(' ', width); + CHECK_WIDTH(n, (width > 0 ? width : 0)); + if (!(flags & FMINUS) && (width > 0)) FILL_(' ', width); rb_enc_mbcput(c, &buf[blen], enc); blen += n; + if ((flags & FMINUS) && (width > 0)) FILL_(' ', width); } } break; @@ -525,7 +524,7 @@ rb_str_format(int argc, const VALUE *argv, VALUE fmt) /* need to adjust multi-byte string pos */ if ((flags&FWIDTH) && (width > slen)) { width -= (int)slen; - CHECK(len + width); + CHECK_WIDTH(len, width); if (!(flags&FMINUS)) { FILL_(' ', width); width = 0; @@ -839,7 +838,7 @@ rb_str_format(int argc, const VALUE *argv, VALUE fmt) if (sign || (flags&FSPACE)) ++len; if (prec > 0) ++len; /* period */ fill = width > len ? width - len : 0; - CHECK(fill + len); + CHECK(fill + len); /* max(width, len) */ if (fill && !(flags&(FMINUS|FZERO))) { FILL_(' ', fill); } diff --git a/test/ruby/test_sprintf.rb b/test/ruby/test_sprintf.rb index 1c7e89c2651a9d..bbbe6e7ec38589 100644 --- a/test/ruby/test_sprintf.rb +++ b/test/ruby/test_sprintf.rb @@ -1,5 +1,6 @@ # frozen_string_literal: false require 'test/unit' +require 'rbconfig/sizeof' class TestSprintf < Test::Unit::TestCase def test_positional @@ -539,6 +540,11 @@ def test_named_with_nil def test_width_underflow bug = 'https://github.com/mruby/mruby/issues/3347' assert_equal("!", sprintf("%*c", 0, ?!.ord), bug) + + int_max = RbConfig::LIMITS["INT_MAX"] + assert_raise_with_message(ArgumentError, /width too big/) { + sprintf "%*c", int_max, 0x80 + } end def test_negative_width_overflow From 2c72ee12e4707da53b78c1a0c2555b07fa812258 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Sun, 7 Jun 2026 16:37:10 +0900 Subject: [PATCH 168/188] Skip the hang-up test in mmtk [ci skip] --- test/.excludes-mmtk/TestObjSpace.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/test/.excludes-mmtk/TestObjSpace.rb b/test/.excludes-mmtk/TestObjSpace.rb index 94eb2c436d4435..feb05063df63bf 100644 --- a/test/.excludes-mmtk/TestObjSpace.rb +++ b/test/.excludes-mmtk/TestObjSpace.rb @@ -2,3 +2,4 @@ exclude(:test_dump_flag_age, "testing behaviour specific to default GC") exclude(:test_dump_flags, "testing behaviour specific to default GC") exclude(:test_dump_objects_dumps_page_slot_sizes, "testing behaviour specific to default GC") +exclude(:test_trace_object_allocations_does_not_reuse_freed_allocation_info, "hang up") From c022eccde0a62929ecaf66368205ec4b1392669e Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Sun, 7 Jun 2026 10:11:23 +0200 Subject: [PATCH 169/188] [ruby/json] Mark JSON_Parser_frame_stack_type as WB protected It already implictly is on recent rubies because it has no mark function, but might as well make it explicit. https://github.com/ruby/json/commit/8d7f975b01 --- ext/json/parser/parser.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/json/parser/parser.c b/ext/json/parser/parser.c index c0631728c38c80..524b80200fcf77 100644 --- a/ext/json/parser/parser.c +++ b/ext/json/parser/parser.c @@ -488,7 +488,7 @@ static const rb_data_type_t JSON_Parser_frame_stack_type = { .dfree = json_frame_stack_free, .dsize = json_frame_stack_memsize, }, - .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_EMBEDDABLE, + .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED | RUBY_TYPED_EMBEDDABLE, }; static json_frame_stack *json_frame_stack_spill(json_frame_stack *old_stack, VALUE *handle, json_frame_stack **stack_ref) From c332b80bfe30565654de71111c821ed3338a203f Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Sun, 7 Jun 2026 10:07:27 +0200 Subject: [PATCH 170/188] [ruby/json] Compile UNREACHABLE_RETURN into `rb_bug` when in debug mode This makes it much easier to debug. https://github.com/ruby/json/commit/bbcf0a3254 --- ext/json/json.h | 8 ++++++++ ext/json/parser/parser.c | 20 ++++++++++---------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/ext/json/json.h b/ext/json/json.h index cf9420d4dd2456..78b0eee0a4d608 100644 --- a/ext/json/json.h +++ b/ext/json/json.h @@ -11,6 +11,9 @@ #if defined(RUBY_DEBUG) && RUBY_DEBUG # define JSON_ASSERT RUBY_ASSERT +# ifndef JSON_DEBUG +# define JSON_DEBUG 1 +# endif #else # ifdef JSON_DEBUG # include @@ -20,6 +23,11 @@ # endif #endif +#ifdef JSON_DEBUG +# define JSON_UNREACHABLE_RETURN(val) rb_bug("Unreachable") +#else +# define JSON_UNREACHABLE_RETURN UNREACHABLE_RETURN +#endif /* shims */ #if SIZEOF_UINT64_T == SIZEOF_LONG_LONG diff --git a/ext/json/parser/parser.c b/ext/json/parser/parser.c index 524b80200fcf77..6b8164c062daca 100644 --- a/ext/json/parser/parser.c +++ b/ext/json/parser/parser.c @@ -1489,7 +1489,7 @@ static VALUE json_parse_any(JSON_ParserState *state, JSON_ParserConfig *config) case JSON_PHASE_OBJECT_KEY: goto JSON_PHASE_OBJECT_KEY; case JSON_PHASE_OBJECT_COLON: goto JSON_PHASE_OBJECT_COLON; } - UNREACHABLE_RETURN(Qundef); + JSON_UNREACHABLE_RETURN(Qundef); JSON_PHASE_DONE: { // The root document value is parsed; it is the lone survivor on @@ -1623,10 +1623,10 @@ static VALUE json_parse_any(JSON_ParserState *state, JSON_ParserConfig *config) case JSON_PHASE_ARRAY_COMMA: goto JSON_PHASE_ARRAY_COMMA; case JSON_PHASE_OBJECT_COMMA: goto JSON_PHASE_OBJECT_COMMA; case JSON_PHASE_VALUE: goto JSON_PHASE_VALUE; - case JSON_PHASE_OBJECT_KEY: UNREACHABLE_RETURN(Qundef); + case JSON_PHASE_OBJECT_KEY: JSON_UNREACHABLE_RETURN(Qundef); case JSON_PHASE_OBJECT_COLON: goto JSON_PHASE_OBJECT_COLON; } - UNREACHABLE_RETURN(Qundef); + JSON_UNREACHABLE_RETURN(Qundef); } JSON_PHASE_OBJECT_KEY: { @@ -1648,7 +1648,7 @@ static VALUE json_parse_any(JSON_ParserState *state, JSON_ParserConfig *config) raise_parse_error("expected object key, got: %s", state); } } - UNREACHABLE_RETURN(Qundef); + JSON_UNREACHABLE_RETURN(Qundef); } JSON_PHASE_OBJECT_COLON: { @@ -1669,7 +1669,7 @@ static VALUE json_parse_any(JSON_ParserState *state, JSON_ParserConfig *config) raise_parse_error("expected ':' after object key, got: %s", state); } } - UNREACHABLE_RETURN(Qundef); + JSON_UNREACHABLE_RETURN(Qundef); } JSON_PHASE_ARRAY_COMMA: { @@ -1705,13 +1705,13 @@ static VALUE json_parse_any(JSON_ParserState *state, JSON_ParserConfig *config) case JSON_PHASE_ARRAY_COMMA: goto JSON_PHASE_ARRAY_COMMA; case JSON_PHASE_OBJECT_COMMA: goto JSON_PHASE_OBJECT_COMMA; case JSON_PHASE_VALUE: goto JSON_PHASE_VALUE; - case JSON_PHASE_OBJECT_KEY: UNREACHABLE_RETURN(Qundef); + case JSON_PHASE_OBJECT_KEY: JSON_UNREACHABLE_RETURN(Qundef); case JSON_PHASE_OBJECT_COLON: goto JSON_PHASE_OBJECT_COLON; } } else { raise_parse_error("expected ',' or ']' after array value", state); } - UNREACHABLE_RETURN(Qundef); + JSON_UNREACHABLE_RETURN(Qundef); } JSON_PHASE_OBJECT_COMMA: { @@ -1754,16 +1754,16 @@ static VALUE json_parse_any(JSON_ParserState *state, JSON_ParserConfig *config) case JSON_PHASE_ARRAY_COMMA: goto JSON_PHASE_ARRAY_COMMA; case JSON_PHASE_OBJECT_COMMA: goto JSON_PHASE_OBJECT_COMMA; case JSON_PHASE_VALUE: goto JSON_PHASE_VALUE; - case JSON_PHASE_OBJECT_KEY: UNREACHABLE_RETURN(Qundef); + case JSON_PHASE_OBJECT_KEY: JSON_UNREACHABLE_RETURN(Qundef); case JSON_PHASE_OBJECT_COLON: goto JSON_PHASE_OBJECT_COLON; } } else { raise_parse_error("expected ',' or '}' after object value, got: %s", state); } - UNREACHABLE_RETURN(Qundef); + JSON_UNREACHABLE_RETURN(Qundef); } - UNREACHABLE_RETURN(Qundef); + JSON_UNREACHABLE_RETURN(Qundef); } static void json_ensure_eof(JSON_ParserState *state) From 4bd3e14fc2623414680008c7b1d38f1a3df2877e Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Sun, 7 Jun 2026 16:40:52 +0900 Subject: [PATCH 171/188] IO::Buffer: Validate the mask argument of bit operations --- io_buffer.c | 12 ++++++++++++ test/ruby/test_io_buffer.rb | 9 +++++++++ 2 files changed, 21 insertions(+) diff --git a/io_buffer.c b/io_buffer.c index faa53042481144..d9f50fc234c016 100644 --- a/io_buffer.c +++ b/io_buffer.c @@ -3625,6 +3625,10 @@ io_buffer_and_inplace(VALUE self, VALUE mask) size_t size; io_buffer_get_bytes_for_writing(buffer, &base, &size); + const void *mask_base; + size_t mask_size; + io_buffer_get_bytes_for_reading(mask_buffer, &mask_base, &mask_size); + memory_and_inplace(base, size, mask_buffer->base, mask_buffer->size); return self; @@ -3671,6 +3675,10 @@ io_buffer_or_inplace(VALUE self, VALUE mask) size_t size; io_buffer_get_bytes_for_writing(buffer, &base, &size); + const void *mask_base; + size_t mask_size; + io_buffer_get_bytes_for_reading(mask_buffer, &mask_base, &mask_size); + memory_or_inplace(base, size, mask_buffer->base, mask_buffer->size); return self; @@ -3717,6 +3725,10 @@ io_buffer_xor_inplace(VALUE self, VALUE mask) size_t size; io_buffer_get_bytes_for_writing(buffer, &base, &size); + const void *mask_base; + size_t mask_size; + io_buffer_get_bytes_for_reading(mask_buffer, &mask_base, &mask_size); + memory_xor_inplace(base, size, mask_buffer->base, mask_buffer->size); return self; diff --git a/test/ruby/test_io_buffer.rb b/test/ruby/test_io_buffer.rb index b6372f25b88ef6..327a3ece9c3398 100644 --- a/test/ruby/test_io_buffer.rb +++ b/test/ruby/test_io_buffer.rb @@ -712,6 +712,10 @@ def test_operators_raise_on_freed_self assert_raise(IO::Buffer::InvalidatedError) { slice | mask } assert_raise(IO::Buffer::InvalidatedError) { slice ^ mask } assert_raise(IO::Buffer::InvalidatedError) { ~slice } + + assert_raise(IO::Buffer::InvalidatedError) { slice.and!(mask) } + assert_raise(IO::Buffer::InvalidatedError) { slice.or!(mask) } + assert_raise(IO::Buffer::InvalidatedError) { slice.xor!(mask) } end def test_operators_raise_on_freed_mask @@ -723,6 +727,11 @@ def test_operators_raise_on_freed_mask assert_raise(IO::Buffer::InvalidatedError) { source & mask_slice } assert_raise(IO::Buffer::InvalidatedError) { source | mask_slice } assert_raise(IO::Buffer::InvalidatedError) { source ^ mask_slice } + + source = source.dup + assert_raise(IO::Buffer::InvalidatedError) { source.and!(mask_slice) } + assert_raise(IO::Buffer::InvalidatedError) { source.or!(mask_slice) } + assert_raise(IO::Buffer::InvalidatedError) { source.xor!(mask_slice) } end def test_bit_count From be9725c383f7e554180df7c77a201df1f3ae7f15 Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Sun, 7 Jun 2026 21:17:10 +0900 Subject: [PATCH 172/188] [DOC] Fix missing parentheses in Kernel#print --- io.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/io.c b/io.c index effcb349c3c47b..f121167102b2ef 100644 --- a/io.c +++ b/io.c @@ -8798,14 +8798,14 @@ rb_io_print(int argc, const VALUE *argv, VALUE out) * * Writes the given objects to $stdout; returns +nil+. * Appends the output record separator $OUTPUT_RECORD_SEPARATOR - * $\\), if it is not +nil+. + * ($\\), if it is not +nil+. * * With argument +objects+ given, for each object: * * - Converts via its method +to_s+ if not a string. * - Writes to stdout. * - If not the last object, writes the output field separator - * $OUTPUT_FIELD_SEPARATOR ($, if it is not +nil+. + * $OUTPUT_FIELD_SEPARATOR ($,) if it is not +nil+. * * With default separators: * From fe5df19f1cd9eb82b8bdcac109912dc3271ce4f3 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Sun, 7 Jun 2026 10:38:16 +0200 Subject: [PATCH 173/188] [ruby/json] Deprecate default support of JavaScript comments in the parser Add the `allow_comments: true` parsing option. https://github.com/ruby/json/commit/138b9a2c49 --- ext/json/lib/json.rb | 18 +++++++++-- ext/json/parser/parser.c | 59 ++++++++++++++++++++++------------- test/json/json_parser_test.rb | 30 ++++++++++++------ 3 files changed, 73 insertions(+), 34 deletions(-) diff --git a/ext/json/lib/json.rb b/ext/json/lib/json.rb index 26d601926f9b8f..f8dc4ccc9ed0dd 100644 --- a/ext/json/lib/json.rb +++ b/ext/json/lib/json.rb @@ -145,11 +145,11 @@ # # warning: detected duplicate keys in JSON object. # # This will raise an error in json 3.0 unless enabled via `allow_duplicate_key: true` # -# When set to `+true+` +# When set to +true+: # # The last value is used. # JSON.parse('{"a": 1, "a":2}') => {"a" => 2} # -# When set to `+false+`, the future default: +# When set to +false+, the future default: # JSON.parse('{"a": 1, "a":2}') => duplicate key at line 1 column 1 (JSON::ParserError) # # --- @@ -184,6 +184,20 @@ # # --- # +# Option +allow_comments+ (boolean) specifies whether to allow +# JavaScript style comments (either // comment or /* comment */); +# defaults to +false+. +# +# When not specified, a deprecation warning is emitted if a comment is encountered. +# +# When set to +true+, comments are ignored: +# JSON.parse('/* comment */ {"a": 1, "a":2}') # => {"a" => 2} +# +# When set to +false+, the future default: +# JSON.parse('/* comment */ {"a": 1, "a":2}') # unexpected character: '/' at line 1 column 1 (JSON::ParserError) +# +# --- +# # Option +allow_control_characters+ (boolean) specifies whether to allow # unescaped ASCII control characters, such as newlines, in strings; # defaults to +false+. diff --git a/ext/json/parser/parser.c b/ext/json/parser/parser.c index 6b8164c062daca..dc76ca2cda5ea6 100644 --- a/ext/json/parser/parser.c +++ b/ext/json/parser/parser.c @@ -7,9 +7,9 @@ static VALUE CNaN, CInfinity, CMinusInfinity; static ID i_new, i_try_convert, i_uminus, i_encode; -static VALUE sym_max_nesting, sym_allow_nan, sym_allow_trailing_comma, sym_allow_control_characters, - sym_allow_invalid_escape, sym_symbolize_names, sym_freeze, sym_decimal_class, sym_on_load, - sym_allow_duplicate_key; +static VALUE sym_max_nesting, sym_allow_nan, sym_allow_trailing_comma, sym_allow_comments, + sym_allow_control_characters, sym_allow_invalid_escape, sym_symbolize_names, + sym_freeze, sym_decimal_class, sym_on_load, sym_allow_duplicate_key; static int binary_encindex; static int utf8_encindex; @@ -382,7 +382,7 @@ typedef struct json_frame_stack_struct { json_frame *ptr; } json_frame_stack; -enum duplicate_key_action { +enum deprecatable_action { JSON_DEPRECATED = 0, JSON_IGNORE, JSON_RAISE, @@ -392,7 +392,8 @@ typedef struct JSON_ParserStruct { VALUE on_load_proc; VALUE decimal_class; ID decimal_method_id; - enum duplicate_key_action on_duplicate_key; + enum deprecatable_action on_duplicate_key; + enum deprecatable_action on_comment; int max_nesting; bool allow_nan; bool allow_trailing_comma; @@ -590,6 +591,8 @@ static void cursor_position(JSON_ParserState *state, long *line_out, long *colum *column_out = column; } +static const unsigned int MAX_DEPRECATIONS = 5; + static void emit_parse_warning(const char *message, JSON_ParserState *state) { long line, column; @@ -707,9 +710,14 @@ static uint32_t unescape_unicode(JSON_ParserState *state, const char *sp, const static const rb_data_type_t JSON_ParserConfig_type; +const char *COMMENT_DEPRECATION_MESSAGE = "Encountered comment in JSON. This will raise an error in json 3.0 unless enabled via `allow_comments: true`"; NOINLINE(static) void -json_eat_comments(JSON_ParserState *state) +json_eat_comments(JSON_ParserState *state, JSON_ParserConfig *config) { + if (config->on_comment == JSON_RAISE) { + raise_parse_error("unexpected token %s", state); + } + const char *start = state->cursor; state->cursor++; @@ -744,10 +752,15 @@ json_eat_comments(JSON_ParserState *state) raise_parse_error_at("unexpected token %s", state, start); break; } + + if (config->on_comment == JSON_DEPRECATED && state->emitted_deprecations < MAX_DEPRECATIONS) { + state->emitted_deprecations++; + emit_parse_warning(COMMENT_DEPRECATION_MESSAGE, state); + } } ALWAYS_INLINE(static) void -json_eat_whitespace(JSON_ParserState *state) +json_eat_whitespace(JSON_ParserState *state, JSON_ParserConfig *config) { while (true) { switch (peek(state)) { @@ -778,7 +791,7 @@ json_eat_whitespace(JSON_ParserState *state) state->cursor++; break; case '/': - json_eat_comments(state); + json_eat_comments(state, config); break; default: @@ -1127,9 +1140,9 @@ NOINLINE(static) void json_on_duplicate_key(JSON_ParserState *state, JSON_Parser case JSON_DEPRECATED: // Only emit the first few deprecations to avoid spamming. - if (state->emitted_deprecations < 5) { - emit_duplicate_key_warning(state, json_find_duplicated_key(count, pairs)); + if (state->emitted_deprecations < MAX_DEPRECATIONS) { state->emitted_deprecations++; + emit_duplicate_key_warning(state, json_find_duplicated_key(count, pairs)); } return; @@ -1498,7 +1511,7 @@ static VALUE json_parse_any(JSON_ParserState *state, JSON_ParserConfig *config) } JSON_PHASE_VALUE: { - json_eat_whitespace(state); + json_eat_whitespace(state, config); VALUE value; switch (peek(state)) { @@ -1559,7 +1572,7 @@ static VALUE json_parse_any(JSON_ParserState *state, JSON_ParserConfig *config) case '[': { state->cursor++; - json_eat_whitespace(state); + json_eat_whitespace(state, config); if (peek(state) == ']') { state->cursor++; @@ -1585,7 +1598,7 @@ static VALUE json_parse_any(JSON_ParserState *state, JSON_ParserConfig *config) const char *object_start_cursor = state->cursor; state->cursor++; - json_eat_whitespace(state); + json_eat_whitespace(state, config); if (peek(state) == '}') { state->cursor++; @@ -1632,7 +1645,7 @@ static VALUE json_parse_any(JSON_ParserState *state, JSON_ParserConfig *config) JSON_PHASE_OBJECT_KEY: { JSON_ASSERT(frame->type == JSON_FRAME_OBJECT); - json_eat_whitespace(state); + json_eat_whitespace(state, config); if (RB_LIKELY(peek(state) == '"')) { json_push_value(state, config, json_parse_string(state, config, true)); @@ -1654,7 +1667,7 @@ static VALUE json_parse_any(JSON_ParserState *state, JSON_ParserConfig *config) JSON_PHASE_OBJECT_COLON: { JSON_ASSERT(frame->type == JSON_FRAME_OBJECT); - json_eat_whitespace(state); + json_eat_whitespace(state, config); if (RB_LIKELY(peek(state) == ':')) { state->cursor++; @@ -1675,14 +1688,14 @@ static VALUE json_parse_any(JSON_ParserState *state, JSON_ParserConfig *config) JSON_PHASE_ARRAY_COMMA: { JSON_ASSERT(frame->type == JSON_FRAME_ARRAY); - json_eat_whitespace(state); + json_eat_whitespace(state, config); const char next_char = peek(state); if (RB_LIKELY(next_char == ',')) { state->cursor++; if (config->allow_trailing_comma) { - json_eat_whitespace(state); + json_eat_whitespace(state, config); if (peek(state) == ']') { // Trailing comma: stay in COMMA to close on the next iteration. goto JSON_PHASE_ARRAY_COMMA; @@ -1717,14 +1730,14 @@ static VALUE json_parse_any(JSON_ParserState *state, JSON_ParserConfig *config) JSON_PHASE_OBJECT_COMMA: { JSON_ASSERT(frame->type == JSON_FRAME_OBJECT); - json_eat_whitespace(state); + json_eat_whitespace(state, config); const char next_char = peek(state); if (RB_LIKELY(next_char == ',')) { state->cursor++; if (config->allow_trailing_comma) { - json_eat_whitespace(state); + json_eat_whitespace(state, config); if (peek(state) == '}') { // Trailing comma: stay in COMMA to close on the next iteration. goto JSON_PHASE_OBJECT_COMMA; @@ -1766,9 +1779,9 @@ static VALUE json_parse_any(JSON_ParserState *state, JSON_ParserConfig *config) JSON_UNREACHABLE_RETURN(Qundef); } -static void json_ensure_eof(JSON_ParserState *state) +static void json_ensure_eof(JSON_ParserState *state, JSON_ParserConfig *config) { - json_eat_whitespace(state); + json_eat_whitespace(state, config); if (!eos(state)) { raise_parse_error("unexpected token at end of stream %s", state); } @@ -1825,6 +1838,7 @@ static int parser_config_init_i(VALUE key, VALUE val, VALUE data) if (key == sym_max_nesting) { config->max_nesting = RTEST(val) ? FIX2INT(val) : 0; } else if (key == sym_allow_nan) { config->allow_nan = RTEST(val); } else if (key == sym_allow_trailing_comma) { config->allow_trailing_comma = RTEST(val); } + else if (key == sym_allow_comments) { config->on_comment = RTEST(val) ? JSON_IGNORE : JSON_RAISE; } else if (key == sym_allow_control_characters) { config->allow_control_characters = RTEST(val); } else if (key == sym_allow_invalid_escape) { config->allow_invalid_escape = RTEST(val); } else if (key == sym_symbolize_names) { config->symbolize_names = RTEST(val); } @@ -1977,7 +1991,7 @@ static VALUE cParser_parse(JSON_ParserConfig *config, VALUE src) RB_GC_GUARD(value_stack_handle); RB_GC_GUARD(frame_stack_handle); RB_GC_GUARD(Vsource); - json_ensure_eof(state); + json_ensure_eof(state, config); return result; } @@ -2079,6 +2093,7 @@ void Init_parser(void) sym_max_nesting = ID2SYM(rb_intern("max_nesting")); sym_allow_nan = ID2SYM(rb_intern("allow_nan")); sym_allow_trailing_comma = ID2SYM(rb_intern("allow_trailing_comma")); + sym_allow_comments = ID2SYM(rb_intern("allow_comments")); sym_allow_control_characters = ID2SYM(rb_intern("allow_control_characters")); sym_allow_invalid_escape = ID2SYM(rb_intern("allow_invalid_escape")); sym_symbolize_names = ID2SYM(rb_intern("symbolize_names")); diff --git a/test/json/json_parser_test.rb b/test/json/json_parser_test.rb index 292ca1a6701147..943d932851bdd3 100644 --- a/test/json/json_parser_test.rb +++ b/test/json/json_parser_test.rb @@ -489,7 +489,7 @@ def test_parse_comments JSON assert_equal( { "key1" => "value1", "key2" => "value2", "key3" => "value3" }, - parse(json)) + parse(json, allow_comments: true)) json = <<~JSON { "key1":"value1" /* multi line @@ -498,7 +498,7 @@ def test_parse_comments * comment */ } JSON - assert_raise(ParserError) { parse(json) } + assert_raise(ParserError) { parse(json, allow_comments: true) } json = <<~JSON { "key1":"value1" /* multi line @@ -506,7 +506,7 @@ def test_parse_comments /* legal nested multi line comment start sequence */ } JSON - assert_equal({ "key1" => "value1" }, parse(json)) + assert_equal({ "key1" => "value1" }, parse(json, allow_comments: true)) json = <<~JSON { "key1":"value1" /* multi line @@ -515,18 +515,28 @@ def test_parse_comments and again, throw an Error */ } JSON - assert_raise(ParserError) { parse(json) } + assert_raise(ParserError) { parse(json, allow_comments: true) } json = <<~JSON { "key1":"value1" /*/*/ } JSON - assert_equal({ "key1" => "value1" }, parse(json)) - assert_equal({}, parse('{} /**/')) - assert_raise(ParserError) { parse('{} /* comment not closed') } - assert_raise(ParserError) { parse('{} /*/') } - assert_raise(ParserError) { parse('{} /x wrong comment') } - assert_raise(ParserError) { parse('{} /') } + assert_equal({ "key1" => "value1" }, parse(json, allow_comments: true)) + assert_equal({}, parse('{} /**/', allow_comments: true)) + assert_raise(ParserError) { parse('{} /* comment not closed', allow_comments: true) } + assert_raise(ParserError) { parse('{} /*/', allow_comments: true) } + assert_raise(ParserError) { parse('{} /x wrong comment', allow_comments: true) } + assert_raise(ParserError) { parse('{} /', allow_comments: true) } + end + + def test_parse_comments_deprecation + assert_equal({}, parse('/**/ {}', allow_comments: true)) + assert_raise(ParserError) { parse('/**/ {}', allow_comments: false) } + if RUBY_ENGINE == 'ruby' + assert_deprecated_warning(/Encountered comment in JSON/) do + parse('/**/ {}') + end + end end def test_nesting From ccca0be7628b23a0dada5fa6346df541ae20966c Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Sun, 7 Jun 2026 20:43:42 +0200 Subject: [PATCH 174/188] [ruby/json] parser.c: Precompute JSON::ParserError and ivar IDs https://github.com/ruby/json/commit/8972c6da6e --- ext/json/parser/parser.c | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/ext/json/parser/parser.c b/ext/json/parser/parser.c index dc76ca2cda5ea6..5559561e26004f 100644 --- a/ext/json/parser/parser.c +++ b/ext/json/parser/parser.c @@ -2,10 +2,10 @@ #include "../vendor/ryu.h" #include "../simd/simd.h" -static VALUE mJSON, eNestingError, Encoding_UTF_8; +static VALUE mJSON, eNestingError, eParserError, Encoding_UTF_8; static VALUE CNaN, CInfinity, CMinusInfinity; -static ID i_new, i_try_convert, i_uminus, i_encode; +static ID i_new, i_try_convert, i_uminus, i_encode, i_at_line, i_at_column; static VALUE sym_max_nesting, sym_allow_nan, sym_allow_trailing_comma, sym_allow_comments, sym_allow_control_characters, sym_allow_invalid_escape, sym_symbolize_names, @@ -645,9 +645,9 @@ static VALUE build_parse_error_message(const char *format, JSON_ParserState *sta static VALUE parse_error_new(VALUE message, long line, long column) { - VALUE exc = rb_exc_new_str(rb_path2class("JSON::ParserError"), message); - rb_ivar_set(exc, rb_intern("@line"), LONG2NUM(line)); - rb_ivar_set(exc, rb_intern("@column"), LONG2NUM(column)); + VALUE exc = rb_exc_new_str(eParserError, message); + rb_ivar_set(exc, i_at_line, LONG2NUM(line)); + rb_ivar_set(exc, i_at_column, LONG2NUM(column)); return exc; } @@ -2069,8 +2069,13 @@ void Init_parser(void) mJSON = rb_define_module("JSON"); VALUE mExt = rb_define_module_under(mJSON, "Ext"); VALUE cParserConfig = rb_define_class_under(mExt, "ParserConfig", rb_cObject); + + rb_global_variable(&eParserError); + eParserError = rb_path2class("JSON::ParserError"); + + rb_global_variable(&eNestingError); eNestingError = rb_path2class("JSON::NestingError"); - rb_gc_register_mark_object(eNestingError); + rb_define_alloc_func(cParserConfig, cJSON_parser_s_allocate); rb_define_method(cParserConfig, "initialize", cParserConfig_initialize, 1); rb_define_method(cParserConfig, "parse", cParserConfig_parse, 1); @@ -2078,14 +2083,14 @@ void Init_parser(void) VALUE cParser = rb_define_class_under(mExt, "Parser", rb_cObject); rb_define_singleton_method(cParser, "parse", cParser_m_parse, 2); + rb_global_variable(&CNaN); CNaN = rb_const_get(mJSON, rb_intern("NaN")); - rb_gc_register_mark_object(CNaN); + rb_global_variable(&CInfinity); CInfinity = rb_const_get(mJSON, rb_intern("Infinity")); - rb_gc_register_mark_object(CInfinity); + rb_global_variable(&CMinusInfinity); CMinusInfinity = rb_const_get(mJSON, rb_intern("MinusInfinity")); - rb_gc_register_mark_object(CMinusInfinity); rb_global_variable(&Encoding_UTF_8); Encoding_UTF_8 = rb_const_get(rb_path2class("Encoding"), rb_intern("UTF_8")); @@ -2106,6 +2111,8 @@ void Init_parser(void) i_try_convert = rb_intern("try_convert"); i_uminus = rb_intern("-@"); i_encode = rb_intern("encode"); + i_at_line = rb_intern("@line"); + i_at_column = rb_intern("@column"); binary_encindex = rb_ascii8bit_encindex(); utf8_encindex = rb_utf8_encindex(); From 6be7b1f9c7d1fa03641ea2f7ca3a45547428d536 Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Sun, 7 Jun 2026 20:03:50 +0900 Subject: [PATCH 175/188] [DOC] Improve docs for ObjectSpace.each_object --- gc.c | 40 +++++++++++++++++++++++----------------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/gc.c b/gc.c index 0219fa6e78cbee..72c3f2d24720b6 100644 --- a/gc.c +++ b/gc.c @@ -1806,18 +1806,21 @@ os_obj_of(VALUE of) /* * call-seq: - * ObjectSpace.each_object([module]) {|obj| ... } -> integer - * ObjectSpace.each_object([module]) -> an_enumerator + * ObjectSpace.each_object {|obj| ... } -> integer + * ObjectSpace.each_object(module) {|obj| ... } -> integer + * ObjectSpace.each_object -> enumerator + * ObjectSpace.each_object(module) -> enumerator * - * Calls the block once for each living, nonimmediate object in this - * Ruby process. If module is specified, calls the block - * for only those classes or modules that match (or are a subclass of) - * module. Returns the number of objects found. Immediate - * objects (such as Fixnums, static Symbols - * true, false and nil) are - * never returned. + * Calls the block once for each living, non-immediate object in this Ruby + * process, and returns the number of objects found. * - * If no block is given, an enumerator is returned instead. + * If +module+ is given, calls the block only for objects that are an instance + * of +module+ or one of its subclasses. + * + * Immediate objects (such as small integers, static symbols, +true+, +false+, + * and +nil+) are never yielded. + * + * With no block given, returns a new Enumerator. * * Job = Class.new * jobs = [Job.new, Job.new] @@ -1828,18 +1831,21 @@ os_obj_of(VALUE of) * * # * # - * Total count: 2 + * Total count: 2 + * + * Because every live object is visited, this method is mainly useful for + * debugging, profiling, and introspecting a running process. * * Due to a current Ractor implementation issue, this method does not yield - * Ractor-unshareable objects when the process is in multi-Ractor mode. Multi-ractor - * mode is enabled when Ractor.new has been called for the first time. - * See https://bugs.ruby-lang.org/issues/19387 for more information. + * Ractor-unshareable objects when the process is in multi-Ractor mode. + * Multi-Ractor mode is enabled when Ractor.new has been called for the first + * time. See https://bugs.ruby-lang.org/issues/19387 for more information. * * a = 12345678987654321 # shareable - * b = [].freeze # shareable - * c = {} # not shareable + * b = [].freeze # shareable + * c = {} # not shareable * ObjectSpace.each_object {|x| x } # yields a, b, and c - * Ractor.new {} # enter multi-Ractor mode + * Ractor.new {} # enter multi-Ractor mode * ObjectSpace.each_object {|x| x } # does not yield c * */ From 86cf1e8c06258d2207572d67eb3b020f36ae3cab Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Mon, 8 Jun 2026 12:23:44 +0900 Subject: [PATCH 176/188] [ruby/json] Suppres the warning for comment in JSON Suppres it for now because JRuby version does not support it yet. https://github.com/ruby/json/commit/8144d4cb34 --- test/json/json_parser_test.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/json/json_parser_test.rb b/test/json/json_parser_test.rb index 943d932851bdd3..a4292871aed878 100644 --- a/test/json/json_parser_test.rb +++ b/test/json/json_parser_test.rb @@ -219,7 +219,9 @@ def test_parse_arrays def test_parse_json_primitive_values assert_raise(JSON::ParserError) { parse('') } assert_raise(TypeError) { parse(nil) } - assert_raise(JSON::ParserError) { parse(' /* foo */ ') } + EnvUtil.suppress_warning do # still no warning in JRuby verions + assert_raise(JSON::ParserError) { parse(' /* foo */ ') } + end assert_equal nil, parse('null') assert_equal false, parse('false') assert_equal true, parse('true') From 91023dbc74479c2f92f4f1bff21900f31e30e02c Mon Sep 17 00:00:00 2001 From: Bryan Woods Date: Fri, 5 Jun 2026 13:54:48 -0400 Subject: [PATCH 177/188] [ruby/rubygems] Preserve per-source cooldown when converging sources from the lockfile https://github.com/ruby/rubygems/commit/66dd16f025 --- lib/bundler/source/rubygems.rb | 2 +- lib/bundler/source_list.rb | 4 ++ spec/bundler/install/cooldown_spec.rb | 58 +++++++++++++++++++++++++++ 3 files changed, 63 insertions(+), 1 deletion(-) diff --git a/lib/bundler/source/rubygems.rb b/lib/bundler/source/rubygems.rb index ed864604fe1649..9109f399a7d71b 100644 --- a/lib/bundler/source/rubygems.rb +++ b/lib/bundler/source/rubygems.rb @@ -11,7 +11,7 @@ class Rubygems < Source API_REQUEST_SIZE = 100 REQUIRE_MUTEX = Mutex.new - attr_accessor :remotes + attr_accessor :remotes, :remote_cooldowns def initialize(options = {}) @options = options diff --git a/lib/bundler/source_list.rb b/lib/bundler/source_list.rb index ab7002d6e5b3d7..954efbb65fc14d 100644 --- a/lib/bundler/source_list.rb +++ b/lib/bundler/source_list.rb @@ -169,6 +169,10 @@ def replace_rubygems_source(replacement_sources, gemfile_source) # locked sources never include credentials so always prefer remotes from the gemfile replacement_source.remotes = gemfile_source.remotes + # cooldowns are only ever declared in the Gemfile, so carry them over + # along with the remotes they apply to + replacement_source.remote_cooldowns = gemfile_source.remote_cooldowns + yield replacement_source if block_given? replacement_source diff --git a/spec/bundler/install/cooldown_spec.rb b/spec/bundler/install/cooldown_spec.rb index bad7b7cf3472d7..5cdfe72284a764 100644 --- a/spec/bundler/install/cooldown_spec.rb +++ b/spec/bundler/install/cooldown_spec.rb @@ -143,6 +143,64 @@ expect(the_bundle).to include_gems("ripe_gem 1.0.0") end + it "applies per-source Gemfile cooldown on bundle update when a lockfile exists" do + # Converging the Gemfile sources with the lockfile sources used to drop + # the per-source cooldown, so it only ever worked on a first resolve + # without a lockfile. + gemfile <<-G + source "https://gem.repo3", cooldown: 7 + gem "ripe_gem" + G + + lockfile <<-L + GEM + remote: https://gem.repo3/ + specs: + ripe_gem (1.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + ripe_gem + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "update ripe_gem", artifice: "compact_index_cooldown" + + expect(the_bundle).to include_gems("ripe_gem 1.0.0") + end + + it "applies per-source Gemfile cooldown to gems added after the lockfile was written" do + gemfile <<-G + source "https://gem.repo3", cooldown: 7 + gem "ripe_gem" + gem "child" + G + + lockfile <<-L + GEM + remote: https://gem.repo3/ + specs: + ripe_gem (1.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + ripe_gem + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "install", artifice: "compact_index_cooldown" + + expect(the_bundle).to include_gems("ripe_gem 1.0.0", "child 1.0.0") + end + it "is overridden by CLI --cooldown when Gemfile sets a different per-source value" do gemfile <<-G source "https://gem.repo3", cooldown: 0 From 2c8002d58302e4fff51484826e1fd706cc2bfb19 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Mon, 8 Jun 2026 10:55:20 +0900 Subject: [PATCH 178/188] IO::Buffer: Validate the buffer after argument conversion --- io_buffer.c | 12 +++++++++++- test/ruby/test_io_buffer.rb | 21 +++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/io_buffer.c b/io_buffer.c index d9f50fc234c016..fa6c8c5d5ac38e 100644 --- a/io_buffer.c +++ b/io_buffer.c @@ -930,11 +930,17 @@ rb_io_buffer_get_bytes_for_writing(VALUE self, void **base, size_t *size) } static void -io_buffer_get_bytes_for_reading(struct rb_io_buffer *buffer, const void **base, size_t *size) +io_buffer_validate_for_reading(struct rb_io_buffer *buffer) { if (!io_buffer_validate(buffer)) { rb_raise(rb_eIOBufferInvalidatedError, "Buffer has been invalidated!"); } +} + +static void +io_buffer_get_bytes_for_reading(struct rb_io_buffer *buffer, const void **base, size_t *size) +{ + io_buffer_validate_for_reading(buffer); if (buffer->base) { *base = buffer->base; @@ -1548,6 +1554,8 @@ size_sum_is_bigger_than(size_t a, size_t b, size_t x) static inline void io_buffer_validate_range(struct rb_io_buffer *buffer, size_t offset, size_t length) { + io_buffer_validate_for_reading(buffer); + if (size_sum_is_bigger_than(offset, length, buffer->size)) { rb_raise(rb_eArgError, "Specified offset+length is bigger than the buffer size!"); } @@ -1752,6 +1760,8 @@ rb_io_buffer_resize(VALUE self, size_t size) struct rb_io_buffer *buffer = NULL; TypedData_Get_Struct(self, struct rb_io_buffer, &rb_io_buffer_type, buffer); + io_buffer_validate_for_reading(buffer); + if (buffer->flags & RB_IO_BUFFER_LOCKED) { rb_raise(rb_eIOBufferLockedError, "Cannot resize locked buffer!"); } diff --git a/test/ruby/test_io_buffer.rb b/test/ruby/test_io_buffer.rb index 327a3ece9c3398..bed80e64dc5d2c 100644 --- a/test/ruby/test_io_buffer.rb +++ b/test/ruby/test_io_buffer.rb @@ -242,6 +242,16 @@ def test_resize_zero_external end end + def test_resize_invalidated_slice + inner = IO::Buffer.new(IO::Buffer::PAGE_SIZE) + slice = inner.slice(0, 8) + inner.free + + assert_raise(IO::Buffer::InvalidatedError) do + slice.resize(16) + end + end + def test_compare_same_size buffer1 = IO::Buffer.new(1) assert_equal buffer1, buffer1 @@ -376,6 +386,17 @@ def test_get_string assert_raise_with_message(ArgumentError, /Offset can't be negative/) do buffer.get_string(-1) end + + encoding = Struct.new(:buffer) do + def to_str + buffer.free + "BINARY" + end + end.new(buffer.dup) + slice = encoding.buffer.slice(0, 8) + assert_raise(IO::Buffer::InvalidatedError) do + slice.get_string(0, 8, encoding) + end end def test_zero_length_get_string From 773e0c3a0f2ab2bd235c8d44cad1f999bfe2514b Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Mon, 8 Jun 2026 11:41:48 +0900 Subject: [PATCH 179/188] IO::Buffer: Validate the buffer after type argument conversion --- io_buffer.c | 113 ++++++++++++++++++++++++++++-------- test/ruby/test_io_buffer.rb | 20 +++++++ 2 files changed, 108 insertions(+), 25 deletions(-) diff --git a/io_buffer.c b/io_buffer.c index fa6c8c5d5ac38e..16407159b5c50f 100644 --- a/io_buffer.c +++ b/io_buffer.c @@ -899,8 +899,8 @@ rb_io_buffer_get_bytes(VALUE self, void **base, size_t *size) } // Internal function for accessing bytes for writing, wil -static inline void -io_buffer_get_bytes_for_writing(struct rb_io_buffer *buffer, void **base, size_t *size) +static void +io_buffer_validate_for_writing(struct rb_io_buffer *buffer) { if (buffer->flags & RB_IO_BUFFER_READONLY || (!NIL_P(buffer->source) && OBJ_FROZEN(buffer->source))) { @@ -910,6 +910,21 @@ io_buffer_get_bytes_for_writing(struct rb_io_buffer *buffer, void **base, size_t if (!io_buffer_validate(buffer)) { rb_raise(rb_eIOBufferInvalidatedError, "Buffer is invalid!"); } +} + +static struct rb_io_buffer * +get_io_buffer_for_writing(VALUE self) +{ + struct rb_io_buffer *buffer = NULL; + TypedData_Get_Struct(self, struct rb_io_buffer, &rb_io_buffer_type, buffer); + io_buffer_validate_for_writing(buffer); + return buffer; +} + +static inline void +io_buffer_get_bytes_for_writing(struct rb_io_buffer *buffer, void **base, size_t *size) +{ + io_buffer_validate_for_writing(buffer); if (buffer->base) { *base = buffer->base; @@ -1379,14 +1394,23 @@ io_buffer_readonly_p(VALUE self) return RBOOL(rb_io_buffer_readonly_p(self)); } -static void -io_buffer_lock(struct rb_io_buffer *buffer) +static int +io_buffer_try_lock(struct rb_io_buffer *buffer) { if (buffer->flags & RB_IO_BUFFER_LOCKED) { - rb_raise(rb_eIOBufferLockedError, "Buffer already locked!"); + return 0; } buffer->flags |= RB_IO_BUFFER_LOCKED; + return 1; +} + +static void +io_buffer_lock(struct rb_io_buffer *buffer) +{ + if (!io_buffer_try_lock(buffer)) { + rb_raise(rb_eIOBufferLockedError, "Buffer already locked!"); + } } VALUE @@ -1964,6 +1988,10 @@ ruby_swap128_int(rb_int128_t x) return conversion.int128; } +#define IO_BUFFER_VALIDATE_TYPE_FOR_WRITING(buffer, base, size, offset, type) \ + (io_buffer_get_bytes_for_writing(buffer, &(base), &(size)), \ + io_buffer_validate_type(size, offset, sizeof(type))) + #define IO_BUFFER_DECLARE_TYPE(name, type, endian, wrap, unwrap, swap) \ static ID RB_IO_BUFFER_DATA_TYPE_##name; \ \ @@ -1979,10 +2007,12 @@ io_buffer_read_##name(const void* base, size_t size, size_t *offset) \ } \ \ static void \ -io_buffer_write_##name(const void* base, size_t size, size_t *offset, VALUE _value) \ +io_buffer_write_##name(struct rb_io_buffer* buffer, size_t *offset, VALUE _value) \ { \ - io_buffer_validate_type(size, *offset, sizeof(type)); \ + void* base; size_t size; \ + IO_BUFFER_VALIDATE_TYPE_FOR_WRITING(buffer, base, size, *offset, type); \ type value = unwrap(_value); \ + IO_BUFFER_VALIDATE_TYPE_FOR_WRITING(buffer, base, size, *offset, type); \ if (endian != RB_IO_BUFFER_HOST_ENDIAN) value = swap(value); \ memcpy((char*)base + *offset, &value, sizeof(type)); \ *offset += sizeof(type); \ @@ -2357,9 +2387,10 @@ io_buffer_each_byte(int argc, VALUE *argv, VALUE self) } static inline void -rb_io_buffer_set_value(const void* base, size_t size, ID buffer_type, size_t *offset, VALUE value) +rb_io_buffer_set_value(struct rb_io_buffer *buffer, VALUE buffer_type, size_t *offset, VALUE value) { -#define IO_BUFFER_SET_VALUE(name) if (buffer_type == RB_IO_BUFFER_DATA_TYPE_##name) {io_buffer_write_##name(base, size, offset, value); return;} + ID type = RB_SYM2ID(buffer_type); +#define IO_BUFFER_SET_VALUE(name) if (type == RB_IO_BUFFER_DATA_TYPE_##name) {io_buffer_write_##name(buffer, offset, value); return;} IO_BUFFER_SET_VALUE(U8); IO_BUFFER_SET_VALUE(S8); @@ -2392,6 +2423,21 @@ rb_io_buffer_set_value(const void* base, size_t size, ID buffer_type, size_t *of rb_raise(rb_eArgError, "Invalid type name!"); } +struct io_buffer_set_value_arguments { + struct rb_io_buffer *buffer; + size_t offset; + VALUE type, value; +}; + +static VALUE +io_buffer_set_value_try(VALUE arguments) +{ + struct io_buffer_set_value_arguments *args = (void *)arguments; + size_t offset = args->offset; + rb_io_buffer_set_value(args->buffer, args->type, &offset, args->value); + return SIZET2NUM(offset); +} + /* * call-seq: set_value(type, offset, value) -> offset * @@ -2425,13 +2471,30 @@ rb_io_buffer_set_value(const void* base, size_t size, ID buffer_type, size_t *of static VALUE io_buffer_set_value(VALUE self, VALUE type, VALUE _offset, VALUE value) { - void *base; - size_t size; - size_t offset = io_buffer_extract_offset(_offset); + struct io_buffer_set_value_arguments arguments = { + .buffer = get_io_buffer_for_writing(self), + .offset = io_buffer_extract_offset(_offset), + .type = type, + .value = value, + }; + + if (!io_buffer_try_lock(arguments.buffer)) { + return io_buffer_set_value_try((VALUE)&arguments); + } + return rb_ensure(io_buffer_set_value_try, (VALUE)&arguments, rb_io_buffer_locked_ensure, self); +} - rb_io_buffer_get_bytes_for_writing(self, &base, &size); +static VALUE +io_buffer_set_values_try(VALUE arguments) +{ + struct io_buffer_set_value_arguments *args = (void *)arguments; + size_t offset = args->offset; - rb_io_buffer_set_value(base, size, RB_SYM2ID(type), &offset, value); + for (long i = 0; i < RARRAY_LEN(args->type); i++) { + VALUE type = rb_ary_entry(args->type, i); + VALUE value = rb_ary_entry(args->value, i); + rb_io_buffer_set_value(args->buffer, type, &offset, value); + } return SIZET2NUM(offset); } @@ -2453,31 +2516,31 @@ io_buffer_set_value(VALUE self, VALUE type, VALUE _offset, VALUE value) static VALUE io_buffer_set_values(VALUE self, VALUE buffer_types, VALUE _offset, VALUE values) { + struct io_buffer_set_value_arguments arguments = { + .buffer = get_io_buffer_for_writing(self), + }; + if (!RB_TYPE_P(buffer_types, T_ARRAY)) { rb_raise(rb_eArgError, "Argument buffer_types should be an array!"); } + arguments.type = buffer_types; + + arguments.offset = io_buffer_extract_offset(_offset); if (!RB_TYPE_P(values, T_ARRAY)) { rb_raise(rb_eArgError, "Argument values should be an array!"); } + arguments.value = values; if (RARRAY_LEN(buffer_types) != RARRAY_LEN(values)) { rb_raise(rb_eArgError, "Argument buffer_types and values should have the same length!"); } - size_t offset = io_buffer_extract_offset(_offset); - - void *base; - size_t size; - rb_io_buffer_get_bytes_for_writing(self, &base, &size); - - for (long i = 0; i < RARRAY_LEN(buffer_types); i++) { - VALUE type = rb_ary_entry(buffer_types, i); - VALUE value = rb_ary_entry(values, i); - rb_io_buffer_set_value(base, size, RB_SYM2ID(type), &offset, value); + if (!io_buffer_try_lock(arguments.buffer)) { + return io_buffer_set_values_try((VALUE)&arguments); } + return rb_ensure(io_buffer_set_values_try, (VALUE)&arguments, rb_io_buffer_locked_ensure, self); - return SIZET2NUM(offset); } static size_t IO_BUFFER_BLOCKING_SIZE = 1024*1024; diff --git a/test/ruby/test_io_buffer.rb b/test/ruby/test_io_buffer.rb index bed80e64dc5d2c..fdf99589ef8825 100644 --- a/test/ruby/test_io_buffer.rb +++ b/test/ruby/test_io_buffer.rb @@ -470,6 +470,26 @@ def test_get_set_values end end + def test_set_values_invalidated_slice + to_int = Struct.new(:buffer) do + def to_int + buffer.free + 0x41 + end + end + buffer = IO::Buffer.new(128) + slice = buffer.slice(0, 8) + value = to_int.new(buffer) + assert_raise(IO::Buffer::InvalidatedError) {slice.set_value(:U8, 0, value)} + + buffer = IO::Buffer.new(128) + slice = buffer.slice(0, 8) + value = to_int.new(buffer) + assert_raise(IO::Buffer::InvalidatedError) { + slice.set_values([:U8, :U8], 0, [0, value]) + } + end + def test_zero_length_get_set_values buffer = IO::Buffer.new(0) From 09e76c6cb715bd476188bab26dd6be471bb2649c Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Mon, 8 Jun 2026 13:19:33 +0900 Subject: [PATCH 180/188] IO::Buffer: Avoid inadvertent ID creation --- io_buffer.c | 23 +++++++--- .../-ext-/symbol/test_inadvertent_creation.rb | 43 +++++++++++++++++++ 2 files changed, 59 insertions(+), 7 deletions(-) diff --git a/io_buffer.c b/io_buffer.c index 16407159b5c50f..dc8f547a322096 100644 --- a/io_buffer.c +++ b/io_buffer.c @@ -2082,6 +2082,15 @@ io_buffer_buffer_type_size(ID buffer_type) rb_raise(rb_eArgError, "Invalid type name!"); } +static inline ID +io_buffer_type_id(VALUE name) +{ + Check_Type(name, T_SYMBOL); + if (!STATIC_SYM_P(name)) return 0; + return rb_sym2id(name); +} +#define TYPE_ID(name) io_buffer_type_id(name) + /* * call-seq: * size_of(buffer_type) -> byte size @@ -2098,12 +2107,12 @@ io_buffer_size_of(VALUE klass, VALUE buffer_type) if (RB_TYPE_P(buffer_type, T_ARRAY)) { size_t total = 0; for (long i = 0; i < RARRAY_LEN(buffer_type); i++) { - total += io_buffer_buffer_type_size(RB_SYM2ID(RARRAY_AREF(buffer_type, i))); + total += io_buffer_buffer_type_size(TYPE_ID(RARRAY_AREF(buffer_type, i))); } return SIZET2NUM(total); } else { - return SIZET2NUM(io_buffer_buffer_type_size(RB_SYM2ID(buffer_type))); + return SIZET2NUM(io_buffer_buffer_type_size(TYPE_ID(buffer_type))); } } @@ -2190,7 +2199,7 @@ io_buffer_get_value(VALUE self, VALUE type, VALUE _offset) rb_io_buffer_get_bytes_for_reading(self, &base, &size); - return rb_io_buffer_get_value(base, size, RB_SYM2ID(type), &offset); + return rb_io_buffer_get_value(base, size, TYPE_ID(type), &offset); } /* @@ -2220,7 +2229,7 @@ io_buffer_get_values(VALUE self, VALUE buffer_types, VALUE _offset) for (long i = 0; i < RARRAY_LEN(buffer_types); i++) { VALUE type = rb_ary_entry(buffer_types, i); - VALUE value = rb_io_buffer_get_value(base, size, RB_SYM2ID(type), &offset); + VALUE value = rb_io_buffer_get_value(base, size, TYPE_ID(type), &offset); rb_ary_push(array, value); } @@ -2289,7 +2298,7 @@ io_buffer_each(int argc, VALUE *argv, VALUE self) ID buffer_type; if (argc >= 1) { - buffer_type = RB_SYM2ID(argv[0]); + buffer_type = TYPE_ID(argv[0]); } else { buffer_type = RB_IO_BUFFER_DATA_TYPE_U8; @@ -2327,7 +2336,7 @@ io_buffer_values(int argc, VALUE *argv, VALUE self) ID buffer_type; if (argc >= 1) { - buffer_type = RB_SYM2ID(argv[0]); + buffer_type = TYPE_ID(argv[0]); } else { buffer_type = RB_IO_BUFFER_DATA_TYPE_U8; @@ -2389,7 +2398,7 @@ io_buffer_each_byte(int argc, VALUE *argv, VALUE self) static inline void rb_io_buffer_set_value(struct rb_io_buffer *buffer, VALUE buffer_type, size_t *offset, VALUE value) { - ID type = RB_SYM2ID(buffer_type); + ID type = TYPE_ID(buffer_type); #define IO_BUFFER_SET_VALUE(name) if (type == RB_IO_BUFFER_DATA_TYPE_##name) {io_buffer_write_##name(buffer, offset, value); return;} IO_BUFFER_SET_VALUE(U8); IO_BUFFER_SET_VALUE(S8); diff --git a/test/-ext-/symbol/test_inadvertent_creation.rb b/test/-ext-/symbol/test_inadvertent_creation.rb index 995e01ee157cad..44705e10c76598 100644 --- a/test/-ext-/symbol/test_inadvertent_creation.rb +++ b/test/-ext-/symbol/test_inadvertent_creation.rb @@ -489,5 +489,48 @@ def test_iv_get Bug::Symbol.iv_get(obj, name) end end + + def assert_io_buffer_no_immortal_symbol_created(buffer = IO::Buffer.new(128)) + assert_no_immortal_symbol_created("io_buffer") do |name| + yield buffer, name.to_sym + end + end + + def test_io_buffer_size_of_inadvertent_id_creation + assert_io_buffer_no_immortal_symbol_created(nil) do |_, name| + assert_raise(ArgumentError) {IO::Buffer.size_of(name)} + assert_raise(ArgumentError) {IO::Buffer.size_of([name])} + end + end + + def test_io_buffer_each_inadvertent_id_creation + assert_io_buffer_no_immortal_symbol_created do |buffer, name| + assert_raise(ArgumentError) {buffer.each(name, 0, 1) {}} + end + end + + def test_io_buffer_get_value_inadvertent_id_creation + assert_io_buffer_no_immortal_symbol_created do |buffer, name| + assert_raise(ArgumentError) {buffer.get_value(name, 0)} + end + end + + def test_io_buffer_get_values_inadvertent_id_creation + assert_io_buffer_no_immortal_symbol_created do |buffer, name| + assert_raise(ArgumentError) {buffer.get_values([name], 0)} + end + end + + def test_io_buffer_set_value_inadvertent_id_creation + assert_io_buffer_no_immortal_symbol_created do |buffer, name| + assert_raise(ArgumentError) {buffer.set_value(name, 0, 0)} + end + end + + def test_io_buffer_set_values_inadvertent_id_creation + assert_io_buffer_no_immortal_symbol_created do |buffer, name| + assert_raise(ArgumentError) {buffer.set_values([name], 0, [0])} + end + end end end From ca3d14672114cc5356f41378422000195f212cac Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Mon, 8 Jun 2026 16:49:23 +0900 Subject: [PATCH 181/188] [ruby/rubygems] Pin that cooldown is inactive without publish dates The legacy dependency API exposes no per-version publish dates, so the cooldown filter has nothing to compare against and silently does nothing. Document that limitation as a regression guard rather than a surprise. https://github.com/ruby/rubygems/commit/ce952c1b9f Co-Authored-By: Claude Opus 4.8 --- spec/bundler/install/cooldown_spec.rb | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/spec/bundler/install/cooldown_spec.rb b/spec/bundler/install/cooldown_spec.rb index 5cdfe72284a764..bf525288169649 100644 --- a/spec/bundler/install/cooldown_spec.rb +++ b/spec/bundler/install/cooldown_spec.rb @@ -488,4 +488,28 @@ expect(the_bundle).to include_gems("upgradable 3.0.0") end end + + context "with a source that does not provide publish dates" do + before do + build_repo3 do + build_gem "ripe_gem", "1.0.0" + build_gem "ripe_gem", "2.0.0" + end + end + + it "cannot apply cooldown and installs the latest version" do + # The legacy dependency API does not expose per-version publish dates, so + # the cooldown filter has nothing to compare against and is silently + # inactive. This pins that limitation; flip the expectation if publish + # dates ever become available over this endpoint. + gemfile <<-G + source "https://gem.repo3", cooldown: 7 + gem "ripe_gem" + G + + bundle "install", artifice: "endpoint" + + expect(the_bundle).to include_gems("ripe_gem 2.0.0") + end + end end From c428294f1d199af5e97411dca71909cfbefc8ec1 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Mon, 8 Jun 2026 17:25:25 +0900 Subject: [PATCH 182/188] [ruby/rubygems] Cover per-source cooldown across multiple sources A single converge can hold several rubygems sources, each keyed by its own remotes. A partial update re-converges the still-locked sources, the path that used to drop cooldown, so lock in that the cooldown stays attached to the source that declared it for both a top-level source and a gem-block source. https://github.com/ruby/rubygems/commit/2fcb2d70cc Co-Authored-By: Claude Opus 4.8 --- spec/bundler/install/cooldown_spec.rb | 96 +++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/spec/bundler/install/cooldown_spec.rb b/spec/bundler/install/cooldown_spec.rb index bf525288169649..547048ef3400df 100644 --- a/spec/bundler/install/cooldown_spec.rb +++ b/spec/bundler/install/cooldown_spec.rb @@ -487,6 +487,102 @@ expect(the_bundle).to include_gems("upgradable 3.0.0") end + + it "keeps a top-level source cooldown through a partial update with multiple sources" do + build_repo4 do + build_gem "solo_gem", "1.0.0" do |s| + s.date = Time.now.utc - (30 * 86_400) + end + build_gem "solo_gem", "2.0.0" do |s| + s.date = Time.now.utc - (1 * 86_400) + end + end + + gemfile <<-G + source "https://gem.repo3", cooldown: 7 + gem "ripe_gem" + source "https://gem.repo4" do + gem "solo_gem" + end + G + + lockfile <<-L + GEM + remote: https://gem.repo3/ + specs: + ripe_gem (1.0.0) + + GEM + remote: https://gem.repo4/ + specs: + solo_gem (1.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + ripe_gem + solo_gem! + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "update ripe_gem", artifice: "compact_index_cooldown" + + # A partial update converges the still-locked sources, the path that used + # to drop cooldown. repo3's cooldown must survive that even with a second + # source in the Gemfile, so its in-window 2.0.0 stays excluded. + expect(the_bundle).to include_gems("ripe_gem 1.0.0", "solo_gem 1.0.0") + end + + it "carries cooldown declared on a gem-block source" do + build_repo4 do + build_gem "solo_gem", "1.0.0" do |s| + s.date = Time.now.utc - (30 * 86_400) + end + build_gem "solo_gem", "2.0.0" do |s| + s.date = Time.now.utc - (1 * 86_400) + end + end + + gemfile <<-G + source "https://gem.repo3" + gem "ripe_gem" + source "https://gem.repo4", cooldown: 7 do + gem "solo_gem" + end + G + + lockfile <<-L + GEM + remote: https://gem.repo3/ + specs: + ripe_gem (1.0.0) + + GEM + remote: https://gem.repo4/ + specs: + solo_gem (1.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + ripe_gem + solo_gem! + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "update solo_gem", artifice: "compact_index_cooldown" + + # The cooldown lives on the gem-block source, which is also converged from + # the lockfile. A partial update of solo_gem must keep that cooldown, so + # its in-window 2.0.0 stays excluded. + expect(the_bundle).to include_gems("ripe_gem 1.0.0", "solo_gem 1.0.0") + end end context "with a source that does not provide publish dates" do From 7c9aeb90c9fab9c21b995ea3f770184f7dd956a0 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Mon, 8 Jun 2026 17:25:38 +0900 Subject: [PATCH 183/188] [ruby/rubygems] Cover per-source cooldown through bundle add bundle add re-resolves the Gemfile against the existing lockfile via the injector, which converges sources the same way install and update do. A gem added there must inherit the source's cooldown instead of grabbing an in-window release. https://github.com/ruby/rubygems/commit/cfed95d1dd Co-Authored-By: Claude Opus 4.8 --- spec/bundler/install/cooldown_spec.rb | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/spec/bundler/install/cooldown_spec.rb b/spec/bundler/install/cooldown_spec.rb index 547048ef3400df..480e3997975dab 100644 --- a/spec/bundler/install/cooldown_spec.rb +++ b/spec/bundler/install/cooldown_spec.rb @@ -583,6 +583,33 @@ # its in-window 2.0.0 stays excluded. expect(the_bundle).to include_gems("ripe_gem 1.0.0", "solo_gem 1.0.0") end + + it "applies per-source Gemfile cooldown to a gem added via bundle add" do + gemfile <<-G + source "https://gem.repo3", cooldown: 7 + gem "ripe_gem" + G + + lockfile <<-L + GEM + remote: https://gem.repo3/ + specs: + ripe_gem (1.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + ripe_gem + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "add child", artifice: "compact_index_cooldown" + + expect(the_bundle).to include_gems("child 1.0.0") + end end context "with a source that does not provide publish dates" do From 55554bd73a7735745ad1508c5e8bb0c32fcc866b Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Mon, 8 Jun 2026 17:26:37 +0900 Subject: [PATCH 184/188] [ruby/rubygems] Cover per-source cooldown on bundle lock --update This is the exact path from the original report: regenerating the lockfile must not advance a gem into the cooldown window. Assert the written lockfile to guard the lock-only flow that install and update specs do not touch. https://github.com/ruby/rubygems/commit/293d7c7dc3 Co-Authored-By: Claude Opus 4.8 --- spec/bundler/install/cooldown_spec.rb | 28 +++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/spec/bundler/install/cooldown_spec.rb b/spec/bundler/install/cooldown_spec.rb index 480e3997975dab..66c4b99231bb54 100644 --- a/spec/bundler/install/cooldown_spec.rb +++ b/spec/bundler/install/cooldown_spec.rb @@ -610,6 +610,34 @@ expect(the_bundle).to include_gems("child 1.0.0") end + + it "applies per-source Gemfile cooldown on bundle lock --update" do + gemfile <<-G + source "https://gem.repo3", cooldown: 7 + gem "ripe_gem" + G + + lockfile <<-L + GEM + remote: https://gem.repo3/ + specs: + ripe_gem (1.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + ripe_gem + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "lock --update ripe_gem", artifice: "compact_index_cooldown" + + expect(lockfile).to include("ripe_gem (1.0.0)") + expect(lockfile).not_to include("ripe_gem (2.0.0)") + end end context "with a source that does not provide publish dates" do From d9a6ca69c5e7f4b4f9604bd62add7ec1ff2b894a Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Mon, 8 Jun 2026 17:27:05 +0900 Subject: [PATCH 185/188] [ruby/rubygems] Pin that frozen installs ignore cooldown A frozen install reads the lockfile rather than resolving, so cooldown never runs. Document that a version locked inside the window still installs, so the bypass stays intentional. https://github.com/ruby/rubygems/commit/bfc099bc26 Co-Authored-By: Claude Opus 4.8 --- spec/bundler/install/cooldown_spec.rb | 30 +++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/spec/bundler/install/cooldown_spec.rb b/spec/bundler/install/cooldown_spec.rb index 66c4b99231bb54..ada48ac5d48dde 100644 --- a/spec/bundler/install/cooldown_spec.rb +++ b/spec/bundler/install/cooldown_spec.rb @@ -638,6 +638,36 @@ expect(lockfile).to include("ripe_gem (1.0.0)") expect(lockfile).not_to include("ripe_gem (2.0.0)") end + + it "ignores cooldown and installs the locked version when frozen" do + # Frozen installs read the lockfile instead of resolving, so cooldown has + # no say. A version already locked inside the window must still install. + gemfile <<-G + source "https://gem.repo3", cooldown: 7 + gem "ripe_gem" + G + + lockfile <<-L + GEM + remote: https://gem.repo3/ + specs: + ripe_gem (2.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + ripe_gem + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle "config set frozen true" + bundle "install", artifice: "compact_index_cooldown" + + expect(the_bundle).to include_gems("ripe_gem 2.0.0") + end end context "with a source that does not provide publish dates" do From fadf8f0ea07c94b16d99e369481c3f62614e9ebe Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Mon, 8 Jun 2026 17:28:24 +0900 Subject: [PATCH 186/188] [ruby/rubygems] Cover per-source cooldown behind a mirror A mirror rewrites the fetch URI while cooldown stays keyed by the URI declared in the Gemfile. Confirm the redirect to the serving mirror does not lose the cooldown. https://github.com/ruby/rubygems/commit/ea2bb164f1 Co-Authored-By: Claude Opus 4.8 --- spec/bundler/install/cooldown_spec.rb | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/spec/bundler/install/cooldown_spec.rb b/spec/bundler/install/cooldown_spec.rb index ada48ac5d48dde..e85267ff4121f1 100644 --- a/spec/bundler/install/cooldown_spec.rb +++ b/spec/bundler/install/cooldown_spec.rb @@ -668,6 +668,23 @@ expect(the_bundle).to include_gems("ripe_gem 2.0.0") end + + it "keys per-source cooldown by the declared URI even behind a mirror" do + # A mirror rewrites the fetch URI, but cooldown is recorded under the URI + # written in the Gemfile. The cooldown must still apply through the + # redirect to the mirror that actually serves the gems. + bundle "config set mirror.https://gem.repo2 https://gem.repo3" + + gemfile <<-G + source "https://gem.repo2", cooldown: 7 + gem "ripe_gem" + G + + bundle "install", artifice: "compact_index_cooldown", + env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo3.to_s } + + expect(the_bundle).to include_gems("ripe_gem 1.0.0") + end end context "with a source that does not provide publish dates" do From 7ed00ca7a9d3b28330afd4451b8f1613ccd01d83 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Mon, 8 Jun 2026 17:53:34 +0900 Subject: [PATCH 187/188] [ruby/rubygems] Take one timestamp per multi-source repo build Stamping each solo_gem with its own Time.now.utc lets the two dates drift apart and matches neither the surrounding before block. Snapshot the time once so the cooldown window stays stable as thresholds tighten. https://github.com/ruby/rubygems/commit/69c6d876d4 Co-Authored-By: Claude Opus 4.8 --- spec/bundler/install/cooldown_spec.rb | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/spec/bundler/install/cooldown_spec.rb b/spec/bundler/install/cooldown_spec.rb index e85267ff4121f1..01e87be663c1b8 100644 --- a/spec/bundler/install/cooldown_spec.rb +++ b/spec/bundler/install/cooldown_spec.rb @@ -489,12 +489,13 @@ end it "keeps a top-level source cooldown through a partial update with multiple sources" do + now = Time.now.utc build_repo4 do build_gem "solo_gem", "1.0.0" do |s| - s.date = Time.now.utc - (30 * 86_400) + s.date = now - (30 * 86_400) end build_gem "solo_gem", "2.0.0" do |s| - s.date = Time.now.utc - (1 * 86_400) + s.date = now - (1 * 86_400) end end @@ -537,12 +538,13 @@ end it "carries cooldown declared on a gem-block source" do + now = Time.now.utc build_repo4 do build_gem "solo_gem", "1.0.0" do |s| - s.date = Time.now.utc - (30 * 86_400) + s.date = now - (30 * 86_400) end build_gem "solo_gem", "2.0.0" do |s| - s.date = Time.now.utc - (1 * 86_400) + s.date = now - (1 * 86_400) end end From 9f5fc989a24503d83b00b38124b3e95586be4698 Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Wed, 30 Apr 2025 10:28:25 -0700 Subject: [PATCH 188/188] Add a check in non-debug mode for a T_NONE class Although a crash could occur anywhere, one of the most common symptoms we see from getting a reference to a garbage collected object is crashing while attempting to call a method on it. These crashes usually occur when trying to perform an rb_id_table_lookup in the "class" cc_tbl, where the class is usually another garbage collected object, because the freelist is stored in the class pointer. This commit aims to have a better error message in this case, in hopes of having a better grouping of errors and to give a better hint to those investigating and triaging crashes. --- vm_insnhelper.c | 12 ++++++++++++ vm_method.c | 12 ++++++++++++ 2 files changed, 24 insertions(+) diff --git a/vm_insnhelper.c b/vm_insnhelper.c index f515662bf07765..dceb14413a7e62 100644 --- a/vm_insnhelper.c +++ b/vm_insnhelper.c @@ -2178,6 +2178,18 @@ rb_vm_search_method_slowpath(const struct rb_callinfo *ci, VALUE klass) { const struct rb_callcache *cc; + VM_ASSERT(!SPECIAL_CONST_P(klass)); + + if (RB_BUILTIN_TYPE(klass) == T_NONE) { + // If we find a T_NONE here, it's most likely we called CLASS_OF(obj) on a + // garbage collected object (the freelist is stored in the class pointer), + // but it's possible that just the class was GC'd. + // This message intentionally tries to imply the former, but make an + // accurate statement for either case. + rb_bug("attempted to search method '%s' on a garbage collected object", + rb_id2name(vm_ci_mid(ci))); + } + VM_ASSERT_TYPE2(klass, T_CLASS, T_ICLASS); cc = vm_search_cc(klass, ci); diff --git a/vm_method.c b/vm_method.c index 59906f9b164a0f..e2648c0ee9e627 100644 --- a/vm_method.c +++ b/vm_method.c @@ -2007,6 +2007,18 @@ callable_method_entry_or_negative(VALUE klass, ID mid, VALUE *defined_class_ptr) { const rb_callable_method_entry_t *cme; + VM_ASSERT(!SPECIAL_CONST_P(klass)); + + if (RB_BUILTIN_TYPE(klass) == T_NONE) { + // If we find a T_NONE here, it's most likely we called CLASS_OF(obj) on a + // garbage collected object (the freelist is stored in the class pointer), + // but it's possible that just the class was GC'd. + // This message intentionally tries to imply the former, but make an + // accurate statement for either case. + rb_bug("attempted to search method '%s' on a garbage collected object", + rb_id2name(mid)); + } + VM_ASSERT_TYPE2(klass, T_CLASS, T_ICLASS); /* Fast path: lock-free read from cache */