diff --git a/.github/workflows/annocheck.yml b/.github/workflows/annocheck.yml index 2c1d35899fb722..e839cddc179b0b 100644 --- a/.github/workflows/annocheck.yml +++ b/.github/workflows/annocheck.yml @@ -73,7 +73,7 @@ jobs: builddir: build makeup: true - - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0 + - uses: ruby/setup-ruby@8e41b362d2589a22a44c1cfa214b3c83052c195b # v1.318.0 with: ruby-version: '3.1' bundler: none diff --git a/.github/workflows/auto_review_pr.yml b/.github/workflows/auto_review_pr.yml index 72611269a40eca..93ef85f4963d73 100644 --- a/.github/workflows/auto_review_pr.yml +++ b/.github/workflows/auto_review_pr.yml @@ -29,7 +29,7 @@ jobs: with: persist-credentials: false - - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0 + - uses: ruby/setup-ruby@8e41b362d2589a22a44c1cfa214b3c83052c195b # v1.318.0 with: ruby-version: '3.4' bundler: none diff --git a/.github/workflows/baseruby.yml b/.github/workflows/baseruby.yml index 695c468153be36..413e5d7ee7ec17 100644 --- a/.github/workflows/baseruby.yml +++ b/.github/workflows/baseruby.yml @@ -48,7 +48,7 @@ jobs: - ruby-3.3 steps: - - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0 + - uses: ruby/setup-ruby@8e41b362d2589a22a44c1cfa214b3c83052c195b # v1.318.0 with: ruby-version: ${{ matrix.ruby }} bundler: none diff --git a/.github/workflows/bundled_gems.yml b/.github/workflows/bundled_gems.yml index 464a44dfceb9dd..5621e5bc7a8a1f 100644 --- a/.github/workflows/bundled_gems.yml +++ b/.github/workflows/bundled_gems.yml @@ -38,7 +38,7 @@ jobs: with: token: ${{ (github.repository == 'ruby/ruby' && !startsWith(github.event_name, 'pull')) && secrets.MATZBOT_AUTO_UPDATE_TOKEN || secrets.GITHUB_TOKEN }} - - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0 + - uses: ruby/setup-ruby@8e41b362d2589a22a44c1cfa214b3c83052c195b # v1.318.0 with: ruby-version: 4.0 diff --git a/.github/workflows/check_dependencies.yml b/.github/workflows/check_dependencies.yml index 56eea4001fd274..327734c9e21363 100644 --- a/.github/workflows/check_dependencies.yml +++ b/.github/workflows/check_dependencies.yml @@ -42,7 +42,7 @@ jobs: - uses: ./.github/actions/setup/directories - - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0 + - uses: ruby/setup-ruby@8e41b362d2589a22a44c1cfa214b3c83052c195b # v1.318.0 with: ruby-version: '3.1' bundler: none diff --git a/.github/workflows/check_misc.yml b/.github/workflows/check_misc.yml index 55238ffba504d9..e50ebb3f9961d6 100644 --- a/.github/workflows/check_misc.yml +++ b/.github/workflows/check_misc.yml @@ -23,7 +23,7 @@ jobs: token: ${{ (github.repository == 'ruby/ruby' && !startsWith(github.event_name, 'pull')) && secrets.MATZBOT_AUTO_UPDATE_TOKEN || secrets.GITHUB_TOKEN }} persist-credentials: false - - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0 + - uses: ruby/setup-ruby@8e41b362d2589a22a44c1cfa214b3c83052c195b # v1.318.0 with: ruby-version: head diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index c9c5af3b1952b9..191e3a8988bac8 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -30,12 +30,12 @@ jobs: - test_task: check os: macos-15 configure_args: '--with-gcc=gcc-14' + # No dedicated --with-gmp job: gmp is enabled by default and + # setup/macos passes --with-gmp-dir to every job, so each check + # here already builds and tests with -lgmp. - test_task: check os: macos-26 configure_args: '--with-jemalloc --with-opt-dir=$(brew --prefix jemalloc)' - - test_task: check - os: macos-26 - configure_args: '--with-gmp' - test_task: test-all test_opts: --repeat-count=2 os: macos-26 diff --git a/.github/workflows/modgc.yml b/.github/workflows/modgc.yml index b0fe1c827f51a8..2cc77eefed2f6f 100644 --- a/.github/workflows/modgc.yml +++ b/.github/workflows/modgc.yml @@ -67,7 +67,7 @@ jobs: uses: ./.github/actions/setup/ubuntu if: ${{ contains(matrix.os, 'ubuntu') }} - - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0 + - uses: ruby/setup-ruby@8e41b362d2589a22a44c1cfa214b3c83052c195b # v1.318.0 with: ruby-version: '3.1' bundler: none diff --git a/.github/workflows/parse_y.yml b/.github/workflows/parse_y.yml index 0e51c852335355..7f355d3d0cc0e7 100644 --- a/.github/workflows/parse_y.yml +++ b/.github/workflows/parse_y.yml @@ -59,7 +59,7 @@ jobs: - uses: ./.github/actions/setup/ubuntu - - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0 + - uses: ruby/setup-ruby@8e41b362d2589a22a44c1cfa214b3c83052c195b # v1.318.0 with: ruby-version: '3.1' bundler: none diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index bea2aba1012685..4ce3fd0476ad73 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -22,7 +22,7 @@ jobs: with: persist-credentials: false - - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0 + - uses: ruby/setup-ruby@8e41b362d2589a22a44c1cfa214b3c83052c195b # v1.318.0 with: ruby-version: 3.3.4 diff --git a/.github/workflows/spec_guards.yml b/.github/workflows/spec_guards.yml index bf6b6f1d0155ec..d3a497ea8cbacd 100644 --- a/.github/workflows/spec_guards.yml +++ b/.github/workflows/spec_guards.yml @@ -49,7 +49,7 @@ jobs: with: persist-credentials: false - - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0 + - uses: ruby/setup-ruby@8e41b362d2589a22a44c1cfa214b3c83052c195b # v1.318.0 with: ruby-version: ${{ matrix.ruby }} bundler: none diff --git a/.github/workflows/sync_default_gems.yml b/.github/workflows/sync_default_gems.yml index 98957ab01acb29..3b1e5e191ed11a 100644 --- a/.github/workflows/sync_default_gems.yml +++ b/.github/workflows/sync_default_gems.yml @@ -39,7 +39,7 @@ jobs: with: token: ${{ github.repository == 'ruby/ruby' && secrets.MATZBOT_AUTO_UPDATE_TOKEN || secrets.GITHUB_TOKEN }} - - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0 + - uses: ruby/setup-ruby@8e41b362d2589a22a44c1cfa214b3c83052c195b # v1.318.0 with: ruby-version: '3.4' bundler: none diff --git a/.github/workflows/tarball-macos.yml b/.github/workflows/tarball-macos.yml index 2b3ac7ab37b6ae..1302b653f0c401 100644 --- a/.github/workflows/tarball-macos.yml +++ b/.github/workflows/tarball-macos.yml @@ -12,6 +12,11 @@ on: required: false type: boolean default: false + full-matrix: + description: 'Run the full test matrix (nightly schedule and release verification). Off, the default, runs a single smoke check.' + required: false + type: boolean + default: false secrets: SIMPLER_ALERTS_URL: required: false @@ -24,16 +29,17 @@ permissions: jobs: macos: strategy: + # The smoke matrix keeps only what is tarball-specific: that the + # packaged tarball configures, builds and installs. macos.yml runs the + # same test tasks from the git tree on every PR/push already. + # macos-15-intel: the runner image ships a pre-installed Ruby under + # /usr/local whose gems dir collides with the default prefix on Intel. + # Use an isolated prefix so RDoc generation does not scan that stale + # default-gem stub. matrix: - test_task: [check, test-bundled-gems, test-bundler-parallel] - os: [macos-26, macos-15, macos-14] - include: - - os: macos-15-intel - test_task: check - # The runner image ships a pre-installed Ruby under /usr/local whose - # gems dir collides with the default prefix on Intel. Use an isolated - # prefix so RDoc generation does not scan that stale default-gem stub. - prefix: /tmp/ruby-prefix + test_task: ${{ inputs.full-matrix && fromJSON('["check", "test-bundled-gems", "test-bundler-parallel"]') || fromJSON('["check"]') }} + os: ${{ inputs.full-matrix && fromJSON('["macos-26", "macos-15", "macos-14"]') || fromJSON('["macos-26"]') }} + include: ${{ inputs.full-matrix && fromJSON('[{"os":"macos-15-intel", "test_task":"check", "prefix":"/tmp/ruby-prefix"}]') || fromJSON('[]') }} fail-fast: false runs-on: ${{ matrix.os }} env: diff --git a/.github/workflows/tarball-test.yml b/.github/workflows/tarball-test.yml index 814304eb2a7b95..f27f842332ba4f 100644 --- a/.github/workflows/tarball-test.yml +++ b/.github/workflows/tarball-test.yml @@ -83,6 +83,11 @@ jobs: uses: ./.github/workflows/tarball-macos.yml with: archname: snapshot-${{ needs.tarball.outputs.branch }} + # Full fan-out only for workflow_dispatch (nightly schedule and release + # verification); pull requests and pushes run a single smoke check. + # Only macOS is trimmed: its org-wide concurrency pool of 50 runners + # saturates during peak hours, while Linux and Windows have headroom. + full-matrix: ${{ github.event_name == 'workflow_dispatch' }} notify-release-channel: ${{ github.event_name == 'workflow_dispatch' && inputs.notify-release-channel || false }} secrets: SIMPLER_ALERTS_URL: ${{ secrets.SIMPLER_ALERTS_URL }} diff --git a/.github/workflows/tarball-ubuntu.yml b/.github/workflows/tarball-ubuntu.yml index d0d4e5ba575d66..88782a801c7c4a 100644 --- a/.github/workflows/tarball-ubuntu.yml +++ b/.github/workflows/tarball-ubuntu.yml @@ -43,7 +43,7 @@ jobs: set -x sudo apt-get update -q sudo apt-get install --no-install-recommends -q -y build-essential libssl-dev libyaml-dev zlib1g-dev libffi-dev libgmp-dev bison- autoconf- - - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0 + - uses: ruby/setup-ruby@8e41b362d2589a22a44c1cfa214b3c83052c195b # v1.318.0 with: ruby-version: '3.2' # test-bundled-gems requires executable host ruby diff --git a/.github/workflows/tarball-windows.yml b/.github/workflows/tarball-windows.yml index 2f591341d45d0a..24bfe8dde1bd00 100644 --- a/.github/workflows/tarball-windows.yml +++ b/.github/workflows/tarball-windows.yml @@ -48,7 +48,7 @@ jobs: - run: md build working-directory: - - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0 + - uses: ruby/setup-ruby@8e41b362d2589a22a44c1cfa214b3c83052c195b # v1.318.0 with: ruby-version: '3.2' bundler: none diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml index 6518e77d72b07b..19f89eae6f5c86 100644 --- a/.github/workflows/ubuntu.yml +++ b/.github/workflows/ubuntu.yml @@ -79,7 +79,7 @@ jobs: with: arch: ${{ matrix.arch }} - - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0 + - uses: ruby/setup-ruby@8e41b362d2589a22a44c1cfa214b3c83052c195b # v1.318.0 with: ruby-version: '3.1' bundler: none diff --git a/.github/workflows/wasm.yml b/.github/workflows/wasm.yml index 75344395bb4155..cba593d1c0d6e0 100644 --- a/.github/workflows/wasm.yml +++ b/.github/workflows/wasm.yml @@ -65,7 +65,7 @@ jobs: sparse-checkout: /.github persist-credentials: false - - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0 + - uses: ruby/setup-ruby@8e41b362d2589a22a44c1cfa214b3c83052c195b # v1.318.0 with: ruby-version: '3.1' bundler: none diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index a48b0f6471073f..d83fa0144ebc54 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -63,7 +63,7 @@ jobs: - run: md build working-directory: - - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0 + - uses: ruby/setup-ruby@8e41b362d2589a22a44c1cfa214b3c83052c195b # v1.318.0 with: # windows-11-arm has only 3.4.1, 3.4.2, 3.4.3, head ruby-version: ${{ !endsWith(matrix.os, 'arm') && '3.1' || '3.4' }} diff --git a/.github/workflows/yjit-macos.yml b/.github/workflows/yjit-macos.yml index fadcaeac30cc80..a17bffcf41e709 100644 --- a/.github/workflows/yjit-macos.yml +++ b/.github/workflows/yjit-macos.yml @@ -32,35 +32,6 @@ env: CARGO_HTTP_MULTIPLEXING: false jobs: - cargo: - name: cargo test - - runs-on: macos-26 - - if: >- - ${{!(false - || contains(github.event.head_commit.message, '[DOC]') - || contains(github.event.pull_request.title, '[DOC]') - || contains(github.event.pull_request.labels.*.name, 'Documentation') - || (github.event.pull_request.user.login == 'dependabot[bot]' && !startsWith(github.head_ref, 'dependabot/cargo')) - )}} - - steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - persist-credentials: false - - - run: RUST_BACKTRACE=1 cargo test - working-directory: yjit - - # Also compile and test with all features enabled - - run: RUST_BACKTRACE=1 cargo test --all-features - working-directory: yjit - - # Check that we can build in release mode too - - run: cargo build --release - working-directory: yjit - make: strategy: matrix: @@ -68,6 +39,10 @@ jobs: - test_task: 'check' configure: '--enable-yjit' yjit_opts: '--yjit' + # Run the Rust unit tests on one entry only; a separate job + # would occupy another runner from the org-wide macOS + # concurrency pool just for ~1 minute of cargo. + cargo: true - test_task: 'check' configure: '--enable-yjit=dev' yjit_opts: '--yjit-call-threshold=1 --yjit-verify-ctx --yjit-code-gc' @@ -177,6 +152,25 @@ jobs: if: ${{ matrix.test_task == 'check' && matrix.skipped_tests }} continue-on-error: ${{ matrix.continue-on-skipped_tests || false }} + # Run even when an earlier test step failed, so a Ruby-side failure + # does not mask the Rust unit test result. + - name: cargo test + run: RUST_BACKTRACE=1 cargo test + working-directory: src/yjit + if: ${{ matrix.cargo && !cancelled() }} + + # Also compile and test with all features enabled + - name: cargo test --all-features + run: RUST_BACKTRACE=1 cargo test --all-features + working-directory: src/yjit + if: ${{ matrix.cargo && !cancelled() }} + + # Check that we can build in release mode too + - name: cargo build --release + run: cargo build --release + working-directory: src/yjit + if: ${{ matrix.cargo && !cancelled() }} + - name: Dump crash logs if: ${{ failure() }} continue-on-error: true diff --git a/.github/workflows/yjit-ubuntu.yml b/.github/workflows/yjit-ubuntu.yml index 793f93d6d10f44..8385cbd9b112f7 100644 --- a/.github/workflows/yjit-ubuntu.yml +++ b/.github/workflows/yjit-ubuntu.yml @@ -138,7 +138,7 @@ jobs: - uses: ./.github/actions/setup/ubuntu - - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0 + - uses: ruby/setup-ruby@8e41b362d2589a22a44c1cfa214b3c83052c195b # v1.318.0 with: ruby-version: '3.1' bundler: none diff --git a/.github/workflows/zjit-ubuntu.yml b/.github/workflows/zjit-ubuntu.yml index 80c7a0cb7986bf..25bde6b8b2a16c 100644 --- a/.github/workflows/zjit-ubuntu.yml +++ b/.github/workflows/zjit-ubuntu.yml @@ -136,7 +136,7 @@ jobs: - uses: ./.github/actions/setup/ubuntu - - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0 + - uses: ruby/setup-ruby@8e41b362d2589a22a44c1cfa214b3c83052c195b # v1.318.0 with: ruby-version: '3.1' bundler: none diff --git a/array.c b/array.c index 58f41287bdb3cd..35892be43e5ecf 100644 --- a/array.c +++ b/array.c @@ -7723,7 +7723,7 @@ rb_ary_repeated_combination(VALUE ary, VALUE num) * If no argument is given, returns an array of 1-element arrays, * each containing an element of +self+: * - * a.product # => [[0], [1], [2]] + * [0, 1, 2].product # => [[0], [1], [2]] * * With a block given, calls the block with each combination; returns +self+: * diff --git a/compile.c b/compile.c index 1d762c485ef17b..3888134c7c8402 100644 --- a/compile.c +++ b/compile.c @@ -2481,8 +2481,9 @@ fix_sp_depth(rb_iseq_t *iseq, LINK_ANCHOR *const anchor) lobj->sp = sp; } else if (lobj->sp != sp) { - debugs("%s:%d: sp inconsistency found but ignored (" LABEL_FORMAT " sp: %d, calculated sp: %d)\n", - RSTRING_PTR(rb_iseq_path(iseq)), line, + VALUE path = rb_iseq_path(iseq); + debugs("%.*s:%d: sp inconsistency found but ignored (" LABEL_FORMAT " sp: %d, calculated sp: %d)\n", + RSTRING_LENINT(path), RSTRING_PTR(path), line, lobj->label_no, lobj->sp, sp); } } @@ -2497,8 +2498,9 @@ fix_sp_depth(rb_iseq_t *iseq, LINK_ANCHOR *const anchor) } else { if (lobj->sp != sp) { - debugs("%s:%d: sp inconsistency found but ignored (" LABEL_FORMAT " sp: %d, calculated sp: %d)\n", - RSTRING_PTR(rb_iseq_path(iseq)), line, + VALUE path = rb_iseq_path(iseq); + debugs("%.*s:%d: sp inconsistency found but ignored (" LABEL_FORMAT " sp: %d, calculated sp: %d)\n", + RSTRING_LENINT(path), RSTRING_PTR(path), line, lobj->label_no, lobj->sp, sp); } sp = lobj->sp; @@ -2645,8 +2647,9 @@ iseq_set_sequence(rb_iseq_t *iseq, LINK_ANCHOR *const anchor) LABEL *lobj = (LABEL *)list; lobj->position = code_index; if (lobj->sp != sp) { - debugs("%s: sp inconsistency found but ignored (" LABEL_FORMAT " sp: %d, calculated sp: %d)\n", - RSTRING_PTR(rb_iseq_path(iseq)), + VALUE path = rb_iseq_path(iseq); + debugs("%.*s: sp inconsistency found but ignored (" LABEL_FORMAT " sp: %d, calculated sp: %d)\n", + RSTRING_LENINT(path), RSTRING_PTR(path), lobj->label_no, lobj->sp, sp); } sp = lobj->sp; @@ -2870,8 +2873,9 @@ iseq_set_sequence(rb_iseq_t *iseq, LINK_ANCHOR *const anchor) { LABEL *lobj = (LABEL *)list; if (lobj->sp != sp) { - debugs("%s: sp inconsistency found but ignored (" LABEL_FORMAT " sp: %d, calculated sp: %d)\n", - RSTRING_PTR(rb_iseq_path(iseq)), + VALUE path = rb_iseq_path(iseq); + debugs("%.*s: sp inconsistency found but ignored (" LABEL_FORMAT " sp: %d, calculated sp: %d)\n", + RSTRING_LENINT(path), RSTRING_PTR(path), lobj->label_no, lobj->sp, sp); } sp = lobj->sp; @@ -9273,12 +9277,37 @@ delegate_call_p(const rb_iseq_t *iseq, unsigned int argc, const LINK_ANCHOR *arg } } +static int +compile_builtin_attr_symbol(rb_iseq_t *iseq, VALUE symbol) +{ + VALUE string = rb_sym2str(symbol); + if (rb_streql_lit(string, "leaf")) { + ISEQ_BODY(iseq)->builtin_attrs |= BUILTIN_ATTR_LEAF; + } + else if (rb_streql_lit(string, "inline_block")) { + ISEQ_BODY(iseq)->builtin_attrs |= BUILTIN_ATTR_INLINE_BLOCK; + } + else if (rb_streql_lit(string, "use_block")) { + iseq_set_use_block(iseq); + } + else if (rb_streql_lit(string, "c_trace")) { + // Let the iseq act like a C method in backtraces + ISEQ_BODY(iseq)->builtin_attrs |= BUILTIN_ATTR_C_TRACE; + } + else if (rb_streql_lit(string, "without_interrupts")) { + ISEQ_BODY(iseq)->builtin_attrs |= BUILTIN_ATTR_WITHOUT_INTERRUPTS; + } + else { + return COMPILE_NG; + } + return COMPILE_OK; +} + // Compile Primitive.attr! :leaf, ... static int compile_builtin_attr(rb_iseq_t *iseq, const NODE *node) { VALUE symbol; - VALUE string; if (!node) goto no_arg; while (node) { if (!nd_type_p(node, NODE_LIST)) goto bad_arg; @@ -9295,27 +9324,7 @@ compile_builtin_attr(rb_iseq_t *iseq, const NODE *node) } if (!SYMBOL_P(symbol)) goto non_symbol_arg; - - string = rb_sym2str(symbol); - if (strcmp(RSTRING_PTR(string), "leaf") == 0) { - ISEQ_BODY(iseq)->builtin_attrs |= BUILTIN_ATTR_LEAF; - } - else if (strcmp(RSTRING_PTR(string), "inline_block") == 0) { - ISEQ_BODY(iseq)->builtin_attrs |= BUILTIN_ATTR_INLINE_BLOCK; - } - else if (strcmp(RSTRING_PTR(string), "use_block") == 0) { - iseq_set_use_block(iseq); - } - else if (strcmp(RSTRING_PTR(string), "c_trace") == 0) { - // Let the iseq act like a C method in backtraces - ISEQ_BODY(iseq)->builtin_attrs |= BUILTIN_ATTR_C_TRACE; - } - else if (strcmp(RSTRING_PTR(string), "without_interrupts") == 0) { - ISEQ_BODY(iseq)->builtin_attrs |= BUILTIN_ATTR_WITHOUT_INTERRUPTS; - } - else { - goto unknown_arg; - } + if (compile_builtin_attr_symbol(iseq, symbol) != COMPILE_OK) goto unknown_arg; node = next; } return COMPILE_OK; @@ -9326,7 +9335,7 @@ compile_builtin_attr(rb_iseq_t *iseq, const NODE *node) COMPILE_ERROR(ERROR_ARGS "non symbol argument to attr!: %s", rb_builtin_class_name(symbol)); return COMPILE_NG; unknown_arg: - COMPILE_ERROR(ERROR_ARGS "unknown argument to attr!: %s", RSTRING_PTR(string)); + COMPILE_ERROR(ERROR_ARGS "unknown argument to attr!: %" PRIsVALUE, symbol); return COMPILE_NG; bad_arg: UNKNOWN_NODE("attr!", node, COMPILE_NG); @@ -11730,10 +11739,10 @@ opobj_inspect(VALUE obj) if (!SPECIAL_CONST_P(obj) && !RBASIC_CLASS(obj)) { switch (BUILTIN_TYPE(obj)) { case T_STRING: - obj = rb_str_new_cstr(RSTRING_PTR(obj)); + obj = rb_str_resurrect(obj); break; case T_ARRAY: - obj = rb_ary_dup(obj); + obj = rb_ary_resurrect(obj); break; default: break; @@ -13713,8 +13722,9 @@ ibf_dump_iseq_each(struct ibf_dump *dump, const rb_iseq_t *iseq) const ibf_offset_t lvar_states_offset = ibf_dump_lvar_states(dump, iseq); const unsigned int catch_table_size = body->catch_table ? body->catch_table->size : 0; const ibf_offset_t catch_table_offset = ibf_dump_catch_table(dump, iseq); - const int parent_iseq_index = ibf_dump_iseq(dump, ISEQ_BODY(iseq)->parent_iseq); - const int local_iseq_index = ibf_dump_iseq(dump, ISEQ_BODY(iseq)->local_iseq); + /* a NULL parent (persisted root) and references outside a Proc#refined subtree dump come out absent (-1) */ + const int parent_iseq_index = ibf_table_lookup(dump->iseq_table, (st_data_t)ISEQ_BODY(iseq)->parent_iseq); + const int local_iseq_index = ibf_table_lookup(dump->iseq_table, (st_data_t)ISEQ_BODY(iseq)->local_iseq); const int mandatory_only_iseq_index = ibf_dump_iseq(dump, ISEQ_BODY(iseq)->mandatory_only_iseq); const ibf_offset_t ci_entries_offset = ibf_dump_ci_entries(dump, iseq); const ibf_offset_t outer_variables_offset = ibf_dump_outer_variables(dump, iseq); @@ -14875,24 +14885,10 @@ ibf_dump_setup(struct ibf_dump *dump, VALUE dumper_obj) dump->current_buffer = &dump->global_buffer; } -VALUE -rb_iseq_ibf_dump(const rb_iseq_t *iseq, VALUE opt) +static VALUE +iseq_ibf_dump_0(struct ibf_dump *dump, const rb_iseq_t *iseq, VALUE opt) { - struct ibf_dump *dump; struct ibf_header header = {{0}}; - VALUE dump_obj; - VALUE str; - - if (ISEQ_BODY(iseq)->parent_iseq != NULL || - ISEQ_BODY(iseq)->local_iseq != iseq) { - rb_raise(rb_eRuntimeError, "should be top of iseq"); - } - if (RTEST(ISEQ_COVERAGE(iseq))) { - rb_raise(rb_eRuntimeError, "should not compile with coverage"); - } - - dump_obj = TypedData_Make_Struct(0, struct ibf_dump, &ibf_dump_type, dump); - ibf_dump_setup(dump, dump_obj); ibf_dump_write(dump, &header, sizeof(header)); ibf_dump_iseq(dump, iseq); @@ -14921,7 +14917,28 @@ rb_iseq_ibf_dump(const rb_iseq_t *iseq, VALUE opt) ibf_dump_overwrite(dump, &header, sizeof(header), 0); - str = dump->global_buffer.str; + return dump->global_buffer.str; +} + +VALUE +rb_iseq_ibf_dump(const rb_iseq_t *iseq, VALUE opt) +{ + struct ibf_dump *dump; + VALUE dump_obj; + VALUE str; + + if (ISEQ_BODY(iseq)->parent_iseq != NULL || + ISEQ_BODY(iseq)->local_iseq != iseq) { + rb_raise(rb_eRuntimeError, "should be top of iseq"); + } + if (RTEST(ISEQ_COVERAGE(iseq))) { + rb_raise(rb_eRuntimeError, "should not compile with coverage"); + } + + dump_obj = TypedData_Make_Struct(0, struct ibf_dump, &ibf_dump_type, dump); + ibf_dump_setup(dump, dump_obj); + + str = iseq_ibf_dump_0(dump, iseq, opt); RB_GC_GUARD(dump_obj); return str; } @@ -15127,6 +15144,91 @@ rb_iseq_ibf_load_bytes(const char *bytes, size_t size) return iseq; } +/* collect the iseq table into an array indexed by iseq-list index */ +static int +ibf_dump_iseq_table_collect_i(st_data_t key, st_data_t val, st_data_t arg) +{ + const rb_iseq_t **srcs = (const rb_iseq_t **)arg; + srcs[(int)val] = (const rb_iseq_t *)key; + return ST_CONTINUE; +} + +/* pc2branchindex (branch coverage) is per-iseq, so pair each copy with its + * source by list index */ +static void +iseq_subtree_copy_pc2branchindex(const struct ibf_dump *dump, const struct ibf_load *load) +{ + unsigned int iseq_count = (unsigned int)dump->iseq_table->num_entries; + VALUE srcs_buf; + const rb_iseq_t **srcs = ALLOCV_N(const rb_iseq_t *, srcs_buf, iseq_count); + st_foreach(dump->iseq_table, ibf_dump_iseq_table_collect_i, (st_data_t)srcs); + + for (unsigned int i = 0; i < iseq_count; i++) { + rb_iseq_t *copy = ibf_load_iseq(load, (const rb_iseq_t *)(VALUE)i); + VALUE pc2branchindex = ISEQ_PC2BRANCHINDEX(srcs[i]); + if (pc2branchindex != Qnil) { + ISEQ_PC2BRANCHINDEX_SET(copy, pc2branchindex); + } + } + + ALLOCV_END(srcs_buf); +} + +/* Deep-copy an iseq subtree with independent inline caches for Proc#refined */ +const rb_iseq_t * +rb_iseq_dup_with_independent_caches(const rb_iseq_t *src_root) +{ + struct ibf_dump *dump; + VALUE dump_obj; + VALUE str; + + /* --- dump the subtree --- */ + dump_obj = TypedData_Make_Struct(0, struct ibf_dump, &ibf_dump_type, dump); + ibf_dump_setup(dump, dump_obj); + + str = iseq_ibf_dump_0(dump, src_root, Qnil); + + /* --- load it back --- */ + struct ibf_load *load; + VALUE loader_obj = TypedData_Make_Struct(0, struct ibf_load, &ibf_load_type, load); + ibf_load_setup(load, loader_obj, str); + + /* What the loader could not reconstruct is the same for every iseq in + * the subtree: the only absent parent is the top's, every absent + * local_iseq is the source's local root (runtime walks compare it + * against the original frames, so a copy must not stand in), and + * pathobj/script_lines/coverage are per-file values. */ + const struct rb_iseq_constant_body *sb = ISEQ_BODY(src_root); + unsigned int iseq_count = (unsigned int)dump->iseq_table->num_entries; + const rb_iseq_t *result = NULL; + for (unsigned int i = 0; i < iseq_count; i++) { + rb_iseq_t *copy = ibf_load_iseq(load, (const rb_iseq_t *)(VALUE)i); + /* force completion even under USE_LAZY_LOAD */ + if (FL_TEST((VALUE)copy, ISEQ_NOT_LOADED_YET)) { + rb_ibf_load_iseq_complete(copy); + } + + struct rb_iseq_constant_body *cb = ISEQ_BODY(copy); + if (!cb->local_iseq) RB_OBJ_WRITE(copy, &cb->local_iseq, sb->local_iseq); + RB_OBJ_WRITE(copy, &cb->location.pathobj, sb->location.pathobj); + RB_OBJ_WRITE(copy, &cb->variable.script_lines, sb->variable.script_lines); + ISEQ_COVERAGE_SET(copy, ISEQ_COVERAGE(src_root)); + + if (i == 0) { + RB_OBJ_WRITE(copy, &cb->parent_iseq, sb->parent_iseq); + result = copy; + } + } + + if (ISEQ_PC2BRANCHINDEX(src_root) != Qnil) { + iseq_subtree_copy_pc2branchindex(dump, load); + } + + RB_GC_GUARD(dump_obj); + RB_GC_GUARD(loader_obj); + return result; +} + VALUE rb_iseq_ibf_load_extra_data(VALUE str) { diff --git a/configure.ac b/configure.ac index a88cd219b9fffc..a461adc697cc80 100644 --- a/configure.ac +++ b/configure.ac @@ -1344,10 +1344,6 @@ main() AC_CHECK_FUNCS(_wfreopen_s) AC_LIBOBJ([langinfo]) ], -[bsdi*], [ LIBS="-lm $LIBS" - AC_DEFINE(BROKEN_SETREUID, 1) - AC_DEFINE(BROKEN_SETREGID, 1) - ac_cv_sizeof_rlim_t=8], [freebsd*], [ LIBS="-lm $LIBS" ac_cv_func_getpeername=no ac_cv_func_getsockname=no @@ -2135,6 +2131,7 @@ AC_REPLACE_FUNCS(flock) AC_REPLACE_FUNCS(hypot) AC_REPLACE_FUNCS(lgamma_r) AC_REPLACE_FUNCS(memmove) +AC_REPLACE_FUNCS(memrchr) AC_REPLACE_FUNCS(nan) AC_REPLACE_FUNCS(nextafter) AC_REPLACE_FUNCS(setproctitle) @@ -2250,7 +2247,6 @@ AC_CHECK_FUNCS(mblen) AC_CHECK_FUNCS(memalign) AC_CHECK_FUNCS(memset_s) AC_CHECK_FUNCS(writev) -AC_CHECK_FUNCS(memrchr) AC_CHECK_FUNCS(memmem) AC_CHECK_FUNCS(mkfifo) AC_CHECK_FUNCS(mknod) @@ -3112,7 +3108,7 @@ AS_IF([test "$ac_cv_header_mach_o_loader_h" = yes], [ ]) AS_CASE(["$target_os"], -[linux* | gnu* | k*bsd*-gnu | bsdi* | kopensolaris*-gnu], [ +[linux* | gnu* | k*bsd*-gnu | kopensolaris*-gnu], [ AS_IF([test "$rb_cv_binary_elf" = no], [ AC_MSG_ERROR(Not ELF) ], [ @@ -3147,7 +3143,7 @@ STATIC= # mkmf.rb's have_header() to fail if the desired resource happens to be # installed in the /usr/local tree. RUBY_APPEND_OPTION(CCDLFLAGS, -fno-common)], - [bsdi*|cygwin*|msys*|mingw*|aix*|interix*], [ ], + [cygwin*|msys*|mingw*|aix*|interix*], [ ], [ RUBY_APPEND_OPTION(CCDLFLAGS, -fPIC)]) ], [ @@ -3206,10 +3202,7 @@ AC_SUBST(EXTOBJS) rb_cv_dlopen=yes], [osf*], [ : ${LDSHARED='$(LD) -shared -expect_unresolved "*"'} rb_cv_dlopen=yes], - [bsdi3*], [ AS_CASE(["$CC"], - [*shlicc*], [ : ${LDSHARED='$(CC) -r'} - rb_cv_dlopen=yes])], - [linux* | gnu* | k*bsd*-gnu | netbsd* | bsdi* | kopensolaris*-gnu | haiku*], [ + [linux* | gnu* | k*bsd*-gnu | netbsd* | kopensolaris*-gnu | haiku*], [ : ${LDSHARED='$(CC) -shared'} AS_IF([test "$rb_cv_binary_elf" = yes], [ LDFLAGS="$LDFLAGS -Wl,-export-dynamic" diff --git a/cont.c b/cont.c index 77ed342e2438f9..2c4fafac8e4c75 100644 --- a/cont.c +++ b/cont.c @@ -2659,7 +2659,8 @@ rb_fiber_start(rb_fiber_t *fiber) th->ec->root_svar = Qfalse; EXEC_EVENT_HOOK(th->ec, RUBY_EVENT_FIBER_SWITCH, th->self, 0, 0, 0, Qnil); - cont->value = rb_vm_invoke_proc(th->ec, proc, argc, argv, cont->kw_splat, VM_BLOCK_HANDLER_NONE); + cont->value = rb_vm_invoke_proc(th->ec, proc, argc, argv, cont->kw_splat, VM_BLOCK_HANDLER_NONE, + rb_proc_refinements_cref(fiber->first_proc)); } EC_POP_TAG(); diff --git a/dir.c b/dir.c index b1e4f1edb01d16..c24030b59f9f0e 100644 --- a/dir.c +++ b/dir.c @@ -1616,19 +1616,22 @@ rb_dir_getwd_ospath_slowpath(void) VALUE rb_dir_getwd_ospath(void) { - char buf[PATH_MAX]; + char buf[PATH_MAX < LONG_MAX ? PATH_MAX : -1]; char *path = getcwd(buf, PATH_MAX); if (!path) { return rb_dir_getwd_ospath_slowpath(); } + size_t len = strlen(path); + RBIMPL_ASSERT_OR_ASSUME(len < PATH_MAX); VALUE cached_cwd = RUBY_ATOMIC_VALUE_LOAD(last_cwd); - if (!cached_cwd || strcmp(RSTRING_PTR(cached_cwd), path) != 0) { + if (!cached_cwd || (size_t)RSTRING_LEN(cached_cwd) != len || + memcmp(RSTRING_PTR(cached_cwd), path, len) != 0) { #ifdef __APPLE__ - cached_cwd = rb_str_normalize_ospath(path, strlen(path)); + cached_cwd = rb_str_normalize_ospath(path, (long)len); #else - cached_cwd = rb_str_new2(path); + cached_cwd = rb_str_new(path, (long)len); #endif rb_str_freeze(cached_cwd); RUBY_ATOMIC_VALUE_SET(last_cwd, cached_cwd); diff --git a/doc/stringio/stringio.md b/doc/stringio/stringio.md index f81f79cfea728a..4a0550e10a9fbd 100644 --- a/doc/stringio/stringio.md +++ b/doc/stringio/stringio.md @@ -173,7 +173,7 @@ strio.gets # Raises IOError: not opened for reading Mode specified as one of: - String: `'r+'`. -- Constant: `File::RDRW`. +- Constant: `File::RDWR`. Initial state: diff --git a/error.c b/error.c index 4515590845fc83..751300565e2aa8 100644 --- a/error.c +++ b/error.c @@ -2624,6 +2624,8 @@ name_err_mesg_to_str(VALUE obj) int state = 0; rb_encoding *usascii = rb_usascii_encoding(); +#define rb_memsearch_lit(str, v) \ + rb_memsearch((str), rb_strlen_lit(str), RSTRING_PTR(v), RSTRING_LEN(v), rb_enc_get(v)) #define FAKE_CSTR(v, str) rb_setup_fake_str((v), (str), rb_strlen_lit(str), usascii) c = s = FAKE_CSTR(&s_str, ""); obj = ptr->recv; @@ -2638,7 +2640,7 @@ name_err_mesg_to_str(VALUE obj) c = d = FAKE_CSTR(&d_str, "false"); break; default: - if (strstr(RSTRING_PTR(mesg), "%2$s")) { + if (rb_memsearch_lit("%2$s", mesg) >= 0) { d = rb_protect(name_err_mesg_receiver_name, obj, &state); if (state || NIL_OR_UNDEF_P(d)) d = rb_protect(rb_inspect, obj, &state); diff --git a/eval.c b/eval.c index 0d2d2c4089ec09..307762d29229ef 100644 --- a/eval.c +++ b/eval.c @@ -1462,15 +1462,20 @@ using_refinement(VALUE klass, VALUE module, VALUE arg) return ST_CONTINUE; } -static void -using_module_recursive(const rb_cref_t *cref, VALUE klass) +/*! + * \private + * rb_using_module without the refinement method cache flush, for a fresh + * cref that no call site references yet (Proc#refined). + */ +void +rb_using_module_recursive(rb_cref_t *cref, VALUE klass) { ID id_refinements; VALUE super, module, refinements; super = RCLASS_SUPER(klass); if (super) { - using_module_recursive(cref, super); + rb_using_module_recursive(cref, super); } switch (BUILTIN_TYPE(klass)) { case T_MODULE: @@ -1496,10 +1501,10 @@ using_module_recursive(const rb_cref_t *cref, VALUE klass) * \private */ static void -rb_using_module(const rb_cref_t *cref, VALUE module) +rb_using_module(rb_cref_t *cref, VALUE module) { Check_Type(module, T_MODULE); - using_module_recursive(cref, module); + rb_using_module_recursive(cref, module); rb_clear_all_refinement_method_cache(); } @@ -1638,6 +1643,21 @@ ignored_block(VALUE module, const char *klass) rb_warn("%s""using doesn't call the given block""%s.", klass, anon); } +/* Reject `using` anywhere inside a refined proc's body: the procs sharing + * the memoized iseq copy (and its call caches) must all run under the same + * refinement set. */ +static void +check_not_refined_proc_scope(const char *using_name) +{ + const rb_cref_t *cref; + for (cref = rb_vm_cref(); cref; cref = CREF_NEXT(cref)) { + if (CREF_REFINED_PROC(cref)) { + rb_raise(rb_eRuntimeError, + "%s is not permitted in a proc with refinements", using_name); + } + } +} + /* * call-seq: * using(module) -> self @@ -1661,6 +1681,7 @@ mod_using(VALUE self, VALUE module) if (rb_block_given_p()) { ignored_block(module, "Module#"); } + check_not_refined_proc_scope("Module#using"); rb_using_module(rb_vm_cref_replace_with_duplicated_cref(), module); return self; } @@ -2004,6 +2025,7 @@ top_using(VALUE self, VALUE module) if (rb_block_given_p()) { ignored_block(module, "main."); } + check_not_refined_proc_scope("main.using"); rb_using_module(rb_vm_cref_replace_with_duplicated_cref(), module); return self; } diff --git a/eval_intern.h b/eval_intern.h index 5e4249e0b87d6c..d323165107f119 100644 --- a/eval_intern.h +++ b/eval_intern.h @@ -189,6 +189,7 @@ rb_ec_tag_jump(const rb_execution_context_t *ec, enum ruby_tag_type st) #define CREF_FL_OMOD_SHARED IMEMO_FL_USER2 #define CREF_FL_SINGLETON IMEMO_FL_USER3 #define CREF_FL_DYNAMIC_CREF IMEMO_FL_USER4 +#define CREF_FL_REFINED_PROC IMEMO_FL_USER5 static inline int CREF_SINGLETON(const rb_cref_t *cref); @@ -292,6 +293,18 @@ CREF_OMOD_SHARED_UNSET(rb_cref_t *cref) cref->flags &= ~CREF_FL_OMOD_SHARED; } +static inline int +CREF_REFINED_PROC(const rb_cref_t *cref) +{ + return cref->flags & CREF_FL_REFINED_PROC; +} + +static inline void +CREF_REFINED_PROC_SET(rb_cref_t *cref) +{ + cref->flags |= CREF_FL_REFINED_PROC; +} + enum { RAISED_EXCEPTION = 1, RAISED_STACKOVERFLOW = 2, diff --git a/ext/objspace/object_tracing.c b/ext/objspace/object_tracing.c index 74d793a6e232c1..0497c89210e6f5 100644 --- a/ext/objspace/object_tracing.c +++ b/ext/objspace/object_tracing.c @@ -390,7 +390,7 @@ object_allocations_reporter_i(st_data_t key, st_data_t val, st_data_t ptr) fprintf(out, "@%s:%lu", info->path ? info->path : "", info->line); if (!NIL_P(info->mid)) { VALUE m = rb_sym2str(info->mid); - fprintf(out, " (%s)", RSTRING_PTR(m)); + fprintf(out, " (%.*s)", RSTRING_LENINT(m), RSTRING_PTR(m)); } fprintf(out, ")\n"); diff --git a/ext/socket/socket.c b/ext/socket/socket.c index 7902d3b606a8b1..64f5822ae53fd3 100644 --- a/ext/socket/socket.c +++ b/ext/socket/socket.c @@ -873,7 +873,8 @@ sock_gethostname(VALUE obj) rb_str_modify_expand(name, len); len += len; } - rb_str_resize(name, strlen(RSTRING_PTR(name))); + char *s = RSTRING_PTR(name), *p = memchr(s, 0, len); + if (p) rb_str_resize(name, (long)(p - s)); return name; } #else diff --git a/gc.c b/gc.c index 650e4ea4ff0a57..4930ba63521911 100644 --- a/gc.c +++ b/gc.c @@ -377,15 +377,6 @@ void rb_gc_obj_changed_slot_size(VALUE obj, size_t slot_size) { shape_id_t shape_id = rb_obj_shape_transition_capacity(obj, rb_shape_capacity_for_slot_size(slot_size)); - - if (RB_TYPE_P(obj, T_OBJECT) && FL_TEST_RAW(obj, ROBJECT_HEAP) && !rb_obj_shape_complex_p(obj)) { - VALUE *embedded_fields = ROBJECT_EMBEDDED_FIELDS(obj); - VALUE *extended_fields = ROBJECT_FIELDS(obj); - MEMCPY(embedded_fields, extended_fields, VALUE, RSHAPE_LEN(RBASIC_SHAPE_ID(obj))); - FL_UNSET_RAW(obj, ROBJECT_HEAP); - shape_id = rb_shape_transition_robject(shape_id); - } - RBASIC_SET_FULL_SHAPE_ID(obj, shape_id); } @@ -1110,9 +1101,9 @@ rb_newobj_of(VALUE klass, VALUE flags, size_t size) static VALUE class_allocate_complex_instance(VALUE klass, uint32_t capacity) { - shape_id_t initial_shape_id = rb_shape_transition_robject(0); - 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)); + VALUE obj = rb_newobj_of_with_shape(klass, T_OBJECT, rb_shape_transition_extended(ROOT_COMPLEX_SHAPE_ID), sizeof(struct RObject)); + VALUE fields_obj = rb_imemo_fields_new_complex(obj, ROOT_COMPLEX_SHAPE_ID, capacity, false); + ROBJECT_SET_EXTENDED(obj, fields_obj); return obj; } @@ -3668,14 +3659,28 @@ gc_ref_update_array(void *objspace, VALUE v) static void gc_ref_update_object(void *objspace, VALUE v) { + RUBY_ASSERT(rb_gc_obj_slot_size(v) == rb_obj_shape_slot_size(v)); + shape_id_t shape_id = RBASIC_SHAPE_ID(v); + if (FL_TEST_RAW(v, ROBJECT_HEAP)) { UPDATE_IF_MOVED(objspace, ROBJECT(v)->as.extended); - } - else { - VALUE *ptr = ROBJECT_FIELDS(v); - for (uint32_t i = 0; i < ROBJECT_FIELDS_COUNT(v); i++) { - UPDATE_IF_MOVED(objspace, ptr[i]); + + if (!rb_shape_complex_p(shape_id) && rb_shape_embedded_capacity(shape_id) >= RSHAPE_LEN(shape_id)) { + VALUE *embedded_fields = ROBJECT_EMBEDDED_FIELDS(v); + VALUE *extended_fields = ROBJECT_FIELDS(v); + MEMCPY(embedded_fields, extended_fields, VALUE, RSHAPE_LEN(shape_id)); + FL_UNSET_RAW(v, ROBJECT_HEAP); + RBASIC_SET_FULL_SHAPE_ID(v, rb_shape_transition_robject(shape_id)); + rb_gc_writebarrier_remember(v); } + else { + return; + } + } + + VALUE *ptr = ROBJECT_FIELDS(v); + for (uint32_t i = 0; i < ROBJECT_FIELDS_COUNT(v); i++) { + UPDATE_IF_MOVED(objspace, ptr[i]); } } @@ -4654,9 +4659,10 @@ rb_raw_iseq_info(char *const buff, const size_t buff_size, const rb_iseq_t *iseq if (buff_size > 0 && ISEQ_BODY(iseq) && ISEQ_BODY(iseq)->location.label && !RB_TYPE_P(ISEQ_BODY(iseq)->location.pathobj, T_MOVED)) { VALUE path = rb_iseq_path(iseq); int n = ISEQ_BODY(iseq)->location.first_lineno; - snprintf(buff, buff_size, " %s@%s:%d", - RSTRING_PTR(ISEQ_BODY(iseq)->location.label), - RSTRING_PTR(path), n); + VALUE label = ISEQ_BODY(iseq)->location.label; + snprintf(buff, buff_size, " %.*s@%.*s:%d", + RSTRING_LENINT(label), RSTRING_PTR(label), + RSTRING_LENINT(path), RSTRING_PTR(path), n); } } @@ -4728,7 +4734,7 @@ rb_raw_obj_info_common(char *const buff, const size_t buff_size, const VALUE obj else if (RTEST(RBASIC(obj)->klass)) { VALUE class_path = rb_mod_name(RBASIC(obj)->klass); if (!NIL_P(class_path)) { - APPEND_F("%s ", RSTRING_PTR(class_path)); + APPEND_F("%.*s ", str_len_no_raise(class_path), RSTRING_PTR(class_path)); } } } @@ -4792,7 +4798,7 @@ rb_raw_obj_info_buitin_type(char *const buff, const size_t buff_size, const VALU VALUE fstr = RSYMBOL(obj)->fstr; ID id = RSYMBOL(obj)->id; if (RB_TYPE_P(fstr, T_STRING)) { - APPEND_F(":%s id:%d", RSTRING_PTR(fstr), (unsigned int)id); + APPEND_F(":%.*s id:%d", str_len_no_raise(fstr), RSTRING_PTR(fstr), (unsigned int)id); } else { APPEND_F("(%p) id:%d", (void *)fstr, (unsigned int)id); @@ -4814,7 +4820,7 @@ rb_raw_obj_info_buitin_type(char *const buff, const size_t buff_size, const VALU { VALUE class_path = rb_mod_name(obj); if (!NIL_P(class_path)) { - APPEND_F("%s", RSTRING_PTR(class_path)); + APPEND_F("%.*s", str_len_no_raise(class_path), RSTRING_PTR(class_path)); } else { APPEND_S("(anon)"); @@ -4825,7 +4831,7 @@ rb_raw_obj_info_buitin_type(char *const buff, const size_t buff_size, const VALU { VALUE class_path = rb_mod_name(RBASIC_CLASS(obj)); if (!NIL_P(class_path)) { - APPEND_F("src:%s", RSTRING_PTR(class_path)); + APPEND_F("src:%.*s", str_len_no_raise(class_path), RSTRING_PTR(class_path)); } break; } @@ -4931,9 +4937,20 @@ rb_raw_obj_info_buitin_type(char *const buff, const size_t buff_size, const VALU const struct rb_callcache *cc = (const struct rb_callcache *)obj; VALUE class_path = vm_cc_valid(cc) ? rb_mod_name(cc->klass) : Qnil; const rb_callable_method_entry_t *cme = vm_cc_cme(cc); + const char *class_name; + int class_name_len; + + if (NIL_P(class_path)) { + class_name = vm_cc_valid(cc) ? "??" : ""; + class_name_len = vm_cc_valid(cc) ? 2 : 6; + } + else { + class_name = RSTRING_PTR(class_path); + class_name_len = str_len_no_raise(class_path); + } - APPEND_F("(klass:%s cme:%s%s (%p) call:%p", - NIL_P(class_path) ? (vm_cc_valid(cc) ? "??" : "") : RSTRING_PTR(class_path), + APPEND_F("(klass:%.*s cme:%s%s (%p) call:%p", + class_name_len, class_name, cme ? rb_id2name(cme->called_id) : "", cme ? (METHOD_ENTRY_INVALIDATED(cme) ? " [inv]" : "") : "", (void *)cme, diff --git a/gc/default/default.c b/gc/default/default.c index f8f4ab2259b34c..cee550df6b6c79 100644 --- a/gc/default/default.c +++ b/gc/default/default.c @@ -7610,6 +7610,14 @@ gc_move(rb_objspace_t *objspace, VALUE src, VALUE dest, struct heap_page *src_pa } RVALUE_AGE_SET(dest, age); + + /* A re-embedded object (rb_gc_obj_changed_slot_size) references its + * former fields_obj's contents directly; the write-barrier history + * lived on the discarded fields_obj, so remember the object. */ + if (src_slot_size != slot_size && age >= RVALUE_OLD_AGE && !remembered) { + rgengc_remember(objspace, dest); + } + /* Assign forwarding address */ RMOVED(src)->flags = T_MOVED; RMOVED(src)->dummy = Qundef; diff --git a/hash.c b/hash.c index b863f217bcae99..64c343b15f0338 100644 --- a/hash.c +++ b/hash.c @@ -2097,7 +2097,7 @@ rb_hash_stlike_lookup(VALUE hash, st_data_t key, st_data_t *pval) * * If the key is found, returns its value: * - * {foo: 0, bar: 1, baz: 2} + * h = {foo: 0, bar: 1, baz: 2} * h[:bar] # => 1 * * Otherwise, returns a default value (see {Hash Default}[rdoc-ref:Hash@Hash+Default]). diff --git a/imemo.c b/imemo.c index 06a94d0d19a628..f416e219ed175b 100644 --- a/imemo.c +++ b/imemo.c @@ -131,11 +131,11 @@ rb_imemo_cdhash_new(size_t size, const struct st_hash_type *type) } static VALUE -imemo_fields_new(VALUE owner, VALUE flags, shape_id_t shape_id, size_t size, bool is_shareable) +imemo_fields_new(VALUE owner, shape_id_t shape_id, size_t size, bool is_shareable) { RUBY_ASSERT(rb_gc_size_allocatable_p(size)); - flags |= T_IMEMO | (imemo_fields << FL_USHIFT) | (is_shareable ? FL_SHAREABLE : 0); + VALUE flags = T_IMEMO | (imemo_fields << FL_USHIFT) | (is_shareable ? FL_SHAREABLE : 0); // 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 @@ -150,7 +150,7 @@ rb_imemo_fields_new(VALUE owner, shape_id_t shape_id, bool shareable) size_t capa = RSHAPE(shape_id)->capacity; size_t embedded_size = offsetof(struct rb_fields, as.embed) + capa * sizeof(VALUE); - VALUE fields = imemo_fields_new(owner, 0, shape_id, embedded_size, shareable); + VALUE fields = imemo_fields_new(owner, shape_id, embedded_size, shareable); RUBY_ASSERT(IMEMO_TYPE_P(fields, imemo_fields)); RUBY_ASSERT(rb_shape_embedded_capacity(RBASIC_SHAPE_ID(fields)) >= capa); @@ -158,20 +158,19 @@ rb_imemo_fields_new(VALUE owner, shape_id_t shape_id, bool shareable) } VALUE -rb_imemo_fields_new_complex(VALUE owner, shape_id_t shape_id, size_t capa, bool shareable) +rb_imemo_fields_new_complex_empty(VALUE owner) { - st_table *tbl = st_init_numtable_with_size(capa); - VALUE fields = imemo_fields_new(owner, OBJ_FIELD_HEAP, shape_id, sizeof(struct rb_fields), shareable); - IMEMO_OBJ_FIELDS(fields)->as.complex.table = tbl; - return fields; + return imemo_fields_new(owner, ROOT_SHAPE_ID, sizeof(struct rb_fields), false); } -static int -imemo_fields_trigger_wb_i(st_data_t key, st_data_t value, st_data_t arg) +VALUE +rb_imemo_fields_new_complex(VALUE owner, shape_id_t shape_id, size_t capa, bool shareable) { - VALUE field_obj = (VALUE)arg; - RB_OBJ_WRITTEN(field_obj, Qundef, (VALUE)value); - return ST_CONTINUE; + st_table tbl; + st_init_existing_numtable_with_size(&tbl, capa); + VALUE fields = imemo_fields_new(owner, shape_id, sizeof(struct rb_fields), shareable); + MEMCPY(&IMEMO_OBJ_FIELDS(fields)->as.complex.table, &tbl, st_table, 1); + return fields; } static int @@ -181,33 +180,26 @@ imemo_fields_complex_wb_i(st_data_t key, st_data_t value, st_data_t arg) return ST_CONTINUE; } -VALUE -rb_imemo_fields_new_complex_tbl(VALUE owner, shape_id_t shape_id, st_table *tbl, bool shareable) -{ - VALUE fields = imemo_fields_new(owner, OBJ_FIELD_HEAP, shape_id, sizeof(struct rb_fields), shareable); - IMEMO_OBJ_FIELDS(fields)->as.complex.table = tbl; - st_foreach(tbl, imemo_fields_trigger_wb_i, (st_data_t)fields); - return fields; -} - VALUE rb_imemo_fields_clone(VALUE fields_obj) { shape_id_t shape_id = RBASIC_SHAPE_ID(fields_obj); + VALUE owner = rb_imemo_fields_owner(fields_obj); VALUE clone; if (rb_shape_complex_p(shape_id)) { st_table *src_table = rb_imemo_fields_complex_tbl(fields_obj); - st_table *dest_table = xcalloc(1, sizeof(st_table)); - clone = rb_imemo_fields_new_complex_tbl(rb_imemo_fields_owner(fields_obj), shape_id, dest_table, false /* TODO: check */); - + // We start with ROOT_SHAPE_ID so that if GC trigger in `st_replace` it won't try + // to mark an uninitialized table. + clone = imemo_fields_new(owner, ROOT_SHAPE_ID, sizeof(struct rb_fields), false /* TODO: check */); + st_table *dest_table = rb_imemo_fields_complex_tbl(clone); st_replace(dest_table, src_table); - st_foreach(dest_table, imemo_fields_complex_wb_i, (st_data_t)clone); + RBASIC_SET_FULL_SHAPE_ID(clone, shape_id); } else { - clone = rb_imemo_fields_new(rb_imemo_fields_owner(fields_obj), shape_id, false /* TODO: check */); + clone = rb_imemo_fields_new(owner, shape_id, false /* TODO: check */); VALUE *fields = rb_imemo_fields_ptr(clone); attr_index_t fields_count = RSHAPE_LEN(shape_id); MEMCPY(fields, rb_imemo_fields_ptr(fields_obj), VALUE, fields_count); @@ -297,7 +289,7 @@ rb_imemo_memsize(VALUE obj) break; case imemo_fields: if (rb_obj_shape_complex_p(obj)) { - size += st_memsize(IMEMO_OBJ_FIELDS(obj)->as.complex.table); + size += st_memsize(rb_imemo_fields_complex_tbl(obj)) - sizeof(st_table); } break; @@ -620,9 +612,8 @@ rb_free_const_table(struct rb_id_table *tbl) static inline void imemo_fields_free(struct rb_fields *fields) { - if (FL_TEST_RAW((VALUE)fields, OBJ_FIELD_HEAP)) { - RUBY_ASSERT(rb_shape_complex_p(RBASIC_SHAPE_ID((VALUE)fields))); - st_free_table(fields->as.complex.table); + if (rb_obj_shape_complex_p((VALUE)fields)) { + st_free_table(&fields->as.complex.table); } } diff --git a/include/ruby/missing.h b/include/ruby/missing.h index aea6c9088d9d22..8e2e0f8f601fb0 100644 --- a/include/ruby/missing.h +++ b/include/ruby/missing.h @@ -183,6 +183,10 @@ RUBY_EXTERN int memcmp(const void *, const void *, size_t); RUBY_EXTERN void *memmove(void *, const void *, size_t); #endif +#ifndef HAVE_MEMRCHR +RUBY_EXTERN void *memrchr(const void *, int, size_t); +#endif + /* #ifndef HAVE_MODF RUBY_EXTERN double modf(double, double *); diff --git a/internal/eval.h b/internal/eval.h index 3f6b62d8fe5341..d7f55bffee80de 100644 --- a/internal/eval.h +++ b/internal/eval.h @@ -29,6 +29,7 @@ void rb_class_modify_check(VALUE); NORETURN(VALUE rb_f_raise(int argc, VALUE *argv)); VALUE rb_exception_setup(int argc, VALUE *argv); void rb_refinement_setup(struct rb_refinements_data *data, VALUE module, VALUE klass); +void rb_using_module_recursive(rb_cref_t *cref, VALUE module); VALUE rb_top_main_class(const char *method); VALUE rb_ec_ensure(rb_execution_context_t *ec, VALUE (*b_proc)(VALUE), VALUE data1, VALUE (*e_proc)(VALUE), VALUE data2); diff --git a/internal/imemo.h b/internal/imemo.h index a66ed1161b424b..0bd9fc0bfb50d0 100644 --- a/internal/imemo.h +++ b/internal/imemo.h @@ -245,8 +245,7 @@ struct rb_fields { VALUE fields[1]; } embed; struct { - // TODO: the st_table should be embedded - st_table *table; + st_table table; } complex; } as; }; @@ -281,7 +280,7 @@ rb_imemo_subclasses_entries(VALUE v) VALUE rb_imemo_fields_new(VALUE owner, /* shape_id_t */ uint32_t shape_id, bool shareable); VALUE rb_imemo_subclasses_new(uint32_t capacity); VALUE rb_imemo_fields_new_complex(VALUE owner, /* shape_id_t */ uint32_t shape_id, size_t capa, bool shareable); -VALUE rb_imemo_fields_new_complex_tbl(VALUE owner, /* shape_id_t */ uint32_t shape_id, st_table *tbl, bool shareable); +VALUE rb_imemo_fields_new_complex_empty(VALUE owner); VALUE rb_imemo_fields_clone(VALUE fields_obj); void rb_imemo_fields_clear(VALUE fields_obj); @@ -303,15 +302,9 @@ rb_imemo_fields_ptr(VALUE fields_obj) RUBY_ASSERT(IMEMO_TYPE_P(fields_obj, imemo_fields) || RB_TYPE_P(fields_obj, T_OBJECT)); if (UNLIKELY(FL_TEST_RAW(fields_obj, OBJ_FIELD_HEAP))) { - if (RB_TYPE_P(fields_obj, T_OBJECT)) { - fields_obj = ROBJECT(fields_obj)->as.extended; - } + RUBY_ASSERT(RB_TYPE_P(fields_obj, T_OBJECT)); + fields_obj = ROBJECT(fields_obj)->as.extended; RUBY_ASSERT(IMEMO_TYPE_P(fields_obj, imemo_fields)); - - // This will go away once we embed the `st_table` inside `IMEMO/fields`. - if (UNLIKELY(FL_TEST_RAW(fields_obj, OBJ_FIELD_HEAP))) { - return (VALUE *)IMEMO_OBJ_FIELDS(fields_obj)->as.complex.table; - } } return IMEMO_OBJ_FIELDS(fields_obj)->as.embed.fields; @@ -325,13 +318,13 @@ rb_imemo_fields_complex_tbl(VALUE fields_obj) } RUBY_ASSERT(IMEMO_TYPE_P(fields_obj, imemo_fields)); - RUBY_ASSERT(FL_TEST_RAW(fields_obj, OBJ_FIELD_HEAP)); + RUBY_ASSERT(!FL_TEST_RAW(fields_obj, OBJ_FIELD_HEAP)); // Some codepaths unconditionally access the fields_ptr, and assume it can be used as st_table if the // shape is complex. - RUBY_ASSERT((st_table *)rb_imemo_fields_ptr(fields_obj) == IMEMO_OBJ_FIELDS(fields_obj)->as.complex.table); + RUBY_ASSERT((st_table *)rb_imemo_fields_ptr(fields_obj) == &IMEMO_OBJ_FIELDS(fields_obj)->as.complex.table); - return IMEMO_OBJ_FIELDS(fields_obj)->as.complex.table; + return &IMEMO_OBJ_FIELDS(fields_obj)->as.complex.table; } #endif /* INTERNAL_IMEMO_H */ diff --git a/internal/string.h b/internal/string.h index 060703e2856639..6dd6c2e4dd231e 100644 --- a/internal/string.h +++ b/internal/string.h @@ -220,6 +220,14 @@ rb_str_eql_internal(const VALUE str1, const VALUE str2) return Qfalse; } +static inline bool +rb_streql_cstr(VALUE str, const char *lit, size_t len) +{ + if ((size_t)RSTRING_LEN(str) != len) return false; + return memcmp(RSTRING_PTR(str), lit, len) == 0; +} +#define rb_streql_lit(str, lit) rb_streql_cstr(str, lit, rb_strlen_lit(lit)) + #if __has_builtin(__builtin_constant_p) # define rb_fstring_cstr(str) \ (__builtin_constant_p(str) ? \ diff --git a/internal/variable.h b/internal/variable.h index 70142815e89710..461676e6fae300 100644 --- a/internal/variable.h +++ b/internal/variable.h @@ -47,9 +47,8 @@ void rb_gvar_box_dynamic(const char *name); */ VALUE rb_mod_set_temporary_name(VALUE, VALUE); -void rb_obj_copy_ivs_to_hash_table(VALUE obj, st_table *table); -void rb_obj_init_complex(VALUE obj, st_table *table); -void rb_evict_ivars_to_hash(VALUE obj); +void rb_obj_replace_fields(VALUE obj, VALUE fields_obj); +VALUE rb_obj_complex_fields_build(VALUE obj); VALUE rb_obj_field_get(VALUE obj, shape_id_t target_shape_id); void rb_ivar_set_internal(VALUE obj, ID id, VALUE val); void rb_ivar_foreach_buffered(VALUE obj, int (*func)(ID name, VALUE val, st_data_t arg), st_data_t arg); diff --git a/internal/vm.h b/internal/vm.h index 4e50ec27106f91..99e956bdc143f2 100644 --- a/internal/vm.h +++ b/internal/vm.h @@ -24,6 +24,7 @@ struct rb_callable_method_entry_struct; /* in method.h */ struct rb_method_definition_struct; /* in method.h */ +struct rb_cref_struct; /* in method.h */ struct rb_execution_context_struct; /* in vm_core.h */ struct rb_control_frame_struct; /* in vm_core.h */ struct rb_callinfo; /* in vm_core.h */ @@ -55,6 +56,7 @@ void rb_vm_pop_cfunc_frame(void); void rb_vm_check_redefinition_by_prepend(VALUE klass); int rb_vm_check_optimizable_mid(VALUE mid); VALUE rb_yield_refine_block(VALUE refinement, VALUE refinements); +struct rb_cref_struct *rb_vm_cref_dup(const struct rb_cref_struct *cref); VALUE ruby_vm_special_exception_copy(VALUE); void rb_lastline_set_up(VALUE val, unsigned int up); diff --git a/iseq.c b/iseq.c index 720c8f976b5ad5..93e58db2e65287 100644 --- a/iseq.c +++ b/iseq.c @@ -616,7 +616,7 @@ iseq_location_setup(rb_iseq_t *iseq, VALUE name, VALUE path, VALUE realpath, int RB_OBJ_WRITE(iseq, &loc->base_label, name); loc->first_lineno = first_lineno; - if (ISEQ_BODY(iseq)->local_iseq == iseq && strcmp(RSTRING_PTR(name), "initialize") == 0) { + if (ISEQ_BODY(iseq)->local_iseq == iseq && rb_streql_lit(name, "initialize")) { ISEQ_BODY(iseq)->param.flags.use_block = 1; } @@ -4112,7 +4112,11 @@ rb_iseq_add_local_tracepoint_recursively(const rb_iseq_t *iseq, rb_event_flag_t data.r = GET_RACTOR(); iseq_add_local_tracepoint_i(iseq, (void *)&data); - if (0) fprintf(stderr, "Iseq disasm:\n:%s", RSTRING_PTR(rb_iseq_disasm(iseq))); /* for debug */ + if (0) { + VALUE disasm = rb_iseq_disasm(iseq); + fprintf(stderr, "Iseq disasm:\n:%.*s", + RSTRING_LENINT(disasm), RSTRING_PTR(disasm)); + } return data.n; } diff --git a/iseq.h b/iseq.h index b9bc03c23c80fd..df74ec86e3fdf4 100644 --- a/iseq.h +++ b/iseq.h @@ -189,6 +189,7 @@ void rb_ibf_load_iseq_complete(rb_iseq_t *iseq); const rb_iseq_t *rb_iseq_ibf_load(VALUE str); const rb_iseq_t *rb_iseq_ibf_load_bytes(const char *cstr, size_t); VALUE rb_iseq_ibf_load_extra_data(VALUE str); +const rb_iseq_t *rb_iseq_dup_with_independent_caches(const rb_iseq_t *iseq); void rb_iseq_init_trace(rb_iseq_t *iseq); int rb_iseq_add_local_tracepoint_recursively(const rb_iseq_t *iseq, rb_event_flag_t turnon_events, VALUE tpval, unsigned int target_line, bool target_bmethod); int rb_iseq_remove_local_tracepoint_recursively(const rb_iseq_t *iseq, VALUE tpval, rb_ractor_t *r); diff --git a/jit.c b/jit.c index 377ab3fe16177f..e142ab44c4e45a 100644 --- a/jit.c +++ b/jit.c @@ -234,7 +234,8 @@ rb_optimized_call(VALUE recv, rb_execution_context_t *ec, int argc, VALUE *argv, { rb_proc_t *proc; GetProcPtr(recv, proc); - return rb_vm_invoke_proc(ec, proc, argc, argv, kw_splat, block_handler); + return rb_vm_invoke_proc(ec, proc, argc, argv, kw_splat, block_handler, + rb_proc_refinements_cref(recv)); } unsigned int diff --git a/lib/rubygems/source.rb b/lib/rubygems/source.rb index 348bf6a24bc08f..b01619371efc4e 100644 --- a/lib/rubygems/source.rb +++ b/lib/rubygems/source.rb @@ -109,6 +109,13 @@ def fetch_spec(name_tuple) spec_file_name = name_tuple.spec_name + # The name tuple comes from a remote index and is not otherwise + # validated, so refuse anything that would escape the spec cache + # directory when used as a path component. + if File.basename(spec_file_name) != spec_file_name + raise Gem::Exception, "malformed spec name: #{spec_file_name.inspect}" + end + source_uri = enforce_trailing_slash(uri) + "#{Gem::MARSHAL_SPEC_DIR}#{spec_file_name}" cache_dir = cache_dir source_uri diff --git a/lib/rubygems/yaml_serializer.rb b/lib/rubygems/yaml_serializer.rb index 5a46b10a1c038d..cf88a942c7e492 100644 --- a/lib/rubygems/yaml_serializer.rb +++ b/lib/rubygems/yaml_serializer.rb @@ -52,7 +52,7 @@ class Parser }.freeze def initialize(source) - @lines = source.split("\n") + @lines = source.split(/\r?\n/) @anchors = {} @depth = 0 strip_document_prefix @@ -815,7 +815,9 @@ def emit_time(time) end def emit_string(str, indent, quote: false) - if str.include?("\n") + # A CR cannot be represented in a block scalar because parsing + # normalizes CRLF line breaks to LF, so quote and escape instead. + if str.include?("\n") && !str.include?("\r") emit_block_scalar(str, indent) elsif needs_quoting?(str, quote) " #{quote_string(str)}\n" diff --git a/load.c b/load.c index 933e3047ae8aac..069617397affee 100644 --- a/load.c +++ b/load.c @@ -1077,6 +1077,26 @@ rb_f_require_relative(VALUE obj, VALUE fname) return rb_require_relative_entrypoint(fname); } +static char * +find_ext(VALUE str, char **ptr, const char **end) +{ + long len = RSTRING_LEN(str); + *ptr = RSTRING_PTR(str); + *end = *ptr + len; + return memrchr(*ptr, '.', len); +} + +static bool +ext_equal(const char *ext, const char *end, const char *suffix, size_t len) +{ + return (size_t)(end - ext) == len && memcmp(ext, suffix, len) == 0; +} + +#define EXT_RB_P(ext, end) ext_equal(ext, end, ".rb", rb_strlen_lit(".rb")) +#define EXT_SO_P(ext, end) (ext_equal(ext, end, ".so", rb_strlen_lit(".so")) || \ + ext_equal(ext, end, ".o", rb_strlen_lit(".o"))) +#define EXT_DLEXT_P(ext, end) ext_equal(ext, end, DLEXT, rb_strlen_lit(DLEXT)) + typedef int (*feature_func)(const rb_box_t *box, const char *feature, const char *ext, int rb, int expanded, const char **fn); static int @@ -1085,25 +1105,25 @@ search_required(const rb_box_t *box, VALUE fname, volatile VALUE *path, feature_ VALUE tmp; char *ext, *ftptr; int ft = 0; - const char *loading; + const char *ftend, *loading; *path = 0; - ext = strrchr(ftptr = RSTRING_PTR(fname), '.'); - if (ext && !strchr(ext, '/')) { - if (IS_RBEXT(ext)) { + ext = find_ext(fname, &ftptr, &ftend); + if (ext && !memchr(ext, '/', ftend - ext)) { + if (EXT_RB_P(ext, ftend)) { if (rb_feature_p(box, ftptr, ext, TRUE, FALSE, &loading)) { if (loading) *path = rb_filesystem_str_new_cstr(loading); return 'r'; } if ((tmp = rb_find_file(fname)) != 0) { - ext = strrchr(ftptr = RSTRING_PTR(tmp), '.'); + ext = find_ext(tmp, &ftptr, &ftend); if (!rb_feature_p(box, ftptr, ext, TRUE, TRUE, &loading) || loading) *path = tmp; return 'r'; } return 0; } - else if (IS_SOEXT(ext)) { + else if (EXT_SO_P(ext, ftend)) { if (rb_feature_p(box, ftptr, ext, FALSE, FALSE, &loading)) { if (loading) *path = rb_filesystem_str_new_cstr(loading); return 's'; @@ -1112,19 +1132,19 @@ search_required(const rb_box_t *box, VALUE fname, volatile VALUE *path, feature_ rb_str_cat2(tmp, DLEXT); OBJ_FREEZE(tmp); if ((tmp = rb_find_file(tmp)) != 0) { - ext = strrchr(ftptr = RSTRING_PTR(tmp), '.'); + ext = find_ext(tmp, &ftptr, &ftend); if (!rb_feature_p(box, ftptr, ext, FALSE, TRUE, &loading) || loading) *path = tmp; return 's'; } } - else if (IS_DLEXT(ext)) { + else if (EXT_DLEXT_P(ext, ftend)) { if (rb_feature_p(box, ftptr, ext, FALSE, FALSE, &loading)) { if (loading) *path = rb_filesystem_str_new_cstr(loading); return 's'; } if ((tmp = rb_find_file(fname)) != 0) { - ext = strrchr(ftptr = RSTRING_PTR(tmp), '.'); + ext = find_ext(tmp, &ftptr, &ftend); if (!rb_feature_p(box, ftptr, ext, FALSE, TRUE, &loading) || loading) *path = tmp; return 's'; @@ -1173,7 +1193,7 @@ search_required(const rb_box_t *box, VALUE fname, volatile VALUE *path, feature_ } /* fall through */ case loadable_ext_rb: - ext = strrchr(ftptr = RSTRING_PTR(tmp), '.'); + ext = find_ext(tmp, &ftptr, &ftend); if (rb_feature_p(box, ftptr, ext, type == loadable_ext_rb, TRUE, &loading) && !loading) break; *path = tmp; diff --git a/missing/dtoa.c b/missing/dtoa.c index ba8cd46ebd9484..2a460959b07953 100644 --- a/missing/dtoa.c +++ b/missing/dtoa.c @@ -511,6 +511,9 @@ extern double rnd_prod(double, double), rnd_quot(double, double); #ifndef ATOMIC_PTR_CAS #define ATOMIC_PTR_CAS(var, old, new) ((var) = (new), (void *)(old)) #endif +#ifndef RUBY_ATOMIC_PTR_LOAD +#define RUBY_ATOMIC_PTR_LOAD(var) (var) +#endif #ifndef LIKELY #define LIKELY(x) (x) #endif @@ -863,10 +866,10 @@ pow5mult(Bigint *b, int k) } #define b_cache(var, addr, new_expr) \ - if ((var = addr) != 0) {} else { \ + if ((var = RUBY_ATOMIC_PTR_LOAD(addr)) != 0) {} else { \ Bigint *tmp = 0; \ ACQUIRE_DTOA_LOCK(1); \ - if (!(var = addr) && (var = (new_expr)) != 0) { \ + if (!(var = RUBY_ATOMIC_PTR_LOAD(addr)) && (var = (new_expr)) != 0) { \ var->next = 0; \ tmp = ATOMIC_PTR_CAS(addr, NULL, var); \ } \ diff --git a/missing/memrchr.c b/missing/memrchr.c new file mode 100644 index 00000000000000..89147aad22b1af --- /dev/null +++ b/missing/memrchr.c @@ -0,0 +1,12 @@ +#include "ruby/missing.h" + +void * +memrchr(const void *ptr, int ch, size_t len) +{ + const unsigned char *p = (const unsigned char *)ptr + len; + + while (p > (const unsigned char *)ptr) { + if (*--p == (unsigned char)ch) return (void *)p; + } + return NULL; +} diff --git a/object.c b/object.c index 3b382bcc5c0a7b..033b3dc35fb5de 100644 --- a/object.c +++ b/object.c @@ -337,21 +337,12 @@ rb_obj_copy_ivar(VALUE dest, VALUE obj) } shape_id_t src_shape_id = RBASIC_SHAPE_ID(obj); - - if (rb_shape_complex_p(src_shape_id)) { - rb_shape_copy_complex_ivars(dest, obj, src_shape_id, ROBJECT_FIELDS_HASH(obj)); - return; - } - shape_id_t initial_shape_id = RBASIC_SHAPE_ID(dest); RUBY_ASSERT(RSHAPE_TYPE_P(initial_shape_id, SHAPE_ROOT)); shape_id_t dest_shape_id = rb_shape_rebuild(initial_shape_id, src_shape_id); if (UNLIKELY(rb_shape_complex_p(dest_shape_id))) { - st_table *table = rb_st_init_numtable_with_size(src_num_ivs); - rb_obj_copy_ivs_to_hash_table(obj, table); - rb_obj_init_complex(dest, table); - + rb_obj_replace_fields(dest, rb_obj_complex_fields_build(obj)); return; } diff --git a/pathname_builtin.rb b/pathname_builtin.rb index dc9f81a3b369d8..da9806650d3685 100644 --- a/pathname_builtin.rb +++ b/pathname_builtin.rb @@ -305,7 +305,7 @@ def inspect # :nodoc: # :markup: markdown # # call-seq: - # mkpath(permissions = 0775) -> self + # mkpath(mode: nil) -> self # # Creates a directory at the path in `self`; # creates intermediate directories as needed: @@ -318,7 +318,7 @@ def inspect # :nodoc: # pn.rmtree # Clean up. # ``` # - # Directories are created with the given permissions; + # When `mode` is given, directories are created with those permissions; # see {File Permissions}[rdoc-ref:File@File+Permissions]. # The permissions for already-existing directories are not changed. def mkpath(mode: nil) diff --git a/prism/compiler/assume.h b/prism/compiler/assume.h index 32e68f829745b8..256e3873b88d7c 100644 --- a/prism/compiler/assume.h +++ b/prism/compiler/assume.h @@ -10,12 +10,12 @@ * range analysis so it can prune impossible paths. Use it to communicate an * invariant the caller guarantees but that the compiler cannot otherwise prove. */ -#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 202311L /* C23 or later */ && !defined(__APPLE__) - #define PRISM_ASSUME(expr_) [[assume(expr_)]] -#elif defined(__clang__) +#if defined(__clang__) #define PRISM_ASSUME(expr_) __builtin_assume(expr_) #elif defined(_MSC_VER) #define PRISM_ASSUME(expr_) __assume(expr_) +#elif defined(__GNUC__) && (__GNUC__ >= 13) + #define PRISM_ASSUME(expr_) __attribute__((assume(expr_))) #elif defined(__GNUC__) #define PRISM_ASSUME(expr_) ((expr_) ? (void) 0 : __builtin_unreachable()) #else diff --git a/prism_compile.c b/prism_compile.c index 6a0422c4f4daab..d1ed2cbb339cb6 100644 --- a/prism_compile.c +++ b/prism_compile.c @@ -3534,26 +3534,8 @@ pm_compile_builtin_attr(rb_iseq_t *iseq, pm_scope_node_t *scope_node, const pm_a } VALUE symbol = pm_static_literal_value(iseq, argument, scope_node); - VALUE string = rb_sym2str(symbol); - - if (strcmp(RSTRING_PTR(string), "leaf") == 0) { - ISEQ_BODY(iseq)->builtin_attrs |= BUILTIN_ATTR_LEAF; - } - else if (strcmp(RSTRING_PTR(string), "inline_block") == 0) { - ISEQ_BODY(iseq)->builtin_attrs |= BUILTIN_ATTR_INLINE_BLOCK; - } - else if (strcmp(RSTRING_PTR(string), "use_block") == 0) { - iseq_set_use_block(iseq); - } - else if (strcmp(RSTRING_PTR(string), "c_trace") == 0) { - // Let the iseq act like a C method in backtraces - ISEQ_BODY(iseq)->builtin_attrs |= BUILTIN_ATTR_C_TRACE; - } - else if (strcmp(RSTRING_PTR(string), "without_interrupts") == 0) { - ISEQ_BODY(iseq)->builtin_attrs |= BUILTIN_ATTR_WITHOUT_INTERRUPTS; - } - else { - COMPILE_ERROR(iseq, node_location->line, "unknown argument to attr!: %s", RSTRING_PTR(string)); + if (compile_builtin_attr_symbol(iseq, symbol) != COMPILE_OK) { + COMPILE_ERROR(iseq, node_location->line, "unknown argument to attr!: %" PRIsVALUE, symbol); return COMPILE_NG; } } @@ -11558,8 +11540,7 @@ pm_load_file(pm_parse_result_t *result, VALUE filepath, bool load_error) error = rb_exc_new3(rb_eLoadError, message); rb_ivar_set(error, rb_intern_const("@path"), filepath); } else { - error = rb_syserr_new(err, RSTRING_PTR(filepath)); - RB_GC_GUARD(filepath); + error = rb_syserr_new_str(err, filepath); } return error; diff --git a/proc.c b/proc.c index ec08f8ad52de0c..716e4586a2a6b3 100644 --- a/proc.c +++ b/proc.c @@ -19,6 +19,7 @@ #include "internal/object.h" #include "internal/proc.h" #include "internal/symbol.h" +#include "internal/vm.h" #include "method.h" #include "iseq.h" #include "vm_core.h" @@ -267,6 +268,9 @@ block_mark_and_move(struct rb_block *block) } } +/* hidden ivar holding a refined proc's cref; see Proc#refined */ +static ID id_refinements_cref; + static void proc_mark_and_move(void *ptr) { @@ -274,6 +278,24 @@ proc_mark_and_move(void *ptr) block_mark_and_move((struct rb_block *)&proc->block); } +const rb_cref_t * +rb_proc_refinements_cref(VALUE procval) +{ + rb_proc_t *proc; + GetProcPtr(procval, proc); + if (!proc->is_refined) return NULL; + return (const rb_cref_t *)rb_ivar_get(procval, id_refinements_cref); +} + +void +rb_proc_set_refinements_cref(VALUE procval, const rb_cref_t *cref) +{ + rb_proc_t *proc; + GetProcPtr(procval, proc); + rb_ivar_set(procval, id_refinements_cref, (VALUE)cref); + proc->is_refined = 1; +} + typedef struct { rb_proc_t basic; VALUE env[VM_ENV_DATA_SIZE + 1]; /* ..., envval */ @@ -318,7 +340,7 @@ rb_obj_is_proc(VALUE proc) static VALUE proc_clone(VALUE self) { - VALUE procval = rb_proc_dup(self); + VALUE procval = rb_proc_dup_0(self); return rb_obj_clone_setup(self, procval, Qnil); } @@ -326,10 +348,211 @@ proc_clone(VALUE self) static VALUE proc_dup(VALUE self) { - VALUE procval = rb_proc_dup(self); + VALUE procval = rb_proc_dup_0(self); return rb_obj_dup_setup(self, procval); } +rb_cref_t *rb_vm_get_cref(const VALUE *ep); +VALUE rb_proc_dup_with_iseq_and_cref(VALUE self, const rb_iseq_t *iseq, const rb_cref_t *cref); + +/* Proc#refined memoizes the most recent {copied iseq, cref} pair per + * source iseq, since rb_iseq_dup_with_independent_caches is expensive. + * The memo lives in a hidden identity Hash (source iseq -> frozen Array): + * + * [base_cref, copied_iseq, cref, mod1, mod2, ...] + * + * keyed by (base_cref, modules). + * An entry is retained for the VM's lifetime */ + +enum refinement_memo_index { + REFINEMENT_MEMO_BASE_CREF, /* key: captured cref of the source proc */ + REFINEMENT_MEMO_COPIED_ISEQ, /* value: copied iseq with independent caches */ + REFINEMENT_MEMO_CREF, /* value: cref with refinements activated */ + REFINEMENT_MEMO_MODS /* key: modules, in argument order */ +}; + +static VALUE refinement_memo_map; /* set once under the VM lock */ + +static bool +refinement_memo_key_match(VALUE memo, VALUE base_cref, long argc, const VALUE *mods) +{ + const VALUE *p = RARRAY_CONST_PTR(memo); + if (p[REFINEMENT_MEMO_BASE_CREF] != base_cref) return false; + if (RARRAY_LEN(memo) - REFINEMENT_MEMO_MODS != argc) return false; + for (long i = 0; i < argc; i++) { + if (p[REFINEMENT_MEMO_MODS + i] != mods[i]) return false; + } + return true; +} + +static bool +refinement_memo_lookup(const rb_iseq_t *src_iseq, VALUE base_cref, + long argc, const VALUE *mods, + const rb_iseq_t **iseq_out, const rb_cref_t **cref_out) +{ + VM_ASSERT(ISEQ_BODY(src_iseq)->type == ISEQ_TYPE_BLOCK); + VALUE memo = Qnil; + RB_VM_LOCKING() { + if (refinement_memo_map) { + memo = rb_hash_lookup(refinement_memo_map, (VALUE)src_iseq); + } + } + if (!NIL_P(memo)) { + const VALUE *p = RARRAY_CONST_PTR(memo); + if (refinement_memo_key_match(memo, base_cref, argc, mods)) { + const rb_iseq_t *copied_iseq = (const rb_iseq_t *)p[REFINEMENT_MEMO_COPIED_ISEQ]; + if (ISEQ_BODY(copied_iseq)->param.flags.ruby2_keywords == + ISEQ_BODY(src_iseq)->param.flags.ruby2_keywords) { + *iseq_out = copied_iseq; + *cref_out = (const rb_cref_t *)p[REFINEMENT_MEMO_CREF]; + return true; + } + rb_category_warn( + RB_WARN_CATEGORY_PERFORMANCE, + "Proc#refined re-copies the block because the ruby2_keywords flag changed after the copy was memoized" + ); + return false; + } + rb_category_warn( + RB_WARN_CATEGORY_PERFORMANCE, + "Proc#refined called with different modules for the same block disables memoization" + ); + } + return false; +} + +static void +refinement_memo_store(const rb_iseq_t *src_iseq, VALUE base_cref, + long argc, const VALUE *mods, + const rb_iseq_t *copied_iseq, const rb_cref_t *cref) +{ + VM_ASSERT(ISEQ_BODY(src_iseq)->type == ISEQ_TYPE_BLOCK); + + VALUE memo = rb_ary_hidden_new(REFINEMENT_MEMO_MODS + argc); + rb_ary_push(memo, base_cref); + rb_ary_push(memo, (VALUE)copied_iseq); + rb_ary_push(memo, (VALUE)cref); + for (long i = 0; i < argc; i++) { + rb_ary_push(memo, mods[i]); + } + OBJ_FREEZE(memo); + + /* create the map outside the lock; losing the race just discards it */ + VALUE new_map = 0; + if (!refinement_memo_map) { + new_map = rb_obj_hide(rb_ident_hash_new()); + } + + RB_VM_LOCKING() { + if (!refinement_memo_map) { + rb_vm_register_global_object(new_map); + refinement_memo_map = new_map; + } + rb_hash_aset(refinement_memo_map, (VALUE)src_iseq, memo); + } +} + +/* + * call-seq: + * prc.refined(mod, ...) -> a_proc + * + * Returns a new Proc that behaves like the receiver but with the refinements + * activated by the given modules in effect inside its body. The receiver is + * left unchanged. + * + * module StringRefinement + * refine String do + * def shout = upcase + "!" + * end + * end + * + * original = ->(s) { s.shout } + * refined_proc = original.refined(StringRefinement) + * refined_proc.call("hi") #=> "HI!" + * original.call("hi") #=> NoMethodError + * + * Only Procs created from a Ruby block are supported; calling this on a Proc + * backed by a C function, a Symbol, or a method raises ArgumentError. + * + * Calling this method on a Proc that already has refinements applied by this + * method also raises ArgumentError. To activate the refinements of multiple + * modules, pass them all in a single call: + * + * refined_proc = original.refined(StringRefinement, OtherRefinement) + * + * The refinement set of the returned Proc is fixed when it is created: + * calling +using+ inside its body raises RuntimeError. + * + * The refinements are in effect throughout the body, including nested blocks + * and methods defined with +def+ inside it. As with a +def+ inside a +using+ + * scope, such a method keeps the refinements even when it is called later: + * + * refined_proc = ->(s) { + * -> { s.shout }.call # nested block: "HI!" + * }.refined(StringRefinement) + * + * refined_proc = -> { + * obj = Object.new + * def obj.shout_hi = "hi".shout # the method sees the refinement + * obj.shout_hi #=> "HI!" + * }.refined(StringRefinement) + * + * This method copies the instruction sequence of the block and of all of its + * nested blocks so that the copy can resolve methods through the refinements + * without affecting the original Proc. Applying refinements therefore + * increases memory use roughly in proportion to the size of the block. The + * copy is cached and reused for the same receiver and the same modules. + */ +static VALUE +proc_refined(int argc, VALUE *argv, VALUE self) +{ + rb_proc_t *src; + GetProcPtr(self, src); + + rb_check_arity(argc, 1, UNLIMITED_ARGUMENTS); + + if (vm_block_type(&src->block) != block_type_iseq || src->is_from_method) { + rb_raise(rb_eArgError, "can't apply refinements to a Proc without a Ruby block"); + } + + if (src->is_refined) { + rb_raise(rb_eArgError, "can't apply refinements to a Proc that already has refinements"); + } + + for (int i = 0; i < argc; i++) { + Check_Type(argv[i], T_MODULE); + } + + VALUE base_cref = (VALUE)rb_vm_get_cref(src->block.as.captured.ep); + const rb_iseq_t *src_iseq = src->block.as.captured.code.iseq; + + const rb_iseq_t *new_iseq; + const rb_cref_t *new_cref; + if (!refinement_memo_lookup(src_iseq, base_cref, argc, argv, &new_iseq, &new_cref)) { + new_iseq = rb_iseq_dup_with_independent_caches(src_iseq); + rb_cref_t *cref = rb_vm_cref_dup((const rb_cref_t *)base_cref); + /* rb_using_module_recursive modifies shared subclass lists */ + RB_VM_LOCKING() { + for (int i = 0; i < argc; i++) { + rb_using_module_recursive(cref, argv[i]); + } + } + /* Freeze the refinements table and mark it shareable so the memoized + * cref can be reused from any Ractor. */ + VALUE refs = CREF_REFINEMENTS(cref); + if (!NIL_P(refs)) { + OBJ_FREEZE(refs); + RB_OBJ_SET_SHAREABLE(refs); + } + CREF_OMOD_SHARED_SET(cref); + CREF_REFINED_PROC_SET(cref); + new_cref = cref; + refinement_memo_store(src_iseq, base_cref, argc, argv, new_iseq, new_cref); + } + + return rb_proc_dup_with_iseq_and_cref(self, new_iseq, new_cref); +} + /* * call-seq: * prc.lambda? -> true or false @@ -1342,7 +1565,8 @@ rb_proc_call_kw(VALUE self, VALUE args, int kw_splat) VALUE *argv = RARRAY_PTR(args); GetProcPtr(self, proc); vret = rb_vm_invoke_proc(GET_EC(), proc, argc, argv, - kw_splat, VM_BLOCK_HANDLER_NONE); + kw_splat, VM_BLOCK_HANDLER_NONE, + rb_proc_refinements_cref(self)); RB_GC_GUARD(self); RB_GC_GUARD(args); return vret; @@ -1367,7 +1591,8 @@ rb_proc_call_with_block_kw(VALUE self, int argc, const VALUE *argv, VALUE passed VALUE vret; rb_proc_t *proc; GetProcPtr(self, proc); - vret = rb_vm_invoke_proc(ec, proc, argc, argv, kw_splat, proc_to_block_handler(passed_procval)); + vret = rb_vm_invoke_proc(ec, proc, argc, argv, kw_splat, proc_to_block_handler(passed_procval), + rb_proc_refinements_cref(self)); RB_GC_GUARD(self); return vret; } @@ -2695,6 +2920,14 @@ rb_mod_define_method_with_visibility(int argc, VALUE *argv, VALUE mod, const str RB_GC_GUARD(body); } else { + rb_proc_t *body_proc; + GetProcPtr(body, body_proc); + /* A bmethod never reads the refinement cref carried on the proc; + * reject rather than silently drop the refinements. */ + if (body_proc->is_refined) { + rb_raise(rb_eArgError, + "can't define a method from a Proc with refinements"); + } VALUE procval = rb_proc_dup(body); if (vm_proc_iseq(procval) != NULL) { rb_proc_t *proc; @@ -4901,6 +5134,8 @@ void Init_Proc(void) { #undef rb_intern + id_refinements_cref = rb_make_internal_id(); + VALUE mRuby = rb_define_module("Ruby"); /* Ruby::SourceRange */ @@ -4936,6 +5171,7 @@ Init_Proc(void) rb_define_method(rb_cProc, "arity", proc_arity, 0); rb_define_method(rb_cProc, "clone", proc_clone, 0); rb_define_method(rb_cProc, "dup", proc_dup, 0); + rb_define_method(rb_cProc, "refined", proc_refined, -1); rb_define_method(rb_cProc, "hash", proc_hash, 0); rb_define_method(rb_cProc, "to_s", proc_to_s, 0); rb_define_alias(rb_cProc, "inspect", "to_s"); diff --git a/process.c b/process.c index 8bb631fe5d46c6..82e3e376fe6f1e 100644 --- a/process.c +++ b/process.c @@ -154,7 +154,7 @@ static VALUE rb_cProcessTms; #define WSTOPSIG WEXITSTATUS #endif -#if defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__) || defined(__bsdi__) +#if defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__) #define HAVE_44BSD_SETUID 1 #define HAVE_44BSD_SETGID 1 #endif @@ -164,20 +164,11 @@ static VALUE rb_cProcessTms; #undef HAVE_SETRGID #endif -#ifdef BROKEN_SETREUID -#define setreuid ruby_setreuid -int setreuid(rb_uid_t ruid, rb_uid_t euid); -#endif -#ifdef BROKEN_SETREGID -#define setregid ruby_setregid -int setregid(rb_gid_t rgid, rb_gid_t egid); -#endif - #if defined(HAVE_44BSD_SETUID) || defined(__APPLE__) -#if !defined(USE_SETREUID) && !defined(BROKEN_SETREUID) +#if !defined(USE_SETREUID) #define OBSOLETE_SETREUID 1 #endif -#if !defined(USE_SETREGID) && !defined(BROKEN_SETREGID) +#if !defined(USE_SETREGID) #define OBSOLETE_SETREGID 1 #endif #endif @@ -2445,6 +2436,8 @@ compare_posix_sh(const void *key, const void *el) } #endif +#define append_terminator(buf) rb_str_buf_cat(buf, "", 1) /* append '\0' */ + static void rb_exec_fillarg(VALUE prog, int argc, VALUE *argv, VALUE env, VALUE opthash, VALUE execarg_obj) { @@ -2502,7 +2495,10 @@ rb_exec_fillarg(VALUE prog, int argc, VALUE *argv, VALUE env, VALUE opthash, VAL "while", /* reserved */ }; const char *p; + const char *const s = rb_str_null_check(prog); + const char *const e = RSTRING_END(prog); struct string_part first = {0, 0}; + int has_slash = 0; int has_meta = 0; /* * meta characters: @@ -2528,7 +2524,7 @@ rb_exec_fillarg(VALUE prog, int argc, VALUE *argv, VALUE env, VALUE opthash, VAL * = Assignment preceding command name * % (used in Parameter Expansion) */ - for (p = RSTRING_PTR(prog); *p; p++) { + for (p = s; p < e; p++) { if (*p == ' ' || *p == '\t') { if (first.ptr && !first.len) first.len = p - first.ptr; } @@ -2542,15 +2538,17 @@ rb_exec_fillarg(VALUE prog, int argc, VALUE *argv, VALUE env, VALUE opthash, VAL has_meta = 1; } else if (*p == '/') { - first.len = 0x100; /* longer than any posix_sh_cmds */ + has_slash = 1; } } if (has_meta) break; } - if (!has_meta && first.ptr) { + if (!has_meta) { + if (!first.ptr) first.ptr = e; if (!first.len) first.len = p - first.ptr; if (first.len > 0 && first.len <= sizeof(posix_sh_cmds[0]) && + !has_slash && bsearch(&first, posix_sh_cmds, numberof(posix_sh_cmds), sizeof(posix_sh_cmds[0]), compare_posix_sh)) has_meta = 1; } @@ -2561,21 +2559,22 @@ rb_exec_fillarg(VALUE prog, int argc, VALUE *argv, VALUE env, VALUE opthash, VAL if (!eargp->use_shell) { VALUE argv_buf; argv_buf = hide_obj(rb_str_buf_new(0)); - p = RSTRING_PTR(prog); - while (*p) { - while (*p == ' ' || *p == '\t') + rb_str_buf_cat(argv_buf, first.ptr, first.len); + append_terminator(argv_buf); + for (p = first.ptr + first.len; p < e;) { + while (p < e && (*p == ' ' || *p == '\t')) p++; - if (*p) { + if (p < e) { const char *w = p; - while (*p && *p != ' ' && *p != '\t') + while (p < e && *p != ' ' && *p != '\t') p++; rb_str_buf_cat(argv_buf, w, p-w); - rb_str_buf_cat(argv_buf, "", 1); /* append '\0' */ + append_terminator(argv_buf); } } eargp->invoke.cmd.argv_buf = argv_buf; eargp->invoke.cmd.command_name = - hide_obj(rb_str_subseq(argv_buf, 0, strlen(RSTRING_PTR(argv_buf)))); + hide_obj(rb_str_subseq(argv_buf, 0, first.len)); rb_enc_copy(eargp->invoke.cmd.command_name, prog); } } @@ -2605,7 +2604,8 @@ rb_exec_fillarg(VALUE prog, int argc, VALUE *argv, VALUE env, VALUE opthash, VAL arg = EXPORT_STR(arg); s = RSTRING_PTR(arg); #endif - rb_str_buf_cat(argv_buf, s, RSTRING_LEN(arg) + 1); /* include '\0' */ + rb_str_buf_cat(argv_buf, s, RSTRING_LEN(arg)); + append_terminator(argv_buf); } eargp->invoke.cmd.argv_buf = argv_buf; } @@ -2685,7 +2685,7 @@ fill_envp_buf_i(st_data_t st_key, st_data_t st_val, st_data_t arg) rb_str_buf_cat2(envp_buf, StringValueCStr(key)); rb_str_buf_cat2(envp_buf, "="); rb_str_buf_cat2(envp_buf, StringValueCStr(val)); - rb_str_buf_cat(envp_buf, "", 1); /* append '\0' */ + append_terminator(envp_buf); return ST_CONTINUE; } @@ -6293,21 +6293,6 @@ proc_setuid(VALUE obj, VALUE id) static rb_uid_t SAVED_USER_ID = -1; -#ifdef BROKEN_SETREUID -int -setreuid(rb_uid_t ruid, rb_uid_t euid) -{ - if (ruid != (rb_uid_t)-1 && ruid != getuid()) { - if (euid == (rb_uid_t)-1) euid = geteuid(); - if (setuid(ruid) < 0) return -1; - } - if (euid != (rb_uid_t)-1 && euid != geteuid()) { - if (seteuid(euid) < 0) return -1; - } - return 0; -} -#endif - /* * call-seq: * Process::UID.change_privilege(user) -> integer @@ -7007,21 +6992,6 @@ rb_daemon(int nochdir, int noclose) static rb_gid_t SAVED_GROUP_ID = -1; -#ifdef BROKEN_SETREGID -int -setregid(rb_gid_t rgid, rb_gid_t egid) -{ - if (rgid != (rb_gid_t)-1 && rgid != getgid()) { - if (egid == (rb_gid_t)-1) egid = getegid(); - if (setgid(rgid) < 0) return -1; - } - if (egid != (rb_gid_t)-1 && egid != getegid()) { - if (setegid(egid) < 0) return -1; - } - return 0; -} -#endif - /* * call-seq: * Process::GID.change_privilege(group) -> integer diff --git a/set.c b/set.c index 4e76750a9f0400..2efdab74173dae 100644 --- a/set.c +++ b/set.c @@ -1198,7 +1198,7 @@ set_i_intersection(VALUE set, VALUE other) * * Returns whether the given +object+ is an element of +self+: * - * set = [0, :zero, '0'] + * set = Set[0, :zero, '0'] * set.include?('0') # => true * set.include?('zero') # => false * @@ -1585,7 +1585,7 @@ set_each_i(st_data_t key, st_data_t dummy) * * sum = 0 * Set[1, 2, 3].each {|i| sum += i } - * sum => 6 + * sum # => 6 * * With no block given, returns an Enumerator. */ diff --git a/shape.c b/shape.c index 8c1955fc939c43..c54c86b937c923 100644 --- a/shape.c +++ b/shape.c @@ -33,8 +33,6 @@ #define MAX_SHAPE_ID (INVALID_SHAPE_ID - 1) #define ANCESTOR_SEARCH_MAX_DEPTH 2 -static ID id_object_id; - // Should be on its own cache line static RUBY_ALIGNAS(128) rb_atomic_t redblack_cache_size; @@ -701,7 +699,7 @@ rb_shape_transition_object_id(shape_id_t original_shape_id) bool dont_care; rb_shape_t *shape = NULL; if (LIKELY(original_shape->next_field_index < rb_shape_max_capacity())) { - shape = get_next_shape_internal(original_shape, id_object_id, SHAPE_OBJ_ID, &dont_care, true); + shape = get_next_shape_internal(original_shape, rb_shape_tree.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); @@ -1093,8 +1091,16 @@ shape_rebuild(rb_shape_t *initial_shape, rb_shape_t *dest_shape) shape_id_t rb_shape_rebuild(shape_id_t initial_shape_id, shape_id_t dest_shape_id) { - RUBY_ASSERT(!rb_shape_complex_p(initial_shape_id)); - RUBY_ASSERT(!rb_shape_complex_p(dest_shape_id)); + RUBY_ASSERT(RSHAPE_TYPE_P(initial_shape_id, SHAPE_ROOT)); + + if (RB_UNLIKELY(rb_shape_complex_p(initial_shape_id))) { + // The class has been marked as too complex. + return initial_shape_id; + } + + if (RB_UNLIKELY(rb_shape_complex_p(dest_shape_id))) { + return rb_shape_transition_complex(initial_shape_id); + } shape_id_t next_shape_id; // The shape has a SHAPE_OBJ_ID edge, it needs to be rebuilt. @@ -1146,19 +1152,6 @@ rb_shape_copy_fields(VALUE dest, VALUE *dest_buf, shape_id_t dest_shape_id, VALU } } -void -rb_shape_copy_complex_ivars(VALUE dest, VALUE obj, shape_id_t src_shape_id, st_table *fields_table) -{ - // obj is COMPLEX so we can copy its iv_hash - st_table *table = st_copy(fields_table); - if (rb_shape_has_object_id(src_shape_id)) { - st_data_t id = (st_data_t)id_object_id; - st_delete(table, &id, NULL); - } - rb_obj_init_complex(dest, table); - rb_gc_writebarrier_remember(dest); -} - size_t rb_shape_edges_count(shape_id_t shape_id) { @@ -1295,36 +1288,39 @@ rb_shape_verify_consistency(VALUE obj, shape_id_t shape_id) rb_shape_t *shape = RSHAPE(shape_id); - bool has_object_id = false; - while (shape->parent_offset != INVALID_SHAPE_ID) { - if (shape->type == SHAPE_OBJ_ID) { - has_object_id = true; - break; - } - shape = RSHAPE(shape->parent_offset); - } - - if (rb_shape_has_object_id(shape_id)) { - if (!has_object_id) { - 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_bug("shape_id claim not having obj_id but it does shape_id=%u, obj=%s", shape_id, rb_obj_info(obj)); - } - } - // Make sure SHAPE_ID_HAS_IVAR_MASK is valid. if (rb_shape_complex_p(shape_id)) { RUBY_ASSERT(shape_id & SHAPE_ID_HAS_IVAR_MASK); // Ensure complex object don't appear as embedded - if (RB_TYPE_P(obj, T_OBJECT) || IMEMO_TYPE_P(obj, imemo_fields)) { + if (RB_TYPE_P(obj, T_OBJECT)) { RUBY_ASSERT(FL_TEST_RAW(obj, ROBJECT_HEAP)); } + else if (IMEMO_TYPE_P(obj, imemo_fields)) { + RUBY_ASSERT(!FL_TEST_RAW(obj, ROBJECT_HEAP)); + } } else { + bool has_object_id = false; + while (shape->parent_offset != INVALID_SHAPE_ID) { + if (shape->type == SHAPE_OBJ_ID) { + has_object_id = true; + break; + } + shape = RSHAPE(shape->parent_offset); + } + + if (rb_shape_has_object_id(shape_id)) { + if (!has_object_id) { + 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_bug("shape_id claim not having obj_id but it does shape_id=%u, obj=%s", shape_id, rb_obj_info(obj)); + } + } + attr_index_t ivar_count = RSHAPE_LEN(shape_id); if (has_object_id) { ivar_count--; @@ -1614,7 +1610,7 @@ Init_default_shapes(void) rb_memerror(); } - id_object_id = rb_make_internal_id(); + rb_shape_tree.id_object_id = rb_make_internal_id(); #ifdef HAVE_MMAP size_t shape_cache_mmap_size = rb_size_mul_or_raise(REDBLACK_CACHE_SIZE, sizeof(redblack_node_t), rb_eRuntimeError); @@ -1649,11 +1645,11 @@ Init_default_shapes(void) RUBY_ASSERT(!(SHAPE_OFFSET(root) & SHAPE_ID_HAS_IVAR_MASK)); bool dontcare; - rb_shape_t *root_with_obj_id = get_next_shape_internal(root, id_object_id, SHAPE_OBJ_ID, &dontcare, true); + rb_shape_t *root_with_obj_id = get_next_shape_internal(root, rb_shape_tree.id_object_id, SHAPE_OBJ_ID, &dontcare, true); RUBY_ASSERT(root_with_obj_id); RUBY_ASSERT(SHAPE_OFFSET(root_with_obj_id) == ROOT_SHAPE_WITH_OBJ_ID); RUBY_ASSERT(root_with_obj_id->type == SHAPE_OBJ_ID); - RUBY_ASSERT(root_with_obj_id->edge_name == id_object_id); + RUBY_ASSERT(root_with_obj_id->edge_name == rb_shape_tree.id_object_id); RUBY_ASSERT(root_with_obj_id->next_field_index == 1); RUBY_ASSERT(!(SHAPE_OFFSET(root_with_obj_id) & SHAPE_ID_HAS_IVAR_MASK)); (void)root_with_obj_id; diff --git a/shape.h b/shape.h index 51ebf4f72d762f..f885bab74f6d85 100644 --- a/shape.h +++ b/shape.h @@ -134,6 +134,7 @@ enum shape_flags { typedef struct { rb_shape_t *shape_list; attr_index_t max_capacity; + ID id_object_id; } rb_shape_tree_t; RUBY_SYMBOL_EXPORT_BEGIN @@ -267,7 +268,6 @@ shape_id_t rb_shape_transition_add_ivar_no_warnings(shape_id_t shape_id, ID id, shape_id_t rb_shape_object_id(shape_id_t original_shape_id); shape_id_t rb_shape_rebuild(shape_id_t initial_shape_id, shape_id_t dest_shape_id); void rb_shape_copy_fields(VALUE dest, VALUE *dest_buf, shape_id_t dest_shape_id, VALUE *src_buf, shape_id_t src_shape_id); -void rb_shape_copy_complex_ivars(VALUE dest, VALUE obj, shape_id_t src_shape_id, st_table *fields_table); static inline bool rb_shape_frozen_p(shape_id_t shape_id) @@ -497,10 +497,22 @@ rb_obj_using_gen_fields_table_p(VALUE obj) return rb_obj_gen_fields_p(obj); } +static inline shape_id_t +rb_shape_transition_layout(shape_id_t shape_id, shape_id_t layout) +{ + return (shape_id & (~SHAPE_ID_LAYOUT_MASK)) | layout; +} + static inline shape_id_t rb_shape_transition_robject(shape_id_t shape_id) { - return (shape_id & ~SHAPE_ID_LAYOUT_MASK) | SHAPE_ID_LAYOUT_ROBJECT; + return rb_shape_transition_layout(shape_id, SHAPE_ID_LAYOUT_ROBJECT); +} + +static inline shape_id_t +rb_shape_transition_extended(shape_id_t shape_id) +{ + return rb_shape_transition_layout(shape_id, SHAPE_ID_LAYOUT_EXTENDED); } static inline shape_id_t @@ -548,12 +560,6 @@ rb_shape_transition_slot_size(shape_id_t shape_id, size_t slot_size) return rb_shape_transition_capacity(shape_id, rb_shape_capacity_for_slot_size(slot_size)); } -static inline shape_id_t -rb_shape_transition_layout(shape_id_t shape_id, shape_id_t layout) -{ - return (shape_id & (~SHAPE_ID_LAYOUT_MASK)) | layout; -} - shape_id_t rb_shape_transition_object_id(shape_id_t shape_id); static inline shape_id_t diff --git a/spec/bundler/commands/newgem_options_spec.rb b/spec/bundler/commands/newgem_options_spec.rb new file mode 100644 index 00000000000000..0b2e6af14325fb --- /dev/null +++ b/spec/bundler/commands/newgem_options_spec.rb @@ -0,0 +1,1454 @@ +# frozen_string_literal: true + +RSpec.describe "bundle gem" do + def gem_skeleton_assertions + expect(bundled_app("#{gem_name}/#{gem_name}.gemspec")).to exist + expect(bundled_app("#{gem_name}/README.md")).to exist + expect(bundled_app("#{gem_name}/Gemfile")).to exist + expect(bundled_app("#{gem_name}/Rakefile")).to exist + expect(bundled_app("#{gem_name}/lib/#{gem_name}.rb")).to exist + expect(bundled_app("#{gem_name}/lib/#{gem_name}/version.rb")).to exist + + expect(ignore_paths).to include("bin/") + expect(ignore_paths).to include("Gemfile") + end + + def bundle_exec_rubocop + prepare_gemspec(bundled_app(gem_name, "#{gem_name}.gemspec")) + bundle "config set path #{rubocop_gem_path}", dir: bundled_app(gem_name) + bundle "exec rubocop --debug --config .rubocop.yml", dir: bundled_app(gem_name) + end + + def bundle_exec_standardrb + prepare_gemspec(bundled_app(gem_name, "#{gem_name}.gemspec")) + bundle "config set path #{standard_gem_path}", dir: bundled_app(gem_name) + bundle "exec standardrb --debug", dir: bundled_app(gem_name) + end + + def ignore_paths + generated = bundled_app("#{gem_name}/#{gem_name}.gemspec").read + matched = generated.match(/^\s+f\.start_with\?\(\*%w\[(?.*)\]\)$/) + matched[:ignored]&.split(" ") + end + + def installed_go? + sys_exec("go version", raise_on_error: true) + true + rescue StandardError + false + end + + let(:generated_gemspec) { Bundler.load_gemspec_uncached(bundled_app(gem_name).join("#{gem_name}.gemspec")) } + + let(:gem_name) { "mygem" } + + before do + # Write the global git config directly instead of shelling out to `git + # config --global` three times per example: this `before` runs for every + # example in the file, and each `git` call is a separate subprocess. + File.write(home(".gitconfig"), <<~GITCONFIG) + [user] + name = Bundler User + email = user@example.com + [github] + user = bundleuser + GITCONFIG + + bundle_config_global "gem.mit false" + bundle_config_global "gem.test false" + bundle_config_global "gem.coc false" + bundle_config_global "gem.linter false" + bundle_config_global "gem.ci false" + bundle_config_global "gem.changelog false" + bundle_config_global "gem.bundle false" + end + + shared_examples_for "--mit flag" do + before do + bundle "gem #{gem_name} --mit" + end + it "generates a gem skeleton with MIT license" do + gem_skeleton_assertions + expect(bundled_app("#{gem_name}/LICENSE.txt")).to exist + expect(generated_gemspec.license).to eq("MIT") + end + end + + shared_examples_for "--no-mit flag" do + before do + bundle "gem #{gem_name} --no-mit" + end + it "generates a gem skeleton without MIT license" do + gem_skeleton_assertions + expect(bundled_app("#{gem_name}/LICENSE.txt")).to_not exist + end + end + + shared_examples_for "--coc flag" do + it "generates a gem skeleton with MIT license" do + bundle "gem #{gem_name} --coc" + gem_skeleton_assertions + expect(bundled_app("#{gem_name}/CODE_OF_CONDUCT.md")).to exist + end + + it "generates the README with a section for the Code of Conduct" do + bundle "gem #{gem_name} --coc" + expect(bundled_app("#{gem_name}/README.md").read).to include("## Code of Conduct") + expect(bundled_app("#{gem_name}/README.md").read).to match(%r{https://github\.com/bundleuser/#{gem_name}/blob/.*/CODE_OF_CONDUCT.md}) + end + + it "generates the README with a section for the Code of Conduct, respecting the configured git default branch", git: ">= 2.28.0" do + git("config --global init.defaultBranch main") + bundle "gem #{gem_name} --coc" + + expect(bundled_app("#{gem_name}/README.md").read).to include("## Code of Conduct") + expect(bundled_app("#{gem_name}/README.md").read).to include("https://github.com/bundleuser/#{gem_name}/blob/main/CODE_OF_CONDUCT.md") + end + end + + shared_examples_for "--no-coc flag" do + before do + bundle "gem #{gem_name} --no-coc" + end + it "generates a gem skeleton without Code of Conduct" do + gem_skeleton_assertions + expect(bundled_app("#{gem_name}/CODE_OF_CONDUCT.md")).to_not exist + end + + it "generates the README without a section for the Code of Conduct" do + expect(bundled_app("#{gem_name}/README.md").read).not_to include("## Code of Conduct") + expect(bundled_app("#{gem_name}/README.md").read).not_to match(%r{https://github\.com/bundleuser/#{gem_name}/blob/.*/CODE_OF_CONDUCT.md}) + end + end + + shared_examples_for "--changelog flag" do + before do + bundle "gem #{gem_name} --changelog" + end + it "generates a gem skeleton with a CHANGELOG" do + gem_skeleton_assertions + expect(bundled_app("#{gem_name}/CHANGELOG.md")).to exist + end + end + + shared_examples_for "--no-changelog flag" do + before do + bundle "gem #{gem_name} --no-changelog" + end + it "generates a gem skeleton without a CHANGELOG" do + gem_skeleton_assertions + expect(bundled_app("#{gem_name}/CHANGELOG.md")).to_not exist + end + end + + shared_examples_for "--bundle flag" do + before do + bundle "gem #{gem_name} --bundle" + end + it "generates a gem skeleton with bundle install" do + gem_skeleton_assertions + expect(out).to include("Running bundle install in the new gem directory.") + end + end + + shared_examples_for "--no-bundle flag" do + before do + bundle "gem #{gem_name} --no-bundle" + end + it "generates a gem skeleton without bundle install" do + gem_skeleton_assertions + expect(out).to_not include("Running bundle install in the new gem directory.") + end + end + + shared_examples_for "--linter=rubocop flag" do + before do + bundle "gem #{gem_name} --linter=rubocop" + end + + it "generates a gem skeleton with rubocop" do + gem_skeleton_assertions + expect(bundled_app("#{gem_name}/Rakefile")).to read_as( + include("# frozen_string_literal: true"). + and(include('require "rubocop/rake_task"'). + and(include("RuboCop::RakeTask.new"). + and(match(/default:.+:rubocop/)))) + ) + end + + it "includes rubocop in generated Gemfile" do + allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile) + builder = Bundler::Dsl.new + builder.eval_gemfile(bundled_app("#{gem_name}/Gemfile")) + builder.dependencies + rubocop_dep = builder.dependencies.find {|d| d.name == "rubocop" } + expect(rubocop_dep).not_to be_specific + expect(rubocop_dep.requirement).to eq(Gem::Requirement.new([">= 0"])) + end + + it "generates a default .rubocop.yml" do + expect(bundled_app("#{gem_name}/.rubocop.yml")).to exist + end + + it "includes .rubocop.yml into ignore list" do + expect(ignore_paths).to include(".rubocop.yml") + end + end + + shared_examples_for "--linter=standard flag" do + before do + bundle "gem #{gem_name} --linter=standard" + end + + it "generates a gem skeleton with standard" do + gem_skeleton_assertions + expect(bundled_app("#{gem_name}/Rakefile")).to read_as( + include('require "standard/rake"'). + and(match(/default:.+:standard/)) + ) + end + + it "includes standard in generated Gemfile" do + allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile) + builder = Bundler::Dsl.new + builder.eval_gemfile(bundled_app("#{gem_name}/Gemfile")) + builder.dependencies + standard_dep = builder.dependencies.find {|d| d.name == "standard" } + expect(standard_dep).not_to be_specific + expect(standard_dep.requirement).to eq(Gem::Requirement.new([">= 0"])) + end + + it "generates a default .standard.yml" do + expect(bundled_app("#{gem_name}/.standard.yml")).to exist + end + + it "includes .standard.yml into ignore list" do + expect(ignore_paths).to include(".standard.yml") + end + end + + shared_examples_for "--no-linter flag" do + define_negated_matcher :exclude, :include + + before do + bundle "gem #{gem_name} --no-linter" + end + + it "generates a gem skeleton without rubocop" do + gem_skeleton_assertions + expect(bundled_app("#{gem_name}/Rakefile")).to read_as(exclude("rubocop")) + expect(bundled_app("#{gem_name}/#{gem_name}.gemspec")).to read_as(exclude("rubocop")) + end + + it "does not include rubocop in generated Gemfile" do + allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile) + builder = Bundler::Dsl.new + builder.eval_gemfile(bundled_app("#{gem_name}/Gemfile")) + builder.dependencies + rubocop_dep = builder.dependencies.find {|d| d.name == "rubocop" } + expect(rubocop_dep).to be_nil + end + + it "does not include standard in generated Gemfile" do + allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile) + builder = Bundler::Dsl.new + builder.eval_gemfile(bundled_app("#{gem_name}/Gemfile")) + builder.dependencies + standard_dep = builder.dependencies.find {|d| d.name == "standard" } + expect(standard_dep).to be_nil + end + + it "doesn't generate a default .rubocop.yml" do + expect(bundled_app("#{gem_name}/.rubocop.yml")).to_not exist + end + + it "does not add .rubocop.yml into ignore list" do + expect(ignore_paths).not_to include(".rubocop.yml") + end + + it "doesn't generate a default .standard.yml" do + expect(bundled_app("#{gem_name}/.standard.yml")).to_not exist + end + + it "does not add .standard.yml into ignore list" do + expect(ignore_paths).not_to include(".standard.yml") + end + end + + shared_examples_for "CI config is absent" do + it "does not create any CI files" do + expect(bundled_app("#{gem_name}/.github/workflows/main.yml")).to_not exist + expect(bundled_app("#{gem_name}/.gitlab-ci.yml")).to_not exist + expect(bundled_app("#{gem_name}/.circleci/config.yml")).to_not exist + end + end + + shared_examples_for "--github-username option" do |github_username| + before do + bundle "gem #{gem_name} --github-username=#{github_username}" + end + + it "generates a gem skeleton" do + gem_skeleton_assertions + end + + it "contribute URL set to given github username" do + expect(bundled_app("#{gem_name}/README.md").read).not_to include("[USERNAME]") + expect(bundled_app("#{gem_name}/README.md").read).to include("github.com/#{github_username}") + end + end + + shared_examples_for "github_username configuration" do + context "with github_username setting set to some value" do + before do + bundle_config_global "gem.github_username different_username" + bundle "gem #{gem_name}" + end + + it "generates a gem skeleton" do + gem_skeleton_assertions + end + + it "contribute URL set to bundle config setting" do + expect(bundled_app("#{gem_name}/README.md").read).not_to include("[USERNAME]") + expect(bundled_app("#{gem_name}/README.md").read).to include("github.com/different_username") + end + end + + context "with github_username setting set to false" do + before do + bundle_config_global "gem.github_username false" + bundle "gem #{gem_name}" + end + + it "generates a gem skeleton" do + gem_skeleton_assertions + end + + it "contribute URL set to [USERNAME]" do + expect(bundled_app("#{gem_name}/README.md").read).to include("[USERNAME]") + expect(bundled_app("#{gem_name}/README.md").read).not_to include("github.com/bundleuser") + end + end + end + + context "--ci with no argument" do + before do + bundle "gem #{gem_name}" + end + + it "does not generate any CI config" do + expect(bundled_app("#{gem_name}/.github/workflows/main.yml")).to_not exist + expect(bundled_app("#{gem_name}/.gitlab-ci.yml")).to_not exist + expect(bundled_app("#{gem_name}/.circleci/config.yml")).to_not exist + end + + it "does not add any CI config files into ignore list" do + expect(ignore_paths).not_to include(".github/") + expect(ignore_paths).not_to include(".gitlab-ci.yml") + expect(ignore_paths).not_to include(".circleci/") + end + end + + context "--ci set to github" do + before do + bundle "gem #{gem_name} --ci=github" + end + + it "generates a GitHub Actions config file" do + expect(bundled_app("#{gem_name}/.github/workflows/main.yml")).to exist + end + + it "includes .github/ into ignore list" do + expect(ignore_paths).to include(".github/") + end + end + + context "--ci set to gitlab" do + before do + bundle "gem #{gem_name} --ci=gitlab" + end + + it "generates a GitLab CI config file" do + expect(bundled_app("#{gem_name}/.gitlab-ci.yml")).to exist + end + + it "includes .gitlab-ci.yml into ignore list" do + expect(ignore_paths).to include(".gitlab-ci.yml") + end + end + + context "--ci set to circle" do + before do + bundle "gem #{gem_name} --ci=circle" + end + + it "generates a CircleCI config file" do + expect(bundled_app("#{gem_name}/.circleci/config.yml")).to exist + end + + it "includes .circleci/ into ignore list" do + expect(ignore_paths).to include(".circleci/") + end + end + + context "--ci set to an invalid value" do + before do + bundle "gem #{gem_name} --ci=foo", raise_on_error: false + end + + it "fails loudly" do + expect(last_command).to be_failure + expect(err).to match(/Expected '--ci' to be one of .*; got foo/) + end + end + + context "gem.ci setting set to none" do + it "doesn't generate any CI config" do + expect(bundled_app("#{gem_name}/.github/workflows/main.yml")).to_not exist + expect(bundled_app("#{gem_name}/.gitlab-ci.yml")).to_not exist + expect(bundled_app("#{gem_name}/.circleci/config.yml")).to_not exist + end + end + + context "gem.ci setting set to github" do + it "generates a GitHub Actions config file" do + bundle_config "gem.ci github" + bundle "gem #{gem_name}" + + expect(bundled_app("#{gem_name}/.github/workflows/main.yml")).to exist + end + end + + context "gem.ci setting set to gitlab" do + it "generates a GitLab CI config file" do + bundle_config "gem.ci gitlab" + bundle "gem #{gem_name}" + + expect(bundled_app("#{gem_name}/.gitlab-ci.yml")).to exist + end + end + + context "gem.ci setting set to circle" do + it "generates a CircleCI config file" do + bundle_config "gem.ci circle" + bundle "gem #{gem_name}" + + expect(bundled_app("#{gem_name}/.circleci/config.yml")).to exist + end + end + + context "gem.ci set to github and --ci with no arguments" do + before do + bundle_config "gem.ci github" + bundle "gem #{gem_name} --ci" + end + + it "generates a GitHub Actions config file" do + expect(bundled_app("#{gem_name}/.github/workflows/main.yml")).to exist + end + + it "hints that --ci is already configured" do + expect(out).to match("github is already configured, ignoring --ci flag.") + end + end + + context "gem.ci setting set to false and --ci with no arguments", :readline do + before do + bundle_config "gem.ci false" + bundle "gem #{gem_name} --ci" do |input, _, _| + input.puts "github" + end + end + + it "asks to setup CI" do + expect(out).to match("Do you want to set up continuous integration for your gem?") + end + + it "hints that the choice will only be applied to the current gem" do + expect(out).to match("Your choice will only be applied to this gem.") + end + end + + context "gem.ci setting not set and --ci with no arguments", :readline do + before do + bundle_config_global "BUNDLE_GEM__CI" => nil + bundle "gem #{gem_name} --ci" do |input, _, _| + input.puts "github" + end + end + + it "asks to setup CI" do + expect(out).to match("Do you want to set up continuous integration for your gem?") + end + + it "hints that the choice will be applied to future bundle gem calls" do + hint = "Future `bundle gem` calls will use your choice. " \ + "This setting can be changed anytime with `bundle config gem.ci`." + expect(out).to match(hint) + end + end + + context "gem.ci setting set to a CI service and --no-ci" do + before do + bundle_config "gem.ci github" + bundle "gem #{gem_name} --no-ci" + end + + it "does not generate any CI config" do + expect(bundled_app("#{gem_name}/.github/workflows/main.yml")).to_not exist + expect(bundled_app("#{gem_name}/.gitlab-ci.yml")).to_not exist + expect(bundled_app("#{gem_name}/.circleci/config.yml")).to_not exist + end + end + + context "--linter with no argument" do + before do + bundle "gem #{gem_name}" + end + + it "does not generate any linter config" do + expect(bundled_app("#{gem_name}/.rubocop.yml")).to_not exist + expect(bundled_app("#{gem_name}/.standard.yml")).to_not exist + end + + it "does not add any linter config files into ignore list" do + expect(ignore_paths).not_to include(".rubocop.yml") + expect(ignore_paths).not_to include(".standard.yml") + end + end + + context "--linter set to rubocop" do + before do + bundle "gem #{gem_name} --linter=rubocop" + end + + it "generates a RuboCop config" do + expect(bundled_app("#{gem_name}/.rubocop.yml")).to exist + expect(bundled_app("#{gem_name}/.standard.yml")).to_not exist + end + + it "includes .rubocop.yml into ignore list" do + expect(ignore_paths).to include(".rubocop.yml") + expect(ignore_paths).not_to include(".standard.yml") + end + end + + context "--linter set to standard" do + before do + bundle "gem #{gem_name} --linter=standard" + end + + it "generates a Standard config" do + expect(bundled_app("#{gem_name}/.standard.yml")).to exist + expect(bundled_app("#{gem_name}/.rubocop.yml")).to_not exist + end + + it "includes .standard.yml into ignore list" do + expect(ignore_paths).to include(".standard.yml") + expect(ignore_paths).not_to include(".rubocop.yml") + end + end + + context "--linter set to an invalid value" do + before do + bundle "gem #{gem_name} --linter=foo", raise_on_error: false + end + + it "fails loudly" do + expect(last_command).to be_failure + expect(err).to match(/Expected '--linter' to be one of .*; got foo/) + end + end + + context "gem.linter setting set to none" do + before do + bundle "gem #{gem_name}" + end + + it "doesn't generate any linter config" do + expect(bundled_app("#{gem_name}/.rubocop.yml")).to_not exist + expect(bundled_app("#{gem_name}/.standard.yml")).to_not exist + end + + it "does not add any linter config files into ignore list" do + expect(ignore_paths).not_to include(".rubocop.yml") + expect(ignore_paths).not_to include(".standard.yml") + end + end + + context "gem.linter setting set to rubocop" do + before do + bundle_config "gem.linter rubocop" + bundle "gem #{gem_name}" + end + + it "generates a RuboCop config file" do + expect(bundled_app("#{gem_name}/.rubocop.yml")).to exist + end + + it "includes .rubocop.yml into ignore list" do + expect(ignore_paths).to include(".rubocop.yml") + end + end + + context "gem.linter setting set to standard" do + before do + bundle_config "gem.linter standard" + bundle "gem #{gem_name}" + end + + it "generates a Standard config file" do + expect(bundled_app("#{gem_name}/.standard.yml")).to exist + end + + it "includes .standard.yml into ignore list" do + expect(ignore_paths).to include(".standard.yml") + end + end + + context "gem.linter set to rubocop and --linter with no arguments" do + before do + bundle_config "gem.linter rubocop" + bundle "gem #{gem_name} --linter" + end + + it "generates a RuboCop config file" do + expect(bundled_app("#{gem_name}/.rubocop.yml")).to exist + end + + it "includes .rubocop.yml into ignore list" do + expect(ignore_paths).to include(".rubocop.yml") + end + + it "hints that --linter is already configured" do + expect(out).to match("rubocop is already configured, ignoring --linter flag.") + end + end + + context "gem.linter setting set to false and --linter with no arguments", :readline do + before do + bundle_config "gem.linter false" + bundle "gem #{gem_name} --linter" do |input, _, _| + input.puts "rubocop" + end + end + + it "asks to setup a linter" do + expect(out).to match("Do you want to add a code linter and formatter to your gem?") + end + + it "hints that the choice will only be applied to the current gem" do + expect(out).to match("Your choice will only be applied to this gem.") + end + end + + context "gem.linter setting not set and --linter with no arguments", :readline do + before do + bundle_config_global "BUNDLE_GEM__LINTER" => nil + bundle "gem #{gem_name} --linter" do |input, _, _| + input.puts "rubocop" + end + end + + it "asks to setup a linter" do + expect(out).to match("Do you want to add a code linter and formatter to your gem?") + end + + it "hints that the choice will be applied to future bundle gem calls" do + hint = "Future `bundle gem` calls will use your choice. " \ + "This setting can be changed anytime with `bundle config gem.linter`." + expect(out).to match(hint) + end + end + + context "gem.linter setting set to a linter and --no-linter" do + before do + bundle_config "gem.linter rubocop" + bundle "gem #{gem_name} --no-linter" + end + + it "does not generate any linter config" do + expect(bundled_app("#{gem_name}/.rubocop.yml")).to_not exist + expect(bundled_app("#{gem_name}/.standard.yml")).to_not exist + end + + it "does not add any linter config files into ignore list" do + expect(ignore_paths).not_to include(".rubocop.yml") + expect(ignore_paths).not_to include(".standard.yml") + end + end + + context "--edit option" do + it "opens the generated gemspec in the user's text editor" do + output = bundle "gem #{gem_name} --edit=echo" + gemspec_path = File.join(bundled_app, gem_name, "#{gem_name}.gemspec") + expect(output).to include("echo \"#{gemspec_path}\"") + end + end + + shared_examples_for "paths that depend on gem name" do + it "generates entrypoint, version file and signatures file at the proper path, with the proper content" do + bundle "gem #{gem_name}" + + expect(bundled_app("#{gem_name}/lib/#{require_path}.rb")).to exist + expect(bundled_app("#{gem_name}/lib/#{require_path}.rb").read).to match(%r{require_relative "#{require_relative_path}/version"}) + expect(bundled_app("#{gem_name}/lib/#{require_path}.rb").read).to match(/class Error < StandardError; end$/) + + expect(bundled_app("#{gem_name}/lib/#{require_path}/version.rb")).to exist + expect(bundled_app("#{gem_name}/sig/#{require_path}.rbs")).to exist + end + + context "--exe parameter set" do + before do + bundle "gem #{gem_name} --exe" + end + + it "builds an exe file that requires the proper entrypoint" do + expect(bundled_app("#{gem_name}/exe/#{gem_name}")).to exist + expect(bundled_app("#{gem_name}/exe/#{gem_name}").read).to match(/require "#{require_path}"/) + end + end + + context "--bin parameter set" do + before do + bundle "gem #{gem_name} --bin" + end + + it "builds an exe file that requires the proper entrypoint" do + expect(bundled_app("#{gem_name}/exe/#{gem_name}")).to exist + expect(bundled_app("#{gem_name}/exe/#{gem_name}").read).to match(/require "#{require_path}"/) + end + end + + context "--test parameter set to rspec" do + before do + bundle "gem #{gem_name} --test=rspec" + end + + it "builds a spec helper that requires the proper entrypoint, and a default test in the proper path which fails" do + expect(bundled_app("#{gem_name}/spec/spec_helper.rb")).to exist + expect(bundled_app("#{gem_name}/spec/spec_helper.rb").read).to include(%(require "#{require_path}")) + expect(bundled_app("#{gem_name}/spec/#{require_path}_spec.rb")).to exist + expect(bundled_app("#{gem_name}/spec/#{require_path}_spec.rb").read).to include("expect(false).to eq(true)") + end + end + + context "--test parameter set to minitest" do + before do + bundle "gem #{gem_name} --test=minitest" + end + + it "builds a test helper that requires the proper entrypoint, and default test file in the proper path that defines the proper test class name, requires helper, and fails" do + expect(bundled_app("#{gem_name}/test/test_helper.rb")).to exist + expect(bundled_app("#{gem_name}/test/test_helper.rb").read).to include(%(require "#{require_path}")) + + expect(bundled_app("#{gem_name}/#{minitest_test_file_path}")).to exist + expect(bundled_app("#{gem_name}/#{minitest_test_file_path}").read).to include(minitest_test_class_name) + expect(bundled_app("#{gem_name}/#{minitest_test_file_path}").read).to include(%(require "test_helper")) + expect(bundled_app("#{gem_name}/#{minitest_test_file_path}").read).to include("assert false") + end + end + + context "--test parameter set to test-unit" do + before do + bundle "gem #{gem_name} --test=test-unit" + end + + it "builds a test helper that requires the proper entrypoint, and default test file in the proper path which requires helper and fails" do + expect(bundled_app("#{gem_name}/test/test_helper.rb")).to exist + expect(bundled_app("#{gem_name}/test/test_helper.rb").read).to include(%(require "#{require_path}")) + expect(bundled_app("#{gem_name}/test/#{require_path}_test.rb")).to exist + expect(bundled_app("#{gem_name}/test/#{require_path}_test.rb").read).to include(%(require "test_helper")) + expect(bundled_app("#{gem_name}/test/#{require_path}_test.rb").read).to include("assert_equal(\"expected\", \"actual\")") + end + end + end + + context "with mit option in bundle config settings set to true" do + before do + bundle_config_global "gem.mit true" + end + it_behaves_like "--mit flag" + it_behaves_like "--no-mit flag" + end + + context "with mit option in bundle config settings set to false" do + before do + bundle_config_global "gem.mit false" + end + it_behaves_like "--mit flag" + it_behaves_like "--no-mit flag" + end + + context "with coc option in bundle config settings set to true" do + before do + bundle_config_global "gem.coc true" + end + it_behaves_like "--coc flag" + it_behaves_like "--no-coc flag" + end + + context "with coc option in bundle config settings set to false" do + before do + bundle_config_global "gem.coc false" + end + it_behaves_like "--coc flag" + it_behaves_like "--no-coc flag" + end + + context "with rubocop option in bundle config settings set to true" do + before do + bundle_config_global "gem.rubocop true" + end + it_behaves_like "--linter=rubocop flag" + it_behaves_like "--linter=standard flag" + it_behaves_like "--no-linter flag" + end + + context "with rubocop option in bundle config settings set to false" do + before do + bundle_config_global "gem.rubocop false" + end + it_behaves_like "--linter=rubocop flag" + it_behaves_like "--linter=standard flag" + it_behaves_like "--no-linter flag" + end + + context "with linter option in bundle config settings set to rubocop" do + before do + bundle_config_global "gem.linter rubocop" + end + it_behaves_like "--linter=rubocop flag" + it_behaves_like "--linter=standard flag" + it_behaves_like "--no-linter flag" + end + + context "with linter option in bundle config settings set to standard" do + before do + bundle_config_global "gem.linter standard" + end + it_behaves_like "--linter=rubocop flag" + it_behaves_like "--linter=standard flag" + it_behaves_like "--no-linter flag" + end + + context "with linter option in bundle config settings set to false" do + before do + bundle_config_global "gem.linter false" + end + it_behaves_like "--linter=rubocop flag" + it_behaves_like "--linter=standard flag" + it_behaves_like "--no-linter flag" + end + + context "with changelog option in bundle config settings set to true" do + before do + bundle_config_global "gem.changelog true" + end + it_behaves_like "--changelog flag" + it_behaves_like "--no-changelog flag" + end + + context "with changelog option in bundle config settings set to false" do + before do + bundle_config_global "gem.changelog false" + end + it_behaves_like "--changelog flag" + it_behaves_like "--no-changelog flag" + end + + context "with bundle option in bundle config settings set to true" do + before do + bundle_config_global "gem.bundle true" + end + it_behaves_like "--bundle flag" + it_behaves_like "--no-bundle flag" + + it "runs bundle install" do + bundle "gem #{gem_name}" + expect(out).to include("Running bundle install in the new gem directory.") + end + end + + context "with bundle option in bundle config settings set to false" do + before do + bundle_config_global "gem.bundle false" + end + it_behaves_like "--bundle flag" + it_behaves_like "--no-bundle flag" + + it "does not run bundle install" do + bundle "gem #{gem_name}" + expect(out).to_not include("Running bundle install in the new gem directory.") + end + end + + context "without git config github.user set" do + before do + git("config --global --unset github.user") + end + context "with github-username option in bundle config settings set to some value" do + before do + bundle_config_global "gem.github_username different_username" + end + it_behaves_like "--github-username option", "gh_user" + end + + it_behaves_like "github_username configuration" + + context "with github-username option in bundle config settings set to false" do + before do + bundle_config_global "gem.github_username false" + end + it_behaves_like "--github-username option", "gh_user" + end + + context "when changelog is enabled" do + it "sets gemspec changelog_uri, homepage, homepage_uri, source_code_uri to TODOs" do + bundle "gem #{gem_name} --changelog" + + expect(generated_gemspec.metadata["changelog_uri"]). + to eq("TODO: Put your gem's CHANGELOG.md URL here.") + expect(generated_gemspec.homepage).to eq("TODO: Put your gem's website or public repo URL here.") + expect(generated_gemspec.metadata["homepage_uri"]).to eq("TODO: Put your gem's website or public repo URL here.") + expect(generated_gemspec.metadata["source_code_uri"]).to eq("TODO: Put your gem's public repo URL here.") + end + end + + context "when changelog is not enabled" do + it "sets gemspec homepage, homepage_uri, source_code_uri to TODOs and changelog_uri to nil" do + bundle "gem #{gem_name}" + + expect(generated_gemspec.metadata["changelog_uri"]).to be_nil + expect(generated_gemspec.homepage).to eq("TODO: Put your gem's website or public repo URL here.") + expect(generated_gemspec.metadata["homepage_uri"]).to eq("TODO: Put your gem's website or public repo URL here.") + expect(generated_gemspec.metadata["source_code_uri"]).to eq("TODO: Put your gem's public repo URL here.") + end + end + end + + context "with git config github.user set" do + context "with github-username option in bundle config settings set to some value" do + before do + bundle_config_global "gem.github_username different_username" + end + it_behaves_like "--github-username option", "gh_user" + end + + it_behaves_like "github_username configuration" + + context "with github-username option in bundle config settings set to false" do + before do + bundle_config_global "gem.github_username false" + end + it_behaves_like "--github-username option", "gh_user" + end + + context "when changelog is enabled" do + it "sets gemspec changelog_uri, homepage, homepage_uri, source_code_uri based on git username" do + bundle "gem #{gem_name} --changelog" + + expect(generated_gemspec.metadata["changelog_uri"]). + to eq("https://github.com/bundleuser/#{gem_name}/blob/main/CHANGELOG.md") + expect(generated_gemspec.homepage).to eq("https://github.com/bundleuser/#{gem_name}") + expect(generated_gemspec.metadata["homepage_uri"]).to eq("https://github.com/bundleuser/#{gem_name}") + expect(generated_gemspec.metadata["source_code_uri"]).to eq("https://github.com/bundleuser/#{gem_name}") + end + end + + context "when changelog is not enabled" do + it "sets gemspec source_code_uri, homepage, homepage_uri but not changelog_uri" do + bundle "gem #{gem_name}" + + expect(generated_gemspec.metadata["changelog_uri"]).to be_nil + expect(generated_gemspec.homepage).to eq("https://github.com/bundleuser/#{gem_name}") + expect(generated_gemspec.metadata["homepage_uri"]).to eq("https://github.com/bundleuser/#{gem_name}") + expect(generated_gemspec.metadata["source_code_uri"]).to eq("https://github.com/bundleuser/#{gem_name}") + end + end + end + + context "standard gem naming" do + let(:require_path) { gem_name } + + let(:require_relative_path) { gem_name } + + let(:minitest_test_file_path) { "test/test_#{gem_name}.rb" } + + let(:minitest_test_class_name) { "class TestMygem < Minitest::Test" } + + include_examples "paths that depend on gem name" + end + + context "gem naming with underscore" do + let(:gem_name) { "test_gem" } + + let(:require_path) { "test_gem" } + + let(:require_relative_path) { "test_gem" } + + let(:minitest_test_file_path) { "test/test_test_gem.rb" } + + let(:minitest_test_class_name) { "class TestTestGem < Minitest::Test" } + + let(:flags) { nil } + + it "does not nest constants" do + bundle ["gem", gem_name, flags].compact.join(" ") + expect(bundled_app("#{gem_name}/lib/#{require_path}/version.rb").read).to match(/module TestGem/) + expect(bundled_app("#{gem_name}/lib/#{require_path}.rb").read).to match(/module TestGem/) + end + + include_examples "paths that depend on gem name" + + context "--ext parameter set with C" do + let(:flags) { "--ext=c" } + + before do + bundle ["gem", gem_name, flags].compact.join(" ") + end + + it "builds ext skeleton" do + expect(bundled_app("#{gem_name}/ext/#{gem_name}/extconf.rb")).to exist + expect(bundled_app("#{gem_name}/ext/#{gem_name}/#{gem_name}.h")).to exist + expect(bundled_app("#{gem_name}/ext/#{gem_name}/#{gem_name}.c")).to exist + end + + it "generates native extension loading code" do + expect(bundled_app("#{gem_name}/lib/#{gem_name}.rb").read).to include(<<~RUBY) + require_relative "test_gem/version" + require "#{gem_name}/#{gem_name}" + RUBY + end + + it "includes rake-compiler, but no Rust related changes" do + expect(bundled_app("#{gem_name}/Gemfile").read).to include('gem "rake-compiler"') + + expect(bundled_app("#{gem_name}/#{gem_name}.gemspec").read).to_not include('spec.add_dependency "rb_sys"') + expect(bundled_app("#{gem_name}/#{gem_name}.gemspec").read).to_not include('spec.required_rubygems_version = ">= ') + end + + it "depends on compile task for build" do + rakefile = <<~RAKEFILE + # frozen_string_literal: true + + require "bundler/gem_tasks" + require "rake/extensiontask" + + task build: :compile + + GEMSPEC = Gem::Specification.load("#{gem_name}.gemspec") + + Rake::ExtensionTask.new("#{gem_name}", GEMSPEC) do |ext| + ext.lib_dir = "lib/#{gem_name}" + end + + task default: %i[clobber compile] + RAKEFILE + + expect(bundled_app("#{gem_name}/Rakefile").read).to eq(rakefile) + end + end + + context "--ext parameter set with rust" do + let(:flags) { "--ext=rust" } + + before do + bundle ["gem", gem_name, flags].compact.join(" ") + end + + it "is not deprecated" do + expect(err).not_to include "[DEPRECATED] Option `--ext` without explicit value is deprecated." + end + + it "builds ext skeleton" do + expect(bundled_app("#{gem_name}/Cargo.toml")).to exist + expect(bundled_app("#{gem_name}/ext/#{gem_name}/Cargo.toml")).to exist + expect(bundled_app("#{gem_name}/ext/#{gem_name}/extconf.rb")).to exist + expect(bundled_app("#{gem_name}/ext/#{gem_name}/src/lib.rs")).to exist + expect(bundled_app("#{gem_name}/ext/#{gem_name}/build.rs")).to exist + end + + it "includes rake-compiler and rb_sys gems constraint" do + expect(bundled_app("#{gem_name}/Gemfile").read).to include('gem "rake-compiler"') + expect(bundled_app("#{gem_name}/#{gem_name}.gemspec").read).to include('spec.add_dependency "rb_sys"') + end + + it "depends on compile task for build" do + rakefile = <<~RAKEFILE + # frozen_string_literal: true + + require "bundler/gem_tasks" + require "rb_sys/extensiontask" + + task build: :compile + + GEMSPEC = Gem::Specification.load("#{gem_name}.gemspec") + + RbSys::ExtensionTask.new("#{gem_name}", GEMSPEC) do |ext| + ext.lib_dir = "lib/#{gem_name}" + end + + task default: :compile + RAKEFILE + + expect(bundled_app("#{gem_name}/Rakefile").read).to eq(rakefile) + end + + it "configures the crate such that `cargo test` works", :ruby_repo, :mri_only do + env = setup_rust_env + gem_path = bundled_app(gem_name) + result = sys_exec("cargo test", env: env, dir: gem_path, timeout: 300) + + expect(result).to include("1 passed") + end + + def setup_rust_env + skip "rust toolchain of mingw is broken" if RUBY_PLATFORM.match?("mingw") + + env = { + "CARGO_HOME" => ENV.fetch("CARGO_HOME", File.join(ENV["HOME"], ".cargo")), + "RUSTUP_HOME" => ENV.fetch("RUSTUP_HOME", File.join(ENV["HOME"], ".rustup")), + "RUSTUP_TOOLCHAIN" => ENV.fetch("RUSTUP_TOOLCHAIN", "stable"), + } + + system(env, "cargo", "-V", out: IO::NULL, err: [:child, :out]) + skip "cargo not present" unless $?.success? + # Hermetic Cargo setup + RbConfig::CONFIG.each {|k, v| env["RBCONFIG_#{k}"] = v } + env + end + end + + context "--ext parameter set with go" do + let(:flags) { "--ext=go" } + + before do + bundle ["gem", gem_name, flags].compact.join(" ") + end + + after do + sys_exec("go clean -modcache", raise_on_error: true) if installed_go? + end + + it "is not deprecated" do + expect(err).not_to include "[DEPRECATED] Option `--ext` without explicit value is deprecated." + end + + it "builds ext skeleton" do + expect(bundled_app("#{gem_name}/ext/#{gem_name}/#{gem_name}.c")).to exist + expect(bundled_app("#{gem_name}/ext/#{gem_name}/#{gem_name}.go")).to exist + expect(bundled_app("#{gem_name}/ext/#{gem_name}/#{gem_name}.h")).to exist + expect(bundled_app("#{gem_name}/ext/#{gem_name}/extconf.rb")).to exist + expect(bundled_app("#{gem_name}/ext/#{gem_name}/go.mod")).to exist + end + + it "includes extconf.rb in gem_name.gemspec" do + expect(bundled_app("#{gem_name}/#{gem_name}.gemspec").read).to include(%(spec.extensions = ["ext/#{gem_name}/extconf.rb"])) + end + + it "includes go_gem in gem_name.gemspec" do + expect(bundled_app("#{gem_name}/#{gem_name}.gemspec").read).to include('spec.add_dependency "go_gem", ">= 0.2"') + end + + it "includes go_gem extension in extconf.rb" do + expect(bundled_app("#{gem_name}/ext/#{gem_name}/extconf.rb").read).to include(<<~RUBY) + require "mkmf" + require "go_gem/mkmf" + RUBY + + expect(bundled_app("#{gem_name}/ext/#{gem_name}/extconf.rb").read).to include(%(create_go_makefile("#{gem_name}/#{gem_name}"))) + expect(bundled_app("#{gem_name}/ext/#{gem_name}/extconf.rb").read).not_to include("create_makefile") + end + + it "includes go_gem extension in gem_name.c" do + expect(bundled_app("#{gem_name}/ext/#{gem_name}/#{gem_name}.c").read).to eq(<<~C) + #include "#{gem_name}.h" + #include "_cgo_export.h" + C + end + + it "includes skeleton code in gem_name.go" do + expect(bundled_app("#{gem_name}/ext/#{gem_name}/#{gem_name}.go").read).to include(<<~GO) + /* + #include "#{gem_name}.h" + + VALUE rb_#{gem_name}_sum(VALUE self, VALUE a, VALUE b); + */ + import "C" + GO + + expect(bundled_app("#{gem_name}/ext/#{gem_name}/#{gem_name}.go").read).to include(<<~GO) + //export rb_#{gem_name}_sum + func rb_#{gem_name}_sum(_ C.VALUE, a C.VALUE, b C.VALUE) C.VALUE { + GO + + expect(bundled_app("#{gem_name}/ext/#{gem_name}/#{gem_name}.go").read).to include(<<~GO) + //export Init_#{gem_name} + func Init_#{gem_name}() { + GO + end + + it "includes valid module name in go.mod" do + expect(bundled_app("#{gem_name}/ext/#{gem_name}/go.mod").read).to include("module github.com/bundleuser/#{gem_name}") + end + + it "includes go_gem extension in Rakefile" do + expect(bundled_app("#{gem_name}/Rakefile").read).to include(<<~RUBY) + require "go_gem/rake_task" + + GoGem::RakeTask.new("#{gem_name}") + RUBY + end + + context "with --no-ci" do + let(:flags) { "--ext=go --no-ci" } + + it_behaves_like "CI config is absent" + end + + context "--ci set to github" do + let(:flags) { "--ext=go --ci=github" } + + it "generates .github/workflows/main.yml" do + expect(bundled_app("#{gem_name}/.github/workflows/main.yml")).to exist + expect(bundled_app("#{gem_name}/.github/workflows/main.yml").read).to include("go-version-file: ext/#{gem_name}/go.mod") + end + end + + context "--ci set to circle" do + let(:flags) { "--ext=go --ci=circle" } + + it "generates a .circleci/config.yml" do + expect(bundled_app("#{gem_name}/.circleci/config.yml")).to exist + + expect(bundled_app("#{gem_name}/.circleci/config.yml").read).to include(<<-YAML.strip) + environment: + GO_VERSION: + YAML + + expect(bundled_app("#{gem_name}/.circleci/config.yml").read).to include(<<-YAML) + - run: + name: Install Go + command: | + wget https://go.dev/dl/go$GO_VERSION.linux-amd64.tar.gz -O /tmp/go.tar.gz + tar -C /usr/local -xzf /tmp/go.tar.gz + echo 'export PATH=/usr/local/go/bin:"$PATH"' >> "$BASH_ENV" + YAML + end + end + + context "--ci set to gitlab" do + let(:flags) { "--ext=go --ci=gitlab" } + + it "generates a .gitlab-ci.yml" do + expect(bundled_app("#{gem_name}/.gitlab-ci.yml")).to exist + + expect(bundled_app("#{gem_name}/.gitlab-ci.yml").read).to include(<<-YAML) + - wget https://go.dev/dl/go$GO_VERSION.linux-amd64.tar.gz -O /tmp/go.tar.gz + - tar -C /usr/local -xzf /tmp/go.tar.gz + - export PATH=/usr/local/go/bin:$PATH + YAML + + expect(bundled_app("#{gem_name}/.gitlab-ci.yml").read).to include(<<-YAML.strip) + variables: + GO_VERSION: + YAML + end + end + + context "without github.user" do + before do + # FIXME: GitHub Actions Windows Runner hang up here for some reason... + skip "Workaround for hung up" if Gem.win_platform? + + git("config --global --unset github.user") + bundle ["gem", gem_name, flags].compact.join(" ") + end + + it "includes valid module name in go.mod" do + expect(bundled_app("#{gem_name}/ext/#{gem_name}/go.mod").read).to include("module github.com/username/#{gem_name}") + end + end + end + end + + context "gem naming with dashed" do + let(:gem_name) { "test-gem" } + + let(:require_path) { "test/gem" } + + let(:require_relative_path) { "gem" } + + let(:minitest_test_file_path) { "test/test/test_gem.rb" } + + let(:minitest_test_class_name) { "class Test::TestGem < Minitest::Test" } + + it "nests constants so they work" do + bundle "gem #{gem_name}" + expect(bundled_app("#{gem_name}/lib/#{require_path}/version.rb").read).to match(/module Test\n module Gem/) + expect(bundled_app("#{gem_name}/lib/#{require_path}.rb").read).to match(/module Test\n module Gem/) + end + + include_examples "paths that depend on gem name" + end + + describe "uncommon gem names" do + it "can deal with two dashes" do + bundle "gem a--a" + + expect(bundled_app("a--a/a--a.gemspec")).to exist + end + + it "fails gracefully with a ." do + bundle "gem foo.gemspec", raise_on_error: false + expect(err).to end_with("Invalid gem name foo.gemspec -- `Foo.gemspec` is an invalid constant name") + end + + it "fails gracefully with a ^" do + bundle "gem ^", raise_on_error: false + expect(err).to end_with("Invalid gem name ^ -- `^` is an invalid constant name") + end + + it "fails gracefully with a space" do + bundle "gem 'foo bar'", raise_on_error: false + expect(err).to end_with("Invalid gem name foo bar -- `Foo bar` is an invalid constant name") + end + + it "fails gracefully when multiple names are passed" do + bundle "gem foo bar baz", raise_on_error: false + expect(err).to eq(<<-E.strip) +ERROR: "bundle gem" was called with arguments ["foo", "bar", "baz"] +Usage: "bundle gem NAME [OPTIONS]" + E + end + end + + describe "#ensure_safe_gem_name" do + before do + bundle "gem #{subject}", raise_on_error: false + end + + context "with an existing const name" do + subject { "gem" } + it { expect(err).to include("Invalid gem name #{subject}") } + end + + context "with an existing hyphenated const name" do + subject { "gem-specification" } + it { expect(err).to include("Invalid gem name #{subject}") } + end + + context "starting with a number" do + subject { "1gem" } + it { expect(err).to include("Invalid gem name #{subject}") } + end + + context "including capital letter" do + subject { "CAPITAL" } + it "should warn but not error" do + expect(err).to include("Gem names with capital letters are not recommended") + expect(bundled_app("#{subject}/#{subject}.gemspec")).to exist + end + end + + context "starting with an existing const name" do + subject { "gem-somenewconstantname" } + it { expect(err).not_to include("Invalid gem name #{subject}") } + end + + context "ending with an existing const name" do + subject { "somenewconstantname-gem" } + it { expect(err).not_to include("Invalid gem name #{subject}") } + end + end + + context "on first run", :readline do + it "asks about test framework" do + bundle_config_global "BUNDLE_GEM__TEST" => nil + + bundle "gem foobar" do |input, _, _| + input.puts "rspec" + end + + expect(bundled_app("foobar/spec/spec_helper.rb")).to exist + rakefile = <<~RAKEFILE + # frozen_string_literal: true + + require "bundler/gem_tasks" + require "rspec/core/rake_task" + + RSpec::Core::RakeTask.new(:spec) + + task default: :spec + RAKEFILE + + expect(bundled_app("foobar/Rakefile").read).to eq(rakefile) + expect(bundled_app("foobar/Gemfile").read).to include('gem "rspec"') + end + + it "asks about CI service" do + bundle_config_global "BUNDLE_GEM__CI" => nil + + bundle "gem foobar" do |input, _, _| + input.puts "github" + end + + expect(bundled_app("foobar/.github/workflows/main.yml")).to exist + end + + it "asks about MIT license just once" do + bundle_config_global "BUNDLE_GEM__MIT" => nil + + bundle "config list" + + bundle "gem foobar" do |input, _, _| + input.puts "yes" + end + + expect(bundled_app("foobar/LICENSE.txt")).to exist + expect(out).to include("Using a MIT license means").once + end + + it "asks about CoC just once" do + bundle_config_global "BUNDLE_GEM__COC" => nil + + bundle "gem foobar" do |input, _, _| + input.puts "yes" + end + + expect(bundled_app("foobar/CODE_OF_CONDUCT.md")).to exist + expect(out).to include("Codes of conduct can increase contributions to your project").once + end + + it "asks about CHANGELOG just once" do + bundle_config_global "BUNDLE_GEM__CHANGELOG" => nil + + bundle "gem foobar" do |input, _, _| + input.puts "yes" + end + + expect(bundled_app("foobar/CHANGELOG.md")).to exist + expect(out).to include("A changelog is a file which contains").once + end + end + + context "on conflicts with a previously created file" do + it "should fail gracefully" do + FileUtils.touch(bundled_app("conflict-foobar")) + bundle "gem conflict-foobar", raise_on_error: false + expect(err).to eq("Couldn't create a new gem named `conflict-foobar` because there's an existing file named `conflict-foobar`.") + expect(exitstatus).to eql(32) + end + end + + context "on conflicts with a previously created directory" do + it "should succeed" do + FileUtils.mkdir_p(bundled_app("conflict-foobar/Gemfile")) + bundle "gem conflict-foobar" + expect(out).to include("file_clash conflict-foobar/Gemfile"). + and include "Initializing git repo in #{bundled_app("conflict-foobar")}" + end + end +end diff --git a/spec/bundler/commands/newgem_spec.rb b/spec/bundler/commands/newgem_spec.rb index 2b630d30b6c5d1..f26468bd512500 100644 --- a/spec/bundler/commands/newgem_spec.rb +++ b/spec/bundler/commands/newgem_spec.rb @@ -94,218 +94,6 @@ def installed_go? end end - shared_examples_for "--mit flag" do - before do - bundle "gem #{gem_name} --mit" - end - it "generates a gem skeleton with MIT license" do - gem_skeleton_assertions - expect(bundled_app("#{gem_name}/LICENSE.txt")).to exist - expect(generated_gemspec.license).to eq("MIT") - end - end - - shared_examples_for "--no-mit flag" do - before do - bundle "gem #{gem_name} --no-mit" - end - it "generates a gem skeleton without MIT license" do - gem_skeleton_assertions - expect(bundled_app("#{gem_name}/LICENSE.txt")).to_not exist - end - end - - shared_examples_for "--coc flag" do - it "generates a gem skeleton with MIT license" do - bundle "gem #{gem_name} --coc" - gem_skeleton_assertions - expect(bundled_app("#{gem_name}/CODE_OF_CONDUCT.md")).to exist - end - - it "generates the README with a section for the Code of Conduct" do - bundle "gem #{gem_name} --coc" - expect(bundled_app("#{gem_name}/README.md").read).to include("## Code of Conduct") - expect(bundled_app("#{gem_name}/README.md").read).to match(%r{https://github\.com/bundleuser/#{gem_name}/blob/.*/CODE_OF_CONDUCT.md}) - end - - it "generates the README with a section for the Code of Conduct, respecting the configured git default branch", git: ">= 2.28.0" do - git("config --global init.defaultBranch main") - bundle "gem #{gem_name} --coc" - - expect(bundled_app("#{gem_name}/README.md").read).to include("## Code of Conduct") - expect(bundled_app("#{gem_name}/README.md").read).to include("https://github.com/bundleuser/#{gem_name}/blob/main/CODE_OF_CONDUCT.md") - end - end - - shared_examples_for "--no-coc flag" do - before do - bundle "gem #{gem_name} --no-coc" - end - it "generates a gem skeleton without Code of Conduct" do - gem_skeleton_assertions - expect(bundled_app("#{gem_name}/CODE_OF_CONDUCT.md")).to_not exist - end - - it "generates the README without a section for the Code of Conduct" do - expect(bundled_app("#{gem_name}/README.md").read).not_to include("## Code of Conduct") - expect(bundled_app("#{gem_name}/README.md").read).not_to match(%r{https://github\.com/bundleuser/#{gem_name}/blob/.*/CODE_OF_CONDUCT.md}) - end - end - - shared_examples_for "--changelog flag" do - before do - bundle "gem #{gem_name} --changelog" - end - it "generates a gem skeleton with a CHANGELOG" do - gem_skeleton_assertions - expect(bundled_app("#{gem_name}/CHANGELOG.md")).to exist - end - end - - shared_examples_for "--no-changelog flag" do - before do - bundle "gem #{gem_name} --no-changelog" - end - it "generates a gem skeleton without a CHANGELOG" do - gem_skeleton_assertions - expect(bundled_app("#{gem_name}/CHANGELOG.md")).to_not exist - end - end - - shared_examples_for "--bundle flag" do - before do - bundle "gem #{gem_name} --bundle" - end - it "generates a gem skeleton with bundle install" do - gem_skeleton_assertions - expect(out).to include("Running bundle install in the new gem directory.") - end - end - - shared_examples_for "--no-bundle flag" do - before do - bundle "gem #{gem_name} --no-bundle" - end - it "generates a gem skeleton without bundle install" do - gem_skeleton_assertions - expect(out).to_not include("Running bundle install in the new gem directory.") - end - end - - shared_examples_for "--linter=rubocop flag" do - before do - bundle "gem #{gem_name} --linter=rubocop" - end - - it "generates a gem skeleton with rubocop" do - gem_skeleton_assertions - expect(bundled_app("#{gem_name}/Rakefile")).to read_as( - include("# frozen_string_literal: true"). - and(include('require "rubocop/rake_task"'). - and(include("RuboCop::RakeTask.new"). - and(match(/default:.+:rubocop/)))) - ) - end - - it "includes rubocop in generated Gemfile" do - allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile) - builder = Bundler::Dsl.new - builder.eval_gemfile(bundled_app("#{gem_name}/Gemfile")) - builder.dependencies - rubocop_dep = builder.dependencies.find {|d| d.name == "rubocop" } - expect(rubocop_dep).not_to be_specific - expect(rubocop_dep.requirement).to eq(Gem::Requirement.new([">= 0"])) - end - - it "generates a default .rubocop.yml" do - expect(bundled_app("#{gem_name}/.rubocop.yml")).to exist - end - - it "includes .rubocop.yml into ignore list" do - expect(ignore_paths).to include(".rubocop.yml") - end - end - - shared_examples_for "--linter=standard flag" do - before do - bundle "gem #{gem_name} --linter=standard" - end - - it "generates a gem skeleton with standard" do - gem_skeleton_assertions - expect(bundled_app("#{gem_name}/Rakefile")).to read_as( - include('require "standard/rake"'). - and(match(/default:.+:standard/)) - ) - end - - it "includes standard in generated Gemfile" do - allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile) - builder = Bundler::Dsl.new - builder.eval_gemfile(bundled_app("#{gem_name}/Gemfile")) - builder.dependencies - standard_dep = builder.dependencies.find {|d| d.name == "standard" } - expect(standard_dep).not_to be_specific - expect(standard_dep.requirement).to eq(Gem::Requirement.new([">= 0"])) - end - - it "generates a default .standard.yml" do - expect(bundled_app("#{gem_name}/.standard.yml")).to exist - end - - it "includes .standard.yml into ignore list" do - expect(ignore_paths).to include(".standard.yml") - end - end - - shared_examples_for "--no-linter flag" do - define_negated_matcher :exclude, :include - - before do - bundle "gem #{gem_name} --no-linter" - end - - it "generates a gem skeleton without rubocop" do - gem_skeleton_assertions - expect(bundled_app("#{gem_name}/Rakefile")).to read_as(exclude("rubocop")) - expect(bundled_app("#{gem_name}/#{gem_name}.gemspec")).to read_as(exclude("rubocop")) - end - - it "does not include rubocop in generated Gemfile" do - allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile) - builder = Bundler::Dsl.new - builder.eval_gemfile(bundled_app("#{gem_name}/Gemfile")) - builder.dependencies - rubocop_dep = builder.dependencies.find {|d| d.name == "rubocop" } - expect(rubocop_dep).to be_nil - end - - it "does not include standard in generated Gemfile" do - allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile) - builder = Bundler::Dsl.new - builder.eval_gemfile(bundled_app("#{gem_name}/Gemfile")) - builder.dependencies - standard_dep = builder.dependencies.find {|d| d.name == "standard" } - expect(standard_dep).to be_nil - end - - it "doesn't generate a default .rubocop.yml" do - expect(bundled_app("#{gem_name}/.rubocop.yml")).to_not exist - end - - it "does not add .rubocop.yml into ignore list" do - expect(ignore_paths).not_to include(".rubocop.yml") - end - - it "doesn't generate a default .standard.yml" do - expect(bundled_app("#{gem_name}/.standard.yml")).to_not exist - end - - it "does not add .standard.yml into ignore list" do - expect(ignore_paths).not_to include(".standard.yml") - end - end - it "has no rubocop offenses when using --linter=rubocop flag" do skip "ruby_core has an 'ast.rb' file that gets in the middle and breaks this spec" if ruby_core? bundle "gem #{gem_name} --linter=rubocop" @@ -380,14 +168,6 @@ def installed_go? expect(last_command).to be_success end - shared_examples_for "CI config is absent" do - it "does not create any CI files" do - expect(bundled_app("#{gem_name}/.github/workflows/main.yml")).to_not exist - expect(bundled_app("#{gem_name}/.gitlab-ci.yml")).to_not exist - expect(bundled_app("#{gem_name}/.circleci/config.yml")).to_not exist - end - end - shared_examples_for "test framework is absent" do it "does not create any test framework files" do expect(bundled_app("#{gem_name}/.rspec")).to_not exist @@ -518,55 +298,6 @@ def create_temporary_dir(dir) end end - shared_examples_for "--github-username option" do |github_username| - before do - bundle "gem #{gem_name} --github-username=#{github_username}" - end - - it "generates a gem skeleton" do - gem_skeleton_assertions - end - - it "contribute URL set to given github username" do - expect(bundled_app("#{gem_name}/README.md").read).not_to include("[USERNAME]") - expect(bundled_app("#{gem_name}/README.md").read).to include("github.com/#{github_username}") - end - end - - shared_examples_for "github_username configuration" do - context "with github_username setting set to some value" do - before do - bundle_config_global "gem.github_username different_username" - bundle "gem #{gem_name}" - end - - it "generates a gem skeleton" do - gem_skeleton_assertions - end - - it "contribute URL set to bundle config setting" do - expect(bundled_app("#{gem_name}/README.md").read).not_to include("[USERNAME]") - expect(bundled_app("#{gem_name}/README.md").read).to include("github.com/different_username") - end - end - - context "with github_username setting set to false" do - before do - bundle_config_global "gem.github_username false" - bundle "gem #{gem_name}" - end - - it "generates a gem skeleton" do - gem_skeleton_assertions - end - - it "contribute URL set to [USERNAME]" do - expect(bundled_app("#{gem_name}/README.md").read).to include("[USERNAME]") - expect(bundled_app("#{gem_name}/README.md").read).not_to include("github.com/bundleuser") - end - end - end - it "generates a gem skeleton" do bundle "gem #{gem_name}" @@ -1021,1124 +752,4 @@ def create_temporary_dir(dir) it_behaves_like "test framework is absent" end - - context "--ci with no argument" do - before do - bundle "gem #{gem_name}" - end - - it "does not generate any CI config" do - expect(bundled_app("#{gem_name}/.github/workflows/main.yml")).to_not exist - expect(bundled_app("#{gem_name}/.gitlab-ci.yml")).to_not exist - expect(bundled_app("#{gem_name}/.circleci/config.yml")).to_not exist - end - - it "does not add any CI config files into ignore list" do - expect(ignore_paths).not_to include(".github/") - expect(ignore_paths).not_to include(".gitlab-ci.yml") - expect(ignore_paths).not_to include(".circleci/") - end - end - - context "--ci set to github" do - before do - bundle "gem #{gem_name} --ci=github" - end - - it "generates a GitHub Actions config file" do - expect(bundled_app("#{gem_name}/.github/workflows/main.yml")).to exist - end - - it "includes .github/ into ignore list" do - expect(ignore_paths).to include(".github/") - end - end - - context "--ci set to gitlab" do - before do - bundle "gem #{gem_name} --ci=gitlab" - end - - it "generates a GitLab CI config file" do - expect(bundled_app("#{gem_name}/.gitlab-ci.yml")).to exist - end - - it "includes .gitlab-ci.yml into ignore list" do - expect(ignore_paths).to include(".gitlab-ci.yml") - end - end - - context "--ci set to circle" do - before do - bundle "gem #{gem_name} --ci=circle" - end - - it "generates a CircleCI config file" do - expect(bundled_app("#{gem_name}/.circleci/config.yml")).to exist - end - - it "includes .circleci/ into ignore list" do - expect(ignore_paths).to include(".circleci/") - end - end - - context "--ci set to an invalid value" do - before do - bundle "gem #{gem_name} --ci=foo", raise_on_error: false - end - - it "fails loudly" do - expect(last_command).to be_failure - expect(err).to match(/Expected '--ci' to be one of .*; got foo/) - end - end - - context "gem.ci setting set to none" do - it "doesn't generate any CI config" do - expect(bundled_app("#{gem_name}/.github/workflows/main.yml")).to_not exist - expect(bundled_app("#{gem_name}/.gitlab-ci.yml")).to_not exist - expect(bundled_app("#{gem_name}/.circleci/config.yml")).to_not exist - end - end - - context "gem.ci setting set to github" do - it "generates a GitHub Actions config file" do - bundle_config "gem.ci github" - bundle "gem #{gem_name}" - - expect(bundled_app("#{gem_name}/.github/workflows/main.yml")).to exist - end - end - - context "gem.ci setting set to gitlab" do - it "generates a GitLab CI config file" do - bundle_config "gem.ci gitlab" - bundle "gem #{gem_name}" - - expect(bundled_app("#{gem_name}/.gitlab-ci.yml")).to exist - end - end - - context "gem.ci setting set to circle" do - it "generates a CircleCI config file" do - bundle_config "gem.ci circle" - bundle "gem #{gem_name}" - - expect(bundled_app("#{gem_name}/.circleci/config.yml")).to exist - end - end - - context "gem.ci set to github and --ci with no arguments" do - before do - bundle_config "gem.ci github" - bundle "gem #{gem_name} --ci" - end - - it "generates a GitHub Actions config file" do - expect(bundled_app("#{gem_name}/.github/workflows/main.yml")).to exist - end - - it "hints that --ci is already configured" do - expect(out).to match("github is already configured, ignoring --ci flag.") - end - end - - context "gem.ci setting set to false and --ci with no arguments", :readline do - before do - bundle_config "gem.ci false" - bundle "gem #{gem_name} --ci" do |input, _, _| - input.puts "github" - end - end - - it "asks to setup CI" do - expect(out).to match("Do you want to set up continuous integration for your gem?") - end - - it "hints that the choice will only be applied to the current gem" do - expect(out).to match("Your choice will only be applied to this gem.") - end - end - - context "gem.ci setting not set and --ci with no arguments", :readline do - before do - bundle_config_global "BUNDLE_GEM__CI" => nil - bundle "gem #{gem_name} --ci" do |input, _, _| - input.puts "github" - end - end - - it "asks to setup CI" do - expect(out).to match("Do you want to set up continuous integration for your gem?") - end - - it "hints that the choice will be applied to future bundle gem calls" do - hint = "Future `bundle gem` calls will use your choice. " \ - "This setting can be changed anytime with `bundle config gem.ci`." - expect(out).to match(hint) - end - end - - context "gem.ci setting set to a CI service and --no-ci" do - before do - bundle_config "gem.ci github" - bundle "gem #{gem_name} --no-ci" - end - - it "does not generate any CI config" do - expect(bundled_app("#{gem_name}/.github/workflows/main.yml")).to_not exist - expect(bundled_app("#{gem_name}/.gitlab-ci.yml")).to_not exist - expect(bundled_app("#{gem_name}/.circleci/config.yml")).to_not exist - end - end - - context "--linter with no argument" do - before do - bundle "gem #{gem_name}" - end - - it "does not generate any linter config" do - expect(bundled_app("#{gem_name}/.rubocop.yml")).to_not exist - expect(bundled_app("#{gem_name}/.standard.yml")).to_not exist - end - - it "does not add any linter config files into ignore list" do - expect(ignore_paths).not_to include(".rubocop.yml") - expect(ignore_paths).not_to include(".standard.yml") - end - end - - context "--linter set to rubocop" do - before do - bundle "gem #{gem_name} --linter=rubocop" - end - - it "generates a RuboCop config" do - expect(bundled_app("#{gem_name}/.rubocop.yml")).to exist - expect(bundled_app("#{gem_name}/.standard.yml")).to_not exist - end - - it "includes .rubocop.yml into ignore list" do - expect(ignore_paths).to include(".rubocop.yml") - expect(ignore_paths).not_to include(".standard.yml") - end - end - - context "--linter set to standard" do - before do - bundle "gem #{gem_name} --linter=standard" - end - - it "generates a Standard config" do - expect(bundled_app("#{gem_name}/.standard.yml")).to exist - expect(bundled_app("#{gem_name}/.rubocop.yml")).to_not exist - end - - it "includes .standard.yml into ignore list" do - expect(ignore_paths).to include(".standard.yml") - expect(ignore_paths).not_to include(".rubocop.yml") - end - end - - context "--linter set to an invalid value" do - before do - bundle "gem #{gem_name} --linter=foo", raise_on_error: false - end - - it "fails loudly" do - expect(last_command).to be_failure - expect(err).to match(/Expected '--linter' to be one of .*; got foo/) - end - end - - context "gem.linter setting set to none" do - before do - bundle "gem #{gem_name}" - end - - it "doesn't generate any linter config" do - expect(bundled_app("#{gem_name}/.rubocop.yml")).to_not exist - expect(bundled_app("#{gem_name}/.standard.yml")).to_not exist - end - - it "does not add any linter config files into ignore list" do - expect(ignore_paths).not_to include(".rubocop.yml") - expect(ignore_paths).not_to include(".standard.yml") - end - end - - context "gem.linter setting set to rubocop" do - before do - bundle_config "gem.linter rubocop" - bundle "gem #{gem_name}" - end - - it "generates a RuboCop config file" do - expect(bundled_app("#{gem_name}/.rubocop.yml")).to exist - end - - it "includes .rubocop.yml into ignore list" do - expect(ignore_paths).to include(".rubocop.yml") - end - end - - context "gem.linter setting set to standard" do - before do - bundle_config "gem.linter standard" - bundle "gem #{gem_name}" - end - - it "generates a Standard config file" do - expect(bundled_app("#{gem_name}/.standard.yml")).to exist - end - - it "includes .standard.yml into ignore list" do - expect(ignore_paths).to include(".standard.yml") - end - end - - context "gem.linter set to rubocop and --linter with no arguments" do - before do - bundle_config "gem.linter rubocop" - bundle "gem #{gem_name} --linter" - end - - it "generates a RuboCop config file" do - expect(bundled_app("#{gem_name}/.rubocop.yml")).to exist - end - - it "includes .rubocop.yml into ignore list" do - expect(ignore_paths).to include(".rubocop.yml") - end - - it "hints that --linter is already configured" do - expect(out).to match("rubocop is already configured, ignoring --linter flag.") - end - end - - context "gem.linter setting set to false and --linter with no arguments", :readline do - before do - bundle_config "gem.linter false" - bundle "gem #{gem_name} --linter" do |input, _, _| - input.puts "rubocop" - end - end - - it "asks to setup a linter" do - expect(out).to match("Do you want to add a code linter and formatter to your gem?") - end - - it "hints that the choice will only be applied to the current gem" do - expect(out).to match("Your choice will only be applied to this gem.") - end - end - - context "gem.linter setting not set and --linter with no arguments", :readline do - before do - bundle_config_global "BUNDLE_GEM__LINTER" => nil - bundle "gem #{gem_name} --linter" do |input, _, _| - input.puts "rubocop" - end - end - - it "asks to setup a linter" do - expect(out).to match("Do you want to add a code linter and formatter to your gem?") - end - - it "hints that the choice will be applied to future bundle gem calls" do - hint = "Future `bundle gem` calls will use your choice. " \ - "This setting can be changed anytime with `bundle config gem.linter`." - expect(out).to match(hint) - end - end - - context "gem.linter setting set to a linter and --no-linter" do - before do - bundle_config "gem.linter rubocop" - bundle "gem #{gem_name} --no-linter" - end - - it "does not generate any linter config" do - expect(bundled_app("#{gem_name}/.rubocop.yml")).to_not exist - expect(bundled_app("#{gem_name}/.standard.yml")).to_not exist - end - - it "does not add any linter config files into ignore list" do - expect(ignore_paths).not_to include(".rubocop.yml") - expect(ignore_paths).not_to include(".standard.yml") - end - end - - context "--edit option" do - it "opens the generated gemspec in the user's text editor" do - output = bundle "gem #{gem_name} --edit=echo" - gemspec_path = File.join(bundled_app, gem_name, "#{gem_name}.gemspec") - expect(output).to include("echo \"#{gemspec_path}\"") - end - end - - shared_examples_for "paths that depend on gem name" do - it "generates entrypoint, version file and signatures file at the proper path, with the proper content" do - bundle "gem #{gem_name}" - - expect(bundled_app("#{gem_name}/lib/#{require_path}.rb")).to exist - expect(bundled_app("#{gem_name}/lib/#{require_path}.rb").read).to match(%r{require_relative "#{require_relative_path}/version"}) - expect(bundled_app("#{gem_name}/lib/#{require_path}.rb").read).to match(/class Error < StandardError; end$/) - - expect(bundled_app("#{gem_name}/lib/#{require_path}/version.rb")).to exist - expect(bundled_app("#{gem_name}/sig/#{require_path}.rbs")).to exist - end - - context "--exe parameter set" do - before do - bundle "gem #{gem_name} --exe" - end - - it "builds an exe file that requires the proper entrypoint" do - expect(bundled_app("#{gem_name}/exe/#{gem_name}")).to exist - expect(bundled_app("#{gem_name}/exe/#{gem_name}").read).to match(/require "#{require_path}"/) - end - end - - context "--bin parameter set" do - before do - bundle "gem #{gem_name} --bin" - end - - it "builds an exe file that requires the proper entrypoint" do - expect(bundled_app("#{gem_name}/exe/#{gem_name}")).to exist - expect(bundled_app("#{gem_name}/exe/#{gem_name}").read).to match(/require "#{require_path}"/) - end - end - - context "--test parameter set to rspec" do - before do - bundle "gem #{gem_name} --test=rspec" - end - - it "builds a spec helper that requires the proper entrypoint, and a default test in the proper path which fails" do - expect(bundled_app("#{gem_name}/spec/spec_helper.rb")).to exist - expect(bundled_app("#{gem_name}/spec/spec_helper.rb").read).to include(%(require "#{require_path}")) - expect(bundled_app("#{gem_name}/spec/#{require_path}_spec.rb")).to exist - expect(bundled_app("#{gem_name}/spec/#{require_path}_spec.rb").read).to include("expect(false).to eq(true)") - end - end - - context "--test parameter set to minitest" do - before do - bundle "gem #{gem_name} --test=minitest" - end - - it "builds a test helper that requires the proper entrypoint, and default test file in the proper path that defines the proper test class name, requires helper, and fails" do - expect(bundled_app("#{gem_name}/test/test_helper.rb")).to exist - expect(bundled_app("#{gem_name}/test/test_helper.rb").read).to include(%(require "#{require_path}")) - - expect(bundled_app("#{gem_name}/#{minitest_test_file_path}")).to exist - expect(bundled_app("#{gem_name}/#{minitest_test_file_path}").read).to include(minitest_test_class_name) - expect(bundled_app("#{gem_name}/#{minitest_test_file_path}").read).to include(%(require "test_helper")) - expect(bundled_app("#{gem_name}/#{minitest_test_file_path}").read).to include("assert false") - end - end - - context "--test parameter set to test-unit" do - before do - bundle "gem #{gem_name} --test=test-unit" - end - - it "builds a test helper that requires the proper entrypoint, and default test file in the proper path which requires helper and fails" do - expect(bundled_app("#{gem_name}/test/test_helper.rb")).to exist - expect(bundled_app("#{gem_name}/test/test_helper.rb").read).to include(%(require "#{require_path}")) - expect(bundled_app("#{gem_name}/test/#{require_path}_test.rb")).to exist - expect(bundled_app("#{gem_name}/test/#{require_path}_test.rb").read).to include(%(require "test_helper")) - expect(bundled_app("#{gem_name}/test/#{require_path}_test.rb").read).to include("assert_equal(\"expected\", \"actual\")") - end - end - end - - context "with mit option in bundle config settings set to true" do - before do - bundle_config_global "gem.mit true" - end - it_behaves_like "--mit flag" - it_behaves_like "--no-mit flag" - end - - context "with mit option in bundle config settings set to false" do - before do - bundle_config_global "gem.mit false" - end - it_behaves_like "--mit flag" - it_behaves_like "--no-mit flag" - end - - context "with coc option in bundle config settings set to true" do - before do - bundle_config_global "gem.coc true" - end - it_behaves_like "--coc flag" - it_behaves_like "--no-coc flag" - end - - context "with coc option in bundle config settings set to false" do - before do - bundle_config_global "gem.coc false" - end - it_behaves_like "--coc flag" - it_behaves_like "--no-coc flag" - end - - context "with rubocop option in bundle config settings set to true" do - before do - bundle_config_global "gem.rubocop true" - end - it_behaves_like "--linter=rubocop flag" - it_behaves_like "--linter=standard flag" - it_behaves_like "--no-linter flag" - end - - context "with rubocop option in bundle config settings set to false" do - before do - bundle_config_global "gem.rubocop false" - end - it_behaves_like "--linter=rubocop flag" - it_behaves_like "--linter=standard flag" - it_behaves_like "--no-linter flag" - end - - context "with linter option in bundle config settings set to rubocop" do - before do - bundle_config_global "gem.linter rubocop" - end - it_behaves_like "--linter=rubocop flag" - it_behaves_like "--linter=standard flag" - it_behaves_like "--no-linter flag" - end - - context "with linter option in bundle config settings set to standard" do - before do - bundle_config_global "gem.linter standard" - end - it_behaves_like "--linter=rubocop flag" - it_behaves_like "--linter=standard flag" - it_behaves_like "--no-linter flag" - end - - context "with linter option in bundle config settings set to false" do - before do - bundle_config_global "gem.linter false" - end - it_behaves_like "--linter=rubocop flag" - it_behaves_like "--linter=standard flag" - it_behaves_like "--no-linter flag" - end - - context "with changelog option in bundle config settings set to true" do - before do - bundle_config_global "gem.changelog true" - end - it_behaves_like "--changelog flag" - it_behaves_like "--no-changelog flag" - end - - context "with changelog option in bundle config settings set to false" do - before do - bundle_config_global "gem.changelog false" - end - it_behaves_like "--changelog flag" - it_behaves_like "--no-changelog flag" - end - - context "with bundle option in bundle config settings set to true" do - before do - bundle_config_global "gem.bundle true" - end - it_behaves_like "--bundle flag" - it_behaves_like "--no-bundle flag" - - it "runs bundle install" do - bundle "gem #{gem_name}" - expect(out).to include("Running bundle install in the new gem directory.") - end - end - - context "with bundle option in bundle config settings set to false" do - before do - bundle_config_global "gem.bundle false" - end - it_behaves_like "--bundle flag" - it_behaves_like "--no-bundle flag" - - it "does not run bundle install" do - bundle "gem #{gem_name}" - expect(out).to_not include("Running bundle install in the new gem directory.") - end - end - - context "without git config github.user set" do - before do - git("config --global --unset github.user") - end - context "with github-username option in bundle config settings set to some value" do - before do - bundle_config_global "gem.github_username different_username" - end - it_behaves_like "--github-username option", "gh_user" - end - - it_behaves_like "github_username configuration" - - context "with github-username option in bundle config settings set to false" do - before do - bundle_config_global "gem.github_username false" - end - it_behaves_like "--github-username option", "gh_user" - end - - context "when changelog is enabled" do - it "sets gemspec changelog_uri, homepage, homepage_uri, source_code_uri to TODOs" do - bundle "gem #{gem_name} --changelog" - - expect(generated_gemspec.metadata["changelog_uri"]). - to eq("TODO: Put your gem's CHANGELOG.md URL here.") - expect(generated_gemspec.homepage).to eq("TODO: Put your gem's website or public repo URL here.") - expect(generated_gemspec.metadata["homepage_uri"]).to eq("TODO: Put your gem's website or public repo URL here.") - expect(generated_gemspec.metadata["source_code_uri"]).to eq("TODO: Put your gem's public repo URL here.") - end - end - - context "when changelog is not enabled" do - it "sets gemspec homepage, homepage_uri, source_code_uri to TODOs and changelog_uri to nil" do - bundle "gem #{gem_name}" - - expect(generated_gemspec.metadata["changelog_uri"]).to be_nil - expect(generated_gemspec.homepage).to eq("TODO: Put your gem's website or public repo URL here.") - expect(generated_gemspec.metadata["homepage_uri"]).to eq("TODO: Put your gem's website or public repo URL here.") - expect(generated_gemspec.metadata["source_code_uri"]).to eq("TODO: Put your gem's public repo URL here.") - end - end - end - - context "with git config github.user set" do - context "with github-username option in bundle config settings set to some value" do - before do - bundle_config_global "gem.github_username different_username" - end - it_behaves_like "--github-username option", "gh_user" - end - - it_behaves_like "github_username configuration" - - context "with github-username option in bundle config settings set to false" do - before do - bundle_config_global "gem.github_username false" - end - it_behaves_like "--github-username option", "gh_user" - end - - context "when changelog is enabled" do - it "sets gemspec changelog_uri, homepage, homepage_uri, source_code_uri based on git username" do - bundle "gem #{gem_name} --changelog" - - expect(generated_gemspec.metadata["changelog_uri"]). - to eq("https://github.com/bundleuser/#{gem_name}/blob/main/CHANGELOG.md") - expect(generated_gemspec.homepage).to eq("https://github.com/bundleuser/#{gem_name}") - expect(generated_gemspec.metadata["homepage_uri"]).to eq("https://github.com/bundleuser/#{gem_name}") - expect(generated_gemspec.metadata["source_code_uri"]).to eq("https://github.com/bundleuser/#{gem_name}") - end - end - - context "when changelog is not enabled" do - it "sets gemspec source_code_uri, homepage, homepage_uri but not changelog_uri" do - bundle "gem #{gem_name}" - - expect(generated_gemspec.metadata["changelog_uri"]).to be_nil - expect(generated_gemspec.homepage).to eq("https://github.com/bundleuser/#{gem_name}") - expect(generated_gemspec.metadata["homepage_uri"]).to eq("https://github.com/bundleuser/#{gem_name}") - expect(generated_gemspec.metadata["source_code_uri"]).to eq("https://github.com/bundleuser/#{gem_name}") - end - end - end - - context "standard gem naming" do - let(:require_path) { gem_name } - - let(:require_relative_path) { gem_name } - - let(:minitest_test_file_path) { "test/test_#{gem_name}.rb" } - - let(:minitest_test_class_name) { "class TestMygem < Minitest::Test" } - - include_examples "paths that depend on gem name" - end - - context "gem naming with underscore" do - let(:gem_name) { "test_gem" } - - let(:require_path) { "test_gem" } - - let(:require_relative_path) { "test_gem" } - - let(:minitest_test_file_path) { "test/test_test_gem.rb" } - - let(:minitest_test_class_name) { "class TestTestGem < Minitest::Test" } - - let(:flags) { nil } - - it "does not nest constants" do - bundle ["gem", gem_name, flags].compact.join(" ") - expect(bundled_app("#{gem_name}/lib/#{require_path}/version.rb").read).to match(/module TestGem/) - expect(bundled_app("#{gem_name}/lib/#{require_path}.rb").read).to match(/module TestGem/) - end - - include_examples "paths that depend on gem name" - - context "--ext parameter set with C" do - let(:flags) { "--ext=c" } - - before do - bundle ["gem", gem_name, flags].compact.join(" ") - end - - it "builds ext skeleton" do - expect(bundled_app("#{gem_name}/ext/#{gem_name}/extconf.rb")).to exist - expect(bundled_app("#{gem_name}/ext/#{gem_name}/#{gem_name}.h")).to exist - expect(bundled_app("#{gem_name}/ext/#{gem_name}/#{gem_name}.c")).to exist - end - - it "generates native extension loading code" do - expect(bundled_app("#{gem_name}/lib/#{gem_name}.rb").read).to include(<<~RUBY) - require_relative "test_gem/version" - require "#{gem_name}/#{gem_name}" - RUBY - end - - it "includes rake-compiler, but no Rust related changes" do - expect(bundled_app("#{gem_name}/Gemfile").read).to include('gem "rake-compiler"') - - expect(bundled_app("#{gem_name}/#{gem_name}.gemspec").read).to_not include('spec.add_dependency "rb_sys"') - expect(bundled_app("#{gem_name}/#{gem_name}.gemspec").read).to_not include('spec.required_rubygems_version = ">= ') - end - - it "depends on compile task for build" do - rakefile = <<~RAKEFILE - # frozen_string_literal: true - - require "bundler/gem_tasks" - require "rake/extensiontask" - - task build: :compile - - GEMSPEC = Gem::Specification.load("#{gem_name}.gemspec") - - Rake::ExtensionTask.new("#{gem_name}", GEMSPEC) do |ext| - ext.lib_dir = "lib/#{gem_name}" - end - - task default: %i[clobber compile] - RAKEFILE - - expect(bundled_app("#{gem_name}/Rakefile").read).to eq(rakefile) - end - end - - context "--ext parameter set with rust" do - let(:flags) { "--ext=rust" } - - before do - bundle ["gem", gem_name, flags].compact.join(" ") - end - - it "is not deprecated" do - expect(err).not_to include "[DEPRECATED] Option `--ext` without explicit value is deprecated." - end - - it "builds ext skeleton" do - expect(bundled_app("#{gem_name}/Cargo.toml")).to exist - expect(bundled_app("#{gem_name}/ext/#{gem_name}/Cargo.toml")).to exist - expect(bundled_app("#{gem_name}/ext/#{gem_name}/extconf.rb")).to exist - expect(bundled_app("#{gem_name}/ext/#{gem_name}/src/lib.rs")).to exist - expect(bundled_app("#{gem_name}/ext/#{gem_name}/build.rs")).to exist - end - - it "includes rake-compiler and rb_sys gems constraint" do - expect(bundled_app("#{gem_name}/Gemfile").read).to include('gem "rake-compiler"') - expect(bundled_app("#{gem_name}/#{gem_name}.gemspec").read).to include('spec.add_dependency "rb_sys"') - end - - it "depends on compile task for build" do - rakefile = <<~RAKEFILE - # frozen_string_literal: true - - require "bundler/gem_tasks" - require "rb_sys/extensiontask" - - task build: :compile - - GEMSPEC = Gem::Specification.load("#{gem_name}.gemspec") - - RbSys::ExtensionTask.new("#{gem_name}", GEMSPEC) do |ext| - ext.lib_dir = "lib/#{gem_name}" - end - - task default: :compile - RAKEFILE - - expect(bundled_app("#{gem_name}/Rakefile").read).to eq(rakefile) - end - - it "configures the crate such that `cargo test` works", :ruby_repo, :mri_only do - env = setup_rust_env - gem_path = bundled_app(gem_name) - result = sys_exec("cargo test", env: env, dir: gem_path, timeout: 300) - - expect(result).to include("1 passed") - end - - def setup_rust_env - skip "rust toolchain of mingw is broken" if RUBY_PLATFORM.match?("mingw") - - env = { - "CARGO_HOME" => ENV.fetch("CARGO_HOME", File.join(ENV["HOME"], ".cargo")), - "RUSTUP_HOME" => ENV.fetch("RUSTUP_HOME", File.join(ENV["HOME"], ".rustup")), - "RUSTUP_TOOLCHAIN" => ENV.fetch("RUSTUP_TOOLCHAIN", "stable"), - } - - system(env, "cargo", "-V", out: IO::NULL, err: [:child, :out]) - skip "cargo not present" unless $?.success? - # Hermetic Cargo setup - RbConfig::CONFIG.each {|k, v| env["RBCONFIG_#{k}"] = v } - env - end - end - - context "--ext parameter set with go" do - let(:flags) { "--ext=go" } - - before do - bundle ["gem", gem_name, flags].compact.join(" ") - end - - after do - sys_exec("go clean -modcache", raise_on_error: true) if installed_go? - end - - it "is not deprecated" do - expect(err).not_to include "[DEPRECATED] Option `--ext` without explicit value is deprecated." - end - - it "builds ext skeleton" do - expect(bundled_app("#{gem_name}/ext/#{gem_name}/#{gem_name}.c")).to exist - expect(bundled_app("#{gem_name}/ext/#{gem_name}/#{gem_name}.go")).to exist - expect(bundled_app("#{gem_name}/ext/#{gem_name}/#{gem_name}.h")).to exist - expect(bundled_app("#{gem_name}/ext/#{gem_name}/extconf.rb")).to exist - expect(bundled_app("#{gem_name}/ext/#{gem_name}/go.mod")).to exist - end - - it "includes extconf.rb in gem_name.gemspec" do - expect(bundled_app("#{gem_name}/#{gem_name}.gemspec").read).to include(%(spec.extensions = ["ext/#{gem_name}/extconf.rb"])) - end - - it "includes go_gem in gem_name.gemspec" do - expect(bundled_app("#{gem_name}/#{gem_name}.gemspec").read).to include('spec.add_dependency "go_gem", ">= 0.2"') - end - - it "includes go_gem extension in extconf.rb" do - expect(bundled_app("#{gem_name}/ext/#{gem_name}/extconf.rb").read).to include(<<~RUBY) - require "mkmf" - require "go_gem/mkmf" - RUBY - - expect(bundled_app("#{gem_name}/ext/#{gem_name}/extconf.rb").read).to include(%(create_go_makefile("#{gem_name}/#{gem_name}"))) - expect(bundled_app("#{gem_name}/ext/#{gem_name}/extconf.rb").read).not_to include("create_makefile") - end - - it "includes go_gem extension in gem_name.c" do - expect(bundled_app("#{gem_name}/ext/#{gem_name}/#{gem_name}.c").read).to eq(<<~C) - #include "#{gem_name}.h" - #include "_cgo_export.h" - C - end - - it "includes skeleton code in gem_name.go" do - expect(bundled_app("#{gem_name}/ext/#{gem_name}/#{gem_name}.go").read).to include(<<~GO) - /* - #include "#{gem_name}.h" - - VALUE rb_#{gem_name}_sum(VALUE self, VALUE a, VALUE b); - */ - import "C" - GO - - expect(bundled_app("#{gem_name}/ext/#{gem_name}/#{gem_name}.go").read).to include(<<~GO) - //export rb_#{gem_name}_sum - func rb_#{gem_name}_sum(_ C.VALUE, a C.VALUE, b C.VALUE) C.VALUE { - GO - - expect(bundled_app("#{gem_name}/ext/#{gem_name}/#{gem_name}.go").read).to include(<<~GO) - //export Init_#{gem_name} - func Init_#{gem_name}() { - GO - end - - it "includes valid module name in go.mod" do - expect(bundled_app("#{gem_name}/ext/#{gem_name}/go.mod").read).to include("module github.com/bundleuser/#{gem_name}") - end - - it "includes go_gem extension in Rakefile" do - expect(bundled_app("#{gem_name}/Rakefile").read).to include(<<~RUBY) - require "go_gem/rake_task" - - GoGem::RakeTask.new("#{gem_name}") - RUBY - end - - context "with --no-ci" do - let(:flags) { "--ext=go --no-ci" } - - it_behaves_like "CI config is absent" - end - - context "--ci set to github" do - let(:flags) { "--ext=go --ci=github" } - - it "generates .github/workflows/main.yml" do - expect(bundled_app("#{gem_name}/.github/workflows/main.yml")).to exist - expect(bundled_app("#{gem_name}/.github/workflows/main.yml").read).to include("go-version-file: ext/#{gem_name}/go.mod") - end - end - - context "--ci set to circle" do - let(:flags) { "--ext=go --ci=circle" } - - it "generates a .circleci/config.yml" do - expect(bundled_app("#{gem_name}/.circleci/config.yml")).to exist - - expect(bundled_app("#{gem_name}/.circleci/config.yml").read).to include(<<-YAML.strip) - environment: - GO_VERSION: - YAML - - expect(bundled_app("#{gem_name}/.circleci/config.yml").read).to include(<<-YAML) - - run: - name: Install Go - command: | - wget https://go.dev/dl/go$GO_VERSION.linux-amd64.tar.gz -O /tmp/go.tar.gz - tar -C /usr/local -xzf /tmp/go.tar.gz - echo 'export PATH=/usr/local/go/bin:"$PATH"' >> "$BASH_ENV" - YAML - end - end - - context "--ci set to gitlab" do - let(:flags) { "--ext=go --ci=gitlab" } - - it "generates a .gitlab-ci.yml" do - expect(bundled_app("#{gem_name}/.gitlab-ci.yml")).to exist - - expect(bundled_app("#{gem_name}/.gitlab-ci.yml").read).to include(<<-YAML) - - wget https://go.dev/dl/go$GO_VERSION.linux-amd64.tar.gz -O /tmp/go.tar.gz - - tar -C /usr/local -xzf /tmp/go.tar.gz - - export PATH=/usr/local/go/bin:$PATH - YAML - - expect(bundled_app("#{gem_name}/.gitlab-ci.yml").read).to include(<<-YAML.strip) - variables: - GO_VERSION: - YAML - end - end - - context "without github.user" do - before do - # FIXME: GitHub Actions Windows Runner hang up here for some reason... - skip "Workaround for hung up" if Gem.win_platform? - - git("config --global --unset github.user") - bundle ["gem", gem_name, flags].compact.join(" ") - end - - it "includes valid module name in go.mod" do - expect(bundled_app("#{gem_name}/ext/#{gem_name}/go.mod").read).to include("module github.com/username/#{gem_name}") - end - end - end - end - - context "gem naming with dashed" do - let(:gem_name) { "test-gem" } - - let(:require_path) { "test/gem" } - - let(:require_relative_path) { "gem" } - - let(:minitest_test_file_path) { "test/test/test_gem.rb" } - - let(:minitest_test_class_name) { "class Test::TestGem < Minitest::Test" } - - it "nests constants so they work" do - bundle "gem #{gem_name}" - expect(bundled_app("#{gem_name}/lib/#{require_path}/version.rb").read).to match(/module Test\n module Gem/) - expect(bundled_app("#{gem_name}/lib/#{require_path}.rb").read).to match(/module Test\n module Gem/) - end - - include_examples "paths that depend on gem name" - end - - describe "uncommon gem names" do - it "can deal with two dashes" do - bundle "gem a--a" - - expect(bundled_app("a--a/a--a.gemspec")).to exist - end - - it "fails gracefully with a ." do - bundle "gem foo.gemspec", raise_on_error: false - expect(err).to end_with("Invalid gem name foo.gemspec -- `Foo.gemspec` is an invalid constant name") - end - - it "fails gracefully with a ^" do - bundle "gem ^", raise_on_error: false - expect(err).to end_with("Invalid gem name ^ -- `^` is an invalid constant name") - end - - it "fails gracefully with a space" do - bundle "gem 'foo bar'", raise_on_error: false - expect(err).to end_with("Invalid gem name foo bar -- `Foo bar` is an invalid constant name") - end - - it "fails gracefully when multiple names are passed" do - bundle "gem foo bar baz", raise_on_error: false - expect(err).to eq(<<-E.strip) -ERROR: "bundle gem" was called with arguments ["foo", "bar", "baz"] -Usage: "bundle gem NAME [OPTIONS]" - E - end - end - - describe "#ensure_safe_gem_name" do - before do - bundle "gem #{subject}", raise_on_error: false - end - - context "with an existing const name" do - subject { "gem" } - it { expect(err).to include("Invalid gem name #{subject}") } - end - - context "with an existing hyphenated const name" do - subject { "gem-specification" } - it { expect(err).to include("Invalid gem name #{subject}") } - end - - context "starting with a number" do - subject { "1gem" } - it { expect(err).to include("Invalid gem name #{subject}") } - end - - context "including capital letter" do - subject { "CAPITAL" } - it "should warn but not error" do - expect(err).to include("Gem names with capital letters are not recommended") - expect(bundled_app("#{subject}/#{subject}.gemspec")).to exist - end - end - - context "starting with an existing const name" do - subject { "gem-somenewconstantname" } - it { expect(err).not_to include("Invalid gem name #{subject}") } - end - - context "ending with an existing const name" do - subject { "somenewconstantname-gem" } - it { expect(err).not_to include("Invalid gem name #{subject}") } - end - end - - context "on first run", :readline do - it "asks about test framework" do - bundle_config_global "BUNDLE_GEM__TEST" => nil - - bundle "gem foobar" do |input, _, _| - input.puts "rspec" - end - - expect(bundled_app("foobar/spec/spec_helper.rb")).to exist - rakefile = <<~RAKEFILE - # frozen_string_literal: true - - require "bundler/gem_tasks" - require "rspec/core/rake_task" - - RSpec::Core::RakeTask.new(:spec) - - task default: :spec - RAKEFILE - - expect(bundled_app("foobar/Rakefile").read).to eq(rakefile) - expect(bundled_app("foobar/Gemfile").read).to include('gem "rspec"') - end - - it "asks about CI service" do - bundle_config_global "BUNDLE_GEM__CI" => nil - - bundle "gem foobar" do |input, _, _| - input.puts "github" - end - - expect(bundled_app("foobar/.github/workflows/main.yml")).to exist - end - - it "asks about MIT license just once" do - bundle_config_global "BUNDLE_GEM__MIT" => nil - - bundle "config list" - - bundle "gem foobar" do |input, _, _| - input.puts "yes" - end - - expect(bundled_app("foobar/LICENSE.txt")).to exist - expect(out).to include("Using a MIT license means").once - end - - it "asks about CoC just once" do - bundle_config_global "BUNDLE_GEM__COC" => nil - - bundle "gem foobar" do |input, _, _| - input.puts "yes" - end - - expect(bundled_app("foobar/CODE_OF_CONDUCT.md")).to exist - expect(out).to include("Codes of conduct can increase contributions to your project").once - end - - it "asks about CHANGELOG just once" do - bundle_config_global "BUNDLE_GEM__CHANGELOG" => nil - - bundle "gem foobar" do |input, _, _| - input.puts "yes" - end - - expect(bundled_app("foobar/CHANGELOG.md")).to exist - expect(out).to include("A changelog is a file which contains").once - end - end - - context "on conflicts with a previously created file" do - it "should fail gracefully" do - FileUtils.touch(bundled_app("conflict-foobar")) - bundle "gem conflict-foobar", raise_on_error: false - expect(err).to eq("Couldn't create a new gem named `conflict-foobar` because there's an existing file named `conflict-foobar`.") - expect(exitstatus).to eql(32) - end - end - - context "on conflicts with a previously created directory" do - it "should succeed" do - FileUtils.mkdir_p(bundled_app("conflict-foobar/Gemfile")) - bundle "gem conflict-foobar" - expect(out).to include("file_clash conflict-foobar/Gemfile"). - and include "Initializing git repo in #{bundled_app("conflict-foobar")}" - end - end end diff --git a/spec/bundler/commands/outdated_filters_spec.rb b/spec/bundler/commands/outdated_filters_spec.rb new file mode 100644 index 00000000000000..44c4b2a8846bd7 --- /dev/null +++ b/spec/bundler/commands/outdated_filters_spec.rb @@ -0,0 +1,629 @@ +# frozen_string_literal: true + +RSpec.describe "bundle outdated" do + it "performs an automatic bundle install" do + gemfile <<-G + source "https://gem.repo1" + gem "myrack", "0.9.1" + gem "foo" + G + + bundle_config "auto_install 1" + bundle :outdated, raise_on_error: false + expect(out).to include("Installing foo 1.0") + end + + context "in deployment mode" do + before do + build_repo2 + + gemfile <<-G + source "https://gem.repo2" + + gem "myrack" + gem "foo" + G + bundle :lock + bundle_config "deployment true" + end + + it "outputs a helpful message about being in deployment mode" do + update_repo2 { build_gem "activesupport", "3.0" } + + bundle "outdated", raise_on_error: false + expect(last_command).to be_failure + expect(err).to include("You are trying to check outdated gems in deployment mode.") + expect(err).to include("Run `bundle outdated` elsewhere.") + expect(err).to include("If this is a development machine, remove the ") + expect(err).to include("Gemfile freeze\nby running `bundle config unset deployment`.") + end + end + + context "after bundle config set --local deployment true" do + before do + build_repo2 do + build_git "foo", path: lib_path("foo") + build_git "zebra", path: lib_path("zebra") + end + + install_gemfile <<-G + source "https://gem.repo2" + + gem "myrack" + gem "foo" + G + bundle_config "deployment true" + end + + it "outputs a helpful message about being in deployment mode" do + update_repo2 { build_gem "activesupport", "3.0" } + + bundle "outdated", raise_on_error: false + expect(last_command).to be_failure + expect(err).to include("You are trying to check outdated gems in deployment mode.") + expect(err).to include("Run `bundle outdated` elsewhere.") + expect(err).to include("If this is a development machine, remove the ") + expect(err).to include("Gemfile freeze\nby running `bundle config unset deployment`.") + end + end + + context "update available for a gem on a different platform" do + before do + build_repo2 + + install_gemfile <<-G + source "https://gem.repo2" + gem "laduradura", '= 5.15.2' + G + end + + it "reports that no updates are available" do + bundle "outdated" + expect(out).to end_with("Bundle up to date!") + end + end + + context "update available for a gem on the same platform while multiple platforms used for gem" do + before do + build_repo2 + end + + it "reports that updates are available if the Ruby platform is used" do + install_gemfile <<-G + source "https://gem.repo2" + gem "laduradura", '= 5.15.2', :platforms => [:ruby, :jruby] + G + + bundle "outdated" + expect(out).to end_with("Bundle up to date!") + end + + it "reports that updates are available if the JRuby platform is used", :jruby_only do + install_gemfile <<-G + source "https://gem.repo2" + gem "laduradura", '= 5.15.2', :platforms => [:ruby, :jruby] + G + + bundle "outdated", raise_on_error: false + + expected_output = <<~TABLE.strip + Gem Current Latest Requested Groups Release Date + laduradura 5.15.2 5.15.3 = 5.15.2 default + TABLE + + expect(out).to end_with(expected_output) + end + end + + shared_examples_for "version update is detected" do + it "reports that a gem has a newer version" do + subject + + outdated_gems = out.split("\n").drop_while {|l| !l.start_with?("Gem") }[1..-1] + + expect(outdated_gems.size).to be > 0 + end + end + + shared_examples_for "major version updates are detected" do + before do + build_repo2 do + build_git "foo", path: lib_path("foo") + build_git "zebra", path: lib_path("zebra") + end + + install_gemfile <<-G + source "https://gem.repo2" + gem "zebra", :git => "#{lib_path("zebra")}" + gem "foo", :git => "#{lib_path("foo")}" + gem "activesupport", "2.3.5" + gem "weakling", "~> 0.0.1" + gem "duradura", '7.0' + gem "terranova", '8' + G + + update_repo2 do + build_gem "activesupport", "3.3.5" + build_gem "weakling", "0.8.0" + end + end + + it_behaves_like "version update is detected" + end + + context "when on a new machine" do + before do + build_repo2 do + build_git "foo", path: lib_path("foo") + build_git "zebra", path: lib_path("zebra") + end + + install_gemfile <<-G + source "https://gem.repo2" + gem "zebra", :git => "#{lib_path("zebra")}" + gem "foo", :git => "#{lib_path("foo")}" + gem "activesupport", "2.3.5" + gem "weakling", "~> 0.0.1" + gem "duradura", '7.0' + gem "terranova", '8' + G + + pristine_system_gems + + update_git "foo", path: lib_path("foo") + update_repo2 do + build_gem "activesupport", "3.3.5" + build_gem "weakling", "0.8.0" + end + end + + subject { bundle "outdated", raise_on_error: false } + it_behaves_like "version update is detected" + end + + shared_examples_for "minor version updates are detected" do + before do + build_repo2 do + build_git "foo", path: lib_path("foo") + build_git "zebra", path: lib_path("zebra") + end + + install_gemfile <<-G + source "https://gem.repo2" + gem "zebra", :git => "#{lib_path("zebra")}" + gem "foo", :git => "#{lib_path("foo")}" + gem "activesupport", "2.3.5" + gem "weakling", "~> 0.0.1" + gem "duradura", '7.0' + gem "terranova", '8' + G + + update_repo2 do + build_gem "activesupport", "2.7.5" + build_gem "weakling", "2.0.1" + end + end + + it_behaves_like "version update is detected" + end + + shared_examples_for "patch version updates are detected" do + before do + build_repo2 do + build_git "foo", path: lib_path("foo") + build_git "zebra", path: lib_path("zebra") + end + + install_gemfile <<-G + source "https://gem.repo2" + gem "zebra", :git => "#{lib_path("zebra")}" + gem "foo", :git => "#{lib_path("foo")}" + gem "activesupport", "2.3.5" + gem "weakling", "~> 0.0.1" + gem "duradura", '7.0' + gem "terranova", '8' + G + + update_repo2 do + build_gem "activesupport", "2.3.7" + build_gem "weakling", "0.3.1" + end + end + + it_behaves_like "version update is detected" + end + + shared_examples_for "no version updates are detected" do + it "does not detect any version updates" do + subject + expect(out).to end_with("updates to display.") + end + end + + shared_examples_for "major version is ignored" do + before do + build_repo2 do + build_git "foo", path: lib_path("foo") + build_git "zebra", path: lib_path("zebra") + end + + install_gemfile <<-G + source "https://gem.repo2" + gem "zebra", :git => "#{lib_path("zebra")}" + gem "foo", :git => "#{lib_path("foo")}" + gem "activesupport", "2.3.5" + gem "weakling", "~> 0.0.1" + gem "duradura", '7.0' + gem "terranova", '8' + G + + update_repo2 do + build_gem "activesupport", "3.3.5" + build_gem "weakling", "1.0.1" + end + end + + it_behaves_like "no version updates are detected" + end + + shared_examples_for "minor version is ignored" do + before do + build_repo2 do + build_git "foo", path: lib_path("foo") + build_git "zebra", path: lib_path("zebra") + end + + install_gemfile <<-G + source "https://gem.repo2" + gem "zebra", :git => "#{lib_path("zebra")}" + gem "foo", :git => "#{lib_path("foo")}" + gem "activesupport", "2.3.5" + gem "weakling", "~> 0.0.1" + gem "duradura", '7.0' + gem "terranova", '8' + G + + update_repo2 do + build_gem "activesupport", "2.4.5" + build_gem "weakling", "0.3.1" + end + end + + it_behaves_like "no version updates are detected" + end + + shared_examples_for "patch version is ignored" do + before do + build_repo2 do + build_git "foo", path: lib_path("foo") + build_git "zebra", path: lib_path("zebra") + end + + install_gemfile <<-G + source "https://gem.repo2" + gem "zebra", :git => "#{lib_path("zebra")}" + gem "foo", :git => "#{lib_path("foo")}" + gem "activesupport", "2.3.5" + gem "weakling", "~> 0.0.1" + gem "duradura", '7.0' + gem "terranova", '8' + G + + update_repo2 do + build_gem "activesupport", "2.3.6" + build_gem "weakling", "0.0.4" + end + end + + it_behaves_like "no version updates are detected" + end + + describe "with --filter-major option" do + subject { bundle "outdated --filter-major", raise_on_error: false } + + it_behaves_like "major version updates are detected" + it_behaves_like "minor version is ignored" + it_behaves_like "patch version is ignored" + end + + describe "with --filter-minor option" do + subject { bundle "outdated --filter-minor", raise_on_error: false } + + it_behaves_like "minor version updates are detected" + it_behaves_like "major version is ignored" + it_behaves_like "patch version is ignored" + end + + describe "with --filter-patch option" do + subject { bundle "outdated --filter-patch", raise_on_error: false } + + it_behaves_like "patch version updates are detected" + it_behaves_like "major version is ignored" + it_behaves_like "minor version is ignored" + end + + describe "with --filter-minor --filter-patch options" do + subject { bundle "outdated --filter-minor --filter-patch", raise_on_error: false } + + it_behaves_like "minor version updates are detected" + it_behaves_like "patch version updates are detected" + it_behaves_like "major version is ignored" + end + + describe "with --filter-major --filter-minor options" do + subject { bundle "outdated --filter-major --filter-minor", raise_on_error: false } + + it_behaves_like "major version updates are detected" + it_behaves_like "minor version updates are detected" + it_behaves_like "patch version is ignored" + end + + describe "with --filter-major --filter-patch options" do + subject { bundle "outdated --filter-major --filter-patch", raise_on_error: false } + + it_behaves_like "major version updates are detected" + it_behaves_like "patch version updates are detected" + it_behaves_like "minor version is ignored" + end + + describe "with --filter-major --filter-minor --filter-patch options" do + subject { bundle "outdated --filter-major --filter-minor --filter-patch", raise_on_error: false } + + it_behaves_like "major version updates are detected" + it_behaves_like "minor version updates are detected" + it_behaves_like "patch version updates are detected" + end + + context "conservative updates" do + before do + build_repo4 do + build_gem "patch", %w[1.0.0 1.0.1] + build_gem "minor", %w[1.0.0 1.0.1 1.1.0] + build_gem "major", %w[1.0.0 1.0.1 1.1.0 2.0.0] + end + + # establish a lockfile set to 1.0.0 + install_gemfile <<-G + source "https://gem.repo4" + gem 'patch', '1.0.0' + gem 'minor', '1.0.0' + gem 'major', '1.0.0' + G + + # remove all version requirements + gemfile <<-G + source "https://gem.repo4" + gem 'patch' + gem 'minor' + gem 'major' + G + end + + it "shows nothing when patching and filtering to minor" do + bundle "outdated --patch --filter-minor" + + expect(out).to end_with("No minor updates to display.") + end + + it "shows all gems when patching and filtering to patch" do + bundle "outdated --patch --filter-patch", raise_on_error: false + + expected_output = <<~TABLE.strip + Gem Current Latest Requested Groups Release Date + major 1.0.0 1.0.1 >= 0 default + minor 1.0.0 1.0.1 >= 0 default + patch 1.0.0 1.0.1 >= 0 default + TABLE + + expect(out).to end_with(expected_output) + end + + it "shows minor and major when updating to minor and filtering to patch and minor" do + bundle "outdated --minor --filter-minor", raise_on_error: false + + expected_output = <<~TABLE.strip + Gem Current Latest Requested Groups Release Date + major 1.0.0 1.1.0 >= 0 default + minor 1.0.0 1.1.0 >= 0 default + TABLE + + expect(out).to end_with(expected_output) + end + + it "shows minor when updating to major and filtering to minor with parseable" do + bundle "outdated --major --filter-minor --parseable", raise_on_error: false + + expect(out).not_to include("patch (newest") + expect(out).to include("minor (newest") + expect(out).not_to include("major (newest") + end + end + + context "tricky conservative updates" do + before do + build_repo4 do + build_gem "foo", %w[1.4.3 1.4.4] do |s| + s.add_dependency "bar", "~> 2.0" + end + build_gem "foo", %w[1.4.5 1.5.0] do |s| + s.add_dependency "bar", "~> 2.1" + end + build_gem "foo", %w[1.5.1] do |s| + s.add_dependency "bar", "~> 3.0" + end + build_gem "bar", %w[2.0.3 2.0.4 2.0.5 2.1.0 2.1.1 3.0.0] + build_gem "qux", %w[1.0.0 1.1.0 2.0.0] + end + + # establish a lockfile set to 1.4.3 + install_gemfile <<-G + source "https://gem.repo4" + gem 'foo', '1.4.3' + gem 'bar', '2.0.3' + gem 'qux', '1.0.0' + G + + # remove 1.4.3 requirement and bar altogether + # to setup update specs below + gemfile <<-G + source "https://gem.repo4" + gem 'foo' + gem 'qux' + G + end + + it "shows gems updating to patch and filtering to patch" do + bundle "outdated --patch --filter-patch", raise_on_error: false, env: { "DEBUG_RESOLVER" => "1" } + + expected_output = <<~TABLE.strip + Gem Current Latest Requested Groups Release Date + bar 2.0.3 2.0.5 + foo 1.4.3 1.4.4 >= 0 default + TABLE + + expect(out).to end_with(expected_output) + end + + it "shows gems updating to patch and filtering to patch, in debug mode" do + bundle "outdated --patch --filter-patch", raise_on_error: false, env: { "DEBUG" => "1" } + + expected_output = <<~TABLE.strip + Gem Current Latest Requested Groups Release Date Path + bar 2.0.3 2.0.5 + foo 1.4.3 1.4.4 >= 0 default + TABLE + + expect(out).to end_with(expected_output) + end + end + + describe "with --only-explicit" do + it "does not report outdated dependent gems" do + build_repo4 do + build_gem "weakling", %w[0.2 0.3] do |s| + s.add_dependency "bar", "~> 2.1" + end + build_gem "bar", %w[2.1 2.2] + end + + install_gemfile <<-G + source "https://gem.repo4" + gem 'weakling', '0.2' + gem 'bar', '2.1' + G + + gemfile <<-G + source "https://gem.repo4" + gem 'weakling' + G + + bundle "outdated --only-explicit", raise_on_error: false + + expected_output = <<~TABLE.strip + Gem Current Latest Requested Groups Release Date + weakling 0.2 0.3 >= 0 default + TABLE + + expect(out).to end_with(expected_output) + end + end + + describe "with a multiplatform lockfile" do + before do + build_repo4 do + build_gem "nokogiri", "1.11.1" + build_gem "nokogiri", "1.11.1" do |s| + s.platform = Bundler.local_platform + end + + build_gem "nokogiri", "1.11.2" + build_gem "nokogiri", "1.11.2" do |s| + s.platform = Bundler.local_platform + end + end + + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + nokogiri (1.11.1) + nokogiri (1.11.1-#{Bundler.local_platform}) + + PLATFORMS + ruby + #{Bundler.local_platform} + + DEPENDENCIES + nokogiri + + BUNDLED WITH + #{Bundler::VERSION} + L + + gemfile <<-G + source "https://gem.repo4" + gem "nokogiri" + G + end + + it "reports a single entry per gem" do + bundle "outdated", raise_on_error: false + + expected_output = <<~TABLE.strip + Gem Current Latest Requested Groups Release Date + nokogiri 1.11.1 1.11.2 >= 0 default + TABLE + + expect(out).to end_with(expected_output) + end + end + + context "when a gem is no longer a dependency after a full update" do + before do + build_repo4 do + build_gem "mini_portile2", "2.5.2" do |s| + s.add_dependency "net-ftp", "~> 0.1" + end + + build_gem "mini_portile2", "2.5.3" + + build_gem "net-ftp", "0.1.2" + end + + gemfile <<~G + source "https://gem.repo4" + + gem "mini_portile2" + G + + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + mini_portile2 (2.5.2) + net-ftp (~> 0.1) + net-ftp (0.1.2) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + mini_portile2 + + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "works" do + bundle "outdated", raise_on_error: false + + expected_output = <<~TABLE.strip + Gem Current Latest Requested Groups Release Date + mini_portile2 2.5.2 2.5.3 >= 0 default + TABLE + + expect(out).to end_with(expected_output) + end + end +end diff --git a/spec/bundler/commands/outdated_spec.rb b/spec/bundler/commands/outdated_spec.rb index dea78ba83f0c5d..e6a276cceafde4 100644 --- a/spec/bundler/commands/outdated_spec.rb +++ b/spec/bundler/commands/outdated_spec.rb @@ -744,630 +744,4 @@ def test_group_option(group) expect(exitstatus).to_not be_zero end end - - it "performs an automatic bundle install" do - gemfile <<-G - source "https://gem.repo1" - gem "myrack", "0.9.1" - gem "foo" - G - - bundle_config "auto_install 1" - bundle :outdated, raise_on_error: false - expect(out).to include("Installing foo 1.0") - end - - context "in deployment mode" do - before do - build_repo2 - - gemfile <<-G - source "https://gem.repo2" - - gem "myrack" - gem "foo" - G - bundle :lock - bundle_config "deployment true" - end - - it "outputs a helpful message about being in deployment mode" do - update_repo2 { build_gem "activesupport", "3.0" } - - bundle "outdated", raise_on_error: false - expect(last_command).to be_failure - expect(err).to include("You are trying to check outdated gems in deployment mode.") - expect(err).to include("Run `bundle outdated` elsewhere.") - expect(err).to include("If this is a development machine, remove the ") - expect(err).to include("Gemfile freeze\nby running `bundle config unset deployment`.") - end - end - - context "after bundle config set --local deployment true" do - before do - build_repo2 do - build_git "foo", path: lib_path("foo") - build_git "zebra", path: lib_path("zebra") - end - - install_gemfile <<-G - source "https://gem.repo2" - - gem "myrack" - gem "foo" - G - bundle_config "deployment true" - end - - it "outputs a helpful message about being in deployment mode" do - update_repo2 { build_gem "activesupport", "3.0" } - - bundle "outdated", raise_on_error: false - expect(last_command).to be_failure - expect(err).to include("You are trying to check outdated gems in deployment mode.") - expect(err).to include("Run `bundle outdated` elsewhere.") - expect(err).to include("If this is a development machine, remove the ") - expect(err).to include("Gemfile freeze\nby running `bundle config unset deployment`.") - end - end - - context "update available for a gem on a different platform" do - before do - build_repo2 - - install_gemfile <<-G - source "https://gem.repo2" - gem "laduradura", '= 5.15.2' - G - end - - it "reports that no updates are available" do - bundle "outdated" - expect(out).to end_with("Bundle up to date!") - end - end - - context "update available for a gem on the same platform while multiple platforms used for gem" do - before do - build_repo2 - end - - it "reports that updates are available if the Ruby platform is used" do - install_gemfile <<-G - source "https://gem.repo2" - gem "laduradura", '= 5.15.2', :platforms => [:ruby, :jruby] - G - - bundle "outdated" - expect(out).to end_with("Bundle up to date!") - end - - it "reports that updates are available if the JRuby platform is used", :jruby_only do - install_gemfile <<-G - source "https://gem.repo2" - gem "laduradura", '= 5.15.2', :platforms => [:ruby, :jruby] - G - - bundle "outdated", raise_on_error: false - - expected_output = <<~TABLE.strip - Gem Current Latest Requested Groups Release Date - laduradura 5.15.2 5.15.3 = 5.15.2 default - TABLE - - expect(out).to end_with(expected_output) - end - end - - shared_examples_for "version update is detected" do - it "reports that a gem has a newer version" do - subject - - outdated_gems = out.split("\n").drop_while {|l| !l.start_with?("Gem") }[1..-1] - - expect(outdated_gems.size).to be > 0 - end - end - - shared_examples_for "major version updates are detected" do - before do - build_repo2 do - build_git "foo", path: lib_path("foo") - build_git "zebra", path: lib_path("zebra") - end - - install_gemfile <<-G - source "https://gem.repo2" - gem "zebra", :git => "#{lib_path("zebra")}" - gem "foo", :git => "#{lib_path("foo")}" - gem "activesupport", "2.3.5" - gem "weakling", "~> 0.0.1" - gem "duradura", '7.0' - gem "terranova", '8' - G - - update_repo2 do - build_gem "activesupport", "3.3.5" - build_gem "weakling", "0.8.0" - end - end - - it_behaves_like "version update is detected" - end - - context "when on a new machine" do - before do - build_repo2 do - build_git "foo", path: lib_path("foo") - build_git "zebra", path: lib_path("zebra") - end - - install_gemfile <<-G - source "https://gem.repo2" - gem "zebra", :git => "#{lib_path("zebra")}" - gem "foo", :git => "#{lib_path("foo")}" - gem "activesupport", "2.3.5" - gem "weakling", "~> 0.0.1" - gem "duradura", '7.0' - gem "terranova", '8' - G - - pristine_system_gems - - update_git "foo", path: lib_path("foo") - update_repo2 do - build_gem "activesupport", "3.3.5" - build_gem "weakling", "0.8.0" - end - end - - subject { bundle "outdated", raise_on_error: false } - it_behaves_like "version update is detected" - end - - shared_examples_for "minor version updates are detected" do - before do - build_repo2 do - build_git "foo", path: lib_path("foo") - build_git "zebra", path: lib_path("zebra") - end - - install_gemfile <<-G - source "https://gem.repo2" - gem "zebra", :git => "#{lib_path("zebra")}" - gem "foo", :git => "#{lib_path("foo")}" - gem "activesupport", "2.3.5" - gem "weakling", "~> 0.0.1" - gem "duradura", '7.0' - gem "terranova", '8' - G - - update_repo2 do - build_gem "activesupport", "2.7.5" - build_gem "weakling", "2.0.1" - end - end - - it_behaves_like "version update is detected" - end - - shared_examples_for "patch version updates are detected" do - before do - build_repo2 do - build_git "foo", path: lib_path("foo") - build_git "zebra", path: lib_path("zebra") - end - - install_gemfile <<-G - source "https://gem.repo2" - gem "zebra", :git => "#{lib_path("zebra")}" - gem "foo", :git => "#{lib_path("foo")}" - gem "activesupport", "2.3.5" - gem "weakling", "~> 0.0.1" - gem "duradura", '7.0' - gem "terranova", '8' - G - - update_repo2 do - build_gem "activesupport", "2.3.7" - build_gem "weakling", "0.3.1" - end - end - - it_behaves_like "version update is detected" - end - - shared_examples_for "no version updates are detected" do - it "does not detect any version updates" do - subject - expect(out).to end_with("updates to display.") - end - end - - shared_examples_for "major version is ignored" do - before do - build_repo2 do - build_git "foo", path: lib_path("foo") - build_git "zebra", path: lib_path("zebra") - end - - install_gemfile <<-G - source "https://gem.repo2" - gem "zebra", :git => "#{lib_path("zebra")}" - gem "foo", :git => "#{lib_path("foo")}" - gem "activesupport", "2.3.5" - gem "weakling", "~> 0.0.1" - gem "duradura", '7.0' - gem "terranova", '8' - G - - update_repo2 do - build_gem "activesupport", "3.3.5" - build_gem "weakling", "1.0.1" - end - end - - it_behaves_like "no version updates are detected" - end - - shared_examples_for "minor version is ignored" do - before do - build_repo2 do - build_git "foo", path: lib_path("foo") - build_git "zebra", path: lib_path("zebra") - end - - install_gemfile <<-G - source "https://gem.repo2" - gem "zebra", :git => "#{lib_path("zebra")}" - gem "foo", :git => "#{lib_path("foo")}" - gem "activesupport", "2.3.5" - gem "weakling", "~> 0.0.1" - gem "duradura", '7.0' - gem "terranova", '8' - G - - update_repo2 do - build_gem "activesupport", "2.4.5" - build_gem "weakling", "0.3.1" - end - end - - it_behaves_like "no version updates are detected" - end - - shared_examples_for "patch version is ignored" do - before do - build_repo2 do - build_git "foo", path: lib_path("foo") - build_git "zebra", path: lib_path("zebra") - end - - install_gemfile <<-G - source "https://gem.repo2" - gem "zebra", :git => "#{lib_path("zebra")}" - gem "foo", :git => "#{lib_path("foo")}" - gem "activesupport", "2.3.5" - gem "weakling", "~> 0.0.1" - gem "duradura", '7.0' - gem "terranova", '8' - G - - update_repo2 do - build_gem "activesupport", "2.3.6" - build_gem "weakling", "0.0.4" - end - end - - it_behaves_like "no version updates are detected" - end - - describe "with --filter-major option" do - subject { bundle "outdated --filter-major", raise_on_error: false } - - it_behaves_like "major version updates are detected" - it_behaves_like "minor version is ignored" - it_behaves_like "patch version is ignored" - end - - describe "with --filter-minor option" do - subject { bundle "outdated --filter-minor", raise_on_error: false } - - it_behaves_like "minor version updates are detected" - it_behaves_like "major version is ignored" - it_behaves_like "patch version is ignored" - end - - describe "with --filter-patch option" do - subject { bundle "outdated --filter-patch", raise_on_error: false } - - it_behaves_like "patch version updates are detected" - it_behaves_like "major version is ignored" - it_behaves_like "minor version is ignored" - end - - describe "with --filter-minor --filter-patch options" do - subject { bundle "outdated --filter-minor --filter-patch", raise_on_error: false } - - it_behaves_like "minor version updates are detected" - it_behaves_like "patch version updates are detected" - it_behaves_like "major version is ignored" - end - - describe "with --filter-major --filter-minor options" do - subject { bundle "outdated --filter-major --filter-minor", raise_on_error: false } - - it_behaves_like "major version updates are detected" - it_behaves_like "minor version updates are detected" - it_behaves_like "patch version is ignored" - end - - describe "with --filter-major --filter-patch options" do - subject { bundle "outdated --filter-major --filter-patch", raise_on_error: false } - - it_behaves_like "major version updates are detected" - it_behaves_like "patch version updates are detected" - it_behaves_like "minor version is ignored" - end - - describe "with --filter-major --filter-minor --filter-patch options" do - subject { bundle "outdated --filter-major --filter-minor --filter-patch", raise_on_error: false } - - it_behaves_like "major version updates are detected" - it_behaves_like "minor version updates are detected" - it_behaves_like "patch version updates are detected" - end - - context "conservative updates" do - before do - build_repo4 do - build_gem "patch", %w[1.0.0 1.0.1] - build_gem "minor", %w[1.0.0 1.0.1 1.1.0] - build_gem "major", %w[1.0.0 1.0.1 1.1.0 2.0.0] - end - - # establish a lockfile set to 1.0.0 - install_gemfile <<-G - source "https://gem.repo4" - gem 'patch', '1.0.0' - gem 'minor', '1.0.0' - gem 'major', '1.0.0' - G - - # remove all version requirements - gemfile <<-G - source "https://gem.repo4" - gem 'patch' - gem 'minor' - gem 'major' - G - end - - it "shows nothing when patching and filtering to minor" do - bundle "outdated --patch --filter-minor" - - expect(out).to end_with("No minor updates to display.") - end - - it "shows all gems when patching and filtering to patch" do - bundle "outdated --patch --filter-patch", raise_on_error: false - - expected_output = <<~TABLE.strip - Gem Current Latest Requested Groups Release Date - major 1.0.0 1.0.1 >= 0 default - minor 1.0.0 1.0.1 >= 0 default - patch 1.0.0 1.0.1 >= 0 default - TABLE - - expect(out).to end_with(expected_output) - end - - it "shows minor and major when updating to minor and filtering to patch and minor" do - bundle "outdated --minor --filter-minor", raise_on_error: false - - expected_output = <<~TABLE.strip - Gem Current Latest Requested Groups Release Date - major 1.0.0 1.1.0 >= 0 default - minor 1.0.0 1.1.0 >= 0 default - TABLE - - expect(out).to end_with(expected_output) - end - - it "shows minor when updating to major and filtering to minor with parseable" do - bundle "outdated --major --filter-minor --parseable", raise_on_error: false - - expect(out).not_to include("patch (newest") - expect(out).to include("minor (newest") - expect(out).not_to include("major (newest") - end - end - - context "tricky conservative updates" do - before do - build_repo4 do - build_gem "foo", %w[1.4.3 1.4.4] do |s| - s.add_dependency "bar", "~> 2.0" - end - build_gem "foo", %w[1.4.5 1.5.0] do |s| - s.add_dependency "bar", "~> 2.1" - end - build_gem "foo", %w[1.5.1] do |s| - s.add_dependency "bar", "~> 3.0" - end - build_gem "bar", %w[2.0.3 2.0.4 2.0.5 2.1.0 2.1.1 3.0.0] - build_gem "qux", %w[1.0.0 1.1.0 2.0.0] - end - - # establish a lockfile set to 1.4.3 - install_gemfile <<-G - source "https://gem.repo4" - gem 'foo', '1.4.3' - gem 'bar', '2.0.3' - gem 'qux', '1.0.0' - G - - # remove 1.4.3 requirement and bar altogether - # to setup update specs below - gemfile <<-G - source "https://gem.repo4" - gem 'foo' - gem 'qux' - G - end - - it "shows gems updating to patch and filtering to patch" do - bundle "outdated --patch --filter-patch", raise_on_error: false, env: { "DEBUG_RESOLVER" => "1" } - - expected_output = <<~TABLE.strip - Gem Current Latest Requested Groups Release Date - bar 2.0.3 2.0.5 - foo 1.4.3 1.4.4 >= 0 default - TABLE - - expect(out).to end_with(expected_output) - end - - it "shows gems updating to patch and filtering to patch, in debug mode" do - bundle "outdated --patch --filter-patch", raise_on_error: false, env: { "DEBUG" => "1" } - - expected_output = <<~TABLE.strip - Gem Current Latest Requested Groups Release Date Path - bar 2.0.3 2.0.5 - foo 1.4.3 1.4.4 >= 0 default - TABLE - - expect(out).to end_with(expected_output) - end - end - - describe "with --only-explicit" do - it "does not report outdated dependent gems" do - build_repo4 do - build_gem "weakling", %w[0.2 0.3] do |s| - s.add_dependency "bar", "~> 2.1" - end - build_gem "bar", %w[2.1 2.2] - end - - install_gemfile <<-G - source "https://gem.repo4" - gem 'weakling', '0.2' - gem 'bar', '2.1' - G - - gemfile <<-G - source "https://gem.repo4" - gem 'weakling' - G - - bundle "outdated --only-explicit", raise_on_error: false - - expected_output = <<~TABLE.strip - Gem Current Latest Requested Groups Release Date - weakling 0.2 0.3 >= 0 default - TABLE - - expect(out).to end_with(expected_output) - end - end - - describe "with a multiplatform lockfile" do - before do - build_repo4 do - build_gem "nokogiri", "1.11.1" - build_gem "nokogiri", "1.11.1" do |s| - s.platform = Bundler.local_platform - end - - build_gem "nokogiri", "1.11.2" - build_gem "nokogiri", "1.11.2" do |s| - s.platform = Bundler.local_platform - end - end - - lockfile <<~L - GEM - remote: https://gem.repo4/ - specs: - nokogiri (1.11.1) - nokogiri (1.11.1-#{Bundler.local_platform}) - - PLATFORMS - ruby - #{Bundler.local_platform} - - DEPENDENCIES - nokogiri - - BUNDLED WITH - #{Bundler::VERSION} - L - - gemfile <<-G - source "https://gem.repo4" - gem "nokogiri" - G - end - - it "reports a single entry per gem" do - bundle "outdated", raise_on_error: false - - expected_output = <<~TABLE.strip - Gem Current Latest Requested Groups Release Date - nokogiri 1.11.1 1.11.2 >= 0 default - TABLE - - expect(out).to end_with(expected_output) - end - end - - context "when a gem is no longer a dependency after a full update" do - before do - build_repo4 do - build_gem "mini_portile2", "2.5.2" do |s| - s.add_dependency "net-ftp", "~> 0.1" - end - - build_gem "mini_portile2", "2.5.3" - - build_gem "net-ftp", "0.1.2" - end - - gemfile <<~G - source "https://gem.repo4" - - gem "mini_portile2" - G - - lockfile <<~L - GEM - remote: https://gem.repo4/ - specs: - mini_portile2 (2.5.2) - net-ftp (~> 0.1) - net-ftp (0.1.2) - - PLATFORMS - #{lockfile_platforms} - - DEPENDENCIES - mini_portile2 - - BUNDLED WITH - #{Bundler::VERSION} - L - end - - it "works" do - bundle "outdated", raise_on_error: false - - expected_output = <<~TABLE.strip - Gem Current Latest Requested Groups Release Date - mini_portile2 2.5.2 2.5.3 >= 0 default - TABLE - - expect(out).to end_with(expected_output) - end - end end diff --git a/spec/bundler/commands/update_scenarios_spec.rb b/spec/bundler/commands/update_scenarios_spec.rb new file mode 100644 index 00000000000000..f1d9635bb22b48 --- /dev/null +++ b/spec/bundler/commands/update_scenarios_spec.rb @@ -0,0 +1,1100 @@ +# frozen_string_literal: true + +RSpec.describe "bundle update in more complicated situations" do + before do + build_repo2 + end + + it "will eagerly unlock dependencies of a specified gem" do + install_gemfile <<-G + source "https://gem.repo2" + + gem "thin" + gem "myrack-obama" + G + + update_repo2 do + build_gem "myrack", "1.2" do |s| + s.executables = "myrackup" + end + + build_gem "thin", "2.0" do |s| + s.add_dependency "myrack" + end + end + + bundle "update thin" + expect(the_bundle).to include_gems "thin 2.0", "myrack 1.2", "myrack-obama 1.0" + end + + it "will warn when some explicitly updated gems are not updated" do + install_gemfile <<-G + source "https://gem.repo2" + + gem "thin" + gem "myrack-obama" + G + + update_repo2 do + build_gem("thin", "2.0") {|s| s.add_dependency "myrack" } + build_gem "myrack", "10.0" + end + + bundle "update thin myrack-obama" + expect(stdboth).to include "Bundler attempted to update myrack-obama but its version stayed the same" + expect(the_bundle).to include_gems "thin 2.0", "myrack 10.0", "myrack-obama 1.0" + end + + it "will not warn when an explicitly updated git gem changes sha but not version" do + build_git "foo" + + install_gemfile <<-G + source "https://gem.repo1" + gem "foo", :git => '#{lib_path("foo-1.0")}' + G + + update_git "foo" do |s| + s.write "lib/foo2.rb", "puts :foo2" + end + + bundle "update foo" + + expect(stdboth).not_to include "attempted to update" + end + + it "will not warn when changing gem sources but not versions" do + build_git "myrack" + + install_gemfile <<-G + source "https://gem.repo2" + gem "myrack", :git => '#{lib_path("myrack-1.0")}' + G + + gemfile <<-G + source "https://gem.repo1" + gem "myrack" + G + + bundle "update myrack" + + expect(stdboth).not_to include "attempted to update" + end + + it "will update only from pinned source" do + install_gemfile <<-G + source "https://gem.repo2" + + source "https://gem.repo1" do + gem "thin" + end + G + + update_repo2 do + build_gem "thin", "2.0" + end + + bundle "update", artifice: "compact_index" + expect(the_bundle).to include_gems "thin 1.0" + end + + context "when the lockfile is for a different platform" do + around do |example| + build_repo4 do + build_gem("a", "0.9") + build_gem("a", "0.9") {|s| s.platform = "java" } + build_gem("a", "1.1") + build_gem("a", "1.1") {|s| s.platform = "java" } + end + + gemfile <<-G + source "https://gem.repo4" + gem "a" + G + + lockfile <<-L + GEM + remote: https://gem.repo4 + specs: + a (0.9-java) + + PLATFORMS + java + + DEPENDENCIES + a + L + + simulate_platform "x86_64-linux", &example + end + + it "allows updating" do + bundle :update, all: true + expect(the_bundle).to include_gem "a 1.1" + end + + it "allows updating a specific gem" do + bundle "update a" + expect(the_bundle).to include_gem "a 1.1" + end + end + + context "when the dependency is for a different platform" do + before do + build_repo4 do + build_gem("a", "0.9") {|s| s.platform = "java" } + build_gem("a", "1.1") {|s| s.platform = "java" } + end + + gemfile <<-G + source "https://gem.repo4" + gem "a", platform: :jruby + G + + lockfile <<-L + GEM + remote: https://gem.repo4 + specs: + a (0.9-java) + + PLATFORMS + java + + DEPENDENCIES + a + L + end + + it "is not updated because it is not actually included in the bundle" do + simulate_platform "x86_64-linux" do + bundle "update a" + expect(stdboth).to include "Bundler attempted to update a but it was not considered because it is for a different platform from the current one" + expect(the_bundle).to_not include_gem "a" + end + end + end +end + +RSpec.describe "bundle update without a Gemfile.lock" do + it "should not explode" do + build_repo2 + + gemfile <<-G + source "https://gem.repo2" + + gem "myrack", "1.0" + G + + bundle "update", all: true + + expect(the_bundle).to include_gems "myrack 1.0.0" + end +end + +RSpec.describe "bundle update when a gem depends on a newer version of bundler" do + before do + build_repo2 do + build_gem "rails", "3.0.1" do |s| + s.add_dependency "bundler", "9.9.9" + end + + build_gem "bundler", "9.9.9" + end + + gemfile <<-G + source "https://gem.repo2" + gem "rails", "3.0.1" + G + end + + it "should explain that bundler conflicted and how to resolve the conflict" do + bundle "update", all: true, raise_on_error: false + expect(stdboth).not_to match(/in snapshot/i) + expect(err).to match(/current Bundler version/i). + and match(/Install the necessary version with `gem install bundler:9\.9\.9`/i) + end +end + +RSpec.describe "bundle update --ruby" do + context "when the Gemfile removes the ruby" do + before do + install_gemfile <<-G + ruby '~> #{Gem.ruby_version}' + source "https://gem.repo1" + G + + gemfile <<-G + source "https://gem.repo1" + G + end + + it "removes the Ruby from the Gemfile.lock" do + bundle "update --ruby" + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo1/ + specs: + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + #{checksums_section_when_enabled} + BUNDLED WITH + #{Bundler::VERSION} + L + end + end + + context "when the Gemfile specified an updated Ruby version" do + before do + install_gemfile <<-G + ruby '~> #{Gem.ruby_version}' + source "https://gem.repo1" + G + + gemfile <<-G + ruby '~> #{current_ruby_minor}' + source "https://gem.repo1" + G + end + + it "updates the Gemfile.lock with the latest version" do + bundle "update --ruby" + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo1/ + specs: + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + #{checksums_section_when_enabled} + RUBY VERSION + #{Bundler::RubyVersion.system} + + BUNDLED WITH + #{Bundler::VERSION} + L + end + end + + context "when a different Ruby is being used than has been versioned" do + before do + install_gemfile <<-G + ruby '~> #{Gem.ruby_version}' + source "https://gem.repo1" + G + + gemfile <<-G + ruby '~> 2.1.0' + source "https://gem.repo1" + G + end + it "shows a helpful error message" do + bundle "update --ruby", raise_on_error: false + + expect(err).to include("Your Ruby version is #{Bundler::RubyVersion.system.gem_version}, but your Gemfile specified ~> 2.1.0") + end + end + + context "when updating Ruby version and Gemfile `ruby`" do + before do + lockfile <<~L + GEM + remote: https://gem.repo1/ + specs: + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + + CHECKSUMS + + RUBY VERSION + ruby 2.1.4p222 + + BUNDLED WITH + #{Bundler::VERSION} + L + + gemfile <<-G + ruby '~> #{Gem.ruby_version}' + source "https://gem.repo1" + G + end + + it "updates the Gemfile.lock with the latest version" do + bundle "update --ruby" + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo1/ + specs: + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + #{checksums_section_when_enabled} + RUBY VERSION + #{Bundler::RubyVersion.system} + + BUNDLED WITH + #{Bundler::VERSION} + L + end + end +end + +RSpec.describe "bundle update --bundler" do + it "updates the bundler version in the lockfile" do + build_repo4 do + build_gem "bundler", "2.5.9" + build_gem "myrack", "1.0" + end + + checksums = checksums_section_when_enabled do |c| + c.checksum(gem_repo4, "myrack", "1.0") + end + + install_gemfile <<-G + source "https://gem.repo4" + gem "myrack" + G + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + myrack (1.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + myrack + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + lockfile lockfile.sub(/(^\s*)#{Bundler::VERSION}($)/, '\11.0.0\2') + + bundle :update, bundler: true, verbose: true + expect(out).to include("Using bundler #{Bundler::VERSION}") + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + myrack (1.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + myrack + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + + expect(the_bundle).to include_gem "myrack 1.0" + end + + it "updates the bundler version in the lockfile without re-resolving if the highest version is already installed" do + build_repo4 do + build_gem "bundler", "2.3.9" + build_gem "myrack", "1.0" + end + + install_gemfile <<-G + source "https://gem.repo4" + gem "myrack" + G + lockfile lockfile.sub(/(^\s*)#{Bundler::VERSION}($)/, "2.3.9") + + checksums = checksums_section_when_enabled do |c| + c.checksum(gem_repo4, "myrack", "1.0") + end + + bundle :update, bundler: true, verbose: true + expect(out).to include("Using bundler #{Bundler::VERSION}") + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + myrack (1.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + myrack + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + + expect(the_bundle).to include_gem "myrack 1.0" + end + + it "updates the bundler version in the lockfile even if the latest version is not installed", :ruby_repo do + bundle_config "path.system true" + + pristine_system_gems "bundler-9.0.0" + + build_repo4 do + build_gem "myrack", "1.0" + + build_bundler "999.0.0" + end + + checksums = checksums_section do |c| + c.checksum(gem_repo4, "myrack", "1.0") + c.checksum(gem_repo4, "bundler", "999.0.0") + end + + install_gemfile <<-G + source "https://gem.repo4" + gem "myrack" + G + + bundle :update, bundler: true, verbose: true + + expect(out).to include("Updating bundler to 999.0.0") + expect(out).to include("Running `bundle update --bundler \"> 0.a\" --verbose` with bundler 999.0.0") + expect(out).not_to include("Installing Bundler 2.99.9 and restarting using that version.") + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + myrack (1.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + myrack + #{checksums} + BUNDLED WITH + 999.0.0 + L + + bundle "--version" + expect(out).to include("999.0.0") + + bundle "list" + expect(out).to include("myrack (1.0)") + end + + it "does not claim to update to Bundler version to a wrong version when cached gems are present" do + pristine_system_gems "bundler-4.99.0" + + build_repo4 do + build_gem "myrack", "3.0.9.1" + + build_bundler "4.99.0" + end + + gemfile <<~G + source "https://gem.repo4" + gem "myrack" + G + + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + myrack (3.0.9.1) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + myrack + + BUNDLED WITH + 2.99.0 + L + + bundle :cache, verbose: true + + bundle :update, bundler: true, verbose: true + + expect(out).not_to include("Updating bundler to") + end + + it "does not update the bundler version in the lockfile if the latest version is not compatible with current ruby", :ruby_repo do + pristine_system_gems "bundler-9.9.9" + + build_repo4 do + build_gem "myrack", "1.0" + + build_bundler "9.9.9" + build_bundler "999.0.0" do |s| + s.required_ruby_version = "> #{Gem.ruby_version}" + end + end + + checksums = checksums_section do |c| + c.checksum(gem_repo4, "myrack", "1.0") + c.checksum(gem_repo4, "bundler", "9.9.9") + end + + install_gemfile <<-G + source "https://gem.repo4" + gem "myrack" + G + + bundle :update, bundler: true, verbose: true + + expect(out).to include("Using bundler 9.9.9") + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + myrack (1.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + myrack + #{checksums} + BUNDLED WITH + 9.9.9 + L + + bundle "--version" + expect(out).to include("9.9.9") + + bundle "list" + expect(out).to include("myrack (1.0)") + end + + it "errors if the explicit target version does not exist" do + pristine_system_gems "bundler-9.9.9" + + build_repo4 do + build_gem "myrack", "1.0" + end + + install_gemfile <<-G + source "https://gem.repo4" + gem "myrack" + G + + bundle :update, bundler: "999.999.999", raise_on_error: false + + expect(last_command).to be_failure + expect(err).to eq("The `bundle update --bundler` target version (999.999.999) does not exist") + end + + it "errors if the explicit target version does not exist, even if auto switching is disabled" do + pristine_system_gems "bundler-9.9.9" + + build_repo4 do + build_gem "myrack", "1.0" + end + + install_gemfile <<-G + source "https://gem.repo4" + gem "myrack" + G + + bundle :update, bundler: "999.999.999", raise_on_error: false, env: { "BUNDLER_VERSION" => "9.9.9" } + + expect(last_command).to be_failure + expect(err).to eq("The `bundle update --bundler` target version (999.999.999) does not exist") + end + + it "allows updating to development versions if already installed locally" do + system_gems "bundler-9.9.9" + + build_repo4 do + build_gem "myrack", "1.0" + end + + install_gemfile <<-G + source "https://gem.repo4" + gem "myrack" + G + + system_gems "bundler-9.0.0.dev", path: local_gem_path + bundle :update, bundler: "9.0.0.dev", verbose: "true" + + checksums = checksums_section_when_enabled do |c| + c.checksum(gem_repo4, "myrack", "1.0") + end + checksums.delete("bundler") + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + myrack (1.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + myrack + #{checksums} + BUNDLED WITH + 9.0.0.dev + L + + expect(out).to include("Using bundler 9.0.0.dev") + end + + it "does not touch the network if not necessary" do + system_gems "bundler-9.9.9" + + build_repo4 do + build_gem "myrack", "1.0" + end + + install_gemfile <<-G + source "https://gem.repo4" + gem "myrack" + G + system_gems "bundler-9.0.0", path: local_gem_path + bundle :update, bundler: "9.0.0", verbose: true + + expect(out).not_to include("Fetching gem metadata from https://rubygems.org/") + + # Only updates properly on modern RubyGems. + checksums = checksums_section_when_enabled do |c| + c.checksum(gem_repo4, "myrack", "1.0") + c.checksum(local_gem_path, "bundler", "9.0.0", Gem::Platform::RUBY, "cache") + end + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + myrack (1.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + myrack + #{checksums} + BUNDLED WITH + 9.0.0 + L + + expect(out).to include("Using bundler 9.0.0") + end + + it "preserves the locked bundler checksum when re-locking without the bundler gem cached" do + system_gems "bundler-9.0.0" + + build_repo4 do + build_gem "myrack", "1.0" + build_gem "weakling", "0.0.3" + + build_bundler "9.0.0" + end + + install_gemfile <<-G + source "https://gem.repo4" + gem "myrack" + G + + system_gems "bundler-9.0.0", path: local_gem_path + bundle :update, bundler: "9.0.0", verbose: true + + # Sanity check: the lockfile now records the bundler checksum. + expect(lockfile).to match(/^ bundler \(9\.0\.0\) sha256=/) + + # Simulate a machine where the bundler gem is not present in the cache + # (e.g. a fresh CI checkout that never downloaded bundler-9.0.0.gem). + FileUtils.rm_f Dir[local_gem_path("cache", "bundler-9.0.0.gem")] + FileUtils.rm_f Dir[system_gem_path("cache", "bundler-9.0.0.gem")] + + # Force a re-resolution / lockfile rewrite. + install_gemfile <<-G + source "https://gem.repo4" + gem "myrack" + gem "weakling" + G + + # The bundler checksum must survive the rewrite, since it was already locked. + expect(lockfile).to match(/^ bundler \(9\.0\.0\) sha256=/) + end + + it "drops the locked bundler checksum when the bundler version changes and the gem isn't cached" do + system_gems "bundler-9.0.0" + + build_repo4 do + build_gem "myrack", "1.0" + + build_bundler "9.0.0" + build_bundler "9.9.9" + end + + install_gemfile <<-G + source "https://gem.repo4" + gem "myrack" + G + + system_gems "bundler-9.0.0", path: local_gem_path + bundle :update, bundler: "9.0.0", verbose: true + + # Sanity check: the lockfile records the bundler 9.0.0 checksum and is + # locked to bundler 9.0.0. + expect(lockfile).to match(/^ bundler \(9\.0\.0\) sha256=/) + expect(lockfile).to match(/BUNDLED WITH\n\s+9\.0\.0\n/) + + # Simulate a machine where the bundler gem is not present in the cache + # (e.g. a fresh CI checkout), so a fresh checksum can't be computed. + FileUtils.rm_f Dir[local_gem_path("cache", "bundler-9.0.0.gem")] + FileUtils.rm_f Dir[system_gem_path("cache", "bundler-9.0.0.gem")] + + # Change the locked bundler version. `bundle lock --update --bundler` rewrites + # the BUNDLED WITH section without switching the running bundler, so the gem + # whose checksum is locked (9.0.0) is no longer the version being locked. + bundle "lock --update --bundler 9.9.9", verbose: true + + # The BUNDLED WITH version was bumped... + expect(lockfile).to match(/BUNDLED WITH\n\s+9\.9\.9\n/) + + # ...so the stale `bundler (9.0.0)` checksum must be dropped rather than kept, + # otherwise we'd lock a checksum that no longer matches the BUNDLED WITH + # version (and that we can't recompute since the gem isn't cached). + expect(lockfile).not_to match(/^ bundler \(/) + end + + it "prints an error when trying to update bundler in frozen mode" do + system_gems "bundler-9.0.0" + + gemfile <<~G + source "https://gem.repo2" + G + + lockfile <<-L + GEM + remote: https://gem.repo2/ + specs: + + PLATFORMS + ruby + + DEPENDENCIES + + BUNDLED WITH + 9.0.0 + L + + system_gems "bundler-9.9.9", path: local_gem_path + + bundle "update --bundler=9.9.9", env: { "BUNDLE_FROZEN" => "true" }, raise_on_error: false + expect(err).to include("An update to the version of Bundler itself was requested, but the lockfile can't be updated because frozen mode is set") + end +end + +# these specs are slow and focus on integration and therefore are not exhaustive. unit specs elsewhere handle that. +RSpec.describe "bundle update conservative" do + context "patch and minor options" do + before do + build_repo4 do + build_gem "foo", %w[1.4.3 1.4.4] do |s| + s.add_dependency "bar", "~> 2.0" + end + build_gem "foo", %w[1.4.5 1.5.0] do |s| + s.add_dependency "bar", "~> 2.1" + end + build_gem "foo", %w[1.5.1] do |s| + s.add_dependency "bar", "~> 3.0" + end + build_gem "foo", %w[2.0.0.pre] do |s| + s.add_dependency "bar" + end + build_gem "bar", %w[2.0.3 2.0.4 2.0.5 2.1.0 2.1.1 2.1.2.pre 3.0.0 3.1.0.pre 4.0.0.pre] + build_gem "qux", %w[1.0.0 1.0.1 1.1.0 2.0.0] + end + + # establish a lockfile set to 1.4.3 + install_gemfile <<-G + source "https://gem.repo4" + gem 'foo', '1.4.3' + gem 'bar', '2.0.3' + gem 'qux', '1.0.0' + G + + # remove 1.4.3 requirement and bar altogether + # to setup update specs below + gemfile <<-G + source "https://gem.repo4" + gem 'foo' + gem 'qux' + G + end + + context "with patch set as default update level in config" do + it "should do a patch level update" do + bundle_config "prefer_patch true" + bundle "update foo" + + expect(the_bundle).to include_gems "foo 1.4.5", "bar 2.1.1", "qux 1.0.0" + end + end + + context "patch preferred" do + it "single gem updates dependent gem to minor" do + bundle "update --patch foo" + + expect(the_bundle).to include_gems "foo 1.4.5", "bar 2.1.1", "qux 1.0.0" + end + + it "update all" do + bundle "update --patch", all: true + + expect(the_bundle).to include_gems "foo 1.4.5", "bar 2.1.1", "qux 1.0.1" + end + end + + context "minor preferred" do + it "single gem updates dependent gem to major" do + bundle "update --minor foo" + + expect(the_bundle).to include_gems "foo 1.5.1", "bar 3.0.0", "qux 1.0.0" + end + end + + context "strict" do + it "patch preferred" do + bundle "update --patch foo bar --strict" + + expect(the_bundle).to include_gems "foo 1.4.4", "bar 2.0.5", "qux 1.0.0" + end + + it "minor preferred" do + bundle "update --minor --strict", all: true + + expect(the_bundle).to include_gems "foo 1.5.0", "bar 2.1.1", "qux 1.1.0" + end + end + + context "pre" do + it "defaults to major" do + bundle "update --pre foo bar" + + expect(the_bundle).to include_gems "foo 2.0.0.pre", "bar 4.0.0.pre", "qux 1.0.0" + end + + it "patch preferred" do + bundle "update --patch --pre foo bar" + + expect(the_bundle).to include_gems "foo 1.4.5", "bar 2.1.2.pre", "qux 1.0.0" + end + + it "minor preferred" do + bundle "update --minor --pre foo bar" + + expect(the_bundle).to include_gems "foo 1.5.1", "bar 3.1.0.pre", "qux 1.0.0" + end + + it "major preferred" do + bundle "update --major --pre foo bar" + + expect(the_bundle).to include_gems "foo 2.0.0.pre", "bar 4.0.0.pre", "qux 1.0.0" + end + end + end + + context "eager unlocking" do + before do + build_repo4 do + build_gem "isolated_owner", %w[1.0.1 1.0.2] do |s| + s.add_dependency "isolated_dep", "~> 2.0" + end + build_gem "isolated_dep", %w[2.0.1 2.0.2] + + build_gem "shared_owner_a", %w[3.0.1 3.0.2] do |s| + s.add_dependency "shared_dep", "~> 5.0" + end + build_gem "shared_owner_b", %w[4.0.1 4.0.2] do |s| + s.add_dependency "shared_dep", "~> 5.0" + end + build_gem "shared_dep", %w[5.0.1 5.0.2] + end + + gemfile <<-G + source "https://gem.repo4" + gem 'isolated_owner' + + gem 'shared_owner_a' + gem 'shared_owner_b' + G + + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + isolated_dep (2.0.1) + isolated_owner (1.0.1) + isolated_dep (~> 2.0) + shared_dep (5.0.1) + shared_owner_a (3.0.1) + shared_dep (~> 5.0) + shared_owner_b (4.0.1) + shared_dep (~> 5.0) + + PLATFORMS + #{local_platform} + + DEPENDENCIES + isolated_owner + shared_owner_a + shared_owner_b + + CHECKSUMS + + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "should eagerly unlock isolated dependency" do + bundle "update isolated_owner" + + expect(the_bundle).to include_gems "isolated_owner 1.0.2", "isolated_dep 2.0.2", "shared_dep 5.0.1", "shared_owner_a 3.0.1", "shared_owner_b 4.0.1" + end + + it "should eagerly unlock shared dependency" do + bundle "update shared_owner_a" + + expect(the_bundle).to include_gems "isolated_owner 1.0.1", "isolated_dep 2.0.1", "shared_dep 5.0.2", "shared_owner_a 3.0.2", "shared_owner_b 4.0.1" + end + + it "should not eagerly unlock with --conservative" do + bundle "update --conservative shared_owner_a isolated_owner" + + expect(the_bundle).to include_gems "isolated_owner 1.0.2", "isolated_dep 2.0.1", "shared_dep 5.0.1", "shared_owner_a 3.0.2", "shared_owner_b 4.0.1" + end + + it "should only update direct dependencies when fully updating with --conservative" do + bundle "update --conservative" + + expect(the_bundle).to include_gems "isolated_owner 1.0.2", "isolated_dep 2.0.1", "shared_dep 5.0.1", "shared_owner_a 3.0.2", "shared_owner_b 4.0.2" + end + + it "should only change direct dependencies when updating the lockfile with --conservative" do + bundle "lock --update --conservative" + + checksums = checksums_section_when_enabled do |c| + c.checksum gem_repo4, "isolated_dep", "2.0.1" + c.checksum gem_repo4, "isolated_owner", "1.0.2" + c.checksum gem_repo4, "shared_dep", "5.0.1" + c.checksum gem_repo4, "shared_owner_a", "3.0.2" + c.checksum gem_repo4, "shared_owner_b", "4.0.2" + end + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + isolated_dep (2.0.1) + isolated_owner (1.0.2) + isolated_dep (~> 2.0) + shared_dep (5.0.1) + shared_owner_a (3.0.2) + shared_dep (~> 5.0) + shared_owner_b (4.0.2) + shared_dep (~> 5.0) + + PLATFORMS + #{local_platform} + + DEPENDENCIES + isolated_owner + shared_owner_a + shared_owner_b + #{checksums} + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "should match bundle install conservative update behavior when not eagerly unlocking" do + gemfile <<-G + source "https://gem.repo4" + gem 'isolated_owner', '1.0.2' + + gem 'shared_owner_a', '3.0.2' + gem 'shared_owner_b' + G + + bundle "install" + + expect(the_bundle).to include_gems "isolated_owner 1.0.2", "isolated_dep 2.0.1", "shared_dep 5.0.1", "shared_owner_a 3.0.2", "shared_owner_b 4.0.1" + end + end + + context "when Gemfile dependencies have changed" do + before do + build_repo4 do + build_gem "nokogiri", "1.16.4" do |s| + s.platform = "arm64-darwin" + end + + build_gem "nokogiri", "1.16.4" do |s| + s.platform = "x86_64-linux" + end + + build_gem "prism", "0.25.0" + end + + gemfile <<~G + source "https://gem.repo4" + gem "nokogiri", ">=1.16.4" + gem "prism", ">=0.25.0" + G + + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + nokogiri (1.16.4-arm64-darwin) + nokogiri (1.16.4-x86_64-linux) + + PLATFORMS + arm64-darwin + x86_64-linux + + DEPENDENCIES + nokogiri (>= 1.16.4) + + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "still works" do + simulate_platform "arm64-darwin-23" do + bundle "update" + end + end + end + + context "error handling" do + before do + gemfile "source 'https://gem.repo1'" + end + + it "raises if too many flags are provided" do + bundle "update --patch --minor", all: true, raise_on_error: false + + expect(err).to eq "Provide only one of the following options: minor, patch" + end + end +end diff --git a/spec/bundler/commands/update_spec.rb b/spec/bundler/commands/update_spec.rb index e838468a2771eb..fcba43bbea05c6 100644 --- a/spec/bundler/commands/update_spec.rb +++ b/spec/bundler/commands/update_spec.rb @@ -1096,1102 +1096,3 @@ end end end - -RSpec.describe "bundle update in more complicated situations" do - before do - build_repo2 - end - - it "will eagerly unlock dependencies of a specified gem" do - install_gemfile <<-G - source "https://gem.repo2" - - gem "thin" - gem "myrack-obama" - G - - update_repo2 do - build_gem "myrack", "1.2" do |s| - s.executables = "myrackup" - end - - build_gem "thin", "2.0" do |s| - s.add_dependency "myrack" - end - end - - bundle "update thin" - expect(the_bundle).to include_gems "thin 2.0", "myrack 1.2", "myrack-obama 1.0" - end - - it "will warn when some explicitly updated gems are not updated" do - install_gemfile <<-G - source "https://gem.repo2" - - gem "thin" - gem "myrack-obama" - G - - update_repo2 do - build_gem("thin", "2.0") {|s| s.add_dependency "myrack" } - build_gem "myrack", "10.0" - end - - bundle "update thin myrack-obama" - expect(stdboth).to include "Bundler attempted to update myrack-obama but its version stayed the same" - expect(the_bundle).to include_gems "thin 2.0", "myrack 10.0", "myrack-obama 1.0" - end - - it "will not warn when an explicitly updated git gem changes sha but not version" do - build_git "foo" - - install_gemfile <<-G - source "https://gem.repo1" - gem "foo", :git => '#{lib_path("foo-1.0")}' - G - - update_git "foo" do |s| - s.write "lib/foo2.rb", "puts :foo2" - end - - bundle "update foo" - - expect(stdboth).not_to include "attempted to update" - end - - it "will not warn when changing gem sources but not versions" do - build_git "myrack" - - install_gemfile <<-G - source "https://gem.repo2" - gem "myrack", :git => '#{lib_path("myrack-1.0")}' - G - - gemfile <<-G - source "https://gem.repo1" - gem "myrack" - G - - bundle "update myrack" - - expect(stdboth).not_to include "attempted to update" - end - - it "will update only from pinned source" do - install_gemfile <<-G - source "https://gem.repo2" - - source "https://gem.repo1" do - gem "thin" - end - G - - update_repo2 do - build_gem "thin", "2.0" - end - - bundle "update", artifice: "compact_index" - expect(the_bundle).to include_gems "thin 1.0" - end - - context "when the lockfile is for a different platform" do - around do |example| - build_repo4 do - build_gem("a", "0.9") - build_gem("a", "0.9") {|s| s.platform = "java" } - build_gem("a", "1.1") - build_gem("a", "1.1") {|s| s.platform = "java" } - end - - gemfile <<-G - source "https://gem.repo4" - gem "a" - G - - lockfile <<-L - GEM - remote: https://gem.repo4 - specs: - a (0.9-java) - - PLATFORMS - java - - DEPENDENCIES - a - L - - simulate_platform "x86_64-linux", &example - end - - it "allows updating" do - bundle :update, all: true - expect(the_bundle).to include_gem "a 1.1" - end - - it "allows updating a specific gem" do - bundle "update a" - expect(the_bundle).to include_gem "a 1.1" - end - end - - context "when the dependency is for a different platform" do - before do - build_repo4 do - build_gem("a", "0.9") {|s| s.platform = "java" } - build_gem("a", "1.1") {|s| s.platform = "java" } - end - - gemfile <<-G - source "https://gem.repo4" - gem "a", platform: :jruby - G - - lockfile <<-L - GEM - remote: https://gem.repo4 - specs: - a (0.9-java) - - PLATFORMS - java - - DEPENDENCIES - a - L - end - - it "is not updated because it is not actually included in the bundle" do - simulate_platform "x86_64-linux" do - bundle "update a" - expect(stdboth).to include "Bundler attempted to update a but it was not considered because it is for a different platform from the current one" - expect(the_bundle).to_not include_gem "a" - end - end - end -end - -RSpec.describe "bundle update without a Gemfile.lock" do - it "should not explode" do - build_repo2 - - gemfile <<-G - source "https://gem.repo2" - - gem "myrack", "1.0" - G - - bundle "update", all: true - - expect(the_bundle).to include_gems "myrack 1.0.0" - end -end - -RSpec.describe "bundle update when a gem depends on a newer version of bundler" do - before do - build_repo2 do - build_gem "rails", "3.0.1" do |s| - s.add_dependency "bundler", "9.9.9" - end - - build_gem "bundler", "9.9.9" - end - - gemfile <<-G - source "https://gem.repo2" - gem "rails", "3.0.1" - G - end - - it "should explain that bundler conflicted and how to resolve the conflict" do - bundle "update", all: true, raise_on_error: false - expect(stdboth).not_to match(/in snapshot/i) - expect(err).to match(/current Bundler version/i). - and match(/Install the necessary version with `gem install bundler:9\.9\.9`/i) - end -end - -RSpec.describe "bundle update --ruby" do - context "when the Gemfile removes the ruby" do - before do - install_gemfile <<-G - ruby '~> #{Gem.ruby_version}' - source "https://gem.repo1" - G - - gemfile <<-G - source "https://gem.repo1" - G - end - - it "removes the Ruby from the Gemfile.lock" do - bundle "update --ruby" - - expect(lockfile).to eq <<~L - GEM - remote: https://gem.repo1/ - specs: - - PLATFORMS - #{lockfile_platforms} - - DEPENDENCIES - #{checksums_section_when_enabled} - BUNDLED WITH - #{Bundler::VERSION} - L - end - end - - context "when the Gemfile specified an updated Ruby version" do - before do - install_gemfile <<-G - ruby '~> #{Gem.ruby_version}' - source "https://gem.repo1" - G - - gemfile <<-G - ruby '~> #{current_ruby_minor}' - source "https://gem.repo1" - G - end - - it "updates the Gemfile.lock with the latest version" do - bundle "update --ruby" - - expect(lockfile).to eq <<~L - GEM - remote: https://gem.repo1/ - specs: - - PLATFORMS - #{lockfile_platforms} - - DEPENDENCIES - #{checksums_section_when_enabled} - RUBY VERSION - #{Bundler::RubyVersion.system} - - BUNDLED WITH - #{Bundler::VERSION} - L - end - end - - context "when a different Ruby is being used than has been versioned" do - before do - install_gemfile <<-G - ruby '~> #{Gem.ruby_version}' - source "https://gem.repo1" - G - - gemfile <<-G - ruby '~> 2.1.0' - source "https://gem.repo1" - G - end - it "shows a helpful error message" do - bundle "update --ruby", raise_on_error: false - - expect(err).to include("Your Ruby version is #{Bundler::RubyVersion.system.gem_version}, but your Gemfile specified ~> 2.1.0") - end - end - - context "when updating Ruby version and Gemfile `ruby`" do - before do - lockfile <<~L - GEM - remote: https://gem.repo1/ - specs: - - PLATFORMS - #{lockfile_platforms} - - DEPENDENCIES - - CHECKSUMS - - RUBY VERSION - ruby 2.1.4p222 - - BUNDLED WITH - #{Bundler::VERSION} - L - - gemfile <<-G - ruby '~> #{Gem.ruby_version}' - source "https://gem.repo1" - G - end - - it "updates the Gemfile.lock with the latest version" do - bundle "update --ruby" - - expect(lockfile).to eq <<~L - GEM - remote: https://gem.repo1/ - specs: - - PLATFORMS - #{lockfile_platforms} - - DEPENDENCIES - #{checksums_section_when_enabled} - RUBY VERSION - #{Bundler::RubyVersion.system} - - BUNDLED WITH - #{Bundler::VERSION} - L - end - end -end - -RSpec.describe "bundle update --bundler" do - it "updates the bundler version in the lockfile" do - build_repo4 do - build_gem "bundler", "2.5.9" - build_gem "myrack", "1.0" - end - - checksums = checksums_section_when_enabled do |c| - c.checksum(gem_repo4, "myrack", "1.0") - end - - install_gemfile <<-G - source "https://gem.repo4" - gem "myrack" - G - expect(lockfile).to eq <<~L - GEM - remote: https://gem.repo4/ - specs: - myrack (1.0) - - PLATFORMS - #{lockfile_platforms} - - DEPENDENCIES - myrack - #{checksums} - BUNDLED WITH - #{Bundler::VERSION} - L - lockfile lockfile.sub(/(^\s*)#{Bundler::VERSION}($)/, '\11.0.0\2') - - bundle :update, bundler: true, verbose: true - expect(out).to include("Using bundler #{Bundler::VERSION}") - - expect(lockfile).to eq <<~L - GEM - remote: https://gem.repo4/ - specs: - myrack (1.0) - - PLATFORMS - #{lockfile_platforms} - - DEPENDENCIES - myrack - #{checksums} - BUNDLED WITH - #{Bundler::VERSION} - L - - expect(the_bundle).to include_gem "myrack 1.0" - end - - it "updates the bundler version in the lockfile without re-resolving if the highest version is already installed" do - build_repo4 do - build_gem "bundler", "2.3.9" - build_gem "myrack", "1.0" - end - - install_gemfile <<-G - source "https://gem.repo4" - gem "myrack" - G - lockfile lockfile.sub(/(^\s*)#{Bundler::VERSION}($)/, "2.3.9") - - checksums = checksums_section_when_enabled do |c| - c.checksum(gem_repo4, "myrack", "1.0") - end - - bundle :update, bundler: true, verbose: true - expect(out).to include("Using bundler #{Bundler::VERSION}") - - expect(lockfile).to eq <<~L - GEM - remote: https://gem.repo4/ - specs: - myrack (1.0) - - PLATFORMS - #{lockfile_platforms} - - DEPENDENCIES - myrack - #{checksums} - BUNDLED WITH - #{Bundler::VERSION} - L - - expect(the_bundle).to include_gem "myrack 1.0" - end - - it "updates the bundler version in the lockfile even if the latest version is not installed", :ruby_repo do - bundle_config "path.system true" - - pristine_system_gems "bundler-9.0.0" - - build_repo4 do - build_gem "myrack", "1.0" - - build_bundler "999.0.0" - end - - checksums = checksums_section do |c| - c.checksum(gem_repo4, "myrack", "1.0") - c.checksum(gem_repo4, "bundler", "999.0.0") - end - - install_gemfile <<-G - source "https://gem.repo4" - gem "myrack" - G - - bundle :update, bundler: true, verbose: true - - expect(out).to include("Updating bundler to 999.0.0") - expect(out).to include("Running `bundle update --bundler \"> 0.a\" --verbose` with bundler 999.0.0") - expect(out).not_to include("Installing Bundler 2.99.9 and restarting using that version.") - - expect(lockfile).to eq <<~L - GEM - remote: https://gem.repo4/ - specs: - myrack (1.0) - - PLATFORMS - #{lockfile_platforms} - - DEPENDENCIES - myrack - #{checksums} - BUNDLED WITH - 999.0.0 - L - - bundle "--version" - expect(out).to include("999.0.0") - - bundle "list" - expect(out).to include("myrack (1.0)") - end - - it "does not claim to update to Bundler version to a wrong version when cached gems are present" do - pristine_system_gems "bundler-4.99.0" - - build_repo4 do - build_gem "myrack", "3.0.9.1" - - build_bundler "4.99.0" - end - - gemfile <<~G - source "https://gem.repo4" - gem "myrack" - G - - lockfile <<~L - GEM - remote: https://gem.repo4/ - specs: - myrack (3.0.9.1) - - PLATFORMS - #{lockfile_platforms} - - DEPENDENCIES - myrack - - BUNDLED WITH - 2.99.0 - L - - bundle :cache, verbose: true - - bundle :update, bundler: true, verbose: true - - expect(out).not_to include("Updating bundler to") - end - - it "does not update the bundler version in the lockfile if the latest version is not compatible with current ruby", :ruby_repo do - pristine_system_gems "bundler-9.9.9" - - build_repo4 do - build_gem "myrack", "1.0" - - build_bundler "9.9.9" - build_bundler "999.0.0" do |s| - s.required_ruby_version = "> #{Gem.ruby_version}" - end - end - - checksums = checksums_section do |c| - c.checksum(gem_repo4, "myrack", "1.0") - c.checksum(gem_repo4, "bundler", "9.9.9") - end - - install_gemfile <<-G - source "https://gem.repo4" - gem "myrack" - G - - bundle :update, bundler: true, verbose: true - - expect(out).to include("Using bundler 9.9.9") - - expect(lockfile).to eq <<~L - GEM - remote: https://gem.repo4/ - specs: - myrack (1.0) - - PLATFORMS - #{lockfile_platforms} - - DEPENDENCIES - myrack - #{checksums} - BUNDLED WITH - 9.9.9 - L - - bundle "--version" - expect(out).to include("9.9.9") - - bundle "list" - expect(out).to include("myrack (1.0)") - end - - it "errors if the explicit target version does not exist" do - pristine_system_gems "bundler-9.9.9" - - build_repo4 do - build_gem "myrack", "1.0" - end - - install_gemfile <<-G - source "https://gem.repo4" - gem "myrack" - G - - bundle :update, bundler: "999.999.999", raise_on_error: false - - expect(last_command).to be_failure - expect(err).to eq("The `bundle update --bundler` target version (999.999.999) does not exist") - end - - it "errors if the explicit target version does not exist, even if auto switching is disabled" do - pristine_system_gems "bundler-9.9.9" - - build_repo4 do - build_gem "myrack", "1.0" - end - - install_gemfile <<-G - source "https://gem.repo4" - gem "myrack" - G - - bundle :update, bundler: "999.999.999", raise_on_error: false, env: { "BUNDLER_VERSION" => "9.9.9" } - - expect(last_command).to be_failure - expect(err).to eq("The `bundle update --bundler` target version (999.999.999) does not exist") - end - - it "allows updating to development versions if already installed locally" do - system_gems "bundler-9.9.9" - - build_repo4 do - build_gem "myrack", "1.0" - end - - install_gemfile <<-G - source "https://gem.repo4" - gem "myrack" - G - - system_gems "bundler-9.0.0.dev", path: local_gem_path - bundle :update, bundler: "9.0.0.dev", verbose: "true" - - checksums = checksums_section_when_enabled do |c| - c.checksum(gem_repo4, "myrack", "1.0") - end - checksums.delete("bundler") - - expect(lockfile).to eq <<~L - GEM - remote: https://gem.repo4/ - specs: - myrack (1.0) - - PLATFORMS - #{lockfile_platforms} - - DEPENDENCIES - myrack - #{checksums} - BUNDLED WITH - 9.0.0.dev - L - - expect(out).to include("Using bundler 9.0.0.dev") - end - - it "does not touch the network if not necessary" do - system_gems "bundler-9.9.9" - - build_repo4 do - build_gem "myrack", "1.0" - end - - install_gemfile <<-G - source "https://gem.repo4" - gem "myrack" - G - system_gems "bundler-9.0.0", path: local_gem_path - bundle :update, bundler: "9.0.0", verbose: true - - expect(out).not_to include("Fetching gem metadata from https://rubygems.org/") - - # Only updates properly on modern RubyGems. - checksums = checksums_section_when_enabled do |c| - c.checksum(gem_repo4, "myrack", "1.0") - c.checksum(local_gem_path, "bundler", "9.0.0", Gem::Platform::RUBY, "cache") - end - - expect(lockfile).to eq <<~L - GEM - remote: https://gem.repo4/ - specs: - myrack (1.0) - - PLATFORMS - #{lockfile_platforms} - - DEPENDENCIES - myrack - #{checksums} - BUNDLED WITH - 9.0.0 - L - - expect(out).to include("Using bundler 9.0.0") - end - - it "preserves the locked bundler checksum when re-locking without the bundler gem cached" do - system_gems "bundler-9.0.0" - - build_repo4 do - build_gem "myrack", "1.0" - build_gem "weakling", "0.0.3" - - build_bundler "9.0.0" - end - - install_gemfile <<-G - source "https://gem.repo4" - gem "myrack" - G - - system_gems "bundler-9.0.0", path: local_gem_path - bundle :update, bundler: "9.0.0", verbose: true - - # Sanity check: the lockfile now records the bundler checksum. - expect(lockfile).to match(/^ bundler \(9\.0\.0\) sha256=/) - - # Simulate a machine where the bundler gem is not present in the cache - # (e.g. a fresh CI checkout that never downloaded bundler-9.0.0.gem). - FileUtils.rm_f Dir[local_gem_path("cache", "bundler-9.0.0.gem")] - FileUtils.rm_f Dir[system_gem_path("cache", "bundler-9.0.0.gem")] - - # Force a re-resolution / lockfile rewrite. - install_gemfile <<-G - source "https://gem.repo4" - gem "myrack" - gem "weakling" - G - - # The bundler checksum must survive the rewrite, since it was already locked. - expect(lockfile).to match(/^ bundler \(9\.0\.0\) sha256=/) - end - - it "drops the locked bundler checksum when the bundler version changes and the gem isn't cached" do - system_gems "bundler-9.0.0" - - build_repo4 do - build_gem "myrack", "1.0" - - build_bundler "9.0.0" - build_bundler "9.9.9" - end - - install_gemfile <<-G - source "https://gem.repo4" - gem "myrack" - G - - system_gems "bundler-9.0.0", path: local_gem_path - bundle :update, bundler: "9.0.0", verbose: true - - # Sanity check: the lockfile records the bundler 9.0.0 checksum and is - # locked to bundler 9.0.0. - expect(lockfile).to match(/^ bundler \(9\.0\.0\) sha256=/) - expect(lockfile).to match(/BUNDLED WITH\n\s+9\.0\.0\n/) - - # Simulate a machine where the bundler gem is not present in the cache - # (e.g. a fresh CI checkout), so a fresh checksum can't be computed. - FileUtils.rm_f Dir[local_gem_path("cache", "bundler-9.0.0.gem")] - FileUtils.rm_f Dir[system_gem_path("cache", "bundler-9.0.0.gem")] - - # Change the locked bundler version. `bundle lock --update --bundler` rewrites - # the BUNDLED WITH section without switching the running bundler, so the gem - # whose checksum is locked (9.0.0) is no longer the version being locked. - bundle "lock --update --bundler 9.9.9", verbose: true - - # The BUNDLED WITH version was bumped... - expect(lockfile).to match(/BUNDLED WITH\n\s+9\.9\.9\n/) - - # ...so the stale `bundler (9.0.0)` checksum must be dropped rather than kept, - # otherwise we'd lock a checksum that no longer matches the BUNDLED WITH - # version (and that we can't recompute since the gem isn't cached). - expect(lockfile).not_to match(/^ bundler \(/) - end - - it "prints an error when trying to update bundler in frozen mode" do - system_gems "bundler-9.0.0" - - gemfile <<~G - source "https://gem.repo2" - G - - lockfile <<-L - GEM - remote: https://gem.repo2/ - specs: - - PLATFORMS - ruby - - DEPENDENCIES - - BUNDLED WITH - 9.0.0 - L - - system_gems "bundler-9.9.9", path: local_gem_path - - bundle "update --bundler=9.9.9", env: { "BUNDLE_FROZEN" => "true" }, raise_on_error: false - expect(err).to include("An update to the version of Bundler itself was requested, but the lockfile can't be updated because frozen mode is set") - end -end - -# these specs are slow and focus on integration and therefore are not exhaustive. unit specs elsewhere handle that. -RSpec.describe "bundle update conservative" do - context "patch and minor options" do - before do - build_repo4 do - build_gem "foo", %w[1.4.3 1.4.4] do |s| - s.add_dependency "bar", "~> 2.0" - end - build_gem "foo", %w[1.4.5 1.5.0] do |s| - s.add_dependency "bar", "~> 2.1" - end - build_gem "foo", %w[1.5.1] do |s| - s.add_dependency "bar", "~> 3.0" - end - build_gem "foo", %w[2.0.0.pre] do |s| - s.add_dependency "bar" - end - build_gem "bar", %w[2.0.3 2.0.4 2.0.5 2.1.0 2.1.1 2.1.2.pre 3.0.0 3.1.0.pre 4.0.0.pre] - build_gem "qux", %w[1.0.0 1.0.1 1.1.0 2.0.0] - end - - # establish a lockfile set to 1.4.3 - install_gemfile <<-G - source "https://gem.repo4" - gem 'foo', '1.4.3' - gem 'bar', '2.0.3' - gem 'qux', '1.0.0' - G - - # remove 1.4.3 requirement and bar altogether - # to setup update specs below - gemfile <<-G - source "https://gem.repo4" - gem 'foo' - gem 'qux' - G - end - - context "with patch set as default update level in config" do - it "should do a patch level update" do - bundle_config "prefer_patch true" - bundle "update foo" - - expect(the_bundle).to include_gems "foo 1.4.5", "bar 2.1.1", "qux 1.0.0" - end - end - - context "patch preferred" do - it "single gem updates dependent gem to minor" do - bundle "update --patch foo" - - expect(the_bundle).to include_gems "foo 1.4.5", "bar 2.1.1", "qux 1.0.0" - end - - it "update all" do - bundle "update --patch", all: true - - expect(the_bundle).to include_gems "foo 1.4.5", "bar 2.1.1", "qux 1.0.1" - end - end - - context "minor preferred" do - it "single gem updates dependent gem to major" do - bundle "update --minor foo" - - expect(the_bundle).to include_gems "foo 1.5.1", "bar 3.0.0", "qux 1.0.0" - end - end - - context "strict" do - it "patch preferred" do - bundle "update --patch foo bar --strict" - - expect(the_bundle).to include_gems "foo 1.4.4", "bar 2.0.5", "qux 1.0.0" - end - - it "minor preferred" do - bundle "update --minor --strict", all: true - - expect(the_bundle).to include_gems "foo 1.5.0", "bar 2.1.1", "qux 1.1.0" - end - end - - context "pre" do - it "defaults to major" do - bundle "update --pre foo bar" - - expect(the_bundle).to include_gems "foo 2.0.0.pre", "bar 4.0.0.pre", "qux 1.0.0" - end - - it "patch preferred" do - bundle "update --patch --pre foo bar" - - expect(the_bundle).to include_gems "foo 1.4.5", "bar 2.1.2.pre", "qux 1.0.0" - end - - it "minor preferred" do - bundle "update --minor --pre foo bar" - - expect(the_bundle).to include_gems "foo 1.5.1", "bar 3.1.0.pre", "qux 1.0.0" - end - - it "major preferred" do - bundle "update --major --pre foo bar" - - expect(the_bundle).to include_gems "foo 2.0.0.pre", "bar 4.0.0.pre", "qux 1.0.0" - end - end - end - - context "eager unlocking" do - before do - build_repo4 do - build_gem "isolated_owner", %w[1.0.1 1.0.2] do |s| - s.add_dependency "isolated_dep", "~> 2.0" - end - build_gem "isolated_dep", %w[2.0.1 2.0.2] - - build_gem "shared_owner_a", %w[3.0.1 3.0.2] do |s| - s.add_dependency "shared_dep", "~> 5.0" - end - build_gem "shared_owner_b", %w[4.0.1 4.0.2] do |s| - s.add_dependency "shared_dep", "~> 5.0" - end - build_gem "shared_dep", %w[5.0.1 5.0.2] - end - - gemfile <<-G - source "https://gem.repo4" - gem 'isolated_owner' - - gem 'shared_owner_a' - gem 'shared_owner_b' - G - - lockfile <<~L - GEM - remote: https://gem.repo4/ - specs: - isolated_dep (2.0.1) - isolated_owner (1.0.1) - isolated_dep (~> 2.0) - shared_dep (5.0.1) - shared_owner_a (3.0.1) - shared_dep (~> 5.0) - shared_owner_b (4.0.1) - shared_dep (~> 5.0) - - PLATFORMS - #{local_platform} - - DEPENDENCIES - isolated_owner - shared_owner_a - shared_owner_b - - CHECKSUMS - - BUNDLED WITH - #{Bundler::VERSION} - L - end - - it "should eagerly unlock isolated dependency" do - bundle "update isolated_owner" - - expect(the_bundle).to include_gems "isolated_owner 1.0.2", "isolated_dep 2.0.2", "shared_dep 5.0.1", "shared_owner_a 3.0.1", "shared_owner_b 4.0.1" - end - - it "should eagerly unlock shared dependency" do - bundle "update shared_owner_a" - - expect(the_bundle).to include_gems "isolated_owner 1.0.1", "isolated_dep 2.0.1", "shared_dep 5.0.2", "shared_owner_a 3.0.2", "shared_owner_b 4.0.1" - end - - it "should not eagerly unlock with --conservative" do - bundle "update --conservative shared_owner_a isolated_owner" - - expect(the_bundle).to include_gems "isolated_owner 1.0.2", "isolated_dep 2.0.1", "shared_dep 5.0.1", "shared_owner_a 3.0.2", "shared_owner_b 4.0.1" - end - - it "should only update direct dependencies when fully updating with --conservative" do - bundle "update --conservative" - - expect(the_bundle).to include_gems "isolated_owner 1.0.2", "isolated_dep 2.0.1", "shared_dep 5.0.1", "shared_owner_a 3.0.2", "shared_owner_b 4.0.2" - end - - it "should only change direct dependencies when updating the lockfile with --conservative" do - bundle "lock --update --conservative" - - checksums = checksums_section_when_enabled do |c| - c.checksum gem_repo4, "isolated_dep", "2.0.1" - c.checksum gem_repo4, "isolated_owner", "1.0.2" - c.checksum gem_repo4, "shared_dep", "5.0.1" - c.checksum gem_repo4, "shared_owner_a", "3.0.2" - c.checksum gem_repo4, "shared_owner_b", "4.0.2" - end - - expect(lockfile).to eq <<~L - GEM - remote: https://gem.repo4/ - specs: - isolated_dep (2.0.1) - isolated_owner (1.0.2) - isolated_dep (~> 2.0) - shared_dep (5.0.1) - shared_owner_a (3.0.2) - shared_dep (~> 5.0) - shared_owner_b (4.0.2) - shared_dep (~> 5.0) - - PLATFORMS - #{local_platform} - - DEPENDENCIES - isolated_owner - shared_owner_a - shared_owner_b - #{checksums} - BUNDLED WITH - #{Bundler::VERSION} - L - end - - it "should match bundle install conservative update behavior when not eagerly unlocking" do - gemfile <<-G - source "https://gem.repo4" - gem 'isolated_owner', '1.0.2' - - gem 'shared_owner_a', '3.0.2' - gem 'shared_owner_b' - G - - bundle "install" - - expect(the_bundle).to include_gems "isolated_owner 1.0.2", "isolated_dep 2.0.1", "shared_dep 5.0.1", "shared_owner_a 3.0.2", "shared_owner_b 4.0.1" - end - end - - context "when Gemfile dependencies have changed" do - before do - build_repo4 do - build_gem "nokogiri", "1.16.4" do |s| - s.platform = "arm64-darwin" - end - - build_gem "nokogiri", "1.16.4" do |s| - s.platform = "x86_64-linux" - end - - build_gem "prism", "0.25.0" - end - - gemfile <<~G - source "https://gem.repo4" - gem "nokogiri", ">=1.16.4" - gem "prism", ">=0.25.0" - G - - lockfile <<~L - GEM - remote: https://gem.repo4/ - specs: - nokogiri (1.16.4-arm64-darwin) - nokogiri (1.16.4-x86_64-linux) - - PLATFORMS - arm64-darwin - x86_64-linux - - DEPENDENCIES - nokogiri (>= 1.16.4) - - BUNDLED WITH - #{Bundler::VERSION} - L - end - - it "still works" do - simulate_platform "arm64-darwin-23" do - bundle "update" - end - end - end - - context "error handling" do - before do - gemfile "source 'https://gem.repo1'" - end - - it "raises if too many flags are provided" do - bundle "update --patch --minor", all: true, raise_on_error: false - - expect(err).to eq "Provide only one of the following options: minor, patch" - end - end -end diff --git a/spec/bundler/install/gemfile/git_extensions_spec.rb b/spec/bundler/install/gemfile/git_extensions_spec.rb new file mode 100644 index 00000000000000..ff540955ef34f2 --- /dev/null +++ b/spec/bundler/install/gemfile/git_extensions_spec.rb @@ -0,0 +1,609 @@ +# frozen_string_literal: true + +RSpec.describe "bundle install with git sources" do + describe "switching sources" do + it "doesn't explode when switching Path to Git sources" do + build_gem "foo", "1.0", to_system: true do |s| + s.write "lib/foo.rb", "raise 'fail'" + end + build_lib "foo", "1.0", path: lib_path("bar/foo") + build_git "bar", "1.0", path: lib_path("bar") do |s| + s.add_dependency "foo" + end + + install_gemfile <<-G + source "https://gem.repo1" + gem "bar", :path => "#{lib_path("bar")}" + G + + install_gemfile <<-G + source "https://gem.repo1" + gem "bar", :git => "#{lib_path("bar")}" + G + + expect(the_bundle).to include_gems "foo 1.0", "bar 1.0" + end + + it "doesn't explode when switching Gem to Git source" do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack-obama" + gem "myrack", "1.0.0" + G + + build_git "myrack", "1.0" do |s| + s.write "lib/new_file.rb", "puts 'USING GIT'" + end + + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack-obama" + gem "myrack", "1.0.0", :git => "#{lib_path("myrack-1.0")}" + G + + run "require 'new_file'" + expect(out).to eq("USING GIT") + end + + it "doesn't explode when removing an explicit exact version from a git gem with dependencies" do + build_lib "activesupport", "7.1.4", path: lib_path("rails/activesupport") + build_git "rails", "7.1.4", path: lib_path("rails") do |s| + s.add_dependency "activesupport", "= 7.1.4" + end + + install_gemfile <<-G + source "https://gem.repo1" + gem "rails", "7.1.4", :git => "#{lib_path("rails")}" + G + + install_gemfile <<-G + source "https://gem.repo1" + gem "rails", :git => "#{lib_path("rails")}" + G + + expect(the_bundle).to include_gem "rails 7.1.4", "activesupport 7.1.4" + end + + it "doesn't explode when adding an explicit ref to a git gem with dependencies" do + lib_root = lib_path("rails") + + build_lib "activesupport", "7.1.4", path: lib_root.join("activesupport") + build_git "rails", "7.1.4", path: lib_root do |s| + s.add_dependency "activesupport", "= 7.1.4" + end + + old_revision = revision_for(lib_root) + update_git "rails", "7.1.4", path: lib_root + + install_gemfile <<-G + source "https://gem.repo1" + gem "rails", "7.1.4", :git => "#{lib_root}" + G + + install_gemfile <<-G + source "https://gem.repo1" + gem "rails", :git => "#{lib_root}", :ref => "#{old_revision}" + G + + expect(the_bundle).to include_gem "rails 7.1.4", "activesupport 7.1.4" + end + end + + describe "bundle install after the remote has been updated" do + it "installs" do + build_git "valim" + + install_gemfile <<-G + source "https://gem.repo1" + gem "valim", :git => "#{lib_path("valim-1.0")}" + G + + old_revision = revision_for(lib_path("valim-1.0")) + update_git "valim" + new_revision = revision_for(lib_path("valim-1.0")) + + old_lockfile = File.read(bundled_app_lock) + lockfile(bundled_app_lock, old_lockfile.gsub(/revision: #{old_revision}/, "revision: #{new_revision}")) + + bundle "install" + + run <<-R + require "valim" + puts VALIM_PREV_REF + R + + expect(out).to eq(old_revision) + end + + it "gives a helpful error message when the remote ref no longer exists" do + build_git "foo" + revision = revision_for(lib_path("foo-1.0")) + + install_gemfile <<-G + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo-1.0")}", :ref => "#{revision}" + G + expect(out).to_not match(/Revision.*does not exist/) + + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo-1.0")}", :ref => "deadbeef" + G + expect(err).to include("Revision deadbeef does not exist in the repository") + end + + it "gives a helpful error message when the remote branch no longer exists" do + build_git "foo" + + install_gemfile <<-G, env: { "LANG" => "en" }, raise_on_error: false + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo-1.0")}", :branch => "deadbeef" + G + + expect(err).to include("Revision deadbeef does not exist in the repository") + end + end + + describe "bundle install with deployment mode configured and git sources" do + it "works" do + build_git "valim", path: lib_path("valim") + + install_gemfile <<-G + source "https://gem.repo1" + gem "valim", "= 1.0", :git => "#{lib_path("valim")}" + G + + pristine_system_gems + + bundle_config "deployment true" + bundle :install + end + end + + describe "gem install hooks" do + it "runs pre-install hooks" do + build_git "foo" + gemfile <<-G + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + + File.open(lib_path("install_hooks.rb"), "w") do |h| + h.write <<-H + Gem.pre_install_hooks << lambda do |inst| + STDERR.puts "Ran pre-install hook: \#{inst.spec.full_name}" + end + H + end + + bundle :install, + requires: [lib_path("install_hooks.rb")] + expect(err_without_deprecations).to eq("Ran pre-install hook: foo-1.0") + end + + it "runs post-install hooks" do + build_git "foo" + gemfile <<-G + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + + File.open(lib_path("install_hooks.rb"), "w") do |h| + h.write <<-H + Gem.post_install_hooks << lambda do |inst| + STDERR.puts "Ran post-install hook: \#{inst.spec.full_name}" + end + H + end + + bundle :install, + requires: [lib_path("install_hooks.rb")] + expect(err_without_deprecations).to eq("Ran post-install hook: foo-1.0") + end + + it "complains if the install hook fails" do + build_git "foo" + gemfile <<-G + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + + File.open(lib_path("install_hooks.rb"), "w") do |h| + h.write <<-H + Gem.pre_install_hooks << lambda do |inst| + false + end + H + end + + bundle :install, requires: [lib_path("install_hooks.rb")], raise_on_error: false + expect(err).to include("failed for foo-1.0") + end + end + + context "with an extension" do + it "installs the extension" do + build_git "foo" do |s| + s.add_dependency "rake" + s.extensions << "Rakefile" + s.write "Rakefile", <<-RUBY + task :default do + path = File.expand_path("lib", __dir__) + FileUtils.mkdir_p(path) + File.open("\#{path}/foo.rb", "w") do |f| + f.puts "FOO = 'YES'" + end + end + RUBY + end + + install_gemfile <<-G + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + + run <<-R + require 'foo' + puts FOO + R + expect(out).to eq("YES") + + run <<-R + puts $:.grep(/ext/) + R + expect(out).to include(Pathname.glob(default_bundle_path("bundler/gems/extensions/**/foo-1.0-*")).first.to_s) + end + + it "does not use old extension after ref changes" do + git_reader = build_git "foo", no_default: true do |s| + s.extensions = ["ext/extconf.rb"] + s.write "ext/extconf.rb", <<-RUBY + require "mkmf" + create_makefile("foo") + RUBY + s.write "ext/foo.c", "void Init_foo() {}" + end + + 2.times do |i| + File.open(git_reader.path.join("ext/foo.c"), "w") do |file| + file.write <<-C + #include "ruby.h" + VALUE foo(VALUE self) { return INT2FIX(#{i}); } + void Init_foo() { rb_define_global_function("foo", &foo, 0); } + C + end + git("commit -m \"commit for iteration #{i}\" ext/foo.c", git_reader.path) + + git_commit_sha = git_reader.ref_for("HEAD") + + install_gemfile <<-G + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo-1.0")}", :ref => "#{git_commit_sha}" + G + + run <<-R + require 'foo' + puts foo + R + + expect(out).to eq(i.to_s) + end + end + + it "does not prompt to gem install if extension fails" do + build_git "foo" do |s| + s.add_dependency "rake" + s.extensions << "Rakefile" + s.write "Rakefile", <<-RUBY + task :default do + raise + end + RUBY + end + + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + + expect(err).to end_with(<<-M.strip) +An error occurred while installing foo (1.0), and Bundler cannot continue. + +In Gemfile: + foo + M + expect(out).not_to include("gem install foo") + end + + it "does not reinstall the extension" do + build_git "foo" do |s| + s.add_dependency "rake" + s.extensions << "Rakefile" + s.write "Rakefile", <<-RUBY + task :default do + path = File.expand_path("lib", __dir__) + FileUtils.mkdir_p(path) + cur_time = Time.now.to_f.to_s + File.open("\#{path}/foo.rb", "w") do |f| + f.puts "FOO = \#{cur_time}" + end + end + RUBY + end + + install_gemfile <<-G + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + + run <<-R + require 'foo' + puts FOO + R + + installed_time = out + expect(installed_time).to match(/\A\d+\.\d+\z/) + + install_gemfile <<-G + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + + run <<-R + require 'foo' + puts FOO + R + expect(out).to eq(installed_time) + end + + it "does not reinstall the extension when changing another gem" do + build_git "foo" do |s| + s.add_dependency "rake" + s.extensions << "Rakefile" + s.write "Rakefile", <<-RUBY + task :default do + path = File.expand_path("lib", __dir__) + FileUtils.mkdir_p(path) + cur_time = Time.now.to_f.to_s + File.open("\#{path}/foo.rb", "w") do |f| + f.puts "FOO = \#{cur_time}" + end + end + RUBY + end + + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack", "0.9.1" + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + + run <<-R + require 'foo' + puts FOO + R + + installed_time = out + expect(installed_time).to match(/\A\d+\.\d+\z/) + + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack", "1.0.0" + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + + run <<-R + require 'foo' + puts FOO + R + expect(out).to eq(installed_time) + end + + it "does reinstall the extension when changing refs" do + build_git "foo" do |s| + s.add_dependency "rake" + s.extensions << "Rakefile" + s.write "Rakefile", <<-RUBY + task :default do + path = File.expand_path("lib", __dir__) + FileUtils.mkdir_p(path) + cur_time = Time.now.to_f.to_s + File.open("\#{path}/foo.rb", "w") do |f| + f.puts "FOO = \#{cur_time}" + end + end + RUBY + end + + install_gemfile <<-G + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + + run <<-R + require 'foo' + puts FOO + R + + installed_time = out + + update_git("foo", branch: "branch2") + + expect(installed_time).to match(/\A\d+\.\d+\z/) + + install_gemfile <<-G + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo-1.0")}", :branch => "branch2" + G + + run <<-R + require 'foo' + puts FOO + R + expect(out).not_to eq(installed_time) + + installed_time = out + + update_git("foo") + bundle "update foo" + + run <<-R + require 'foo' + puts FOO + R + expect(out).not_to eq(installed_time) + end + end + + it "ignores git environment variables" do + build_git "xxxxxx" do |s| + s.executables = "xxxxxxbar" + end + + Bundler::SharedHelpers.with_clean_git_env do + ENV["GIT_DIR"] = "bar" + ENV["GIT_WORK_TREE"] = "bar" + + install_gemfile <<-G + source "https://gem.repo1" + git "#{lib_path("xxxxxx-1.0")}" do + gem 'xxxxxx' + end + G + + expect(ENV["GIT_DIR"]).to eq("bar") + expect(ENV["GIT_WORK_TREE"]).to eq("bar") + end + end + + describe "without git installed" do + it "prints a better error message when installing" do + gemfile <<-G + source "https://gem.repo1" + + gem "rake", git: "https://github.com/ruby/rake" + G + + lockfile <<-L + GIT + remote: https://github.com/ruby/rake + revision: 5c60da8644a9e4f655e819252e3b6ca77f42b7af + specs: + rake (13.0.6) + + GEM + remote: https://rubygems.org/ + specs: + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + rake! + + BUNDLED WITH + #{Bundler::VERSION} + L + + with_path_as("") do + bundle "install", raise_on_error: false + end + expect(err). + to include("You need to install git to be able to use gems from git repositories. For help installing git, please refer to GitHub's tutorial at https://help.github.com/articles/set-up-git") + end + + it "prints a better error message when updating" do + build_git "foo" + + install_gemfile <<-G + source "https://gem.repo1" + git "#{lib_path("foo-1.0")}" do + gem 'foo' + end + G + + with_path_as("") do + bundle "update", all: true, raise_on_error: false + end + expect(err). + to include("You need to install git to be able to use gems from git repositories. For help installing git, please refer to GitHub's tutorial at https://help.github.com/articles/set-up-git") + end + + it "doesn't need git in the new machine if an installed git gem is copied to another machine" do + build_git "foo" + + install_gemfile <<-G + source "https://gem.repo1" + git "#{lib_path("foo-1.0")}" do + gem 'foo' + end + G + bundle_config_global "path vendor/bundle" + bundle :install + pristine_system_gems + + bundle "install", env: { "PATH" => "" } + expect(out).to_not include("You need to install git to be able to use gems from git repositories.") + end + end + + describe "when the git source is overridden with a local git repo" do + before do + bundle_config_global "local.foo #{lib_path("foo")}" + end + + describe "and git output is colorized" do + before do + File.open("#{ENV["HOME"]}/.gitconfig", "w") do |f| + f.write("[color]\n\tui = always\n") + end + end + + it "installs successfully" do + build_git "foo", "1.0", path: lib_path("foo") + + gemfile <<-G + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo")}", :branch => "main" + G + + bundle :install + expect(the_bundle).to include_gems "foo 1.0" + end + end + end + + context "git sources that include credentials" do + context "that are username and password" do + let(:credentials) { "user1:password1" } + + it "does not display the password" do + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo1" + git "https://#{credentials}@github.com/company/private-repo" do + gem "foo" + end + G + + expect(stdboth).to_not include("password1") + expect(out).to include("Fetching https://user1@github.com/company/private-repo") + end + end + + context "that is an oauth token" do + let(:credentials) { "oauth_token" } + + it "displays the oauth scheme but not the oauth token" do + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo1" + git "https://#{credentials}:x-oauth-basic@github.com/company/private-repo" do + gem "foo" + end + G + + expect(stdboth).to_not include("oauth_token") + expect(out).to include("Fetching https://x-oauth-basic@github.com/company/private-repo") + end + end + end +end diff --git a/spec/bundler/install/gemfile/git_overrides_spec.rb b/spec/bundler/install/gemfile/git_overrides_spec.rb new file mode 100644 index 00000000000000..750d16c8685509 --- /dev/null +++ b/spec/bundler/install/gemfile/git_overrides_spec.rb @@ -0,0 +1,633 @@ +# frozen_string_literal: true + +RSpec.describe "bundle install with git sources" do + describe "when specifying local override" do + it "uses the local repository instead of checking a new one out" do + build_git "myrack", "0.8", path: lib_path("local-myrack") do |s| + s.write "lib/myrack.rb", "puts :LOCAL" + end + + gemfile <<-G + source "https://gem.repo1" + gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main" + G + + bundle %(config set local.myrack #{lib_path("local-myrack")}) + bundle :install + + run "require 'myrack'" + expect(out).to eq("LOCAL") + end + + it "chooses the local repository on runtime" do + build_git "myrack", "0.8" + + FileUtils.cp_r("#{lib_path("myrack-0.8")}/.", lib_path("local-myrack")) + + update_git "myrack", "0.8", path: lib_path("local-myrack") do |s| + s.write "lib/myrack.rb", "puts :LOCAL" + end + + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main" + G + + bundle %(config set local.myrack #{lib_path("local-myrack")}) + run "require 'myrack'" + expect(out).to eq("LOCAL") + end + + it "unlocks the source when the dependencies have changed while switching to the local" do + build_git "myrack", "0.8" + + FileUtils.cp_r("#{lib_path("myrack-0.8")}/.", lib_path("local-myrack")) + + update_git "myrack", "0.8", path: lib_path("local-myrack") do |s| + s.write "myrack.gemspec", build_spec("myrack", "0.8") { runtime "rspec", "> 0" }.first.to_ruby + s.write "lib/myrack.rb", "puts :LOCAL" + end + + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main" + G + + bundle %(config set local.myrack #{lib_path("local-myrack")}) + bundle :install + run "require 'myrack'" + expect(out).to eq("LOCAL") + end + + it "updates specs on runtime" do + system_gems "nokogiri-1.4.2" + + build_git "myrack", "0.8" + + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main" + G + + lockfile0 = File.read(bundled_app_lock) + + FileUtils.cp_r("#{lib_path("myrack-0.8")}/.", lib_path("local-myrack")) + update_git "myrack", "0.8", path: lib_path("local-myrack") do |s| + s.add_dependency "nokogiri", "1.4.2" + end + + bundle %(config set local.myrack #{lib_path("local-myrack")}) + run "require 'myrack'" + + lockfile1 = File.read(bundled_app_lock) + expect(lockfile1).not_to eq(lockfile0) + end + + it "updates ref on install" do + build_git "myrack", "0.8" + + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main" + G + + lockfile0 = File.read(bundled_app_lock) + + FileUtils.cp_r("#{lib_path("myrack-0.8")}/.", lib_path("local-myrack")) + update_git "myrack", "0.8", path: lib_path("local-myrack") + + bundle %(config set local.myrack #{lib_path("local-myrack")}) + bundle :install + + lockfile1 = File.read(bundled_app_lock) + expect(lockfile1).not_to eq(lockfile0) + end + + it "explodes and gives correct solution if given path does not exist on install" do + build_git "myrack", "0.8" + + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main" + G + + bundle %(config set local.myrack #{lib_path("local-myrack")}) + bundle :install, raise_on_error: false + expect(err).to match(/Cannot use local override for myrack-0.8 because #{Regexp.escape(lib_path("local-myrack").to_s)} does not exist/) + + solution = "config unset local.myrack" + expect(err).to match(/Run `bundle #{solution}` to remove the local override/) + + bundle solution + bundle :install + + expect(err).to be_empty + end + + it "explodes and gives correct solution if branch is not given on install" do + build_git "myrack", "0.8" + FileUtils.cp_r("#{lib_path("myrack-0.8")}/.", lib_path("local-myrack")) + + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack", :git => "#{lib_path("myrack-0.8")}" + G + + bundle %(config set local.myrack #{lib_path("local-myrack")}) + bundle :install, raise_on_error: false + expect(err).to match(/Cannot use local override for myrack-0.8 at #{Regexp.escape(lib_path("local-myrack").to_s)} because :branch is not specified in Gemfile/) + + solution = "config unset local.myrack" + expect(err).to match(/Specify a branch or run `bundle #{solution}` to remove the local override/) + + bundle solution + bundle :install + + expect(err).to be_empty + end + + it "does not explode if disable_local_branch_check is given" do + build_git "myrack", "0.8" + FileUtils.cp_r("#{lib_path("myrack-0.8")}/.", lib_path("local-myrack")) + + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack", :git => "#{lib_path("myrack-0.8")}" + G + + bundle %(config set local.myrack #{lib_path("local-myrack")}) + bundle %(config set disable_local_branch_check true) + bundle :install + expect(out).to match(/Bundle complete!/) + end + + it "explodes on different branches on install" do + build_git "myrack", "0.8" + + FileUtils.cp_r("#{lib_path("myrack-0.8")}/.", lib_path("local-myrack")) + + update_git "myrack", "0.8", path: lib_path("local-myrack"), branch: "another" do |s| + s.write "lib/myrack.rb", "puts :LOCAL" + end + + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main" + G + + bundle %(config set local.myrack #{lib_path("local-myrack")}) + bundle :install, raise_on_error: false + expect(err).to match(/is using branch another but Gemfile specifies main/) + end + + it "explodes on invalid revision on install" do + build_git "myrack", "0.8" + + build_git "myrack", "0.8", path: lib_path("local-myrack") do |s| + s.write "lib/myrack.rb", "puts :LOCAL" + end + + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main" + G + + bundle %(config set local.myrack #{lib_path("local-myrack")}) + bundle :install, raise_on_error: false + expect(err).to match(/The Gemfile lock is pointing to revision \w+/) + end + + it "does not explode on invalid revision on install" do + build_git "myrack", "0.8" + + build_git "myrack", "0.8", path: lib_path("local-myrack") do |s| + s.write "lib/myrack.rb", "puts :LOCAL" + end + + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main" + G + + bundle %(config set local.myrack #{lib_path("local-myrack")}) + bundle %(config set disable_local_revision_check true) + bundle :install + expect(out).to match(/Bundle complete!/) + end + end + + describe "specified inline" do + # TODO: Figure out how to write this test so that it is not flaky depending + # on the current network situation. + # it "supports private git URLs" do + # gemfile <<-G + # gem "thingy", :git => "git@notthere.fallingsnow.net:somebody/thingy.git" + # G + # + # bundle :install + # + # # p out + # # p err + # puts err unless err.empty? # This spec fails randomly every so often + # err.should include("notthere.fallingsnow.net") + # err.should include("ssh") + # end + + it "installs from git even if a newer gem is available elsewhere" do + build_git "myrack", "0.8" + + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack", :git => "#{lib_path("myrack-0.8")}" + G + + expect(the_bundle).to include_gems "myrack 0.8" + end + + it "installs dependencies from git even if a newer gem is available elsewhere" do + system_gems "myrack-1.0.0" + + build_lib "myrack", "1.0", path: lib_path("nested/bar") do |s| + s.write "lib/myrack.rb", "puts 'WIN OVERRIDE'" + end + + build_git "foo", path: lib_path("nested") do |s| + s.add_dependency "myrack", "= 1.0" + end + + install_gemfile <<-G + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("nested")}" + G + + run "require 'myrack'" + expect(out).to eq("WIN OVERRIDE") + end + + it "correctly unlocks when changing to a git source" do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack", "0.9.1" + G + + build_git "myrack", path: lib_path("myrack") + + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack", "1.0.0", :git => "#{lib_path("myrack")}" + G + + expect(the_bundle).to include_gems "myrack 1.0.0" + end + + it "correctly unlocks when changing to a git source without versions" do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + G + + build_git "myrack", "1.2", path: lib_path("myrack") + + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack", :git => "#{lib_path("myrack")}" + G + + expect(the_bundle).to include_gems "myrack 1.2" + end + end + + describe "block syntax" do + it "pulls all gems from a git block" do + build_lib "omg", path: lib_path("hi2u/omg") + build_lib "hi2u", path: lib_path("hi2u") + + install_gemfile <<-G + source "https://gem.repo1" + path "#{lib_path("hi2u")}" do + gem "omg" + gem "hi2u" + end + G + + expect(the_bundle).to include_gems "omg 1.0", "hi2u 1.0" + end + end + + it "uses a ref if specified" do + build_git "foo" + @revision = revision_for(lib_path("foo-1.0")) + update_git "foo" + + install_gemfile <<-G + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo-1.0")}", :ref => "#{@revision}" + G + + run <<-RUBY + require 'foo' + puts "WIN" unless defined?(FOO_PREV_REF) + RUBY + + expect(out).to eq("WIN") + end + + it "correctly handles cases with invalid gemspecs" do + build_git "foo" do |s| + s.summary = nil + end + + install_gemfile <<-G + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo-1.0")}" + gem "rails", "2.3.2" + G + + expect(the_bundle).to include_gems "foo 1.0" + expect(the_bundle).to include_gems "rails 2.3.2" + end + + it "runs the gemspec in the context of its parent directory" do + build_lib "bar", path: lib_path("foo/bar"), gemspec: false do |s| + s.write lib_path("foo/bar/lib/version.rb"), %(BAR_VERSION = '1.0') + s.write "bar.gemspec", <<-G + $:.unshift Dir.pwd + require 'lib/version' + Gem::Specification.new do |s| + s.name = 'bar' + s.author = 'no one' + s.version = BAR_VERSION + s.summary = 'Bar' + s.files = Dir["lib/**/*.rb"] + end + G + end + + build_git "foo", path: lib_path("foo") do |s| + s.write "bin/foo", "" + end + + install_gemfile <<-G + source "https://gem.repo1" + gem "bar", :git => "#{lib_path("foo")}" + gem "rails", "2.3.2" + G + + expect(the_bundle).to include_gems "bar 1.0" + expect(the_bundle).to include_gems "rails 2.3.2" + end + + it "runs the gemspec in the context of its parent directory, when using local overrides" do + build_git "foo", path: lib_path("foo"), gemspec: false do |s| + s.write lib_path("foo/lib/foo/version.rb"), %(FOO_VERSION = '1.0') + s.write "foo.gemspec", <<-G + $:.unshift Dir.pwd + require 'lib/foo/version' + Gem::Specification.new do |s| + s.name = 'foo' + s.author = 'no one' + s.version = FOO_VERSION + s.summary = 'Foo' + s.files = Dir["lib/**/*.rb"] + end + G + end + + gemfile <<-G + source "https://gem.repo1" + gem "foo", :git => "https://github.com/gems/foo", branch: "main" + G + + bundle %(config set local.foo #{lib_path("foo")}) + + expect(the_bundle).to include_gems "foo 1.0" + end + + it "installs from git even if a rubygems gem is present" do + build_gem "foo", "1.0", path: lib_path("fake_foo"), to_system: true do |s| + s.write "lib/foo.rb", "raise 'FAIL'" + end + + build_git "foo", "1.0" + + install_gemfile <<-G + source "https://gem.repo1" + gem "foo", "1.0", :git => "#{lib_path("foo-1.0")}" + G + + expect(the_bundle).to include_gems "foo 1.0" + end + + it "fakes the gem out if there is no gemspec" do + build_git "foo", gemspec: false + + install_gemfile <<-G + source "https://gem.repo1" + gem "foo", "1.0", :git => "#{lib_path("foo-1.0")}" + gem "rails", "2.3.2" + G + + expect(the_bundle).to include_gems("foo 1.0") + expect(the_bundle).to include_gems("rails 2.3.2") + end + + it "catches git errors and spits out useful output" do + gemfile <<-G + source "https://gem.repo1" + gem "foo", "1.0", :git => "omgomg" + G + + bundle :install, raise_on_error: false + + expect(err).to include("Git error:") + expect(err).to include("fatal") + expect(err).to include("omgomg") + end + + it "works when the gem path has spaces in it" do + build_git "foo", path: lib_path("foo space-1.0") + + install_gemfile <<-G + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo space-1.0")}" + G + + expect(the_bundle).to include_gems "foo 1.0" + end + + it "handles repos that have been force-pushed" do + build_git "forced", "1.0" + + install_gemfile <<-G + source "https://gem.repo1" + git "#{lib_path("forced-1.0")}" do + gem 'forced' + end + G + expect(the_bundle).to include_gems "forced 1.0" + + update_git "forced" do |s| + s.write "lib/forced.rb", "FORCED = '1.1'" + end + + bundle "update", all: true + expect(the_bundle).to include_gems "forced 1.1" + + git("reset --hard HEAD^", lib_path("forced-1.0")) + + bundle "update", all: true + expect(the_bundle).to include_gems "forced 1.0" + end + + it "ignores submodules if :submodule is not passed" do + # CVE-2022-39253: https://lore.kernel.org/lkml/xmqq4jw1uku5.fsf@gitster.g/ + system(*%W[git config --global protocol.file.allow always]) + + build_git "submodule", "1.0" + build_git "has_submodule", "1.0" do |s| + s.add_dependency "submodule" + end + git "submodule add #{lib_path("submodule-1.0")} submodule-1.0", lib_path("has_submodule-1.0") + git "commit -m \"submodulator\"", lib_path("has_submodule-1.0") + + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo1" + git "#{lib_path("has_submodule-1.0")}" do + gem "has_submodule" + end + G + expect(err).to match(%r{submodule >= 0 could not be found in rubygems repository https://gem.repo1/ or installed locally}) + + expect(the_bundle).not_to include_gems "has_submodule 1.0" + end + + it "handles repos with submodules" do + # CVE-2022-39253: https://lore.kernel.org/lkml/xmqq4jw1uku5.fsf@gitster.g/ + system(*%W[git config --global protocol.file.allow always]) + + build_git "submodule", "1.0" + build_git "has_submodule", "1.0" do |s| + s.add_dependency "submodule" + end + git "submodule add #{lib_path("submodule-1.0")} submodule-1.0", lib_path("has_submodule-1.0") + git "commit -m \"submodulator\"", lib_path("has_submodule-1.0") + + install_gemfile <<-G + source "https://gem.repo1" + git "#{lib_path("has_submodule-1.0")}", :submodules => true do + gem "has_submodule" + end + G + + expect(the_bundle).to include_gems "has_submodule 1.0" + end + + it "does not warn when deiniting submodules" do + # CVE-2022-39253: https://lore.kernel.org/lkml/xmqq4jw1uku5.fsf@gitster.g/ + system(*%W[git config --global protocol.file.allow always]) + + build_git "submodule", "1.0" + build_git "has_submodule", "1.0" + + git "submodule add #{lib_path("submodule-1.0")} submodule-1.0", lib_path("has_submodule-1.0") + git "commit -m \"submodulator\"", lib_path("has_submodule-1.0") + + install_gemfile <<-G + source "https://gem.repo1" + git "#{lib_path("has_submodule-1.0")}" do + gem "has_submodule" + end + G + expect(err).to be_empty + + expect(the_bundle).to include_gems "has_submodule 1.0" + expect(the_bundle).to_not include_gems "submodule 1.0" + end + + it "handles implicit updates when modifying the source info" do + git = build_git "foo" + + install_gemfile <<-G + source "https://gem.repo1" + git "#{lib_path("foo-1.0")}" do + gem "foo" + end + G + + update_git "foo" + update_git "foo" + + install_gemfile <<-G + source "https://gem.repo1" + git "#{lib_path("foo-1.0")}", :ref => "#{git.ref_for("HEAD^")}" do + gem "foo" + end + G + + run <<-RUBY + require 'foo' + puts "WIN" if FOO_PREV_REF == '#{git.ref_for("HEAD^^")}' + RUBY + + expect(out).to eq("WIN") + end + + it "does not do a remote fetch if the revision is cached locally" do + build_git "foo" + + install_gemfile <<-G + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + + FileUtils.rm_r(lib_path("foo-1.0")) + + bundle "install" + expect(out).not_to match(/updating/i) + end + + it "doesn't blow up if bundle install is run twice in a row" do + build_git "foo" + + gemfile <<-G + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + + bundle "install" + bundle "install" + end + + it "prints a friendly error if a file blocks the git repo" do + build_git "foo" + + FileUtils.mkdir_p(default_bundle_path) + FileUtils.touch(default_bundle_path("bundler")) + + install_gemfile <<-G, raise_on_error: false + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("foo-1.0")}" + G + + expect(last_command).to be_failure + expect(err).to include("Bundler could not install a gem because it " \ + "needs to create a directory, but a file exists " \ + "- #{default_bundle_path("bundler")}") + end + + it "does not duplicate git gem sources" do + build_lib "foo", path: lib_path("nested/foo") + build_lib "bar", path: lib_path("nested/bar") + + build_git "foo", path: lib_path("nested") + build_git "bar", path: lib_path("nested") + + install_gemfile <<-G + source "https://gem.repo1" + gem "foo", :git => "#{lib_path("nested")}" + gem "bar", :git => "#{lib_path("nested")}" + G + + expect(File.read(bundled_app_lock).scan("GIT").size).to eq(1) + end +end diff --git a/spec/bundler/install/gemfile/git_spec.rb b/spec/bundler/install/gemfile/git_spec.rb index 5fa740f4e1c68b..bed0a261a95404 100644 --- a/spec/bundler/install/gemfile/git_spec.rb +++ b/spec/bundler/install/gemfile/git_spec.rb @@ -513,1240 +513,4 @@ end end end - - describe "when specifying local override" do - it "uses the local repository instead of checking a new one out" do - build_git "myrack", "0.8", path: lib_path("local-myrack") do |s| - s.write "lib/myrack.rb", "puts :LOCAL" - end - - gemfile <<-G - source "https://gem.repo1" - gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main" - G - - bundle %(config set local.myrack #{lib_path("local-myrack")}) - bundle :install - - run "require 'myrack'" - expect(out).to eq("LOCAL") - end - - it "chooses the local repository on runtime" do - build_git "myrack", "0.8" - - FileUtils.cp_r("#{lib_path("myrack-0.8")}/.", lib_path("local-myrack")) - - update_git "myrack", "0.8", path: lib_path("local-myrack") do |s| - s.write "lib/myrack.rb", "puts :LOCAL" - end - - install_gemfile <<-G - source "https://gem.repo1" - gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main" - G - - bundle %(config set local.myrack #{lib_path("local-myrack")}) - run "require 'myrack'" - expect(out).to eq("LOCAL") - end - - it "unlocks the source when the dependencies have changed while switching to the local" do - build_git "myrack", "0.8" - - FileUtils.cp_r("#{lib_path("myrack-0.8")}/.", lib_path("local-myrack")) - - update_git "myrack", "0.8", path: lib_path("local-myrack") do |s| - s.write "myrack.gemspec", build_spec("myrack", "0.8") { runtime "rspec", "> 0" }.first.to_ruby - s.write "lib/myrack.rb", "puts :LOCAL" - end - - install_gemfile <<-G - source "https://gem.repo1" - gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main" - G - - bundle %(config set local.myrack #{lib_path("local-myrack")}) - bundle :install - run "require 'myrack'" - expect(out).to eq("LOCAL") - end - - it "updates specs on runtime" do - system_gems "nokogiri-1.4.2" - - build_git "myrack", "0.8" - - install_gemfile <<-G - source "https://gem.repo1" - gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main" - G - - lockfile0 = File.read(bundled_app_lock) - - FileUtils.cp_r("#{lib_path("myrack-0.8")}/.", lib_path("local-myrack")) - update_git "myrack", "0.8", path: lib_path("local-myrack") do |s| - s.add_dependency "nokogiri", "1.4.2" - end - - bundle %(config set local.myrack #{lib_path("local-myrack")}) - run "require 'myrack'" - - lockfile1 = File.read(bundled_app_lock) - expect(lockfile1).not_to eq(lockfile0) - end - - it "updates ref on install" do - build_git "myrack", "0.8" - - install_gemfile <<-G - source "https://gem.repo1" - gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main" - G - - lockfile0 = File.read(bundled_app_lock) - - FileUtils.cp_r("#{lib_path("myrack-0.8")}/.", lib_path("local-myrack")) - update_git "myrack", "0.8", path: lib_path("local-myrack") - - bundle %(config set local.myrack #{lib_path("local-myrack")}) - bundle :install - - lockfile1 = File.read(bundled_app_lock) - expect(lockfile1).not_to eq(lockfile0) - end - - it "explodes and gives correct solution if given path does not exist on install" do - build_git "myrack", "0.8" - - install_gemfile <<-G - source "https://gem.repo1" - gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main" - G - - bundle %(config set local.myrack #{lib_path("local-myrack")}) - bundle :install, raise_on_error: false - expect(err).to match(/Cannot use local override for myrack-0.8 because #{Regexp.escape(lib_path("local-myrack").to_s)} does not exist/) - - solution = "config unset local.myrack" - expect(err).to match(/Run `bundle #{solution}` to remove the local override/) - - bundle solution - bundle :install - - expect(err).to be_empty - end - - it "explodes and gives correct solution if branch is not given on install" do - build_git "myrack", "0.8" - FileUtils.cp_r("#{lib_path("myrack-0.8")}/.", lib_path("local-myrack")) - - install_gemfile <<-G - source "https://gem.repo1" - gem "myrack", :git => "#{lib_path("myrack-0.8")}" - G - - bundle %(config set local.myrack #{lib_path("local-myrack")}) - bundle :install, raise_on_error: false - expect(err).to match(/Cannot use local override for myrack-0.8 at #{Regexp.escape(lib_path("local-myrack").to_s)} because :branch is not specified in Gemfile/) - - solution = "config unset local.myrack" - expect(err).to match(/Specify a branch or run `bundle #{solution}` to remove the local override/) - - bundle solution - bundle :install - - expect(err).to be_empty - end - - it "does not explode if disable_local_branch_check is given" do - build_git "myrack", "0.8" - FileUtils.cp_r("#{lib_path("myrack-0.8")}/.", lib_path("local-myrack")) - - install_gemfile <<-G - source "https://gem.repo1" - gem "myrack", :git => "#{lib_path("myrack-0.8")}" - G - - bundle %(config set local.myrack #{lib_path("local-myrack")}) - bundle %(config set disable_local_branch_check true) - bundle :install - expect(out).to match(/Bundle complete!/) - end - - it "explodes on different branches on install" do - build_git "myrack", "0.8" - - FileUtils.cp_r("#{lib_path("myrack-0.8")}/.", lib_path("local-myrack")) - - update_git "myrack", "0.8", path: lib_path("local-myrack"), branch: "another" do |s| - s.write "lib/myrack.rb", "puts :LOCAL" - end - - install_gemfile <<-G - source "https://gem.repo1" - gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main" - G - - bundle %(config set local.myrack #{lib_path("local-myrack")}) - bundle :install, raise_on_error: false - expect(err).to match(/is using branch another but Gemfile specifies main/) - end - - it "explodes on invalid revision on install" do - build_git "myrack", "0.8" - - build_git "myrack", "0.8", path: lib_path("local-myrack") do |s| - s.write "lib/myrack.rb", "puts :LOCAL" - end - - install_gemfile <<-G - source "https://gem.repo1" - gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main" - G - - bundle %(config set local.myrack #{lib_path("local-myrack")}) - bundle :install, raise_on_error: false - expect(err).to match(/The Gemfile lock is pointing to revision \w+/) - end - - it "does not explode on invalid revision on install" do - build_git "myrack", "0.8" - - build_git "myrack", "0.8", path: lib_path("local-myrack") do |s| - s.write "lib/myrack.rb", "puts :LOCAL" - end - - install_gemfile <<-G - source "https://gem.repo1" - gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main" - G - - bundle %(config set local.myrack #{lib_path("local-myrack")}) - bundle %(config set disable_local_revision_check true) - bundle :install - expect(out).to match(/Bundle complete!/) - end - end - - describe "specified inline" do - # TODO: Figure out how to write this test so that it is not flaky depending - # on the current network situation. - # it "supports private git URLs" do - # gemfile <<-G - # gem "thingy", :git => "git@notthere.fallingsnow.net:somebody/thingy.git" - # G - # - # bundle :install - # - # # p out - # # p err - # puts err unless err.empty? # This spec fails randomly every so often - # err.should include("notthere.fallingsnow.net") - # err.should include("ssh") - # end - - it "installs from git even if a newer gem is available elsewhere" do - build_git "myrack", "0.8" - - install_gemfile <<-G - source "https://gem.repo1" - gem "myrack", :git => "#{lib_path("myrack-0.8")}" - G - - expect(the_bundle).to include_gems "myrack 0.8" - end - - it "installs dependencies from git even if a newer gem is available elsewhere" do - system_gems "myrack-1.0.0" - - build_lib "myrack", "1.0", path: lib_path("nested/bar") do |s| - s.write "lib/myrack.rb", "puts 'WIN OVERRIDE'" - end - - build_git "foo", path: lib_path("nested") do |s| - s.add_dependency "myrack", "= 1.0" - end - - install_gemfile <<-G - source "https://gem.repo1" - gem "foo", :git => "#{lib_path("nested")}" - G - - run "require 'myrack'" - expect(out).to eq("WIN OVERRIDE") - end - - it "correctly unlocks when changing to a git source" do - install_gemfile <<-G - source "https://gem.repo1" - gem "myrack", "0.9.1" - G - - build_git "myrack", path: lib_path("myrack") - - install_gemfile <<-G - source "https://gem.repo1" - gem "myrack", "1.0.0", :git => "#{lib_path("myrack")}" - G - - expect(the_bundle).to include_gems "myrack 1.0.0" - end - - it "correctly unlocks when changing to a git source without versions" do - install_gemfile <<-G - source "https://gem.repo1" - gem "myrack" - G - - build_git "myrack", "1.2", path: lib_path("myrack") - - install_gemfile <<-G - source "https://gem.repo1" - gem "myrack", :git => "#{lib_path("myrack")}" - G - - expect(the_bundle).to include_gems "myrack 1.2" - end - end - - describe "block syntax" do - it "pulls all gems from a git block" do - build_lib "omg", path: lib_path("hi2u/omg") - build_lib "hi2u", path: lib_path("hi2u") - - install_gemfile <<-G - source "https://gem.repo1" - path "#{lib_path("hi2u")}" do - gem "omg" - gem "hi2u" - end - G - - expect(the_bundle).to include_gems "omg 1.0", "hi2u 1.0" - end - end - - it "uses a ref if specified" do - build_git "foo" - @revision = revision_for(lib_path("foo-1.0")) - update_git "foo" - - install_gemfile <<-G - source "https://gem.repo1" - gem "foo", :git => "#{lib_path("foo-1.0")}", :ref => "#{@revision}" - G - - run <<-RUBY - require 'foo' - puts "WIN" unless defined?(FOO_PREV_REF) - RUBY - - expect(out).to eq("WIN") - end - - it "correctly handles cases with invalid gemspecs" do - build_git "foo" do |s| - s.summary = nil - end - - install_gemfile <<-G - source "https://gem.repo1" - gem "foo", :git => "#{lib_path("foo-1.0")}" - gem "rails", "2.3.2" - G - - expect(the_bundle).to include_gems "foo 1.0" - expect(the_bundle).to include_gems "rails 2.3.2" - end - - it "runs the gemspec in the context of its parent directory" do - build_lib "bar", path: lib_path("foo/bar"), gemspec: false do |s| - s.write lib_path("foo/bar/lib/version.rb"), %(BAR_VERSION = '1.0') - s.write "bar.gemspec", <<-G - $:.unshift Dir.pwd - require 'lib/version' - Gem::Specification.new do |s| - s.name = 'bar' - s.author = 'no one' - s.version = BAR_VERSION - s.summary = 'Bar' - s.files = Dir["lib/**/*.rb"] - end - G - end - - build_git "foo", path: lib_path("foo") do |s| - s.write "bin/foo", "" - end - - install_gemfile <<-G - source "https://gem.repo1" - gem "bar", :git => "#{lib_path("foo")}" - gem "rails", "2.3.2" - G - - expect(the_bundle).to include_gems "bar 1.0" - expect(the_bundle).to include_gems "rails 2.3.2" - end - - it "runs the gemspec in the context of its parent directory, when using local overrides" do - build_git "foo", path: lib_path("foo"), gemspec: false do |s| - s.write lib_path("foo/lib/foo/version.rb"), %(FOO_VERSION = '1.0') - s.write "foo.gemspec", <<-G - $:.unshift Dir.pwd - require 'lib/foo/version' - Gem::Specification.new do |s| - s.name = 'foo' - s.author = 'no one' - s.version = FOO_VERSION - s.summary = 'Foo' - s.files = Dir["lib/**/*.rb"] - end - G - end - - gemfile <<-G - source "https://gem.repo1" - gem "foo", :git => "https://github.com/gems/foo", branch: "main" - G - - bundle %(config set local.foo #{lib_path("foo")}) - - expect(the_bundle).to include_gems "foo 1.0" - end - - it "installs from git even if a rubygems gem is present" do - build_gem "foo", "1.0", path: lib_path("fake_foo"), to_system: true do |s| - s.write "lib/foo.rb", "raise 'FAIL'" - end - - build_git "foo", "1.0" - - install_gemfile <<-G - source "https://gem.repo1" - gem "foo", "1.0", :git => "#{lib_path("foo-1.0")}" - G - - expect(the_bundle).to include_gems "foo 1.0" - end - - it "fakes the gem out if there is no gemspec" do - build_git "foo", gemspec: false - - install_gemfile <<-G - source "https://gem.repo1" - gem "foo", "1.0", :git => "#{lib_path("foo-1.0")}" - gem "rails", "2.3.2" - G - - expect(the_bundle).to include_gems("foo 1.0") - expect(the_bundle).to include_gems("rails 2.3.2") - end - - it "catches git errors and spits out useful output" do - gemfile <<-G - source "https://gem.repo1" - gem "foo", "1.0", :git => "omgomg" - G - - bundle :install, raise_on_error: false - - expect(err).to include("Git error:") - expect(err).to include("fatal") - expect(err).to include("omgomg") - end - - it "works when the gem path has spaces in it" do - build_git "foo", path: lib_path("foo space-1.0") - - install_gemfile <<-G - source "https://gem.repo1" - gem "foo", :git => "#{lib_path("foo space-1.0")}" - G - - expect(the_bundle).to include_gems "foo 1.0" - end - - it "handles repos that have been force-pushed" do - build_git "forced", "1.0" - - install_gemfile <<-G - source "https://gem.repo1" - git "#{lib_path("forced-1.0")}" do - gem 'forced' - end - G - expect(the_bundle).to include_gems "forced 1.0" - - update_git "forced" do |s| - s.write "lib/forced.rb", "FORCED = '1.1'" - end - - bundle "update", all: true - expect(the_bundle).to include_gems "forced 1.1" - - git("reset --hard HEAD^", lib_path("forced-1.0")) - - bundle "update", all: true - expect(the_bundle).to include_gems "forced 1.0" - end - - it "ignores submodules if :submodule is not passed" do - # CVE-2022-39253: https://lore.kernel.org/lkml/xmqq4jw1uku5.fsf@gitster.g/ - system(*%W[git config --global protocol.file.allow always]) - - build_git "submodule", "1.0" - build_git "has_submodule", "1.0" do |s| - s.add_dependency "submodule" - end - git "submodule add #{lib_path("submodule-1.0")} submodule-1.0", lib_path("has_submodule-1.0") - git "commit -m \"submodulator\"", lib_path("has_submodule-1.0") - - install_gemfile <<-G, raise_on_error: false - source "https://gem.repo1" - git "#{lib_path("has_submodule-1.0")}" do - gem "has_submodule" - end - G - expect(err).to match(%r{submodule >= 0 could not be found in rubygems repository https://gem.repo1/ or installed locally}) - - expect(the_bundle).not_to include_gems "has_submodule 1.0" - end - - it "handles repos with submodules" do - # CVE-2022-39253: https://lore.kernel.org/lkml/xmqq4jw1uku5.fsf@gitster.g/ - system(*%W[git config --global protocol.file.allow always]) - - build_git "submodule", "1.0" - build_git "has_submodule", "1.0" do |s| - s.add_dependency "submodule" - end - git "submodule add #{lib_path("submodule-1.0")} submodule-1.0", lib_path("has_submodule-1.0") - git "commit -m \"submodulator\"", lib_path("has_submodule-1.0") - - install_gemfile <<-G - source "https://gem.repo1" - git "#{lib_path("has_submodule-1.0")}", :submodules => true do - gem "has_submodule" - end - G - - expect(the_bundle).to include_gems "has_submodule 1.0" - end - - it "does not warn when deiniting submodules" do - # CVE-2022-39253: https://lore.kernel.org/lkml/xmqq4jw1uku5.fsf@gitster.g/ - system(*%W[git config --global protocol.file.allow always]) - - build_git "submodule", "1.0" - build_git "has_submodule", "1.0" - - git "submodule add #{lib_path("submodule-1.0")} submodule-1.0", lib_path("has_submodule-1.0") - git "commit -m \"submodulator\"", lib_path("has_submodule-1.0") - - install_gemfile <<-G - source "https://gem.repo1" - git "#{lib_path("has_submodule-1.0")}" do - gem "has_submodule" - end - G - expect(err).to be_empty - - expect(the_bundle).to include_gems "has_submodule 1.0" - expect(the_bundle).to_not include_gems "submodule 1.0" - end - - it "handles implicit updates when modifying the source info" do - git = build_git "foo" - - install_gemfile <<-G - source "https://gem.repo1" - git "#{lib_path("foo-1.0")}" do - gem "foo" - end - G - - update_git "foo" - update_git "foo" - - install_gemfile <<-G - source "https://gem.repo1" - git "#{lib_path("foo-1.0")}", :ref => "#{git.ref_for("HEAD^")}" do - gem "foo" - end - G - - run <<-RUBY - require 'foo' - puts "WIN" if FOO_PREV_REF == '#{git.ref_for("HEAD^^")}' - RUBY - - expect(out).to eq("WIN") - end - - it "does not do a remote fetch if the revision is cached locally" do - build_git "foo" - - install_gemfile <<-G - source "https://gem.repo1" - gem "foo", :git => "#{lib_path("foo-1.0")}" - G - - FileUtils.rm_r(lib_path("foo-1.0")) - - bundle "install" - expect(out).not_to match(/updating/i) - end - - it "doesn't blow up if bundle install is run twice in a row" do - build_git "foo" - - gemfile <<-G - source "https://gem.repo1" - gem "foo", :git => "#{lib_path("foo-1.0")}" - G - - bundle "install" - bundle "install" - end - - it "prints a friendly error if a file blocks the git repo" do - build_git "foo" - - FileUtils.mkdir_p(default_bundle_path) - FileUtils.touch(default_bundle_path("bundler")) - - install_gemfile <<-G, raise_on_error: false - source "https://gem.repo1" - gem "foo", :git => "#{lib_path("foo-1.0")}" - G - - expect(last_command).to be_failure - expect(err).to include("Bundler could not install a gem because it " \ - "needs to create a directory, but a file exists " \ - "- #{default_bundle_path("bundler")}") - end - - it "does not duplicate git gem sources" do - build_lib "foo", path: lib_path("nested/foo") - build_lib "bar", path: lib_path("nested/bar") - - build_git "foo", path: lib_path("nested") - build_git "bar", path: lib_path("nested") - - install_gemfile <<-G - source "https://gem.repo1" - gem "foo", :git => "#{lib_path("nested")}" - gem "bar", :git => "#{lib_path("nested")}" - G - - expect(File.read(bundled_app_lock).scan("GIT").size).to eq(1) - end - - describe "switching sources" do - it "doesn't explode when switching Path to Git sources" do - build_gem "foo", "1.0", to_system: true do |s| - s.write "lib/foo.rb", "raise 'fail'" - end - build_lib "foo", "1.0", path: lib_path("bar/foo") - build_git "bar", "1.0", path: lib_path("bar") do |s| - s.add_dependency "foo" - end - - install_gemfile <<-G - source "https://gem.repo1" - gem "bar", :path => "#{lib_path("bar")}" - G - - install_gemfile <<-G - source "https://gem.repo1" - gem "bar", :git => "#{lib_path("bar")}" - G - - expect(the_bundle).to include_gems "foo 1.0", "bar 1.0" - end - - it "doesn't explode when switching Gem to Git source" do - install_gemfile <<-G - source "https://gem.repo1" - gem "myrack-obama" - gem "myrack", "1.0.0" - G - - build_git "myrack", "1.0" do |s| - s.write "lib/new_file.rb", "puts 'USING GIT'" - end - - install_gemfile <<-G - source "https://gem.repo1" - gem "myrack-obama" - gem "myrack", "1.0.0", :git => "#{lib_path("myrack-1.0")}" - G - - run "require 'new_file'" - expect(out).to eq("USING GIT") - end - - it "doesn't explode when removing an explicit exact version from a git gem with dependencies" do - build_lib "activesupport", "7.1.4", path: lib_path("rails/activesupport") - build_git "rails", "7.1.4", path: lib_path("rails") do |s| - s.add_dependency "activesupport", "= 7.1.4" - end - - install_gemfile <<-G - source "https://gem.repo1" - gem "rails", "7.1.4", :git => "#{lib_path("rails")}" - G - - install_gemfile <<-G - source "https://gem.repo1" - gem "rails", :git => "#{lib_path("rails")}" - G - - expect(the_bundle).to include_gem "rails 7.1.4", "activesupport 7.1.4" - end - - it "doesn't explode when adding an explicit ref to a git gem with dependencies" do - lib_root = lib_path("rails") - - build_lib "activesupport", "7.1.4", path: lib_root.join("activesupport") - build_git "rails", "7.1.4", path: lib_root do |s| - s.add_dependency "activesupport", "= 7.1.4" - end - - old_revision = revision_for(lib_root) - update_git "rails", "7.1.4", path: lib_root - - install_gemfile <<-G - source "https://gem.repo1" - gem "rails", "7.1.4", :git => "#{lib_root}" - G - - install_gemfile <<-G - source "https://gem.repo1" - gem "rails", :git => "#{lib_root}", :ref => "#{old_revision}" - G - - expect(the_bundle).to include_gem "rails 7.1.4", "activesupport 7.1.4" - end - end - - describe "bundle install after the remote has been updated" do - it "installs" do - build_git "valim" - - install_gemfile <<-G - source "https://gem.repo1" - gem "valim", :git => "#{lib_path("valim-1.0")}" - G - - old_revision = revision_for(lib_path("valim-1.0")) - update_git "valim" - new_revision = revision_for(lib_path("valim-1.0")) - - old_lockfile = File.read(bundled_app_lock) - lockfile(bundled_app_lock, old_lockfile.gsub(/revision: #{old_revision}/, "revision: #{new_revision}")) - - bundle "install" - - run <<-R - require "valim" - puts VALIM_PREV_REF - R - - expect(out).to eq(old_revision) - end - - it "gives a helpful error message when the remote ref no longer exists" do - build_git "foo" - revision = revision_for(lib_path("foo-1.0")) - - install_gemfile <<-G - source "https://gem.repo1" - gem "foo", :git => "#{lib_path("foo-1.0")}", :ref => "#{revision}" - G - expect(out).to_not match(/Revision.*does not exist/) - - install_gemfile <<-G, raise_on_error: false - source "https://gem.repo1" - gem "foo", :git => "#{lib_path("foo-1.0")}", :ref => "deadbeef" - G - expect(err).to include("Revision deadbeef does not exist in the repository") - end - - it "gives a helpful error message when the remote branch no longer exists" do - build_git "foo" - - install_gemfile <<-G, env: { "LANG" => "en" }, raise_on_error: false - source "https://gem.repo1" - gem "foo", :git => "#{lib_path("foo-1.0")}", :branch => "deadbeef" - G - - expect(err).to include("Revision deadbeef does not exist in the repository") - end - end - - describe "bundle install with deployment mode configured and git sources" do - it "works" do - build_git "valim", path: lib_path("valim") - - install_gemfile <<-G - source "https://gem.repo1" - gem "valim", "= 1.0", :git => "#{lib_path("valim")}" - G - - pristine_system_gems - - bundle_config "deployment true" - bundle :install - end - end - - describe "gem install hooks" do - it "runs pre-install hooks" do - build_git "foo" - gemfile <<-G - source "https://gem.repo1" - gem "foo", :git => "#{lib_path("foo-1.0")}" - G - - File.open(lib_path("install_hooks.rb"), "w") do |h| - h.write <<-H - Gem.pre_install_hooks << lambda do |inst| - STDERR.puts "Ran pre-install hook: \#{inst.spec.full_name}" - end - H - end - - bundle :install, - requires: [lib_path("install_hooks.rb")] - expect(err_without_deprecations).to eq("Ran pre-install hook: foo-1.0") - end - - it "runs post-install hooks" do - build_git "foo" - gemfile <<-G - source "https://gem.repo1" - gem "foo", :git => "#{lib_path("foo-1.0")}" - G - - File.open(lib_path("install_hooks.rb"), "w") do |h| - h.write <<-H - Gem.post_install_hooks << lambda do |inst| - STDERR.puts "Ran post-install hook: \#{inst.spec.full_name}" - end - H - end - - bundle :install, - requires: [lib_path("install_hooks.rb")] - expect(err_without_deprecations).to eq("Ran post-install hook: foo-1.0") - end - - it "complains if the install hook fails" do - build_git "foo" - gemfile <<-G - source "https://gem.repo1" - gem "foo", :git => "#{lib_path("foo-1.0")}" - G - - File.open(lib_path("install_hooks.rb"), "w") do |h| - h.write <<-H - Gem.pre_install_hooks << lambda do |inst| - false - end - H - end - - bundle :install, requires: [lib_path("install_hooks.rb")], raise_on_error: false - expect(err).to include("failed for foo-1.0") - end - end - - context "with an extension" do - it "installs the extension" do - build_git "foo" do |s| - s.add_dependency "rake" - s.extensions << "Rakefile" - s.write "Rakefile", <<-RUBY - task :default do - path = File.expand_path("lib", __dir__) - FileUtils.mkdir_p(path) - File.open("\#{path}/foo.rb", "w") do |f| - f.puts "FOO = 'YES'" - end - end - RUBY - end - - install_gemfile <<-G - source "https://gem.repo1" - gem "foo", :git => "#{lib_path("foo-1.0")}" - G - - run <<-R - require 'foo' - puts FOO - R - expect(out).to eq("YES") - - run <<-R - puts $:.grep(/ext/) - R - expect(out).to include(Pathname.glob(default_bundle_path("bundler/gems/extensions/**/foo-1.0-*")).first.to_s) - end - - it "does not use old extension after ref changes" do - git_reader = build_git "foo", no_default: true do |s| - s.extensions = ["ext/extconf.rb"] - s.write "ext/extconf.rb", <<-RUBY - require "mkmf" - create_makefile("foo") - RUBY - s.write "ext/foo.c", "void Init_foo() {}" - end - - 2.times do |i| - File.open(git_reader.path.join("ext/foo.c"), "w") do |file| - file.write <<-C - #include "ruby.h" - VALUE foo(VALUE self) { return INT2FIX(#{i}); } - void Init_foo() { rb_define_global_function("foo", &foo, 0); } - C - end - git("commit -m \"commit for iteration #{i}\" ext/foo.c", git_reader.path) - - git_commit_sha = git_reader.ref_for("HEAD") - - install_gemfile <<-G - source "https://gem.repo1" - gem "foo", :git => "#{lib_path("foo-1.0")}", :ref => "#{git_commit_sha}" - G - - run <<-R - require 'foo' - puts foo - R - - expect(out).to eq(i.to_s) - end - end - - it "does not prompt to gem install if extension fails" do - build_git "foo" do |s| - s.add_dependency "rake" - s.extensions << "Rakefile" - s.write "Rakefile", <<-RUBY - task :default do - raise - end - RUBY - end - - install_gemfile <<-G, raise_on_error: false - source "https://gem.repo1" - gem "foo", :git => "#{lib_path("foo-1.0")}" - G - - expect(err).to end_with(<<-M.strip) -An error occurred while installing foo (1.0), and Bundler cannot continue. - -In Gemfile: - foo - M - expect(out).not_to include("gem install foo") - end - - it "does not reinstall the extension" do - build_git "foo" do |s| - s.add_dependency "rake" - s.extensions << "Rakefile" - s.write "Rakefile", <<-RUBY - task :default do - path = File.expand_path("lib", __dir__) - FileUtils.mkdir_p(path) - cur_time = Time.now.to_f.to_s - File.open("\#{path}/foo.rb", "w") do |f| - f.puts "FOO = \#{cur_time}" - end - end - RUBY - end - - install_gemfile <<-G - source "https://gem.repo1" - gem "foo", :git => "#{lib_path("foo-1.0")}" - G - - run <<-R - require 'foo' - puts FOO - R - - installed_time = out - expect(installed_time).to match(/\A\d+\.\d+\z/) - - install_gemfile <<-G - source "https://gem.repo1" - gem "foo", :git => "#{lib_path("foo-1.0")}" - G - - run <<-R - require 'foo' - puts FOO - R - expect(out).to eq(installed_time) - end - - it "does not reinstall the extension when changing another gem" do - build_git "foo" do |s| - s.add_dependency "rake" - s.extensions << "Rakefile" - s.write "Rakefile", <<-RUBY - task :default do - path = File.expand_path("lib", __dir__) - FileUtils.mkdir_p(path) - cur_time = Time.now.to_f.to_s - File.open("\#{path}/foo.rb", "w") do |f| - f.puts "FOO = \#{cur_time}" - end - end - RUBY - end - - install_gemfile <<-G - source "https://gem.repo1" - gem "myrack", "0.9.1" - gem "foo", :git => "#{lib_path("foo-1.0")}" - G - - run <<-R - require 'foo' - puts FOO - R - - installed_time = out - expect(installed_time).to match(/\A\d+\.\d+\z/) - - install_gemfile <<-G - source "https://gem.repo1" - gem "myrack", "1.0.0" - gem "foo", :git => "#{lib_path("foo-1.0")}" - G - - run <<-R - require 'foo' - puts FOO - R - expect(out).to eq(installed_time) - end - - it "does reinstall the extension when changing refs" do - build_git "foo" do |s| - s.add_dependency "rake" - s.extensions << "Rakefile" - s.write "Rakefile", <<-RUBY - task :default do - path = File.expand_path("lib", __dir__) - FileUtils.mkdir_p(path) - cur_time = Time.now.to_f.to_s - File.open("\#{path}/foo.rb", "w") do |f| - f.puts "FOO = \#{cur_time}" - end - end - RUBY - end - - install_gemfile <<-G - source "https://gem.repo1" - gem "foo", :git => "#{lib_path("foo-1.0")}" - G - - run <<-R - require 'foo' - puts FOO - R - - installed_time = out - - update_git("foo", branch: "branch2") - - expect(installed_time).to match(/\A\d+\.\d+\z/) - - install_gemfile <<-G - source "https://gem.repo1" - gem "foo", :git => "#{lib_path("foo-1.0")}", :branch => "branch2" - G - - run <<-R - require 'foo' - puts FOO - R - expect(out).not_to eq(installed_time) - - installed_time = out - - update_git("foo") - bundle "update foo" - - run <<-R - require 'foo' - puts FOO - R - expect(out).not_to eq(installed_time) - end - end - - it "ignores git environment variables" do - build_git "xxxxxx" do |s| - s.executables = "xxxxxxbar" - end - - Bundler::SharedHelpers.with_clean_git_env do - ENV["GIT_DIR"] = "bar" - ENV["GIT_WORK_TREE"] = "bar" - - install_gemfile <<-G - source "https://gem.repo1" - git "#{lib_path("xxxxxx-1.0")}" do - gem 'xxxxxx' - end - G - - expect(ENV["GIT_DIR"]).to eq("bar") - expect(ENV["GIT_WORK_TREE"]).to eq("bar") - end - end - - describe "without git installed" do - it "prints a better error message when installing" do - gemfile <<-G - source "https://gem.repo1" - - gem "rake", git: "https://github.com/ruby/rake" - G - - lockfile <<-L - GIT - remote: https://github.com/ruby/rake - revision: 5c60da8644a9e4f655e819252e3b6ca77f42b7af - specs: - rake (13.0.6) - - GEM - remote: https://rubygems.org/ - specs: - - PLATFORMS - #{lockfile_platforms} - - DEPENDENCIES - rake! - - BUNDLED WITH - #{Bundler::VERSION} - L - - with_path_as("") do - bundle "install", raise_on_error: false - end - expect(err). - to include("You need to install git to be able to use gems from git repositories. For help installing git, please refer to GitHub's tutorial at https://help.github.com/articles/set-up-git") - end - - it "prints a better error message when updating" do - build_git "foo" - - install_gemfile <<-G - source "https://gem.repo1" - git "#{lib_path("foo-1.0")}" do - gem 'foo' - end - G - - with_path_as("") do - bundle "update", all: true, raise_on_error: false - end - expect(err). - to include("You need to install git to be able to use gems from git repositories. For help installing git, please refer to GitHub's tutorial at https://help.github.com/articles/set-up-git") - end - - it "doesn't need git in the new machine if an installed git gem is copied to another machine" do - build_git "foo" - - install_gemfile <<-G - source "https://gem.repo1" - git "#{lib_path("foo-1.0")}" do - gem 'foo' - end - G - bundle_config_global "path vendor/bundle" - bundle :install - pristine_system_gems - - bundle "install", env: { "PATH" => "" } - expect(out).to_not include("You need to install git to be able to use gems from git repositories.") - end - end - - describe "when the git source is overridden with a local git repo" do - before do - bundle_config_global "local.foo #{lib_path("foo")}" - end - - describe "and git output is colorized" do - before do - File.open("#{ENV["HOME"]}/.gitconfig", "w") do |f| - f.write("[color]\n\tui = always\n") - end - end - - it "installs successfully" do - build_git "foo", "1.0", path: lib_path("foo") - - gemfile <<-G - source "https://gem.repo1" - gem "foo", :git => "#{lib_path("foo")}", :branch => "main" - G - - bundle :install - expect(the_bundle).to include_gems "foo 1.0" - end - end - end - - context "git sources that include credentials" do - context "that are username and password" do - let(:credentials) { "user1:password1" } - - it "does not display the password" do - install_gemfile <<-G, raise_on_error: false - source "https://gem.repo1" - git "https://#{credentials}@github.com/company/private-repo" do - gem "foo" - end - G - - expect(stdboth).to_not include("password1") - expect(out).to include("Fetching https://user1@github.com/company/private-repo") - end - end - - context "that is an oauth token" do - let(:credentials) { "oauth_token" } - - it "displays the oauth scheme but not the oauth token" do - install_gemfile <<-G, raise_on_error: false - source "https://gem.repo1" - git "https://#{credentials}:x-oauth-basic@github.com/company/private-repo" do - gem "foo" - end - G - - expect(stdboth).to_not include("oauth_token") - expect(out).to include("Fetching https://x-oauth-basic@github.com/company/private-repo") - end - end - end end diff --git a/spec/bundler/runtime/setup_gems_spec.rb b/spec/bundler/runtime/setup_gems_spec.rb new file mode 100644 index 00000000000000..1a8df525d3cbf2 --- /dev/null +++ b/spec/bundler/runtime/setup_gems_spec.rb @@ -0,0 +1,825 @@ +# frozen_string_literal: true + +require "tmpdir" + +RSpec.describe "Bundler.setup" do + it "should prepend gemspec require paths to $LOAD_PATH in order" do + update_repo2 do + build_gem("requirepaths") do |s| + s.write("lib/rq.rb", "puts 'yay'") + s.write("src/rq.rb", "puts 'nooo'") + s.require_paths = %w[lib src] + end + end + + install_gemfile <<-G + source "https://gem.repo2" + gem "requirepaths", :require => nil + G + + run "require 'rq'" + expect(out).to eq("yay") + end + + it "should clean $LOAD_PATH properly" do + gem_name = "very_simple_binary" + full_gem_name = gem_name + "-1.0" + + system_gems full_gem_name + + install_gemfile <<-G + source "https://gem.repo1" + G + + ruby <<-R + require 'bundler' + gem '#{gem_name}' + + puts $LOAD_PATH.count {|path| path =~ /#{gem_name}/} >= 2 + + Bundler.setup + + puts $LOAD_PATH.count {|path| path =~ /#{gem_name}/} == 0 + R + + expect(out).to eq("true\ntrue") + end + + context "with bundler is located in symlinked GEM_HOME" do + let(:gem_home) { Dir.mktmpdir } + let(:symlinked_gem_home) { tmp("gem_home-symlink").to_s } + let(:full_name) { "bundler-#{Bundler::VERSION}" } + + before do + File.symlink(gem_home, symlinked_gem_home) + gems_dir = File.join(gem_home, "gems") + specifications_dir = File.join(gem_home, "specifications") + Dir.mkdir(gems_dir) + Dir.mkdir(specifications_dir) + + File.symlink(source_root, File.join(gems_dir, full_name)) + + gemspec_content = File.binread(gemspec). + sub("Bundler::VERSION", %("#{Bundler::VERSION}")). + lines.reject {|line| line.include?("lib/bundler/version") }.join + + File.open(File.join(specifications_dir, "#{full_name}.gemspec"), "wb") do |f| + f.write(gemspec_content) + end + end + + it "should not remove itself from the LOAD_PATH and require a different copy of 'bundler/setup'" do + install_gemfile "source 'https://gem.repo1'" + + ruby <<-R, env: { "GEM_PATH" => symlinked_gem_home } + TracePoint.trace(:class) do |tp| + if tp.path.include?("bundler") && !tp.path.start_with?("#{source_root}") + puts "OMG. Defining a class from another bundler at \#{tp.path}:\#{tp.lineno}" + end + end + gem 'bundler', '#{Bundler::VERSION}' + require 'bundler/setup' + R + + expect(out).to be_empty + end + end + + it "does not reveal system gems even when Gem.refresh is called" do + system_gems "myrack-1.0.0" + + install_gemfile <<-G + source "https://gem.repo1" + gem "activesupport" + G + + run <<-R + puts Bundler.rubygems.installed_specs.map(&:name) + Gem.refresh + puts Bundler.rubygems.installed_specs.map(&:name) + R + + expect(out).to include("activesupport") + expect(out).not_to include("myrack") + end + + describe "when a vendored gem specification uses the :path option" do + let(:filesystem_root) do + current = Pathname.new(Dir.pwd) + current = current.parent until current == current.parent + current + end + + it "should resolve paths relative to the Gemfile" do + path = bundled_app(File.join("vendor", "foo")) + build_lib "foo", path: path + + # If the .gemspec exists, then Bundler handles the path differently. + # See Source::Path.load_spec_files for details. + FileUtils.rm(File.join(path, "foo.gemspec")) + + install_gemfile <<-G + source "https://gem.repo1" + gem 'foo', '1.2.3', :path => 'vendor/foo' + G + + run <<-R, env: { "BUNDLE_GEMFILE" => bundled_app_gemfile.to_s }, dir: bundled_app.parent + require 'foo' + R + expect(err).to be_empty + end + + it "should make sure the Bundler.root is really included in the path relative to the Gemfile" do + relative_path = File.join("vendor", Dir.pwd.gsub(/^#{filesystem_root}/, "")) + absolute_path = bundled_app(relative_path) + FileUtils.mkdir_p(absolute_path) + build_lib "foo", path: absolute_path + + # If the .gemspec exists, then Bundler handles the path differently. + # See Source::Path.load_spec_files for details. + FileUtils.rm(File.join(absolute_path, "foo.gemspec")) + + gemfile <<-G + source "https://gem.repo1" + gem 'foo', '1.2.3', :path => '#{relative_path}' + G + + bundle :install + + run <<-R, env: { "BUNDLE_GEMFILE" => bundled_app_gemfile.to_s }, dir: bundled_app.parent + require 'foo' + R + + expect(err).to be_empty + end + end + + describe "with git gems that don't have gemspecs" do + before :each do + build_git "no_gemspec", gemspec: false + + install_gemfile <<-G + source "https://gem.repo1" + gem "no_gemspec", "1.0", :git => "#{lib_path("no_gemspec-1.0")}" + G + end + + it "loads the library via a virtual spec" do + run <<-R + require 'no_gemspec' + puts NO_GEMSPEC + R + + expect(out).to eq("1.0") + end + end + + describe "with bundled and system gems" do + before :each do + system_gems "myrack-1.0.0" + + install_gemfile <<-G + source "https://gem.repo1" + + gem "activesupport", "2.3.5" + G + end + + it "does not pull in system gems" do + run <<-R + begin; + require 'myrack' + rescue LoadError + puts 'WIN' + end + R + + expect(out).to eq("WIN") + end + + it "provides a gem method" do + run <<-R + gem 'activesupport' + require 'activesupport' + puts ACTIVESUPPORT + R + + expect(out).to eq("2.3.5") + end + + it "raises an exception if gem is used to invoke a system gem not in the bundle" do + run <<-R + begin + gem 'myrack' + rescue LoadError => e + puts e.message + end + R + + expect(out).to eq("myrack is not part of the bundle. Add it to your Gemfile.") + end + + it "sets GEM_HOME appropriately" do + run "puts ENV['GEM_HOME']" + expect(out).to eq(default_bundle_path.to_s) + end + end + + describe "with system gems in the bundle" do + before :each do + bundle_config "path.system true" + system_gems "myrack-1.0.0" + + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack", "1.0.0" + gem "activesupport", "2.3.5" + G + end + + it "sets GEM_PATH appropriately" do + run "puts Gem.path" + paths = out.split("\n") + expect(paths).to include(system_gem_path.to_s) + end + end + + describe "with a gemspec that requires other files" do + before :each do + build_git "bar", gemspec: false do |s| + s.write "lib/bar/version.rb", %(BAR_VERSION = '1.0') + s.write "bar.gemspec", <<-G + require_relative 'lib/bar/version' + + Gem::Specification.new do |s| + s.name = 'bar' + s.version = BAR_VERSION + s.summary = 'Bar' + s.files = Dir["lib/**/*.rb"] + s.author = 'no one' + end + G + end + + gemfile <<-G + source "https://gem.repo1" + gem "bar", :git => "#{lib_path("bar-1.0")}" + G + end + + it "evals each gemspec in the context of its parent directory" do + bundle :install + run "require 'bar'; puts BAR" + expect(out).to eq("1.0") + end + + it "error intelligently if the gemspec has a LoadError" do + ref = update_git "bar", gemspec: false do |s| + s.write "bar.gemspec", "require 'foobarbaz'" + end.ref_for("HEAD") + bundle :install, raise_on_error: false + + expect(err.lines.map(&:chomp)).to include( + a_string_starting_with("[!] There was an error while loading `bar.gemspec`:"), + " # from #{default_bundle_path "bundler", "gems", "bar-1.0-#{ref[0, 12]}", "bar.gemspec"}:1", + " > require 'foobarbaz'" + ) + end + + it "evals each gemspec with a binding from the top level" do + bundle "install" + + ruby <<-RUBY + require 'bundler' + bundler_module = class << Bundler; self; end + bundler_module.send(:remove_method, :require) + def Bundler.require(path) + raise StandardError, "didn't use binding from top level" + end + Bundler.load + RUBY + + expect(err).to be_empty + expect(out).to be_empty + end + end + + describe "when Bundler is bundled" do + it "doesn't blow up" do + install_gemfile <<-G + source "https://gem.repo1" + gem "bundler", :path => "#{root}" + G + + bundle %(exec ruby -e "require 'bundler'; Bundler.setup") + expect(err).to be_empty + end + end + + describe "when BUNDLED WITH" do + def lock_with(bundler_version = nil) + lock = <<~L + GEM + remote: https://gem.repo1/ + specs: + myrack (1.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + myrack + L + + if bundler_version + lock += "\nBUNDLED WITH\n #{bundler_version}\n" + end + + lock + end + + before do + bundle_config "path.system true" + + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + G + end + + context "is not present" do + it "does not change the lock" do + lockfile lock_with(nil) + ruby "require 'bundler/setup'" + expect(lockfile).to eq lock_with(nil) + end + end + + context "is newer" do + it "does not change the lock or warn" do + lockfile lock_with(Bundler::VERSION.succ) + ruby "require 'bundler/setup'" + expect(out).to be_empty + expect(err).to be_empty + expect(lockfile).to eq lock_with(Bundler::VERSION.succ) + end + end + + context "is older" do + it "does not change the lock" do + system_gems "bundler-1.10.1" + lockfile lock_with("1.10.1") + ruby "require 'bundler/setup'" + expect(lockfile).to eq lock_with("1.10.1") + end + end + end + + describe "when RUBY VERSION" do + let(:ruby_version) { nil } + + def lock_with(ruby_version = nil) + checksums = checksums_section do |c| + c.checksum gem_repo1, "myrack", "1.0.0" + end + + lock = <<~L + GEM + remote: https://gem.repo1/ + specs: + myrack (1.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + myrack + #{checksums} + L + + if ruby_version + lock += "\nRUBY VERSION\n ruby #{ruby_version}\n" + end + + lock += <<~L + + BUNDLED WITH + #{Bundler::VERSION} + L + + lock + end + + before do + install_gemfile <<-G + ruby ">= 0" + source "https://gem.repo1" + gem "myrack" + G + lockfile lock_with(ruby_version) + end + + context "is not present" do + # Skipped on ruby-core, and on the release-version CI variant, because + # `ruby "require 'bundler/setup'"` does not activate bundler as a gem + # there, so Source::Metadata falls back to a synthetic spec whose + # cache_file does not exist on disk and LockfileGenerator#bundler_checksum + # drops the bundler checksum, while the on-disk lockfile still has it. + # In-development (.dev) versions never write a bundler checksum, so the + # regular suite stays unaffected. + it "does not change the lock", :ruby_repo do + skip "bundler is loaded from the source tree, not installed as a gem" unless Bundler::VERSION.end_with?(".dev") + + expect { ruby "require 'bundler/setup'" }.not_to change { lockfile } + end + end + + context "is newer" do + let(:ruby_version) { "5.5.5" } + it "does not change the lock or warn" do + expect { ruby "require 'bundler/setup'" }.not_to change { lockfile } + expect(out).to be_empty + expect(err).to be_empty + end + end + + context "is older" do + let(:ruby_version) { "1.0.0" } + it "does not change the lock" do + expect { ruby "require 'bundler/setup'" }.not_to change { lockfile } + end + end + end + + describe "with gemified standard libraries" do + it "does not load Digest", :ruby_repo do + build_git "bar", gemspec: false do |s| + s.write "lib/bar/version.rb", %(BAR_VERSION = '1.0') + s.write "bar.gemspec", <<-G + require_relative 'lib/bar/version' + + Gem::Specification.new do |s| + s.name = 'bar' + s.version = BAR_VERSION + s.summary = 'Bar' + s.files = Dir["lib/**/*.rb"] + s.author = 'no one' + + s.add_dependency 'digest' + end + G + end + + gemfile <<-G + source "https://gem.repo1" + gem "bar", :git => "#{lib_path("bar-1.0")}" + G + + bundle :install, env: { "BUNDLE_LOCKFILE_CHECKSUMS" => "false" } + + ruby <<-RUBY, artifice: nil + require 'bundler/setup' + puts defined?(::Digest) ? "Digest defined" : "Digest undefined" + require 'digest' + RUBY + expect(out).to eq("Digest undefined") + end + + it "does not load Psych" do + gemfile "source 'https://gem.repo1'" + ruby <<-RUBY + require 'bundler/setup' + puts defined?(Psych::VERSION) ? Psych::VERSION : "undefined" + require 'psych' + puts Psych::VERSION + RUBY + pre_bundler, post_bundler = out.split("\n") + expect(pre_bundler).to eq("undefined") + expect(post_bundler).to match(/\d+\.\d+\.\d+/) + end + + it "does not load openssl" do + install_gemfile "source 'https://gem.repo1'" + ruby <<-RUBY, artifice: nil + require "bundler/setup" + puts defined?(OpenSSL) || "undefined" + require "openssl" + puts defined?(OpenSSL) || "undefined" + RUBY + expect(out).to eq("undefined\nconstant") + end + + it "does not load uri while reading gemspecs", rubygems: ">= 3.6.0.dev" do + Dir.mkdir bundled_app("test") + + create_file(bundled_app("test/test.gemspec"), <<-G) + Gem::Specification.new do |s| + s.name = "test" + s.version = "1.0.0" + s.summary = "test" + s.authors = ['John Doe'] + s.homepage = 'https://example.com' + end + G + + install_gemfile <<-G + source "https://gem.repo1" + gem "test", path: "#{bundled_app("test")}" + G + + ruby <<-RUBY, artifice: nil + require "bundler/setup" + puts defined?(URI) || "undefined" + require "uri" + puts defined?(URI) || "undefined" + RUBY + expect(out).to eq("undefined\nconstant") + end + + it "activates default gems when they are part of the bundle, but not installed explicitly", :ruby_repo do + default_delegate_version = ruby "gem 'delegate'; require 'delegate'; puts Delegator::VERSION" + + build_repo2 do + build_gem "delegate", default_delegate_version + end + + gemfile "source \"https://gem.repo2\"; gem 'delegate'" + + ruby <<-RUBY + require "bundler/setup" + require "delegate" + puts defined?(::Delegator) ? "Delegator defined" : "Delegator undefined" + RUBY + + expect(out).to eq("Delegator defined") + expect(err).to be_empty + end + + describe "default gem activation" do + let(:exemptions) do + exempts = %w[did_you_mean bundler uri pathname] + exempts << "error_highlight" # added in Ruby 3.1 as a default gem + exempts << "ruby2_keywords" # added in Ruby 3.1 as a default gem + exempts << "syntax_suggest" # added in Ruby 3.2 as a default gem + exempts + end + + let(:activation_warning_hack) { <<~RUBY } + require #{spec_dir.join("support/hax").to_s.dump} + + Gem::Specification.send(:alias_method, :bundler_spec_activate, :activate) + Gem::Specification.send(:define_method, :activate) do + unless #{exemptions.inspect}.include?(name) + warn '-' * 80 + warn "activating \#{full_name}" + warn(*caller) + warn '*' * 80 + end + bundler_spec_activate + end + RUBY + + let(:activation_warning_hack_rubyopt) do + create_file("activation_warning_hack.rb", activation_warning_hack) + "-r#{bundled_app("activation_warning_hack.rb")} #{ENV["RUBYOPT"]}" + end + + let(:code) { <<~RUBY } + require "pp" + loaded_specs = Gem.loaded_specs.dup + #{exemptions.inspect}.each {|s| loaded_specs.delete(s) } + pp loaded_specs + + # not a default gem, but harmful to have loaded + open_uri = $LOADED_FEATURES.grep(/open.uri/) + unless open_uri.empty? + warn "open_uri: \#{open_uri}" + end + RUBY + + it "activates no gems with -rbundler/setup" do + install_gemfile "source 'https://gem.repo1'" + ruby code, env: { "RUBYOPT" => activation_warning_hack_rubyopt + " -rbundler/setup" }, artifice: nil + expect(out).to eq("{}") + end + + it "activates no gems with bundle exec" do + install_gemfile "source 'https://gem.repo1'" + create_file("script.rb", code) + bundle "exec ruby ./script.rb", env: { "RUBYOPT" => activation_warning_hack_rubyopt } + expect(out).to eq("{}") + end + + it "activates no gems with bundle exec that is loaded" do + skip "not executable" if Gem.win_platform? + + install_gemfile "source 'https://gem.repo1'" + create_file("script.rb", "#!/usr/bin/env ruby\n\n#{code}") + FileUtils.chmod(0o777, bundled_app("script.rb")) + bundle "exec ./script.rb", env: { "RUBYOPT" => activation_warning_hack_rubyopt } + expect(out).to eq("{}") + end + + it "does not load net-http-pipeline too early" do + build_repo4 do + build_gem "net-http-pipeline", "1.0.1" + end + + system_gems "net-http-pipeline-1.0.1", gem_repo: gem_repo4 + + gemfile <<-G + source "https://gem.repo4" + gem "net-http-pipeline", "1.0.1" + G + + bundle_config "path vendor/bundle" + + bundle :install + + bundle :check + + expect(out).to eq("The Gemfile's dependencies are satisfied") + end + + Gem::Specification.select(&:default_gem?).map(&:name).each do |g| + it "activates newer versions of #{g}", :ruby_repo do + skip if exemptions.include?(g) + + build_repo4 do + build_gem g, "999999" + end + + install_gemfile <<-G + source "https://gem.repo4" + gem "#{g}", "999999" + G + + expect(the_bundle).to include_gem("#{g} 999999", env: { "RUBYOPT" => activation_warning_hack_rubyopt }, artifice: nil) + end + + it "activates older versions of #{g}", :ruby_repo do + skip if exemptions.include?(g) + + build_repo4 do + build_gem g, "0.0.0.a" + end + + install_gemfile <<-G + source "https://gem.repo4" + gem "#{g}", "0.0.0.a" + G + + expect(the_bundle).to include_gem("#{g} 0.0.0.a", env: { "RUBYOPT" => activation_warning_hack_rubyopt }, artifice: nil) + end + end + end + end + + describe "after setup" do + it "keeps Kernel#gem private" do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + G + + ruby <<-RUBY, raise_on_error: false + require "bundler/setup" + Object.new.gem "myrack" + puts "FAIL" + RUBY + + expect(stdboth).not_to include "FAIL" + expect(err).to match(/private method [`']gem'/) + end + + it "keeps Kernel#require private" do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + G + + ruby <<-RUBY, raise_on_error: false + require "bundler/setup" + Object.new.require "myrack" + puts "FAIL" + RUBY + + expect(stdboth).not_to include "FAIL" + expect(err).to match(/private method [`']require'/) + end + + it "memoizes initial set of specs when requiring bundler/setup, so that even if further code mutates dependencies, Bundler.definition.specs is not affected" do + install_gemfile <<~G + source "https://gem.repo1" + gem "yard" + gem "myrack", :group => :test + G + + ruby <<-RUBY, raise_on_error: false + require "bundler/setup" + Bundler.require(:test).select! {|d| (d.groups & [:test]).any? } + puts Bundler.definition.specs.map(&:name).join(", ") + RUBY + + expect(out).to include("myrack, yard") + end + + it "does not cause double loads when higher versions of default gems are activated before bundler" do + build_repo2 do + build_gem "json", "999.999.999" do |s| + s.write "lib/json.rb", <<~RUBY + module JSON + VERSION = "999.999.999" + end + RUBY + end + end + + system_gems "json-999.999.999", gem_repo: gem_repo2 + + install_gemfile "source 'https://gem.repo1'" + ruby <<-RUBY + require "json" + require "bundler/setup" + require "json" + RUBY + + expect(err).to be_empty + end + end + + it "does not undo the Kernel.require decorations", rubygems: ">= 3.4.6" do + install_gemfile "source 'https://gem.repo1'" + script = bundled_app("bin/script") + create_file(script, <<~RUBY) + module Kernel + module_function + + alias_method :require_before_extra_monkeypatches, :require + + def require(path) + puts "requiring \#{path} used the monkeypatch" + + require_before_extra_monkeypatches(path) + end + end + + require "bundler/setup" + + require "foo" + RUBY + + sys_exec "#{Gem.ruby} #{script}", raise_on_error: false + expect(out).to include("requiring foo used the monkeypatch") + end + + it "performs an automatic bundle install" do + build_repo4 do + build_gem "myrack", "1.0.0" + end + + gemfile <<-G + source "https://gem.repo1" + gem "myrack", :group => :test + G + + bundle_config "auto_install 1" + + ruby <<-RUBY, artifice: "compact_index" + require 'bundler/setup' + RUBY + expect(err).to be_empty + expect(out).to include("Installing myrack 1.0.0") + end + + context "in a read-only filesystem" do + before do + gemfile <<-G + source "https://gem.repo4" + G + + lockfile <<-L + GEM + remote: https://gem.repo4/ + + PLATFORMS + x86_64-darwin-19 + + DEPENDENCIES + + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "should fail loudly if the lockfile platforms don't include the current platform" do + simulate_platform "x86_64-linux" do + ruby <<-RUBY, raise_on_error: false, env: { "BUNDLER_SPEC_READ_ONLY" => "true", "BUNDLER_FORCE_TTY" => "true" } + require "bundler/setup" + RUBY + end + + expect(err).to include("Your lockfile is missing the current platform, but can't be updated because file system is read-only") + end + end +end diff --git a/spec/bundler/runtime/setup_spec.rb b/spec/bundler/runtime/setup_spec.rb index fc841f1de155ef..46b88513d47fd6 100644 --- a/spec/bundler/runtime/setup_spec.rb +++ b/spec/bundler/runtime/setup_spec.rb @@ -890,824 +890,4 @@ def clean_load_path(lp) expect(lines).to include("LS MANPAGE") end end - - it "should prepend gemspec require paths to $LOAD_PATH in order" do - update_repo2 do - build_gem("requirepaths") do |s| - s.write("lib/rq.rb", "puts 'yay'") - s.write("src/rq.rb", "puts 'nooo'") - s.require_paths = %w[lib src] - end - end - - install_gemfile <<-G - source "https://gem.repo2" - gem "requirepaths", :require => nil - G - - run "require 'rq'" - expect(out).to eq("yay") - end - - it "should clean $LOAD_PATH properly" do - gem_name = "very_simple_binary" - full_gem_name = gem_name + "-1.0" - - system_gems full_gem_name - - install_gemfile <<-G - source "https://gem.repo1" - G - - ruby <<-R - require 'bundler' - gem '#{gem_name}' - - puts $LOAD_PATH.count {|path| path =~ /#{gem_name}/} >= 2 - - Bundler.setup - - puts $LOAD_PATH.count {|path| path =~ /#{gem_name}/} == 0 - R - - expect(out).to eq("true\ntrue") - end - - context "with bundler is located in symlinked GEM_HOME" do - let(:gem_home) { Dir.mktmpdir } - let(:symlinked_gem_home) { tmp("gem_home-symlink").to_s } - let(:full_name) { "bundler-#{Bundler::VERSION}" } - - before do - File.symlink(gem_home, symlinked_gem_home) - gems_dir = File.join(gem_home, "gems") - specifications_dir = File.join(gem_home, "specifications") - Dir.mkdir(gems_dir) - Dir.mkdir(specifications_dir) - - File.symlink(source_root, File.join(gems_dir, full_name)) - - gemspec_content = File.binread(gemspec). - sub("Bundler::VERSION", %("#{Bundler::VERSION}")). - lines.reject {|line| line.include?("lib/bundler/version") }.join - - File.open(File.join(specifications_dir, "#{full_name}.gemspec"), "wb") do |f| - f.write(gemspec_content) - end - end - - it "should not remove itself from the LOAD_PATH and require a different copy of 'bundler/setup'" do - install_gemfile "source 'https://gem.repo1'" - - ruby <<-R, env: { "GEM_PATH" => symlinked_gem_home } - TracePoint.trace(:class) do |tp| - if tp.path.include?("bundler") && !tp.path.start_with?("#{source_root}") - puts "OMG. Defining a class from another bundler at \#{tp.path}:\#{tp.lineno}" - end - end - gem 'bundler', '#{Bundler::VERSION}' - require 'bundler/setup' - R - - expect(out).to be_empty - end - end - - it "does not reveal system gems even when Gem.refresh is called" do - system_gems "myrack-1.0.0" - - install_gemfile <<-G - source "https://gem.repo1" - gem "activesupport" - G - - run <<-R - puts Bundler.rubygems.installed_specs.map(&:name) - Gem.refresh - puts Bundler.rubygems.installed_specs.map(&:name) - R - - expect(out).to include("activesupport") - expect(out).not_to include("myrack") - end - - describe "when a vendored gem specification uses the :path option" do - let(:filesystem_root) do - current = Pathname.new(Dir.pwd) - current = current.parent until current == current.parent - current - end - - it "should resolve paths relative to the Gemfile" do - path = bundled_app(File.join("vendor", "foo")) - build_lib "foo", path: path - - # If the .gemspec exists, then Bundler handles the path differently. - # See Source::Path.load_spec_files for details. - FileUtils.rm(File.join(path, "foo.gemspec")) - - install_gemfile <<-G - source "https://gem.repo1" - gem 'foo', '1.2.3', :path => 'vendor/foo' - G - - run <<-R, env: { "BUNDLE_GEMFILE" => bundled_app_gemfile.to_s }, dir: bundled_app.parent - require 'foo' - R - expect(err).to be_empty - end - - it "should make sure the Bundler.root is really included in the path relative to the Gemfile" do - relative_path = File.join("vendor", Dir.pwd.gsub(/^#{filesystem_root}/, "")) - absolute_path = bundled_app(relative_path) - FileUtils.mkdir_p(absolute_path) - build_lib "foo", path: absolute_path - - # If the .gemspec exists, then Bundler handles the path differently. - # See Source::Path.load_spec_files for details. - FileUtils.rm(File.join(absolute_path, "foo.gemspec")) - - gemfile <<-G - source "https://gem.repo1" - gem 'foo', '1.2.3', :path => '#{relative_path}' - G - - bundle :install - - run <<-R, env: { "BUNDLE_GEMFILE" => bundled_app_gemfile.to_s }, dir: bundled_app.parent - require 'foo' - R - - expect(err).to be_empty - end - end - - describe "with git gems that don't have gemspecs" do - before :each do - build_git "no_gemspec", gemspec: false - - install_gemfile <<-G - source "https://gem.repo1" - gem "no_gemspec", "1.0", :git => "#{lib_path("no_gemspec-1.0")}" - G - end - - it "loads the library via a virtual spec" do - run <<-R - require 'no_gemspec' - puts NO_GEMSPEC - R - - expect(out).to eq("1.0") - end - end - - describe "with bundled and system gems" do - before :each do - system_gems "myrack-1.0.0" - - install_gemfile <<-G - source "https://gem.repo1" - - gem "activesupport", "2.3.5" - G - end - - it "does not pull in system gems" do - run <<-R - begin; - require 'myrack' - rescue LoadError - puts 'WIN' - end - R - - expect(out).to eq("WIN") - end - - it "provides a gem method" do - run <<-R - gem 'activesupport' - require 'activesupport' - puts ACTIVESUPPORT - R - - expect(out).to eq("2.3.5") - end - - it "raises an exception if gem is used to invoke a system gem not in the bundle" do - run <<-R - begin - gem 'myrack' - rescue LoadError => e - puts e.message - end - R - - expect(out).to eq("myrack is not part of the bundle. Add it to your Gemfile.") - end - - it "sets GEM_HOME appropriately" do - run "puts ENV['GEM_HOME']" - expect(out).to eq(default_bundle_path.to_s) - end - end - - describe "with system gems in the bundle" do - before :each do - bundle_config "path.system true" - system_gems "myrack-1.0.0" - - install_gemfile <<-G - source "https://gem.repo1" - gem "myrack", "1.0.0" - gem "activesupport", "2.3.5" - G - end - - it "sets GEM_PATH appropriately" do - run "puts Gem.path" - paths = out.split("\n") - expect(paths).to include(system_gem_path.to_s) - end - end - - describe "with a gemspec that requires other files" do - before :each do - build_git "bar", gemspec: false do |s| - s.write "lib/bar/version.rb", %(BAR_VERSION = '1.0') - s.write "bar.gemspec", <<-G - require_relative 'lib/bar/version' - - Gem::Specification.new do |s| - s.name = 'bar' - s.version = BAR_VERSION - s.summary = 'Bar' - s.files = Dir["lib/**/*.rb"] - s.author = 'no one' - end - G - end - - gemfile <<-G - source "https://gem.repo1" - gem "bar", :git => "#{lib_path("bar-1.0")}" - G - end - - it "evals each gemspec in the context of its parent directory" do - bundle :install - run "require 'bar'; puts BAR" - expect(out).to eq("1.0") - end - - it "error intelligently if the gemspec has a LoadError" do - ref = update_git "bar", gemspec: false do |s| - s.write "bar.gemspec", "require 'foobarbaz'" - end.ref_for("HEAD") - bundle :install, raise_on_error: false - - expect(err.lines.map(&:chomp)).to include( - a_string_starting_with("[!] There was an error while loading `bar.gemspec`:"), - " # from #{default_bundle_path "bundler", "gems", "bar-1.0-#{ref[0, 12]}", "bar.gemspec"}:1", - " > require 'foobarbaz'" - ) - end - - it "evals each gemspec with a binding from the top level" do - bundle "install" - - ruby <<-RUBY - require 'bundler' - bundler_module = class << Bundler; self; end - bundler_module.send(:remove_method, :require) - def Bundler.require(path) - raise StandardError, "didn't use binding from top level" - end - Bundler.load - RUBY - - expect(err).to be_empty - expect(out).to be_empty - end - end - - describe "when Bundler is bundled" do - it "doesn't blow up" do - install_gemfile <<-G - source "https://gem.repo1" - gem "bundler", :path => "#{root}" - G - - bundle %(exec ruby -e "require 'bundler'; Bundler.setup") - expect(err).to be_empty - end - end - - describe "when BUNDLED WITH" do - def lock_with(bundler_version = nil) - lock = <<~L - GEM - remote: https://gem.repo1/ - specs: - myrack (1.0.0) - - PLATFORMS - #{lockfile_platforms} - - DEPENDENCIES - myrack - L - - if bundler_version - lock += "\nBUNDLED WITH\n #{bundler_version}\n" - end - - lock - end - - before do - bundle_config "path.system true" - - install_gemfile <<-G - source "https://gem.repo1" - gem "myrack" - G - end - - context "is not present" do - it "does not change the lock" do - lockfile lock_with(nil) - ruby "require 'bundler/setup'" - expect(lockfile).to eq lock_with(nil) - end - end - - context "is newer" do - it "does not change the lock or warn" do - lockfile lock_with(Bundler::VERSION.succ) - ruby "require 'bundler/setup'" - expect(out).to be_empty - expect(err).to be_empty - expect(lockfile).to eq lock_with(Bundler::VERSION.succ) - end - end - - context "is older" do - it "does not change the lock" do - system_gems "bundler-1.10.1" - lockfile lock_with("1.10.1") - ruby "require 'bundler/setup'" - expect(lockfile).to eq lock_with("1.10.1") - end - end - end - - describe "when RUBY VERSION" do - let(:ruby_version) { nil } - - def lock_with(ruby_version = nil) - checksums = checksums_section do |c| - c.checksum gem_repo1, "myrack", "1.0.0" - end - - lock = <<~L - GEM - remote: https://gem.repo1/ - specs: - myrack (1.0.0) - - PLATFORMS - #{lockfile_platforms} - - DEPENDENCIES - myrack - #{checksums} - L - - if ruby_version - lock += "\nRUBY VERSION\n ruby #{ruby_version}\n" - end - - lock += <<~L - - BUNDLED WITH - #{Bundler::VERSION} - L - - lock - end - - before do - install_gemfile <<-G - ruby ">= 0" - source "https://gem.repo1" - gem "myrack" - G - lockfile lock_with(ruby_version) - end - - context "is not present" do - # Skipped on ruby-core, and on the release-version CI variant, because - # `ruby "require 'bundler/setup'"` does not activate bundler as a gem - # there, so Source::Metadata falls back to a synthetic spec whose - # cache_file does not exist on disk and LockfileGenerator#bundler_checksum - # drops the bundler checksum, while the on-disk lockfile still has it. - # In-development (.dev) versions never write a bundler checksum, so the - # regular suite stays unaffected. - it "does not change the lock", :ruby_repo do - skip "bundler is loaded from the source tree, not installed as a gem" unless Bundler::VERSION.end_with?(".dev") - - expect { ruby "require 'bundler/setup'" }.not_to change { lockfile } - end - end - - context "is newer" do - let(:ruby_version) { "5.5.5" } - it "does not change the lock or warn" do - expect { ruby "require 'bundler/setup'" }.not_to change { lockfile } - expect(out).to be_empty - expect(err).to be_empty - end - end - - context "is older" do - let(:ruby_version) { "1.0.0" } - it "does not change the lock" do - expect { ruby "require 'bundler/setup'" }.not_to change { lockfile } - end - end - end - - describe "with gemified standard libraries" do - it "does not load Digest", :ruby_repo do - build_git "bar", gemspec: false do |s| - s.write "lib/bar/version.rb", %(BAR_VERSION = '1.0') - s.write "bar.gemspec", <<-G - require_relative 'lib/bar/version' - - Gem::Specification.new do |s| - s.name = 'bar' - s.version = BAR_VERSION - s.summary = 'Bar' - s.files = Dir["lib/**/*.rb"] - s.author = 'no one' - - s.add_dependency 'digest' - end - G - end - - gemfile <<-G - source "https://gem.repo1" - gem "bar", :git => "#{lib_path("bar-1.0")}" - G - - bundle :install, env: { "BUNDLE_LOCKFILE_CHECKSUMS" => "false" } - - ruby <<-RUBY, artifice: nil - require 'bundler/setup' - puts defined?(::Digest) ? "Digest defined" : "Digest undefined" - require 'digest' - RUBY - expect(out).to eq("Digest undefined") - end - - it "does not load Psych" do - gemfile "source 'https://gem.repo1'" - ruby <<-RUBY - require 'bundler/setup' - puts defined?(Psych::VERSION) ? Psych::VERSION : "undefined" - require 'psych' - puts Psych::VERSION - RUBY - pre_bundler, post_bundler = out.split("\n") - expect(pre_bundler).to eq("undefined") - expect(post_bundler).to match(/\d+\.\d+\.\d+/) - end - - it "does not load openssl" do - install_gemfile "source 'https://gem.repo1'" - ruby <<-RUBY, artifice: nil - require "bundler/setup" - puts defined?(OpenSSL) || "undefined" - require "openssl" - puts defined?(OpenSSL) || "undefined" - RUBY - expect(out).to eq("undefined\nconstant") - end - - it "does not load uri while reading gemspecs", rubygems: ">= 3.6.0.dev" do - Dir.mkdir bundled_app("test") - - create_file(bundled_app("test/test.gemspec"), <<-G) - Gem::Specification.new do |s| - s.name = "test" - s.version = "1.0.0" - s.summary = "test" - s.authors = ['John Doe'] - s.homepage = 'https://example.com' - end - G - - install_gemfile <<-G - source "https://gem.repo1" - gem "test", path: "#{bundled_app("test")}" - G - - ruby <<-RUBY, artifice: nil - require "bundler/setup" - puts defined?(URI) || "undefined" - require "uri" - puts defined?(URI) || "undefined" - RUBY - expect(out).to eq("undefined\nconstant") - end - - it "activates default gems when they are part of the bundle, but not installed explicitly", :ruby_repo do - default_delegate_version = ruby "gem 'delegate'; require 'delegate'; puts Delegator::VERSION" - - build_repo2 do - build_gem "delegate", default_delegate_version - end - - gemfile "source \"https://gem.repo2\"; gem 'delegate'" - - ruby <<-RUBY - require "bundler/setup" - require "delegate" - puts defined?(::Delegator) ? "Delegator defined" : "Delegator undefined" - RUBY - - expect(out).to eq("Delegator defined") - expect(err).to be_empty - end - - describe "default gem activation" do - let(:exemptions) do - exempts = %w[did_you_mean bundler uri pathname] - exempts << "error_highlight" # added in Ruby 3.1 as a default gem - exempts << "ruby2_keywords" # added in Ruby 3.1 as a default gem - exempts << "syntax_suggest" # added in Ruby 3.2 as a default gem - exempts - end - - let(:activation_warning_hack) { <<~RUBY } - require #{spec_dir.join("support/hax").to_s.dump} - - Gem::Specification.send(:alias_method, :bundler_spec_activate, :activate) - Gem::Specification.send(:define_method, :activate) do - unless #{exemptions.inspect}.include?(name) - warn '-' * 80 - warn "activating \#{full_name}" - warn(*caller) - warn '*' * 80 - end - bundler_spec_activate - end - RUBY - - let(:activation_warning_hack_rubyopt) do - create_file("activation_warning_hack.rb", activation_warning_hack) - "-r#{bundled_app("activation_warning_hack.rb")} #{ENV["RUBYOPT"]}" - end - - let(:code) { <<~RUBY } - require "pp" - loaded_specs = Gem.loaded_specs.dup - #{exemptions.inspect}.each {|s| loaded_specs.delete(s) } - pp loaded_specs - - # not a default gem, but harmful to have loaded - open_uri = $LOADED_FEATURES.grep(/open.uri/) - unless open_uri.empty? - warn "open_uri: \#{open_uri}" - end - RUBY - - it "activates no gems with -rbundler/setup" do - install_gemfile "source 'https://gem.repo1'" - ruby code, env: { "RUBYOPT" => activation_warning_hack_rubyopt + " -rbundler/setup" }, artifice: nil - expect(out).to eq("{}") - end - - it "activates no gems with bundle exec" do - install_gemfile "source 'https://gem.repo1'" - create_file("script.rb", code) - bundle "exec ruby ./script.rb", env: { "RUBYOPT" => activation_warning_hack_rubyopt } - expect(out).to eq("{}") - end - - it "activates no gems with bundle exec that is loaded" do - skip "not executable" if Gem.win_platform? - - install_gemfile "source 'https://gem.repo1'" - create_file("script.rb", "#!/usr/bin/env ruby\n\n#{code}") - FileUtils.chmod(0o777, bundled_app("script.rb")) - bundle "exec ./script.rb", env: { "RUBYOPT" => activation_warning_hack_rubyopt } - expect(out).to eq("{}") - end - - it "does not load net-http-pipeline too early" do - build_repo4 do - build_gem "net-http-pipeline", "1.0.1" - end - - system_gems "net-http-pipeline-1.0.1", gem_repo: gem_repo4 - - gemfile <<-G - source "https://gem.repo4" - gem "net-http-pipeline", "1.0.1" - G - - bundle_config "path vendor/bundle" - - bundle :install - - bundle :check - - expect(out).to eq("The Gemfile's dependencies are satisfied") - end - - Gem::Specification.select(&:default_gem?).map(&:name).each do |g| - it "activates newer versions of #{g}", :ruby_repo do - skip if exemptions.include?(g) - - build_repo4 do - build_gem g, "999999" - end - - install_gemfile <<-G - source "https://gem.repo4" - gem "#{g}", "999999" - G - - expect(the_bundle).to include_gem("#{g} 999999", env: { "RUBYOPT" => activation_warning_hack_rubyopt }, artifice: nil) - end - - it "activates older versions of #{g}", :ruby_repo do - skip if exemptions.include?(g) - - build_repo4 do - build_gem g, "0.0.0.a" - end - - install_gemfile <<-G - source "https://gem.repo4" - gem "#{g}", "0.0.0.a" - G - - expect(the_bundle).to include_gem("#{g} 0.0.0.a", env: { "RUBYOPT" => activation_warning_hack_rubyopt }, artifice: nil) - end - end - end - end - - describe "after setup" do - it "keeps Kernel#gem private" do - install_gemfile <<-G - source "https://gem.repo1" - gem "myrack" - G - - ruby <<-RUBY, raise_on_error: false - require "bundler/setup" - Object.new.gem "myrack" - puts "FAIL" - RUBY - - expect(stdboth).not_to include "FAIL" - expect(err).to match(/private method [`']gem'/) - end - - it "keeps Kernel#require private" do - install_gemfile <<-G - source "https://gem.repo1" - gem "myrack" - G - - ruby <<-RUBY, raise_on_error: false - require "bundler/setup" - Object.new.require "myrack" - puts "FAIL" - RUBY - - expect(stdboth).not_to include "FAIL" - expect(err).to match(/private method [`']require'/) - end - - it "memoizes initial set of specs when requiring bundler/setup, so that even if further code mutates dependencies, Bundler.definition.specs is not affected" do - install_gemfile <<~G - source "https://gem.repo1" - gem "yard" - gem "myrack", :group => :test - G - - ruby <<-RUBY, raise_on_error: false - require "bundler/setup" - Bundler.require(:test).select! {|d| (d.groups & [:test]).any? } - puts Bundler.definition.specs.map(&:name).join(", ") - RUBY - - expect(out).to include("myrack, yard") - end - - it "does not cause double loads when higher versions of default gems are activated before bundler" do - build_repo2 do - build_gem "json", "999.999.999" do |s| - s.write "lib/json.rb", <<~RUBY - module JSON - VERSION = "999.999.999" - end - RUBY - end - end - - system_gems "json-999.999.999", gem_repo: gem_repo2 - - install_gemfile "source 'https://gem.repo1'" - ruby <<-RUBY - require "json" - require "bundler/setup" - require "json" - RUBY - - expect(err).to be_empty - end - end - - it "does not undo the Kernel.require decorations", rubygems: ">= 3.4.6" do - install_gemfile "source 'https://gem.repo1'" - script = bundled_app("bin/script") - create_file(script, <<~RUBY) - module Kernel - module_function - - alias_method :require_before_extra_monkeypatches, :require - - def require(path) - puts "requiring \#{path} used the monkeypatch" - - require_before_extra_monkeypatches(path) - end - end - - require "bundler/setup" - - require "foo" - RUBY - - sys_exec "#{Gem.ruby} #{script}", raise_on_error: false - expect(out).to include("requiring foo used the monkeypatch") - end - - it "performs an automatic bundle install" do - build_repo4 do - build_gem "myrack", "1.0.0" - end - - gemfile <<-G - source "https://gem.repo1" - gem "myrack", :group => :test - G - - bundle_config "auto_install 1" - - ruby <<-RUBY, artifice: "compact_index" - require 'bundler/setup' - RUBY - expect(err).to be_empty - expect(out).to include("Installing myrack 1.0.0") - end - - context "in a read-only filesystem" do - before do - gemfile <<-G - source "https://gem.repo4" - G - - lockfile <<-L - GEM - remote: https://gem.repo4/ - - PLATFORMS - x86_64-darwin-19 - - DEPENDENCIES - - BUNDLED WITH - #{Bundler::VERSION} - L - end - - it "should fail loudly if the lockfile platforms don't include the current platform" do - simulate_platform "x86_64-linux" do - ruby <<-RUBY, raise_on_error: false, env: { "BUNDLER_SPEC_READ_ONLY" => "true", "BUNDLER_FORCE_TTY" => "true" } - require "bundler/setup" - RUBY - end - - expect(err).to include("Your lockfile is missing the current platform, but can't be updated because file system is read-only") - end - end end diff --git a/spec/bundler/support/shards.rb b/spec/bundler/support/shards.rb index 5718e775b23a9b..396ee8049d6b66 100644 --- a/spec/bundler/support/shards.rb +++ b/spec/bundler/support/shards.rb @@ -10,6 +10,7 @@ module Shards EXAMPLE_MAPPINGS = { shard_a: [ "spec/runtime/setup_spec.rb", + "spec/runtime/setup_gems_spec.rb", "spec/commands/install_spec.rb", "spec/commands/add_spec.rb", "spec/install/gems/compact_index_spec.rb", @@ -57,6 +58,8 @@ module Shards ], shard_b: [ "spec/install/gemfile/git_spec.rb", + "spec/install/gemfile/git_overrides_spec.rb", + "spec/install/gemfile/git_extensions_spec.rb", "spec/install/gems/standalone_spec.rb", "spec/commands/lock_spec.rb", "spec/cache/gems_spec.rb", @@ -100,6 +103,7 @@ module Shards ], shard_c: [ "spec/commands/newgem_spec.rb", + "spec/commands/newgem_options_spec.rb", "spec/commands/exec_spec.rb", "spec/commands/clean_spec.rb", "spec/commands/platform_spec.rb", @@ -147,7 +151,9 @@ module Shards "spec/bundler/resolver/cooldown_spec.rb", "spec/install/cooldown_spec.rb", "spec/commands/outdated_spec.rb", + "spec/commands/outdated_filters_spec.rb", "spec/commands/update_spec.rb", + "spec/commands/update_scenarios_spec.rb", "spec/install/deploy_spec.rb", "spec/install/gemfile/sources_spec.rb", "spec/runtime/self_management_spec.rb", @@ -195,6 +201,7 @@ module Shards "spec/install/gemfile/override_spec.rb", "spec/install/path_spec.rb", "spec/bundler/fetcher/gem_remote_fetcher_local_ssl_server_spec.rb", + "spec/bundler/plugin/unloaded_source_spec.rb", ], }.freeze end diff --git a/spec/ruby/core/proc/fixtures/refined.rb b/spec/ruby/core/proc/fixtures/refined.rb new file mode 100644 index 00000000000000..014e8695fdd633 --- /dev/null +++ b/spec/ruby/core/proc/fixtures/refined.rb @@ -0,0 +1,24 @@ +module ProcRefinedSpecs + module StringShout + refine String do + def shout + upcase + "!" + end + end + end + + # Refines the same String#shout as StringShout, plus its own #quiet, so + # specs can observe both which module wins for a conflicting method and + # that all given modules are activated. + module StringQuiet + refine String do + def shout + downcase + end + + def quiet + "..." + end + end + end +end diff --git a/spec/ruby/core/proc/refined_spec.rb b/spec/ruby/core/proc/refined_spec.rb new file mode 100644 index 00000000000000..8e232e52b1c3c7 --- /dev/null +++ b/spec/ruby/core/proc/refined_spec.rb @@ -0,0 +1,115 @@ +require_relative '../../spec_helper' +require_relative 'fixtures/refined' + +ruby_version_is "4.1" do + describe "Proc#refined" do + it "returns a new Proc with the refinements of the given module activated inside its body" do + pr = -> s { s.shout } + refined = pr.refined(ProcRefinedSpecs::StringShout) + refined.should be_an_instance_of(Proc) + refined.should_not equal(pr) + refined.call("hi").should == "HI!" + end + + it "does not change the receiver" do + pr = -> s { s.shout } + pr.refined(ProcRefinedSpecs::StringShout) + -> { pr.call("hi") }.should.raise(NoMethodError) + end + + it "activates the refinements of all the given modules" do + pr = -> s { [s.shout, s.quiet] } + refined = pr.refined(ProcRefinedSpecs::StringShout, ProcRefinedSpecs::StringQuiet) + refined.call("Hi").should == ["hi", "..."] + end + + it "gives precedence to the module applied last for the same refined method, as with nested using" do + pr = -> s { s.shout } + pr.refined(ProcRefinedSpecs::StringShout, ProcRefinedSpecs::StringQuiet).call("Hi").should == "hi" + pr.refined(ProcRefinedSpecs::StringQuiet, ProcRefinedSpecs::StringShout).call("Hi").should == "HI!" + end + + it "shares the closure environment with the receiver" do + counter = 0 + pr = -> { counter += 1 } + refined = pr.refined(ProcRefinedSpecs::StringShout) + refined.call + pr.call + counter.should == 2 + end + + it "preserves lambda-ness and arity" do + l = -> s { s }.refined(ProcRefinedSpecs::StringShout) + l.should.lambda? + l.arity.should == 1 + + pr = proc { |s| s }.refined(ProcRefinedSpecs::StringShout) + pr.should_not.lambda? + end + + it "keeps the refinements active in blocks nested inside the body" do + pr = -> a { a.map { |s| s.shout } } + pr.refined(ProcRefinedSpecs::StringShout).call(%w[a b]).should == ["A!", "B!"] + end + + it "keeps the refinements active in methods defined inside the body" do + pr = -> { + obj = Object.new + def obj.shout_hi + "hi".shout + end + obj.shout_hi + } + pr.refined(ProcRefinedSpecs::StringShout).call.should == "HI!" + end + + it "keeps the refinements active when called via instance_eval, instance_exec and class_eval" do + pr = proc { "hi".shout } + refined = pr.refined(ProcRefinedSpecs::StringShout) + Object.new.instance_eval(&refined).should == "HI!" + Object.new.instance_exec(&refined).should == "HI!" + Class.new.class_eval(&refined).should == "HI!" + end + + it "raises ArgumentError when called with no modules" do + -> { -> {}.refined }.should.raise(ArgumentError) + end + + it "raises TypeError when called with a non-Module argument" do + -> { -> {}.refined(42) }.should.raise(TypeError) + -> { -> {}.refined(String) }.should.raise(TypeError) + end + + it "raises ArgumentError for a Proc not created from a Ruby block" do + -> { :upcase.to_proc.refined(ProcRefinedSpecs::StringShout) }.should.raise(ArgumentError) + method_proc = "hi".method(:upcase).to_proc + -> { method_proc.refined(ProcRefinedSpecs::StringShout) }.should.raise(ArgumentError) + end + + it "raises ArgumentError for a Proc that already has refinements applied" do + refined = -> s { s.shout }.refined(ProcRefinedSpecs::StringShout) + -> { refined.refined(ProcRefinedSpecs::StringQuiet) }.should.raise(ArgumentError) + end + + it "keeps the refinements on dup and clone" do + refined = -> s { s.shout }.refined(ProcRefinedSpecs::StringShout) + refined.dup.call("hi").should == "HI!" + refined.clone.call("hi").should == "HI!" + -> { refined.dup.refined(ProcRefinedSpecs::StringQuiet) }.should.raise(ArgumentError) + -> { refined.clone.refined(ProcRefinedSpecs::StringQuiet) }.should.raise(ArgumentError) + end + + it "raises ArgumentError when the result is passed to define_method" do + refined = -> s { s.shout }.refined(ProcRefinedSpecs::StringShout) + -> { Class.new { define_method(:m, refined) } }.should.raise(ArgumentError) + -> { Object.new.define_singleton_method(:m, refined) }.should.raise(ArgumentError) + end + + it "raises RuntimeError when the body calls using" do + mod = Module.new { refine(String) { def whisper; downcase; end } } + pr = proc { using mod } + refined = pr.refined(ProcRefinedSpecs::StringShout) + -> { Module.new.module_eval(&refined) }.should.raise(RuntimeError) + end + end +end diff --git a/string.c b/string.c index 322e43eb2fd64a..a503a2d505e436 100644 --- a/string.c +++ b/string.c @@ -4763,19 +4763,6 @@ rb_str_byteindex_m(int argc, VALUE *argv, VALUE str) return Qnil; } -#ifndef HAVE_MEMRCHR -static void* -memrchr(const char *search_str, int chr, long search_len) -{ - const char *ptr = search_str + search_len; - while (ptr > search_str) { - if ((unsigned char)*(--ptr) == chr) return (void *)ptr; - } - - return ((void *)0); -} -#endif - static long str_rindex(VALUE str, VALUE sub, const char *s, rb_encoding *enc) { diff --git a/struct.c b/struct.c index 606208e87268e5..32da26860b993a 100644 --- a/struct.c +++ b/struct.c @@ -785,8 +785,8 @@ rb_struct_initialize_m(int argc, const VALUE *argv, VALUE self) rb_mem_clear((VALUE *)RSTRUCT_CONST_PTR(self), n); rb_hash_foreach(argv[0], struct_hash_set_i, (VALUE)&arg); if (UNLIKELY(!NIL_P(arg.unknown_keywords))) { - rb_raise(rb_eArgError, "unknown keywords: %s", - RSTRING_PTR(rb_ary_join(arg.unknown_keywords, rb_str_new2(", ")))); + rb_raise(rb_eArgError, "unknown keywords: %"PRIsVALUE, + rb_ary_join(arg.unknown_keywords, rb_str_new2(", "))); } } else { diff --git a/test/ruby/test_gc_compact.rb b/test/ruby/test_gc_compact.rb index cb5e9d6ccb4b12..4d735bb26371e2 100644 --- a/test/ruby/test_gc_compact.rb +++ b/test/ruby/test_gc_compact.rb @@ -485,4 +485,24 @@ def test_moving_complex_generic_ivar assert_equal("hello", obj.instance_variable_get(:@str)) RUBY end + + def test_object_reembedding + assert_separately([], <<~'RUBY') + GC.auto_compact = true + + objs = [] + 30.times.map { Class.new }.map do |k| + 50.times.each do + obj = k.new + rand(0..30).times do |i| + obj.instance_variable_set(:"@v#{i}", i) + end + objs << obj + end + end + + GC.verify_compaction_references(expand_heap: true, toward: :empty) + assert :ok + RUBY + end end diff --git a/test/ruby/test_object_id.rb b/test/ruby/test_object_id.rb index 034674e5be1a47..acbd96572e175d 100644 --- a/test/ruby/test_object_id.rb +++ b/test/ruby/test_object_id.rb @@ -2,8 +2,10 @@ require "securerandom" class TestObjectId < Test::Unit::TestCase + SomeClass = Class.new + def setup - @obj = Object.new + @obj = SomeClass.new end def test_dup_new_id diff --git a/test/ruby/test_proc.rb b/test/ruby/test_proc.rb index eefd5b189b8943..1caf8a41032775 100644 --- a/test/ruby/test_proc.rb +++ b/test/ruby/test_proc.rb @@ -500,6 +500,615 @@ def test_dup_ifunc_proc_bug_20950 RUBY end + module RefinementsModule + refine String do + def shout = upcase + "!" + end + refine Integer do + def tripled = self * 3 + end + end + + def test_refined + orig = ->(s) { s.shout } + refined = orig.refined(RefinementsModule) + assert_equal("HI!", refined.call("hi")) + # the original Proc is unaffected + assert_raise(NoMethodError) { orig.call("hi") } + # idempotent: calling repeatedly keeps using the refinement + assert_equal("HI!", refined.call("hi")) + end + + def test_refined_nested_block + orig = ->(a) { a.map { |s| s.shout } } + refined = orig.refined(RefinementsModule) + assert_equal(["A!", "B!"], refined.call(["a", "b"])) + assert_raise(NoMethodError) { orig.call(["a"]) } + end + + def test_refined_multiple_modules + refined = ->(s, n) { "#{s.shout}#{n.tripled}" }.refined(RefinementsModule) + assert_equal("A!6", refined.call("a", 2)) + end + + def test_refined_shares_environment + counter = 0 + inc = -> { counter += 1 } + refined = inc.refined(RefinementsModule) + refined.call + inc.call + # the closure environment is shared with the original Proc + assert_equal(2, counter) + end + + def test_refined_via_yield + refined = ->(s) { s.shout }.refined(RefinementsModule) + result = [].tap {|a| [1, 2].each {|i| a << refined.call("x#{i}") } } + assert_equal(["X1!", "X2!"], result) + + forwarded = [] + rr = ->(s) { forwarded << s.shout }.refined(RefinementsModule) + %w[p q].each(&rr) + assert_equal(["P!", "Q!"], forwarded) + end + + def test_refined_via_c_call_paths + refined = ->(s) { s.shout }.refined(RefinementsModule) + # These reach the proc via the C invocation path (rb_vm_invoke_proc) rather + # than the optimized opt_call, so the refinement cref must be carried there. + assert_equal("A!", refined.send(:call, "a")) + assert_equal("B!", refined.method(:call).call("b")) + assert_equal("C!", Fiber.new(&refined).resume("c")) + assert_equal("D!", Thread.new("d", &refined).value) + end + + def test_refined_instance_eval + refined = proc { self.shout }.refined(RefinementsModule) + assert_equal("HI!", "hi".instance_eval(&refined)) + assert_equal("HI!", "hi".instance_exec(&refined)) + # the original Proc is still unaffected via instance_eval + orig = proc { self.shout } + assert_raise(NoMethodError) { "hi".instance_eval(&orig) } + end + + def test_refined_module_eval + refined = proc { "ok".shout }.refined(RefinementsModule) + klass = Class.new + assert_equal("OK!", klass.class_eval(&refined)) + end + + def test_refined_instance_eval_does_not_leak_refinements + # instance_eval/class_eval gets its own copy of the refinements hash, so a + # `using` inside must not mutate the cref shared by every derived proc. + refined = proc { "ok".shout }.refined(RefinementsModule) + Class.new.class_eval(&refined) + # a second proc derived from the same source iseq still sees the refinement + again = proc { "ok".shout }.refined(RefinementsModule) + assert_equal("OK!", Class.new.class_eval(&again)) + end + + def test_refined_once_regexp + # A /o (once) regexp literal interpolating a refined-method call must be + # built under the refinement, and the copy's once cache is independent of + # the source iseq's (which may be mid-flight or already completed). + refined = ->(s) { /\A#{s.shout}\z/o }.refined(RefinementsModule) + r1 = refined.call("ab") + assert_equal('\AAB!\z', r1.source) + # /o caches the first regexp on the copy's own once entry + assert_same(r1, refined.call("zz")) + # the original proc has no refinement, so building the regexp raises + assert_raise(NoMethodError) { ->(s) { /\A#{s.shout}\z/o }.call("ab") } + end + + def test_refined_preserved_by_dup + refined = ->(s) { s.shout }.refined(RefinementsModule) + # dup/clone copy the refinement cref (a hidden ivar) with the other ivars + assert_equal("Z!", refined.dup.call("z")) + assert_equal("Z!", refined.clone.call("z")) + end + + def test_refined_errors + assert_raise(ArgumentError) { ->(s) { s }.refined } + assert_raise(TypeError) { ->(s) { s }.refined(42) } + # non-iseq Procs are not supported + assert_raise(ArgumentError) { :upcase.to_proc.refined(RefinementsModule) } + assert_raise(ArgumentError) { method(:p).to_proc.refined(RefinementsModule) } + end + + def test_refined_non_main_ractor + assert_separately([], <<~'RUBY') + Warning[:experimental] = false + module M1; refine(String) { def shout = upcase + "!" }; end + module M2; refine(String) { def shout = downcase }; end + ractors = 10.times.map { |i| + Ractor.new(i) { |i| + ->(s) { s.shout }.refined(i.even? ? M1 : M2).call("Hi") + } + } + values = ractors.map(&:value) + assert_equal(["HI!", "hi"] * 5, values) + RUBY + end + + def test_refined_shareable_proc_across_ractors + assert_separately([], <<~'RUBY') + Warning[:experimental] = false + module RefMod; refine(String) { def shout = upcase + "!" }; end + module RefHolder + # module body: self is shareable, as make_shareable requires + PROC = Ractor.make_shareable(->(s) { s.shout }) + end + orig = RefHolder::PROC + # Store a memo in the main Ractor first, then refine the same shareable + # proc in another Ractor: cross-Ractor reuse of the memo is legal + # because the memoized cref's refinements table is frozen and shareable. + assert_equal("HI!", orig.refined(RefMod).call("hi")) + r = Ractor.new(orig) { |pr| pr.refined(RefMod).call("hi") } + assert_equal("HI!", r.value) + assert_equal("HI!", orig.refined(RefMod).call("hi")) + RUBY + end + + def test_refined_coverage + assert_separately(%w[-rcoverage -rtempfile], <<~'RUBY') + f = Tempfile.open(["refined_coverage", ".rb"]) + f.write(<<~'FIXTURE') + module RefinedCoverageExt + refine String do + def shout = upcase + "!" + end + end + REFINED_COVERAGE_PROC = ->(s) { + a = s.length + s.shout * a + } + FIXTURE + f.close + Coverage.start(lines: true) + load f.path + refined = REFINED_COVERAGE_PROC.refined(RefinedCoverageExt) + assert_equal("HI!HI!", refined.call("hi")) + lines = Coverage.result[f.path][:lines] + assert_equal(1, lines[6], "line 7 (a = s.length) must be counted when run through the refined copy") + assert_equal(1, lines[7], "line 8 (s.shout * a) must be counted when run through the refined copy") + RUBY + end + + def test_refined_chain_rejected + # Chaining would need merge-or-replace semantics for the refinement sets; + # both are confusing, so a refined proc rejects further refined. + # Multiple modules can be activated by passing them in a single call. + refined = ->(s) { s.shout }.refined(RefinementsModule) + assert_raise(ArgumentError) { refined.refined(RefinementsModule2) } + # the refinement state survives dup, so the dup is rejected too + assert_raise(ArgumentError) { refined.dup.refined(RefinementsModule2) } + # the receiver remains usable + assert_equal("HI!", refined.call("hi")) + end + + def test_refined_using_in_body_rejected + # The refinement set of a refined proc is fixed at refined() time: procs + # derived from the same source and modules share the copied iseq (and its + # refined call caches), so `using` inside the body would diverge one + # proc's refinement set and poison the caches its siblings use. + assert_separately([], <<~'RUBY') + module M1; refine(String) { def shout = upcase + "!" }; end + module M2; refine(String) { def whisper = downcase + "..." }; end + msg = /using is not permitted in a proc with refinements/ + r = proc { using M2 }.refined(M1) + assert_raise_with_message(RuntimeError, msg) { r.call } + # through module_eval (Module#using) as well + e = proc { using M2 }.refined(M1) + assert_raise_with_message(RuntimeError, msg) { Module.new.module_eval(&e) } + # in a class body or a nested block inside the refined proc + c = proc { class ::RefinedUsingTmp; using M2; end }.refined(M1) + assert_raise_with_message(RuntimeError, msg) { c.call } + n = proc { -> { using M2 }.call }.refined(M1) + assert_raise_with_message(RuntimeError, msg) { n.call } + # plain procs are unaffected + assert_equal("ok...", Module.new.module_eval(&proc { using M2; "ok".whisper })) + # and the refined proc itself still works + assert_equal("OK!", proc { "ok".shout }.refined(M1).call) + RUBY + end + + def test_refined_rejected_by_define_method + # A bmethod is invoked against its method entry, not the proc's refinement + # cref, so defining a method from a refined proc would silently drop + # the refinements; it is rejected instead. + refined = ->(s) { s.shout }.refined(RefinementsModule) + assert_raise(ArgumentError) { Class.new { define_method(:m, refined) } } + assert_raise(ArgumentError) { Class.new { define_method(:m, &refined) } } + assert_raise(ArgumentError) { Object.new.define_singleton_method(:m, refined) } + end + + # Each refinement module refines only one class so the nested test below can + # tell apart the refinement added on the inner Proc (String) from the one + # inherited from the enclosing refined Proc (Integer). + module RefinementsStringOnly + refine String do + def shout = upcase + "!" + end + end + + module RefinementsIntegerOnly + refine Integer do + def doubled = self * 2 + end + end + + def test_refined_nested_proc_is_not_a_chain + # A Proc created lexically INSIDE a refined Proc is not itself "a + # Proc that already has refinements": it only inherits the enclosing + # refinements lexically. refined (and define_method) must therefore + # be accepted on it, and the inner Proc must see both the enclosing + # refinement and the one it adds. Only the Proc returned by refined + # is rejected for chaining. + result = -> { + inner = ->(s, n) { [s.shout, n.doubled] } + inner.refined(RefinementsStringOnly).call("hi", 3) + }.refined(RefinementsIntegerOnly).call + # RefinementsStringOnly is added on the inner copy; RefinementsIntegerOnly + # is inherited from the enclosing refined Proc. + assert_equal(["HI!", 6], result) + + # define_method from such a nested Proc is allowed (it is not the + # refined result itself). + assert_nothing_raised do + -> { Class.new { define_method(:m, ->(s) { s }) } }.refined(RefinementsIntegerOnly).call + end + end + + def test_refined_gc + assert_normal_exit(<<~RUBY) + module M + refine(String) { def shout = upcase + "!" } + end + procs = 100.times.map { |i| ->(s) { s.shout } .refined(M) } + GC.start + GC.compact rescue nil + procs.each { |pr| raise "bad" unless pr.call("a") == "A!" } + RUBY + end + + def test_refined_gc_stress + # Under GC.stress, the memo store allocates its module array while the memo + # is reachable from the iseq; a GC during that allocation must not observe a + # half-initialized memo (argc set but mods not yet allocated). Alternating + # module sets keeps missing the memo so each call re-runs the store; one + # small iseq keeps it cheap enough for slow CI runners. + assert_normal_exit(<<~RUBY) + module M1; refine(String) { def shout = upcase + "!" }; end + module M2; refine(String) { def shout = downcase }; end + orig = ->(s) { s.shout } + r = nil + GC.stress = true + 6.times { |i| r = orig.refined(i.even? ? M1 : M2) } + GC.stress = false + raise "bad" unless r.call("Hi") == "hi" + RUBY + end + + def test_refined_iseq_to_binary + # A block iseq that has memoized copies must still serialize cleanly + # (the memo lives outside the iseq). + assert_separately([], <<~'RUBY') + module M + refine(String) { def shout = upcase + "!" } + end + src = 'x = ->(s) { s.shout }; x.refined(M).call("hi")' + iseq = RubyVM::InstructionSequence.compile(src) + eval(src) # runs refined, setting the memo on the block iseq + bin = iseq.to_binary + loaded = RubyVM::InstructionSequence.load_from_binary(bin) + assert_equal("HI!", loaded.eval) + RUBY + end + + module RefinementsModule2 + refine String do + def shout = "?" + end + end + + def test_refined_memoized + orig = ->(s) { s.shout } + # Repeating the same (proc, modules) reuses the cached copy but stays correct. + assert_equal("HI!", orig.refined(RefinementsModule).call("hi")) + assert_equal("HI!", orig.refined(RefinementsModule).call("hi")) + # A different module set must not return the previously cached copy. + assert_equal("?", orig.refined(RefinementsModule2).call("hi")) + # ...and switching back is still correct. + assert_equal("HI!", orig.refined(RefinementsModule).call("hi")) + end + + def test_refined_ruby2_keywords_memo + # Proc#ruby2_keywords marks the shared block iseq, possibly after a copy + # was memoized. The stale memo is rebuilt (with a warning naming the + # cause) rather than reused or mutated: the new proc delegates keywords + # like its source, while procs built before the mark keep their + # creation-time behavior. + assert_separately([], <<~'RUBY') + module M; refine(String) { def shout = upcase + "!" }; end + Warning[:performance] = true + $warned = [] + def Warning.warn(msg, category: nil) = $warned << msg + target = ->(a, k: nil) { [a, k] } + pr = proc { |*args| target.call(*args) } + q1 = pr.refined(M) # memoize a copy before the mark + pr.ruby2_keywords + assert_equal([1, 2], pr.call(1, k: 2)) + q2 = pr.refined(M) + assert_equal([1, 2], q2.call(1, k: 2)) + assert_equal(1, $warned.grep(/ruby2_keywords/).size) + # the copy made before the mark is not retroactively changed + assert_raise(ArgumentError) { q1.call(1, k: 2) } + # the rebuilt memo is hit from now on; no further warnings + assert_equal([1, 2], pr.refined(M).call(1, k: 2)) + assert_equal(1, $warned.grep(/ruby2_keywords/).size) + RUBY + end + + def test_refined_memo_distinct_environments + # Procs sharing the same block iseq but capturing different closure + # environments hit the same memo entry (env is not part of the key), yet each + # result must keep its own environment. + factory = ->(tag) { ->(s) { "#{tag}:#{s.shout}" } } + p1 = factory.call("A") + p2 = factory.call("B") + r1 = p1.refined(RefinementsModule) + r2 = p2.refined(RefinementsModule) + assert_equal("A:X!", r1.call("x")) + assert_equal("B:Y!", r2.call("y")) + # the original closures still see their own captured tag too + assert_equal("A:X!", r1.call("x")) + end + + def test_refined_memo_avoids_recopy + orig = ->(s) { s.shout } + orig.refined(RefinementsModule) # warm the memo + GC.disable + begin + before = GC.stat(:total_allocated_objects) + 100.times { orig.refined(RefinementsModule) } + hits = GC.stat(:total_allocated_objects) - before + + before = GC.stat(:total_allocated_objects) + 100.times do |i| + orig.refined(i.even? ? RefinementsModule : RefinementsModule2) + end + misses = GC.stat(:total_allocated_objects) - before + ensure + GC.enable + end + # Memo hits must allocate far less than recomputing the iseq copy each time. + assert_operator(misses, :>, hits * 2, + "expected memo hits (#{hits}) to allocate much less than misses (#{misses})") + end + + def test_refined_memo_different_modules_warning + assert_separately([], <<~'RUBY') + module M1; refine(String) { def shout = "1" }; end + module M2; refine(String) { def shout = "2" }; end + Warning[:performance] = true + $warned = [] + def Warning.warn(msg, category: nil) = $warned << msg + pr = ->(s) { s.shout } + pr.refined(M1) + pr.refined(M2) # evicts the M1 entry + assert_equal(1, $warned.grep(/different modules/).size) + RUBY + end + + def test_refined_memo_survives_gc + assert_separately([], <<~'RUBY') + module M; refine(String) { def shout = upcase + "!" }; end + pr = ->(s) { [1].each { }; s.shout } + # the memo lives as long as its source iseq, so the copy stays + # memoized even when no refined proc survives the GC + pr.refined(M) + GC.start + assert_equal("HI!", pr.refined(M).call("hi")) + rp = pr.refined(M) + GC.start + assert_equal("HI!", rp.call("hi")) + assert_equal("HI!", pr.refined(M).call("hi")) + RUBY + end + + def test_refined_memo_gc_compact + assert_separately([], <<~'RUBY') + module M; refine(String) { def shout = upcase + "!" }; end + pr = ->(s) { [1].each { }; s.shout } + rp = pr.refined(M) + begin + GC.compact + rescue NotImplementedError + omit "GC compaction not supported on this platform" + end + assert_equal("HI!", rp.call("hi")) + assert_equal("HI!", pr.refined(M).call("hi")) + RUBY + end + + def test_refined_memo_survives_compaction + # the memo is a hidden identity Hash: its keys (source iseqs) are pinned, + # while its values (copied iseqs, crefs) move and must be updated correctly + assert_separately([], <<~'RUBY') + omit "compaction unsupported" unless GC.respond_to?(:verify_compaction_references) + module M; refine(String) { def shout = upcase + "!" }; end + blocks = eval("[" + (["->(s) { s.shout }"] * 500).join(",\n") + "]") + kept = blocks.map { |b| b.refined(M) } + begin + GC.verify_compaction_references(expand_heap: true, toward: :empty) + rescue NotImplementedError + omit "compaction unsupported on this platform" + end + kept.each { |r| assert_equal("HI!", r.call("hi")) } + blocks.each { |b| assert_equal("HI!", b.refined(M).call("hi")) } + GC.compact + blocks.each { |b| assert_equal("HI!", b.refined(M).call("hi")) } + RUBY + end + + def test_refined_rescue_ensure + # exercises the copied catch table and shared rescue local table + refined = ->(s) { + r = nil + begin + r = s.shout + rescue NoMethodError + r = "rescued" + ensure + r = "#{r}." + end + r + }.refined(RefinementsModule) + assert_equal("YO!.", refined.call("yo")) + end + + def test_refined_def_in_block + # a literal def inside the block creates a nested method iseq whose + # local_iseq is itself (in-subtree); the copy must rebuild it. + # Specified behavior: like a def inside a `using` scope, a method defined + # inside the block sees the refinements (the def captures the block's + # cref), so the refinement also applies when the method is called later. + refined = ->(s) { + o = Object.new + def o.m = "hi".shout + [s.shout, o.m] + }.refined(RefinementsModule) + assert_equal(["YO!", "HI!"], refined.call("yo")) + end + + def test_refined_keyword_and_optional_args + refined = ->(a, b = "z", c:, d: "w") { + [a, b, c, d].map(&:shout).join + }.refined(RefinementsModule) + assert_equal("A!Z!C!W!", refined.call("a", c: "c")) + assert_equal("A!B!C!D!", refined.call("a", "b", c: "c", d: "d")) + end + + def test_refined_case_when_literal + # literal when-clauses compile to a CDHASH operand that must round-trip + refined = ->(s) { + case s.shout + when "A!" then 1 + when "B!" then 2 + else 0 + end + }.refined(RefinementsModule) + assert_equal(1, refined.call("a")) + assert_equal(2, refined.call("b")) + assert_equal(0, refined.call("c")) + end + + def test_refined_flip_flop + # flip-flop allocates a special-variable slot keyed off the local iseq; + # the copy must run its own flip state independent of the original + body = ->(arr) { + out = [] + arr.each { |i| out << i if (i == 2)..(i == 4) } + out + } + refined = body.refined(RefinementsModule) + assert_equal([2, 3, 4], refined.call([1, 2, 3, 4, 5])) + # a second call starts from a fresh flip state + assert_equal([2, 3, 4], refined.call([1, 2, 3, 4, 5])) + end + + def test_refined_preserves_location_and_parameters + orig = ->(a, b = 1, *c, d:, **e, &f) { a } + refined = orig.refined(RefinementsModule) + assert_equal(orig.source_location, refined.source_location) + assert_equal(orig.parameters, refined.parameters) + assert_equal(orig.arity, refined.arity) + end + + module RefinementsOperators + refine Integer do + def +(other) = "plus(#{self},#{other})" + def <(other) = "lt" + end + refine Array do + def [](i) = "at#{i}" + end + end + + def test_refined_operators + # Specialized instructions (opt_plus, opt_lt, opt_aref, ...) must honor the + # refinement on the copy without leaking it into the original Proc. + refined = ->(a, b) { [a + b, a < b] }.refined(RefinementsOperators) + assert_equal(["plus(1,2)", "lt"], refined.call(1, 2)) + aref = ->(a) { a[0] }.refined(RefinementsOperators) + assert_equal("at0", aref.call([9])) + # the original Procs keep using the builtin operators + assert_equal(3, ->(a, b) { a + b }.call(1, 2)) + end + + def test_refined_preserves_lambda + # The copy must keep the receiver's lambda/proc nature, including a lambda's + # strict argument checking. + lam = ->(a, b) { [a, b] }.refined(RefinementsModule) + assert_equal(true, lam.lambda?) + assert_raise(ArgumentError) { lam.call(1) } + pr = proc { |a, b| [a, b] }.refined(RefinementsModule) + assert_equal(false, pr.lambda?) + # proc argument handling fills missing parameters with nil instead of raising + assert_equal([1, nil], pr.call(1)) + end + + def test_refined_preserved_by_clone + refined = ->(s) { s.shout }.refined(RefinementsModule) + assert_equal("Z!", refined.clone.call("z")) + # the refinement state survives clone, so chaining on the clone is rejected too + assert_raise(ArgumentError) { refined.clone.refined(RefinementsModule2) } + end + + def test_refined_module_precedence + # When several modules refine the same method, the last one wins, matching + # the precedence of nested `using`. + body = ->(s) { s.shout } + assert_equal("?", body.refined(RefinementsModule, RefinementsModule2).call("Hi")) + assert_equal("HI!", body.refined(RefinementsModule2, RefinementsModule).call("Hi")) + end + + class RefinementsSuperBase + def greet = "base" + end + + module RefinementsSuperModule + refine RefinementsSuperBase do + def greet = "ref-" + super + end + end + + def test_refined_super + # A refined method may call super to reach the method it refines. + refined = ->(o) { o.greet }.refined(RefinementsSuperModule) + assert_equal("ref-base", refined.call(RefinementsSuperBase.new)) + end + + def test_refined_tracepoint + # Line events fire on the copied iseq just like on the original. + src = "->(s) {\n x = s.shout\n x\n}" + refined = eval(src, binding, "wr_trace_eval").refined(RefinementsModule) + lines = [] + tp = TracePoint.new(:line) { |t| lines << t.lineno if t.path == "wr_trace_eval" } + result = tp.enable { refined.call("hi") } + assert_equal("HI!", result) + assert_equal([2, 3], lines) + end + + def test_refined_recursion_sees_refinements + # The copy shares the source Proc's environment, so a recursive call through + # the captured local reaches the refined Proc and keeps the refinements. + fact = nil + fact = ->(s) { s.empty? ? "" : s[0].shout + fact.call(s[1..]) } + .refined(RefinementsModule) + assert_equal("A!B!C!", fact.call("abc")) + end + def test_clone_subclass c1 = Class.new(Proc) assert_equal c1, c1.new{}.clone.class, '[Bug #17545]' diff --git a/test/rubygems/test_gem_safe_yaml.rb b/test/rubygems/test_gem_safe_yaml.rb index 5bbfa57a95e446..15c8f84d21be38 100644 --- a/test/rubygems/test_gem_safe_yaml.rb +++ b/test/rubygems/test_gem_safe_yaml.rb @@ -1100,6 +1100,23 @@ def test_load_crlf_line_endings assert_equal "data", result["other"] end + def test_dump_crlf_string_roundtrip + obj = { "pem" => "line one\r\nline two\r\n" } + assert_equal obj, yaml_load(yaml_dump(obj)) + end + + def test_load_crlf_with_sequence + yaml = <<~YAML.gsub("\n", "\r\n") + --- + nested_hash: + contains_array: + - "quoted item" + - plain item + YAML + + assert_equal({ "nested_hash" => { "contains_array" => ["quoted item", "plain item"] } }, yaml_load(yaml)) + end + def test_load_version_requirement_old_tag yaml = <<~YAML !ruby/object:Gem::Version::Requirement diff --git a/test/rubygems/test_gem_source.rb b/test/rubygems/test_gem_source.rb index a0bc10560df5ec..a030b58851c796 100644 --- a/test/rubygems/test_gem_source.rb +++ b/test/rubygems/test_gem_source.rb @@ -121,6 +121,19 @@ def test_fetch_spec_platform_ruby assert_equal @specs["a-1"].full_name, spec.full_name end + def test_fetch_spec_path_traversal + escape = File.expand_path(File.join(Gem.spec_cache_dir, "..", "owned.gemspec")) + + name_tuple = tuple("../owned", Gem::Version.new(1), "ruby") + + e = assert_raise Gem::Exception do + @source.fetch_spec name_tuple + end + + assert_includes e.message, "malformed spec name" + refute File.exist?(escape), "spec must not be written outside the spec cache" + end + def test_load_specs released = @source.load_specs(:released).map(&:full_name) assert_equal %W[a-2 a-1 b-2], released diff --git a/thread.c b/thread.c index 3607b6c4e333df..e9c67aa7793449 100644 --- a/thread.c +++ b/thread.c @@ -589,7 +589,8 @@ rb_vm_proc_local_ep(VALUE proc) // for ractor, defined in vm.c VALUE rb_vm_invoke_proc_with_self(rb_execution_context_t *ec, rb_proc_t *proc, VALUE self, - int argc, const VALUE *argv, int kw_splat, VALUE passed_block_handler); + int argc, const VALUE *argv, int kw_splat, VALUE passed_block_handler, + const rb_cref_t *cref); static VALUE thread_do_start_proc(rb_thread_t *th) @@ -600,6 +601,7 @@ thread_do_start_proc(rb_thread_t *th) VALUE procval = th->invoke_arg.proc.proc; rb_proc_t *proc; GetProcPtr(procval, proc); + const rb_cref_t *cref = rb_proc_refinements_cref(procval); th->ec->errinfo = Qnil; th->ec->root_lep = rb_vm_proc_local_ep(procval); @@ -621,7 +623,8 @@ thread_do_start_proc(rb_thread_t *th) th->ec, proc, self, args_len, args_ptr, th->invoke_arg.proc.kw_splat, - VM_BLOCK_HANDLER_NONE + VM_BLOCK_HANDLER_NONE, + cref ); } else { @@ -642,7 +645,8 @@ thread_do_start_proc(rb_thread_t *th) th->ec, proc, args_len, args_ptr, th->invoke_arg.proc.kw_splat, - VM_BLOCK_HANDLER_NONE + VM_BLOCK_HANDLER_NONE, + cref ); } } diff --git a/tool/zjit_analyze_perf_map_size.rb b/tool/zjit_analyze_perf_map_size.rb new file mode 100644 index 00000000000000..48b72e4864f820 --- /dev/null +++ b/tool/zjit_analyze_perf_map_size.rb @@ -0,0 +1,40 @@ +#!/usr/bin/env ruby + +required_ruby_version = Gem::Version.new("3.4.0") +raise "Ruby version #{required_ruby_version} or higher is required" if Gem::Version.new(RUBY_VERSION) < required_ruby_version + +PERF_MAP = ARGV[0] || raise("Expected perf map as first argument") + +sizes = Hash.new(0) +counts = Hash.new(0) +File.foreach(PERF_MAP) do |line| + address, size, name = line.split(" ", 3) + name.delete_prefix!("ZJIT: ") + name.delete_suffix!("\n") + sizes[name] += size.to_i(16) + counts[name] += 1 +end + +total_size = sizes.values.sum + +def pretty_size bytes + u = 0 + s = 1024 + while bytes >= s || -bytes >= s + bytes /= s.to_f + u += 1 + end + "#{bytes.round(1)} #{'​KMGTPEZY'[u]}B" +end + +n = 20 + +sizes.sort_by { |name, size| -size }.first(n).each do |name, size| + puts "#{pretty_size(size)} total (#{(size/total_size.to_f*100).round(1)}%); #{pretty_size(size/counts[name].to_f)} each #{name}" +end + +puts + +counts.sort_by { |name, count| -count }.first(n).each do |name, count| + puts "#{count} #{name}" +end diff --git a/variable.c b/variable.c index bcd8c32ea2246b..c29054b57652d2 100644 --- a/variable.c +++ b/variable.c @@ -1603,56 +1603,21 @@ rb_attr_get(VALUE obj, ID id) return rb_ivar_lookup(obj, id, Qnil); } -void rb_obj_copy_fields_to_hash_table(VALUE obj, st_table *table); -static VALUE imemo_fields_complex_from_obj(VALUE owner, VALUE source_fields_obj, shape_id_t shape_id); +static VALUE imemo_fields_evacutate_to_complex(VALUE owner, VALUE source_fields_obj, shape_id_t shape_id, int extra_capa); static shape_id_t -obj_transition_complex(VALUE obj, st_table *table) +rb_obj_convert_too_complex(VALUE obj, VALUE fields_obj, shape_id_t shape_id) { + RUBY_ASSERT(RB_TYPE_P(obj, T_OBJECT)); RUBY_ASSERT(!rb_obj_shape_complex_p(obj)); - RUBY_ASSERT(!RB_TYPE_P(obj, T_IMEMO)); - - shape_id_t shape_id = rb_obj_shape_transition_complex(obj); - VALUE fields_obj = rb_imemo_fields_new_complex_tbl(obj, shape_id, table, RB_OBJ_SHAREABLE_P(obj)); - - rb_obj_set_fields(obj, fields_obj, 0, 0); - RBASIC_SET_SHAPE_ID(obj, shape_id); - - RUBY_ASSERT(FL_TEST_RAW(fields_obj, ROBJECT_HEAP)); - RUBY_ASSERT(rb_obj_shape_complex_p(obj)); - RUBY_ASSERT(rb_obj_shape_complex_p(fields_obj)); + shape_id = rb_shape_transition_complex(shape_id); + VALUE new_fields_obj = imemo_fields_evacutate_to_complex(obj, fields_obj, shape_id, 1); + ROBJECT_SET_EXTENDED(obj, new_fields_obj); + RBASIC_SET_SHAPE_ID_WITH_LAYOUT(obj, shape_id, SHAPE_ID_LAYOUT_EXTENDED); return shape_id; } -// Copy all object fields, including ivars and internal object_id, etc -static shape_id_t -rb_evict_fields_to_hash(VALUE obj) -{ - RUBY_ASSERT(!rb_obj_shape_complex_p(obj)); - - st_table *table = st_init_numtable_with_size(RSHAPE_LEN(RBASIC_SHAPE_ID(obj))); - rb_obj_copy_fields_to_hash_table(obj, table); - shape_id_t new_shape_id = obj_transition_complex(obj, table); - - RUBY_ASSERT(rb_obj_shape_complex_p(obj)); - return new_shape_id; -} - -void -rb_evict_ivars_to_hash(VALUE obj) -{ - RUBY_ASSERT(!rb_obj_shape_complex_p(obj)); - - st_table *table = st_init_numtable_with_size(rb_ivar_count(obj)); - - // Evacuate all previous values from shape into id_table - rb_obj_copy_ivs_to_hash_table(obj, table); - obj_transition_complex(obj, table); - - RUBY_ASSERT(rb_obj_shape_complex_p(obj)); -} - static VALUE rb_ivar_delete(VALUE obj, ID id, VALUE undef) { @@ -1697,7 +1662,7 @@ rb_ivar_delete(VALUE obj, ID id, VALUE undef) if (UNLIKELY(rb_shape_complex_p(next_shape_id))) { if (UNLIKELY(!rb_shape_complex_p(old_shape_id))) { - fields_obj = imemo_fields_complex_from_obj(obj, fields_obj, next_shape_id); + fields_obj = imemo_fields_evacutate_to_complex(obj, fields_obj, next_shape_id, -1); } st_data_t key = id; if (!st_delete(rb_imemo_fields_complex_tbl(fields_obj), &key, (st_data_t *)&val)) { @@ -1783,23 +1748,6 @@ rb_attr_delete(VALUE obj, ID id) return rb_ivar_delete(obj, id, Qnil); } -void -rb_obj_init_complex(VALUE obj, st_table *table) -{ - // This method is meant to be called on newly allocated object. - RUBY_ASSERT(rb_shape_canonical_p(RBASIC_SHAPE_ID(obj))); - RUBY_ASSERT(RSHAPE_LEN(RBASIC_SHAPE_ID(obj)) == 0); - - if (rb_obj_shape_complex_p(obj)) { - shape_id_t shape_id = rb_obj_shape_transition_complex(obj); - VALUE fields_obj = rb_imemo_fields_new_complex_tbl(obj, shape_id, table, false); - ROBJECT_SET_EXTENDED(obj, fields_obj); - } - else { - obj_transition_complex(obj, table); - } -} - static int imemo_fields_complex_from_obj_i(ID key, VALUE val, st_data_t arg) { @@ -1814,16 +1762,31 @@ imemo_fields_complex_from_obj_i(ID key, VALUE val, st_data_t arg) } static VALUE -imemo_fields_complex_from_obj(VALUE owner, VALUE source_fields_obj, shape_id_t shape_id) +imemo_fields_complex_from_obj(VALUE owner, VALUE source, shape_id_t shape_id, bool ivar_only, int extra_capa) { - attr_index_t len = source_fields_obj ? RSHAPE_LEN(RBASIC_SHAPE_ID(source_fields_obj)) : 0; - VALUE fields_obj = rb_imemo_fields_new_complex(owner, shape_id, len + 1, RB_OBJ_SHAREABLE_P(owner)); + attr_index_t len = source ? RSHAPE_LEN(RBASIC_SHAPE_ID(source)) : 0; + int capa = (len + extra_capa); + RUBY_ASSERT(capa >= 0); + + VALUE fields_obj = rb_imemo_fields_new_complex(owner, shape_id, capa, RB_OBJ_SHAREABLE_P(owner)); - rb_field_foreach(source_fields_obj, imemo_fields_complex_from_obj_i, (st_data_t)fields_obj, false); + rb_field_foreach(source, imemo_fields_complex_from_obj_i, (st_data_t)fields_obj, ivar_only); return fields_obj; } +static VALUE +imemo_fields_evacutate_to_complex(VALUE owner, VALUE source, shape_id_t shape_id, int extra_capa) +{ + return imemo_fields_complex_from_obj(owner, source, shape_id, false, extra_capa); +} + +VALUE +rb_obj_complex_fields_build(VALUE obj) +{ + return imemo_fields_complex_from_obj(obj, obj, ROOT_COMPLEX_SHAPE_ID, true, 0); +} + static VALUE imemo_fields_copy_append(VALUE owner, VALUE source_fields_obj, shape_id_t current_shape_id, shape_id_t target_shape_id, VALUE val) { @@ -1861,7 +1824,7 @@ imemo_fields_set(VALUE owner, VALUE fields_obj, shape_id_t target_shape_id, ID f } } else { - fields_obj = imemo_fields_complex_from_obj(owner, original_fields_obj, target_shape_id); + fields_obj = imemo_fields_evacutate_to_complex(owner, original_fields_obj, target_shape_id, 1); current_shape_id = target_shape_id; } @@ -1930,40 +1893,19 @@ generic_ivar_set(VALUE obj, ID id, VALUE val) return generic_field_set(obj, target_shape_id, id, val); } -static int -rb_obj_copy_ivs_to_hash_table_i(ID key, VALUE val, st_data_t arg) -{ - RUBY_ASSERT(!st_lookup((st_table *)arg, (st_data_t)key, NULL)); - - st_add_direct((st_table *)arg, (st_data_t)key, (st_data_t)val); - return ST_CONTINUE; -} - -void -rb_obj_copy_ivs_to_hash_table(VALUE obj, st_table *table) -{ - rb_ivar_foreach(obj, rb_obj_copy_ivs_to_hash_table_i, (st_data_t)table); -} - -void -rb_obj_copy_fields_to_hash_table(VALUE obj, st_table *table) -{ - rb_field_foreach(obj, rb_obj_copy_ivs_to_hash_table_i, (st_data_t)table, false); -} - static attr_index_t obj_field_set(VALUE obj, shape_id_t target_shape_id, ID field_name, VALUE val) { + // may be T_OBJECT or imemo_fields + VALUE fields_obj = ROBJECT_FIELDS_OBJ(obj); shape_id_t current_shape_id = RBASIC_SHAPE_ID(obj); if (UNLIKELY(rb_shape_complex_p(target_shape_id))) { if (UNLIKELY(!rb_shape_complex_p(current_shape_id))) { - current_shape_id = rb_evict_fields_to_hash(obj); + current_shape_id = rb_obj_convert_too_complex(obj, fields_obj, current_shape_id); + fields_obj = ROBJECT_FIELDS_OBJ(obj); } - // may be T_OBJECT or imemo_fields - VALUE fields_obj = ROBJECT_FIELDS_OBJ(obj); - RUBY_ASSERT(rb_obj_shape_complex_p(obj)); RUBY_ASSERT(rb_obj_shape_complex_p(fields_obj)); @@ -1985,9 +1927,6 @@ obj_field_set(VALUE obj, shape_id_t target_shape_id, ID field_name, VALUE val) else { attr_index_t index = RSHAPE_INDEX(target_shape_id); - // may be T_OBJECT or imemo_fields - VALUE fields_obj = ROBJECT_FIELDS_OBJ(obj); - if (index < RSHAPE_LEN(current_shape_id)) { // Replace existing value; RB_OBJ_WRITE(fields_obj, &rb_imemo_fields_ptr(fields_obj)[index], val); @@ -2246,7 +2185,7 @@ each_hash_iv(st_data_t id, st_data_t val, st_data_t data) { struct iv_itr_data * itr_data = (struct iv_itr_data *)data; rb_ivar_foreach_callback_func *callback = itr_data->func; - if (is_internal_id((ID)id)) { + if ((ID)id == rb_shape_tree.id_object_id) { return ST_CONTINUE; } return callback((ID)id, (VALUE)val, itr_data->arg); @@ -2313,27 +2252,16 @@ rb_copy_generic_ivar(VALUE dest, VALUE obj) if (fields_obj) { unsigned long src_num_ivs = rb_ivar_count(fields_obj); if (!src_num_ivs) { - goto clear; - } - - if (rb_shape_complex_p(src_shape_id)) { - rb_shape_copy_complex_ivars(dest, obj, src_shape_id, rb_imemo_fields_complex_tbl(fields_obj)); + rb_free_generic_ivar(dest); return; } - shape_id_t dest_shape_id = src_shape_id; shape_id_t initial_shape_id = rb_obj_shape_id(dest); + shape_id_t dest_shape_id = rb_shape_rebuild(initial_shape_id, src_shape_id); - if (!rb_shape_canonical_p(src_shape_id)) { - RUBY_ASSERT(RSHAPE_TYPE_P(initial_shape_id, SHAPE_ROOT)); - - dest_shape_id = rb_shape_rebuild(initial_shape_id, src_shape_id); - if (UNLIKELY(rb_shape_complex_p(dest_shape_id))) { - st_table *table = rb_st_init_numtable_with_size(src_num_ivs); - rb_obj_copy_ivs_to_hash_table(obj, table); - rb_obj_init_complex(dest, table); - return; - } + if (UNLIKELY(rb_shape_complex_p(dest_shape_id))) { + rb_obj_replace_fields(dest, rb_obj_complex_fields_build(obj)); + return; } if (!RSHAPE_LEN(dest_shape_id)) { @@ -2348,10 +2276,6 @@ rb_copy_generic_ivar(VALUE dest, VALUE obj) rb_obj_replace_fields(dest, new_fields_obj); } - return; - - clear: - rb_free_generic_ivar(dest); } void @@ -4644,7 +4568,7 @@ class_fields_ivar_set(VALUE klass, VALUE fields_obj, ID id, VALUE val, bool conc next_shape_id = generic_shape_ivar(fields_obj, id, &new_ivar); if (UNLIKELY(rb_shape_complex_p(next_shape_id))) { - fields_obj = imemo_fields_complex_from_obj(klass, fields_obj, next_shape_id); + fields_obj = imemo_fields_evacutate_to_complex(klass, fields_obj, next_shape_id, 1); goto complex; } diff --git a/variable.h b/variable.h index f2afead9d3a8dd..034f95f949561c 100644 --- a/variable.h +++ b/variable.h @@ -12,7 +12,6 @@ #include "shape.h" -void rb_copy_complex_ivars(VALUE dest, VALUE obj, shape_id_t src_shape_id, st_table *fields_table); VALUE rb_obj_fields(VALUE obj, ID field_name); static inline VALUE diff --git a/vm.c b/vm.c index 07d03c864ae6f8..df24fa3c300dc8 100644 --- a/vm.c +++ b/vm.c @@ -360,8 +360,8 @@ ref_delete_symkey(VALUE key, VALUE value, VALUE unused) return SYMBOL_P(key) ? ST_DELETE : ST_CONTINUE; } -static rb_cref_t * -vm_cref_dup(const rb_cref_t *cref) +rb_cref_t * +rb_vm_cref_dup(const rb_cref_t *cref) { const rb_scope_visibility_t *visi = CREF_SCOPE_VISI(cref); rb_cref_t *next_cref = CREF_NEXT(cref), *new_cref; @@ -380,7 +380,6 @@ vm_cref_dup(const rb_cref_t *cref) return new_cref; } - rb_cref_t * rb_vm_cref_dup_without_refinements(const rb_cref_t *cref) { @@ -424,7 +423,9 @@ vm_cref_dump(const char *mesg, const rb_cref_t *cref) ruby_debug_printf("vm_cref_dump: %s (%p)\n", mesg, (void *)cref); while (cref) { - ruby_debug_printf("= cref| klass: %s\n", RSTRING_PTR(rb_class_path(CREF_CLASS(cref)))); + VALUE path = rb_class_path(CREF_CLASS(cref)); + ruby_debug_printf("= cref| klass: %.*s\n", + RSTRING_LENINT(path), RSTRING_PTR(path)); cref = CREF_NEXT(cref); } } @@ -454,7 +455,7 @@ static VALUE vm_make_env_object(const rb_execution_context_t *ec, rb_control_fra static VALUE vm_invoke_bmethod(rb_execution_context_t *ec, rb_proc_t *proc, VALUE self, int argc, const VALUE *argv, int kw_splat, VALUE block_handler, const rb_callable_method_entry_t *me); -static VALUE vm_invoke_proc(rb_execution_context_t *ec, rb_proc_t *proc, VALUE self, int argc, const VALUE *argv, int kw_splat, VALUE block_handler); +static VALUE vm_invoke_proc(rb_execution_context_t *ec, rb_proc_t *proc, VALUE self, int argc, const VALUE *argv, int kw_splat, VALUE block_handler, const rb_cref_t *cref); #if USE_YJIT // Counter to serve as a proxy for execution time, total number of calls @@ -1339,7 +1340,7 @@ proc_create(VALUE klass, const struct rb_block *block, int8_t is_from_method, in } VALUE -rb_proc_dup(VALUE self) +rb_proc_dup_0(VALUE self) { VALUE procval; rb_proc_t *src; @@ -1355,11 +1356,45 @@ rb_proc_dup(VALUE self) break; } + if (src->is_refined) { + rb_proc_t *dst; + GetProcPtr(procval, dst); + dst->is_refined = 1; + } + if (RB_OBJ_SHAREABLE_P(self)) RB_OBJ_SET_SHAREABLE(procval); RB_GC_GUARD(self); /* for: body = rb_proc_dup(body) */ return procval; } +VALUE +rb_proc_dup(VALUE self) +{ + VALUE procval = rb_proc_dup_0(self); + const rb_cref_t *cref = rb_proc_refinements_cref(self); + if (cref) rb_proc_set_refinements_cref(procval, cref); + return procval; +} + +/* Proc#refined: build a Proc that runs `iseq` (a copy of self's block iseq) + * with `cref` as its refinement cref, sharing self's environment. */ +VALUE +rb_proc_dup_with_iseq_and_cref(VALUE self, const rb_iseq_t *iseq, const rb_cref_t *cref) +{ + rb_proc_t *src; + GetProcPtr(self, src); + VM_ASSERT(vm_block_type(&src->block) == block_type_iseq); + + struct rb_block block = src->block; + block.as.captured.code.iseq = iseq; + + VALUE procval = proc_create(rb_obj_class(self), &block, src->is_from_method, src->is_lambda); + rb_proc_set_refinements_cref(procval, cref); + + RB_GC_GUARD(self); + return procval; +} + struct collect_outer_variable_name_data { VALUE ary; VALUE read_only; @@ -1842,11 +1877,17 @@ invoke_block_from_c_bh(rb_execution_context_t *ec, VALUE block_handler, return vm_yield_with_symbol(ec, VM_BH_TO_SYMBOL(block_handler), argc, argv, kw_splat, passed_block_handler); case block_handler_type_proc: - if (force_blockarg == FALSE) { - is_lambda = block_proc_is_lambda(VM_BH_TO_PROC(block_handler)); + { + VALUE procval = VM_BH_TO_PROC(block_handler); + rb_proc_t *po; + GetProcPtr(procval, po); + if (po->is_refined) cref = rb_proc_refinements_cref(procval); + if (force_blockarg == FALSE) { + is_lambda = po->is_lambda; + } + block_handler = vm_block_to_block_handler(&po->block); + goto again; } - block_handler = vm_proc_to_block_handler(VM_BH_TO_PROC(block_handler)); - goto again; } VM_UNREACHABLE(invoke_block_from_c_splattable); return Qundef; @@ -1897,12 +1938,14 @@ ALWAYS_INLINE(static VALUE invoke_block_from_c_proc(rb_execution_context_t *ec, const rb_proc_t *proc, VALUE self, int argc, const VALUE *argv, int kw_splat, VALUE passed_block_handler, int is_lambda, + const rb_cref_t *cref, const rb_callable_method_entry_t *me)); static inline VALUE invoke_block_from_c_proc(rb_execution_context_t *ec, const rb_proc_t *proc, VALUE self, int argc, const VALUE *argv, int kw_splat, VALUE passed_block_handler, int is_lambda, + const rb_cref_t *cref, const rb_callable_method_entry_t *me) { const struct rb_block *block = &proc->block; @@ -1910,7 +1953,7 @@ invoke_block_from_c_proc(rb_execution_context_t *ec, const rb_proc_t *proc, again: switch (vm_block_type(block)) { case block_type_iseq: - return invoke_iseq_block_from_c(ec, &block->as.captured, self, argc, argv, kw_splat, passed_block_handler, NULL, is_lambda, me); + return invoke_iseq_block_from_c(ec, &block->as.captured, self, argc, argv, kw_splat, passed_block_handler, cref, is_lambda, me); case block_type_ifunc: if (kw_splat == 1) { VALUE keyword_hash = argv[argc-1]; @@ -1938,21 +1981,24 @@ invoke_block_from_c_proc(rb_execution_context_t *ec, const rb_proc_t *proc, static VALUE vm_invoke_proc(rb_execution_context_t *ec, rb_proc_t *proc, VALUE self, - int argc, const VALUE *argv, int kw_splat, VALUE passed_block_handler) + int argc, const VALUE *argv, int kw_splat, VALUE passed_block_handler, + const rb_cref_t *cref) { - return invoke_block_from_c_proc(ec, proc, self, argc, argv, kw_splat, passed_block_handler, proc->is_lambda, NULL); + return invoke_block_from_c_proc(ec, proc, self, argc, argv, kw_splat, passed_block_handler, proc->is_lambda, cref, NULL); } static VALUE vm_invoke_bmethod(rb_execution_context_t *ec, rb_proc_t *proc, VALUE self, int argc, const VALUE *argv, int kw_splat, VALUE block_handler, const rb_callable_method_entry_t *me) { - return invoke_block_from_c_proc(ec, proc, self, argc, argv, kw_splat, block_handler, TRUE, me); + /* bmethod procs never carry a refinement cref (Proc#refined rejects them) */ + return invoke_block_from_c_proc(ec, proc, self, argc, argv, kw_splat, block_handler, TRUE, NULL, me); } VALUE rb_vm_invoke_proc(rb_execution_context_t *ec, rb_proc_t *proc, - int argc, const VALUE *argv, int kw_splat, VALUE passed_block_handler) + int argc, const VALUE *argv, int kw_splat, VALUE passed_block_handler, + const rb_cref_t *cref) { VALUE self = vm_block_self(&proc->block); vm_block_handler_verify(passed_block_handler); @@ -1961,13 +2007,14 @@ rb_vm_invoke_proc(rb_execution_context_t *ec, rb_proc_t *proc, return vm_invoke_bmethod(ec, proc, self, argc, argv, kw_splat, passed_block_handler, NULL); } else { - return vm_invoke_proc(ec, proc, self, argc, argv, kw_splat, passed_block_handler); + return vm_invoke_proc(ec, proc, self, argc, argv, kw_splat, passed_block_handler, cref); } } VALUE rb_vm_invoke_proc_with_self(rb_execution_context_t *ec, rb_proc_t *proc, VALUE self, - int argc, const VALUE *argv, int kw_splat, VALUE passed_block_handler) + int argc, const VALUE *argv, int kw_splat, VALUE passed_block_handler, + const rb_cref_t *cref) { vm_block_handler_verify(passed_block_handler); @@ -1975,7 +2022,7 @@ rb_vm_invoke_proc_with_self(rb_execution_context_t *ec, rb_proc_t *proc, VALUE s return vm_invoke_bmethod(ec, proc, self, argc, argv, kw_splat, passed_block_handler, NULL); } else { - return vm_invoke_proc(ec, proc, self, argc, argv, kw_splat, passed_block_handler); + return vm_invoke_proc(ec, proc, self, argc, argv, kw_splat, passed_block_handler, cref); } } diff --git a/vm_args.c b/vm_args.c index 3d67c6540a3b48..1b8d10a159e858 100644 --- a/vm_args.c +++ b/vm_args.c @@ -1149,7 +1149,10 @@ vm_caller_setup_arg_block(const rb_execution_context_t *ec, rb_control_frame_t * rb_ary_push(callback_arg, ref); OBJ_FREEZE(callback_arg); func = rb_func_lambda_new(refine_sym_proc_call, callback_arg, 1, UNLIMITED_ARGUMENTS); - rb_hash_aset(ref, block_code, func); + /* the table is frozen when it belongs to a Proc#refined memo; skip the cache then */ + if (!OBJ_FROZEN(ref)) { + rb_hash_aset(ref, block_code, func); + } } block_code = func; } @@ -1184,7 +1187,7 @@ vm_caller_setup_fwd_args(const rb_execution_context_t *ec, rb_control_frame_t *r CALL_INFO site_ci = cd->ci; VALUE bh = Qundef; - RUBY_ASSERT(ISEQ_BODY(ISEQ_BODY(GET_ISEQ())->local_iseq)->param.flags.forwardable); + RUBY_ASSERT(ISEQ_BODY(ISEQ_BODY(CFP_ISEQ(reg_cfp))->local_iseq)->param.flags.forwardable); CALL_INFO caller_ci = (CALL_INFO)TOPN(0); unsigned int site_argc = vm_ci_argc(site_ci); diff --git a/vm_backtrace.c b/vm_backtrace.c index a865b6e71f3f6a..5af6cc341a8237 100644 --- a/vm_backtrace.c +++ b/vm_backtrace.c @@ -452,7 +452,7 @@ location_absolute_path_m(VALUE self) static VALUE location_format(VALUE file, int lineno, VALUE name) { - VALUE s = rb_enc_sprintf(rb_enc_compatible(file, name), "%s", RSTRING_PTR(file)); + VALUE s = rb_enc_sprintf(rb_enc_compatible(file, name), "%"PRIsVALUE, file); if (lineno != 0) { rb_str_catf(s, ":%d", lineno); } @@ -461,7 +461,7 @@ location_format(VALUE file, int lineno, VALUE name) rb_str_cat_cstr(s, "unknown method"); } else { - rb_str_catf(s, "'%s'", RSTRING_PTR(name)); + rb_str_catf(s, "'%"PRIsVALUE"'", name); } RB_GC_GUARD(name); return s; @@ -1114,12 +1114,13 @@ oldbt_print(void *data, VALUE file, int lineno, VALUE name) FILE *fp = (FILE *)data; if (NIL_P(name)) { - fprintf(fp, "\tfrom %s:%d:in unknown method\n", - RSTRING_PTR(file), lineno); + fprintf(fp, "\tfrom %.*s:%d:in unknown method\n", + RSTRING_LENINT(file), RSTRING_PTR(file), lineno); } else { - fprintf(fp, "\tfrom %s:%d:in '%s'\n", - RSTRING_PTR(file), lineno, RSTRING_PTR(name)); + fprintf(fp, "\tfrom %.*s:%d:in '%.*s'\n", + RSTRING_LENINT(file), RSTRING_PTR(file), lineno, + RSTRING_LENINT(name), RSTRING_PTR(name)); } } @@ -1148,16 +1149,20 @@ oldbt_bugreport(void *arg, VALUE file, int line, VALUE method) struct oldbt_bugreport_arg *p = arg; FILE *fp = p->fp; const char *filename = NIL_P(file) ? "ruby" : RSTRING_PTR(file); + int filename_len = NIL_P(file) ? 4 : RSTRING_LENINT(file); if (!p->count) { fprintf(fp, "-- Ruby level backtrace information " "----------------------------------------\n"); p->count = 1; } if (NIL_P(method)) { - fprintf(fp, "%s:%d:in unknown method\n", filename, line); + fprintf(fp, "%.*s:%d:in unknown method\n", + filename_len, filename, line); } else { - fprintf(fp, "%s:%d:in '%s'\n", filename, line, RSTRING_PTR(method)); + fprintf(fp, "%.*s:%d:in '%.*s'\n", + filename_len, filename, line, + RSTRING_LENINT(method), RSTRING_PTR(method)); } } diff --git a/vm_callinfo.h b/vm_callinfo.h index 3ce2f4a3916d29..5c7cc6757e9a03 100644 --- a/vm_callinfo.h +++ b/vm_callinfo.h @@ -439,11 +439,26 @@ vm_cc_valid(const struct rb_callcache *cc) return !UNDEF_P(cc->klass); } +/* Whether another ractor may have invalidated this cc mid-use (see + * vm_cc_invalidate()). Extensions cannot use rb_multi_ractor_p() here: with + * assertions enabled at -O0 (e.g. --enable-yjit=dev on macOS) the inline + * materializes references to core-internal globals (ruby_current_vm_ptr, + * ruby_single_main_ractor) that are not exported to them, and objspace.bundle + * fails to load. RUBY_EXPORT marks a core TU. */ +#ifdef RUBY_EXPORT +# define VM_CC_RACED_P() rb_multi_ractor_p() +#else +# define VM_CC_RACED_P() true +#endif + static inline const struct rb_callable_method_entry_struct * vm_cc_cme(const struct rb_callcache *cc) { VM_ASSERT(IMEMO_TYPE_P(cc, imemo_callcache)); - VM_ASSERT(cc->klass != Qundef || !vm_cc_markable(cc) || vm_cc_invalid_super(cc)); + // VM_CC_RACED_P(): another ractor can invalidate this cc (klass = Qundef) + // while a call using it is in flight; cme_/call_ stay intact, so the call + // proceeds with the method resolved before the redefinition. + VM_ASSERT(cc->klass != Qundef || !vm_cc_markable(cc) || vm_cc_invalid_super(cc) || VM_CC_RACED_P()); VM_ASSERT(cc_check_class(cc->klass)); VM_ASSERT(cc->call_ == NULL || // not initialized yet !vm_cc_markable(cc) || @@ -458,7 +473,7 @@ vm_cc_call(const struct rb_callcache *cc) { VM_ASSERT(IMEMO_TYPE_P(cc, imemo_callcache)); VM_ASSERT(cc->call_ != NULL); - VM_ASSERT(cc->klass != Qundef || !vm_cc_markable(cc) || vm_cc_invalid_super(cc)); + VM_ASSERT(cc->klass != Qundef || !vm_cc_markable(cc) || vm_cc_invalid_super(cc) || VM_CC_RACED_P()); VM_ASSERT(cc_check_class(cc->klass)); return cc->call_; } diff --git a/vm_core.h b/vm_core.h index 320d4cc7eb3f1d..b189023c4c63d5 100644 --- a/vm_core.h +++ b/vm_core.h @@ -1318,8 +1318,14 @@ typedef struct { unsigned int is_from_method: 1; /* bool */ unsigned int is_lambda: 1; /* bool */ unsigned int is_isolated: 1; /* bool */ + unsigned int is_refined: 1; /* bool: Proc#refined */ } rb_proc_t; +/* A refined proc's cref lives in a hidden ivar on the proc object; + * rb_proc_refinements_cref returns NULL unless is_refined is set. */ +const rb_cref_t *rb_proc_refinements_cref(VALUE procval); +void rb_proc_set_refinements_cref(VALUE procval, const rb_cref_t *cref); + RUBY_SYMBOL_EXPORT_BEGIN VALUE rb_proc_isolate(VALUE self); VALUE rb_proc_isolate_bang(VALUE self, VALUE replace_self); @@ -1954,6 +1960,7 @@ VALUE rb_thread_alloc(VALUE klass); VALUE rb_binding_alloc(VALUE klass); VALUE rb_proc_alloc(VALUE klass); VALUE rb_proc_dup(VALUE self); +VALUE rb_proc_dup_0(VALUE self); /* for debug */ extern bool rb_vmdebug_stack_dump_raw(const rb_execution_context_t *ec, const rb_control_frame_t *cfp, FILE *); @@ -1981,7 +1988,7 @@ void rb_iseq_pathobj_set(const rb_iseq_t *iseq, VALUE path, VALUE realpath); int rb_ec_frame_method_id_and_class(const rb_execution_context_t *ec, ID *idp, ID *called_idp, VALUE *klassp); void rb_ec_setup_exception(const rb_execution_context_t *ec, VALUE mesg, VALUE cause); -VALUE rb_vm_invoke_proc(rb_execution_context_t *ec, rb_proc_t *proc, int argc, const VALUE *argv, int kw_splat, VALUE block_handler); +VALUE rb_vm_invoke_proc(rb_execution_context_t *ec, rb_proc_t *proc, int argc, const VALUE *argv, int kw_splat, VALUE block_handler, const rb_cref_t *cref); VALUE rb_vm_make_proc_lambda(const rb_execution_context_t *ec, const struct rb_captured_block *captured, VALUE klass, int8_t is_lambda); static inline VALUE diff --git a/vm_dump.c b/vm_dump.c index 22ed7cfee58505..6c8342c88eef8e 100644 --- a/vm_dump.c +++ b/vm_dump.c @@ -59,6 +59,7 @@ control_frame_dump(const rb_execution_context_t *ec, const rb_control_frame_t *c char ep_in_heap = ' '; char posbuf[MAX_POSBUF+1]; int line = 0; + int iseq_name_len = -1; const char *magic, *iseq_name = "-", *selfstr = "-", *biseq_name = "-"; VALUE tmp; const rb_iseq_t *iseq = NULL; @@ -131,18 +132,22 @@ control_frame_dump(const rb_execution_context_t *ec, const rb_control_frame_t *c else if (SYMBOL_P((VALUE)iseq)) { tmp = rb_sym2str((VALUE)iseq); iseq_name = RSTRING_PTR(tmp); - snprintf(posbuf, MAX_POSBUF, ":%s", iseq_name); + iseq_name_len = RSTRING_LENINT(tmp); + snprintf(posbuf, MAX_POSBUF, ":%.*s", iseq_name_len, iseq_name); line = -1; } else { if (CFP_PC(cfp)) { pc = CFP_PC(cfp) - ISEQ_BODY(iseq)->iseq_encoded; iseq_name = RSTRING_PTR(ISEQ_BODY(iseq)->location.label); + iseq_name_len = RSTRING_LENINT(ISEQ_BODY(iseq)->location.label); if (pc >= 0 && (size_t)pc <= ISEQ_BODY(iseq)->iseq_size) { line = rb_vm_get_sourceline(cfp); } if (line) { - snprintf(posbuf, MAX_POSBUF, "%s:%d", RSTRING_PTR(rb_iseq_path(iseq)), line); + VALUE path = rb_iseq_path(iseq); + snprintf(posbuf, MAX_POSBUF, "%.*s:%d", + RSTRING_LENINT(path), RSTRING_PTR(path), line); } } else { @@ -183,7 +188,12 @@ control_frame_dump(const rb_execution_context_t *ec, const rb_control_frame_t *c } if (0) { kprintf(" \t"); - kprintf("iseq: %-24s ", iseq_name); + if (iseq_name_len < 0) { + kprintf("iseq: %-24s ", iseq_name); + } + else { + kprintf("iseq: %-24.*s ", iseq_name_len, iseq_name); + } kprintf("self: %-24s ", selfstr); kprintf("%-1s ", biseq_name); } @@ -286,7 +296,7 @@ box_env_dump(const rb_execution_context_t *ec, const VALUE *env, const rb_contro char ep_in_heap = ' '; char posbuf[MAX_POSBUF+1]; int line = 0; - const char *magic, *iseq_name = "-"; + const char *magic; VALUE tmp; const rb_iseq_t *iseq = NULL; const rb_box_t *box = NULL; @@ -347,35 +357,27 @@ box_env_dump(const rb_execution_context_t *ec, const VALUE *env, const rb_contro if (cfp && CFP_ISEQ(cfp)) { #define RUBY_VM_IFUNC_P(ptr) IMEMO_TYPE_P(ptr, imemo_ifunc) const rb_iseq_t *resolved_iseq = CFP_ISEQ(cfp); - if (RUBY_VM_IFUNC_P(resolved_iseq)) { - iseq_name = ""; - } - else if (SYMBOL_P((VALUE)resolved_iseq)) { + if (SYMBOL_P((VALUE)resolved_iseq)) { tmp = rb_sym2str((VALUE)resolved_iseq); - iseq_name = RSTRING_PTR(tmp); - snprintf(posbuf, MAX_POSBUF, ":%s", iseq_name); + snprintf(posbuf, MAX_POSBUF, ":%.*s", + RSTRING_LENINT(tmp), RSTRING_PTR(tmp)); line = -1; } - else { - if (CFP_PC(cfp)) { - iseq = resolved_iseq; - pc = CFP_PC(cfp) - ISEQ_BODY(iseq)->iseq_encoded; - iseq_name = RSTRING_PTR(ISEQ_BODY(iseq)->location.label); - if (pc >= 0 && (size_t)pc <= ISEQ_BODY(iseq)->iseq_size) { - line = rb_vm_get_sourceline(cfp); - } - if (line) { - snprintf(posbuf, MAX_POSBUF, "%s:%d", RSTRING_PTR(rb_iseq_path(iseq)), line); - } + else if (!RUBY_VM_IFUNC_P(resolved_iseq) && CFP_PC(cfp)) { + iseq = resolved_iseq; + pc = CFP_PC(cfp) - ISEQ_BODY(iseq)->iseq_encoded; + if (pc >= 0 && (size_t)pc <= ISEQ_BODY(iseq)->iseq_size) { + line = rb_vm_get_sourceline(cfp); } - else { - iseq_name = ""; + if (line) { + VALUE path = rb_iseq_path(iseq); + snprintf(posbuf, MAX_POSBUF, "%.*s:%d", + RSTRING_LENINT(path), RSTRING_PTR(path), line); } } } else if (me != NULL && IMEMO_TYPE_P(me, imemo_ment)) { - iseq_name = rb_id2name(me->def->original_id); - snprintf(posbuf, MAX_POSBUF, ":%s", iseq_name); + snprintf(posbuf, MAX_POSBUF, ":%s", rb_id2name(me->def->original_id)); line = -1; } diff --git a/vm_eval.c b/vm_eval.c index 049131b0914d7b..5bd4be41d12089 100644 --- a/vm_eval.c +++ b/vm_eval.c @@ -290,7 +290,8 @@ vm_call0_body(rb_execution_context_t *ec, struct rb_calling_info *calling, const { rb_proc_t *proc; GetProcPtr(calling->recv, proc); - ret = rb_vm_invoke_proc(ec, proc, calling->argc, argv, calling->kw_splat, calling->block_handler); + ret = rb_vm_invoke_proc(ec, proc, calling->argc, argv, calling->kw_splat, calling->block_handler, + rb_proc_refinements_cref(calling->recv)); goto success; } case OPTIMIZED_METHOD_TYPE_STRUCT_AREF: @@ -2005,7 +2006,7 @@ eval_string_with_cref(VALUE self, VALUE src, rb_cref_t *cref, VALUE file, int li /* TODO: what the code checking? */ if (!cref && block.as.captured.code.val) { rb_cref_t *orig_cref = vm_get_cref(vm_block_ep(&block)); - cref = vm_cref_dup(orig_cref); + cref = rb_vm_cref_dup(orig_cref); } vm_set_eval_stack(ec, iseq, cref, &block); @@ -2217,6 +2218,7 @@ yield_under(VALUE self, int singleton, int argc, const VALUE *argv, int kw_splat const VALUE *ep = NULL; rb_cref_t *cref; int is_lambda = FALSE; + const rb_cref_t *proc_cref = NULL; if (block_handler != VM_BLOCK_HANDLER_NONE) { again: @@ -2232,8 +2234,14 @@ yield_under(VALUE self, int singleton, int argc, const VALUE *argv, int kw_splat new_block_handler = VM_BH_FROM_IFUNC_BLOCK(&new_captured); break; case block_handler_type_proc: - is_lambda = rb_proc_lambda_p(block_handler) != Qfalse; - block_handler = vm_proc_to_block_handler(VM_BH_TO_PROC(block_handler)); + { + VALUE procval = VM_BH_TO_PROC(block_handler); + rb_proc_t *po; + GetProcPtr(procval, po); + is_lambda = po->is_lambda; + if (po->is_refined) proc_cref = rb_proc_refinements_cref(procval); + block_handler = vm_block_to_block_handler(&po->block); + } goto again; case block_handler_type_symbol: return rb_sym_proc_call(SYM2ID(VM_BH_TO_SYMBOL(block_handler)), @@ -2250,6 +2258,11 @@ yield_under(VALUE self, int singleton, int argc, const VALUE *argv, int kw_splat VM_ASSERT(singleton || RB_TYPE_P(self, T_MODULE) || RB_TYPE_P(self, T_CLASS)); cref = vm_cref_push(ec, self, ep, TRUE, singleton); + if (proc_cref && !NIL_P(CREF_REFINEMENTS(proc_cref))) { + CREF_REFINEMENTS_SET(cref, rb_hash_dup(CREF_REFINEMENTS(proc_cref))); + CREF_REFINED_PROC_SET(cref); + } + return vm_yield_with_cref(ec, argc, argv, kw_splat, cref, is_lambda); } diff --git a/vm_exec.h b/vm_exec.h index 0db016ec164f07..b234345d2e73c5 100644 --- a/vm_exec.h +++ b/vm_exec.h @@ -67,10 +67,11 @@ error ! #define INSN_ENTRY_SIG(insn) \ if (0) { \ - ruby_debug_printf("exec: %s@(%"PRIdPTRDIFF", %"PRIdPTRDIFF")@%s:%u\n", #insn, \ + VALUE path = rb_iseq_path(CFP_ISEQ(reg_cfp)); \ + ruby_debug_printf("exec: %s@(%"PRIdPTRDIFF", %"PRIdPTRDIFF")@%.*s:%u\n", #insn, \ (reg_pc - ISEQ_BODY(CFP_ISEQ(reg_cfp))->iseq_encoded), \ (reg_cfp->pc - ISEQ_BODY(CFP_ISEQ(reg_cfp))->iseq_encoded), \ - RSTRING_PTR(rb_iseq_path(CFP_ISEQ(reg_cfp))), \ + RSTRING_LENINT(path), RSTRING_PTR(path), \ rb_iseq_line_no(CFP_ISEQ(reg_cfp), reg_pc - ISEQ_BODY(CFP_ISEQ(reg_cfp))->iseq_encoded)); \ } diff --git a/vm_insnhelper.c b/vm_insnhelper.c index 1abf7dd122aefa..aff6ac1970abd9 100644 --- a/vm_insnhelper.c +++ b/vm_insnhelper.c @@ -908,7 +908,7 @@ cref_replace_with_duplicated_cref_each_frame(const VALUE *vptr, int can_be_svar, switch (imemo_type(v)) { case imemo_cref: cref = (rb_cref_t *)v; - new_cref = vm_cref_dup(cref); + new_cref = rb_vm_cref_dup(cref); if (parent) { RB_OBJ_WRITE(parent, vptr, new_cref); } @@ -5298,9 +5298,9 @@ vm_yield_setup_args(rb_execution_context_t *ec, const rb_iseq_t *iseq, const int /* ruby iseq -> ruby block */ static VALUE -vm_invoke_iseq_block(rb_execution_context_t *ec, rb_control_frame_t *reg_cfp, - struct rb_calling_info *calling, const struct rb_callinfo *ci, - bool is_lambda, VALUE block_handler) +vm_invoke_iseq_block_with_cref(rb_execution_context_t *ec, rb_control_frame_t *reg_cfp, + struct rb_calling_info *calling, const struct rb_callinfo *ci, + bool is_lambda, VALUE block_handler, const rb_cref_t *cref) { const struct rb_captured_block *captured = VM_BH_TO_ISEQ_BLOCK(block_handler); const rb_iseq_t *iseq = rb_iseq_check(captured->code.iseq); @@ -5312,10 +5312,11 @@ vm_invoke_iseq_block(rb_execution_context_t *ec, rb_control_frame_t *reg_cfp, SET_SP(rsp); + /* cref is normally 0; Proc#refined supplies a refinement cref */ vm_push_frame(ec, iseq, frame_flag, captured->self, - VM_GUARDED_PREV_EP(captured->ep), 0, + VM_GUARDED_PREV_EP(captured->ep), (VALUE)cref, ISEQ_BODY(iseq)->iseq_encoded + opt_pc, rsp + arg_size, ISEQ_BODY(iseq)->local_table_size - arg_size, ISEQ_BODY(iseq)->stack_max); @@ -5323,6 +5324,14 @@ vm_invoke_iseq_block(rb_execution_context_t *ec, rb_control_frame_t *reg_cfp, return Qundef; } +static VALUE +vm_invoke_iseq_block(rb_execution_context_t *ec, rb_control_frame_t *reg_cfp, + struct rb_calling_info *calling, const struct rb_callinfo *ci, + bool is_lambda, VALUE block_handler) +{ + return vm_invoke_iseq_block_with_cref(ec, reg_cfp, calling, ci, is_lambda, block_handler, NULL); +} + static VALUE vm_invoke_symbol_block(rb_execution_context_t *ec, rb_control_frame_t *reg_cfp, struct rb_calling_info *calling, const struct rb_callinfo *ci, @@ -5386,11 +5395,9 @@ vm_invoke_ifunc_block(rb_execution_context_t *ec, rb_control_frame_t *reg_cfp, return val; } -static VALUE -vm_proc_to_block_handler(VALUE procval) +static inline VALUE +vm_block_to_block_handler(const struct rb_block *block) { - const struct rb_block *block = vm_proc_block(procval); - switch (vm_block_type(block)) { case block_type_iseq: return VM_BH_FROM_ISEQ_BLOCK(&block->as.captured); @@ -5405,15 +5412,45 @@ vm_proc_to_block_handler(VALUE procval) return Qundef; } +static VALUE +vm_proc_to_block_handler(VALUE procval) +{ + return vm_block_to_block_handler(vm_proc_block(procval)); +} + +NOINLINE(static VALUE +vm_invoke_proc_block_with_cref(rb_execution_context_t *ec, rb_control_frame_t *reg_cfp, + struct rb_calling_info *calling, const struct rb_callinfo *ci, + bool is_lambda, VALUE block_handler, VALUE refined_procval)); +static VALUE +vm_invoke_proc_block_with_cref(rb_execution_context_t *ec, rb_control_frame_t *reg_cfp, + struct rb_calling_info *calling, const struct rb_callinfo *ci, + bool is_lambda, VALUE block_handler, VALUE refined_procval) +{ + const rb_cref_t *cref = rb_proc_refinements_cref(refined_procval); + return vm_invoke_iseq_block_with_cref(ec, reg_cfp, calling, ci, is_lambda, block_handler, cref); +} + static VALUE vm_invoke_proc_block(rb_execution_context_t *ec, rb_control_frame_t *reg_cfp, struct rb_calling_info *calling, const struct rb_callinfo *ci, bool is_lambda, VALUE block_handler) { + VALUE refined_procval = 0; + while (vm_block_handler_type(block_handler) == block_handler_type_proc) { - VALUE proc = VM_BH_TO_PROC(block_handler); - is_lambda = block_proc_is_lambda(proc); - block_handler = vm_proc_to_block_handler(proc); + VALUE procval = VM_BH_TO_PROC(block_handler); + rb_proc_t *po; + GetProcPtr(procval, po); + if (po->is_refined) refined_procval = procval; + is_lambda = po->is_lambda; + block_handler = vm_block_to_block_handler(&po->block); + } + + if (UNLIKELY(refined_procval) && vm_block_handler_type(block_handler) == block_handler_type_iseq) { + /* should not be inlined so that vm_invoke_proc_block stays leaf/frameless. */ + return vm_invoke_proc_block_with_cref(ec, reg_cfp, calling, ci, is_lambda, block_handler, + refined_procval); } return vm_invoke_block(ec, reg_cfp, calling, ci, is_lambda, block_handler); @@ -6548,7 +6585,11 @@ rb_vm_opt_getconstant_path(rb_execution_context_t *ec, rb_control_frame_t *const if (ice && vm_ic_hit_p(ice, GET_EP())) { val = ice->value; - VM_ASSERT(val == vm_get_ev_const_chain(ec, segments)); + // On a non-main ractor another ractor can concurrently reassign a + // shareable constant (which invalidates ICs asynchronously), so the + // cached-but-still-shareable value may legitimately differ from a + // freshly recomputed chain. Only assert equality when single-ractor. + VM_ASSERT(val == vm_get_ev_const_chain(ec, segments) || rb_multi_ractor_p()); } else { ruby_vm_constant_cache_misses++; @@ -7254,12 +7295,14 @@ vm_trace(rb_execution_context_t *ec, rb_control_frame_t *reg_cfp) rb_event_flag_t bmethod_events = ractor_events | bmethod_local_events; if (0) { - ruby_debug_printf("vm_trace>>%4d (%4x) - %s:%d %s\n", + VALUE path = rb_iseq_path(iseq); + VALUE label = rb_iseq_label(iseq); + ruby_debug_printf("vm_trace>>%4d (%4x) - %.*s:%d %.*s\n", (int)pos, (int)pc_events, - RSTRING_PTR(rb_iseq_path(iseq)), + RSTRING_LENINT(path), RSTRING_PTR(path), (int)rb_iseq_line_no(iseq, pos), - RSTRING_PTR(rb_iseq_label(iseq))); + RSTRING_LENINT(label), RSTRING_PTR(label)); } VM_ASSERT(reg_cfp->pc == pc); VM_ASSERT(pc_events != 0); diff --git a/vm_method.c b/vm_method.c index 43ca4313bca487..a51a6155506ebf 100644 --- a/vm_method.c +++ b/vm_method.c @@ -1513,9 +1513,9 @@ rb_method_entry_make(VALUE klass, ID mid, VALUE defined_class, rb_method_visibil } if (iseq) { rb_warning( - "method redefined; discarding old %"PRIsVALUE"\n%s:%d: warning: previous definition of %"PRIsVALUE" was here", + "method redefined; discarding old %"PRIsVALUE"\n%"PRIsVALUE":%d: warning: previous definition of %"PRIsVALUE" was here", rb_id2str(mid), - RSTRING_PTR(rb_iseq_path(iseq)), + rb_iseq_path(iseq), ISEQ_BODY(iseq)->location.first_lineno, rb_id2str(old_def->original_id) ); @@ -1832,9 +1832,9 @@ search_method0(VALUE klass, ID id, VALUE *defined_class_ptr, bool skip_refined) if (me == NULL) RB_DEBUG_COUNTER_INC(mc_search_notfound); VM_ASSERT(me == NULL || !METHOD_ENTRY_INVALIDATED(me), - "invalid me, mid:%s, klass:%s(%s)", + "invalid me, mid:%s, klass:%"PRIsVALUE"(%s)", rb_id2name(id), - RTEST(rb_mod_name(klass)) ? RSTRING_PTR(rb_mod_name(klass)) : "anonymous", + RTEST(rb_mod_name(klass)) ? rb_mod_name(klass) : rb_str_new_lit("anonymous"), rb_obj_info(klass)); return me; } diff --git a/vm_opts.h b/vm_opts.h index ce47745b1102de..8b8b47fa675985 100644 --- a/vm_opts.h +++ b/vm_opts.h @@ -44,7 +44,6 @@ /* VM running option */ #define OPT_CHECKED_RUN 1 #define OPT_INLINE_METHOD_CACHE 1 -#define OPT_GLOBAL_METHOD_CACHE 1 #ifndef OPT_IC_FOR_IVAR #define OPT_IC_FOR_IVAR 1 @@ -53,7 +52,6 @@ /* architecture independent, affects generated code */ #define OPT_OPERANDS_UNIFICATION 1 #define OPT_INSTRUCTIONS_UNIFICATION 0 -#define OPT_UNIFY_ALL_COMBINATION 0 /* misc */ #ifndef OPT_SUPPORT_JOKE diff --git a/win32/Makefile.sub b/win32/Makefile.sub index 619faf6663cae4..e5c099972d2860 100644 --- a/win32/Makefile.sub +++ b/win32/Makefile.sub @@ -320,7 +320,7 @@ LIBS = $(LIBS) imagehlp.lib shlwapi.lib bcrypt.lib $(EXTLIBS) !endif !if !defined(MISSING) MISSING = crypt.obj ffs.obj langinfo.obj lgamma_r.obj strlcat.obj strlcpy.obj win32/win32.obj win32/file.obj setproctitle.obj -MISSING = $(MISSING) explicit_bzero.obj +MISSING = $(MISSING) explicit_bzero.obj memrchr.obj !endif DLNOBJ = dln.obj diff --git a/yjit/src/cruby_bindings.inc.rs b/yjit/src/cruby_bindings.inc.rs index 47c62c4a7f3f63..48569908056e46 100644 --- a/yjit/src/cruby_bindings.inc.rs +++ b/yjit/src/cruby_bindings.inc.rs @@ -569,10 +569,22 @@ impl rb_proc_t { } } #[inline] + pub fn is_refined(&self) -> ::std::os::raw::c_uint { + unsafe { ::std::mem::transmute(self._bitfield_1.get(3usize, 1u8) as u32) } + } + #[inline] + pub fn set_is_refined(&mut self, val: ::std::os::raw::c_uint) { + unsafe { + let val: u32 = ::std::mem::transmute(val); + self._bitfield_1.set(3usize, 1u8, val as u64) + } + } + #[inline] pub fn new_bitfield_1( is_from_method: ::std::os::raw::c_uint, is_lambda: ::std::os::raw::c_uint, is_isolated: ::std::os::raw::c_uint, + is_refined: ::std::os::raw::c_uint, ) -> __BindgenBitfieldUnit<[u8; 1usize]> { let mut __bindgen_bitfield_unit: __BindgenBitfieldUnit<[u8; 1usize]> = Default::default(); __bindgen_bitfield_unit.set(0usize, 1u8, { @@ -587,6 +599,10 @@ impl rb_proc_t { let is_isolated: u32 = unsafe { ::std::mem::transmute(is_isolated) }; is_isolated as u64 }); + __bindgen_bitfield_unit.set(3usize, 1u8, { + let is_refined: u32 = unsafe { ::std::mem::transmute(is_refined) }; + is_refined as u64 + }); __bindgen_bitfield_unit } } diff --git a/zjit/src/codegen_tests.rs b/zjit/src/codegen_tests.rs index 09faf5814daa96..e53f265202d351 100644 --- a/zjit/src/codegen_tests.rs +++ b/zjit/src/codegen_tests.rs @@ -7145,3 +7145,33 @@ fn test_send_no_profiles_with_disabled_specialized_instruction() { fn test_array_each_is_defined_in_ruby() { assert_snapshot!(inspect("Array.instance_method(:each).source_location&.first"), @r#""""#); } + +#[test] +fn test_forward_fallback_with_lightweight_frame_reads_cfp() { + assert_snapshot!(inspect(r#" + class Base + def foo(...) + "base" + end + end + + class Child < Base + def foo(...) + bar do + super + end + end + + def bar + yield + end + end + + c = Child.new + 100.times do + Array.new(50) { |n| n * n } + c.foo(1, 2, 3) + end + :done + "#), @":done"); +} diff --git a/zjit/src/cruby.rs b/zjit/src/cruby.rs index 9024971d1ac61d..74f8e676a8acd4 100644 --- a/zjit/src/cruby.rs +++ b/zjit/src/cruby.rs @@ -265,7 +265,7 @@ pub struct ShapeId(pub u32); pub enum ShapeLayout { RObject, RClass, - RData, + Extended, Other, } @@ -288,7 +288,7 @@ impl ShapeId { match self.0 & SHAPE_ID_LAYOUT_MASK { SHAPE_ID_LAYOUT_ROBJECT => ShapeLayout::RObject, SHAPE_ID_LAYOUT_RCLASS => ShapeLayout::RClass, - SHAPE_ID_LAYOUT_EXTENDED => ShapeLayout::RData, + SHAPE_ID_LAYOUT_EXTENDED => ShapeLayout::Extended, SHAPE_ID_LAYOUT_OTHER => ShapeLayout::Other, layout => unreachable!("unknown shape layout bits: {layout:#x}"), } diff --git a/zjit/src/cruby_bindings.inc.rs b/zjit/src/cruby_bindings.inc.rs index b552e8323b3ec5..6ebcbd5335d5a6 100644 --- a/zjit/src/cruby_bindings.inc.rs +++ b/zjit/src/cruby_bindings.inc.rs @@ -1443,10 +1443,44 @@ impl rb_proc_t { } } #[inline] + pub fn is_refined(&self) -> ::std::os::raw::c_uint { + unsafe { ::std::mem::transmute(self._bitfield_1.get(3usize, 1u8) as u32) } + } + #[inline] + pub fn set_is_refined(&mut self, val: ::std::os::raw::c_uint) { + unsafe { + let val: u32 = ::std::mem::transmute(val); + self._bitfield_1.set(3usize, 1u8, val as u64) + } + } + #[inline] + pub unsafe fn is_refined_raw(this: *const Self) -> ::std::os::raw::c_uint { + unsafe { + ::std::mem::transmute(<__BindgenBitfieldUnit<[u8; 1usize]>>::raw_get( + ::std::ptr::addr_of!((*this)._bitfield_1), + 3usize, + 1u8, + ) as u32) + } + } + #[inline] + pub unsafe fn set_is_refined_raw(this: *mut Self, val: ::std::os::raw::c_uint) { + unsafe { + let val: u32 = ::std::mem::transmute(val); + <__BindgenBitfieldUnit<[u8; 1usize]>>::raw_set( + ::std::ptr::addr_of_mut!((*this)._bitfield_1), + 3usize, + 1u8, + val as u64, + ) + } + } + #[inline] pub fn new_bitfield_1( is_from_method: ::std::os::raw::c_uint, is_lambda: ::std::os::raw::c_uint, is_isolated: ::std::os::raw::c_uint, + is_refined: ::std::os::raw::c_uint, ) -> __BindgenBitfieldUnit<[u8; 1usize]> { let mut __bindgen_bitfield_unit: __BindgenBitfieldUnit<[u8; 1usize]> = Default::default(); __bindgen_bitfield_unit.set(0usize, 1u8, { @@ -1461,6 +1495,10 @@ impl rb_proc_t { let is_isolated: u32 = unsafe { ::std::mem::transmute(is_isolated) }; is_isolated as u64 }); + __bindgen_bitfield_unit.set(3usize, 1u8, { + let is_refined: u32 = unsafe { ::std::mem::transmute(is_refined) }; + is_refined as u64 + }); __bindgen_bitfield_unit } } diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 5fd2369537dd7e..ace63943053ce4 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -1868,6 +1868,19 @@ impl Insn { abstract_heaps::Allocator.includes(self.effects_of().write_bits()) } + + fn counts_against_inlining_budget(&self) -> bool { + match self { + // Don't count metadata-only instructions. + Insn::Comment { .. } + | Insn::IncrCounter { .. } + | Insn::IncrCounterPtr { .. } + | Insn::Snapshot { .. } + | Insn::PatchPoint { .. } + => false, + _ => true, + } + } } /// Print adaptor for [`Insn`]. See [`PtrPrintMap`]. @@ -2710,6 +2723,10 @@ pub struct Function { /// fulfilling `(0..=opt_num)` optional parameters. jit_entry_blocks: Vec, profiles: Option, + /// Rough estimate for the number of (actually executable) instructions in the function. Does + /// not count Snapshot, PatchPoint, etc. + /// Currently updated by `infer_types` as a heuristic but that is not a guarantee. + num_instructions: usize, } /// The kind of a value an ISEQ returns @@ -2842,6 +2859,7 @@ impl Function { jit_entry_blocks: vec![], param_types: vec![], profiles: None, + num_instructions: 0, } } @@ -3386,10 +3404,14 @@ impl Function { let rpo = self.reverse_post_order(); loop { let mut changed = false; + let mut num_instructions = 0; for &block in &rpo { if !reachable.get(block) { continue; } for i in 0..self.blocks[block.0].insns.len() { let insn_id = self.blocks[block.0].insns[i]; + if self.insns[insn_id.0].counts_against_inlining_budget() { + num_instructions += 1; + } // Instructions without output, including branch instructions, can't be targets // of make_equal_to, so we don't need find() here. let insn_type = match &self.insns[insn_id.0] { @@ -3438,6 +3460,7 @@ impl Function { } } if !changed { + self.num_instructions = num_instructions; break; } } @@ -4982,14 +5005,11 @@ impl Function { return false; } - // Per-caller cumulative budget. self.insns is append-only (InsnIds are stable - // indices), so its len() is a high-water mark of total HIR instructions ever - // allocated for this function — not the final compiled size. Once that count - // crosses the budget, every further callee is rejected and the optimization - // fixed-point loop reaches its terminal iteration. See `Options::inline_budget` - // for the full unit/semantics caveat. + // Per-caller cumulative budget. Once that count crosses the budget, every further callee + // is rejected and the optimization fixed-point loop reaches its terminal iteration. See + // `Options::inline_budget` for the full unit/semantics caveat. let budget = get_option!(inline_budget); - if budget != INLINE_BUDGET_UNLIMITED && self.insns.len() > budget { + if budget != INLINE_BUDGET_UNLIMITED && self.num_instructions > budget { incr_counter!(inline_reject_budget_exceeded); return false; } @@ -5083,6 +5103,9 @@ impl Function { let SendDirectData { recv, cme, iseq, args, kw_bits, jit_entry_idx, block: call_block, state, .. } = *data; // SendDirect invariant: block is either None or BlockIseq. // BlockArg is rejected upstream during type specialization. + // TODO(max): If we accept BlockArg here, we need to change the folding of Defined + // in HIR construction for the defined opcode to check the send flags of the method + // being inlined, too. let blockiseq: Option = call_block.map(|bh| match bh { BlockHandler::BlockIseq(bi) => bi, BlockHandler::BlockArg => unreachable!("BlockArg in SendDirect"), @@ -5164,6 +5187,9 @@ impl Function { } }; + // Bump the rough estimate of new instructions added by inlining this function + self.num_instructions += self.insns.len() - pre_insns_len; + // Past the point of no return: commit the inlining. incr_counter!(inline_method_count); did_inline = true; @@ -5371,7 +5397,7 @@ impl Function { let layout = recv_type.shape().layout(); match layout { - ShapeLayout::RClass | ShapeLayout::RData => { + ShapeLayout::RClass | ShapeLayout::Extended => { let offset = if layout == ShapeLayout::RClass { RCLASS_OFFSET_PRIME_FIELDS_OBJ } else { @@ -5434,18 +5460,11 @@ impl Function { return Err(Counter::setivar_fallback_immediate); } - match profiled_type.shape().layout() { - ShapeLayout::RObject => { - // OK - } - ShapeLayout::RData => { - // FIXME: we side exit for now as we're missing SHAPE_ID_FL_PRIVATE_MASK handling. - return Err(Counter::setivar_fallback_not_t_object); - } - _ => { - return Err(Counter::setivar_fallback_not_t_object); - } - } + let extended_robject = match profiled_type.shape().layout() { + ShapeLayout::RObject => false, + ShapeLayout::Extended if profiled_type.flags().is_t_object() => true, + _ => return Err(Counter::setivar_fallback_not_t_object), + }; assert!(profiled_type.shape().is_valid()); if profiled_type.shape().is_frozen() { @@ -5459,6 +5478,13 @@ impl Function { let mut ivar_index: attr_index_t = 0; let mut next_shape = profiled_type.shape(); if !unsafe { rb_shape_get_iv_index(profiled_type.shape().0, id, &mut ivar_index) } { + // Updating the fields object's shape requires preserving its private layout and + // capacity bits, which can differ from the owning RObject's. Existing ivars do not + // change either shape, so they can still use the fast path. + if extended_robject { + return Err(Counter::setivar_fallback_shape_transition); + } + // Current shape does not contain this ivar; do a shape transition. let current_shape_id = profiled_type.shape(); let class = profiled_type.class(); @@ -5496,7 +5522,7 @@ impl Function { ShapeLayout::RObject => { // AKA embedded (self_val, true) }, - ShapeLayout::RData => { // AKA extended + ShapeLayout::Extended => { let fields = self.load_field(block, self_val, FieldName::as_heap, ROBJECT_OFFSET_AS_HEAP_FIELDS, types::BasicObject); (fields, false) }, @@ -8216,18 +8242,31 @@ fn add_iseq_to_hir( // Similar to gen_is_block_given Insn::Const { val: Const::Value(Qnil) } } else { - // For DEFINED_YIELD, codegen materializes the local EP inline (similar to - // gen_is_block_given) to check for a block handler. Precompute the lexical - // distance from this iseq up to local_iseq so codegen does not have to - // walk the parent chain. Any DEFINED_YIELD reaching this branch has a - // method local_iseq by construction -- the above branch has already - // diverted the non-method case to Qnil. - let lep_level = if op_type == DEFINED_YIELD as usize { - get_lvar_level(iseq) + if op_type == DEFINED_YIELD as usize && matches!(mode, AddIseqMode::Inlined { .. }) { + // If we are inlining a method that has a blockiseq handler, we can fold Defined(DEFINED_YIELD). + // TODO(max): If we handle non-blockiseq block arguments such as + // &:symbol or just &block forwarding, we need to revisit this and + // check flags. + let has_block = matches!(mode, AddIseqMode::Inlined { blockiseq: Some(_), .. }); + if has_block { + Insn::Const { val: Const::Value(pushval) } + } else { + Insn::Const { val: Const::Value(Qnil) } + } } else { - 0 - }; - Insn::Defined { op_type, obj, pushval, v, lep_level, state: exit_id } + // For DEFINED_YIELD, codegen materializes the local EP inline (similar to + // gen_is_block_given) to check for a block handler. Precompute the lexical + // distance from this iseq up to local_iseq so codegen does not have to + // walk the parent chain. Any DEFINED_YIELD reaching this branch has a + // method local_iseq by construction -- the above branch has already + // diverted the non-method case to Qnil. + let lep_level = if op_type == DEFINED_YIELD as usize { + get_lvar_level(iseq) + } else { + 0 + }; + Insn::Defined { op_type, obj, pushval, v, lep_level, state: exit_id } + } }; state.stack_push(fun.push_insn(block, insn)); } @@ -8391,10 +8430,10 @@ fn add_iseq_to_hir( } } YARVINSN_branchunless | YARVINSN_branchunless_without_ints => { - if opcode == YARVINSN_branchunless { + let offset = get_arg(pc, 0).as_i64(); + if opcode == YARVINSN_branchunless && offset < 0 { fun.push_insn(block, Insn::CheckInterrupts { state: exit_id }); } - let offset = get_arg(pc, 0).as_i64(); let val = state.stack_pop()?; let test_id = fun.push_insn(block, Insn::Test { val }); let target_idx = insn_idx_at_offset(insn_idx, offset); @@ -8419,10 +8458,10 @@ fn add_iseq_to_hir( queue.push_back((state.clone(), target, target_idx, local_inval)); } YARVINSN_branchif | YARVINSN_branchif_without_ints => { - if opcode == YARVINSN_branchif { + let offset = get_arg(pc, 0).as_i64(); + if opcode == YARVINSN_branchif && offset < 0 { fun.push_insn(block, Insn::CheckInterrupts { state: exit_id }); } - let offset = get_arg(pc, 0).as_i64(); let val = state.stack_pop()?; let test_id = fun.push_insn(block, Insn::Test { val }); let target_idx = insn_idx_at_offset(insn_idx, offset); @@ -8448,10 +8487,10 @@ fn add_iseq_to_hir( queue.push_back((state.clone(), target, target_idx, local_inval)); } YARVINSN_branchnil | YARVINSN_branchnil_without_ints => { - if opcode == YARVINSN_branchnil { + let offset = get_arg(pc, 0).as_i64(); + if opcode == YARVINSN_branchnil && offset < 0 { fun.push_insn(block, Insn::CheckInterrupts { state: exit_id }); } - let offset = get_arg(pc, 0).as_i64(); let val = state.stack_pop()?; let test_id = fun.push_insn(block, Insn::HasType { val, expected: types::NilClass }); let target_idx = insn_idx_at_offset(insn_idx, offset); @@ -8513,7 +8552,7 @@ fn add_iseq_to_hir( } YARVINSN_jump | YARVINSN_jump_without_ints => { let offset = get_arg(pc, 0).as_i64(); - if opcode == YARVINSN_jump { + if opcode == YARVINSN_jump && offset < 0 { fun.push_insn(block, Insn::CheckInterrupts { state: exit_id }); } let target_idx = insn_idx_at_offset(insn_idx, offset); diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index b75bd87f178346..c374e98c85c9e1 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -52,9 +52,9 @@ mod hir_opt_tests { Jump bb3(v5, v6) bb3(v8:BasicObject, v9:NilClass): v13:TrueClass = Const Value(true) + v24:Fixnum[3] = Const Value(3) CheckInterrupts - v25:Fixnum[3] = Const Value(3) - Return v25 + Return v24 "); } @@ -84,9 +84,9 @@ mod hir_opt_tests { Jump bb3(v5, v6) bb3(v8:BasicObject, v9:NilClass): v13:FalseClass = Const Value(false) + v34:Fixnum[4] = Const Value(4) CheckInterrupts - v35:Fixnum[4] = Const Value(4) - Return v35 + Return v34 "); } @@ -957,10 +957,9 @@ mod hir_opt_tests { v10:Fixnum[1] = Const Value(1) v12:Fixnum[2] = Const Value(2) PatchPoint MethodRedefined(Integer@0x1000, <@0x1008, cme:0x1010) - v42:TrueClass = Const Value(true) + v23:Fixnum[3] = Const Value(3) CheckInterrupts - v24:Fixnum[3] = Const Value(3) - Return v24 + Return v23 "); } @@ -989,10 +988,9 @@ mod hir_opt_tests { v10:Fixnum[1] = Const Value(1) v12:Fixnum[2] = Const Value(2) PatchPoint MethodRedefined(Integer@0x1000, <=@0x1008, cme:0x1010) - v58:TrueClass = Const Value(true) + v35:Fixnum[3] = Const Value(3) CheckInterrupts - v37:Fixnum[3] = Const Value(3) - Return v37 + Return v35 "); } @@ -1021,10 +1019,9 @@ mod hir_opt_tests { v10:Fixnum[2] = Const Value(2) v12:Fixnum[1] = Const Value(1) PatchPoint MethodRedefined(Integer@0x1000, >@0x1008, cme:0x1010) - v42:TrueClass = Const Value(true) + v23:Fixnum[3] = Const Value(3) CheckInterrupts - v24:Fixnum[3] = Const Value(3) - Return v24 + Return v23 "); } @@ -1053,10 +1050,9 @@ mod hir_opt_tests { v10:Fixnum[2] = Const Value(2) v12:Fixnum[1] = Const Value(1) PatchPoint MethodRedefined(Integer@0x1000, >=@0x1008, cme:0x1010) - v58:TrueClass = Const Value(true) + v35:Fixnum[3] = Const Value(3) CheckInterrupts - v37:Fixnum[3] = Const Value(3) - Return v37 + Return v35 "); } @@ -1085,10 +1081,9 @@ mod hir_opt_tests { v10:Fixnum[1] = Const Value(1) v12:Fixnum[2] = Const Value(2) PatchPoint MethodRedefined(Integer@0x1000, ==@0x1008, cme:0x1010) - v42:FalseClass = Const Value(false) + v32:Fixnum[4] = Const Value(4) CheckInterrupts - v33:Fixnum[4] = Const Value(4) - Return v33 + Return v32 "); } @@ -1117,10 +1112,9 @@ mod hir_opt_tests { v10:Fixnum[2] = Const Value(2) v12:Fixnum[2] = Const Value(2) PatchPoint MethodRedefined(Integer@0x1000, ==@0x1008, cme:0x1010) - v42:TrueClass = Const Value(true) + v23:Fixnum[3] = Const Value(3) CheckInterrupts - v24:Fixnum[3] = Const Value(3) - Return v24 + Return v23 "); } @@ -1150,10 +1144,9 @@ mod hir_opt_tests { v12:Fixnum[2] = Const Value(2) PatchPoint MethodRedefined(Integer@0x1000, !=@0x1008, cme:0x1010) PatchPoint BOPRedefined(INTEGER_REDEFINED_OP_FLAG, BOP_EQ) - v43:TrueClass = Const Value(true) + v23:Fixnum[3] = Const Value(3) CheckInterrupts - v24:Fixnum[3] = Const Value(3) - Return v24 + Return v23 "); } @@ -1183,10 +1176,9 @@ mod hir_opt_tests { v12:Fixnum[2] = Const Value(2) PatchPoint MethodRedefined(Integer@0x1000, !=@0x1008, cme:0x1010) PatchPoint BOPRedefined(INTEGER_REDEFINED_OP_FLAG, BOP_EQ) - v43:FalseClass = Const Value(false) + v32:Fixnum[4] = Const Value(4) CheckInterrupts - v33:Fixnum[4] = Const Value(4) - Return v33 + Return v32 "); } @@ -5046,12 +5038,11 @@ mod hir_opt_tests { v12:ClassSubclass[C@0x1008] = Const Value(VALUE(0x1008)) v14:NilClass = Const Value(nil) PatchPoint MethodRedefined(C@0x1008, new@0x1009, cme:0x1010) - v45:ObjectSubclass[class_exact:C] = ObjectAllocClass C:VALUE(0x1008) + v44:ObjectSubclass[class_exact:C] = ObjectAllocClass C:VALUE(0x1008) PatchPoint NoSingletonClass(C@0x1008) PatchPoint MethodRedefined(C@0x1008, initialize@0x1038, cme:0x1040) - v50:NilClass = Const Value(nil) CheckInterrupts - Return v45 + Return v44 "); } @@ -5083,19 +5074,19 @@ mod hir_opt_tests { v14:NilClass = Const Value(nil) v17:Fixnum[1] = Const Value(1) PatchPoint MethodRedefined(C@0x1008, new@0x1009, cme:0x1010) - v48:ObjectSubclass[class_exact:C] = ObjectAllocClass C:VALUE(0x1008) + v47:ObjectSubclass[class_exact:C] = ObjectAllocClass C:VALUE(0x1008) PatchPoint NoSingletonClass(C@0x1008) PatchPoint MethodRedefined(C@0x1008, initialize@0x1038, cme:0x1040) - PushInlineFrame v48 (0x1068), v17 - v66:CShape = LoadField v48, :shape_id@0x1070 - v67:CShape[0x1071] = GuardBitEquals v66, CShape(0x1071) recompile - StoreField v48, :@x@0x1072, v17 - WriteBarrier v48, v17 - v70:CShape[0x1073] = Const CShape(0x1073) - StoreField v48, :shape_id@0x1070, v70 + PushInlineFrame v47 (0x1068), v17 + v65:CShape = LoadField v47, :shape_id@0x1070 + v66:CShape[0x1071] = GuardBitEquals v65, CShape(0x1071) recompile + StoreField v47, :@x@0x1072, v17 + WriteBarrier v47, v17 + v69:CShape[0x1073] = Const CShape(0x1073) + StoreField v47, :shape_id@0x1070, v69 CheckInterrupts PopInlineFrame - Return v48 + Return v47 "); } @@ -5121,12 +5112,11 @@ mod hir_opt_tests { v12:ClassSubclass[Object@0x1008] = Const Value(VALUE(0x1008)) v14:NilClass = Const Value(nil) PatchPoint MethodRedefined(Object@0x1008, new@0x1009, cme:0x1010) - v45:ObjectExact = ObjectAllocClass Object:VALUE(0x1008) + v44:ObjectExact = ObjectAllocClass Object:VALUE(0x1008) PatchPoint NoSingletonClass(Object@0x1008) PatchPoint MethodRedefined(Object@0x1008, initialize@0x1038, cme:0x1040) - v50:NilClass = Const Value(nil) CheckInterrupts - Return v45 + Return v44 "); } @@ -5152,12 +5142,11 @@ mod hir_opt_tests { v12:ClassSubclass[BasicObject@0x1008] = Const Value(VALUE(0x1008)) v14:NilClass = Const Value(nil) PatchPoint MethodRedefined(BasicObject@0x1008, new@0x1009, cme:0x1010) - v45:BasicObjectExact = ObjectAllocClass BasicObject:VALUE(0x1008) + v44:BasicObjectExact = ObjectAllocClass BasicObject:VALUE(0x1008) PatchPoint NoSingletonClass(BasicObject@0x1008) PatchPoint MethodRedefined(BasicObject@0x1008, initialize@0x1038, cme:0x1040) - v50:NilClass = Const Value(nil) CheckInterrupts - Return v45 + Return v44 "); } @@ -5183,29 +5172,29 @@ mod hir_opt_tests { v12:ClassSubclass[Hash@0x1008] = Const Value(VALUE(0x1008)) v14:NilClass = Const Value(nil) PatchPoint MethodRedefined(Hash@0x1008, new@0x1009, cme:0x1010) - v45:HashExact = ObjectAllocClass Hash:VALUE(0x1008) - v46:Fixnum[0] = Const Value(0) + v44:HashExact = ObjectAllocClass Hash:VALUE(0x1008) + v45:Fixnum[0] = Const Value(0) PatchPoint NoSingletonClass(Hash@0x1008) PatchPoint MethodRedefined(Hash@0x1008, initialize@0x1038, cme:0x1040) - v98:Fixnum[0] = Const Value(0) - v99:NilClass = Const Value(nil) - PushInlineFrame v45 (0x1068), v46 - v65:TrueClass = Const Value(true) - v83:CPtr = GetEP 0 - v84:CUInt64 = LoadField v83, :VM_ENV_DATA_INDEX_FLAGS@0x1070 - v85:CBool = IsBlockParamModified v84 - CondBranch v85, bb11(), bb12() + v97:Fixnum[0] = Const Value(0) + v98:NilClass = Const Value(nil) + PushInlineFrame v44 (0x1068), v45 + v64:TrueClass = Const Value(true) + v82:CPtr = GetEP 0 + v83:CUInt64 = LoadField v82, :VM_ENV_DATA_INDEX_FLAGS@0x1070 + v84:CBool = IsBlockParamModified v83 + CondBranch v84, bb11(), bb12() bb11(): - v87:BasicObject = LoadField v83, :block@0x1071 - Jump bb13(v87) + v86:BasicObject = LoadField v82, :block@0x1071 + Jump bb13(v86) bb12(): - v89:BasicObject = GetBlockParam :block, l0, EP@4 - Jump bb13(v89) - bb13(v82:BasicObject): - v92:BasicObject = InvokeBuiltin rb_hash_init, v45, v46, v65, v65, v82 + v88:BasicObject = GetBlockParam :block, l0, EP@4 + Jump bb13(v88) + bb13(v81:BasicObject): + v91:BasicObject = InvokeBuiltin rb_hash_init, v44, v45, v64, v64, v81 CheckInterrupts PopInlineFrame - Return v45 + Return v44 "); assert_snapshot!(inspect("test"), @"{}"); } @@ -5234,9 +5223,9 @@ mod hir_opt_tests { v17:Fixnum[1] = Const Value(1) PatchPoint MethodRedefined(Array@0x1008, new@0x1009, cme:0x1010) PatchPoint MethodRedefined(Class@0x1038, new@0x1009, cme:0x1010) - v56:BasicObject = CCallVariadic v12, :Array.new@0x1040, v17 + v55:BasicObject = CCallVariadic v12, :Array.new@0x1040, v17 CheckInterrupts - Return v56 + Return v55 "); } @@ -5265,10 +5254,10 @@ mod hir_opt_tests { v19:HeapBasicObject = ObjectAlloc v12 PatchPoint NoSingletonClass(Set@0x1008) PatchPoint MethodRedefined(Set@0x1008, initialize@0x1038, cme:0x1040) - v48:SetExact = GuardType v19, SetExact recompile - v49:BasicObject = CCallVariadic v48, :Set#initialize@0x1068 + v47:SetExact = GuardType v19, SetExact recompile + v48:BasicObject = CCallVariadic v47, :Set#initialize@0x1068 CheckInterrupts - Return v48 + Return v47 "); } @@ -5295,9 +5284,9 @@ mod hir_opt_tests { v14:NilClass = Const Value(nil) PatchPoint MethodRedefined(String@0x1008, new@0x1009, cme:0x1010) PatchPoint MethodRedefined(Class@0x1038, new@0x1009, cme:0x1010) - v53:BasicObject = CCallVariadic v12, :String.new@0x1040 + v52:BasicObject = CCallVariadic v12, :String.new@0x1040 CheckInterrupts - Return v53 + Return v52 "); } @@ -5325,12 +5314,12 @@ mod hir_opt_tests { v17:StringExact[VALUE(0x1010)] = Const Value(VALUE(0x1010)) v18:StringExact = StringCopy v17 PatchPoint MethodRedefined(Regexp@0x1008, new@0x1018, cme:0x1020) - v49:RegexpExact = ObjectAllocClass Regexp:VALUE(0x1008) + v48:RegexpExact = ObjectAllocClass Regexp:VALUE(0x1008) PatchPoint NoSingletonClass(Regexp@0x1008) PatchPoint MethodRedefined(Regexp@0x1008, initialize@0x1048, cme:0x1050) - v54:BasicObject = CCallVariadic v49, :Regexp#initialize@0x1078, v18 + v53:BasicObject = CCallVariadic v48, :Regexp#initialize@0x1078, v18 CheckInterrupts - Return v49 + Return v48 "); } @@ -5594,33 +5583,32 @@ mod hir_opt_tests { v9:BasicObject = LoadArg :block@2 Jump bb3(v7, v8, v9) bb3(v11:BasicObject, v12:BasicObject, v13:BasicObject): - CheckInterrupts - v19:CBool = Test v12 - v20:Falsy = RefineType v12, Falsy - CondBranch v19, bb5(), bb4(v11, v20, v13) + v18:CBool = Test v12 + v19:Falsy = RefineType v12, Falsy + CondBranch v18, bb5(), bb4(v11, v19, v13) bb5(): - v22:Truthy = RefineType v12, Truthy - v25:Fixnum[0] = Const Value(0) - v29:CPtr = GetEP 0 - v30:CUInt64 = LoadField v29, :VM_ENV_DATA_INDEX_FLAGS@0x1002 - v31:CBool = IsBlockParamModified v30 - CondBranch v31, bb6(), bb7() + v21:Truthy = RefineType v12, Truthy + v24:Fixnum[0] = Const Value(0) + v28:CPtr = GetEP 0 + v29:CUInt64 = LoadField v28, :VM_ENV_DATA_INDEX_FLAGS@0x1002 + v30:CBool = IsBlockParamModified v29 + CondBranch v30, bb6(), bb7() bb6(): - v33:BasicObject = LoadField v29, :block@0x1003 - Jump bb8(v33, v33) + v32:BasicObject = LoadField v28, :block@0x1003 + Jump bb8(v32, v32) bb7(): - v35:CInt64 = LoadField v29, :VM_ENV_DATA_INDEX_SPECVAL@0x1004 - v36:CInt64[0] = GuardBitEquals v35, CInt64(0) recompile - v37:NilClass = Const Value(nil) - Jump bb8(v37, v13) - bb8(v27:BasicObject, v28:BasicObject): - v40:BasicObject = Send v25, &block, :then, v27 # SendFallbackReason: Send: block argument is not nil + v34:CInt64 = LoadField v28, :VM_ENV_DATA_INDEX_SPECVAL@0x1004 + v35:CInt64[0] = GuardBitEquals v34, CInt64(0) recompile + v36:NilClass = Const Value(nil) + Jump bb8(v36, v13) + bb8(v26:BasicObject, v27:BasicObject): + v39:BasicObject = Send v24, &block, :then, v26 # SendFallbackReason: Send: block argument is not nil CheckInterrupts - Return v40 - bb4(v45:BasicObject, v46:Falsy, v47:BasicObject): - v51:StaticSymbol[:skip] = Const Value(VALUE(0x1008)) + Return v39 + bb4(v44:BasicObject, v45:Falsy, v46:BasicObject): + v50:StaticSymbol[:skip] = Const Value(VALUE(0x1008)) CheckInterrupts - Return v51 + Return v50 "); } @@ -6703,6 +6691,49 @@ mod hir_opt_tests { "); } + #[test] + fn test_specialize_monomorphic_setivar_on_extended_robject() { + let obj = eval(r#" + class ExtendedSetIvar + def test(value) + @v0 = value + end + end + + OBJ = ExtendedSetIvar.new + 10.times { |i| OBJ.instance_variable_set(:"@v#{i}", i) } + OBJ.test(10) + TEST = ExtendedSetIvar.instance_method(:test) + OBJ + "#); + assert!(!obj.embedded_p()); + + assert_snapshot!(hir_string_proc("TEST"), @" + fn test@:4: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:CPtr = LoadSP + v3:BasicObject = LoadField v2, :value@0x1000 + Jump bb3(v1, v3) + bb2(): + EntryPoint JIT(0) + v6:BasicObject = LoadArg :self@0 + v7:BasicObject = LoadArg :value@1 + Jump bb3(v6, v7) + bb3(v9:BasicObject, v10:BasicObject): + PatchPoint SingleRactorMode + v17:HeapBasicObject = GuardType v9, HeapBasicObject + v18:CShape = LoadField v17, :shape_id@0x1001 + v19:CShape[0x1002] = GuardBitEquals v18, CShape(0x1002) recompile + v20:BasicObject = LoadField v17, :as_heap@0x1003 + StoreField v20, :@v0@0x1003, v10 + WriteBarrier v20, v10 + CheckInterrupts + Return v10 + "); + } + #[test] fn test_specialize_monomorphic_setivar_with_shape_transition() { eval(" @@ -7526,10 +7557,9 @@ mod hir_opt_tests { v6:NilClass = Const Value(nil) Jump bb3(v5, v6) bb3(v8:BasicObject, v9:NilClass): - v13:NilClass = Const Value(nil) + v20:NilClass = Const Value(nil) CheckInterrupts - v21:NilClass = Const Value(nil) - Return v21 + Return v20 "); } @@ -7556,8 +7586,8 @@ mod hir_opt_tests { Jump bb3(v5, v6) bb3(v8:BasicObject, v9:NilClass): v13:Fixnum[1] = Const Value(1) - CheckInterrupts PatchPoint MethodRedefined(Integer@0x1000, itself@0x1008, cme:0x1010) + CheckInterrupts Return v13 "); } @@ -8387,38 +8417,36 @@ mod hir_opt_tests { v7:BasicObject = LoadArg :a@1 Jump bb3(v6, v7) bb3(v9:BasicObject, v10:BasicObject): - CheckInterrupts - v16:CBool = Test v10 - v17:Falsy = RefineType v10, Falsy - CondBranch v16, bb6(), bb4(v9, v17) + v15:CBool = Test v10 + v16:Falsy = RefineType v10, Falsy + CondBranch v15, bb6(), bb4(v9, v16) bb6(): - v19:Truthy = RefineType v10, Truthy - v21:FalseClass = Const Value(false) - CheckInterrupts - Jump bb5(v9, v19, v21) - bb4(v25:BasicObject, v26:Falsy): - v29:NilClass = Const Value(nil) - Jump bb5(v25, v26, v29) - bb5(v31:BasicObject, v32:BasicObject, v33:Falsy): - v38:CBool = HasType v33, FalseClass - CondBranch v38, bb8(), bb9() + v18:Truthy = RefineType v10, Truthy + v20:FalseClass = Const Value(false) + Jump bb5(v9, v18, v20) + bb4(v23:BasicObject, v24:Falsy): + v27:NilClass = Const Value(nil) + Jump bb5(v23, v24, v27) + bb5(v29:BasicObject, v30:BasicObject, v31:Falsy): + v36:CBool = HasType v31, FalseClass + CondBranch v36, bb8(), bb9() bb8(): PatchPoint MethodRedefined(FalseClass@0x1008, !@0x1010, cme:0x1018) - v59:TrueClass = Const Value(true) - Jump bb7(v59) + v57:TrueClass = Const Value(true) + Jump bb7(v57) bb9(): - v44:CBool = HasType v33, NilClass - CondBranch v44, bb10(), bb11() + v42:CBool = HasType v31, NilClass + CondBranch v42, bb10(), bb11() bb10(): PatchPoint MethodRedefined(NilClass@0x1040, !@0x1010, cme:0x1018) - v62:TrueClass = Const Value(true) - Jump bb7(v62) + v60:TrueClass = Const Value(true) + Jump bb7(v60) bb11(): - v50:BasicObject = Send v33, :! # SendFallbackReason: SendWithoutBlock: polymorphic fallback - Jump bb7(v50) - bb7(v37:BasicObject): + v48:BasicObject = Send v31, :! # SendFallbackReason: SendWithoutBlock: polymorphic fallback + Jump bb7(v48) + bb7(v35:BasicObject): CheckInterrupts - Return v37 + Return v35 "); } @@ -10035,7 +10063,7 @@ mod hir_opt_tests { 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)] recompile - v72:NilClass = Const Value(nil) + v71:NilClass = Const Value(nil) PushInlineFrame v18 (0x1038) v28:CPtr = GetEP 0 v29:CUInt64 = LoadField v28, :VM_ENV_DATA_INDEX_FLAGS@0x1040 @@ -10048,36 +10076,35 @@ mod hir_opt_tests { v34:CInt64 = LoadField v28, :VM_ENV_DATA_INDEX_SPECVAL@0x1042 v35:CInt64[0] = GuardBitEquals v34, CInt64(0) recompile v36:NilClass = Const Value(nil) - Jump bb9(v36, v72) + Jump bb9(v36, v71) bb9(v26:BasicObject, v27:BasicObject): - CheckInterrupts - v40:CBool = Test v26 - CondBranch v40, bb10(), bb6(v18, v27) + v39:CBool = Test v26 + CondBranch v39, bb10(), bb6(v18, v27) bb10(): - v47:CPtr = GetEP 0 - v48:CUInt64 = LoadField v47, :VM_ENV_DATA_INDEX_FLAGS@0x1040 - v49:CBool = IsBlockParamModified v48 - CondBranch v49, bb11(), bb12() + v46:CPtr = GetEP 0 + v47:CUInt64 = LoadField v46, :VM_ENV_DATA_INDEX_FLAGS@0x1040 + v48:CBool = IsBlockParamModified v47 + CondBranch v48, bb11(), bb12() bb11(): - v51:BasicObject = LoadField v47, :blk@0x1041 - Jump bb13(v51, v51) + v50:BasicObject = LoadField v46, :blk@0x1041 + Jump bb13(v50, v50) bb12(): - v53:CInt64 = LoadField v47, :VM_ENV_DATA_INDEX_SPECVAL@0x1042 - v54:CInt64 = GuardAnyBitSet v53, CUInt64(1) recompile - v55:ObjectSubclass[BlockParamProxy] = Const Value(VALUE(0x1048)) - Jump bb13(v55, v27) - bb13(v45:BasicObject, v46:BasicObject): - v58:BasicObject = Send v45, :call # SendFallbackReason: SendWithoutBlock: no profile data available - CheckInterrupts - Jump bb4(v58) - bb6(v63:ObjectSubclass[class_exact*:Object@VALUE(0x1000)], v64:BasicObject): - v67:Fixnum[42] = Const Value(42) - CheckInterrupts - Jump bb4(v67) - bb4(v73:BasicObject): + v52:CInt64 = LoadField v46, :VM_ENV_DATA_INDEX_SPECVAL@0x1042 + v53:CInt64 = GuardAnyBitSet v52, CUInt64(1) recompile + v54:ObjectSubclass[BlockParamProxy] = Const Value(VALUE(0x1048)) + Jump bb13(v54, v27) + bb13(v44:BasicObject, v45:BasicObject): + v57:BasicObject = Send v44, :call # SendFallbackReason: SendWithoutBlock: no profile data available + CheckInterrupts + Jump bb4(v57) + bb6(v62:ObjectSubclass[class_exact*:Object@VALUE(0x1000)], v63:BasicObject): + v66:Fixnum[42] = Const Value(42) + CheckInterrupts + Jump bb4(v66) + bb4(v72:BasicObject): PopInlineFrame CheckInterrupts - Return v73 + Return v72 "); } @@ -10106,12 +10133,12 @@ mod hir_opt_tests { 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)] recompile - v49:NilClass = Const Value(nil) + v48:NilClass = Const Value(nil) PushInlineFrame v19 (0x1038) + v43:Fixnum[42] = Const Value(42) CheckInterrupts - v44:Fixnum[42] = Const Value(42) PopInlineFrame - Return v44 + Return v43 "); } @@ -15021,16 +15048,15 @@ 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] recompile - v44:ClassSubclass[C@0x1000] = Const Value(VALUE(0x1000)) + v42:ObjectSubclass[class_exact:C] = GuardType v6, ObjectSubclass[class_exact:C] recompile + v43:ClassSubclass[C@0x1000] = Const Value(VALUE(0x1000)) v13:StaticSymbol[:_lex_actions] = Const Value(VALUE(0x1038)) v15:TrueClass = Const Value(true) PatchPoint MethodRedefined(Class@0x1040, respond_to?@0x1048, cme:0x1050) PatchPoint MethodRedefined(Class@0x1040, _lex_actions@0x1078, cme:0x1080) - v50:TrueClass = Const Value(true) + v25:StaticSymbol[:CORRECT] = Const Value(VALUE(0x10a8)) CheckInterrupts - v26:StaticSymbol[:CORRECT] = Const Value(VALUE(0x10a8)) - Return v26 + Return v25 "); } @@ -15220,36 +15246,35 @@ mod hir_opt_tests { StoreField v11, :formatted@0x1004, v17 Jump bb3(v9, v10, v13, v15, v17) bb3(v20:BasicObject, v21:BasicObject, v22:BasicObject, v23:BasicObject, v24:NilClass): - CheckInterrupts SetLocal :formatted, l0, EP@3, v21 PatchPoint SingleRactorMode - v48:HeapBasicObject = GuardType v20, HeapBasicObject - v49:CShape = LoadField v48, :shape_id@0x1005 - v50:CShape[0x1006] = Const CShape(0x1006) - v51:CBool = IsBitEqual v49, v50 - CondBranch v51, bb7(), bb8() + v47:HeapBasicObject = GuardType v20, HeapBasicObject + v48:CShape = LoadField v47, :shape_id@0x1005 + v49:CShape[0x1006] = Const CShape(0x1006) + v50:CBool = IsBitEqual v48, v49 + CondBranch v50, bb7(), bb8() bb7(): - StoreField v48, :@formatted@0x1007, v21 - WriteBarrier v48, v21 + StoreField v47, :@formatted@0x1007, v21 + WriteBarrier v47, v21 Jump bb6() bb8(): - v56:CShape[0x1008] = GuardBitEquals v49, CShape(0x1008) recompile - StoreField v48, :@formatted@0x1007, v21 - WriteBarrier v48, v21 - v60:CShape[0x1006] = Const CShape(0x1006) - StoreField v48, :shape_id@0x1005, v60 + v55:CShape[0x1008] = GuardBitEquals v48, CShape(0x1008) recompile + StoreField v47, :@formatted@0x1007, v21 + WriteBarrier v47, v21 + v59:CShape[0x1006] = Const CShape(0x1006) + StoreField v47, :shape_id@0x1005, v59 Jump bb6() bb6(): - v66:ClassSubclass[VMFrozenCore] = Const Value(VALUE(0x1010)) + v65:ClassSubclass[VMFrozenCore] = Const Value(VALUE(0x1010)) PatchPoint MethodRedefined(Class@0x1018, lambda@0x1020, cme:0x1028) - v82:BasicObject = CCallWithFrame v66, :RubyVM::FrozenCore.lambda@0x1050, block=0x1058 - v69:CPtr = GetEP 0 - v70:BasicObject = LoadField v69, :a@0x1001 - v71:BasicObject = LoadField v69, :_b@0x1002 - v72:BasicObject = LoadField v69, :_c@0x1003 - v73:BasicObject = LoadField v69, :formatted@0x1004 + v81:BasicObject = CCallWithFrame v65, :RubyVM::FrozenCore.lambda@0x1050, block=0x1058 + v68:CPtr = GetEP 0 + v69:BasicObject = LoadField v68, :a@0x1001 + v70:BasicObject = LoadField v68, :_b@0x1002 + v71:BasicObject = LoadField v68, :_c@0x1003 + v72:BasicObject = LoadField v68, :formatted@0x1004 CheckInterrupts - Return v82 + Return v81 "); } @@ -16631,19 +16656,18 @@ mod hir_opt_tests { v7:BasicObject = LoadArg :x@1 Jump bb3(v6, v7) bb3(v9:BasicObject, v10:BasicObject): - CheckInterrupts - v16:CBool = Test v10 - v17:Falsy = RefineType v10, Falsy - CondBranch v16, bb7(), bb6(v9, v17) + v15:CBool = Test v10 + v16:Falsy = RefineType v10, Falsy + CondBranch v15, bb7(), bb6(v9, v16) bb7(): - v19:Truthy = RefineType v10, Truthy + v18:Truthy = RefineType v10, Truthy + v35:Fixnum[3] = Const Value(3) CheckInterrupts - v38:Fixnum[3] = Const Value(3) - Return v38 - bb6(v43:BasicObject, v44:Falsy): - v48:Fixnum[6] = Const Value(6) + Return v35 + bb6(v40:BasicObject, v41:Falsy): + v45:Fixnum[6] = Const Value(6) CheckInterrupts - Return v48 + Return v45 "); } @@ -17125,38 +17149,37 @@ mod hir_opt_tests { StoreField v42, :kwsplat@0x1006, v48 Jump bb7(v40, v41, v44, v46, v48) bb7(v73:BasicObject, v74:BasicObject, v75:BasicObject, v76:BasicObject, v77:NilClass): - CheckInterrupts - v83:CBool = Test v75 - v84:Truthy = RefineType v75, Truthy - CondBranch v83, bb8(v73, v74, v84, v76, v77), bb11() + v82:CBool = Test v75 + v83:Truthy = RefineType v75, Truthy + CondBranch v82, bb8(v73, v74, v83, v76, v77), bb11() bb11(): - v86:Falsy = RefineType v75, Falsy + v85:Falsy = RefineType v75, Falsy PatchPoint MethodRedefined(Object@0x1010, lambda@0x1018, cme:0x1020) - v132:ObjectSubclass[class_exact*:Object@VALUE(0x1010)] = GuardType v73, ObjectSubclass[class_exact*:Object@VALUE(0x1010)] recompile - v133:BasicObject = CCallWithFrame v132, :Kernel#lambda@0x1048, block=0x1050 - v90:CPtr = GetEP 0 - v91:BasicObject = LoadField v90, :list@0x1001 - v93:BasicObject = LoadField v90, :iter_method@0x1005 - v94:BasicObject = LoadField v90, :kwsplat@0x1006 - SetLocal :sep, l0, EP@5, v133 - Jump bb8(v132, v91, v133, v93, v94) - bb8(v98:BasicObject, v99:BasicObject, v100:BasicObject, v101:BasicObject, v102:BasicObject): + v131:ObjectSubclass[class_exact*:Object@VALUE(0x1010)] = GuardType v73, ObjectSubclass[class_exact*:Object@VALUE(0x1010)] recompile + v132:BasicObject = CCallWithFrame v131, :Kernel#lambda@0x1048, block=0x1050 + v89:CPtr = GetEP 0 + v90:BasicObject = LoadField v89, :list@0x1001 + v92:BasicObject = LoadField v89, :iter_method@0x1005 + v93:BasicObject = LoadField v89, :kwsplat@0x1006 + SetLocal :sep, l0, EP@5, v132 + Jump bb8(v131, v90, v132, v92, v93) + bb8(v97:BasicObject, v98:BasicObject, v99:BasicObject, v100:BasicObject, v101:BasicObject): PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1058, CONST) - v108:HashExact[VALUE(0x1060)] = Const Value(VALUE(0x1060)) - SetLocal :kwsplat, l0, EP@3, v108 - v113:CPtr = GetEP 0 - v114:BasicObject = LoadField v113, :list@0x1001 - v116:CPtr = GetEP 0 - v117:BasicObject = LoadField v116, :iter_method@0x1005 - v119:BasicObject = Send v114, 0x1068, :__send__, v117 # SendFallbackReason: Send: unsupported method type Optimized - v120:CPtr = GetEP 0 - v121:BasicObject = LoadField v120, :list@0x1001 - v122:BasicObject = LoadField v120, :sep@0x1002 - v123:BasicObject = LoadField v120, :iter_method@0x1005 - v124:BasicObject = LoadField v120, :kwsplat@0x1006 + v107:HashExact[VALUE(0x1060)] = Const Value(VALUE(0x1060)) + SetLocal :kwsplat, l0, EP@3, v107 + v112:CPtr = GetEP 0 + v113:BasicObject = LoadField v112, :list@0x1001 + v115:CPtr = GetEP 0 + v116:BasicObject = LoadField v115, :iter_method@0x1005 + v118:BasicObject = Send v113, 0x1068, :__send__, v116 # SendFallbackReason: Send: unsupported method type Optimized + v119:CPtr = GetEP 0 + v120:BasicObject = LoadField v119, :list@0x1001 + v121:BasicObject = LoadField v119, :sep@0x1002 + v122:BasicObject = LoadField v119, :iter_method@0x1005 + v123:BasicObject = LoadField v119, :kwsplat@0x1006 CheckInterrupts - Return v119 + Return v118 "); } @@ -17218,6 +17241,112 @@ mod hir_opt_tests { "); } + #[test] + fn test_inline_with_block_folds_defined_yield() { + eval(r" + def foo = defined?(yield) + def test = foo { |x| x } + test + "); + assert_snapshot!(hir_string("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): + PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) + v18:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile + PushInlineFrame v18 (0x1038) + v27:StringExact[VALUE(0x1040)] = Const Value(VALUE(0x1040)) + CheckInterrupts + PopInlineFrame + Return v27 + "); + } + + #[test] + fn test_inline_without_block_folds_defined_yield() { + eval(r" + def foo = defined?(yield) + def test = foo + test + "); + assert_snapshot!(hir_string("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): + PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) + v18:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile + PushInlineFrame v18 (0x1038) + v27:NilClass = Const Value(nil) + CheckInterrupts + PopInlineFrame + Return v27 + "); + } + + #[test] + fn test_inline_array_each_with_block_folds_defined_yield() { + set_inline_threshold(100); + eval(r" + def test = [1, 2, 3].each { |x| x } + test + "); + assert_snapshot!(hir_string("test"), @" + fn test@:2: + 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:ArrayExact[VALUE(0x1000)] = Const Value(VALUE(0x1000)) + v11:ArrayExact = ArrayDup v10 + PatchPoint NoSingletonClass(Array@0x1008) + PatchPoint MethodRedefined(Array@0x1008, each@0x1010, cme:0x1018) + PushInlineFrame v11 (0x1040) + v51:Fixnum[0] = Const Value(0) + Jump bb10(v11, v51) + bb10(v64:ArrayExact, v65:Fixnum): + v69:CInt64 = ArrayLength v64 + v70:Fixnum = BoxFixnum v69 + v71:BoolExact = FixnumGe v65, v70 + v73:CBool = Test v71 + CondBranch v73, bb13(), bb9(v64, v65) + bb13(): + CheckInterrupts + PopInlineFrame + Return v64 + bb9(v86:ArrayExact, v87:Fixnum): + v92:CInt64 = UnboxFixnum v87 + v93:BasicObject = ArrayAref v86, v92 + v95:CPtr = GetEP 0 + v96:CInt64 = LoadField v95, :VM_ENV_DATA_INDEX_SPECVAL@0x1048 + v97:CInt64[-4] = Const CInt64(-4) + v98:CInt64 = IntAnd v96, v97 + v99:BasicObject = InvokeBlockIseqDirect (0x1050), v98, v93 + v103:Fixnum[1] = Const Value(1) + v104:Fixnum = FixnumAdd v87, v103 + PatchPoint NoEPEscape(each) + Jump bb10(v86, v104) + "); + } + #[test] fn test_delete_duplicate_store() { eval(" @@ -17442,26 +17571,25 @@ mod hir_opt_tests { v7:BasicObject = LoadArg :flag@1 Jump bb3(v6, v7) bb3(v9:BasicObject, v10:BasicObject): - CheckInterrupts - v16:CBool = Test v10 - v17:Falsy = RefineType v10, Falsy - CondBranch v16, bb5(), bb4(v9, v17) + v15:CBool = Test v10 + v16:Falsy = RefineType v10, Falsy + CondBranch v15, bb5(), bb4(v9, v16) bb5(): - v19:Truthy = RefineType v10, Truthy - v23:Fixnum[42] = Const Value(42) + v18:Truthy = RefineType v10, Truthy + v22: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)] recompile - PushInlineFrame v43 (0x1040), v23 + v42:ObjectSubclass[class_exact*:Object@VALUE(0x1008)] = GuardType v9, ObjectSubclass[class_exact*:Object@VALUE(0x1008)] recompile + PushInlineFrame v42 (0x1040), v22 PatchPoint MethodRedefined(Integer@0x1048, to_s@0x1050, cme:0x1058) - v64:StringExact = CCallVariadic v23, :Integer#to_s@0x1080 + v63:StringExact = CCallVariadic v22, :Integer#to_s@0x1080 CheckInterrupts PopInlineFrame - Return v64 - bb4(v30:BasicObject, v31:Falsy): - v35:StringExact[VALUE(0x1088)] = Const Value(VALUE(0x1088)) - v36:StringExact = StringCopy v35 + Return v63 + bb4(v29:BasicObject, v30:Falsy): + v34:StringExact[VALUE(0x1088)] = Const Value(VALUE(0x1088)) + v35:StringExact = StringCopy v34 CheckInterrupts - Return v36 + Return v35 "); } @@ -17515,21 +17643,20 @@ mod hir_opt_tests { v9:BasicObject = LoadArg :block@2 Jump bb3(v7, v8, v9) bb3(v11:BasicObject, v12:BasicObject, v13:BasicObject): - CheckInterrupts - v19:CBool = Test v12 - v20:Falsy = RefineType v12, Falsy - CondBranch v19, bb5(), bb4(v11, v20, v13) + v18:CBool = Test v12 + v19:Falsy = RefineType v12, Falsy + CondBranch v18, bb5(), bb4(v11, v19, v13) bb5(): - v22:Truthy = RefineType v12, Truthy - v26:Fixnum[42] = Const Value(42) - v29:BasicObject = Send v11, &block, :passthrough_recompile_blockarg, v26, v13 # SendFallbackReason: Send: block argument is not nil + v21:Truthy = RefineType v12, Truthy + v25:Fixnum[42] = Const Value(42) + v28:BasicObject = Send v11, &block, :passthrough_recompile_blockarg, v25, v13 # SendFallbackReason: Send: block argument is not nil CheckInterrupts - Return v29 - bb4(v34:BasicObject, v35:Falsy, v36:BasicObject): - v40:StringExact[VALUE(0x1008)] = Const Value(VALUE(0x1008)) - v41:StringExact = StringCopy v40 + Return v28 + bb4(v33:BasicObject, v34:Falsy, v35:BasicObject): + v39:StringExact[VALUE(0x1008)] = Const Value(VALUE(0x1008)) + v40:StringExact = StringCopy v39 CheckInterrupts - Return v41 + Return v40 "); } @@ -17575,21 +17702,20 @@ mod hir_opt_tests { v7:BasicObject = LoadArg :flag@1 Jump bb3(v6, v7) bb3(v9:BasicObject, v10:BasicObject): - CheckInterrupts - v16:CBool = Test v10 - v17:Falsy = RefineType v10, Falsy - CondBranch v16, bb5(), bb4(v9, v17) + v15:CBool = Test v10 + v16:Falsy = RefineType v10, Falsy + CondBranch v15, bb5(), bb4(v9, v16) bb5(): - v19:Truthy = RefineType v10, Truthy - v23:Fixnum[42] = Const Value(42) - v25:BasicObject = Send v9, :greet_final, v23 # SendFallbackReason: SendWithoutBlock: no profile data available + v18:Truthy = RefineType v10, Truthy + v22:Fixnum[42] = Const Value(42) + v24:BasicObject = Send v9, :greet_final, v22 # SendFallbackReason: SendWithoutBlock: no profile data available CheckInterrupts - Return v25 - bb4(v30:BasicObject, v31:Falsy): - v35:StringExact[VALUE(0x1008)] = Const Value(VALUE(0x1008)) - v36:StringExact = StringCopy v35 + Return v24 + bb4(v29:BasicObject, v30:Falsy): + v34:StringExact[VALUE(0x1008)] = Const Value(VALUE(0x1008)) + v35:StringExact = StringCopy v34 CheckInterrupts - Return v36 + Return v35 "); } @@ -17784,52 +17910,50 @@ mod hir_opt_tests { Jump bb3(v5, v6) bb3(v8:HeapBasicObject, v9:NilClass): v13:Fixnum[0] = Const Value(0) - CheckInterrupts Jump bb6(v8, v13) - bb6(v19:HeapBasicObject, v20:Fixnum): - v24:Fixnum[10] = Const Value(10) + bb6(v18:HeapBasicObject, v19:Fixnum): + v23:Fixnum[10] = Const Value(10) PatchPoint MethodRedefined(Integer@0x1000, <@0x1008, cme:0x1010) - v96:BoolExact = FixnumLt v20, v24 + v94:BoolExact = FixnumLt v19, v23 CheckInterrupts - v30:CBool = Test v96 - CondBranch v30, bb4(v19, v20), bb7() - bb4(v40:HeapBasicObject, v41:Fixnum): + v29:CBool = Test v94 + CondBranch v29, bb4(v18, v19), bb7() + bb4(v39:HeapBasicObject, v40:Fixnum): PatchPoint SingleRactorMode - v47:CShape = LoadField v40, :shape_id@0x1038 - v49:CShape[0x1039] = Const CShape(0x1039) - v50:CBool = IsBitEqual v47, v49 - CondBranch v50, bb9(), bb10() + v46:CShape = LoadField v39, :shape_id@0x1038 + v48:CShape[0x1039] = Const CShape(0x1039) + v49:CBool = IsBitEqual v46, v48 + CondBranch v49, bb9(), bb10() bb9(): - v52:BasicObject = LoadField v40, :@levar@0x103a - Jump bb8(v52) + v51:BasicObject = LoadField v39, :@levar@0x103a + Jump bb8(v51) bb10(): - v54:CShape[0x103b] = GuardBitEquals v47, CShape(0x103b) recompile - v56:NilClass = Const Value(nil) - Jump bb8(v56) - bb8(v48:BasicObject): - CheckInterrupts - v60:CBool = Test v48 - CondBranch v60, bb5(v40, v41), bb12() + v53:CShape[0x103b] = GuardBitEquals v46, CShape(0x103b) recompile + v55:NilClass = Const Value(nil) + Jump bb8(v55) + bb8(v47:BasicObject): + v58:CBool = Test v47 + CondBranch v58, bb5(v39, v40), bb12() bb12(): PatchPoint NoEPEscape(set_value_loop) PatchPoint SingleRactorMode - v70:CShape = LoadField v40, :shape_id@0x1038 - v71:CShape[0x103b] = GuardBitEquals v70, CShape(0x103b) recompile - StoreField v40, :@levar@0x103a, v41 - WriteBarrier v40, v41 - v74:CShape[0x1039] = Const CShape(0x1039) - StoreField v40, :shape_id@0x1038, v74 - Jump bb5(v40, v41) - bb5(v78:HeapBasicObject, v79:Fixnum): + v68:CShape = LoadField v39, :shape_id@0x1038 + v69:CShape[0x103b] = GuardBitEquals v68, CShape(0x103b) recompile + StoreField v39, :@levar@0x103a, v40 + WriteBarrier v39, v40 + v72:CShape[0x1039] = Const CShape(0x1039) + StoreField v39, :shape_id@0x1038, v72 + Jump bb5(v39, v40) + bb5(v76:HeapBasicObject, v77:Fixnum): PatchPoint NoEPEscape(set_value_loop) - v86:Fixnum[1] = Const Value(1) + v84:Fixnum[1] = Const Value(1) PatchPoint MethodRedefined(Integer@0x1000, +@0x103c, cme:0x1040) - v100:Fixnum = FixnumAdd v79, v86 - Jump bb6(v78, v100) + v98:Fixnum = FixnumAdd v77, v84 + Jump bb6(v76, v98) bb7(): - v35:NilClass = Const Value(nil) + v34:NilClass = Const Value(nil) CheckInterrupts - Return v35 + Return v34 "); } @@ -18532,19 +18656,17 @@ 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 recompile - v49:CBool = IsBitEqual v48, v13 - v50:BoolExact = BoxBool v49 - CheckInterrupts - CondBranch v49, bb5(), bb4(v11, v48, v13) + v47:StaticSymbol = GuardType v12, StaticSymbol recompile + v48:CBool = IsBitEqual v47, v13 + CondBranch v48, bb5(), bb4(v11, v47, v13) bb5(): - v29:Fixnum[3] = Const Value(3) + v28:Fixnum[3] = Const Value(3) CheckInterrupts - Return v29 - bb4(v34:BasicObject, v35:StaticSymbol, v36:BasicObject): - v40:Fixnum[4] = Const Value(4) + Return v28 + bb4(v33:BasicObject, v34:StaticSymbol, v35:BasicObject): + v39:Fixnum[4] = Const Value(4) CheckInterrupts - Return v40 + Return v39 "); } @@ -18812,22 +18934,21 @@ mod hir_opt_tests { PushInlineFrame v23 (0x1040), v10 v32:Fixnum[0] = Const Value(0) PatchPoint MethodRedefined(Integer@0x1048, <@0x1050, cme:0x1058) - v63:Fixnum = GuardType v10, Fixnum recompile - v64:BoolExact = FixnumLt v63, v32 - CheckInterrupts - v38:CBool = Test v64 - CondBranch v38, bb7(), bb6(v23, v63) + v62:Fixnum = GuardType v10, Fixnum recompile + v63:BoolExact = FixnumLt v62, v32 + v37:CBool = Test v63 + CondBranch v37, bb7(), bb6(v23, v62) bb7(): - v43:Fixnum[0] = Const Value(0) + v42:Fixnum[0] = Const Value(0) CheckInterrupts - Jump bb4(v43) - bb6(v48:ObjectSubclass[class_exact*:Object@VALUE(0x1008)], v49:Fixnum): + Jump bb4(v42) + bb6(v47:ObjectSubclass[class_exact*:Object@VALUE(0x1008)], v48:Fixnum): CheckInterrupts - Jump bb4(v49) - bb4(v57:Fixnum): + Jump bb4(v48) + bb4(v56:Fixnum): PopInlineFrame CheckInterrupts - Return v57 + Return v56 "); } @@ -19681,26 +19802,25 @@ mod hir_opt_tests { v22:NilClass = Const Value(nil) PatchPoint MethodRedefined(Object@0x1008, add_optkw_dyn@0x1010, cme:0x1018) v25:ObjectSubclass[class_exact*:Object@VALUE(0x1008)] = GuardType v9, ObjectSubclass[class_exact*:Object@VALUE(0x1008)] recompile - v64:Fixnum[1] = Const Value(1) + v63:Fixnum[1] = Const Value(1) PushInlineFrame v25 (0x1040), v10, v22 - v34:BoolExact = FixnumBitCheck v64, 0 - CheckInterrupts - v37:CBool = Test v34 - CondBranch v37, bb6(v25, v10, v22, v64), bb7() + v34:BoolExact = FixnumBitCheck v63, 0 + v36:CBool = Test v34 + CondBranch v36, bb6(v25, v10, v22, v63), bb7() bb7(): - v43:Fixnum[2] = Const Value(2) + v42:Fixnum[2] = Const Value(2) PatchPoint MethodRedefined(Integer@0x1048, *@0x1050, cme:0x1058) - v71:Fixnum = GuardType v10, Fixnum recompile - v72:Fixnum = FixnumMult v71, v43 - Jump bb6(v25, v71, v72, v64) - bb6(v49:ObjectSubclass[class_exact*:Object@VALUE(0x1008)], v50:BasicObject, v51:NilClass|Fixnum, v52:Fixnum[1]): + v70:Fixnum = GuardType v10, Fixnum recompile + v71:Fixnum = FixnumMult v70, v42 + Jump bb6(v25, v70, v71, v63) + bb6(v48:ObjectSubclass[class_exact*:Object@VALUE(0x1008)], v49:BasicObject, v50:NilClass|Fixnum, v51:Fixnum[1]): PatchPoint MethodRedefined(Integer@0x1048, +@0x1080, cme:0x1088) - v75:Fixnum = GuardType v50, Fixnum recompile - v76:Fixnum = GuardType v51, Fixnum - v77:Fixnum = FixnumAdd v75, v76 + v74:Fixnum = GuardType v49, Fixnum recompile + v75:Fixnum = GuardType v50, Fixnum + v76:Fixnum = FixnumAdd v74, v75 CheckInterrupts PopInlineFrame - Return v77 + Return v76 "); } @@ -19999,84 +20119,84 @@ mod hir_opt_tests { v17:Fixnum[1] = Const Value(1) v19:Fixnum[2] = Const Value(2) PatchPoint MethodRedefined(Point@0x1008, new@0x1009, cme:0x1010) - v91:ObjectSubclass[class_exact:Point] = ObjectAllocClass Point:VALUE(0x1008) + v89:ObjectSubclass[class_exact:Point] = ObjectAllocClass Point:VALUE(0x1008) PatchPoint NoSingletonClass(Point@0x1008) PatchPoint MethodRedefined(Point@0x1008, initialize@0x1038, cme:0x1040) - PushInlineFrame v91 (0x1068), v17, v19 - v123:CShape = LoadField v91, :shape_id@0x1070 - v124:CShape[0x1071] = GuardBitEquals v123, CShape(0x1071) recompile - StoreField v91, :@x@0x1072, v17 - WriteBarrier v91, v17 - v127:CShape[0x1073] = Const CShape(0x1073) - StoreField v91, :shape_id@0x1070, v127 + PushInlineFrame v89 (0x1068), v17, v19 + v121:CShape = LoadField v89, :shape_id@0x1070 + v122:CShape[0x1071] = GuardBitEquals v121, CShape(0x1071) recompile + StoreField v89, :@x@0x1072, v17 + WriteBarrier v89, v17 + v125:CShape[0x1073] = Const CShape(0x1073) + StoreField v89, :shape_id@0x1070, v125 PatchPoint NoEPEscape(initialize) PatchPoint SingleRactorMode - StoreField v91, :@y@0x1074, v19 - WriteBarrier v91, v19 - v142:CShape[0x1075] = Const CShape(0x1075) - StoreField v91, :shape_id@0x1070, v142 + StoreField v89, :@y@0x1074, v19 + WriteBarrier v89, v19 + v140:CShape[0x1075] = Const CShape(0x1075) + StoreField v89, :shape_id@0x1070, v140 CheckInterrupts PopInlineFrame PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1078, Point) - v47:ClassSubclass[Point@0x1008] = Const Value(VALUE(0x1008)) - v49:NilClass = Const Value(nil) - v52:Fixnum[1] = Const Value(1) - v54:Fixnum[2] = Const Value(2) + v46:ClassSubclass[Point@0x1008] = Const Value(VALUE(0x1008)) + v48:NilClass = Const Value(nil) + v51:Fixnum[1] = Const Value(1) + v53:Fixnum[2] = Const Value(2) PatchPoint MethodRedefined(Point@0x1008, new@0x1009, cme:0x1010) - v101:ObjectSubclass[class_exact:Point] = ObjectAllocClass Point:VALUE(0x1008) + v99:ObjectSubclass[class_exact:Point] = ObjectAllocClass Point:VALUE(0x1008) PatchPoint NoSingletonClass(Point@0x1008) PatchPoint MethodRedefined(Point@0x1008, initialize@0x1038, cme:0x1040) - PushInlineFrame v101 (0x1068), v52, v54 - v163:CShape = LoadField v101, :shape_id@0x1070 - v164:CShape[0x1071] = GuardBitEquals v163, CShape(0x1071) recompile - StoreField v101, :@x@0x1072, v52 - WriteBarrier v101, v52 - v167:CShape[0x1073] = Const CShape(0x1073) - StoreField v101, :shape_id@0x1070, v167 + PushInlineFrame v99 (0x1068), v51, v53 + v161:CShape = LoadField v99, :shape_id@0x1070 + v162:CShape[0x1071] = GuardBitEquals v161, CShape(0x1071) recompile + StoreField v99, :@x@0x1072, v51 + WriteBarrier v99, v51 + v165:CShape[0x1073] = Const CShape(0x1073) + StoreField v99, :shape_id@0x1070, v165 PatchPoint NoEPEscape(initialize) PatchPoint SingleRactorMode - StoreField v101, :@y@0x1074, v54 - WriteBarrier v101, v54 - v182:CShape[0x1075] = Const CShape(0x1075) - StoreField v101, :shape_id@0x1070, v182 + StoreField v99, :@y@0x1074, v53 + WriteBarrier v99, v53 + v180:CShape[0x1075] = Const CShape(0x1075) + StoreField v99, :shape_id@0x1070, v180 CheckInterrupts PopInlineFrame PatchPoint NoSingletonClass(Point@0x1008) PatchPoint MethodRedefined(Point@0x1008, ==@0x1080, cme:0x1088) - PushInlineFrame v91 (0x1068), v101 + PushInlineFrame v89 (0x1068), v99 PatchPoint SingleRactorMode - v201:CShape = LoadField v91, :shape_id@0x1070 - v202:CShape[0x1075] = GuardBitEquals v201, CShape(0x1075) recompile - v203:BasicObject = LoadField v91, :@x@0x1072 + v199:CShape = LoadField v89, :shape_id@0x1070 + v200:CShape[0x1075] = GuardBitEquals v199, CShape(0x1075) recompile + v201:BasicObject = LoadField v89, :@x@0x1072 PatchPoint NoEPEscape(==) PatchPoint MethodRedefined(Point@0x1008, x@0x10b0, cme:0x10b8) PatchPoint MethodRedefined(Integer@0x10e0, ==@0x1080, cme:0x10e8) - v258:Fixnum = GuardType v203, Fixnum recompile - v260:BoolExact = FixnumEq v258, v52 - v215:CBool = Test v260 - v216:FalseClass = RefineType v260, Falsy - CondBranch v215, bb19(), bb18(v91, v101, v216) + v255:Fixnum = GuardType v201, Fixnum recompile + v257:BoolExact = FixnumEq v255, v51 + v212:CBool = Test v257 + v213:FalseClass = RefineType v257, Falsy + CondBranch v212, bb19(), bb18(v89, v99, v213) bb19(): PatchPoint SingleRactorMode - v223:CShape = LoadField v91, :shape_id@0x1070 - v224:CShape[0x1075] = GuardBitEquals v223, CShape(0x1075) recompile - v225:BasicObject = LoadField v91, :@y@0x1074 + v220:CShape = LoadField v89, :shape_id@0x1070 + v221:CShape[0x1075] = GuardBitEquals v220, CShape(0x1075) recompile + v222:BasicObject = LoadField v89, :@y@0x1074 PatchPoint NoEPEscape(==) PatchPoint NoSingletonClass(Point@0x1008) PatchPoint MethodRedefined(Point@0x1008, y@0x1110, cme:0x1118) - v265:CShape = LoadField v101, :shape_id@0x1070 - v266:CShape[0x1075] = GuardBitEquals v265, CShape(0x1075) recompile - v267:BasicObject = LoadField v101, :@y@0x1074 + v262:CShape = LoadField v99, :shape_id@0x1070 + v263:CShape[0x1075] = GuardBitEquals v262, CShape(0x1075) recompile + v264:BasicObject = LoadField v99, :@y@0x1074 PatchPoint MethodRedefined(Integer@0x10e0, ==@0x1080, cme:0x10e8) - v270:Fixnum = GuardType v225, Fixnum recompile - v271:Fixnum = GuardType v267, Fixnum - v272:BoolExact = FixnumEq v270, v271 - Jump bb18(v91, v101, v272) - bb18(v235:ObjectSubclass[class_exact:Point], v236:ObjectSubclass[class_exact:Point], v237:BoolExact): + v267:Fixnum = GuardType v222, Fixnum recompile + v268:Fixnum = GuardType v264, Fixnum + v269:BoolExact = FixnumEq v267, v268 + Jump bb18(v89, v99, v269) + bb18(v232:ObjectSubclass[class_exact:Point], v233:ObjectSubclass[class_exact:Point], v234:BoolExact): CheckInterrupts PopInlineFrame - Return v237 + Return v234 "); } @@ -20150,18 +20270,17 @@ mod hir_opt_tests { v33:Fixnum[7] = Const Value(7) v35:BasicObject = Send v14, :seven, v21, v23, v25, v27, v29, v31, v33 # SendFallbackReason: Too many arguments for LIR PatchPoint NoEPEscape(test) - CheckInterrupts - v43:CBool = Test v15 - v44:Falsy = RefineType v15, Falsy - CondBranch v43, bb5(), bb4(v13, v14, v44, v35) + v42:CBool = Test v15 + v43:Falsy = RefineType v15, Falsy + CondBranch v42, bb5(), bb4(v13, v14, v43, v35) bb5(): - v46:Truthy = RefineType v15, Truthy + v45:Truthy = RefineType v15, Truthy CheckInterrupts Return v35 - bb4(v53:BasicObject, v54:BasicObject, v55:Falsy, v56:BasicObject): - v60:NilClass = Const Value(nil) + bb4(v52:BasicObject, v53:BasicObject, v54:Falsy, v55:BasicObject): + v59:NilClass = Const Value(nil) CheckInterrupts - Return v60 + Return v59 "); } } diff --git a/zjit/src/hir/tests.rs b/zjit/src/hir/tests.rs index 574cf60b1a1051..d6557c05244f0a 100644 --- a/zjit/src/hir/tests.rs +++ b/zjit/src/hir/tests.rs @@ -405,19 +405,18 @@ pub(crate) mod hir_build_tests { PatchPoint StableConstantNames(0x1008, Integer) v20:ClassSubclass[Integer@0x1010] = Const Value(VALUE(0x1010)) v22:BasicObject = CheckMatch v10, v20, CASE + v24:CBool = Test v22 + v25:Truthy = RefineType v22, Truthy + CondBranch v24, bb4(v9, v10, v14, v10), bb5() + bb4(v37:BasicObject, v38:BasicObject, v39:NilClass, v40:BasicObject): + v45:Fixnum[1] = Const Value(1) CheckInterrupts - v25:CBool = Test v22 - v26:Truthy = RefineType v22, Truthy - CondBranch v25, bb4(v9, v10, v14, v10), bb5() - bb4(v38:BasicObject, v39:BasicObject, v40:NilClass, v41:BasicObject): - v46:Fixnum[1] = Const Value(1) - CheckInterrupts - Return v46 + Return v45 bb5(): - v28:Falsy = RefineType v22, Falsy - v33:Fixnum[2] = Const Value(2) + v27:Falsy = RefineType v22, Falsy + v32:Fixnum[2] = Const Value(2) CheckInterrupts - Return v33 + Return v32 "); } @@ -452,19 +451,18 @@ pub(crate) mod hir_build_tests { v16:ArrayExact[VALUE(0x1008)] = Const Value(VALUE(0x1008)) v17:ArrayExact = ArrayDup v16 v19:BasicObject = CheckMatch v10, v17, CASE|ARRAY + v21:CBool = Test v19 + v22:Truthy = RefineType v19, Truthy + CondBranch v21, bb4(v9, v10, v10), bb5() + bb4(v33:BasicObject, v34:BasicObject, v35:BasicObject): + v40:Fixnum[1] = Const Value(1) CheckInterrupts - v22:CBool = Test v19 - v23:Truthy = RefineType v19, Truthy - CondBranch v22, bb4(v9, v10, v10), bb5() - bb4(v34:BasicObject, v35:BasicObject, v36:BasicObject): - v41:Fixnum[1] = Const Value(1) - CheckInterrupts - Return v41 + Return v40 bb5(): - v25:Falsy = RefineType v19, Falsy - v29:Fixnum[2] = Const Value(2) + v24:Falsy = RefineType v19, Falsy + v28:Fixnum[2] = Const Value(2) CheckInterrupts - Return v29 + Return v28 "); } @@ -497,19 +495,18 @@ pub(crate) mod hir_build_tests { v12:ArrayExact[VALUE(0x1000)] = Const Value(VALUE(0x1000)) v13:ArrayExact = ArrayDup v12 v15:BasicObject = CheckMatch v10, v13, WHEN|ARRAY + v17:CBool = Test v15 + v18:Truthy = RefineType v15, Truthy + CondBranch v17, bb4(v6), bb5() + bb4(v28:BasicObject): + v32:Fixnum[1] = Const Value(1) CheckInterrupts - v18:CBool = Test v15 - v19:Truthy = RefineType v15, Truthy - CondBranch v18, bb4(v6), bb5() - bb4(v29:BasicObject): - v33:Fixnum[1] = Const Value(1) - CheckInterrupts - Return v33 + Return v32 bb5(): - v21:Falsy = RefineType v15, Falsy - v24:Fixnum[2] = Const Value(2) + v20:Falsy = RefineType v15, Falsy + v23:Fixnum[2] = Const Value(2) CheckInterrupts - Return v24 + Return v23 "); } @@ -1378,19 +1375,18 @@ pub(crate) mod hir_build_tests { Jump bb3(v4) bb3(v6:BasicObject): v10:TrueClass|NilClass = DefinedIvar v6, :@foo - CheckInterrupts - v13:CBool = Test v10 - v14:NilClass = RefineType v10, Falsy - CondBranch v13, bb5(), bb4(v6) + v12:CBool = Test v10 + v13:NilClass = RefineType v10, Falsy + CondBranch v12, bb5(), bb4(v6) bb5(): - v16:TrueClass = RefineType v10, Truthy - v19:Fixnum[3] = Const Value(3) + v15:TrueClass = RefineType v10, Truthy + v18:Fixnum[3] = Const Value(3) CheckInterrupts - Return v19 - bb4(v24:BasicObject): - v28:Fixnum[4] = Const Value(4) + Return v18 + bb4(v23:BasicObject): + v27:Fixnum[4] = Const Value(4) CheckInterrupts - Return v28 + Return v27 "); } @@ -1496,19 +1492,18 @@ pub(crate) mod hir_build_tests { v7:BasicObject = LoadArg :cond@1 Jump bb3(v6, v7) bb3(v9:BasicObject, v10:BasicObject): - CheckInterrupts - v16:CBool = Test v10 - v17:Falsy = RefineType v10, Falsy - CondBranch v16, bb5(), bb4(v9, v17) + v15:CBool = Test v10 + v16:Falsy = RefineType v10, Falsy + CondBranch v15, bb5(), bb4(v9, v16) bb5(): - v19:Truthy = RefineType v10, Truthy - v22:Fixnum[3] = Const Value(3) + v18:Truthy = RefineType v10, Truthy + v21:Fixnum[3] = Const Value(3) CheckInterrupts - Return v22 - bb4(v27:BasicObject, v28:Falsy): - v32:Fixnum[4] = Const Value(4) + Return v21 + bb4(v26:BasicObject, v27:Falsy): + v31:Fixnum[4] = Const Value(4) CheckInterrupts - Return v32 + Return v31 "); } @@ -1540,21 +1535,19 @@ pub(crate) mod hir_build_tests { v9:NilClass = Const Value(nil) Jump bb3(v7, v8, v9) bb3(v11:BasicObject, v12:BasicObject, v13:NilClass): - CheckInterrupts - v19:CBool = Test v12 - v20:Falsy = RefineType v12, Falsy - CondBranch v19, bb6(), bb4(v11, v20, v13) + v18:CBool = Test v12 + v19:Falsy = RefineType v12, Falsy + CondBranch v18, bb6(), bb4(v11, v19, v13) bb6(): - v22:Truthy = RefineType v12, Truthy - v25:Fixnum[3] = Const Value(3) - CheckInterrupts - Jump bb5(v11, v22, v25) - bb4(v30:BasicObject, v31:Falsy, v32:NilClass): - v36:Fixnum[4] = Const Value(4) - Jump bb5(v30, v31, v36) - bb5(v39:BasicObject, v40:BasicObject, v41:Fixnum): + v21:Truthy = RefineType v12, Truthy + v24:Fixnum[3] = Const Value(3) + Jump bb5(v11, v21, v24) + bb4(v28:BasicObject, v29:Falsy, v30:NilClass): + v34:Fixnum[4] = Const Value(4) + Jump bb5(v28, v29, v34) + bb5(v37:BasicObject, v38:BasicObject, v39:Fixnum): CheckInterrupts - Return v41 + Return v39 "); } @@ -1879,26 +1872,25 @@ pub(crate) mod hir_build_tests { bb3(v10:BasicObject, v11:NilClass, v12:NilClass): v16:Fixnum[0] = Const Value(0) v20:Fixnum[10] = Const Value(10) - CheckInterrupts Jump bb5(v10, v16, v20) - bb5(v26:BasicObject, v27:BasicObject, v28:BasicObject): - v32:Fixnum[0] = Const Value(0) - v35:BasicObject = Send v28, :>, v32 # SendFallbackReason: Uncategorized(opt_gt) - CheckInterrupts - v38:CBool = Test v35 - v39:Truthy = RefineType v35, Truthy - CondBranch v38, bb4(v26, v27, v28), bb6() - bb4(v51:BasicObject, v52:BasicObject, v53:BasicObject): - v58:Fixnum[1] = Const Value(1) - v61:BasicObject = Send v52, :+, v58 # SendFallbackReason: Uncategorized(opt_plus) - v66:Fixnum[1] = Const Value(1) - v69:BasicObject = Send v53, :-, v66 # SendFallbackReason: Uncategorized(opt_minus) - Jump bb5(v51, v61, v69) + bb5(v25:BasicObject, v26:BasicObject, v27:BasicObject): + v31:Fixnum[0] = Const Value(0) + v34:BasicObject = Send v27, :>, v31 # SendFallbackReason: Uncategorized(opt_gt) + CheckInterrupts + v37:CBool = Test v34 + v38:Truthy = RefineType v34, Truthy + CondBranch v37, bb4(v25, v26, v27), bb6() + bb4(v50:BasicObject, v51:BasicObject, v52:BasicObject): + v57:Fixnum[1] = Const Value(1) + v60:BasicObject = Send v51, :+, v57 # SendFallbackReason: Uncategorized(opt_plus) + v65:Fixnum[1] = Const Value(1) + v68:BasicObject = Send v52, :-, v65 # SendFallbackReason: Uncategorized(opt_minus) + Jump bb5(v50, v60, v68) bb6(): - v41:Falsy = RefineType v35, Falsy - v43:NilClass = Const Value(nil) + v40:Falsy = RefineType v34, Falsy + v42:NilClass = Const Value(nil) CheckInterrupts - Return v27 + Return v26 "); } @@ -1957,19 +1949,18 @@ pub(crate) mod hir_build_tests { Jump bb3(v5, v6) bb3(v8:BasicObject, v9:NilClass): v13:TrueClass = Const Value(true) - CheckInterrupts - v19:CBool[true] = Test v13 - v20 = RefineType v13, Falsy - CondBranch v19, bb5(), bb4(v8, v20) + v18:CBool[true] = Test v13 + v19 = RefineType v13, Falsy + CondBranch v18, bb5(), bb4(v8, v19) bb5(): - v22:TrueClass = RefineType v13, Truthy - v25:Fixnum[3] = Const Value(3) + v21:TrueClass = RefineType v13, Truthy + v24:Fixnum[3] = Const Value(3) CheckInterrupts - Return v25 - bb4(v30, v31): - v35 = Const Value(4) + Return v24 + bb4(v29, v30): + v34 = Const Value(4) CheckInterrupts - Return v35 + Return v34 "); } @@ -2824,14 +2815,13 @@ pub(crate) mod hir_build_tests { bb6(): v17:HeapBasicObject = ObjectAlloc v10 v19:BasicObject = Send v17, :initialize # SendFallbackReason: Uncategorized(opt_send_without_block) - CheckInterrupts Jump bb5(v6, v17, v19) - bb4(v23:BasicObject, v24:NilClass, v25:BasicObject): - v28:BasicObject = Send v25, :new # SendFallbackReason: Uncategorized(opt_send_without_block) - Jump bb5(v23, v28, v24) - bb5(v31:BasicObject, v32:BasicObject, v33:BasicObject): + bb4(v22:BasicObject, v23:NilClass, v24:BasicObject): + v27:BasicObject = Send v24, :new # SendFallbackReason: Uncategorized(opt_send_without_block) + Jump bb5(v22, v27, v23) + bb5(v30:BasicObject, v31:BasicObject, v32:BasicObject): CheckInterrupts - Return v32 + Return v31 "); } @@ -4873,17 +4863,16 @@ pub(crate) mod hir_build_tests { v7:BasicObject = LoadArg :x@1 Jump bb3(v6, v7) bb3(v9:BasicObject, v10:BasicObject): - CheckInterrupts - v17:CBool = HasType v10, NilClass - v18:NilClass = Const Value(nil) - CondBranch v17, bb4(v9, v18, v18), bb5() + v16:CBool = HasType v10, NilClass + v17:NilClass = Const Value(nil) + CondBranch v16, bb4(v9, v17, v17), bb5() bb5(): - v20:NotNil = RefineType v10, NotNil - v22:BasicObject = Send v20, :itself # SendFallbackReason: Uncategorized(opt_send_without_block) - Jump bb4(v9, v20, v22) - bb4(v24:BasicObject, v25:BasicObject, v26:BasicObject): + v19:NotNil = RefineType v10, NotNil + v21:BasicObject = Send v19, :itself # SendFallbackReason: Uncategorized(opt_send_without_block) + Jump bb4(v9, v19, v21) + bb4(v23:BasicObject, v24:BasicObject, v25:BasicObject): CheckInterrupts - Return v26 + Return v25 "); } @@ -4914,27 +4903,25 @@ pub(crate) mod hir_build_tests { v7:BasicObject = LoadArg :x@1 Jump bb3(v6, v7) bb3(v9:BasicObject, v10:BasicObject): - CheckInterrupts - v16:CBool = Test v10 - v17:Falsy = RefineType v10, Falsy - CondBranch v16, bb6(), bb4(v9, v17) + v15:CBool = Test v10 + v16:Falsy = RefineType v10, Falsy + CondBranch v15, bb6(), bb4(v9, v16) bb6(): - v19:Truthy = RefineType v10, Truthy - CheckInterrupts - v25:CBool[false] = HasType v19, NilClass - v26:NilClass = Const Value(nil) - CondBranch v25, bb5(v9, v26, v26), bb7() + v18:Truthy = RefineType v10, Truthy + v23:CBool[false] = HasType v18, NilClass + v24:NilClass = Const Value(nil) + CondBranch v23, bb5(v9, v24, v24), bb7() bb7(): - v28:Truthy = RefineType v19, NotNil - v30:BasicObject = Send v28, :itself # SendFallbackReason: Uncategorized(opt_send_without_block) + v26:Truthy = RefineType v18, NotNil + v28:BasicObject = Send v26, :itself # SendFallbackReason: Uncategorized(opt_send_without_block) CheckInterrupts - Return v30 - bb4(v35:BasicObject, v36:Falsy): - v40:Fixnum[4] = Const Value(4) - Jump bb5(v35, v36, v40) - bb5(v42:BasicObject, v43:Falsy, v44:Fixnum[4]): + Return v28 + bb4(v33:BasicObject, v34:Falsy): + v38:Fixnum[4] = Const Value(4) + Jump bb5(v33, v34, v38) + bb5(v40:BasicObject, v41:Falsy, v42:Fixnum[4]): CheckInterrupts - Return v44 + Return v42 "); } @@ -4971,39 +4958,36 @@ pub(crate) mod hir_build_tests { v7:BasicObject = LoadArg :x@1 Jump bb3(v6, v7) bb3(v9:BasicObject, v10:BasicObject): - CheckInterrupts - v16:CBool = Test v10 - v17:Falsy = RefineType v10, Falsy - CondBranch v16, bb7(), bb6(v9, v17) + v15:CBool = Test v10 + v16:Falsy = RefineType v10, Falsy + CondBranch v15, bb7(), bb6(v9, v16) bb7(): - v19:Truthy = RefineType v10, Truthy - CheckInterrupts - v24:CBool[true] = Test v19 - v25 = RefineType v19, Falsy - CondBranch v24, bb8(), bb5(v9, v25) + v18:Truthy = RefineType v10, Truthy + v22:CBool[true] = Test v18 + v23 = RefineType v18, Falsy + CondBranch v22, bb8(), bb5(v9, v23) bb8(): - v27:Truthy = RefineType v19, Truthy - CheckInterrupts - v32:CBool[true] = Test v27 - v33 = RefineType v27, Falsy - CondBranch v32, bb9(), bb4(v9, v33) + v25:Truthy = RefineType v18, Truthy + v29:CBool[true] = Test v25 + v30 = RefineType v25, Falsy + CondBranch v29, bb9(), bb4(v9, v30) bb9(): - v35:Truthy = RefineType v27, Truthy - v38:Fixnum[3] = Const Value(3) + v32:Truthy = RefineType v25, Truthy + v35:Fixnum[3] = Const Value(3) CheckInterrupts - Return v38 - bb4(v63, v64): - v68 = Const Value(4) + Return v35 + bb4(v60, v61): + v65 = Const Value(4) CheckInterrupts - Return v68 - bb5(v53, v54): - v58 = Const Value(5) + Return v65 + bb5(v50, v51): + v55 = Const Value(5) CheckInterrupts - Return v58 - bb6(v43:BasicObject, v44:Falsy): - v48:Fixnum[6] = Const Value(6) + Return v55 + bb6(v40:BasicObject, v41:Falsy): + v45:Fixnum[6] = Const Value(6) CheckInterrupts - Return v48 + Return v45 "); } @@ -5152,19 +5136,18 @@ pub(crate) mod hir_build_tests { v43:ObjectSubclass[BlockParamProxy] = Const Value(VALUE(0x1008)) Jump bb8(v43, v22) bb8(v33:BasicObject, v34:BasicObject): - CheckInterrupts - v47:CBool = Test v33 - v48:Falsy = RefineType v33, Falsy - CondBranch v47, bb9(), bb4(v18, v19, v20, v21, v34, v27) + v46:CBool = Test v33 + v47:Falsy = RefineType v33, Falsy + CondBranch v46, bb9(), bb4(v18, v19, v20, v21, v34, v27) bb9(): - v50:Truthy = RefineType v33, Truthy - v54:BasicObject = InvokeBlock v27 # SendFallbackReason: InvokeBlock: not yet specialized - v57:BasicObject = InvokeBuiltin dir_s_close, v18, v27 + v49:Truthy = RefineType v33, Truthy + v53:BasicObject = InvokeBlock v27 # SendFallbackReason: InvokeBlock: not yet specialized + v56:BasicObject = InvokeBuiltin dir_s_close, v18, v27 CheckInterrupts - Return v54 - bb4(v63:BasicObject, v64:BasicObject, v65:BasicObject, v66:BasicObject, v67:BasicObject, v68:BasicObject): + Return v53 + bb4(v62:BasicObject, v63:BasicObject, v64:BasicObject, v65:BasicObject, v66:BasicObject, v67:BasicObject): CheckInterrupts - Return v68 + Return v67 "); } @@ -5295,19 +5278,18 @@ pub(crate) mod hir_build_tests { v17:Fixnum[0] = Const Value(0) v19:Fixnum[1] = Const Value(1) v22:BasicObject = Send v10, :[], v17, v19 # SendFallbackReason: Uncategorized(opt_send_without_block) + v25:CBool = Test v22 + v26:Truthy = RefineType v22, Truthy + CondBranch v25, bb4(v9, v10, v14, v10, v17, v19, v26), bb5() + bb4(v40:BasicObject, v41:BasicObject, v42:NilClass, v43:BasicObject, v44:Fixnum[0], v45:Fixnum[1], v46:Truthy): CheckInterrupts - v26:CBool = Test v22 - v27:Truthy = RefineType v22, Truthy - CondBranch v26, bb4(v9, v10, v14, v10, v17, v19, v27), bb5() - bb4(v41:BasicObject, v42:BasicObject, v43:NilClass, v44:BasicObject, v45:Fixnum[0], v46:Fixnum[1], v47:Truthy): - CheckInterrupts - Return v47 + Return v46 bb5(): - v29:Falsy = RefineType v22, Falsy - v32:Fixnum[2] = Const Value(2) - v35:BasicObject = Send v10, :[]=, v17, v19, v32 # SendFallbackReason: Uncategorized(opt_send_without_block) + v28:Falsy = RefineType v22, Falsy + v31:Fixnum[2] = Const Value(2) + v34:BasicObject = Send v10, :[]=, v17, v19, v31 # SendFallbackReason: Uncategorized(opt_send_without_block) CheckInterrupts - Return v32 + Return v31 "); } @@ -5844,19 +5826,18 @@ pub(crate) mod hir_build_tests { Jump bb3(v7, v8, v10) bb3(v12:BasicObject, v13:BasicObject, v14:BasicObject): v17:BoolExact = FixnumBitCheck v14, 0 - CheckInterrupts - v20:CBool = Test v17 - v21:TrueClass = RefineType v17, Truthy - CondBranch v20, bb4(v12, v13, v14), bb5() + v19:CBool = Test v17 + v20:TrueClass = RefineType v17, Truthy + CondBranch v19, bb4(v12, v13, v14), bb5() bb5(): - v23:FalseClass = RefineType v17, Falsy - v25:Fixnum[1] = Const Value(1) - v27:Fixnum[1] = Const Value(1) - v30:BasicObject = Send v25, :+, v27 # SendFallbackReason: Uncategorized(opt_plus) - Jump bb4(v12, v30, v14) - bb4(v33:BasicObject, v34:BasicObject, v35:BasicObject): + v22:FalseClass = RefineType v17, Falsy + v24:Fixnum[1] = Const Value(1) + v26:Fixnum[1] = Const Value(1) + v29:BasicObject = Send v24, :+, v26 # SendFallbackReason: Uncategorized(opt_plus) + Jump bb4(v12, v29, v14) + bb4(v32:BasicObject, v33:BasicObject, v34:BasicObject): CheckInterrupts - Return v34 + Return v33 "); } diff --git a/zjit/src/options.rs b/zjit/src/options.rs index 55c9728a1944f1..bffecaeb99b0fe 100644 --- a/zjit/src/options.rs +++ b/zjit/src/options.rs @@ -149,27 +149,16 @@ pub struct Options { pub inline_threshold: InlineThreshold, /// Per-caller cumulative size budget for inlining, measured as the caller - /// `Function`'s `insns.len()` at the moment `should_inline` is consulted (during + /// `Function::num_instructions` at the moment `should_inline` is consulted (during /// `inline_methods` inside the `optimize()` fixed-point loop). Once a caller has /// grown past this many HIR instructions, `should_inline` rejects further callees, /// bounding runaway code-size growth from depth-N inlining (and providing the /// optimization fixed-point loop's effective terminating condition). /// `INLINE_BUDGET_UNLIMITED` disables the budget. /// - /// Caveat on the unit: `self.insns` is append-only across the whole pipeline — - /// `InsnId`s are stable indices into it, so passes never shrink it. `len()` is - /// therefore a high-water mark of total HIR instructions ever allocated for the - /// function, including ones that later optimization passes mark dead via - /// `eliminate_dead_code` or alias away via `union_find`. By the time - /// `should_inline` runs in a given fixed-point iteration, `self.insns.len()` has - /// already been bumped by `iseq_to_hir`'s initial build, then by `type_specialize` - /// and the trivial `inline` pass, and (in iterations 2+) by every prior pass in - /// the loop including the previous round's `inline_methods`. It is a useful proxy - /// for "compile work done" but not for "size of the compiled output". - /// /// Note: this is a different unit than `inline_threshold` — that field is callee - /// YARV bytecode words; this one is caller HIR instructions, allocation high-water - /// mark. They aren't directly comparable; YARV → HIR typically expands roughly 1-3x. + /// YARV bytecode words; this one is caller HIR instructions. They aren't directly comparable; + /// YARV → HIR typically expands roughly 1-3x. pub inline_budget: InlineBudget, /// Set of qualified method names (e.g. `Class#method`, `Module::Class.method`) that