diff --git a/.envrc b/.envrc index 92f925b17..2d6fa5f4f 100644 --- a/.envrc +++ b/.envrc @@ -1,3 +1,10 @@ +#!/bin/bash + +if [[ "$(uname)" == "Darwin" ]]; then + WORKERS=$(sysctl -n hw.physicalcpu) + export WORKERS +fi + # current git branch SOLARGRAPH_FORCE_VERSION=0.0.1.dev-$(git rev-parse --abbrev-ref HEAD | tr -d '\n' | tr -d '/' | tr -d '-'| tr -d '_') export SOLARGRAPH_FORCE_VERSION diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index 7a5ef4d05..170f8b85a 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -86,7 +86,7 @@ jobs: uses: ruby/setup-ruby@v1 with: ruby-version: 3.4 - bundler-cache: false + bundler-cache: true - name: Install gems run: bundle install @@ -103,7 +103,7 @@ jobs: uses: ruby/setup-ruby@v1 with: ruby-version: 3.4 - bundler-cache: false + bundler-cache: true - name: Install gems run: bundle install diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml index 3de7288eb..59843b74e 100644 --- a/.github/workflows/plugins.yml +++ b/.github/workflows/plugins.yml @@ -15,7 +15,7 @@ permissions: contents: read jobs: - regression: + rails_and_rspec_typechecking: runs-on: ubuntu-latest steps: @@ -44,9 +44,52 @@ jobs: run: bundle exec rbs collection update - name: Ensure typechecking still works run: bundle exec solargraph typecheck --level strong + rails_and_rspec_specs: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: 3.4 + bundler-cache: true + - uses: awalsh128/cache-apt-pkgs-action@latest + with: + packages: yq + version: 1.0 + - name: Install gems + run: | + echo 'gem "solargraph-rails"' > .Gemfile + echo 'gem "solargraph-rspec"' >> .Gemfile + bundle install + bundle update --pre rbs + - name: Configure to use plugins + run: | + bundle exec solargraph config + yq -yi '.plugins += ["solargraph-rails"]' .solargraph.yml + yq -yi '.plugins += ["solargraph-rspec"]' .solargraph.yml + - name: Install gem types + run: | + bundle exec rbs collection update + # avoid trying to do this in parallel during the specs + time bundle exec solargraph gems core stdlib default - name: Ensure specs still run - run: bundle exec rake spec - rails: + run: | + # Speed up some of the bundle installs we run inside the tests + # as well when we're testing different solargraph usage + # scenarios. This is already set in the local bundle config by + # the setup-ruby action. + # + # See + # https://github.com/ruby/setup-ruby?tab=readme-ov-file#caching-bundle-install-automatically + bundle config set path $PWD/vendor/bundle + + SIMPLECOV_DISABLED=true + export SIMPLECOV_DISABLED + + bundle exec rake full_spec + rails_typechecking: runs-on: ubuntu-latest steps: @@ -57,7 +100,7 @@ jobs: ruby-version: 3.4 # keep same as typecheck.yml # See https://github.com/castwide/solargraph/actions/runs/19000135777/job/54265647107?pr=1119 rubygems: latest - bundler-cache: false + bundler-cache: true - uses: awalsh128/cache-apt-pkgs-action@latest with: packages: yq @@ -75,9 +118,51 @@ jobs: run: bundle exec rbs collection update - name: Ensure typechecking still works run: bundle exec solargraph typecheck --level strong + rails_specs: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: 3.4 + rubygems: latest + bundler-cache: true + - uses: awalsh128/cache-apt-pkgs-action@latest + with: + packages: yq + version: 1.0 + - name: Install gems + run: | + echo 'gem "solargraph-rails"' > .Gemfile + bundle install + bundle update --pre rbs + - name: Configure to use plugins + run: | + bundle exec solargraph config + yq -yi '.plugins += ["solargraph-rails"]' .solargraph.yml + - name: Install gem types + run: | + bundle exec rbs collection update + # avoid trying to do this in parallel during the specs + bundle exec solargraph gems core stdlib - name: Ensure specs still run - run: bundle exec rake spec - rspec: + run: | + # Speed up some of the bundle installs we run inside the tests + # as well when we're testing different solargraph usage + # scenarios. This is already set in the local bundle config by + # the setup-ruby action. + # + # See + # https://github.com/ruby/setup-ruby?tab=readme-ov-file#caching-bundle-install-automatically + bundle config set path $PWD/vendor/bundle + + SIMPLECOV_DISABLED=true + export SIMPLECOV_DISABLED + + bundle exec rake full_spec + rspec_typechecking: runs-on: ubuntu-latest steps: @@ -86,7 +171,7 @@ jobs: uses: ruby/setup-ruby@v1 with: ruby-version: 3.4 # keep same as typecheck.yml - bundler-cache: false + bundler-cache: true - uses: awalsh128/cache-apt-pkgs-action@latest with: packages: yq @@ -104,9 +189,53 @@ jobs: run: bundle exec rbs collection update - name: Ensure typechecking still works run: bundle exec solargraph typecheck --level strong + rspec_specs: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: 3.4 # keep same as typecheck.yml + bundler-cache: true + - uses: awalsh128/cache-apt-pkgs-action@latest + with: + packages: yq + version: 1.0 + - name: Install gems + run: | + echo 'gem "solargraph-rspec"' >> .Gemfile + bundle install + bundle update --pre rbs + - name: Configure to use plugins + run: | + bundle exec solargraph config + yq -yi '.plugins += ["solargraph-rspec"]' .solargraph.yml + - name: Install gem types + run: | + set -x + + bundle exec rbs collection update + + rspec_gems=$(bundle exec ruby -r 'solargraph-rspec' -e 'puts Solargraph::Rspec::Gems.gem_names.join(" ")' 2>/dev/null | tail -n1) + # avoid trying to do this in parallel during the specs + bundle exec solargraph gems core stdlib $rspec_gems diff-lcs addressable ast rexml crack hashdiff rspec-support bigdecimal public_suffix - name: Ensure specs still run - run: bundle exec rake spec + run: | + # Speed up some of the bundle installs we run inside the tests + # as well when we're testing different solargraph usage + # scenarios. This is already set in the local bundle config by + # the setup-ruby action. + # + # See + # https://github.com/ruby/setup-ruby?tab=readme-ov-file#caching-bundle-install-automatically + bundle config set path $PWD/vendor/bundle + SIMPLECOV_DISABLED=true + export SIMPLECOV_DISABLED + + bundle exec rake full_spec run_solargraph_rspec_specs: # check out solargraph-rspec as well as this project, and point the former to use the latter as a local gem runs-on: ubuntu-latest @@ -129,14 +258,14 @@ jobs: with: ruby-version: 3.4 rubygems: latest - bundler-cache: false + bundler-cache: true - name: Install gems run: | set -x cd ../solargraph-rspec echo "gem 'solargraph', path: '../solargraph'" >> Gemfile - bundle config path ${{ env.BUNDLE_PATH }} + bundle config set path ${{ env.BUNDLE_PATH }} bundle install --jobs 4 --retry 3 bundle exec appraisal install # @todo some kind of appraisal/bundle conflict? @@ -166,7 +295,21 @@ jobs: bundle exec appraisal solargraph gems $rspec_gems - name: Run specs run: | + # Speed up some of the bundle installs we run inside the tests + # as well when we're testing different solargraph usage + # scenarios. This is already set in the local bundle config by + # the setup-ruby action. + # + # See + # https://github.com/ruby/setup-ruby?tab=readme-ov-file#caching-bundle-install-automatically + bundle config set path $PWD/vendor/bundle cd ../solargraph-rspec + + SIMPLECOV_DISABLED=true + export SIMPLECOV_DISABLED + + # avoid trying to do this in parallel during the specs + bundle exec solargraph gems core stdlib bundle exec appraisal rspec --format progress run_solargraph_rails_specs: @@ -184,7 +327,7 @@ jobs: with: # solargraph-rails supports Ruby 3.0+ ruby-version: '3.0' - bundler-cache: false + bundler-cache: true # https://github.com/apiology/solargraph/actions/runs/19400815835/job/55508092473?pr=17 rubygems: latest bundler: latest @@ -192,34 +335,50 @@ jobs: MATRIX_RAILS_VERSION: "7.0" - name: Install gems run: | - set -x - BUNDLE_PATH="${GITHUB_WORKSPACE:?}/vendor/bundle" - export BUNDLE_PATH - cd ../solargraph-rails - echo "gem 'solargraph', path: '${GITHUB_WORKSPACE:?}'" >> Gemfile - bundle install - bundle update --pre rbs - RAILS_DIR="$(pwd)/spec/rails7" - export RAILS_DIR - cd ${RAILS_DIR} - bundle install - bundle exec --gemfile ../../Gemfile rbs --version - bundle exec --gemfile ../../Gemfile rbs collection install - cd ../../ - # bundle exec rbs collection init - # bundle exec rbs collection install + set -x + # Share caches to speed up bundle install + # + # See + # https://github.com/ruby/setup-ruby?tab=readme-ov-file#caching-bundle-install-automatically + cd ../solargraph-rails + echo "gem 'solargraph', path: '${GITHUB_WORKSPACE:?}'" >> Gemfile + bundle install + bundle update --pre rbs + env: + MATRIX_RAILS_VERSION: "7.0" + MATRIX_RAILS_MAJOR_VERSION: '7' + - name: Install gem types + run: | + cd ../solargraph-rails + + RAILS_DIR="$(pwd)/spec/rails7" + export RAILS_DIR + cd ${RAILS_DIR} + bundle install + bundle exec --gemfile ../../Gemfile rbs --version + bundle exec --gemfile ../../Gemfile rbs collection install + cd ../../ + # bundle exec rbs collection init + # bundle exec rbs collection install env: MATRIX_RAILS_VERSION: "7.0" MATRIX_RAILS_MAJOR_VERSION: '7' - name: Run specs run: | - BUNDLE_PATH="${GITHUB_WORKSPACE:?}/vendor/bundle" - export BUNDLE_PATH + # Share caches to speed up bundle install + # + # See + # https://github.com/ruby/setup-ruby?tab=readme-ov-file#caching-bundle-install-automatically + bundle config set path $PWD/vendor/bundle cd ../solargraph-rails bundle exec solargraph --version bundle info solargraph bundle info rbs bundle info yard + + SIMPLECOV_DISABLED=true + export SIMPLECOV_DISABLED + ALLOW_IMPROVEMENTS=true bundle exec rake spec env: MATRIX_RAILS_VERSION: "7.0" diff --git a/.github/workflows/rspec.yml b/.github/workflows/rspec.yml index 174a1a6e3..393dc268d 100644 --- a/.github/workflows/rspec.yml +++ b/.github/workflows/rspec.yml @@ -70,6 +70,8 @@ jobs: uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby-version }} + # See https://github.com/apiology/solargraph/actions/runs/21799883466/job/62893545490?pr=27 + rubygems: latest bundler-cache: true - name: Set rbs version run: echo "gem 'rbs', '${{ matrix.rbs-version }}'" >> .Gemfile @@ -83,9 +85,24 @@ jobs: run: | bundle update rbs # use latest available for this Ruby version - name: Update types - run: bundle exec rbs collection update + run: | + bundle exec rbs collection update + # avoid trying to do this in parallel during the specs + bundle exec solargraph gems core stdlib - name: Run tests - run: bundle exec rake spec + run: | + # Speed up some of the bundle installs we run inside the tests + # as well when we're testing different solargraph usage + # scenarios. This is already set in the local bundle config by + # the setup-ruby action. + # + # See + # https://github.com/ruby/setup-ruby?tab=readme-ov-file#caching-bundle-install-automatically + bundle config set path $PWD/vendor/bundle + SIMPLECOV_DISABLED=true + export SIMPLECOV_DISABLED + + bundle exec rake full_spec undercover: runs-on: ubuntu-latest steps: @@ -99,10 +116,31 @@ jobs: with: ruby-version: '3.4' bundler-cache: true + - name: Update gems + run: | + bundle update rbs # use latest available for this Ruby version - name: Update types - run: bundle exec rbs collection update + run: | + set -x + + bundle exec rbs collection update + # avoid trying to do this in parallel during the specs + bundle exec solargraph gems core stdlib ast parser - name: Run tests - run: bundle exec rake spec + run: | + set -x + + # Speed up some of the bundle installs we run inside the tests + # as well when we're testing different solargraph usage + # scenarios. This is already set in the local bundle config by + # the setup-ruby action. + # + # See + # https://github.com/ruby/setup-ruby?tab=readme-ov-file#caching-bundle-install-automatically + bundle config set path $PWD/vendor/bundle + + bundle exec rake full_spec - name: Check PR coverage - run: bundle exec rake undercover + run: | + bundle exec rake undercover continue-on-error: true diff --git a/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml index ddb3e6527..d6f1fc92f 100644 --- a/.github/workflows/typecheck.yml +++ b/.github/workflows/typecheck.yml @@ -28,7 +28,7 @@ jobs: uses: ruby/setup-ruby@v1 with: ruby-version: 3.4 - bundler-cache: false + bundler-cache: true - name: Install gems run: | bundle install diff --git a/.rspec_parallel b/.rspec_parallel new file mode 100644 index 000000000..3af9812e9 --- /dev/null +++ b/.rspec_parallel @@ -0,0 +1,12 @@ +--seed 123 +--color +--require spec_helper +--profile +--order defined +--format progress +# to update test relative timings for better balancing in CI, +# uncomment the next line, run the specs, and PR in the changes: +# --format ParallelTests::RSpec::RuntimeLogger --out spec/parallel_runtime_rspec.log +--format ParallelTests::RSpec::SummaryLogger # --out tmp/spec_summary.log +# useful for debugging concurrency issues: +--format ParallelTests::RSpec::VerboseLogger diff --git a/Rakefile b/Rakefile index 398957b1a..7ae79019b 100755 --- a/Rakefile +++ b/Rakefile @@ -34,19 +34,21 @@ task :typecheck_alpha do end desc 'Run RSpec tests, starting with the ones that failed last time' -task spec: %i[spec_failed undercover_no_fail full_spec] do +task spec: %i[spec_failed full_spec] do undercover end desc 'Run all RSpec tests' task :full_spec do warn 'starting spec' - sh 'TEST_COVERAGE_COMMAND_NAME=full-new bundle exec rspec' # --profile' - warn 'ending spec' - # move coverage/full-new to coverage/full on success so that we - # always have the last successful run's 'coverage info + sh 'TEST_COVERAGE_COMMAND_NAME=full-new bundle exec parallel_rspec --runtime-log spec/parallel_runtime_rspec.log --verbose-command spec/' # --profile' + # clear now-outdated coverage FileUtils.rm_rf('coverage/full') - FileUtils.mv('coverage/full-new', 'coverage/full') + # move coverage/full-new to coverage/full on success so that we + # always have the last successful run's coverage info + unless ENV['SIMPLECOV_DISABLED'] + FileUtils.mv('coverage/full-new', 'coverage/full') + end end # @sg-ignore #undercover return type could not be inferred @@ -90,6 +92,8 @@ desc 'Re-run failed specs. Add --fail-fast in your .rspec-local file if desired task :spec_failed do # allow user to check out any persistent failures while looking for # more in the whole test suite + # + # Note: prspec doesn't support --only-failures, so we have to use rspec directly here. sh 'TEST_COVERAGE_COMMAND_NAME=next-failure bundle exec rspec --only-failures || true' end diff --git a/lib/solargraph/api_map.rb b/lib/solargraph/api_map.rb index 88e82a692..182327ea8 100755 --- a/lib/solargraph/api_map.rb +++ b/lib/solargraph/api_map.rb @@ -219,17 +219,19 @@ class << self # # @param directory [String] # @param out [IO, StringIO, nil] The output stream for messages + # @param rebuild [Boolean] whether to rebuild the pins even if they are cached # @param loose_unions [Boolean] See #initialize # # @return [ApiMap] - def self.load_with_cache directory, out = $stderr, loose_unions: true + # @api Used by solargraph-rails at least + def self.load_with_cache directory, out = $stderr, rebuild: false, loose_unions: true api_map = load(directory, loose_unions: loose_unions) - if api_map.uncached_gemspecs.empty? + if api_map.uncached_gemspecs.empty? && !rebuild logger.info { "All gems cached for #{directory}" } return api_map end - api_map.cache_all_for_doc_map!(out: out) + api_map.cache_all_for_doc_map!(out: out, rebuild: rebuild) load(directory, loose_unions: loose_unions) end @@ -754,6 +756,13 @@ def qualify_superclass fq_sub_tag store.qualify_superclass fq_sub_tag end + # @param require_path [String] + # + # @return [Array, nil] + def resolve_require require_path + workspace.resolve_require require_path + end + private # A hash of source maps with filename keys. diff --git a/lib/solargraph/complex_type.rb b/lib/solargraph/complex_type.rb index c2dcf469b..17a2815c8 100644 --- a/lib/solargraph/complex_type.rb +++ b/lib/solargraph/complex_type.rb @@ -33,6 +33,7 @@ def initialize types = [UniqueType::UNDEFINED] # @param gates [Array] # # @return [ComplexType] + # @param [Array] gates def qualify api_map, *gates red = reduce_object types = red.items.map do |t| @@ -434,6 +435,7 @@ class << self # Chain::Call needs to know the decl type (:arg, :optarg, # :kwarg, etc) of the arguments given, instead of just having # an array of Chains as the arguments. + # @param [Boolean] partial def parse *strings, partial: false # @type [Hash{Array => ComplexType, Array}] @cache ||= {} diff --git a/lib/solargraph/complex_type/unique_type.rb b/lib/solargraph/complex_type/unique_type.rb index 067b01082..f5f551c31 100644 --- a/lib/solargraph/complex_type/unique_type.rb +++ b/lib/solargraph/complex_type/unique_type.rb @@ -302,7 +302,6 @@ def desc rooted_tags end - # @sg-ignore Need better if/elseanalysis # @return [String] def to_rbs if duck_type? @@ -311,8 +310,8 @@ def to_rbs 'bool' elsif name.downcase == 'nil' 'nil' - elsif name == GENERIC_TAG_NAME - all_params.first&.name + elsif name == GENERIC_TAG_NAME && !all_params.empty? + all_params.first.name elsif %w[Class Module].include?(name) rbs_name elsif %w[Tuple Array].include?(name) && fixed_parameters? @@ -554,6 +553,7 @@ def transform new_name = nil, &transform_type # # @param api_map [ApiMap] The ApiMap that performs qualification # @param gates [Array] The namespaces from which to resolve names + # # @return [self, ComplexType, UniqueType] The generated ComplexType def qualify api_map, *gates transform do |t| diff --git a/lib/solargraph/diagnostics/base.rb b/lib/solargraph/diagnostics/base.rb index ff91a9062..cbc181e7c 100644 --- a/lib/solargraph/diagnostics/base.rb +++ b/lib/solargraph/diagnostics/base.rb @@ -20,8 +20,12 @@ def initialize *args # # @param source [Solargraph::Source] # @param api_map [Solargraph::ApiMap] + # @param workspace [Solargraph::Workspace, nil] + # Explicit workspace to use, instead of the current working + # directory's workspace. Useful in specs for isolation. + # # @return [Array] - def diagnose source, api_map + def diagnose source, api_map, workspace: nil [] end end diff --git a/lib/solargraph/diagnostics/type_check.rb b/lib/solargraph/diagnostics/type_check.rb index b1333f9d9..b2ff446c4 100644 --- a/lib/solargraph/diagnostics/type_check.rb +++ b/lib/solargraph/diagnostics/type_check.rb @@ -7,12 +7,13 @@ module Diagnostics # class TypeCheck < Base # @return [Array] - def diagnose source, api_map + def diagnose source, api_map, workspace: nil # return [] unless args.include?('always') || api_map.workspaced?(source.filename) severity = Diagnostics::Severities::ERROR level = args.reverse.find { |a| %w[normal typed strict strong].include?(a) } || :normal # @sg-ignore sensitive typing needs to handle || on nil types - checker = Solargraph::TypeChecker.new(source.filename, api_map: api_map, level: level.to_sym) + checker = Solargraph::TypeChecker.new(source.filename, api_map: api_map, level: level.to_sym, + workspace: workspace) checker.problems .sort { |a, b| a.location.range.start.line <=> b.location.range.start.line } .map do |problem| diff --git a/lib/solargraph/doc_map.rb b/lib/solargraph/doc_map.rb index 61589e016..1c3d06baf 100644 --- a/lib/solargraph/doc_map.rb +++ b/lib/solargraph/doc_map.rb @@ -3,6 +3,7 @@ require 'pathname' require 'benchmark' require 'open3' +require 'concurrent-ruby' module Solargraph # A collection of pins generated from specific 'require' statements @@ -69,20 +70,31 @@ def any_uncached? # @param rebuild [Boolean] whether to rebuild the pins even if they are cached # @return [void] def cache_doc_map_gems! out, rebuild: false + out&.puts 'Caching gems used by project' + PinCache.cache_core(out: out) unless PinCache.core? && !rebuild unless uncached_gemspecs.empty? logger.info do gem_desc = uncached_gemspecs.map { |gemspec| "#{gemspec.name}:#{gemspec.version}" }.join(', ') "Caching pins for gems: #{gem_desc}" end end + pool_size = Concurrent.processor_count # roughly your CPU count + pool = Concurrent::FixedThreadPool.new(pool_size) time = Benchmark.measure do - uncached_gemspecs.each do |gemspec| - cache(gemspec, rebuild: rebuild, out: out) + # Using 'names' as queue, run! + futures = uncached_gemspecs.map do |spec| + Concurrent::Promises.future_on(pool, spec) do + cache(spec, rebuild: rebuild, out: out) + end end + + Concurrent::Promises.zip(*futures).value! + pool.shutdown + pool.wait_for_termination end milliseconds = (time.real * 1000).round if (milliseconds > 500) && uncached_gemspecs.any? && out && uncached_gemspecs.any? - out.puts "Built #{uncached_gemspecs.length} gems in #{milliseconds} ms" + out.puts "Built #{uncached_gemspecs.length} gems in #{milliseconds} ms in #{pool_size} threads" end reset_pins! end diff --git a/lib/solargraph/language_server/host.rb b/lib/solargraph/language_server/host.rb index d61283567..ffdd7cc57 100644 --- a/lib/solargraph/language_server/host.rb +++ b/lib/solargraph/language_server/host.rb @@ -468,10 +468,28 @@ def stop notify_observers end + # @return [void] + def fully_stop + stop + # try for two minutes, raise if not fully stopped by then + start_time = Time.now + until fully_stopped? + if Time.now - start_time > 240 + raise 'Host did not fully stop within 240 seconds.' + end + logger.info 'Waiting for host to fully stop...' + sleep 0.1 + end + end + def stopped? @stopped end + def fully_stopped? + @stopped && diagnoser.fully_stopped? + end + # Locate multiple pins that match a completion item. The first match is # based on the corresponding location in a library source if available. # Subsequent matches are based on path. diff --git a/lib/solargraph/language_server/host/diagnoser.rb b/lib/solargraph/language_server/host/diagnoser.rb index 8c259c131..257b0ea8d 100644 --- a/lib/solargraph/language_server/host/diagnoser.rb +++ b/lib/solargraph/language_server/host/diagnoser.rb @@ -12,6 +12,7 @@ def initialize host @mutex = Mutex.new @queue = [] @stopped = true + @fully_stopped = true end # Schedule a file to be diagnosed. @@ -36,17 +37,25 @@ def stopped? @stopped end + def fully_stopped? + @fully_stopped + end + # Start the diagnosis thread. # # @return [self, nil] def start return unless @stopped - @stopped = false + @fully_stopped = @stopped = false + old_thread_id = Thread.current.object_id Thread.new do until stopped? + STDERR.puts "Diagnoser: start tick in thread #{old_thread_id}, current thread #{Thread.current.object_id}" tick + STDERR.puts "Diagnoser: end tick in thread #{old_thread_id}, current thread #{Thread.current.object_id}" sleep 0.1 end + @fully_stopped = true end self end diff --git a/lib/solargraph/pin_cache.rb b/lib/solargraph/pin_cache.rb index 886d55838..0e1d69088 100644 --- a/lib/solargraph/pin_cache.rb +++ b/lib/solargraph/pin_cache.rb @@ -134,19 +134,24 @@ def yardoc_processing? gemspec # @return [Array] a list of possible standard library names def possible_stdlibs - # all dirs and .rb files in Gem::RUBYGEMS_DIR - Dir.glob(File.join(Gem::RUBYGEMS_DIR, '*')).map do |file_or_dir| - basename = File.basename(file_or_dir) - # remove .rb - # @sg-ignore flow sensitive typing should be able to handle redefinition - basename = basename[0..-4] if basename.end_with?('.rb') - basename - end.sort.uniq - rescue StandardError => e - logger.info { "Failed to get possible stdlibs: #{e.message}" } - # @sg-ignore Need to add nil check here - logger.debug { e.backtrace.join("\n") } - [] + # all dirs and .rb files in Gem::RUBYGEMS_DIR/rubygems + local_stdlibs = + begin + Dir.glob(File.join(Gem::RUBYGEMS_DIR, 'rubygems', '*')).map do |file_or_dir| + basename = File.basename(file_or_dir) + # remove .rb + # @sg-ignore flow sensitive typing should be able to handle redefinition + basename = basename[0..-4] if basename.end_with?('.rb') + basename + end.sort.uniq + rescue StandardError => e + logger.info { "Failed to get possible stdlibs: #{e.message}" } + # @sg-ignore Need to add nil check here + logger.debug { e.backtrace.join("\n") } + [] + end + rbs_stdlibs = RbsMap::StdlibMap.possible_stdlibs + (local_stdlibs + rbs_stdlibs).sort.uniq end private diff --git a/lib/solargraph/rbs_map/stdlib_map.rb b/lib/solargraph/rbs_map/stdlib_map.rb index e6ebcf90f..d80d3b2a9 100644 --- a/lib/solargraph/rbs_map/stdlib_map.rb +++ b/lib/solargraph/rbs_map/stdlib_map.rb @@ -32,6 +32,7 @@ def initialize library, rebuild: false, out: $stderr end generated_pins = pins logger.debug { "Found #{generated_pins.length} pins for stdlib library #{library}" } + out&.puts "Caching RBS gem standard library pins for #{library}" PinCache.serialize_stdlib_require library, generated_pins end end @@ -66,6 +67,11 @@ def resolve_dependencies? def self.load library @stdlib_maps_hash[library] ||= StdlibMap.new(library) end + + # @return [Array] + def self.possible_stdlibs + RBS::Repository.default.gems.keys + end end end end diff --git a/lib/solargraph/shell.rb b/lib/solargraph/shell.rb index 8ee13eacf..3c2bfe04b 100755 --- a/lib/solargraph/shell.rb +++ b/lib/solargraph/shell.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require 'benchmark' +require 'concurrent-ruby' require 'thor' require 'yard' require 'yaml' @@ -158,43 +159,62 @@ def uncache *gems The 'core' argument can be used to cache the type documentation for the core Ruby libraries. + The literal 'stdlib' argument will cache all standard + libraries available. + + 'bundler/require' as a gem name will cache all auto-required + gems. + + 'default' will cache all gems used by Solargraph absent + specific requires in the files being looked at. + If the library is already cached, it will be rebuilt if the --rebuild option is set. Cached documentation is stored in #{PinCache.base_dir}, which can be stored between CI runs. ) + option :workspace, type: :boolean, desc: 'Rebuild all accessible gems, not just those used', default: false option :rebuild, type: :boolean, desc: 'Rebuild existing documentation', default: false # @param names [Array] # @return [void] def gems *names # print time with ms - workspace = Solargraph::Workspace.new('.') + api_map = Solargraph::ApiMap.new + workspace = api_map.workspace if names.empty? - workspace.cache_all_for_workspace!($stdout, rebuild: options[:rebuild]) + if options[:workspace] + workspace.cache_all_for_workspace!($stdout, rebuild: options[:rebuild]) + else + api_map.cache_all_for_doc_map!(out: $stdout, rebuild: options[:rebuild]) + end else - warn("Caching these gems: #{names}") - names.each do |name| - if name == 'core' - PinCache.cache_core(out: $stdout) if !PinCache.core? || options[:rebuild] - next - end - - gemspec = workspace.find_gem(*name.split('=')) - if gemspec.nil? + # run in parallel with a thread pool + + # create thread pool + pool_size = Concurrent.processor_count # roughly your CPU count + pool = Concurrent::FixedThreadPool.new(pool_size) + warn("Caching these gems with #{pool_size} workers: #{names}") + + # Using 'names' as queue, run! + futures = names.map do |name| + Concurrent::Promises.future_on(pool, name) do |_x| + cache_library(workspace, name) + rescue Gem::MissingSpecError warn "Gem '#{name}' not found" - else - workspace.cache_gem(gemspec, rebuild: options[:rebuild], out: $stdout) + rescue Gem::Requirement::BadRequirementError => e + warn "Gem '#{name}' failed while loading" + warn e.message + # @sg-ignore Need to add nil check here + warn e.backtrace.join("\n") end - rescue Gem::MissingSpecError - warn "Gem '#{name}' not found" - rescue Gem::Requirement::BadRequirementError => e - warn "Gem '#{name}' failed while loading" - warn e.message - # @sg-ignore Need to add nil check here - warn e.backtrace.join("\n") end + + Concurrent::Promises.zip(*futures).value! # raises if any failed + pool.shutdown + pool.wait_for_termination + warn "Documentation cached for #{names.count} gems." end end @@ -467,6 +487,43 @@ def host.send_notification method, params private + # @param name [String] + # @param [Workspace] workspace + # + # @return [void] + def cache_library workspace, name + if name == 'core' + PinCache.cache_core(out: $stdout) if !PinCache.core? || options[:rebuild] + return + end + + if name == 'stdlib' + workspace.cache_all_stdlibs(out: $stdout, rebuild: options[:rebuild]) + return + end + + if name == 'default' + doc_map = Solargraph::DocMap.new([], workspace) + doc_map.cache_doc_map_gems! $stdout + return + end + + if name == 'bundler/require' + gemspecs = workspace.resolve_require(name) + gemspecs&.each do |gs| + workspace.cache_gem(gs, rebuild: options[:rebuild], out: $stdout) + end + return + end + + gemspec = workspace.find_gem(*name.split('=')) + if gemspec.nil? + warn "Gem '#{name}' not found" + else + workspace.cache_gem(gemspec, rebuild: options[:rebuild], out: $stdout) + end + end + # @param pin [Solargraph::Pin::Base] # @return [String] def pin_description pin diff --git a/lib/solargraph/source/chain.rb b/lib/solargraph/source/chain.rb index 4bd1b67b6..205fa6ff4 100644 --- a/lib/solargraph/source/chain.rb +++ b/lib/solargraph/source/chain.rb @@ -148,6 +148,7 @@ def infer api_map, name_pin, locals @@inference_invalidation_key = api_map.hash @@inference_cache = {} end + # @todo Missed nil violation out = infer_uncached(api_map, name_pin, locals).downcast_to_literal_if_possible logger.debug do "Chain#infer() - caching result - cache_key_hash=#{cache_key.hash}, links.map(&:hash)=#{links.map(&:hash)}, links=#{links}, cache_key.map(&:hash) = #{cache_key.map(&:hash)}, cache_key=#{cache_key}" @@ -170,7 +171,11 @@ def infer_uncached api_map, name_pin, locals type = infer_from_definitions(pins, links.last.last_context, api_map, locals) out = maybe_nil(type) logger.debug do - "Chain#infer_uncached(links=#{links.map(&:desc)}, locals=#{locals.map(&:desc)}, name_pin=#{name_pin}, name_pin.closure=#{name_pin.closure.inspect}, name_pin.binder=#{name_pin.binder}) => #{out.rooted_tags.inspect}" + "Chain#infer_uncached(links=#{links.map(&:desc)}, " \ + "locals=#{locals.map(&:desc)}, " \ + "name_pin=#{name_pin}, " \ + "name_pin.closure=#{name_pin&.closure.inspect}, " \ + "name_pin.binder=#{name_pin&.binder}) => #{out.rooted_tags.inspect}" end out end diff --git a/lib/solargraph/workspace.rb b/lib/solargraph/workspace.rb index 351ee28a5..2cc57b1b0 100644 --- a/lib/solargraph/workspace.rb +++ b/lib/solargraph/workspace.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +require 'concurrent-ruby' require 'open3' require 'json' require 'yaml' @@ -23,8 +24,9 @@ class Workspace attr_reader :gemnames alias source_gems gemnames - # @todo Remove '' and '*' special cases - # @param directory [String] + # @todo Remove '*' special case + # @param directory [String] If empty, no config will be loaded, + # and no RBS collection will be used. Useful for specs. # @param config [Config, nil] # @param server [Hash] def initialize directory = '', config = nil, server = {} @@ -245,24 +247,45 @@ def all_gemspecs_from_bundle # @param rebuild [Boolean] whether to rebuild the pins even if they are cached # @return [void] def cache_all_for_workspace! out, rebuild: false + out&.puts 'Caching all gems available' PinCache.cache_core(out: out) unless PinCache.core? && !rebuild gem_specs = all_gemspecs_from_bundle # try any possible standard libraries, but be quiet about it stdlib_specs = pin_cache.possible_stdlibs.map { |stdlib| find_gem(stdlib, out: nil) }.compact specs = (gem_specs + stdlib_specs) - specs.each do |spec| - pin_cache.cache_gem(gemspec: spec, rebuild: rebuild, out: out) unless pin_cache.cached?(spec) + + pool_size = Concurrent.processor_count # roughly your CPU count + pool = Concurrent::FixedThreadPool.new(pool_size) + + # Using 'names' as queue, run! + futures = specs.map do |spec| + Concurrent::Promises.future_on(pool, spec) do + pin_cache.cache_gem(gemspec: spec, rebuild: rebuild, out: out) unless pin_cache.cached?(spec) + end end - out&.puts "Documentation cached for all #{specs.length} gems." + + Concurrent::Promises.zip(*futures).value! + pool.shutdown + pool.wait_for_termination + + out&.puts "Documentation cached for all #{specs.length} gems using #{pool_size} threads." # do this after so that we prefer stdlib requires from gems, # which are likely to be newer and have more pins - pin_cache.cache_all_stdlibs(out: out, rebuild: rebuild) + cache_all_stdlibs(out: out, rebuild: rebuild) out&.puts 'Documentation cached for core, standard library and gems.' end + # @param out [StringIO, IO, nil] output stream for logging + # @param rebuild [Boolean] whether to rebuild the pins even if they are cached + # + # @return [void] + def cache_all_stdlibs out: nil, rebuild: false + pin_cache.cache_all_stdlibs(out: out, rebuild: rebuild) + end + # Synchronize the workspace from the provided updater. # # @param updater [Source::Updater] diff --git a/lib/solargraph/workspace/config.rb b/lib/solargraph/workspace/config.rb index bd494b380..45e0ebb95 100644 --- a/lib/solargraph/workspace/config.rb +++ b/lib/solargraph/workspace/config.rb @@ -20,7 +20,11 @@ class Config # @param directory [String] def initialize directory = '' - @directory = File.absolute_path(directory) + @directory = if directory.empty? + '' + else + File.absolute_path(directory) + end @raw_data = config_data included excluded diff --git a/lib/solargraph/workspace/require_paths.rb b/lib/solargraph/workspace/require_paths.rb index d12364b07..23057a1af 100644 --- a/lib/solargraph/workspace/require_paths.rb +++ b/lib/solargraph/workspace/require_paths.rb @@ -13,7 +13,7 @@ class RequirePaths attr_reader :directory, :config # @param directory [String, nil] - # @param config [Config, nil] + # @param config [Config] def initialize directory, config @directory = directory @config = config diff --git a/lib/solargraph/yardoc.rb b/lib/solargraph/yardoc.rb index 4accf9425..57c81e6f9 100644 --- a/lib/solargraph/yardoc.rb +++ b/lib/solargraph/yardoc.rb @@ -18,6 +18,21 @@ module Yardoc def build_docs gem_yardoc_path, yard_plugins, gemspec return if docs_built?(gem_yardoc_path) + # @todo there's still a small race condition here + if processing?(gem_yardoc_path) + Solargraph.logger.info { "YARD doc build already in process for #{gemspec.name} #{gemspec.version}" } + + # Wait for up to 60 seconds for another process to finish building + timeout = 60 + start_time = Time.now + + sleep 1 until docs_built?(gem_yardoc_path) || (Time.now - start_time > timeout) + + return if docs_built?(gem_yardoc_path) + + raise "YARD doc build for #{gemspec.name} #{gemspec.version} did not finish in #{timeout} seconds" + end + unless Dir.exist? gemspec.gem_dir # Can happen in at least some (old?) RubyGems versions when we # have a gemspec describing a standard library like bundler. diff --git a/solargraph.gemspec b/solargraph.gemspec index 06edbf19f..4fc171c4b 100755 --- a/solargraph.gemspec +++ b/solargraph.gemspec @@ -34,6 +34,7 @@ Gem::Specification.new do |s| s.add_dependency 'backport', '~> 1.2' s.add_dependency 'benchmark', '~> 0.4' s.add_dependency 'bundler', '>= 2.0' + s.add_dependency 'concurrent-ruby', '~> 1.3', '>= 1.3.5' s.add_dependency 'diff-lcs', '~> 1.4' s.add_dependency 'jaro_winkler', '~> 1.6', '>= 1.6.1' s.add_dependency 'kramdown', '~> 2.3' @@ -53,10 +54,14 @@ Gem::Specification.new do |s| s.add_dependency 'yard-activesupport-concern', '~> 0.0' s.add_dependency 'yard-solargraph', '~> 0.1' + # use latest available based on Ruby version support - might need to + # set an upper bound if/when parallel_tests breaks compatibility + s.add_development_dependency 'parallel_tests', '>= 4.10.1' s.add_development_dependency 'pry', '~> 0.15' s.add_development_dependency 'public_suffix', '~> 3.1' s.add_development_dependency 'rake', '~> 13.2' s.add_development_dependency 'rspec', '~> 3.5' + s.add_development_dependency 'rspec-time-guard', '~> 0.2.0' # # very specific development-time RuboCop version patterns for CI # stability - feel free to update in an isolated PR diff --git a/spec/api_map_method_spec.rb b/spec/api_map_method_spec.rb index 87469562b..a9b5f8ce2 100644 --- a/spec/api_map_method_spec.rb +++ b/spec/api_map_method_spec.rb @@ -3,12 +3,14 @@ describe Solargraph::ApiMap do let(:api_map) { described_class.new } let(:bench) do - Solargraph::Bench.new(external_requires: external_requires, workspace: Solargraph::Workspace.new('.')) + Solargraph::Bench.new(external_requires: external_requires, + workspace: Solargraph::Workspace.new) end let(:external_requires) { [] } + let(:catalog) { false } before do - api_map.catalog bench + api_map.catalog bench if catalog end describe '#resolve_method_alias' do @@ -118,23 +120,34 @@ class B end describe '#get_method_stack' do - let(:out) { StringIO.new } - let(:api_map) { described_class.load_with_cache(Dir.pwd, out) } - context 'with stdlib that has vital dependencies' do let(:external_requires) { ['yaml'] } let(:method_stack) { api_map.get_method_stack('YAML', 'safe_load', scope: :class) } it 'handles the YAML gem aliased to Psych' do - expect(method_stack).not_to be_empty + if method_stack.nil? + specs = (api_map.resolve_require('yaml') || []) + (api_map.resolve_require('psych') || []) + expect(specs).not_to be_empty + specs.each { |spec| api_map.cache_gem(spec) } + api_map.catalog bench + end + + expect(method_stack).not_to be_nil end end context 'with thor' do let(:external_requires) { ['thor'] } + let(:method_stack) { api_map.get_method_stack('Thor', 'desc', scope: :class) } + let(:catalog) { true } it 'handles finding Thor.desc' do + specs = api_map.resolve_require('thor') + specs.each { |spec| api_map.cache_gem(spec) } + api_map.catalog bench + + # if this fails you may not have an rbs collection installed expect(method_stack).not_to be_empty end end diff --git a/spec/api_map_spec.rb b/spec/api_map_spec.rb index facf9489c..a1a93be80 100755 --- a/spec/api_map_spec.rb +++ b/spec/api_map_spec.rb @@ -770,10 +770,12 @@ def bar; end it 'resolves aliases for YARD methods' do dir = File.absolute_path(File.join('spec', 'fixtures', 'yard_map')) - yard_pins = Dir.chdir dir do - YARD::Registry.load([File.join(dir, 'attr.rb')], true) - mapper = Solargraph::YardMap::Mapper.new(YARD::Registry.all) - mapper.map + yard_pins = Solargraph::CHDIR_MUTEX.synchronize do + Dir.chdir dir do + YARD::Registry.load([File.join(dir, 'attr.rb')], true) + mapper = Solargraph::YardMap::Mapper.new(YARD::Registry.all) + mapper.map + end end source_pins = Solargraph::SourceMap.load_string(%( class Foo diff --git a/spec/diagnostics/rubocop_helpers_spec.rb b/spec/diagnostics/rubocop_helpers_spec.rb index 7bf374d67..98e31c233 100644 --- a/spec/diagnostics/rubocop_helpers_spec.rb +++ b/spec/diagnostics/rubocop_helpers_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -describe Solargraph::Diagnostics::RubocopHelpers do +describe Solargraph::Diagnostics::RubocopHelpers, order: :defined do context 'with custom version' do around do |example| old_gem_path = Gem.paths.path diff --git a/spec/doc_map_spec.rb b/spec/doc_map_spec.rb index 8ff1e70b1..3f1056cf4 100644 --- a/spec/doc_map_spec.rb +++ b/spec/doc_map_spec.rb @@ -9,7 +9,7 @@ end let(:out) { StringIO.new } - let(:pre_cache) { true } + let(:pre_cache) { false } let(:requires) { [] } let(:workspace) do @@ -19,10 +19,12 @@ let(:plain_doc_map) { described_class.new([], workspace, out: nil) } before do - doc_map.cache_doc_map_gems!(nil) if pre_cache + doc_map.cache_doc_map_gems!($stderr) if pre_cache end context 'with a require in solargraph test bundle' do + let(:pre_cache) { true } + let(:requires) do ['ast'] end @@ -34,6 +36,8 @@ end context 'when understanding rspec + rspec-mocks require pattern' do + let(:pre_cache) { true } + let(:requires) do ['rspec-mocks'] end @@ -67,17 +71,21 @@ end end - it 'does not warn for redundant requires' do - # Requiring 'set' is unnecessary because it's already included in core. It - # might make sense to log redundant requires, but a warning is overkill. - allow(Solargraph.logger).to receive(:warn).and_call_original - described_class.new(['set'], workspace) - expect(Solargraph.logger).not_to have_received(:warn).with(/path set/) + context 'with a redundant require' do + let(:pre_cache) { false } + + it 'does not warn' do + # Requiring 'set' is unnecessary because it's already included in core. It + # might make sense to log redundant requires, but a warning is overkill. + allow(Solargraph.logger).to receive(:warn).and_call_original + described_class.new(['set'], workspace) + expect(Solargraph.logger).not_to have_received(:warn).with(/path set/) + end end context 'when deserialization takes a while' do let(:pre_cache) { false } - let(:requires) { ['backport'] } + let(:requires) { ['rubocop-rspec'] } before do # proxy this method to simulate a long-running deserialization @@ -114,9 +122,13 @@ end context 'with require as bundle/require' do + let(:pre_cache) { false } + it 'imports all gems when bundler/require used' do - doc_map_with_bundler_require = described_class.new(['bundler/require'], workspace, out: nil) - doc_map_with_bundler_require.cache_doc_map_gems!(nil) + doc_map_with_bundler_require = described_class.new(['bundler/require'], workspace, out: $stderr) + if doc_map_with_bundler_require.pins.length <= plain_doc_map.pins.length + doc_map_with_bundler_require.cache_doc_map_gems!(nil) + end expect(doc_map_with_bundler_require.pins.length - plain_doc_map.pins.length).to be_positive end end diff --git a/spec/fixtures/workspace-with-gemfile/Gemfile b/spec/fixtures/workspace-with-gemfile/Gemfile index 9b1ff9e31..4c5f03f4c 100644 --- a/spec/fixtures/workspace-with-gemfile/Gemfile +++ b/spec/fixtures/workspace-with-gemfile/Gemfile @@ -1,3 +1,3 @@ source 'https://rubygems.org' -gem 'backport' +gem 'pry' diff --git a/spec/gem_pins_spec.rb b/spec/gem_pins_spec.rb index 944afd331..a3216c147 100644 --- a/spec/gem_pins_spec.rb +++ b/spec/gem_pins_spec.rb @@ -10,23 +10,29 @@ end context 'with a combined method pin' do - let(:path) { 'RBS::EnvironmentLoader#core_root' } - let(:requires) { ['rbs'] } + let(:path) { 'Hashdiff.diff' } + let(:requires) { ['hashdiff'] } it 'can merge YARD and RBS' do - expect(pin.source).to eq(:combined) + expect(pin.source).to eq(:combined), "Expected to merge YARD and RBS for #{path} in #{workspace.directory}" end it 'finds types from RBS' do - expect(pin.return_type.to_s).to eq('Pathname, nil') + expect(pin.return_type.to_s).to eq('Array') end it 'finds locations from YARD' do - expect(pin.location.filename).to end_with('environment_loader.rb') + expect(pin).not_to be_nil, "Expected to find pin for #{path} in #{workspace.directory}" + expect(pin.location.filename).to end_with('diff.rb') end end context 'with a YARD-only pin' do + before :context do + # run these on same runner so we don't cache rake in parallel; + # seems like we still have a race condition in pin caching + end + let(:requires) { ['rake'] } let(:path) { 'Rake::Task#prerequisites' } diff --git a/spec/language_server/host_spec.rb b/spec/language_server/host_spec.rb index f0497b8f3..360a25110 100644 --- a/spec/language_server/host_spec.rb +++ b/spec/language_server/host_spec.rb @@ -67,8 +67,11 @@ File.write(file, "foo = 'foo'") host.start host.prepare dir - Solargraph::LanguageServer::UriHelpers.file_to_uri(file) + file_uri = Solargraph::LanguageServer::UriHelpers.file_to_uri(file) host.open(file, File.read(file), 1) + # keep this from syncing a bunch of bundle gems in background + library = host.library_for(file_uri) + allow(library).to receive(:cacheable_specs).and_return([]) buffer = host.flush times = 0 # @todo Weak timeout for waiting until the diagnostics thread @@ -79,7 +82,8 @@ buffer = host.flush end expect(buffer).to include('textDocument/publishDiagnostics') - host.stop + ensure + host.fully_stop end end @@ -110,7 +114,13 @@ file1_uri = Solargraph::LanguageServer::UriHelpers.file_to_uri("#{app1_folder}/app.rb") file2_uri = Solargraph::LanguageServer::UriHelpers.file_to_uri("#{app2_folder}/app.rb") host.open_from_disk file1_uri + library = host.library_for(file1_uri) + # keep this from syncing a bunch of bundle gems in background + allow(library).to receive(:cacheable_specs).and_return([]) host.open_from_disk file2_uri + library = host.library_for(file2_uri) + # keep this from syncing a bunch of bundle gems in background + allow(library).to receive(:cacheable_specs).and_return([]) app1_map = host.document_symbols(file1_uri).map(&:path) expect(app1_map).to include('Folder1App') expect(app1_map).not_to include('Folder2App') @@ -121,8 +131,8 @@ it 'stops' do host = described_class.new - host.stop - expect(host.stopped?).to be(true) + host.fully_stop + expect(host.fully_stopped?).to be(true) end it 'retains orphaned sources' do @@ -133,6 +143,9 @@ host.prepare(dir) host.open(file_uri, File.read(file), 1) host.remove(dir) + # keep this from syncing a bunch of bundle gems in background + library = host.library_for(file_uri) + allow(library).to receive(:cacheable_specs).and_return([]) expect do host.document_symbols(file_uri) end.not_to raise_error @@ -209,6 +222,9 @@ def initialize(foo); end host = described_class.new host.prepare '' host.open uri, code, 1 + library = host.library_for(uri) + # keep this from syncing a bunch of bundle gems in background + allow(library).to receive(:cacheable_specs).and_return([]) sleep 0.1 until host.libraries.all?(&:mapped?) result = host.locate_pins({ 'data' => { @@ -247,15 +263,23 @@ def initialize(foo); end it 'rescues InvalidOffset errors' do host = described_class.new - host.open('file:///file.rb', 'class Foo; end', 1) + uri = 'file:///file.rb' + host.open(uri, 'class Foo; end', 1) + library = host.library_for(uri) + # keep this from syncing a bunch of bundle gems in background + allow(library).to receive(:cacheable_specs).and_return([]) expect { host.references_from('file:///file.rb', 0, 100) }.not_to raise_error end it 'logs InvalidOffset errors' do allow(Solargraph.logger).to receive(:warn) host = described_class.new - host.open('file:///file.rb', 'class Foo; end', 1) - host.references_from('file:///file.rb', 0, 100) + uri = 'file:///file.rb' + host.open(uri, 'class Foo; end', 1) + library = host.library_for(uri) + # keep this from syncing a bunch of bundle gems in background + allow(library).to receive(:cacheable_specs).and_return([]) + host.references_from(uri, 0, 100) expect(Solargraph.logger).to have_received(:warn).with(/InvalidOffsetError/) end end @@ -266,19 +290,27 @@ def initialize(foo); end end after do - @host.stop + @host.fully_stop end it 'creates a library for a file without a workspace' do - @host.open('file:///file.rb', 'class Foo; end', 1) - symbols = @host.document_symbols('file:///file.rb') + uri = 'file:///file.rb' + @host.open(uri, 'class Foo; end', 1) + library = @host.library_for(uri) + # keep this from syncing a bunch of bundle gems in background + allow(library).to receive(:cacheable_specs).and_return([]) + symbols = @host.document_symbols(uri) expect(symbols).not_to be_empty end it 'opens a file outside of prepared libraries' do @host.prepare(File.absolute_path(File.join('spec', 'fixtures', 'workspace'))) - @host.open('file:///file.rb', 'class Foo; end', 1) - symbols = @host.document_symbols('file:///file.rb') + uri = 'file:///file.rb' + @host.open(uri, 'class Foo; end', 1) + library = @host.library_for(uri) + # keep this from syncing a bunch of bundle gems in background + allow(library).to receive(:cacheable_specs).and_return([]) + symbols = @host.document_symbols(uri) expect(symbols).not_to be_empty end end diff --git a/spec/language_server/message/extended/check_gem_version_spec.rb b/spec/language_server/message/extended/check_gem_version_spec.rb index 26023f505..02f0b1c4a 100644 --- a/spec/language_server/message/extended/check_gem_version_spec.rb +++ b/spec/language_server/message/extended/check_gem_version_spec.rb @@ -36,6 +36,10 @@ end it 'responds to update actions' do + status = instance_double(Process::Status) + allow(status).to receive(:==).with(0).and_return(true) + allow(Open3).to receive(:capture2).with('gem update solargraph').and_return(['', status]) + host = Solargraph::LanguageServer::Host.new message = described_class.new(host, {}, current: Gem::Version.new('0.0.1')) message.process @@ -52,6 +56,7 @@ } host.receive action end.not_to raise_error + expect(Open3).to have_received(:capture2).with('gem update solargraph') end it 'uses bundler' do diff --git a/spec/language_server/message/text_document/definition_spec.rb b/spec/language_server/message/text_document/definition_spec.rb index d84d23cbe..541a9e397 100644 --- a/spec/language_server/message/text_document/definition_spec.rb +++ b/spec/language_server/message/text_document/definition_spec.rb @@ -25,6 +25,9 @@ } } }) + # keep this from syncing a bunch of bundle gems in background + library = host.library_for(file_uri) + allow(library).to receive(:cacheable_specs).and_return([]) message.process expect(message.result.first[:uri]).to eq(other_uri) end @@ -48,6 +51,9 @@ } } }) + # keep this from syncing a bunch of bundle gems in background + library = host.library_for(file_uri) + allow(library).to receive(:cacheable_specs).and_return([]) message.process expect(message.result.first[:uri]).to eq(other_uri) end @@ -58,12 +64,11 @@ host.prepare(path) sleep 0.1 until host.libraries.all?(&:mapped?) host.catalog + file_uri = Solargraph::LanguageServer::UriHelpers.file_to_uri(File.join(path, 'lib', 'other.rb')) message = described_class.new(host, { 'params' => { 'textDocument' => { - 'uri' => Solargraph::LanguageServer::UriHelpers.file_to_uri(File.join( - path, 'lib', 'other.rb' - )) + 'uri' => file_uri }, 'position' => { 'line' => 0, @@ -71,6 +76,9 @@ } } }) + # keep this from syncing a bunch of bundle gems in background + library = host.library_for(file_uri) + allow(library).to receive(:cacheable_specs).and_return([]) message.process expect(message.result.first[:uri]).to eq(Solargraph::LanguageServer::UriHelpers.file_to_uri(File.join(path, 'lib', 'thing.rb'))) diff --git a/spec/language_server/message/text_document/hover_spec.rb b/spec/language_server/message/text_document/hover_spec.rb index 76b3c9082..6ef633db2 100644 --- a/spec/language_server/message/text_document/hover_spec.rb +++ b/spec/language_server/message/text_document/hover_spec.rb @@ -29,12 +29,13 @@ def foo x = foo.upcase ) host = Solargraph::LanguageServer::Host.new - host.open('file:///test.rb', code, 1) + file_uri = 'file:///test.rb' + host.open(file_uri, code, 1) host.catalog message = described_class.new(host, { 'params' => { 'textDocument' => { - 'uri' => 'file:///test.rb' + 'uri' => file_uri }, 'position' => { 'line' => 4, @@ -42,7 +43,10 @@ def foo } } }) + library = host.library_for(file_uri) + allow(library).to receive(:cacheable_specs).and_return([]) message.process + # keep this from syncing a bunch of bundle gems in background expect(message.result[:contents][:value]).to eq("x\n\n`=~ String`") end end diff --git a/spec/language_server/message/text_document/rename_spec.rb b/spec/language_server/message/text_document/rename_spec.rb index 19903eaf9..4443f5cb4 100644 --- a/spec/language_server/message/text_document/rename_spec.rb +++ b/spec/language_server/message/text_document/rename_spec.rb @@ -1,10 +1,15 @@ # frozen_string_literal: true describe Solargraph::LanguageServer::Message::TextDocument::Rename do + let(:temp_file_url) do + # "file://#{Dir.mktmpdir}/file.rb" + 'file:///file.rb' + end + it 'renames a symbol' do host = Solargraph::LanguageServer::Host.new host.start - host.open('file:///file.rb', %( + host.open(temp_file_url, %( class Foo end foo = Foo.new @@ -15,7 +20,7 @@ class Foo 'method' => 'textDocument/rename', 'params' => { 'textDocument' => { - 'uri' => 'file:///file.rb' + 'uri' => temp_file_url }, 'position' => { 'line' => 1, @@ -24,14 +29,19 @@ class Foo 'newName' => 'Bar' } }) + # keep this from syncing a bunch of bundle gems in background + library = host.library_for(temp_file_url) + allow(library).to receive(:cacheable_specs).and_return([]) rename.process - expect(rename.result[:changes]['file:///file.rb'].length).to eq(2) + expect(rename.result[:changes][temp_file_url].length).to eq(2) + ensure + host.fully_stop end it 'renames an argument symbol from method signature' do host = Solargraph::LanguageServer::Host.new host.start - host.open('file:///file.rb', %( + host.open(temp_file_url, %( class Example def foo(bar) bar += 1 @@ -40,41 +50,15 @@ def foo(bar) end ), 1) + # keep this from syncing a bunch of bundle gems in background + library = host.library_for(temp_file_url) + allow(library).to receive(:cacheable_specs).and_return([]) rename = described_class.new(host, { 'id' => 1, 'method' => 'textDocument/rename', 'params' => { 'textDocument' => { - 'uri' => 'file:///file.rb' - }, - 'position' => { - 'line' => 2, - 'character' => 14 - }, - 'newName' => 'baz' - } - }) - rename.process - expect(rename.result[:changes]['file:///file.rb'].length).to eq(3) - end - - it 'renames an argument symbol from method body' do - host = Solargraph::LanguageServer::Host.new - host.start - host.open('file:///file.rb', %( - class Example - def foo(bar) - bar += 1 - return bar - end - end - ), 1) - rename = described_class.new(host, { - 'id' => 1, - 'method' => 'textDocument/rename', - 'params' => { - 'textDocument' => { - 'uri' => 'file:///file.rb' + 'uri' => temp_file_url }, 'position' => { 'line' => 3, @@ -83,14 +67,27 @@ def foo(bar) 'newName' => 'baz' } }) + # keep this from syncing a bunch of bundle gems in background + library = host.library_for(temp_file_url) + allow(library).to receive(:cacheable_specs).and_return([]) rename.process - expect(rename.result[:changes]['file:///file.rb'].length).to eq(3) + # try for 20 seconds to get the result, since this can be slow on CI + timeout = Time.now + 20 + until rename.result[:changes] && rename.result[:changes][temp_file_url] && !rename.result[:changes][temp_file_url].empty? + sleep 0.1 + if Time.now > timeout + raise "Timed out waiting for rename result: #{rename.result.inspect}" + end + end + expect(rename.result[:changes][temp_file_url].length).to eq(3) + ensure + host.fully_stop end it 'renames namespace symbol with proper range' do host = Solargraph::LanguageServer::Host.new host.start - host.open('file:///file.rb', %( + host.open(temp_file_url, %( module Namespace; end class Namespace::ExampleClass end @@ -101,7 +98,7 @@ class Namespace::ExampleClass 'method' => 'textDocument/rename', 'params' => { 'textDocument' => { - 'uri' => 'file:///file.rb' + 'uri' => temp_file_url }, 'position' => { 'line' => 2, @@ -110,10 +107,24 @@ class Namespace::ExampleClass 'newName' => 'Nameplace' } }) + # keep this from syncing a bunch of bundle gems in background + library = host.library_for(temp_file_url) + allow(library).to receive(:cacheable_specs).and_return([]) rename.process - changes = rename.result[:changes]['file:///file.rb'] + # try for 20 seconds to get the result, since this can be slow on CI + timeout = Time.now + 20 + until rename.result[:changes] && rename.result[:changes][temp_file_url] && !rename.result[:changes][temp_file_url].empty? + sleep 0.1 + if Time.now > timeout + raise "Timed out waiting for rename result: #{rename.result.inspect}" + end + end + changes = rename.result[:changes][temp_file_url] + expect(changes).not_to be_nil, -> { "Expected to find changes for #{temp_file_url} in #{rename.result.inspect}" } expect(changes.length).to eq(3) expect(changes.first[:range][:start][:character]).to eq(13) expect(changes.first[:range][:end][:character]).to eq(22) + ensure + host.fully_stop end end diff --git a/spec/language_server/message/text_document/type_definition_spec.rb b/spec/language_server/message/text_document/type_definition_spec.rb index 16f7f3006..52daf5ecd 100644 --- a/spec/language_server/message/text_document/type_definition_spec.rb +++ b/spec/language_server/message/text_document/type_definition_spec.rb @@ -1,6 +1,13 @@ # frozen_string_literal: true describe Solargraph::LanguageServer::Message::TextDocument::TypeDefinition do + around do |testobj| + # we need a consistent directory + Solargraph::CHDIR_MUTEX.synchronize do + testobj.run + end + end + it 'finds definitions of methods' do host = Solargraph::LanguageServer::Host.new host.prepare('spec/fixtures/workspace') @@ -19,6 +26,9 @@ } } }) + library = host.library_for(file_uri) + # keep this from syncing a bunch of bundle gems in background + allow(library).to receive(:cacheable_specs).and_return([]) message.process expect(message.result.first[:uri]).to eq(something_uri) end diff --git a/spec/language_server/message/workspace/did_change_watched_files_spec.rb b/spec/language_server/message/workspace/did_change_watched_files_spec.rb index ebe76fc50..c5c75fd5f 100644 --- a/spec/language_server/message/workspace/did_change_watched_files_spec.rb +++ b/spec/language_server/message/workspace/did_change_watched_files_spec.rb @@ -73,9 +73,12 @@ ] } }) + library = host.library_for(uri) + # keep this from syncing a bunch of bundle gems in background + allow(library).to receive(:cacheable_specs).and_return([]) changed.process expect(host.synchronizing?).to be(false) - library = host.library_for(uri) + expect(library.path_pins('Foo')).to be_empty expect(library.path_pins('FooBar')).not_to be_empty expect(changed.error).to be_nil diff --git a/spec/language_server/protocol_spec.rb b/spec/language_server/protocol_spec.rb index 25764e6eb..bfae331ca 100644 --- a/spec/language_server/protocol_spec.rb +++ b/spec/language_server/protocol_spec.rb @@ -1,11 +1,12 @@ # frozen_string_literal: true -class Protocol - attr_reader :response +require 'tmpdir' +require 'rubocop' - # @return [Solargraph::LanguageServer::Host] - attr_reader :host +class Protocol + attr_reader :response, :host + # @param host [Solargraph::LanguageServer::Host] def initialize host @host = host @host.start @@ -14,6 +15,7 @@ def initialize host @response = message end @message_id = 0 + STDERR.puts "Started in thread #{Thread.current.object_id}" end def request method, params @@ -30,16 +32,30 @@ def request method, params end def stop - @host.stop + @host.fully_stop end end -describe Protocol do +describe Protocol, order: :defined do before :all do @protocol = described_class.new(Solargraph::LanguageServer::Host.new) end - after :all do + # Ensure we don't start caching gems from current bundle in background + around do |testobj| + raise "Requests not finished #{testobj} - #{@protocol.host.pending_requests.inspect}" unless @protocol.host.pending_requests.empty? + temp_dir = Dir.mktmpdir + Dir.chdir temp_dir + Solargraph.with_clean_env do + testobj.run + end + raise "Requests not finished - #{@protocol.host.send(:requests).inspect}" unless @protocol.host.pending_requests.empty? + ensure + Dir.chdir PROJECT_DIRECTORY + FileUtils.remove_entry(temp_dir) + end + + after :context do @protocol.stop end @@ -84,6 +100,9 @@ def stop @protocol.request 'initialized', nil response = @protocol.response expect(response['error']).to be_nil + expect(@protocol.host.pending_requests.size).to eq(1) + pending_id = @protocol.host.pending_requests.first + @protocol.host.receive({ 'id' => pending_id }) end it 'configured default dynamic registration capabilities from initialized' do @@ -113,9 +132,10 @@ def bar baz end it 'handles textDocument/documentHighlight' do + file_uri = 'file:///file.rb' @protocol.request 'textDocument/documentHighlight', { 'textDocument' => { - 'uri' => 'file:///file.rb' + 'uri' => file_uri }, 'position' => { 'line' => 1, @@ -123,8 +143,9 @@ def bar baz } } response = @protocol.response + expect(response['result']).not_to be_nil, -> { "Expected result to be non-nil, got #{response.inspect}" } # Two references to Foo: the class definition and the Foo.new call - expect(response['result'].length).to eq(2) + expect(response['result'].length).to eq(2), -> { "Expected 2 highlights for Foo, got #{response['result'].length} in #{response.inspect}" } end it 'handles textDocument/didChange' do @@ -290,6 +311,7 @@ def bar baz } response = @protocol.response expect(response['error']).to be_nil + expect(response['result']).not_to be_nil, -> { "Expected result to be non-nil, got #{response.inspect}" } expect(response['result']['signatures']).not_to be_empty end @@ -414,6 +436,9 @@ def bar baz } expect(@protocol.host.options['autoformat']).to be(false) expect(@protocol.host.registered?('textDocument/completion')).to be(false) + expect(@protocol.host.pending_requests.size).to eq(1) + pending_id = @protocol.host.pending_requests.first + @protocol.host.receive({ 'id' => pending_id }) end it 'handles $/solargraph/checkGemVersion' do @@ -422,25 +447,35 @@ def bar baz expect(response['error']).to be_nil expect(response['result']['installed']).to be_a(String) expect(response['result']['available']).to be_a(String) + expect(@protocol.host.pending_requests.size).to eq(1) + pending_id = @protocol.host.pending_requests.first + @protocol.host.receive({ 'id' => pending_id }) end it 'handles $/solargraph/documentGems' do + status = instance_double(Process::Status) + allow(status).to receive(:==).with(0).and_return(true) + allow(Open3).to receive(:capture2).with('solargraph', 'gems').and_return(['', status]) + @protocol.request '$/solargraph/documentGems', {} response = @protocol.response + expect(response['error']).to be_nil + expect(Open3).to have_received(:capture2).with('solargraph', 'gems') end it 'handles textDocument/formatting' do + filename = File.realpath('spec/fixtures/formattable.rb', PROJECT_DIRECTORY) @protocol.request 'textDocument/didOpen', { 'textDocument' => { - 'uri' => Solargraph::LanguageServer::UriHelpers.file_to_uri(File.realpath('spec/fixtures/formattable.rb')), - 'text' => File.read('spec/fixtures/formattable.rb'), + 'uri' => Solargraph::LanguageServer::UriHelpers.file_to_uri(filename), + 'text' => File.read(filename), 'version' => 1 } } @protocol.request 'textDocument/formatting', { 'textDocument' => { - 'uri' => Solargraph::LanguageServer::UriHelpers.file_to_uri(File.realpath('spec/fixtures/formattable.rb')) + 'uri' => Solargraph::LanguageServer::UriHelpers.file_to_uri(filename) } } response = @protocol.response @@ -449,16 +484,17 @@ def bar baz end it 'can format file without file extension' do + filename = File.realpath('spec/fixtures/formattable', PROJECT_DIRECTORY) @protocol.request 'textDocument/didOpen', { 'textDocument' => { - 'uri' => Solargraph::LanguageServer::UriHelpers.file_to_uri(File.realpath('spec/fixtures/formattable')), - 'text' => File.read('spec/fixtures/formattable'), + 'uri' => Solargraph::LanguageServer::UriHelpers.file_to_uri(filename), + 'text' => File.read(filename), 'version' => 1 } } @protocol.request 'textDocument/formatting', { 'textDocument' => { - 'uri' => Solargraph::LanguageServer::UriHelpers.file_to_uri(File.realpath('spec/fixtures/formattable')) + 'uri' => Solargraph::LanguageServer::UriHelpers.file_to_uri(filename) } } response = @protocol.response diff --git a/spec/language_server/transport/adapter_spec.rb b/spec/language_server/transport/adapter_spec.rb index 23d6ac123..eb8ef30d5 100644 --- a/spec/language_server/transport/adapter_spec.rb +++ b/spec/language_server/transport/adapter_spec.rb @@ -21,7 +21,8 @@ def flush tester = AdapterTester.new tester.opening expect(tester.host).to be_a(Solargraph::LanguageServer::Host) - expect(tester.host).not_to be_stopped + expect(tester.host).not_to be_fully_stopped + tester.host.fully_stop end it 'stops a host on close' do @@ -29,6 +30,7 @@ def flush tester.opening tester.closing expect(tester.host).to be_stopped + tester.host.fully_stop end it 'stops Backport when the host stops' do @@ -40,6 +42,8 @@ def flush end end expect(tester.host).to be_stopped + tester.host.fully_stop + expect(tester.host).to be_fully_stopped end it 'processes sent data' do @@ -50,5 +54,6 @@ def flush tester.receiving "Content-Length: #{message.length}\r\n\r\n#{message}" end.not_to raise_error tester.closing + tester.host.fully_stop end end diff --git a/spec/library_spec.rb b/spec/library_spec.rb index 9f9ab87dc..f1bd60b6f 100644 --- a/spec/library_spec.rb +++ b/spec/library_spec.rb @@ -3,12 +3,16 @@ require 'tmpdir' require 'yard' -describe Solargraph::Library do +# run these in order so we don't uncache backport right when we +# need it before +describe Solargraph::Library, order: :defined do + let(:filename) { "file#{rand(1000)}.rb" } + it 'does not open created files in the workspace' do Dir.mktmpdir do |temp_dir_path| # Ensure we resolve any symlinks to their real path workspace_path = File.realpath(temp_dir_path) - file_path = File.join(workspace_path, 'file.rb') + file_path = File.join(workspace_path, filename) File.write(file_path, 'a = b') library = described_class.load(workspace_path) result = library.create(file_path, File.read(file_path)) @@ -19,11 +23,13 @@ it 'returns a Completion' do library = described_class.new + # keep this from syncing a bunch of bundle gems in background + allow(library).to receive(:cacheable_specs).and_return([]) library.attach Solargraph::Source.load_string(%( x = 1 x - ), 'file.rb', 0) - completion = library.completions_at('file.rb', 2, 7) + ), filename, 0) + completion = library.completions_at(filename, 2, 7) expect(completion).to be_a(Solargraph::SourceMap::Completion) expect(completion.pins.map(&:name)).to include('x') end @@ -34,7 +40,7 @@ end it 'returns a Completion', time_limit_seconds: 50 do - library = described_class.new(Solargraph::Workspace.new(Dir.pwd, + library = described_class.new(Solargraph::Workspace.new(PROJECT_DIRECTORY, Solargraph::Workspace::Config.new)) library.attach Solargraph::Source.load_string(%( require 'backport' @@ -43,9 +49,9 @@ def foo(adapter) adapter.remo end - ), 'file.rb', 0) + ), filename, 0) # give Solargraph time to cache the gem - while (completion = library.completions_at('file.rb', 5, 19)).pins.empty? + while (completion = library.completions_at(filename, 5, 19)).pins.empty? sleep 0.25 end expect(completion).to be_a(Solargraph::SourceMap::Completion) @@ -53,13 +59,9 @@ def foo(adapter) end end - context 'with a require from an already-cached external gem' do - before do - Solargraph::Shell.new.gems('backport') - end - + context 'with a require from an already-cached external gem', order: :defined do it 'returns a Completion' do - library = described_class.new(Solargraph::Workspace.new(Dir.pwd, + library = described_class.new(Solargraph::Workspace.new(PROJECT_DIRECTORY, Solargraph::Workspace::Config.new)) library.attach Solargraph::Source.load_string(%( require 'backport' @@ -68,8 +70,8 @@ def foo(adapter) def foo(adapter) adapter.remo end - ), 'file.rb', 0) - completion = library.completions_at('file.rb', 5, 19) + ), filename, 0) + completion = library.completions_at(filename, 5, 19) expect(completion).to be_a(Solargraph::SourceMap::Completion) expect(completion.pins.map(&:name)).to include('remote') end @@ -82,9 +84,11 @@ class Foo def bar end end - ), 'file.rb', 0 + ), filename, 0 + # keep this from syncing a bunch of bundle gems in background + allow(library).to receive(:cacheable_specs).and_return([]) library.attach src - paths = library.definitions_at('file.rb', 2, 13).map(&:path) + paths = library.definitions_at(filename, 2, 13).map(&:path) expect(paths).to include('Foo#bar') end @@ -98,9 +102,11 @@ def self.bar end end Foo.bar - ), 'file.rb', 0 + ), filename, 0 library.attach src - paths = library.type_definitions_at('file.rb', 7, 13).map(&:path) + # keep this from syncing a bunch of bundle gems in background + allow(library).to receive(:cacheable_specs).and_return([]) + paths = library.type_definitions_at(filename, 7, 13).map(&:path) expect(paths).to include('Bar') end @@ -112,9 +118,11 @@ def bar baz, key: '' end end Foo.new.bar() - ), 'file.rb', 0 + ), filename, 0 + # keep this from syncing a bunch of bundle gems in background + allow(library).to receive(:cacheable_specs).and_return([]) library.attach src - pins = library.signatures_at('file.rb', 5, 18) + pins = library.signatures_at(filename, 5, 18) expect(pins.length).to eq(1) expect(pins.first.path).to eq('Foo#bar') end @@ -152,9 +160,11 @@ def bar baz, key: '' library = described_class.new src = Solargraph::Source.load_string(%( puts 'hello' - ), 'file.rb', 0) + ), filename, 0) + # keep this from syncing a bunch of bundle gems in background + allow(library).to receive(:cacheable_specs).and_return([]) library.attach src - result = library.diagnose 'file.rb' + result = library.diagnose filename expect(result).to be_a(Array) # @todo More tests end @@ -165,11 +175,13 @@ def bar baz, key: '' allow(config).to receive_messages(plugins: [], required: [], reporters: ['all!']) workspace = Solargraph::Workspace.new directory, config library = described_class.new workspace + # keep this from syncing a bunch of bundle gems in background + allow(library).to receive(:cacheable_specs).and_return([]) src = Solargraph::Source.load_string(%( puts 'hello' - ), 'file.rb', 0) + ), filename, 0) library.attach src - result = library.diagnose 'file.rb' + result = library.diagnose filename expect(result.to_s).to include('rubocop') end @@ -180,18 +192,26 @@ class Foo def bar end end - ), 'file.rb', 0) + ), filename, 0) + # keep this from syncing a bunch of bundle gems in background + allow(library).to receive(:cacheable_specs).and_return([]) library.attach src - pins = library.document_symbols 'file.rb' + pins = library.document_symbols filename expect(pins.length).to eq(2) expect(pins.map(&:path)).to include('Foo') expect(pins.map(&:path)).to include('Foo#bar') end describe '#references_from' do + before :context do + Solargraph::Shell.new.gems('backport') + end + it 'collects references to a new method on a constant from assignment of Class.new' do workspace = Solargraph::Workspace.new('*') library = described_class.new(workspace) + # keep this from syncing a bunch of bundle gems in background + allow(library).to receive(:cacheable_specs).and_return([]) src1 = Solargraph::Source.load_string(%( Foo.new ), 'file1.rb', 0) @@ -209,6 +229,8 @@ def bar it 'collects references to a new method to a constant from assignment' do workspace = Solargraph::Workspace.new('*') library = described_class.new(workspace) + # keep this from syncing a bunch of bundle gems in background + allow(library).to receive(:cacheable_specs).and_return([]) src1 = Solargraph::Source.load_string(%( Foo.new ), 'file1.rb', 0) @@ -228,6 +250,8 @@ class Foo it 'collects references to an instance method symbol' do workspace = Solargraph::Workspace.new('*') library = described_class.new(workspace) + # keep this from syncing a bunch of bundle gems in background + allow(library).to receive(:cacheable_specs).and_return([]) src1 = Solargraph::Source.load_string(%( class Foo def bar @@ -255,6 +279,8 @@ def bar; end it 'collects references to a class method symbol' do workspace = Solargraph::Workspace.new('*') library = described_class.new(workspace) + # keep this from syncing a bunch of bundle gems in background + allow(library).to receive(:cacheable_specs).and_return([]) src1 = Solargraph::Source.load_string(%( class Foo def self.bar @@ -290,6 +316,8 @@ def bar; end it 'collects stripped references to constant symbols' do workspace = Solargraph::Workspace.new('*') library = described_class.new(workspace) + # keep this from syncing a bunch of bundle gems in background + allow(library).to receive(:cacheable_specs).and_return([]) src1 = Solargraph::Source.load_string(%( class Foo def bar @@ -319,6 +347,8 @@ class Other it 'rejects new references from different classes' do workspace = Solargraph::Workspace.new('*') library = described_class.new(workspace) + # keep this from syncing a bunch of bundle gems in background + allow(library).to receive(:cacheable_specs).and_return([]) source = Solargraph::Source.load_string(%( class Foo def bar @@ -350,6 +380,8 @@ def bar it 'returns YARD documentation from sources' do library = described_class.new + # keep this from syncing a bunch of bundle gems in background + allow(library).to receive(:cacheable_specs).and_return([]) src = Solargraph::Source.load_string(%( class Foo # My bar method @@ -385,6 +417,8 @@ def bar; end it 'finds unique references' do library = described_class.new(Solargraph::Workspace.new('*')) + # keep this from syncing a bunch of bundle gems in background + allow(library).to receive(:cacheable_specs).and_return([]) src1 = Solargraph::Source.load_string(%( class Foo end @@ -401,6 +435,8 @@ class Foo it 'includes method parameters in references' do library = described_class.new(Solargraph::Workspace.new('*')) + # keep this from syncing a bunch of bundle gems in background + allow(library).to receive(:cacheable_specs).and_return([]) source = Solargraph::Source.load_string(%( class Foo def bar(baz) @@ -417,6 +453,8 @@ def bar(baz) it "lies about names when client can't handle the truth" do library = described_class.new(Solargraph::Workspace.new('*')) + # keep this from syncing a bunch of bundle gems in background + allow(library).to receive(:cacheable_specs).and_return([]) source = Solargraph::Source.load_string(%( class Foo def 🤦🏻foo♀️; 123; end @@ -429,6 +467,8 @@ def 🤦🏻foo♀️; 123; end it 'tells the truth about names when client can handle the truth' do library = described_class.new(Solargraph::Workspace.new('*')) + # keep this from syncing a bunch of bundle gems in background + allow(library).to receive(:cacheable_specs).and_return([]) source = Solargraph::Source.load_string(%( class Foo def 🤦🏻foo♀️; 123; end @@ -441,6 +481,8 @@ def 🤦🏻foo♀️; 123; end it 'includes block parameters in references' do library = described_class.new(Solargraph::Workspace.new('*')) + # keep this from syncing a bunch of bundle gems in background + allow(library).to receive(:cacheable_specs).and_return([]) source = Solargraph::Source.load_string(%( 100.times do |foo| puts foo @@ -464,6 +506,8 @@ class CallerExample def foo; end end ), 'test.rb') + # keep this from syncing a bunch of bundle gems in background + allow(library).to receive(:cacheable_specs).and_return([]) library.attach source # Start of tag pins = library.definitions_at('test.rb', 4, 19) @@ -488,6 +532,8 @@ def foo; end end ), 'test.rb') library.attach source + # keep this from syncing a bunch of bundle gems in background + allow(library).to receive(:cacheable_specs).and_return([]) pins = library.definitions_at('test.rb', 5, 19) expect(pins.map(&:path)).to include('Tagged') pins = library.definitions_at('test.rb', 5, 26) @@ -506,6 +552,8 @@ def foo; end end ), 'test.rb') library.attach source + # keep this from syncing a bunch of bundle gems in background + allow(library).to receive(:cacheable_specs).and_return([]) pins = library.definitions_at('test.rb', 3, 31) expect(pins.map(&:path)).to include('TaggedExample') end @@ -520,12 +568,16 @@ def foo; end end ), 'test.rb') library.attach source + # keep this from syncing a bunch of bundle gems in background + allow(library).to receive(:cacheable_specs).and_return([]) pins = library.definitions_at('test.rb', 3, 31) expect(pins.map(&:path)).to include('TaggedExample') end it 'skips comment text outside of tags' do library = described_class.new + # keep this from syncing a bunch of bundle gems in background + allow(library).to receive(:cacheable_specs).and_return([]) source = Solargraph::Source.load_string(%( # String def foo; end @@ -537,6 +589,8 @@ def foo; end it 'marks aliases as methods or attributes in completion items' do library = described_class.new + # keep this from syncing a bunch of bundle gems in background + allow(library).to receive(:cacheable_specs).and_return([]) source = Solargraph::Source.load_string(%( class Example attr_reader :foo @@ -560,6 +614,8 @@ def baz it 'marks aliases as methods or attributes in definitions' do library = described_class.new + # keep this from syncing a bunch of bundle gems in background + allow(library).to receive(:cacheable_specs).and_return([]) source = Solargraph::Source.load_string(%( class Example attr_reader :foo @@ -608,6 +664,8 @@ def bar; end it 'removes files from Library#source_map_hash' do workspace = File.absolute_path(File.join('spec', 'fixtures', 'workspace')) library = described_class.load(workspace) + # keep this from syncing a bunch of bundle gems in background + allow(library).to receive(:cacheable_specs).and_return([]) library.map! library.catalog other_file = File.absolute_path(File.join('spec', 'fixtures', 'workspace', 'lib', 'other.rb')) diff --git a/spec/parallel_runtime_rspec.log b/spec/parallel_runtime_rspec.log new file mode 100644 index 000000000..9edbc950c --- /dev/null +++ b/spec/parallel_runtime_rspec.log @@ -0,0 +1,111 @@ +spec/parser/flow_sensitive_typing_spec.rb:2.833531000011135 +spec/pin/method_spec.rb:2.1155049999943003 +spec/source/chain_spec.rb:2.051872000010917 +spec/rbs_map_spec.rb:1.6378260000201408 +spec/source/source_chainer_spec.rb:0.782007000001613 +spec/diagnostics/type_check_spec.rb:0.5934520000009798 +spec/language_server/message/text_document/type_definition_spec.rb:0.48828400002093986 +spec/convention_spec.rb:0.24250100000062957 +spec/api_map/index_spec.rb:0.022086999990278855 +spec/source/chain/constant_spec.rb:0.019666000007418916 +spec/source/chain/literal_spec.rb:0.017955000017536804 +spec/pin/combine_with_spec.rb:0.005975999985821545 +spec/language_server/host/message_worker_spec.rb:0.005041999975219369 +spec/workspace/config_spec.rb:0.003792000003159046 +spec/api_map/cache_spec.rb:0.0030030000198166817 +spec/pin/search_spec.rb:0.000901999999769032 +spec/complex_type/unique_type_spec.rb:0.00044499998330138624 +spec/source/chain/call_spec.rb:8.694724999979371 +spec/rbs_map/conversions_spec.rb:2.364409000001615 +spec/convention/activesupport_concern_spec.rb:1.3549990000028629 +spec/language_server/message/text_document/definition_spec.rb:1.1764510000066366 +spec/language_server/protocol_spec.rb:0.9354319999984 +spec/complex_type_spec.rb:0.6957209999964107 +spec/source_map/mapper_spec.rb:0.6735090000147466 +spec/language_server/message/text_document/hover_spec.rb:0.1836759999860078 +spec/language_server/message/workspace/did_change_watched_files_spec.rb:0.05829699998139404 +spec/language_server/message/initialize_spec.rb:0.0123240000102669 +spec/parser/node_chainer_spec.rb:0.012198999989777803 +spec/parser/node_methods_spec.rb:0.005478000006405637 +spec/language_server/uri_helpers_spec.rb:0.00205199999618344 +spec/parser_spec.rb:0.0012719999940600246 +spec/pin/documenting_spec.rb:0.0008420000085607171 +spec/pin/constant_spec.rb:0.0005150000215508044 +spec/source/chain/link_spec.rb:0.00048000001697801054 +spec/source/chain/array_spec.rb:0.00034899997990578413 +spec/pin/keyword_spec.rb:9.200000204145908e-05 +spec/type_checker/levels/typed_spec.rb:17.06391200001235 +spec/pin/base_variable_spec.rb:0.49064100001123734 +spec/diagnostics/require_not_found_spec.rb:0.37786599999526516 +spec/api_map/store_spec.rb:0.24749399998108856 +spec/pin/base_spec.rb:0.22538600000552833 +spec/source/chain/global_variable_spec.rb:0.025301999994553626 +spec/language_server/host/diagnoser_spec.rb:0.007955000008223578 +spec/source/cursor_spec.rb:0.004138999996939674 +spec/language_server/message_spec.rb:0.0015610000118613243 +spec/pin/method_alias_spec.rb:0.00045399999362416565 +spec/position_spec.rb:0.00041700000292621553 +spec/source/chain/class_variable_spec.rb:0.0004140000091865659 +spec/library_spec.rb:15.087453000014648 +spec/pin/local_variable_spec.rb:1.257486999995308 +spec/diagnostics/rubocop_spec.rb:1.097308999975212 +spec/convention/struct_definition_spec.rb:0.8356930000009015 +spec/pin/delegated_method_spec.rb:0.37205700000049546 +spec/source/chain/q_call_spec.rb:0.22859600000083447 +spec/source/chain/z_super_spec.rb:0.06280600000172853 +spec/diagnostics/update_errors_spec.rb:0.026749999989988282 +spec/api_map/config_spec.rb:0.00786000001244247 +spec/rbs_map/stdlib_map_spec.rb:0.006338000006508082 +spec/parser/node_processor_spec.rb:0.005274999974062666 +spec/yard_map/mapper/to_method_spec.rb:0.0030719999922439456 +spec/source/change_spec.rb:0.001357999979518354 +spec/language_server/transport/data_reader_spec.rb:0.00020400001085363328 +spec/source/chain/head_spec.rb:0.00017899999511428177 +spec/pin/parameter_spec.rb:6.296593999984907 +spec/api_map_method_spec.rb:3.729546000016853 +spec/workspace/gemspecs_resolve_require_spec.rb:3.406676000013249 +spec/shell_spec.rb:2.0584040000103414 +spec/yard_map/mapper_spec.rb:1.5831159999943338 +spec/workspace/gemspecs_fetch_dependencies_spec.rb:1.2263030000030994 +spec/api_map/constants_spec.rb:0.6155290000024252 +spec/diagnostics/rubocop_helpers_spec.rb:0.5695389999891631 +spec/language_server/transport/adapter_spec.rb:0.20772500001476146 +spec/source/chain/instance_variable_spec.rb:0.0337850000069011 +spec/api_map/source_to_yard_spec.rb:0.02226700002211146 +spec/workspace/gemspecs_find_gem_spec.rb:0.01667399998405017 +spec/language_server/message/completion_item/resolve_spec.rb:0.00914300000295043 +spec/pin/namespace_spec.rb:0.0015030000067781657 +spec/type_checker/rules_spec.rb:0.0011689999955706298 +spec/diagnostics_spec.rb:0.0004610000178217888 +spec/diagnostics/base_spec.rb:0.0001449999981559813 +spec/doc_map_spec.rb:4.773218000016641 +spec/workspace/require_paths_spec.rb:3.413786999997683 +spec/convention/gemfile_spec.rb:2.8036969999957364 +spec/language_server/message/extended/check_gem_version_spec.rb:2.2211780000070576 +spec/yardoc_spec.rb:2.139534999994794 +spec/language_server/message/text_document/rename_spec.rb:1.623290999996243 +spec/complex_type/conforms_to_spec.rb:0.987976999982493 +spec/language_server/message/text_document/formatting_spec.rb:0.9810560000187252 +spec/type_checker_spec.rb:0.7090679999964777 +spec/source_spec.rb:0.09150699997553602 +spec/logging_spec.rb:0.0027310000150464475 +spec/source_map/node_processor_spec.rb:0.002203000010922551 +spec/language_server/message/workspace/did_change_configuration_spec.rb:0.0013220000255387276 +spec/pin/block_spec.rb:0.0011569999915082008 +spec/pin/symbol_spec.rb:0.000614999997196719 +spec/source/updater_spec.rb:0.0005689999961759895 +spec/pin/instance_variable_spec.rb:0.0004830000107176602 +spec/type_checker/levels/alpha_spec.rb:4.420573000010336 +spec/language_server/host_spec.rb:4.271099999983562 +spec/gem_pins_spec.rb:3.252444000012474 +spec/api_map_spec.rb:2.8428080000157934 +spec/pin_cache_spec.rb:2.1292580000008456 +spec/rbs_map/core_map_spec.rb:1.4518410000018775 +spec/workspace_spec.rb:1.1714219999848865 +spec/source/chain/or_spec.rb:0.4800159999867901 +spec/language_server/host/dispatch_spec.rb:0.35667099998681806 +spec/source_map_spec.rb:0.281221999990521 +spec/source_map/clip_spec.rb:22.63660699999309 +spec/type_checker/levels/strong_spec.rb:25.854432000021916 +spec/type_checker/levels/normal_spec.rb:31.006626999995206 +spec/type_checker/levels/strict_spec.rb:33.33830899998429 diff --git a/spec/parser/flow_sensitive_typing_spec.rb b/spec/parser/flow_sensitive_typing_spec.rb index cee6afef1..f531c2c07 100644 --- a/spec/parser/flow_sensitive_typing_spec.rb +++ b/spec/parser/flow_sensitive_typing_spec.rb @@ -1,8 +1,13 @@ # frozen_string_literal: true +require 'tempfile' + # @todo These tests depend on `Clip`, but we're putting the tests here to # avoid overloading clip_spec.rb. describe Solargraph::Parser::FlowSensitiveTyping do + # random temporary filename ending in '.rb' using tmpfile + let(:filename) { Tempfile.new(['flow_sensitive_typing_spec', '.rb']).path } + it 'uses is_a? in a simple if() to refine types' do source = Solargraph::Source.load_string(%( class ReproBase; end @@ -15,12 +20,12 @@ def verify_repro(repr) repr end end - ), 'test.rb') + ), filename) api_map = Solargraph::ApiMap.new.map(source) - clip = api_map.clip_at('test.rb', [6, 10]) + clip = api_map.clip_at(filename, [6, 10]) expect(clip.infer.to_s).to eq('Repro') - clip = api_map.clip_at('test.rb', [8, 10]) + clip = api_map.clip_at(filename, [8, 10]) expect(clip.infer.to_s).to eq('ReproBase') end @@ -37,12 +42,12 @@ def verify_repro(repr) repr end end - ), 'test.rb') + ), filename) api_map = Solargraph::ApiMap.new.map(source) - clip = api_map.clip_at('test.rb', [7, 10]) + clip = api_map.clip_at(filename, [7, 10]) expect(clip.infer.to_s).to eq('Repro1') - clip = api_map.clip_at('test.rb', [9, 10]) + clip = api_map.clip_at(filename, [9, 10]) expect(clip.infer.to_s).to eq('Repro2') end @@ -60,12 +65,12 @@ def verify_repro(repr) repr end end - ), 'test.rb') + ), filename) api_map = Solargraph::ApiMap.new.map(source) - clip = api_map.clip_at('test.rb', [8, 10]) + clip = api_map.clip_at(filename, [8, 10]) expect(clip.infer.to_s).to eq('Foo::Repro') - clip = api_map.clip_at('test.rb', [10, 10]) + clip = api_map.clip_at(filename, [10, 10]) expect(clip.infer.to_s).to eq('ReproBase') end @@ -85,12 +90,12 @@ def verify_repro(repr) repr end end - ), 'test.rb') + ), filename) api_map = Solargraph::ApiMap.new.map(source) - clip = api_map.clip_at('test.rb', [10, 10]) + clip = api_map.clip_at(filename, [10, 10]) expect(clip.infer.to_s).to eq('Foo::Bar::Repro') - clip = api_map.clip_at('test.rb', [12, 10]) + clip = api_map.clip_at(filename, [12, 10]) expect(clip.infer.to_s).to eq('ReproBase') end @@ -106,12 +111,12 @@ def verify_repro(repr) repr end end - ), 'test.rb') + ), filename) api_map = Solargraph::ApiMap.new.map(source) - clip = api_map.clip_at('test.rb', [6, 10]) + clip = api_map.clip_at(filename, [6, 10]) expect(clip.infer.to_s).to eq('ReproBase') - clip = api_map.clip_at('test.rb', [8, 10]) + clip = api_map.clip_at(filename, [8, 10]) expect(clip.infer.to_s).to eq('Repro') end @@ -127,12 +132,12 @@ def verify_repro(repr) repr end end - ), 'test.rb') + ), filename) api_map = Solargraph::ApiMap.new.map(source) - clip = api_map.clip_at('test.rb', [6, 10]) + clip = api_map.clip_at(filename, [6, 10]) expect(clip.infer.to_s).to eq('Repro1') - clip = api_map.clip_at('test.rb', [8, 10]) + clip = api_map.clip_at(filename, [8, 10]) expect(clip.infer.to_s).to eq('ReproBase') end @@ -151,15 +156,15 @@ def verify_repro(repr) repr end end - ), 'test.rb') + ), filename) api_map = Solargraph::ApiMap.new.map(source) - clip = api_map.clip_at('test.rb', [7, 10]) + clip = api_map.clip_at(filename, [7, 10]) expect(clip.infer.to_s).to eq('Repro1') - clip = api_map.clip_at('test.rb', [9, 10]) + clip = api_map.clip_at(filename, [9, 10]) expect(clip.infer.to_s).to eq('Repro2') - clip = api_map.clip_at('test.rb', [11, 10]) + clip = api_map.clip_at(filename, [11, 10]) expect(clip.infer.to_s).to eq('ReproBase') end @@ -173,9 +178,9 @@ class Repro < ReproBase; end break unless value.is_a? Repro value end - ), 'test.rb') + ), filename) api_map = Solargraph::ApiMap.new.map(source) - clip = api_map.clip_at('test.rb', [7, 8]) + clip = api_map.clip_at(filename, [7, 8]) expect(clip.infer.to_s).to eq('Repro') end @@ -189,9 +194,9 @@ class Repro < ReproBase; end break unless value.is_a? Repro value end - ), 'test.rb') + ), filename) api_map = Solargraph::ApiMap.new.map(source) - clip = api_map.clip_at('test.rb', [7, 8]) + clip = api_map.clip_at(filename, [7, 8]) expect(clip.infer.to_s).to eq('Repro') end @@ -205,9 +210,9 @@ class Repro < ReproBase; end break unless value.is_a? Repro value end - ), 'test.rb') + ), filename) api_map = Solargraph::ApiMap.new.map(source) - clip = api_map.clip_at('test.rb', [7, 8]) + clip = api_map.clip_at(filename, [7, 8]) expect(clip.infer.to_s).to eq('Repro') end @@ -222,16 +227,16 @@ class Repro < ReproBase; end value end - ), 'test.rb') + ), filename) api_map = Solargraph::ApiMap.new.map(source) - clip = api_map.clip_at('test.rb', [3, 6]) + clip = api_map.clip_at(filename, [3, 6]) expect(clip.infer.to_s).to eq('Array') - clip = api_map.clip_at('test.rb', [5, 8]) + clip = api_map.clip_at(filename, [5, 8]) expect(clip.infer.to_s).to eq('Numeric') - clip = api_map.clip_at('test.rb', [7, 8]) + clip = api_map.clip_at(filename, [7, 8]) expect(clip.infer.to_s).to eq('Float') end @@ -247,16 +252,16 @@ def verify_repro(repr, throw_the_dice) repr end end - ), 'test.rb') + ), filename) api_map = Solargraph::ApiMap.new.map(source) - clip = api_map.clip_at('test.rb', [4, 8]) + clip = api_map.clip_at(filename, [4, 8]) expect(clip.infer.rooted_tags).to eq('::Integer, nil') - clip = api_map.clip_at('test.rb', [6, 10]) + clip = api_map.clip_at(filename, [6, 10]) expect(clip.infer.rooted_tags).to eq('::Integer') - clip = api_map.clip_at('test.rb', [8, 10]) + clip = api_map.clip_at(filename, [8, 10]) expect(clip.infer.rooted_tags).to eq('nil') end @@ -270,10 +275,10 @@ class Repro < ReproBase; end break unless value value end - ), 'test.rb') + ), filename) api_map = Solargraph::ApiMap.new.map(source) - clip = api_map.clip_at('test.rb', [7, 8]) + clip = api_map.clip_at(filename, [7, 8]) expect(clip.infer.to_s).to eq('ReproBase') end @@ -287,10 +292,10 @@ class Repro < ReproBase; end break if value.nil? value end - ), 'test.rb') + ), filename) api_map = Solargraph::ApiMap.new.map(source) - clip = api_map.clip_at('test.rb', [7, 8]) + clip = api_map.clip_at(filename, [7, 8]) expect(clip.infer.to_s).to eq('ReproBase') end @@ -304,13 +309,13 @@ def baz; end bar bar = Foo.new bar - ), 'test.rb') + ), filename) api_map = Solargraph::ApiMap.new.map(source) - clip = api_map.clip_at('test.rb', [6, 6]) + clip = api_map.clip_at(filename, [6, 6]) expect(clip.infer.to_s).to eq('Foo') - clip = api_map.clip_at('test.rb', [8, 6]) + clip = api_map.clip_at(filename, [8, 6]) expect(clip.infer.to_s).to eq('Foo') end @@ -319,9 +324,9 @@ def baz; end if is_a? Object x end - ), 'test.rb') + ), filename) api_map = Solargraph::ApiMap.new.map(source) - clip = api_map.clip_at('test.rb', [2, 6]) + clip = api_map.clip_at(filename, [2, 6]) expect { clip.infer.to_s }.not_to raise_error end @@ -331,9 +336,9 @@ def baz; end if r.is_a? x end - ), 'test.rb') + ), filename) api_map = Solargraph::ApiMap.new.map(source) - clip = api_map.clip_at('test.rb', [3, 6]) + clip = api_map.clip_at(filename, [3, 6]) expect { clip.infer.to_s }.not_to raise_error end @@ -349,15 +354,15 @@ def verify_repro(repr) repr end end - ), 'test.rb') + ), filename) api_map = Solargraph::ApiMap.new.map(source) - clip = api_map.clip_at('test.rb', [4, 8]) + clip = api_map.clip_at(filename, [4, 8]) expect(clip.infer.rooted_tags).to eq('::Integer, nil') - clip = api_map.clip_at('test.rb', [6, 10]) + clip = api_map.clip_at(filename, [6, 10]) expect(clip.infer.rooted_tags).to eq('nil') - clip = api_map.clip_at('test.rb', [8, 10]) + clip = api_map.clip_at(filename, [8, 10]) expect(clip.infer.rooted_tags).to eq('::Integer') end @@ -373,15 +378,15 @@ def verify_repro(repr, throw_the_dice) repr end end - ), 'test.rb') + ), filename) api_map = Solargraph::ApiMap.new.map(source) - clip = api_map.clip_at('test.rb', [4, 8]) + clip = api_map.clip_at(filename, [4, 8]) expect(clip.infer.rooted_tags).to eq('::Integer, nil') - clip = api_map.clip_at('test.rb', [6, 10]) + clip = api_map.clip_at(filename, [6, 10]) expect(clip.infer.rooted_tags).to eq('nil') - clip = api_map.clip_at('test.rb', [8, 10]) + clip = api_map.clip_at(filename, [8, 10]) expect(clip.infer.rooted_tags).to eq('::Integer, nil') end @@ -397,15 +402,15 @@ def verify_repro(repr, throw_the_dice) repr end end - ), 'test.rb') + ), filename) api_map = Solargraph::ApiMap.new.map(source) - clip = api_map.clip_at('test.rb', [4, 8]) + clip = api_map.clip_at(filename, [4, 8]) expect(clip.infer.rooted_tags).to eq('::Integer, nil') - clip = api_map.clip_at('test.rb', [6, 10]) + clip = api_map.clip_at(filename, [6, 10]) expect(clip.infer.rooted_tags).to eq('nil') - clip = api_map.clip_at('test.rb', [8, 10]) + clip = api_map.clip_at(filename, [8, 10]) expect(clip.infer.rooted_tags).to eq('::Integer, nil') end @@ -421,15 +426,15 @@ def verify_repro(repr, throw_the_dice) repr end end - ), 'test.rb') + ), filename) api_map = Solargraph::ApiMap.new.map(source) - clip = api_map.clip_at('test.rb', [4, 8]) + clip = api_map.clip_at(filename, [4, 8]) expect(clip.infer.rooted_tags).to eq('::Integer, nil') - clip = api_map.clip_at('test.rb', [6, 10]) + clip = api_map.clip_at(filename, [6, 10]) expect(clip.infer.rooted_tags).to eq('::Integer, nil') - clip = api_map.clip_at('test.rb', [8, 10]) + clip = api_map.clip_at(filename, [8, 10]) expect(clip.infer.rooted_tags).to eq('::Integer') end @@ -445,15 +450,15 @@ def verify_repro(repr, throw_the_dice) repr end end - ), 'test.rb') + ), filename) api_map = Solargraph::ApiMap.new.map(source) - clip = api_map.clip_at('test.rb', [4, 8]) + clip = api_map.clip_at(filename, [4, 8]) expect(clip.infer.rooted_tags).to eq('::Integer, nil') - clip = api_map.clip_at('test.rb', [6, 10]) + clip = api_map.clip_at(filename, [6, 10]) expect(clip.infer.rooted_tags).to eq('::Integer, nil') - clip = api_map.clip_at('test.rb', [8, 10]) + clip = api_map.clip_at(filename, [8, 10]) expect(clip.infer.rooted_tags).to eq('::Integer') end @@ -469,15 +474,15 @@ def verify_repro(repr, throw_the_dice) repr end end - ), 'test.rb') + ), filename) api_map = Solargraph::ApiMap.new.map(source) - clip = api_map.clip_at('test.rb', [4, 8]) + clip = api_map.clip_at(filename, [4, 8]) expect(clip.infer.rooted_tags).to eq('::Integer, nil') - clip = api_map.clip_at('test.rb', [6, 10]) + clip = api_map.clip_at(filename, [6, 10]) expect(clip.infer.rooted_tags).to eq('::Integer, nil') - clip = api_map.clip_at('test.rb', [8, 10]) + clip = api_map.clip_at(filename, [8, 10]) expect(clip.infer.rooted_tags).to eq('nil') end @@ -493,15 +498,15 @@ def verify_repro(repr, throw_the_dice) repr end end - ), 'test.rb') + ), filename) api_map = Solargraph::ApiMap.new.map(source) - clip = api_map.clip_at('test.rb', [4, 8]) + clip = api_map.clip_at(filename, [4, 8]) expect(clip.infer.rooted_tags).to eq('::Integer, nil') - clip = api_map.clip_at('test.rb', [6, 10]) + clip = api_map.clip_at(filename, [6, 10]) expect(clip.infer.rooted_tags).to eq('::Integer, nil') - clip = api_map.clip_at('test.rb', [8, 10]) + clip = api_map.clip_at(filename, [8, 10]) expect(clip.infer.rooted_tags).to eq('nil') end @@ -513,15 +518,15 @@ def verify_repro(repr) repr unless repr.nil? || repr.downcase repr end - ), 'test.rb') + ), filename) api_map = Solargraph::ApiMap.new.map(source) - clip = api_map.clip_at('test.rb', [4, 33]) + clip = api_map.clip_at(filename, [4, 33]) expect(clip.infer.rooted_tags).to eq('::String') - clip = api_map.clip_at('test.rb', [4, 8]) + clip = api_map.clip_at(filename, [4, 8]) expect(clip.infer.rooted_tags).to eq('::String') - clip = api_map.clip_at('test.rb', [5, 8]) + clip = api_map.clip_at(filename, [5, 8]) expect(clip.infer.rooted_tags).to eq('::String, nil') end @@ -537,15 +542,15 @@ def verify_repro(repr, throw_the_dice) repr end end - ), 'test.rb') + ), filename) api_map = Solargraph::ApiMap.new.map(source) - clip = api_map.clip_at('test.rb', [4, 8]) + clip = api_map.clip_at(filename, [4, 8]) expect(clip.infer.rooted_tags).to eq('::Integer, nil') - clip = api_map.clip_at('test.rb', [6, 10]) + clip = api_map.clip_at(filename, [6, 10]) expect(clip.infer.rooted_tags).to eq('::Integer') - clip = api_map.clip_at('test.rb', [8, 10]) + clip = api_map.clip_at(filename, [8, 10]) expect(clip.infer.rooted_tags).to eq('::Integer, nil') end @@ -561,15 +566,15 @@ def verify_repro(repr, throw_the_dice) repr end end - ), 'test.rb') + ), filename) api_map = Solargraph::ApiMap.new.map(source) - clip = api_map.clip_at('test.rb', [4, 8]) + clip = api_map.clip_at(filename, [4, 8]) expect(clip.infer.rooted_tags).to eq('::Integer, nil') - clip = api_map.clip_at('test.rb', [6, 10]) + clip = api_map.clip_at(filename, [6, 10]) expect(clip.infer.rooted_tags).to eq('::Integer') - clip = api_map.clip_at('test.rb', [8, 10]) + clip = api_map.clip_at(filename, [8, 10]) expect(clip.infer.rooted_tags).to eq('::Integer, nil') end @@ -585,15 +590,15 @@ def verify_repro(repr) repr end end - ), 'test.rb') + ), filename) api_map = Solargraph::ApiMap.new.map(source) - clip = api_map.clip_at('test.rb', [4, 8]) + clip = api_map.clip_at(filename, [4, 8]) expect(clip.infer.rooted_tags).to eq('::Integer, nil') - clip = api_map.clip_at('test.rb', [6, 10]) + clip = api_map.clip_at(filename, [6, 10]) expect(clip.infer.rooted_tags).to eq('::Integer') - clip = api_map.clip_at('test.rb', [8, 10]) + clip = api_map.clip_at(filename, [8, 10]) expect(clip.infer.rooted_tags).to eq('nil') end @@ -608,15 +613,15 @@ def verify_repro(repr = nil) repr end end - ), 'test.rb') + ), filename) api_map = Solargraph::ApiMap.new.map(source) - clip = api_map.clip_at('test.rb', [3, 8]) + clip = api_map.clip_at(filename, [3, 8]) expect(clip.infer.rooted_tags).to eq('nil, 10') - clip = api_map.clip_at('test.rb', [5, 10]) + clip = api_map.clip_at(filename, [5, 10]) expect(clip.infer.rooted_tags).to eq('10') - clip = api_map.clip_at('test.rb', [7, 10]) + clip = api_map.clip_at(filename, [7, 10]) expect(clip.infer.rooted_tags).to eq('nil, false') end @@ -633,10 +638,10 @@ def bar(baz: nil) baz end end - ), 'test.rb') + ), filename) api_map = Solargraph::ApiMap.new.map(source) - clip = api_map.clip_at('test.rb', [7, 12]) + clip = api_map.clip_at(filename, [7, 12]) expect(clip.infer.rooted_tags).to eq('::Boolean') end @@ -652,10 +657,10 @@ def bar(baz: nil) baz end end - ), 'test.rb') + ), filename) api_map = Solargraph::ApiMap.new.map(source) - clip = api_map.clip_at('test.rb', [6, 10]) + clip = api_map.clip_at(filename, [6, 10]) expect(clip.infer.rooted_tags).to eq('::Boolean') end @@ -674,16 +679,16 @@ def bar(arr, baz: nil) baz end end - ), 'test.rb') + ), filename) api_map = Solargraph::ApiMap.new.map(source) - clip = api_map.clip_at('test.rb', [6, 10]) + clip = api_map.clip_at(filename, [6, 10]) expect(clip.infer.rooted_tags).to eq('::Boolean, nil') - clip = api_map.clip_at('test.rb', [9, 12]) + clip = api_map.clip_at(filename, [9, 12]) expect(clip.infer.rooted_tags).to eq('::Boolean') - clip = api_map.clip_at('test.rb', [11, 10]) + clip = api_map.clip_at(filename, [11, 10]) expect(clip.infer.rooted_tags).to eq('::Boolean, nil') end @@ -701,16 +706,16 @@ def bar(baz: nil) baz end end - ), 'test.rb') + ), filename) api_map = Solargraph::ApiMap.new.map(source) - clip = api_map.clip_at('test.rb', [5, 10]) + clip = api_map.clip_at(filename, [5, 10]) expect(clip.infer.rooted_tags).to eq('::Boolean, nil') - clip = api_map.clip_at('test.rb', [8, 12]) + clip = api_map.clip_at(filename, [8, 12]) expect(clip.infer.rooted_tags).to eq('::Boolean') - clip = api_map.clip_at('test.rb', [10, 10]) + clip = api_map.clip_at(filename, [10, 10]) expect(clip.infer.rooted_tags).to eq('::Boolean, nil') end @@ -727,13 +732,13 @@ def bar(baz: nil) baz end end - ), 'test.rb') + ), filename) api_map = Solargraph::ApiMap.new.map(source) - clip = api_map.clip_at('test.rb', [7, 12]) + clip = api_map.clip_at(filename, [7, 12]) expect(clip.infer.rooted_tags).to eq('::Boolean') - clip = api_map.clip_at('test.rb', [9, 10]) + clip = api_map.clip_at(filename, [9, 10]) expect(clip.infer.rooted_tags).to eq('::Boolean, nil') end @@ -752,16 +757,16 @@ def bar(baz: nil, other: nil) baz end end - ), 'test.rb') + ), filename) api_map = Solargraph::ApiMap.new.map(source) - clip = api_map.clip_at('test.rb', [6, 10]) + clip = api_map.clip_at(filename, [6, 10]) expect(clip.infer.rooted_tags).to eq('::Boolean, nil') - clip = api_map.clip_at('test.rb', [8, 12]) + clip = api_map.clip_at(filename, [8, 12]) expect(clip.infer.rooted_tags).to eq('::Boolean') - clip = api_map.clip_at('test.rb', [11, 10]) + clip = api_map.clip_at(filename, [11, 10]) expect(clip.infer.rooted_tags).to eq('::Boolean, nil') end @@ -778,13 +783,13 @@ def bar(baz: nil) baz end end - ), 'test.rb') + ), filename) api_map = Solargraph::ApiMap.new.map(source) - clip = api_map.clip_at('test.rb', [7, 12]) + clip = api_map.clip_at(filename, [7, 12]) expect(clip.infer.rooted_tags).to eq('::Boolean') - clip = api_map.clip_at('test.rb', [9, 10]) + clip = api_map.clip_at(filename, [9, 10]) expect(clip.infer.rooted_tags).to eq('::Boolean, nil') end @@ -804,16 +809,16 @@ def bar(baz: nil) baz end end - ), 'test.rb') + ), filename) api_map = Solargraph::ApiMap.new.map(source) - clip = api_map.clip_at('test.rb', [8, 12]) + clip = api_map.clip_at(filename, [8, 12]) expect(clip.infer.rooted_tags).to eq('::Boolean') - clip = api_map.clip_at('test.rb', [10, 12]) + clip = api_map.clip_at(filename, [10, 12]) expect(clip.infer.rooted_tags).to eq('::Boolean, nil') - clip = api_map.clip_at('test.rb', [12, 10]) + clip = api_map.clip_at(filename, [12, 10]) expect(clip.infer.rooted_tags).to eq('::Boolean, nil') end @@ -828,19 +833,19 @@ def bar(baz: nil) baz end end - ), 'test.rb') + ), filename) api_map = Solargraph::ApiMap.new.map(source) - clip = api_map.clip_at('test.rb', [5, 10]) + clip = api_map.clip_at(filename, [5, 10]) expect(clip.infer.rooted_tags).to eq('::Boolean, nil') - clip = api_map.clip_at('test.rb', [6, 44]) + clip = api_map.clip_at(filename, [6, 44]) expect(clip.infer.rooted_tags).to eq('::Boolean') - clip = api_map.clip_at('test.rb', [6, 51]) + clip = api_map.clip_at(filename, [6, 51]) expect(clip.infer.rooted_tags).to eq('::Boolean, nil') - clip = api_map.clip_at('test.rb', [7, 10]) + clip = api_map.clip_at(filename, [7, 10]) expect(clip.infer.rooted_tags).to eq('::Boolean, nil') end @@ -858,16 +863,16 @@ def bar(baz: nil) baz end end - ), 'test.rb') + ), filename) api_map = Solargraph::ApiMap.new.map(source) - clip = api_map.clip_at('test.rb', [5, 10]) + clip = api_map.clip_at(filename, [5, 10]) expect(clip.infer.rooted_tags).to eq('::Boolean, nil') - clip = api_map.clip_at('test.rb', [8, 12]) + clip = api_map.clip_at(filename, [8, 12]) expect(clip.infer.rooted_tags).to eq('::Boolean') - clip = api_map.clip_at('test.rb', [10, 10]) + clip = api_map.clip_at(filename, [10, 10]) expect(clip.infer.rooted_tags).to eq('::Boolean') end @@ -885,16 +890,16 @@ def bar(baz: nil) baz end end - ), 'test.rb') + ), filename) api_map = Solargraph::ApiMap.new.map(source) - clip = api_map.clip_at('test.rb', [5, 10]) + clip = api_map.clip_at(filename, [5, 10]) expect(clip.infer.rooted_tags).to eq('::Boolean, nil') - clip = api_map.clip_at('test.rb', [8, 12]) + clip = api_map.clip_at(filename, [8, 12]) expect(clip.infer.rooted_tags).to eq('::Boolean') - clip = api_map.clip_at('test.rb', [10, 10]) + clip = api_map.clip_at(filename, [10, 10]) expect(clip.infer.rooted_tags).to eq('::Boolean') end @@ -916,24 +921,24 @@ def bar(baz: nil) baz end end - ), 'test.rb') + ), filename) api_map = Solargraph::ApiMap.new.map(source) - clip = api_map.clip_at('test.rb', [5, 10]) + clip = api_map.clip_at(filename, [5, 10]) expect(clip.infer.rooted_tags).to eq('::Boolean, nil') - clip = api_map.clip_at('test.rb', [8, 12]) + clip = api_map.clip_at(filename, [8, 12]) expect(clip.infer.rooted_tags).to eq('::Boolean') - clip = api_map.clip_at('test.rb', [10, 12]) + clip = api_map.clip_at(filename, [10, 12]) expect(clip.infer.rooted_tags).to eq('::Boolean') pending('better scoping of return if in begin/rescue/ensure') - clip = api_map.clip_at('test.rb', [12, 12]) + clip = api_map.clip_at(filename, [12, 12]) expect(clip.infer.rooted_tags).to eq('::Boolean, nil') - clip = api_map.clip_at('test.rb', [14, 10]) + clip = api_map.clip_at(filename, [14, 10]) expect(clip.infer.rooted_tags).to eq('::Boolean, nil') end @@ -949,17 +954,17 @@ def a b c end end - ), 'test.rb') + ), filename) api_map = Solargraph::ApiMap.new.map(source) - clip = api_map.clip_at('test.rb', [6, 10]) + clip = api_map.clip_at(filename, [6, 10]) expect(clip.infer.to_s).to eq('String') - clip = api_map.clip_at('test.rb', [7, 17]) + clip = api_map.clip_at(filename, [7, 17]) expect(clip.infer.to_s).to eq('nil') - clip = api_map.clip_at('test.rb', [8, 10]) + clip = api_map.clip_at(filename, [8, 10]) expect(clip.infer.to_s).to eq('String') end @@ -973,9 +978,9 @@ def foo a 123 end end - ), 'test.rb') + ), filename) api_map = Solargraph::ApiMap.new.map(source) - clip = api_map.clip_at('test.rb', [5, 17]) + clip = api_map.clip_at(filename, [5, 17]) expect(clip.infer.to_s).to eq('Integer') end @@ -992,9 +997,9 @@ def foo? out end end - ), 'test.rb') + ), filename) api_map = Solargraph::ApiMap.new.map(source) - clip = api_map.clip_at('test.rb', [9, 10]) + clip = api_map.clip_at(filename, [9, 10]) expect(clip.infer.to_s).to eq('Boolean') end @@ -1016,12 +1021,12 @@ def check end end end - ), 'test.rb') + ), filename) api_map = Solargraph::ApiMap.new.map(source) - clip = api_map.clip_at('test.rb', [11, 12]) + clip = api_map.clip_at(filename, [11, 12]) expect(clip.infer.to_s).to eq('Repro') - clip = api_map.clip_at('test.rb', [13, 12]) + clip = api_map.clip_at(filename, [13, 12]) expect(clip.infer.to_s).to eq('ReproBase') end end diff --git a/spec/pin/base_spec.rb b/spec/pin/base_spec.rb index 4d4315040..90d9a8850 100644 --- a/spec/pin/base_spec.rb +++ b/spec/pin/base_spec.rb @@ -52,8 +52,14 @@ end it 'deals well with known closure combination issue' do - Solargraph::Shell.new.uncache('yard') - api_map = Solargraph::ApiMap.load_with_cache('.', $stderr) + # if this fails you might not have an rbs collection installed + api_map = Solargraph::ApiMap.load '' + + spec = Gem::Specification.find_by_name('yard') + api_map.cache_gem(spec) + + bench = Solargraph::Bench.new(external_requires: ['yard']) + api_map.catalog bench pins = api_map.get_method_stack('YARD::Docstring', 'parser', scope: :class) expect(pins.length).to eq(1) parser_method_pin = pins.first diff --git a/spec/pin/method_spec.rb b/spec/pin/method_spec.rb index c109746af..c01cfbf72 100644 --- a/spec/pin/method_spec.rb +++ b/spec/pin/method_spec.rb @@ -545,14 +545,18 @@ class Foo expect(pin.return_type).to be_undefined end - it 'combines signatures by type' do - # Integer+ in RBS is a number of signatures that dispatch based - # on type. Let's make sure we combine those with anything else - # found (e.g., additions from the BigDecimal RBS collection) - # without collapsing signatures - api_map = Solargraph::ApiMap.load_with_cache(Dir.pwd, nil) - method = api_map.get_method_stack('Integer', '+', scope: :instance).first - expect(method.signatures.count).to be > 3 + context 'with loaded bigdecimal require' do + it 'combines signatures by type' do + # Integer+ in RBS is a number of signatures that dispatch based + # on type. Let's make sure we combine those with anything else + # found (e.g., additions from the BigDecimal RBS collection) + # without collapsing signatures + api_map = Solargraph::ApiMap.new + bench = Solargraph::Bench.new external_requires: ['bigdecimal'] + api_map.catalog(bench) + method = api_map.get_method_stack('Integer', '+', scope: :instance).first + expect(method.signatures.count).to be > 3 + end end it 'infers untagged types from instance variables' do diff --git a/spec/pin_cache_spec.rb b/spec/pin_cache_spec.rb index 83cf7a3b7..7e17894da 100644 --- a/spec/pin_cache_spec.rb +++ b/spec/pin_cache_spec.rb @@ -3,7 +3,7 @@ require 'bundler' require 'benchmark' -describe Solargraph::PinCache do +describe Solargraph::PinCache, order: :defined do subject(:pin_cache) do described_class.new(rbs_collection_path: '.gem_rbs_collection', rbs_collection_config_path: 'rbs_collection.yaml', @@ -12,11 +12,13 @@ end describe '#cached?' do + let(:gem_name) { 'not_a_gem_vmb' } + it 'returns true for a gem that is cached' do - allow(File).to receive(:file?).with(%r{.*stdlib/backport.ser$}).and_return(false) - allow(File).to receive(:file?).with(%r{.*combined/.*/backport-.*.ser$}).and_return(true) + allow(File).to receive(:file?).with(%r{.*stdlib/#{gem_name}.ser$}).and_return(false) + allow(File).to receive(:file?).with(%r{.*combined/.*/#{gem_name}-.*.ser$}).and_return(true) - gemspec = Gem::Specification.find_by_name('backport') + gemspec = instance_double(Gem::Specification, name: gem_name, version: '0.0.1') expect(pin_cache.cached?(gemspec)).to be true end @@ -47,7 +49,7 @@ it 'is tolerant of less usual Ruby installations' do stub_const('Gem::RUBYGEMS_DIR', nil) - expect(pin_cache.possible_stdlibs).to eq([]) + expect { pin_cache.possible_stdlibs }.not_to raise_error end end @@ -63,16 +65,16 @@ describe '#cache_gem' do context 'with an already in-memory gem' do - let(:backport_gemspec) { Gem::Specification.find_by_name('backport') } + let(:jaro_winkler_gemspec) { Gem::Specification.find_by_name('jaro_winkler') } before do - pin_cache.cache_gem(gemspec: backport_gemspec, out: nil) + pin_cache.cache_gem(gemspec: jaro_winkler_gemspec, out: $stderr) end it 'does not load the gem again' do allow(Marshal).to receive(:load).and_call_original - pin_cache.cache_gem(gemspec: backport_gemspec, out: nil) + pin_cache.cache_gem(gemspec: jaro_winkler_gemspec, out: $stderr) expect(Marshal).not_to have_received(:load).with(anything) end @@ -86,7 +88,7 @@ it 'chooses not to use YARD' do parser_gemspec = Gem::Specification.find_by_name('parser') - pin_cache.cache_gem(gemspec: parser_gemspec, out: nil) + pin_cache.cache_gem(gemspec: parser_gemspec, out: $stderr) # if this fails, you may not have run `bundle exec rbs collection update` expect(Solargraph::Yardoc).not_to have_received(:build_docs).with(any_args) end @@ -98,9 +100,11 @@ end it 'uncaches when asked' do + allow(FileUtils).to receive(:rm_rf) + gemspec = Gem::Specification.find_by_name('kramdown') expect do - pin_cache.uncache_gem(gemspec, out: nil) + pin_cache.uncache_gem(gemspec, out: $stderr) end.not_to raise_error end end @@ -112,7 +116,7 @@ it 'chooses not to use YARD' do parser_gemspec = Gem::Specification.find_by_name('parser') - pin_cache.cache_gem(gemspec: parser_gemspec, rebuild: true, out: nil) + pin_cache.cache_gem(gemspec: parser_gemspec, rebuild: true, out: $stderr) # if this fails, you may not have run `bundle exec rbs collection update` expect(Solargraph::Yardoc).not_to have_received(:build_docs).with(any_args) end @@ -129,7 +133,7 @@ yaml_gemspec = Gem::Specification.find_by_name(gem_name) allow(File).to receive(:write).and_call_original - pin_cache.cache_gem(gemspec: yaml_gemspec, out: nil) + pin_cache.cache_gem(gemspec: yaml_gemspec, out: $stderr) # match arguments with regexp using rspec-matchers syntax expect(File).to have_received(:write).with(%r{combined/.*/logger-.*-stdlib.ser$}, any_args).once @@ -147,7 +151,7 @@ yaml_gemspec = Gem::Specification.find_by_name(gem_name) allow(File).to receive(:write).and_call_original - pin_cache.cache_gem(gemspec: yaml_gemspec, out: nil) + pin_cache.cache_gem(gemspec: yaml_gemspec, out: $stderr) # match arguments with regexp using rspec-matchers syntax expect(File).to have_received(:write).with(%r{combined/.*/rubocop-yard-.*-export.ser$}, any_args, @@ -166,9 +170,11 @@ end context 'with an already cached gem' do - let(:gemspec) { Gem::Specification.find_by_name('backport') } + let(:gemspec) { Gem::Specification.find_by_name('ast') } it 'deletes files' do + pin_cache.cache_gem(gemspec: gemspec, out: $stderr) + call expect(FileUtils).to have_received(:rm_rf).at_least(:once) diff --git a/spec/rbs_map/conversions_spec.rb b/spec/rbs_map/conversions_spec.rb index e9dc1ccf8..a5524d0ba 100644 --- a/spec/rbs_map/conversions_spec.rb +++ b/spec/rbs_map/conversions_spec.rb @@ -95,63 +95,74 @@ def bar: () -> untyped end end - context 'with standard loads for solargraph project' do - before :all do # rubocop:disable RSpec/BeforeAfterAll - @api_map = Solargraph::ApiMap.load_with_cache('.') - end - - let(:api_map) { @api_map } + context 'with superclass pin for Parser::AST::Node' do + let(:api_map) { Solargraph::ApiMap.new } - context 'with superclass pin for Parser::AST::Node' do - let(:superclass_pin) do - api_map.pins.find do |pin| - pin.is_a?(Solargraph::Pin::Reference::Superclass) && pin.context.namespace == 'Parser::AST::Node' - end + let(:superclass_pin) do + api_map.pins.find do |pin| + pin.is_a?(Solargraph::Pin::Reference::Superclass) && pin.context.namespace == 'Parser::AST::Node' end + end - it 'generates a rooted pin' do - # rooted! - expect(superclass_pin&.name).to eq('::AST::Node') - end + before do + gems = %w[parser ast open3] + bench = Solargraph::Bench.new(workspace: api_map.workspace, external_requires: gems) + api_map.catalog(bench) + api_map.cache_all_for_doc_map! + api_map.catalog(bench) end - # https://github.com/castwide/solargraph/issues/1042 - context 'with Hash superclass with untyped value and alias' do - let(:rbs) do - <<~RBS - class Sub < Hash[Symbol, untyped] - alias meth_alias [] - end - RBS + it 'generates a rooted pin' do + # rooted! + expect(superclass_pin&.name).to eq('::AST::Node'), -> do + "superclass pin: #{superclass_pin.inspect}" + + `bundle exec solargraph pin --references Parser::AST::Node` + + "\n" + + `find ~/.cache/solargraph -type f | xargs ls -l` end + end + end - let(:sup_method_stack) { api_map.get_method_stack('Hash{Symbol => undefined}', '[]', scope: :instance) } + # https://github.com/castwide/solargraph/issues/1042 + context 'with Hash superclass with untyped value and alias' do + let(:api_map) { Solargraph::ApiMap.new } - let(:sub_alias_stack) { api_map.get_method_stack('Sub', 'meth_alias', scope: :instance) } + let(:rbs) do + <<~RBS + class Sub < Hash[Symbol, untyped] + alias meth_alias [] + end + RBS + end - it 'does not crash looking at superclass method' do - expect { sup_method_stack }.not_to raise_error - end + let(:sup_method_stack) { api_map.get_method_stack('Hash{Symbol => undefined}', '[]', scope: :instance) } - it 'does not crash looking at alias' do - expect { sub_alias_stack }.not_to raise_error - end + let(:sub_alias_stack) { api_map.get_method_stack('Sub', 'meth_alias', scope: :instance) } - it 'finds superclass method pin return type' do - expect(sup_method_stack.map(&:return_type).map(&:rooted_tags).uniq).to eq(['undefined']) - end + it 'does not crash looking at superclass method' do + expect { sup_method_stack }.not_to raise_error + end - it 'finds superclass method pin parameter type' do - expect(sup_method_stack.flat_map(&:signatures).flat_map(&:parameters).map(&:return_type).map(&:rooted_tags) - .uniq).to eq(['Symbol']) - end + it 'does not crash looking at alias' do + expect { sub_alias_stack }.not_to raise_error + end + + it 'finds superclass method pin return type' do + expect(sup_method_stack.map(&:return_type).map(&:rooted_tags).uniq).to eq(['undefined']) + end + + it 'finds superclass method pin parameter type' do + expect(sup_method_stack.flat_map(&:signatures).flat_map(&:parameters).map(&:return_type).map(&:rooted_tags) + .uniq).to eq(['Symbol']) end end if Gem::Version.new(RBS::VERSION) >= Gem::Version.new('3.9.1') context 'with method pin for Open3.capture2e' do it 'accepts chdir kwarg' do - api_map = Solargraph::ApiMap.load_with_cache('.', $stdout) + api_map = Solargraph::ApiMap.new + bench = Solargraph::Bench.new(external_requires: ['open3']) + api_map.catalog(bench) method_pin = api_map.pins.find do |pin| pin.is_a?(Solargraph::Pin::Method) && pin.path == 'Open3.capture2e' diff --git a/spec/rbs_map_spec.rb b/spec/rbs_map_spec.rb index 09e7a1a80..91af85362 100644 --- a/spec/rbs_map_spec.rb +++ b/spec/rbs_map_spec.rb @@ -9,7 +9,7 @@ end it 'fails if it does not find data from gemspec' do - spec = Gem::Specification.find_by_name('backport') + spec = Gem::Specification.find_by_name('rspec-time-guard') rbs_map = described_class.from_gemspec(spec, nil, nil) expect(rbs_map).not_to be_resolved end diff --git a/spec/shell_spec.rb b/spec/shell_spec.rb index e37b3d9b6..877747ddc 100644 --- a/spec/shell_spec.rb +++ b/spec/shell_spec.rb @@ -6,58 +6,45 @@ describe Solargraph::Shell do let(:shell) { described_class.new } - let(:temp_dir) { Dir.mktmpdir } - - before do - File.open(File.join(temp_dir, 'Gemfile'), 'w') do |file| - file.puts "source 'https://rubygems.org'" - file.puts "gem 'solargraph', path: '#{File.expand_path('..', __dir__)}'" - end - output, status = Open3.capture2e('bundle install', chdir: temp_dir) - raise "Failure installing bundle: #{output}" unless status.success? - end - - # @type cmd [Array] - # @return [String] - def bundle_exec(*cmd) - # run the command in the temporary directory with bundle exec - output, status = Open3.capture2e("bundle exec #{cmd.join(' ')}", chdir: temp_dir) - expect(status.success?).to be(true), "Command failed: #{output}" - output - end - - after do - # remove the temporary directory after the tests - FileUtils.rm_rf(temp_dir) - end - describe '--version' do - let(:output) { bundle_exec('solargraph', '--version') } - - it 'returns output' do - expect(output).not_to be_empty - end - it 'returns a version when run' do + output = capture_stdout do + shell.version + end + expect(output).to eq("#{Solargraph::VERSION}\n") end end describe 'uncache' do it 'uncaches without erroring out' do - output = capture_stdout do - shell.uncache('backport') + allow(Solargraph::PinCache).to receive(:uncache) + + capture_stdout do + shell.uncache('public_suffix') end - expect(output).to include('Clearing pin cache in') + expect(Solargraph::PinCache).to have_received(:uncache).twice end it 'uncaches stdlib without erroring out' do - expect { shell.uncache('stdlib') }.not_to raise_error + allow(Solargraph::PinCache).to receive(:uncache) + + capture_stdout do + shell.uncache('stdlib') + end + + expect(Solargraph::PinCache).to have_received(:uncache) end it 'uncaches core without erroring out' do - expect { shell.uncache('core') }.not_to raise_error + allow(Solargraph::PinCache).to receive(:uncache) + + capture_stdout do + shell.uncache('core') + end + + expect(Solargraph::PinCache).to have_received(:uncache) end end @@ -114,11 +101,12 @@ def bundle_exec(*cmd) end it 'caches core without erroring out' do - capture_both do - shell.uncache('core') - end + allow(Solargraph::PinCache).to receive(:core?).and_return(false) + allow(Solargraph::PinCache).to receive(:cache_core) expect { shell.cache('core') }.not_to raise_error + + expect(Solargraph::PinCache).to have_received(:cache_core) end it 'gives sensible error for gem that does not exist' do @@ -132,27 +120,29 @@ def bundle_exec(*cmd) context 'with mocked Workspace' do let(:workspace) { instance_double(Solargraph::Workspace) } - let(:gemspec) { instance_double(Gem::Specification, name: 'backport') } + let(:api_map) { instance_double(Solargraph::ApiMap) } + let(:gemspec) { instance_double(Gem::Specification, name: 'abcd343kfk') } before do - allow(Solargraph::Workspace).to receive(:new).and_return(workspace) + allow(Solargraph::ApiMap).to receive(:new).and_return(api_map) + allow(api_map).to receive(:workspace).and_return(workspace) end it 'caches all without erroring out' do - allow(workspace).to receive(:cache_all_for_workspace!) + allow(api_map).to receive(:cache_all_for_doc_map!) _output = capture_both { shell.gems } - expect(workspace).to have_received(:cache_all_for_workspace!) + expect(api_map).to have_received(:cache_all_for_doc_map!) end it 'caches single gem without erroring out' do - allow(workspace).to receive(:find_gem).with('backport').and_return(gemspec) + allow(workspace).to receive(:find_gem).with('98765').and_return(gemspec) allow(workspace).to receive(:cache_gem) capture_both do shell.options = { rebuild: false } - shell.gems('backport') + shell.gems('98765') end expect(workspace).to have_received(:cache_gem).with(gemspec, out: an_instance_of(StringIO), rebuild: false) @@ -175,17 +165,6 @@ def bundle_exec(*cmd) end end - # @type cmd [Array] - # @return [String] - def bundle_exec(*cmd) - # run the command in the temporary directory with bundle exec - Bundler.with_unbundled_env do - output, status = Open3.capture2e("bundle exec #{cmd.join(' ')}") - expect(status.success?).to be(true), "Command failed: #{output}" - output - end - end - describe 'pin on a class' do let(:api_map) { instance_double(Solargraph::ApiMap) } let(:string_pin) { instance_double(Solargraph::Pin::Namespace, name: 'String') } diff --git a/spec/source/chain_spec.rb b/spec/source/chain_spec.rb index 4cccd285c..c784a430e 100644 --- a/spec/source/chain_spec.rb +++ b/spec/source/chain_spec.rb @@ -1,25 +1,25 @@ describe Solargraph::Source::Chain do - it "gets empty definitions for undefined links" do + it 'gets empty definitions for undefined links' do chain = described_class.new([Solargraph::Source::Chain::Link.new]) expect(chain.define(nil, nil, [])).to be_empty end - it "infers undefined types for undefined links" do + it 'infers undefined types for undefined links' do chain = described_class.new([Solargraph::Source::Chain::Link.new]) expect(chain.infer(nil, nil, [])).to be_undefined end - it "calls itself undefined if any of its links are undefined" do + it 'calls itself undefined if any of its links are undefined' do chain = described_class.new([Solargraph::Source::Chain::Link.new]) expect(chain).to be_undefined end - it "returns undefined bases for single links" do + it 'returns undefined bases for single links' do chain = described_class.new([Solargraph::Source::Chain::Link.new]) expect(chain.base).to be_undefined end - it "defines constants from core classes" do + it 'defines constants from core classes' do api_map = Solargraph::ApiMap.new chain = described_class.new([Solargraph::Source::Chain::Constant.new('String')]) pins = chain.define(api_map, Solargraph::Pin::ROOT_PIN, []) @@ -27,7 +27,7 @@ expect(pins.first.path).to eq('String') end - it "infers types from core classes" do + it 'infers types from core classes' do api_map = Solargraph::ApiMap.new chain = described_class.new([Solargraph::Source::Chain::Constant.new('String')]) type = chain.infer(api_map, Solargraph::Pin::ROOT_PIN, []) @@ -35,25 +35,26 @@ expect(type.scope).to eq(:class) end - it "infers types from core methods" do + it 'infers types from core methods' do api_map = Solargraph::ApiMap.new - chain = described_class.new([Solargraph::Source::Chain::Constant.new('String'), Solargraph::Source::Chain::Call.new('new', nil)]) + chain = described_class.new([Solargraph::Source::Chain::Constant.new('String'), + Solargraph::Source::Chain::Call.new('new', nil)]) type = chain.infer(api_map, Solargraph::Pin::ROOT_PIN, []) expect(type.namespace).to eq('String') expect(type.scope).to eq(:instance) end - it "recognizes literals" do + it 'recognizes literals' do chain = described_class.new([Solargraph::Source::Chain::Literal.new('String', nil)]) expect(chain.literal?).to be(true) end - it "recognizes constants" do + it 'recognizes constants' do chain = described_class.new([Solargraph::Source::Chain::Constant.new('String')]) expect(chain.constant?).to be(true) end - it "recognizes unfinished constants" do + it 'recognizes unfinished constants' do chain = described_class.new([Solargraph::Source::Chain::Constant.new('String'), Solargraph::Source::Chain::Constant.new('')]) expect(chain.constant?).to be(true) expect(chain.base.constant?).to be(true) @@ -61,7 +62,7 @@ expect(chain.base.undefined?).to be(false) end - it "infers types from new subclass calls without a subclass initialize method" do + it 'infers types from new subclass calls without a subclass initialize method' do code = %( class Sup def initialize; end @@ -80,7 +81,7 @@ def meth; end expect(type.name).to eq('Sub') end - it "follows constant chains" do + it 'follows constant chains' do source = Solargraph::Source.load_string(%( module Mixin; end module Container @@ -95,7 +96,7 @@ class Foo; end expect(pins).to be_empty end - it "rebases inner constants chains" do + it 'rebases inner constants chains' do source = Solargraph::Source.load_string(%( class Foo class Bar; end @@ -105,11 +106,12 @@ class Bar; end api_map = Solargraph::ApiMap.new api_map.map source chain = Solargraph::Source::SourceChainer.chain(source, Solargraph::Position.new(3, 16)) - pins = chain.define(api_map, Solargraph::Pin::ProxyType.new(closure: Solargraph::Pin::Namespace.new(name: 'Foo'), return_type: Solargraph::ComplexType.parse('Class')), []) + pins = chain.define(api_map, + Solargraph::Pin::ProxyType.new(closure: Solargraph::Pin::Namespace.new(name: 'Foo'), return_type: Solargraph::ComplexType.parse('Class')), []) expect(pins.first.path).to eq('Foo::Bar') end - it "resolves relative constant paths" do + it 'resolves relative constant paths' do source = Solargraph::Source.load_string(%( class Foo class Bar @@ -123,11 +125,12 @@ module Other api_map = Solargraph::ApiMap.new api_map.map source chain = Solargraph::Source::SourceChainer.chain(source, Solargraph::Position.new(6, 16)) - pins = chain.define(api_map, Solargraph::Pin::ProxyType.anonymous(Solargraph::ComplexType.parse('Class')), []) + pins = chain.define(api_map, + Solargraph::Pin::ProxyType.anonymous(Solargraph::ComplexType.parse('Class')), []) expect(pins.first.path).to eq('Foo::Bar::Baz') end - it "avoids recursive variable assignments" do + it 'avoids recursive variable assignments' do source = Solargraph::Source.load_string(%( @foo = @bar @bar = @foo.quz @@ -135,12 +138,12 @@ module Other api_map = Solargraph::ApiMap.new api_map.map source chain = Solargraph::Source::SourceChainer.chain(source, Solargraph::Position.new(2, 18)) - expect { + expect do chain.define(api_map, Solargraph::Pin::ROOT_PIN, []) - }.not_to raise_error + end.not_to raise_error end - it "pulls types from multiple lines of code" do + it 'pulls types from multiple lines of code' do source = Solargraph::Source.load_string(%( 123 'abc' @@ -152,7 +155,7 @@ module Other expect(type.simple_tags).to eq('String') end - it "uses last line of a begin expression as return type" do + it 'uses last line of a begin expression as return type' do source = Solargraph::Source.load_string(%( begin 123 @@ -166,7 +169,7 @@ module Other expect(type.simple_tags).to eq('String') end - it "matches constants on complete symbols" do + it 'matches constants on complete symbols' do source = Solargraph::Source.load_string(%( class Correct; end class NotCorrect; end diff --git a/spec/source_map/clip_spec.rb b/spec/source_map/clip_spec.rb index 67b3085b2..dddcef640 100644 --- a/spec/source_map/clip_spec.rb +++ b/spec/source_map/clip_spec.rb @@ -2590,7 +2590,7 @@ def bar; end ), 'test.rb') api_map = Solargraph::ApiMap.new.map(source) clip = api_map.clip_at('test.rb', [7, 6]) - expect(clip.infer.to_s).to eq('Symbol, Integer') + expect(clip.infer.to_s).to eq('123, :foo') end it 'replaces type with alternate reassignments' do diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 65d3bb7d4..2ed0f6674 100755 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,7 +1,9 @@ # frozen_string_literal: true +require 'parallel_tests' require 'bundler/setup' require 'webmock/rspec' +require 'rspec_time_guard' WebMock.disable_net_connect!(allow_localhost: true) unless ENV['SIMPLECOV_DISABLED'] # set up lcov reporting for undercover @@ -23,18 +25,44 @@ enable_coverage(:branch) if ENV['SOLARGRAPH_BRANCH_COVERAGE'] end end +PROJECT_DIRECTORY = File.expand_path('..', __dir__) + +module ::RuboCop + class Runner + def run + raise 'this should always be mocked' + end + end +end + RSpec.configure do |c| # Allow use of --only-failures with rspec, handy for local development c.example_status_persistence_file_path = 'rspec-examples.txt' + c.before(:suite) do + files = c.files_to_run + banner = "PID (#{Process.pid}) #{files.count} files to run:" + puts [banner, *files].join("\n\t") + end +end +RspecTimeGuard.setup +RspecTimeGuard.configure do |config| + config.global_time_limit_seconds = 300 + config.continue_on_timeout = false end require 'solargraph' -# execute any logging blocks to make sure they don't blow up -Solargraph::Logging.logger.sev_threshold = Logger::DEBUG -# ...but still suppress logger output in specs (if possible) -if Solargraph::Logging.logger.respond_to?(:reopen) && !ENV.key?('SOLARGRAPH_LOG') + +# Suppress logger output in specs (if possible) +def set_logging + # execute any logging blocks to make sure they don't blow up + Solargraph::Logging.logger.sev_threshold = Logger::DEBUG + # ...but still suppress logger output in specs (if possible) + return unless Solargraph::Logging.logger.respond_to?(:reopen) && !ENV.key?('SOLARGRAPH_LOG') Solargraph::Logging.logger.reopen(File::NULL) + warn 'Logging set to null' end +set_logging + # @param name [String] # @param value [String] def with_env_var name, value diff --git a/spec/type_checker/levels/strict_spec.rb b/spec/type_checker/levels/strict_spec.rb index ea6515b80..dbbe3f566 100644 --- a/spec/type_checker/levels/strict_spec.rb +++ b/spec/type_checker/levels/strict_spec.rb @@ -105,8 +105,13 @@ def bar(a); end require 'kramdown-parser-gfm' Kramdown::Parser::GFM.undefined_call ), 'test.rb') - api_map = Solargraph::ApiMap.load '.' - api_map.catalog Solargraph::Bench.new(source_maps: [source_map], external_requires: ['kramdown-parser-gfm']) + + api_map = Solargraph::ApiMap.new + specs = api_map.resolve_require('kramdown-parser-gfm') + specs.each { |spec| api_map.cache_gem(spec) } + bench = Solargraph::Bench.new(source_maps: [source_map], external_requires: ['kramdown-parser-gfm']) + api_map.catalog bench + checker = described_class.new('test.rb', api_map: api_map, level: :strict) expect(checker.problems).to be_empty end @@ -826,8 +831,6 @@ def meth(param1) end it 'uses nil? to refine type' do - pending 'nil? support in flow sensitive typing' - checker = type_checker(%( # @sg-ignore # @type [String, nil] @@ -838,7 +841,7 @@ def meth(param1) foo.downcase end )) - expect(checker.problems.map(&:message)).to eq(['Unresolved call to upcase']) + expect(checker.problems.map(&:message)).to eq(['Unresolved call to upcase on nil']) end it 'refines types on is_a? and && to downcast and avoid false positives' do diff --git a/spec/workspace/gemspecs_fetch_dependencies_spec.rb b/spec/workspace/gemspecs_fetch_dependencies_spec.rb index 56504e7dd..90fa55828 100644 --- a/spec/workspace/gemspecs_fetch_dependencies_spec.rb +++ b/spec/workspace/gemspecs_fetch_dependencies_spec.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +require 'benchmark' require 'fileutils' require 'tmpdir' require 'rubygems/commands/install_command' @@ -17,7 +18,7 @@ end it 'finds a known dependency' do - expect(deps.map(&:name)).to include('backport') + expect(deps.map(&:name)).to include('ostruct') end end @@ -27,7 +28,7 @@ end it 'finds a known dependency' do - expect(deps.map(&:name)).to include('backport') + expect(deps.map(&:name)).to include('rbs') end end @@ -64,7 +65,7 @@ # run bundle install output, status = Solargraph.with_clean_env do - Open3.capture2e('bundle install --verbose', chdir: dir_path) + Open3.capture2e('bundle install --verbose --local || bundle install --verbose', chdir: dir_path) end raise "Failure installing bundle: #{output}" unless status.success? @@ -75,20 +76,20 @@ end context 'with gem that exists in our bundle' do - let(:gem_name) { 'undercover' } + let(:gem_name) { 'simplecov' } it 'finds dependencies' do - expect(deps.map(&:name)).to include('ast') + expect(deps.map(&:name)).to include('simplecov-html') end end context 'with gem does not exist in our bundle' do - let(:gem_name) { 'activerecord' } + let(:gem_name) { 'functional-ruby' } it 'gives a useful message' do dep_names = nil output = capture_both { dep_names = deps.map(&:name) } - expect(output).to include('Please install the gem activerecord') + expect(output).to include("Please install the gem #{gem_name}") end end end diff --git a/spec/workspace/gemspecs_resolve_require_spec.rb b/spec/workspace/gemspecs_resolve_require_spec.rb index 8deba9ff8..3d0267811 100644 --- a/spec/workspace/gemspecs_resolve_require_spec.rb +++ b/spec/workspace/gemspecs_resolve_require_spec.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +require 'benchmark' require 'fileutils' require 'tmpdir' require 'rubygems/commands/install_command' @@ -9,38 +10,6 @@ let(:gemspecs) { described_class.new(dir_path) } - def find_or_install gem_name, version - Gem::Specification.find_by_name(gem_name, version) - rescue Gem::LoadError - install_gem(gem_name, version) - end - - def add_bundle - # write out Gemfile - File.write(File.join(dir_path, 'Gemfile'), <<~GEMFILE) - source 'https://rubygems.org' - gem 'backport' - GEMFILE - # run bundle install - output, status = Solargraph.with_clean_env do - Open3.capture2e('bundle install --verbose', chdir: dir_path) - end - raise "Failure installing bundle: #{output}" unless status.success? - # ensure Gemfile.lock exists - return if File.exist?(File.join(dir_path, 'Gemfile.lock')) - raise "Gemfile.lock not found after bundle install in #{dir_path}" - end - - def install_gem gem_name, version - Bundler.with_unbundled_env do - cmd = Gem::Commands::InstallCommand.new - cmd.handle_options [gem_name, '-v', version] - cmd.execute - rescue Gem::SystemExitException => e - raise unless e.exit_code == 0 - end - end - context 'with local bundle' do let(:dir_path) { File.realpath(Dir.pwd) } @@ -185,6 +154,24 @@ def configure_bundler_spec stub_value context 'with external bundle' do let(:dir_path) { File.realpath(Dir.mktmpdir).to_s } + def add_bundle + # write out Gemfile + File.write(File.join(dir_path, 'Gemfile'), <<~GEMFILE) + source 'https://rubygems.org' + gem 'public_suffix' + GEMFILE + + # run bundle install + output, status = Solargraph.with_clean_env do + Open3.capture2e('bundle install --verbose --local || bundle install --verbose', chdir: dir_path) + end + raise "Failure installing bundle: #{output}" unless status.success? + + # ensure Gemfile.lock exists + return if File.exist?(File.join(dir_path, 'Gemfile.lock')) + raise "Gemfile.lock not found after bundle install in #{dir_path}" + end + context 'with no actual bundle' do let(:require) { 'bundler/require' } @@ -194,7 +181,9 @@ def configure_bundler_spec stub_value end context 'with Gemfile and Bundler.require' do - before { add_bundle } + before do + add_bundle + end let(:require) { 'bundler/require' } @@ -203,12 +192,14 @@ def configure_bundler_spec stub_value end it 'returns gems' do - expect(specs.map(&:name)).to include('backport') + expect(specs.map(&:name)).to include('public_suffix') end end context 'with Gemfile and deep require into a possibly-core gem' do - before { add_bundle } + before do + add_bundle + end let(:require) { 'bundler/gem_tasks' } @@ -218,7 +209,9 @@ def configure_bundler_spec stub_value end context 'with Gemfile and deep require into a gem' do - before { add_bundle } + before do + add_bundle + end let(:require) { 'rspec/mocks' } @@ -228,7 +221,9 @@ def configure_bundler_spec stub_value end context 'with Gemfile but an unknown gem' do - before { add_bundle } + before do + add_bundle + end let(:require) { 'unknown_gemlaksdflkdf' } @@ -240,16 +235,32 @@ def configure_bundler_spec stub_value context 'with a Gemfile and a gem preference' do # find_or_install helper doesn't seem to work on older versions if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('3.1.0') + def find_or_install gem_name, version + Gem::Specification.find_by_name(gem_name, version) + rescue Gem::LoadError + install_gem(gem_name, version) + end + + def install_gem gem_name, version + Bundler.with_unbundled_env do + cmd = Gem::Commands::InstallCommand.new + cmd.handle_options [gem_name, '-v', version] + cmd.execute + rescue Gem::SystemExitException => e + raise unless e.exit_code == 0 + end + end + before do add_bundle - find_or_install('backport', '1.0.0') - Gem::Specification.find_by_name('backport', '= 1.0.0') + find_or_install('public_suffix', '1.0.0') + Gem::Specification.find_by_name('public_suffix', '= 1.0.0') end let(:preferences) do [ Gem::Specification.new.tap do |spec| - spec.name = 'backport' + spec.name = 'public_suffix' spec.version = '1.0.0' end ] @@ -257,17 +268,17 @@ def configure_bundler_spec stub_value it 'returns the preferred gemspec' do gemspecs = described_class.new(dir_path, preferences: preferences) - specs = gemspecs.resolve_require('backport') - backport = specs.find { |spec| spec.name == 'backport' } + specs = gemspecs.resolve_require('public_suffix') + public_suffix = specs.find { |spec| spec.name == 'public_suffix' } - expect(backport.version.to_s).to eq('1.0.0') + expect(public_suffix.version.to_s).to eq('1.0.0') end context 'with a gem preference that does not exist' do let(:preferences) do [ Gem::Specification.new.tap do |spec| - spec.name = 'backport' + spec.name = 'public_suffix' spec.version = '99.0.0' end ] @@ -275,20 +286,20 @@ def configure_bundler_spec stub_value it 'returns the gemspec we do have' do gemspecs = described_class.new(dir_path, preferences: preferences) - specs = gemspecs.resolve_require('backport') - backport = specs.find { |spec| spec.name == 'backport' } + specs = gemspecs.resolve_require('public_suffix') + public_suffix = specs.find { |spec| spec.name == 'public_suffix' } - expect(backport.version.to_s).to eq('1.2.0') + expect(public_suffix.version.to_s).to eq('3.1.1') end end context 'with a gem preference already set to the version we use' do - let(:version) { Gem::Specification.find_by_name('backport').version.to_s } + let(:version) { Gem::Specification.find_by_name('public_suffix').version.to_s } let(:preferences) do [ Gem::Specification.new.tap do |spec| - spec.name = 'backport' + spec.name = 'public_suffix' spec.version = version end ] @@ -296,10 +307,10 @@ def configure_bundler_spec stub_value it 'returns the gemspec we do have' do gemspecs = described_class.new(dir_path, preferences: preferences) - specs = gemspecs.resolve_require('backport') - backport = specs.find { |spec| spec.name == 'backport' } + specs = gemspecs.resolve_require('public_suffix') + public_suffix = specs.find { |spec| spec.name == 'public_suffix' } - expect(backport.version.to_s).to eq(version) + expect(public_suffix.version.to_s).to eq(version) end end end diff --git a/spec/workspace/require_paths_spec.rb b/spec/workspace/require_paths_spec.rb index eb95d0c5b..b89979022 100644 --- a/spec/workspace/require_paths_spec.rb +++ b/spec/workspace/require_paths_spec.rb @@ -2,25 +2,23 @@ require 'fileutils' require 'tmpdir' +require 'benchmark' describe Solargraph::Workspace::RequirePaths do - subject(:paths) { described_class.new(dir_path, config).generate } + subject(:paths) do + described_class.new(dir_path, config).generate + end let(:config) { Solargraph::Workspace::Config.new(dir_path) } - context 'with no config' do - let(:dir_path) { Dir.pwd } - let(:config) { nil } - - it 'includes the lib directory' do - expect(paths).to include(File.join(dir_path, 'lib')) - end - end - context 'with config and no gemspec' do let(:dir_path) { File.realpath(Dir.pwd) } - let(:config) { instance_double(Solargraph::Workspace::Config, require_paths: [], allow?: true) } + let(:config) do + instance_double(Solargraph::Workspace::Config, + require_paths: [], + allow?: false) + end it 'includes the lib directory' do expect(paths).to include(File.join(dir_path, 'lib')) diff --git a/spec/yard_map/mapper_spec.rb b/spec/yard_map/mapper_spec.rb index 96f71ace6..2aae3fdc2 100644 --- a/spec/yard_map/mapper_spec.rb +++ b/spec/yard_map/mapper_spec.rb @@ -1,24 +1,29 @@ # frozen_string_literal: true describe Solargraph::YardMap::Mapper do - before :all do # rubocop:disable RSpec/BeforeAfterAll - @api_map = Solargraph::ApiMap.load('.') + # before :context here disables parallel tests in prspec, which + # would be needed regardless as we are changing the working + # directory + before :context do + @api_map = Solargraph::ApiMap.new end def pins_with require - doc_map = Solargraph::DocMap.new([require], @api_map.workspace, out: nil) - doc_map.cache_doc_map_gems!(nil) + doc_map = Solargraph::DocMap.new([require], @api_map.workspace, out: $stderr) + doc_map.cache_doc_map_gems!($stderr) doc_map.pins end it 'converts nil docstrings to empty strings' do dir = File.absolute_path(File.join('spec', 'fixtures', 'yard_map')) - Dir.chdir dir do - YARD::Registry.load([File.join(dir, 'attr.rb')], true) - mapper = described_class.new(YARD::Registry.all) - pins = mapper.map - pin = pins.select { |pin| pin.path == 'Foo#bar' }.first - expect(pin.comments).to be_a(String) + Solargraph::CHDIR_MUTEX.synchronize do + Dir.chdir dir do + YARD::Registry.load([File.join(dir, 'attr.rb')], true) + mapper = described_class.new(YARD::Registry.all) + pins = mapper.map + pin = pins.select { |pin| pin.path == 'Foo#bar' }.first + expect(pin.comments).to be_a(String) + end end # Cleanup FileUtils.remove_entry_secure File.join(dir, '.yardoc') @@ -39,14 +44,16 @@ def pins_with require it 'marks correct return type from RuboCop::Options.new' do # Using rubocop because it's a known dependency - pins = pins_with('rubocop').select { |pin| pin.path == 'RuboCop::Options.new' } - expect(pins.map(&:return_type).uniq.map(&:to_s)).to eq(['self']) - expect(pins.flat_map(&:signatures).map(&:return_type).uniq.map(&:to_s)).to eq(['self']) + all_pins = pins_with('open3') + pins = all_pins.select { |pin| pin.path == 'Open3.capture2e' } + expect(pins.map(&:return_type).uniq.map(&:to_s)).to eq(['Array(String, Process::Status)']) + expect(pins.flat_map(&:signatures).map(&:return_type).uniq.map(&:to_s)).to eq(['Array(String, Process::Status)']) end it 'marks non-explicit methods' do # Using rspec-expectations because it's a known dependency pin = pins_with('rspec/expectations').find { |pin| pin.path == 'RSpec::Matchers#expect' } + expect(pin.explicit?).to be(false) end diff --git a/spec/yardoc_spec.rb b/spec/yardoc_spec.rb index 6cd575de0..d90b13b98 100644 --- a/spec/yardoc_spec.rb +++ b/spec/yardoc_spec.rb @@ -39,7 +39,7 @@ describe '#build_docs' do let(:workspace) { Solargraph::Workspace.new(Dir.pwd) } - let(:gemspec) { workspace.find_gem('rubocop') } + let(:gemspec) { workspace.find_gem('reverse_markdown') } let(:output) { '' } before do @@ -62,9 +62,18 @@ end it 'is idempotent' do + result = instance_double(Process::Status) + allow(result).to receive(:success?).and_return(true) + allow(Open3).to receive(:capture2e).and_return([output, result]).once do + # write the complete file to simulate successful run + FileUtils.mkdir_p(gem_yardoc_path) + FileUtils.touch(File.join(gem_yardoc_path, 'complete')) + end + described_class.build_docs(gem_yardoc_path, [], gemspec) described_class.build_docs(gem_yardoc_path, [], gemspec) # second time - expect(File.exist?(File.join(gem_yardoc_path, 'complete'))).to be true + + expect(Open3).to have_received(:capture2e).once end context 'with an error from yard' do