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..7d078f5b9 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 path $PWD/vendor/bundle + WORKERS=$(nproc --all) + export WORKERS + SIMPLECOV_DISABLED=true + export SIMPLECOV_DISABLED + bundle exec parallel_rspec 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,49 @@ 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 path $PWD/vendor/bundle + WORKERS=$(nproc --all) + export WORKERS + bundle exec rake full_spec + rspec_typechecking: runs-on: ubuntu-latest steps: @@ -86,7 +169,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 +187,52 @@ 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 path $PWD/vendor/bundle + WORKERS=$(nproc --all) + export WORKERS + 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,7 +255,7 @@ jobs: with: ruby-version: 3.4 rubygems: latest - bundler-cache: false + bundler-cache: true - name: Install gems run: | set -x @@ -166,7 +292,20 @@ 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 path $PWD/vendor/bundle cd ../solargraph-rspec + WORKERS=$(nproc --all) + export WORKERS + + # 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 +323,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 +331,43 @@ 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 + 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 path $PWD/vendor/bundle cd ../solargraph-rails bundle exec solargraph --version bundle info solargraph bundle info rbs bundle info yard + WORKERS=$(nproc --all) + export WORKERS + # avoid trying to do this in parallel during the specs + bundle exec solargraph gems core stdlib 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..d24246adb 100644 --- a/.github/workflows/rspec.yml +++ b/.github/workflows/rspec.yml @@ -21,7 +21,10 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - ruby-version: ['3.0', '3.1', '3.2', '3.3', '3.4', '4.0', 'head'] + ruby-version: [ + '3.0', '3.1', '3.2', '3.3', '3.4', '4.0', + # 'head' + ] rbs-version: ['3.6.1', '3.8.1', '3.9.5', '3.10.0', '4.0.0.dev.5'] # Ruby 3.0 doesn't work with RBS 3.9.4 or 4.0.0.dev.4 exclude: @@ -83,9 +86,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 path $PWD/vendor/bundle + WORKERS=$(nproc --all) + export WORKERS + + bundle exec rake full_spec undercover: runs-on: ubuntu-latest steps: @@ -99,10 +117,33 @@ 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: | + 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: | + WORKERS=$(nproc --all) + export WORKERS + + bundle exec rake full_spec - name: Check PR coverage - run: bundle exec rake undercover + 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 path $PWD/vendor/bundle + WORKERS=$(nproc --all) + export WORKERS + # avoid trying to do this in parallel during the specs + bundle exec solargraph gems core stdlib + 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 b/.rspec index 7198dff51..1cf25e355 100644 --- a/.rspec +++ b/.rspec @@ -1,3 +1,3 @@ ---color +# --color --require spec_helper ---profile +# --profile diff --git a/.rspec_parallel b/.rspec_parallel new file mode 100644 index 000000000..ae99528f5 --- /dev/null +++ b/.rspec_parallel @@ -0,0 +1,10 @@ +--require spec_helper +-- +# --format progress +# --verbose-process-command +# --format ParallelTests::RSpec::SummaryLogger --out tmp/spec_summary.log +--format ParallelTests::RSpec::RuntimeLogger --out tmp/parallel_runtime_rspec.log +# --verbose +# --color +# --profile +-- diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index eea514d1c..acc8d3b13 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -176,13 +176,7 @@ Naming/PredicatePrefix: - 'lib/solargraph/workspace.rb' RSpec/BeforeAfterAll: - Exclude: - - '**/spec/spec_helper.rb' - - '**/spec/rails_helper.rb' - - '**/spec/support/**/*.rb' - - 'spec/api_map_spec.rb' - - 'spec/language_server/host/dispatch_spec.rb' - - 'spec/language_server/protocol_spec.rb' + Enabled: false # Configuration parameters: IgnoredMetadata. RSpec/DescribeClass: @@ -194,6 +188,15 @@ RSpec/DescribeClass: - '**/spec/views/**/*' - 'spec/complex_type_spec.rb' +# This cop supports safe autocorrection (--autocorrect). +RSpec/EmptyHook: + Exclude: + - 'spec/gem_pins_spec.rb' + - 'spec/library_spec.rb' + - 'spec/logging_spec.rb' + - 'spec/pin/method_spec.rb' + - 'spec/workspace/gemspecs_resolve_require_spec.rb' + # This cop supports safe autocorrection (--autocorrect). RSpec/ExpectActual: Exclude: @@ -240,16 +243,6 @@ Style/ArgumentsForwarding: Exclude: - 'lib/solargraph/complex_type.rb' -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle, ProceduralMethods, FunctionalMethods, AllowedMethods, AllowedPatterns, AllowBracesOnProceduralOneLiners, BracesRequiredMethods. -# SupportedStyles: line_count_based, semantic, braces_for_chaining, always_braces -# ProceduralMethods: benchmark, bm, bmbm, create, each_with_object, measure, new, realtime, tap, with_object -# FunctionalMethods: let, let!, subject, watch -# AllowedMethods: lambda, proc, it -Style/BlockDelimiters: - Exclude: - - 'spec/source/chain_spec.rb' - # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle, SingleLineConditionsOnly, IncludeTernaryExpressions. # SupportedStyles: assign_to_condition, assign_inside_condition @@ -338,7 +331,6 @@ Style/SlicingWithRange: Style/StringLiterals: Exclude: - 'spec/fixtures/rdoc-lib/rdoc-lib.gemspec' - - 'spec/source/chain_spec.rb' # This cop supports safe autocorrection (--autocorrect). Style/SuperArguments: @@ -360,4 +352,4 @@ YARD/TagTypeSyntax: # Configuration parameters: AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, IgnoreCopDirectives, AllowedPatterns, SplitStrings. # URISchemes: http, https Layout/LineLength: - Max: 224 + Max: 215 diff --git a/Rakefile b/Rakefile index 398957b1a..21b6150d8 100755 --- a/Rakefile +++ b/Rakefile @@ -34,14 +34,14 @@ 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' + sh 'TEST_COVERAGE_COMMAND_NAME=full-new bundle exec parallel_rspec spec/' # --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 @@ -90,6 +90,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..7d0e02b23 100755 --- a/lib/solargraph/api_map.rb +++ b/lib/solargraph/api_map.rb @@ -219,10 +219,12 @@ 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? logger.info { "All gems cached for #{directory}" } @@ -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/rubocop.rb b/lib/solargraph/diagnostics/rubocop.rb index 39b79d9b7..5d99db9a7 100644 --- a/lib/solargraph/diagnostics/rubocop.rb +++ b/lib/solargraph/diagnostics/rubocop.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require 'stringio' +require 'rubocop' module Solargraph module Diagnostics diff --git a/lib/solargraph/diagnostics/rubocop_helpers.rb b/lib/solargraph/diagnostics/rubocop_helpers.rb index e97ca628e..167de9351 100644 --- a/lib/solargraph/diagnostics/rubocop_helpers.rb +++ b/lib/solargraph/diagnostics/rubocop_helpers.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require 'rubocop' + module Solargraph module Diagnostics # Utility methods for the RuboCop diagnostics reporter. 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/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..e08bdd069 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) + 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..f54446db9 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,12 @@ Gem::Specification.new do |s| s.add_dependency 'yard-activesupport-concern', '~> 0.0' s.add_dependency 'yard-solargraph', '~> 0.1' + s.add_development_dependency 'parallel_tests', '~> 3.8', '>= 3.8.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..99f48cfcf 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 @@ -160,6 +173,7 @@ class B describe '#uncached_gemspecs' do it 'can get uncached gemspecs workspace without a bench' do api_map = described_class.new + expect(api_map.uncached_gemspecs).not_to be_nil end end diff --git a/spec/api_map_spec.rb b/spec/api_map_spec.rb index facf9489c..28f700af2 100755 --- a/spec/api_map_spec.rb +++ b/spec/api_map_spec.rb @@ -3,7 +3,10 @@ require 'tmpdir' describe Solargraph::ApiMap do - before :all do + # 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 = described_class.new end 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..45f65d743 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,12 +71,16 @@ 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 @@ -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/gem_pins_spec.rb b/spec/gem_pins_spec.rb index 944afd331..f4e83c4dd 100644 --- a/spec/gem_pins_spec.rb +++ b/spec/gem_pins_spec.rb @@ -10,23 +10,34 @@ end context 'with a combined method pin' do - let(:path) { 'RBS::EnvironmentLoader#core_root' } - let(:requires) { ['rbs'] } + before :context do + # run these on same runner so we don't cache rbs in parallel; + # seems like we still have a race condition in pin caching + end + + 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/dispatch_spec.rb b/spec/language_server/host/dispatch_spec.rb index 8db9c1246..394309ef4 100644 --- a/spec/language_server/host/dispatch_spec.rb +++ b/spec/language_server/host/dispatch_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true describe Solargraph::LanguageServer::Host::Dispatch do - before :all do + before :context do # @dispatch = Solargraph::LanguageServer::Host::Dispatch @dispatch = Object.new @dispatch.extend described_class diff --git a/spec/language_server/host_spec.rb b/spec/language_server/host_spec.rb index f0497b8f3..c70924cb6 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 @@ -110,7 +113,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') @@ -133,6 +142,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 +221,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 +262,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 @@ -270,15 +293,23 @@ def initialize(foo); end 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..100b77794 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 @@ -45,13 +49,12 @@ response = data end reader.receive host.flush - expect do - action = { - 'id' => response['id'], - 'result' => response['params']['actions'].first - } - host.receive action - end.not_to raise_error + action = { + 'id' => response['id'], + 'result' => response['params']['actions'].first + } + host.receive action + 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..6ce0c953f 100644 --- a/spec/language_server/message/text_document/rename_spec.rb +++ b/spec/language_server/message/text_document/rename_spec.rb @@ -1,10 +1,14 @@ # frozen_string_literal: true describe Solargraph::LanguageServer::Message::TextDocument::Rename do + let(:temp_file_url) do + "file://#{Dir.mktmpdir}/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 +19,7 @@ class Foo 'method' => 'textDocument/rename', 'params' => { 'textDocument' => { - 'uri' => 'file:///file.rb' + 'uri' => temp_file_url }, 'position' => { 'line' => 1, @@ -24,14 +28,17 @@ 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) 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,12 +47,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' + 'uri' => temp_file_url }, 'position' => { 'line' => 2, @@ -55,13 +65,23 @@ def foo(bar) } }) rename.process - expect(rename.result[:changes]['file:///file.rb'].length).to eq(3) + # wait to get the result generated in a background thread, since this can be slow on CI + timeout = Time.now + 40 + 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]).not_to be_nil, -> { "Expected to find changes for #{temp_file_url} in #{rename.result.inspect}" } + expect(rename.result[:changes][temp_file_url].length).to eq(3), -> { "Expected to find 3 changes for #{temp_file_url} in #{rename.result.inspect}" } end it 'renames an argument symbol from method body' 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 @@ -74,7 +94,7 @@ def foo(bar) 'method' => 'textDocument/rename', 'params' => { 'textDocument' => { - 'uri' => 'file:///file.rb' + 'uri' => temp_file_url }, 'position' => { 'line' => 3, @@ -83,14 +103,25 @@ 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) 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 +132,7 @@ class Namespace::ExampleClass 'method' => 'textDocument/rename', 'params' => { 'textDocument' => { - 'uri' => 'file:///file.rb' + 'uri' => temp_file_url }, 'position' => { 'line' => 2, @@ -110,8 +141,20 @@ 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) 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..059c24fde 100644 --- a/spec/language_server/protocol_spec.rb +++ b/spec/language_server/protocol_spec.rb @@ -1,5 +1,8 @@ # frozen_string_literal: true +require 'tmpdir' +require 'rubocop' + class Protocol attr_reader :response @@ -34,12 +37,24 @@ def stop end end -describe Protocol do - before :all do +describe Protocol, order: :defined do + before :context 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| + temp_dir = Dir.mktmpdir + Dir.chdir temp_dir do + Solargraph.with_clean_env do + testobj.run + end + end + ensure + FileUtils.remove_entry(temp_dir) + end + + after :context do @protocol.stop end @@ -47,6 +62,11 @@ def stop version = instance_double(Gem::Version, version: Gem::Version.new('1.0.0')) Solargraph::LanguageServer::Message::Extended::CheckGemVersion.fetcher = instance_double(Gem::SpecFetcher, search_for_dependency: [version]) + example_name = RSpec.current_example.description + allow(Dir).to receive(:chdir) do + raise "where did this happen - came from #{example_name}" + end + allow(RuboCop::Runner).to receive(:new).and_return(instance_double(RuboCop::Runner, run: [])) end after do @@ -113,9 +133,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 +144,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 +312,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 @@ -425,22 +448,29 @@ def bar baz 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 +479,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/library_spec.rb b/spec/library_spec.rb index 9f9ab87dc..cd129fa3d 100644 --- a/spec/library_spec.rb +++ b/spec/library_spec.rb @@ -3,7 +3,9 @@ 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 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 @@ -19,16 +21,24 @@ it 'returns a Completion' do library = described_class.new + filename = 'file.rb' + # 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) + ), filename, 0) completion = library.completions_at('file.rb', 2, 7) expect(completion).to be_a(Solargraph::SourceMap::Completion) expect(completion.pins.map(&:name)).to include('x') end context 'with a require from a not-yet-cached external gem' do + before :context do + # run these in order so we don't uncache backport right when we + # need it before + end + before do Solargraph::Shell.new.uncache('backport') end @@ -53,11 +63,7 @@ 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, Solargraph::Workspace::Config.new)) @@ -83,6 +89,8 @@ def bar end end ), 'file.rb', 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) expect(paths).to include('Foo#bar') @@ -100,6 +108,8 @@ def self.bar Foo.bar ), 'file.rb', 0 library.attach src + # keep this from syncing a bunch of bundle gems in background + allow(library).to receive(:cacheable_specs).and_return([]) paths = library.type_definitions_at('file.rb', 7, 13).map(&:path) expect(paths).to include('Bar') end @@ -113,6 +123,8 @@ def bar baz, key: '' end Foo.new.bar() ), 'file.rb', 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) expect(pins.length).to eq(1) @@ -153,6 +165,8 @@ def bar baz, key: '' src = Solargraph::Source.load_string(%( puts 'hello' ), 'file.rb', 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' expect(result).to be_a(Array) @@ -165,6 +179,8 @@ 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) @@ -181,6 +197,8 @@ def bar end end ), 'file.rb', 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' expect(pins.length).to eq(2) @@ -189,9 +207,17 @@ def bar end describe '#references_from' do + before :context do + # run these in order so we don't uncache backport right when we + # need it before + 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 +235,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 +256,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 +285,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 +322,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 +353,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 +386,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 +423,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 +441,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 +459,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 +473,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 +487,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 +512,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 +538,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 +558,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 +574,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 +595,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 +620,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 @@ -594,6 +656,11 @@ def bar; end end describe '#locate_ref' do + before :context do + # run these in order so we don't uncache backport right when we + # need it before + end + it 'returns nil without a matching reference location' do workspace = File.absolute_path(File.join('spec', 'fixtures', 'workspace')) library = described_class.load(workspace) @@ -605,9 +672,16 @@ def bar; end end describe '#delete' do + before :context do + # run these in order so we don't uncache backport right when we + # need it before + 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')) @@ -625,11 +699,21 @@ def bar; end end context 'when unsynchronized' do + before :context do + # run these in order so we don't uncache backport right when we + # need it before + end + let(:library) { described_class.load File.absolute_path(File.join('spec', 'fixtures', 'workspace')) } let(:good_file) { File.join(library.workspace.directory, 'lib', 'thing.rb') } let(:bad_file) { File.join(library.workspace.directory, 'lib', 'not_a_thing.rb') } describe 'Library#completions_at' do + before :context do + # run these in order so we don't uncache backport right when we + # need it before + end + it 'gracefully handles unmapped sources' do expect do library.completions_at(good_file, 0, 0) @@ -644,6 +728,11 @@ def bar; end end describe 'Library#definitions_at' do + before :context do + # run these in order so we don't uncache backport right when we + # need it before + end + it 'gracefully handles unmapped sources' do expect do library.definitions_at(good_file, 0, 0) diff --git a/spec/logging_spec.rb b/spec/logging_spec.rb index 7dcd52000..584b07c76 100644 --- a/spec/logging_spec.rb +++ b/spec/logging_spec.rb @@ -3,6 +3,11 @@ require 'tempfile' describe Solargraph::Logging do + before :context do + # We're messing with global state, so let's try to keep it to main + # runner + end + it 'logs messages with levels' do file = Tempfile.new('log') described_class.logger.reopen file 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..e0a9fc791 100644 --- a/spec/pin/method_spec.rb +++ b/spec/pin/method_spec.rb @@ -545,14 +545,23 @@ 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 + before :context do + # serialize this along with other things that might write to + # solargraph cache so we don't interfere with each other + 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.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..12deda597 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', @@ -47,7 +47,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 @@ -66,13 +66,13 @@ let(:backport_gemspec) { Gem::Specification.find_by_name('backport') } before do - pin_cache.cache_gem(gemspec: backport_gemspec, out: nil) + pin_cache.cache_gem(gemspec: backport_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: backport_gemspec, out: $stderr) expect(Marshal).not_to have_received(:load).with(anything) end @@ -86,7 +86,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 +98,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 +114,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 +131,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 +149,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, diff --git a/spec/rbs_map/conversions_spec.rb b/spec/rbs_map/conversions_spec.rb index e9dc1ccf8..e1e34e106 100644 --- a/spec/rbs_map/conversions_spec.rb +++ b/spec/rbs_map/conversions_spec.rb @@ -95,63 +95,69 @@ 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('.') + context 'with superclass pin for Parser::AST::Node' do + # Use :context here instead of :all so that parallel_rspec runs these on the same worker and we only have to cache these gems on one worker + before :context do + @api_map = Solargraph::ApiMap.new + 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 - let(:api_map) { @api_map } - - 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 + it 'generates a rooted pin' do + # rooted! + expect(superclass_pin&.name).to eq('::AST::Node') end + 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 - end + # https://github.com/castwide/solargraph/issues/1042 + context 'with Hash superclass with untyped value and alias' do + let(:api_map) { Solargraph::ApiMap.new } - let(:sup_method_stack) { api_map.get_method_stack('Hash{Symbol => undefined}', '[]', scope: :instance) } + let(:rbs) do + <<~RBS + class Sub < Hash[Symbol, untyped] + alias meth_alias [] + end + RBS + end - let(:sub_alias_stack) { api_map.get_method_stack('Sub', 'meth_alias', scope: :instance) } + let(:sup_method_stack) { api_map.get_method_stack('Hash{Symbol => undefined}', '[]', scope: :instance) } - it 'does not crash looking at superclass method' do - expect { sup_method_stack }.not_to raise_error - end + let(:sub_alias_stack) { api_map.get_method_stack('Sub', 'meth_alias', scope: :instance) } - it 'does not crash looking at alias' do - expect { sub_alias_stack }.not_to raise_error - end + it 'does not crash looking at superclass method' do + expect { sup_method_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 'does not crash looking at alias' do + expect { sub_alias_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 '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/shell_spec.rb b/spec/shell_spec.rb index e37b3d9b6..a76a7fc89 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 + allow(Solargraph::PinCache).to receive(:uncache) + + capture_stdout do shell.uncache('backport') 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,18 +120,20 @@ def bundle_exec(*cmd) context 'with mocked Workspace' do let(:workspace) { instance_double(Solargraph::Workspace) } + let(:api_map) { instance_double(Solargraph::ApiMap) } let(:gemspec) { instance_double(Gem::Specification, name: 'backport') } 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 @@ -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..74a297577 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,38 @@ enable_coverage(:branch) if ENV['SOLARGRAPH_BRANCH_COVERAGE'] end end +PROJECT_DIRECTORY = File.expand_path('..', __dir__) + 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 + # normalized = files.map { Pathname.new(File.absolute_path(it)).relative_path_from(Rails.root) } + normalized = files + banner = "PID (#{Process.pid}) #{files.count} files to run:" + puts [banner, *normalized].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..2ea996ab3 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' @@ -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..30a039d8d 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 'backport' + 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,15 @@ def configure_bundler_spec stub_value end context 'with Gemfile and Bundler.require' do - before { add_bundle } + before :context do + # this tells parallel rspec to run this serially in the same + # worker, so we don't end up doing the bundle installs in + # parallel + end + + before do + add_bundle + end let(:require) { 'bundler/require' } @@ -208,7 +203,15 @@ def configure_bundler_spec stub_value end context 'with Gemfile and deep require into a possibly-core gem' do - before { add_bundle } + before :context do + # this tells parallel rspec to run this serially in the same + # worker, so we don't end up doing the bundle installs in + # parallel + end + + before do + add_bundle + end let(:require) { 'bundler/gem_tasks' } @@ -218,7 +221,15 @@ def configure_bundler_spec stub_value end context 'with Gemfile and deep require into a gem' do - before { add_bundle } + before :context do + # this tells parallel rspec to run this serially in the same + # worker, so we don't end up doing the bundle installs in + # parallel + end + + before do + add_bundle + end let(:require) { 'rspec/mocks' } @@ -228,7 +239,15 @@ def configure_bundler_spec stub_value end context 'with Gemfile but an unknown gem' do - before { add_bundle } + before :context do + # this tells parallel rspec to run this serially in the same + # worker, so we don't end up doing the bundle installs in + # parallel + end + + before do + add_bundle + end let(:require) { 'unknown_gemlaksdflkdf' } @@ -238,8 +257,30 @@ def configure_bundler_spec stub_value end context 'with a Gemfile and a gem preference' do + before :context do + # this tells parallel rspec to run this serially in the same + # worker, so we don't end up doing the bundle installs in + # parallel + end + # 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') 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..5f317bd8b 100644 --- a/spec/yard_map/mapper_spec.rb +++ b/spec/yard_map/mapper_spec.rb @@ -1,13 +1,16 @@ # 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 @@ -39,14 +42,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..6ce0877ef 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('backport') } 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