command_test 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,76 @@
1
+ require 'open3'
2
+
3
+ module CommandTest
4
+ module CoreExtensions
5
+ def self.define_included_hook(mod, *methods) # :nodoc:
6
+ name_map = methods.last.is_a?(Hash) ? methods.pop : {}
7
+ methods.each{|m| name_map[m] = m}
8
+ aliasings = name_map.map do |name, massaged|
9
+ <<-EOS
10
+ unless method_defined?(:#{massaged}_without_command_test)
11
+ alias #{massaged}_without_command_test #{name}
12
+ alias #{name} #{massaged}_with_command_test
13
+ end
14
+ EOS
15
+ end.join("\n")
16
+
17
+ mod.module_eval <<-EOS
18
+ def self.included(base)
19
+ base.module_eval do
20
+ #{aliasings}
21
+ end
22
+ end
23
+
24
+ def self.extended(base)
25
+ included((class << base; self; end))
26
+ end
27
+ EOS
28
+ end
29
+
30
+ module Kernel
31
+ def system_with_command_test(*args, &block)
32
+ CommandTest.record_command(*args)
33
+ system_without_command_test(*args, &block)
34
+ end
35
+
36
+ def backtick_with_command_test(*args, &block)
37
+ (command = args.first) and
38
+ CommandTest.record_interpreted_command(command)
39
+ backtick_without_command_test(*args, &block)
40
+ end
41
+
42
+ def open_with_command_test(*args, &block)
43
+ (command = args.first) && command =~ /\A\|/ and
44
+ CommandTest.record_interpreted_command($')
45
+ open_without_command_test(*args, &block)
46
+ end
47
+ end
48
+
49
+ define_included_hook(Kernel, :system, :open, :'`' => :backtick)
50
+ ::Kernel.send :include, Kernel
51
+ ::Kernel.send :extend, Kernel
52
+
53
+ module IO
54
+ def popen_with_command_test(*args, &block)
55
+ command = args.first and
56
+ CommandTest.record_interpreted_command(command)
57
+ popen_without_command_test(*args, &block)
58
+ end
59
+ end
60
+
61
+ define_included_hook(IO, :popen)
62
+ ::IO.send :extend, IO
63
+
64
+ module Open3
65
+ def popen3_with_command_test(*args, &block)
66
+ command = args.first and
67
+ CommandTest.record_command(*args)
68
+ popen3_without_command_test(*args, &block)
69
+ end
70
+ end
71
+
72
+ define_included_hook(Open3, :popen3)
73
+ ::Open3.send :include, Open3
74
+ ::Open3.send :extend, Open3
75
+ end
76
+ end
@@ -0,0 +1,43 @@
1
+ module CommandTest
2
+ class Matcher
3
+ def match?(expected, actual, e=0, a=0)
4
+ while e < expected.length
5
+ case (specifier = expected[e])
6
+ when nil
7
+ return false # passed end of actual
8
+ when String, Regexp
9
+ if specifier === actual[a]
10
+ a += 1
11
+ else
12
+ return false
13
+ end
14
+ when Integer
15
+ specifier >= 0 or
16
+ raise ArgumentError, "negative integer matcher: #{specifier}"
17
+ a += specifier
18
+ when Range, :*, :+
19
+ case specifier
20
+ when :*
21
+ specifier = 0...(actual.length - a)
22
+ when :+
23
+ specifier = 1...(actual.length - a)
24
+ end
25
+ specifier.end >= specifier.begin or
26
+ raise ArgumentError, "descending range matcher: #{specifier}"
27
+ specifier.begin >= 0 or
28
+ raise ArgumentError, "negative range bounds: #{specifier}"
29
+ specifier.each do |n|
30
+ if match?(expected, actual, e + 1, a + n)
31
+ return true
32
+ end
33
+ end
34
+ return false
35
+ else
36
+ raise ArgumentError, "invalid matcher: #{specifier}"
37
+ end
38
+ e += 1
39
+ end
40
+ a == actual.length
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,62 @@
1
+ module CommandTest
2
+ class Parser
3
+ #
4
+ # Parse the given command line into words.
5
+ #
6
+ # Supports backslash escaping, single quoting, double quoting, and
7
+ # redirects, a la bash.
8
+ #
9
+ def parse(line)
10
+ # Implementation heavily inspired by Ruby's Shellwords.
11
+ line = line.lstrip
12
+ words = []
13
+ while (word = extract_word_skipping_redirects(line))
14
+ words << word
15
+ end
16
+ words
17
+ end
18
+
19
+ def extract_word_skipping_redirects(line)
20
+ while true
21
+ if line.sub!(/\A\d*>>?(&\d+)?\s*/, '')
22
+ if $1.nil?
23
+ extract_word(line) or
24
+ raise ArgumentError, "missing redirection target: #{line}"
25
+ end
26
+ next
27
+ elsif line.sub!(/\A\d*<\s*/, '')
28
+ extract_word(line) or
29
+ raise ArgumentError, "missing redirection source: #{line}"
30
+ next
31
+ end
32
+
33
+ return extract_word(line)
34
+ end
35
+ end
36
+
37
+ def extract_word(line)
38
+ return nil if line.empty?
39
+ word = ''
40
+ loop do
41
+ if line.sub!(/\A"(([^"\\]|\\.)*)"/, '')
42
+ chunk = $1.gsub(/\\(.)/, '\1')
43
+ elsif line =~ /\A"/
44
+ raise ArgumentError, "unmatched double quote: #{line}"
45
+ elsif line.sub!(/\A'([^']*)'/, '')
46
+ chunk = $1
47
+ elsif line =~ /\A'/
48
+ raise ArgumentError, "unmatched single quote: #{line}"
49
+ elsif line.sub!(/\A\\(.)?/, '')
50
+ chunk = $1 || ''
51
+ elsif line.sub!(/\A([^\s\\'"<>]+)/, '')
52
+ chunk = $1
53
+ else
54
+ line.lstrip!
55
+ break
56
+ end
57
+ word << chunk
58
+ end
59
+ word
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,39 @@
1
+ module CommandTest
2
+ module Tests
3
+ class RunsCommand
4
+ def initialize(expected, &proc)
5
+ @expected = expected
6
+ @proc = proc
7
+ end
8
+
9
+ def matches?
10
+ @actual = CommandTest.record(&@proc)
11
+ @actual.any? do |actual|
12
+ CommandTest.match?(@expected, actual)
13
+ end
14
+ end
15
+
16
+ def positive_failure_message
17
+ expected_string = display_commands([@expected])
18
+ actual_string = display_commands(@actual)
19
+ "This command should have been run, but was not:\n#{expected_string}\n" <<
20
+ "These were the commands run:\n#{actual_string}\n"
21
+ end
22
+
23
+ def negative_failure_message
24
+ expected_string = display_commands([@expected])
25
+ actual_string = display_commands(@actual)
26
+ "This command should not have been run, but was:\n#{expected_string}\n" <<
27
+ "These were the commands run:\n#{actual_string}\n"
28
+ end
29
+
30
+ private
31
+
32
+ def display_commands(commands)
33
+ commands.map do |command|
34
+ command.map{|arg| arg.inspect}.join(' ')
35
+ end.join("\n").gsub(/^/, ' ')
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,11 @@
1
+ module CommandTest
2
+ VERSION = [0, 0, 1]
3
+
4
+ class << VERSION
5
+ include Comparable
6
+
7
+ def to_s
8
+ join('.')
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,5 @@
1
+ require 'command_test'
2
+
3
+ ROOT = File.dirname(File.dirname(__FILE__))
4
+
5
+ require 'support/temporary_directory'
@@ -0,0 +1,48 @@
1
+ require 'fileutils'
2
+
3
+ module TemporaryDirectory
4
+ def create_temporary_directory
5
+ remove_temporary_directory
6
+ FileUtils.mkdir_p temporary_directory
7
+ end
8
+
9
+ def remove_temporary_directory
10
+ FileUtils.rm_rf temporary_directory
11
+ end
12
+
13
+ #
14
+ # Return the given path relative to the temporary directory.
15
+ #
16
+ def temporary_path(path=nil)
17
+ if path
18
+ File.join(temporary_directory, path)
19
+ else
20
+ temporary_directory
21
+ end
22
+ end
23
+
24
+ #
25
+ # Write to the given path in the temporary directory.
26
+ #
27
+ # Return the full path to the file.
28
+ #
29
+ def write_temporary_file(path, content)
30
+ path = temporary_path(path)
31
+ open(path, 'w') do |file|
32
+ file.print content
33
+ end
34
+ path
35
+ end
36
+
37
+ private # ---------------------------------------------------------
38
+
39
+ def temporary_directory
40
+ "#{ROOT}/spec/tmp"
41
+ end
42
+ end
43
+
44
+ Spec::Runner.configure do |config|
45
+ config.before { create_temporary_directory }
46
+ config.after(:all) { remove_temporary_directory }
47
+ config.include TemporaryDirectory
48
+ end
@@ -0,0 +1,82 @@
1
+ require 'spec_helper'
2
+
3
+ describe CommandTest::CoreExtensions do
4
+ it "should not affect system" do
5
+ system 'true'
6
+ $?.success?.should be_true
7
+ system 'false'
8
+ $?.success?.should be_false
9
+ end
10
+
11
+ it "should not affect Kernel.system" do
12
+ Kernel.system 'true'
13
+ $?.success?.should be_true
14
+ Kernel.system 'false'
15
+ $?.success?.should be_false
16
+ end
17
+
18
+ it "should not affect `" do
19
+ `true`
20
+ $?.success?.should be_true
21
+ `false`
22
+ $?.success?.should be_false
23
+ end
24
+
25
+ it "should not affect Kernel.`" do
26
+ Kernel.send('`', 'true')
27
+ $?.success?.should be_true
28
+ Kernel.send('`', 'false')
29
+ $?.success?.should be_false
30
+ end
31
+
32
+ it "should not affect open with a pipe" do
33
+ open('|true'){}
34
+ $?.success?.should be_true
35
+ open('|false'){}
36
+ $?.success?.should be_false
37
+ end
38
+
39
+ it "should not affect open without a pipe" do
40
+ path = write_temporary_file('file', 'content')
41
+ content = nil
42
+ open(path){|f| content = f.read}
43
+ content.should == 'content'
44
+ end
45
+
46
+ it "should not affect Kernel.open with a pipe" do
47
+ Kernel.open('|true'){}
48
+ $?.success?.should be_true
49
+ Kernel.open('|false'){}
50
+ $?.success?.should be_false
51
+ end
52
+
53
+ it "should not affect Kernel.open without a pipe" do
54
+ path = write_temporary_file('file', 'content')
55
+ content = nil
56
+ Kernel.open(path){|f| content = f.read}
57
+ content.should == 'content'
58
+ end
59
+
60
+ it "should not affect IO.popen" do
61
+ IO.popen('true'){}
62
+ $?.success?.should be_true
63
+ IO.popen('false'){}
64
+ $?.success?.should be_false
65
+ end
66
+
67
+ it "should not affect popen3" do
68
+ out = nil
69
+ Class.new{include Open3}.new.instance_eval do
70
+ popen3('echo a'){|stdin, stdout, stderr| out = stdout.read}
71
+ end
72
+ out.should == "a\n"
73
+ end
74
+
75
+ it "should not affect Open3.popen3" do
76
+ out = nil
77
+ Class.new{include Open3}.new.instance_eval do
78
+ Open3.popen3('echo a'){|stdin, stdout, stderr| out = stdout.read}
79
+ end
80
+ out.should == "a\n"
81
+ end
82
+ end
@@ -0,0 +1,139 @@
1
+ require 'spec_helper'
2
+
3
+ describe CommandTest::Matcher do
4
+ before do
5
+ @matcher = CommandTest::Matcher.new
6
+ end
7
+
8
+ describe "#match?" do
9
+ describe "when only Strings are in expected" do
10
+ it "should be true if expected is equal to actual" do
11
+ @matcher.match?(['a', 'b'], ['a', 'b']).should be_true
12
+ end
13
+
14
+ it "should be false if any element of expected is not the same as actual" do
15
+ @matcher.match?(['a', 'b'], ['a', 'x']).should be_false
16
+ end
17
+ end
18
+
19
+ describe "when a Regexp is present" do
20
+ it "should match the corresponding element in actual" do
21
+ @matcher.match?(['a', /\A.\z/], ['a', 'b']).should be_true
22
+ end
23
+
24
+ it "should return false if the Regexp does not match" do
25
+ @matcher.match?(['a', /\A..\z/], ['a', 'x']).should be_false
26
+ end
27
+ end
28
+
29
+ describe "when an Integer is present" do
30
+ it "should match n elements at that position in actual" do
31
+ @matcher.match?(['a', 2, 'z'], ['a', 'b', 'c', 'z']).should be_true
32
+ end
33
+
34
+ it "should support matching 0 elements" do
35
+ @matcher.match?(['a', 0, 'z'], ['a', 'z']).should be_true
36
+ end
37
+
38
+ it "should return false if there are not enough elements in actual" do
39
+ @matcher.match?(['a', 2, 'z'], ['a', 'z']).should be_false
40
+ end
41
+
42
+ it "should return false if there are too many elements in actual" do
43
+ @matcher.match?(['a', 2, 'z'], ['a', 'b', 'c', 'd', 'z']).should be_false
44
+ end
45
+
46
+ it "should raise ArgumentError if the integer is negative" do
47
+ lambda{@matcher.match?(['a', -1, 'b'], ['a', 'b'])}.should raise_error(ArgumentError, /negative/)
48
+ end
49
+ end
50
+
51
+ describe "when a Range, a..b, is present" do
52
+ it "should match a elements in the range at that position in actual" do
53
+ @matcher.match?(['a', 2..4, 'z'], ['a', 'b', 'c', 'z']).should be_true
54
+ end
55
+
56
+ it "should match b elements in the range at that position in actual" do
57
+ @matcher.match?(['a', 2..4, 'z'], ['a', 'b', 'c', 'd', 'e', 'z']).should be_true
58
+ end
59
+
60
+ it "should i elements, for in in a..b" do
61
+ @matcher.match?(['a', 2..4, 'z'], ['a', 'b', 'c', 'd', 'z']).should be_true
62
+ end
63
+
64
+ it "should return false if there are not enough elements in actual" do
65
+ @matcher.match?(['a', 2...4, 'z'], ['a', 'b', 'z']).should be_false
66
+ end
67
+
68
+ it "should return false if there are too many elements in actual" do
69
+ @matcher.match?(['a', 2...4, 'z'], ['a', 'b', 'c', 'd', 'e', 'f', 'z']).should be_false
70
+ end
71
+
72
+ it "should honor open-endedness of the range" do
73
+ @matcher.match?(['a', 2...4, 'z'], ['a', 'b', 'c', 'd', 'e', 'z']).should be_false
74
+ end
75
+
76
+ it "should raise ArgumentError if a is negative" do
77
+ lambda{@matcher.match?(['a', -2..4, 'b'], ['a', 'b'])}.should raise_error(ArgumentError, /negative/)
78
+ end
79
+
80
+ it "should raise ArgumentError if b is negative" do
81
+ lambda{@matcher.match?(['a', -4..-2, 'b'], ['a', 'b'])}.should raise_error(ArgumentError, /negative/)
82
+ end
83
+
84
+ it "should raise ArgumentError if the range is descending" do
85
+ lambda{@matcher.match?(['a', 4..2, 'b'], ['a', 'b'])}.should raise_error(ArgumentError, /descending/)
86
+ end
87
+
88
+ it "should return false if the range is empty" do
89
+ @matcher.match?(['a', 2...2, 'z'], ['a', 'z']).should be_false
90
+ end
91
+
92
+ it "should backtrack to find a match" do
93
+ @matcher.match?(['a', 1..3, 'm', 1..3, 'z'], ['a', 'b', 'c', 'd', 'm', 'n', 'o', 'p', 'z']).should be_true
94
+ end
95
+ end
96
+
97
+ describe "when :* is present" do
98
+ it "should match zero elements at that position in actual" do
99
+ @matcher.match?(['a', :*, 'z'], ['a', 'z']).should be_true
100
+ end
101
+
102
+ it "should match one element at that position in actual" do
103
+ @matcher.match?(['a', :*, 'z'], ['a', 'b', 'z']).should be_true
104
+ end
105
+
106
+ it "should match two elements at that position in actual" do
107
+ @matcher.match?(['a', :*, 'z'], ['a', 'b', 'c', 'z']).should be_true
108
+ end
109
+
110
+ it "should backtrack to find a match" do
111
+ @matcher.match?(['a', :*, 'm', :*, 'z'], ['a', 'b', 'c', 'd', 'm', 'n', 'o', 'p', 'z']).should be_true
112
+ end
113
+ end
114
+
115
+ describe "when :+ is present" do
116
+ it "should not match zero elements at that position in actual" do
117
+ @matcher.match?(['a', :+, 'z'], ['a', 'z']).should be_false
118
+ end
119
+
120
+ it "should match one element at that position in actual" do
121
+ @matcher.match?(['a', :+, 'z'], ['a', 'b', 'z']).should be_true
122
+ end
123
+
124
+ it "should match two elements at that position in actual" do
125
+ @matcher.match?(['a', :+, 'z'], ['a', 'b', 'c', 'z']).should be_true
126
+ end
127
+
128
+ it "should backtrack to find a match" do
129
+ @matcher.match?(['a', :+, 'm', :+, 'z'], ['a', 'b', 'c', 'd', 'e', 'm', 'n', 'o', 'p', 'q', 'z']).should be_true
130
+ end
131
+ end
132
+
133
+ describe "when junk is present" do
134
+ it "should raise an argument error" do
135
+ lambda{@matcher.match?(['a', Object.new, 'b'], ['a', 'b'])}.should raise_error(ArgumentError, /invalid/)
136
+ end
137
+ end
138
+ end
139
+ end