From 4288016b2447ece1a1bf2e7ba558e6b279e3dfdc Mon Sep 17 00:00:00 2001 From: Nikita Chernukhin Date: Fri, 20 Dec 2024 18:53:37 +0100 Subject: [PATCH] Remove legacy adapters. Add more integration specs --- lib/remote_ruby/connection_adapter.rb | 8 +- .../connection_adapter/local_stdin_adapter.rb | 25 ---- .../connection_adapter/ssh_stdin_adapter.rb | 30 ---- .../ssh_tmp_file_adapter.rb | 60 -------- .../stdin_process_adapter.rb | 35 ----- lib/remote_ruby/execution_context.rb | 4 +- spec/integration/ssh_spec.rb | 141 ++++++++++++++++++ spec/integration/ssh_stdin_spec.rb | 35 ----- .../local_stdin_adapter_spec.rb | 43 ------ .../ssh_stdin_adapter_spec.rb | 24 --- .../stdin_process_adapter_spec.rb | 19 --- spec/spec_helper.rb | 4 + .../shared_examples/stdin_process_adapter.rb | 64 -------- spec/support/stdin_helper.rb | 9 ++ 14 files changed, 159 insertions(+), 342 deletions(-) delete mode 100644 lib/remote_ruby/connection_adapter/local_stdin_adapter.rb delete mode 100644 lib/remote_ruby/connection_adapter/ssh_stdin_adapter.rb delete mode 100644 lib/remote_ruby/connection_adapter/ssh_tmp_file_adapter.rb delete mode 100644 lib/remote_ruby/connection_adapter/stdin_process_adapter.rb create mode 100644 spec/integration/ssh_spec.rb delete mode 100644 spec/integration/ssh_stdin_spec.rb delete mode 100644 spec/remote_ruby/connection_adapter/local_stdin_adapter_spec.rb delete mode 100644 spec/remote_ruby/connection_adapter/ssh_stdin_adapter_spec.rb delete mode 100644 spec/remote_ruby/connection_adapter/stdin_process_adapter_spec.rb delete mode 100644 spec/support/shared_examples/stdin_process_adapter.rb create mode 100644 spec/support/stdin_helper.rb diff --git a/lib/remote_ruby/connection_adapter.rb b/lib/remote_ruby/connection_adapter.rb index f1e2600..3ec5731 100644 --- a/lib/remote_ruby/connection_adapter.rb +++ b/lib/remote_ruby/connection_adapter.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module RemoteRuby - # Base class for other connection adapters. + # Base class for connection adapters. class ConnectionAdapter # Initializers of adapters should receive only keyword arguments. # May be overriden in a child class. @@ -17,11 +17,7 @@ def open(_code) end require 'remote_ruby/connection_adapter/eval_adapter' -require 'remote_ruby/connection_adapter/stdin_process_adapter' -require 'remote_ruby/connection_adapter/ssh_stdin_adapter' -require 'remote_ruby/connection_adapter/local_stdin_adapter' require 'remote_ruby/connection_adapter/cache_adapter' require 'remote_ruby/connection_adapter/caching_adapter' -require 'remote_ruby/connection_adapter/tmp_file_adapter' -require 'remote_ruby/connection_adapter/ssh_tmp_file_adapter' require 'remote_ruby/connection_adapter/ssh_adapter' +require 'remote_ruby/connection_adapter/tmp_file_adapter' diff --git a/lib/remote_ruby/connection_adapter/local_stdin_adapter.rb b/lib/remote_ruby/connection_adapter/local_stdin_adapter.rb deleted file mode 100644 index f0ad830..0000000 --- a/lib/remote_ruby/connection_adapter/local_stdin_adapter.rb +++ /dev/null @@ -1,25 +0,0 @@ -# frozen_string_literal: true - -module RemoteRuby - # An adapter to expecute Ruby code on the local macine - # inside a specified directory - class LocalStdinAdapter < ::RemoteRuby::StdinProcessAdapter - attr_reader :working_dir, :bundler - - def initialize(working_dir: '.', bundler: false) - super - @working_dir = working_dir - @bundler = bundler - end - - private - - def command - if bundler - "cd \"#{working_dir}\" && bundle exec ruby" - else - "cd \"#{working_dir}\" && ruby" - end - end - end -end diff --git a/lib/remote_ruby/connection_adapter/ssh_stdin_adapter.rb b/lib/remote_ruby/connection_adapter/ssh_stdin_adapter.rb deleted file mode 100644 index 9f508b7..0000000 --- a/lib/remote_ruby/connection_adapter/ssh_stdin_adapter.rb +++ /dev/null @@ -1,30 +0,0 @@ -# frozen_string_literal: true - -module RemoteRuby - # An adapter to execute Ruby code on the remote server via SSH - class SSHStdinAdapter < StdinProcessAdapter - attr_reader :server, :working_dir, :user, :key_file, :bundler - - def initialize(server:, working_dir: '~', user: nil, key_file: nil, bundler: false) - super - @working_dir = working_dir - @server = user.nil? ? server : "#{user}@#{server}" - @user = user - @key_file = key_file - @bundler = bundler - end - - private - - def command - command = 'ssh' - command = "#{command} -i #{key_file}" if key_file - - if bundler - "#{command} #{server} \"cd #{working_dir} && bundle exec ruby\"" - else - "#{command} #{server} \"cd #{working_dir} && ruby\"" - end - end - end -end diff --git a/lib/remote_ruby/connection_adapter/ssh_tmp_file_adapter.rb b/lib/remote_ruby/connection_adapter/ssh_tmp_file_adapter.rb deleted file mode 100644 index 72957b6..0000000 --- a/lib/remote_ruby/connection_adapter/ssh_tmp_file_adapter.rb +++ /dev/null @@ -1,60 +0,0 @@ -# frozen_string_literal: true - -require 'open3' - -module RemoteRuby - # An adapter to expecute Ruby code on a remote SSH server. - # It will attempt tp write the code to a temporary file on the server and then execute. - class SSHTmpFileAdapter < ::RemoteRuby::TmpFileAdapter - include Open3 - - attr_reader :server, :working_dir, :user, :key_file, :bundler - - def initialize(server:, working_dir: nil, user: nil, key_file: nil, bundler: false) - super(working_dir: working_dir, bundler: bundler) - @server = user.nil? ? server : "#{user}@#{server}" - @user = user - @key_file = key_file - end - - protected - - def write_code_to_temp_file(code) - popen3(ssh_command('f=$(mktemp) && cat > $f && echo $f')) do |stdin, stdout, stderr, wait_thr| - stdin.write(code) - stdin.close - - raise "Failed to create temporary file: #{stderr}" unless wait_thr.value.success? - - stdout.read.chomp - end - end - - def with_temp_file(code) - fname = write_code_to_temp_file(code) - yield fname - ensure - _, stderr, status = capture3(ssh_command("rm #{fname}")) - warn "Failed to remove temporary file: #{stderr}" unless status.success? - end - - def command(code_path) - cmd = if bundler - "cd \"#{working_dir}\" && bundle exec ruby #{code_path}" - else - "cd \"#{working_dir}\" && ruby #{code_path}" - end - - ssh_command(cmd) - end - - private - - def ssh_command(cmd) - command = 'ssh' - command = "#{command} -i #{key_file}" if key_file - - "#{command} #{server} '#{cmd}'" - end - end -end diff --git a/lib/remote_ruby/connection_adapter/stdin_process_adapter.rb b/lib/remote_ruby/connection_adapter/stdin_process_adapter.rb deleted file mode 100644 index ccc2d05..0000000 --- a/lib/remote_ruby/connection_adapter/stdin_process_adapter.rb +++ /dev/null @@ -1,35 +0,0 @@ -# frozen_string_literal: true - -require 'open3' - -module RemoteRuby - # Base class for adapters which launch an external process to execute - # Ruby code and send the code to its standard input. - class StdinProcessAdapter < ::RemoteRuby::ConnectionAdapter - include Open3 - - def open(code) - result = nil - - popen3(command) do |stdin, stdout, stderr, wait_thr| - stdin.write(code) - stdin.close - - yield nil, stdout, stderr - - result = wait_thr.value - end - - return if result.success? - - raise "Remote connection exited with code #{result}" - end - - protected - - # Command to run an external process. Override in a child class. - def command - raise NotImplementedError - end - end -end diff --git a/lib/remote_ruby/execution_context.rb b/lib/remote_ruby/execution_context.rb index 88ae5e8..420746c 100644 --- a/lib/remote_ruby/execution_context.rb +++ b/lib/remote_ruby/execution_context.rb @@ -20,6 +20,7 @@ def initialize(**params) @use_cache = params.delete(:use_cache) || false @save_cache = params.delete(:save_cache) || false @cache_dir = params.delete(:cache_dir) || File.join(Dir.pwd, 'cache') + @in_stream = params.delete(:in_stream) || $stdin @out_stream = params.delete(:out_stream) || $stdout @err_stream = params.delete(:err_stream) || $stderr @adapter_klass = params.delete(:adapter) || ::RemoteRuby::SSHAdapter @@ -45,7 +46,7 @@ def execute(locals = nil, &block) private attr_reader :params, :adapter_klass, :use_cache, :save_cache, :cache_dir, - :out_prefix, :out_stream, :err_stream, :flavours, :cache_prefix + :out_prefix, :in_stream, :out_stream, :err_stream, :flavours, :cache_prefix def assign_locals(local_names, values, block) local_names.each do |local| @@ -102,6 +103,7 @@ def execute_code(ruby_code, client_locals = {}) code: compiler.compiled_code, adapter: adapter(compiler.code_hash), prefix: "#{cp}#{out_prefix}", + in_stream: in_stream, out_stream: out_stream, err_stream: err_stream ) diff --git a/spec/integration/ssh_spec.rb b/spec/integration/ssh_spec.rb new file mode 100644 index 0000000..144875f --- /dev/null +++ b/spec/integration/ssh_spec.rb @@ -0,0 +1,141 @@ +# frozen_string_literal: true + +describe 'Connecting to remote host with SSH adapter', + type: :integration do + let(:ec) do + RemoteRuby::ExecutionContext.new( + adapter: RemoteRuby::SSHAdapter, + host: ssh_host, + user: ssh_user, + working_dir: ssh_workdir + ) + end + + context 'with do-blocks' do + it 'reads string from stdin' do + with_stdin_redirect("John doe\n") do + expect do + ec.execute do + puts gets + end + end.to output("John doe\n").to_stdout + end + end + + it 'receives integer result' do + result = ec.execute do + 17 + 5 + end + + expect(result).to eq(22) + end + + it 'passers integer locals' do + x = 17 + y = 5 + result = ec.execute do + x + y + end + + expect(result).to eq(22) + end + + it 'receives string result' do + result = ec.execute do + 'a' * 3 + end + + expect(result).to eq('aaa') + end + + it 'prints to stdout' do + s = 'Something' + + expect do + ec.execute do + puts s + end + end.to output("#{s}\n").to_stdout + end + + it 'prints to stderr' do + s = 'Something' + + expect do + ec.execute do + warn s + end + end.to output("#{s}\n").to_stderr + end + + it 'receives complex local context' do + a = 3 + b = 'Hello' + c = 5.0 + d = Time.new(2025, 1, 1) + e = nil + f = [1, 2, 3] + g = { a: 1, b: 2 } + + result = ec.execute do + { + a: a * 2, + b: "#{b} World", + c: c * 2, + d: Time.new(d.year + 1, d.month, d.day), + e: e.nil?, + f: f.map { |x| x * 2 }, + g: g.transform_values { |v| v * 2 } + } + end + + expect(result).to eq( + a: 6, + b: 'Hello World', + c: 10.0, + d: Time.new(2026, 1, 1), + e: true, + f: [2, 4, 6], + g: { a: 2, b: 4 } + ) + end + + it 'modifies local variables' do + a = 3 + b = 'Hello' + c = 5.0 + d = Time.new(2025, 1, 1) + e = nil + f = [1, 2, 3] + g = { a: 1, b: 2 } + + ec.execute do + a *= 2 + b = "#{b} World" + c *= 2 + d = Time.new(d.year + 1, d.month, d.day) + e = e.nil? + f = f.map { |x| x * 2 } + g = g.transform_values { |v| v * 2 } + end + + expect(a).to eq(6) + expect(b).to eq('Hello World') + expect(c).to eq(10.0) + expect(d).to eq(Time.new(2026, 1, 1)) + expect(e).to eq(true) + expect(f).to eq([2, 4, 6]) + expect(g).to eq(a: 2, b: 4) + end + end + + context 'with {}-blocks' do + it 'prints to stdout' do + s = 'Something' + + expect do + ec.execute { puts s } + end.to output(Regexp.new(s)).to_stdout + end + end +end diff --git a/spec/integration/ssh_stdin_spec.rb b/spec/integration/ssh_stdin_spec.rb deleted file mode 100644 index 4172006..0000000 --- a/spec/integration/ssh_stdin_spec.rb +++ /dev/null @@ -1,35 +0,0 @@ -# frozen_string_literal: true - -describe 'Connecting to remote host with SSH STDIN adapter', - type: :integration do - let(:context) do - RemoteRuby::ExecutionContext.new( - adapter: RemoteRuby::SSHAdapter, - host: ssh_host, - user: ssh_user, - working_dir: ssh_workdir - ) - end - - context 'with do-blocks' do - it 'succeeds and prints' do - s = 'Something' - - expect do - context.execute do - puts s - end - end.to output(Regexp.new(s)).to_stdout - end - end - - context 'with {}-blocks' do - it 'succeeds and prints' do - s = 'Something' - - expect do - context.execute { puts s } - end.to output(Regexp.new(s)).to_stdout - end - end -end diff --git a/spec/remote_ruby/connection_adapter/local_stdin_adapter_spec.rb b/spec/remote_ruby/connection_adapter/local_stdin_adapter_spec.rb deleted file mode 100644 index 53d0990..0000000 --- a/spec/remote_ruby/connection_adapter/local_stdin_adapter_spec.rb +++ /dev/null @@ -1,43 +0,0 @@ -# frozen_string_literal: true - -require 'support/shared_examples/stdin_process_adapter' - -describe RemoteRuby::LocalStdinAdapter do - include_context 'STDIN adapter' - - subject(:adapter) { described_class.new(working_dir: working_dir, bundler: bundler) } - - let(:bundler) { false } - let(:working_dir) do - File.realpath(Dir.mktmpdir) - end - - after(:each) do - FileUtils.rm_rf(working_dir) - end - - describe '#open' do - it 'changes to the working dir' do - pwd = nil - - adapter.open('puts Dir.pwd') do |_stdin, stdout, _stderr| - pwd = stdout.read - pwd.strip! - end - - expect(pwd).to eq(working_dir) - end - - it 'is launched in a separate process' do - new_pid = nil - - adapter.open('puts Process.pid') do |_stdin, stdout, _stderr| - new_pid = stdout.read.to_i - end - - expect(new_pid).not_to be_nil - expect(new_pid).not_to be_zero - expect(new_pid).not_to eq(Process.pid) - end - end -end diff --git a/spec/remote_ruby/connection_adapter/ssh_stdin_adapter_spec.rb b/spec/remote_ruby/connection_adapter/ssh_stdin_adapter_spec.rb deleted file mode 100644 index 6ad419a..0000000 --- a/spec/remote_ruby/connection_adapter/ssh_stdin_adapter_spec.rb +++ /dev/null @@ -1,24 +0,0 @@ -# frozen_string_literal: true - -require 'support/shared_examples/stdin_process_adapter' - -describe RemoteRuby::SSHStdinAdapter do - include_context 'STDIN adapter' - - subject(:adapter) { described_class.new(**params) } - - let(:working_dir) { '/var/ruby_project' } - let(:server) { 'ssh_host' } - let(:username) { 'dev' } - let(:key_file) { '/home/dev/.ssh/special_key' } - - let(:params) do - { - working_dir: working_dir, - server: server, - user: username, - key_file: key_file, - bundler: bundler - } - end -end diff --git a/spec/remote_ruby/connection_adapter/stdin_process_adapter_spec.rb b/spec/remote_ruby/connection_adapter/stdin_process_adapter_spec.rb deleted file mode 100644 index 8644de1..0000000 --- a/spec/remote_ruby/connection_adapter/stdin_process_adapter_spec.rb +++ /dev/null @@ -1,19 +0,0 @@ -# frozen_string_literal: true - -describe RemoteRuby::StdinProcessAdapter do - # rubocop:disable Lint/ConstantDefinitionInBlock - class TestStdinAdapter < described_class; end - # rubocop:enable Lint/ConstantDefinitionInBlock - - subject(:adapter) { TestStdinAdapter.new } - - describe '#open' do - it 'raises NotImplementedError' do - expect(adapter).to receive(:command).and_call_original - - expect do - adapter.open('1+1') - end.to raise_error(NotImplementedError) - end - end -end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 2492af0..792e3e6 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -23,7 +23,11 @@ Bundler.require(:development, :test) +require_relative 'support/stdin_helper' + RSpec.configure do |config| + config.include StdinHelper + config.expect_with :rspec do |expectations| expectations.include_chain_clauses_in_custom_matcher_descriptions = true end diff --git a/spec/support/shared_examples/stdin_process_adapter.rb b/spec/support/shared_examples/stdin_process_adapter.rb deleted file mode 100644 index 040badc..0000000 --- a/spec/support/shared_examples/stdin_process_adapter.rb +++ /dev/null @@ -1,64 +0,0 @@ -# frozen_string_literal: true - -shared_context 'STDIN adapter' do - describe '#open' do - let(:code) { '1 + 1' } - let(:output_content) { 'output' } - let(:error_content) { 'error' } - let(:fake_stdin) { StringIO.new } - let(:wait_thr) { double(:wait_thr, value: value) } - let(:value) { double(:value, success?: success?, to_s: exit_code.to_s) } - let(:success?) { true } - let(:exit_code) { 0 } - let(:bundler) { false } - - before(:example) do - allow(adapter).to receive(:popen3).and_yield( - fake_stdin, - StringIO.new(output_content), - StringIO.new(error_content), - wait_thr - ) - end - - # rubocop:disable Lint/EmptyBlock - it 'writes code to stdin' do - adapter.open(code) {} - expect(fake_stdin.string).to eq(code) - end - - it 'yields streams' do - adapter.open(code) do |_stdin, stdout, stderr| - expect(stdout.read).to eq(output_content) - expect(stderr.read).to eq(error_content) - end - end - - it 'calls external command' do - allow(adapter).to receive(:command).and_return('echo') - expect(adapter).to receive(:popen3).with('echo') - adapter.open(code) {} - end - - context 'with bundler' do - let(:bundler) { true } - - it 'includes bundle exec to the command' do - expect(adapter).to receive(:popen3).with(match(/bundle exec/)) - adapter.open(code) {} - end - end - - context 'when process fails' do - let(:success?) { false } - let(:exit_code) { 127 } - - it 'raises error' do - expect do - adapter.open(code) {} - end.to raise_error(RuntimeError, Regexp.new(value.to_s)) - end - end - # rubocop:enable Lint/EmptyBlock - end -end diff --git a/spec/support/stdin_helper.rb b/spec/support/stdin_helper.rb new file mode 100644 index 0000000..e22b7a5 --- /dev/null +++ b/spec/support/stdin_helper.rb @@ -0,0 +1,9 @@ +module StdinHelper + def with_stdin_redirect(input) + old_stdin = $stdin + $stdin = StringIO.new(input) + yield + ensure + $stdin = old_stdin + end +end