polyamory 0.0.5 → 0.6.0

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.
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: []