diff --git a/.github/workflows/check_sast.yml b/.github/workflows/check_sast.yml index 34cf7e4b9b7a44..669261535b13a0 100644 --- a/.github/workflows/check_sast.yml +++ b/.github/workflows/check_sast.yml @@ -78,14 +78,14 @@ jobs: persist-credentials: false - name: Initialize CodeQL - uses: github/codeql-action/init@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8 + uses: github/codeql-action/init@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9 with: languages: ${{ matrix.language }} build-mode: none config-file: .github/codeql/codeql-config.yml - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8 + uses: github/codeql-action/analyze@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9 with: category: '/language:${{ matrix.language }}' upload: False @@ -127,7 +127,7 @@ jobs: continue-on-error: true - name: Upload SARIF - uses: github/codeql-action/upload-sarif@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8 + uses: github/codeql-action/upload-sarif@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9 with: sarif_file: sarif-results/${{ matrix.language }}.sarif continue-on-error: true diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index 6a2f162b3a9c56..8f3ed45b13cb0f 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -73,6 +73,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard (optional). # Commenting out will disable upload of results to your repo's Code Scanning dashboard - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8 + uses: github/codeql-action/upload-sarif@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9 with: sarif_file: results.sarif diff --git a/.github/workflows/zjit-macos.yml b/.github/workflows/zjit-macos.yml index 85688e3024946c..feb8c462890303 100644 --- a/.github/workflows/zjit-macos.yml +++ b/.github/workflows/zjit-macos.yml @@ -98,7 +98,7 @@ jobs: rustup install ${{ matrix.rust_version }} --profile minimal rustup default ${{ matrix.rust_version }} - - uses: taiki-e/install-action@b6ff580856c41316412a0b9b60540fbc6f8c82cc # v2.86.7 + - uses: taiki-e/install-action@37f7c5781271959fb65b6b35224e28652ff2b63d # v2.87.0 with: tool: nextest@0.9 if: ${{ matrix.test_task == 'zjit-check' }} diff --git a/.github/workflows/zjit-ubuntu.yml b/.github/workflows/zjit-ubuntu.yml index b8f64735891d9c..9623059e035e18 100644 --- a/.github/workflows/zjit-ubuntu.yml +++ b/.github/workflows/zjit-ubuntu.yml @@ -152,7 +152,7 @@ jobs: ruby-version: '3.1' bundler: none - - uses: taiki-e/install-action@b6ff580856c41316412a0b9b60540fbc6f8c82cc # v2.86.7 + - uses: taiki-e/install-action@37f7c5781271959fb65b6b35224e28652ff2b63d # v2.87.0 with: tool: nextest@0.9 if: ${{ matrix.test_task == 'zjit-check' }} diff --git a/.gitignore b/.gitignore index b49185e46879c1..1ea0a323d92558 100644 --- a/.gitignore +++ b/.gitignore @@ -88,12 +88,10 @@ lcov*.info /config.status /config.status.lineno /configure -/coverage/simplecov -/coverage/simplecov-html -/coverage/doclie /coverage/.last_run.json +/coverage/.report_stamp /coverage/.resultset.json* -/coverage/assets +/coverage/coverage.json /coverage/index.html /doc/capi /enc.mk diff --git a/box.c b/box.c index 9121ba003d6ada..559170c7a452ce 100644 --- a/box.c +++ b/box.c @@ -11,6 +11,7 @@ #include "internal/hash.h" #include "internal/io.h" #include "internal/load.h" +#include "internal/random.h" #include "internal/st.h" #include "internal/variable.h" #include "iseq.h" @@ -47,12 +48,16 @@ static rb_box_gem_flags_t box_gem_flags[1]; static char *tmp_dir; static bool tmp_dir_has_dirsep; -#define BOX_TMP_PREFIX "_ruby_box_" - #ifndef MAXPATHLEN # define MAXPATHLEN 1024 #endif +/* process-private 0700 directory for box-local copies of extensions */ +static char box_ext_tmp_dir[MAXPATHLEN]; +static unsigned long box_ext_seq; + +#define BOX_TMP_PREFIX "_ruby_box_" + #if defined(_WIN32) # define DIRSEP "\\" #else @@ -560,13 +565,40 @@ system_tmpdir(void) /* end of copy */ -static int -sprint_ext_filename(char *str, size_t size, long box_id, const char *prefix, const char *basename) +/* Box-local copies of extensions are placed in a process-private 0700 + * directory with an unpredictable name, so that other local users cannot + * occupy the copy destination in a shared TMPDIR [Bug #22110], and so + * that the file name of each copy stays short regardless of the depth of + * the original path (a full path flattened into a single file name can + * exceed NAME_MAX). */ +static void +ensure_box_ext_tmp_dir(void) { - if (tmp_dir_has_dirsep) { - return snprintf(str, size, "%s%sp%"PRI_PIDT_PREFIX"u_%ld_%s", tmp_dir, prefix, getpid(), box_id, basename); + if (box_ext_tmp_dir[0]) return; + + int last_errno = 0; + for (int retry = 0; retry < 10; retry++) { + char path[MAXPATHLEN]; + uint64_t suffix; + if (ruby_fill_random_bytes(&suffix, sizeof(suffix), FALSE) != 0) { + /* no random source; mkdir(0700) below still refuses hijacked names */ + suffix = (uint64_t)(uintptr_t)&suffix ^ (uint64_t)retry; + } + int wrote = snprintf(path, sizeof(path), "%s%s%sp%"PRI_PIDT_PREFIX"u_%.16"PRIx64, + tmp_dir, tmp_dir_has_dirsep ? "" : DIRSEP, + BOX_TMP_PREFIX, getpid(), suffix); + if (wrote >= (int)sizeof(path)) { + rb_raise(rb_eLoadError, "TMPDIR for Ruby Box extensions is too long: %s", tmp_dir); + } + if (mkdir(path, 0700) == 0) { + strlcpy(box_ext_tmp_dir, path, sizeof(box_ext_tmp_dir)); + return; + } + last_errno = errno; + if (last_errno != EEXIST) break; } - return snprintf(str, size, "%s%s%sp%"PRI_PIDT_PREFIX"u_%ld_%s", tmp_dir, DIRSEP, prefix, getpid(), box_id, basename); + rb_raise(rb_eLoadError, "can't create the temporary directory for Ruby Box extensions under %s: %s", + tmp_dir, strerror(last_errno)); } enum copy_error_type { @@ -738,41 +770,14 @@ copy_ext_file(const char *src_path, const char *dst_path) #define isdirsep(x) ((x) == '/') #endif -#define IS_SOEXT(e) (strcmp((e), ".so") == 0 || strcmp((e), ".o") == 0) -#define IS_DLEXT(e) (strcmp((e), DLEXT) == 0) - -static void -fname_without_suffix(const char *fname, char *rvalue, size_t rsize) -{ - size_t len = strlen(fname); - const char *pos; - for (pos = fname + len; pos > fname; pos--) { - if (IS_SOEXT(pos) || IS_DLEXT(pos)) { - len = pos - fname; - break; - } - if (fname + len - pos > DLEXT_MAXLEN) break; - } - if (len > rsize - 1) len = rsize - 1; - memcpy(rvalue, fname, len); - rvalue[len] = '\0'; -} - -static void -escaped_basename(const char *path, const char *fname, char *rvalue, size_t rsize) +static const char * +ext_basename(const char *path) { - char *pos; - const char *leaf = path, *found; - // `leaf + 1` looks uncomfortable (when leaf == path), but fname must not be the top-dir itself - while ((found = strstr(leaf + 1, fname)) != NULL) { - leaf = found; // find the last occurrence for the path like /etc/my-crazy-lib-dir/etc.so - } - strlcpy(rvalue, leaf, rsize); - for (pos = rvalue; *pos; pos++) { - if (isdirsep(*pos)) { - *pos = '+'; - } + const char *base = path; + for (const char *pos = path; *pos; pos++) { + if (isdirsep(*pos)) base = pos + 1; } + return base; } static void @@ -847,22 +852,25 @@ rb_box_unload_local_extensions(void) ext = next; } #endif + if (box_ext_tmp_dir[0]) { + rmdir(box_ext_tmp_dir); + box_ext_tmp_dir[0] = '\0'; + } } VALUE -rb_box_local_extension(VALUE box_value, VALUE fname, VALUE path, VALUE *cleanup) +rb_box_local_extension(VALUE box_value, VALUE path, VALUE *cleanup) { - char ext_path[MAXPATHLEN], fname2[MAXPATHLEN], basename[MAXPATHLEN]; + char ext_path[MAXPATHLEN]; int wrote; - const char *src_path = RSTRING_PTR(path), *fname_ptr = RSTRING_PTR(fname); + const char *src_path = RSTRING_PTR(path); rb_box_t *box = rb_get_box_t(box_value); - fname_without_suffix(fname_ptr, fname2, sizeof(fname2)); - escaped_basename(src_path, fname2, basename, sizeof(basename)); - - wrote = sprint_ext_filename(ext_path, sizeof(ext_path), box->box_id, BOX_TMP_PREFIX, basename); + ensure_box_ext_tmp_dir(); + wrote = snprintf(ext_path, sizeof(ext_path), "%s%s%ld_%lu_%s", + box_ext_tmp_dir, DIRSEP, box->box_id, box_ext_seq++, ext_basename(src_path)); if (wrote >= (int)sizeof(ext_path)) { - rb_bug("Extension file path in the box was too long"); + rb_raise(rb_eLoadError, "extension file path in the box is too long: %"PRIsVALUE, path); } VALUE new_path = rb_str_new_cstr(ext_path); *cleanup = TypedData_Wrap_Struct(0, &box_ext_cleanup_type, NULL); diff --git a/common.mk b/common.mk index 68c0e1e33587fa..fcf64566f5c1e4 100644 --- a/common.mk +++ b/common.mk @@ -1537,7 +1537,7 @@ update-config_files: PHONY update-coverage: main PHONY $(XRUBY) -C "$(srcdir)" bin/gem install --no-document \ - --install-dir .bundle --conservative "simplecov" + --install-dir .bundle --conservative "simplecov" -v "~> 1.1" refresh-gems: update-bundled_gems prepare-gems # can't recall exactly, but `make` somewhere (not GNU or nmake) diff --git a/coverage/README b/coverage/README index a4c3dfcb039400..a69efd9da2a5ef 100644 --- a/coverage/README +++ b/coverage/README @@ -13,5 +13,4 @@ Limitation TODO - * more reduce bundled simplecov(additional configuration, formatter, etc.) * measure rubyspec coverage diff --git a/file.c b/file.c index 995111e89d88a7..526c6efd338032 100644 --- a/file.c +++ b/file.c @@ -2023,13 +2023,22 @@ rb_file_chardev_p(VALUE obj, VALUE fname) /* * call-seq: - * File.exist?(file_name) -> true or false + * File.exist?(object) -> true or false * - * Return true if the named file exists. + * Return whether the specified +object+, a string path or IO object, exists: * - * _file_name_ can be an IO object. + * # String paths. + * File.exist?('README.md') # => true + * File.exist?('.') # => true + * filepath = 't.tmp' + * File.exist?(filepath) # => false + * File.write(filepath, 'foo') + * File.exist?(filepath) # => true + * # File (IO object). + * file = File.new(filepath) + * File.exist?(file) # => true + * file.close # Clean up. * - * "file exists" means that stat() or fstat() system call is successful. */ static VALUE @@ -2179,17 +2188,36 @@ rb_file_world_writable_p(VALUE obj, VALUE fname) /* * call-seq: - * File.executable?(file_name) -> true or false + * File.executable?(path) -> true or false * - * Returns true if the named file is executable by the effective - * user and group id of this process. See eaccess(3). + * Returns whether the filesystem entry at the given string +path+ + * exists and is executable. * - * Windows does not support execute permissions separately from read - * permissions. On Windows, a file is only considered executable if it ends in - * .bat, .cmd, .com, or .exe. + * On Windows, the entry is executable if its path has file extension + * +.bat+, +.cmd+, +.com+, or +.exe+: * - * Note that some OS-level security features may cause this to return true - * even though the file is not executable by the effective user/group. + * File.executable?('win32/rtname.cmd') # => true + * File.executable?('win32/rtname') # => false + * File.executable?('win32/nosuch.cmd') # => false + * + * On other systems, the entry is executable if it has the execute/search + * permission for the effective user and group id of the current process; + * see {Permissions}[rdoc-ref:file/filesystem_modes.md@Permissions]. + * + * These examples use + * a {helper method}[rdoc-ref:file/filesystem_modes.md@Helper+Method], +mode+, + * that displays a mode both in octal digits and in characters: + * + * File.executable?('.') # => true + * mode('.') # => "040775 drwxrwxr-x" + * File.executable?('bin/gem') # => true + * mode('bin/gem') # => "100775 -rwxrwxr-x" + * File.executable?('/etc/passwd') # => false + * mode('/etc/passwd') # => "100644 -rw-r--r--" + * File.executable?('nosuch') # => false + * + * Note that some filesystem settings may cause this method to return +true+ + * even though the entry is not executable by the effective user/group. */ static VALUE diff --git a/hash.c b/hash.c index 172ed3bd49a502..2686b913c3381a 100644 --- a/hash.c +++ b/hash.c @@ -1941,7 +1941,8 @@ rb_hash_init(rb_execution_context_t *ec, VALUE hash, VALUE capa_value, VALUE ifn if (capa_value != INT2FIX(0)) { long capa = NUM2LONG(capa_value); - if (capa > 0 && RHASH_AR_TABLE_P(hash) && RHASH_SIZE(hash) == 0 && capa > RHASH_AR_TABLE_MAX_BOUND(hash)) { + if (capa > 0 && RHASH_AR_TABLE_P(hash) && RHASH_SIZE(hash) == 0 && + (unsigned long)capa > RHASH_AR_TABLE_MAX_BOUND(hash)) { hash_st_table_init(hash, capa); } } diff --git a/internal/box.h b/internal/box.h index fa01a47307ed66..b5ebc2c3d2c185 100644 --- a/internal/box.h +++ b/internal/box.h @@ -88,7 +88,7 @@ void rb_box_gc_update_references(void *ptr); rb_box_t * rb_get_box_t(VALUE ns); VALUE rb_get_box_object(rb_box_t *ns); -VALUE rb_box_local_extension(VALUE box, VALUE fname, VALUE path, VALUE *cleanup); +VALUE rb_box_local_extension(VALUE box, VALUE path, VALUE *cleanup); void rb_box_cleanup_local_extension(VALUE cleanup); void rb_box_defer_unload_local_extension(void *handle); void rb_box_unload_local_extensions(void); diff --git a/load.c b/load.c index c10de25a45e3b6..ecc3da22597d50 100644 --- a/load.c +++ b/load.c @@ -1218,7 +1218,7 @@ load_ext(VALUE path, VALUE fname) const rb_box_t *box = rb_loading_box(); VALUE cleanup = 0; if (BOX_USER_P(box)) { - loaded = rb_box_local_extension(box->box_object, fname, path, &cleanup); + loaded = rb_box_local_extension(box->box_object, path, &cleanup); } rb_scope_visibility_set(METHOD_VISI_PUBLIC); void *handle = dln_load_feature(RSTRING_PTR(loaded), RSTRING_PTR(fname)); diff --git a/spec/bundler/spec_helper.rb b/spec/bundler/spec_helper.rb index f4030e70a6fa16..e39c75aa61f5d5 100644 --- a/spec/bundler/spec_helper.rb +++ b/spec/bundler/spec_helper.rb @@ -43,30 +43,27 @@ begin raise LoadError if File.exist?(File.expand_path("../../lib/bundler/bundler.gemspec", __dir__)) - gem "simplecov_json_formatter" require "simplecov" SimpleCov.start do command_name "bundler:#{Process.pid}" - root File.expand_path("../bundler", __dir__) + root File.expand_path("..", __dir__) coverage_dir File.expand_path("../coverage", __dir__) - add_filter "/spec/" - add_filter "/test/" - add_filter "/lib/rubygems/" - add_filter "/lib/bundler/vendor/" - add_filter "/tool/" - add_filter "/tmp/" - add_filter ".gemspec" + skip "/spec/" + skip "/test/" + skip "/lib/rubygems/" + skip "/lib/bundler/vendor/" + skip "/tool/" + skip "/tmp/" + skip ".gemspec" end SimpleCov.print_error_status = false - SimpleCov.at_exit do - $stdout = File.open(File::NULL, "w") - SimpleCov.result.format! - ensure - $stdout = STDOUT - end + + # Only merge this process result into the resultset. Parallel workers share a + # coverage directory, so the report is formatted once by `rake coverage:report`. + SimpleCov.at_exit { SimpleCov.result } rescue LoadError # SimpleCov is not installed end diff --git a/test/ruby/test_box.rb b/test/ruby/test_box.rb index 43f13494de3782..5491578e9fb303 100644 --- a/test/ruby/test_box.rb +++ b/test/ruby/test_box.rb @@ -1279,6 +1279,28 @@ def test_loaded_extension_deleted_in_user_box end end + def test_loading_extension_from_deep_path_in_user_box + require 'date' + dlext = RbConfig::CONFIG['DLEXT'] + src = $LOADED_FEATURES.find {|f| f.end_with?("date_core.#{dlext}")} + omit "date_core.#{dlext} is not loaded dynamically" unless src && File.exist?(src) + + require 'tmpdir' + require 'fileutils' + Dir.mktmpdir do |tmpdir| + # deep enough that this path flattened into a single file name exceeds NAME_MAX + deep = File.join(tmpdir, "d" * 90, "e" * 90, "f" * 90) + FileUtils.mkdir_p(deep) + FileUtils.cp(src, deep) + env = ENV_ENABLE_BOX.merge({'BOX_TEST_EXT_FEATURE'=>File.join(deep, "date_core")}) + assert_ruby_status([env], "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + require ENV['BOX_TEST_EXT_FEATURE'] or raise "already loaded" + raise "Date is not defined" unless defined?(Date) + end; + end + end + def test_root_box_iclasses_should_be_boxable assert_separately([ENV_ENABLE_BOX], __FILE__, __LINE__, "#{<<~"begin;"}\n#{<<~'end;'}", ignore_stderr: true) begin; diff --git a/test/rubygems/helper.rb b/test/rubygems/helper.rb index 73e109796a7ba5..1fe8577b4fc94b 100644 --- a/test/rubygems/helper.rb +++ b/test/rubygems/helper.rb @@ -5,7 +5,6 @@ begin raise LoadError if ENV["GEM_COMMAND"] - gem "simplecov_json_formatter" require "simplecov" unless ENV["SIMPLECOV_SUBPROCESS"] @@ -14,11 +13,11 @@ root File.expand_path("../..", __dir__) coverage_dir File.expand_path("../../coverage", __dir__) - add_filter "/test/" - add_filter "/bundler/" - add_filter "/tool/" - add_filter "/lib/rubygems/vendor/" - add_filter ".gemspec" + skip "/test/" + skip "/bundler/" + skip "/tool/" + skip "/lib/rubygems/vendor/" + skip ".gemspec" end # Prevent SimpleCov from running in subprocesses spawned by assert_separately diff --git a/tool/bundler/dev_gems.rb b/tool/bundler/dev_gems.rb index c8e4d5345c4297..7dc32f80e00c20 100644 --- a/tool/bundler/dev_gems.rb +++ b/tool/bundler/dev_gems.rb @@ -14,7 +14,7 @@ gem "rspec-expectations", "~> 3.12" gem "rspec-mocks", "~> 3.12" gem "rubygems-generate_index", "~> 1.1" -gem "simplecov", "~> 0.22" +gem "simplecov", "~> 1.1" group :doc do gem "ronn-ng", "~> 0.10.1", platform: :ruby diff --git a/tool/bundler/dev_gems.rb.lock b/tool/bundler/dev_gems.rb.lock index ee91c8baffd35f..72916fb1e1d95f 100644 --- a/tool/bundler/dev_gems.rb.lock +++ b/tool/bundler/dev_gems.rb.lock @@ -3,7 +3,6 @@ GEM specs: compact_index (0.15.0) diff-lcs (1.6.2) - docile (1.4.1) kramdown (2.5.2) rexml (>= 3.4.4) kramdown-parser-gfm (1.1.0) @@ -58,12 +57,7 @@ GEM rspec-support (3.13.7) rubygems-generate_index (1.1.3) compact_index (~> 0.15.0) - simplecov (0.22.0) - docile (~> 1.1) - simplecov-html (~> 0.11) - simplecov_json_formatter (~> 0.1) - simplecov-html (0.13.2) - simplecov_json_formatter (0.1.4) + simplecov (1.1.1) test-unit (3.7.7) power_assert test-unit-ruby-core (1.0.14) @@ -96,7 +90,7 @@ DEPENDENCIES rspec-expectations (~> 3.12) rspec-mocks (~> 3.12) rubygems-generate_index (~> 1.1) - simplecov (~> 0.22) + simplecov (~> 1.1) test-unit (~> 3.0) test-unit-ruby-core turbo_tests (~> 2.2.3) @@ -104,7 +98,6 @@ DEPENDENCIES CHECKSUMS compact_index (0.15.0) sha256=5c6c404afca8928a7d9f4dde9524f6e1610db17e675330803055db282da84a8b diff-lcs (1.6.2) sha256=9ae0d2cba7d4df3075fe8cd8602a8604993efc0dfa934cff568969efb1909962 - docile (1.4.1) sha256=96159be799bfa73cdb721b840e9802126e4e03dfc26863db73647204c727f21e kramdown (2.5.2) sha256=1ba542204c66b6f9111ff00dcc26075b95b220b07f2905d8261740c82f7f02fa kramdown-parser-gfm (1.1.0) sha256=fb39745516427d2988543bf01fc4cf0ab1149476382393e0e9c48592f6581729 mini_portile2 (2.8.9) sha256=0cd7c7f824e010c072e33f68bc02d85a00aeb6fce05bb4819c03dfd3c140c289 @@ -133,9 +126,7 @@ CHECKSUMS rspec-mocks (3.13.8) sha256=086ad3d3d17533f4237643de0b5c42f04b66348c28bf6b9c2d3f4a3b01af1d47 rspec-support (3.13.7) sha256=0640e5570872aafefd79867901deeeeb40b0c9875a36b983d85f54fb7381c47c rubygems-generate_index (1.1.3) sha256=3571424322666598e9586a906485e1543b617f87644913eaf137d986a3393f5c - simplecov (0.22.0) sha256=fe2622c7834ff23b98066bb0a854284b2729a569ac659f82621fc22ef36213a5 - simplecov-html (0.13.2) sha256=bd0b8e54e7c2d7685927e8d6286466359b6f16b18cb0df47b508e8d73c777246 - simplecov_json_formatter (0.1.4) sha256=529418fbe8de1713ac2b2d612aa3daa56d316975d307244399fa4838c601b428 + simplecov (1.1.1) sha256=25825ef13f0b2e74694d769817dad6ab8e90131dabdaa666e522fea105521e78 test-unit (3.7.7) sha256=3c89d5ff0690a16bef9946156c4624390402b9d54dfcf4ce9cbd5b06bead1e45 test-unit-ruby-core (1.0.14) sha256=d2e997796c9c5c5e8e31ac014f83a473ff5c2523a67cfa491b08893e12d43d22 turbo_tests (2.2.5) sha256=3fa31497d12976d11ccc298add29107b92bda94a90d8a0a5783f06f05102509f diff --git a/tool/test-coverage.rb b/tool/test-coverage.rb index 28ef0bf7f8ce21..4e321213c61edf 100644 --- a/tool/test-coverage.rb +++ b/tool/test-coverage.rb @@ -72,11 +72,8 @@ def save_coverage_data(res1) end def invoke_simplecov_formatter - # XXX docile-x.y.z and simplecov-x.y.z, simplecov-html-x.y.z, simplecov_json_formatter-x.y.z - %w[simplecov simplecov-html simplecov_json_formatter docile].each do |f| - Dir.glob("#{__dir__}/../.bundle/gems/#{f}-*/lib").each do |d| - $LOAD_PATH.unshift d - end + Dir.glob("#{__dir__}/../.bundle/gems/simplecov-*/lib").each do |d| + $LOAD_PATH.unshift d end require "simplecov"