1
0
Fork 0
mirror of https://github.com/ruby/ruby.git synced 2022-11-09 12:17:21 -05:00

Add in-tree mspec and ruby/spec

* For easier modifications of ruby/spec by MRI developers.
* .gitignore: track changes under spec.
* spec/mspec, spec/rubyspec: add in-tree mspec and ruby/spec.
  These files can therefore be updated like any other file in MRI.
  Instructions are provided in spec/README.
  [Feature #13156] [ruby-core:79246]

git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@58595 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
This commit is contained in:
eregon 2017-05-07 12:04:49 +00:00
parent ed7d803500
commit 95e8c48dd3
4645 changed files with 230678 additions and 4 deletions

4
.gitignore vendored
View file

@ -172,10 +172,6 @@ y.tab.c
/gems/*.gem
/gems/*-*
# /spec/
/spec/mspec
/spec/rubyspec
# /tool/
/tool/config.guess
/tool/config.sub

26
spec/mspec/.gitignore vendored Normal file
View file

@ -0,0 +1,26 @@
pkg
*.rbc
*.iml
*.iws
*.ipr
*.sw?
.rbx
# ctags dir
/tags
*.gem
.bundle
.config
.yardoc
InstalledFiles
_yardoc
coverage
doc/
lib/bundler/man
rdoc
spec/reports
test/tmp
test/version_tmp
tmp

9
spec/mspec/.travis.yml Normal file
View file

@ -0,0 +1,9 @@
sudo: false
language: ruby
script:
- bundle exec rspec
rvm:
- 2.2.7
- 2.3.4
- 2.4.1
- ruby-head

4
spec/mspec/Gemfile Normal file
View file

@ -0,0 +1,4 @@
source 'https://rubygems.org'
# Specify your gem's dependencies in mspec.gemspec
gemspec

30
spec/mspec/Gemfile.lock Normal file
View file

@ -0,0 +1,30 @@
PATH
remote: .
specs:
mspec (1.8.0)
GEM
remote: https://rubygems.org/
specs:
diff-lcs (1.2.5)
rake (10.4.2)
rspec (2.14.1)
rspec-core (~> 2.14.0)
rspec-expectations (~> 2.14.0)
rspec-mocks (~> 2.14.0)
rspec-core (2.14.8)
rspec-expectations (2.14.5)
diff-lcs (>= 1.1.3, < 2.0)
rspec-mocks (2.14.6)
PLATFORMS
java
ruby
DEPENDENCIES
mspec!
rake (~> 10.0)
rspec (~> 2.14.1)
BUNDLED WITH
1.10.2

22
spec/mspec/LICENSE Normal file
View file

@ -0,0 +1,22 @@
Copyright (c) 2008 Engine Yard, Inc. All rights reserved.
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.

88
spec/mspec/README.md Normal file
View file

@ -0,0 +1,88 @@
[![Build Status](https://travis-ci.org/ruby/mspec.svg?branch=master)](https://travis-ci.org/ruby/mspec)
## Overview
MSpec is a specialized framework that is syntax-compatible with RSpec for
basic things like 'describe', 'it' blocks and 'before', 'after' actions. MSpec
contains additional features that assist in writing the RubySpecs used by
multiple Ruby implementations.
MSpec attempts to use the simplest Ruby language features so that beginning
Ruby implementations can run the Ruby specs.
MSpec is not intended as a replacement for RSpec. MSpec attempts to provide a
subset of RSpec's features in some cases and a superset in others. It does not
provide all the matchers, for instance.
However, MSpec provides several extensions to facilitate writing the Ruby
specs in a manner compatible with multiple Ruby implementations.
1. MSpec offers a set of guards to control execution of the specs. These
guards not only enable or disable execution but also annotate the specs
with additional information about why they are run or not run.
2. MSpec provides a different shared spec implementation specifically
designed to ease writing specs for the numerous aliased methods in Ruby.
The MSpec shared spec implementation should not conflict with RSpec's own
shared behavior facility.
3. MSpec provides various helper methods to simplify some specs, for
example, creating temporary file names.
4. MSpec has several specialized runner scripts that includes a
configuration facility with a default project file and user-specific
overrides.
## Bundler
A Gemfile is provided. Use Bundler to install gem dependencies. To install
Bundler, run the following:
```bash
gem install bundler
```
To install the gem dependencies with Bundler, run the following:
```bash
ruby -S bundle install
```
## Running Specs
Use RSpec to run the MSpec specs. There are no plans currently to make the
MSpec specs runnable by MSpec.
After installing the gem dependencies, the specs can be run as follows:
```bash
ruby -S bundle exec rspec
```
Or
```bash
ruby -S rake
```
To run an individual spec file, use the following example:
```bash
ruby -S bundle exec rspec spec/helpers/ruby_exe_spec.rb
```
## Documentation
See http://ruby.github.io/rubyspec.github.io/
## Source Code
See https://github.com/ruby/mspec
## License
See the LICENSE in the source code.

7
spec/mspec/Rakefile Normal file
View file

@ -0,0 +1,7 @@
require 'bundler/gem_tasks'
require 'bundler/setup'
require 'rspec/core/rake_task'
RSpec::Core::RakeTask.new(:spec)
task :default => :spec

7
spec/mspec/bin/mkspec Executable file
View file

@ -0,0 +1,7 @@
#!/usr/bin/env ruby
$:.unshift File.expand_path('../../lib', __FILE__)
require 'mspec/commands/mkspec'
MkSpec.main

1
spec/mspec/bin/mkspec.bat Executable file
View file

@ -0,0 +1 @@
@"ruby.exe" "%~dpn0" %*

7
spec/mspec/bin/mspec Executable file
View file

@ -0,0 +1,7 @@
#!/usr/bin/env ruby
$:.unshift File.expand_path('../../lib', __FILE__)
require 'mspec/commands/mspec'
MSpecMain.main

7
spec/mspec/bin/mspec-ci Executable file
View file

@ -0,0 +1,7 @@
#!/usr/bin/env ruby
$:.unshift File.expand_path('../../lib', __FILE__)
require 'mspec/commands/mspec-ci'
MSpecCI.main

1
spec/mspec/bin/mspec-ci.bat Executable file
View file

@ -0,0 +1 @@
@"ruby.exe" "%~dpn0" %*

7
spec/mspec/bin/mspec-run Executable file
View file

@ -0,0 +1,7 @@
#!/usr/bin/env ruby
$:.unshift File.expand_path('../../lib', __FILE__)
require 'mspec/commands/mspec-run'
MSpecRun.main

1
spec/mspec/bin/mspec-run.bat Executable file
View file

@ -0,0 +1 @@
@"ruby.exe" "%~dpn0" %*

7
spec/mspec/bin/mspec-tag Executable file
View file

@ -0,0 +1,7 @@
#!/usr/bin/env ruby
$:.unshift File.expand_path('../../lib', __FILE__)
require 'mspec/commands/mspec-tag'
MSpecTag.main

1
spec/mspec/bin/mspec-tag.bat Executable file
View file

@ -0,0 +1 @@
@"ruby.exe" "%~dpn0" %*

1
spec/mspec/bin/mspec.bat Executable file
View file

@ -0,0 +1 @@
@"ruby.exe" "%~dpn0" %*

20
spec/mspec/lib/mspec.rb Normal file
View file

@ -0,0 +1,20 @@
require 'mspec/matchers'
require 'mspec/expectations'
require 'mspec/mocks'
require 'mspec/runner'
require 'mspec/guards'
require 'mspec/helpers'
require 'mspec/version'
# If the implementation on which the specs are run cannot
# load pp from the standard library, add a pp.rb file that
# defines the #pretty_inspect method on Object or Kernel.
begin
require 'pp'
rescue LoadError
module Kernel
def pretty_inspect
inspect
end
end
end

View file

@ -0,0 +1,155 @@
#!/usr/bin/env ruby
require 'rbconfig'
require 'mspec/version'
require 'mspec/utils/options'
require 'mspec/utils/name_map'
require 'mspec/helpers/fs'
class MkSpec
attr_reader :config
def initialize
@config = {
:constants => [],
:requires => [],
:base => "core",
:version => nil
}
@map = NameMap.new true
end
def options(argv=ARGV)
options = MSpecOptions.new "mkspec [options]", 32
options.on("-c", "--constant", "CONSTANT",
"Class or Module to generate spec stubs for") do |name|
config[:constants] << name
end
options.on("-b", "--base", "DIR",
"Directory to generate specs into") do |directory|
config[:base] = File.expand_path directory
end
options.on("-r", "--require", "LIBRARY",
"A library to require") do |file|
config[:requires] << file
end
options.on("-V", "--version-guard", "VERSION",
"Specify version for ruby_version_is guards") do |version|
config[:version] = version
end
options.version MSpec::VERSION
options.help
options.doc "\n How might this work in the real world?\n"
options.doc " 1. To create spec stubs for every class or module in Object\n"
options.doc " $ mkspec\n"
options.doc " 2. To create spec stubs for Fixnum\n"
options.doc " $ mkspec -c Fixnum\n"
options.doc " 3. To create spec stubs for Complex in 'superspec/complex'\n"
options.doc " $ mkspec -c Complex -r complex -b superspec"
options.doc ""
options.parse argv
end
def create_directory(mod)
subdir = @map.dir_name mod, config[:base]
if File.exist? subdir
unless File.directory? subdir
puts "#{subdir} already exists and is not a directory."
return nil
end
else
mkdir_p subdir
end
subdir
end
def write_requires(dir, file)
prefix = config[:base] + '/'
raise dir unless dir.start_with? prefix
sub = dir[prefix.size..-1]
parents = '../' * (sub.split('/').length + 1)
File.open(file, 'w') do |f|
f.puts "require File.expand_path('../#{parents}spec_helper', __FILE__)"
config[:requires].each do |lib|
f.puts "require '#{lib}'"
end
end
end
def write_version(f)
f.puts ""
if version = config[:version]
f.puts "ruby_version_is #{version} do"
yield " "
f.puts "end"
else
yield ""
end
end
def write_spec(file, meth, exists)
if exists
out = `#{ruby} #{MSPEC_HOME}/bin/mspec-run --dry-run --unguarded -fs -e '#{meth}' #{file}`
return if out.include?(meth)
end
File.open file, 'a' do |f|
write_version(f) do |indent|
f.puts <<-EOS
#{indent}describe "#{meth}" do
#{indent} it "needs to be reviewed for spec completeness"
#{indent}end
EOS
end
end
puts file
end
def create_file(dir, mod, meth, name)
file = File.join dir, @map.file_name(meth, mod)
exists = File.exist? file
write_requires dir, file unless exists
write_spec file, name, exists
end
def run
config[:requires].each { |lib| require lib }
constants = config[:constants]
constants = Object.constants if constants.empty?
@map.map({}, constants).each do |mod, methods|
name = mod.chop
next unless dir = create_directory(name)
methods.each { |method| create_file dir, name, method, mod + method }
end
end
##
# Determine and return the path of the ruby executable.
def ruby
ruby = File.join(RbConfig::CONFIG['bindir'],
RbConfig::CONFIG['ruby_install_name'])
ruby.gsub! File::SEPARATOR, File::ALT_SEPARATOR if File::ALT_SEPARATOR
return ruby
end
def self.main
ENV['MSPEC_RUNNER'] = '1'
script = new
script.options
script.run
end
end

View file

@ -0,0 +1,79 @@
#!/usr/bin/env ruby
$:.unshift File.expand_path(File.dirname(__FILE__) + '/../lib')
require 'mspec/version'
require 'mspec/utils/options'
require 'mspec/utils/script'
class MSpecCI < MSpecScript
def options(argv=ARGV)
options = MSpecOptions.new "mspec ci [options] (FILE|DIRECTORY|GLOB)+", 30, config
options.doc " Ask yourself:"
options.doc " 1. How to run the specs?"
options.doc " 2. How to modify the guard behavior?"
options.doc " 2. How to display the output?"
options.doc " 3. What action to perform?"
options.doc " 4. When to perform it?"
options.doc "\n How to run the specs"
options.chdir
options.prefix
options.configure { |f| load f }
options.name
options.pretend
options.interrupt
options.doc "\n How to modify the guard behavior"
options.unguarded
options.verify
options.doc "\n How to display their output"
options.formatters
options.verbose
options.doc "\n What action to perform"
options.actions
options.doc "\n When to perform it"
options.action_filters
options.doc "\n Help!"
options.debug
options.version MSpec::VERSION
options.help
options.doc "\n Custom options"
custom_options options
options.doc "\n How might this work in the real world?"
options.doc "\n 1. To simply run the known good specs"
options.doc "\n $ mspec ci"
options.doc "\n 2. To run a subset of the known good specs"
options.doc "\n $ mspec ci path/to/specs"
options.doc "\n 3. To start the debugger before the spec matching 'this crashes'"
options.doc "\n $ mspec ci --spec-debug -S 'this crashes'"
options.doc ""
patterns = options.parse argv
patterns = config[:ci_files] if patterns.empty?
@files = files patterns
end
def run
MSpec.register_tags_patterns config[:tags_patterns]
MSpec.register_files @files
tags = ["fails", "critical", "unstable", "incomplete", "unsupported"]
tags += Array(config[:ci_xtags])
require 'mspec/runner/filters/tag'
filter = TagFilter.new(:exclude, *tags)
filter.register
MSpec.process
exit MSpec.exit_code
end
end

View file

@ -0,0 +1,87 @@
#!/usr/bin/env ruby
$:.unshift File.expand_path(File.dirname(__FILE__) + '/../lib')
require 'mspec/version'
require 'mspec/utils/options'
require 'mspec/utils/script'
class MSpecRun < MSpecScript
def initialize
super
config[:files] = []
end
def options(argv=ARGV)
options = MSpecOptions.new "mspec run [options] (FILE|DIRECTORY|GLOB)+", 30, config
options.doc " Ask yourself:"
options.doc " 1. What specs to run?"
options.doc " 2. How to modify the execution?"
options.doc " 3. How to modify the guard behavior?"
options.doc " 4. How to display the output?"
options.doc " 5. What action to perform?"
options.doc " 6. When to perform it?"
options.doc "\n What specs to run"
options.filters
options.doc "\n How to modify the execution"
options.chdir
options.prefix
options.configure { |f| load f }
options.name
options.randomize
options.repeat
options.pretend
options.interrupt
options.doc "\n How to modify the guard behavior"
options.unguarded
options.verify
options.doc "\n How to display their output"
options.formatters
options.verbose
options.doc "\n What action to perform"
options.actions
options.doc "\n When to perform it"
options.action_filters
options.doc "\n Help!"
options.debug
options.version MSpec::VERSION
options.help
options.doc "\n Custom options"
custom_options options
options.doc "\n How might this work in the real world?"
options.doc "\n 1. To simply run some specs"
options.doc "\n $ mspec path/to/the/specs"
options.doc " mspec path/to/the_file_spec.rb"
options.doc "\n 2. To run specs tagged with 'fails'"
options.doc "\n $ mspec -g fails path/to/the_file_spec.rb"
options.doc "\n 3. To start the debugger before the spec matching 'this crashes'"
options.doc "\n $ mspec --spec-debug -S 'this crashes' path/to/the_file_spec.rb"
options.doc "\n 4. To run some specs matching 'this crashes'"
options.doc "\n $ mspec -e 'this crashes' path/to/the_file_spec.rb"
options.doc ""
patterns = options.parse argv
@files = files_from_patterns(patterns)
end
def run
MSpec.register_tags_patterns config[:tags_patterns]
MSpec.register_files @files
MSpec.process
exit MSpec.exit_code
end
end

View file

@ -0,0 +1,133 @@
#!/usr/bin/env ruby
require 'mspec/version'
require 'mspec/utils/options'
require 'mspec/utils/script'
class MSpecTag < MSpecScript
def initialize
super
config[:tagger] = :add
config[:tag] = 'fails:'
config[:outcome] = :fail
config[:ltags] = []
end
def options(argv=ARGV)
options = MSpecOptions.new "mspec tag [options] (FILE|DIRECTORY|GLOB)+", 30, config
options.doc " Ask yourself:"
options.doc " 1. What specs to run?"
options.doc " 2. How to modify the execution?"
options.doc " 3. How to display the output?"
options.doc " 4. What tag action to perform?"
options.doc " 5. When to perform it?"
options.doc "\n What specs to run"
options.filters
options.doc "\n How to modify the execution"
options.configure { |f| load f }
options.name
options.pretend
options.unguarded
options.interrupt
options.doc "\n How to display their output"
options.formatters
options.verbose
options.doc "\n What action to perform and when to perform it"
options.on("-N", "--add", "TAG",
"Add TAG with format 'tag' or 'tag(comment)' (see -Q, -F, -L)") do |o|
config[:tagger] = :add
config[:tag] = "#{o}:"
end
options.on("-R", "--del", "TAG",
"Delete TAG (see -Q, -F, -L)") do |o|
config[:tagger] = :del
config[:tag] = "#{o}:"
config[:outcome] = :pass
end
options.on("-Q", "--pass", "Apply action to specs that pass (default for --del)") do
config[:outcome] = :pass
end
options.on("-F", "--fail", "Apply action to specs that fail (default for --add)") do
config[:outcome] = :fail
end
options.on("-L", "--all", "Apply action to all specs") do
config[:outcome] = :all
end
options.on("--list", "TAG", "Display descriptions of any specs tagged with TAG") do |t|
config[:tagger] = :list
config[:ltags] << t
end
options.on("--list-all", "Display descriptions of any tagged specs") do
config[:tagger] = :list_all
end
options.on("--purge", "Remove all tags not matching any specs") do
config[:tagger] = :purge
end
options.doc "\n Help!"
options.debug
options.version MSpec::VERSION
options.help
options.doc "\n Custom options"
custom_options options
options.doc "\n How might this work in the real world?"
options.doc "\n 1. To add the 'fails' tag to failing specs"
options.doc "\n $ mspec tag path/to/the_file_spec.rb"
options.doc "\n 2. To remove the 'fails' tag from passing specs"
options.doc "\n $ mspec tag --del fails path/to/the_file_spec.rb"
options.doc "\n 3. To display the descriptions for all specs tagged with 'fails'"
options.doc "\n $ mspec tag --list fails path/to/the/specs"
options.doc ""
patterns = options.parse argv
if patterns.empty?
puts options
puts "No files specified."
exit 1
end
@files = files patterns
end
def register
require 'mspec/runner/actions'
case config[:tagger]
when :add, :del
tag = SpecTag.new config[:tag]
tagger = TagAction.new(config[:tagger], config[:outcome], tag.tag, tag.comment,
config[:atags], config[:astrings])
when :list, :list_all
tagger = TagListAction.new config[:tagger] == :list_all ? nil : config[:ltags]
MSpec.register_mode :pretend
config[:formatter] = false
when :purge
tagger = TagPurgeAction.new
MSpec.register_mode :pretend
MSpec.register_mode :unguarded
config[:formatter] = false
else
raise ArgumentError, "No recognized action given"
end
tagger.register
super
end
def run
MSpec.register_tags_patterns config[:tags_patterns]
MSpec.register_files @files
MSpec.process
exit MSpec.exit_code
end
end

View file

@ -0,0 +1,163 @@
#!/usr/bin/env ruby
require 'mspec/version'
require 'mspec/utils/options'
require 'mspec/utils/script'
require 'mspec/helpers/tmp'
require 'mspec/runner/actions/filter'
require 'mspec/runner/actions/timer'
class MSpecMain < MSpecScript
def initialize
super
config[:loadpath] = []
config[:requires] = []
config[:target] = ENV['RUBY'] || 'ruby'
config[:flags] = []
config[:command] = nil
config[:options] = []
config[:launch] = []
end
def options(argv=ARGV)
config[:command] = argv.shift if ["ci", "run", "tag"].include?(argv[0])
options = MSpecOptions.new "mspec [COMMAND] [options] (FILE|DIRECTORY|GLOB)+", 30, config
options.doc " The mspec command sets up and invokes the sub-commands"
options.doc " (see below) to enable, for instance, running the specs"
options.doc " with different implementations like ruby, jruby, rbx, etc.\n"
options.configure do |f|
load f
config[:options] << '-B' << f
end
options.targets
options.on("--warnings", "Don't supress warnings") do
config[:flags] << '-w'
ENV['OUTPUT_WARNINGS'] = '1'
end
options.on("-j", "--multi", "Run multiple (possibly parallel) subprocesses") do
config[:multi] = true
config[:options] << "-fy"
end
options.version MSpec::VERSION do
if config[:command]
config[:options] << "-v"
else
puts "#{File.basename $0} #{MSpec::VERSION}"
exit
end
end
options.help do
if config[:command]
config[:options] << "-h"
else
puts options
exit 1
end
end
options.doc "\n Custom options"
custom_options options
# The rest of the help output
options.doc "\n where COMMAND is one of:\n"
options.doc " run - Run the specified specs (default)"
options.doc " ci - Run the known good specs"
options.doc " tag - Add or remove tags\n"
options.doc " mspec COMMAND -h for more options\n"
options.doc " example: $ mspec run -h\n"
options.on_extra { |o| config[:options] << o }
options.parse(argv)
if config[:multi]
options = MSpecOptions.new "mspec", 30, config
options.all
patterns = options.parse(config[:options])
@files = files_from_patterns(patterns)
end
end
def register; end
def multi_exec(argv)
MSpec.register_files @files
require 'mspec/runner/formatters/multi'
formatter = MultiFormatter.new
output_files = []
processes = [cores, @files.size].min
children = processes.times.map { |i|
name = tmp "mspec-multi-#{i}"
output_files << name
env = {
"SPEC_TEMP_DIR" => "rubyspec_temp_#{i}",
"MSPEC_MULTI" => i.to_s
}
command = argv + ["-o", name]
$stderr.puts "$ #{command.join(' ')}" if $MSPEC_DEBUG
IO.popen([env, *command], "rb+")
}
puts children.map { |child| child.gets }.uniq
formatter.start
until @files.empty?
IO.select(children)[0].each { |io|
reply = io.read(1)
case reply
when '.'
formatter.unload
when nil
raise "Worker died!"
else
while chunk = (io.read_nonblock(4096) rescue nil)
reply += chunk
end
raise reply
end
io.puts @files.shift unless @files.empty?
}
end
ok = true
children.each { |child|
child.puts "QUIT"
Process.wait(child.pid)
ok &&= $?.success?
}
formatter.aggregate_results(output_files)
formatter.finish
ok
end
def run
argv = config[:target].split(/\s+/)
argv.concat config[:launch]
argv.concat config[:flags]
argv.concat config[:loadpath]
argv.concat config[:requires]
argv << "#{MSPEC_HOME}/bin/mspec-#{ config[:command] || "run" }"
argv.concat config[:options]
if config[:multi]
exit multi_exec(argv)
else
$stderr.puts "$ #{argv.join(' ')}"
exec(*argv)
end
end
end

View file

@ -0,0 +1,2 @@
require 'mspec/expectations/expectations'
require 'mspec/expectations/should'

View file

@ -0,0 +1,21 @@
class SpecExpectationNotMetError < StandardError
end
class SpecExpectationNotFoundError < StandardError
def message
"No behavior expectation was found in the example"
end
end
class SpecExpectation
def self.fail_with(expected, actual)
expected_to_s = expected.to_s
actual_to_s = actual.to_s
if expected_to_s.size + actual_to_s.size > 80
message = "#{expected_to_s.chomp}\n#{actual_to_s}"
else
message = "#{expected_to_s} #{actual_to_s}"
end
Kernel.raise SpecExpectationNotMetError, message
end
end

View file

@ -0,0 +1,29 @@
class Object
NO_MATCHER_GIVEN = Object.new
def should(matcher = NO_MATCHER_GIVEN)
MSpec.expectation
MSpec.actions :expectation, MSpec.current.state
unless matcher.equal? NO_MATCHER_GIVEN
unless matcher.matches? self
expected, actual = matcher.failure_message
SpecExpectation.fail_with(expected, actual)
end
else
SpecPositiveOperatorMatcher.new(self)
end
end
def should_not(matcher = NO_MATCHER_GIVEN)
MSpec.expectation
MSpec.actions :expectation, MSpec.current.state
unless matcher.equal? NO_MATCHER_GIVEN
if matcher.matches? self
expected, actual = matcher.negative_failure_message
SpecExpectation.fail_with(expected, actual)
end
else
SpecNegativeOperatorMatcher.new(self)
end
end
end

View file

@ -0,0 +1,12 @@
require 'mspec/utils/ruby_name'
require 'mspec/guards/block_device'
require 'mspec/guards/bug'
require 'mspec/guards/conflict'
require 'mspec/guards/endian'
require 'mspec/guards/feature'
require 'mspec/guards/guard'
require 'mspec/guards/platform'
require 'mspec/guards/quarantine'
require 'mspec/guards/support'
require 'mspec/guards/superuser'
require 'mspec/guards/version'

View file

@ -0,0 +1,18 @@
require 'mspec/guards/guard'
class BlockDeviceGuard < SpecGuard
def match?
platform_is_not :freebsd, :windows, :opal do
block = `find /dev /devices -type b 2> /dev/null`
return !(block.nil? || block.empty?)
end
false
end
end
class Object
def with_block_device(&block)
BlockDeviceGuard.new.run_if(:with_block_device, &block)
end
end

View file

@ -0,0 +1,30 @@
require 'mspec/guards/version'
class BugGuard < VersionGuard
def initialize(bug, version)
@bug = bug
if String === version
MSpec.deprecate "ruby_bug with a single version", 'an exclusive range ("2.1"..."2.3")'
@version = SpecVersion.new version, true
else
super(version)
end
@parameters = [@bug, @version]
end
def match?
return false if MSpec.mode? :no_ruby_bug
return false unless PlatformGuard.standard?
if Range === @version
super
else
FULL_RUBY_VERSION <= @version
end
end
end
class Object
def ruby_bug(bug, version, &block)
BugGuard.new(bug, version).run_unless(:ruby_bug, &block)
end
end

View file

@ -0,0 +1,19 @@
require 'mspec/guards/guard'
class ConflictsGuard < SpecGuard
def match?
# Always convert constants to symbols regardless of version.
constants = Object.constants.map { |x| x.to_sym }
@parameters.any? { |mod| constants.include? mod }
end
end
class Object
# In some cases, libraries will modify another Ruby method's
# behavior. The specs for the method's behavior will then fail
# if that library is loaded. This guard will not run if any of
# the specified constants exist in Object.constants.
def conflicts_with(*modules, &block)
ConflictsGuard.new(*modules).run_unless(:conflicts_with, &block)
end
end

View file

@ -0,0 +1,27 @@
require 'mspec/guards/guard'
# Despite that these are inverses, the two classes are
# used to simplify MSpec guard reporting modes
class EndianGuard < SpecGuard
def pattern
@pattern ||= [1].pack('L')
end
private :pattern
end
class BigEndianGuard < EndianGuard
def match?
pattern[-1] == ?\001
end
end
class Object
def big_endian(&block)
BigEndianGuard.new.run_if(:big_endian, &block)
end
def little_endian(&block)
BigEndianGuard.new.run_unless(:little_endian, &block)
end
end

View file

@ -0,0 +1,43 @@
require 'mspec/guards/guard'
class FeatureGuard < SpecGuard
def self.enabled?(*features)
new(*features).match?
end
def match?
@parameters.all? { |f| MSpec.feature_enabled? f }
end
end
class Object
# Provides better documentation in the specs by
# naming sets of features that work together as
# a whole. Examples include :encoding, :fiber,
# :continuation, :fork.
#
# Usage example:
#
# with_feature :encoding do
# # specs for a method that provides aspects
# # of the encoding feature
# end
#
# Multiple features must all be enabled for the
# guard to run:
#
# with_feature :one, :two do
# # these specs will run if features :one AND
# # :two are enabled.
# end
#
# The implementation must explicitly enable a feature
# by adding code like the following to the .mspec
# configuration file:
#
# MSpec.enable_feature :encoding
#
def with_feature(*features, &block)
FeatureGuard.new(*features).run_if(:with_feature, &block)
end
end

View file

@ -0,0 +1,118 @@
require 'mspec/runner/mspec'
require 'mspec/runner/actions/tally'
require 'mspec/utils/ruby_name'
class SpecGuard
def self.report
@report ||= Hash.new { |h,k| h[k] = [] }
end
def self.clear
@report = nil
end
def self.finish
report.keys.sort.each do |key|
desc = report[key]
size = desc.size
spec = size == 1 ? "spec" : "specs"
print "\n\n#{size} #{spec} omitted by guard: #{key}:\n"
desc.each { |description| print "\n", description; }
end
print "\n\n"
end
def self.guards
@guards ||= []
end
def self.clear_guards
@guards = []
end
# Returns a partial Ruby version string based on +which+.
# For example, if RUBY_VERSION = 8.2.3:
#
# :major => "8"
# :minor => "8.2"
# :tiny => "8.2.3"
# :teeny => "8.2.3"
# :full => "8.2.3"
def self.ruby_version(which = :minor)
case which
when :major
n = 1
when :minor
n = 2
when :tiny, :teeny, :full
n = 3
end
RUBY_VERSION.split('.')[0,n].join('.')
end
attr_accessor :name
def initialize(*args)
@parameters = args
end
def yield?(invert = false)
return true if MSpec.mode? :unguarded
allow = match? ^ invert
if !allow and reporting?
MSpec.guard
MSpec.register :finish, SpecGuard
MSpec.register :add, self
return true
elsif MSpec.mode? :verify
return true
end
allow
end
def run_if(name, &block)
@name = name
yield if yield?(false)
ensure
unregister
end
def run_unless(name, &block)
@name = name
yield if yield?(true)
ensure
unregister
end
def reporting?
MSpec.mode?(:report) or
(MSpec.mode?(:report_on) and SpecGuard.guards.include?(name))
end
def report_key
"#{name} #{@parameters.join(", ")}"
end
def record(description)
SpecGuard.report[report_key] << description
end
def add(example)
record example.description
MSpec.retrieve(:formatter).tally.counter.guards!
end
def unregister
MSpec.unguard
MSpec.unregister :add, self
end
def match?
raise "must be implemented by the subclass"
end
end

View file

@ -0,0 +1,78 @@
require 'mspec/guards/guard'
class PlatformGuard < SpecGuard
def self.implementation?(*args)
args.any? do |name|
case name
when :rubinius
RUBY_NAME.start_with?('rbx')
when :ruby, :jruby, :truffleruby, :ironruby, :macruby, :maglev, :topaz, :opal
RUBY_NAME.start_with?(name.to_s)
else
raise "unknown implementation #{name}"
end
end
end
def self.standard?
implementation? :ruby
end
HOST_OS = begin
require 'rbconfig'
RbConfig::CONFIG['host_os'] || RUBY_PLATFORM
rescue LoadError
RUBY_PLATFORM
end.downcase
def self.os?(*oses)
oses.any? do |os|
raise ":java is not a valid OS" if os == :java
if os == :windows
HOST_OS =~ /(mswin|mingw)/
else
HOST_OS.include?(os.to_s)
end
end
end
def self.windows?
os?(:windows)
end
def self.wordsize?(size)
size == 8 * 1.size
end
def initialize(*args)
if args.last.is_a?(Hash)
@options, @platforms = args.last, args[0..-2]
else
@options, @platforms = {}, args
end
@parameters = args
end
def match?
match = @platforms.empty? ? true : PlatformGuard.os?(*@platforms)
@options.each do |key, value|
case key
when :os
match &&= PlatformGuard.os?(*value)
when :wordsize
match &&= PlatformGuard.wordsize? value
end
end
match
end
end
class Object
def platform_is(*args, &block)
PlatformGuard.new(*args).run_if(:platform_is, &block)
end
def platform_is_not(*args, &block)
PlatformGuard.new(*args).run_unless(:platform_is_not, &block)
end
end

View file

@ -0,0 +1,13 @@
require 'mspec/guards/guard'
class QuarantineGuard < SpecGuard
def match?
true
end
end
class Object
def quarantine!(&block)
QuarantineGuard.new.run_unless(:quarantine!, &block)
end
end

View file

@ -0,0 +1,17 @@
require 'mspec/guards/guard'
class SuperUserGuard < SpecGuard
def match?
Process.euid == 0
end
end
class Object
def as_superuser(&block)
SuperUserGuard.new.run_if(:as_superuser, &block)
end
def as_user(&block)
SuperUserGuard.new.run_unless(:as_user, &block)
end
end

View file

@ -0,0 +1,16 @@
require 'mspec/guards/platform'
class SupportedGuard < SpecGuard
def match?
if @parameters.include? :ruby
raise Exception, "improper use of not_supported_on guard"
end
!PlatformGuard.standard? and PlatformGuard.implementation?(*@parameters)
end
end
class Object
def not_supported_on(*args, &block)
SupportedGuard.new(*args).run_unless(:not_supported_on, &block)
end
end

View file

@ -0,0 +1,39 @@
require 'mspec/utils/deprecate'
require 'mspec/utils/version'
require 'mspec/guards/guard'
class VersionGuard < SpecGuard
FULL_RUBY_VERSION = SpecVersion.new SpecGuard.ruby_version(:full)
def initialize(version)
case version
when String
@version = SpecVersion.new version
when Range
MSpec.deprecate "an empty version range end", 'a specific version' if version.end.empty?
a = SpecVersion.new version.begin
b = SpecVersion.new version.end
unless version.exclude_end?
MSpec.deprecate "ruby_version_is with an inclusive range", 'an exclusive range ("2.1"..."2.3")'
end
@version = version.exclude_end? ? a...b : a..b
else
raise "version must be a String or Range but was a #{version.class}"
end
@parameters = [version]
end
def match?
if Range === @version
@version.include? FULL_RUBY_VERSION
else
FULL_RUBY_VERSION >= @version
end
end
end
class Object
def ruby_version_is(*args, &block)
VersionGuard.new(*args).run_if(:ruby_version_is, &block)
end
end

View file

@ -0,0 +1,12 @@
require 'mspec/helpers/argf'
require 'mspec/helpers/argv'
require 'mspec/helpers/datetime'
require 'mspec/helpers/fixture'
require 'mspec/helpers/flunk'
require 'mspec/helpers/fs'
require 'mspec/helpers/io'
require 'mspec/helpers/mock_to_path'
require 'mspec/helpers/numeric'
require 'mspec/helpers/ruby_exe'
require 'mspec/helpers/scratch'
require 'mspec/helpers/tmp'

View file

@ -0,0 +1,37 @@
class Object
# Convenience helper for specs using ARGF.
# Set @argf to an instance of ARGF.class with the given +argv+.
# That instance must be used instead of ARGF as ARGF is global
# and it is not always possible to reset its state correctly.
#
# The helper yields to the block and then close
# the files open by the instance. Example:
#
# describe "That" do
# it "does something" do
# argf ['a', 'b'] do
# # do something
# end
# end
# end
def argf(argv)
if argv.empty? or argv.length > 2
raise "Only 1 or 2 filenames are allowed for the argf helper so files can be properly closed: #{argv.inspect}"
end
@argf ||= nil
raise "Cannot nest calls to the argf helper" if @argf
@argf = ARGF.class.new(*argv)
@__mspec_saved_argf_file__ = @argf.file
begin
yield
ensure
file1 = @__mspec_saved_argf_file__
file2 = @argf.file # Either the first file or the second
file1.close if !file1.closed? and file1 != STDIN
file2.close if !file2.closed? and file2 != STDIN
@argf = nil
@__mspec_saved_argf_file__ = nil
end
end
end

View file

@ -0,0 +1,46 @@
class Object
# Convenience helper for altering ARGV. Saves the
# value of ARGV and sets it to +args+. If a block
# is given, yields to the block and then restores
# the value of ARGV. The previously saved value of
# ARGV can be restored by passing +:restore+. The
# former is useful in a single spec. The latter is
# useful in before/after actions. For example:
#
# describe "This" do
# before do
# argv ['a', 'b']
# end
#
# after do
# argv :restore
# end
#
# it "does something" do
# # do something
# end
# end
#
# describe "That" do
# it "does something" do
# argv ['a', 'b'] do
# # do something
# end
# end
# end
def argv(args)
if args == :restore
ARGV.replace(@__mspec_saved_argv__ || [])
else
@__mspec_saved_argv__ = ARGV.dup
ARGV.replace args
if block_given?
begin
yield
ensure
argv :restore
end
end
end
end
end

View file

@ -0,0 +1,51 @@
class Object
# The new_datetime helper makes writing DateTime specs more simple by
# providing default constructor values and accepting a Hash of only the
# constructor values needed for the particular spec. For example:
#
# new_datetime :hour => 1, :minute => 20
#
# Possible keys are:
# :year, :month, :day, :hour, :minute, :second, :offset and :sg.
def new_datetime(opts={})
require 'date'
value = {
:year => -4712,
:month => 1,
:day => 1,
:hour => 0,
:minute => 0,
:second => 0,
:offset => 0,
:sg => Date::ITALY
}.merge opts
DateTime.new value[:year], value[:month], value[:day], value[:hour],
value[:minute], value[:second], value[:offset], value[:sg]
end
def with_timezone(name, offset = nil, daylight_saving_zone = "")
zone = name.dup
if offset
# TZ convention is backwards
offset = -offset
zone += offset.to_s
zone += ":00:00"
end
zone += daylight_saving_zone
old = ENV["TZ"]
ENV["TZ"] = zone
begin
yield
ensure
ENV["TZ"] = old
end
end
end

View file

@ -0,0 +1,26 @@
class Object
# Returns the name of a fixture file by adjoining the directory
# of the +file+ argument with "fixtures" and the contents of the
# +args+ array. For example,
#
# +file+ == "some/example_spec.rb"
#
# and
#
# +args+ == ["subdir", "file.txt"]
#
# then the result is the expanded path of
#
# "some/fixtures/subdir/file.txt".
def fixture(file, *args)
path = File.dirname(file)
path = path[0..-7] if path[-7..-1] == "/shared"
fixtures = path[-9..-1] == "/fixtures" ? "" : "fixtures"
if File.respond_to?(:realpath)
path = File.realpath(path)
else
path = File.expand_path(path)
end
File.join(path, fixtures, args)
end
end

View file

@ -0,0 +1,5 @@
class Object
def flunk(msg="This example is a failure")
SpecExpectation.fail_with "Failed:", msg
end
end

View file

@ -0,0 +1,62 @@
class Object
# Copies a file
def cp(source, dest)
File.open(dest, "w") do |d|
File.open(source, "r") do |s|
while data = s.read(1024)
d.write data
end
end
end
end
# Creates each directory in path that does not exist.
def mkdir_p(path)
parts = File.expand_path(path).split %r[/|\\]
name = parts.shift
parts.each do |part|
name = File.join name, part
if File.file? name
raise ArgumentError, "path component of #{path} is a file"
end
Dir.mkdir name unless File.directory? name
end
end
# Recursively removes all files and directories in +path+
# if +path+ is a directory. Removes the file if +path+ is
# a file.
def rm_r(*paths)
paths.each do |path|
path = File.expand_path path
prefix = SPEC_TEMP_DIR
unless path[0, prefix.size] == prefix
raise ArgumentError, "#{path} is not prefixed by #{prefix}"
end
# File.symlink? needs to be checked first as
# File.exist? returns false for dangling symlinks
if File.symlink? path
File.unlink path
elsif File.directory? path
Dir.entries(path).each { |x| rm_r "#{path}/#{x}" unless x =~ /^\.\.?$/ }
Dir.rmdir path
elsif File.exist? path
File.delete path
end
end
end
# Creates a file +name+. Creates the directory for +name+
# if it does not exist.
def touch(name, mode="w")
mkdir_p File.dirname(name)
File.open(name, mode) do |f|
yield f if block_given?
end
end
end

View file

@ -0,0 +1,113 @@
require 'mspec/guards/feature'
class IOStub
def initialize
@buffer = []
@output = ''
end
def write(*str)
self << str.join
end
def << str
@buffer << str
self
end
def print(*str)
write(str.join + $\.to_s)
end
def method_missing(name, *args, &block)
to_s.send(name, *args, &block)
end
def == other
to_s == other
end
def =~ other
to_s =~ other
end
def puts(*str)
if str.empty?
write "\n"
else
write(str.collect { |s| s.to_s.chomp }.concat([nil]).join("\n"))
end
end
def printf(format, *args)
self << sprintf(format, *args)
end
def flush
@output += @buffer.join('')
@buffer.clear
self
end
def to_s
flush
@output
end
alias_method :to_str, :to_s
def inspect
to_s.inspect
end
end
class Object
# Creates a "bare" file descriptor (i.e. one that is not associated
# with any Ruby object). The file descriptor can safely be passed
# to IO.new without creating a Ruby object alias to the fd.
def new_fd(name, mode="w:utf-8")
mode = options_or_mode(mode)
if mode.kind_of? Hash
if mode.key? :mode
mode = mode[:mode]
else
raise ArgumentError, "new_fd options Hash must include :mode"
end
end
IO.sysopen name, fmode(mode)
end
# Creates an IO instance for a temporary file name. The file
# must be deleted.
def new_io(name, mode="w:utf-8")
IO.new new_fd(name, options_or_mode(mode)), options_or_mode(mode)
end
# This helper simplifies passing file access modes regardless of
# whether the :encoding feature is enabled. Only the access specifier
# itself will be returned if :encoding is not enabled. Otherwise,
# the full mode string will be returned (i.e. the helper is a no-op).
def fmode(mode)
if FeatureGuard.enabled? :encoding
mode
else
mode.split(':').first
end
end
# This helper simplifies passing file access modes or options regardless of
# whether the :encoding feature is enabled. Only the access specifier itself
# will be returned if :encoding is not enabled. Otherwise, the full mode
# string or option will be returned (i.e. the helper is a no-op).
def options_or_mode(oom)
return fmode(oom) if oom.kind_of? String
if FeatureGuard.enabled? :encoding
oom
else
fmode(oom[:mode] || "r:utf-8")
end
end
end

View file

@ -0,0 +1,8 @@
class Object
def mock_to_path(path)
# Cannot use our Object#mock here since it conflicts with RSpec
obj = MockObject.new('path')
obj.should_receive(:to_path).and_return(path)
obj
end
end

View file

@ -0,0 +1,72 @@
require 'mspec/guards/platform'
class Object
def nan_value
0/0.0
end
def infinity_value
1/0.0
end
def bignum_value(plus=0)
0x8000_0000_0000_0000 + plus
end
# This is a bit hairy, but we need to be able to write specs that cover the
# boundary between Fixnum and Bignum for operations like Fixnum#<<. Since
# this boundary is implementation-dependent, we use these helpers to write
# specs based on the relationship between values rather than specific
# values.
if PlatformGuard.standard? or PlatformGuard.implementation? :topaz
if PlatformGuard.wordsize? 32
def fixnum_max
(2**30) - 1
end
def fixnum_min
-(2**30)
end
elsif PlatformGuard.wordsize? 64
def fixnum_max
(2**62) - 1
end
def fixnum_min
-(2**62)
end
end
elsif PlatformGuard.implementation? :opal
def fixnum_max
Integer::MAX
end
def fixnum_min
Integer::MIN
end
elsif PlatformGuard.implementation? :rubinius
def fixnum_max
Fixnum::MAX
end
def fixnum_min
Fixnum::MIN
end
elsif PlatformGuard.implementation?(:jruby) || PlatformGuard.implementation?(:truffleruby)
def fixnum_max
9223372036854775807
end
def fixnum_min
-9223372036854775808
end
else
def fixnum_max
raise "unknown implementation for fixnum_max() helper"
end
def fixnum_min
raise "unknown implementation for fixnum_min() helper"
end
end
end

View file

@ -0,0 +1,178 @@
require 'mspec/utils/ruby_name'
require 'mspec/guards/platform'
require 'mspec/helpers/tmp'
# The ruby_exe helper provides a wrapper for invoking the
# same Ruby interpreter with the same falgs as the one running
# the specs and getting the output from running the code.
# If +code+ is a file that exists, it will be run.
# Otherwise, +code+ should be Ruby code that will be run with
# the -e command line option. For example:
#
# ruby_exe('path/to/some/file.rb')
#
# will be executed as
#
# `#{RUBY_EXE} 'path/to/some/file.rb'`
#
# while
#
# ruby_exe('puts "hello, world."')
#
# will be executed as
#
# `#{RUBY_EXE} -e 'puts "hello, world."'`
#
# The ruby_exe helper also accepts an options hash with three
# keys: :options, :args and :env. For example:
#
# ruby_exe('file.rb', :options => "-w",
# :args => "> file.txt",
# :env => { :FOO => "bar" })
#
# will be executed as
#
# `#{RUBY_EXE} -w #{'file.rb'} > file.txt`
#
# with access to ENV["FOO"] with value "bar".
#
# If +nil+ is passed for the first argument, the command line
# will be built only from the options hash.
#
# The RUBY_EXE constant is setup by mspec automatically
# and is used by ruby_exe and ruby_cmd. The mspec runner script
# will set ENV['RUBY_EXE'] to the name of the executable used
# to invoke the mspec-run script. The value of RUBY_EXE will be
# constructed as follows:
#
# 1. the value of ENV['RUBY_EXE']
# 2. an explicit value based on RUBY_NAME
# 3. cwd/(RUBY_NAME + $(EXEEXT) || $(exeext) || '')
# 4. $(bindir)/$(RUBY_INSTALL_NAME)
#
# The value will only be used if the file exists and is executable.
# The flags will then be appended to the resulting value.
#
# These 4 ways correspond to the following scenarios:
#
# 1. Using the MSpec runner scripts, the name of the
# executable is explicitly passed by ENV['RUBY_EXE']
# so there is no ambiguity.
#
# Otherwise, if using RSpec (or something else)
#
# 2. Running the specs while developing an alternative
# Ruby implementation. This explicitly names the
# executable in the development directory based on
# the value of RUBY_NAME, which is probably initialized
# from the value of RUBY_ENGINE.
# 3. Running the specs within the source directory for
# some implementation. (E.g. a local build directory.)
# 4. Running the specs against some installed Ruby
# implementation.
#
# Additionally, the flags passed to mspec
# (with -T on the command line or in the config with set :flags)
# will be appended to RUBY_EXE so that the interpreter
# is always called with those flags.
class Object
def ruby_exe_options(option)
case option
when :env
ENV['RUBY_EXE']
when :engine
case RUBY_NAME
when 'rbx'
"bin/rbx"
when 'jruby'
"bin/jruby"
when 'maglev'
"maglev-ruby"
when 'topaz'
"topaz"
when 'ironruby'
"ir"
end
when :name
require 'rbconfig'
bin = RUBY_NAME + (RbConfig::CONFIG['EXEEXT'] || RbConfig::CONFIG['exeext'] || '')
File.join(".", bin)
when :install_name
require 'rbconfig'
bin = RbConfig::CONFIG["RUBY_INSTALL_NAME"] || RbConfig::CONFIG["ruby_install_name"]
bin << (RbConfig::CONFIG['EXEEXT'] || RbConfig::CONFIG['exeext'] || '')
File.join(RbConfig::CONFIG['bindir'], bin)
end
end
def resolve_ruby_exe
[:env, :engine, :name, :install_name].each do |option|
next unless exe = ruby_exe_options(option)
if File.file?(exe) and File.executable?(exe)
exe = File.expand_path(exe)
exe = exe.tr('/', '\\') if PlatformGuard.windows?
flags = ENV['RUBY_FLAGS']
if flags and !flags.empty?
return exe + ' ' + flags
else
return exe
end
end
end
raise Exception, "Unable to find a suitable ruby executable."
end
def ruby_exe(code, opts = {})
if opts[:dir]
raise "ruby_exe(..., dir: dir) is no longer supported, use Dir.chdir"
end
env = opts[:env] || {}
saved_env = {}
env.each do |key, value|
key = key.to_s
saved_env[key] = ENV[key] if ENV.key? key
ENV[key] = value
end
escape = opts.delete(:escape)
if code and !File.exist?(code) and escape != false
tmpfile = tmp("rubyexe.rb")
File.open(tmpfile, "w") { |f| f.write(code) }
code = tmpfile
end
begin
platform_is_not :opal do
`#{ruby_cmd(code, opts)}`
end
ensure
saved_env.each { |key, value| ENV[key] = value }
env.keys.each do |key|
key = key.to_s
ENV.delete key unless saved_env.key? key
end
File.delete tmpfile if tmpfile
end
end
def ruby_cmd(code, opts = {})
body = code
if opts[:escape]
raise "escape: true is no longer supported in ruby_cmd, use ruby_exe or a fixture"
end
if code and !File.exist?(code)
body = "-e #{code.inspect}"
end
[RUBY_EXE, opts[:options], body, opts[:args]].compact.join(' ')
end
unless Object.const_defined?(:RUBY_EXE) and RUBY_EXE
RUBY_EXE = resolve_ruby_exe
end
end

View file

@ -0,0 +1,17 @@
module ScratchPad
def self.clear
@record = nil
end
def self.record(arg)
@record = arg
end
def self.<<(arg)
@record << arg
end
def self.recorded
@record
end
end

View file

@ -0,0 +1,45 @@
# Creates a temporary directory in the current working directory
# for temporary files created while running the specs. All specs
# should clean up any temporary files created so that the temp
# directory is empty when the process exits.
SPEC_TEMP_DIR = File.expand_path(ENV["SPEC_TEMP_DIR"] || "rubyspec_temp")
SPEC_TEMP_UNIQUIFIER = "0"
SPEC_TEMP_DIR_PID = Process.pid
at_exit do
begin
if SPEC_TEMP_DIR_PID == Process.pid
Dir.delete SPEC_TEMP_DIR if File.directory? SPEC_TEMP_DIR
end
rescue SystemCallError
STDERR.puts <<-EOM
-----------------------------------------------------
The rubyspec temp directory is not empty. Ensure that
all specs are cleaning up temporary files:
#{SPEC_TEMP_DIR}
-----------------------------------------------------
EOM
rescue Object => e
STDERR.puts "failed to remove spec temp directory"
STDERR.puts e.message
end
end
class Object
def tmp(name, uniquify=true)
Dir.mkdir SPEC_TEMP_DIR unless Dir.exist? SPEC_TEMP_DIR
if uniquify and !name.empty?
slash = name.rindex "/"
index = slash ? slash + 1 : 0
name.insert index, "#{SPEC_TEMP_UNIQUIFIER.succ!}-"
end
File.join SPEC_TEMP_DIR, name
end
end

View file

@ -0,0 +1,35 @@
require 'mspec/matchers/base'
require 'mspec/matchers/be_an_instance_of'
require 'mspec/matchers/be_ancestor_of'
require 'mspec/matchers/be_close'
require 'mspec/matchers/be_computed_by'
require 'mspec/matchers/be_empty'
require 'mspec/matchers/be_false'
require 'mspec/matchers/be_kind_of'
require 'mspec/matchers/be_nan'
require 'mspec/matchers/be_nil'
require 'mspec/matchers/be_true'
require 'mspec/matchers/be_true_or_false'
require 'mspec/matchers/complain'
require 'mspec/matchers/eql'
require 'mspec/matchers/equal'
require 'mspec/matchers/equal_element'
require 'mspec/matchers/have_constant'
require 'mspec/matchers/have_class_variable'
require 'mspec/matchers/have_instance_method'
require 'mspec/matchers/have_instance_variable'
require 'mspec/matchers/have_method'
require 'mspec/matchers/have_private_instance_method'
require 'mspec/matchers/have_private_method'
require 'mspec/matchers/have_protected_instance_method'
require 'mspec/matchers/have_public_instance_method'
require 'mspec/matchers/have_singleton_method'
require 'mspec/matchers/include'
require 'mspec/matchers/infinity'
require 'mspec/matchers/match_yaml'
require 'mspec/matchers/raise_error'
require 'mspec/matchers/output'
require 'mspec/matchers/output_to_fd'
require 'mspec/matchers/respond_to'
require 'mspec/matchers/signed_zero'
require 'mspec/matchers/block_caller'

View file

@ -0,0 +1,95 @@
class SpecPositiveOperatorMatcher
def initialize(actual)
@actual = actual
end
def ==(expected)
unless @actual == expected
SpecExpectation.fail_with("Expected #{@actual.pretty_inspect}",
"to equal #{expected.pretty_inspect}")
end
end
def <(expected)
unless @actual < expected
SpecExpectation.fail_with("Expected #{@actual.pretty_inspect}",
"to be less than #{expected.pretty_inspect}")
end
end
def <=(expected)
unless @actual <= expected
SpecExpectation.fail_with("Expected #{@actual.pretty_inspect}",
"to be less than or equal to #{expected.pretty_inspect}")
end
end
def >(expected)
unless @actual > expected
SpecExpectation.fail_with("Expected #{@actual.pretty_inspect}",
"to be greater than #{expected.pretty_inspect}")
end
end
def >=(expected)
unless @actual >= expected
SpecExpectation.fail_with("Expected #{@actual.pretty_inspect}",
"to be greater than or equal to #{expected.pretty_inspect}")
end
end
def =~(expected)
unless @actual =~ expected
SpecExpectation.fail_with("Expected #{@actual.pretty_inspect}",
"to match #{expected.pretty_inspect}")
end
end
end
class SpecNegativeOperatorMatcher
def initialize(actual)
@actual = actual
end
def ==(expected)
if @actual == expected
SpecExpectation.fail_with("Expected #{@actual.pretty_inspect}",
"not to equal #{expected.pretty_inspect}")
end
end
def <(expected)
if @actual < expected
SpecExpectation.fail_with("Expected #{@actual.pretty_inspect}",
"not to be less than #{expected.pretty_inspect}")
end
end
def <=(expected)
if @actual <= expected
SpecExpectation.fail_with("Expected #{@actual.pretty_inspect}",
"not to be less than or equal to #{expected.pretty_inspect}")
end
end
def >(expected)
if @actual > expected
SpecExpectation.fail_with("Expected #{@actual.pretty_inspect}",
"not to be greater than #{expected.pretty_inspect}")
end
end
def >=(expected)
if @actual >= expected
SpecExpectation.fail_with("Expected #{@actual.pretty_inspect}",
"not to be greater than or equal to #{expected.pretty_inspect}")
end
end
def =~(expected)
if @actual =~ expected
SpecExpectation.fail_with("Expected #{@actual.pretty_inspect}",
"not to match #{expected.pretty_inspect}")
end
end
end

View file

@ -0,0 +1,26 @@
class BeAnInstanceOfMatcher
def initialize(expected)
@expected = expected
end
def matches?(actual)
@actual = actual
@actual.instance_of?(@expected)
end
def failure_message
["Expected #{@actual.inspect} (#{@actual.class})",
"to be an instance of #{@expected}"]
end
def negative_failure_message
["Expected #{@actual.inspect} (#{@actual.class})",
"not to be an instance of #{@expected}"]
end
end
class Object
def be_an_instance_of(expected)
BeAnInstanceOfMatcher.new(expected)
end
end

View file

@ -0,0 +1,24 @@
class BeAncestorOfMatcher
def initialize(expected)
@expected = expected
end
def matches?(actual)
@actual = actual
@expected.ancestors.include? @actual
end
def failure_message
["Expected #{@actual}", "to be an ancestor of #{@expected}"]
end
def negative_failure_message
["Expected #{@actual}", "not to be an ancestor of #{@expected}"]
end
end
class Object
def be_ancestor_of(expected)
BeAncestorOfMatcher.new(expected)
end
end

View file

@ -0,0 +1,27 @@
TOLERANCE = 0.00003 unless Object.const_defined?(:TOLERANCE)
class BeCloseMatcher
def initialize(expected, tolerance)
@expected = expected
@tolerance = tolerance
end
def matches?(actual)
@actual = actual
(@actual - @expected).abs < @tolerance
end
def failure_message
["Expected #{@expected}", "to be within +/- #{@tolerance} of #{@actual}"]
end
def negative_failure_message
["Expected #{@expected}", "not to be within +/- #{@tolerance} of #{@actual}"]
end
end
class Object
def be_close(expected, tolerance)
BeCloseMatcher.new(expected, tolerance)
end
end

View file

@ -0,0 +1,37 @@
class BeComputedByMatcher
def initialize(sym, *args)
@method = sym
@args = args
end
def matches?(array)
array.each do |line|
@receiver = line.shift
@value = line.pop
@arguments = line
@arguments += @args
@actual = @receiver.send(@method, *@arguments)
return false unless @actual == @value
end
return true
end
def method_call
method_call = "#{@receiver.inspect}.#{@method}"
unless @arguments.empty?
method_call = "#{method_call} from #{@arguments.map { |x| x.inspect }.join(", ")}"
end
method_call
end
def failure_message
["Expected #{@value.inspect}", "to be computed by #{method_call} (computed #{@actual.inspect} instead)"]
end
end
class Object
def be_computed_by(sym, *args)
BeComputedByMatcher.new(sym, *args)
end
end

View file

@ -0,0 +1,20 @@
class BeEmptyMatcher
def matches?(actual)
@actual = actual
@actual.empty?
end
def failure_message
["Expected #{@actual.inspect}", "to be empty"]
end
def negative_failure_message
["Expected #{@actual.inspect}", "not to be empty"]
end
end
class Object
def be_empty
BeEmptyMatcher.new
end
end

View file

@ -0,0 +1,20 @@
class BeFalseMatcher
def matches?(actual)
@actual = actual
@actual == false
end
def failure_message
["Expected #{@actual.inspect}", "to be false"]
end
def negative_failure_message
["Expected #{@actual.inspect}", "not to be false"]
end
end
class Object
def be_false
BeFalseMatcher.new
end
end

View file

@ -0,0 +1,24 @@
class BeKindOfMatcher
def initialize(expected)
@expected = expected
end
def matches?(actual)
@actual = actual
@actual.is_a?(@expected)
end
def failure_message
["Expected #{@actual.inspect} (#{@actual.class})", "to be kind of #{@expected}"]
end
def negative_failure_message
["Expected #{@actual.inspect} (#{@actual.class})", "not to be kind of #{@expected}"]
end
end
class Object
def be_kind_of(expected)
BeKindOfMatcher.new(expected)
end
end

View file

@ -0,0 +1,20 @@
class BeNaNMatcher
def matches?(actual)
@actual = actual
@actual.kind_of?(Float) && @actual.nan?
end
def failure_message
["Expected #{@actual}", "to be NaN"]
end
def negative_failure_message
["Expected #{@actual}", "not to be NaN"]
end
end
class Object
def be_nan
BeNaNMatcher.new
end
end

View file

@ -0,0 +1,20 @@
class BeNilMatcher
def matches?(actual)
@actual = actual
@actual.nil?
end
def failure_message
["Expected #{@actual.inspect}", "to be nil"]
end
def negative_failure_message
["Expected #{@actual.inspect}", "not to be nil"]
end
end
class Object
def be_nil
BeNilMatcher.new
end
end

View file

@ -0,0 +1,20 @@
class BeTrueMatcher
def matches?(actual)
@actual = actual
@actual == true
end
def failure_message
["Expected #{@actual.inspect}", "to be true"]
end
def negative_failure_message
["Expected #{@actual.inspect}", "not to be true"]
end
end
class Object
def be_true
BeTrueMatcher.new
end
end

View file

@ -0,0 +1,20 @@
class BeTrueOrFalseMatcher
def matches?(actual)
@actual = actual
@actual == true || @actual == false
end
def failure_message
["Expected #{@actual.inspect}", "to be true or false"]
end
def negative_failure_message
["Expected #{@actual.inspect}", "not to be true or false"]
end
end
class Object
def be_true_or_false
BeTrueOrFalseMatcher.new
end
end

View file

@ -0,0 +1,35 @@
class BlockingMatcher
def matches?(block)
started = false
blocking = true
thread = Thread.new do
started = true
block.call
blocking = false
end
while !started and status = thread.status and status != "sleep"
Thread.pass
end
thread.kill
thread.join
blocking
end
def failure_message
['Expected the given Proc', 'to block the caller']
end
def negative_failure_message
['Expected the given Proc', 'to not block the caller']
end
end
class Object
def block_caller(timeout = 0.1)
BlockingMatcher.new
end
end

View file

@ -0,0 +1,56 @@
require 'mspec/helpers/io'
class ComplainMatcher
def initialize(complaint)
@complaint = complaint
end
def matches?(proc)
@saved_err = $stderr
@stderr = $stderr = IOStub.new
@verbose = $VERBOSE
$VERBOSE = false
proc.call
unless @complaint.nil?
case @complaint
when Regexp
return false unless $stderr =~ @complaint
else
return false unless $stderr == @complaint
end
end
return $stderr.empty? ? false : true
ensure
$VERBOSE = @verbose
$stderr = @saved_err
end
def failure_message
if @complaint.nil?
["Expected a warning", "but received none"]
elsif @complaint.kind_of? Regexp
["Expected warning to match: #{@complaint.inspect}", "but got: #{@stderr.chomp.inspect}"]
else
["Expected warning: #{@complaint.inspect}", "but got: #{@stderr.chomp.inspect}"]
end
end
def negative_failure_message
if @complaint.nil?
["Unexpected warning: ", @stderr.chomp.inspect]
elsif @complaint.kind_of? Regexp
["Expected warning not to match: #{@complaint.inspect}", "but got: #{@stderr.chomp.inspect}"]
else
["Expected warning: #{@complaint.inspect}", "but got: #{@stderr.chomp.inspect}"]
end
end
end
class Object
def complain(complaint=nil)
ComplainMatcher.new(complaint)
end
end

View file

@ -0,0 +1,26 @@
class EqlMatcher
def initialize(expected)
@expected = expected
end
def matches?(actual)
@actual = actual
@actual.eql?(@expected)
end
def failure_message
["Expected #{@actual.pretty_inspect}",
"to have same value and type as #{@expected.pretty_inspect}"]
end
def negative_failure_message
["Expected #{@actual.pretty_inspect}",
"not to have same value or type as #{@expected.pretty_inspect}"]
end
end
class Object
def eql(expected)
EqlMatcher.new(expected)
end
end

View file

@ -0,0 +1,26 @@
class EqualMatcher
def initialize(expected)
@expected = expected
end
def matches?(actual)
@actual = actual
@actual.equal?(@expected)
end
def failure_message
["Expected #{@actual.pretty_inspect}",
"to be identical to #{@expected.pretty_inspect}"]
end
def negative_failure_message
["Expected #{@actual.pretty_inspect}",
"not to be identical to #{@expected.pretty_inspect}"]
end
end
class Object
def equal(expected)
EqualMatcher.new(expected)
end
end

View file

@ -0,0 +1,78 @@
class EqualElementMatcher
def initialize(element, attributes = nil, content = nil, options = {})
@element = element
@attributes = attributes
@content = content
@options = options
end
def matches?(actual)
@actual = actual
matched = true
if @options[:not_closed]
matched &&= actual =~ /^#{Regexp.quote("<" + @element)}.*#{Regexp.quote(">" + (@content || ''))}$/
else
matched &&= actual =~ /^#{Regexp.quote("<" + @element)}/
matched &&= actual =~ /#{Regexp.quote("</" + @element + ">")}$/
matched &&= actual =~ /#{Regexp.quote(">" + @content + "</")}/ if @content
end
if @attributes
if @attributes.empty?
matched &&= actual.scan(/\w+\=\"(.*)\"/).size == 0
else
@attributes.each do |key, value|
if value == true
matched &&= (actual.scan(/#{Regexp.quote(key)}(\s|>)/).size == 1)
else
matched &&= (actual.scan(%Q{ #{key}="#{value}"}).size == 1)
end
end
end
end
!!matched
end
def failure_message
["Expected #{@actual.pretty_inspect}",
"to be a '#{@element}' element with #{attributes_for_failure_message} and #{content_for_failure_message}"]
end
def negative_failure_message
["Expected #{@actual.pretty_inspect}",
"not to be a '#{@element}' element with #{attributes_for_failure_message} and #{content_for_failure_message}"]
end
def attributes_for_failure_message
if @attributes
if @attributes.empty?
"no attributes"
else
@attributes.inject([]) { |memo, n| memo << %Q{#{n[0]}="#{n[1]}"} }.join(" ")
end
else
"any attributes"
end
end
def content_for_failure_message
if @content
if @content.empty?
"no content"
else
"#{@content.inspect} as content"
end
else
"any content"
end
end
end
class Object
def equal_element(*args)
EqualElementMatcher.new(*args)
end
end

View file

@ -0,0 +1,12 @@
require 'mspec/matchers/variable'
class HaveClassVariableMatcher < VariableMatcher
self.variables_method = :class_variables
self.description = 'class variable'
end
class Object
def have_class_variable(variable)
HaveClassVariableMatcher.new(variable)
end
end

View file

@ -0,0 +1,12 @@
require 'mspec/matchers/variable'
class HaveConstantMatcher < VariableMatcher
self.variables_method = :constants
self.description = 'constant'
end
class Object
def have_constant(variable)
HaveConstantMatcher.new(variable)
end
end

View file

@ -0,0 +1,24 @@
require 'mspec/matchers/method'
class HaveInstanceMethodMatcher < MethodMatcher
def matches?(mod)
@mod = mod
mod.instance_methods(@include_super).include? @method
end
def failure_message
["Expected #{@mod} to have instance method '#{@method.to_s}'",
"but it does not"]
end
def negative_failure_message
["Expected #{@mod} NOT to have instance method '#{@method.to_s}'",
"but it does"]
end
end
class Object
def have_instance_method(method, include_super=true)
HaveInstanceMethodMatcher.new method, include_super
end
end

View file

@ -0,0 +1,12 @@
require 'mspec/matchers/variable'
class HaveInstanceVariableMatcher < VariableMatcher
self.variables_method = :instance_variables
self.description = 'instance variable'
end
class Object
def have_instance_variable(variable)
HaveInstanceVariableMatcher.new(variable)
end
end

View file

@ -0,0 +1,24 @@
require 'mspec/matchers/method'
class HaveMethodMatcher < MethodMatcher
def matches?(mod)
@mod = mod
@mod.methods(@include_super).include? @method
end
def failure_message
["Expected #{@mod} to have method '#{@method.to_s}'",
"but it does not"]
end
def negative_failure_message
["Expected #{@mod} NOT to have method '#{@method.to_s}'",
"but it does"]
end
end
class Object
def have_method(method, include_super=true)
HaveMethodMatcher.new method, include_super
end
end

View file

@ -0,0 +1,24 @@
require 'mspec/matchers/method'
class HavePrivateInstanceMethodMatcher < MethodMatcher
def matches?(mod)
@mod = mod
mod.private_instance_methods(@include_super).include? @method
end
def failure_message
["Expected #{@mod} to have private instance method '#{@method.to_s}'",
"but it does not"]
end
def negative_failure_message
["Expected #{@mod} NOT to have private instance method '#{@method.to_s}'",
"but it does"]
end
end
class Object
def have_private_instance_method(method, include_super=true)
HavePrivateInstanceMethodMatcher.new method, include_super
end
end

View file

@ -0,0 +1,24 @@
require 'mspec/matchers/method'
class HavePrivateMethodMatcher < MethodMatcher
def matches?(mod)
@mod = mod
mod.private_methods(@include_super).include? @method
end
def failure_message
["Expected #{@mod} to have private method '#{@method.to_s}'",
"but it does not"]
end
def negative_failure_message
["Expected #{@mod} NOT to have private method '#{@method.to_s}'",
"but it does"]
end
end
class Object
def have_private_method(method, include_super=true)
HavePrivateMethodMatcher.new method, include_super
end
end

View file

@ -0,0 +1,24 @@
require 'mspec/matchers/method'
class HaveProtectedInstanceMethodMatcher < MethodMatcher
def matches?(mod)
@mod = mod
mod.protected_instance_methods(@include_super).include? @method
end
def failure_message
["Expected #{@mod} to have protected instance method '#{@method.to_s}'",
"but it does not"]
end
def negative_failure_message
["Expected #{@mod} NOT to have protected instance method '#{@method.to_s}'",
"but it does"]
end
end
class Object
def have_protected_instance_method(method, include_super=true)
HaveProtectedInstanceMethodMatcher.new method, include_super
end
end

View file

@ -0,0 +1,24 @@
require 'mspec/matchers/method'
class HavePublicInstanceMethodMatcher < MethodMatcher
def matches?(mod)
@mod = mod
mod.public_instance_methods(@include_super).include? @method
end
def failure_message
["Expected #{@mod} to have public instance method '#{@method.to_s}'",
"but it does not"]
end
def negative_failure_message
["Expected #{@mod} NOT to have public instance method '#{@method.to_s}'",
"but it does"]
end
end
class Object
def have_public_instance_method(method, include_super=true)
HavePublicInstanceMethodMatcher.new method, include_super
end
end

View file

@ -0,0 +1,24 @@
require 'mspec/matchers/method'
class HaveSingletonMethodMatcher < MethodMatcher
def matches?(obj)
@obj = obj
obj.singleton_methods(@include_super).include? @method
end
def failure_message
["Expected #{@obj} to have singleton method '#{@method.to_s}'",
"but it does not"]
end
def negative_failure_message
["Expected #{@obj} NOT to have singleton method '#{@method.to_s}'",
"but it does"]
end
end
class Object
def have_singleton_method(method, include_super=true)
HaveSingletonMethodMatcher.new method, include_super
end
end

View file

@ -0,0 +1,32 @@
class IncludeMatcher
def initialize(*expected)
@expected = expected
end
def matches?(actual)
@actual = actual
@expected.each do |e|
@element = e
unless @actual.include?(e)
return false
end
end
return true
end
def failure_message
["Expected #{@actual.inspect}", "to include #{@element.inspect}"]
end
def negative_failure_message
["Expected #{@actual.inspect}", "not to include #{@element.inspect}"]
end
end
# Cannot override #include at the toplevel in MRI
module MSpec
def include(*expected)
IncludeMatcher.new(*expected)
end
module_function :include
end

View file

@ -0,0 +1,28 @@
class InfinityMatcher
def initialize(expected_sign)
@expected_sign = expected_sign
end
def matches?(actual)
@actual = actual
@actual.kind_of?(Float) && @actual.infinite? == @expected_sign
end
def failure_message
["Expected #{@actual}", "to be #{"-" if @expected_sign == -1}Infinity"]
end
def negative_failure_message
["Expected #{@actual}", "not to be #{"-" if @expected_sign == -1}Infinity"]
end
end
class Object
def be_positive_infinity
InfinityMatcher.new(1)
end
def be_negative_infinity
InfinityMatcher.new(-1)
end
end

View file

@ -0,0 +1,47 @@
class MatchYAMLMatcher
def initialize(expected)
if valid_yaml?(expected)
@expected = expected
else
@expected = expected.to_yaml
end
end
def matches?(actual)
@actual = actual
clean_yaml(@actual) == clean_yaml(@expected)
end
def failure_message
["Expected #{@actual.inspect}", " to match #{@expected.inspect}"]
end
def negative_failure_message
["Expected #{@actual.inspect}", " to match #{@expected.inspect}"]
end
protected
def clean_yaml(yaml)
yaml.gsub(/([^-]|^---)\s+\n/, "\\1\n").sub(/\n\.\.\.\n$/, "\n")
end
def valid_yaml?(obj)
require 'yaml'
begin
YAML.load(obj)
rescue
false
else
true
end
end
end
class Object
def match_yaml(expected)
MatchYAMLMatcher.new(expected)
end
end

View file

@ -0,0 +1,10 @@
class MethodMatcher
def initialize(method, include_super=true)
@include_super = include_super
@method = method.to_sym
end
def matches?(mod)
raise Exception, "define #matches? in the subclass"
end
end

View file

@ -0,0 +1,67 @@
require 'mspec/helpers/io'
class OutputMatcher
def initialize(stdout, stderr)
@out = stdout
@err = stderr
end
def matches?(proc)
@saved_out = $stdout
@saved_err = $stderr
@stdout = $stdout = IOStub.new
@stderr = $stderr = IOStub.new
proc.call
unless @out.nil?
case @out
when Regexp
return false unless $stdout =~ @out
else
return false unless $stdout == @out
end
end
unless @err.nil?
case @err
when Regexp
return false unless $stderr =~ @err
else
return false unless $stderr == @err
end
end
return true
ensure
$stdout = @saved_out
$stderr = @saved_err
end
def failure_message
expected_out = "\n"
actual_out = "\n"
unless @out.nil?
expected_out += " $stdout: #{@out.inspect}\n"
actual_out += " $stdout: #{@stdout.inspect}\n"
end
unless @err.nil?
expected_out += " $stderr: #{@err.inspect}\n"
actual_out += " $stderr: #{@stderr.inspect}\n"
end
["Expected:#{expected_out}", " got:#{actual_out}"]
end
def negative_failure_message
out = ""
out += " $stdout: #{@stdout.chomp.dump}\n" unless @out.nil?
out += " $stderr: #{@stderr.chomp.dump}\n" unless @err.nil?
["Expected output not to be:\n", out]
end
end
class Object
def output(stdout=nil, stderr=nil)
OutputMatcher.new(stdout, stderr)
end
end

View file

@ -0,0 +1,71 @@
require 'mspec/helpers/tmp'
# Lower-level output speccing mechanism for a single
# output stream. Unlike OutputMatcher which provides
# methods to capture the output, we actually replace
# the FD itself so that there is no reliance on a
# certain method being used.
class OutputToFDMatcher
def initialize(expected, to)
@to, @expected = to, expected
case @to
when STDOUT
@to_name = "STDOUT"
when STDERR
@to_name = "STDERR"
when IO
@to_name = @to.object_id.to_s
else
raise ArgumentError, "#{@to.inspect} is not a supported output target"
end
end
def with_tmp
path = tmp("mspec_output_to_#{$$}_#{Time.now.to_i}")
File.open(path, 'w+') { |io|
yield(io)
}
ensure
File.delete path if path
end
def matches?(block)
old_to = @to.dup
with_tmp do |out|
# Replacing with a file handle so that Readline etc. work
@to.reopen out
begin
block.call
ensure
@to.reopen old_to
old_to.close
end
out.rewind
@actual = out.read
case @expected
when Regexp
!(@actual =~ @expected).nil?
else
@actual == @expected
end
end
end
def failure_message()
["Expected (#{@to_name}): #{@expected.inspect}\n",
"#{'but got'.rjust(@to_name.length + 10)}: #{@actual.inspect}\nBacktrace"]
end
def negative_failure_message()
["Expected output (#{@to_name}) to NOT be:\n", @actual.inspect]
end
end
class Object
def output_to_fd(what, where = STDOUT)
OutputToFDMatcher.new what, where
end
end

View file

@ -0,0 +1,79 @@
require 'mspec/utils/deprecate'
class RaiseErrorMatcher
def initialize(exception, message, &block)
@exception = exception
@message = message
@block = block
end
def matches?(proc)
@result = proc.call
return false
rescue Exception => @actual
if matching_exception?(@actual)
return true
else
raise @actual
end
end
def matching_exception?(exc)
return false unless @exception === exc
if @message then
case @message
when String
return false if @message != exc.message
when Regexp
return false if @message !~ exc.message
end
end
# The block has its own expectations and will throw an exception if it fails
@block[exc] if @block
return true
end
def exception_class_and_message(exception_class, message)
if message
"#{exception_class} (#{message})"
else
"#{exception_class}"
end
end
def format_expected_exception
exception_class_and_message(@exception, @message)
end
def format_exception(exception)
exception_class_and_message(exception.class, exception.message)
end
def failure_message
message = ["Expected #{format_expected_exception}"]
if @actual then
message << "but got #{format_exception(@actual)}"
else
message << "but no exception was raised (#{@result.pretty_inspect.chomp} was returned)"
end
message
end
def negative_failure_message
message = ["Expected to not get #{format_expected_exception}", ""]
unless @actual.class == @exception
message[1] = "but got #{format_exception(@actual)}"
end
message
end
end
class Object
def raise_error(exception=Exception, message=nil, &block)
RaiseErrorMatcher.new(exception, message, &block)
end
end

View file

@ -0,0 +1,24 @@
class RespondToMatcher
def initialize(expected)
@expected = expected
end
def matches?(actual)
@actual = actual
@actual.respond_to?(@expected)
end
def failure_message
["Expected #{@actual.inspect} (#{@actual.class})", "to respond to #{@expected}"]
end
def negative_failure_message
["Expected #{@actual.inspect} (#{@actual.class})", "not to respond to #{@expected}"]
end
end
class Object
def respond_to(expected)
RespondToMatcher.new(expected)
end
end

View file

@ -0,0 +1,28 @@
class SignedZeroMatcher
def initialize(expected_sign)
@expected_sign = expected_sign
end
def matches?(actual)
@actual = actual
(1.0/actual).infinite? == @expected_sign
end
def failure_message
["Expected #{@actual}", "to be #{"-" if @expected_sign == -1}0.0"]
end
def negative_failure_message
["Expected #{@actual}", "not to be #{"-" if @expected_sign == -1}0.0"]
end
end
class Object
def be_positive_zero
SignedZeroMatcher.new(1)
end
def be_negative_zero
SignedZeroMatcher.new(-1)
end
end

View file

@ -0,0 +1,24 @@
class VariableMatcher
class << self
attr_accessor :variables_method, :description
end
def initialize(variable)
@variable = variable.to_sym
end
def matches?(object)
@object = object
@object.send(self.class.variables_method).include? @variable
end
def failure_message
["Expected #{@object} to have #{self.class.description} '#{@variable}'",
"but it does not"]
end
def negative_failure_message
["Expected #{@object} NOT to have #{self.class.description} '#{@variable}'",
"but it does"]
end
end

View file

@ -0,0 +1,3 @@
require 'mspec/mocks/mock'
require 'mspec/mocks/proxy'
require 'mspec/mocks/object'

View file

@ -0,0 +1,197 @@
require 'mspec/expectations/expectations'
class Object
alias_method :__mspec_object_id__, :object_id
end
module Mock
def self.reset
@mocks = @stubs = @objects = nil
end
def self.objects
@objects ||= {}
end
def self.mocks
@mocks ||= Hash.new { |h,k| h[k] = [] }
end
def self.stubs
@stubs ||= Hash.new { |h,k| h[k] = [] }
end
def self.replaced_name(obj, sym)
:"__mspec_#{obj.__mspec_object_id__}_#{sym}__"
end
def self.replaced_key(obj, sym)
[replaced_name(obj, sym), sym]
end
def self.has_key?(keys, sym)
!!keys.find { |k| k.first == sym }
end
def self.replaced?(sym)
has_key?(mocks.keys, sym) or has_key?(stubs.keys, sym)
end
def self.clear_replaced(key)
mocks.delete key
stubs.delete key
end
def self.mock_respond_to?(obj, sym, include_private = false)
name = replaced_name(obj, :respond_to?)
if replaced? name
obj.__send__ name, sym, include_private
else
obj.respond_to? sym, include_private
end
end
def self.install_method(obj, sym, type=nil)
meta = obj.singleton_class
key = replaced_key obj, sym
sym = sym.to_sym
if (sym == :respond_to? or mock_respond_to?(obj, sym, true)) and !replaced?(key.first)
meta.__send__ :alias_method, key.first, sym
end
meta.class_eval {
define_method(sym) do |*args, &block|
Mock.verify_call self, sym, *args, &block
end
}
proxy = MockProxy.new type
if proxy.mock?
MSpec.expectation
MSpec.actions :expectation, MSpec.current.state
end
if proxy.stub?
stubs[key].unshift proxy
else
mocks[key] << proxy
end
objects[key] = obj
proxy
end
def self.name_or_inspect(obj)
obj.instance_variable_get(:@name) || obj.inspect
end
def self.verify_count
mocks.each do |key, proxies|
obj = objects[key]
proxies.each do |proxy|
qualifier, count = proxy.count
pass = case qualifier
when :at_least
proxy.calls >= count
when :at_most
proxy.calls <= count
when :exactly
proxy.calls == count
when :any_number_of_times
true
else
false
end
unless pass
SpecExpectation.fail_with(
"Mock '#{name_or_inspect obj}' expected to receive '#{key.last}' " + \
"#{qualifier.to_s.sub('_', ' ')} #{count} times",
"but received it #{proxy.calls} times")
end
end
end
end
def self.verify_call(obj, sym, *args, &block)
compare = *args
compare = compare.first if compare.length <= 1
key = replaced_key obj, sym
[mocks, stubs].each do |proxies|
proxies[key].each do |proxy|
pass = case proxy.arguments
when :any_args
true
when :no_args
compare.nil?
else
proxy.arguments == compare
end
if proxy.yielding?
if block
proxy.yielding.each do |args_to_yield|
if block.arity == -1 || block.arity == args_to_yield.size
block.call(*args_to_yield)
else
SpecExpectation.fail_with(
"Mock '#{name_or_inspect obj}' asked to yield " + \
"|#{proxy.yielding.join(', ')}| on #{sym}\n",
"but a block with arity #{block.arity} was passed")
end
end
else
SpecExpectation.fail_with(
"Mock '#{name_or_inspect obj}' asked to yield " + \
"|[#{proxy.yielding.join('], [')}]| on #{sym}\n",
"but no block was passed")
end
end
if pass
proxy.called
if proxy.raising?
raise proxy.raising
else
return proxy.returning
end
end
end
end
if sym.to_sym == :respond_to?
mock_respond_to? obj, compare
else
SpecExpectation.fail_with("Mock '#{name_or_inspect obj}': method #{sym}\n",
"called with unexpected arguments (#{Array(compare).join(' ')})")
end
end
def self.cleanup
objects.each do |key, obj|
if obj.kind_of? MockIntObject
clear_replaced key
next
end
replaced = key.first
sym = key.last
meta = obj.singleton_class
if mock_respond_to? obj, replaced, true
meta.__send__ :alias_method, sym, replaced
meta.__send__ :remove_method, replaced
else
meta.__send__ :remove_method, sym
end
clear_replaced key
end
ensure
reset
end
end

View file

@ -0,0 +1,28 @@
require 'mspec/mocks/proxy'
class Object
def stub!(sym)
Mock.install_method self, sym, :stub
end
def should_receive(sym)
Mock.install_method self, sym
end
def should_not_receive(sym)
proxy = Mock.install_method self, sym
proxy.exactly(0).times
end
def mock(name, options={})
MockObject.new name, options
end
def mock_int(val)
MockIntObject.new(val)
end
def mock_numeric(name, options={})
NumericMockObject.new name, options
end
end

View file

@ -0,0 +1,186 @@
class MockObject
def initialize(name, options={})
@name = name
@null = options[:null_object]
end
def method_missing(sym, *args, &block)
@null ? self : super
end
private :method_missing
end
class NumericMockObject < Numeric
def initialize(name, options={})
@name = name
@null = options[:null_object]
end
def method_missing(sym, *args, &block)
@null ? self : super
end
def singleton_method_added(val)
end
end
class MockIntObject
def initialize(val)
@value = val
@calls = 0
key = [self, :to_int]
Mock.objects[key] = self
Mock.mocks[key] << self
end
attr_reader :calls
def to_int
@calls += 1
@value.to_int
end
def count
[:at_least, 1]
end
end
class MockProxy
attr_reader :raising, :yielding
def initialize(type=nil)
@multiple_returns = nil
@returning = nil
@raising = nil
@yielding = []
@arguments = :any_args
@type = type || :mock
end
def mock?
@type == :mock
end
def stub?
@type == :stub
end
def count
@count ||= mock? ? [:exactly, 1] : [:any_number_of_times, 0]
end
def arguments
@arguments
end
def returning
if @multiple_returns
if @returning.size == 1
@multiple_returns = false
return @returning = @returning.shift
end
return @returning.shift
end
@returning
end
def times
self
end
def calls
@calls ||= 0
end
def called
@calls = calls + 1
end
def exactly(n)
@count = [:exactly, n_times(n)]
self
end
def at_least(n)
@count = [:at_least, n_times(n)]
self
end
def at_most(n)
@count = [:at_most, n_times(n)]
self
end
def once
exactly 1
end
def twice
exactly 2
end
def any_number_of_times
@count = [:any_number_of_times, 0]
self
end
def with(*args)
raise ArgumentError, "you must specify the expected arguments" if args.empty?
if args.length == 1
@arguments = args.first
else
@arguments = args
end
self
end
def and_return(*args)
case args.size
when 0
@returning = nil
when 1
@returning = args[0]
else
@multiple_returns = true
@returning = args
count[1] = args.size if count[1] < args.size
end
self
end
def and_raise(exception)
if exception.kind_of? String
@raising = RuntimeError.new exception
else
@raising = exception
end
end
def raising?
@raising != nil
end
def and_yield(*args)
@yielding << args
self
end
def yielding?
!@yielding.empty?
end
private
def n_times(n)
case n
when :once
1
when :twice
2
else
Integer n
end
end
end

View file

@ -0,0 +1,12 @@
require 'mspec/mocks'
require 'mspec/runner/mspec'
require 'mspec/runner/context'
require 'mspec/runner/evaluate'
require 'mspec/runner/example'
require 'mspec/runner/exception'
require 'mspec/runner/object'
require 'mspec/runner/formatters'
require 'mspec/runner/actions'
require 'mspec/runner/filters'
require 'mspec/runner/shared'
require 'mspec/runner/tag'

View file

@ -0,0 +1,6 @@
require 'mspec/runner/actions/tally'
require 'mspec/runner/actions/timer'
require 'mspec/runner/actions/filter'
require 'mspec/runner/actions/tag'
require 'mspec/runner/actions/taglist'
require 'mspec/runner/actions/tagpurge'

View file

@ -0,0 +1,40 @@
require 'mspec/runner/filters/match'
# ActionFilter is a base class for actions that are triggered by
# specs that match the filter. The filter may be specified by
# strings that match spec descriptions or by tags for strings
# that match spec descriptions.
#
# Unlike TagFilter and RegexpFilter, ActionFilter instances do
# not affect the specs that are run. The filter is only used to
# trigger the action.
class ActionFilter
def initialize(tags=nil, descs=nil)
@tags = Array(tags)
descs = Array(descs)
@sfilter = descs.empty? ? nil : MatchFilter.new(nil, *descs)
@tfilter = nil
end
def ===(string)
@sfilter === string or @tfilter === string
end
def load
return if @tags.empty?
desc = MSpec.read_tags(@tags).map { |t| t.description }
return if desc.empty?
@tfilter = MatchFilter.new(nil, *desc)
end
def register
MSpec.register :load, self
end
def unregister
MSpec.unregister :load, self
end
end

View file

@ -0,0 +1,301 @@
# Adapted from ruby's test/lib/leakchecker.rb.
# Ruby's 2-clause BSDL follows.
# Copyright (C) 1993-2013 Yukihiro Matsumoto. All rights reserved.
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
class LeakChecker
def initialize
@fd_info = find_fds
@tempfile_info = find_tempfiles
@thread_info = find_threads
@env_info = find_env
@argv_info = find_argv
@encoding_info = find_encodings
end
def check(test_name)
@no_leaks = true
leaks = [
check_fd_leak(test_name),
check_tempfile_leak(test_name),
check_thread_leak(test_name),
check_process_leak(test_name),
check_env(test_name),
check_argv(test_name),
check_encodings(test_name)
]
GC.start if leaks.any?
return leaks.none?
end
private
def find_fds
fd_dir = "/proc/self/fd"
if File.directory?(fd_dir)
fds = Dir.open(fd_dir) {|d|
a = d.grep(/\A\d+\z/, &:to_i)
if d.respond_to? :fileno
a -= [d.fileno]
end
a
}
fds.sort
else
[]
end
end
def check_fd_leak(test_name)
leaked = false
live1 = @fd_info
if IO.respond_to?(:console) and (m = IO.method(:console)).arity.nonzero?
m[:close]
end
live2 = find_fds
fd_closed = live1 - live2
if !fd_closed.empty?
fd_closed.each {|fd|
puts "Closed file descriptor: #{test_name}: #{fd}"
}
end
fd_leaked = live2 - live1
if !fd_leaked.empty?
leaked = true
h = {}
ObjectSpace.each_object(IO) {|io|
inspect = io.inspect
begin
autoclose = io.autoclose?
fd = io.fileno
rescue IOError # closed IO object
next
end
(h[fd] ||= []) << [io, autoclose, inspect]
}
fd_leaked.each {|fd|
str = ''
if h[fd]
str << ' :'
h[fd].map {|io, autoclose, inspect|
s = ' ' + inspect
s << "(not-autoclose)" if !autoclose
s
}.sort.each {|s|
str << s
}
end
puts "Leaked file descriptor: #{test_name}: #{fd}#{str}"
}
#system("lsof -p #$$") if !fd_leaked.empty?
h.each {|fd, list|
next if list.length <= 1
if 1 < list.count {|io, autoclose, inspect| autoclose }
str = list.map {|io, autoclose, inspect| " #{inspect}" + (autoclose ? "(autoclose)" : "") }.sort.join
puts "Multiple autoclose IO object for a file descriptor:#{str}"
end
}
end
@fd_info = live2
return leaked
end
def extend_tempfile_counter
return if defined? LeakChecker::TempfileCounter
m = Module.new {
@count = 0
class << self
attr_accessor :count
end
def new(data)
LeakChecker::TempfileCounter.count += 1
super(data)
end
}
LeakChecker.const_set(:TempfileCounter, m)
class << Tempfile::Remover
prepend LeakChecker::TempfileCounter
end
end
def find_tempfiles(prev_count=-1)
return [prev_count, []] unless defined? Tempfile
extend_tempfile_counter
count = TempfileCounter.count
if prev_count == count
[prev_count, []]
else
tempfiles = ObjectSpace.each_object(Tempfile).find_all {|t| t.path }
[count, tempfiles]
end
end
def check_tempfile_leak(test_name)
return false unless defined? Tempfile
count1, initial_tempfiles = @tempfile_info
count2, current_tempfiles = find_tempfiles(count1)
leaked = false
tempfiles_leaked = current_tempfiles - initial_tempfiles
if !tempfiles_leaked.empty?
leaked = true
list = tempfiles_leaked.map {|t| t.inspect }.sort
list.each {|str|
puts "Leaked tempfile: #{test_name}: #{str}"
}
tempfiles_leaked.each {|t| t.close! }
end
@tempfile_info = [count2, initial_tempfiles]
return leaked
end
def find_threads
Thread.list.find_all {|t|
t != Thread.current && t.alive?
}
end
def check_thread_leak(test_name)
live1 = @thread_info
live2 = find_threads
thread_finished = live1 - live2
leaked = false
if !thread_finished.empty?
list = thread_finished.map {|t| t.inspect }.sort
list.each {|str|
puts "Finished thread: #{test_name}: #{str}"
}
end
thread_leaked = live2 - live1
if !thread_leaked.empty?
leaked = true
list = thread_leaked.map {|t| t.inspect }.sort
list.each {|str|
puts "Leaked thread: #{test_name}: #{str}"
}
end
@thread_info = live2
return leaked
end
def check_process_leak(test_name)
subprocesses_leaked = Process.waitall
subprocesses_leaked.each { |pid, status|
puts "Leaked subprocess: #{pid}: #{status}"
}
return !subprocesses_leaked.empty?
end
def find_env
ENV.to_h
end
def check_env(test_name)
old_env = @env_info
new_env = find_env
return false if old_env == new_env
(old_env.keys | new_env.keys).sort.each {|k|
if old_env.has_key?(k)
if new_env.has_key?(k)
if old_env[k] != new_env[k]
puts "Environment variable changed: #{test_name} : #{k.inspect} changed : #{old_env[k].inspect} -> #{new_env[k].inspect}"
end
else
puts "Environment variable changed: #{test_name} : #{k.inspect} deleted"
end
else
if new_env.has_key?(k)
puts "Environment variable changed: #{test_name} : #{k.inspect} added"
else
flunk "unreachable"
end
end
}
@env_info = new_env
return true
end
def find_argv
ARGV.map { |e| e.dup }
end
def check_argv(test_name)
old_argv = @argv_info
new_argv = find_argv
leaked = false
if new_argv != old_argv
puts "ARGV changed: #{test_name} : #{old_argv.inspect} to #{new_argv.inspect}"
@argv_info = new_argv
leaked = true
end
return leaked
end
def find_encodings
[Encoding.default_internal, Encoding.default_external]
end
def check_encodings(test_name)
old_internal, old_external = @encoding_info
new_internal, new_external = find_encodings
leaked = false
if new_internal != old_internal
leaked = true
puts "Encoding.default_internal changed: #{test_name} : #{old_internal} to #{new_internal}"
end
if new_external != old_external
leaked = true
puts "Encoding.default_external changed: #{test_name} : #{old_external} to #{new_external}"
end
@encoding_info = [new_internal, new_external]
return leaked
end
def puts(*args)
if @no_leaks
@no_leaks = false
print "\n"
end
super(*args)
end
end
class LeakCheckerAction
def register
MSpec.register :start, self
MSpec.register :after, self
end
def start
@checker = LeakChecker.new
end
def after(state)
unless @checker.check(state.description)
if state.example
puts state.example.source_location.join(':')
end
end
end
end

View file

@ -0,0 +1,133 @@
require 'mspec/runner/actions/filter'
# TagAction - Write tagged spec description string to a
# tag file associated with each spec file.
#
# The action is triggered by specs whose descriptions
# match the filter created with 'tags' and/or 'desc'
#
# The action fires in the :after event, after the spec
# had been run. The action fires if the outcome of
# running the spec matches 'outcome'.
#
# The arguments are:
#
# action: :add, :del
# outcome: :pass, :fail, :all
# tag: the tag to create/delete
# comment: the comment to create
# tags: zero or more tags to get matching
# spec description strings from
# desc: zero or more strings to match the
# spec description strings
class TagAction < ActionFilter
def initialize(action, outcome, tag, comment, tags=nil, descs=nil)
super tags, descs
@action = action
@outcome = outcome
@tag = tag
@comment = comment
@report = []
@exception = false
end
# Returns true if there are no _tag_ or _description_ filters. This
# means that a TagAction matches any example by default. Otherwise,
# returns true if either the _tag_ or the _description_ filter
# matches +string+.
def ===(string)
return true unless @sfilter or @tfilter
@sfilter === string or @tfilter === string
end
# Callback for the MSpec :before event. Resets the +#exception?+
# flag to false.
def before(state)
@exception = false
end
# Callback for the MSpec :exception event. Sets the +#exception?+
# flag to true.
def exception(exception)
@exception = true
end
# Callback for the MSpec :after event. Performs the tag action
# depending on the type of action and the outcome of evaluating
# the example. See +TagAction+ for a description of the actions.
def after(state)
if self === state.description and outcome?
tag = SpecTag.new
tag.tag = @tag
tag.comment = @comment
tag.description = state.description
case @action
when :add
changed = MSpec.write_tag tag
when :del
changed = MSpec.delete_tag tag
end
@report << state.description if changed
end
end
# Returns true if the result of evaluating the example matches
# the _outcome_ registered for this tag action. See +TagAction+
# for a description of the _outcome_ types.
def outcome?
@outcome == :all or
(@outcome == :pass and not exception?) or
(@outcome == :fail and exception?)
end
# Returns true if an exception was raised while evaluating the
# current example.
def exception?
@exception
end
def report
@report.join("\n") + "\n"
end
private :report
# Callback for the MSpec :finish event. Prints the actions
# performed while evaluating the examples.
def finish
case @action
when :add
if @report.empty?
print "\nTagAction: no specs were tagged with '#{@tag}'\n"
else
print "\nTagAction: specs tagged with '#{@tag}':\n\n"
print report
end
when :del
if @report.empty?
print "\nTagAction: no tags '#{@tag}' were deleted\n"
else
print "\nTagAction: tag '#{@tag}' deleted for specs:\n\n"
print report
end
end
end
def register
super
MSpec.register :before, self
MSpec.register :exception, self
MSpec.register :after, self
MSpec.register :finish, self
end
def unregister
super
MSpec.unregister :before, self
MSpec.unregister :exception, self
MSpec.unregister :after, self
MSpec.unregister :finish, self
end
end

View file

@ -0,0 +1,56 @@
require 'mspec/runner/actions/filter'
# TagListAction - prints out the descriptions for any specs
# tagged with +tags+. If +tags+ is an empty list, prints out
# descriptions for any specs that are tagged.
class TagListAction
def initialize(tags=nil)
@tags = tags.nil? || tags.empty? ? nil : Array(tags)
@filter = nil
end
# Returns true. This enables us to match any tag when loading
# tags from the file.
def include?(arg)
true
end
# Returns true if any tagged descriptions matches +string+.
def ===(string)
@filter === string
end
# Prints a banner about matching tagged specs.
def start
if @tags
print "\nListing specs tagged with #{@tags.map { |t| "'#{t}'" }.join(", ") }\n\n"
else
print "\nListing all tagged specs\n\n"
end
end
# Creates a MatchFilter for specific tags or for all tags.
def load
@filter = nil
desc = MSpec.read_tags(@tags || self).map { |t| t.description }
@filter = MatchFilter.new(nil, *desc) unless desc.empty?
end
# Prints the spec description if it matches the filter.
def after(state)
return unless self === state.description
print state.description, "\n"
end
def register
MSpec.register :start, self
MSpec.register :load, self
MSpec.register :after, self
end
def unregister
MSpec.unregister :start, self
MSpec.unregister :load, self
MSpec.unregister :after, self
end
end

Some files were not shown because too many files have changed in this diff Show more