polyamory 0.0.5 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -1,35 +1,43 @@
1
- # Polyamory
1
+ # Polyamory – the promiscuous test runner
2
2
 
3
- Polyamory loves *all* of your testing frameworks. It is a command-line tool that is able to run your test files regardless of the framework being used.
3
+ Polyamory is a command-line tool that knows how to run your tests regardless of
4
+ the framework. It can either run the whole test suite or filter by keywords,
5
+ test case names, or tags. It remembers the differences between arguments for
6
+ different testing frameworks so you don't have to.
7
+
8
+ Frameworks supported:
9
+
10
+ * Cucumber in `features/**/*.feature`
11
+ * RSpec + Shoulda in `spec/**/*_spec.rb`
12
+ * test/unit, Shoulda, or anything else in `test/**/*_test.rb` or `test/**/test*.rb`
4
13
 
5
14
  Features:
6
15
 
7
- * easily run the full test suite for any project: just type `polyamory`
8
- * use a directory name on the command line to run everything inside that directory
9
- * use a keyword to run all test files which contain that word
10
- * Bundler support
16
+ * `polyamory` - Runs the full test suite for any project. For example, it will
17
+ run all of the following:
11
18
 
12
- Frameworks supported:
19
+ rspec spec
20
+ cucumber features
21
+ ruby -e 'ARGV.each {|f| require f }' test/**/*_test.rb
22
+
23
+ * `polyamory <dirname>` - Runs all tests inside a subdirectory. For example:
24
+
25
+ polyamory models
26
+ -> runs test/models/**/*_test.rb
27
+ -> runs spec/models/**/*_spec.rb
28
+
29
+ * `polyamory <keyword>` - Runs all test files that match a keyword. For example:
30
+
31
+ polyamory search
32
+ -> runs test/models/user_search_test.rb
33
+ -> runs spec/controllers/search_controller_spec.rb
34
+ -> runs features/site_search.feature
35
+
36
+ * `polyamory <file>:<line>` - Runs focused test. Provides this feature for
37
+ test/unit and minitest which don't support it.
38
+
39
+ * `polyamory -n <pattern>` - Runs only tests whose names match given patterns.
13
40
 
14
- * Cucumber in `"features/**/*.feature"`
15
- * RSpec + Shoulda in `"spec/**/*_spec.rb"`
16
- * test/unit, Shoulda, or anything else in `"test/**/*_test.rb"`
17
-
18
- ## Examples
19
-
20
- Here, `polyamory` is aliased as `pam` for brevity.
21
-
22
- # run everything
23
- $ pam
24
- > rspec spec &&
25
- cucumber -f progress -t ~@wip features &&
26
- polyamory -t test
27
-
28
- # everyting inside a single directory
29
- $ pam test/unit
30
- > polyamory -t test/unit
31
-
32
- # run test files matching keyword
33
- $ pam user
34
- > polyamory -t spec/models/user_spec.rb spec/controllers/user_controller.rb &&
35
- cucumber -f progress -t ~@wip features/user_registration.feature
41
+ * `polyamory -t <tag>` - Runs RSpec/Cucumber tests that match given tags.
42
+ Tag exclusion is done with `~<tag>`. Tag names are normalized for Cucumber
43
+ (which expects them in form of `@<tag>`).
@@ -1,24 +1,31 @@
1
1
  #!/usr/bin/env ruby
2
+ require 'polyamory'
3
+
2
4
  if ARGV.delete '-t'
3
- test_files = ARGV.map { |arg|
4
- unless arg.index('-') == 0
5
- if File.directory? arg
6
- Dir["#{arg}/**/*_test.rb", "#{arg}/**/test_*.rb"]
7
- else
8
- arg
9
- end
5
+ root = Pathname.pwd
6
+
7
+ if idx = ARGV.index('--')
8
+ names = ARGV[0...idx]
9
+ ARGV.slice! 0..idx
10
+ else
11
+ names = ARGV.dup
12
+ ARGV.clear
13
+ end
14
+
15
+ test_files = names.map { |arg|
16
+ if File.directory? arg
17
+ locator = Polyamory.new(names, root)
18
+ locator.find_test_files(root + arg)
19
+ else
20
+ arg
10
21
  end
11
22
  }.compact.flatten
12
-
23
+
13
24
  if test_files.empty?
14
- warn "polyamory: nothing to load"
15
- exit 1
25
+ abort "polyamory: nothing to load"
16
26
  else
17
- test_files.each { |f| load f }
27
+ test_files.each { |f| require root + f }
18
28
  end
19
29
  else
20
- require 'polyamory'
21
- options = {}
22
- options[:noop] = !!ARGV.delete('-n')
23
- Polyamory.run ARGV, Dir.pwd, options
30
+ Polyamory.run ARGV, Dir.pwd
24
31
  end
@@ -1,217 +1,82 @@
1
- require 'pathname'
1
+ require 'optparse'
2
+ require 'polyamory/runner'
2
3
 
3
- class Polyamory
4
- def self.run(*args)
5
- new(*args).run
6
- end
7
-
8
- def initialize(names, root, options = {})
9
- @names = names
10
- @root = Pathname.new(root).expand_path
11
- @options = options
12
- end
13
-
14
- def noop?
15
- @options[:noop]
16
- end
17
-
18
- def file_exists?(path)
19
- (@root + path).exist?
20
- end
21
-
22
- def bundler?
23
- file_exists? 'Gemfile'
24
- end
25
-
26
- def test_dir
27
- @root + 'test'
28
- end
29
-
30
- def test_glob(dir = test_dir)
31
- ["#{dir}/**/*_test.rb", "#{dir}/**/test_*.rb"]
32
- end
33
-
34
- def spec_dir
35
- @root + 'spec'
36
- end
37
-
38
- def spec_glob(dir = spec_dir)
39
- "#{dir}/**/*_spec.rb"
40
- end
41
-
42
- def features_dir
43
- @root + 'features'
44
- end
45
-
46
- def features_glob(dir = features_dir)
47
- "#{dir}/**/*.feature"
4
+ module Polyamory
5
+ VERSION = '0.6.0'
6
+
7
+ def self.run(args, dir)
8
+ options = parse_options! args
9
+ Runner.new(args, dir, options).run
48
10
  end
49
-
50
- class Pathname < Pathname
51
- attr_reader :root
52
-
53
- def self.glob(patterns, root)
54
- patterns = Array(patterns)
55
- Dir[*patterns].map do |path|
56
- self.new(path, root)
11
+
12
+ def self.parse_options!(args)
13
+ options = {
14
+ :warnings => false,
15
+ :verbose => false,
16
+ :backtrace => nil,
17
+ :test_seed => nil,
18
+ :bundler => nil,
19
+ :name_filters => [],
20
+ :tag_filters => [],
21
+ :load_paths => [],
22
+ }
23
+
24
+ OptionParser.new do |opts|
25
+ opts.banner = 'Usage: polyamory [<dirname>] [<file>[:<line>]] [-n <pattern>] [-t <tag>]'
26
+ opts.version = VERSION
27
+
28
+ opts.summary_indent = " " * 4
29
+
30
+ opts.separator "\n Ruby options:"
31
+
32
+ opts.on '-w', "Turn on Ruby warnings" do
33
+ options[:warnings] = true
57
34
  end
58
- end
59
-
60
- def initialize(path, root_path = nil)
61
- super(path)
62
- self.root = root_path
63
- end
64
-
65
- def root=(path)
66
- @relativized = nil
67
- @root = path
68
- end
69
-
70
- def relative
71
- @relativized ||= relative_path_from root
72
- end
73
-
74
- def =~(pattern)
75
- relative.to_s =~ pattern
76
- end
77
-
78
- def +(other)
79
- result = self.class.new(plus(@path, other.to_s))
80
- result.root ||= self
81
- result
82
- end
83
- end
84
-
85
- def find_files
86
- all_paths = Pathname.glob([test_glob, spec_glob, features_glob].flatten, @root)
87
-
88
- if @names.any?
89
- @names.map { |name|
90
- path = @root + name
91
- pattern = /\b#{Regexp.escape name}(\b|_)/
92
-
93
- if path.directory? or not path.extname.empty?
94
- path
95
- else
96
- all_paths.select { |p| p =~ pattern }
97
- end
98
- }.flatten
99
- else
100
- [test_dir, spec_dir, features_dir].select { |p| p.directory? }
101
- end
102
- end
103
-
104
- def relativize(paths)
105
- Array(paths).map { |p| p.relative }
106
- end
107
-
108
- def run
109
- paths = relativize(find_files)
110
- if paths.empty?
111
- warn "nothing found to run"
112
- exit 1
113
- end
114
-
115
- jobs = index_by_path_prefix(paths).map do |prefix, files|
116
- [runner_for_prefix(prefix), *files].flatten
117
- end
118
-
119
- prepare_env
120
- execute_jobs jobs
121
- end
122
-
123
- def runner_for_prefix(prefix)
124
- case prefix
125
- when 'features' then %w[cucumber -f progress -t ~@wip]
126
- when 'spec' then detect_rspec_version
127
- when 'test' then %w[polyamory -t]
128
- else
129
- raise ArgumentError, "don't know a runner for #{prefix}"
130
- end
131
- end
132
-
133
- def detect_rspec_version
134
- helper = 'spec/spec_helper.rb'
135
-
136
- if file_exists? 'spec/spec.opts' or file_exists? 'lib/tasks/rspec.rake'
137
- 'spec'
138
- elsif file_exists? '.rspec'
139
- 'rspec'
140
- elsif file_exists? helper
141
- File.open(helper) do |file|
142
- while file.gets
143
- return $&.downcase if $_ =~ /\bR?Spec\b/
144
- end
35
+
36
+ opts.on '-I PATH', "Directory to load on $LOAD_PATH" do |str|
37
+ options[:load_paths] << str
145
38
  end
146
- 'rspec'
147
- else
148
- 'rspec'
149
- end
150
- end
151
-
152
- def index_by_path_prefix(paths)
153
- paths.inject(Hash.new {|h,k| h[k] = [] }) do |index, path|
154
- prefix = path.to_s.split('/', 2).first
155
- index[prefix] << path
156
- index
157
- end
158
- end
159
-
160
- def execute_jobs(jobs)
161
- if jobs.size > 1
162
- jobs.each { |j| cmd(j, true) }
163
- else
164
- cmd(jobs.first)
165
- end
166
- end
167
-
168
- def prepare_cmdline(args)
169
- args = args.map { |p| p.to_s }
170
- args = %w[bundle exec] + args if bundler?
171
- args
172
- end
173
-
174
- def cmd(args, many = false)
175
- args = prepare_cmdline(args)
176
- puts args.join(' ')
177
-
178
- unless noop?
179
- # TODO: hack; make this configurable, use bundler
180
- with_rubyopt(!bundler? ? '-rubygems' : nil) do
181
- with_rubylib('lib', args.include?('polyamory') ? 'test' : nil) do
182
- if many
183
- system(*args)
184
- exit $?.exitstatus unless $?.success?
185
- else
186
- exec(*args)
187
- end
188
- end
39
+
40
+ opts.separator "\n Test options:"
41
+
42
+ opts.on '-s', '--seed SEED', Integer, "Set random seed" do |m|
43
+ options[:test_seed] = m.to_i
44
+ end
45
+
46
+ opts.on '-b', '--[no-]backtrace', "Show full backtrace" do |set|
47
+ options[:backtrace] = set
48
+ end
49
+
50
+ opts.on '-n', '--name PATTERN', "Filter test names on pattern" do |str|
51
+ options[:name_filters] << str
52
+ end
53
+
54
+ opts.on '-t', '--tag TAG', "Filter tests on tag" do |str|
55
+ options[:tag_filters] << str
56
+ end
57
+
58
+ opts.on '-v', '--verbose', "Show progress processing files" do
59
+ options[:verbose] = true
60
+ end
61
+
62
+ opts.on '--[no-]bundler', "Use `bundle exec' for running tests" do |set|
63
+ options[:bundler] = set
64
+ end
65
+
66
+ opts.separator "\n Other options:"
67
+
68
+ opts.on_tail '-h', '--help', 'Display this help' do
69
+ puts opts
70
+ exit
71
+ end
72
+
73
+ begin
74
+ opts.parse! args
75
+ rescue OptionParser::InvalidOption
76
+ abort opts.banner
189
77
  end
190
78
  end
79
+
80
+ options
191
81
  end
192
-
193
- def with_rubyopt(value)
194
- with_env('RUBYOPT', "#{value} %s") { yield }
195
- end
196
-
197
- def with_rubylib(*values)
198
- value = values.flatten.compact.join(':')
199
- with_env('RUBYLIB', "#{value}:%s") { yield }
200
- end
201
-
202
- def with_env(key, value)
203
- old_value = ENV[key]
204
- ENV[key] = value % old_value
205
-
206
- begin
207
- yield
208
- ensure
209
- ENV[key] = old_value
210
- end
211
- end
212
-
213
- def prepare_env
214
- # TODO: make this per-job
215
- ENV['RUBYOPT'] = ENV['RUBYOPT'].gsub(/(^| )-w( |$)/, '\1\2') if ENV['RUBYOPT']
216
- end
217
- end
82
+ end
@@ -0,0 +1,25 @@
1
+ require 'forwardable'
2
+
3
+ module Polyamory
4
+ # Internal: Represents a single command to run.
5
+ class Command
6
+ attr_reader :env
7
+
8
+ def initialize cmd
9
+ @args = Array(cmd)
10
+ @env = Hash.new
11
+ yield self if block_given?
12
+ end
13
+
14
+ extend Forwardable
15
+ def_delegators :@args, :<<, :concat
16
+
17
+ def to_exec
18
+ @args.map {|a| a.to_s }
19
+ end
20
+
21
+ def to_s
22
+ to_exec.join(' ')
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,50 @@
1
+ require 'polyamory/rspec'
2
+ require 'polyamory/command'
3
+
4
+ module Polyamory
5
+ # Internal: Deals with finding specs to test
6
+ class Cucumber < RSpec
7
+ def test_dir_name
8
+ 'features'
9
+ end
10
+
11
+ def file_pattern dir
12
+ "#{dir}/**/*.feature"
13
+ end
14
+
15
+ def test_command paths
16
+ Command.new 'cucumber' do |test_job|
17
+ add_ruby_options test_job
18
+ test_job.concat cucumber_options
19
+ test_job.concat paths.map {|p| p.relative }
20
+ end
21
+ end
22
+
23
+ def add_ruby_options cmd
24
+ opts = []
25
+ opts << '-w' if context.warnings?
26
+ for path in context.load_paths
27
+ opts << "-I#{path}"
28
+ end
29
+ opts << '%'
30
+ cmd.env['RUBYOPT'] = opts.join(' ')
31
+ end
32
+
33
+ def cucumber_options
34
+ opts = []
35
+ opts << '-b' if context.full_backtrace?
36
+ for filter in context.name_filters
37
+ opts << '-n' << filter
38
+ end
39
+ for tag in context.tag_filters
40
+ opts << '-t' << normalize_tag(tag)
41
+ end
42
+ opts
43
+ end
44
+
45
+ def normalize_tag tag
46
+ tag = "#$1@#$2" if tag =~ /^(~)?(\w+)$/
47
+ tag
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,51 @@
1
+ require 'pathname'
2
+
3
+ module Polyamory
4
+ # A kind of Pathname that keeps a reference to a root directory and is able to
5
+ # return a relativized pathname from that particular root.
6
+ class RootedPathname < ::Pathname
7
+ attr_reader :root
8
+
9
+ # Find pathnames matching the glob pattern and assign to them a root
10
+ def self.glob(patterns, root)
11
+ patterns = Array(patterns)
12
+ Dir[*patterns].map do |path|
13
+ self.new(path, root)
14
+ end
15
+ end
16
+
17
+ def initialize(path, root_path = nil)
18
+ super(path)
19
+ self.root = root_path
20
+ end
21
+
22
+ def root=(path)
23
+ @relativized = nil
24
+ @root = path
25
+ end
26
+
27
+ # Return the relative portion of the path from root
28
+ def relative
29
+ return self if relative?
30
+ @relativized ||= relative_path_from root
31
+ end
32
+
33
+ # Check if current path is contained in directory
34
+ def in_dir? dir
35
+ self == dir or
36
+ self.to_s.index(File.join(dir, '')) == 0
37
+ end
38
+
39
+ # Perform a regex match only on the relative portion of the path
40
+ def =~(pattern)
41
+ relative.to_s =~ pattern
42
+ end
43
+
44
+ # Add to the current path; the result has the current path as root
45
+ def +(other)
46
+ result = self.class.new(plus(@path, other.to_s))
47
+ result.root ||= self
48
+ result
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,137 @@
1
+ require 'polyamory/rooted_pathname'
2
+ require 'polyamory/command'
3
+
4
+ module Polyamory
5
+ # Internal: Deals with finding specs to test
6
+ class RSpec
7
+ attr_reader :context
8
+
9
+ def initialize context
10
+ @context = context
11
+ end
12
+
13
+ def test_dir_name
14
+ 'spec'
15
+ end
16
+
17
+ def test_dir
18
+ @test_dir ||= context.root + test_dir_name
19
+ end
20
+
21
+ def file_pattern dir
22
+ "#{dir}/**/*_spec.rb"
23
+ end
24
+
25
+ # Internal: Finds test files matching glob pattern
26
+ def find_files dir = test_dir
27
+ glob file_pattern(dir)
28
+ end
29
+
30
+ # Internal: Memoized find_files in primary dir
31
+ def all_matching_files
32
+ @all_matching_files ||= find_files
33
+ end
34
+
35
+ # Public: Resolve a set of files, directories, and patterns to a list of
36
+ # paths to test.
37
+ def resolve_paths names
38
+ if names.any?
39
+ paths = []
40
+ for name in names
41
+ paths.concat Array(resolve_name name)
42
+ end
43
+ paths
44
+ else
45
+ Array(resolve_as_directory test_dir_name)
46
+ end
47
+ end
48
+
49
+ def handle? path
50
+ path.in_dir? test_dir
51
+ end
52
+
53
+ def resolve_name name
54
+ resolve_as_directory(name) or
55
+ resolve_as_filename(name) or
56
+ resolve_as_file_pattern(name) or
57
+ raise "nothing resolved from #{name}"
58
+ end
59
+
60
+ # "functional" => "test/functional/**"
61
+ # "test/functional" => "test/functional/**"
62
+ def resolve_as_directory name
63
+ [test_dir + name, context.root + name].detect { |dir|
64
+ dir.directory? and handle? dir
65
+ }
66
+ end
67
+
68
+ # "test/unit/test_user.rb:42"
69
+ def resolve_as_filename name
70
+ filename = name.sub(/:(\d+)$/, '')
71
+ file = context.root + filename
72
+
73
+ if file.file? and handle? file
74
+ context.root + name
75
+ end
76
+ end
77
+
78
+ # "word" => "test/**" that match "word"
79
+ def resolve_as_file_pattern name
80
+ pattern = /(?:\b|_)#{Regexp.escape name}(?:\b|_)/
81
+ all_matching_files.select {|p| p =~ pattern }
82
+ end
83
+
84
+ # Public: From a list of paths, yank the ones that this knows how to handle,
85
+ # and build test jobs from it.
86
+ def pick_jobs paths
87
+ to_test = []
88
+ paths.reject! do |path|
89
+ if handle? path
90
+ to_test << path
91
+ true
92
+ end
93
+ end
94
+
95
+ unless to_test.empty?
96
+ [test_command(to_test)]
97
+ else
98
+ []
99
+ end
100
+ end
101
+
102
+ def test_command paths
103
+ Command.new 'rspec' do |test_job|
104
+ add_ruby_options test_job
105
+ test_job.concat rspec_options
106
+ test_job.concat paths.map {|p| p.relative }
107
+ end
108
+ end
109
+
110
+ def glob pattern
111
+ RootedPathname.glob pattern, context.root
112
+ end
113
+
114
+ def add_ruby_options cmd
115
+ opts = []
116
+ opts << '-w' if context.warnings?
117
+ opts << '%'
118
+ cmd.env['RUBYOPT'] = opts.join(' ')
119
+ end
120
+
121
+ def rspec_options
122
+ opts = []
123
+ opts << '-b' if context.full_backtrace?
124
+ opts << '--seed' << context.test_seed if context.test_seed
125
+ for path in context.load_paths
126
+ opts << "-I#{path}"
127
+ end
128
+ for filter in context.name_filters
129
+ opts << '-e' << filter
130
+ end
131
+ for tag in context.tag_filters
132
+ opts << '-t' << tag
133
+ end
134
+ opts
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,135 @@
1
+ require 'polyamory/rooted_pathname'
2
+ require 'polyamory/test_unit'
3
+ require 'polyamory/rspec'
4
+ require 'polyamory/cucumber'
5
+
6
+ module Polyamory
7
+ # Public: Collects test jobs in the root directory and runs them.
8
+ class Runner
9
+ attr_reader :root, :options
10
+
11
+ def initialize(names, root, options = {})
12
+ @names = names
13
+ @root = RootedPathname.new(root).expand_path
14
+ @options = options
15
+ end
16
+
17
+ def warnings?
18
+ options.fetch(:warnings)
19
+ end
20
+
21
+ def verbose?
22
+ options.fetch(:verbose)
23
+ end
24
+
25
+ def full_backtrace?
26
+ options.fetch(:backtrace)
27
+ end
28
+
29
+ def name_filters
30
+ options.fetch(:name_filters)
31
+ end
32
+
33
+ def tag_filters
34
+ options.fetch(:tag_filters)
35
+ end
36
+
37
+ def load_paths
38
+ options.fetch(:load_paths)
39
+ end
40
+
41
+ def test_seed
42
+ options.fetch(:test_seed)
43
+ end
44
+
45
+ def bundle_exec?
46
+ return @bundle_exec if defined? @bundle_exec
47
+ if (setting = options.fetch(:bundler)).nil?
48
+ setting = !ENV['BUNDLE_GEMFILE'].to_s.empty? ||
49
+ (root + 'Gemfile').exist?
50
+ end
51
+ @bundle_exec = setting
52
+ end
53
+
54
+ BundlerJob = Struct.new(:job) do
55
+ def env() job.env end
56
+ def to_exec() ['bundle', 'exec', *job.to_exec] end
57
+ def to_s() "bundle exec #{job.to_s}" end
58
+ end
59
+
60
+ def run
61
+ jobs = collect_jobs
62
+
63
+ unless jobs.empty?
64
+ for job in jobs
65
+ job = BundlerJob.new(job) if bundle_exec?
66
+ exec_job job
67
+ end
68
+ else
69
+ abort "nothing to run."
70
+ end
71
+ end
72
+
73
+ def collect_jobs
74
+ [TestUnit, RSpec, Cucumber].inject([]) do |jobs, klass|
75
+ framework = klass.new self
76
+ paths = framework.resolve_paths @names
77
+ jobs.concat framework.pick_jobs(paths)
78
+ end
79
+ end
80
+
81
+ def exec_job job
82
+ with_env job.env do |env_keys|
83
+ display_job job, env_keys
84
+ system(*job.to_exec)
85
+ exit $?.exitstatus unless $?.success?
86
+ end
87
+ end
88
+
89
+ def display_job job, env_keys
90
+ display_env env_keys
91
+ puts job
92
+ end
93
+
94
+ def display_env env_keys
95
+ env_keys.each do |name|
96
+ value = ENV[name].strip
97
+ next if value.empty?
98
+ value = %("#{value}") if value.index(' ')
99
+ print "#{name}=#{value} "
100
+ end
101
+ end
102
+
103
+ def rbenv_clear
104
+ rbenv_root = `rbenv root 2>/dev/null`.chomp
105
+ unless rbenv_root.empty?
106
+ re = /^#{Regexp.escape rbenv_root}\/(versions|plugins|libexec)\b/
107
+ paths = ENV["PATH"].split(":")
108
+ paths.reject! {|p| p =~ re }
109
+ update_env 'PATH' => paths.join(":")
110
+ end
111
+ end
112
+
113
+ def with_env env
114
+ rbenv_clear
115
+ saved = update_env env
116
+ begin
117
+ yield saved.keys
118
+ ensure
119
+ restore_env saved
120
+ end
121
+ end
122
+
123
+ def update_env env
124
+ env.inject({}) { |saved, (name, value)|
125
+ saved[name] = ENV[name]
126
+ ENV[name] = value.sub('%', saved[name].to_s)
127
+ saved
128
+ }
129
+ end
130
+
131
+ def restore_env env
132
+ env.each {|name, value| ENV[name] = value }
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,196 @@
1
+ require 'polyamory/rooted_pathname'
2
+ require 'polyamory/command'
3
+
4
+ module Polyamory
5
+ # Internal: Deals with finding Test::Unit or MiniTest files to test.
6
+ class TestUnit
7
+ attr_reader :context, :test_filters
8
+
9
+ def initialize context
10
+ @context = context
11
+ @test_filters = context.name_filters.dup
12
+ end
13
+
14
+ def test_dir
15
+ @test_dir ||= context.root + 'test'
16
+ end
17
+
18
+ def file_pattern dir
19
+ "#{dir}/**/*_test.rb"
20
+ end
21
+
22
+ def file_pattern_alt dir
23
+ "#{dir}/**/test*.rb"
24
+ end
25
+
26
+ # Internal: Finds test files with one glob pattern or the other. The pattern
27
+ # that matches the most files wins.
28
+ def find_files dir = test_dir
29
+ paths = glob file_pattern(dir)
30
+ paths_alt = glob file_pattern_alt(dir)
31
+ paths.size > paths_alt.size ? paths : paths_alt
32
+ end
33
+
34
+ # Internal: Memoized find_files in primary dir
35
+ def all_matching_files
36
+ @all_matching_files ||= find_files
37
+ end
38
+
39
+ # Public: Resolve a set of files, directories, and patterns to a list of
40
+ # paths to test.
41
+ def resolve_paths names
42
+ if context.tag_filters.any?
43
+ # test/unit and minitest don't support tags
44
+ []
45
+ elsif names.any?
46
+ paths = []
47
+ for name in names
48
+ paths.concat Array(resolve_name name)
49
+ end
50
+ paths
51
+ else
52
+ all_matching_files
53
+ end
54
+ end
55
+
56
+ def handle? path
57
+ path.in_dir? test_dir
58
+ end
59
+
60
+ def resolve_name name
61
+ resolve_as_directory(name) or
62
+ resolve_as_filename(name) or
63
+ resolve_as_file_pattern(name) or
64
+ raise "nothing resolved from #{name}"
65
+ end
66
+
67
+ # "functional" => "test/functional/**"
68
+ # "test/functional" => "test/functional/**"
69
+ def resolve_as_directory name
70
+ dir = [test_dir + name, context.root + name].detect { |dir|
71
+ dir.directory? and handle? dir
72
+ }
73
+ find_files(dir) if dir
74
+ end
75
+
76
+ # "test/unit/test_user.rb:42"
77
+ def resolve_as_filename name
78
+ filename = name.sub(/:(\d+)$/, '')
79
+ line_number = $1
80
+ file = context.root + filename
81
+
82
+ if file.file? and handle? file
83
+ add_test_filter_for_line(file, line_number) if line_number
84
+ file
85
+ end
86
+ end
87
+
88
+ # "word" => "test/**" that match "word"
89
+ def resolve_as_file_pattern name
90
+ pattern = /(?:\b|_)#{Regexp.escape name}(?:\b|_)/
91
+ all_matching_files.select {|p| p =~ pattern }
92
+ end
93
+
94
+ # Public: From a list of paths, yank the ones that this knows how to handle,
95
+ # and build test jobs from it.
96
+ def pick_jobs paths
97
+ to_test = []
98
+ paths.reject! do |path|
99
+ if handle? path
100
+ to_test << path
101
+ true
102
+ end
103
+ end
104
+
105
+ unless to_test.empty?
106
+ [test_command(to_test)]
107
+ else
108
+ []
109
+ end
110
+ end
111
+
112
+ def test_command paths
113
+ Command.new %w'polyamory -t' do |test_job|
114
+ add_ruby_options test_job
115
+ test_job.concat paths.map {|p| p.relative }
116
+
117
+ tunit_opts = testunit_options
118
+ if tunit_opts.any?
119
+ test_job << '--'
120
+ test_job.concat tunit_opts
121
+ end
122
+ end
123
+ end
124
+
125
+ def glob pattern
126
+ RootedPathname.glob pattern, context.root
127
+ end
128
+
129
+ def add_ruby_options cmd
130
+ opts = []
131
+ opts << '-w' if context.warnings?
132
+ opts << '-Ilib:test'
133
+ for path in context.load_paths
134
+ opts << "-I#{path}"
135
+ end
136
+ opts << '%'
137
+ cmd.env['RUBYOPT'] = opts.join(' ')
138
+ end
139
+
140
+ def testunit_options
141
+ opts = []
142
+ opts << '-n' << test_filter_regexp(test_filters) if test_filters.any?
143
+ opts << '-s' << context.test_seed if context.test_seed
144
+ opts << '-v' if context.verbose?
145
+ opts
146
+ end
147
+
148
+ def test_filter_regexp filters
149
+ if filters.size == 1
150
+ '/%s/' % filters.first
151
+ else
152
+ '/(%s)/' % filters.join('|')
153
+ end
154
+ end
155
+
156
+ def add_test_filter_for_line file, linenum
157
+ @test_filters << find_test_filter_for_line(file, linenum)
158
+ end
159
+
160
+ def find_test_filter_for_line file, linenum
161
+ focused_test_finder.call(file, linenum) or
162
+ raise "test method not found (#{file.relative}:#{linenum})"
163
+ end
164
+
165
+ def focused_test_finder() FocusedTestFinder end
166
+
167
+ FocusedTestFinder = Struct.new(:file, :line) do
168
+ def self.call *args
169
+ new(*args).scan
170
+ end
171
+
172
+ def readlines
173
+ file.readlines[0, line.to_i]
174
+ end
175
+
176
+ def scan
177
+ readlines.reverse.each do |line|
178
+ found = test_from_line line
179
+ return found if found
180
+ end
181
+ nil
182
+ end
183
+
184
+ def test_from_line line
185
+ case line
186
+ when /^\s*def\s+(test_\w+)/
187
+ $1
188
+ when /^\s*(test|it|specify)[\s(]+(['"])((.*?)[^\\])\2/
189
+ if 'test' == $1 then $3.gsub(/\s+/, '_') # ActiveSupport::TestCase
190
+ else $3 # minitest/spec
191
+ end
192
+ end
193
+ end
194
+ end
195
+ end
196
+ end
@@ -0,0 +1,250 @@
1
+ require 'minitest/autorun'
2
+ require 'minitest/pride'
3
+ require 'polyamory/runner'
4
+ require 'fileutils'
5
+
6
+ describe Polyamory::Runner do
7
+
8
+ let(:default_options) {
9
+ options = {
10
+ :warnings => false,
11
+ :verbose => false,
12
+ :backtrace => false,
13
+ :test_seed => nil,
14
+ :name_filters => [],
15
+ :tag_filters => [],
16
+ :load_paths => [],
17
+ }
18
+ }
19
+ subject { Polyamory::Runner.new(names, root, default_options.merge(options)) }
20
+
21
+ let(:options) { Hash.new }
22
+ let(:names) { [] }
23
+ let(:root) { File.join(ENV['TMPDIR'] || '/tmp', 'polyamory') }
24
+
25
+ before { FileUtils.rm_rf root }
26
+
27
+ it "finds no jobs when directory doesn't exist" do
28
+ subject.collect_jobs.must_be_empty
29
+ end
30
+
31
+ describe "test/unit project" do
32
+ before {
33
+ %w[ app/models/user.rb
34
+ lib/sync.rb
35
+ test/unit/user_test.rb
36
+ test/unit/blog_test.rb
37
+ test/functional/lib_user_test.rb
38
+ ].each do |path|
39
+ file = File.join(root, path)
40
+ FileUtils.mkdir_p File.dirname(file)
41
+ FileUtils.touch file
42
+ end
43
+ }
44
+
45
+ let(:job) { subject.collect_jobs.first }
46
+ let(:job_files) {
47
+ files = job.to_exec
48
+ end_at = files.index('--').to_i - 1
49
+ files[2..end_at]
50
+ }
51
+
52
+ it "finds one job" do
53
+ subject.collect_jobs.size.must_equal 1
54
+ end
55
+
56
+ it "tests all files" do
57
+ job_files.must_equal %w[
58
+ test/functional/lib_user_test.rb
59
+ test/unit/blog_test.rb
60
+ test/unit/user_test.rb
61
+ ]
62
+ end
63
+
64
+ it "sets ruby options" do
65
+ job.env['RUBYOPT'].must_equal "-Ilib:test %"
66
+ end
67
+
68
+ describe "with verbose" do
69
+ let(:options) { {:warnings => true} }
70
+
71
+ it "sets warning option" do
72
+ job.env['RUBYOPT'].must_equal "-w -Ilib:test %"
73
+ end
74
+ end
75
+
76
+ describe "with pattern" do
77
+ let(:names) { %w[user] }
78
+
79
+ it "finds files by pattern" do
80
+ job_files.must_equal %w[
81
+ test/functional/lib_user_test.rb
82
+ test/unit/user_test.rb
83
+ ]
84
+ end
85
+ end
86
+
87
+ describe "with directory" do
88
+ let(:names) { %w[unit] }
89
+
90
+ it "finds files in dir" do
91
+ job_files.must_equal %w[
92
+ test/unit/blog_test.rb
93
+ test/unit/user_test.rb
94
+ ]
95
+ end
96
+ end
97
+
98
+ describe "with non-matching name" do
99
+ let(:names) { %w[nonexist] }
100
+
101
+ it "finds no jobs" do
102
+ subject.collect_jobs.must_be_empty
103
+ end
104
+ end
105
+
106
+ describe "test filters" do
107
+ describe "from option" do
108
+ let(:options) { {:name_filters => %w'filly'} }
109
+
110
+ it "generates test/unit argument" do
111
+ job.to_s.must_include "-- -n /filly/"
112
+ end
113
+ end
114
+
115
+ describe "from line numbers" do
116
+ before {
117
+ File.open(File.join(root, 'test/unit/blog_test.rb'), 'w') do |file|
118
+ file.write <<-RUBY
119
+ require 'moo'
120
+ def test_blog
121
+ # normal
122
+ test "has feed"
123
+ # ActiveSupport::TestCase
124
+ it "needs posts" do
125
+ # minitest/spec
126
+ end
127
+ specify('no comments') { ... }
128
+ RUBY
129
+ end
130
+ }
131
+
132
+ describe "normal syntax" do
133
+ let(:names) { %w[ test/unit/blog_test.rb:3 ] }
134
+ it("finds method") { job.to_s.must_include "-n /test_blog/" }
135
+ end
136
+
137
+ describe "ActiveSupport syntax" do
138
+ let(:names) { %w[ test/unit/blog_test.rb:5 ] }
139
+ it("finds method") { job.to_s.must_include "-n /has_feed/" }
140
+ end
141
+
142
+ describe "normal syntax" do
143
+ let(:names) { %w[ test/unit/blog_test.rb:7 ] }
144
+ it("finds method") { job.to_s.must_include "-n /needs posts/" }
145
+ end
146
+
147
+ describe "normal syntax" do
148
+ let(:names) { %w[ test/unit/blog_test.rb:9 ] }
149
+ it("finds method") { job.to_s.must_include "-n /no comments/" }
150
+ end
151
+ end
152
+ end
153
+ end
154
+
155
+ describe "RSpec project" do
156
+ before {
157
+ %w[ app/models/user.rb
158
+ lib/sync.rb
159
+ spec/models/user_spec.rb
160
+ spec/models/blog_spec.rb
161
+ spec/integration/sync_spec.rb
162
+ ].each do |path|
163
+ file = File.join(root, path)
164
+ FileUtils.mkdir_p File.dirname(file)
165
+ FileUtils.touch file
166
+ end
167
+ }
168
+
169
+ let(:job) { subject.collect_jobs.first }
170
+ let(:job_files) {
171
+ files = job.to_exec
172
+ files[1..-1]
173
+ }
174
+
175
+ it "finds one job" do
176
+ subject.collect_jobs.size.must_equal 1
177
+ end
178
+
179
+ it "tests all files" do
180
+ job_files.must_equal %w[spec]
181
+ end
182
+
183
+ describe "name args" do
184
+ let(:names) { %w[user blog] }
185
+
186
+ it "tests some files" do
187
+ job_files.must_equal %w[
188
+ spec/models/user_spec.rb
189
+ spec/models/blog_spec.rb
190
+ ]
191
+ end
192
+ end
193
+
194
+ it "sets no ruby options" do
195
+ job.env['RUBYOPT'].must_equal "%"
196
+ end
197
+
198
+ describe "example filters" do
199
+ let(:options) { {:name_filters => %w'willy filly'} }
200
+
201
+ it "generates arguments" do
202
+ job.to_s.must_include " -e willy -e filly "
203
+ end
204
+ end
205
+
206
+ describe "tag filters" do
207
+ let(:options) { {:tag_filters => %w'willy filly'} }
208
+
209
+ it "generates arguments" do
210
+ job.to_s.must_include " -t willy -t filly "
211
+ end
212
+ end
213
+ end
214
+
215
+ describe "mixed project" do
216
+ before {
217
+ %w[ app/models/user.rb
218
+ lib/sync.rb
219
+ test/unit/test_user.rb
220
+ spec/models/blog_spec.rb
221
+ features/sync.feature
222
+ ].each do |path|
223
+ file = File.join(root, path)
224
+ FileUtils.mkdir_p File.dirname(file)
225
+ FileUtils.touch file
226
+ end
227
+ }
228
+
229
+ let(:jobs) { subject.collect_jobs }
230
+
231
+ it "finds three jobs" do
232
+ jobs.map {|j| j.to_s }.must_equal [
233
+ 'polyamory -t test/unit/test_user.rb',
234
+ 'rspec spec',
235
+ 'cucumber features',
236
+ ]
237
+ end
238
+
239
+ describe "with tags" do
240
+ let(:options) { {:tag_filters => %w[willy ~nilly]} }
241
+ it "is filtered by tags" do
242
+ jobs.map {|j| j.to_s }.must_equal [
243
+ 'rspec -t willy -t ~nilly spec',
244
+ 'cucumber -t @willy -t ~@nilly features',
245
+ ]
246
+ end
247
+ end
248
+ end
249
+
250
+ end
metadata CHANGED
@@ -1,17 +1,17 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: polyamory
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.5
5
4
  prerelease:
5
+ version: 0.6.0
6
6
  platform: ruby
7
7
  authors:
8
8
  - Mislav Marohnić
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2011-10-08 00:00:00.000000000 Z
12
+ date: 2013-03-08 00:00:00.000000000 Z
13
13
  dependencies: []
14
- description: A tool that knows how to run your tests regardless of framework
14
+ description: A cli runner for all tests regardless of framework
15
15
  email: mislav.marohnic@gmail.com
16
16
  executables:
17
17
  - polyamory
@@ -19,9 +19,16 @@ extensions: []
19
19
  extra_rdoc_files: []
20
20
  files:
21
21
  - bin/polyamory
22
+ - lib/polyamory/command.rb
23
+ - lib/polyamory/cucumber.rb
24
+ - lib/polyamory/rooted_pathname.rb
25
+ - lib/polyamory/rspec.rb
26
+ - lib/polyamory/runner.rb
27
+ - lib/polyamory/test_unit.rb
22
28
  - lib/polyamory.rb
29
+ - test/test_runner.rb
23
30
  - README.md
24
- homepage: https://github.com/mislav/polyamory
31
+ homepage: https://github.com/mislav/polyamory#readme
25
32
  licenses: []
26
33
  post_install_message:
27
34
  rdoc_options: []
@@ -41,8 +48,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
41
48
  version: '0'
42
49
  requirements: []
43
50
  rubyforge_project:
44
- rubygems_version: 1.8.10
51
+ rubygems_version: 1.8.23
45
52
  signing_key:
46
53
  specification_version: 3
47
- summary: Runs your tests
54
+ summary: The promiscuous test runner
48
55
  test_files: []