command_test 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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