Small cleanups, add tests.
authorMahlon E. Smith <mahlon@martini.nu>
Tue, 13 May 2014 14:38:21 -0700
changeset 4 3972315383b3
parent 3 62196065e9ea
child 5 6177a734f764
Small cleanups, add tests.
.pryrc
.rvm.gems
lib/symphony/tasks/ssh.rb
lib/symphony/tasks/sshscript.rb
spec/fake_ssh
spec/symphony/tasks/.placeholder
spec/symphony/tasks/ssh_spec.rb
spec/symphony/tasks/sshscript_spec.rb
--- a/.pryrc	Mon May 12 16:34:12 2014 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,14 +0,0 @@
-#!/usr/bin/ruby -*- ruby -*-
-
-require 'pathname'
-
-begin
-	$LOAD_PATH.unshift( Pathname(__FILE__).dirname + 'lib' )
-	require 'symphony'
-	require 'symphony/metronome'
-
-rescue => e
-	$stderr.puts "Ack! Libraries failed to load: #{e.message}\n\t" +
-		e.backtrace.join( "\n\t" )
-end
-
--- a/.rvm.gems	Mon May 12 16:34:12 2014 -0700
+++ b/.rvm.gems	Tue May 13 14:38:21 2014 -0700
@@ -1,3 +1,4 @@
+configurability -v2.1.2
 inversion -v0.12.3
 net-sftp -v2.1.2
 net-ssh -v2.9.0
@@ -9,4 +10,5 @@
 rspec-expectations -v3.0.0.beta2
 rspec-mocks -v3.0.0.beta2
 rspec-support -v3.0.0.beta2
+simplecov -v0.7.1
 symphony -v0.6.0
--- a/lib/symphony/tasks/ssh.rb	Mon May 12 16:34:12 2014 -0700
+++ b/lib/symphony/tasks/ssh.rb	Tue May 13 14:38:21 2014 -0700
@@ -149,7 +149,7 @@
 		parent_reader, child_writer = IO.pipe
 		child_reader, parent_writer = IO.pipe
 
-		pid = spawn( *cmd, :out => child_writer, :in => child_reader, :close_others => true )
+		pid = Process.spawn( *cmd, :out => child_writer, :in => child_reader, :close_others => true )
 		child_writer.close
 		child_reader.close
 
--- a/lib/symphony/tasks/sshscript.rb	Mon May 12 16:34:12 2014 -0700
+++ b/lib/symphony/tasks/sshscript.rb	Tue May 13 14:38:21 2014 -0700
@@ -108,7 +108,7 @@
 		key        = payload[ 'key'  ]    || Symphony::Task::SSHScript.key
 		nocleanup  = payload[ 'nocleanup' ]
 
-		raise ArgumentError, "Missing required option 'command'" unless template
+		raise ArgumentError, "Missing required option 'template'" unless template
 		raise ArgumentError, "Missing required option 'host'"    unless payload[ 'host' ]
 
 		remote_filename = self.make_remote_filename( template )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/spec/fake_ssh	Tue May 13 14:38:21 2014 -0700
@@ -0,0 +1,12 @@
+#!/usr/bin/env ruby
+#
+# Rather than mocking out all of spawn() and its pipes, test end-to-end.
+
+print <<-EOF
+Warning: no access to tty;
+Thus no job control in this shell.
+Hi there!
+EOF
+
+exit 0
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/spec/symphony/tasks/ssh_spec.rb	Tue May 13 14:38:21 2014 -0700
@@ -0,0 +1,64 @@
+
+require_relative '../../helpers'
+require 'symphony/tasks/ssh'
+
+context Symphony::Task::SSH do
+	let( :ssh ) { (Pathname( __FILE__ ).dirname.parent.parent + 'fake_ssh').realpath }
+
+	before( :each ) do
+		described_class.configure(
+			path: ssh.to_s,
+			key:  '/tmp/sekrit.rsa',
+			user: 'symphony'
+		)
+	end
+
+	it_should_behave_like "an object with Configurability"
+
+	describe 'subclassed' do
+		let( :instance ) { Class.new(described_class).new('queue') }
+		let( :payload ) {
+			{ 'command' => 'woohoo', 'host' => 'example.com' }
+		}
+
+		it "aborts if there is no command in the payload" do
+			expect {
+				instance.work( {}, {} )
+			}.to raise_exception( ArgumentError, /missing required option 'command'/i )
+		end
+
+		it "aborts if there is no host in the payload" do
+			expect {
+				instance.work({ 'command' => 'boop' }, {} )
+			}.to raise_exception( ArgumentError, /missing required option 'host'/i )
+		end
+
+		it "builds the proper command line" do
+			pipe = double( :fake_pipes ).as_null_object
+			allow( IO ).to receive( :pipe ).and_return([ pipe, pipe ])
+
+			args = [
+				'-p', '22', '-i', '/tmp/sekrit.rsa', '-l', 'symphony', 'example.com'
+			]
+
+			expect( Process ).to receive( :spawn ).with(
+				*[ ssh.to_s, described_class.opts, args ].flatten,
+				:out => pipe, :in => pipe, :close_others => true
+			).and_return( 12 )
+
+			expect( Process ).to receive( :waitpid2 ).with( 12 ).and_return([ 12, 1 ])
+
+			code = instance.work( payload, {} )
+			expect( code ).to eq( 1 )
+		end
+
+		it "execs and captures output" do
+			code = instance.work( payload, {} )
+			expect( code ).to eq( 0 )
+
+			output = instance.instance_variable_get( :@output )
+			expect( output ).to eq( 'Hi there!' )
+		end
+	end
+end
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/spec/symphony/tasks/sshscript_spec.rb	Tue May 13 14:38:21 2014 -0700
@@ -0,0 +1,129 @@
+
+require_relative '../../helpers'
+require 'symphony/tasks/sshscript'
+
+context Symphony::Task::SSHScript do
+
+	before( :each ) do
+		described_class.configure(
+			key:  '/tmp/sekrit.rsa',
+			user: 'symphony'
+		)
+	end
+
+	it_should_behave_like "an object with Configurability"
+
+	describe 'subclassed' do
+		let( :instance ) { Class.new(described_class).new('queue') }
+		let( :payload ) {
+			{ 'template' => 'script', 'host' => 'example.com' }
+		}
+		let( :opts ) {
+			opts = described_class::DEFAULT_SSH_OPTIONS
+			opts.merge!(
+				:port    => 22,
+				:keys    => ['/tmp/sekrit.rsa']
+			)
+			opts
+		}
+		let( :template ) { Inversion::Template.new("Hi there, <?attr name?>!") }
+
+		before( :each ) do
+			allow( Inversion::Template ).to receive( :load ).and_return( template )
+			allow( Dir::Tmpname ).to receive( :make_tmpname ).and_return( "script_temp" )
+		end
+
+		it "aborts if there is no template in the payload" do
+			expect {
+				instance.work( {}, {} )
+			}.to raise_exception( ArgumentError, /missing required option 'template'/i )
+		end
+
+		it "aborts if there is no host in the payload" do
+			expect {
+				instance.work({ 'template' => 'boop' }, {} )
+			}.to raise_exception( ArgumentError, /missing required option 'host'/i )
+		end
+
+		it "adds debugging output if specified in the payload" do
+			payload[ 'debug' ] = true
+
+			options = opts.dup
+			options.merge!(
+				:logger  => Loggability[ Net::SSH ],
+				:verbose => :debug
+			)
+
+			expect( Net::SSH ).to receive( :start ).with( 'example.com', 'symphony', options )
+			instance.work( payload, {} )
+		end
+
+		it "attaches attributes to the scripts from the payload" do
+			payload[ 'attributes' ] = { :name => 'Handsome' }
+
+			conn = double( :ssh_connection )
+			expect( instance ).to receive( :upload_script ).
+				with( conn, "Hi there, Handsome!", "/tmp/script_temp" )
+			expect( conn ).to receive( :exec! ).with( "/tmp/script_temp" )
+			expect( conn ).to receive( :exec! ).with( "rm /tmp/script_temp" )
+
+			expect( Net::SSH ).to receive( :start ).
+				with( 'example.com', 'symphony', opts ).and_yield( conn )
+
+			instance.work( payload, {} )
+		end
+
+		it "uploads the file and sets it executable" do
+			conn = double( :ssh_connection )
+			sftp = double( :sftp_connection )
+			file = double( :remote_file_obj )
+			fh   = double( :remote_filehandle )
+
+			expect( conn ).to receive( :sftp ).and_return( sftp )
+			expect( sftp ).to receive( :file ).and_return( file )
+
+			expect( file ).to receive( :open ).
+				with( "/tmp/script_temp", "w", 0755 ).and_yield( fh )
+			expect( fh ).to receive( :print ).with( "Hi there, !" )
+
+			expect( conn ).to receive( :exec! ).with( "/tmp/script_temp" )
+			expect( conn ).to receive( :exec! ).with( "rm /tmp/script_temp" )
+
+			expect( Net::SSH ).to receive( :start ).
+				with( 'example.com', 'symphony', opts ).and_yield( conn )
+
+			instance.work( payload, {} )
+		end
+
+		it "leaves the remote script in place if asked" do
+			payload[ 'nocleanup' ] = true
+
+			conn = double( :ssh_connection )
+			expect( instance ).to receive( :upload_script ).
+				with( conn, "Hi there, !", "/tmp/script_temp" )
+			expect( conn ).to receive( :exec! ).with( "/tmp/script_temp" )
+			expect( conn ).to_not receive( :exec! ).with( "rm /tmp/script_temp" )
+
+			expect( Net::SSH ).to receive( :start ).
+				with( 'example.com', 'symphony', opts ).and_yield( conn )
+
+			instance.work( payload, {} )
+		end
+
+		it "remembers the output of the remote script" do
+			conn = double( :ssh_connection )
+			expect( instance ).to receive( :upload_script ).
+				with( conn, "Hi there, !", "/tmp/script_temp" )
+			expect( conn ).to receive( :exec! ).with( "/tmp/script_temp" ).and_return( "Hi there, !" )
+			expect( conn ).to receive( :exec! ).with( "rm /tmp/script_temp" )
+
+			expect( Net::SSH ).to receive( :start ).
+				with( 'example.com', 'symphony', opts ).and_yield( conn )
+
+			instance.work( payload, {} )
+			output = instance.instance_variable_get( :@output )
+			expect( output ).to eq( 'Hi there, !' )
+		end
+	end
+end
+