Skip to content

Commit

Permalink
Add launch mode integration tests (#2766)
Browse files Browse the repository at this point in the history
  • Loading branch information
vinistock authored Oct 24, 2024
1 parent 43520f0 commit 664b8cc
Show file tree
Hide file tree
Showing 9 changed files with 160 additions and 22 deletions.
2 changes: 2 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ Sorbet/TrueSigil:
- "lib/ruby_indexer/test/**/*.rb"
- "lib/ruby_indexer/lib/ruby_indexer/prefix_tree.rb"
- "lib/ruby_lsp/load_sorbet.rb"
- "lib/ruby_lsp/scripts/compose_bundle.rb"
Exclude:
- "**/*.rake"
- "lib/**/*.rb"
Expand All @@ -58,6 +59,7 @@ Sorbet/StrictSigil:
- "lib/ruby-lsp.rb"
- "lib/ruby_indexer/lib/ruby_indexer/prefix_tree.rb"
- "lib/ruby_lsp/load_sorbet.rb"
- "lib/ruby_lsp/scripts/compose_bundle.rb"

Layout/ClassStructure:
Enabled: true
Expand Down
28 changes: 10 additions & 18 deletions exe/ruby-lsp-launcher
Original file line number Diff line number Diff line change
Expand Up @@ -19,24 +19,16 @@ raw_initialize = $stdin.read(content_length)
bundle_gemfile_file = File.join(".ruby-lsp", "bundle_gemfile")
locked_bundler_version_file = File.join(".ruby-lsp", "locked_bundler_version")

# Composed the Ruby LSP bundle in a forked process so that we can require gems without polluting the main process
# `$LOAD_PATH` and `Gem.loaded_specs`
pid = fork do
require_relative "../lib/ruby_lsp/setup_bundler"
require "json"
require "uri"
require "core_ext/uri"

initialize_request = JSON.parse(raw_initialize, symbolize_names: true)
workspace_uri = initialize_request.dig(:params, :workspaceFolders, 0, :uri)
workspace_path = workspace_uri && URI(workspace_uri).to_standardized_path
workspace_path ||= Dir.pwd

env = RubyLsp::SetupBundler.new(workspace_path).setup!
File.write(bundle_gemfile_file, env["BUNDLE_GEMFILE"])
File.write(locked_bundler_version_file, env["BUNDLER_VERSION"]) if env["BUNDLER_VERSION"]
rescue RubyLsp::SetupBundler::BundleNotLocked
warn("Project contains a Gemfile, but no Gemfile.lock. Run `bundle install` to lock gems and restart the server")
# Compose the Ruby LSP bundle in a forked process so that we can require gems without polluting the main process
# `$LOAD_PATH` and `Gem.loaded_specs`. Windows doesn't support forking, so we need a separate path to support it
pid = if Gem.win_platform?
spawn(Gem.ruby, File.expand_path("../lib/ruby_lsp/scripts/compose_bundle_windows.rb", __dir__), raw_initialize)
else
fork do
$LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
require "ruby_lsp/scripts/compose_bundle"
compose(raw_initialize)
end
end

# Wait until the composed Bundle is finished
Expand Down
4 changes: 3 additions & 1 deletion lib/ruby_lsp/global_state.rb
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,9 @@ def dot_rubocop_yml_present
sig { returns(T::Array[String]) }
def gather_direct_dependencies
Bundler.with_original_env { Bundler.default_gemfile }
Bundler.locked_gems.dependencies.keys + gemspec_dependencies

dependencies = Bundler.locked_gems&.dependencies&.keys || []
dependencies + gemspec_dependencies
rescue Bundler::GemfileNotFound
[]
end
Expand Down
18 changes: 18 additions & 0 deletions lib/ruby_lsp/scripts/compose_bundle.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# typed: true
# frozen_string_literal: true

def compose(raw_initialize)
require "ruby_lsp/setup_bundler"
require "json"
require "uri"
require "core_ext/uri"

initialize_request = JSON.parse(raw_initialize, symbolize_names: true)
workspace_uri = initialize_request.dig(:params, :workspaceFolders, 0, :uri)
workspace_path = workspace_uri && URI(workspace_uri).to_standardized_path
workspace_path ||= Dir.pwd

env = RubyLsp::SetupBundler.new(workspace_path, launcher: true).setup!
File.write(File.join(".ruby-lsp", "bundle_gemfile"), env["BUNDLE_GEMFILE"])
File.write(File.join(".ruby-lsp", "locked_bundler_version"), env["BUNDLER_VERSION"]) if env["BUNDLER_VERSION"]
end
9 changes: 9 additions & 0 deletions lib/ruby_lsp/scripts/compose_bundle_windows.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# typed: strict
# frozen_string_literal: true

$LOAD_PATH.unshift(File.expand_path("../../../lib", __dir__))
require_relative "compose_bundle"

# When this is invoked on Windows, we pass the raw initialize as an argument to this script. On other platforms, we
# invoke the compose method from inside a forked process
compose(ARGV.first)
2 changes: 1 addition & 1 deletion lib/ruby_lsp/server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1043,7 +1043,7 @@ def workspace_dependencies(message)
}
end
end
rescue Bundler::GemfileNotFound
rescue Bundler::GemfileNotFound, Bundler::GitError
[]
end

Expand Down
5 changes: 3 additions & 2 deletions lib/ruby_lsp/setup_bundler.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class BundleInstallFailure < StandardError; end
def initialize(project_path, **options)
@project_path = project_path
@branch = T.let(options[:branch], T.nilable(String))
@launcher = T.let(options[:launcher], T.nilable(T::Boolean))

# Regular bundle paths
@gemfile = T.let(
Expand Down Expand Up @@ -59,7 +60,7 @@ def initialize(project_path, **options)
# used for running the server
sig { returns(T::Hash[String, String]) }
def setup!
raise BundleNotLocked if @gemfile&.exist? && !@lockfile&.exist?
raise BundleNotLocked if !@launcher && @gemfile&.exist? && !@lockfile&.exist?

# Automatically create and ignore the .ruby-lsp folder for users
@custom_dir.mkpath unless @custom_dir.exist?
Expand Down Expand Up @@ -129,7 +130,7 @@ def write_custom_gemfile

# If there's a top level Gemfile, we want to evaluate from the custom bundle. We get the source from the top level
# Gemfile, so if there isn't one we need to add a default source
if @gemfile&.exist?
if @gemfile&.exist? && @lockfile&.exist?
parts << "eval_gemfile(File.expand_path(\"../#{@gemfile_name}\", __dir__))"
else
parts.unshift('source "https://rubygems.org"')
Expand Down
4 changes: 4 additions & 0 deletions project-words
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ autoloaded
autorun
bigdecimal
bindir
binmode
binread
Bizt
Bizw
bufnr
binstub
bytesize
byteslice
codepoint
codepoints
Expand All @@ -19,6 +21,7 @@ dont
eglot
Eglot
eruby
exitstatus
EXTGLOB
FIXEDENCODING
Floo
Expand Down Expand Up @@ -47,6 +50,7 @@ nargs
nodoc
noreturn
nvim
popen
qtlzwssomeking
quickfixes
quxx
Expand Down
110 changes: 110 additions & 0 deletions test/integration_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,118 @@ def test_avoids_bundler_version_if_local_bin_is_in_path
end
end

def test_launch_mode_with_no_gemfile
skip("CI only") unless ENV["CI"]

in_temp_dir do |dir|
Bundler.with_unbundled_env do
launch(dir)
end
end
end

def test_launch_mode_with_missing_lockfile
skip("CI only") unless ENV["CI"]

in_temp_dir do |dir|
File.write(File.join(dir, "Gemfile"), <<~RUBY)
source "https://rubygems.org"
gem "stringio"
RUBY

Bundler.with_unbundled_env do
launch(dir)
end
end
end

def test_launch_mode_with_full_bundle
skip("CI only") unless ENV["CI"]

in_temp_dir do |dir|
File.write(File.join(dir, "Gemfile"), <<~RUBY)
source "https://rubygems.org"
gem "stringio"
RUBY

lockfile_contents = <<~LOCKFILE
GEM
remote: https://rubygems.org/
specs:
stringio (3.1.0)
PLATFORMS
arm64-darwin-23
ruby
DEPENDENCIES
stringio
BUNDLED WITH
2.5.7
LOCKFILE
File.write(File.join(dir, "Gemfile.lock"), lockfile_contents)

Bundler.with_unbundled_env do
launch(dir)
end
end
end

def test_launch_mode_with_no_gemfile_and_bundle_path
skip("CI only") unless ENV["CI"]

in_temp_dir do |dir|
Bundler.with_unbundled_env do
system("bundle config --local path #{File.join("vendor", "bundle")}")
assert_path_exists(File.join(dir, ".bundle", "config"))

launch(dir)
end
end
end

private

def launch(workspace_path)
initialize_request = {
id: 1,
method: "initialize",
params: {
initializationOptions: {},
capabilities: { general: { positionEncodings: ["utf-8"] } },
workspaceFolders: [{ uri: URI::Generic.from_path(path: workspace_path).to_s }],
},
}.to_json

$stdin.expects(:gets).with("\r\n\r\n").once.returns("Content-Length: #{initialize_request.bytesize}")
$stdin.expects(:read).with(initialize_request.bytesize).once.returns(initialize_request)

# Make `new` return a mock that raises so that we don't print to stdout and stop immediately after boot
server_object = mock("server")
server_object.expects(:start).once.raises(StandardError.new("stop"))
RubyLsp::Server.expects(:new).returns(server_object)

# We load the launcher binary in the same process as the tests are running. We cannot try to re-activate a different
# Bundler version, because that throws an error
if File.exist?(File.join(workspace_path, "Gemfile.lock"))
spec_mock = mock("specification")
spec_mock.expects(:activate).once
Gem::Specification.expects(:find_by_name).with do |name, version|
name == "bundler" && !version.empty?
end.returns(spec_mock)
end

# Verify that we are setting up the bundle, but there's no actual need to do it
Bundler.expects(:setup).once

assert_raises(StandardError) do
load(File.expand_path("../exe/ruby-lsp-launcher", __dir__))
end

assert_path_exists(File.join(workspace_path, ".ruby-lsp", "bundle_gemfile"))
end

def in_temp_dir
Dir.mktmpdir do |dir|
Dir.chdir(dir) do
Expand Down

0 comments on commit 664b8cc

Please sign in to comment.