├── .rspec ├── .ruby-version ├── .travis.yml ├── lib └── rspec │ └── shell │ ├── expectations │ ├── version.rb │ ├── call_log.rb │ ├── stubbed_env.rb │ ├── call_configuration.rb │ ├── stubbed_call.rb │ └── stubbed_command.rb │ └── expectations.rb ├── .gitignore ├── Gemfile ├── features ├── developer │ ├── support │ │ ├── tempfiles.rb │ │ ├── simulated_env.rb │ │ └── workfolder.rb │ ├── assert-stdin.feature │ ├── assert-calls.feature │ ├── chain-arguments.feature │ ├── stub-commands.feature │ ├── provide-env-vars.feature │ ├── change-exitstatus.feature │ ├── stub-output.feature │ └── step_definitions │ │ └── general_steps.rb └── README.md ├── Rakefile ├── CHANGELOG.md ├── spec ├── provide_env_vars_spec.rb ├── chain_args_spec.rb ├── stubbed_env_spec.rb ├── assert_stdin_spec.rb ├── replace_shell_commands_spec.rb ├── assert_called_spec.rb ├── change_exitstatus_spec.rb └── stub_output_spec.rb ├── rspec-shell-expectations.gemspec ├── LICENSE.txt ├── bin └── stub └── README.md /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.1.2 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | sudo: false 3 | rvm: 4 | - 2.1.0 5 | - 2.0.0 6 | - ruby-head 7 | -------------------------------------------------------------------------------- /lib/rspec/shell/expectations/version.rb: -------------------------------------------------------------------------------- 1 | module Rspec 2 | module Shell 3 | #:nodoc: 4 | module Expectations 5 | VERSION = '1.3.0' 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | *.bundle 11 | *.so 12 | *.o 13 | *.a 14 | mkmf.log 15 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in rspec-shell-expectations.gemspec 4 | gemspec 5 | gem 'rubocop' 6 | gem 'rake' 7 | gem 'cucumber' 8 | gem 'rspec' 9 | -------------------------------------------------------------------------------- /features/developer/support/tempfiles.rb: -------------------------------------------------------------------------------- 1 | def files_to_delete 2 | @files_to_delete ||= [] 3 | end 4 | 5 | After do 6 | files_to_delete.each do |f| 7 | f.delete if f.exist? 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /features/developer/support/simulated_env.rb: -------------------------------------------------------------------------------- 1 | require 'rspec/shell/expectations' 2 | #:nodoc: 3 | module SimulatedEnv 4 | def simulated_environment 5 | @sim_env ||= Rspec::Shell::Expectations::StubbedEnv.new 6 | end 7 | end 8 | 9 | World(SimulatedEnv) 10 | -------------------------------------------------------------------------------- /features/developer/support/workfolder.rb: -------------------------------------------------------------------------------- 1 | require 'tmpdir' 2 | require 'pathname' 3 | 4 | Before do 5 | @dir = Dir.mktmpdir 6 | end 7 | 8 | After do 9 | FileUtils.remove_entry_secure @dir 10 | end 11 | 12 | def workfolder 13 | Pathname.new(@dir) 14 | end 15 | -------------------------------------------------------------------------------- /lib/rspec/shell/expectations.rb: -------------------------------------------------------------------------------- 1 | require 'rspec/shell/expectations/version' 2 | require 'rspec/shell/expectations/stubbed_command' 3 | require 'rspec/shell/expectations/stubbed_call' 4 | require 'rspec/shell/expectations/call_configuration' 5 | require 'rspec/shell/expectations/call_log' 6 | require 'rspec/shell/expectations/stubbed_env' 7 | -------------------------------------------------------------------------------- /features/README.md: -------------------------------------------------------------------------------- 1 | # Features of this application 2 | 3 | The features of this applications are added in the form of userstories: 4 | 5 | ```gerkhin 6 | In order to 7 | As a 8 | I want 9 | ``` 10 | 11 | These features are also in this directory structure this way: 12 | 13 | ``` 14 | features//.feature 15 | ``` 16 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | 3 | require 'rspec/core/rake_task' 4 | RSpec::Core::RakeTask.new(:spec) do |t| 5 | t.verbose = false 6 | end 7 | 8 | require 'rubocop/rake_task' 9 | RuboCop::RakeTask.new 10 | 11 | require 'cucumber' 12 | require 'cucumber/rake/task' 13 | Cucumber::Rake::Task.new(:features) do |t| 14 | t.cucumber_opts = 'features --strict --format progress' 15 | end 16 | 17 | task default: [:rubocop, :features, :spec] 18 | -------------------------------------------------------------------------------- /features/developer/assert-stdin.feature: -------------------------------------------------------------------------------- 1 | Feature: 2 | In order to check the input of a command 3 | As a developer 4 | I want to check the standard-in stream 5 | 6 | Scenario: Assert standard-in 7 | Given I have the shell script 8 | """ 9 | echo "text to stdin" | command_call 10 | """ 11 | And I have stubbed "command_call" 12 | When I run this script in a simulated environment 13 | Then the command "command_call" has received "text to stdin" from standard-in 14 | -------------------------------------------------------------------------------- /features/developer/assert-calls.feature: -------------------------------------------------------------------------------- 1 | Feature: 2 | In order to make sure the script reaches a certain point 3 | As a developer 4 | I want to assert a command call 5 | 6 | Scenario: Assert command call 7 | Given I have the shell script 8 | """ 9 | command_call 10 | """ 11 | And I have stubbed "command_call" 12 | And I have stubbed "other_call" 13 | When I run this script in a simulated environment 14 | Then the command "command_call" is called 15 | And the command "other_call" is not called 16 | -------------------------------------------------------------------------------- /features/developer/chain-arguments.feature: -------------------------------------------------------------------------------- 1 | Feature: 2 | In order to reduce repetition of arguments 3 | As a developer 4 | I want to chain arguments seperately 5 | 6 | 7 | Scenario: Chain arguments 8 | Given I have the shell script 9 | """ 10 | #!/bin/bash 11 | bundle exec rake test 12 | bundle exec rake build 13 | """ 14 | And I have stubbed "bundle" with args as "rake": 15 | | args | 16 | | exec | 17 | | rake | 18 | When I run this script in a simulated environment 19 | Then the command "rake" is called with "test" 20 | And the command "rake" is called with "build" 21 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 1.3.0 2 | 3 | * Improved assertion message 4 | 5 | # 1.2.0 6 | 7 | * Support for output filenames based on input arguments 8 | (`.outputs('something', to: [:arg2, '.png'])` 9 | * Updates local `ENV['PATH']` to easy test execution from private code 10 | * Add `stubbed_env.cleanup` to cleanup `ENV['PATH']` manually 11 | 12 | # 1.1.0 13 | 14 | * Support chaining of arguments in multiple steps 15 | (`.with_args(...).with_args(...)` 16 | 17 | # 1.0.0 18 | 19 | * Initial release 20 | * Support for 21 | * `create_stubbed_env` 22 | * Execute script with env-vars 23 | * Stubbing commands, output and exitstatus 24 | * Asserting calls, arguments and stdin 25 | -------------------------------------------------------------------------------- /features/developer/stub-commands.feature: -------------------------------------------------------------------------------- 1 | Feature: 2 | In order to run scripts without triggering real commands 3 | As a developer 4 | I want to stub commands 5 | 6 | Scenario: Run a script without stub 7 | Given I have the shell script 8 | """ 9 | command_that_does_not_exist 10 | """ 11 | When I run this script in a simulated environment 12 | Then the exitstatus will not be 0 13 | 14 | Scenario: Run a script with stub 15 | Given I have the shell script 16 | """ 17 | command_that_does_not_exist 18 | """ 19 | And I have stubbed "command_that_does_not_exist" 20 | When I run this script in a simulated environment 21 | Then the exitstatus will be 0 22 | -------------------------------------------------------------------------------- /spec/provide_env_vars_spec.rb: -------------------------------------------------------------------------------- 1 | require 'English' 2 | require 'rspec/shell/expectations' 3 | 4 | describe 'Provide environment vars' do 5 | include Rspec::Shell::Expectations 6 | let(:script) do 7 | <<-SCRIPT 8 | echo $SOME_ENV_VAR 9 | SCRIPT 10 | end 11 | let(:script_path) { Pathname.new '/tmp/test_script.sh' } 12 | 13 | before do 14 | script_path.open('w') { |f| f.puts script } 15 | script_path.chmod 0777 16 | end 17 | 18 | after do 19 | script_path.delete 20 | end 21 | 22 | let(:stubbed_env) { create_stubbed_env } 23 | 24 | it 'exits with an error' do 25 | o, _e, _s = stubbed_env.execute( 26 | script_path, 27 | 'SOME_ENV_VAR' => 'SekretCredential' 28 | ) 29 | expect(o).to eql "SekretCredential\n" 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /features/developer/provide-env-vars.feature: -------------------------------------------------------------------------------- 1 | Feature: 2 | In order to simulate an environment better 3 | As a developer 4 | I want to provide environment variables with execution 5 | 6 | Scenario: Provide environment variables 7 | Given I have the shell script 8 | """ 9 | echo $MESSAGE | command_call 10 | other_command $CREDENTIAL 11 | """ 12 | And I have stubbed "command_call" 13 | And I have stubbed "other_command" 14 | When I run this script in a simulated environment with env: 15 | | name | value | 16 | | MESSAGE | message contents | 17 | | CREDENTIAL | supa-sekret | 18 | Then the command "command_call" has received "message contents" from standard-in 19 | And the command "other_command" is called with "supa-sekret" 20 | 21 | -------------------------------------------------------------------------------- /spec/chain_args_spec.rb: -------------------------------------------------------------------------------- 1 | require 'English' 2 | require 'rspec/shell/expectations' 3 | 4 | describe 'Assert called' do 5 | include Rspec::Shell::Expectations 6 | let(:stubbed_env) { create_stubbed_env } 7 | let!(:rake) { stubbed_env.stub_command('bundle').with_args('exec', 'rake') } 8 | 9 | let(:script) do 10 | <<-SCRIPT 11 | bundle exec rake foo:bar 12 | SCRIPT 13 | end 14 | let(:script_path) { Pathname.new '/tmp/test_script.sh' } 15 | 16 | before do 17 | script_path.open('w') { |f| f.puts script } 18 | script_path.chmod 0777 19 | end 20 | 21 | after do 22 | script_path.delete 23 | end 24 | 25 | subject do 26 | stubbed_env.execute script_path.to_s 27 | end 28 | 29 | describe 'assert called' do 30 | it 'returns called status' do 31 | subject 32 | expect(rake.with_args('foo:bar')).to be_called 33 | expect(rake.with_args('foo')).not_to be_called 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/rspec/shell/expectations/call_log.rb: -------------------------------------------------------------------------------- 1 | module Rspec 2 | module Shell 3 | module Expectations 4 | # Log of calls to a command 5 | class CallLog 6 | def initialize(call_log_path) 7 | @call_log_path = call_log_path 8 | end 9 | 10 | def exist? 11 | @call_log_path.exist? 12 | end 13 | 14 | def called_with_args?(*args) 15 | return true if find_call(*args) 16 | false 17 | end 18 | 19 | def stdin_for_args(*args) 20 | call = find_call(*args) 21 | return call['stdin'] if call 22 | nil 23 | end 24 | 25 | private 26 | 27 | def find_call(*args) 28 | call_log.each do |call| 29 | call_args = call['args'] || [] 30 | return call if (args - call_args).empty? 31 | end 32 | nil 33 | end 34 | 35 | def call_log 36 | YAML.load_file @call_log_path 37 | end 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /spec/stubbed_env_spec.rb: -------------------------------------------------------------------------------- 1 | require 'English' 2 | require 'rspec/shell/expectations' 3 | 4 | describe 'StubbedEnv' do 5 | include Rspec::Shell::Expectations 6 | 7 | describe 'creating a stubbed env' do 8 | it 'extends the PATH with the stubbed folder first' do 9 | expect { create_stubbed_env }.to change { ENV['PATH'] } 10 | end 11 | 12 | it 'creates a folder to place the stubbed commands in' do 13 | env = create_stubbed_env 14 | expect(Pathname.new(env.dir)).to exist 15 | expect(Pathname.new(env.dir)).to be_directory 16 | end 17 | end 18 | 19 | describe '#cleanup' do 20 | it 'restores the environment variable PATH' do 21 | original_path = ENV['PATH'] 22 | env = create_stubbed_env 23 | 24 | expect { env.cleanup }.to change { ENV['PATH'] }.to original_path 25 | end 26 | 27 | it 'removes the folder with stubbed commands' do 28 | env = create_stubbed_env 29 | env.cleanup 30 | expect(Pathname.new(env.dir)).not_to exist 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/assert_stdin_spec.rb: -------------------------------------------------------------------------------- 1 | require 'English' 2 | require 'rspec/shell/expectations' 3 | 4 | describe 'Assert stdin' do 5 | include Rspec::Shell::Expectations 6 | let(:stubbed_env) { create_stubbed_env } 7 | let!(:command1_stub) { stubbed_env.stub_command('command1') } 8 | 9 | let(:script) do 10 | <<-SCRIPT 11 | echo "foo bar" | command1 12 | echo "baz" | command1 'hello' 13 | SCRIPT 14 | end 15 | let(:script_path) { Pathname.new '/tmp/test_script.sh' } 16 | 17 | before do 18 | script_path.open('w') { |f| f.puts script } 19 | script_path.chmod 0777 20 | end 21 | 22 | after do 23 | script_path.delete 24 | end 25 | 26 | subject do 27 | stubbed_env.execute script_path.to_s 28 | end 29 | 30 | describe '#stdin' do 31 | it 'returns the stdin' do 32 | subject 33 | expect(command1_stub.stdin).to match 'foo bar' 34 | end 35 | 36 | context 'with arguments' do 37 | it 'returns the stdin' do 38 | subject 39 | expect(command1_stub.with_args('hello').stdin).to match 'baz' 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /rspec-shell-expectations.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'rspec/shell/expectations/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = 'rspec-shell-expectations' 8 | spec.version = Rspec::Shell::Expectations::VERSION 9 | spec.authors = ['Matthijs Groen'] 10 | spec.email = ['matthijs.groen@gmail.com'] 11 | spec.summary = 'Fake execution environments to TDD shell scripts' 12 | spec.description = <<-DESCRIPTION 13 | Stub results of commands. 14 | Assert calls and input using RSpec for your shell scripts 15 | DESCRIPTION 16 | spec.homepage = '' 17 | spec.license = 'MIT' 18 | 19 | spec.files = `git ls-files -z`.split("\x0") 20 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 21 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 22 | spec.require_paths = ['lib'] 23 | 24 | spec.add_development_dependency 'bundler', '>= 1.6' 25 | spec.add_development_dependency 'rake', '~> 10.0' 26 | end 27 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Matthijs Groen 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /lib/rspec/shell/expectations/stubbed_env.rb: -------------------------------------------------------------------------------- 1 | require 'tmpdir' 2 | require 'English' 3 | require 'open3' 4 | 5 | module Rspec 6 | module Shell 7 | # Define stubbed environment to set and assert expectations 8 | module Expectations 9 | def create_stubbed_env 10 | StubbedEnv.new 11 | end 12 | 13 | # A shell environment that can manipulate behaviour 14 | # of executables 15 | class StubbedEnv 16 | attr_reader :dir 17 | 18 | def initialize 19 | @dir = Dir.mktmpdir 20 | ENV['PATH'] = "#{@dir}:#{ENV['PATH']}" 21 | at_exit { cleanup } 22 | end 23 | 24 | def cleanup 25 | paths = (ENV['PATH'].split ':') - [@dir] 26 | ENV['PATH'] = paths.join ':' 27 | FileUtils.remove_entry_secure @dir if Pathname.new(@dir).exist? 28 | end 29 | 30 | def stub_command(command) 31 | StubbedCommand.new command, @dir 32 | end 33 | 34 | def execute(command, env_vars = {}) 35 | Open3.capture3(env_vars, "#{env} #{command}") 36 | end 37 | 38 | private 39 | 40 | def env 41 | "PATH=#{@dir}:$PATH" 42 | end 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /features/developer/change-exitstatus.feature: -------------------------------------------------------------------------------- 1 | Feature: 2 | In order to manipulate code paths in scripts 3 | As a developer 4 | I want to change the exitstatus of a command 5 | 6 | Scenario: Change exitstatus code unconditional 7 | Given I have the shell script 8 | """ 9 | #!/bin/bash 10 | command_with_status 11 | """ 12 | And I have stubbed "command_with_status" 13 | And the stubbed command returns exitstatus 5 14 | When I run this script in a simulated environment 15 | Then the exitstatus will be 5 16 | 17 | Scenario: Change exitstatus code when argument matches 18 | Given I have the shell script 19 | """ 20 | #!/bin/bash 21 | command_with_status --flag 22 | if [ $? -neq 4 ]; then 23 | exit 3 24 | fi 25 | command_with_status --foo 26 | """ 27 | And I have stubbed "command_with_status" with args: 28 | | args | 29 | | --flag | 30 | And the stubbed command returns exitstatus 4 31 | And I have stubbed "command_with_status" with args: 32 | | args | 33 | | --foo | 34 | And the stubbed command returns exitstatus 5 35 | When I run this script in a simulated environment 36 | Then the exitstatus will be 5 37 | -------------------------------------------------------------------------------- /lib/rspec/shell/expectations/call_configuration.rb: -------------------------------------------------------------------------------- 1 | require 'yaml' 2 | 3 | module Rspec 4 | module Shell 5 | module Expectations 6 | # Configuration of a stubbed command 7 | class CallConfiguration 8 | attr_reader :command 9 | 10 | def initialize(config_path, command) 11 | @config_path = config_path 12 | @configuration = {} 13 | @command = command 14 | end 15 | 16 | def set_exitcode(statuscode, args = []) 17 | @configuration[args] ||= {} 18 | @configuration[args][:statuscode] = statuscode 19 | end 20 | 21 | def set_output(content, target, args = []) 22 | @configuration[args] ||= {} 23 | @configuration[args][:outputs] ||= [] 24 | @configuration[args][:outputs] << { target: target, content: content } 25 | end 26 | 27 | def write 28 | structure = [] 29 | @configuration.each do |args, results| 30 | call = { 31 | args: args 32 | }.merge results 33 | structure << call 34 | end 35 | 36 | @config_path.open('w') do |f| 37 | f.puts structure.to_yaml 38 | end 39 | end 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /spec/replace_shell_commands_spec.rb: -------------------------------------------------------------------------------- 1 | require 'English' 2 | require 'rspec/shell/expectations' 3 | 4 | describe 'Replace shell commands' do 5 | include Rspec::Shell::Expectations 6 | let(:script) do 7 | <<-SCRIPT 8 | command1 "foo bar" 9 | SCRIPT 10 | end 11 | let(:script_path) { Pathname.new '/tmp/test_script.sh' } 12 | 13 | before do 14 | script_path.open('w') { |f| f.puts script } 15 | script_path.chmod 0777 16 | end 17 | 18 | after do 19 | script_path.delete 20 | end 21 | 22 | describe 'running a file with non-existing commands' do 23 | it 'exits with an error' do 24 | `#{script_path} 2>&1` 25 | expect($CHILD_STATUS.exitstatus).not_to eq 0 26 | end 27 | 28 | context 'with stubbed environment' do 29 | let(:stubbed_env) { create_stubbed_env } 30 | 31 | it 'exits with an error' do 32 | stubbed_env.execute "#{script_path} 2>&1" 33 | expect($CHILD_STATUS.exitstatus).not_to eq 0 34 | end 35 | 36 | context 'with a stubbed command' do 37 | before do 38 | stubbed_env.stub_command('command1') 39 | end 40 | 41 | it 'exits with status code 0' do 42 | _o, _e, s = stubbed_env.execute "#{script_path} 2>&1" 43 | expect(s.exitstatus).to eq 0 44 | end 45 | end 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /features/developer/stub-output.feature: -------------------------------------------------------------------------------- 1 | Feature: 2 | In order to simulate command behaviour 3 | As a developer 4 | I want to stub the output of a command 5 | 6 | Scenario: Stub standard-out 7 | Given I have the shell script 8 | """ 9 | command_call 1> file.txt 10 | """ 11 | And I have stubbed "command_call" 12 | And the stubbed command outputs "hello there" to standard-out 13 | When I run this script in a simulated environment 14 | Then the file "file.txt" contains "hello there" 15 | 16 | Scenario: Stub standard-error 17 | Given I have the shell script 18 | """ 19 | command_call 2> file.txt 20 | """ 21 | And I have stubbed "command_call" 22 | And the stubbed command outputs "hello there" to standard-error 23 | When I run this script in a simulated environment 24 | Then the file "file.txt" contains "hello there" 25 | 26 | Scenario: Stub to file 27 | Given I have the shell script 28 | """ 29 | command_call 30 | """ 31 | And I have stubbed "command_call" 32 | And the stubbed command outputs "hello there" to "file.txt" 33 | And the stubbed command outputs "there" to "other_file.txt" 34 | When I run this script in a simulated environment 35 | Then the file "file.txt" contains "hello there" 36 | And the file "other_file.txt" contains "there" 37 | 38 | -------------------------------------------------------------------------------- /lib/rspec/shell/expectations/stubbed_call.rb: -------------------------------------------------------------------------------- 1 | module Rspec 2 | module Shell 3 | module Expectations 4 | # A specific call with arguments on a StubbedCommand 5 | class StubbedCall 6 | def initialize(config, call_log, args) 7 | @config = config 8 | @call_log = call_log 9 | @args = args 10 | end 11 | 12 | def with_args(*args) 13 | StubbedCall.new(@config, @call_log, @args + args) 14 | end 15 | 16 | def returns_exitstatus(statuscode) 17 | @config.set_exitcode(statuscode, @args) 18 | @config.write 19 | self 20 | end 21 | 22 | def outputs(content, to: :stdout) 23 | @config.set_output(content, to, @args) 24 | @config.write 25 | self 26 | end 27 | 28 | def stdin 29 | return nil unless @call_log.exist? 30 | @call_log.stdin_for_args(*@args) 31 | end 32 | 33 | def called? 34 | return false unless @call_log.exist? 35 | @call_log.called_with_args?(*@args) 36 | end 37 | 38 | def inspect 39 | if @args.any? 40 | "" 42 | else 43 | "" 44 | end 45 | end 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /spec/assert_called_spec.rb: -------------------------------------------------------------------------------- 1 | require 'English' 2 | require 'rspec/shell/expectations' 3 | 4 | describe 'Assert called' do 5 | include Rspec::Shell::Expectations 6 | let(:stubbed_env) { create_stubbed_env } 7 | let!(:command1_stub) { stubbed_env.stub_command('command1') } 8 | 9 | let(:script) do 10 | <<-SCRIPT 11 | command1 "foo bar" 12 | SCRIPT 13 | end 14 | let(:script_path) { Pathname.new '/tmp/test_script.sh' } 15 | 16 | before do 17 | script_path.open('w') { |f| f.puts script } 18 | script_path.chmod 0777 19 | end 20 | 21 | after do 22 | script_path.delete 23 | end 24 | 25 | subject do 26 | stubbed_env.execute script_path.to_s 27 | end 28 | 29 | describe 'assert called' do 30 | it 'returns called status' do 31 | subject 32 | expect(command1_stub).to be_called 33 | end 34 | 35 | context 'assert with args' do 36 | it 'returns called status' do 37 | subject 38 | expect(command1_stub.with_args('foo bar')).to be_called 39 | expect(command1_stub.with_args('foo')).not_to be_called 40 | end 41 | end 42 | 43 | describe 'assertion message' do 44 | it 'provides a helpful message' do 45 | expect(command1_stub.inspect).to eql '' 46 | expect(command1_stub.with_args('foo bar').inspect).to \ 47 | eql '' 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /spec/change_exitstatus_spec.rb: -------------------------------------------------------------------------------- 1 | require 'English' 2 | require 'rspec/shell/expectations' 3 | 4 | describe 'Change exitstatus' do 5 | include Rspec::Shell::Expectations 6 | let(:stubbed_env) { create_stubbed_env } 7 | let!(:command1_stub) { stubbed_env.stub_command('command1') } 8 | let(:script) do 9 | <<-SCRIPT 10 | command1 "foo bar" 11 | SCRIPT 12 | end 13 | let(:script_path) { Pathname.new '/tmp/test_script.sh' } 14 | 15 | before do 16 | script_path.open('w') { |f| f.puts script } 17 | script_path.chmod 0777 18 | end 19 | 20 | after do 21 | script_path.delete 22 | end 23 | 24 | subject do 25 | stubbed_env.execute script_path.to_s 26 | end 27 | 28 | describe 'default exitstatus' do 29 | it 'is 0' do 30 | _o, _e, s = subject 31 | expect(s.exitstatus).to eq 0 32 | end 33 | end 34 | 35 | describe 'changing exitstatus' do 36 | before do 37 | command1_stub.returns_exitstatus(4) 38 | end 39 | 40 | it 'returns the stubbed exitstatus' do 41 | _o, _e, s = subject 42 | expect(s.exitstatus).to eq 4 43 | end 44 | 45 | context 'with specific args only' do 46 | before do 47 | command1_stub.with_args('foo bar').returns_exitstatus(2) 48 | command1_stub.with_args('bar').returns_exitstatus(6) 49 | end 50 | 51 | it 'returns the stubbed exitstatus' do 52 | _o, _e, s = subject 53 | expect(s.exitstatus).to eq 2 54 | end 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/rspec/shell/expectations/stubbed_command.rb: -------------------------------------------------------------------------------- 1 | module Rspec 2 | module Shell 3 | module Expectations 4 | # Command that produces specific output 5 | # and monitors input 6 | class StubbedCommand 7 | def initialize(command, dir) 8 | FileUtils.cp(stub_filepath, File.join(dir, command)) 9 | @call_configuration = CallConfiguration.new( 10 | Pathname.new(dir).join("#{command}_stub.yml"), 11 | command 12 | ) 13 | @call_log = CallLog.new( 14 | Pathname.new(dir).join("#{command}_calls.yml") 15 | ) 16 | end 17 | 18 | def with_args(*args) 19 | StubbedCall.new(@call_configuration, @call_log, args) 20 | end 21 | 22 | def called? 23 | with_args.called? 24 | end 25 | 26 | def called_with_args?(*args) 27 | with_args.called_with_args?(*args) 28 | end 29 | 30 | def returns_exitstatus(statuscode) 31 | with_args.returns_exitstatus(statuscode) 32 | end 33 | 34 | def stdin 35 | with_args.stdin 36 | end 37 | 38 | def outputs(contents, to: :stdout) 39 | with_args.outputs(contents, to: to) 40 | end 41 | 42 | def inspect 43 | with_args.inspect 44 | end 45 | 46 | private 47 | 48 | def stub_filepath 49 | project_root.join('bin', 'stub') 50 | end 51 | 52 | def project_root 53 | Pathname.new(File.dirname(File.expand_path(__FILE__))) 54 | .join('..', '..', '..', '..') 55 | end 56 | end 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /bin/stub: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'pathname' 3 | require 'yaml' 4 | 5 | command = File.basename(__FILE__) 6 | folder = File.dirname(__FILE__) 7 | 8 | call_logs = Pathname.new(folder).join("#{command}_calls.yml") 9 | call_logs.open('a') do |f| 10 | f.puts '- args:' 11 | ARGV.each { |arg| f.puts " - #{arg.inspect}" } 12 | f.puts " stdin: #{$stdin.read.inspect}" unless STDIN.tty? 13 | end 14 | 15 | def interpolate_filename(elements) 16 | return elements if elements.is_a? String 17 | return nil unless elements.is_a? Array 18 | 19 | elements.map do |element| 20 | case element 21 | when String then element 22 | when Symbol then interpolate_argument(element) 23 | end 24 | end.join 25 | end 26 | 27 | def interpolate_argument(name) 28 | return unless (data = /^arg(\d+)$/.match(name.to_s)) 29 | ARGV[data[1].to_i - 1] 30 | end 31 | 32 | call_configurations = Pathname.new(folder).join("#{command}_stub.yml") 33 | if call_configurations.exist? 34 | config = YAML.load call_configurations.read 35 | 36 | matching_calls = [] 37 | config.each do |call_config| 38 | next unless (call_config[:args] - ARGV).empty? 39 | matching_calls << { 40 | config: call_config, 41 | specific: call_config[:args].length 42 | } 43 | end 44 | exit 0 if matching_calls.empty? 45 | 46 | call_config = matching_calls.sort do |a, b| 47 | b[:specific] <=> a[:specific] 48 | end.first[:config] 49 | (call_config[:outputs] || []).each do |data| 50 | $stdout.print data[:content] if data[:target] == :stdout 51 | $stderr.print data[:content] if data[:target] == :stderr 52 | 53 | output_filename = interpolate_filename(data[:target]) 54 | next unless output_filename 55 | Pathname.new(output_filename).open('w') do |f| 56 | f.print data[:content] 57 | end 58 | end 59 | exit call_config[:statuscode] || 0 60 | end 61 | -------------------------------------------------------------------------------- /spec/stub_output_spec.rb: -------------------------------------------------------------------------------- 1 | require 'English' 2 | require 'rspec/shell/expectations' 3 | 4 | describe 'Stub command output' do 5 | include Rspec::Shell::Expectations 6 | let(:stubbed_env) { create_stubbed_env } 7 | let!(:command1_stub) { stubbed_env.stub_command('command1') } 8 | 9 | let(:script) do 10 | <<-SCRIPT 11 | command1 12 | SCRIPT 13 | end 14 | let(:script_path) { Pathname.new '/tmp/test_script.sh' } 15 | 16 | before do 17 | script_path.open('w') { |f| f.puts script } 18 | script_path.chmod 0777 19 | end 20 | 21 | after do 22 | script_path.delete 23 | end 24 | 25 | describe 'stubbing standard-out' do 26 | subject do 27 | stubbed_env.execute "#{script_path} 2>/dev/null" 28 | end 29 | 30 | it 'changes standard-out' do 31 | command1_stub.outputs('hello', to: :stdout) 32 | o, e, _s = subject 33 | expect(o).to eql 'hello' 34 | expect(e).to be_empty 35 | end 36 | end 37 | 38 | describe 'stubbing standard-err' do 39 | subject do 40 | stubbed_env.execute "#{script_path} 1>/dev/null" 41 | end 42 | 43 | it 'changes standard-out' do 44 | command1_stub.outputs('world', to: :stderr) 45 | o, e, _s = subject 46 | expect(e).to eql 'world' 47 | expect(o).to be_empty 48 | end 49 | end 50 | 51 | describe 'stubbing contents to file' do 52 | subject do 53 | stubbed_env.execute "#{script_path}" 54 | end 55 | let(:filename) { 'test-log.nice' } 56 | after do 57 | f = Pathname.new(filename) 58 | f.delete if f.exist? 59 | end 60 | 61 | it 'write data to a file' do 62 | command1_stub.outputs('world', to: filename) 63 | o, e, _s = subject 64 | expect(e).to be_empty 65 | expect(o).to be_empty 66 | expect(Pathname.new(filename).read).to eql 'world' 67 | end 68 | 69 | describe 'using passed argument as filename' do 70 | let(:script) do 71 | <<-SCRIPT 72 | command1 input output 73 | SCRIPT 74 | end 75 | 76 | let(:passed_filename) { ['hello-', :arg2, '.foo'] } 77 | let(:filename) { 'hello-output.foo' } 78 | 79 | it 'writes data to a interpolated filename' do 80 | command1_stub.outputs('world', to: passed_filename) 81 | subject 82 | expect(Pathname.new(filename).read).to eql 'world' 83 | end 84 | end 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /features/developer/step_definitions/general_steps.rb: -------------------------------------------------------------------------------- 1 | Given(/^I have the shell script$/) do |script_contents| 2 | @script = workfolder.join('script.sh') 3 | @script.open('w') do |w| 4 | w.puts script_contents 5 | end 6 | @script.chmod 0777 7 | end 8 | 9 | Given(/^I have stubbed "(.*?)"$/) do |command| 10 | @stubbed_command = simulated_environment.stub_command command 11 | end 12 | 13 | Given(/^I have stubbed "(.*?)" with args as "(.*)":$/) do |command, call, table| 14 | # table is a Cucumber::Ast::Table 15 | args = table.hashes.map { |d| d['args'] } 16 | @stubbed_command = simulated_environment.stub_command(command) 17 | .with_args(*args) 18 | @remembered_commands ||= {} 19 | @remembered_commands[call] = @stubbed_command 20 | end 21 | 22 | Given(/^I have stubbed "(.*?)" with args:$/) do |command, table| 23 | # table is a Cucumber::Ast::Table 24 | args = table.hashes.map { |d| d['args'] } 25 | @stubbed_command = simulated_environment.stub_command(command) 26 | .with_args(*args) 27 | end 28 | 29 | sc = /^the stubbed command/ 30 | Given(/#{sc} returns exitstatus (\d+)$/) do |statuscode| 31 | @stubbed_command.returns_exitstatus(statuscode.to_i) 32 | end 33 | 34 | Given(/#{sc} outputs "(.*?)" to standard\-out$/) do |output| 35 | @stubbed_command.outputs(output, to: :stdout) 36 | end 37 | 38 | Given(/#{sc} outputs "(.*?)" to standard\-error$/) do |output| 39 | @stubbed_command.outputs(output, to: :stderr) 40 | end 41 | 42 | Given(/#{sc} outputs "(.*?)" to "(.*?)"$/) do |output, target| 43 | @stubbed_command.outputs(output, to: target) 44 | files_to_delete.push Pathname.new(target) 45 | end 46 | 47 | When(/^I run this script in a simulated environment$/) do 48 | @stdout, @stderr, @status = simulated_environment.execute "#{@script} 2>&1" 49 | end 50 | 51 | When(/^I run this script in a simulated environment with env:$/) do |table| 52 | env = Hash[table.hashes.map do |hash| 53 | [hash[:name], hash[:value]] 54 | end] 55 | 56 | @stdout, @stderr, @status = simulated_environment.execute( 57 | @script, 58 | env 59 | ) 60 | end 61 | 62 | Then(/^the exitstatus will not be (\d+)$/) do |statuscode| 63 | expect(@status.exitstatus).not_to eql statuscode.to_i 64 | end 65 | 66 | Then(/^the exitstatus will be (\d+)$/) do |statuscode| 67 | expect(@status.exitstatus).to eql statuscode.to_i 68 | end 69 | 70 | c = /^(the command "[^"]+")/ 71 | Transform(/^the command "(.*)"/) do |command| 72 | cmd = (@remembered_commands || {})[command] 73 | cmd || simulated_environment.stub_command(command) 74 | end 75 | 76 | Then(/#{c} is called$/) do |command| 77 | expect(command).to be_called 78 | end 79 | 80 | Then(/#{c} is called with "(.*?)"$/) do |command, argument| 81 | expect(command.with_args(argument)).to be_called 82 | end 83 | 84 | Then(/#{c} is not called$/) do |command| 85 | expect(command).not_to be_called 86 | end 87 | 88 | Then(/#{c} has received "(.*?)" from standard\-in$/) do |command, contents| 89 | expect(command.stdin).to match contents 90 | end 91 | 92 | Then(/^the file "(.*?)" contains "(.*?)"$/) do |filename, contents| 93 | files_to_delete.push Pathname.new(filename) 94 | expect(Pathname.new(filename).read).to eql contents 95 | end 96 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rspec::Shell::Expectations 2 | [![Build Status](https://travis-ci.org/matthijsgroen/rspec-shell-expectations.png?branch=master)](https://travis-ci.org/matthijsgroen/rspec-shell-expectations) 3 | [![Gem Version](https://badge.fury.io/rb/rspec-shell-expectations.svg)](http://badge.fury.io/rb/rspec-shell-expectations) 4 | [![Code Climate](https://codeclimate.com/github/matthijsgroen/rspec-shell-expectations/badges/gpa.svg)](https://codeclimate.com/github/matthijsgroen/rspec-shell-expectations) 5 | 6 | Run your shell script in a mocked environment to test its behaviour 7 | using RSpec. 8 | 9 | ## Features 10 | 11 | - Setup a fake shell environment that has preference in exucing commands 12 | (highest path value) 13 | - Stub shell commands in this environment 14 | - Control multiple outputs (through STDOUT, STDERR or files) 15 | - Control exit status codes 16 | - Set different configurations based on command line arguments 17 | - Verify if command is called 18 | - Verify arguments command is called with 19 | - Verify STDIN 20 | 21 | 22 | ## Installation 23 | 24 | Add this line to your application's Gemfile: 25 | 26 | ```ruby 27 | gem 'rspec-shell-expectations' 28 | ``` 29 | 30 | And then execute: 31 | 32 | $ bundle 33 | 34 | Or install it yourself as: 35 | 36 | $ gem install rspec-shell-expectations 37 | 38 | 39 | You can setup rspec-shell-expectations globally for your spec suite: 40 | 41 | in `spec_helper.rb`: 42 | 43 | ```ruby 44 | require 'rspec/shell/expectations' 45 | 46 | RSpec.configure do |c| 47 | c.include Rspec::Shell::Expectations 48 | end 49 | ``` 50 | 51 | ## Usage 52 | 53 | see specs in `spec/` folder: 54 | 55 | ### Running script through stubbed env: 56 | 57 | ```ruby 58 | require 'rspec/shell/expectations' 59 | 60 | describe 'my shell script' do 61 | include Rspec::Shell::Expectations 62 | 63 | let(:stubbed_env) { create_stubbed_env } 64 | 65 | it 'runs the script' do 66 | stdout, stderr, status = stubbed_env.execute( 67 | 'my-shell-script.sh', 68 | { 'SOME_OPTIONAL' => 'env vars' } 69 | ) 70 | expect(status.exitstatus).to eq 0 71 | end 72 | end 73 | ``` 74 | 75 | ### Stubbing commands: 76 | 77 | ```ruby 78 | let(:stubbed_env) { create_stubbed_env } 79 | let!(:bundle) { stubbed_env.stub_command('bundle') } 80 | let(:rake) { bundle.with_args('exec', 'rake') } 81 | 82 | it 'is stubbed' do 83 | stubbed_env.execute 'my-script.sh' 84 | expect(rake.with_args('test')).to be_called 85 | expect(bundle.with_args('install)).to be_called 86 | end 87 | ``` 88 | 89 | ### Changing exitstatus: 90 | 91 | ```ruby 92 | let(:stubbed_env) { create_stubbed_env } 93 | before do 94 | stubbed_env.stub_command('rake').returns_exitstatus(5) 95 | stubbed_env.stub_command('rake').with_args('spec').returns_exitstatus(3) 96 | end 97 | ``` 98 | 99 | ### Stubbing output: 100 | 101 | ```ruby 102 | let(:stubbed_env) { create_stubbed_env } 103 | let(:rake_stub) { stubbed_env.stub_command 'rake' } 104 | before do 105 | rake_stub.outputs('informative message', to: :stdout) 106 | .outputs('error message', to: :stderr) 107 | .outputs('log contents', to: 'logfile.log') 108 | .outputs('converted result', to: ['prefix-', :arg2, '.txt']) 109 | # last one creates 'prefix-foo.txt' when called as 'rake convert foo' 110 | end 111 | ``` 112 | 113 | ### Verifying called: 114 | 115 | ```ruby 116 | let(:stubbed_env) { create_stubbed_env } 117 | let(:rake_stub) { stubbed_env.stub_command 'rake' } 118 | it 'verifies called' do 119 | stubbed_env.execute_script 'script.sh' 120 | 121 | expect(rake_stub).to be_called 122 | expect(rake_stub.with_args('spec')).to be_called 123 | expect(rake_stub.with_args('features')).not_to be_called 124 | end 125 | ``` 126 | 127 | ### Verifying stdin: 128 | 129 | ```ruby 130 | let(:stubbed_env) { create_stubbed_env } 131 | let(:cat_stub) { stubbed_env.stub_command 'cat' } 132 | let(:mail_stub) { stubbed_env.stub_command 'mail' } 133 | it 'verifies stdin' do 134 | stubbed_env.execute_script 'script.sh' 135 | expect(cat_stub.stdin).to eql 'hello' 136 | expect(mail_stub.with_args('-s', 'hello').stdin).to eql 'world' 137 | end 138 | ``` 139 | 140 | ## More examples 141 | 142 | see the *features* and *spec* folder 143 | 144 | ## Supported ruby versions 145 | 146 | Ruby 2+, no JRuby, due to issues with `Open3.capture3` 147 | 148 | ## Contributing 149 | 150 | 1. Fork it ( https://github.com/matthijsgroen/rspec-shell-expectations/fork ) 151 | 2. Create your feature branch (`git checkout -b my-new-feature`) 152 | 3. Commit your changes (`git commit -am 'Add some feature'`) 153 | 4. Push to the branch (`git push origin my-new-feature`) 154 | 5. Create a new Pull Request 155 | --------------------------------------------------------------------------------