diff options
Diffstat (limited to 'spec/mspec')
282 files changed, 22547 insertions, 0 deletions
diff --git a/spec/mspec/.rspec b/spec/mspec/.rspec new file mode 100644 index 0000000000..4e1e0d2f72 --- /dev/null +++ b/spec/mspec/.rspec @@ -0,0 +1 @@ +--color diff --git a/spec/mspec/Gemfile b/spec/mspec/Gemfile new file mode 100644 index 0000000000..617a995cad --- /dev/null +++ b/spec/mspec/Gemfile @@ -0,0 +1,4 @@ +source 'https://rubygems.org' + +gem "rake", "~> 12.3" +gem "rspec", "~> 3.0" diff --git a/spec/mspec/Gemfile.lock b/spec/mspec/Gemfile.lock new file mode 100644 index 0000000000..cd39906044 --- /dev/null +++ b/spec/mspec/Gemfile.lock @@ -0,0 +1,26 @@ +GEM + remote: https://rubygems.org/ + specs: + diff-lcs (1.4.4) + rake (12.3.3) + rspec (3.10.0) + rspec-core (~> 3.10.0) + rspec-expectations (~> 3.10.0) + rspec-mocks (~> 3.10.0) + rspec-core (3.10.1) + rspec-support (~> 3.10.0) + rspec-expectations (3.10.1) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.10.0) + rspec-mocks (3.10.2) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.10.0) + rspec-support (3.10.2) + +PLATFORMS + java + ruby + +DEPENDENCIES + rake (~> 12.3) + rspec (~> 3.0) diff --git a/spec/mspec/LICENSE b/spec/mspec/LICENSE new file mode 100644 index 0000000000..d581dd1c9f --- /dev/null +++ b/spec/mspec/LICENSE @@ -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. diff --git a/spec/mspec/README.md b/spec/mspec/README.md new file mode 100644 index 0000000000..94ab608031 --- /dev/null +++ b/spec/mspec/README.md @@ -0,0 +1,84 @@ +## Overview + +MSpec is a specialized framework that is syntax-compatible with RSpec 2 for +basic things like `describe`, `it` blocks and `before`, `after` actions. +MSpec contains additional features that assist in writing specs for +Ruby implementations in [ruby/spec](https://github.com/ruby/spec). + +MSpec attempts to use the simplest Ruby language features so that beginning +Ruby implementations can run the Ruby specs. For example, no file from the +standard library or RubyGems is necessary to run MSpec. + +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. + + 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. + + 5. MSpec support "tagging", that is excluding specs known as failing on + a particular Ruby implementation, and automatically adding and removing tags + while running the specs. + +## Requirements + +MSpec requires Ruby 2.6 or more recent. + +## 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 +``` + +## Development + +Use RSpec to run the MSpec specs. There are no plans currently to make the +MSpec specs runnable by MSpec: https://github.com/ruby/mspec/issues/19. + +After installing the gem dependencies, the specs can be run as follows: + +```bash +ruby -S bundle exec rspec +``` + +To run an individual spec file, use the following example: + +```bash +ruby -S bundle exec rspec spec/helpers/ruby_exe_spec.rb +``` + +## Documentation + +See [CONTRIBUTING.md](https://github.com/ruby/spec/blob/master/CONTRIBUTING.md) in ruby/spec +for a list of matchers and how to use `mspec`. + +## Source Code + +See https://github.com/ruby/mspec + +## License + +See the LICENSE in the source code. diff --git a/spec/mspec/Rakefile b/spec/mspec/Rakefile new file mode 100644 index 0000000000..6a9de7a95e --- /dev/null +++ b/spec/mspec/Rakefile @@ -0,0 +1,6 @@ +require 'bundler/setup' +require 'rspec/core/rake_task' + +RSpec::Core::RakeTask.new(:spec) + +task :default => :spec diff --git a/spec/mspec/bin/mkspec b/spec/mspec/bin/mkspec new file mode 100755 index 0000000000..00f1fdff47 --- /dev/null +++ b/spec/mspec/bin/mkspec @@ -0,0 +1,7 @@ +#!/usr/bin/env ruby + +$:.unshift File.expand_path('../../lib', __FILE__) + +require 'mspec/commands/mkspec' + +MkSpec.main diff --git a/spec/mspec/bin/mkspec.bat b/spec/mspec/bin/mkspec.bat new file mode 100755 index 0000000000..1073d20a9b --- /dev/null +++ b/spec/mspec/bin/mkspec.bat @@ -0,0 +1 @@ +@"ruby.exe" "%~dpn0" %* diff --git a/spec/mspec/bin/mspec b/spec/mspec/bin/mspec new file mode 100755 index 0000000000..5bd753c06d --- /dev/null +++ b/spec/mspec/bin/mspec @@ -0,0 +1,7 @@ +#!/usr/bin/env ruby + +$:.unshift File.expand_path('../../lib', __FILE__) + +require 'mspec/commands/mspec' + +MSpecMain.main(false) diff --git a/spec/mspec/bin/mspec-ci b/spec/mspec/bin/mspec-ci new file mode 100755 index 0000000000..d7cd50a827 --- /dev/null +++ b/spec/mspec/bin/mspec-ci @@ -0,0 +1,7 @@ +#!/usr/bin/env ruby + +$:.unshift File.expand_path('../../lib', __FILE__) + +require 'mspec/commands/mspec-ci' + +MSpecCI.main diff --git a/spec/mspec/bin/mspec-ci.bat b/spec/mspec/bin/mspec-ci.bat new file mode 100755 index 0000000000..1073d20a9b --- /dev/null +++ b/spec/mspec/bin/mspec-ci.bat @@ -0,0 +1 @@ +@"ruby.exe" "%~dpn0" %* diff --git a/spec/mspec/bin/mspec-run b/spec/mspec/bin/mspec-run new file mode 100755 index 0000000000..010ecefe35 --- /dev/null +++ b/spec/mspec/bin/mspec-run @@ -0,0 +1,7 @@ +#!/usr/bin/env ruby + +$:.unshift File.expand_path('../../lib', __FILE__) + +require 'mspec/commands/mspec-run' + +MSpecRun.main diff --git a/spec/mspec/bin/mspec-run.bat b/spec/mspec/bin/mspec-run.bat new file mode 100755 index 0000000000..1073d20a9b --- /dev/null +++ b/spec/mspec/bin/mspec-run.bat @@ -0,0 +1 @@ +@"ruby.exe" "%~dpn0" %* diff --git a/spec/mspec/bin/mspec-tag b/spec/mspec/bin/mspec-tag new file mode 100755 index 0000000000..a5f9fffaaa --- /dev/null +++ b/spec/mspec/bin/mspec-tag @@ -0,0 +1,7 @@ +#!/usr/bin/env ruby + +$:.unshift File.expand_path('../../lib', __FILE__) + +require 'mspec/commands/mspec-tag' + +MSpecTag.main diff --git a/spec/mspec/bin/mspec-tag.bat b/spec/mspec/bin/mspec-tag.bat new file mode 100755 index 0000000000..1073d20a9b --- /dev/null +++ b/spec/mspec/bin/mspec-tag.bat @@ -0,0 +1 @@ +@"ruby.exe" "%~dpn0" %* diff --git a/spec/mspec/bin/mspec.bat b/spec/mspec/bin/mspec.bat new file mode 100755 index 0000000000..1073d20a9b --- /dev/null +++ b/spec/mspec/bin/mspec.bat @@ -0,0 +1 @@ +@"ruby.exe" "%~dpn0" %* diff --git a/spec/mspec/lib/mspec.rb b/spec/mspec/lib/mspec.rb new file mode 100644 index 0000000000..d24abd96f1 --- /dev/null +++ b/spec/mspec/lib/mspec.rb @@ -0,0 +1,8 @@ +require 'mspec/utils/format' +require 'mspec/matchers' +require 'mspec/expectations' +require 'mspec/mocks' +require 'mspec/runner' +require 'mspec/guards' +require 'mspec/helpers' +require 'mspec/version' diff --git a/spec/mspec/lib/mspec/commands/mkspec.rb b/spec/mspec/lib/mspec/commands/mkspec.rb new file mode 100644 index 0000000000..f75e683b19 --- /dev/null +++ b/spec/mspec/lib/mspec/commands/mkspec.rb @@ -0,0 +1,143 @@ +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_relative '#{parents}spec_helper'" + 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 + command = "#{RbConfig.ruby} #{MSPEC_HOME}/bin/mspec-run --dry-run --unguarded -fs -e '#{meth}' #{file}" + puts "$ #{command}" if $DEBUG + out = `#{command}` + 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 + + def self.main + ENV['MSPEC_RUNNER'] = '1' + + script = new + script.options + script.run + end +end diff --git a/spec/mspec/lib/mspec/commands/mspec-ci.rb b/spec/mspec/lib/mspec/commands/mspec-ci.rb new file mode 100644 index 0000000000..8951572f69 --- /dev/null +++ b/spec/mspec/lib/mspec/commands/mspec-ci.rb @@ -0,0 +1,76 @@ +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.repeat + options.pretend + options.interrupt + options.timeout + + 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 diff --git a/spec/mspec/lib/mspec/commands/mspec-run.rb b/spec/mspec/lib/mspec/commands/mspec-run.rb new file mode 100644 index 0000000000..0fb338fa23 --- /dev/null +++ b/spec/mspec/lib/mspec/commands/mspec-run.rb @@ -0,0 +1,87 @@ +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.env + options.randomize + options.repeat + options.pretend + options.interrupt + options.timeout + + 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 Launchable" + options.launchable + + 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 diff --git a/spec/mspec/lib/mspec/commands/mspec-tag.rb b/spec/mspec/lib/mspec/commands/mspec-tag.rb new file mode 100644 index 0000000000..9ce9f048c6 --- /dev/null +++ b/spec/mspec/lib/mspec/commands/mspec-tag.rb @@ -0,0 +1,132 @@ +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.pretend + options.unguarded + options.interrupt + options.timeout + + 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 + config[:xtags] = [] + 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 + diff --git a/spec/mspec/lib/mspec/commands/mspec.rb b/spec/mspec/lib/mspec/commands/mspec.rb new file mode 100644 index 0000000000..a9d94ca354 --- /dev/null +++ b/spec/mspec/lib/mspec/commands/mspec.rb @@ -0,0 +1,113 @@ +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 = options + + 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("-j", "--multi", "Run multiple (possibly parallel) subprocesses") do + config[:multi] = true + 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) + require 'mspec/runner/formatters/multi' + formatter = config_formatter.extend(MultiFormatter) + + require 'mspec/runner/parallel' + processes = cores(@files.size) + ParallelRunner.new(@files, processes, formatter, argv).run + 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 + log = config[:options].include?('--error-output') ? $stdout : $stderr + log.puts "$ #{argv.join(' ')}" + log.flush + exec(*argv, close_others: false) + end + end +end diff --git a/spec/mspec/lib/mspec/expectations.rb b/spec/mspec/lib/mspec/expectations.rb new file mode 100644 index 0000000000..d07f959b27 --- /dev/null +++ b/spec/mspec/lib/mspec/expectations.rb @@ -0,0 +1,2 @@ +require 'mspec/expectations/expectations' +require 'mspec/expectations/should' diff --git a/spec/mspec/lib/mspec/expectations/expectations.rb b/spec/mspec/lib/mspec/expectations/expectations.rb new file mode 100644 index 0000000000..09852ab557 --- /dev/null +++ b/spec/mspec/lib/mspec/expectations/expectations.rb @@ -0,0 +1,39 @@ +class SpecExpectationNotMetError < StandardError +end + +class SpecExpectationNotFoundError < StandardError + def message + "No behavior expectation was found in the example" + end +end + +class SkippedSpecError < StandardError +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}\n#{actual_to_s}" + else + message = "#{expected_to_s} #{actual_to_s}" + end + raise SpecExpectationNotMetError, message + end + + def self.fail_predicate(receiver, predicate, args, block, result, expectation) + receiver_to_s = MSpec.format(receiver) + before_method = predicate.to_s =~ /^[a-z]/ ? "." : " " + predicate_to_s = "#{before_method}#{predicate}" + predicate_to_s += " " unless args.empty? + args_to_s = args.map { |arg| MSpec.format(arg) }.join(', ') + args_to_s += " { ... }" if block + result_to_s = MSpec.format(result) + raise SpecExpectationNotMetError, "Expected #{receiver_to_s}#{predicate_to_s}#{args_to_s}\n#{expectation} but was #{result_to_s}" + end + + def self.fail_single_arg_predicate(receiver, predicate, arg, result, expectation) + fail_predicate(receiver, predicate, [arg], nil, result, expectation) + end +end diff --git a/spec/mspec/lib/mspec/expectations/should.rb b/spec/mspec/lib/mspec/expectations/should.rb new file mode 100644 index 0000000000..c1790e0ac8 --- /dev/null +++ b/spec/mspec/lib/mspec/expectations/should.rb @@ -0,0 +1,41 @@ +class Object + NO_MATCHER_GIVEN = Object.new + + def should(matcher = NO_MATCHER_GIVEN, &block) + MSpec.expectation + state = MSpec.current.state + raise "should outside example" unless state + MSpec.actions :expectation, state + + if NO_MATCHER_GIVEN.equal?(matcher) + SpecPositiveOperatorMatcher.new(self) + else + # The block was given to #should syntactically, but it was intended for a matcher like #raise_error + matcher.block = block if block + + unless matcher.matches? self + expected, actual = matcher.failure_message + SpecExpectation.fail_with(expected, actual) + end + end + end + + def should_not(matcher = NO_MATCHER_GIVEN, &block) + MSpec.expectation + state = MSpec.current.state + raise "should_not outside example" unless state + MSpec.actions :expectation, state + + if NO_MATCHER_GIVEN.equal?(matcher) + SpecNegativeOperatorMatcher.new(self) + else + # The block was given to #should_not syntactically, but it was intended for the matcher + matcher.block = block if block + + if matcher.matches? self + expected, actual = matcher.negative_failure_message + SpecExpectation.fail_with(expected, actual) + end + end + end +end diff --git a/spec/mspec/lib/mspec/guards.rb b/spec/mspec/lib/mspec/guards.rb new file mode 100644 index 0000000000..454ac0c776 --- /dev/null +++ b/spec/mspec/lib/mspec/guards.rb @@ -0,0 +1,11 @@ +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' diff --git a/spec/mspec/lib/mspec/guards/block_device.rb b/spec/mspec/lib/mspec/guards/block_device.rb new file mode 100644 index 0000000000..ae736a2d4e --- /dev/null +++ b/spec/mspec/lib/mspec/guards/block_device.rb @@ -0,0 +1,16 @@ +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 + +def with_block_device(&block) + BlockDeviceGuard.new.run_if(:with_block_device, &block) +end diff --git a/spec/mspec/lib/mspec/guards/bug.rb b/spec/mspec/lib/mspec/guards/bug.rb new file mode 100644 index 0000000000..a6af0ef964 --- /dev/null +++ b/spec/mspec/lib/mspec/guards/bug.rb @@ -0,0 +1,29 @@ +require 'mspec/guards/version' + +class BugGuard < VersionGuard + def initialize(bug, requirement) + @bug = bug + if String === requirement + MSpec.deprecate "ruby_bug with a single version", 'an exclusive range ("2.1"..."2.3")' + super(FULL_RUBY_VERSION, requirement) + @requirement = SpecVersion.new requirement, true + else + super(FULL_RUBY_VERSION, requirement) + end + end + + def match? + return false if MSpec.mode? :no_ruby_bug + return false unless PlatformGuard.standard? + + if Range === @requirement + super + else + FULL_RUBY_VERSION <= @requirement + end + end +end + +def ruby_bug(bug, requirement, &block) + BugGuard.new(bug, requirement).run_unless(:ruby_bug, &block) +end diff --git a/spec/mspec/lib/mspec/guards/conflict.rb b/spec/mspec/lib/mspec/guards/conflict.rb new file mode 100644 index 0000000000..4930e5734d --- /dev/null +++ b/spec/mspec/lib/mspec/guards/conflict.rb @@ -0,0 +1,23 @@ +require 'mspec/guards/guard' +require 'mspec/utils/deprecate' + +class ConflictsGuard < SpecGuard + def initialize(*args) + MSpec.deprecate 'conflicts_with', 'guard -> { condition } do' + super(*args) + end + + 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 + +# 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 diff --git a/spec/mspec/lib/mspec/guards/endian.rb b/spec/mspec/lib/mspec/guards/endian.rb new file mode 100644 index 0000000000..79335a8933 --- /dev/null +++ b/spec/mspec/lib/mspec/guards/endian.rb @@ -0,0 +1,25 @@ +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 + +def big_endian(&block) + BigEndianGuard.new.run_if(:big_endian, &block) +end + +def little_endian(&block) + BigEndianGuard.new.run_unless(:little_endian, &block) +end diff --git a/spec/mspec/lib/mspec/guards/feature.rb b/spec/mspec/lib/mspec/guards/feature.rb new file mode 100644 index 0000000000..d4c6dd1cde --- /dev/null +++ b/spec/mspec/lib/mspec/guards/feature.rb @@ -0,0 +1,45 @@ +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 + +# 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 + +def without_feature(*features, &block) + FeatureGuard.new(*features).run_unless(:without_feature, &block) +end diff --git a/spec/mspec/lib/mspec/guards/guard.rb b/spec/mspec/lib/mspec/guards/guard.rb new file mode 100644 index 0000000000..3a6372a660 --- /dev/null +++ b/spec/mspec/lib/mspec/guards/guard.rb @@ -0,0 +1,141 @@ +require 'mspec/runner/mspec' +require 'mspec/runner/actions/tally' + +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 + if block + yield if yield?(false) + else + yield?(false) + end + ensure + unregister + end + + def run_unless(name, &block) + @name = name + if block + yield if yield?(true) + else + yield?(true) + end + 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.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 + +# Combined guards + +def guard(condition, &block) + raise "condition must be a Proc" unless condition.is_a?(Proc) + raise LocalJumpError, "no block given" unless block + return yield if MSpec.mode? :unguarded or MSpec.mode? :verify or MSpec.mode? :report + yield if condition.call +end + +def guard_not(condition, &block) + raise "condition must be a Proc" unless condition.is_a?(Proc) + raise LocalJumpError, "no block given" unless block + return yield if MSpec.mode? :unguarded or MSpec.mode? :verify or MSpec.mode? :report + yield unless condition.call +end diff --git a/spec/mspec/lib/mspec/guards/platform.rb b/spec/mspec/lib/mspec/guards/platform.rb new file mode 100644 index 0000000000..fadd8d75ef --- /dev/null +++ b/spec/mspec/lib/mspec/guards/platform.rb @@ -0,0 +1,122 @@ +require 'mspec/guards/guard' + +class PlatformGuard < SpecGuard + def self.implementation?(*args) + args.any? do |name| + case name + when :rubinius + RUBY_ENGINE.start_with?('rbx') + else + RUBY_ENGINE.start_with?(name.to_s) + end + end + end + + def self.standard? + implementation? :ruby + end + + PLATFORM = if RUBY_ENGINE == "jruby" + require 'rbconfig' + "#{RbConfig::CONFIG['host_cpu']}-#{RbConfig::CONFIG['host_os']}" + else + RUBY_PLATFORM + end + + def self.os?(*oses) + oses.any? do |os| + raise ":java is not a valid OS" if os == :java + case os + when :windows + PLATFORM =~ /(mswin|mingw)/ + when :wsl + wsl? + else + PLATFORM.include?(os.to_s) + end + end + end + + def self.windows? + os?(:windows) + end + + def self.wasi? + os?(:wasi) + end + + def self.wsl? + if defined?(@wsl_p) + @wsl_p + else + @wsl_p = `uname -r`.match?(/microsoft/i) + end + end + + # In bits + WORD_SIZE = 1.size * 8 + deprecate_constant :WORD_SIZE + + # In bits + POINTER_SIZE = begin + require 'rbconfig/sizeof' + RbConfig::SIZEOF["void*"] * 8 + rescue LoadError + [0].pack('j').size * 8 + end + + # In bits + C_LONG_SIZE = if defined?(RbConfig::SIZEOF[]) + RbConfig::SIZEOF["long"] * 8 + else + [0].pack('l!').size * 8 + end + + def self.wordsize?(size) + warn "#wordsize? is deprecated, use #c_long_size?" + size == WORD_SIZE + end + + def self.pointer_size?(size) + size == POINTER_SIZE + end + + def self.c_long_size?(size) + size == C_LONG_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 :pointer_size + match &&= PlatformGuard.pointer_size? value + when :wordsize + warn ":wordsize is deprecated, use :c_long_size" + match &&= PlatformGuard.wordsize? value + when :c_long_size + match &&= PlatformGuard::c_long_size? value + end + end + match + end +end + +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 diff --git a/spec/mspec/lib/mspec/guards/quarantine.rb b/spec/mspec/lib/mspec/guards/quarantine.rb new file mode 100644 index 0000000000..ec4d01f9ea --- /dev/null +++ b/spec/mspec/lib/mspec/guards/quarantine.rb @@ -0,0 +1,11 @@ +require 'mspec/guards/guard' + +class QuarantineGuard < SpecGuard + def match? + true + end +end + +def quarantine!(&block) + QuarantineGuard.new.run_unless(:quarantine!, &block) +end diff --git a/spec/mspec/lib/mspec/guards/superuser.rb b/spec/mspec/lib/mspec/guards/superuser.rb new file mode 100644 index 0000000000..24daf9b26c --- /dev/null +++ b/spec/mspec/lib/mspec/guards/superuser.rb @@ -0,0 +1,25 @@ +require 'mspec/guards/guard' + +class SuperUserGuard < SpecGuard + def match? + Process.euid == 0 + end +end + +class RealSuperUserGuard < SpecGuard + def match? + Process.uid == 0 + end +end + +def as_superuser(&block) + SuperUserGuard.new.run_if(:as_superuser, &block) +end + +def as_real_superuser(&block) + RealSuperUserGuard.new.run_if(:as_real_superuser, &block) +end + +def as_user(&block) + SuperUserGuard.new.run_unless(:as_user, &block) +end diff --git a/spec/mspec/lib/mspec/guards/support.rb b/spec/mspec/lib/mspec/guards/support.rb new file mode 100644 index 0000000000..790bea1077 --- /dev/null +++ b/spec/mspec/lib/mspec/guards/support.rb @@ -0,0 +1,14 @@ +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 + +def not_supported_on(*args, &block) + SupportedGuard.new(*args).run_unless(:not_supported_on, &block) +end diff --git a/spec/mspec/lib/mspec/guards/version.rb b/spec/mspec/lib/mspec/guards/version.rb new file mode 100644 index 0000000000..f5ea1988ae --- /dev/null +++ b/spec/mspec/lib/mspec/guards/version.rb @@ -0,0 +1,72 @@ +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, requirement) + version = SpecVersion.new(version) unless SpecVersion === version + @version = version + + case requirement + when String + @requirement = SpecVersion.new requirement + when Range + MSpec.deprecate "an empty version range end", 'a specific version' if requirement.end.empty? + a = SpecVersion.new requirement.begin + b = SpecVersion.new requirement.end + unless requirement.exclude_end? + MSpec.deprecate "ruby_version_is with an inclusive range", 'an exclusive range ("2.1"..."2.3")' + end + @requirement = requirement.exclude_end? ? a...b : a..b + else + raise "version must be a String or Range but was a #{requirement.class}" + end + super(@version, @requirement) + end + + def match? + if Range === @requirement + @requirement.include? @version + else + @version >= @requirement + end + end + + @kernel_version = nil + def self.kernel_version + if @kernel_version + @kernel_version + else + if v = RUBY_PLATFORM[/darwin(\d+)/, 1] # build time version + uname = v + else + begin + require 'etc' + etc = true + rescue LoadError + etc = false + end + if etc and Etc.respond_to?(:uname) + uname = Etc.uname.fetch(:release) + else + uname = `uname -r`.chomp + end + end + @kernel_version = uname + end + end +end + +def version_is(base_version, requirement, &block) + VersionGuard.new(base_version, requirement).run_if(:version_is, &block) +end + +def ruby_version_is(requirement, &block) + VersionGuard.new(VersionGuard::FULL_RUBY_VERSION, requirement).run_if(:ruby_version_is, &block) +end + +def kernel_version_is(requirement, &block) + VersionGuard.new(VersionGuard.kernel_version, requirement).run_if(:kernel_version_is, &block) +end diff --git a/spec/mspec/lib/mspec/helpers.rb b/spec/mspec/lib/mspec/helpers.rb new file mode 100644 index 0000000000..90f9fd3fd4 --- /dev/null +++ b/spec/mspec/lib/mspec/helpers.rb @@ -0,0 +1,13 @@ +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' +require 'mspec/helpers/warning' diff --git a/spec/mspec/lib/mspec/helpers/argf.rb b/spec/mspec/lib/mspec/helpers/argf.rb new file mode 100644 index 0000000000..4d3e0f46b3 --- /dev/null +++ b/spec/mspec/lib/mspec/helpers/argf.rb @@ -0,0 +1,35 @@ +# 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 diff --git a/spec/mspec/lib/mspec/helpers/argv.rb b/spec/mspec/lib/mspec/helpers/argv.rb new file mode 100644 index 0000000000..9dac384dbd --- /dev/null +++ b/spec/mspec/lib/mspec/helpers/argv.rb @@ -0,0 +1,44 @@ +# 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 diff --git a/spec/mspec/lib/mspec/helpers/datetime.rb b/spec/mspec/lib/mspec/helpers/datetime.rb new file mode 100644 index 0000000000..84ac86b686 --- /dev/null +++ b/spec/mspec/lib/mspec/helpers/datetime.rb @@ -0,0 +1,48 @@ +# 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 = "") + skip "WASI doesn't have TZ concept" if PlatformGuard.wasi? + 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 diff --git a/spec/mspec/lib/mspec/helpers/fixture.rb b/spec/mspec/lib/mspec/helpers/fixture.rb new file mode 100644 index 0000000000..f3bbe423bd --- /dev/null +++ b/spec/mspec/lib/mspec/helpers/fixture.rb @@ -0,0 +1,24 @@ +# 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 diff --git a/spec/mspec/lib/mspec/helpers/flunk.rb b/spec/mspec/lib/mspec/helpers/flunk.rb new file mode 100644 index 0000000000..84fb3ab39c --- /dev/null +++ b/spec/mspec/lib/mspec/helpers/flunk.rb @@ -0,0 +1,3 @@ +def flunk(msg = "This example is a failure") + SpecExpectation.fail_with "Failed:", msg +end diff --git a/spec/mspec/lib/mspec/helpers/fs.rb b/spec/mspec/lib/mspec/helpers/fs.rb new file mode 100644 index 0000000000..67453eb302 --- /dev/null +++ b/spec/mspec/lib/mspec/helpers/fs.rb @@ -0,0 +1,64 @@ +# Copies a file +def cp(source, dest) + IO.copy_stream source, dest +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 + + unless File.directory? name + begin + Dir.mkdir name + rescue Errno::EEXIST => e + if File.directory? name + # OK, another process/thread created the same directory + else + raise e + end + end + end + 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 diff --git a/spec/mspec/lib/mspec/helpers/io.rb b/spec/mspec/lib/mspec/helpers/io.rb new file mode 100644 index 0000000000..2ad14f47a1 --- /dev/null +++ b/spec/mspec/lib/mspec/helpers/io.rb @@ -0,0 +1,87 @@ +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 + +# 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") + 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, mode +end + +# Creates an IO instance for a temporary file name. The file +# must be deleted. +def new_io(name, mode = "w:utf-8") + if Hash === mode # Avoid kwargs warnings on Ruby 2.7+ + File.new(name, **mode) + else + File.new(name, mode) + end +end diff --git a/spec/mspec/lib/mspec/helpers/mock_to_path.rb b/spec/mspec/lib/mspec/helpers/mock_to_path.rb new file mode 100644 index 0000000000..2780afc54a --- /dev/null +++ b/spec/mspec/lib/mspec/helpers/mock_to_path.rb @@ -0,0 +1,6 @@ +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 diff --git a/spec/mspec/lib/mspec/helpers/numeric.rb b/spec/mspec/lib/mspec/helpers/numeric.rb new file mode 100644 index 0000000000..0b47855cd2 --- /dev/null +++ b/spec/mspec/lib/mspec/helpers/numeric.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true +require 'mspec/guards/platform' + +def nan_value + 0/0.0 +end + +def infinity_value + 1/0.0 +end + +def bignum_value(plus = 0) + # Must be >= fixnum_max + 2, so -bignum_value is < fixnum_min + # A fixed value has the advantage to be the same numeric value for all Rubies and is much easier to spec + (2**64) + plus +end + +def max_long + 2**(PlatformGuard::C_LONG_SIZE - 1) - 1 +end + +def min_long + -(2**(PlatformGuard::C_LONG_SIZE - 1)) +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 + limits_available = begin + require 'rbconfig/sizeof' + defined?(RbConfig::LIMITS.[]) && ['FIXNUM_MAX', 'FIXNUM_MIN'].all? do |key| + Integer === RbConfig::LIMITS[key] + end + rescue LoadError + false + end + + if limits_available + def fixnum_max + RbConfig::LIMITS['FIXNUM_MAX'] + end + + def fixnum_min + RbConfig::LIMITS['FIXNUM_MIN'] + end + elsif PlatformGuard.c_long_size? 32 + def fixnum_max + (2**30) - 1 + end + + def fixnum_min + -(2**30) + end + elsif PlatformGuard.c_long_size? 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 diff --git a/spec/mspec/lib/mspec/helpers/ruby_exe.rb b/spec/mspec/lib/mspec/helpers/ruby_exe.rb new file mode 100644 index 0000000000..2e499d6f9a --- /dev/null +++ b/spec/mspec/lib/mspec/helpers/ruby_exe.rb @@ -0,0 +1,205 @@ +require 'mspec/guards/platform' +require 'mspec/helpers/tmp' + +# The ruby_exe helper provides a wrapper for invoking the +# same Ruby interpreter with the same flags 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+ will be written to a temporary file and be run. +# For example: +# +# ruby_exe('path/to/some/file.rb') +# +# will be executed as +# +# `#{RUBY_EXE} 'path/to/some/file.rb'` +# +# The ruby_exe helper also accepts an options hash with four +# keys: :options, :args :env and :exception. +# +# For example: +# +# ruby_exe('file.rb', :options => "-w", +# :args => "arg1 arg2", +# :env => { :FOO => "bar" }) +# +# will be executed as +# +# `#{RUBY_EXE} -w file.rb arg1 arg2` +# +# with access to ENV["FOO"] with value "bar". +# +# When `exception: false` and Ruby command fails then exception will not be +# raised. +# +# If +nil+ is passed for the first argument, the command line +# will be built only from the options hash. +# +# If no arguments are passed to ruby_exe, it returns an Array +# containing the interpreter executable and the flags: +# +# spawn(*ruby_exe, "-e", "puts :hello") +# +# This avoids spawning an extra shell, and ensure the pid returned by spawn +# corresponds to the ruby process and not the shell. +# +# 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 will only be used if the file exists and is executable. +# The flags will then be appended to the resulting value, such that +# the RUBY_EXE constant contains both the executable and the flags. +# +# 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. +# +# Failure of a Ruby command leads to raising exception by default. + +def ruby_exe_options(option) + case option + when :env + ENV['RUBY_EXE'] + when :engine + case RUBY_ENGINE + 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_ENGINE + (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'] || '') + 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 + +unless Object.const_defined?(:RUBY_EXE) and RUBY_EXE + RUBY_EXE = resolve_ruby_exe +end + +def ruby_exe(code = :not_given, opts = {}) + skip "WASI doesn't provide subprocess" if PlatformGuard.wasi? + + if opts[:dir] + raise "ruby_exe(..., dir: dir) is no longer supported, use Dir.chdir" + end + + if code == :not_given + return RUBY_EXE.split(' ') + 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 + + expected_status = opts.fetch(:exit_status, 0) + + begin + command = ruby_cmd(code, opts) + + # Try to avoid the extra shell for 2>&1 + # This is notably useful for TimeoutAction which can then signal the ruby subprocess and not the shell + popen_options = [] + if command.end_with?(' 2>&1') + command = command[0...-5] + popen_options = [{ err: [:child, :out] }] + end + + output = IO.popen(command, *popen_options) do |io| + pid = io.pid + MSpec.subprocesses << pid + begin + io.read + ensure + MSpec.subprocesses.delete(pid) + end + end + + status = Process.last_status + + exit_status = if status.exited? + status.exitstatus + elsif status.signaled? + signame = Signal.signame status.termsig + raise "No signal name?" unless signame + :"SIG#{signame}" + else + raise SpecExpectationNotMetError, "#{exit_status.inspect} is neither exited? nor signaled?" + end + if exit_status != expected_status + formatted_output = output.lines.map { |line| " #{line}" }.join + raise SpecExpectationNotMetError, + "Expected exit status is #{expected_status.inspect} but actual is #{exit_status.inspect} for command ruby_exe(#{command.inspect})\nOutput:\n#{formatted_output}" + end + + output + 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 + + command = [RUBY_EXE, opts[:options], body, opts[:args]].compact.join(' ') + STDERR.puts "\nruby_cmd: #{command}" if ENV["DEBUG_MSPEC_RUBY_CMD"] == "true" + command +end diff --git a/spec/mspec/lib/mspec/helpers/scratch.rb b/spec/mspec/lib/mspec/helpers/scratch.rb new file mode 100644 index 0000000000..0da3315cd8 --- /dev/null +++ b/spec/mspec/lib/mspec/helpers/scratch.rb @@ -0,0 +1,21 @@ +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 + + def self.inspect + "<ScratchPad @record=#{@record.inspect}>" + end +end diff --git a/spec/mspec/lib/mspec/helpers/tmp.rb b/spec/mspec/lib/mspec/helpers/tmp.rb new file mode 100644 index 0000000000..e903dd9f50 --- /dev/null +++ b/spec/mspec/lib/mspec/helpers/tmp.rb @@ -0,0 +1,62 @@ +# 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_PID = Process.pid + +if spec_temp_dir = ENV["SPEC_TEMP_DIR"] + spec_temp_dir = File.realdirpath(spec_temp_dir) +else + spec_temp_dir = "#{File.realpath(Dir.pwd)}/rubyspec_temp/#{SPEC_TEMP_DIR_PID}" +end +SPEC_TEMP_DIR = spec_temp_dir + +SPEC_TEMP_UNIQUIFIER = +"0" + +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 + +def tmp(name, uniquify = true) + if Dir.exist? SPEC_TEMP_DIR + stat = File.stat(SPEC_TEMP_DIR) + if stat.world_writable? and !stat.sticky? + raise ArgumentError, "SPEC_TEMP_DIR (#{SPEC_TEMP_DIR}) is world writable but not sticky" + end + else + platform_is_not :windows do + umask = File.umask + if (umask & 0002) == 0 # o+w + raise ArgumentError, "File.umask #=> #{umask.to_s(8)} (world-writable)" + end + end + mkdir_p SPEC_TEMP_DIR + end + + if uniquify and !name.empty? + slash = name.rindex "/" + index = slash ? slash + 1 : 0 + name = +name + name.insert index, "#{SPEC_TEMP_UNIQUIFIER.succ!}-" + end + + File.join SPEC_TEMP_DIR, name +end diff --git a/spec/mspec/lib/mspec/helpers/warning.rb b/spec/mspec/lib/mspec/helpers/warning.rb new file mode 100644 index 0000000000..e3d72b78bd --- /dev/null +++ b/spec/mspec/lib/mspec/helpers/warning.rb @@ -0,0 +1,21 @@ +require 'mspec/guards/version' + +# You might be looking for #silence_warnings, use #suppress_warning instead. +# MSpec calls it #suppress_warning for consistency with EnvUtil.suppress_warning in CRuby test/. +def suppress_warning + verbose = $VERBOSE + $VERBOSE = nil + yield +ensure + $VERBOSE = verbose +end + +if ruby_version_is("2.7") + def suppress_keyword_warning(&block) + suppress_warning(&block) + end +else + def suppress_keyword_warning + yield + end +end diff --git a/spec/mspec/lib/mspec/matchers.rb b/spec/mspec/lib/mspec/matchers.rb new file mode 100644 index 0000000000..356e4a9f32 --- /dev/null +++ b/spec/mspec/lib/mspec/matchers.rb @@ -0,0 +1,37 @@ +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/include_any_of' +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' +require 'mspec/matchers/skip' diff --git a/spec/mspec/lib/mspec/matchers/base.rb b/spec/mspec/lib/mspec/matchers/base.rb new file mode 100644 index 0000000000..3534520d88 --- /dev/null +++ b/spec/mspec/lib/mspec/matchers/base.rb @@ -0,0 +1,95 @@ +module MSpecMatchers +end + +class MSpecEnv + include MSpecMatchers +end + +# Expectations are sometimes used in a module body +class Module + include MSpecMatchers +end + +class SpecPositiveOperatorMatcher < BasicObject + def initialize(actual) + @actual = actual + end + + def ==(expected) + result = @actual == expected + unless result + ::SpecExpectation.fail_single_arg_predicate(@actual, :==, expected, result, "to be truthy") + end + end + + def !=(expected) + result = @actual != expected + unless result + ::SpecExpectation.fail_single_arg_predicate(@actual, :!=, expected, result, "to be truthy") + end + end + + def equal?(expected) + result = @actual.equal?(expected) + unless result + ::SpecExpectation.fail_single_arg_predicate(@actual, :equal?, expected, result, "to be truthy") + end + end + + def raise(exception = ::Exception, message = nil, options = nil, &block) + matcher = ::RaiseErrorMatcher.new(exception, message, options, &block) + unless matcher.matches? @actual + expected, actual = matcher.failure_message + ::SpecExpectation.fail_with(expected, actual) + end + end + + def method_missing(name, *args, &block) + result = @actual.__send__(name, *args, &block) + unless result + ::SpecExpectation.fail_predicate(@actual, name, args, block, result, "to be truthy") + end + end +end + +class SpecNegativeOperatorMatcher < BasicObject + def initialize(actual) + @actual = actual + end + + def ==(expected) + result = @actual == expected + if result + ::SpecExpectation.fail_single_arg_predicate(@actual, :==, expected, result, "to be falsy") + end + end + + def !=(expected) + result = @actual != expected + if result + ::SpecExpectation.fail_single_arg_predicate(@actual, :!=, expected, result, "to be falsy") + end + end + + def equal?(expected) + result = @actual.equal?(expected) + if result + ::SpecExpectation.fail_single_arg_predicate(@actual, :equal?, expected, result, "to be falsy") + end + end + + def raise(exception = ::Exception, message = nil, options = nil, &block) + matcher = ::RaiseErrorMatcher.new(exception, message, options, &block) + if matcher.matches? @actual + expected, actual = matcher.negative_failure_message + ::SpecExpectation.fail_with(expected, actual) + end + end + + def method_missing(name, *args, &block) + result = @actual.__send__(name, *args, &block) + if result + ::SpecExpectation.fail_predicate(@actual, name, args, block, result, "to be falsy") + end + end +end diff --git a/spec/mspec/lib/mspec/matchers/be_an_instance_of.rb b/spec/mspec/lib/mspec/matchers/be_an_instance_of.rb new file mode 100644 index 0000000000..fdf3736ac2 --- /dev/null +++ b/spec/mspec/lib/mspec/matchers/be_an_instance_of.rb @@ -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 + +module MSpecMatchers + private def be_an_instance_of(expected) + BeAnInstanceOfMatcher.new(expected) + end +end diff --git a/spec/mspec/lib/mspec/matchers/be_ancestor_of.rb b/spec/mspec/lib/mspec/matchers/be_ancestor_of.rb new file mode 100644 index 0000000000..05f72099e4 --- /dev/null +++ b/spec/mspec/lib/mspec/matchers/be_ancestor_of.rb @@ -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 + +module MSpecMatchers + private def be_ancestor_of(expected) + BeAncestorOfMatcher.new(expected) + end +end diff --git a/spec/mspec/lib/mspec/matchers/be_close.rb b/spec/mspec/lib/mspec/matchers/be_close.rb new file mode 100644 index 0000000000..d6a6626f31 --- /dev/null +++ b/spec/mspec/lib/mspec/matchers/be_close.rb @@ -0,0 +1,29 @@ +TOLERANCE = 0.00003 unless Object.const_defined?(:TOLERANCE) +# To account for GC, context switches, other processes, load, etc. +TIME_TOLERANCE = 20.0 unless Object.const_defined?(:TIME_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 #{@actual}", "to be within #{@expected} +/- #{@tolerance}"] + end + + def negative_failure_message + ["Expected #{@actual}", "not to be within #{@expected} +/- #{@tolerance}"] + end +end + +module MSpecMatchers + private def be_close(expected, tolerance) + BeCloseMatcher.new(expected, tolerance) + end +end diff --git a/spec/mspec/lib/mspec/matchers/be_computed_by.rb b/spec/mspec/lib/mspec/matchers/be_computed_by.rb new file mode 100644 index 0000000000..2e31bc93af --- /dev/null +++ b/spec/mspec/lib/mspec/matchers/be_computed_by.rb @@ -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 + +module MSpecMatchers + private def be_computed_by(sym, *args) + BeComputedByMatcher.new(sym, *args) + end +end diff --git a/spec/mspec/lib/mspec/matchers/be_empty.rb b/spec/mspec/lib/mspec/matchers/be_empty.rb new file mode 100644 index 0000000000..5abd5c9485 --- /dev/null +++ b/spec/mspec/lib/mspec/matchers/be_empty.rb @@ -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 + +module MSpecMatchers + private def be_empty + BeEmptyMatcher.new + end +end diff --git a/spec/mspec/lib/mspec/matchers/be_false.rb b/spec/mspec/lib/mspec/matchers/be_false.rb new file mode 100644 index 0000000000..9e9a2608e1 --- /dev/null +++ b/spec/mspec/lib/mspec/matchers/be_false.rb @@ -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 + +module MSpecMatchers + private def be_false + BeFalseMatcher.new + end +end diff --git a/spec/mspec/lib/mspec/matchers/be_kind_of.rb b/spec/mspec/lib/mspec/matchers/be_kind_of.rb new file mode 100644 index 0000000000..a69906f210 --- /dev/null +++ b/spec/mspec/lib/mspec/matchers/be_kind_of.rb @@ -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 + +module MSpecMatchers + private def be_kind_of(expected) + BeKindOfMatcher.new(expected) + end +end diff --git a/spec/mspec/lib/mspec/matchers/be_nan.rb b/spec/mspec/lib/mspec/matchers/be_nan.rb new file mode 100644 index 0000000000..b279d8f1cf --- /dev/null +++ b/spec/mspec/lib/mspec/matchers/be_nan.rb @@ -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 + +module MSpecMatchers + private def be_nan + BeNaNMatcher.new + end +end diff --git a/spec/mspec/lib/mspec/matchers/be_nil.rb b/spec/mspec/lib/mspec/matchers/be_nil.rb new file mode 100644 index 0000000000..049b1e3a53 --- /dev/null +++ b/spec/mspec/lib/mspec/matchers/be_nil.rb @@ -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 + +module MSpecMatchers + private def be_nil + BeNilMatcher.new + end +end diff --git a/spec/mspec/lib/mspec/matchers/be_true.rb b/spec/mspec/lib/mspec/matchers/be_true.rb new file mode 100644 index 0000000000..52f5013752 --- /dev/null +++ b/spec/mspec/lib/mspec/matchers/be_true.rb @@ -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 + +module MSpecMatchers + private def be_true + BeTrueMatcher.new + end +end diff --git a/spec/mspec/lib/mspec/matchers/be_true_or_false.rb b/spec/mspec/lib/mspec/matchers/be_true_or_false.rb new file mode 100644 index 0000000000..4294b08d1b --- /dev/null +++ b/spec/mspec/lib/mspec/matchers/be_true_or_false.rb @@ -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 + +module MSpecMatchers + private def be_true_or_false + BeTrueOrFalseMatcher.new + end +end diff --git a/spec/mspec/lib/mspec/matchers/block_caller.rb b/spec/mspec/lib/mspec/matchers/block_caller.rb new file mode 100644 index 0000000000..30fab4fc68 --- /dev/null +++ b/spec/mspec/lib/mspec/matchers/block_caller.rb @@ -0,0 +1,37 @@ +class BlockingMatcher + def matches?(block) + t = Thread.new do + block.call + end + + loop do + case t.status + when "sleep" # blocked + t.kill + t.join + return true + when false # terminated normally, so never blocked + t.join + return false + when nil # terminated exceptionally + t.value + else + Thread.pass + end + end + 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 + +module MSpecMatchers + private def block_caller + BlockingMatcher.new + end +end diff --git a/spec/mspec/lib/mspec/matchers/complain.rb b/spec/mspec/lib/mspec/matchers/complain.rb new file mode 100644 index 0000000000..19310c0bbb --- /dev/null +++ b/spec/mspec/lib/mspec/matchers/complain.rb @@ -0,0 +1,69 @@ +require 'mspec/helpers/io' + +class ComplainMatcher + def initialize(complaint = nil, options = nil) + # the proper solution is to use double splat operator e.g. + # def initialize(complaint = nil, **options) + # but we are trying to minimize language features required to run MSpec + if complaint.is_a?(Hash) + @complaint = nil + @options = complaint + else + @complaint = complaint + @options = options || {} + end + end + + def matches?(proc) + @saved_err = $stderr + @verbose = $VERBOSE + err = IOStub.new + + $stderr = err + $VERBOSE = @options.key?(:verbose) ? @options[:verbose] : false + begin + proc.call + ensure + $VERBOSE = @verbose + $stderr = @saved_err + end + + @warning = err.to_s + unless @complaint.nil? + case @complaint + when Regexp + return false unless @warning =~ @complaint + else + return false unless @warning == @complaint + end + end + + return @warning.empty? ? false : true + 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: #{@warning.chomp.inspect}"] + else + ["Expected warning: #{@complaint.inspect}", "but got: #{@warning.chomp.inspect}"] + end + end + + def negative_failure_message + if @complaint.nil? + ["Unexpected warning: ", @warning.chomp.inspect] + elsif @complaint.kind_of? Regexp + ["Expected warning not to match: #{@complaint.inspect}", "but got: #{@warning.chomp.inspect}"] + else + ["Expected warning: #{@complaint.inspect}", "but got: #{@warning.chomp.inspect}"] + end + end +end + +module MSpecMatchers + private def complain(complaint = nil, options = nil) + ComplainMatcher.new(complaint, options) + end +end diff --git a/spec/mspec/lib/mspec/matchers/eql.rb b/spec/mspec/lib/mspec/matchers/eql.rb new file mode 100644 index 0000000000..bcab88ebee --- /dev/null +++ b/spec/mspec/lib/mspec/matchers/eql.rb @@ -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 #{MSpec.format(@actual)}", + "to have same value and type as #{MSpec.format(@expected)}"] + end + + def negative_failure_message + ["Expected #{MSpec.format(@actual)}", + "not to have same value or type as #{MSpec.format(@expected)}"] + end +end + +module MSpecMatchers + private def eql(expected) + EqlMatcher.new(expected) + end +end diff --git a/spec/mspec/lib/mspec/matchers/equal.rb b/spec/mspec/lib/mspec/matchers/equal.rb new file mode 100644 index 0000000000..5ba4856d82 --- /dev/null +++ b/spec/mspec/lib/mspec/matchers/equal.rb @@ -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 #{MSpec.format(@actual)}", + "to be identical to #{MSpec.format(@expected)}"] + end + + def negative_failure_message + ["Expected #{MSpec.format(@actual)}", + "not to be identical to #{MSpec.format(@expected)}"] + end +end + +module MSpecMatchers + private def equal(expected) + EqualMatcher.new(expected) + end +end diff --git a/spec/mspec/lib/mspec/matchers/equal_element.rb b/spec/mspec/lib/mspec/matchers/equal_element.rb new file mode 100644 index 0000000000..8da2567fcf --- /dev/null +++ b/spec/mspec/lib/mspec/matchers/equal_element.rb @@ -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 #{MSpec.format(@actual)}", + "to be a '#{@element}' element with #{attributes_for_failure_message} and #{content_for_failure_message}"] + end + + def negative_failure_message + ["Expected #{MSpec.format(@actual)}", + "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 + +module MSpecMatchers + private def equal_element(*args) + EqualElementMatcher.new(*args) + end +end diff --git a/spec/mspec/lib/mspec/matchers/have_class_variable.rb b/spec/mspec/lib/mspec/matchers/have_class_variable.rb new file mode 100644 index 0000000000..dd43ced621 --- /dev/null +++ b/spec/mspec/lib/mspec/matchers/have_class_variable.rb @@ -0,0 +1,12 @@ +require 'mspec/matchers/variable' + +class HaveClassVariableMatcher < VariableMatcher + self.variables_method = :class_variables + self.description = 'class variable' +end + +module MSpecMatchers + private def have_class_variable(variable) + HaveClassVariableMatcher.new(variable) + end +end diff --git a/spec/mspec/lib/mspec/matchers/have_constant.rb b/spec/mspec/lib/mspec/matchers/have_constant.rb new file mode 100644 index 0000000000..6ec7c75b85 --- /dev/null +++ b/spec/mspec/lib/mspec/matchers/have_constant.rb @@ -0,0 +1,12 @@ +require 'mspec/matchers/variable' + +class HaveConstantMatcher < VariableMatcher + self.variables_method = :constants + self.description = 'constant' +end + +module MSpecMatchers + private def have_constant(variable) + HaveConstantMatcher.new(variable) + end +end diff --git a/spec/mspec/lib/mspec/matchers/have_instance_method.rb b/spec/mspec/lib/mspec/matchers/have_instance_method.rb new file mode 100644 index 0000000000..9a5a31aa0f --- /dev/null +++ b/spec/mspec/lib/mspec/matchers/have_instance_method.rb @@ -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 + +module MSpecMatchers + private def have_instance_method(method, include_super = true) + HaveInstanceMethodMatcher.new method, include_super + end +end diff --git a/spec/mspec/lib/mspec/matchers/have_instance_variable.rb b/spec/mspec/lib/mspec/matchers/have_instance_variable.rb new file mode 100644 index 0000000000..de51b3209d --- /dev/null +++ b/spec/mspec/lib/mspec/matchers/have_instance_variable.rb @@ -0,0 +1,12 @@ +require 'mspec/matchers/variable' + +class HaveInstanceVariableMatcher < VariableMatcher + self.variables_method = :instance_variables + self.description = 'instance variable' +end + +module MSpecMatchers + private def have_instance_variable(variable) + HaveInstanceVariableMatcher.new(variable) + end +end diff --git a/spec/mspec/lib/mspec/matchers/have_method.rb b/spec/mspec/lib/mspec/matchers/have_method.rb new file mode 100644 index 0000000000..e962e69e0a --- /dev/null +++ b/spec/mspec/lib/mspec/matchers/have_method.rb @@ -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 + +module MSpecMatchers + private def have_method(method, include_super = true) + HaveMethodMatcher.new method, include_super + end +end diff --git a/spec/mspec/lib/mspec/matchers/have_private_instance_method.rb b/spec/mspec/lib/mspec/matchers/have_private_instance_method.rb new file mode 100644 index 0000000000..d32db76c6a --- /dev/null +++ b/spec/mspec/lib/mspec/matchers/have_private_instance_method.rb @@ -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 + +module MSpecMatchers + private def have_private_instance_method(method, include_super = true) + HavePrivateInstanceMethodMatcher.new method, include_super + end +end diff --git a/spec/mspec/lib/mspec/matchers/have_private_method.rb b/spec/mspec/lib/mspec/matchers/have_private_method.rb new file mode 100644 index 0000000000..c74165cfc7 --- /dev/null +++ b/spec/mspec/lib/mspec/matchers/have_private_method.rb @@ -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 + +module MSpecMatchers + private def have_private_method(method, include_super = true) + HavePrivateMethodMatcher.new method, include_super + end +end diff --git a/spec/mspec/lib/mspec/matchers/have_protected_instance_method.rb b/spec/mspec/lib/mspec/matchers/have_protected_instance_method.rb new file mode 100644 index 0000000000..1deb2f995d --- /dev/null +++ b/spec/mspec/lib/mspec/matchers/have_protected_instance_method.rb @@ -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 + +module MSpecMatchers + private def have_protected_instance_method(method, include_super = true) + HaveProtectedInstanceMethodMatcher.new method, include_super + end +end diff --git a/spec/mspec/lib/mspec/matchers/have_public_instance_method.rb b/spec/mspec/lib/mspec/matchers/have_public_instance_method.rb new file mode 100644 index 0000000000..0e620532c0 --- /dev/null +++ b/spec/mspec/lib/mspec/matchers/have_public_instance_method.rb @@ -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 + +module MSpecMatchers + private def have_public_instance_method(method, include_super = true) + HavePublicInstanceMethodMatcher.new method, include_super + end +end diff --git a/spec/mspec/lib/mspec/matchers/have_singleton_method.rb b/spec/mspec/lib/mspec/matchers/have_singleton_method.rb new file mode 100644 index 0000000000..b60dd2536b --- /dev/null +++ b/spec/mspec/lib/mspec/matchers/have_singleton_method.rb @@ -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 + +module MSpecMatchers + private def have_singleton_method(method, include_super = true) + HaveSingletonMethodMatcher.new method, include_super + end +end diff --git a/spec/mspec/lib/mspec/matchers/include.rb b/spec/mspec/lib/mspec/matchers/include.rb new file mode 100644 index 0000000000..3f07f35548 --- /dev/null +++ b/spec/mspec/lib/mspec/matchers/include.rb @@ -0,0 +1,31 @@ +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 #{MSpec.format(@actual)}", "to include #{MSpec.format(@element)}"] + end + + def negative_failure_message + ["Expected #{MSpec.format(@actual)}", "not to include #{MSpec.format(@element)}"] + end +end + +# Cannot override #include at the toplevel in MRI +module MSpecMatchers + private def include(*expected) + IncludeMatcher.new(*expected) + end +end diff --git a/spec/mspec/lib/mspec/matchers/include_any_of.rb b/spec/mspec/lib/mspec/matchers/include_any_of.rb new file mode 100644 index 0000000000..ce097ccf0f --- /dev/null +++ b/spec/mspec/lib/mspec/matchers/include_any_of.rb @@ -0,0 +1,29 @@ +class IncludeAnyOfMatcher + def initialize(*expected) + @expected = expected + end + + def matches?(actual) + @actual = actual + @expected.each do |e| + if @actual.include?(e) + return true + end + end + return false + end + + def failure_message + ["Expected #{@actual.inspect}", "to include any of #{@expected.inspect}"] + end + + def negative_failure_message + ["Expected #{@actual.inspect}", "not to include any of #{@expected.inspect}"] + end +end + +module MSpecMatchers + private def include_any_of(*expected) + IncludeAnyOfMatcher.new(*expected) + end +end diff --git a/spec/mspec/lib/mspec/matchers/infinity.rb b/spec/mspec/lib/mspec/matchers/infinity.rb new file mode 100644 index 0000000000..8bfa6dbd10 --- /dev/null +++ b/spec/mspec/lib/mspec/matchers/infinity.rb @@ -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 + +module MSpecMatchers + private def be_positive_infinity + InfinityMatcher.new(1) + end + + private def be_negative_infinity + InfinityMatcher.new(-1) + end +end diff --git a/spec/mspec/lib/mspec/matchers/match_yaml.rb b/spec/mspec/lib/mspec/matchers/match_yaml.rb new file mode 100644 index 0000000000..30561627c3 --- /dev/null +++ b/spec/mspec/lib/mspec/matchers/match_yaml.rb @@ -0,0 +1,50 @@ +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 + if YAML.respond_to?(:unsafe_load) + YAML.unsafe_load(obj) + else + YAML.load(obj) + end + rescue + false + else + true + end + end +end + +module MSpecMatchers + private def match_yaml(expected) + MatchYAMLMatcher.new(expected) + end +end diff --git a/spec/mspec/lib/mspec/matchers/method.rb b/spec/mspec/lib/mspec/matchers/method.rb new file mode 100644 index 0000000000..2b54419faa --- /dev/null +++ b/spec/mspec/lib/mspec/matchers/method.rb @@ -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 diff --git a/spec/mspec/lib/mspec/matchers/output.rb b/spec/mspec/lib/mspec/matchers/output.rb new file mode 100644 index 0000000000..5bb5d55027 --- /dev/null +++ b/spec/mspec/lib/mspec/matchers/output.rb @@ -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: #{MSpec.format(@out)}\n" + actual_out += " $stdout: #{MSpec.format(@stdout.to_s)}\n" + end + unless @err.nil? + expected_out += " $stderr: #{MSpec.format(@err)}\n" + actual_out += " $stderr: #{MSpec.format(@stderr.to_s)}\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 + +module MSpecMatchers + private def output(stdout = nil, stderr = nil) + OutputMatcher.new(stdout, stderr) + end +end diff --git a/spec/mspec/lib/mspec/matchers/output_to_fd.rb b/spec/mspec/lib/mspec/matchers/output_to_fd.rb new file mode 100644 index 0000000000..f4d7b4ea1f --- /dev/null +++ b/spec/mspec/lib/mspec/matchers/output_to_fd.rb @@ -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 + +module MSpecMatchers + private def output_to_fd(what, where = STDOUT) + OutputToFDMatcher.new what, where + end +end diff --git a/spec/mspec/lib/mspec/matchers/raise_error.rb b/spec/mspec/lib/mspec/matchers/raise_error.rb new file mode 100644 index 0000000000..8cba842ce3 --- /dev/null +++ b/spec/mspec/lib/mspec/matchers/raise_error.rb @@ -0,0 +1,132 @@ +class RaiseErrorMatcher + FAILURE_MESSAGE_FOR_EXCEPTION = {}.compare_by_identity + UNDEF_CAUSE = Object.new + + attr_writer :block + + def initialize(exception, message = nil, options = nil, &block) + if message.is_a? Hash + @message = nil + options = message + else + @message = message + end + @cause = options ? options.fetch(:cause, UNDEF_CAUSE) : UNDEF_CAUSE + @exception = exception + @block = block + @actual = nil + end + + # This #matches? method is unusual because it doesn't always return a boolean but instead + # re-raises the original exception if proc.call raises an exception and #matching_exception? is false. + # The reasoning is the original exception class matters and we don't want to change it by raising another exception, + # so instead we attach the #failure_message and extract it in ExceptionState#message. + def matches?(proc) + @result = proc.call + return false + rescue Object => actual + @actual = actual + + if matching_exception?(actual) + # The block has its own expectations and will throw an exception if it fails + @block[actual] if @block + return true + else + FAILURE_MESSAGE_FOR_EXCEPTION[actual] = failure_message + raise actual + end + end + + def matching_class?(exc) + @exception === exc + end + + def matching_message?(exc) + case @message + when String + @message == exc.message + when Regexp + @message =~ exc.message + else + true + end + end + + def matching_cause?(exc) + case @cause + when UNDEF_CAUSE + true + else + @cause == exc.cause + end + end + + def matching_exception?(exc) + matching_class?(exc) and matching_message?(exc) and matching_cause?(exc) + end + + def exception_class_and_message_and_cause(exception_class, message, cause) + string = "#{exception_class}" + prefixed = false + prefix = -> { prefixed ? ", " : prefixed = "(" } + + if message != nil + string << "#{prefix.()}#{message.inspect}" + end + + if cause != UNDEF_CAUSE + string << "#{prefix.()}cause: #{cause.inspect}" + end + + string << ")" if prefixed + + string + end + + def format_expected_exception + exception_class_and_message_and_cause(@exception, @message, @cause) + end + + def format_exception(exception) + exception_class_and_message_and_cause(exception.class, + @message == nil ? nil : exception.message, + @cause == UNDEF_CAUSE ? UNDEF_CAUSE : exception.cause) + end + + def failure_message + message = ["Expected #{format_expected_exception}"] + + if @actual + message << "but got: #{format_exception(@actual)}" + else + message << "but no exception was raised (#{MSpec.format(@result)} 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 + +module MSpecMatchers + private def raise_error(exception = Exception, message = nil, options = nil, &block) + RaiseErrorMatcher.new(exception, message, options, &block) + end + + # CRuby < 4.1 has inconsistent coercion errors: + # https://bugs.ruby-lang.org/issues/21864 + # This matcher ignores the message on CRuby < 4.1 + # and checks the message for all other cases, including other Rubies + private def raise_consistent_error(exception = Exception, message = nil, options = nil, &block) + if RUBY_ENGINE == "ruby" and ruby_version_is ""..."4.1" + message = nil + end + RaiseErrorMatcher.new(exception, message, options, &block) + end +end diff --git a/spec/mspec/lib/mspec/matchers/respond_to.rb b/spec/mspec/lib/mspec/matchers/respond_to.rb new file mode 100644 index 0000000000..6b35ae2d3c --- /dev/null +++ b/spec/mspec/lib/mspec/matchers/respond_to.rb @@ -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 + +module MSpecMatchers + private def respond_to(expected) + RespondToMatcher.new(expected) + end +end diff --git a/spec/mspec/lib/mspec/matchers/signed_zero.rb b/spec/mspec/lib/mspec/matchers/signed_zero.rb new file mode 100644 index 0000000000..2ff90f4994 --- /dev/null +++ b/spec/mspec/lib/mspec/matchers/signed_zero.rb @@ -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 + +module MSpecMatchers + private def be_positive_zero + SignedZeroMatcher.new(1) + end + + private def be_negative_zero + SignedZeroMatcher.new(-1) + end +end diff --git a/spec/mspec/lib/mspec/matchers/skip.rb b/spec/mspec/lib/mspec/matchers/skip.rb new file mode 100644 index 0000000000..7c175d358d --- /dev/null +++ b/spec/mspec/lib/mspec/matchers/skip.rb @@ -0,0 +1,5 @@ +module MSpecMatchers + private def skip(reason = 'no reason') + raise SkippedSpecError, reason + end +end diff --git a/spec/mspec/lib/mspec/matchers/variable.rb b/spec/mspec/lib/mspec/matchers/variable.rb new file mode 100644 index 0000000000..4d801ea337 --- /dev/null +++ b/spec/mspec/lib/mspec/matchers/variable.rb @@ -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 diff --git a/spec/mspec/lib/mspec/mocks.rb b/spec/mspec/lib/mspec/mocks.rb new file mode 100644 index 0000000000..6a029c7b53 --- /dev/null +++ b/spec/mspec/lib/mspec/mocks.rb @@ -0,0 +1,3 @@ +require 'mspec/mocks/mock' +require 'mspec/mocks/proxy' +require 'mspec/mocks/object' diff --git a/spec/mspec/lib/mspec/mocks/mock.rb b/spec/mspec/lib/mspec/mocks/mock.rb new file mode 100644 index 0000000000..c61ba35ea7 --- /dev/null +++ b/spec/mspec/lib/mspec/mocks/mock.rb @@ -0,0 +1,209 @@ +require 'mspec/expectations/expectations' +require 'mspec/helpers/warning' + +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(key) + :"__mspec_#{key.last}__" + end + + def self.replaced_key(obj, sym) + [obj.__id__, sym] + end + + def self.replaced?(key) + mocks.include?(key) or stubs.include?(key) + end + + def self.clear_replaced(key) + mocks.delete key + stubs.delete key + end + + def self.mock_respond_to?(obj, sym, include_private = false) + key = replaced_key(obj, :respond_to?) + if replaced? key + name = replaced_name(key) + 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 type == :stub and mocks.key?(key) + # Defining a stub and there is already a mock, ignore the stub + return + end + + if (sym == :respond_to? or mock_respond_to?(obj, sym, true)) and !replaced?(key) + meta.__send__ :alias_method, replaced_name(key), sym + end + + suppress_warning { + 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.mock? and stubs.key?(key) + # Defining a mock and there is already a stub, remove the stub + stubs.delete key + 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.inspect_args(args) + "(#{Array(args).map(&:inspect).join(', ')})" + 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}#{inspect_args proxy.arguments} " + \ + "#{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.fetch(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, *args + else + SpecExpectation.fail_with("Mock '#{name_or_inspect obj}': method #{sym}\n", + "called with unexpected arguments #{inspect_args args}") + end + end + + def self.cleanup + objects.each do |key, obj| + if obj.kind_of? MockIntObject + clear_replaced key + next + end + + replaced = replaced_name(key) + sym = key.last + meta = obj.singleton_class + + if mock_respond_to? obj, replaced, true + suppress_warning do + meta.__send__ :alias_method, sym, replaced + end + meta.__send__ :remove_method, replaced + else + meta.__send__ :remove_method, sym + end + + clear_replaced key + end + ensure + reset + end +end diff --git a/spec/mspec/lib/mspec/mocks/object.rb b/spec/mspec/lib/mspec/mocks/object.rb new file mode 100644 index 0000000000..fcaa1caef0 --- /dev/null +++ b/spec/mspec/lib/mspec/mocks/object.rb @@ -0,0 +1,28 @@ +require 'mspec/mocks/proxy' + +class Object + 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 stub!(sym) + Mock.install_method self, sym, :stub + end +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 diff --git a/spec/mspec/lib/mspec/mocks/proxy.rb b/spec/mspec/lib/mspec/mocks/proxy.rb new file mode 100644 index 0000000000..8473132b0b --- /dev/null +++ b/spec/mspec/lib/mspec/mocks/proxy.rb @@ -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 diff --git a/spec/mspec/lib/mspec/runner.rb b/spec/mspec/lib/mspec/runner.rb new file mode 100644 index 0000000000..df57b9f69b --- /dev/null +++ b/spec/mspec/lib/mspec/runner.rb @@ -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' diff --git a/spec/mspec/lib/mspec/runner/actions.rb b/spec/mspec/lib/mspec/runner/actions.rb new file mode 100644 index 0000000000..0a5a05fbd1 --- /dev/null +++ b/spec/mspec/lib/mspec/runner/actions.rb @@ -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' diff --git a/spec/mspec/lib/mspec/runner/actions/constants_leak_checker.rb b/spec/mspec/lib/mspec/runner/actions/constants_leak_checker.rb new file mode 100644 index 0000000000..abfb6dd0ee --- /dev/null +++ b/spec/mspec/lib/mspec/runner/actions/constants_leak_checker.rb @@ -0,0 +1,84 @@ +class ConstantsLockFile + LOCK_FILE_NAME = '.mspec.constants' + + def self.lock_file + @prefix ||= File.expand_path(MSpecScript.get(:prefix) || '.') + "#{@prefix}/#{LOCK_FILE_NAME}" + end + + def self.load + if File.exist?(lock_file) + File.readlines(lock_file).map(&:chomp) + else + [] + end + end + + def self.dump(ary) + contents = ary.map(&:to_s).uniq.sort.join("\n") + "\n" + File.write(lock_file, contents) + end +end + +class ConstantLeakError < StandardError +end + +class ConstantsLeakCheckerAction + def initialize(save) + @save = save + @check = !save + @constants_locked = ConstantsLockFile.load + @exclude_patterns = MSpecScript.get(:toplevel_constants_excludes) || [] + end + + def register + MSpec.register :start, self + MSpec.register :before, self + MSpec.register :after, self + MSpec.register :finish, self + end + + def start + @constants_start = constants_now + end + + def before(state) + @constants_before = constants_now + end + + def after(state) + constants = remove_excludes(constants_now - @constants_before - @constants_locked) + + if @check && !constants.empty? + MSpec.protect 'Constants leak check' do + raise ConstantLeakError, "Top level constants leaked: #{constants.join(', ')}" + end + end + end + + def finish + constants = remove_excludes(constants_now - @constants_start - @constants_locked) + + if @save + ConstantsLockFile.dump(@constants_locked + constants) + end + + if @check && !constants.empty? + MSpec.protect 'Global constants leak check' do + raise ConstantLeakError, "Top level constants leaked in the whole test suite: #{constants.join(', ')}" + end + end + end + + private + + def constants_now + Object.constants.map(&:to_s) + end + + def remove_excludes(constants) + constants.reject { |name| + @exclude_patterns.any? { |pattern| pattern === name } + } + end +end diff --git a/spec/mspec/lib/mspec/runner/actions/filter.rb b/spec/mspec/lib/mspec/runner/actions/filter.rb new file mode 100644 index 0000000000..b0ad7080da --- /dev/null +++ b/spec/mspec/lib/mspec/runner/actions/filter.rb @@ -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 diff --git a/spec/mspec/lib/mspec/runner/actions/leakchecker.rb b/spec/mspec/lib/mspec/runner/actions/leakchecker.rb new file mode 100644 index 0000000000..0a8c9c3252 --- /dev/null +++ b/spec/mspec/lib/mspec/runner/actions/leakchecker.rb @@ -0,0 +1,377 @@ +# 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 LeakError < StandardError +end + +class LeakChecker + attr_reader :leaks + + def initialize + @fd_info = find_fds + @tempfile_info = find_tempfiles + @thread_info = find_threads + @env_info = find_env + @argv_info = find_argv + @globals_info = find_globals + @encoding_info = find_encodings + end + + def check(state) + @state = state + @leaks = [] + check_fd_leak + check_tempfile_leak + check_thread_leak + check_process_leak + check_env + check_argv + check_globals + check_encodings + check_tracepoints + GC.start unless @leaks.empty? + @leaks.empty? + 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 + 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| + leak "Closed file descriptor: #{fd}" + } + end + fd_leaked = live2 - live1 + if !fd_leaked.empty? + 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 + leak "Leaked file descriptor: #{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 + leak "Multiple autoclose IO object for a file descriptor:#{str}" + end + } + end + @fd_info = live2 + end + + def extend_tempfile_counter + return if defined? LeakChecker::TempfileCounter + m = Module.new { + @count = 0 + class << self + attr_accessor :count + end + + def new(...) + LeakChecker::TempfileCounter.count += 1 + super + end + } + LeakChecker.const_set(:TempfileCounter, m) + + class << Tempfile + 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 + return false unless defined? Tempfile + count1, initial_tempfiles = @tempfile_info + count2, current_tempfiles = find_tempfiles(count1) + tempfiles_leaked = current_tempfiles - initial_tempfiles + if !tempfiles_leaked.empty? + list = tempfiles_leaked.map {|t| t.inspect }.sort + list.each {|str| + leak "Leaked tempfile: #{str}" + } + tempfiles_leaked.each {|t| t.close! } + end + @tempfile_info = [count2, initial_tempfiles] + end + + def find_threads + Thread.list.find_all {|t| + t != Thread.current && t.alive? && + !(t.thread_variable?(:"\0__detached_thread__") && t.thread_variable_get(:"\0__detached_thread__")) + } + end + + def check_thread_leak + live1 = @thread_info + live2 = find_threads + thread_finished = live1 - live2 + if !thread_finished.empty? + list = thread_finished.map {|t| t.inspect }.sort + list.each {|str| + leak "Finished thread: #{str}" + } + end + thread_leaked = live2 - live1 + if !thread_leaked.empty? + list = thread_leaked.map {|t| t.inspect }.sort + list.each {|str| + leak "Leaked thread: #{str}" + } + end + @thread_info = live2 + end + + def check_process_leak + subprocesses_leaked = Process.waitall + subprocesses_leaked.each { |pid, status| + leak "Leaked subprocess: #{pid}: #{status}" + } + end + + def find_env + ENV.to_h + end + + def check_env + old_env = @env_info + new_env = find_env + return 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] + leak "Environment variable changed : #{k.inspect} changed : #{old_env[k].inspect} -> #{new_env[k].inspect}" + end + else + leak "Environment variable changed: #{k.inspect} deleted" + end + else + if new_env.has_key?(k) + leak "Environment variable changed: #{k.inspect} added" + else + flunk "unreachable" + end + end + } + @env_info = new_env + end + + def find_argv + ARGV.map { |e| e.dup } + end + + def check_argv + old_argv = @argv_info + new_argv = find_argv + if new_argv != old_argv + leak "ARGV changed: #{old_argv.inspect} to #{new_argv.inspect}" + @argv_info = new_argv + end + end + + def find_globals + { verbose: $VERBOSE, debug: $DEBUG } + end + + def check_globals + old_globals = @globals_info + new_globals = find_globals + if new_globals != old_globals + leak "Globals changed: #{old_globals.inspect} to #{new_globals.inspect}" + @globals_info = new_globals + end + end + + def find_encodings + [Encoding.default_internal, Encoding.default_external] + end + + def check_encodings + old_internal, old_external = @encoding_info + new_internal, new_external = find_encodings + if new_internal != old_internal + leak "Encoding.default_internal changed: #{old_internal.inspect} to #{new_internal.inspect}" + end + if new_external != old_external + leak "Encoding.default_external changed: #{old_external.inspect} to #{new_external.inspect}" + end + @encoding_info = [new_internal, new_external] + end + + def check_tracepoints + ObjectSpace.each_object(TracePoint) do |tp| + if tp.enabled? + leak "TracePoint is still enabled: #{tp.inspect}" + end + end + end + + def leak(message) + if @leaks.empty? + $stderr.puts "\n" + $stderr.puts @state.description + end + @leaks << message + $stderr.puts message + end +end + +class LeakCheckerAction + def register + MSpec.register :start, self + MSpec.register :after, self + end + + def start + disable_nss_modules + @checker = LeakChecker.new + end + + def after(state) + unless @checker.check(state) + leak_messages = @checker.leaks + location = state.description + if state.example + location = "#{location}\n#{state.example.source_location.join(':')}" + end + MSpec.protect(location) do + raise LeakError, leak_messages.join("\n") + end + end + end + + private + + # This function is intended to disable all NSS modules when ruby is compiled + # against glibc. NSS modules allow the system administrator to load custom + # shared objects into all processes using glibc, and use them to customise + # the behaviour of username, groupname, hostname, etc lookups. This is + # normally configured in the file /etc/nsswitch.conf. + # These modules often do things like open cache files or connect to system + # daemons like sssd or dbus, which of course means they have open file + # descriptors of their own. This can cause the leak-checking functionality + # in this file to report that such descriptors have been leaked, and fail + # the test suite. + # This function uses glibc's __nss_configure_lookup function to override any + # configuration in /etc/nsswitch.conf, and just use the built in files/dns + # name lookup functionality (which is of course perfectly sufficient for + # running ruby/spec). + def disable_nss_modules + begin + require 'fiddle' + rescue LoadError + # Make sure it's possible to run the test suite on a ruby implementation + # which does not (yet?) have Fiddle. + return + end + + begin + libc = Fiddle.dlopen(nil) + # Older versions of fiddle don't have Fiddle::Type (and instead rely on Fiddle::TYPE_) + # Even older versions of fiddle don't have CONST_STRING, + string_type = defined?(Fiddle::TYPE_CONST_STRING) ? Fiddle::TYPE_CONST_STRING : Fiddle::TYPE_VOIDP + nss_configure_lookup = Fiddle::Function.new( + libc['__nss_configure_lookup'], + [string_type, string_type], + Fiddle::TYPE_INT + ) + rescue Fiddle::DLError + # We're not running with glibc - no need to do this. + return + end + + nss_configure_lookup.call 'passwd', 'files' + nss_configure_lookup.call 'shadow', 'files' + nss_configure_lookup.call 'group', 'files' + nss_configure_lookup.call 'hosts', 'files dns' + nss_configure_lookup.call 'services', 'files' + nss_configure_lookup.call 'netgroup', 'files' + nss_configure_lookup.call 'automount', 'files' + nss_configure_lookup.call 'aliases', 'files' + nss_configure_lookup.call 'ethers', 'files' + nss_configure_lookup.call 'gshadow', 'files' + nss_configure_lookup.call 'initgroups', 'files' + nss_configure_lookup.call 'networks', 'files dns' + nss_configure_lookup.call 'protocols', 'files' + nss_configure_lookup.call 'publickey', 'files' + nss_configure_lookup.call 'rpc', 'files' + end +end diff --git a/spec/mspec/lib/mspec/runner/actions/profile.rb b/spec/mspec/lib/mspec/runner/actions/profile.rb new file mode 100644 index 0000000000..c743d6e3e8 --- /dev/null +++ b/spec/mspec/lib/mspec/runner/actions/profile.rb @@ -0,0 +1,60 @@ +class ProfileAction + def initialize + @describe_name = nil + @describe_time = nil + @describes = [] + @its = [] + end + + def register + MSpec.register :enter, self + MSpec.register :before,self + MSpec.register :after, self + MSpec.register :finish,self + end + + def enter(describe) + if @describe_time + @describes << [@describe_name, now - @describe_time] + end + + @describe_name = describe + @describe_time = now + end + + def before(state) + @it_name = state.it + @it_time = now + end + + def after(state = nil) + @its << [@describe_name, @it_name, now - @it_time] + end + + def finish + puts "\nProfiling info:" + + desc = @describes.sort { |a,b| b.last <=> a.last } + desc.delete_if { |a| a.last <= 0.001 } + show = desc[0, 100] + + puts "Top #{show.size} describes:" + + show.each do |des, time| + printf "%3.3f - %s\n", time, des + end + + its = @its.sort { |a,b| b.last <=> a.last } + its.delete_if { |a| a.last <= 0.001 } + show = its[0, 100] + + puts "\nTop #{show.size} its:" + show.each do |des, it, time| + printf "%3.3f - %s %s\n", time, des, it + end + end + + def now + Time.now.to_f + end +end diff --git a/spec/mspec/lib/mspec/runner/actions/tag.rb b/spec/mspec/lib/mspec/runner/actions/tag.rb new file mode 100644 index 0000000000..d40d562451 --- /dev/null +++ b/spec/mspec/lib/mspec/runner/actions/tag.rb @@ -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 diff --git a/spec/mspec/lib/mspec/runner/actions/taglist.rb b/spec/mspec/lib/mspec/runner/actions/taglist.rb new file mode 100644 index 0000000000..3097e655d5 --- /dev/null +++ b/spec/mspec/lib/mspec/runner/actions/taglist.rb @@ -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 diff --git a/spec/mspec/lib/mspec/runner/actions/tagpurge.rb b/spec/mspec/lib/mspec/runner/actions/tagpurge.rb new file mode 100644 index 0000000000..f4587de6bc --- /dev/null +++ b/spec/mspec/lib/mspec/runner/actions/tagpurge.rb @@ -0,0 +1,56 @@ +require 'mspec/runner/actions/filter' +require 'mspec/runner/actions/taglist' + +# TagPurgeAction - removes all tags not matching any spec +# descriptions. +class TagPurgeAction < TagListAction + attr_reader :matching + + def initialize + @matching = [] + @filter = nil + @tags = nil + end + + # Prints a banner about purging tags. + def start + print "\nRemoving tags not matching any specs\n\n" + end + + # Creates a MatchFilter for all tags. + def load + @filter = nil + @tags = MSpec.read_tags self + desc = @tags.map { |t| t.description } + @filter = MatchFilter.new(nil, *desc) unless desc.empty? + end + + # Saves any matching tags + def after(state) + @matching << state.description if self === state.description + end + + # Rewrites any matching tags. Prints non-matching tags. + # Deletes the tag file if there were no tags (this cleans + # up empty or malformed tag files). + def unload + if @filter + matched = @tags.select { |t| @matching.any? { |s| s == t.description } } + MSpec.write_tags matched + + (@tags - matched).each { |t| print t.description, "\n" } + else + MSpec.delete_tags + end + end + + def register + super + MSpec.register :unload, self + end + + def unregister + super + MSpec.unregister :unload, self + end +end diff --git a/spec/mspec/lib/mspec/runner/actions/tally.rb b/spec/mspec/lib/mspec/runner/actions/tally.rb new file mode 100644 index 0000000000..d6ada53bab --- /dev/null +++ b/spec/mspec/lib/mspec/runner/actions/tally.rb @@ -0,0 +1,133 @@ +class Tally + attr_accessor :files, :examples, :expectations, :failures, :errors, :guards, :tagged + + def initialize + @files = @examples = @expectations = @failures = @errors = @guards = @tagged = 0 + end + + def files!(add = 1) + @files += add + end + + def examples!(add = 1) + @examples += add + end + + def expectations!(add = 1) + @expectations += add + end + + def failures!(add = 1) + @failures += add + end + + def errors!(add = 1) + @errors += add + end + + def guards!(add = 1) + @guards += add + end + + def tagged!(add = 1) + @tagged += add + end + + def file + pluralize files, "file" + end + + def example + pluralize examples, "example" + end + + def expectation + pluralize expectations, "expectation" + end + + def failure + pluralize failures, "failure" + end + + def error + pluralize errors, "error" + end + + def guard + pluralize guards, "guard" + end + + def tag + "#{tagged} tagged" + end + + def format + results = [ file, example, expectation, failure, error, tag ] + if [:report, :report_on, :verify].any? { |m| MSpec.mode? m } + results << guard + end + results.join(", ") + end + + alias_method :to_s, :format + + def pluralize(count, singular) + "#{count} #{singular}#{'s' unless count == 1}" + end + private :pluralize +end + +class TallyAction + attr_reader :counter + + def initialize + @counter = Tally.new + end + + def register + MSpec.register :load, self + MSpec.register :exception, self + MSpec.register :example, self + MSpec.register :tagged, self + MSpec.register :expectation, self + end + + def unregister + MSpec.unregister :load, self + MSpec.unregister :exception, self + MSpec.unregister :example, self + MSpec.unregister :tagged, self + MSpec.unregister :expectation, self + end + + def load + @counter.files! + end + + # Callback for the MSpec :expectation event. Increments the + # tally of expectations (e.g. #should, #should_receive, etc.). + def expectation(state) + @counter.expectations! + end + + # Callback for the MSpec :exception event. Increments the + # tally of errors and failures. + def exception(exception) + exception.failure? ? @counter.failures! : @counter.errors! + end + + # Callback for the MSpec :example event. Increments the tally + # of examples. + def example(state, block) + @counter.examples! + end + + def tagged(state) + @counter.examples! + @counter.tagged! + end + + def format + @counter.format + end +end diff --git a/spec/mspec/lib/mspec/runner/actions/timeout.rb b/spec/mspec/lib/mspec/runner/actions/timeout.rb new file mode 100644 index 0000000000..1200926872 --- /dev/null +++ b/spec/mspec/lib/mspec/runner/actions/timeout.rb @@ -0,0 +1,145 @@ +class TimeoutAction + def initialize(timeout) + @timeout = timeout + @queue = Queue.new + @started = now + @fail = false + @error_message = "took longer than the configured timeout of #{@timeout}s" + end + + def register + MSpec.register :start, self + MSpec.register :before, self + MSpec.register :after, self + MSpec.register :finish, self + end + + private def now + Process.clock_gettime(Process::CLOCK_MONOTONIC) + end + + private def fetch_item + @queue.pop(true) + rescue ThreadError + nil + end + + def start + @thread = Thread.new do + loop do + if action = fetch_item + action.call + else + wakeup_at = @started + @timeout + left = wakeup_at - now + sleep left if left > 0 + Thread.pass # Let the main thread run + + if @queue.empty? + elapsed = now - @started + if elapsed > @timeout + if @current_state + STDERR.puts "\nExample #{@error_message}:" + STDERR.puts "#{@current_state.description}" + else + STDERR.puts "\nSome code outside an example #{@error_message}" + end + STDERR.flush + + show_backtraces + if MSpec.subprocesses.empty? + exit! 2 + else + # Do not exit but signal the subprocess so we can get their output + MSpec.subprocesses.each do |pid| + kill_wait_one_second :SIGTERM, pid + hard_kill :SIGKILL, pid + end + @fail = true + @current_state = nil + break # stop this thread, will fail in #after + end + end + end + end + end + end + end + + def before(state = nil) + time = now + @queue << -> do + @current_state = state + @started = time + end + end + + def after(state = nil) + @queue << -> do + @current_state = nil + end + + if @fail + STDERR.puts "\n\nThe last example #{@error_message}. See above for the subprocess stacktrace." + exit! 2 + end + end + + def finish + @thread.kill + @thread.join + end + + private def hard_kill(signal, pid) + begin + Process.kill signal, pid + rescue Errno::ESRCH + # Process already terminated + end + end + + private def kill_wait_one_second(signal, pid) + begin + Process.kill signal, pid + sleep 1 + rescue Errno::ESRCH + # Process already terminated + end + end + + private def show_backtraces + java_stacktraces = -> pid { + if RUBY_ENGINE == 'truffleruby' || RUBY_ENGINE == 'jruby' + STDERR.puts 'Java stacktraces:' + kill_wait_one_second :SIGQUIT, pid + end + } + + if MSpec.subprocesses.empty? + java_stacktraces.call Process.pid + + STDERR.puts "\nRuby backtraces:" + if defined?(Truffle::Debug.show_backtraces) + Truffle::Debug.show_backtraces + else + Thread.list.each do |thread| + unless thread == Thread.current + STDERR.puts thread.inspect, thread.backtrace, '' + end + end + end + else + MSpec.subprocesses.each do |pid| + STDERR.puts "\nFor subprocess #{pid}" + java_stacktraces.call pid + + if RUBY_ENGINE == 'truffleruby' + STDERR.puts "\nRuby backtraces:" + kill_wait_one_second :SIGALRM, pid + else + STDERR.puts "Don't know how to print backtraces of a subprocess on #{RUBY_ENGINE}" + end + end + end + end +end diff --git a/spec/mspec/lib/mspec/runner/actions/timer.rb b/spec/mspec/lib/mspec/runner/actions/timer.rb new file mode 100644 index 0000000000..e7ebfebe0d --- /dev/null +++ b/spec/mspec/lib/mspec/runner/actions/timer.rb @@ -0,0 +1,22 @@ +class TimerAction + def register + MSpec.register :start, self + MSpec.register :finish, self + end + + def start + @start = Time.now + end + + def finish + @stop = Time.now + end + + def elapsed + @stop - @start + end + + def format + "Finished in %f seconds" % elapsed + end +end diff --git a/spec/mspec/lib/mspec/runner/context.rb b/spec/mspec/lib/mspec/runner/context.rb new file mode 100644 index 0000000000..bcd83b2465 --- /dev/null +++ b/spec/mspec/lib/mspec/runner/context.rb @@ -0,0 +1,237 @@ +# Holds the state of the +describe+ block that is being +# evaluated. Every example (i.e. +it+ block) is evaluated +# in a context, which may include state set up in <tt>before +# :each</tt> or <tt>before :all</tt> blocks. +# +#-- +# A note on naming: this is named _ContextState_ rather +# than _DescribeState_ because +describe+ is the keyword +# in the DSL for referring to the context in which an example +# is evaluated, just as +it+ refers to the example itself. +#++ +class ContextState + attr_reader :state, :parent, :parents, :children, :examples, :to_s + + MOCK_VERIFY = -> { Mock.verify_count } + MOCK_CLEANUP = -> { Mock.cleanup } + EXPECTATION_MISSING = -> { raise SpecExpectationNotFoundError } + + def initialize(description, options = nil) + raise "#describe options should be a Hash or nil" unless Hash === options or options.nil? + @to_s = description.to_s + @shared = options && options[:shared] + + @parsed = false + @before = { :all => [], :each => [] } + @after = { :all => [], :each => [] } + @pre = {} + @post = {} + @examples = [] + @state = nil + @parent = nil + @parents = [self] + @children = [] + end + + # Remove caching when a ContextState is dup'd for shared specs. + def initialize_copy(other) + @pre = {} + @post = {} + end + + # Returns true if this is a shared +ContextState+. Essentially, when + # created with: describe "Something", :shared => true { ... } + def shared? + @shared + end + + # Set the parent (enclosing) +ContextState+ for this state. Creates + # the +parents+ list. + def parent=(parent) + @description = nil + + if shared? + @parent = nil + else + @parent = parent + parent.child self if parent + + @parents = [self] + state = parent + while state + @parents.unshift state + state = state.parent + end + end + end + + # Add the ContextState instance +child+ to the list of nested + # describe blocks. + def child(child) + @children << child + end + + # Adds a nested ContextState in a shared ContextState to a containing + # ContextState. + # + # Normal adoption is from the parent's perspective. But adopt is a good + # verb and it's reasonable for the child to adopt the parent as well. In + # this case, manipulating state from inside the child avoids needlessly + # exposing the state to manipulate it externally in the dup. (See + # #it_should_behave_like) + def adopt(parent) + self.parent = parent + + @examples = @examples.map do |example| + example = example.dup + example.context = self + example + end + + children = @children + @children = [] + + children.each { |child| child.dup.adopt self } + end + + # Returns a list of all before(+what+) blocks from self and any parents. + def pre(what) + @pre[what] ||= parents.inject([]) { |l, s| l.push(*s.before(what)) } + end + + # Returns a list of all after(+what+) blocks from self and any parents. + # The list is in reverse order. In other words, the blocks defined in + # inner describes are in the list before those defined in outer describes, + # and in a particular describe block those defined later are in the list + # before those defined earlier. + def post(what) + @post[what] ||= parents.inject([]) { |l, s| l.unshift(*s.after(what)) } + end + + # Records before(:each) and before(:all) blocks. + def before(what, &block) + return if MSpec.guarded? + block ? @before[what].push(block) : @before[what] + end + + # Records after(:each) and after(:all) blocks. + def after(what, &block) + return if MSpec.guarded? + block ? @after[what].unshift(block) : @after[what] + end + + # Creates an ExampleState instance for the block and stores it + # in a list of examples to evaluate unless the example is filtered. + def it(desc, &block) + raise "nested #it" if @state + example = ExampleState.new(self, desc, block) + MSpec.actions :add, example + return if MSpec.guarded? + @examples << example + end + + # Evaluates the block and resets the toplevel +ContextState+ to #parent. + def describe(&block) + @parsed = protect @to_s, block, false + MSpec.register_current parent + MSpec.register_shared self if shared? + end + + # Returns a description string generated from self and all parents + def description + @description ||= parents.map { |p| p.to_s }.compact.join(" ") + end + + # Injects the before/after blocks and examples from the shared + # describe block into this +ContextState+ instance. + def it_should_behave_like(desc) + return if MSpec.guarded? + + unless state = MSpec.retrieve_shared(desc) + raise Exception, "Unable to find shared 'describe' for #{desc}" + end + + state.before(:all).each { |b| before :all, &b } + state.before(:each).each { |b| before :each, &b } + state.after(:each).each { |b| after :each, &b } + state.after(:all).each { |b| after :all, &b } + + state.examples.each do |example| + example = example.dup + example.context = self + @examples << example + end + + state.children.each do |child| + child.dup.adopt self + end + end + + # Evaluates each block in +blocks+ using the +MSpec.protect+ method + # so that exceptions are handled and tallied. Returns true and does + # NOT evaluate any blocks if +check+ is true and + # <tt>MSpec.mode?(:pretend)</tt> is true. + def protect(what, blocks, check = true) + return true if check and MSpec.mode? :pretend + Array(blocks).all? { |block| MSpec.protect what, &block } + end + + # Removes filtered examples. Returns true if there are examples + # left to evaluate. + def filter_examples + filtered, @examples = @examples.partition do |ex| + ex.filtered? + end + + filtered.each do |ex| + MSpec.actions :tagged, ex + end + + !@examples.empty? + end + + # Evaluates the examples in a +ContextState+. Invokes the MSpec events + # for :enter, :before, :after, :leave. + def process + MSpec.register_current self + + if @parsed and filter_examples + MSpec.shuffle @examples if MSpec.randomize? + MSpec.actions :enter, description + + if protect "before :all", pre(:all) + @examples.each do |state| + MSpec.repeat do + @state = state + example = state.example + MSpec.actions :before, state + + if protect "before :each", pre(:each) + MSpec.clear_expectations + if example + passed = protect nil, example + passed = protect nil, -> { MSpec.actions :passed, state, example } if passed + MSpec.actions :example, state, example + protect nil, EXPECTATION_MISSING if !MSpec.expectation? and passed + end + end + protect "after :each", post(:each) + protect "Mock.verify_count", MOCK_VERIFY + + protect "Mock.cleanup", MOCK_CLEANUP + MSpec.actions :after, state + @state = nil + end + end + protect "after :all", post(:all) + else + protect "Mock.cleanup", MOCK_CLEANUP + end + + MSpec.actions :leave + end + + MSpec.register_current nil + children.each { |child| child.process } + end +end diff --git a/spec/mspec/lib/mspec/runner/evaluate.rb b/spec/mspec/lib/mspec/runner/evaluate.rb new file mode 100644 index 0000000000..396a84c118 --- /dev/null +++ b/spec/mspec/lib/mspec/runner/evaluate.rb @@ -0,0 +1,54 @@ +class SpecEvaluate + include MSpecMatchers + + def self.desc=(desc) + @desc = desc + end + + def self.desc + @desc ||= "evaluates " + end + + def initialize(ruby, desc) + @ruby = ruby.rstrip + @desc = desc || self.class.desc + end + + # Formats the Ruby source code for reabable output in the -fs formatter + # option. If the source contains no newline characters, wraps the source in + # single quotes to set if off from the rest of the description string. If + # the source does contain newline characters, sets the indent level to four + # characters. + def format(ruby, newline = true) + if ruby.include?("\n") + lines = ruby.each_line.to_a + if /( *)/ =~ lines.first + if $1.size > 4 + dedent = $1.size - 4 + ruby = lines.map { |l| l[dedent..-1] }.join + else + indent = " " * (4 - $1.size) + ruby = lines.map { |l| "#{indent}#{l}" }.join + end + end + "\n#{ruby}" + else + "'#{ruby.lstrip}'" + end + end + + def define(&block) + ruby = @ruby + desc = @desc + evaluator = self + + specify "#{desc} #{format ruby}" do + evaluator.instance_eval(ruby) + evaluator.instance_eval(&block) + end + end +end + +def evaluate(str, desc = nil, &block) + SpecEvaluate.new(str, desc).define(&block) +end diff --git a/spec/mspec/lib/mspec/runner/example.rb b/spec/mspec/lib/mspec/runner/example.rb new file mode 100644 index 0000000000..0d9f0d618c --- /dev/null +++ b/spec/mspec/lib/mspec/runner/example.rb @@ -0,0 +1,34 @@ +require 'mspec/runner/mspec' + +# Holds some of the state of the example (i.e. +it+ block) that is +# being evaluated. See also +ContextState+. +class ExampleState + attr_reader :context, :it, :example + + def initialize(context, it, example = nil) + @context = context + @it = it + @example = example + end + + def context=(context) + @description = nil + @context = context + end + + def describe + @context.description + end + + def description + @description ||= "#{describe} #{@it}" + end + + def filtered? + incl = MSpec.include + excl = MSpec.exclude + included = incl.empty? || incl.any? { |f| f === description } + included &&= excl.empty? || !excl.any? { |f| f === description } + !included + end +end diff --git a/spec/mspec/lib/mspec/runner/exception.rb b/spec/mspec/lib/mspec/runner/exception.rb new file mode 100644 index 0000000000..23375733e6 --- /dev/null +++ b/spec/mspec/lib/mspec/runner/exception.rb @@ -0,0 +1,54 @@ +# Initialize $MSPEC_DEBUG +$MSPEC_DEBUG ||= false + +class ExceptionState + attr_reader :description, :describe, :it, :exception + + def initialize(state, location, exception) + @exception = exception + @failure = exception.class == SpecExpectationNotMetError || exception.class == SpecExpectationNotFoundError + + @description = location ? "An exception occurred during: #{location}" : "" + if state + @description += "\n" unless @description.empty? + @description += state.description + @describe = state.describe + @it = state.it + else + @describe = @it = "" + end + end + + def failure? + @failure + end + + def message + message = @exception.message + message = "<No message>" if message.empty? + + if @failure + message + elsif raise_error_message = RaiseErrorMatcher::FAILURE_MESSAGE_FOR_EXCEPTION[@exception] + raise_error_message.join("\n") + else + "#{@exception.class}: #{message}" + end + end + + def backtrace + @backtrace_filter ||= MSpecScript.config[:backtrace_filter] || %r{(?:/bin/mspec|/lib/mspec/)} + + bt = @exception.backtrace || [] + unless $MSPEC_DEBUG + # Exclude <internal: entries inside MSpec code, so only after the first ignored entry + first_excluded_line = bt.index { |line| @backtrace_filter =~ line } + if first_excluded_line + bt = bt[0...first_excluded_line] + bt[first_excluded_line..-1].reject { |line| + @backtrace_filter =~ line || /^<internal:/ =~ line + } + end + end + bt.join("\n") + end +end diff --git a/spec/mspec/lib/mspec/runner/filters.rb b/spec/mspec/lib/mspec/runner/filters.rb new file mode 100644 index 0000000000..d0420faca6 --- /dev/null +++ b/spec/mspec/lib/mspec/runner/filters.rb @@ -0,0 +1,4 @@ +require 'mspec/runner/filters/match' +require 'mspec/runner/filters/regexp' +require 'mspec/runner/filters/tag' +require 'mspec/runner/filters/profile' diff --git a/spec/mspec/lib/mspec/runner/filters/match.rb b/spec/mspec/lib/mspec/runner/filters/match.rb new file mode 100644 index 0000000000..539fd02d01 --- /dev/null +++ b/spec/mspec/lib/mspec/runner/filters/match.rb @@ -0,0 +1,18 @@ +class MatchFilter + def initialize(what, *strings) + @what = what + @strings = strings + end + + def ===(string) + @strings.any? { |s| string.include?(s) } + end + + def register + MSpec.register @what, self + end + + def unregister + MSpec.unregister @what, self + end +end diff --git a/spec/mspec/lib/mspec/runner/filters/profile.rb b/spec/mspec/lib/mspec/runner/filters/profile.rb new file mode 100644 index 0000000000..a59722c451 --- /dev/null +++ b/spec/mspec/lib/mspec/runner/filters/profile.rb @@ -0,0 +1,54 @@ +class ProfileFilter + def initialize(what, *files) + @what = what + @methods = load(*files) + @pattern = /([^ .#]+[.#])([^ ]+)/ + end + + def find(name) + return name if File.exist?(File.expand_path(name)) + + ["spec/profiles", "spec", "profiles", "."].each do |dir| + file = File.join dir, name + return file if File.exist? file + end + end + + def parse(file) + pattern = /(\S+):\s*/ + key = "" + file.inject(Hash.new { |h,k| h[k] = [] }) do |hash, line| + line.chomp! + if line[0,2] == "- " + hash[key] << line[2..-1].gsub(/[ '"]/, "") + elsif m = pattern.match(line) + key = m[1] + end + hash + end + end + + def load(*files) + files.inject({}) do |hash, file| + next hash unless name = find(file) + + File.open name, "r" do |f| + hash.merge parse(f) + end + end + end + + def ===(string) + return false unless m = @pattern.match(string) + return false unless l = @methods[m[1]] + l.include? m[2] + end + + def register + MSpec.register @what, self + end + + def unregister + MSpec.unregister @what, self + end +end diff --git a/spec/mspec/lib/mspec/runner/filters/regexp.rb b/spec/mspec/lib/mspec/runner/filters/regexp.rb new file mode 100644 index 0000000000..097ec6a755 --- /dev/null +++ b/spec/mspec/lib/mspec/runner/filters/regexp.rb @@ -0,0 +1,23 @@ +class RegexpFilter + def initialize(what, *regexps) + @what = what + @regexps = to_regexp(*regexps) + end + + def ===(string) + @regexps.any? { |regexp| regexp === string } + end + + def register + MSpec.register @what, self + end + + def unregister + MSpec.unregister @what, self + end + + def to_regexp(*regexps) + regexps.map { |str| Regexp.new str } + end + private :to_regexp +end diff --git a/spec/mspec/lib/mspec/runner/filters/tag.rb b/spec/mspec/lib/mspec/runner/filters/tag.rb new file mode 100644 index 0000000000..c641c01606 --- /dev/null +++ b/spec/mspec/lib/mspec/runner/filters/tag.rb @@ -0,0 +1,29 @@ +class TagFilter + def initialize(what, *tags) + @what = what + @tags = tags + end + + def load + @descriptions = MSpec.read_tags(@tags).map { |t| t.description } + MSpec.register @what, self + end + + def unload + MSpec.unregister @what, self + end + + def ===(string) + @descriptions.include?(string) + end + + def register + MSpec.register :load, self + MSpec.register :unload, self + end + + def unregister + MSpec.unregister :load, self + MSpec.unregister :unload, self + end +end diff --git a/spec/mspec/lib/mspec/runner/formatters.rb b/spec/mspec/lib/mspec/runner/formatters.rb new file mode 100644 index 0000000000..66f515ddff --- /dev/null +++ b/spec/mspec/lib/mspec/runner/formatters.rb @@ -0,0 +1,13 @@ +require 'mspec/runner/formatters/describe' +require 'mspec/runner/formatters/dotted' +require 'mspec/runner/formatters/file' +require 'mspec/runner/formatters/specdoc' +require 'mspec/runner/formatters/html' +require 'mspec/runner/formatters/summary' +require 'mspec/runner/formatters/unit' +require 'mspec/runner/formatters/spinner' +require 'mspec/runner/formatters/method' +require 'mspec/runner/formatters/stats' +require 'mspec/runner/formatters/yaml' +require 'mspec/runner/formatters/profile' +require 'mspec/runner/formatters/junit' diff --git a/spec/mspec/lib/mspec/runner/formatters/base.rb b/spec/mspec/lib/mspec/runner/formatters/base.rb new file mode 100644 index 0000000000..882e15c8c2 --- /dev/null +++ b/spec/mspec/lib/mspec/runner/formatters/base.rb @@ -0,0 +1,150 @@ +require 'mspec/expectations/expectations' +require 'mspec/runner/actions/timer' +require 'mspec/runner/actions/tally' +require 'mspec/utils/options' + +if ENV['CHECK_LEAKS'] + require 'mspec/runner/actions/leakchecker' +end + +if ENV['CHECK_LEAKS'] || ENV['CHECK_CONSTANT_LEAKS'] + require 'mspec/runner/actions/constants_leak_checker' +end + +class BaseFormatter + attr_reader :exceptions, :timer, :tally + + def initialize(out = nil) + @current_state = nil + @exception = false + @failure = false + @exceptions = [] + + @count = 0 # For subclasses + + if out + @out = File.open out, "w" + else + @out = $stdout + end + + err = MSpecOptions.latest && MSpecOptions.latest.config[:error_output] + if err + @err = (err == 'stderr') ? $stderr : File.open(err, "w") + else + @err = @out + end + end + + # Creates the +TimerAction+ and +TallyAction+ instances and registers them. + def register + (@timer = TimerAction.new).register + (@tally = TallyAction.new).register + @counter = @tally.counter + + if ENV['CHECK_LEAKS'] + LeakCheckerAction.new.register + end + + if (ENV['CHECK_LEAKS'] || ENV['CHECK_CONSTANT_LEAKS']) && ENV['CHECK_CONSTANT_LEAKS'] != 'false' + save = ENV['CHECK_LEAKS'] == 'save' || ENV['CHECK_CONSTANT_LEAKS'] == 'save' + ConstantsLeakCheckerAction.new(save).register + end + + MSpec.register :abort, self + MSpec.register :before, self + MSpec.register :after, self + MSpec.register :exception, self + MSpec.register :finish, self + end + + def abort + if @current_state + puts "\naborting example: #{@current_state.description}" + end + end + + # Returns true if any exception is raised while running + # an example. This flag is reset before each example + # is evaluated. + def exception? + @exception + end + + # Returns true if all exceptions during the evaluation + # of an example are failures rather than errors. See + # <tt>ExceptionState#failure</tt>. This flag is reset + # before each example is evaluated. + def failure? + @failure + end + + # Callback for the MSpec :before event. Resets the + # +#exception?+ and +#failure+ flags. + def before(state = nil) + @current_state = state + @failure = @exception = false + end + + # Callback for the MSpec :exception event. Stores the + # +ExceptionState+ object to generate the list of backtraces + # after all the specs are run. Also updates the internal + # +#exception?+ and +#failure?+ flags. + def exception(exception) + @count += 1 + @failure = @exception ? @failure && exception.failure? : exception.failure? + @exception = true + @exceptions << exception + end + + # Callback for the MSpec :after event. + def after(state = nil) + @current_state = nil + end + + # Callback for the MSpec :start event. Calls :after event. + # Defined here, in the base class, and used by MultiFormatter. + def start + after + end + + # Callback for the MSpec :unload event. Calls :after event. + # Defined here, in the base class, and used by MultiFormatter. + def unload + after + end + + # Callback for the MSpec :finish event. Prints a description + # and backtrace for every exception that occurred while + # evaluating the examples. + def finish + print "\n" + + if MSpecOptions.latest && MSpecOptions.latest.config[:print_skips] + print "\nSkips:\n" unless MSpec.skips.empty? + MSpec.skips.each do |skip, block| + print "#{skip.message} in #{(block.source_location || ['?']).join(':')}\n" + end + end + + count = 0 + @exceptions.each do |exc| + count += 1 + print_exception(exc, count) + end + print "\n#{@timer.format}\n\n#{@tally.format}\n" + end + + def print_exception(exc, count) + outcome = exc.failure? ? "FAILED" : "ERROR" + @err.print "\n#{count})\n#{exc.description} #{outcome}\n" + @err.print exc.message, "\n" + @err.print exc.backtrace, "\n" + end + + # A convenience method to allow printing to different outputs. + def print(*args) + @out.print(*args) + @out.flush + end +end diff --git a/spec/mspec/lib/mspec/runner/formatters/describe.rb b/spec/mspec/lib/mspec/runner/formatters/describe.rb new file mode 100644 index 0000000000..fc4122d13b --- /dev/null +++ b/spec/mspec/lib/mspec/runner/formatters/describe.rb @@ -0,0 +1,23 @@ +require 'mspec/runner/formatters/dotted' + +class DescribeFormatter < DottedFormatter + # Callback for the MSpec :finish event. Prints a summary of + # the number of errors and failures for each +describe+ block. + def finish + describes = Hash.new { |h,k| h[k] = Tally.new } + + @exceptions.each do |exc| + desc = describes[exc.describe] + exc.failure? ? desc.failures! : desc.errors! + end + + print "\n" + describes.each do |d, t| + text = d.size > 40 ? "#{d[0,37]}..." : d.ljust(40) + print "\n#{text} #{t.failure}, #{t.error}" + end + print "\n" unless describes.empty? + + print "\n#{@timer.format}\n\n#{@tally.format}\n" + end +end diff --git a/spec/mspec/lib/mspec/runner/formatters/dotted.rb b/spec/mspec/lib/mspec/runner/formatters/dotted.rb new file mode 100644 index 0000000000..672cdf81dc --- /dev/null +++ b/spec/mspec/lib/mspec/runner/formatters/dotted.rb @@ -0,0 +1,23 @@ +require 'mspec/runner/formatters/base' + +class DottedFormatter < BaseFormatter + def register + super + MSpec.register :after, self + end + + # Callback for the MSpec :after event. Prints an indicator + # for the result of evaluating this example as follows: + # . = No failure or error + # F = An SpecExpectationNotMetError was raised + # E = Any exception other than SpecExpectationNotMetError + def after(state = nil) + super(state) + + if exception? + print failure? ? "F" : "E" + else + print "." + end + end +end diff --git a/spec/mspec/lib/mspec/runner/formatters/file.rb b/spec/mspec/lib/mspec/runner/formatters/file.rb new file mode 100644 index 0000000000..65cfb1f75b --- /dev/null +++ b/spec/mspec/lib/mspec/runner/formatters/file.rb @@ -0,0 +1,24 @@ +require 'mspec/runner/formatters/dotted' + +class FileFormatter < DottedFormatter + # Unregisters DottedFormatter#before, #after methods and + # registers #load, #unload, which perform the same duties + # as #before, #after in DottedFormatter. + def register + super + + MSpec.unregister :before, self + MSpec.unregister :after, self + + MSpec.register :load, self + MSpec.register :unload, self + end + + def load(state = nil) + before(state) + end + + def unload(state = nil) + after(state) + end +end diff --git a/spec/mspec/lib/mspec/runner/formatters/html.rb b/spec/mspec/lib/mspec/runner/formatters/html.rb new file mode 100644 index 0000000000..e37e89a088 --- /dev/null +++ b/spec/mspec/lib/mspec/runner/formatters/html.rb @@ -0,0 +1,81 @@ +require 'mspec/runner/formatters/base' + +class HtmlFormatter < BaseFormatter + def register + super + MSpec.register :start, self + MSpec.register :enter, self + MSpec.register :leave, self + end + + def start + print <<-EOH +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" + "http://www.w3.org/TR/html4/strict.dtd"> +<html> +<head> +<title>Spec Output For #{RUBY_ENGINE} (#{RUBY_VERSION})</title> +<style type="text/css"> +ul { + list-style: none; +} +.fail { + color: red; +} +.pass { + color: green; +} +#details :target { + background-color: #ffffe0; +} +</style> +</head> +<body> +EOH + end + + def enter(describe) + print "<div><p>#{describe}</p>\n<ul>\n" + end + + def leave + print "</ul>\n</div>\n" + end + + def exception(exception) + super(exception) + outcome = exception.failure? ? "FAILED" : "ERROR" + print %[<li class="fail">- #{exception.it} (<a href="#details-#{@count}">] + print %[#{outcome} - #{@count}</a>)</li>\n] + end + + def after(state = nil) + super(state) + print %[<li class="pass">- #{state.it}</li>\n] unless exception? + end + + def finish + success = @exceptions.empty? + unless success + print "<hr>\n" + print %[<ol id="details">] + count = 0 + @exceptions.each do |exc| + outcome = exc.failure? ? "FAILED" : "ERROR" + print %[\n<li id="details-#{count += 1}"><p>#{escape(exc.description)} #{outcome}</p>\n<p>] + print escape(exc.message) + print "</p>\n<pre>\n" + print escape(exc.backtrace) + print "</pre>\n</li>\n" + end + print "</ol>\n" + end + print %[<p>#{@timer.format}</p>\n] + print %[<p class="#{success ? "pass" : "fail"}">#{@tally.format}</p>\n] + print "</body>\n</html>\n" + end + + def escape(string) + string.gsub("&", " ").gsub("<", "<").gsub(">", ">") + end +end diff --git a/spec/mspec/lib/mspec/runner/formatters/junit.rb b/spec/mspec/lib/mspec/runner/formatters/junit.rb new file mode 100644 index 0000000000..6351ccbce9 --- /dev/null +++ b/spec/mspec/lib/mspec/runner/formatters/junit.rb @@ -0,0 +1,87 @@ +require 'mspec/runner/formatters/yaml' + +class JUnitFormatter < YamlFormatter + def initialize(out = nil) + super(out) + @tests = [] + end + + def after(state = nil) + super(state) + @tests << {:test => state, :exception => false} unless exception? + end + + def exception(exception) + super(exception) + @tests << {:test => exception, :exception => true} + end + + def finish + switch + + time = @timer.elapsed + tests = @tally.counter.examples + errors = @tally.counter.errors + failures = @tally.counter.failures + + print <<-XML + +<?xml version="1.0" encoding="UTF-8" ?> + <testsuites + testCount="#{tests}" + errorCount="#{errors}" + failureCount="#{failures}" + timeCount="#{time}" time="#{time}"> + <testsuite + tests="#{tests}" + errors="#{errors}" + failures="#{failures}" + time="#{time}" + name="Spec Output For #{::RUBY_ENGINE} (#{::RUBY_VERSION})"> + XML + @tests.each do |h| + description = encode_for_xml h[:test].description + + print <<-XML + <testcase classname="Spec" name="#{description}" time="0.0"> + XML + if h[:exception] + outcome = h[:test].failure? ? "failure" : "error" + message = encode_for_xml h[:test].message + backtrace = encode_for_xml h[:test].backtrace + print <<-XML + <#{outcome} message="error in #{description}" type="#{outcome}"> + #{message} + #{backtrace} + </#{outcome}> + XML + end + print <<-XML + </testcase> + XML + end + + print <<-XML + </testsuite> + </testsuites> + XML + end + + private + LT = "<" + GT = ">" + QU = """ + AP = "'" + AM = "&" + TARGET_ENCODING = "ISO-8859-1" + + def encode_for_xml(str) + encode_as_latin1(str).gsub("<", LT).gsub(">", GT). + gsub('"', QU).gsub("'", AP).gsub("&", AM). + tr("\x00-\x08", "?") + end + + def encode_as_latin1(str) + str.encode(TARGET_ENCODING, :undef => :replace, :invalid => :replace) + end +end diff --git a/spec/mspec/lib/mspec/runner/formatters/launchable.rb b/spec/mspec/lib/mspec/runner/formatters/launchable.rb new file mode 100644 index 0000000000..f738781c71 --- /dev/null +++ b/spec/mspec/lib/mspec/runner/formatters/launchable.rb @@ -0,0 +1,88 @@ +module LaunchableFormatter + def self.extend_object(obj) + super + obj.init + end + + def self.setDir(dir) + @@path = File.join(dir, "#{rand.to_s}.json") + self + end + + def init + @timer = nil + @tests = [] + end + + def before(state = nil) + super + @timer = TimerAction.new + @timer.start + end + + def after(state = nil) + super + @timer.finish + file = MSpec.file + return if file.nil? || state&.example.nil? || exception? + + @tests << {:test => state, :file => file, :exception => false, duration: @timer.elapsed} + end + + def exception(exception) + super + @timer.finish + file = MSpec.file + return if file.nil? + + @tests << {:test => exception, :file => file, :exception => true, duration: @timer.elapsed} + end + + def finish + super + + require_relative '../../../../../../tool/lib/launchable' + + @writer = writer = Launchable::JsonStreamWriter.new(@@path) + @writer.write_array('testCases') + at_exit { + @writer.close + } + + repo_path = File.expand_path("#{__dir__}/../../../../../../") + + @tests.each do |t| + testcase = t[:test].description + relative_path = t[:file].delete_prefix("#{repo_path}/") + # The test path is a URL-encoded representation. + # https://github.com/launchableinc/cli/blob/v1.81.0/launchable/testpath.py#L18 + test_path = {file: relative_path, testcase: testcase}.map{|key, val| + "#{encode_test_path_component(key)}=#{encode_test_path_component(val)}" + }.join('#') + + status = 'TEST_PASSED' + if t[:exception] + message = t[:test].message + backtrace = t[:test].backtrace + e = "#{message}\n#{backtrace}" + status = 'TEST_FAILED' + end + + @writer.write_object( + { + testPath: test_path, + status: status, + duration: t[:duration], + createdAt: Time.now.to_s, + stderr: e, + stdout: nil + } + ) + end + end + + private + def encode_test_path_component component + component.to_s.gsub('%', '%25').gsub('=', '%3D').gsub('#', '%23').gsub('&', '%26').tr("\x00-\x08", "") + end +end diff --git a/spec/mspec/lib/mspec/runner/formatters/method.rb b/spec/mspec/lib/mspec/runner/formatters/method.rb new file mode 100644 index 0000000000..925858c845 --- /dev/null +++ b/spec/mspec/lib/mspec/runner/formatters/method.rb @@ -0,0 +1,95 @@ +require 'mspec/runner/formatters/base' + +class MethodFormatter < BaseFormatter + attr_accessor :methods + + def initialize(out = nil) + super(out) + @methods = Hash.new do |h, k| + h[k] = { + examples: 0, + expectations: 0, + failures: 0, + errors: 0, + exceptions: [] + } + end + end + + # Returns the type of method as a "class", "instance", + # or "unknown". + def method_type(sep) + case sep + when '.', '::' + "class" + when '#' + "instance" + else + "unknown" + end + end + + # Callback for the MSpec :before event. Parses the + # describe string into class and method if possible. + # Resets the tallies so the counts are only for this + # example. + def before(state) + super(state) + + # The pattern for a method name is not correctly + # restrictive but it is simplistic and useful + # for our purpose. + /^([A-Za-z_]+\w*)(\.|#|::)([^ ]+)/ =~ state.describe + @key = $1 && $2 && $3 ? "#{$1}#{$2}#{$3}" : state.describe + + unless methods.key? @key + h = methods[@key] + h[:class] = "#{$1}" + h[:method] = "#{$3}" + h[:type] = method_type $2 + h[:description] = state.description + end + + tally.counter.examples = 0 + tally.counter.expectations = 0 + tally.counter.failures = 0 + tally.counter.errors = 0 + + @exceptions = [] + end + + # Callback for the MSpec :after event. Sets or adds to + # tallies for the example block. + def after(state = nil) + super(state) + + h = methods[@key] + h[:examples] += tally.counter.examples + h[:expectations] += tally.counter.expectations + h[:failures] += tally.counter.failures + h[:errors] += tally.counter.errors + @exceptions.each do |exc| + h[:exceptions] << "#{exc.message}\n#{exc.backtrace}\n" + end + end + + # Callback for the MSpec :finish event. Prints out the + # summary information in YAML format for all the methods. + def finish + print "---\n" + + methods.each do |key, hash| + print key.inspect, ":\n" + print " class: ", hash[:class].inspect, "\n" + print " method: ", hash[:method].inspect, "\n" + print " type: ", hash[:type], "\n" + print " description: ", hash[:description].inspect, "\n" + print " examples: ", hash[:examples], "\n" + print " expectations: ", hash[:expectations], "\n" + print " failures: ", hash[:failures], "\n" + print " errors: ", hash[:errors], "\n" + print " exceptions:\n" + hash[:exceptions].each { |exc| print " - ", exc.inspect, "\n" } + end + end +end diff --git a/spec/mspec/lib/mspec/runner/formatters/multi.rb b/spec/mspec/lib/mspec/runner/formatters/multi.rb new file mode 100644 index 0000000000..fa1da3766b --- /dev/null +++ b/spec/mspec/lib/mspec/runner/formatters/multi.rb @@ -0,0 +1,47 @@ +module MultiFormatter + def self.extend_object(obj) + super + obj.multi_initialize + end + + def multi_initialize + @tally = TallyAction.new + @counter = @tally.counter + @timer = TimerAction.new + @timer.start + end + + def register + super + + MSpec.register :start, self + MSpec.register :unload, self + MSpec.unregister :before, self + end + + def aggregate_results(files) + require 'yaml' + + @timer.finish + @exceptions = [] + + files.each do |file| + contents = File.read(file) + d = YAML.load(contents) + File.delete file + + if d # The file might be empty if the child process died + @exceptions += Array(d['exceptions']) + @counter.files! d['files'] + @counter.examples! d['examples'] + @counter.expectations! d['expectations'] + @counter.errors! d['errors'] + @counter.failures! d['failures'] + end + end + end + + def print_exception(exc, count) + @err.print "\n#{count})\n#{exc}\n" + end +end diff --git a/spec/mspec/lib/mspec/runner/formatters/profile.rb b/spec/mspec/lib/mspec/runner/formatters/profile.rb new file mode 100644 index 0000000000..38ef5b12ed --- /dev/null +++ b/spec/mspec/lib/mspec/runner/formatters/profile.rb @@ -0,0 +1,18 @@ +require 'mspec/runner/formatters/dotted' +require 'mspec/runner/actions/profile' + +class ProfileFormatter < DottedFormatter + def initialize(out = nil) + super(out) + + @describe_name = nil + @describe_time = nil + @describes = [] + @its = [] + end + + def register + (@profile = ProfileAction.new).register + super + end +end diff --git a/spec/mspec/lib/mspec/runner/formatters/specdoc.rb b/spec/mspec/lib/mspec/runner/formatters/specdoc.rb new file mode 100644 index 0000000000..d3a5c3d729 --- /dev/null +++ b/spec/mspec/lib/mspec/runner/formatters/specdoc.rb @@ -0,0 +1,41 @@ +require 'mspec/runner/formatters/base' + +class SpecdocFormatter < BaseFormatter + def register + super + MSpec.register :enter, self + end + + # Callback for the MSpec :enter event. Prints the + # +describe+ block string. + def enter(describe) + print "\n#{describe}\n" + end + + # Callback for the MSpec :before event. Prints the + # +it+ block string. + def before(state) + super(state) + print "- #{state.it}" + end + + # Callback for the MSpec :exception event. Prints + # either 'ERROR - X' or 'FAILED - X' where _X_ is + # the sequential number of the exception raised. If + # there has already been an exception raised while + # evaluating this example, it prints another +it+ + # block description string so that each description + # string has an associated 'ERROR' or 'FAILED' + def exception(exception) + print "\n- #{exception.it}" if exception? + super(exception) + print " (#{exception.failure? ? 'FAILED' : 'ERROR'} - #{@count})" + end + + # Callback for the MSpec :after event. Prints a + # newline to finish the description string output. + def after(state = nil) + super(state) + print "\n" + end +end diff --git a/spec/mspec/lib/mspec/runner/formatters/spinner.rb b/spec/mspec/lib/mspec/runner/formatters/spinner.rb new file mode 100644 index 0000000000..817d8c02be --- /dev/null +++ b/spec/mspec/lib/mspec/runner/formatters/spinner.rb @@ -0,0 +1,111 @@ +require 'mspec/runner/formatters/base' + +class SpinnerFormatter < BaseFormatter + attr_reader :length + + Spins = %w!| / - \\! + HOUR = 3600 + MIN = 60 + + def initialize(out = nil) + super(nil) + + @which = 0 + @loaded = 0 + self.length = 40 + @percent = 0 + @start = Time.now + + term = ENV['TERM'] + @color = (term != "dumb") + @fail_color = "32" + @error_color = "32" + end + + def register + super + + MSpec.register :start, self + MSpec.register :unload, self + end + + def length=(length) + @length = length + @ratio = 100.0 / length + @position = length / 2 - 2 + end + + def compute_etr + return @etr = "00:00:00" if @percent == 0 + elapsed = Time.now - @start + remain = (100 * elapsed / @percent) - elapsed + + hour = remain >= HOUR ? (remain / HOUR).to_i : 0 + remain -= hour * HOUR + min = remain >= MIN ? (remain / MIN).to_i : 0 + sec = remain - min * MIN + + @etr = "%02d:%02d:%02d" % [hour, min, sec] + end + + def compute_percentage + @percent = @loaded * 100 / @total + bar = ("=" * (@percent / @ratio)).ljust @length + label = "%d%%" % @percent + bar[@position, label.size] = label + @bar = bar + end + + def compute_progress + compute_percentage + compute_etr + end + + def progress_line + @which = (@which + 1) % Spins.size + data = [Spins[@which], @bar, @etr, @counter.failures, @counter.errors] + if @color + "\r[%s | %s | %s] \e[0;#{@fail_color}m%6dF \e[0;#{@error_color}m%6dE\e[0m " % data + else + "\r[%s | %s | %s] %6dF %6dE " % data + end + end + + def clear_progress_line + print "\r#{' '*progress_line.length}" + end + + # Callback for the MSpec :start event. Stores the total + # number of files that will be processed. + def start + @total = MSpec.files_array.size + compute_progress + print progress_line + end + + # Callback for the MSpec :unload event. Increments the number + # of files that have been run. + def unload + @loaded += 1 + compute_progress + print progress_line + end + + # Callback for the MSpec :exception event. Changes the color + # used to display the tally of errors and failures + def exception(exception) + super + @fail_color = "31" if exception.failure? + @error_color = "33" unless exception.failure? + + clear_progress_line + print_exception(exception, @count) + exceptions.clear + end + + # Callback for the MSpec :after event. Updates the spinner. + def after(state = nil) + super(state) + print progress_line + end +end diff --git a/spec/mspec/lib/mspec/runner/formatters/stats.rb b/spec/mspec/lib/mspec/runner/formatters/stats.rb new file mode 100644 index 0000000000..8cff96d145 --- /dev/null +++ b/spec/mspec/lib/mspec/runner/formatters/stats.rb @@ -0,0 +1,57 @@ +require 'mspec/runner/formatters/base' + +class StatsPerFileFormatter < BaseFormatter + def initialize(out = nil) + super(out) + @data = {} + @root = File.expand_path(MSpecScript.get(:prefix) || '.') + end + + def register + super + MSpec.register :load, self + MSpec.register :unload, self + end + + # Resets the tallies so the counts are only for this file. + def load + tally.counter.examples = 0 + tally.counter.errors = 0 + tally.counter.failures = 0 + tally.counter.tagged = 0 + end + + def unload + file = format_file MSpec.file + + raise if @data.key?(file) + @data[file] = { + examples: tally.counter.examples, + errors: tally.counter.errors, + failures: tally.counter.failures, + tagged: tally.counter.tagged, + } + end + + def finish + width = @data.keys.max_by(&:size).size + f = "%3d" + @data.each_pair do |file, data| + total = data[:examples] + passing = total - data[:errors] - data[:failures] - data[:tagged] + puts "#{file.ljust(width)} #{f % passing}/#{f % total}" + end + + require 'yaml' + yaml = YAML.dump(@data) + File.write "results-#{RUBY_ENGINE}-#{RUBY_ENGINE_VERSION}.yml", yaml + end + + private def format_file(file) + if file.start_with?(@root) + file[@root.size+1..-1] + else + raise file + end + end +end diff --git a/spec/mspec/lib/mspec/runner/formatters/summary.rb b/spec/mspec/lib/mspec/runner/formatters/summary.rb new file mode 100644 index 0000000000..41819d2158 --- /dev/null +++ b/spec/mspec/lib/mspec/runner/formatters/summary.rb @@ -0,0 +1,4 @@ +require 'mspec/runner/formatters/base' + +class SummaryFormatter < BaseFormatter +end diff --git a/spec/mspec/lib/mspec/runner/formatters/unit.rb b/spec/mspec/lib/mspec/runner/formatters/unit.rb new file mode 100644 index 0000000000..d03ae79e9f --- /dev/null +++ b/spec/mspec/lib/mspec/runner/formatters/unit.rb @@ -0,0 +1,20 @@ +require 'mspec/runner/formatters/dotted' + +class UnitdiffFormatter < DottedFormatter + def finish + print "\n\n#{@timer.format}\n" + count = 0 + @exceptions.each do |exc| + outcome = exc.failure? ? "FAILED" : "ERROR" + print "\n#{count += 1})\n#{exc.description} #{outcome}\n" + print exc.message, ":\n" + print exc.backtrace, "\n" + end + print "\n#{@tally.format}\n" + end + + def backtrace(exc) + exc.backtrace && exc.backtrace.join("\n") + end + private :backtrace +end diff --git a/spec/mspec/lib/mspec/runner/formatters/yaml.rb b/spec/mspec/lib/mspec/runner/formatters/yaml.rb new file mode 100644 index 0000000000..6c05cc902f --- /dev/null +++ b/spec/mspec/lib/mspec/runner/formatters/yaml.rb @@ -0,0 +1,38 @@ +require 'mspec/runner/formatters/base' + +class YamlFormatter < BaseFormatter + def initialize(out = nil) + super(nil) + + if out.nil? + @finish = $stdout + else + @finish = File.open out, "w" + end + end + + def switch + @out = @finish + end + + def finish + switch + + print "---\n" + print "exceptions:\n" + @exceptions.each do |exc| + outcome = exc.failure? ? "FAILED" : "ERROR" + str = "#{exc.description} #{outcome}\n" + str << exc.message << "\n" << exc.backtrace + print "- ", str.inspect, "\n" + end + + print "time: ", @timer.elapsed, "\n" + print "files: ", @tally.counter.files, "\n" + print "examples: ", @tally.counter.examples, "\n" + print "expectations: ", @tally.counter.expectations, "\n" + print "failures: ", @tally.counter.failures, "\n" + print "errors: ", @tally.counter.errors, "\n" + print "tagged: ", @tally.counter.tagged, "\n" + end +end diff --git a/spec/mspec/lib/mspec/runner/mspec.rb b/spec/mspec/lib/mspec/runner/mspec.rb new file mode 100644 index 0000000000..0e016c67a7 --- /dev/null +++ b/spec/mspec/lib/mspec/runner/mspec.rb @@ -0,0 +1,424 @@ +require 'mspec/runner/context' +require 'mspec/runner/exception' +require 'mspec/runner/tag' + +module MSpec +end + +class MSpecEnv + include MSpec +end + +module MSpec + @exit = nil + @abort = nil + @start = nil + @enter = nil + @before = nil + @add = nil + @after = nil + @leave = nil + @finish = nil + @exclude = [] + @include = [] + @leave = nil + @load = nil + @unload = nil + @tagged = nil + @current = nil + @passed = nil + @example = nil + @modes = [] + @shared = {} + @guarded = [] + @features = {} + @exception = nil + @randomize = false + @repeat = 1 + @expectation = nil + @expectations = false + @skips = [] + @subprocesses = [] + + class << self + attr_reader :file, :include, :exclude, :skips, :subprocesses + attr_writer :repeat, :randomize + attr_accessor :formatter + end + + def self.describe(description, options = nil, &block) + state = ContextState.new description, options + state.parent = current + + MSpec.register_current state + state.describe(&block) + + state.process unless state.shared? or current + end + + def self.process + STDOUT.puts RUBY_DESCRIPTION + STDOUT.flush + + actions :start + files + actions :finish + end + + def self.files_array + @files + end + + def self.each_file(&block) + if ENV["MSPEC_MULTI"] + while file = STDIN.gets + file = file.chomp + return if file == "QUIT" + yield file + begin + STDOUT.print "." + STDOUT.flush + rescue Errno::EPIPE + # The parent died + exit 1 + end + end + # The parent closed the connection without QUIT + abort "the parent did not send QUIT" + else + return unless files = @files + shuffle files if randomize? + files.each(&block) + end + end + + def self.files + each_file do |file| + setup_env + @file = file + actions :load + protect("loading #{file}") { Kernel.load file } + actions :unload + raise "#{file} was executed but did not reset the current example: #{@current}" if @current + end + end + + def self.setup_env + @env = MSpecEnv.new + end + + def self.actions(action, *args) + actions = retrieve(action) + actions.each { |obj| obj.send action, *args } if actions + end + + def self.protect(location, &block) + begin + @env.instance_exec(&block) + return true + rescue SystemExit => e + raise e + rescue SkippedSpecError => e + @skips << [e, block] + return false + rescue Object => exc + register_exit 1 + actions :exception, ExceptionState.new(current && current.state, location, exc) + return false + end + end + + # Guards can be nested, so a stack is necessary to know when we have + # exited the toplevel guard. + def self.guard + @guarded << true + end + + def self.unguard + @guarded.pop + end + + def self.guarded? + !@guarded.empty? + end + + # Sets the toplevel ContextState to +state+. + def self.register_current(state) + @current = state + end + + # Sets the toplevel ContextState to +nil+. + def self.clear_current + @current = nil + end + + # Returns the toplevel ContextState. + def self.current + @current + end + + # Stores the shared ContextState keyed by description. + def self.register_shared(state) + name = state.to_s + raise "duplicated shared #describe: #{name}" if @shared.key?(name) + @shared[name] = state + end + + # Returns the shared ContextState matching description. + def self.retrieve_shared(desc) + @shared[desc.to_s] + end + + # Stores the exit code used by the runner scripts. + def self.register_exit(code) + @exit = code + end + + # Retrieves the stored exit code. + def self.exit_code + @exit.to_i + end + + # Stores the list of files to be evaluated. + def self.register_files(files) + @files = files + end + + # Stores one or more substitution patterns for transforming + # a spec filename into a tags filename, where each pattern + # has the form: + # + # [Regexp, String] + # + # See also +tags_file+. + def self.register_tags_patterns(patterns) + @tags_patterns = patterns + end + + # Registers an operating mode. Modes recognized by MSpec: + # + # :pretend - actions execute but specs are not run + # :verify - specs are run despite guards and the result is + # verified to match the expectation of the guard + # :report - specs that are guarded are reported + # :unguarded - all guards are forced off + def self.register_mode(mode) + modes = @modes + modes << mode unless modes.include? mode + end + + # Clears all registered modes. + def self.clear_modes + @modes = [] + end + + # Returns +true+ if +mode+ is registered. + def self.mode?(mode) + @modes.include? mode + end + + def self.enable_feature(feature) + @features[feature] = true + end + + def self.disable_feature(feature) + @features[feature] = false + end + + def self.feature_enabled?(feature) + @features[feature] || false + end + + def self.retrieve(symbol) + instance_variable_get :"@#{symbol}" + end + + def self.store(symbol, value) + instance_variable_set :"@#{symbol}", value + end + + # This method is used for registering actions that are + # run at particular points in the spec cycle: + # :start before any specs are run + # :load before a spec file is loaded + # :enter before a describe block is run + # :before before a single spec is run + # :add while a describe block is adding examples to run later + # :expectation before a 'should', 'should_receive', etc. + # :passed after an example block is run and passes, passed the block, run before :example action + # :example after an example block is run, passed the block + # :exception after an exception is rescued + # :after after a single spec is run + # :leave after a describe block is run + # :unload after a spec file is run + # :finish after all specs are run + # + # Objects registered as actions above should respond to + # a method of the same name. For example, if an object + # is registered as a :start action, it should respond to + # a #start method call. + # + # Additionally, there are two "action" lists for + # filtering specs: + # :include return true if the spec should be run + # :exclude return true if the spec should NOT be run + # + def self.register(symbol, action) + unless value = retrieve(symbol) + value = store symbol, [] + end + value << action unless value.include? action + end + + def self.unregister(symbol, action) + if value = retrieve(symbol) + value.delete action + end + end + + def self.randomize? + @randomize + end + + def self.repeat + if @repeat == 1 + yield + else + @repeat.times do + yield + end + end + end + + def self.shuffle(ary) + return if ary.empty? + + size = ary.size + size.times do |i| + r = rand(size - i - 1) + ary[i], ary[r] = ary[r], ary[i] + end + end + + # Records that an expectation has been encountered in an example. + def self.expectation + @expectations = true + end + + # Returns true if an expectation has been encountered + def self.expectation? + @expectations + end + + # Resets the flag that an expectation has been encountered in an example. + def self.clear_expectations + @expectations = false + end + + # Transforms a spec filename into a tags filename by applying each + # substitution pattern in :tags_pattern. The default patterns are: + # + # [%r(/spec/), '/spec/tags/'], [/_spec.rb$/, '_tags.txt'] + # + # which will perform the following transformation: + # + # path/to/spec/class/method_spec.rb => path/to/spec/tags/class/method_tags.txt + # + # See also +register_tags_patterns+. + def self.tags_file + patterns = @tags_patterns || + [[%r(spec/), 'spec/tags/'], [/_spec.rb$/, '_tags.txt']] + patterns.inject(@file.dup) do |file, pattern| + file.gsub(*pattern) + end + end + + # Returns a list of tags matching any tag string in +keys+ based + # on the return value of <tt>keys.include?("tag_name")</tt> + def self.read_tags(keys) + tags = [] + file = tags_file + if File.exist? file + File.open(file, "r:utf-8") do |f| + f.each_line do |line| + line.chomp! + next if line.empty? + tag = SpecTag.new line + tags << tag if keys.include? tag.tag + end + end + end + tags + end + + def self.make_tag_dir(path) + parent = File.dirname(path) + return if File.exist? parent + begin + Dir.mkdir(parent) + rescue SystemCallError + make_tag_dir(parent) + Dir.mkdir(parent) + end + end + + # Writes each tag in +tags+ to the tag file. Overwrites the + # tag file if it exists. + def self.write_tags(tags) + return delete_tags if tags.empty? + file = tags_file + make_tag_dir(file) + File.open(file, "w:utf-8") do |f| + tags.each { |t| f.puts t } + end + end + + # Writes +tag+ to the tag file if it does not already exist. + # Returns +true+ if the tag is written, +false+ otherwise. + def self.write_tag(tag) + tags = read_tags([tag.tag]) + tags.each do |t| + if t.tag == tag.tag and t.description == tag.description + return false + end + end + + file = tags_file + make_tag_dir(file) + File.open(file, "a:utf-8") { |f| f.puts tag.to_s } + return true + end + + # Deletes +tag+ from the tag file if it exists. Returns +true+ + # if the tag is deleted, +false+ otherwise. Deletes the tag + # file if it is empty. + def self.delete_tag(tag) + deleted = false + desc = tag.escape(tag.description) + file = tags_file + if File.exist? file + lines = File.readlines(file) + File.open(file, "w:utf-8") do |f| + lines.each do |line| + line = line.chomp + if line.start_with?(tag.tag) and line.end_with?(desc) + deleted = true + else + f.puts line unless line.empty? + end + end + end + File.delete file unless File.size? file + end + return deleted + end + + # Removes the tag file associated with a spec file. + def self.delete_tags + file = tags_file + File.delete file if File.exist? file + end + + # Initialize @env + setup_env +end diff --git a/spec/mspec/lib/mspec/runner/object.rb b/spec/mspec/lib/mspec/runner/object.rb new file mode 100644 index 0000000000..58d98cc4df --- /dev/null +++ b/spec/mspec/lib/mspec/runner/object.rb @@ -0,0 +1,26 @@ +class Object + private def before(at = :each, &block) + MSpec.current.before at, &block + end + + private def after(at = :each, &block) + MSpec.current.after at, &block + end + + private def describe(description, options = nil, &block) + MSpec.describe description, options, &block + end + + private def it(desc, &block) + MSpec.current.it desc, &block + end + + private def it_should_behave_like(desc) + MSpec.current.it_should_behave_like desc + end + + alias_method :context, :describe + private :context + alias_method :specify, :it + private :specify +end diff --git a/spec/mspec/lib/mspec/runner/parallel.rb b/spec/mspec/lib/mspec/runner/parallel.rb new file mode 100644 index 0000000000..6a9ecd155d --- /dev/null +++ b/spec/mspec/lib/mspec/runner/parallel.rb @@ -0,0 +1,98 @@ +class ParallelRunner + def initialize(files, processes, formatter, argv) + @files = files + @processes = processes + @formatter = formatter + @argv = argv + @last_files = {} + @output_files = [] + @success = true + end + + def launch_children + @children = @processes.times.map { |i| + name = tmp "mspec-multi-#{i}" + @output_files << name + + env = { + "SPEC_TEMP_DIR" => "#{SPEC_TEMP_DIR}_#{i}", + "MSPEC_MULTI" => i.to_s + } + command = @argv + ["-fy", "-o", name] + $stderr.puts "$ #{command.join(' ')}" if $MSPEC_DEBUG + IO.popen([env, *command, close_others: false], "rb+") + } + end + + def handle(child, message) + case message + when '.' + @formatter.unload + send_new_file_or_quit(child) + else + if message == nil + msg = "A child mspec-run process died unexpectedly" + else + msg = "A child mspec-run process printed unexpected output on STDOUT" + while chunk = (child.read_nonblock(4096) rescue nil) + message += chunk + end + message.chomp!('.') + msg += ": #{message.inspect}" + end + + if last_file = @last_files[child] + msg += " while running #{last_file}" + end + + @success = false + quit(child) + abort "\n#{msg}" + end + end + + def quit(child) + begin + child.puts "QUIT" + rescue Errno::EPIPE + # The child process already died + end + _pid, status = Process.wait2(child.pid) + @success &&= status.success? + child.close + @children.delete(child) + end + + def send_new_file_or_quit(child) + if @files.empty? + quit(child) + else + file = @files.shift + @last_files[child] = file + child.puts file + end + end + + def run + MSpec.register_files @files + launch_children + + puts @children.map { |child| child.gets }.uniq + @formatter.start + begin + @children.each { |child| send_new_file_or_quit(child) } + + until @children.empty? + IO.select(@children)[0].each { |child| + handle(child, child.read(1)) + } + end + ensure + @children.dup.each { |child| quit(child) } + @formatter.aggregate_results(@output_files) + @formatter.finish + end + + @success + end +end diff --git a/spec/mspec/lib/mspec/runner/shared.rb b/spec/mspec/lib/mspec/runner/shared.rb new file mode 100644 index 0000000000..283711c1d7 --- /dev/null +++ b/spec/mspec/lib/mspec/runner/shared.rb @@ -0,0 +1,14 @@ +require 'mspec/runner/mspec' + +def it_behaves_like(desc, meth, obj = nil) + before :all do + @method = meth + @object = obj + end + after :all do + @method = nil + @object = nil + end + + it_should_behave_like desc.to_s +end diff --git a/spec/mspec/lib/mspec/runner/tag.rb b/spec/mspec/lib/mspec/runner/tag.rb new file mode 100644 index 0000000000..820df9159e --- /dev/null +++ b/spec/mspec/lib/mspec/runner/tag.rb @@ -0,0 +1,38 @@ +class SpecTag + attr_accessor :tag, :comment, :description + + def initialize(string = nil) + parse(string) if string + end + + def parse(string) + m = /^([^()#:]+)(\(([^)]+)?\))?:(.*)$/.match string + @tag, @comment, description = m.values_at(1, 3, 4) if m + @description = unescape description + end + + def unescape(str) + return unless str + if str[0] == ?" and str[-1] == ?" + str[1..-2].gsub('\n', "\n") + else + str + end + end + + def escape(str) + if str.include? "\n" + %["#{str.gsub("\n", '\n')}"] + else + str + end + end + + def to_s + "#{@tag}#{ "(#{@comment})" if @comment }:#{escape @description}" + end + + def ==(o) + @tag == o.tag and @comment == o.comment and @description == o.description + end +end diff --git a/spec/mspec/lib/mspec/utils/deprecate.rb b/spec/mspec/lib/mspec/utils/deprecate.rb new file mode 100644 index 0000000000..1db843b329 --- /dev/null +++ b/spec/mspec/lib/mspec/utils/deprecate.rb @@ -0,0 +1,6 @@ +module MSpec + def self.deprecate(what, replacement) + user_caller = caller.find { |line| !line.include?('lib/mspec') } + $stderr.puts "\n#{what} is deprecated, use #{replacement} instead.\nfrom #{user_caller}" + end +end diff --git a/spec/mspec/lib/mspec/utils/format.rb b/spec/mspec/lib/mspec/utils/format.rb new file mode 100644 index 0000000000..425dd4d11c --- /dev/null +++ b/spec/mspec/lib/mspec/utils/format.rb @@ -0,0 +1,24 @@ +# 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 + +module MSpec + def self.format(obj) + if String === obj and obj.include?("\n") + "\n#{obj.inspect.gsub('\n', "\n")}" + else + obj.pretty_inspect.chomp + end + rescue => e + "#<#{obj.class}>(#pretty_inspect raised #{e.inspect})" + end +end diff --git a/spec/mspec/lib/mspec/utils/name_map.rb b/spec/mspec/lib/mspec/utils/name_map.rb new file mode 100644 index 0000000000..9b04112e2e --- /dev/null +++ b/spec/mspec/lib/mspec/utils/name_map.rb @@ -0,0 +1,133 @@ +class NameMap + MAP = { + '`' => 'backtick', + '+' => 'plus', + '-' => 'minus', + '+@' => 'uplus', + '-@' => 'uminus', + '*' => 'multiply', + '/' => 'divide', + '%' => 'modulo', + '<<' => {'Integer' => 'left_shift', + 'IO' => 'output', + :default => 'append' }, + '>>' => 'right_shift', + '<' => 'lt', + '<=' => 'lte', + '>' => 'gt', + '>=' => 'gte', + '=' => 'assignment', + '==' => 'equal_value', + '===' => 'case_compare', + '<=>' => 'comparison', + '[]' => 'element_reference', + '[]=' => 'element_set', + '**' => 'exponent', + '!' => 'not', + '~' => {'Integer' => 'complement', + :default => 'match' }, + '!=' => 'not_equal', + '!~' => 'not_match', + '=~' => 'match', + '&' => {'Integer' => 'bit_and', + 'Array' => 'intersection', + 'Set' => 'intersection', + :default => 'and' }, + '|' => {'Integer' => 'bit_or', + 'Array' => 'union', + 'Set' => 'union', + :default => 'or' }, + '^' => {'Integer' => 'bit_xor', + 'Set' => 'exclusion', + :default => 'xor' }, + } + + EXCLUDED = %w[ + MSpecScript + MkSpec + MSpecOption + MSpecOptions + NameMap + SpecVersion + ] + + ALWAYS_PRIVATE = %w[ + initialize initialize_copy initialize_clone initialize_dup respond_to_missing? + ].map(&:to_sym) + + def initialize(filter = false) + @seen = {} + @filter = filter + end + + def exception?(name) + return false unless c = class_or_module(name) + c == Errno or c.ancestors.include? Exception + end + + def class_or_module(c) + begin + const = Object.const_get(c, false) + rescue NameError, RuntimeError + # Either the constant doesn't exist or it is + # explicitly raising an error, like `SortedSet`. + return nil + end + return nil unless Module === const + + filtered = @filter && EXCLUDED.include?(const.name) + return const unless filtered + end + + def namespace(mod, const) + return const.to_s if mod.nil? or %w[Object Class Module].include? mod + "#{mod}::#{const}" + end + + def map(hash, constants, mod = nil) + @seen = {} unless mod + + constants.each do |const| + name = namespace mod, const + m = class_or_module name + next unless m and !@seen[m] + @seen[m] = true + + ms = m.methods(false).map { |x| x.to_s } + hash["#{name}."] = ms.sort unless ms.empty? + + ms = m.public_instance_methods(false) + + m.protected_instance_methods(false) + + (m.private_instance_methods(false) & ALWAYS_PRIVATE) + ms.map! { |x| x.to_s } + hash["#{name}#"] = ms.sort unless ms.empty? + + map hash, m.constants(false), name + end + + hash + end + + def dir_name(c, base) + return File.join(base, 'exception') if exception? c + + c.split('::').inject(base) do |dir, name| + name.gsub!(/Class/, '') unless name == 'Class' + File.join dir, name.downcase + end + end + + def file_name(m, c) + if MAP.key?(m) + mapping = MAP[m] + if mapping.is_a?(Hash) + name = mapping[c.split('::').last] || mapping.fetch(:default) + else + name = mapping + end + else + name = m.gsub(/[?!=]/, '') + end + "#{name}_spec.rb" + end +end diff --git a/spec/mspec/lib/mspec/utils/options.rb b/spec/mspec/lib/mspec/utils/options.rb new file mode 100644 index 0000000000..adeafa1f81 --- /dev/null +++ b/spec/mspec/lib/mspec/utils/options.rb @@ -0,0 +1,521 @@ +require 'mspec/version' + +MSPEC_HOME = File.expand_path('../../../..', __FILE__) + +class MSpecOption + attr_reader :short, :long, :arg, :description, :block + + def initialize(short, long, arg, description, block) + @short = short + @long = long + @arg = arg + @description = description + @block = block + end + + def arg? + @arg != nil + end + + def match?(opt) + opt == @short or opt == @long + end +end + +# MSpecOptions provides a parser for command line options. It also +# provides a composable set of options from which the runner scripts +# can select for their particular functionality. +class MSpecOptions + # Raised if incorrect or incomplete formats are passed to #on. + class OptionError < Exception; end + + # Raised if an unrecognized option is encountered. + class ParseError < Exception; end + + class << self + attr_accessor :latest + end + + attr_accessor :config, :banner, :width, :options + + def initialize(banner = "", width = 30, config = nil) + @banner = banner + @config = config + @width = width + @options = [] + @doc = [] + @extra = [] + @on_extra = lambda { |x| + raise ParseError, "Unrecognized option: #{x}" if x[0] == ?- + @extra << x + } + + MSpecOptions.latest = self + end + + # Registers an option. Acceptable formats for arguments are: + # + # on "-a", "description" + # on "-a", "--abdc", "description" + # on "-a", "ARG", "description" + # on "--abdc", "ARG", "description" + # on "-a", "--abdc", "ARG", "description" + # + # If an block is passed, it will be invoked when the option is + # matched. Not passing a block is permitted, but nonsensical. + def on(*args, &block) + raise OptionError, "option and description are required" if args.size < 2 + + description = args.pop + short, long, argument = nil + args.each do |arg| + if arg[0] == ?- + if arg[1] == ?- + long = arg + else + short = arg + end + else + argument = arg + end + end + + add short, long, argument, description, block + end + + # Adds documentation text for an option and adds an +MSpecOption+ + # instance to the list of registered options. + def add(short, long, arg, description, block) + s = short ? short.dup : " " + s += (short ? ", " : " ") if long + doc " #{s}#{long} #{arg}".ljust(@width-1) + " #{description}" + @options << MSpecOption.new(short, long, arg, description, block) + end + + # Searches all registered options to find a match for +opt+. Returns + # +nil+ if no registered options match. + def match?(opt) + @options.find { |o| o.match? opt } + end + + # Processes an option. Calls the #on_extra block (or default) for + # unrecognized options. For registered options, possibly fetches an + # argument and invokes the option's block if it is not nil. + def process(argv, entry, opt, arg) + unless option = match?(opt) + @on_extra[entry] + else + if option.arg? + arg = argv.shift if arg.nil? + raise ParseError, "No argument provided for #{opt}" unless arg + option.block[arg] if option.block + else + option.block[] if option.block + end + end + option + end + + # Splits a string at +n+ characters into the +opt+ and the +rest+. + # The +arg+ is set to +nil+ if +rest+ is an empty string. + def split(str, n) + opt = str[0, n] + rest = str[n, str.size] + arg = rest == "" ? nil : rest + return opt, arg, rest + end + + # Parses an array of command line entries, calling blocks for + # registered options. + def parse(argv = ARGV) + argv = Array(argv).dup + + while entry = argv.shift + # collect everything that is not an option + if entry[0] != ?- or entry.size < 2 + @on_extra[entry] + next + end + + # this is a long option + if entry[1] == ?- + opt, arg = entry.split "=" + process argv, entry, opt, arg + next + end + + # disambiguate short option group from short option with argument + opt, arg, rest = split entry, 2 + + # process first option + option = process argv, entry, opt, arg + next unless option and !option.arg? + + # process the rest of the options + while rest.size > 0 + opt, arg, rest = split rest, 1 + opt = "-" + opt + option = process argv, opt, opt, arg + break if !option or option.arg? + end + end + + @extra + rescue ParseError => e + puts self + puts e + exit 1 + end + + # Adds a string of documentation text inline in the text generated + # from the options. See #on and #add. + def doc(str) + @doc << str + end + + # Convenience method for providing -v, --version options. + def version(version, &block) + show = block || lambda { puts "#{File.basename $0} #{version}"; exit } + on "-v", "--version", "Show version", &show + end + + # Convenience method for providing -h, --help options. + def help(&block) + help = block || lambda { puts self; exit 1 } + on "-h", "--help", "Show this message", &help + end + + # Stores a block that will be called with unrecognized options + def on_extra(&block) + @on_extra = block + end + + # Returns a string representation of the options and doc strings. + def to_s + @banner + "\n\n" + @doc.join("\n") + "\n" + end + + # The methods below provide groups of options that + # are composed by the particular runners to provide + # their functionality + + def configure(&block) + on("-B", "--config", "FILE", + "Load FILE containing configuration options", &block) + end + + def env + on("--env", "KEY=VALUE", "Set environment variable") do |env| + key, value = env.split('=', 2) + ENV[key] = value + end + end + + def targets + on("-t", "--target", "TARGET", + "Implementation to run the specs, where TARGET is:") do |t| + case t + when 'r', 'ruby' + config[:target] = 'ruby' + when 'x', 'rubinius' + config[:target] = './bin/rbx' + when 'X', 'rbx' + config[:target] = 'rbx' + when 'j', 'jruby' + config[:target] = 'jruby' + when 'i','ironruby' + config[:target] = 'ir' + when 'm','maglev' + config[:target] = 'maglev-ruby' + when 't','topaz' + config[:target] = 'topaz' + when 'o','opal' + mspec_lib = File.expand_path('../../../', __FILE__) + config[:target] = "./bin/opal -syaml -sfileutils -rnodejs -rnodejs/require -rnodejs/yaml -rprocess -Derror -I#{mspec_lib} -I./lib/ -I. " + else + config[:target] = t + end + end + + doc "" + doc " r or ruby invokes ruby in PATH" + doc " x or rubinius invokes ./bin/rbx" + doc " X or rbx invokes rbx in PATH" + doc " j or jruby invokes jruby in PATH" + doc " i or ironruby invokes ir in PATH" + doc " m or maglev invokes maglev-ruby in PATH" + doc " t or topaz invokes topaz in PATH" + doc " o or opal invokes ./bin/opal with options" + doc " full path to EXE invokes EXE directly\n" + + on("-T", "--target-opt", "OPT", + "Pass OPT as a flag to the target implementation") do |t| + config[:flags] << t + end + on("-I", "--include", "DIR", + "Pass DIR through as the -I option to the target") do |d| + config[:loadpath] << "-I#{d}" + end + on("-r", "--require", "LIBRARY", + "Pass LIBRARY through as the -r option to the target") do |f| + config[:requires] << "-r#{f}" + end + end + + def formatters + on("-f", "--format", "FORMAT", + "Formatter for reporting, where FORMAT is one of:") do |o| + require 'mspec/runner/formatters' + case o + when 's', 'spec', 'specdoc' + config[:formatter] = SpecdocFormatter + when 'h', 'html' + config[:formatter] = HtmlFormatter + when 'd', 'dot', 'dotted' + config[:formatter] = DottedFormatter + when 'b', 'describe' + config[:formatter] = DescribeFormatter + when 'f', 'file' + config[:formatter] = FileFormatter + when 'u', 'unit', 'unitdiff' + config[:formatter] = UnitdiffFormatter + when 'm', 'summary' + config[:formatter] = SummaryFormatter + when 'a', '*', 'spin' + config[:formatter] = SpinnerFormatter + when 't', 'method' + config[:formatter] = MethodFormatter + when 'e', 'stats' + config[:formatter] = StatsPerFileFormatter + when 'y', 'yaml' + config[:formatter] = YamlFormatter + when 'p', 'profile' + config[:formatter] = ProfileFormatter + when 'j', 'junit' + config[:formatter] = JUnitFormatter + else + abort "Unknown format: #{o}" unless File.exist?(o) + require File.expand_path(o) + if Object.const_defined?(:CUSTOM_MSPEC_FORMATTER) + config[:formatter] = CUSTOM_MSPEC_FORMATTER + else + abort "You must define CUSTOM_MSPEC_FORMATTER in your custom formatter file" + end + end + end + + doc "" + doc " s, spec, specdoc SpecdocFormatter" + doc " h, html, HtmlFormatter" + doc " d, dot, dotted DottedFormatter" + doc " f, file FileFormatter" + doc " u, unit, unitdiff UnitdiffFormatter" + doc " m, summary SummaryFormatter" + doc " a, *, spin SpinnerFormatter" + doc " t, method MethodFormatter" + doc " e, stats StatsPerFileFormatter" + doc " y, yaml YamlFormatter" + doc " p, profile ProfileFormatter" + doc " j, junit JUnitFormatter\n" + + on("-o", "--output", "FILE", + "Write formatter output to FILE") do |f| + config[:output] = f + end + + on("--error-output", "FILE", + "Write error output of failing specs to FILE, or $stderr if value is 'stderr'.") do |f| + config[:error_output] = f + end + end + + def filters + on("-e", "--example", "STR", + "Run examples with descriptions matching STR") do |o| + config[:includes] << o + end + on("-E", "--exclude", "STR", + "Exclude examples with descriptions matching STR") do |o| + config[:excludes] << o + end + on("-p", "--pattern", "PATTERN", + "Run examples with descriptions matching PATTERN") do |o| + config[:patterns] << Regexp.new(o) + end + on("-P", "--excl-pattern", "PATTERN", + "Exclude examples with descriptions matching PATTERN") do |o| + config[:xpatterns] << Regexp.new(o) + end + on("-g", "--tag", "TAG", + "Run examples with descriptions matching ones tagged with TAG") do |o| + config[:tags] << o + end + on("-G", "--excl-tag", "TAG", + "Exclude examples with descriptions matching ones tagged with TAG") do |o| + config[:xtags] << o + end + on("-w", "--profile", "FILE", + "Run examples for methods listed in the profile FILE") do |f| + config[:profiles] << f + end + on("-W", "--excl-profile", "FILE", + "Exclude examples for methods listed in the profile FILE") do |f| + config[:xprofiles] << f + end + end + + def chdir + on("-C", "--chdir", "DIR", + "Change the working directory to DIR before running specs") do |d| + Dir.chdir d + end + end + + def prefix + on("--prefix", "STR", "Prepend STR when resolving spec file names") do |p| + config[:prefix] = p + end + end + + def pretend + on("-Z", "--dry-run", + "Invoke formatters and other actions, but don't execute the specs") do + MSpec.register_mode :pretend + end + end + + def unguarded + on("--unguarded", "Turn off all guards") do + MSpec.register_mode :unguarded + end + on("--no-ruby_bug", "Turn off the ruby_bug guard") do + MSpec.register_mode :no_ruby_bug + end + end + + def randomize + on("-H", "--random", + "Randomize the list of spec files") do + MSpec.randomize = true + end + end + + def repeat + on("-R", "--repeat", "NUMBER", + "Repeatedly run an example NUMBER times") do |o| + MSpec.repeat = Integer(o) + end + end + + def verbose + on("-V", "--verbose", "Output the name of each file processed") do + obj = Object.new + def obj.start + @width = MSpec.files_array.inject(0) { |max, f| f.size > max ? f.size : max } + end + def obj.load + file = MSpec.file + STDERR.print "\n#{file.ljust(@width)}\n" + end + MSpec.register :start, obj + MSpec.register :load, obj + end + + on("-m", "--marker", "MARKER", + "Output MARKER for each file processed") do |o| + obj = Object.new + obj.instance_variable_set :@marker, o + def obj.load + STDERR.print @marker + end + MSpec.register :load, obj + end + + on("--print-skips", "Print skips") do + config[:print_skips] = true + end + end + + def interrupt + on("--int-spec", "Control-C interrupts the current spec only") do + config[:abort] = false + end + end + + def timeout + on("--timeout", "TIMEOUT", "Abort if a spec takes longer than TIMEOUT seconds") do |timeout| + require 'mspec/runner/actions/timeout' + timeout = Float(timeout) + TimeoutAction.new(timeout).register + end + end + + def verify + on("--report-on", "GUARD", "Report specs guarded by GUARD") do |g| + MSpec.register_mode :report_on + SpecGuard.guards << g.to_sym + end + on("-O", "--report", "Report guarded specs") do + MSpec.register_mode :report + end + on("-Y", "--verify", + "Verify that guarded specs pass and fail as expected") do + MSpec.register_mode :verify + end + end + + def action_filters + on("-K", "--action-tag", "TAG", + "Spec descriptions marked with TAG will trigger the specified action") do |o| + config[:atags] << o + end + on("-S", "--action-string", "STR", + "Spec descriptions matching STR will trigger the specified action") do |o| + config[:astrings] << o + end + end + + def actions + on("--spec-debug", + "Invoke the debugger when a spec description matches (see -K, -S)") do + config[:debugger] = true + end + end + + def debug + on("-d", "--debug", + "Disable MSpec backtrace filtering") do + $MSPEC_DEBUG = true + end + end + + def launchable + on("--launchable-test-reports", "DIR", + "DIR The directory for reporting test results in Launchable JSON format") do |o| + require 'mspec/runner/formatters/launchable' + config[:launchable] = LaunchableFormatter.setDir(o) + end + end + + def all + configure {} + env + targets + formatters + filters + chdir + prefix + pretend + unguarded + randomize + repeat + verbose + interrupt + timeout + verify + action_filters + actions + debug + launchable + end +end diff --git a/spec/mspec/lib/mspec/utils/script.rb b/spec/mspec/lib/mspec/utils/script.rb new file mode 100644 index 0000000000..15fd23fabf --- /dev/null +++ b/spec/mspec/lib/mspec/utils/script.rb @@ -0,0 +1,305 @@ +require 'mspec/guards/guard' +require 'mspec/guards/version' +require 'mspec/utils/warnings' + +# MSpecScript provides a skeleton for all the MSpec runner scripts. +class MSpecScript + # Returns the config object. Maintained at the class + # level to easily enable simple config files. See the + # class method +set+. + def self.config + @config ||= { + :path => ['.', 'spec'], + :config_ext => '.mspec' + } + end + + # Associates +value+ with +key+ in the config object. Enables + # simple config files of the form: + # + # class MSpecScript + # set :target, "ruby" + # set :files, ["one_spec.rb", "two_spec.rb"] + # end + def self.set(key, value) + config[key] = value + end + + # Gets the value of +key+ from the config object. Simplifies + # getting values in a config file: + # + # class MSpecScript + # set :a, 1 + # set :b, 2 + # set :c, get(:a) + get(:b) + # end + def self.get(key) + config[key] + end + + class << self + attr_accessor :child_process + end + + # True if the current process is the one going to run the specs with `MSpec.process`. + # False for e.g. `mspec` which exec's to `mspec-run`. + # This is useful in .mspec config files. + def self.child_process? + MSpecScript.child_process + end + + def initialize + check_version! + + config[:formatter] = nil + config[:includes] = [] + config[:excludes] = [] + config[:patterns] = [] + config[:xpatterns] = [] + config[:tags] = [] + config[:xtags] = [] + config[:profiles] = [] + config[:xprofiles] = [] + config[:atags] = [] + config[:astrings] = [] + config[:ltags] = [] + config[:abort] = true + @loaded = [] + end + + # Returns the config object maintained by the instance's class. + # See the class methods +set+ and +config+. + def config + MSpecScript.config + end + + # Returns +true+ if the file was located in +config[:path]+, + # possibly appending +config[:config_ext]. Returns +false+ + # otherwise. + def try_load(target) + names = [target] + unless target[-6..-1] == config[:config_ext] + names << target + config[:config_ext] + end + + names.each do |name| + config[:path].each do |dir| + file = File.expand_path name, dir + if @loaded.include?(file) + return true + elsif File.exist? file + value = Kernel.load(file) + @loaded << file + return value + end + end + end + + false + end + + def load(target) + try_load(target) or abort "Could not load config file #{target}" + end + + # Attempts to load a default config file. First tries to load + # 'default.mspec'. If that fails, attempts to load a config + # file name constructed from the value of RUBY_ENGINE and the + # first two numbers in RUBY_VERSION. For example, on MRI 1.8.6, + # the file name would be 'ruby.1.8.mspec'. + def load_default + try_load 'default.mspec' + + if Object.const_defined?(:RUBY_ENGINE) + engine = RUBY_ENGINE + else + engine = 'ruby' + end + try_load "#{engine}.#{SpecGuard.ruby_version}.mspec" + try_load "#{engine}.mspec" + end + + # Callback for enabling custom options. This version is a no-op. + # Provide an implementation specific version in a config file. + # Called by #options after the MSpec-provided options are added. + def custom_options(options) + options.doc " No custom options registered" + end + + # Registers all filters and actions. + def register + require 'mspec/runner/formatters/dotted' + require 'mspec/runner/formatters/spinner' + require 'mspec/runner/formatters/file' + require 'mspec/runner/filters' + + if formatter = config_formatter + formatter.register + MSpec.formatter = formatter + end + + MatchFilter.new(:include, *config[:includes]).register unless config[:includes].empty? + MatchFilter.new(:exclude, *config[:excludes]).register unless config[:excludes].empty? + RegexpFilter.new(:include, *config[:patterns]).register unless config[:patterns].empty? + RegexpFilter.new(:exclude, *config[:xpatterns]).register unless config[:xpatterns].empty? + TagFilter.new(:include, *config[:tags]).register unless config[:tags].empty? + TagFilter.new(:exclude, *config[:xtags]).register unless config[:xtags].empty? + ProfileFilter.new(:include, *config[:profiles]).register unless config[:profiles].empty? + ProfileFilter.new(:exclude, *config[:xprofiles]).register unless config[:xprofiles].empty? + + DebugAction.new(config[:atags], config[:astrings]).register if config[:debugger] + + custom_register + end + + # Makes a formatter specified by :formatter option. + def config_formatter + if config[:formatter].nil? + config[:formatter] = STDOUT.tty? ? SpinnerFormatter : @files.size < 50 ? DottedFormatter : FileFormatter + end + + if config[:formatter] + config[:formatter] = config[:formatter].new(config[:output]) + end + + if config[:launchable] + config[:formatter].extend config[:launchable] + end + + config[:formatter] + end + + # Callback for enabling custom actions, etc. This version is a + # no-op. Provide an implementation specific version in a config + # file. Called by #register. + def custom_register + end + + # Sets up signal handlers. Only a handler for SIGINT is + # registered currently. + def signals + if config[:abort] + Signal.trap "INT" do + MSpec.actions :abort + puts "\nProcess aborted!" + exit! 1 + end + end + end + + # Attempts to resolve +partial+ as a file or directory name in the + # following order: + # + # 1. +partial+ + # 2. +partial+ + "_spec.rb" + # 3. <tt>File.join(config[:prefix], partial)</tt> + # 4. <tt>File.join(config[:prefix], partial + "_spec.rb")</tt> + # + # If it is a file name, returns the name as an entry in an array. + # If it is a directory, returns all *_spec.rb files in the + # directory and subdirectories. + # + # If unable to resolve +partial+, +Kernel.abort+ is called. + def entries(partial) + file = partial + "_spec.rb" + patterns = [partial, file] + if config[:prefix] + patterns << File.join(config[:prefix], partial) + patterns << File.join(config[:prefix], file) + end + + patterns.each do |pattern| + begin + expanded = File.realpath(pattern) + rescue Errno::ENOENT, Errno::ENOTDIR + next + end + if File.file?(expanded) && expanded.end_with?('.rb') + return [expanded] + elsif File.directory?(expanded) + specs = Dir["#{expanded}/**/*_spec.rb"].sort + return specs unless specs.empty? + end + end + + abort "Could not find spec file #{partial}" + end + + # Resolves each entry in +patterns+ to a set of files. + # + # If the pattern has a leading '^' character, the list of files + # is subtracted from the list of files accumulated to that point. + # + # If the entry has a leading ':' character, the corresponding + # key is looked up in the config object and the entries in the + # value retrieved are processed through #entries. + def files(patterns) + list = [] + patterns.each do |pattern| + case pattern[0] + when ?^ + list -= entries(pattern[1..-1]) + when ?: + key = pattern[1..-1].to_sym + value = config[key] + abort "Key #{pattern} not found in mspec config." unless value + list += files(Array(value)) + else + list += entries(pattern) + end + end + list + end + + def files_from_patterns(patterns) + unless $0.end_with?("_spec.rb") + if patterns.empty? + patterns = config[:files] + end + if patterns.empty? and File.directory? "./spec" + patterns = ["spec/"] + end + end + list = files(patterns) + abort "No files specified." if list.empty? + list + end + + def cores(max) + require 'etc' + [Etc.nprocessors, max].min + end + + def setup_env + ENV['MSPEC_RUNNER'] = '1' + + unless ENV['RUBY_EXE'] + ENV['RUBY_EXE'] = config[:target] if config[:target] + end + + unless ENV['RUBY_FLAGS'] + ENV['RUBY_FLAGS'] = config[:flags].join(" ") if config[:flags] + end + end + + # Instantiates an instance and calls the series of methods to + # invoke the script. + def self.main(child_process = true) + MSpecScript.child_process = child_process + + script = new + script.load_default + script.options + script.signals + script.register + script.setup_env + require 'mspec' + script.run + end + + private def check_version! + ruby_version_is ""..."2.6" do + warn "MSpec is supported for Ruby 2.6 and above only" + end + end +end diff --git a/spec/mspec/lib/mspec/utils/version.rb b/spec/mspec/lib/mspec/utils/version.rb new file mode 100644 index 0000000000..9c1c58b8df --- /dev/null +++ b/spec/mspec/lib/mspec/utils/version.rb @@ -0,0 +1,52 @@ +class SpecVersion + # If beginning implementations have a problem with this include, we can + # manually implement the relational operators that are needed. + include Comparable + + # SpecVersion handles comparison correctly for the context by filling in + # missing version parts according to the value of +ceil+. If +ceil+ is + # +false+, 0 digits fill in missing version parts. If +ceil+ is +true+, 9 + # digits fill in missing parts. (See e.g. VersionGuard and BugGuard.) + def initialize(version, ceil = false) + @version = version + @ceil = ceil + @integer = nil + end + + def to_s + @version + end + + def to_str + to_s + end + + # Converts a string representation of a version major.minor.tiny + # to an integer representation so that comparisons can be made. For example, + # "2.2.10" < "2.2.2" would be false if compared as strings. + def to_i + unless @integer + major, minor, tiny = @version.split "." + if @ceil + tiny = 99 unless tiny + end + parts = [major, minor, tiny].map { |x| x.to_i } + @integer = ("1%02d%02d%02d" % parts).to_i + end + @integer + end + + def to_int + to_i + end + + def <=>(other) + if other.respond_to? :to_int + other = Integer(other.to_int) + else + other = SpecVersion.new(String(other)).to_i + end + + self.to_i <=> other + end +end diff --git a/spec/mspec/lib/mspec/utils/warnings.rb b/spec/mspec/lib/mspec/utils/warnings.rb new file mode 100644 index 0000000000..23efc696a5 --- /dev/null +++ b/spec/mspec/lib/mspec/utils/warnings.rb @@ -0,0 +1,10 @@ +require 'mspec/guards/version' + +# Always enable deprecation warnings when running MSpec, as ruby/spec tests for them, +# and like in most test frameworks, deprecation warnings should be enabled by default, +# so that deprecations are noticed before the breaking change. +# Disable experimental warnings, we want to test new experimental features in ruby/spec. +if Object.const_defined?(:Warning) and Warning.respond_to?(:[]=) + Warning[:deprecated] = true + Warning[:experimental] = false +end diff --git a/spec/mspec/lib/mspec/version.rb b/spec/mspec/lib/mspec/version.rb new file mode 100644 index 0000000000..9126f5366e --- /dev/null +++ b/spec/mspec/lib/mspec/version.rb @@ -0,0 +1,5 @@ +require 'mspec/utils/version' + +module MSpec + VERSION = SpecVersion.new "1.8.0" +end diff --git a/spec/mspec/spec/commands/fixtures/four.txt b/spec/mspec/spec/commands/fixtures/four.txt new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/spec/mspec/spec/commands/fixtures/four.txt diff --git a/spec/mspec/spec/commands/fixtures/level2/three_spec.rb b/spec/mspec/spec/commands/fixtures/level2/three_spec.rb new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/spec/mspec/spec/commands/fixtures/level2/three_spec.rb @@ -0,0 +1 @@ + diff --git a/spec/mspec/spec/commands/fixtures/one_spec.rb b/spec/mspec/spec/commands/fixtures/one_spec.rb new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/spec/mspec/spec/commands/fixtures/one_spec.rb @@ -0,0 +1 @@ + diff --git a/spec/mspec/spec/commands/fixtures/three.rb b/spec/mspec/spec/commands/fixtures/three.rb new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/spec/mspec/spec/commands/fixtures/three.rb @@ -0,0 +1 @@ + diff --git a/spec/mspec/spec/commands/fixtures/two_spec.rb b/spec/mspec/spec/commands/fixtures/two_spec.rb new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/spec/mspec/spec/commands/fixtures/two_spec.rb @@ -0,0 +1 @@ + diff --git a/spec/mspec/spec/commands/mkspec_spec.rb b/spec/mspec/spec/commands/mkspec_spec.rb new file mode 100644 index 0000000000..32262723de --- /dev/null +++ b/spec/mspec/spec/commands/mkspec_spec.rb @@ -0,0 +1,363 @@ +require 'spec_helper' +require 'mspec/commands/mkspec' +require 'fileutils' + +RSpec.describe "The -c, --constant CONSTANT option" do + before :each do + @options = MSpecOptions.new + allow(MSpecOptions).to receive(:new).and_return(@options) + @script = MkSpec.new + @config = @script.config + end + + it "is enabled by #options" do + allow(@options).to receive(:on) + expect(@options).to receive(:on).with("-c", "--constant", "CONSTANT", + an_instance_of(String)) + @script.options [] + end + + it "adds CONSTANT to the list of constants" do + ["-c", "--constant"].each do |opt| + @config[:constants] = [] + @script.options [opt, "Object"] + expect(@config[:constants]).to include("Object") + end + end +end + +RSpec.describe "The -b, --base DIR option" do + before :each do + @options = MSpecOptions.new + allow(MSpecOptions).to receive(:new).and_return(@options) + @script = MkSpec.new + @config = @script.config + end + + it "is enabled by #options" do + allow(@options).to receive(:on) + expect(@options).to receive(:on).with("-b", "--base", "DIR", + an_instance_of(String)) + @script.options [] + end + + it "sets the base directory relative to which the spec directories are created" do + ["-b", "--base"].each do |opt| + @config[:base] = nil + @script.options [opt, "superspec"] + expect(@config[:base]).to eq(File.expand_path("superspec")) + end + end +end + +RSpec.describe "The -r, --require LIBRARY option" do + before :each do + @options = MSpecOptions.new + allow(MSpecOptions).to receive(:new).and_return(@options) + @script = MkSpec.new + @config = @script.config + end + + it "is enabled by #options" do + allow(@options).to receive(:on) + expect(@options).to receive(:on).with("-r", "--require", "LIBRARY", + an_instance_of(String)) + @script.options [] + end + + it "adds CONSTANT to the list of constants" do + ["-r", "--require"].each do |opt| + @config[:requires] = [] + @script.options [opt, "libspec"] + expect(@config[:requires]).to include("libspec") + end + end +end + +RSpec.describe "The -V, --version-guard VERSION option" do + before :each do + @options = MSpecOptions.new + allow(MSpecOptions).to receive(:new).and_return(@options) + @script = MkSpec.new + @config = @script.config + end + + it "is enabled by #options" do + allow(@options).to receive(:on) + expect(@options).to receive(:on).with("-V", "--version-guard", "VERSION", + an_instance_of(String)) + @script.options [] + end + + it "sets the version for the ruby_version_is guards to VERSION" do + ["-r", "--require"].each do |opt| + @config[:requires] = [] + @script.options [opt, "libspec"] + expect(@config[:requires]).to include("libspec") + end + end +end + +RSpec.describe MkSpec, "#options" do + before :each do + @options = MSpecOptions.new + allow(MSpecOptions).to receive(:new).and_return(@options) + @script = MkSpec.new + end + + it "parses the command line options" do + expect(@options).to receive(:parse).with(["--this", "and", "--that"]) + @script.options ["--this", "and", "--that"] + end + + it "parses ARGV unless passed other options" do + expect(@options).to receive(:parse).with(ARGV) + @script.options + end + + it "prints help and exits if passed an unrecognized option" do + expect(@options).to receive(:raise).with(MSpecOptions::ParseError, an_instance_of(String)) + allow(@options).to receive(:puts) + allow(@options).to receive(:exit) + @script.options ["--iunknown"] + end +end + +RSpec.describe MkSpec, "#create_directory" do + before :each do + @script = MkSpec.new + @script.config[:base] = "spec" + end + + it "prints a warning if a file with the directory name exists" do + expect(File).to receive(:exist?).and_return(true) + expect(File).to receive(:directory?).and_return(false) + expect(FileUtils).not_to receive(:mkdir_p) + expect(@script).to receive(:puts).with("spec/class already exists and is not a directory.") + expect(@script.create_directory("Class")).to eq(nil) + end + + it "does nothing if the directory already exists" do + expect(File).to receive(:exist?).and_return(true) + expect(File).to receive(:directory?).and_return(true) + expect(FileUtils).not_to receive(:mkdir_p) + expect(@script.create_directory("Class")).to eq("spec/class") + end + + it "creates the directory if it does not exist" do + expect(File).to receive(:exist?).and_return(false) + expect(@script).to receive(:mkdir_p).with("spec/class") + expect(@script.create_directory("Class")).to eq("spec/class") + end + + it "creates the directory for a namespaced module if it does not exist" do + expect(File).to receive(:exist?).and_return(false) + expect(@script).to receive(:mkdir_p).with("spec/struct/tms") + expect(@script.create_directory("Struct::Tms")).to eq("spec/struct/tms") + end +end + +RSpec.describe MkSpec, "#write_requires" do + before :each do + @script = MkSpec.new + @script.config[:base] = "spec" + + @file = double("file") + allow(File).to receive(:open).and_yield(@file) + end + + it "writes the spec_helper require line" do + expect(@file).to receive(:puts).with("require_relative '../../../spec_helper'") + @script.write_requires("spec/core/tcejbo", "spec/core/tcejbo/inspect_spec.rb") + end + + it "writes require lines for each library specified on the command line" do + allow(@file).to receive(:puts) + expect(@file).to receive(:puts).with("require_relative '../../../spec_helper'") + expect(@file).to receive(:puts).with("require 'complex'") + @script.config[:requires] << 'complex' + @script.write_requires("spec/core/tcejbo", "spec/core/tcejbo/inspect_spec.rb") + end +end + +RSpec.describe MkSpec, "#write_spec" do + before :each do + @file = IOStub.new + allow(File).to receive(:open).and_yield(@file) + + @script = MkSpec.new + allow(@script).to receive(:puts) + + @response = double("system command response") + allow(@response).to receive(:include?).and_return(false) + allow(@script).to receive(:`).and_return(@response) + end + + it "checks if specs exist for the method if the spec file exists" do + name = Regexp.escape(RbConfig.ruby) + expect(@script).to receive(:`).with( + %r"#{name} #{MSPEC_HOME}/bin/mspec-run --dry-run --unguarded -fs -e 'Object#inspect' spec/core/tcejbo/inspect_spec.rb") + @script.write_spec("spec/core/tcejbo/inspect_spec.rb", "Object#inspect", true) + end + + it "checks for the method name in the spec file output" do + expect(@response).to receive(:include?).with("Array#[]=") + @script.write_spec("spec/core/yarra/element_set_spec.rb", "Array#[]=", true) + end + + it "returns nil if the spec file exists and contains a spec for the method" do + allow(@response).to receive(:include?).and_return(true) + expect(@script.write_spec("spec/core/tcejbo/inspect_spec.rb", "Object#inspect", true)).to eq(nil) + end + + it "does not print the spec file name if it exists and contains a spec for the method" do + allow(@response).to receive(:include?).and_return(true) + expect(@script).not_to receive(:puts) + @script.write_spec("spec/core/tcejbo/inspect_spec.rb", "Object#inspect", true) + end + + it "prints the spec file name if a template spec is written" do + expect(@script).to receive(:puts).with("spec/core/tcejbo/inspect_spec.rb") + @script.write_spec("spec/core/tcejbo/inspect_spec.rb", "Object#inspect", true) + end + + it "writes a template spec to the file if the spec file does not exist" do + expect(@file).to receive(:puts).twice + expect(@script).to receive(:puts).with("spec/core/tcejbo/inspect_spec.rb") + @script.write_spec("spec/core/tcejbo/inspect_spec.rb", "Object#inspect", false) + end + + it "writes a template spec to the file if it exists but contains no spec for the method" do + expect(@response).to receive(:include?).and_return(false) + expect(@file).to receive(:puts).twice + expect(@script).to receive(:puts).with("spec/core/tcejbo/inspect_spec.rb") + @script.write_spec("spec/core/tcejbo/inspect_spec.rb", "Object#inspect", true) + end + + it "writes a template spec" do + @script.write_spec("spec/core/tcejbo/inspect_spec.rb", "Object#inspect", true) + expect(@file).to eq <<EOS + +describe "Object#inspect" do + it "needs to be reviewed for spec completeness" +end +EOS + end + + it "writes a template spec with version guard" do + @script.config[:version] = '""..."1.9"' + @script.write_spec("spec/core/tcejbo/inspect_spec.rb", "Object#inspect", true) + expect(@file).to eq <<EOS + +ruby_version_is ""..."1.9" do + describe "Object#inspect" do + it "needs to be reviewed for spec completeness" + end +end +EOS + + end +end + +RSpec.describe MkSpec, "#create_file" do + before :each do + @script = MkSpec.new + allow(@script).to receive(:write_requires) + allow(@script).to receive(:write_spec) + + allow(File).to receive(:exist?).and_return(false) + end + + it "generates a file name based on the directory, class/module, and method" do + expect(File).to receive(:join).with("spec/tcejbo", "inspect_spec.rb" + ).and_return("spec/tcejbo/inspect_spec.rb") + @script.create_file("spec/tcejbo", "Object", "inspect", "Object#inspect") + end + + it "does not call #write_requires if the spec file already exists" do + expect(File).to receive(:exist?).and_return(true) + expect(@script).not_to receive(:write_requires) + @script.create_file("spec/tcejbo", "Object", "inspect", "Object#inspect") + end + + it "calls #write_requires if the spec file does not exist" do + expect(File).to receive(:exist?).and_return(false) + expect(@script).to receive(:write_requires).with( + "spec/tcejbo", "spec/tcejbo/inspect_spec.rb") + @script.create_file("spec/tcejbo", "Object", "inspect", "Object#inspect") + end + + it "calls #write_spec with the file, method name" do + expect(@script).to receive(:write_spec).with( + "spec/tcejbo/inspect_spec.rb", "Object#inspect", false) + @script.create_file("spec/tcejbo", "Object", "inspect", "Object#inspect") + end +end + +RSpec.describe MkSpec, "#run" do + before :each do + @options = MSpecOptions.new + allow(MSpecOptions).to receive(:new).and_return(@options) + + @map = NameMap.new + allow(NameMap).to receive(:new).and_return(@map) + + @script = MkSpec.new + allow(@script).to receive(:create_directory).and_return("spec/mkspec") + allow(@script).to receive(:create_file) + @script.config[:constants] = [MkSpec] + end + + it "loads files in the requires list" do + allow(@script).to receive(:require) + expect(@script).to receive(:require).with("alib") + expect(@script).to receive(:require).with("blib") + @script.config[:requires] = ["alib", "blib"] + @script.run + end + + it "creates a map of constants to methods" do + expect(@map).to receive(:map).with({}, @script.config[:constants]).and_return({}) + @script.run + end + + it "calls #create_directory for each class/module in the map" do + expect(@script).to receive(:create_directory).with("MkSpec").twice + @script.run + end + + it "calls #create_file for each method on each class/module in the map" do + expect(@map).to receive(:map).with({}, @script.config[:constants] + ).and_return({"MkSpec#" => ["run"]}) + expect(@script).to receive(:create_file).with("spec/mkspec", "MkSpec", "run", "MkSpec#run") + @script.run + end +end + +RSpec.describe MkSpec, ".main" do + before :each do + @script = double("MkSpec").as_null_object + allow(MkSpec).to receive(:new).and_return(@script) + end + + it "sets MSPEC_RUNNER = '1' in the environment" do + ENV["MSPEC_RUNNER"] = "0" + MkSpec.main + expect(ENV["MSPEC_RUNNER"]).to eq("1") + end + + it "creates an instance of MSpecScript" do + expect(MkSpec).to receive(:new).and_return(@script) + MkSpec.main + end + + it "calls the #options method on the script" do + expect(@script).to receive(:options) + MkSpec.main + end + + it "calls the #run method on the script" do + expect(@script).to receive(:run) + MkSpec.main + end +end diff --git a/spec/mspec/spec/commands/mspec_ci_spec.rb b/spec/mspec/spec/commands/mspec_ci_spec.rb new file mode 100644 index 0000000000..b8dc9d062f --- /dev/null +++ b/spec/mspec/spec/commands/mspec_ci_spec.rb @@ -0,0 +1,155 @@ +require 'spec_helper' +require 'mspec/runner/mspec' +require 'mspec/runner/filters/tag' +require 'mspec/commands/mspec-ci' + +RSpec.describe MSpecCI, "#options" do + before :each do + @options, @config = new_option + allow(MSpecOptions).to receive(:new).and_return(@options) + + @script = MSpecCI.new + allow(@script).to receive(:config).and_return(@config) + allow(@script).to receive(:files).and_return([]) + end + + it "enables the chdir option" do + expect(@options).to receive(:chdir) + @script.options [] + end + + it "enables the prefix option" do + expect(@options).to receive(:prefix) + @script.options [] + end + + it "enables the config option" do + expect(@options).to receive(:configure) + @script.options [] + end + + it "provides a custom action (block) to the config option" do + expect(@script).to receive(:load).with("cfg.mspec") + @script.options ["-B", "cfg.mspec"] + end + + it "enables the dry run option" do + expect(@options).to receive(:pretend) + @script.options [] + end + + it "enables the unguarded option" do + expect(@options).to receive(:unguarded) + @script.options [] + end + + it "enables the interrupt single specs option" do + expect(@options).to receive(:interrupt) + @script.options [] + end + + it "enables the formatter options" do + expect(@options).to receive(:formatters) + @script.options [] + end + + it "enables the verbose option" do + expect(@options).to receive(:verbose) + @script.options [] + end + + it "enables the action options" do + expect(@options).to receive(:actions) + @script.options [] + end + + it "enables the action filter options" do + expect(@options).to receive(:action_filters) + @script.options [] + end + + it "enables the version option" do + expect(@options).to receive(:version) + @script.options [] + end + + it "enables the help option" do + expect(@options).to receive(:help) + @script.options [] + end + + it "enables the repeat option" do + expect(@options).to receive(:repeat) + @script.options @argv + end + + it "calls #custom_options" do + expect(@script).to receive(:custom_options).with(@options) + @script.options [] + end +end + +RSpec.describe MSpecCI, "#run" do + before :each do + allow(MSpec).to receive(:process) + + @filter = double("TagFilter") + allow(TagFilter).to receive(:new).and_return(@filter) + allow(@filter).to receive(:register) + + @tags = ["fails", "critical", "unstable", "incomplete", "unsupported"] + + @config = { :ci_files => ["one", "two"] } + @script = MSpecCI.new + allow(@script).to receive(:exit) + allow(@script).to receive(:config).and_return(@config) + allow(@script).to receive(:files).and_return(["one", "two"]) + @script.options [] + end + + it "registers the tags patterns" do + @config[:tags_patterns] = [/spec/, "tags"] + expect(MSpec).to receive(:register_tags_patterns).with([/spec/, "tags"]) + @script.run + end + + it "registers the files to process" do + expect(MSpec).to receive(:register_files).with(["one", "two"]) + @script.run + end + + it "registers a tag filter for 'fails', 'unstable', 'incomplete', 'critical', 'unsupported'" do + filter = double("fails filter") + expect(TagFilter).to receive(:new).with(:exclude, *@tags).and_return(filter) + expect(filter).to receive(:register) + @script.run + end + + it "registers an additional exclude tag specified by :ci_xtags" do + @config[:ci_xtags] = "windows" + filter = double("fails filter") + expect(TagFilter).to receive(:new).with(:exclude, *(@tags + ["windows"])).and_return(filter) + expect(filter).to receive(:register) + @script.run + end + + it "registers additional exclude tags specified by a :ci_xtags array" do + @config[:ci_xtags] = ["windows", "windoze"] + filter = double("fails filter") + expect(TagFilter).to receive(:new).with(:exclude, + *(@tags + ["windows", "windoze"])).and_return(filter) + expect(filter).to receive(:register) + @script.run + end + + it "processes the files" do + expect(MSpec).to receive(:process) + @script.run + end + + it "exits with the exit code registered with MSpec" do + allow(MSpec).to receive(:exit_code).and_return(7) + expect(@script).to receive(:exit).with(7) + @script.run + end +end diff --git a/spec/mspec/spec/commands/mspec_run_spec.rb b/spec/mspec/spec/commands/mspec_run_spec.rb new file mode 100644 index 0000000000..f96be2b43e --- /dev/null +++ b/spec/mspec/spec/commands/mspec_run_spec.rb @@ -0,0 +1,178 @@ +require 'spec_helper' +require 'mspec/runner/mspec' +require 'mspec/commands/mspec-run' + +one_spec = File.expand_path(File.dirname(__FILE__)) + '/fixtures/one_spec.rb' +two_spec = File.expand_path(File.dirname(__FILE__)) + '/fixtures/two_spec.rb' + +RSpec.describe MSpecRun, ".new" do + before :each do + @script = MSpecRun.new + end + + it "sets config[:files] to an empty list" do + expect(@script.config[:files]).to eq([]) + end +end + +RSpec.describe MSpecRun, "#options" do + before :each do + @argv = [one_spec, two_spec] + @options, @config = new_option + allow(MSpecOptions).to receive(:new).and_return(@options) + + @script = MSpecRun.new + allow(@script).to receive(:config).and_return(@config) + end + + it "enables the filter options" do + expect(@options).to receive(:filters) + @script.options @argv + end + + it "enables the chdir option" do + expect(@options).to receive(:chdir) + @script.options @argv + end + + it "enables the prefix option" do + expect(@options).to receive(:prefix) + @script.options @argv + end + + it "enables the configure option" do + expect(@options).to receive(:configure) + @script.options @argv + end + + it "provides a custom action (block) to the config option" do + expect(@script).to receive(:load).with("cfg.mspec") + @script.options ["-B", "cfg.mspec", one_spec] + end + + it "enables the randomize option to runs specs in random order" do + expect(@options).to receive(:randomize) + @script.options @argv + end + + it "enables the dry run option" do + expect(@options).to receive(:pretend) + @script.options @argv + end + + it "enables the unguarded option" do + expect(@options).to receive(:unguarded) + @script.options @argv + end + + it "enables the interrupt single specs option" do + expect(@options).to receive(:interrupt) + @script.options @argv + end + + it "enables the formatter options" do + expect(@options).to receive(:formatters) + @script.options @argv + end + + it "enables the verbose option" do + expect(@options).to receive(:verbose) + @script.options @argv + end + + it "enables the verify options" do + expect(@options).to receive(:verify) + @script.options @argv + end + + it "enables the action options" do + expect(@options).to receive(:actions) + @script.options @argv + end + + it "enables the action filter options" do + expect(@options).to receive(:action_filters) + @script.options @argv + end + + it "enables the version option" do + expect(@options).to receive(:version) + @script.options @argv + end + + it "enables the help option" do + expect(@options).to receive(:help) + @script.options @argv + end + + it "enables the repeat option" do + expect(@options).to receive(:repeat) + @script.options @argv + end + + it "exits if there are no files to process and './spec' is not a directory" do + expect(File).to receive(:directory?).with("./spec").and_return(false) + expect(@options).to receive(:parse).and_return([]) + expect(@script).to receive(:abort).with("No files specified.") + @script.options + end + + it "process 'spec/' if it is a directory and no files were specified" do + expect(File).to receive(:directory?).with("./spec").and_return(true) + expect(@options).to receive(:parse).and_return([]) + expect(@script).to receive(:files).with(["spec/"]).and_return(["spec/a_spec.rb"]) + @script.options + end + + it "calls #custom_options" do + expect(@script).to receive(:custom_options).with(@options) + @script.options @argv + end +end + +RSpec.describe MSpecRun, "#run" do + before :each do + @script = MSpecRun.new + allow(@script).to receive(:exit) + @spec_dir = File.expand_path(File.dirname(__FILE__)+"/fixtures") + @file_patterns = [ + @spec_dir+"/level2", + @spec_dir+"/one_spec.rb", + @spec_dir+"/two_spec.rb"] + @files = [ + @spec_dir+"/level2/three_spec.rb", + @spec_dir+"/one_spec.rb", + @spec_dir+"/two_spec.rb"] + @script.options @file_patterns + allow(MSpec).to receive :process + end + + it "registers the tags patterns" do + @script.config[:tags_patterns] = [/spec/, "tags"] + expect(MSpec).to receive(:register_tags_patterns).with([/spec/, "tags"]) + @script.run + end + + it "registers the files to process" do + expect(MSpec).to receive(:register_files).with(@files) + @script.run + end + + it "uses config[:files] if no files are given on the command line" do + @script.config[:files] = @file_patterns + expect(MSpec).to receive(:register_files).with(@files) + @script.options [] + @script.run + end + + it "processes the files" do + expect(MSpec).to receive(:process) + @script.run + end + + it "exits with the exit code registered with MSpec" do + allow(MSpec).to receive(:exit_code).and_return(7) + expect(@script).to receive(:exit).with(7) + @script.run + end +end diff --git a/spec/mspec/spec/commands/mspec_spec.rb b/spec/mspec/spec/commands/mspec_spec.rb new file mode 100644 index 0000000000..d19bebb2d6 --- /dev/null +++ b/spec/mspec/spec/commands/mspec_spec.rb @@ -0,0 +1,180 @@ +require 'spec_helper' +require 'yaml' +require 'mspec/commands/mspec' + +RSpec.describe MSpecMain, "#options" do + before :each do + @options, @config = new_option + allow(MSpecOptions).to receive(:new).and_return(@options) + + @script = MSpecMain.new + allow(@script).to receive(:config).and_return(@config) + allow(@script).to receive(:load) + end + + it "enables the configure option" do + expect(@options).to receive(:configure) + @script.options + end + + it "provides a custom action (block) to the config option" do + @script.options ["-B", "config"] + expect(@config[:options]).to include("-B", "config") + end + + it "loads the file specified by the config option" do + expect(@script).to receive(:load).with("config") + @script.options ["-B", "config"] + end + + it "enables the target options" do + expect(@options).to receive(:targets) + @script.options + end + + it "sets config[:options] to all argv entries that are not registered options" do + @options.on "-X", "--exclude", "ARG", "description" + @script.options [".", "-G", "fail", "-X", "ARG", "--list", "unstable", "some/file.rb"] + expect(@config[:options]).to eq([".", "-G", "fail", "--list", "unstable", "some/file.rb"]) + end + + it "calls #custom_options" do + expect(@script).to receive(:custom_options).with(@options) + @script.options + end +end + +RSpec.describe MSpecMain, "#run" do + before :each do + @options, @config = new_option + allow(MSpecOptions).to receive(:new).and_return(@options) + @script = MSpecMain.new + allow(@script).to receive(:config).and_return(@config) + allow(@script).to receive(:exec) + @err = $stderr + $stderr = IOStub.new + end + + after :each do + $stderr = @err + end + + it "uses exec to invoke the runner script" do + expect(@script).to receive(:exec).with("ruby", "#{MSPEC_HOME}/bin/mspec-run", close_others: false) + @script.options [] + @script.run + end + + it "shows the command line on stderr" do + expect(@script).to receive(:exec).with("ruby", "#{MSPEC_HOME}/bin/mspec-run", close_others: false) + @script.options [] + @script.run + expect($stderr.to_s).to eq("$ ruby #{Dir.pwd}/bin/mspec-run\n") + end + + it "adds config[:launch] to the exec options" do + expect(@script).to receive(:exec).with("ruby", + "-Xlaunch.option", "#{MSPEC_HOME}/bin/mspec-run", close_others: false) + @config[:launch] << "-Xlaunch.option" + @script.options [] + @script.run + expect($stderr.to_s).to eq("$ ruby -Xlaunch.option #{Dir.pwd}/bin/mspec-run\n") + end + + it "calls #multi_exec if the command is 'ci' and the multi option is passed" do + expect(@script).to receive(:multi_exec) do |argv| + expect(argv).to eq(["ruby", "#{MSPEC_HOME}/bin/mspec-ci"]) + end + @script.options ["ci", "-j"] + expect do + @script.run + end.to raise_error(SystemExit) + end +end + +RSpec.describe "The -j, --multi option" do + before :each do + @options, @config = new_option + allow(MSpecOptions).to receive(:new).and_return(@options) + @script = MSpecMain.new + allow(@script).to receive(:config).and_return(@config) + end + + it "is enabled by #options" do + allow(@options).to receive(:on) + expect(@options).to receive(:on).with("-j", "--multi", an_instance_of(String)) + @script.options + end + + it "sets the multiple process option" do + ["-j", "--multi"].each do |opt| + @config[:multi] = nil + @script.options [opt] + expect(@config[:multi]).to eq(true) + end + end +end + +RSpec.describe "The -h, --help option" do + before :each do + @options, @config = new_option + allow(MSpecOptions).to receive(:new).and_return(@options) + @script = MSpecMain.new + allow(@script).to receive(:config).and_return(@config) + end + + it "is enabled by #options" do + allow(@options).to receive(:on) + expect(@options).to receive(:on).with("-h", "--help", an_instance_of(String)) + @script.options + end + + it "passes the option to the subscript" do + ["-h", "--help"].each do |opt| + @config[:options] = [] + @script.options ["ci", opt] + expect(@config[:options].sort).to eq(["-h"]) + end + end + + it "prints help and exits" do + expect(@script).to receive(:puts).twice + expect(@script).to receive(:exit).twice + ["-h", "--help"].each do |opt| + @script.options [opt] + end + end +end + +RSpec.describe "The -v, --version option" do + before :each do + @options, @config = new_option + allow(MSpecOptions).to receive(:new).and_return(@options) + @script = MSpecMain.new + allow(@script).to receive(:config).and_return(@config) + end + + it "is enabled by #options" do + allow(@options).to receive(:on) + expect(@options).to receive(:on).with("-v", "--version", an_instance_of(String)) + @script.options + end + + it "passes the option to the subscripts" do + ["-v", "--version"].each do |opt| + @config[:options] = [] + @script.options ["ci", opt] + expect(@config[:options].sort).to eq(["-v"]) + end + end + + it "prints the version and exits if no subscript is invoked" do + @config[:command] = nil + allow(File).to receive(:basename).and_return("mspec") + expect(@script).to receive(:puts).twice.with("mspec #{MSpec::VERSION}") + expect(@script).to receive(:exit).twice + ["-v", "--version"].each do |opt| + @script.options [opt] + end + end +end diff --git a/spec/mspec/spec/commands/mspec_tag_spec.rb b/spec/mspec/spec/commands/mspec_tag_spec.rb new file mode 100644 index 0000000000..1ab5f6ea58 --- /dev/null +++ b/spec/mspec/spec/commands/mspec_tag_spec.rb @@ -0,0 +1,414 @@ +require 'spec_helper' +require 'mspec/runner/mspec' +require 'mspec/commands/mspec-tag' +require 'mspec/runner/actions/tag' +require 'mspec/runner/actions/taglist' +require 'mspec/runner/actions/tagpurge' + +one_spec = File.expand_path(File.dirname(__FILE__)) + '/fixtures/one_spec.rb' +two_spec = File.expand_path(File.dirname(__FILE__)) + '/fixtures/two_spec.rb' + +RSpec.describe MSpecTag, ".new" do + before :each do + @script = MSpecTag.new + end + + it "sets config[:ltags] to an empty list" do + expect(@script.config[:ltags]).to eq([]) + end + + it "sets config[:tagger] to :add" do + @script.config[:tagger] = :add + end + + it "sets config[:tag] to 'fails:'" do + @script.config[:tag] = 'fails:' + end + + it "sets config[:outcome] to :fail" do + @script.config[:outcome] = :fail + end +end + +RSpec.describe MSpecTag, "#options" do + before :each do + @stdout, $stdout = $stdout, IOStub.new + + @argv = [one_spec, two_spec] + @options, @config = new_option + allow(MSpecOptions).to receive(:new).and_return(@options) + + @script = MSpecTag.new + allow(@script).to receive(:config).and_return(@config) + end + + after :each do + $stdout = @stdout + end + + it "enables the filter options" do + expect(@options).to receive(:filters) + @script.options @argv + end + + it "enables the configure option" do + expect(@options).to receive(:configure) + @script.options @argv + end + + it "provides a custom action (block) to the config option" do + expect(@script).to receive(:load).with("cfg.mspec") + @script.options ["-B", "cfg.mspec", one_spec] + end + + it "enables the dry run option" do + expect(@options).to receive(:pretend) + @script.options @argv + end + + it "enables the unguarded option" do + expect(@options).to receive(:unguarded) + @script.options @argv + end + + it "enables the interrupt single specs option" do + expect(@options).to receive(:interrupt) + @script.options @argv + end + + it "enables the formatter options" do + expect(@options).to receive(:formatters) + @script.options @argv + end + + it "enables the verbose option" do + expect(@options).to receive(:verbose) + @script.options @argv + end + + it "enables the version option" do + expect(@options).to receive(:version) + @script.options @argv + end + + it "enables the help option" do + expect(@options).to receive(:help) + @script.options @argv + end + + it "calls #custom_options" do + expect(@script).to receive(:custom_options).with(@options) + @script.options @argv + end + + it "exits if there are no files to process" do + expect(@options).to receive(:parse).and_return([]) + expect(@script).to receive(:exit) + @script.options + expect($stdout.to_s).to include "No files specified" + end +end + +RSpec.describe MSpecTag, "options" do + before :each do + @options, @config = new_option + allow(MSpecOptions).to receive(:new).and_return(@options) + @script = MSpecTag.new + allow(@script).to receive(:config).and_return(@config) + end + + describe "-N, --add TAG" do + it "is enabled with #options" do + allow(@options).to receive(:on) + expect(@options).to receive(:on).with("-N", "--add", "TAG", an_instance_of(String)) + @script.options [one_spec] + end + + it "sets the mode to :add and sets the tag to TAG" do + ["-N", "--add"].each do |opt| + @config[:tagger] = nil + @config[:tag] = nil + @script.options [opt, "taggit", one_spec] + expect(@config[:tagger]).to eq(:add) + expect(@config[:tag]).to eq("taggit:") + end + end + end + + describe "-R, --del TAG" do + it "is enabled with #options" do + allow(@options).to receive(:on) + expect(@options).to receive(:on).with("-R", "--del", "TAG", + an_instance_of(String)) + @script.options [one_spec] + end + + it "it sets the mode to :del, the tag to TAG, and the outcome to :pass" do + ["-R", "--del"].each do |opt| + @config[:tagger] = nil + @config[:tag] = nil + @config[:outcome] = nil + @script.options [opt, "taggit", one_spec] + expect(@config[:tagger]).to eq(:del) + expect(@config[:tag]).to eq("taggit:") + expect(@config[:outcome]).to eq(:pass) + end + end + end + + describe "-Q, --pass" do + it "is enabled with #options" do + allow(@options).to receive(:on) + expect(@options).to receive(:on).with("-Q", "--pass", an_instance_of(String)) + @script.options [one_spec] + end + + it "sets the outcome to :pass" do + ["-Q", "--pass"].each do |opt| + @config[:outcome] = nil + @script.options [opt, one_spec] + expect(@config[:outcome]).to eq(:pass) + end + end + end + + describe "-F, --fail" do + it "is enabled with #options" do + allow(@options).to receive(:on) + expect(@options).to receive(:on).with("-F", "--fail", an_instance_of(String)) + @script.options [one_spec] + end + + it "sets the outcome to :fail" do + ["-F", "--fail"].each do |opt| + @config[:outcome] = nil + @script.options [opt, one_spec] + expect(@config[:outcome]).to eq(:fail) + end + end + end + + describe "-L, --all" do + it "is enabled with #options" do + allow(@options).to receive(:on) + expect(@options).to receive(:on).with("-L", "--all", an_instance_of(String)) + @script.options [one_spec] + end + + it "sets the outcome to :all" do + ["-L", "--all"].each do |opt| + @config[:outcome] = nil + @script.options [opt, one_spec] + expect(@config[:outcome]).to eq(:all) + end + end + end + + describe "--list TAG" do + it "is enabled with #options" do + allow(@options).to receive(:on) + expect(@options).to receive(:on).with("--list", "TAG", an_instance_of(String)) + @script.options [one_spec] + end + + it "sets the mode to :list" do + @config[:tagger] = nil + @script.options ["--list", "TAG", one_spec] + expect(@config[:tagger]).to eq(:list) + end + + it "sets ltags to include TAG" do + @config[:tag] = nil + @script.options ["--list", "TAG", one_spec] + expect(@config[:ltags]).to eq(["TAG"]) + end + end + + describe "--list-all" do + it "is enabled with #options" do + allow(@options).to receive(:on) + expect(@options).to receive(:on).with("--list-all", an_instance_of(String)) + @script.options [one_spec] + end + + it "sets the mode to :list_all" do + @config[:tagger] = nil + @script.options ["--list-all", one_spec] + expect(@config[:tagger]).to eq(:list_all) + end + end + + describe "--purge" do + it "is enabled with #options" do + allow(@options).to receive(:on) + expect(@options).to receive(:on).with("--purge", an_instance_of(String)) + @script.options [one_spec] + end + + it "sets the mode to :purge" do + @config[:tagger] = nil + @script.options ["--purge", one_spec] + expect(@config[:tagger]).to eq(:purge) + end + end +end + +RSpec.describe MSpecTag, "#run" do + before :each do + allow(MSpec).to receive(:process) + + options = double("MSpecOptions").as_null_object + allow(options).to receive(:parse).and_return(["one", "two"]) + allow(MSpecOptions).to receive(:new).and_return(options) + + @config = { } + @script = MSpecTag.new + allow(@script).to receive(:exit) + allow(@script).to receive(:config).and_return(@config) + allow(@script).to receive(:files).and_return(["one", "two"]) + @script.options + end + + it "registers the tags patterns" do + @config[:tags_patterns] = [/spec/, "tags"] + expect(MSpec).to receive(:register_tags_patterns).with([/spec/, "tags"]) + @script.run + end + + it "registers the files to process" do + expect(MSpec).to receive(:register_files).with(["one", "two"]) + @script.run + end + + it "processes the files" do + expect(MSpec).to receive(:process) + @script.run + end + + it "exits with the exit code registered with MSpec" do + allow(MSpec).to receive(:exit_code).and_return(7) + expect(@script).to receive(:exit).with(7) + @script.run + end +end + +RSpec.describe MSpecTag, "#register" do + before :each do + @script = MSpecTag.new + @config = @script.config + @config[:tag] = "fake:" + @config[:atags] = [] + @config[:astrings] = [] + @config[:ltags] = ["fails", "unstable"] + + allow(@script).to receive(:files).and_return([]) + @script.options "fake" + + @t = double("TagAction") + allow(@t).to receive(:register) + + @tl = double("TagListAction") + allow(@tl).to receive(:register) + end + + it "raises an ArgumentError if no recognized action is given" do + @config[:tagger] = :totally_whack + expect { @script.register }.to raise_error(ArgumentError) + end + + describe "when config[:tagger] is the default (:add)" do + before :each do + @config[:formatter] = false + end + + it "creates a TagAction" do + expect(TagAction).to receive(:new).and_return(@t) + @script.register + end + + it "creates a TagAction if config[:tagger] is :del" do + @config[:tagger] = :del + @config[:outcome] = :pass + expect(TagAction).to receive(:new).with(:del, :pass, "fake", nil, [], []).and_return(@t) + @script.register + end + + it "calls #register on the TagAction instance" do + expect(TagAction).to receive(:new).and_return(@t) + expect(@t).to receive(:register) + @script.register + end + end + + describe "when config[:tagger] is :list" do + before :each do + expect(TagListAction).to receive(:new).with(@config[:ltags]).and_return(@tl) + @config[:tagger] = :list + end + + it "creates a TagListAction" do + expect(@tl).to receive(:register) + @script.register + end + + it "registers MSpec pretend mode" do + expect(MSpec).to receive(:register_mode).with(:pretend) + @script.register + end + + it "sets config[:formatter] to false" do + @script.register + expect(@config[:formatter]).to be_falsey + end + end + + describe "when config[:tagger] is :list_all" do + before :each do + expect(TagListAction).to receive(:new).with(nil).and_return(@tl) + @config[:tagger] = :list_all + end + + it "creates a TagListAction" do + expect(@tl).to receive(:register) + @script.register + end + + it "registers MSpec pretend mode" do + expect(MSpec).to receive(:register_mode).with(:pretend) + @script.register + end + + it "sets config[:formatter] to false" do + @script.register + expect(@config[:formatter]).to be_falsey + end + end + + describe "when config[:tagger] is :purge" do + before :each do + expect(TagPurgeAction).to receive(:new).and_return(@tl) + allow(MSpec).to receive(:register_mode) + @config[:tagger] = :purge + end + + it "creates a TagPurgeAction" do + expect(@tl).to receive(:register) + @script.register + end + + it "registers MSpec in pretend mode" do + expect(MSpec).to receive(:register_mode).with(:pretend) + @script.register + end + + it "registers MSpec in unguarded mode" do + expect(MSpec).to receive(:register_mode).with(:unguarded) + @script.register + end + + it "sets config[:formatter] to false" do + @script.register + expect(@config[:formatter]).to be_falsey + end + end +end diff --git a/spec/mspec/spec/expectations/expectations_spec.rb b/spec/mspec/spec/expectations/expectations_spec.rb new file mode 100644 index 0000000000..371829d4f9 --- /dev/null +++ b/spec/mspec/spec/expectations/expectations_spec.rb @@ -0,0 +1,29 @@ +require 'spec_helper' +require 'mspec/expectations/expectations' + +RSpec.describe SpecExpectationNotMetError do + it "is a subclass of StandardError" do + expect(SpecExpectationNotMetError.ancestors).to include(StandardError) + end +end + +RSpec.describe SpecExpectationNotFoundError do + it "is a subclass of StandardError" do + expect(SpecExpectationNotFoundError.ancestors).to include(StandardError) + end +end + +RSpec.describe SpecExpectationNotFoundError, "#message" do + it "returns 'No behavior expectation was found in the example'" do + m = SpecExpectationNotFoundError.new.message + expect(m).to eq("No behavior expectation was found in the example") + end +end + +RSpec.describe SpecExpectation, "#fail_with" do + it "raises an SpecExpectationNotMetError" do + expect { + SpecExpectation.fail_with "expected this", "to equal that" + }.to raise_error(SpecExpectationNotMetError, "expected this to equal that") + end +end diff --git a/spec/mspec/spec/expectations/should_spec.rb b/spec/mspec/spec/expectations/should_spec.rb new file mode 100644 index 0000000000..472890979d --- /dev/null +++ b/spec/mspec/spec/expectations/should_spec.rb @@ -0,0 +1,61 @@ +require 'spec_helper' +require 'rbconfig' + +RSpec.describe "MSpec" do + before :all do + path = RbConfig::CONFIG['bindir'] + exe = RbConfig::CONFIG['ruby_install_name'] + file = File.expand_path('../../fixtures/should.rb', __FILE__) + @out = `#{path}/#{exe} #{file}` + end + + describe "#should" do + it "records failures" do + expect(@out).to include <<-EOS +1) +MSpec expectation method #should causes a failure to be recorded FAILED +Expected 1 == 2 +to be truthy but was false +EOS + end + + it "raises exceptions for examples with no expectations" do + expect(@out).to include <<-EOS +2) +MSpec expectation method #should registers that an expectation has been encountered FAILED +No behavior expectation was found in the example +EOS + end + end + + describe "#should_not" do + it "records failures" do + expect(@out).to include <<-EOS +3) +MSpec expectation method #should_not causes a failure to be recorded FAILED +Expected 1 == 1 +to be falsy but was true +EOS + end + + it "raises exceptions for examples with no expectations" do + expect(@out).to include <<-EOS +4) +MSpec expectation method #should_not registers that an expectation has been encountered FAILED +No behavior expectation was found in the example +EOS + end + end + + it "prints status information" do + expect(@out).to include ".FF..FF." + end + + it "prints out a summary" do + expect(@out).to include "0 files, 8 examples, 6 expectations, 4 failures, 0 errors" + end + + it "records expectations" do + expect(@out).to include "I was called 6 times" + end +end diff --git a/spec/mspec/spec/fixtures/a_spec.rb b/spec/mspec/spec/fixtures/a_spec.rb new file mode 100644 index 0000000000..17a7e8b664 --- /dev/null +++ b/spec/mspec/spec/fixtures/a_spec.rb @@ -0,0 +1,15 @@ +unless defined?(RSpec) + describe "Foo#bar" do + it "passes" do + 1.should == 1 + end + + it "errors" do + 1.should == 2 + end + + it "fails" do + raise "failure" + end + end +end diff --git a/spec/mspec/spec/fixtures/b_spec.rb b/spec/mspec/spec/fixtures/b_spec.rb new file mode 100644 index 0000000000..f1f63317cb --- /dev/null +++ b/spec/mspec/spec/fixtures/b_spec.rb @@ -0,0 +1,7 @@ +unless defined?(RSpec) + describe "Bar#baz" do + it "works" do + 1.should == 1 + end + end +end diff --git a/spec/mspec/spec/fixtures/chatty_spec.rb b/spec/mspec/spec/fixtures/chatty_spec.rb new file mode 100644 index 0000000000..2d110d8ce4 --- /dev/null +++ b/spec/mspec/spec/fixtures/chatty_spec.rb @@ -0,0 +1,8 @@ +unless defined?(RSpec) + describe "Chatty#spec" do + it "prints too much" do + STDOUT.puts "Hello\nIt's me!" + 1.should == 1 + end + end +end diff --git a/spec/mspec/spec/fixtures/config.mspec b/spec/mspec/spec/fixtures/config.mspec new file mode 100644 index 0000000000..01654c5094 --- /dev/null +++ b/spec/mspec/spec/fixtures/config.mspec @@ -0,0 +1,8 @@ +class MSpecScript + set :target, 'ruby' + + set :tags_patterns, [ + [%r(spec/fixtures/), 'spec/fixtures/tags/'], + [/_spec.rb$/, '_tags.txt'] + ] +end diff --git a/spec/mspec/spec/fixtures/die_spec.rb b/spec/mspec/spec/fixtures/die_spec.rb new file mode 100644 index 0000000000..0f66793274 --- /dev/null +++ b/spec/mspec/spec/fixtures/die_spec.rb @@ -0,0 +1,7 @@ +unless defined?(RSpec) + describe "Deadly#spec" do + it "dies" do + abort "DEAD" + end + end +end diff --git a/spec/mspec/spec/fixtures/my_ruby b/spec/mspec/spec/fixtures/my_ruby new file mode 100755 index 0000000000..eeda3eeeec --- /dev/null +++ b/spec/mspec/spec/fixtures/my_ruby @@ -0,0 +1,4 @@ +#!/usr/bin/env bash + +echo $RUBY_EXE +exec ruby "$@" diff --git a/spec/mspec/spec/fixtures/object_methods_spec.rb b/spec/mspec/spec/fixtures/object_methods_spec.rb new file mode 100644 index 0000000000..9b7c1523e5 --- /dev/null +++ b/spec/mspec/spec/fixtures/object_methods_spec.rb @@ -0,0 +1,8 @@ +unless defined?(RSpec) + describe "Object" do + it ".public_instance_methods(false) is empty" do + Object.public_instance_methods(false).sort.should == + [:should, :should_not, :should_not_receive, :should_receive, :stub!] + end + end +end diff --git a/spec/mspec/spec/fixtures/print_interpreter_spec.rb b/spec/mspec/spec/fixtures/print_interpreter_spec.rb new file mode 100644 index 0000000000..a662346d0a --- /dev/null +++ b/spec/mspec/spec/fixtures/print_interpreter_spec.rb @@ -0,0 +1,4 @@ +unless defined?(RSpec) + puts ENV["RUBY_EXE"] + puts ruby_cmd(nil).split.first +end diff --git a/spec/mspec/spec/fixtures/should.rb b/spec/mspec/spec/fixtures/should.rb new file mode 100644 index 0000000000..f494775c5f --- /dev/null +++ b/spec/mspec/spec/fixtures/should.rb @@ -0,0 +1,75 @@ +$: << File.dirname(__FILE__) + '/../../lib' +require 'mspec' +require 'mspec/utils/script' + +# The purpose of these specs is to confirm that the #should +# and #should_not methods are functioning appropriately. We +# use a separate spec file that is invoked from the MSpec +# specs but is run by MSpec. This avoids conflicting with +# RSpec's #should and #should_not methods. + +raise "RSpec should not be loaded" if defined?(RSpec) + +class ShouldSpecsMonitor + def initialize + @called = 0 + end + + def expectation(state) + @called += 1 + end + + def finish + puts "I was called #{@called} times" + end +end + +# Simplistic runner +formatter = DottedFormatter.new +formatter.register + +monitor = ShouldSpecsMonitor.new +MSpec.register :expectation, monitor +MSpec.register :finish, monitor + +at_exit { MSpec.actions :finish } + +MSpec.actions :start +MSpec.setup_env + +# Specs +describe "MSpec expectation method #should" do + it "accepts a matcher" do + :sym.should be_kind_of(Symbol) + end + + it "causes a failure to be recorded" do + 1.should == 2 + end + + it "registers that an expectation has been encountered" do + # an empty example block causes an exception because + # no expectation was encountered + end + + it "invokes the MSpec :expectation actions" do + 1.should == 1 + end +end + +describe "MSpec expectation method #should_not" do + it "accepts a matcher" do + "sym".should_not be_kind_of(Symbol) + end + + it "causes a failure to be recorded" do + 1.should_not == 1 + end + + it "registers that an expectation has been encountered" do + end + + it "invokes the MSpec :expectation actions" do + 1.should_not == 2 + end +end diff --git a/spec/mspec/spec/fixtures/tagging_spec.rb b/spec/mspec/spec/fixtures/tagging_spec.rb new file mode 100644 index 0000000000..0097fd1808 --- /dev/null +++ b/spec/mspec/spec/fixtures/tagging_spec.rb @@ -0,0 +1,16 @@ +# encoding: utf-8 +unless defined?(RSpec) + describe "Tag#me" do + it "passes" do + 1.should == 1 + end + + it "errors" do + 1.should == 2 + end + + it "érròrs in unicode" do + 1.should == 2 + end + end +end diff --git a/spec/mspec/spec/guards/block_device_spec.rb b/spec/mspec/spec/guards/block_device_spec.rb new file mode 100644 index 0000000000..dd420d4a81 --- /dev/null +++ b/spec/mspec/spec/guards/block_device_spec.rb @@ -0,0 +1,46 @@ +require 'spec_helper' +require 'mspec/guards' + +RSpec.describe Object, "#with_block_device" do + before :each do + ScratchPad.clear + + @guard = BlockDeviceGuard.new + allow(BlockDeviceGuard).to receive(:new).and_return(@guard) + end + + platform_is_not :freebsd, :windows do + it "yields if block device is available" do + expect(@guard).to receive(:`).and_return("block devices") + with_block_device { ScratchPad.record :yield } + expect(ScratchPad.recorded).to eq(:yield) + end + + it "does not yield if block device is not available" do + expect(@guard).to receive(:`).and_return(nil) + with_block_device { ScratchPad.record :yield } + expect(ScratchPad.recorded).not_to eq(:yield) + end + end + + platform_is :freebsd, :windows do + it "does not yield, since platform does not support block devices" do + expect(@guard).not_to receive(:`) + with_block_device { ScratchPad.record :yield } + expect(ScratchPad.recorded).not_to eq(:yield) + end + end + + it "sets the name of the guard to :with_block_device" do + with_block_device { } + expect(@guard.name).to eq(:with_block_device) + end + + it "calls #unregister even when an exception is raised in the guard block" do + expect(@guard).to receive(:match?).and_return(true) + expect(@guard).to receive(:unregister) + expect do + with_block_device { raise Exception } + end.to raise_error(Exception) + end +end diff --git a/spec/mspec/spec/guards/bug_spec.rb b/spec/mspec/spec/guards/bug_spec.rb new file mode 100644 index 0000000000..72a3405dbc --- /dev/null +++ b/spec/mspec/spec/guards/bug_spec.rb @@ -0,0 +1,151 @@ +require 'spec_helper' +require 'mspec/guards' + +RSpec.describe BugGuard, "#match? when #implementation? is 'ruby'" do + before :all do + @verbose = $VERBOSE + $VERBOSE = nil + end + + after :all do + $VERBOSE = @verbose + end + + before :each do + hide_deprecation_warnings + stub_const "VersionGuard::FULL_RUBY_VERSION", SpecVersion.new('1.8.6') + @ruby_engine = Object.const_get :RUBY_ENGINE + Object.const_set :RUBY_ENGINE, 'ruby' + end + + after :each do + Object.const_set :RUBY_ENGINE, @ruby_engine + end + + it "returns false when version argument is less than RUBY_VERSION" do + expect(BugGuard.new("#1", "1.8.5").match?).to eq(false) + end + + it "returns true when version argument is equal to RUBY_VERSION" do + expect(BugGuard.new("#1", "1.8.6").match?).to eq(true) + end + + it "returns true when version argument is greater than RUBY_VERSION" do + expect(BugGuard.new("#1", "1.8.7").match?).to eq(true) + end + + it "returns true when version argument implicitly includes RUBY_VERSION" do + expect(BugGuard.new("#1", "1.8").match?).to eq(true) + expect(BugGuard.new("#1", "1.8.6").match?).to eq(true) + end + + it "returns true when the argument range includes RUBY_VERSION" do + expect(BugGuard.new("#1", '1.8.5'..'1.8.7').match?).to eq(true) + expect(BugGuard.new("#1", '1.8'..'1.9').match?).to eq(true) + expect(BugGuard.new("#1", '1.8'...'1.9').match?).to eq(true) + expect(BugGuard.new("#1", '1.8'..'1.8.6').match?).to eq(true) + expect(BugGuard.new("#1", '1.8.5'..'1.8.6').match?).to eq(true) + expect(BugGuard.new("#1", ''...'1.8.7').match?).to eq(true) + end + + it "returns false when the argument range does not include RUBY_VERSION" do + expect(BugGuard.new("#1", '1.8.7'..'1.8.9').match?).to eq(false) + expect(BugGuard.new("#1", '1.8.4'..'1.8.5').match?).to eq(false) + expect(BugGuard.new("#1", '1.8.4'...'1.8.6').match?).to eq(false) + expect(BugGuard.new("#1", '1.8.5'...'1.8.6').match?).to eq(false) + expect(BugGuard.new("#1", ''...'1.8.6').match?).to eq(false) + end + + it "returns false when MSpec.mode?(:no_ruby_bug) is true" do + expect(MSpec).to receive(:mode?).with(:no_ruby_bug).twice.and_return(:true) + expect(BugGuard.new("#1", "1.8.5").match?).to eq(false) + expect(BugGuard.new("#1", "1.8").match?).to eq(false) + end +end + +RSpec.describe BugGuard, "#match? when #implementation? is not 'ruby'" do + before :all do + @verbose = $VERBOSE + $VERBOSE = nil + end + + after :all do + $VERBOSE = @verbose + end + + before :each do + hide_deprecation_warnings + @ruby_version = Object.const_get :RUBY_VERSION + @ruby_engine = Object.const_get :RUBY_ENGINE + + Object.const_set :RUBY_VERSION, '1.8.6' + Object.const_set :RUBY_ENGINE, 'jruby' + end + + after :each do + Object.const_set :RUBY_VERSION, @ruby_version + Object.const_set :RUBY_ENGINE, @ruby_engine + end + + it "returns false when version argument is less than RUBY_VERSION" do + expect(BugGuard.new("#1", "1.8").match?).to eq(false) + expect(BugGuard.new("#1", "1.8.6").match?).to eq(false) + end + + it "returns false when version argument is equal to RUBY_VERSION" do + expect(BugGuard.new("#1", "1.8.6").match?).to eq(false) + end + + it "returns false when version argument is greater than RUBY_VERSION" do + expect(BugGuard.new("#1", "1.8.7").match?).to eq(false) + end + + it "returns false no matter if the argument range includes RUBY_VERSION" do + expect(BugGuard.new("#1", '1.8'...'1.9').match?).to eq(false) + expect(BugGuard.new("#1", '1.8.5'...'1.8.7').match?).to eq(false) + expect(BugGuard.new("#1", '1.8.4'...'1.8.6').match?).to eq(false) + end + + it "returns false when MSpec.mode?(:no_ruby_bug) is true" do + allow(MSpec).to receive(:mode?).and_return(:true) + expect(BugGuard.new("#1", "1.8.6").match?).to eq(false) + end +end + +RSpec.describe Object, "#ruby_bug" do + before :each do + hide_deprecation_warnings + @guard = BugGuard.new "#1234", "x.x.x" + allow(BugGuard).to receive(:new).and_return(@guard) + ScratchPad.clear + end + + it "yields when #match? returns false" do + allow(@guard).to receive(:match?).and_return(false) + ruby_bug("#1234", "1.8.6") { ScratchPad.record :yield } + expect(ScratchPad.recorded).to eq(:yield) + end + + it "does not yield when #match? returns true" do + allow(@guard).to receive(:match?).and_return(true) + ruby_bug("#1234", "1.8.6") { ScratchPad.record :yield } + expect(ScratchPad.recorded).not_to eq(:yield) + end + + it "requires a bug tracker number and a version number" do + expect { ruby_bug { } }.to raise_error(ArgumentError) + expect { ruby_bug("#1234") { } }.to raise_error(ArgumentError) + end + + it "sets the name of the guard to :ruby_bug" do + ruby_bug("#1234", "1.8.6") { } + expect(@guard.name).to eq(:ruby_bug) + end + + it "calls #unregister even when an exception is raised in the guard block" do + expect(@guard).to receive(:unregister) + expect do + ruby_bug("", "") { raise Exception } + end.to raise_error(Exception) + end +end diff --git a/spec/mspec/spec/guards/conflict_spec.rb b/spec/mspec/spec/guards/conflict_spec.rb new file mode 100644 index 0000000000..7dbe83153d --- /dev/null +++ b/spec/mspec/spec/guards/conflict_spec.rb @@ -0,0 +1,53 @@ +require 'spec_helper' +require 'mspec/guards' + +RSpec.describe Object, "#conflicts_with" do + before :each do + hide_deprecation_warnings + ScratchPad.clear + end + + it "does not yield if Object.constants includes any of the arguments" do + allow(Object).to receive(:constants).and_return(["SomeClass", "OtherClass"]) + conflicts_with(:SomeClass, :AClass, :BClass) { ScratchPad.record :yield } + expect(ScratchPad.recorded).not_to eq(:yield) + end + + it "does not yield if Object.constants (as Symbols) includes any of the arguments" do + allow(Object).to receive(:constants).and_return([:SomeClass, :OtherClass]) + conflicts_with(:SomeClass, :AClass, :BClass) { ScratchPad.record :yield } + expect(ScratchPad.recorded).not_to eq(:yield) + end + + it "yields if Object.constants does not include any of the arguments" do + allow(Object).to receive(:constants).and_return(["SomeClass", "OtherClass"]) + conflicts_with(:AClass, :BClass) { ScratchPad.record :yield } + expect(ScratchPad.recorded).to eq(:yield) + end + + it "yields if Object.constants (as Symbols) does not include any of the arguments" do + allow(Object).to receive(:constants).and_return([:SomeClass, :OtherClass]) + conflicts_with(:AClass, :BClass) { ScratchPad.record :yield } + expect(ScratchPad.recorded).to eq(:yield) + end +end + +RSpec.describe Object, "#conflicts_with" do + before :each do + hide_deprecation_warnings + @guard = ConflictsGuard.new + allow(ConflictsGuard).to receive(:new).and_return(@guard) + end + + it "sets the name of the guard to :conflicts_with" do + conflicts_with(:AClass, :BClass) { } + expect(@guard.name).to eq(:conflicts_with) + end + + it "calls #unregister even when an exception is raised in the guard block" do + expect(@guard).to receive(:unregister) + expect do + conflicts_with(:AClass, :BClass) { raise Exception } + end.to raise_error(Exception) + end +end diff --git a/spec/mspec/spec/guards/endian_spec.rb b/spec/mspec/spec/guards/endian_spec.rb new file mode 100644 index 0000000000..943b558ed3 --- /dev/null +++ b/spec/mspec/spec/guards/endian_spec.rb @@ -0,0 +1,55 @@ +require 'spec_helper' +require 'mspec/guards' + +RSpec.describe Object, "#big_endian" do + before :each do + @guard = BigEndianGuard.new + allow(BigEndianGuard).to receive(:new).and_return(@guard) + ScratchPad.clear + end + + it "yields on big-endian platforms" do + allow(@guard).to receive(:pattern).and_return([?\001]) + big_endian { ScratchPad.record :yield } + expect(ScratchPad.recorded).to eq(:yield) + end + + it "does not yield on little-endian platforms" do + allow(@guard).to receive(:pattern).and_return([?\000]) + big_endian { ScratchPad.record :yield } + expect(ScratchPad.recorded).not_to eq(:yield) + end + + it "sets the name of the guard to :big_endian" do + big_endian { } + expect(@guard.name).to eq(:big_endian) + end + + it "calls #unregister even when an exception is raised in the guard block" do + allow(@guard).to receive(:pattern).and_return([?\001]) + expect(@guard).to receive(:unregister) + expect do + big_endian { raise Exception } + end.to raise_error(Exception) + end +end + +RSpec.describe Object, "#little_endian" do + before :each do + @guard = BigEndianGuard.new + allow(BigEndianGuard).to receive(:new).and_return(@guard) + ScratchPad.clear + end + + it "yields on little-endian platforms" do + allow(@guard).to receive(:pattern).and_return([?\000]) + little_endian { ScratchPad.record :yield } + expect(ScratchPad.recorded).to eq(:yield) + end + + it "does not yield on big-endian platforms" do + allow(@guard).to receive(:pattern).and_return([?\001]) + little_endian { ScratchPad.record :yield } + expect(ScratchPad.recorded).not_to eq(:yield) + end +end diff --git a/spec/mspec/spec/guards/feature_spec.rb b/spec/mspec/spec/guards/feature_spec.rb new file mode 100644 index 0000000000..fcb8997591 --- /dev/null +++ b/spec/mspec/spec/guards/feature_spec.rb @@ -0,0 +1,120 @@ +require 'spec_helper' +require 'mspec/guards' + +RSpec.describe FeatureGuard, ".enabled?" do + it "returns true if the feature is enabled" do + expect(MSpec).to receive(:feature_enabled?).with(:encoding).and_return(true) + expect(FeatureGuard.enabled?(:encoding)).to be_truthy + end + + it "returns false if the feature is not enabled" do + expect(MSpec).to receive(:feature_enabled?).with(:encoding).and_return(false) + expect(FeatureGuard.enabled?(:encoding)).to be_falsey + end + + it "returns true if all the features are enabled" do + expect(MSpec).to receive(:feature_enabled?).with(:one).and_return(true) + expect(MSpec).to receive(:feature_enabled?).with(:two).and_return(true) + expect(FeatureGuard.enabled?(:one, :two)).to be_truthy + end + + it "returns false if any of the features are not enabled" do + expect(MSpec).to receive(:feature_enabled?).with(:one).and_return(true) + expect(MSpec).to receive(:feature_enabled?).with(:two).and_return(false) + expect(FeatureGuard.enabled?(:one, :two)).to be_falsey + end +end + +RSpec.describe Object, "#with_feature" do + before :each do + ScratchPad.clear + + @guard = FeatureGuard.new :encoding + allow(FeatureGuard).to receive(:new).and_return(@guard) + end + + it "sets the name of the guard to :with_feature" do + with_feature(:encoding) { } + expect(@guard.name).to eq(:with_feature) + end + + it "calls #unregister even when an exception is raised in the guard block" do + expect(@guard).to receive(:match?).and_return(true) + expect(@guard).to receive(:unregister) + expect do + with_feature { raise Exception } + end.to raise_error(Exception) + end +end + +RSpec.describe Object, "#with_feature" do + before :each do + ScratchPad.clear + end + + it "yields if the feature is enabled" do + expect(MSpec).to receive(:feature_enabled?).with(:encoding).and_return(true) + with_feature(:encoding) { ScratchPad.record :yield } + expect(ScratchPad.recorded).to eq(:yield) + end + + it "yields if all the features are enabled" do + expect(MSpec).to receive(:feature_enabled?).with(:one).and_return(true) + expect(MSpec).to receive(:feature_enabled?).with(:two).and_return(true) + with_feature(:one, :two) { ScratchPad.record :yield } + expect(ScratchPad.recorded).to eq(:yield) + end + + it "does not yield if the feature is not enabled" do + expect(MSpec).to receive(:feature_enabled?).with(:encoding).and_return(false) + with_feature(:encoding) { ScratchPad.record :yield } + expect(ScratchPad.recorded).to be_nil + end + + it "does not yield if any of the features are not enabled" do + expect(MSpec).to receive(:feature_enabled?).with(:one).and_return(true) + expect(MSpec).to receive(:feature_enabled?).with(:two).and_return(false) + with_feature(:one, :two) { ScratchPad.record :yield } + expect(ScratchPad.recorded).to be_nil + end +end + +RSpec.describe Object, "#without_feature" do + before :each do + ScratchPad.clear + + @guard = FeatureGuard.new :encoding + allow(FeatureGuard).to receive(:new).and_return(@guard) + end + + it "sets the name of the guard to :without_feature" do + without_feature(:encoding) { } + expect(@guard.name).to eq(:without_feature) + end + + it "calls #unregister even when an exception is raised in the guard block" do + expect(@guard).to receive(:match?).and_return(false) + expect(@guard).to receive(:unregister) + expect do + without_feature { raise Exception } + end.to raise_error(Exception) + end +end + +RSpec.describe Object, "#without_feature" do + before :each do + ScratchPad.clear + end + + it "does not yield if the feature is enabled" do + expect(MSpec).to receive(:feature_enabled?).with(:encoding).and_return(true) + without_feature(:encoding) { ScratchPad.record :yield } + expect(ScratchPad.recorded).to be_nil + end + + it "yields if the feature is disabled" do + expect(MSpec).to receive(:feature_enabled?).with(:encoding).and_return(false) + without_feature(:encoding) { ScratchPad.record :yield } + expect(ScratchPad.recorded).to eq(:yield) + end +end diff --git a/spec/mspec/spec/guards/guard_spec.rb b/spec/mspec/spec/guards/guard_spec.rb new file mode 100644 index 0000000000..e29d235747 --- /dev/null +++ b/spec/mspec/spec/guards/guard_spec.rb @@ -0,0 +1,421 @@ +require 'spec_helper' +require 'mspec/guards' +require 'rbconfig' + +RSpec.describe SpecGuard, ".ruby_version" do + before :each do + stub_const "RUBY_VERSION", "8.2.3" + end + + it "returns the full version for :full" do + expect(SpecGuard.ruby_version(:full)).to eq("8.2.3") + end + + it "returns major.minor.tiny for :tiny" do + expect(SpecGuard.ruby_version(:tiny)).to eq("8.2.3") + end + + it "returns major.minor.tiny for :teeny" do + expect(SpecGuard.ruby_version(:tiny)).to eq("8.2.3") + end + + it "returns major.minor for :minor" do + expect(SpecGuard.ruby_version(:minor)).to eq("8.2") + end + + it "defaults to :minor" do + expect(SpecGuard.ruby_version).to eq("8.2") + end + + it "returns major for :major" do + expect(SpecGuard.ruby_version(:major)).to eq("8") + end +end + +RSpec.describe SpecGuard, "#yield?" do + before :each do + MSpec.clear_modes + @guard = SpecGuard.new + allow(@guard).to receive(:match?).and_return(false) + end + + after :each do + MSpec.unregister :add, @guard + MSpec.clear_modes + SpecGuard.clear_guards + end + + it "returns true if MSpec.mode?(:unguarded) is true" do + MSpec.register_mode :unguarded + expect(@guard.yield?).to eq(true) + end + + it "returns true if MSpec.mode?(:verify) is true" do + MSpec.register_mode :verify + expect(@guard.yield?).to eq(true) + end + + it "returns true if MSpec.mode?(:verify) is true regardless of invert being true" do + MSpec.register_mode :verify + expect(@guard.yield?(true)).to eq(true) + end + + it "returns true if MSpec.mode?(:report) is true" do + MSpec.register_mode :report + expect(@guard.yield?).to eq(true) + end + + it "returns true if MSpec.mode?(:report) is true regardless of invert being true" do + MSpec.register_mode :report + expect(@guard.yield?(true)).to eq(true) + end + + it "returns true if MSpec.mode?(:report_on) is true and SpecGuards.guards contains the named guard" do + MSpec.register_mode :report_on + SpecGuard.guards << :guard_name + expect(@guard.yield?).to eq(false) + @guard.name = :guard_name + expect(@guard.yield?).to eq(true) + end + + it "returns #match? if neither report nor verify mode are true" do + allow(@guard).to receive(:match?).and_return(false) + expect(@guard.yield?).to eq(false) + allow(@guard).to receive(:match?).and_return(true) + expect(@guard.yield?).to eq(true) + end + + it "returns #match? if invert is true and neither report nor verify mode are true" do + allow(@guard).to receive(:match?).and_return(false) + expect(@guard.yield?(true)).to eq(true) + allow(@guard).to receive(:match?).and_return(true) + expect(@guard.yield?(true)).to eq(false) + end +end + +RSpec.describe SpecGuard, "#match?" do + before :each do + @guard = SpecGuard.new + end + + it "must be implemented in subclasses" do + expect { + @guard.match? + }.to raise_error("must be implemented by the subclass") + end +end + +RSpec.describe SpecGuard, "#unregister" do + before :each do + allow(MSpec).to receive(:unregister) + @guard = SpecGuard.new + end + + it "unregisters from MSpec :add actions" do + expect(MSpec).to receive(:unregister).with(:add, @guard) + @guard.unregister + end +end + +RSpec.describe SpecGuard, "#record" do + after :each do + SpecGuard.clear + end + + it "saves the name of the guarded spec under the name of the guard" do + guard = SpecGuard.new "a", "1.8"..."1.9" + guard.name = :named_guard + guard.record "SomeClass#action returns true" + expect(SpecGuard.report).to eq({ + 'named_guard a, 1.8...1.9' => ["SomeClass#action returns true"] + }) + end +end + +RSpec.describe SpecGuard, ".guards" do + it "returns an Array" do + expect(SpecGuard.guards).to be_kind_of(Array) + end +end + +RSpec.describe SpecGuard, ".clear_guards" do + it "resets the array to empty" do + SpecGuard.guards << :guard + expect(SpecGuard.guards).to eq([:guard]) + SpecGuard.clear_guards + expect(SpecGuard.guards).to eq([]) + end +end + +RSpec.describe SpecGuard, ".finish" do + before :each do + $stdout = @out = IOStub.new + end + + after :each do + $stdout = STDOUT + SpecGuard.clear + end + + it "prints the descriptions of the guarded specs" do + guard = SpecGuard.new "a", "1.8"..."1.9" + guard.name = :named_guard + guard.record "SomeClass#action returns true" + guard.record "SomeClass#reverse returns false" + SpecGuard.finish + expect($stdout).to eq(%[ + +2 specs omitted by guard: named_guard a, 1.8...1.9: + +SomeClass#action returns true +SomeClass#reverse returns false + +]) + end +end + +RSpec.describe SpecGuard, ".run_if" do + before :each do + @guard = SpecGuard.new + ScratchPad.clear + end + + it "yields if match? returns true" do + allow(@guard).to receive(:match?).and_return(true) + @guard.run_if(:name) { ScratchPad.record :yield } + expect(ScratchPad.recorded).to eq(:yield) + end + + it "does not yield if match? returns false" do + allow(@guard).to receive(:match?).and_return(false) + @guard.run_if(:name) { fail } + end + + it "returns the result of the block if match? is true" do + allow(@guard).to receive(:match?).and_return(true) + expect(@guard.run_if(:name) { 42 }).to eq(42) + end + + it "returns nil if given a block and match? is false" do + allow(@guard).to receive(:match?).and_return(false) + expect(@guard.run_if(:name) { 42 }).to eq(nil) + end + + it "returns what #match? returns when no block is given" do + allow(@guard).to receive(:match?).and_return(true) + expect(@guard.run_if(:name)).to eq(true) + allow(@guard).to receive(:match?).and_return(false) + expect(@guard.run_if(:name)).to eq(false) + end +end + +RSpec.describe SpecGuard, ".run_unless" do + before :each do + @guard = SpecGuard.new + ScratchPad.clear + end + + it "yields if match? returns false" do + allow(@guard).to receive(:match?).and_return(false) + @guard.run_unless(:name) { ScratchPad.record :yield } + expect(ScratchPad.recorded).to eq(:yield) + end + + it "does not yield if match? returns true" do + allow(@guard).to receive(:match?).and_return(true) + @guard.run_unless(:name) { fail } + end + + it "returns the result of the block if match? is false" do + allow(@guard).to receive(:match?).and_return(false) + expect(@guard.run_unless(:name) { 42 }).to eq(42) + end + + it "returns nil if given a block and match? is true" do + allow(@guard).to receive(:match?).and_return(true) + expect(@guard.run_unless(:name) { 42 }).to eq(nil) + end + + it "returns the opposite of what #match? returns when no block is given" do + allow(@guard).to receive(:match?).and_return(true) + expect(@guard.run_unless(:name)).to eq(false) + allow(@guard).to receive(:match?).and_return(false) + expect(@guard.run_unless(:name)).to eq(true) + end +end + +RSpec.describe Object, "#guard" do + before :each do + ScratchPad.clear + end + + after :each do + MSpec.clear_modes + end + + it "allows to combine guards" do + guard1 = VersionGuard.new '1.2.3', 'x.x.x' + allow(VersionGuard).to receive(:new).and_return(guard1) + guard2 = PlatformGuard.new :dummy + allow(PlatformGuard).to receive(:new).and_return(guard2) + + allow(guard1).to receive(:match?).and_return(true) + allow(guard2).to receive(:match?).and_return(true) + guard -> { ruby_version_is "2.4" and platform_is :linux } do + ScratchPad.record :yield + end + expect(ScratchPad.recorded).to eq(:yield) + + allow(guard1).to receive(:match?).and_return(false) + allow(guard2).to receive(:match?).and_return(true) + guard -> { ruby_version_is "2.4" and platform_is :linux } do + fail + end + + allow(guard1).to receive(:match?).and_return(true) + allow(guard2).to receive(:match?).and_return(false) + guard -> { ruby_version_is "2.4" and platform_is :linux } do + fail + end + + allow(guard1).to receive(:match?).and_return(false) + allow(guard2).to receive(:match?).and_return(false) + guard -> { ruby_version_is "2.4" and platform_is :linux } do + fail + end + end + + it "yields when the Proc returns true" do + guard -> { true } do + ScratchPad.record :yield + end + expect(ScratchPad.recorded).to eq(:yield) + end + + it "does not yield when the Proc returns false" do + guard -> { false } do + fail + end + end + + it "yields if MSpec.mode?(:unguarded) is true" do + MSpec.register_mode :unguarded + + guard -> { false } do + ScratchPad.record :yield1 + end + expect(ScratchPad.recorded).to eq(:yield1) + + guard -> { true } do + ScratchPad.record :yield2 + end + expect(ScratchPad.recorded).to eq(:yield2) + end + + it "yields if MSpec.mode?(:verify) is true" do + MSpec.register_mode :verify + + guard -> { false } do + ScratchPad.record :yield1 + end + expect(ScratchPad.recorded).to eq(:yield1) + + guard -> { true } do + ScratchPad.record :yield2 + end + expect(ScratchPad.recorded).to eq(:yield2) + end + + it "yields if MSpec.mode?(:report) is true" do + MSpec.register_mode :report + + guard -> { false } do + ScratchPad.record :yield1 + end + expect(ScratchPad.recorded).to eq(:yield1) + + guard -> { true } do + ScratchPad.record :yield2 + end + expect(ScratchPad.recorded).to eq(:yield2) + end + + it "raises an error if no Proc is given" do + expect { guard :foo }.to raise_error(RuntimeError) + end + + it "requires a block" do + expect { + guard(-> { true }) + }.to raise_error(LocalJumpError) + expect { + guard(-> { false }) + }.to raise_error(LocalJumpError) + end +end + +RSpec.describe Object, "#guard_not" do + before :each do + ScratchPad.clear + end + + it "allows to combine guards" do + guard1 = VersionGuard.new '1.2.3', 'x.x.x' + allow(VersionGuard).to receive(:new).and_return(guard1) + guard2 = PlatformGuard.new :dummy + allow(PlatformGuard).to receive(:new).and_return(guard2) + + allow(guard1).to receive(:match?).and_return(true) + allow(guard2).to receive(:match?).and_return(true) + guard_not -> { ruby_version_is "2.4" and platform_is :linux } do + fail + end + + allow(guard1).to receive(:match?).and_return(false) + allow(guard2).to receive(:match?).and_return(true) + guard_not -> { ruby_version_is "2.4" and platform_is :linux } do + ScratchPad.record :yield1 + end + expect(ScratchPad.recorded).to eq(:yield1) + + allow(guard1).to receive(:match?).and_return(true) + allow(guard2).to receive(:match?).and_return(false) + guard_not -> { ruby_version_is "2.4" and platform_is :linux } do + ScratchPad.record :yield2 + end + expect(ScratchPad.recorded).to eq(:yield2) + + allow(guard1).to receive(:match?).and_return(false) + allow(guard2).to receive(:match?).and_return(false) + guard_not -> { ruby_version_is "2.4" and platform_is :linux } do + ScratchPad.record :yield3 + end + expect(ScratchPad.recorded).to eq(:yield3) + end + + it "yields when the Proc returns false" do + guard_not -> { false } do + ScratchPad.record :yield + end + expect(ScratchPad.recorded).to eq(:yield) + end + + it "does not yield when the Proc returns true" do + guard_not -> { true } do + fail + end + end + + it "raises an error if no Proc is given" do + expect { guard_not :foo }.to raise_error(RuntimeError) + end + + it "requires a block" do + expect { + guard_not(-> { true }) + }.to raise_error(LocalJumpError) + expect { + guard_not(-> { false }) + }.to raise_error(LocalJumpError) + end +end diff --git a/spec/mspec/spec/guards/platform_spec.rb b/spec/mspec/spec/guards/platform_spec.rb new file mode 100644 index 0000000000..bd37432800 --- /dev/null +++ b/spec/mspec/spec/guards/platform_spec.rb @@ -0,0 +1,337 @@ +require 'spec_helper' +require 'mspec/guards' + +RSpec.describe Object, "#platform_is" do + before :each do + @guard = PlatformGuard.new :dummy + allow(PlatformGuard).to receive(:new).and_return(@guard) + ScratchPad.clear + end + + it "does not yield when #os? returns false" do + allow(PlatformGuard).to receive(:os?).and_return(false) + platform_is(:ruby) { ScratchPad.record :yield } + expect(ScratchPad.recorded).not_to eq(:yield) + end + + it "yields when #os? returns true" do + allow(PlatformGuard).to receive(:os?).and_return(true) + platform_is(:solarce) { ScratchPad.record :yield } + expect(ScratchPad.recorded).to eq(:yield) + end + + it "returns what #os? returns when no block is given" do + allow(PlatformGuard).to receive(:os?).and_return(true) + expect(platform_is(:solarce)).to eq(true) + allow(PlatformGuard).to receive(:os?).and_return(false) + expect(platform_is(:solarce)).to eq(false) + end + + it "sets the name of the guard to :platform_is" do + platform_is(:solarce) { } + expect(@guard.name).to eq(:platform_is) + end + + it "calls #unregister even when an exception is raised in the guard block" do + expect(@guard).to receive(:match?).and_return(true) + expect(@guard).to receive(:unregister) + expect do + platform_is(:solarce) { raise Exception } + end.to raise_error(Exception) + end +end + +RSpec.describe Object, "#platform_is_not" do + before :each do + @guard = PlatformGuard.new :dummy + allow(PlatformGuard).to receive(:new).and_return(@guard) + ScratchPad.clear + end + + it "does not yield when #os? returns true" do + allow(PlatformGuard).to receive(:os?).and_return(true) + platform_is_not(:ruby) { ScratchPad.record :yield } + expect(ScratchPad.recorded).not_to eq(:yield) + end + + it "yields when #os? returns false" do + allow(PlatformGuard).to receive(:os?).and_return(false) + platform_is_not(:solarce) { ScratchPad.record :yield } + expect(ScratchPad.recorded).to eq(:yield) + end + + it "returns the opposite of what #os? returns when no block is given" do + allow(PlatformGuard).to receive(:os?).and_return(true) + expect(platform_is_not(:solarce)).to eq(false) + allow(PlatformGuard).to receive(:os?).and_return(false) + expect(platform_is_not(:solarce)).to eq(true) + end + + it "sets the name of the guard to :platform_is_not" do + platform_is_not(:solarce) { } + expect(@guard.name).to eq(:platform_is_not) + end + + it "calls #unregister even when an exception is raised in the guard block" do + expect(@guard).to receive(:match?).and_return(false) + expect(@guard).to receive(:unregister) + expect do + platform_is_not(:solarce) { raise Exception } + end.to raise_error(Exception) + end +end + +RSpec.describe Object, "#platform_is :c_long_size => SIZE_SPEC" do + before :each do + @guard = PlatformGuard.new :darwin, :c_long_size => 32 + allow(PlatformGuard).to receive(:os?).and_return(true) + allow(PlatformGuard).to receive(:new).and_return(@guard) + ScratchPad.clear + end + + it "yields when #c_long_size? returns true" do + allow(PlatformGuard).to receive(:c_long_size?).and_return(true) + platform_is(:c_long_size => 32) { ScratchPad.record :yield } + expect(ScratchPad.recorded).to eq(:yield) + end + + it "doesn not yield when #c_long_size? returns false" do + allow(PlatformGuard).to receive(:c_long_size?).and_return(false) + platform_is(:c_long_size => 32) { ScratchPad.record :yield } + expect(ScratchPad.recorded).not_to eq(:yield) + end +end + +RSpec.describe Object, "#platform_is_not :c_long_size => SIZE_SPEC" do + before :each do + @guard = PlatformGuard.new :darwin, :c_long_size => 32 + allow(PlatformGuard).to receive(:os?).and_return(true) + allow(PlatformGuard).to receive(:new).and_return(@guard) + ScratchPad.clear + end + + it "yields when #c_long_size? returns false" do + allow(PlatformGuard).to receive(:c_long_size?).and_return(false) + platform_is_not(:c_long_size => 32) { ScratchPad.record :yield } + expect(ScratchPad.recorded).to eq(:yield) + end + + it "doesn not yield when #c_long_size? returns true" do + allow(PlatformGuard).to receive(:c_long_size?).and_return(true) + platform_is_not(:c_long_size => 32) { ScratchPad.record :yield } + expect(ScratchPad.recorded).not_to eq(:yield) + end +end + +RSpec.describe PlatformGuard, ".implementation?" do + it "returns true if passed :ruby and RUBY_ENGINE == 'ruby'" do + stub_const 'RUBY_ENGINE', 'ruby' + expect(PlatformGuard.implementation?(:ruby)).to eq(true) + end + + it "returns true if passed :rubinius and RUBY_ENGINE == 'rbx'" do + stub_const 'RUBY_ENGINE', 'rbx' + expect(PlatformGuard.implementation?(:rubinius)).to eq(true) + end + + it "returns true if passed :jruby and RUBY_ENGINE == 'jruby'" do + stub_const 'RUBY_ENGINE', 'jruby' + expect(PlatformGuard.implementation?(:jruby)).to eq(true) + end + + it "returns true if passed :ironruby and RUBY_ENGINE == 'ironruby'" do + stub_const 'RUBY_ENGINE', 'ironruby' + expect(PlatformGuard.implementation?(:ironruby)).to eq(true) + end + + it "returns true if passed :maglev and RUBY_ENGINE == 'maglev'" do + stub_const 'RUBY_ENGINE', 'maglev' + expect(PlatformGuard.implementation?(:maglev)).to eq(true) + end + + it "returns true if passed :topaz and RUBY_ENGINE == 'topaz'" do + stub_const 'RUBY_ENGINE', 'topaz' + expect(PlatformGuard.implementation?(:topaz)).to eq(true) + end + + it "returns true if passed :ruby and RUBY_ENGINE matches /^ruby/" do + stub_const 'RUBY_ENGINE', 'ruby' + expect(PlatformGuard.implementation?(:ruby)).to eq(true) + + stub_const 'RUBY_ENGINE', 'ruby1.8' + expect(PlatformGuard.implementation?(:ruby)).to eq(true) + + stub_const 'RUBY_ENGINE', 'ruby1.9' + expect(PlatformGuard.implementation?(:ruby)).to eq(true) + end + + it "works for an unrecognized name" do + stub_const 'RUBY_ENGINE', 'myrubyimplementation' + expect(PlatformGuard.implementation?(:myrubyimplementation)).to eq(true) + expect(PlatformGuard.implementation?(:other)).to eq(false) + end +end + +RSpec.describe PlatformGuard, ".standard?" do + it "returns true if implementation? returns true" do + expect(PlatformGuard).to receive(:implementation?).with(:ruby).and_return(true) + expect(PlatformGuard.standard?).to be_truthy + end + + it "returns false if implementation? returns false" do + expect(PlatformGuard).to receive(:implementation?).with(:ruby).and_return(false) + expect(PlatformGuard.standard?).to be_falsey + end +end + +RSpec.describe PlatformGuard, ".c_long_size?" do + it "returns true when arg is 32 and 1.size is 4" do + expect(PlatformGuard.c_long_size?(32)).to eq(1.size == 4) + end + + it "returns true when arg is 64 and 1.size is 8" do + expect(PlatformGuard.c_long_size?(64)).to eq(1.size == 8) + end +end + +RSpec.describe PlatformGuard, ".os?" do + before :each do + stub_const 'PlatformGuard::PLATFORM', 'solarce' + end + + it "returns false when arg does not match the platform" do + expect(PlatformGuard.os?(:ruby)).to eq(false) + end + + it "returns false when no arg matches the platform" do + expect(PlatformGuard.os?(:ruby, :jruby, :rubinius, :maglev)).to eq(false) + end + + it "returns true when arg matches the platform" do + expect(PlatformGuard.os?(:solarce)).to eq(true) + end + + it "returns true when any arg matches the platform" do + expect(PlatformGuard.os?(:ruby, :jruby, :solarce, :rubinius, :maglev)).to eq(true) + end + + it "returns true when arg is :windows and the platform contains 'mswin'" do + stub_const 'PlatformGuard::PLATFORM', 'mswin32' + expect(PlatformGuard.os?(:windows)).to eq(true) + end + + it "returns true when arg is :windows and the platform contains 'mingw'" do + stub_const 'PlatformGuard::PLATFORM', 'i386-mingw32' + expect(PlatformGuard.os?(:windows)).to eq(true) + end + + it "returns false when arg is not :windows and RbConfig::CONFIG['host_os'] contains 'mswin'" do + stub_const 'PlatformGuard::PLATFORM', 'i386-mswin32' + expect(PlatformGuard.os?(:linux)).to eq(false) + end + + it "returns false when arg is not :windows and RbConfig::CONFIG['host_os'] contains 'mingw'" do + stub_const 'PlatformGuard::PLATFORM', 'i386-mingw32' + expect(PlatformGuard.os?(:linux)).to eq(false) + end +end + +RSpec.describe PlatformGuard, ".os?" do + it "returns true if called with the current OS or architecture" do + os = RbConfig::CONFIG["host_os"].sub("-gnu", "") + arch = RbConfig::CONFIG["host_arch"] + expect(PlatformGuard.os?(os)).to eq(true) + expect(PlatformGuard.os?(arch)).to eq(true) + expect(PlatformGuard.os?("#{arch}-#{os}")).to eq(true) + end +end + +RSpec.describe PlatformGuard, ".os? on JRuby" do + before :all do + @verbose = $VERBOSE + $VERBOSE = nil + end + + after :all do + $VERBOSE = @verbose + end + + before :each do + @ruby_platform = Object.const_get :RUBY_PLATFORM + Object.const_set :RUBY_PLATFORM, 'java' + end + + after :each do + Object.const_set :RUBY_PLATFORM, @ruby_platform + end + + it "raises an error when testing for a :java platform" do + expect { + PlatformGuard.os?(:java) + }.to raise_error(":java is not a valid OS") + end + + it "returns true when arg is :windows and RUBY_PLATFORM contains 'java' and os?(:windows) is true" do + stub_const 'PlatformGuard::PLATFORM', 'mswin32' + expect(PlatformGuard.os?(:windows)).to eq(true) + end + + it "returns true when RUBY_PLATFORM contains 'java' and os?(argument) is true" do + stub_const 'PlatformGuard::PLATFORM', 'amiga' + expect(PlatformGuard.os?(:amiga)).to eq(true) + end +end + +RSpec.describe PlatformGuard, ".os?" do + before :each do + stub_const 'PlatformGuard::PLATFORM', 'unreal' + end + + it "returns true if argument matches RbConfig::CONFIG['host_os']" do + expect(PlatformGuard.os?(:unreal)).to eq(true) + end + + it "returns true if any argument matches RbConfig::CONFIG['host_os']" do + expect(PlatformGuard.os?(:bsd, :unreal, :amiga)).to eq(true) + end + + it "returns false if no argument matches RbConfig::CONFIG['host_os']" do + expect(PlatformGuard.os?(:bsd, :netbsd, :amiga, :msdos)).to eq(false) + end + + it "returns false if argument does not match RbConfig::CONFIG['host_os']" do + expect(PlatformGuard.os?(:amiga)).to eq(false) + end + + it "returns true when arg is :windows and RbConfig::CONFIG['host_os'] contains 'mswin'" do + stub_const 'PlatformGuard::PLATFORM', 'i386-mswin32' + expect(PlatformGuard.os?(:windows)).to eq(true) + end + + it "returns true when arg is :windows and RbConfig::CONFIG['host_os'] contains 'mingw'" do + stub_const 'PlatformGuard::PLATFORM', 'i386-mingw32' + expect(PlatformGuard.os?(:windows)).to eq(true) + end + + it "returns false when arg is not :windows and RbConfig::CONFIG['host_os'] contains 'mswin'" do + stub_const 'PlatformGuard::PLATFORM', 'i386-mingw32' + expect(PlatformGuard.os?(:linux)).to eq(false) + end + + it "returns false when arg is not :windows and RbConfig::CONFIG['host_os'] contains 'mingw'" do + stub_const 'PlatformGuard::PLATFORM', 'i386-mingw32' + expect(PlatformGuard.os?(:linux)).to eq(false) + end +end + +RSpec.describe PlatformGuard, ".windows?" do + it "returns true on windows" do + stub_const 'PlatformGuard::PLATFORM', 'i386-mingw32' + expect(PlatformGuard.windows?).to eq(true) + end + + it "returns false on non-windows" do + stub_const 'PlatformGuard::PLATFORM', 'i586-linux' + expect(PlatformGuard.windows?).to eq(false) + end +end diff --git a/spec/mspec/spec/guards/quarantine_spec.rb b/spec/mspec/spec/guards/quarantine_spec.rb new file mode 100644 index 0000000000..eb5ff1da27 --- /dev/null +++ b/spec/mspec/spec/guards/quarantine_spec.rb @@ -0,0 +1,35 @@ +require 'spec_helper' +require 'mspec/guards' + +RSpec.describe QuarantineGuard, "#match?" do + it "returns true" do + expect(QuarantineGuard.new.match?).to eq(true) + end +end + +RSpec.describe Object, "#quarantine!" do + before :each do + ScratchPad.clear + + @guard = QuarantineGuard.new + allow(QuarantineGuard).to receive(:new).and_return(@guard) + end + + it "does not yield" do + quarantine! { ScratchPad.record :yield } + expect(ScratchPad.recorded).not_to eq(:yield) + end + + it "sets the name of the guard to :quarantine!" do + quarantine! { } + expect(@guard.name).to eq(:quarantine!) + end + + it "calls #unregister even when an exception is raised in the guard block" do + expect(@guard).to receive(:match?).and_return(false) + expect(@guard).to receive(:unregister) + expect do + quarantine! { raise Exception } + end.to raise_error(Exception) + end +end diff --git a/spec/mspec/spec/guards/superuser_spec.rb b/spec/mspec/spec/guards/superuser_spec.rb new file mode 100644 index 0000000000..aba2cc2bb0 --- /dev/null +++ b/spec/mspec/spec/guards/superuser_spec.rb @@ -0,0 +1,35 @@ +require 'spec_helper' +require 'mspec/guards' + +RSpec.describe Object, "#as_superuser" do + before :each do + @guard = SuperUserGuard.new + allow(SuperUserGuard).to receive(:new).and_return(@guard) + ScratchPad.clear + end + + it "does not yield when Process.euid is not 0" do + allow(Process).to receive(:euid).and_return(501) + as_superuser { ScratchPad.record :yield } + expect(ScratchPad.recorded).not_to eq(:yield) + end + + it "yields when Process.euid is 0" do + allow(Process).to receive(:euid).and_return(0) + as_superuser { ScratchPad.record :yield } + expect(ScratchPad.recorded).to eq(:yield) + end + + it "sets the name of the guard to :as_superuser" do + as_superuser { } + expect(@guard.name).to eq(:as_superuser) + end + + it "calls #unregister even when an exception is raised in the guard block" do + expect(@guard).to receive(:match?).and_return(true) + expect(@guard).to receive(:unregister) + expect do + as_superuser { raise Exception } + end.to raise_error(Exception) + end +end diff --git a/spec/mspec/spec/guards/support_spec.rb b/spec/mspec/spec/guards/support_spec.rb new file mode 100644 index 0000000000..a61d003d6c --- /dev/null +++ b/spec/mspec/spec/guards/support_spec.rb @@ -0,0 +1,54 @@ +require 'spec_helper' +require 'mspec/guards' + +RSpec.describe Object, "#not_supported_on" do + before :each do + ScratchPad.clear + end + + it "raises an Exception when passed :ruby" do + stub_const "RUBY_ENGINE", "jruby" + expect { + not_supported_on(:ruby) { ScratchPad.record :yield } + }.to raise_error(Exception) + expect(ScratchPad.recorded).not_to eq(:yield) + end + + it "does not yield when #implementation? returns true" do + stub_const "RUBY_ENGINE", "jruby" + not_supported_on(:jruby) { ScratchPad.record :yield } + expect(ScratchPad.recorded).not_to eq(:yield) + end + + it "yields when #standard? returns true" do + stub_const "RUBY_ENGINE", "ruby" + not_supported_on(:rubinius) { ScratchPad.record :yield } + expect(ScratchPad.recorded).to eq(:yield) + end + + it "yields when #implementation? returns false" do + stub_const "RUBY_ENGINE", "jruby" + not_supported_on(:rubinius) { ScratchPad.record :yield } + expect(ScratchPad.recorded).to eq(:yield) + end +end + +RSpec.describe Object, "#not_supported_on" do + before :each do + @guard = SupportedGuard.new + allow(SupportedGuard).to receive(:new).and_return(@guard) + end + + it "sets the name of the guard to :not_supported_on" do + not_supported_on(:rubinius) { } + expect(@guard.name).to eq(:not_supported_on) + end + + it "calls #unregister even when an exception is raised in the guard block" do + expect(@guard).to receive(:match?).and_return(false) + expect(@guard).to receive(:unregister) + expect do + not_supported_on(:rubinius) { raise Exception } + end.to raise_error(Exception) + end +end diff --git a/spec/mspec/spec/guards/user_spec.rb b/spec/mspec/spec/guards/user_spec.rb new file mode 100644 index 0000000000..2526504656 --- /dev/null +++ b/spec/mspec/spec/guards/user_spec.rb @@ -0,0 +1,20 @@ +require 'spec_helper' +require 'mspec/guards' + +RSpec.describe Object, "#as_user" do + before :each do + ScratchPad.clear + end + + it "yields when the Process.euid is not 0" do + allow(Process).to receive(:euid).and_return(501) + as_user { ScratchPad.record :yield } + expect(ScratchPad.recorded).to eq(:yield) + end + + it "does not yield when the Process.euid is 0" do + allow(Process).to receive(:euid).and_return(0) + as_user { ScratchPad.record :yield } + expect(ScratchPad.recorded).not_to eq(:yield) + end +end diff --git a/spec/mspec/spec/guards/version_spec.rb b/spec/mspec/spec/guards/version_spec.rb new file mode 100644 index 0000000000..5a5f4ddc3b --- /dev/null +++ b/spec/mspec/spec/guards/version_spec.rb @@ -0,0 +1,112 @@ +require 'spec_helper' +require 'mspec/guards' + +# The VersionGuard specifies a version of Ruby with a String of +# the form: v = 'major.minor.tiny'. +# +# A VersionGuard instance can be created with a single String, +# which means any version >= each component of v. +# Or, the guard can be created with a Range, a..b, or a...b, +# where a, b are of the same form as v. The meaning of the Range +# is as typically understood: a..b means v >= a and v <= b; +# a...b means v >= a and v < b. + +RSpec.describe VersionGuard, "#match?" do + before :each do + hide_deprecation_warnings + @current = '1.8.6' + end + + it "returns true when the argument is equal to RUBY_VERSION" do + expect(VersionGuard.new(@current, '1.8.6').match?).to eq(true) + end + + it "returns true when the argument is less than RUBY_VERSION" do + expect(VersionGuard.new(@current, '1.8').match?).to eq(true) + expect(VersionGuard.new(@current, '1.8.5').match?).to eq(true) + end + + it "returns false when the argument is greater than RUBY_VERSION" do + expect(VersionGuard.new(@current, '1.8.7').match?).to eq(false) + expect(VersionGuard.new(@current, '1.9.2').match?).to eq(false) + end + + it "returns true when the argument range includes RUBY_VERSION" do + expect(VersionGuard.new(@current, '1.8.5'..'1.8.7').match?).to eq(true) + expect(VersionGuard.new(@current, '1.8'..'1.9').match?).to eq(true) + expect(VersionGuard.new(@current, '1.8'...'1.9').match?).to eq(true) + expect(VersionGuard.new(@current, '1.8'..'1.8.6').match?).to eq(true) + expect(VersionGuard.new(@current, '1.8.5'..'1.8.6').match?).to eq(true) + expect(VersionGuard.new(@current, ''...'1.8.7').match?).to eq(true) + end + + it "returns false when the argument range does not include RUBY_VERSION" do + expect(VersionGuard.new(@current, '1.8.7'..'1.8.9').match?).to eq(false) + expect(VersionGuard.new(@current, '1.8.4'..'1.8.5').match?).to eq(false) + expect(VersionGuard.new(@current, '1.8.4'...'1.8.6').match?).to eq(false) + expect(VersionGuard.new(@current, '1.8.5'...'1.8.6').match?).to eq(false) + expect(VersionGuard.new(@current, ''...'1.8.6').match?).to eq(false) + end +end + +RSpec.describe Object, "#ruby_version_is" do + before :each do + @guard = VersionGuard.new '1.2.3', 'x.x.x' + allow(VersionGuard).to receive(:new).and_return(@guard) + ScratchPad.clear + end + + it "yields when #match? returns true" do + allow(@guard).to receive(:match?).and_return(true) + ruby_version_is('x.x.x') { ScratchPad.record :yield } + expect(ScratchPad.recorded).to eq(:yield) + end + + it "does not yield when #match? returns false" do + allow(@guard).to receive(:match?).and_return(false) + ruby_version_is('x.x.x') { ScratchPad.record :yield } + expect(ScratchPad.recorded).not_to eq(:yield) + end + + it "returns what #match? returns when no block is given" do + allow(@guard).to receive(:match?).and_return(true) + expect(ruby_version_is('x.x.x')).to eq(true) + allow(@guard).to receive(:match?).and_return(false) + expect(ruby_version_is('x.x.x')).to eq(false) + end + + it "sets the name of the guard to :ruby_version_is" do + ruby_version_is("") { } + expect(@guard.name).to eq(:ruby_version_is) + end + + it "calls #unregister even when an exception is raised in the guard block" do + expect(@guard).to receive(:match?).and_return(true) + expect(@guard).to receive(:unregister) + expect do + ruby_version_is("") { raise Exception } + end.to raise_error(Exception) + end +end + +RSpec.describe Object, "#version_is" do + before :each do + hide_deprecation_warnings + end + + it "returns the expected values" do + expect(version_is('1.2.3', '1.2.2')).to eq(true) + expect(version_is('1.2.3', '1.2.3')).to eq(true) + expect(version_is('1.2.3', '1.2.4')).to eq(false) + + expect(version_is('1.2.3', '1')).to eq(true) + expect(version_is('1.2.3', '1.0')).to eq(true) + expect(version_is('1.2.3', '2')).to eq(false) + expect(version_is('1.2.3', '2.0')).to eq(false) + + expect(version_is('1.2.3', '1.2.2'..'1.2.4')).to eq(true) + expect(version_is('1.2.3', '1.2.2'..'1.2.3')).to eq(true) + expect(version_is('1.2.3', '1.2.2'...'1.2.3')).to eq(false) + expect(version_is('1.2.3', '1.2.3'..'1.2.4')).to eq(true) + end +end diff --git a/spec/mspec/spec/helpers/argf_spec.rb b/spec/mspec/spec/helpers/argf_spec.rb new file mode 100644 index 0000000000..1412d71f84 --- /dev/null +++ b/spec/mspec/spec/helpers/argf_spec.rb @@ -0,0 +1,37 @@ +require 'spec_helper' +require 'mspec/guards' +require 'mspec/helpers' + +RSpec.describe Object, "#argf" do + before :each do + @saved_argv = ARGV.dup + @argv = [__FILE__] + end + + it "sets @argf to an instance of ARGF.class with the given argv" do + argf @argv do + expect(@argf).to be_an_instance_of ARGF.class + expect(@argf.filename).to eq(@argv.first) + end + expect(@argf).to be_nil + end + + it "does not alter ARGV nor ARGF" do + argf @argv do + end + expect(ARGV).to eq(@saved_argv) + expect(ARGF.argv).to eq(@saved_argv) + end + + it "does not close STDIN" do + argf ['-'] do + end + expect(STDIN).not_to be_closed + end + + it "disallows nested calls" do + argf @argv do + expect { argf @argv }.to raise_error + end + end +end diff --git a/spec/mspec/spec/helpers/argv_spec.rb b/spec/mspec/spec/helpers/argv_spec.rb new file mode 100644 index 0000000000..1db7e38650 --- /dev/null +++ b/spec/mspec/spec/helpers/argv_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' +require 'mspec/guards' +require 'mspec/helpers' + +RSpec.describe Object, "#argv" do + before :each do + ScratchPad.clear + + @saved_argv = ARGV.dup + @argv = ["a", "b"] + end + + it "replaces and restores the value of ARGV" do + argv @argv + expect(ARGV).to eq(@argv) + argv :restore + expect(ARGV).to eq(@saved_argv) + end + + it "yields to the block after setting ARGV" do + argv @argv do + ScratchPad.record ARGV.dup + end + expect(ScratchPad.recorded).to eq(@argv) + expect(ARGV).to eq(@saved_argv) + end +end diff --git a/spec/mspec/spec/helpers/datetime_spec.rb b/spec/mspec/spec/helpers/datetime_spec.rb new file mode 100644 index 0000000000..af4f557376 --- /dev/null +++ b/spec/mspec/spec/helpers/datetime_spec.rb @@ -0,0 +1,44 @@ +require 'spec_helper' +require 'mspec/guards' +require 'mspec/helpers' + +RSpec.describe Object, "#new_datetime" do + it "returns a default DateTime instance" do + expect(new_datetime).to eq(DateTime.new) + end + + it "returns a DateTime instance with the specified year value" do + d = new_datetime :year => 1970 + expect(d.year).to eq(1970) + end + + it "returns a DateTime instance with the specified month value" do + d = new_datetime :month => 11 + expect(d.mon).to eq(11) + end + + it "returns a DateTime instance with the specified day value" do + d = new_datetime :day => 23 + expect(d.day).to eq(23) + end + + it "returns a DateTime instance with the specified hour value" do + d = new_datetime :hour => 10 + expect(d.hour).to eq(10) + end + + it "returns a DateTime instance with the specified minute value" do + d = new_datetime :minute => 10 + expect(d.min).to eq(10) + end + + it "returns a DateTime instance with the specified second value" do + d = new_datetime :second => 2 + expect(d.sec).to eq(2) + end + + it "returns a DateTime instance with the specified offset value" do + d = new_datetime :offset => Rational(3,24) + expect(d.offset).to eq(Rational(3,24)) + end +end diff --git a/spec/mspec/spec/helpers/fixture_spec.rb b/spec/mspec/spec/helpers/fixture_spec.rb new file mode 100644 index 0000000000..d8e2ae7be4 --- /dev/null +++ b/spec/mspec/spec/helpers/fixture_spec.rb @@ -0,0 +1,25 @@ +require 'spec_helper' +require 'mspec/guards' +require 'mspec/helpers' + +RSpec.describe Object, "#fixture" do + before :each do + @dir = File.realpath("..", __FILE__) + end + + it "returns the expanded path to a fixture file" do + name = fixture(__FILE__, "subdir", "file.txt") + expect(name).to eq("#{@dir}/fixtures/subdir/file.txt") + end + + it "omits '/shared' if it is the suffix of the directory string" do + name = fixture("#{@dir}/shared/file.rb", "subdir", "file.txt") + expect(name).to eq("#{@dir}/fixtures/subdir/file.txt") + end + + it "does not append '/fixtures' if it is the suffix of the directory string" do + commands_dir = "#{File.dirname(@dir)}/commands" + name = fixture("#{commands_dir}/fixtures/file.rb", "subdir", "file.txt") + expect(name).to eq("#{commands_dir}/fixtures/subdir/file.txt") + end +end diff --git a/spec/mspec/spec/helpers/flunk_spec.rb b/spec/mspec/spec/helpers/flunk_spec.rb new file mode 100644 index 0000000000..b6a1f21c12 --- /dev/null +++ b/spec/mspec/spec/helpers/flunk_spec.rb @@ -0,0 +1,20 @@ +require 'spec_helper' +require 'mspec/expectations/expectations' +require 'mspec/runner/mspec' +require 'mspec/guards' +require 'mspec/helpers' + +RSpec.describe Object, "#flunk" do + before :each do + allow(MSpec).to receive(:actions) + allow(MSpec).to receive(:current).and_return(double("spec state").as_null_object) + end + + it "raises an SpecExpectationNotMetError unconditionally" do + expect { flunk }.to raise_error(SpecExpectationNotMetError) + end + + it "accepts on argument for an optional message" do + expect {flunk "test"}.to raise_error(SpecExpectationNotMetError) + end +end diff --git a/spec/mspec/spec/helpers/fs_spec.rb b/spec/mspec/spec/helpers/fs_spec.rb new file mode 100644 index 0000000000..15bb43903a --- /dev/null +++ b/spec/mspec/spec/helpers/fs_spec.rb @@ -0,0 +1,195 @@ +require 'spec_helper' +require 'mspec/guards' +require 'mspec/helpers' + +RSpec.describe Object, "#cp" do + before :each do + @source = tmp("source.txt") + @copy = tmp("copied.txt") + + @contents = "This is a copy." + File.open(@source, "w") { |f| f.write @contents } + end + + after :each do + File.delete @source if File.exist? @source + File.delete @copy if File.exist? @copy + end + + it "copies a file" do + cp @source, @copy + data = IO.read(@copy) + expect(data).to eq(@contents) + expect(data).to eq(IO.read(@source)) + end +end + +RSpec.describe Object, "#touch" do + before :all do + @name = tmp("touched.txt") + end + + after :each do + File.delete @name if File.exist? @name + end + + it "creates a file" do + touch @name + expect(File.exist?(@name)).to be_truthy + end + + it "accepts an optional mode argument" do + touch @name, "wb" + expect(File.exist?(@name)).to be_truthy + end + + it "overwrites an existing file" do + File.open(@name, "w") { |f| f.puts "used" } + expect(File.size(@name)).to be > 0 + + touch @name + expect(File.size(@name)).to eq(0) + end + + it "yields the open file if passed a block" do + touch(@name) { |f| f.write "touching" } + expect(IO.read(@name)).to eq("touching") + end +end + +RSpec.describe Object, "#touch" do + before :all do + @name = tmp("subdir/touched.txt") + end + + after :each do + rm_r File.dirname(@name) + end + + it "creates all the directories in the path to the file" do + touch @name + expect(File.exist?(@name)).to be_truthy + end +end + +RSpec.describe Object, "#mkdir_p" do + before :all do + @dir1 = tmp("/nested") + @dir2 = @dir1 + "/directory" + @paths = [ @dir2, @dir1 ] + end + + after :each do + File.delete @dir1 if File.file? @dir1 + @paths.each { |path| Dir.rmdir path if File.directory? path } + end + + it "creates all the directories in a path" do + mkdir_p @dir2 + expect(File.directory?(@dir2)).to be_truthy + end + + it "raises an ArgumentError if a path component is a file" do + File.open(@dir1, "w") { |f| } + expect { mkdir_p @dir2 }.to raise_error(ArgumentError) + end + + it "works if multiple processes try to create the same directory concurrently" do + original = File.method(:directory?) + expect(File).to receive(:directory?).at_least(:once) { |dir| + ret = original.call(dir) + if !ret and dir == @dir1 + Dir.mkdir(dir) # Simulate race + end + ret + } + mkdir_p @dir1 + expect(original.call(@dir1)).to be_truthy + end +end + +RSpec.describe Object, "#rm_r" do + before :all do + @topdir = tmp("rm_r_tree") + @topfile = @topdir + "/file.txt" + @link = @topdir + "/file.lnk" + @socket = @topdir + "/socket.sck" + @subdir1 = @topdir + "/subdir1" + @subdir2 = @subdir1 + "/subdir2" + @subfile = @subdir1 + "/subfile.txt" + end + + before :each do + mkdir_p @subdir2 + touch @topfile + touch @subfile + end + + after :each do + File.delete @link if File.exist? @link or File.symlink? @link + File.delete @socket if File.exist? @socket + File.delete @subfile if File.exist? @subfile + File.delete @topfile if File.exist? @topfile + + Dir.rmdir @subdir2 if File.directory? @subdir2 + Dir.rmdir @subdir1 if File.directory? @subdir1 + Dir.rmdir @topdir if File.directory? @topdir + end + + it "raises an ArgumentError if the path is not prefixed by MSPEC_RM_PREFIX" do + expect { rm_r "some_file.txt" }.to raise_error(ArgumentError) + end + + it "removes a single file" do + rm_r @subfile + expect(File.exist?(@subfile)).to be_falsey + end + + it "removes multiple files" do + rm_r @topfile, @subfile + expect(File.exist?(@topfile)).to be_falsey + expect(File.exist?(@subfile)).to be_falsey + end + + platform_is_not :windows do + it "removes a symlink to a file" do + File.symlink @topfile, @link + rm_r @link + expect(File.exist?(@link)).to be_falsey + end + + it "removes a symlink to a directory" do + File.symlink @subdir1, @link + rm_r @link + expect do + File.lstat(@link) + end.to raise_error(Errno::ENOENT) + expect(File.exist?(@subdir1)).to be_truthy + end + + it "removes a dangling symlink" do + File.symlink "non_existent_file", @link + rm_r @link + expect do + File.lstat(@link) + end.to raise_error(Errno::ENOENT) + end + + it "removes a socket" do + require 'socket' + UNIXServer.new(@socket).close + rm_r @socket + expect(File.exist?(@socket)).to be_falsey + end + end + + it "removes a single directory" do + rm_r @subdir2 + expect(File.directory?(@subdir2)).to be_falsey + end + + it "recursively removes a directory tree" do + rm_r @topdir + expect(File.directory?(@topdir)).to be_falsey + end +end diff --git a/spec/mspec/spec/helpers/io_spec.rb b/spec/mspec/spec/helpers/io_spec.rb new file mode 100644 index 0000000000..14c1a2d6b5 --- /dev/null +++ b/spec/mspec/spec/helpers/io_spec.rb @@ -0,0 +1,136 @@ +require 'spec_helper' +require 'mspec/guards' +require 'mspec/helpers' + +RSpec.describe IOStub do + before :each do + @out = IOStub.new + @sep = $\ + end + + after :each do + $\ = @sep + end + + it "provides a write method" do + @out.write "this" + expect(@out).to eq("this") + end + + it "concatenates the arguments sent to write" do + @out.write "flim ", "flam" + expect(@out).to eq("flim flam") + end + + it "provides a print method that appends the default separator" do + $\ = " [newline] " + @out.print "hello" + @out.print "world" + expect(@out).to eq("hello [newline] world [newline] ") + end + + it "provides a puts method that appends the default separator" do + @out.puts "hello", 1, 2, 3 + expect(@out).to eq("hello\n1\n2\n3\n") + end + + it "provides a puts method that appends separator if argument not given" do + @out.puts + expect(@out).to eq("\n") + end + + it "provides a printf method" do + @out.printf "%-10s, %03d, %2.1f", "test", 42, 4.2 + expect(@out).to eq("test , 042, 4.2") + end + + it "provides a flush method that does nothing and returns self" do + expect(@out.flush).to eq(@out) + end +end + +RSpec.describe Object, "#new_fd" do + before :each do + @name = tmp("io_specs") + @io = nil + end + + after :each do + @io.close if @io and not @io.closed? + rm_r @name + end + + it "returns an Integer that can be used to create an IO instance" do + fd = new_fd @name + expect(fd).to be_kind_of(Integer) + + @io = IO.new fd, 'w:utf-8' + @io.sync = true + @io.print "io data" + + expect(IO.read(@name)).to eq("io data") + end + + it "accepts an options Hash" do + allow(FeatureGuard).to receive(:enabled?).and_return(true) + fd = new_fd @name, { :mode => 'w:utf-8' } + expect(fd).to be_kind_of(Integer) + + @io = IO.new fd, 'w:utf-8' + @io.sync = true + @io.print "io data" + + expect(IO.read(@name)).to eq("io data") + end + + it "raises an ArgumentError if the options Hash does not include :mode" do + allow(FeatureGuard).to receive(:enabled?).and_return(true) + expect { new_fd @name, { :encoding => "utf-8" } }.to raise_error(ArgumentError) + end +end + +RSpec.describe Object, "#new_io" do + before :each do + @name = tmp("io_specs.txt") + end + + after :each do + @io.close if @io and !@io.closed? + rm_r @name + end + + it "returns a File instance" do + @io = new_io @name + expect(@io).to be_an_instance_of(File) + end + + it "opens the IO for reading if passed 'r'" do + touch(@name) { |f| f.print "io data" } + @io = new_io @name, "r" + expect(@io.read).to eq("io data") + expect { @io.puts "more data" }.to raise_error(IOError) + end + + it "opens the IO for writing if passed 'w'" do + @io = new_io @name, "w" + @io.sync = true + + @io.print "io data" + expect(IO.read(@name)).to eq("io data") + end + + it "opens the IO for reading if passed { :mode => 'r' }" do + touch(@name) { |f| f.print "io data" } + @io = new_io @name, { :mode => "r" } + expect(@io.read).to eq("io data") + expect { @io.puts "more data" }.to raise_error(IOError) + end + + it "opens the IO for writing if passed { :mode => 'w' }" do + @io = new_io @name, { :mode => "w" } + @io.sync = true + + @io.print "io data" + expect(IO.read(@name)).to eq("io data") + end +end diff --git a/spec/mspec/spec/helpers/mock_to_path_spec.rb b/spec/mspec/spec/helpers/mock_to_path_spec.rb new file mode 100644 index 0000000000..c2ce985190 --- /dev/null +++ b/spec/mspec/spec/helpers/mock_to_path_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' +require 'mspec/guards' +require 'mspec/helpers' +require 'mspec/mocks' + +RSpec.describe Object, "#mock_to_path" do + before :each do + state = double("run state").as_null_object + expect(MSpec).to receive(:current).and_return(state) + end + + it "returns an object that responds to #to_path" do + obj = mock_to_path("foo") + expect(obj).to be_a(MockObject) + expect(obj).to respond_to(:to_path) + obj.to_path + end + + it "returns the provided path when #to_path is called" do + obj = mock_to_path("/tmp/foo") + expect(obj.to_path).to eq("/tmp/foo") + end +end diff --git a/spec/mspec/spec/helpers/numeric_spec.rb b/spec/mspec/spec/helpers/numeric_spec.rb new file mode 100644 index 0000000000..64495b7276 --- /dev/null +++ b/spec/mspec/spec/helpers/numeric_spec.rb @@ -0,0 +1,31 @@ +require 'spec_helper' +require 'mspec/guards' +require 'mspec/helpers' + +RSpec.describe Object, "#bignum_value" do + it "returns a value that is an instance of Bignum on any platform" do + expect(bignum_value).to be > fixnum_max + end + + it "returns the default value incremented by the argument" do + expect(bignum_value(42)).to eq(bignum_value + 42) + end +end + +RSpec.describe Object, "-bignum_value" do + it "returns a value that is an instance of Bignum on any platform" do + expect(-bignum_value).to be < fixnum_min + end +end + +RSpec.describe Object, "#nan_value" do + it "returns NaN" do + expect(nan_value.nan?).to be_truthy + end +end + +RSpec.describe Object, "#infinity_value" do + it "returns Infinity" do + expect(infinity_value.infinite?).to eq(1) + end +end diff --git a/spec/mspec/spec/helpers/ruby_exe_spec.rb b/spec/mspec/spec/helpers/ruby_exe_spec.rb new file mode 100644 index 0000000000..56bade1ba9 --- /dev/null +++ b/spec/mspec/spec/helpers/ruby_exe_spec.rb @@ -0,0 +1,256 @@ +require 'spec_helper' +require 'mspec/guards' +require 'mspec/helpers' +require 'rbconfig' + +class RubyExeSpecs + public :ruby_exe_options + public :resolve_ruby_exe + public :ruby_cmd + public :ruby_exe +end + +RSpec.describe "#ruby_exe_options" do + before :each do + @ruby_exe_env = ENV['RUBY_EXE'] + @script = RubyExeSpecs.new + end + + after :each do + ENV['RUBY_EXE'] = @ruby_exe_env + end + + it "returns ENV['RUBY_EXE'] when passed :env" do + ENV['RUBY_EXE'] = "kowabunga" + expect(@script.ruby_exe_options(:env)).to eq("kowabunga") + end + + it "returns 'bin/jruby' when passed :engine and RUBY_ENGINE is 'jruby'" do + stub_const "RUBY_ENGINE", 'jruby' + expect(@script.ruby_exe_options(:engine)).to eq('bin/jruby') + end + + it "returns 'bin/rbx' when passed :engine, RUBY_ENGINE is 'rbx'" do + stub_const "RUBY_ENGINE", 'rbx' + expect(@script.ruby_exe_options(:engine)).to eq('bin/rbx') + end + + it "returns 'ir' when passed :engine and RUBY_ENGINE is 'ironruby'" do + stub_const "RUBY_ENGINE", 'ironruby' + expect(@script.ruby_exe_options(:engine)).to eq('ir') + end + + it "returns 'maglev-ruby' when passed :engine and RUBY_ENGINE is 'maglev'" do + stub_const "RUBY_ENGINE", 'maglev' + expect(@script.ruby_exe_options(:engine)).to eq('maglev-ruby') + end + + it "returns 'topaz' when passed :engine and RUBY_ENGINE is 'topaz'" do + stub_const "RUBY_ENGINE", 'topaz' + expect(@script.ruby_exe_options(:engine)).to eq('topaz') + end + + it "returns RUBY_ENGINE + $(EXEEXT) when passed :name" do + bin = RUBY_ENGINE + (RbConfig::CONFIG['EXEEXT'] || RbConfig::CONFIG['exeext'] || '') + name = File.join ".", bin + expect(@script.ruby_exe_options(:name)).to eq(name) + end + + it "returns $(bindir)/$(RUBY_INSTALL_NAME) + $(EXEEXT) when passed :install_name" do + bin = RbConfig::CONFIG['RUBY_INSTALL_NAME'] + (RbConfig::CONFIG['EXEEXT'] || RbConfig::CONFIG['exeext'] || '') + name = File.join RbConfig::CONFIG['bindir'], bin + expect(@script.ruby_exe_options(:install_name)).to eq(name) + end +end + +RSpec.describe "#resolve_ruby_exe" do + before :each do + @name = "ruby_spec_exe" + @script = RubyExeSpecs.new + end + + it "returns the value returned by #ruby_exe_options if it exists and is executable" do + expect(@script).to receive(:ruby_exe_options).and_return(@name) + expect(File).to receive(:file?).with(@name).and_return(true) + expect(File).to receive(:executable?).with(@name).and_return(true) + expect(File).to receive(:expand_path).with(@name).and_return(@name) + expect(@script.resolve_ruby_exe).to eq(@name) + end + + it "expands the path portion of the result of #ruby_exe_options" do + expect(@script).to receive(:ruby_exe_options).and_return("#{@name}") + expect(File).to receive(:file?).with(@name).and_return(true) + expect(File).to receive(:executable?).with(@name).and_return(true) + expect(File).to receive(:expand_path).with(@name).and_return("/usr/bin/#{@name}") + expect(@script.resolve_ruby_exe).to eq("/usr/bin/#{@name}") + end + + it "adds the flags after the executable" do + @name = 'bin/rbx' + expect(@script).to receive(:ruby_exe_options).and_return(@name) + expect(File).to receive(:file?).with(@name).and_return(true) + expect(File).to receive(:executable?).with(@name).and_return(true) + expect(File).to receive(:expand_path).with(@name).and_return(@name) + + expect(ENV).to receive(:[]).with("RUBY_FLAGS").and_return('-X19') + expect(@script.resolve_ruby_exe).to eq('bin/rbx -X19') + end + + it "raises an exception if no exe is found" do + expect(File).to receive(:file?).at_least(:once).and_return(false) + expect { + @script.resolve_ruby_exe + }.to raise_error(Exception) + end +end + +RSpec.describe Object, "#ruby_cmd" do + before :each do + stub_const 'RUBY_EXE', 'ruby_spec_exe -w -Q' + + @file = "some/ruby/file.rb" + @code = %(some "real" 'ruby' code) + + @script = RubyExeSpecs.new + end + + it "returns a command that runs the given file if it is a file that exists" do + expect(File).to receive(:exist?).with(@file).and_return(true) + expect(@script.ruby_cmd(@file)).to eq("ruby_spec_exe -w -Q some/ruby/file.rb") + end + + it "includes the given options and arguments with a file" do + expect(File).to receive(:exist?).with(@file).and_return(true) + expect(@script.ruby_cmd(@file, :options => "-w -Cdir", :args => "< file.txt")).to eq( + "ruby_spec_exe -w -Q -w -Cdir some/ruby/file.rb < file.txt" + ) + end + + it "includes the given options and arguments with -e" do + expect(File).to receive(:exist?).with(@code).and_return(false) + expect(@script.ruby_cmd(@code, :options => "-W0 -Cdir", :args => "< file.txt")).to eq( + %(ruby_spec_exe -w -Q -W0 -Cdir -e "some \\"real\\" 'ruby' code" < file.txt) + ) + end + + it "returns a command with options and arguments but without code or file" do + expect(@script.ruby_cmd(nil, :options => "-c", :args => "> file.txt")).to eq( + "ruby_spec_exe -w -Q -c > file.txt" + ) + end +end + +RSpec.describe Object, "#ruby_exe" do + before :each do + stub_const 'RUBY_EXE', 'ruby_spec_exe -w -Q' + + @script = RubyExeSpecs.new + allow(IO).to receive(:popen).and_return('OUTPUT') + + status_successful = double(Process::Status, exited?: true, exitstatus: 0) + allow(Process).to receive(:last_status).and_return(status_successful) + end + + it "returns command STDOUT when given command" do + code = "code" + options = {} + output = "output" + expect(IO).to receive(:popen).and_return(output) + + expect(@script.ruby_exe(code, options)).to eq output + end + + it "returns an Array containing the interpreter executable and flags when given no arguments" do + expect(@script.ruby_exe).to eq(['ruby_spec_exe', '-w', '-Q']) + end + + it "executes (using `) the result of calling #ruby_cmd with the given arguments" do + code = "code" + options = {} + expect(@script).to receive(:ruby_cmd).and_return("ruby_cmd") + expect(IO).to receive(:popen).with("ruby_cmd") + @script.ruby_exe(code, options) + end + + it "raises exception when command exit status is not successful" do + code = "code" + options = {} + + status_failed = double(Process::Status, exited?: true, exitstatus: 4) + allow(Process).to receive(:last_status).and_return(status_failed) + + expect { + @script.ruby_exe(code, options) + }.to raise_error(%r{Expected exit status is 0 but actual is 4 for command ruby_exe\(.+\)}) + end + + it "shows in the exception message if a signal killed the process" do + code = "code" + options = {} + + status_failed = double(Process::Status, exited?: false, signaled?: true, termsig: Signal.list.fetch('TERM')) + allow(Process).to receive(:last_status).and_return(status_failed) + + expect { + @script.ruby_exe(code, options) + }.to raise_error(%r{Expected exit status is 0 but actual is :SIGTERM for command ruby_exe\(.+\)}) + end + + describe "with :dir option" do + it "is deprecated" do + expect { + @script.ruby_exe nil, :dir => "tmp" + }.to raise_error(/no longer supported, use Dir\.chdir/) + end + end + + describe "with :env option" do + it "preserves the values of existing ENV keys" do + ENV["ABC"] = "123" + allow(ENV).to receive(:[]) + expect(ENV).to receive(:[]).with("ABC") + @script.ruby_exe nil, :env => { :ABC => "xyz" } + end + + it "adds the :env entries to ENV" do + expect(ENV).to receive(:[]=).with("ABC", "xyz") + @script.ruby_exe nil, :env => { :ABC => "xyz" } + end + + it "deletes the :env entries in ENV when an exception is raised" do + expect(ENV).to receive(:delete).with("XYZ") + @script.ruby_exe nil, :env => { :XYZ => "xyz" } + end + + it "resets the values of existing ENV keys when an exception is raised" do + ENV["ABC"] = "123" + expect(ENV).to receive(:[]=).with("ABC", "xyz") + expect(ENV).to receive(:[]=).with("ABC", "123") + + expect(IO).to receive(:popen).and_raise(Exception) + expect do + @script.ruby_exe nil, :env => { :ABC => "xyz" } + end.to raise_error(Exception) + end + end + + describe "with :exit_status option" do + before do + status_failed = double(Process::Status, exited?: true, exitstatus: 4) + allow(Process).to receive(:last_status).and_return(status_failed) + end + + it "raises exception when command ends with not expected status" do + expect { + @script.ruby_exe("path", exit_status: 1) + }.to raise_error(%r{Expected exit status is 1 but actual is 4 for command ruby_exe\(.+\)}) + end + + it "does not raise exception when command ends with expected status" do + output = "output" + expect(IO).to receive(:popen).and_return(output) + + expect(@script.ruby_exe("path", exit_status: 4)).to eq output + end + end +end diff --git a/spec/mspec/spec/helpers/scratch_spec.rb b/spec/mspec/spec/helpers/scratch_spec.rb new file mode 100644 index 0000000000..9dbef94aa3 --- /dev/null +++ b/spec/mspec/spec/helpers/scratch_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' +require 'mspec/guards' +require 'mspec/helpers' + +RSpec.describe ScratchPad do + it "records an object and returns a previously recorded object" do + ScratchPad.record :this + expect(ScratchPad.recorded).to eq(:this) + end + + it "clears the recorded object" do + ScratchPad.record :that + expect(ScratchPad.recorded).to eq(:that) + ScratchPad.clear + expect(ScratchPad.recorded).to eq(nil) + end + + it "provides a convenience shortcut to append to a previously recorded object" do + ScratchPad.record [] + ScratchPad << :new + ScratchPad << :another + expect(ScratchPad.recorded).to eq([:new, :another]) + end +end diff --git a/spec/mspec/spec/helpers/suppress_warning_spec.rb b/spec/mspec/spec/helpers/suppress_warning_spec.rb new file mode 100644 index 0000000000..4cae189bd3 --- /dev/null +++ b/spec/mspec/spec/helpers/suppress_warning_spec.rb @@ -0,0 +1,19 @@ +require 'spec_helper' +require 'mspec/guards' +require 'mspec/helpers' + +RSpec.describe Object, "#suppress_warning" do + it "hides warnings" do + suppress_warning do + warn "should not be shown" + end + end + + it "yields the block" do + a = 0 + suppress_warning do + a = 1 + end + expect(a).to eq(1) + end +end diff --git a/spec/mspec/spec/helpers/tmp_spec.rb b/spec/mspec/spec/helpers/tmp_spec.rb new file mode 100644 index 0000000000..c41dcd57b3 --- /dev/null +++ b/spec/mspec/spec/helpers/tmp_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' +require 'mspec/guards' +require 'mspec/helpers' + +RSpec.describe Object, "#tmp" do + before :all do + @dir = SPEC_TEMP_DIR + end + + it "returns a name relative to the current working directory" do + expect(tmp("test.txt")).to eq("#{@dir}/#{SPEC_TEMP_UNIQUIFIER}-test.txt") + end + + it "returns a 'unique' name on repeated calls" do + a = tmp("text.txt") + b = tmp("text.txt") + expect(a).not_to eq(b) + end + + it "does not 'uniquify' the name if requested not to" do + expect(tmp("test.txt", false)).to eq("#{@dir}/test.txt") + end + + it "returns the name of the temporary directory when passed an empty string" do + expect(tmp("")).to eq("#{@dir}/") + end +end diff --git a/spec/mspec/spec/integration/interpreter_spec.rb b/spec/mspec/spec/integration/interpreter_spec.rb new file mode 100644 index 0000000000..dbf3987a08 --- /dev/null +++ b/spec/mspec/spec/integration/interpreter_spec.rb @@ -0,0 +1,18 @@ +require 'spec_helper' + +RSpec.describe "The interpreter passed with -t" do + it "is used in subprocess" do + fixtures = "spec/fixtures" + interpreter = "#{fixtures}/my_ruby" + out, ret = run_mspec("run", "#{fixtures}/print_interpreter_spec.rb -t #{interpreter}") + out = out.lines.map(&:chomp).reject { |line| + line == 'RUBY_DESCRIPTION' + }.take(3) + expect(out).to eq([ + interpreter, + interpreter, + "CWD/#{interpreter}" + ]) + expect(ret.success?).to eq(true) + end +end diff --git a/spec/mspec/spec/integration/object_methods_spec.rb b/spec/mspec/spec/integration/object_methods_spec.rb new file mode 100644 index 0000000000..697fbd10fa --- /dev/null +++ b/spec/mspec/spec/integration/object_methods_spec.rb @@ -0,0 +1,18 @@ +require 'spec_helper' + +expected_output = <<EOS +RUBY_DESCRIPTION +. + +Finished in D.DDDDDD seconds + +1 file, 1 example, 1 expectation, 0 failures, 0 errors, 0 tagged +EOS + +RSpec.describe "MSpec" do + it "does not define public methods on Object" do + out, ret = run_mspec("run", "spec/fixtures/object_methods_spec.rb") + expect(out).to eq(expected_output) + expect(ret.success?).to eq(true) + end +end diff --git a/spec/mspec/spec/integration/run_spec.rb b/spec/mspec/spec/integration/run_spec.rb new file mode 100644 index 0000000000..ea0735e9b2 --- /dev/null +++ b/spec/mspec/spec/integration/run_spec.rb @@ -0,0 +1,72 @@ +require 'spec_helper' + +RSpec.describe "Running mspec" do + q = BACKTRACE_QUOTE + a_spec_output = <<EOS + +1) +Foo#bar errors FAILED +Expected 1 == 2 +to be truthy but was false +CWD/spec/fixtures/a_spec.rb:8:in #{q}block (2 levels) in <top (required)>' +CWD/spec/fixtures/a_spec.rb:2:in #{q}<top (required)>' + +2) +Foo#bar fails ERROR +RuntimeError: failure +CWD/spec/fixtures/a_spec.rb:12:in #{q}block (2 levels) in <top (required)>' +CWD/spec/fixtures/a_spec.rb:2:in #{q}<top (required)>' + +Finished in D.DDDDDD seconds +EOS + + a_stats = "1 file, 3 examples, 2 expectations, 1 failure, 1 error, 0 tagged\n" + ab_stats = "2 files, 4 examples, 3 expectations, 1 failure, 1 error, 0 tagged\n" + fixtures = "spec/fixtures" + + it "runs the specs" do + out, ret = run_mspec("run", "#{fixtures}/a_spec.rb") + expect(out).to eq("RUBY_DESCRIPTION\n.FE\n#{a_spec_output}\n#{a_stats}") + expect(ret.success?).to eq(false) + end + + it "directly with mspec-run runs the specs" do + out, ret = run_mspec("-run", "#{fixtures}/a_spec.rb") + expect(out).to eq("RUBY_DESCRIPTION\n.FE\n#{a_spec_output}\n#{a_stats}") + expect(ret.success?).to eq(false) + end + + it "runs the specs in parallel with -j using the dotted formatter" do + out, ret = run_mspec("run", "-j #{fixtures}/a_spec.rb #{fixtures}/b_spec.rb") + expect(out).to eq("RUBY_DESCRIPTION\n...\n#{a_spec_output}\n#{ab_stats}") + expect(ret.success?).to eq(false) + end + + it "runs the specs in parallel with -j -fa" do + out, ret = run_mspec("run", "-j -fa #{fixtures}/a_spec.rb #{fixtures}/b_spec.rb") + progress_bar = + "\r[/ | 0% | 00:00:00] \e[0;32m 0F \e[0;32m 0E\e[0m " + + "\r[- | ==================50% | 00:00:00] \e[0;32m 0F \e[0;32m 0E\e[0m " + + "\r[\\ | ==================100%================== | 00:00:00] \e[0;32m 0F \e[0;32m 0E\e[0m " + expect(out).to eq("RUBY_DESCRIPTION\n#{progress_bar}\n#{a_spec_output}\n#{ab_stats}") + expect(ret.success?).to eq(false) + end + + it "gives a useful error message when a subprocess dies in parallel mode" do + out, ret = run_mspec("run", "-j #{fixtures}/b_spec.rb #{fixtures}/die_spec.rb") + lines = out.lines + expect(lines).to include "A child mspec-run process died unexpectedly while running CWD/spec/fixtures/die_spec.rb\n" + expect(lines).to include "Finished in D.DDDDDD seconds\n" + expect(lines.last).to match(/^\d files?, \d examples?, \d expectations?, 0 failures, 0 errors, 0 tagged$/) + expect(ret.success?).to eq(false) + end + + it "gives a useful error message when a subprocess prints unexpected output on STDOUT in parallel mode" do + out, ret = run_mspec("run", "-j #{fixtures}/b_spec.rb #{fixtures}/chatty_spec.rb") + lines = out.lines + expect(lines).to include "A child mspec-run process printed unexpected output on STDOUT: #{'"Hello\nIt\'s me!\n"'} while running CWD/spec/fixtures/chatty_spec.rb\n" + expect(lines).to include "Finished in D.DDDDDD seconds\n" + expect(lines.last).to eq("2 files, 2 examples, 2 expectations, 0 failures, 0 errors, 0 tagged\n") + expect(ret.success?).to eq(false) + end +end diff --git a/spec/mspec/spec/integration/tag_spec.rb b/spec/mspec/spec/integration/tag_spec.rb new file mode 100644 index 0000000000..ae08e9d45f --- /dev/null +++ b/spec/mspec/spec/integration/tag_spec.rb @@ -0,0 +1,60 @@ +# encoding: utf-8 +require 'spec_helper' + +RSpec.describe "Running mspec tag" do + before :all do + FileUtils.rm_rf 'spec/fixtures/tags' + end + + after :all do + FileUtils.rm_rf 'spec/fixtures/tags' + end + + it "tags the failing specs" do + fixtures = "spec/fixtures" + out, ret = run_mspec("tag", "--add fails --fail #{fixtures}/tagging_spec.rb") + q = BACKTRACE_QUOTE + expect(out).to eq <<EOS +RUBY_DESCRIPTION +.FF +TagAction: specs tagged with 'fails': + +Tag#me errors +Tag#me érròrs in unicode + + +1) +Tag#me errors FAILED +Expected 1 == 2 +to be truthy but was false +CWD/spec/fixtures/tagging_spec.rb:9:in #{q}block (2 levels) in <top (required)>' +CWD/spec/fixtures/tagging_spec.rb:3:in #{q}<top (required)>' + +2) +Tag#me érròrs in unicode FAILED +Expected 1 == 2 +to be truthy but was false +CWD/spec/fixtures/tagging_spec.rb:13:in #{q}block (2 levels) in <top (required)>' +CWD/spec/fixtures/tagging_spec.rb:3:in #{q}<top (required)>' + +Finished in D.DDDDDD seconds + +1 file, 3 examples, 3 expectations, 2 failures, 0 errors, 0 tagged +EOS + expect(ret.success?).to eq(false) + end + + it "does not run already tagged specs" do + fixtures = "spec/fixtures" + out, ret = run_mspec("run", "--excl-tag fails #{fixtures}/tagging_spec.rb") + expect(out).to eq <<EOS +RUBY_DESCRIPTION +. + +Finished in D.DDDDDD seconds + +1 file, 3 examples, 1 expectation, 0 failures, 0 errors, 2 tagged +EOS + expect(ret.success?).to eq(true) + end +end diff --git a/spec/mspec/spec/matchers/base_spec.rb b/spec/mspec/spec/matchers/base_spec.rb new file mode 100644 index 0000000000..6b5a3fbd72 --- /dev/null +++ b/spec/mspec/spec/matchers/base_spec.rb @@ -0,0 +1,228 @@ +require 'spec_helper' +require 'mspec/expectations/expectations' +require 'mspec/matchers' +require 'time' + +RSpec.describe SpecPositiveOperatorMatcher, "== operator" do + it "provides a failure message that 'Expected x to equal y'" do + expect { + SpecPositiveOperatorMatcher.new(1) == 2 + }.to raise_error(SpecExpectationNotMetError, "Expected 1 == 2\nto be truthy but was false") + end + + it "does not raise an exception when == returns true" do + SpecPositiveOperatorMatcher.new(1) == 1 + end +end + +RSpec.describe SpecPositiveOperatorMatcher, "=~ operator" do + it "provides a failure message that 'Expected \"x\" to match y'" do + expect { + SpecPositiveOperatorMatcher.new('real') =~ /fake/ + }.to raise_error(SpecExpectationNotMetError, "Expected \"real\" =~ /fake/\nto be truthy but was nil") + end + + it "does not raise an exception when =~ returns true" do + SpecPositiveOperatorMatcher.new('real') =~ /real/ + end +end + +RSpec.describe SpecPositiveOperatorMatcher, "> operator" do + it "provides a failure message that 'Expected x to be greater than y'" do + expect { + SpecPositiveOperatorMatcher.new(4) > 5 + }.to raise_error(SpecExpectationNotMetError, "Expected 4 > 5\nto be truthy but was false") + end + + it "does not raise an exception when > returns true" do + SpecPositiveOperatorMatcher.new(5) > 4 + end +end + +RSpec.describe SpecPositiveOperatorMatcher, ">= operator" do + it "provides a failure message that 'Expected x to be greater than or equal to y'" do + expect { + SpecPositiveOperatorMatcher.new(4) >= 5 + }.to raise_error(SpecExpectationNotMetError, "Expected 4 >= 5\nto be truthy but was false") + end + + it "does not raise an exception when > returns true" do + SpecPositiveOperatorMatcher.new(5) >= 4 + SpecPositiveOperatorMatcher.new(5) >= 5 + end +end + +RSpec.describe SpecPositiveOperatorMatcher, "< operator" do + it "provides a failure message that 'Expected x to be less than y'" do + expect { + SpecPositiveOperatorMatcher.new(5) < 4 + }.to raise_error(SpecExpectationNotMetError, "Expected 5 < 4\nto be truthy but was false") + end + + it "does not raise an exception when < returns true" do + SpecPositiveOperatorMatcher.new(4) < 5 + end +end + +RSpec.describe SpecPositiveOperatorMatcher, "<= operator" do + it "provides a failure message that 'Expected x to be less than or equal to y'" do + expect { + SpecPositiveOperatorMatcher.new(5) <= 4 + }.to raise_error(SpecExpectationNotMetError, "Expected 5 <= 4\nto be truthy but was false") + end + + it "does not raise an exception when < returns true" do + SpecPositiveOperatorMatcher.new(4) <= 5 + SpecPositiveOperatorMatcher.new(4) <= 4 + end +end + +RSpec.describe SpecPositiveOperatorMatcher, "arbitrary predicates" do + it "do not raise an exception when the predicate is truthy" do + SpecPositiveOperatorMatcher.new(2).eql?(2) + SpecPositiveOperatorMatcher.new(2).equal?(2) + SpecPositiveOperatorMatcher.new([1, 2, 3]).include?(2) + SpecPositiveOperatorMatcher.new("abc").start_with?("ab") + SpecPositiveOperatorMatcher.new("abc").start_with?("d", "a") + SpecPositiveOperatorMatcher.new(3).odd? + SpecPositiveOperatorMatcher.new([1, 2]).any? { |e| e.even? } + end + + it "provide a failure message when the predicate returns a falsy value" do + expect { + SpecPositiveOperatorMatcher.new(2).eql?(3) + }.to raise_error(SpecExpectationNotMetError, "Expected 2.eql? 3\nto be truthy but was false") + expect { + SpecPositiveOperatorMatcher.new(2).equal?(3) + }.to raise_error(SpecExpectationNotMetError, "Expected 2.equal? 3\nto be truthy but was false") + expect { + SpecPositiveOperatorMatcher.new([1, 2, 3]).include?(4) + }.to raise_error(SpecExpectationNotMetError, "Expected [1, 2, 3].include? 4\nto be truthy but was false") + expect { + SpecPositiveOperatorMatcher.new("abc").start_with?("de") + }.to raise_error(SpecExpectationNotMetError, "Expected \"abc\".start_with? \"de\"\nto be truthy but was false") + expect { + SpecPositiveOperatorMatcher.new("abc").start_with?("d", "e") + }.to raise_error(SpecExpectationNotMetError, "Expected \"abc\".start_with? \"d\", \"e\"\nto be truthy but was false") + expect { + SpecPositiveOperatorMatcher.new(2).odd? + }.to raise_error(SpecExpectationNotMetError, "Expected 2.odd?\nto be truthy but was false") + expect { + SpecPositiveOperatorMatcher.new([1, 3]).any? { |e| e.even? } + }.to raise_error(SpecExpectationNotMetError, "Expected [1, 3].any? { ... }\nto be truthy but was false") + end +end + +RSpec.describe SpecNegativeOperatorMatcher, "arbitrary predicates" do + it "do not raise an exception when the predicate returns a falsy value" do + SpecNegativeOperatorMatcher.new(2).eql?(3) + SpecNegativeOperatorMatcher.new(2).equal?(3) + SpecNegativeOperatorMatcher.new([1, 2, 3]).include?(4) + SpecNegativeOperatorMatcher.new("abc").start_with?("de") + SpecNegativeOperatorMatcher.new("abc").start_with?("d", "e") + SpecNegativeOperatorMatcher.new(2).odd? + SpecNegativeOperatorMatcher.new([1, 3]).any? { |e| e.even? } + end + + it "provide a failure message when the predicate returns a truthy value" do + expect { + SpecNegativeOperatorMatcher.new(2).eql?(2) + }.to raise_error(SpecExpectationNotMetError, "Expected 2.eql? 2\nto be falsy but was true") + expect { + SpecNegativeOperatorMatcher.new(2).equal?(2) + }.to raise_error(SpecExpectationNotMetError, "Expected 2.equal? 2\nto be falsy but was true") + expect { + SpecNegativeOperatorMatcher.new([1, 2, 3]).include?(2) + }.to raise_error(SpecExpectationNotMetError, "Expected [1, 2, 3].include? 2\nto be falsy but was true") + expect { + SpecNegativeOperatorMatcher.new("abc").start_with?("ab") + }.to raise_error(SpecExpectationNotMetError, "Expected \"abc\".start_with? \"ab\"\nto be falsy but was true") + expect { + SpecNegativeOperatorMatcher.new("abc").start_with?("d", "a") + }.to raise_error(SpecExpectationNotMetError, "Expected \"abc\".start_with? \"d\", \"a\"\nto be falsy but was true") + expect { + SpecNegativeOperatorMatcher.new(3).odd? + }.to raise_error(SpecExpectationNotMetError, "Expected 3.odd?\nto be falsy but was true") + expect { + SpecNegativeOperatorMatcher.new([1, 2]).any? { |e| e.even? } + }.to raise_error(SpecExpectationNotMetError, "Expected [1, 2].any? { ... }\nto be falsy but was true") + end +end + +RSpec.describe SpecNegativeOperatorMatcher, "== operator" do + it "provides a failure message that 'Expected x not to equal y'" do + expect { + SpecNegativeOperatorMatcher.new(1) == 1 + }.to raise_error(SpecExpectationNotMetError, "Expected 1 == 1\nto be falsy but was true") + end + + it "does not raise an exception when == returns false" do + SpecNegativeOperatorMatcher.new(1) == 2 + end +end + +RSpec.describe SpecNegativeOperatorMatcher, "=~ operator" do + it "provides a failure message that 'Expected \"x\" not to match /y/'" do + expect { + SpecNegativeOperatorMatcher.new('real') =~ /real/ + }.to raise_error(SpecExpectationNotMetError, "Expected \"real\" =~ /real/\nto be falsy but was 0") + end + + it "does not raise an exception when =~ returns false" do + SpecNegativeOperatorMatcher.new('real') =~ /fake/ + end +end + +RSpec.describe SpecNegativeOperatorMatcher, "< operator" do + it "provides a failure message that 'Expected x not to be less than y'" do + expect { + SpecNegativeOperatorMatcher.new(4) < 5 + }.to raise_error(SpecExpectationNotMetError, "Expected 4 < 5\nto be falsy but was true") + end + + it "does not raise an exception when < returns false" do + SpecNegativeOperatorMatcher.new(5) < 4 + end +end + +RSpec.describe SpecNegativeOperatorMatcher, "<= operator" do + it "provides a failure message that 'Expected x not to be less than or equal to y'" do + expect { + SpecNegativeOperatorMatcher.new(4) <= 5 + }.to raise_error(SpecExpectationNotMetError, "Expected 4 <= 5\nto be falsy but was true") + expect { + SpecNegativeOperatorMatcher.new(5) <= 5 + }.to raise_error(SpecExpectationNotMetError, "Expected 5 <= 5\nto be falsy but was true") + end + + it "does not raise an exception when <= returns false" do + SpecNegativeOperatorMatcher.new(5) <= 4 + end +end + +RSpec.describe SpecNegativeOperatorMatcher, "> operator" do + it "provides a failure message that 'Expected x not to be greater than y'" do + expect { + SpecNegativeOperatorMatcher.new(5) > 4 + }.to raise_error(SpecExpectationNotMetError, "Expected 5 > 4\nto be falsy but was true") + end + + it "does not raise an exception when > returns false" do + SpecNegativeOperatorMatcher.new(4) > 5 + end +end + +RSpec.describe SpecNegativeOperatorMatcher, ">= operator" do + it "provides a failure message that 'Expected x not to be greater than or equal to y'" do + expect { + SpecNegativeOperatorMatcher.new(5) >= 4 + }.to raise_error(SpecExpectationNotMetError, "Expected 5 >= 4\nto be falsy but was true") + expect { + SpecNegativeOperatorMatcher.new(5) >= 5 + }.to raise_error(SpecExpectationNotMetError, "Expected 5 >= 5\nto be falsy but was true") + end + + it "does not raise an exception when >= returns false" do + SpecNegativeOperatorMatcher.new(4) >= 5 + end +end diff --git a/spec/mspec/spec/matchers/be_an_instance_of_spec.rb b/spec/mspec/spec/matchers/be_an_instance_of_spec.rb new file mode 100644 index 0000000000..7c74249d24 --- /dev/null +++ b/spec/mspec/spec/matchers/be_an_instance_of_spec.rb @@ -0,0 +1,50 @@ +require 'spec_helper' +require 'mspec/expectations/expectations' +require 'mspec/matchers' + +module BeAnInOfSpecs + class A + end + + class B < A + end + + class C < B + end +end + +RSpec.describe BeAnInstanceOfMatcher do + it "matches when actual is an instance_of? expected" do + a = BeAnInOfSpecs::A.new + expect(BeAnInstanceOfMatcher.new(BeAnInOfSpecs::A).matches?(a)).to be_truthy + + b = BeAnInOfSpecs::B.new + expect(BeAnInstanceOfMatcher.new(BeAnInOfSpecs::B).matches?(b)).to be_truthy + end + + it "does not match when actual is not an instance_of? expected" do + a = BeAnInOfSpecs::A.new + expect(BeAnInstanceOfMatcher.new(BeAnInOfSpecs::B).matches?(a)).to be_falsey + + b = BeAnInOfSpecs::B.new + expect(BeAnInstanceOfMatcher.new(BeAnInOfSpecs::A).matches?(b)).to be_falsey + + c = BeAnInOfSpecs::C.new + expect(BeAnInstanceOfMatcher.new(BeAnInOfSpecs::A).matches?(c)).to be_falsey + expect(BeAnInstanceOfMatcher.new(BeAnInOfSpecs::B).matches?(c)).to be_falsey + end + + it "provides a useful failure message" do + matcher = BeAnInstanceOfMatcher.new(Numeric) + matcher.matches?("string") + expect(matcher.failure_message).to eq([ + "Expected \"string\" (String)", "to be an instance of Numeric"]) + end + + it "provides a useful negative failure message" do + matcher = BeAnInstanceOfMatcher.new(Numeric) + matcher.matches?(4.0) + expect(matcher.negative_failure_message).to eq([ + "Expected 4.0 (Float)", "not to be an instance of Numeric"]) + end +end diff --git a/spec/mspec/spec/matchers/be_ancestor_of_spec.rb b/spec/mspec/spec/matchers/be_ancestor_of_spec.rb new file mode 100644 index 0000000000..abc05e0f7a --- /dev/null +++ b/spec/mspec/spec/matchers/be_ancestor_of_spec.rb @@ -0,0 +1,28 @@ +require 'spec_helper' +require 'mspec/expectations/expectations' +require 'mspec/matchers' + +class Parent; end +class Child < Parent; end + +RSpec.describe BeAncestorOfMatcher do + it "matches when actual is an ancestor of expected" do + expect(BeAncestorOfMatcher.new(Child).matches?(Parent)).to eq(true) + end + + it "does not match when actual is not an ancestor of expected" do + expect(BeAncestorOfMatcher.new(Parent).matches?(Child)).to eq(false) + end + + it "provides a useful failure message" do + matcher = BeAncestorOfMatcher.new(Parent) + matcher.matches?(Child) + expect(matcher.failure_message).to eq(["Expected Child", "to be an ancestor of Parent"]) + end + + it "provides a useful negative failure message" do + matcher = BeAncestorOfMatcher.new(Child) + matcher.matches?(Parent) + expect(matcher.negative_failure_message).to eq(["Expected Parent", "not to be an ancestor of Child"]) + end +end diff --git a/spec/mspec/spec/matchers/be_close_spec.rb b/spec/mspec/spec/matchers/be_close_spec.rb new file mode 100644 index 0000000000..dfd4f4ddbb --- /dev/null +++ b/spec/mspec/spec/matchers/be_close_spec.rb @@ -0,0 +1,48 @@ +require 'spec_helper' +require 'mspec/expectations/expectations' +require 'mspec/matchers' + +# Adapted from RSpec 1.0.8 +RSpec.describe BeCloseMatcher do + it "matches when actual == expected" do + expect(BeCloseMatcher.new(5.0, 0.5).matches?(5.0)).to eq(true) + end + + it "matches when actual < (expected + tolerance)" do + expect(BeCloseMatcher.new(5.0, 0.5).matches?(5.49)).to eq(true) + end + + it "matches when actual > (expected - tolerance)" do + expect(BeCloseMatcher.new(5.0, 0.5).matches?(4.51)).to eq(true) + end + + it "matches when actual == (expected + tolerance)" do + expect(BeCloseMatcher.new(5.0, 0.5).matches?(5.5)).to eq(true) + expect(BeCloseMatcher.new(3, 2).matches?(5)).to eq(true) + end + + it "matches when actual == (expected - tolerance)" do + expect(BeCloseMatcher.new(5.0, 0.5).matches?(4.5)).to eq(true) + expect(BeCloseMatcher.new(3, 2).matches?(1)).to eq(true) + end + + it "does not match when actual < (expected - tolerance)" do + expect(BeCloseMatcher.new(5.0, 0.5).matches?(4.49)).to eq(false) + end + + it "does not match when actual > (expected + tolerance)" do + expect(BeCloseMatcher.new(5.0, 0.5).matches?(5.51)).to eq(false) + end + + it "provides a useful failure message" do + matcher = BeCloseMatcher.new(5.0, 0.5) + matcher.matches?(6.5) + expect(matcher.failure_message).to eq(["Expected 6.5", "to be within 5.0 +/- 0.5"]) + end + + it "provides a useful negative failure message" do + matcher = BeCloseMatcher.new(5.0, 0.5) + matcher.matches?(4.9) + expect(matcher.negative_failure_message).to eq(["Expected 4.9", "not to be within 5.0 +/- 0.5"]) + end +end diff --git a/spec/mspec/spec/matchers/be_computed_by_spec.rb b/spec/mspec/spec/matchers/be_computed_by_spec.rb new file mode 100644 index 0000000000..f73861a576 --- /dev/null +++ b/spec/mspec/spec/matchers/be_computed_by_spec.rb @@ -0,0 +1,42 @@ +require 'spec_helper' +require 'mspec/matchers' + +RSpec.describe BeComputedByMatcher do + it "matches when all entries in the Array compute" do + array = [ [65, "A"], + [90, "Z"] ] + expect(BeComputedByMatcher.new(:chr).matches?(array)).to be_truthy + end + + it "matches when all entries in the Array with arguments compute" do + array = [ [1, 2, 3], + [2, 4, 6] ] + expect(BeComputedByMatcher.new(:+).matches?(array)).to be_truthy + end + + it "does not match when any entry in the Array does not compute" do + array = [ [65, "A" ], + [91, "Z" ] ] + expect(BeComputedByMatcher.new(:chr).matches?(array)).to be_falsey + end + + it "accepts an argument list to apply to each method call" do + array = [ [65, "1000001" ], + [90, "1011010" ] ] + expect(BeComputedByMatcher.new(:to_s, 2).matches?(array)).to be_truthy + end + + it "does not match when any entry in the Array with arguments does not compute" do + array = [ [1, 2, 3], + [2, 4, 7] ] + expect(BeComputedByMatcher.new(:+).matches?(array)).to be_falsey + end + + it "provides a useful failure message" do + array = [ [65, "A" ], + [91, "Z" ] ] + matcher = BeComputedByMatcher.new(:chr) + matcher.matches?(array) + expect(matcher.failure_message).to eq(["Expected \"Z\"", "to be computed by 91.chr (computed \"[\" instead)"]) + end +end diff --git a/spec/mspec/spec/matchers/be_empty_spec.rb b/spec/mspec/spec/matchers/be_empty_spec.rb new file mode 100644 index 0000000000..30678fe85f --- /dev/null +++ b/spec/mspec/spec/matchers/be_empty_spec.rb @@ -0,0 +1,26 @@ +require 'spec_helper' +require 'mspec/expectations/expectations' +require 'mspec/matchers' + +RSpec.describe BeEmptyMatcher do + it "matches when actual is empty" do + expect(BeEmptyMatcher.new.matches?("")).to eq(true) + end + + it "does not match when actual is not empty" do + expect(BeEmptyMatcher.new.matches?([10])).to eq(false) + end + + it "provides a useful failure message" do + matcher = BeEmptyMatcher.new + matcher.matches?("not empty string") + expect(matcher.failure_message).to eq(["Expected \"not empty string\"", "to be empty"]) + end + + it "provides a useful negative failure message" do + matcher = BeEmptyMatcher.new + matcher.matches?("") + expect(matcher.negative_failure_message).to eq(["Expected \"\"", "not to be empty"]) + end +end + diff --git a/spec/mspec/spec/matchers/be_false_spec.rb b/spec/mspec/spec/matchers/be_false_spec.rb new file mode 100644 index 0000000000..46d7253220 --- /dev/null +++ b/spec/mspec/spec/matchers/be_false_spec.rb @@ -0,0 +1,28 @@ +require 'spec_helper' +require 'mspec/expectations/expectations' +require 'mspec/matchers' + +RSpec.describe BeFalseMatcher do + it "matches when actual is false" do + expect(BeFalseMatcher.new.matches?(false)).to eq(true) + end + + it "does not match when actual is not false" do + expect(BeFalseMatcher.new.matches?("")).to eq(false) + expect(BeFalseMatcher.new.matches?(true)).to eq(false) + expect(BeFalseMatcher.new.matches?(nil)).to eq(false) + expect(BeFalseMatcher.new.matches?(0)).to eq(false) + end + + it "provides a useful failure message" do + matcher = BeFalseMatcher.new + matcher.matches?("some string") + expect(matcher.failure_message).to eq(["Expected \"some string\"", "to be false"]) + end + + it "provides a useful negative failure message" do + matcher = BeFalseMatcher.new + matcher.matches?(false) + expect(matcher.negative_failure_message).to eq(["Expected false", "not to be false"]) + end +end diff --git a/spec/mspec/spec/matchers/be_kind_of_spec.rb b/spec/mspec/spec/matchers/be_kind_of_spec.rb new file mode 100644 index 0000000000..1e19058411 --- /dev/null +++ b/spec/mspec/spec/matchers/be_kind_of_spec.rb @@ -0,0 +1,31 @@ +require 'spec_helper' +require 'mspec/expectations/expectations' +require 'mspec/matchers' + +RSpec.describe BeKindOfMatcher do + it "matches when actual is a kind_of? expected" do + expect(BeKindOfMatcher.new(Numeric).matches?(1)).to eq(true) + expect(BeKindOfMatcher.new(Integer).matches?(2)).to eq(true) + expect(BeKindOfMatcher.new(Regexp).matches?(/m/)).to eq(true) + end + + it "does not match when actual is not a kind_of? expected" do + expect(BeKindOfMatcher.new(Integer).matches?(1.5)).to eq(false) + expect(BeKindOfMatcher.new(String).matches?(:a)).to eq(false) + expect(BeKindOfMatcher.new(Hash).matches?([])).to eq(false) + end + + it "provides a useful failure message" do + matcher = BeKindOfMatcher.new(Numeric) + matcher.matches?('string') + expect(matcher.failure_message).to eq([ + "Expected \"string\" (String)", "to be kind of Numeric"]) + end + + it "provides a useful negative failure message" do + matcher = BeKindOfMatcher.new(Numeric) + matcher.matches?(4.0) + expect(matcher.negative_failure_message).to eq([ + "Expected 4.0 (Float)", "not to be kind of Numeric"]) + end +end diff --git a/spec/mspec/spec/matchers/be_nan_spec.rb b/spec/mspec/spec/matchers/be_nan_spec.rb new file mode 100644 index 0000000000..baa7447943 --- /dev/null +++ b/spec/mspec/spec/matchers/be_nan_spec.rb @@ -0,0 +1,28 @@ +require 'spec_helper' +require 'mspec/expectations/expectations' +require 'mspec/guards' +require 'mspec/helpers' +require 'mspec/matchers' + +RSpec.describe BeNaNMatcher do + it "matches when actual is NaN" do + expect(BeNaNMatcher.new.matches?(nan_value)).to eq(true) + end + + it "does not match when actual is not NaN" do + expect(BeNaNMatcher.new.matches?(1.0)).to eq(false) + expect(BeNaNMatcher.new.matches?(0)).to eq(false) + end + + it "provides a useful failure message" do + matcher = BeNaNMatcher.new + matcher.matches?(0) + expect(matcher.failure_message).to eq(["Expected 0", "to be NaN"]) + end + + it "provides a useful negative failure message" do + matcher = BeNaNMatcher.new + matcher.matches?(nan_value) + expect(matcher.negative_failure_message).to eq(["Expected NaN", "not to be NaN"]) + end +end diff --git a/spec/mspec/spec/matchers/be_nil_spec.rb b/spec/mspec/spec/matchers/be_nil_spec.rb new file mode 100644 index 0000000000..e2768acf83 --- /dev/null +++ b/spec/mspec/spec/matchers/be_nil_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' +require 'mspec/expectations/expectations' +require 'mspec/matchers' + +RSpec.describe BeNilMatcher do + it "matches when actual is nil" do + expect(BeNilMatcher.new.matches?(nil)).to eq(true) + end + + it "does not match when actual is not nil" do + expect(BeNilMatcher.new.matches?("")).to eq(false) + expect(BeNilMatcher.new.matches?(false)).to eq(false) + expect(BeNilMatcher.new.matches?(0)).to eq(false) + end + + it "provides a useful failure message" do + matcher = BeNilMatcher.new + matcher.matches?("some string") + expect(matcher.failure_message).to eq(["Expected \"some string\"", "to be nil"]) + end + + it "provides a useful negative failure message" do + matcher = BeNilMatcher.new + matcher.matches?(nil) + expect(matcher.negative_failure_message).to eq(["Expected nil", "not to be nil"]) + end +end diff --git a/spec/mspec/spec/matchers/be_true_or_false_spec.rb b/spec/mspec/spec/matchers/be_true_or_false_spec.rb new file mode 100644 index 0000000000..e4b456eafc --- /dev/null +++ b/spec/mspec/spec/matchers/be_true_or_false_spec.rb @@ -0,0 +1,19 @@ +require 'spec_helper' +require 'mspec/expectations/expectations' +require 'mspec/matchers' + +RSpec.describe BeTrueOrFalseMatcher do + it "matches when actual is true" do + expect(BeTrueOrFalseMatcher.new.matches?(true)).to eq(true) + end + + it "matches when actual is false" do + expect(BeTrueOrFalseMatcher.new.matches?(false)).to eq(true) + end + + it "provides a useful failure message" do + matcher = BeTrueOrFalseMatcher.new + matcher.matches?("some string") + expect(matcher.failure_message).to eq(["Expected \"some string\"", "to be true or false"]) + end +end diff --git a/spec/mspec/spec/matchers/be_true_spec.rb b/spec/mspec/spec/matchers/be_true_spec.rb new file mode 100644 index 0000000000..39ef05a0f8 --- /dev/null +++ b/spec/mspec/spec/matchers/be_true_spec.rb @@ -0,0 +1,28 @@ +require 'spec_helper' +require 'mspec/expectations/expectations' +require 'mspec/matchers' + +RSpec.describe BeTrueMatcher do + it "matches when actual is true" do + expect(BeTrueMatcher.new.matches?(true)).to eq(true) + end + + it "does not match when actual is not true" do + expect(BeTrueMatcher.new.matches?("")).to eq(false) + expect(BeTrueMatcher.new.matches?(false)).to eq(false) + expect(BeTrueMatcher.new.matches?(nil)).to eq(false) + expect(BeTrueMatcher.new.matches?(0)).to eq(false) + end + + it "provides a useful failure message" do + matcher = BeTrueMatcher.new + matcher.matches?("some string") + expect(matcher.failure_message).to eq(["Expected \"some string\"", "to be true"]) + end + + it "provides a useful negative failure message" do + matcher = BeTrueMatcher.new + matcher.matches?(true) + expect(matcher.negative_failure_message).to eq(["Expected true", "not to be true"]) + end +end diff --git a/spec/mspec/spec/matchers/block_caller_spec.rb b/spec/mspec/spec/matchers/block_caller_spec.rb new file mode 100644 index 0000000000..5d7085fa63 --- /dev/null +++ b/spec/mspec/spec/matchers/block_caller_spec.rb @@ -0,0 +1,13 @@ +require 'spec_helper' +require 'mspec/expectations/expectations' +require 'mspec/matchers' + +RSpec.describe BlockingMatcher do + it 'matches when a Proc blocks the caller' do + expect(BlockingMatcher.new.matches?(proc { sleep })).to eq(true) + end + + it 'does not match when a Proc does not block the caller' do + expect(BlockingMatcher.new.matches?(proc { 1 })).to eq(false) + end +end diff --git a/spec/mspec/spec/matchers/complain_spec.rb b/spec/mspec/spec/matchers/complain_spec.rb new file mode 100644 index 0000000000..399ef3105b --- /dev/null +++ b/spec/mspec/spec/matchers/complain_spec.rb @@ -0,0 +1,102 @@ +require 'spec_helper' +require 'mspec/expectations/expectations' +require 'mspec/matchers' + +RSpec.describe ComplainMatcher do + it "matches when executing the proc results in output to $stderr" do + proc = lambda { warn "I'm gonna tell yo mama" } + expect(ComplainMatcher.new(nil).matches?(proc)).to eq(true) + end + + it "matches when executing the proc results in the expected output to $stderr" do + proc = lambda { warn "Que haces?" } + expect(ComplainMatcher.new("Que haces?\n").matches?(proc)).to eq(true) + expect(ComplainMatcher.new("Que pasa?\n").matches?(proc)).to eq(false) + expect(ComplainMatcher.new(/Que/).matches?(proc)).to eq(true) + expect(ComplainMatcher.new(/Quoi/).matches?(proc)).to eq(false) + end + + it "does not match when there is no output to $stderr" do + expect(ComplainMatcher.new(nil).matches?(lambda {})).to eq(false) + end + + it "provides a useful failure message" do + matcher = ComplainMatcher.new(nil) + matcher.matches?(lambda { }) + expect(matcher.failure_message).to eq(["Expected a warning", "but received none"]) + matcher = ComplainMatcher.new("listen here") + matcher.matches?(lambda { warn "look out" }) + expect(matcher.failure_message).to eq( + ["Expected warning: \"listen here\"", "but got: \"look out\""] + ) + matcher = ComplainMatcher.new(/talk/) + matcher.matches?(lambda { warn "listen up" }) + expect(matcher.failure_message).to eq( + ["Expected warning to match: /talk/", "but got: \"listen up\""] + ) + end + + it "provides a useful negative failure message" do + proc = lambda { warn "ouch" } + matcher = ComplainMatcher.new(nil) + matcher.matches?(proc) + expect(matcher.negative_failure_message).to eq( + ["Unexpected warning: ", "\"ouch\""] + ) + matcher = ComplainMatcher.new("ouchy") + matcher.matches?(proc) + expect(matcher.negative_failure_message).to eq( + ["Expected warning: \"ouchy\"", "but got: \"ouch\""] + ) + matcher = ComplainMatcher.new(/ou/) + matcher.matches?(proc) + expect(matcher.negative_failure_message).to eq( + ["Expected warning not to match: /ou/", "but got: \"ouch\""] + ) + end + + context "`verbose` option specified" do + before do + $VERBOSE, @verbose = nil, $VERBOSE + end + + after do + $VERBOSE = @verbose + end + + it "sets $VERBOSE with specified second optional parameter" do + verbose = nil + proc = lambda { verbose = $VERBOSE } + + ComplainMatcher.new(nil, verbose: true).matches?(proc) + expect(verbose).to eq(true) + + ComplainMatcher.new(nil, verbose: false).matches?(proc) + expect(verbose).to eq(false) + end + + it "sets $VERBOSE with false by default" do + verbose = nil + proc = lambda { verbose = $VERBOSE } + + ComplainMatcher.new(nil).matches?(proc) + expect(verbose).to eq(false) + end + + it "does not have side effect" do + proc = lambda { safe_value = $VERBOSE } + + expect do + ComplainMatcher.new(nil, verbose: true).matches?(proc) + end.not_to change { $VERBOSE } + end + + it "accepts a verbose level as single argument" do + verbose = nil + proc = lambda { verbose = $VERBOSE } + + ComplainMatcher.new(verbose: true).matches?(proc) + expect(verbose).to eq(true) + end + end +end diff --git a/spec/mspec/spec/matchers/eql_spec.rb b/spec/mspec/spec/matchers/eql_spec.rb new file mode 100644 index 0000000000..66307d2a9d --- /dev/null +++ b/spec/mspec/spec/matchers/eql_spec.rb @@ -0,0 +1,33 @@ +require 'spec_helper' +require 'mspec/expectations/expectations' +require 'mspec/matchers' + +RSpec.describe EqlMatcher do + it "matches when actual is eql? to expected" do + expect(EqlMatcher.new(1).matches?(1)).to eq(true) + expect(EqlMatcher.new(1.5).matches?(1.5)).to eq(true) + expect(EqlMatcher.new("red").matches?("red")).to eq(true) + expect(EqlMatcher.new(:blue).matches?(:blue)).to eq(true) + expect(EqlMatcher.new(Object).matches?(Object)).to eq(true) + + o = Object.new + expect(EqlMatcher.new(o).matches?(o)).to eq(true) + end + + it "does not match when actual is not eql? to expected" do + expect(EqlMatcher.new(1).matches?(1.0)).to eq(false) + expect(EqlMatcher.new(Hash).matches?(Object)).to eq(false) + end + + it "provides a useful failure message" do + matcher = EqlMatcher.new("red") + matcher.matches?("red") + expect(matcher.failure_message).to eq(["Expected \"red\"", "to have same value and type as \"red\""]) + end + + it "provides a useful negative failure message" do + matcher = EqlMatcher.new(1) + matcher.matches?(1.0) + expect(matcher.negative_failure_message).to eq(["Expected 1.0", "not to have same value or type as 1"]) + end +end diff --git a/spec/mspec/spec/matchers/equal_element_spec.rb b/spec/mspec/spec/matchers/equal_element_spec.rb new file mode 100644 index 0000000000..3a5ae4ede2 --- /dev/null +++ b/spec/mspec/spec/matchers/equal_element_spec.rb @@ -0,0 +1,75 @@ +require 'spec_helper' +require 'mspec/expectations/expectations' +require 'mspec/matchers' + +RSpec.describe EqualElementMatcher do + it "matches if it finds an element with the passed name, no matter what attributes/content" do + expect(EqualElementMatcher.new("A").matches?('<A></A>')).to be_truthy + expect(EqualElementMatcher.new("A").matches?('<A HREF="http://example.com"></A>')).to be_truthy + expect(EqualElementMatcher.new("A").matches?('<A HREF="http://example.com"></A>')).to be_truthy + + expect(EqualElementMatcher.new("BASE").matches?('<BASE></A>')).to be_falsey + expect(EqualElementMatcher.new("BASE").matches?('<A></BASE>')).to be_falsey + expect(EqualElementMatcher.new("BASE").matches?('<A></A>')).to be_falsey + expect(EqualElementMatcher.new("BASE").matches?('<A HREF="http://example.com"></A>')).to be_falsey + expect(EqualElementMatcher.new("BASE").matches?('<A HREF="http://example.com"></A>')).to be_falsey + end + + it "matches if it finds an element with the passed name and the passed attributes" do + expect(EqualElementMatcher.new("A", {}).matches?('<A></A>')).to be_truthy + expect(EqualElementMatcher.new("A", nil).matches?('<A HREF="http://example.com"></A>')).to be_truthy + expect(EqualElementMatcher.new("A", "HREF" => "http://example.com").matches?('<A HREF="http://example.com"></A>')).to be_truthy + + expect(EqualElementMatcher.new("A", {}).matches?('<A HREF="http://example.com"></A>')).to be_falsey + expect(EqualElementMatcher.new("A", "HREF" => "http://example.com").matches?('<A></A>')).to be_falsey + expect(EqualElementMatcher.new("A", "HREF" => "http://example.com").matches?('<A HREF="http://test.com"></A>')).to be_falsey + expect(EqualElementMatcher.new("A", "HREF" => "http://example.com").matches?('<A HREF="http://example.com" HREF="http://example.com"></A>')).to be_falsey + end + + it "matches if it finds an element with the passed name, the passed attributes and the passed content" do + expect(EqualElementMatcher.new("A", {}, "").matches?('<A></A>')).to be_truthy + expect(EqualElementMatcher.new("A", {"HREF" => "http://example.com"}, "Example").matches?('<A HREF="http://example.com">Example</A>')).to be_truthy + + expect(EqualElementMatcher.new("A", {}, "Test").matches?('<A></A>')).to be_falsey + expect(EqualElementMatcher.new("A", {"HREF" => "http://example.com"}, "Example").matches?('<A HREF="http://example.com"></A>')).to be_falsey + expect(EqualElementMatcher.new("A", {"HREF" => "http://example.com"}, "Example").matches?('<A HREF="http://example.com">Test</A>')).to be_falsey + end + + it "can match unclosed elements" do + expect(EqualElementMatcher.new("BASE", nil, nil, :not_closed => true).matches?('<BASE>')).to be_truthy + expect(EqualElementMatcher.new("BASE", {"HREF" => "http://example.com"}, nil, :not_closed => true).matches?('<BASE HREF="http://example.com">')).to be_truthy + expect(EqualElementMatcher.new("BASE", {"HREF" => "http://example.com"}, "Example", :not_closed => true).matches?('<BASE HREF="http://example.com">Example')).to be_truthy + + expect(EqualElementMatcher.new("BASE", {}, nil, :not_closed => true).matches?('<BASE HREF="http://example.com">')).to be_falsey + expect(EqualElementMatcher.new("BASE", {"HREF" => "http://example.com"}, "", :not_closed => true).matches?('<BASE HREF="http://example.com">Example')).to be_falsey + expect(EqualElementMatcher.new("BASE", {"HREF" => "http://example.com"}, "Test", :not_closed => true).matches?('<BASE HREF="http://example.com">Example')).to be_falsey + end + + it "provides a useful failure message" do + equal_element = EqualElementMatcher.new("A", {}, "Test") + expect(equal_element.matches?('<A></A>')).to be_falsey + expect(equal_element.failure_message).to eq([%{Expected "<A></A>"}, %{to be a 'A' element with no attributes and "Test" as content}]) + + equal_element = EqualElementMatcher.new("A", {}, "") + expect(equal_element.matches?('<A>Test</A>')).to be_falsey + expect(equal_element.failure_message).to eq([%{Expected "<A>Test</A>"}, %{to be a 'A' element with no attributes and no content}]) + + equal_element = EqualElementMatcher.new("A", "HREF" => "http://www.example.com") + expect(equal_element.matches?('<A>Test</A>')).to be_falsey + expect(equal_element.failure_message).to eq([%{Expected "<A>Test</A>"}, %{to be a 'A' element with HREF="http://www.example.com" and any content}]) + end + + it "provides a useful negative failure message" do + equal_element = EqualElementMatcher.new("A", {}, "Test") + expect(equal_element.matches?('<A></A>')).to be_falsey + expect(equal_element.negative_failure_message).to eq([%{Expected "<A></A>"}, %{not to be a 'A' element with no attributes and "Test" as content}]) + + equal_element = EqualElementMatcher.new("A", {}, "") + expect(equal_element.matches?('<A>Test</A>')).to be_falsey + expect(equal_element.negative_failure_message).to eq([%{Expected "<A>Test</A>"}, %{not to be a 'A' element with no attributes and no content}]) + + equal_element = EqualElementMatcher.new("A", "HREF" => "http://www.example.com") + expect(equal_element.matches?('<A>Test</A>')).to be_falsey + expect(equal_element.negative_failure_message).to eq([%{Expected "<A>Test</A>"}, %{not to be a 'A' element with HREF="http://www.example.com" and any content}]) + end +end diff --git a/spec/mspec/spec/matchers/equal_spec.rb b/spec/mspec/spec/matchers/equal_spec.rb new file mode 100644 index 0000000000..2df1de54b4 --- /dev/null +++ b/spec/mspec/spec/matchers/equal_spec.rb @@ -0,0 +1,32 @@ +require 'spec_helper' +require 'mspec/expectations/expectations' +require 'mspec/matchers' + +RSpec.describe EqualMatcher do + it "matches when actual is equal? to expected" do + expect(EqualMatcher.new(1).matches?(1)).to eq(true) + expect(EqualMatcher.new(:blue).matches?(:blue)).to eq(true) + expect(EqualMatcher.new(Object).matches?(Object)).to eq(true) + + o = Object.new + expect(EqualMatcher.new(o).matches?(o)).to eq(true) + end + + it "does not match when actual is not a equal? to expected" do + expect(EqualMatcher.new(1).matches?(1.0)).to eq(false) + expect(EqualMatcher.new("blue").matches?("blue")).to eq(false) + expect(EqualMatcher.new(Hash).matches?(Object)).to eq(false) + end + + it "provides a useful failure message" do + matcher = EqualMatcher.new("red") + matcher.matches?("red") + expect(matcher.failure_message).to eq(["Expected \"red\"", "to be identical to \"red\""]) + end + + it "provides a useful negative failure message" do + matcher = EqualMatcher.new(1) + matcher.matches?(1) + expect(matcher.negative_failure_message).to eq(["Expected 1", "not to be identical to 1"]) + end +end diff --git a/spec/mspec/spec/matchers/have_class_variable_spec.rb b/spec/mspec/spec/matchers/have_class_variable_spec.rb new file mode 100644 index 0000000000..d6fcf9d4e2 --- /dev/null +++ b/spec/mspec/spec/matchers/have_class_variable_spec.rb @@ -0,0 +1,49 @@ +require 'spec_helper' +require 'mspec/expectations/expectations' +require 'mspec/matchers' + +class IVarModMock + def self.class_variables + [:@foo] + end +end + +RSpec.describe HaveClassVariableMatcher, "on RUBY_VERSION >= 1.9" do + it "matches when mod has the class variable, given as string" do + matcher = HaveClassVariableMatcher.new('@foo') + expect(matcher.matches?(IVarModMock)).to be_truthy + end + + it "matches when mod has the class variable, given as symbol" do + matcher = HaveClassVariableMatcher.new(:@foo) + expect(matcher.matches?(IVarModMock)).to be_truthy + end + + it "does not match when mod hasn't got the class variable, given as string" do + matcher = HaveClassVariableMatcher.new('@bar') + expect(matcher.matches?(IVarModMock)).to be_falsey + end + + it "does not match when mod hasn't got the class variable, given as symbol" do + matcher = HaveClassVariableMatcher.new(:@bar) + expect(matcher.matches?(IVarModMock)).to be_falsey + end + + it "provides a failure message for #should" do + matcher = HaveClassVariableMatcher.new(:@bar) + matcher.matches?(IVarModMock) + expect(matcher.failure_message).to eq([ + "Expected IVarModMock to have class variable '@bar'", + "but it does not" + ]) + end + + it "provides a failure messoge for #should_not" do + matcher = HaveClassVariableMatcher.new(:@bar) + matcher.matches?(IVarModMock) + expect(matcher.negative_failure_message).to eq([ + "Expected IVarModMock NOT to have class variable '@bar'", + "but it does" + ]) + end +end diff --git a/spec/mspec/spec/matchers/have_constant_spec.rb b/spec/mspec/spec/matchers/have_constant_spec.rb new file mode 100644 index 0000000000..0bf44dbe2b --- /dev/null +++ b/spec/mspec/spec/matchers/have_constant_spec.rb @@ -0,0 +1,37 @@ +require 'spec_helper' +require 'mspec/expectations/expectations' +require 'mspec/matchers' + +class HCMSpecs + X = :x +end + +RSpec.describe HaveConstantMatcher do + it "matches when mod has the constant" do + matcher = HaveConstantMatcher.new :X + expect(matcher.matches?(HCMSpecs)).to be_truthy + end + + it "does not match when mod does not have the constant" do + matcher = HaveConstantMatcher.new :A + expect(matcher.matches?(HCMSpecs)).to be_falsey + end + + it "provides a failure message for #should" do + matcher = HaveConstantMatcher.new :A + matcher.matches?(HCMSpecs) + expect(matcher.failure_message).to eq([ + "Expected HCMSpecs to have constant 'A'", + "but it does not" + ]) + end + + it "provides a failure messoge for #should_not" do + matcher = HaveConstantMatcher.new :X + matcher.matches?(HCMSpecs) + expect(matcher.negative_failure_message).to eq([ + "Expected HCMSpecs NOT to have constant 'X'", + "but it does" + ]) + end +end diff --git a/spec/mspec/spec/matchers/have_instance_method_spec.rb b/spec/mspec/spec/matchers/have_instance_method_spec.rb new file mode 100644 index 0000000000..7c2e50dba6 --- /dev/null +++ b/spec/mspec/spec/matchers/have_instance_method_spec.rb @@ -0,0 +1,53 @@ +require 'spec_helper' +require 'mspec/expectations/expectations' +require 'mspec/matchers' + +class HIMMSpecs + def instance_method + end + + class Subclass < HIMMSpecs + def instance_sub_method + end + end +end + +RSpec.describe HaveInstanceMethodMatcher do + it "inherits from MethodMatcher" do + expect(HaveInstanceMethodMatcher.new(:m)).to be_kind_of(MethodMatcher) + end + + it "matches when mod has the instance method" do + matcher = HaveInstanceMethodMatcher.new :instance_method + expect(matcher.matches?(HIMMSpecs)).to be_truthy + expect(matcher.matches?(HIMMSpecs::Subclass)).to be_truthy + end + + it "does not match when mod does not have the instance method" do + matcher = HaveInstanceMethodMatcher.new :another_method + expect(matcher.matches?(HIMMSpecs)).to be_falsey + end + + it "does not match if the method is in a superclass and include_super is false" do + matcher = HaveInstanceMethodMatcher.new :instance_method, false + expect(matcher.matches?(HIMMSpecs::Subclass)).to be_falsey + end + + it "provides a failure message for #should" do + matcher = HaveInstanceMethodMatcher.new :some_method + matcher.matches?(HIMMSpecs) + expect(matcher.failure_message).to eq([ + "Expected HIMMSpecs to have instance method 'some_method'", + "but it does not" + ]) + end + + it "provides a failure messoge for #should_not" do + matcher = HaveInstanceMethodMatcher.new :some_method + matcher.matches?(HIMMSpecs) + expect(matcher.negative_failure_message).to eq([ + "Expected HIMMSpecs NOT to have instance method 'some_method'", + "but it does" + ]) + end +end diff --git a/spec/mspec/spec/matchers/have_instance_variable_spec.rb b/spec/mspec/spec/matchers/have_instance_variable_spec.rb new file mode 100644 index 0000000000..12e2470f14 --- /dev/null +++ b/spec/mspec/spec/matchers/have_instance_variable_spec.rb @@ -0,0 +1,50 @@ +require 'spec_helper' +require 'mspec/expectations/expectations' +require 'mspec/matchers' + +RSpec.describe HaveInstanceVariableMatcher do + before :each do + @object = Object.new + def @object.instance_variables + [:@foo] + end + end + + it "matches when object has the instance variable, given as string" do + matcher = HaveInstanceVariableMatcher.new('@foo') + expect(matcher.matches?(@object)).to be_truthy + end + + it "matches when object has the instance variable, given as symbol" do + matcher = HaveInstanceVariableMatcher.new(:@foo) + expect(matcher.matches?(@object)).to be_truthy + end + + it "does not match when object hasn't got the instance variable, given as string" do + matcher = HaveInstanceVariableMatcher.new('@bar') + expect(matcher.matches?(@object)).to be_falsey + end + + it "does not match when object hasn't got the instance variable, given as symbol" do + matcher = HaveInstanceVariableMatcher.new(:@bar) + expect(matcher.matches?(@object)).to be_falsey + end + + it "provides a failure message for #should" do + matcher = HaveInstanceVariableMatcher.new(:@bar) + matcher.matches?(@object) + expect(matcher.failure_message).to eq([ + "Expected #{@object.inspect} to have instance variable '@bar'", + "but it does not" + ]) + end + + it "provides a failure messoge for #should_not" do + matcher = HaveInstanceVariableMatcher.new(:@bar) + matcher.matches?(@object) + expect(matcher.negative_failure_message).to eq([ + "Expected #{@object.inspect} NOT to have instance variable '@bar'", + "but it does" + ]) + end +end diff --git a/spec/mspec/spec/matchers/have_method_spec.rb b/spec/mspec/spec/matchers/have_method_spec.rb new file mode 100644 index 0000000000..4fc0bf5e45 --- /dev/null +++ b/spec/mspec/spec/matchers/have_method_spec.rb @@ -0,0 +1,55 @@ +require 'spec_helper' +require 'mspec/expectations/expectations' +require 'mspec/matchers' + +class HMMSpecs + def instance_method + end + + class Subclass < HMMSpecs + def instance_sub_method + end + end +end + +RSpec.describe HaveMethodMatcher do + it "inherits from MethodMatcher" do + expect(HaveMethodMatcher.new(:m)).to be_kind_of(MethodMatcher) + end + + it "matches when mod has the method" do + matcher = HaveMethodMatcher.new :instance_method + expect(matcher.matches?(HMMSpecs)).to be_truthy + expect(matcher.matches?(HMMSpecs.new)).to be_truthy + expect(matcher.matches?(HMMSpecs::Subclass)).to be_truthy + expect(matcher.matches?(HMMSpecs::Subclass.new)).to be_truthy + end + + it "does not match when mod does not have the method" do + matcher = HaveMethodMatcher.new :another_method + expect(matcher.matches?(HMMSpecs)).to be_falsey + end + + it "does not match if the method is in a superclass and include_super is false" do + matcher = HaveMethodMatcher.new :instance_method, false + expect(matcher.matches?(HMMSpecs::Subclass)).to be_falsey + end + + it "provides a failure message for #should" do + matcher = HaveMethodMatcher.new :some_method + matcher.matches?(HMMSpecs) + expect(matcher.failure_message).to eq([ + "Expected HMMSpecs to have method 'some_method'", + "but it does not" + ]) + end + + it "provides a failure messoge for #should_not" do + matcher = HaveMethodMatcher.new :some_method + matcher.matches?(HMMSpecs) + expect(matcher.negative_failure_message).to eq([ + "Expected HMMSpecs NOT to have method 'some_method'", + "but it does" + ]) + end +end diff --git a/spec/mspec/spec/matchers/have_private_instance_method_spec.rb b/spec/mspec/spec/matchers/have_private_instance_method_spec.rb new file mode 100644 index 0000000000..0e65c264d9 --- /dev/null +++ b/spec/mspec/spec/matchers/have_private_instance_method_spec.rb @@ -0,0 +1,57 @@ +require 'spec_helper' +require 'mspec/expectations/expectations' +require 'mspec/matchers' + +class HPIMMSpecs + private + + def private_method + end + + class Subclass < HPIMMSpecs + private + + def private_sub_method + end + end +end + +RSpec.describe HavePrivateInstanceMethodMatcher do + it "inherits from MethodMatcher" do + expect(HavePrivateInstanceMethodMatcher.new(:m)).to be_kind_of(MethodMatcher) + end + + it "matches when mod has the private instance method" do + matcher = HavePrivateInstanceMethodMatcher.new :private_method + expect(matcher.matches?(HPIMMSpecs)).to be_truthy + expect(matcher.matches?(HPIMMSpecs::Subclass)).to be_truthy + end + + it "does not match when mod does not have the private instance method" do + matcher = HavePrivateInstanceMethodMatcher.new :another_method + expect(matcher.matches?(HPIMMSpecs)).to be_falsey + end + + it "does not match if the method is in a superclass and include_super is false" do + matcher = HavePrivateInstanceMethodMatcher.new :private_method, false + expect(matcher.matches?(HPIMMSpecs::Subclass)).to be_falsey + end + + it "provides a failure message for #should" do + matcher = HavePrivateInstanceMethodMatcher.new :some_method + matcher.matches?(HPIMMSpecs) + expect(matcher.failure_message).to eq([ + "Expected HPIMMSpecs to have private instance method 'some_method'", + "but it does not" + ]) + end + + it "provides a failure message for #should_not" do + matcher = HavePrivateInstanceMethodMatcher.new :some_method + matcher.matches?(HPIMMSpecs) + expect(matcher.negative_failure_message).to eq([ + "Expected HPIMMSpecs NOT to have private instance method 'some_method'", + "but it does" + ]) + end +end diff --git a/spec/mspec/spec/matchers/have_private_method_spec.rb b/spec/mspec/spec/matchers/have_private_method_spec.rb new file mode 100644 index 0000000000..f433288057 --- /dev/null +++ b/spec/mspec/spec/matchers/have_private_method_spec.rb @@ -0,0 +1,44 @@ +require 'spec_helper' +require 'mspec/expectations/expectations' +require 'mspec/matchers' + +class HPMMSpecs + def self.private_method + end + + private_class_method :private_method +end + +RSpec.describe HavePrivateMethodMatcher do + it "inherits from MethodMatcher" do + expect(HavePrivateMethodMatcher.new(:m)).to be_kind_of(MethodMatcher) + end + + it "matches when mod has the private method" do + matcher = HavePrivateMethodMatcher.new :private_method + expect(matcher.matches?(HPMMSpecs)).to be_truthy + end + + it "does not match when mod does not have the private method" do + matcher = HavePrivateMethodMatcher.new :another_method + expect(matcher.matches?(HPMMSpecs)).to be_falsey + end + + it "provides a failure message for #should" do + matcher = HavePrivateMethodMatcher.new :some_method + matcher.matches?(HPMMSpecs) + expect(matcher.failure_message).to eq([ + "Expected HPMMSpecs to have private method 'some_method'", + "but it does not" + ]) + end + + it "provides a failure message for #should_not" do + matcher = HavePrivateMethodMatcher.new :private_method + matcher.matches?(HPMMSpecs) + expect(matcher.negative_failure_message).to eq([ + "Expected HPMMSpecs NOT to have private method 'private_method'", + "but it does" + ]) + end +end diff --git a/spec/mspec/spec/matchers/have_protected_instance_method_spec.rb b/spec/mspec/spec/matchers/have_protected_instance_method_spec.rb new file mode 100644 index 0000000000..45b39004a3 --- /dev/null +++ b/spec/mspec/spec/matchers/have_protected_instance_method_spec.rb @@ -0,0 +1,57 @@ +require 'spec_helper' +require 'mspec/expectations/expectations' +require 'mspec/matchers' + +class HPIMMSpecs + protected + + def protected_method + end + + class Subclass < HPIMMSpecs + protected + + def protected_sub_method + end + end +end + +RSpec.describe HaveProtectedInstanceMethodMatcher do + it "inherits from MethodMatcher" do + expect(HaveProtectedInstanceMethodMatcher.new(:m)).to be_kind_of(MethodMatcher) + end + + it "matches when mod has the protected instance method" do + matcher = HaveProtectedInstanceMethodMatcher.new :protected_method + expect(matcher.matches?(HPIMMSpecs)).to be_truthy + expect(matcher.matches?(HPIMMSpecs::Subclass)).to be_truthy + end + + it "does not match when mod does not have the protected instance method" do + matcher = HaveProtectedInstanceMethodMatcher.new :another_method + expect(matcher.matches?(HPIMMSpecs)).to be_falsey + end + + it "does not match if the method is in a superclass and include_super is false" do + matcher = HaveProtectedInstanceMethodMatcher.new :protected_method, false + expect(matcher.matches?(HPIMMSpecs::Subclass)).to be_falsey + end + + it "provides a failure message for #should" do + matcher = HaveProtectedInstanceMethodMatcher.new :some_method + matcher.matches?(HPIMMSpecs) + expect(matcher.failure_message).to eq([ + "Expected HPIMMSpecs to have protected instance method 'some_method'", + "but it does not" + ]) + end + + it "provides a failure messoge for #should_not" do + matcher = HaveProtectedInstanceMethodMatcher.new :some_method + matcher.matches?(HPIMMSpecs) + expect(matcher.negative_failure_message).to eq([ + "Expected HPIMMSpecs NOT to have protected instance method 'some_method'", + "but it does" + ]) + end +end diff --git a/spec/mspec/spec/matchers/have_public_instance_method_spec.rb b/spec/mspec/spec/matchers/have_public_instance_method_spec.rb new file mode 100644 index 0000000000..771d5b7911 --- /dev/null +++ b/spec/mspec/spec/matchers/have_public_instance_method_spec.rb @@ -0,0 +1,53 @@ +require 'spec_helper' +require 'mspec/expectations/expectations' +require 'mspec/matchers' + +class HPIMMSpecs + def public_method + end + + class Subclass < HPIMMSpecs + def public_sub_method + end + end +end + +RSpec.describe HavePublicInstanceMethodMatcher do + it "inherits from MethodMatcher" do + expect(HavePublicInstanceMethodMatcher.new(:m)).to be_kind_of(MethodMatcher) + end + + it "matches when mod has the public instance method" do + matcher = HavePublicInstanceMethodMatcher.new :public_method + expect(matcher.matches?(HPIMMSpecs)).to be_truthy + expect(matcher.matches?(HPIMMSpecs::Subclass)).to be_truthy + end + + it "does not match when mod does not have the public instance method" do + matcher = HavePublicInstanceMethodMatcher.new :another_method + expect(matcher.matches?(HPIMMSpecs)).to be_falsey + end + + it "does not match if the method is in a superclass and include_super is false" do + matcher = HavePublicInstanceMethodMatcher.new :public_method, false + expect(matcher.matches?(HPIMMSpecs::Subclass)).to be_falsey + end + + it "provides a failure message for #should" do + matcher = HavePublicInstanceMethodMatcher.new :some_method + matcher.matches?(HPIMMSpecs) + expect(matcher.failure_message).to eq([ + "Expected HPIMMSpecs to have public instance method 'some_method'", + "but it does not" + ]) + end + + it "provides a failure messoge for #should_not" do + matcher = HavePublicInstanceMethodMatcher.new :some_method + matcher.matches?(HPIMMSpecs) + expect(matcher.negative_failure_message).to eq([ + "Expected HPIMMSpecs NOT to have public instance method 'some_method'", + "but it does" + ]) + end +end diff --git a/spec/mspec/spec/matchers/have_singleton_method_spec.rb b/spec/mspec/spec/matchers/have_singleton_method_spec.rb new file mode 100644 index 0000000000..61ef00d49c --- /dev/null +++ b/spec/mspec/spec/matchers/have_singleton_method_spec.rb @@ -0,0 +1,45 @@ +require 'spec_helper' +require 'mspec/expectations/expectations' +require 'mspec/matchers' + +class HSMMSpecs + def self.singleton_method + end +end + +RSpec.describe HaveSingletonMethodMatcher do + it "inherits from MethodMatcher" do + expect(HaveSingletonMethodMatcher.new(:m)).to be_kind_of(MethodMatcher) + end + + it "matches when the class has a singleton method" do + matcher = HaveSingletonMethodMatcher.new :singleton_method + expect(matcher.matches?(HSMMSpecs)).to be_truthy + end + + it "matches when the object has a singleton method" do + obj = double("HSMMSpecs") + def obj.singleton_method; end + + matcher = HaveSingletonMethodMatcher.new :singleton_method + expect(matcher.matches?(obj)).to be_truthy + end + + it "provides a failure message for #should" do + matcher = HaveSingletonMethodMatcher.new :some_method + matcher.matches?(HSMMSpecs) + expect(matcher.failure_message).to eq([ + "Expected HSMMSpecs to have singleton method 'some_method'", + "but it does not" + ]) + end + + it "provides a failure message for #should_not" do + matcher = HaveSingletonMethodMatcher.new :singleton_method + matcher.matches?(HSMMSpecs) + expect(matcher.negative_failure_message).to eq([ + "Expected HSMMSpecs NOT to have singleton method 'singleton_method'", + "but it does" + ]) + end +end diff --git a/spec/mspec/spec/matchers/include_any_of_spec.rb b/spec/mspec/spec/matchers/include_any_of_spec.rb new file mode 100644 index 0000000000..1473bb6d0b --- /dev/null +++ b/spec/mspec/spec/matchers/include_any_of_spec.rb @@ -0,0 +1,42 @@ +require 'spec_helper' +require 'mspec/expectations/expectations' +require 'mspec/matchers' + +RSpec.describe IncludeAnyOfMatcher do + it "matches when actual includes expected" do + expect(IncludeAnyOfMatcher.new(2).matches?([1,2,3])).to eq(true) + expect(IncludeAnyOfMatcher.new("b").matches?("abc")).to eq(true) + end + + it "does not match when actual does not include expected" do + expect(IncludeAnyOfMatcher.new(4).matches?([1,2,3])).to eq(false) + expect(IncludeAnyOfMatcher.new("d").matches?("abc")).to eq(false) + end + + it "matches when actual includes all expected" do + expect(IncludeAnyOfMatcher.new(3, 2, 1).matches?([1,2,3])).to eq(true) + expect(IncludeAnyOfMatcher.new("a", "b", "c").matches?("abc")).to eq(true) + end + + it "matches when actual includes any expected" do + expect(IncludeAnyOfMatcher.new(3, 4, 5).matches?([1,2,3])).to eq(true) + expect(IncludeAnyOfMatcher.new("c", "d", "e").matches?("abc")).to eq(true) + end + + it "does not match when actual does not include any expected" do + expect(IncludeAnyOfMatcher.new(4, 5).matches?([1,2,3])).to eq(false) + expect(IncludeAnyOfMatcher.new("de").matches?("abc")).to eq(false) + end + + it "provides a useful failure message" do + matcher = IncludeAnyOfMatcher.new(5, 6) + matcher.matches?([1,2,3]) + expect(matcher.failure_message).to eq(["Expected [1, 2, 3]", "to include any of [5, 6]"]) + end + + it "provides a useful negative failure message" do + matcher = IncludeAnyOfMatcher.new(1, 2, 3) + matcher.matches?([1,2]) + expect(matcher.negative_failure_message).to eq(["Expected [1, 2]", "not to include any of [1, 2, 3]"]) + end +end diff --git a/spec/mspec/spec/matchers/include_spec.rb b/spec/mspec/spec/matchers/include_spec.rb new file mode 100644 index 0000000000..6bf1bef085 --- /dev/null +++ b/spec/mspec/spec/matchers/include_spec.rb @@ -0,0 +1,37 @@ +require 'spec_helper' +require 'mspec/expectations/expectations' +require 'mspec/matchers' + +RSpec.describe IncludeMatcher do + it "matches when actual includes expected" do + expect(IncludeMatcher.new(2).matches?([1,2,3])).to eq(true) + expect(IncludeMatcher.new("b").matches?("abc")).to eq(true) + end + + it "does not match when actual does not include expected" do + expect(IncludeMatcher.new(4).matches?([1,2,3])).to eq(false) + expect(IncludeMatcher.new("d").matches?("abc")).to eq(false) + end + + it "matches when actual includes all expected" do + expect(IncludeMatcher.new(3, 2, 1).matches?([1,2,3])).to eq(true) + expect(IncludeMatcher.new("a", "b", "c").matches?("abc")).to eq(true) + end + + it "does not match when actual does not include all expected" do + expect(IncludeMatcher.new(3, 2, 4).matches?([1,2,3])).to eq(false) + expect(IncludeMatcher.new("a", "b", "c", "d").matches?("abc")).to eq(false) + end + + it "provides a useful failure message" do + matcher = IncludeMatcher.new(5, 2) + matcher.matches?([1,2,3]) + expect(matcher.failure_message).to eq(["Expected [1, 2, 3]", "to include 5"]) + end + + it "provides a useful negative failure message" do + matcher = IncludeMatcher.new(1, 2, 3) + matcher.matches?([1,2,3]) + expect(matcher.negative_failure_message).to eq(["Expected [1, 2, 3]", "not to include 3"]) + end +end diff --git a/spec/mspec/spec/matchers/infinity_spec.rb b/spec/mspec/spec/matchers/infinity_spec.rb new file mode 100644 index 0000000000..78c4194526 --- /dev/null +++ b/spec/mspec/spec/matchers/infinity_spec.rb @@ -0,0 +1,34 @@ +require 'spec_helper' +require 'mspec/expectations/expectations' +require 'mspec/guards' +require 'mspec/helpers' +require 'mspec/matchers' + +RSpec.describe InfinityMatcher do + it "matches when actual is infinite and has the correct sign" do + expect(InfinityMatcher.new(1).matches?(infinity_value)).to eq(true) + expect(InfinityMatcher.new(-1).matches?(-infinity_value)).to eq(true) + end + + it "does not match when actual is not infinite" do + expect(InfinityMatcher.new(1).matches?(1.0)).to eq(false) + expect(InfinityMatcher.new(-1).matches?(-1.0)).to eq(false) + end + + it "does not match when actual is infinite but has the incorrect sign" do + expect(InfinityMatcher.new(1).matches?(-infinity_value)).to eq(false) + expect(InfinityMatcher.new(-1).matches?(infinity_value)).to eq(false) + end + + it "provides a useful failure message" do + matcher = InfinityMatcher.new(-1) + matcher.matches?(0) + expect(matcher.failure_message).to eq(["Expected 0", "to be -Infinity"]) + end + + it "provides a useful negative failure message" do + matcher = InfinityMatcher.new(1) + matcher.matches?(infinity_value) + expect(matcher.negative_failure_message).to eq(["Expected Infinity", "not to be Infinity"]) + end +end diff --git a/spec/mspec/spec/matchers/match_yaml_spec.rb b/spec/mspec/spec/matchers/match_yaml_spec.rb new file mode 100644 index 0000000000..85123bb87d --- /dev/null +++ b/spec/mspec/spec/matchers/match_yaml_spec.rb @@ -0,0 +1,39 @@ +require 'spec_helper' +require 'mspec/expectations/expectations' +require 'mspec/matchers' + +RSpec.describe MatchYAMLMatcher do + before :each do + @matcher = MatchYAMLMatcher.new("--- \nfoo: bar\n") + end + + it "compares YAML documents and matches if they're equivalent" do + expect(@matcher.matches?("--- \nfoo: bar\n")).to eq(true) + end + + it "compares YAML documents and does not match if they're not equivalent" do + expect(@matcher.matches?("--- \nbar: foo\n")).to eq(false) + expect(@matcher.matches?("--- \nfoo: \nbar\n")).to eq(false) + end + + it "also receives objects that respond_to to_yaml" do + matcher = MatchYAMLMatcher.new("some string") + expect(matcher.matches?("some string")).to eq(true) + + matcher = MatchYAMLMatcher.new(['a', 'b']) + expect(matcher.matches?("--- \n- a\n- b\n")).to eq(true) + + matcher = MatchYAMLMatcher.new("foo" => "bar") + expect(matcher.matches?("--- \nfoo: bar\n")).to eq(true) + end + + it "matches documents with trailing whitespace" do + expect(@matcher.matches?("--- \nfoo: bar \n")).to eq(true) + expect(@matcher.matches?("--- \nfoo: bar \n")).to eq(true) + end + + it "fails with a descriptive error message" do + expect(@matcher.matches?("foo")).to eq(false) + expect(@matcher.failure_message).to eq(["Expected \"foo\"", " to match \"--- \\nfoo: bar\\n\""]) + end +end diff --git a/spec/mspec/spec/matchers/output_spec.rb b/spec/mspec/spec/matchers/output_spec.rb new file mode 100644 index 0000000000..3baad9a4b2 --- /dev/null +++ b/spec/mspec/spec/matchers/output_spec.rb @@ -0,0 +1,84 @@ +require 'spec_helper' +require 'mspec/expectations/expectations' +require 'mspec/matchers' + +RSpec.describe OutputMatcher do + it "matches when executing the proc results in the expected output to $stdout" do + proc = Proc.new { puts "bang!" } + expect(OutputMatcher.new("bang!\n", nil).matches?(proc)).to eq(true) + expect(OutputMatcher.new("pop", nil).matches?(proc)).to eq(false) + expect(OutputMatcher.new(/bang/, nil).matches?(proc)).to eq(true) + expect(OutputMatcher.new(/po/, nil).matches?(proc)).to eq(false) + end + + it "matches when executing the proc results in the expected output to $stderr" do + proc = Proc.new { $stderr.write "boom!" } + expect(OutputMatcher.new(nil, "boom!").matches?(proc)).to eq(true) + expect(OutputMatcher.new(nil, "fizzle").matches?(proc)).to eq(false) + expect(OutputMatcher.new(nil, /boom/).matches?(proc)).to eq(true) + expect(OutputMatcher.new(nil, /fizzl/).matches?(proc)).to eq(false) + end + + it "provides a useful failure message" do + proc = Proc.new { print "unexpected"; $stderr.print "unerror" } + matcher = OutputMatcher.new("expected", "error") + matcher.matches?(proc) + expect(matcher.failure_message).to eq( + ["Expected:\n $stdout: \"expected\"\n $stderr: \"error\"\n", + " got:\n $stdout: \"unexpected\"\n $stderr: \"unerror\"\n"] + ) + matcher = OutputMatcher.new("expected", nil) + matcher.matches?(proc) + expect(matcher.failure_message).to eq( + ["Expected:\n $stdout: \"expected\"\n", + " got:\n $stdout: \"unexpected\"\n"] + ) + matcher = OutputMatcher.new(nil, "error") + matcher.matches?(proc) + expect(matcher.failure_message).to eq( + ["Expected:\n $stderr: \"error\"\n", + " got:\n $stderr: \"unerror\"\n"] + ) + matcher = OutputMatcher.new(/base/, nil) + matcher.matches?(proc) + expect(matcher.failure_message).to eq( + ["Expected:\n $stdout: /base/\n", + " got:\n $stdout: \"unexpected\"\n"] + ) + matcher = OutputMatcher.new(nil, /octave/) + matcher.matches?(proc) + expect(matcher.failure_message).to eq( + ["Expected:\n $stderr: /octave/\n", + " got:\n $stderr: \"unerror\"\n"] + ) + end + + it "provides a useful negative failure message" do + proc = Proc.new { puts "expected"; $stderr.puts "error" } + matcher = OutputMatcher.new("expected", "error") + matcher.matches?(proc) + expect(matcher.negative_failure_message).to eq( + ["Expected output not to be:\n", " $stdout: \"expected\"\n $stderr: \"error\"\n"] + ) + matcher = OutputMatcher.new("expected", nil) + matcher.matches?(proc) + expect(matcher.negative_failure_message).to eq( + ["Expected output not to be:\n", " $stdout: \"expected\"\n"] + ) + matcher = OutputMatcher.new(nil, "error") + matcher.matches?(proc) + expect(matcher.negative_failure_message).to eq( + ["Expected output not to be:\n", " $stderr: \"error\"\n"] + ) + matcher = OutputMatcher.new(/expect/, nil) + matcher.matches?(proc) + expect(matcher.negative_failure_message).to eq( + ["Expected output not to be:\n", " $stdout: \"expected\"\n"] + ) + matcher = OutputMatcher.new(nil, /err/) + matcher.matches?(proc) + expect(matcher.negative_failure_message).to eq( + ["Expected output not to be:\n", " $stderr: \"error\"\n"] + ) + end +end diff --git a/spec/mspec/spec/matchers/output_to_fd_spec.rb b/spec/mspec/spec/matchers/output_to_fd_spec.rb new file mode 100644 index 0000000000..a39cab3206 --- /dev/null +++ b/spec/mspec/spec/matchers/output_to_fd_spec.rb @@ -0,0 +1,44 @@ +require 'spec_helper' +require 'mspec/expectations/expectations' +require 'mspec/matchers' + +RSpec.describe OutputToFDMatcher do + # Figure out how in the hell to achieve this + it "matches when running the block produces the expected output to the given FD" do + expect(OutputToFDMatcher.new("Hi\n", STDERR).matches?(lambda { $stderr.print "Hi\n" })).to eq(true) + end + + it "does not match if running the block does not produce the expected output to the FD" do + expect(OutputToFDMatcher.new("Hi\n", STDERR).matches?(lambda { $stderr.puts("Hello\n") })).to eq(false) + end + + it "propagate the exception if one is thrown while matching" do + exc = RuntimeError.new("propagates") + expect { + expect(OutputToFDMatcher.new("Hi\n", STDERR).matches?(lambda { + raise exc + })).to eq(false) + }.to raise_error(exc) + end + + it "defaults to matching against STDOUT" do + object = Object.new + object.extend MSpecMatchers + expect(object.send(:output_to_fd, "Hi\n").matches?(lambda { $stdout.print "Hi\n" })).to eq(true) + end + + it "accepts any IO instance" do + io = IO.new STDOUT.fileno + expect(OutputToFDMatcher.new("Hi\n", io).matches?(lambda { io.print "Hi\n" })).to eq(true) + end + + it "allows matching with a Regexp" do + s = "Hi there\n" + expect(OutputToFDMatcher.new(/Hi/, STDERR).matches?(lambda { $stderr.print s })).to eq(true) + expect(OutputToFDMatcher.new(/Hi?/, STDERR).matches?(lambda { $stderr.print s })).to eq(true) + expect(OutputToFDMatcher.new(/[hH]i?/, STDERR).matches?(lambda { $stderr.print s })).to eq(true) + expect(OutputToFDMatcher.new(/.*/, STDERR).matches?(lambda { $stderr.print s })).to eq(true) + expect(OutputToFDMatcher.new(/H.*?here/, STDERR).matches?(lambda { $stderr.print s })).to eq(true) + expect(OutputToFDMatcher.new(/Ahoy/, STDERR).matches?(lambda { $stderr.print s })).to eq(false) + end +end diff --git a/spec/mspec/spec/matchers/raise_error_spec.rb b/spec/mspec/spec/matchers/raise_error_spec.rb new file mode 100644 index 0000000000..3849c7dd2a --- /dev/null +++ b/spec/mspec/spec/matchers/raise_error_spec.rb @@ -0,0 +1,234 @@ +require 'spec_helper' + +class ExpectedException < Exception; end +class UnexpectedException < Exception; end + +RSpec.describe RaiseErrorMatcher do + before :each do + state = double("run state").as_null_object + allow(MSpec).to receive(:current).and_return(state) + end + + it "matches when the proc raises the expected exception" do + proc = Proc.new { raise ExpectedException } + matcher = RaiseErrorMatcher.new(ExpectedException, nil) + expect(matcher.matches?(proc)).to eq(true) + end + + it "executes its optional {/} block if matched" do + ensure_mspec_method(-> {}.method(:should)) + + run = false + -> { raise ExpectedException }.should PublicMSpecMatchers.raise_error { |error| + expect(error.class).to eq(ExpectedException) + run = true + } + expect(run).to eq(true) + end + + it "executes its optional do/end block if matched" do + ensure_mspec_method(-> {}.method(:should)) + + run = false + -> { raise ExpectedException }.should PublicMSpecMatchers.raise_error do |error| + expect(error.class).to eq(ExpectedException) + run = true + end + expect(run).to eq(true) + end + + it "matches when the proc raises the expected exception with the expected message" do + proc = Proc.new { raise ExpectedException, "message" } + matcher = RaiseErrorMatcher.new(ExpectedException, "message") + expect(matcher.matches?(proc)).to eq(true) + end + + it "matches when the proc raises the expected exception with a matching message" do + proc = Proc.new { raise ExpectedException, "some message" } + matcher = RaiseErrorMatcher.new(ExpectedException, /some/) + expect(matcher.matches?(proc)).to eq(true) + end + + it "does not match when the proc does not raise the expected exception" do + exc = UnexpectedException.new + matcher = RaiseErrorMatcher.new(ExpectedException, nil) + + expect(matcher.matching_exception?(exc)).to eq(false) + expect { + matcher.matches?(Proc.new { raise exc }) + }.to raise_error(UnexpectedException) + end + + it "does not match when the proc raises the expected exception with an unexpected message" do + exc = ExpectedException.new("unexpected") + matcher = RaiseErrorMatcher.new(ExpectedException, "expected") + + expect(matcher.matching_exception?(exc)).to eq(false) + expect { + matcher.matches?(Proc.new { raise exc }) + }.to raise_error(ExpectedException) + end + + it "does not match when the proc does not raise an exception" do + proc = Proc.new {} + matcher = RaiseErrorMatcher.new(ExpectedException, "expected") + expect(matcher.matches?(proc)).to eq(false) + end + + it "provides a useful failure message when the exception class differs" do + exc = UnexpectedException.new("message") + matcher = RaiseErrorMatcher.new(ExpectedException, "message") + + expect(matcher.matching_exception?(exc)).to eq(false) + begin + matcher.matches?(Proc.new { raise exc }) + rescue UnexpectedException => e + expect(matcher.failure_message).to eq( + ['Expected ExpectedException("message")', 'but got: UnexpectedException("message")'] + ) + expect(ExceptionState.new(nil, nil, e).message).to eq( + "Expected ExpectedException(\"message\")\nbut got: UnexpectedException(\"message\")" + ) + else + raise "no exception" + end + end + + it "provides a useful failure message when the proc raises the expected exception with an unexpected message" do + exc = ExpectedException.new("unexpected") + matcher = RaiseErrorMatcher.new(ExpectedException, "expected") + + expect(matcher.matching_exception?(exc)).to eq(false) + begin + matcher.matches?(Proc.new { raise exc }) + rescue ExpectedException => e + expect(matcher.failure_message).to eq( + ['Expected ExpectedException("expected")', 'but got: ExpectedException("unexpected")'] + ) + expect(ExceptionState.new(nil, nil, e).message).to eq( + "Expected ExpectedException(\"expected\")\nbut got: ExpectedException(\"unexpected\")" + ) + else + raise "no exception" + end + end + + it "provides a useful failure message when both the exception class and message differ" do + exc = UnexpectedException.new("unexpected") + matcher = RaiseErrorMatcher.new(ExpectedException, "expected") + + expect(matcher.matching_exception?(exc)).to eq(false) + begin + matcher.matches?(Proc.new { raise exc }) + rescue UnexpectedException => e + expect(matcher.failure_message).to eq( + ['Expected ExpectedException("expected")', 'but got: UnexpectedException("unexpected")'] + ) + expect(ExceptionState.new(nil, nil, e).message).to eq( + "Expected ExpectedException(\"expected\")\nbut got: UnexpectedException(\"unexpected\")" + ) + else + raise "no exception" + end + end + + it "provides a useful failure message when no exception is raised" do + proc = Proc.new { 120 } + matcher = RaiseErrorMatcher.new(ExpectedException, "expected") + matcher.matches?(proc) + expect(matcher.failure_message).to eq( + ['Expected ExpectedException("expected")', "but no exception was raised (120 was returned)"] + ) + end + + it "provides a useful failure message when no exception is raised and nil is returned" do + proc = Proc.new { nil } + matcher = RaiseErrorMatcher.new(ExpectedException, "expected") + matcher.matches?(proc) + expect(matcher.failure_message).to eq( + ['Expected ExpectedException("expected")', "but no exception was raised (nil was returned)"] + ) + end + + it "provides a useful failure message when no exception is raised and the result raises in #pretty_inspect" do + result = Object.new + def result.pretty_inspect + raise ArgumentError, "bad" + end + proc = Proc.new { result } + matcher = RaiseErrorMatcher.new(ExpectedException, "expected") + matcher.matches?(proc) + expect(matcher.failure_message).to eq( + ['Expected ExpectedException("expected")', 'but no exception was raised (#<Object>(#pretty_inspect raised #<ArgumentError: bad>) was returned)'] + ) + end + + it "provides a useful negative failure message" do + proc = Proc.new { raise ExpectedException, "expected" } + matcher = RaiseErrorMatcher.new(ExpectedException, "expected") + matcher.matches?(proc) + expect(matcher.negative_failure_message).to eq( + ['Expected to not get ExpectedException("expected")', ""] + ) + end + + it "provides a useful negative failure message for strict subclasses of the matched exception class" do + proc = Proc.new { raise UnexpectedException, "unexpected" } + matcher = RaiseErrorMatcher.new(Exception, nil) + matcher.matches?(proc) + expect(matcher.negative_failure_message).to eq( + ['Expected to not get Exception', 'but got: UnexpectedException'] + ) + end + + it "matches cause if given" do + cause = RuntimeError.new("foo") + proc = -> do + raise cause + rescue + raise "bar" + end + + matcher = RaiseErrorMatcher.new(RuntimeError, cause: cause) + expect(matcher.matches?(proc)).to eq(true) + end + + it "matches message and cause if given" do + cause = RuntimeError.new("foo") + proc = -> do + raise cause + rescue + raise "bar" + end + + matcher = RaiseErrorMatcher.new(RuntimeError, "bar", cause: cause) + expect(matcher.matches?(proc)).to eq(true) + end + + it "provides useful negative failure message when cause does not match" do + cause = RuntimeError.new("bar") + proc = -> do + raise "foo" + end + + matcher = RaiseErrorMatcher.new(RuntimeError, cause: cause) + + begin + matcher.matches?(proc) + rescue RuntimeError + expect(matcher.failure_message).to eq( + ['Expected RuntimeError(cause: #<RuntimeError: bar>)', 'but got: RuntimeError(cause: nil)'] + ) + end + + matcher = RaiseErrorMatcher.new(RuntimeError, "foo", cause: cause) + + begin + matcher.matches?(proc) + rescue RuntimeError + expect(matcher.failure_message).to eq( + ['Expected RuntimeError("foo", cause: #<RuntimeError: bar>)', 'but got: RuntimeError("foo", cause: nil)'] + ) + end + end +end diff --git a/spec/mspec/spec/matchers/respond_to_spec.rb b/spec/mspec/spec/matchers/respond_to_spec.rb new file mode 100644 index 0000000000..6f1cd8d148 --- /dev/null +++ b/spec/mspec/spec/matchers/respond_to_spec.rb @@ -0,0 +1,33 @@ +require 'spec_helper' +require 'mspec/expectations/expectations' +require 'mspec/matchers' + +RSpec.describe RespondToMatcher do + it "matches when actual does respond_to? expected" do + expect(RespondToMatcher.new(:to_s).matches?(Object.new)).to eq(true) + expect(RespondToMatcher.new(:inject).matches?([])).to eq(true) + expect(RespondToMatcher.new(:[]).matches?(1)).to eq(true) + expect(RespondToMatcher.new(:[]=).matches?("string")).to eq(true) + end + + it "does not match when actual does not respond_to? expected" do + expect(RespondToMatcher.new(:to_i).matches?(Object.new)).to eq(false) + expect(RespondToMatcher.new(:inject).matches?(1)).to eq(false) + expect(RespondToMatcher.new(:non_existent_method).matches?([])).to eq(false) + expect(RespondToMatcher.new(:[]=).matches?(1)).to eq(false) + end + + it "provides a useful failure message" do + matcher = RespondToMatcher.new(:non_existent_method) + matcher.matches?('string') + expect(matcher.failure_message).to eq([ + "Expected \"string\" (String)", "to respond to non_existent_method"]) + end + + it "provides a useful negative failure message" do + matcher = RespondToMatcher.new(:to_i) + matcher.matches?(4.0) + expect(matcher.negative_failure_message).to eq([ + "Expected 4.0 (Float)", "not to respond to to_i"]) + end +end diff --git a/spec/mspec/spec/matchers/signed_zero_spec.rb b/spec/mspec/spec/matchers/signed_zero_spec.rb new file mode 100644 index 0000000000..6d1c1007bc --- /dev/null +++ b/spec/mspec/spec/matchers/signed_zero_spec.rb @@ -0,0 +1,32 @@ +require 'spec_helper' +require 'mspec/expectations/expectations' +require 'mspec/matchers' + +RSpec.describe SignedZeroMatcher do + it "matches when actual is zero and has the correct sign" do + expect(SignedZeroMatcher.new(1).matches?(0.0)).to eq(true) + expect(SignedZeroMatcher.new(-1).matches?(-0.0)).to eq(true) + end + + it "does not match when actual is non-zero" do + expect(SignedZeroMatcher.new(1).matches?(1.0)).to eq(false) + expect(SignedZeroMatcher.new(-1).matches?(-1.0)).to eq(false) + end + + it "does not match when actual is zero but has the incorrect sign" do + expect(SignedZeroMatcher.new(1).matches?(-0.0)).to eq(false) + expect(SignedZeroMatcher.new(-1).matches?(0.0)).to eq(false) + end + + it "provides a useful failure message" do + matcher = SignedZeroMatcher.new(-1) + matcher.matches?(0.0) + expect(matcher.failure_message).to eq(["Expected 0.0", "to be -0.0"]) + end + + it "provides a useful negative failure message" do + matcher = SignedZeroMatcher.new(-1) + matcher.matches?(-0.0) + expect(matcher.negative_failure_message).to eq(["Expected -0.0", "not to be -0.0"]) + end +end diff --git a/spec/mspec/spec/mocks/mock_spec.rb b/spec/mspec/spec/mocks/mock_spec.rb new file mode 100644 index 0000000000..7426e0ff88 --- /dev/null +++ b/spec/mspec/spec/mocks/mock_spec.rb @@ -0,0 +1,529 @@ +# This is a bit awkward. Currently the way to verify that the +# opposites are true (for example a failure when the specified +# arguments are NOT provided) is to simply alter the particular +# spec to a failure condition. +require 'spec_helper' +require 'mspec/runner/mspec' +require 'mspec/mocks/mock' +require 'mspec/mocks/proxy' + +RSpec.describe Mock, ".mocks" do + it "returns a Hash" do + expect(Mock.mocks).to be_kind_of(Hash) + end +end + +RSpec.describe Mock, ".stubs" do + it "returns a Hash" do + expect(Mock.stubs).to be_kind_of(Hash) + end +end + +RSpec.describe Mock, ".replaced_name" do + it "returns the name for a method that is being replaced by a mock method" do + m = double('a fake id') + expect(Mock.replaced_name(Mock.replaced_key(m, :method_call))).to eq(:"__mspec_method_call__") + end +end + +RSpec.describe Mock, ".replaced_key" do + it "returns a key used internally by Mock" do + m = double('a fake id') + expect(Mock.replaced_key(m, :method_call)).to eq([m.object_id, :method_call]) + end +end + +RSpec.describe Mock, ".replaced?" do + before :each do + @mock = double('install_method') + allow(MSpec).to receive(:actions) + allow(MSpec).to receive(:current).and_return(double("spec state").as_null_object) + end + + it "returns true if a method has been stubbed on an object" do + Mock.install_method @mock, :method_call + expect(Mock.replaced?(Mock.replaced_key(@mock, :method_call))).to be_truthy + end + + it "returns true if a method has been mocked on an object" do + Mock.install_method @mock, :method_call, :stub + expect(Mock.replaced?(Mock.replaced_key(@mock, :method_call))).to be_truthy + end + + it "returns false if a method has not been stubbed or mocked" do + expect(Mock.replaced?(Mock.replaced_key(@mock, :method_call))).to be_falsey + end +end + +RSpec.describe Mock, ".name_or_inspect" do + before :each do + @mock = double("I have a #name") + end + + it "returns the value of @name if set" do + @mock.instance_variable_set(:@name, "Myself") + expect(Mock.name_or_inspect(@mock)).to eq("Myself") + end +end + +RSpec.describe Mock, ".install_method for mocks" do + before :each do + @mock = double('install_method') + allow(MSpec).to receive(:actions) + allow(MSpec).to receive(:current).and_return(double("spec state").as_null_object) + end + + after :each do + Mock.reset + end + + it "returns a MockProxy instance" do + expect(Mock.install_method(@mock, :method_call)).to be_an_instance_of(MockProxy) + end + + it "does not override a previously mocked method with the same name" do + Mock.install_method(@mock, :method_call).with(:a, :b).and_return(1) + Mock.install_method(@mock, :method_call).with(:c).and_return(2) + @mock.method_call(:a, :b) + @mock.method_call(:c) + expect { @mock.method_call(:d) }.to raise_error(SpecExpectationNotMetError) + end + + # This illustrates RSpec's behavior. This spec fails in mock call count verification + # on RSpec (i.e. Mock 'foo' expected :foo with (any args) once, but received it 0 times) + # and we mimic the behavior of RSpec. + # + # describe "A mock receiving multiple calls to #should_receive" do + # it "returns the first value mocked" do + # m = mock 'multiple #should_receive' + # m.should_receive(:foo).and_return(true) + # m.foo.should == true + # m.should_receive(:foo).and_return(false) + # m.foo.should == true + # end + # end + # + it "does not override a previously mocked method having the same arguments" do + Mock.install_method(@mock, :method_call).with(:a).and_return(true) + expect(@mock.method_call(:a)).to eq(true) + Mock.install_method(@mock, :method_call).with(:a).and_return(false) + expect(@mock.method_call(:a)).to eq(true) + expect { Mock.verify_count }.to raise_error(SpecExpectationNotMetError) + end + + it "properly sends #respond_to? calls to the aliased respond_to? method when not matching mock expectations" do + Mock.install_method(@mock, :respond_to?).with(:to_str).and_return('mock to_str') + Mock.install_method(@mock, :respond_to?).with(:to_int).and_return('mock to_int') + expect(@mock.respond_to?(:to_str)).to eq('mock to_str') + expect(@mock.respond_to?(:to_int)).to eq('mock to_int') + expect(@mock.respond_to?(:to_s)).to eq(true) + expect(@mock.respond_to?(:not_really_a_real_method_seriously)).to eq(false) + end + + it "adds to the expectation tally" do + state = double("run state").as_null_object + allow(state).to receive(:state).and_return(double("spec state")) + expect(MSpec).to receive(:current).and_return(state) + expect(MSpec).to receive(:actions).with(:expectation, state.state) + Mock.install_method(@mock, :method_call).and_return(1) + expect(@mock.method_call).to eq(1) + end + + it "registers that an expectation has been encountered" do + state = double("run state").as_null_object + allow(state).to receive(:state).and_return(double("spec state")) + expect(MSpec).to receive(:expectation) + Mock.install_method(@mock, :method_call).and_return(1) + expect(@mock.method_call).to eq(1) + end +end + +RSpec.describe Mock, ".install_method for stubs" do + before :each do + @mock = double('install_method') + allow(MSpec).to receive(:actions) + allow(MSpec).to receive(:current).and_return(double("spec state").as_null_object) + end + + after :each do + Mock.cleanup + end + + it "returns a MockProxy instance" do + expect(Mock.install_method(@mock, :method_call, :stub)).to be_an_instance_of(MockProxy) + end + + # This illustrates RSpec's behavior. This spec passes on RSpec and we mimic it + # + # describe "A mock receiving multiple calls to #stub" do + # it "returns the last value stubbed" do + # m = mock 'multiple #stub' + # m.stub(:foo).and_return(true) + # m.foo.should == true + # m.stub(:foo).and_return(false) + # m.foo.should == false + # end + # end + it "inserts new stubs before old stubs" do + Mock.install_method(@mock, :method_call, :stub).with(:a).and_return(true) + expect(@mock.method_call(:a)).to eq(true) + Mock.install_method(@mock, :method_call, :stub).with(:a).and_return(false) + expect(@mock.method_call(:a)).to eq(false) + Mock.verify_count + end + + it "does not add to the expectation tally" do + state = double("run state").as_null_object + allow(state).to receive(:state).and_return(double("spec state")) + expect(MSpec).not_to receive(:actions) + Mock.install_method(@mock, :method_call, :stub).and_return(1) + expect(@mock.method_call).to eq(1) + end +end + +RSpec.describe Mock, ".install_method" do + before :each do + @mock = double('install_method') + allow(MSpec).to receive(:actions) + allow(MSpec).to receive(:current).and_return(double("spec state").as_null_object) + end + + after :each do + Mock.cleanup + end + + it "does not alias a mocked or stubbed method when installing a new mock or stub" do + expect(@mock).not_to respond_to(:method_call) + + Mock.install_method @mock, :method_call + expect(@mock).to respond_to(:method_call) + expect(@mock).not_to respond_to(Mock.replaced_name(Mock.replaced_key(@mock, :method_call))) + + Mock.install_method @mock, :method_call, :stub + expect(@mock).to respond_to(:method_call) + expect(@mock).not_to respond_to(Mock.replaced_name(Mock.replaced_key(@mock, :method_call))) + end +end + +class MockAndRaiseError < Exception; end + +RSpec.describe Mock, ".verify_call" do + before :each do + allow(MSpec).to receive(:actions) + allow(MSpec).to receive(:current).and_return(double("spec state").as_null_object) + + @mock = double('verify_call') + @proxy = Mock.install_method @mock, :method_call + end + + after :each do + ScratchPad.clear + Mock.cleanup + end + + it "does not raise an exception when the mock method receives the expected arguments" do + @proxy.with(1, 'two', :three) + Mock.verify_call @mock, :method_call, 1, 'two', :three + end + + it "raises an SpecExpectationNotMetError when the mock method does not receive the expected arguments" do + @proxy.with(4, 2) + expect { + Mock.verify_call @mock, :method_call, 42 + }.to raise_error(SpecExpectationNotMetError) + end + + it "raises an SpecExpectationNotMetError when the mock method is called with arguments but expects none" do + expect { + @proxy.with(:no_args) + Mock.verify_call @mock, :method_call, "hello" + }.to raise_error(SpecExpectationNotMetError) + end + + it "raises an SpecExpectationNotMetError when the mock method is called with no arguments but expects some" do + @proxy.with("hello", "beautiful", "world") + expect { + Mock.verify_call @mock, :method_call + }.to raise_error(SpecExpectationNotMetError) + end + + it "does not raise an exception when the mock method is called with arguments and is expecting :any_args" do + @proxy.with(:any_args) + Mock.verify_call @mock, :method_call, 1, 2, 3 + end + + it "yields a passed block when it is expected to" do + @proxy.and_yield() + Mock.verify_call @mock, :method_call do + ScratchPad.record true + end + expect(ScratchPad.recorded).to eq(true) + end + + it "does not yield a passed block when it is not expected to" do + Mock.verify_call @mock, :method_call do + ScratchPad.record true + end + expect(ScratchPad.recorded).to eq(nil) + end + + it "can yield subsequently" do + @proxy.and_yield(1).and_yield(2).and_yield(3) + + ScratchPad.record [] + Mock.verify_call @mock, :method_call do |arg| + ScratchPad << arg + end + expect(ScratchPad.recorded).to eq([1, 2, 3]) + end + + it "can yield and return an expected value" do + @proxy.and_yield(1).and_return(3) + + expect(Mock.verify_call(@mock, :method_call) { |arg| ScratchPad.record arg }).to eq(3) + expect(ScratchPad.recorded).to eq(1) + end + + it "raises an exception when it is expected to yield but no block is given" do + @proxy.and_yield(1, 2, 3) + expect { + Mock.verify_call(@mock, :method_call) + }.to raise_error(SpecExpectationNotMetError) + end + + it "raises an exception when it is expected to yield more arguments than the block can take" do + @proxy.and_yield(1, 2, 3) + expect { + Mock.verify_call(@mock, :method_call) {|a, b|} + }.to raise_error(SpecExpectationNotMetError) + end + + it "does not raise an exception when it is expected to yield to a block that can take any number of arguments" do + @proxy.and_yield(1, 2, 3) + expect { + Mock.verify_call(@mock, :method_call) {|*a|} + }.not_to raise_error + end + + it "raises an exception when expected to" do + @proxy.and_raise(MockAndRaiseError) + expect { + Mock.verify_call @mock, :method_call + }.to raise_error(MockAndRaiseError) + end +end + +RSpec.describe Mock, ".verify_call mixing mocks and stubs" do + before :each do + allow(MSpec).to receive(:actions) + allow(MSpec).to receive(:current).and_return(double("spec state").as_null_object) + + @mock = double('verify_call') + end + + after :each do + ScratchPad.clear + Mock.cleanup + end + + it "checks the mock arguments when a mock is defined after a stub" do + Mock.install_method @mock, :method_call, :stub + Mock.install_method(@mock, :method_call, :mock).with("arg") + + expect { + @mock.method_call + }.to raise_error(SpecExpectationNotMetError, /called with unexpected arguments \(\)/) + + expect { + @mock.method_call("a", "b") + }.to raise_error(SpecExpectationNotMetError, /called with unexpected arguments \("a", "b"\)/) + + expect { + @mock.method_call("foo") + }.to raise_error(SpecExpectationNotMetError, /called with unexpected arguments \("foo"\)/) + + @mock.method_call("arg") + end + + it "checks the mock arguments when a stub is defined after a mock" do + Mock.install_method(@mock, :method_call, :mock).with("arg") + Mock.install_method @mock, :method_call, :stub + + expect { + @mock.method_call + }.to raise_error(SpecExpectationNotMetError, /called with unexpected arguments \(\)/) + + expect { + @mock.method_call("a", "b") + }.to raise_error(SpecExpectationNotMetError, /called with unexpected arguments \("a", "b"\)/) + + expect { + @mock.method_call("foo") + }.to raise_error(SpecExpectationNotMetError, /called with unexpected arguments \("foo"\)/) + + @mock.method_call("arg") + end +end + +RSpec.describe Mock, ".verify_count" do + before :each do + allow(MSpec).to receive(:actions) + allow(MSpec).to receive(:current).and_return(double("spec state").as_null_object) + + @mock = double('verify_count') + @proxy = Mock.install_method @mock, :method_call + end + + after :each do + Mock.cleanup + end + + it "does not raise an exception when the mock receives at least the expected number of calls" do + @proxy.at_least(2) + @mock.method_call + @mock.method_call + Mock.verify_count + end + + it "raises an SpecExpectationNotMetError when the mock receives less than at least the expected number of calls" do + @proxy.at_least(2) + @mock.method_call + expect { Mock.verify_count }.to raise_error(SpecExpectationNotMetError) + end + + it "does not raise an exception when the mock receives at most the expected number of calls" do + @proxy.at_most(2) + @mock.method_call + @mock.method_call + Mock.verify_count + end + + it "raises an SpecExpectationNotMetError when the mock receives more than at most the expected number of calls" do + @proxy.at_most(2) + @mock.method_call + @mock.method_call + @mock.method_call + expect { Mock.verify_count }.to raise_error(SpecExpectationNotMetError) + end + + it "does not raise an exception when the mock receives exactly the expected number of calls" do + @proxy.exactly(2) + @mock.method_call + @mock.method_call + Mock.verify_count + end + + it "raises an SpecExpectationNotMetError when the mock receives less than exactly the expected number of calls" do + @proxy.exactly(2) + @mock.method_call + expect { Mock.verify_count }.to raise_error(SpecExpectationNotMetError) + end + + it "raises an SpecExpectationNotMetError when the mock receives more than exactly the expected number of calls" do + @proxy.exactly(2) + @mock.method_call + @mock.method_call + @mock.method_call + expect { Mock.verify_count }.to raise_error(SpecExpectationNotMetError) + end +end + +RSpec.describe Mock, ".verify_count mixing mocks and stubs" do + before :each do + allow(MSpec).to receive(:actions) + allow(MSpec).to receive(:current).and_return(double("spec state").as_null_object) + + @mock = double('verify_count') + end + + after :each do + Mock.cleanup + end + + it "does not raise an exception for a stubbed method that is never called" do + Mock.install_method @mock, :method_call, :stub + Mock.verify_count + end + + it "verifies the calls to the mocked method when a mock is defined after a stub" do + Mock.install_method @mock, :method_call, :stub + Mock.install_method @mock, :method_call, :mock + + expect { + Mock.verify_count + }.to raise_error(SpecExpectationNotMetError, /received it 0 times/) + + @mock.method_call + Mock.verify_count + end + + it "verifies the calls to the mocked method when a mock is defined before a stub" do + Mock.install_method @mock, :method_call, :mock + Mock.install_method @mock, :method_call, :stub + + expect { + Mock.verify_count + }.to raise_error(SpecExpectationNotMetError, /received it 0 times/) + + @mock.method_call + Mock.verify_count + end +end + +RSpec.describe Mock, ".cleanup" do + before :each do + allow(MSpec).to receive(:actions) + allow(MSpec).to receive(:current).and_return(double("spec state").as_null_object) + + @mock = double('cleanup') + @proxy = Mock.install_method @mock, :method_call + end + + after :each do + Mock.cleanup + end + + it "removes the mock method call if it did not override an existing method" do + expect(@mock).to respond_to(:method_call) + + Mock.cleanup + expect(@mock).not_to respond_to(:method_call) + end + + it "removes the replaced method if the mock method overrides an existing method" do + def @mock.already_here() :hey end + expect(@mock).to respond_to(:already_here) + replaced_name = Mock.replaced_name(Mock.replaced_key(@mock, :already_here)) + Mock.install_method @mock, :already_here + expect(@mock).to respond_to(replaced_name) + + Mock.cleanup + expect(@mock).not_to respond_to(replaced_name) + expect(@mock).to respond_to(:already_here) + expect(@mock.already_here).to eq(:hey) + end + + it "removes all mock expectations" do + expect(Mock.mocks).to eq({ Mock.replaced_key(@mock, :method_call) => [@proxy] }) + Mock.cleanup + expect(Mock.mocks).to eq({}) + end + + it "removes all stubs" do + Mock.cleanup # remove @proxy + @stub = Mock.install_method @mock, :method_call, :stub + expect(Mock.stubs).to eq({ Mock.replaced_key(@mock, :method_call) => [@stub] }) + Mock.cleanup + expect(Mock.stubs).to eq({}) + end + + it "removes the replaced name for mocks" do + replaced_key = Mock.replaced_key(@mock, :method_call) + expect(Mock).to receive(:clear_replaced).with(replaced_key) + + expect(Mock.replaced?(replaced_key)).to be_truthy + + Mock.cleanup + expect(Mock.replaced?(replaced_key)).to be_falsey + end +end diff --git a/spec/mspec/spec/mocks/proxy_spec.rb b/spec/mspec/spec/mocks/proxy_spec.rb new file mode 100644 index 0000000000..b994634694 --- /dev/null +++ b/spec/mspec/spec/mocks/proxy_spec.rb @@ -0,0 +1,405 @@ +require 'spec_helper' +require 'mspec/mocks/proxy' + +RSpec.describe MockObject, ".new" do + it "creates a new mock object" do + m = MockObject.new('not a null object') + expect { m.not_a_method }.to raise_error(NoMethodError) + end + + it "creates a new mock object that follows the NullObject pattern" do + m = MockObject.new('null object', :null_object => true) + expect(m.not_really_a_method).to equal(m) + end +end + +RSpec.describe MockProxy, ".new" do + it "creates a mock proxy by default" do + expect(MockProxy.new.mock?).to be_truthy + end + + it "creates a stub proxy by request" do + expect(MockProxy.new(:stub).stub?).to be_truthy + end + + it "sets the call expectation to 1 call for a mock" do + expect(MockProxy.new.count).to eq([:exactly, 1]) + end + + it "sets the call expectation to any number of times for a stub" do + expect(MockProxy.new(:stub).count).to eq([:any_number_of_times, 0]) + end +end + +RSpec.describe MockProxy, "#count" do + before :each do + @proxy = MockProxy.new + end + + it "returns the expected number of calls the mock should receive" do + expect(@proxy.count).to eq([:exactly, 1]) + expect(@proxy.at_least(3).count).to eq([:at_least, 3]) + end +end + +RSpec.describe MockProxy, "#arguments" do + before :each do + @proxy = MockProxy.new + end + + it "returns the expected arguments" do + expect(@proxy.arguments).to eq(:any_args) + end +end + +RSpec.describe MockProxy, "#with" do + before :each do + @proxy = MockProxy.new + end + + it "returns self" do + expect(@proxy.with(:a)).to be_equal(@proxy) + end + + it "raises an ArgumentError if no arguments are given" do + expect { @proxy.with }.to raise_error(ArgumentError) + end + + it "accepts any number of arguments" do + expect(@proxy.with(1, 2, 3)).to be_an_instance_of(MockProxy) + expect(@proxy.arguments).to eq([1,2,3]) + end +end + +RSpec.describe MockProxy, "#once" do + before :each do + @proxy = MockProxy.new + end + + it "returns self" do + expect(@proxy.once).to be_equal(@proxy) + end + + it "sets the expected calls to 1" do + @proxy.once + expect(@proxy.count).to eq([:exactly, 1]) + end + + it "accepts no arguments" do + expect { @proxy.once(:a) }.to raise_error + end +end + +RSpec.describe MockProxy, "#twice" do + before :each do + @proxy = MockProxy.new + end + + it "returns self" do + expect(@proxy.twice).to be_equal(@proxy) + end + + it "sets the expected calls to 2" do + @proxy.twice + expect(@proxy.count).to eq([:exactly, 2]) + end + + it "accepts no arguments" do + expect { @proxy.twice(:b) }.to raise_error + end +end + +RSpec.describe MockProxy, "#exactly" do + before :each do + @proxy = MockProxy.new + end + + it "returns self" do + expect(@proxy.exactly(2)).to be_equal(@proxy) + end + + it "sets the expected calls to exactly n" do + @proxy.exactly(5) + expect(@proxy.count).to eq([:exactly, 5]) + end + + it "does not accept an argument that Integer() cannot convert" do + expect { @proxy.exactly('x') }.to raise_error + end +end + +RSpec.describe MockProxy, "#at_least" do + before :each do + @proxy = MockProxy.new + end + + it "returns self" do + expect(@proxy.at_least(3)).to be_equal(@proxy) + end + + it "sets the expected calls to at least n" do + @proxy.at_least(3) + expect(@proxy.count).to eq([:at_least, 3]) + end + + it "accepts :once :twice" do + @proxy.at_least(:once) + expect(@proxy.count).to eq([:at_least, 1]) + @proxy.at_least(:twice) + expect(@proxy.count).to eq([:at_least, 2]) + end + + it "does not accept an argument that Integer() cannot convert" do + expect { @proxy.at_least('x') }.to raise_error + end +end + +RSpec.describe MockProxy, "#at_most" do + before :each do + @proxy = MockProxy.new + end + + it "returns self" do + expect(@proxy.at_most(2)).to be_equal(@proxy) + end + + it "sets the expected calls to at most n" do + @proxy.at_most(2) + expect(@proxy.count).to eq([:at_most, 2]) + end + + it "accepts :once, :twice" do + @proxy.at_most(:once) + expect(@proxy.count).to eq([:at_most, 1]) + @proxy.at_most(:twice) + expect(@proxy.count).to eq([:at_most, 2]) + end + + it "does not accept an argument that Integer() cannot convert" do + expect { @proxy.at_most('x') }.to raise_error + end +end + +RSpec.describe MockProxy, "#any_number_of_times" do + before :each do + @proxy = MockProxy.new + end + + it "returns self" do + expect(@proxy.any_number_of_times).to be_equal(@proxy) + end + + it "sets the expected calls to any number of times" do + @proxy.any_number_of_times + expect(@proxy.count).to eq([:any_number_of_times, 0]) + end + + it "does not accept an argument" do + expect { @proxy.any_number_of_times(2) }.to raise_error + end +end + +RSpec.describe MockProxy, "#and_return" do + before :each do + @proxy = MockProxy.new + end + + it "returns self" do + expect(@proxy.and_return(false)).to equal(@proxy) + end + + it "sets the expected return value" do + @proxy.and_return(false) + expect(@proxy.returning).to eq(false) + end + + it "accepts any number of return values" do + @proxy.and_return(1, 2, 3) + expect(@proxy.returning).to eq(1) + expect(@proxy.returning).to eq(2) + expect(@proxy.returning).to eq(3) + end + + it "implicitly sets the expected number of calls" do + @proxy.and_return(1, 2, 3) + expect(@proxy.count).to eq([:exactly, 3]) + end + + it "only sets the expected number of calls if it is higher than what is already set" do + @proxy.at_least(5).times.and_return(1, 2, 3) + expect(@proxy.count).to eq([:at_least, 5]) + + @proxy.at_least(2).times.and_return(1, 2, 3) + expect(@proxy.count).to eq([:at_least, 3]) + end +end + +RSpec.describe MockProxy, "#returning" do + before :each do + @proxy = MockProxy.new + end + + it "returns nil by default" do + expect(@proxy.returning).to be_nil + end + + it "returns the value set by #and_return" do + @proxy.and_return(2) + expect(@proxy.returning).to eq(2) + expect(@proxy.returning).to eq(2) + end + + it "returns a sequence of values set by #and_return" do + @proxy.and_return(1,2,3,4) + expect(@proxy.returning).to eq(1) + expect(@proxy.returning).to eq(2) + expect(@proxy.returning).to eq(3) + expect(@proxy.returning).to eq(4) + expect(@proxy.returning).to eq(4) + expect(@proxy.returning).to eq(4) + end +end + +RSpec.describe MockProxy, "#calls" do + before :each do + @proxy = MockProxy.new + end + + it "returns the number of times the proxy is called" do + expect(@proxy.calls).to eq(0) + end +end + +RSpec.describe MockProxy, "#called" do + before :each do + @proxy = MockProxy.new + end + + it "increments the number of times the proxy is called" do + @proxy.called + @proxy.called + expect(@proxy.calls).to eq(2) + end +end + +RSpec.describe MockProxy, "#times" do + before :each do + @proxy = MockProxy.new + end + + it "is a no-op" do + expect(@proxy.times).to eq(@proxy) + end +end + +RSpec.describe MockProxy, "#stub?" do + it "returns true if the proxy is created as a stub" do + expect(MockProxy.new(:stub).stub?).to be_truthy + end + + it "returns false if the proxy is created as a mock" do + expect(MockProxy.new(:mock).stub?).to be_falsey + end +end + +RSpec.describe MockProxy, "#mock?" do + it "returns true if the proxy is created as a mock" do + expect(MockProxy.new(:mock).mock?).to be_truthy + end + + it "returns false if the proxy is created as a stub" do + expect(MockProxy.new(:stub).mock?).to be_falsey + end +end + +RSpec.describe MockProxy, "#and_yield" do + before :each do + @proxy = MockProxy.new + end + + it "returns self" do + expect(@proxy.and_yield(false)).to equal(@proxy) + end + + it "sets the expected values to yield" do + expect(@proxy.and_yield(1).yielding).to eq([[1]]) + end + + it "accepts multiple values to yield" do + expect(@proxy.and_yield(1, 2, 3).yielding).to eq([[1, 2, 3]]) + end +end + +RSpec.describe MockProxy, "#raising" do + before :each do + @proxy = MockProxy.new + end + + it "returns nil by default" do + expect(@proxy.raising).to be_nil + end + + it "returns the exception object passed to #and_raise" do + exc = double("exception") + @proxy.and_raise(exc) + expect(@proxy.raising).to equal(exc) + end + + it "returns an instance of RuntimeError when a String is passed to #and_raise" do + @proxy.and_raise("an error") + exc = @proxy.raising + expect(exc).to be_an_instance_of(RuntimeError) + expect(exc.message).to eq("an error") + end +end + +RSpec.describe MockProxy, "#yielding" do + before :each do + @proxy = MockProxy.new + end + + it "returns an empty array by default" do + expect(@proxy.yielding).to eq([]) + end + + it "returns an array of arrays of values the proxy should yield" do + @proxy.and_yield(3) + expect(@proxy.yielding).to eq([[3]]) + end + + it "returns an accumulation of arrays of values the proxy should yield" do + @proxy.and_yield(1).and_yield(2, 3) + expect(@proxy.yielding).to eq([[1], [2, 3]]) + end +end + +RSpec.describe MockProxy, "#yielding?" do + before :each do + @proxy = MockProxy.new + end + + it "returns false if the proxy is not yielding" do + expect(@proxy.yielding?).to be_falsey + end + + it "returns true if the proxy is yielding" do + @proxy.and_yield(1) + expect(@proxy.yielding?).to be_truthy + end +end + +RSpec.describe MockIntObject, "#to_int" do + before :each do + @int = MockIntObject.new(10) + end + + it "returns the number if to_int is called" do + expect(@int.to_int).to eq(10) + expect(@int.count).to eq([:at_least, 1]) + end + + it "tries to convert the target to int if to_int is called" do + expect(MockIntObject.new(@int).to_int).to eq(10) + expect(@int.count).to eq([:at_least, 1]) + end +end diff --git a/spec/mspec/spec/runner/actions/filter_spec.rb b/spec/mspec/spec/runner/actions/filter_spec.rb new file mode 100644 index 0000000000..7582b31c1d --- /dev/null +++ b/spec/mspec/spec/runner/actions/filter_spec.rb @@ -0,0 +1,84 @@ +require File.dirname(__FILE__) + '/../../spec_helper' +require 'mspec/runner/actions/filter' +require 'mspec/runner/mspec' +require 'mspec/runner/tag' + +RSpec.describe ActionFilter do + it "creates a filter when not passed a description" do + expect(MatchFilter).not_to receive(:new) + ActionFilter.new(nil, nil) + end + + it "creates a filter from a single description" do + expect(MatchFilter).to receive(:new).with(nil, "match me") + ActionFilter.new(nil, "match me") + end + + it "creates a filter from an array of descriptions" do + expect(MatchFilter).to receive(:new).with(nil, "match me", "again") + ActionFilter.new(nil, ["match me", "again"]) + end +end + +RSpec.describe ActionFilter, "#===" do + before :each do + allow(MSpec).to receive(:read_tags).and_return(["match"]) + @action = ActionFilter.new(nil, ["catch", "if you"]) + end + + it "returns false if there are no filters" do + action = ActionFilter.new + expect(action.===("anything")).to eq(false) + end + + it "returns true if the argument matches any of the descriptions" do + expect(@action.===("catch")).to eq(true) + expect(@action.===("if you can")).to eq(true) + end + + it "returns false if the argument does not match any of the descriptions" do + expect(@action.===("patch me")).to eq(false) + expect(@action.===("if I can")).to eq(false) + end +end + +RSpec.describe ActionFilter, "#load" do + before :each do + @tag = SpecTag.new "tag(comment):description" + end + + it "creates a filter from a single tag" do + expect(MSpec).to receive(:read_tags).with(["tag"]).and_return([@tag]) + expect(MatchFilter).to receive(:new).with(nil, "description") + ActionFilter.new("tag", nil).load + end + + it "creates a filter from an array of tags" do + expect(MSpec).to receive(:read_tags).with(["tag", "key"]).and_return([@tag]) + expect(MatchFilter).to receive(:new).with(nil, "description") + ActionFilter.new(["tag", "key"], nil).load + end + + it "creates a filter from both tags and descriptions" do + expect(MSpec).to receive(:read_tags).and_return([@tag]) + filter = ActionFilter.new("tag", ["match me", "again"]) + expect(MatchFilter).to receive(:new).with(nil, "description") + filter.load + end +end + +RSpec.describe ActionFilter, "#register" do + it "registers itself with MSpec for the :load actions" do + filter = ActionFilter.new + expect(MSpec).to receive(:register).with(:load, filter) + filter.register + end +end + +RSpec.describe ActionFilter, "#unregister" do + it "unregisters itself with MSpec for the :load actions" do + filter = ActionFilter.new + expect(MSpec).to receive(:unregister).with(:load, filter) + filter.unregister + end +end diff --git a/spec/mspec/spec/runner/actions/tag_spec.rb b/spec/mspec/spec/runner/actions/tag_spec.rb new file mode 100644 index 0000000000..738e9a18c9 --- /dev/null +++ b/spec/mspec/spec/runner/actions/tag_spec.rb @@ -0,0 +1,313 @@ +require File.dirname(__FILE__) + '/../../spec_helper' +require 'mspec/runner/actions/tag' +require 'mspec/runner/mspec' +require 'mspec/runner/example' +require 'mspec/runner/tag' + +RSpec.describe TagAction, ".new" do + it "creates an MatchFilter with its tag and desc arguments" do + filter = double('action filter').as_null_object + expect(MatchFilter).to receive(:new).with(nil, "some", "thing").and_return(filter) + TagAction.new :add, :all, nil, nil, ["tag", "key"], ["some", "thing"] + end +end + +RSpec.describe TagAction, "#===" do + before :each do + allow(MSpec).to receive(:read_tags).and_return(["match"]) + @action = TagAction.new :add, :fail, nil, nil, nil, ["catch", "if you"] + end + + it "returns true if there are no filters" do + action = TagAction.new :add, :all, nil, nil + expect(action.===("anything")).to eq(true) + end + + it "returns true if the argument matches any of the descriptions" do + expect(@action.===("catch")).to eq(true) + expect(@action.===("if you can")).to eq(true) + end + + it "returns false if the argument does not match any of the descriptions" do + expect(@action.===("patch me")).to eq(false) + expect(@action.===("if I can")).to eq(false) + end +end + +RSpec.describe TagAction, "#exception?" do + before :each do + @action = TagAction.new :add, :fail, nil, nil, nil, nil + end + + it "returns false if no exception has been raised while evaluating an example" do + expect(@action.exception?).to be_falsey + end + + it "returns true if an exception was raised while evaluating an example" do + @action.exception ExceptionState.new nil, nil, Exception.new("failed") + expect(@action.exception?).to be_truthy + end +end + +RSpec.describe TagAction, "#outcome?" do + before :each do + allow(MSpec).to receive(:read_tags).and_return([]) + @exception = ExceptionState.new nil, nil, Exception.new("failed") + end + + it "returns true if outcome is :fail and the spec fails" do + action = TagAction.new :add, :fail, nil, nil, nil, nil + action.exception @exception + expect(action.outcome?).to eq(true) + end + + it "returns false if the outcome is :fail and the spec passes" do + action = TagAction.new :add, :fail, nil, nil, nil, nil + expect(action.outcome?).to eq(false) + end + + it "returns true if the outcome is :pass and the spec passes" do + action = TagAction.new :del, :pass, nil, nil, nil, nil + expect(action.outcome?).to eq(true) + end + + it "returns false if the outcome is :pass and the spec fails" do + action = TagAction.new :del, :pass, nil, nil, nil, nil + action.exception @exception + expect(action.outcome?).to eq(false) + end + + it "returns true if the outcome is :all" do + action = TagAction.new :add, :all, nil, nil, nil, nil + action.exception @exception + expect(action.outcome?).to eq(true) + end +end + +RSpec.describe TagAction, "#before" do + it "resets the #exception? flag to false" do + action = TagAction.new :add, :fail, nil, nil, nil, nil + expect(action.exception?).to be_falsey + action.exception ExceptionState.new(nil, nil, Exception.new("Fail!")) + expect(action.exception?).to be_truthy + action.before(ExampleState.new(ContextState.new("describe"), "it")) + expect(action.exception?).to be_falsey + end +end + +RSpec.describe TagAction, "#exception" do + it "sets the #exception? flag" do + action = TagAction.new :add, :fail, nil, nil, nil, nil + expect(action.exception?).to be_falsey + action.exception ExceptionState.new(nil, nil, Exception.new("Fail!")) + expect(action.exception?).to be_truthy + end +end + +RSpec.describe TagAction, "#after when action is :add" do + before :each do + allow(MSpec).to receive(:read_tags).and_return([]) + context = ContextState.new "Catch#me" + @state = ExampleState.new context, "if you can" + @tag = SpecTag.new "tag(comment):Catch#me if you can" + allow(SpecTag).to receive(:new).and_return(@tag) + @exception = ExceptionState.new nil, nil, Exception.new("failed") + end + + it "does not write a tag if the description does not match" do + expect(MSpec).not_to receive(:write_tag) + action = TagAction.new :add, :all, "tag", "comment", nil, "match" + action.after @state + end + + it "does not write a tag if outcome is :fail and the spec passed" do + expect(MSpec).not_to receive(:write_tag) + action = TagAction.new :add, :fail, "tag", "comment", nil, "can" + action.after @state + end + + it "writes a tag if the outcome is :fail and the spec failed" do + expect(MSpec).to receive(:write_tag).with(@tag) + action = TagAction.new :add, :fail, "tag", "comment", nil, "can" + action.exception @exception + action.after @state + end + + it "does not write a tag if outcome is :pass and the spec failed" do + expect(MSpec).not_to receive(:write_tag) + action = TagAction.new :add, :pass, "tag", "comment", nil, "can" + action.exception @exception + action.after @state + end + + it "writes a tag if the outcome is :pass and the spec passed" do + expect(MSpec).to receive(:write_tag).with(@tag) + action = TagAction.new :add, :pass, "tag", "comment", nil, "can" + action.after @state + end + + it "writes a tag if the outcome is :all" do + expect(MSpec).to receive(:write_tag).with(@tag) + action = TagAction.new :add, :all, "tag", "comment", nil, "can" + action.after @state + end +end + +RSpec.describe TagAction, "#after when action is :del" do + before :each do + allow(MSpec).to receive(:read_tags).and_return([]) + context = ContextState.new "Catch#me" + @state = ExampleState.new context, "if you can" + @tag = SpecTag.new "tag(comment):Catch#me if you can" + allow(SpecTag).to receive(:new).and_return(@tag) + @exception = ExceptionState.new nil, nil, Exception.new("failed") + end + + it "does not delete a tag if the description does not match" do + expect(MSpec).not_to receive(:delete_tag) + action = TagAction.new :del, :all, "tag", "comment", nil, "match" + action.after @state + end + + it "does not delete a tag if outcome is :fail and the spec passed" do + expect(MSpec).not_to receive(:delete_tag) + action = TagAction.new :del, :fail, "tag", "comment", nil, "can" + action.after @state + end + + it "deletes a tag if the outcome is :fail and the spec failed" do + expect(MSpec).to receive(:delete_tag).with(@tag) + action = TagAction.new :del, :fail, "tag", "comment", nil, "can" + action.exception @exception + action.after @state + end + + it "does not delete a tag if outcome is :pass and the spec failed" do + expect(MSpec).not_to receive(:delete_tag) + action = TagAction.new :del, :pass, "tag", "comment", nil, "can" + action.exception @exception + action.after @state + end + + it "deletes a tag if the outcome is :pass and the spec passed" do + expect(MSpec).to receive(:delete_tag).with(@tag) + action = TagAction.new :del, :pass, "tag", "comment", nil, "can" + action.after @state + end + + it "deletes a tag if the outcome is :all" do + expect(MSpec).to receive(:delete_tag).with(@tag) + action = TagAction.new :del, :all, "tag", "comment", nil, "can" + action.after @state + end +end + +RSpec.describe TagAction, "#finish" do + before :each do + $stdout = @out = IOStub.new + context = ContextState.new "Catch#me" + @state = ExampleState.new context, "if you can" + allow(MSpec).to receive(:write_tag).and_return(true) + allow(MSpec).to receive(:delete_tag).and_return(true) + end + + after :each do + $stdout = STDOUT + end + + it "reports no specs tagged if none where tagged" do + action = TagAction.new :add, :fail, "tag", "comment", nil, "can" + allow(action).to receive(:outcome?).and_return(false) + action.after @state + action.finish + expect(@out).to eq("\nTagAction: no specs were tagged with 'tag'\n") + end + + it "reports no specs tagged if none where tagged" do + action = TagAction.new :del, :fail, "tag", "comment", nil, "can" + allow(action).to receive(:outcome?).and_return(false) + action.after @state + action.finish + expect(@out).to eq("\nTagAction: no tags 'tag' were deleted\n") + end + + it "reports the spec descriptions that were tagged" do + action = TagAction.new :add, :fail, "tag", "comment", nil, "can" + allow(action).to receive(:outcome?).and_return(true) + action.after @state + action.finish + expect(@out).to eq(%[ +TagAction: specs tagged with 'tag': + +Catch#me if you can +]) + end + + it "reports the spec descriptions for the tags that were deleted" do + action = TagAction.new :del, :fail, "tag", "comment", nil, "can" + allow(action).to receive(:outcome?).and_return(true) + action.after @state + action.finish + expect(@out).to eq(%[ +TagAction: tag 'tag' deleted for specs: + +Catch#me if you can +]) + end +end + +RSpec.describe TagAction, "#register" do + before :each do + allow(MSpec).to receive(:register) + allow(MSpec).to receive(:read_tags).and_return([]) + @action = TagAction.new :add, :all, nil, nil, nil, nil + end + + it "registers itself with MSpec for the :before event" do + expect(MSpec).to receive(:register).with(:before, @action) + @action.register + end + + it "registers itself with MSpec for the :after event" do + expect(MSpec).to receive(:register).with(:after, @action) + @action.register + end + + it "registers itself with MSpec for the :exception event" do + expect(MSpec).to receive(:register).with(:exception, @action) + @action.register + end + + it "registers itself with MSpec for the :finish event" do + expect(MSpec).to receive(:register).with(:finish, @action) + @action.register + end +end + +RSpec.describe TagAction, "#unregister" do + before :each do + allow(MSpec).to receive(:unregister) + allow(MSpec).to receive(:read_tags).and_return([]) + @action = TagAction.new :add, :all, nil, nil, nil, nil + end + + it "unregisters itself with MSpec for the :before event" do + expect(MSpec).to receive(:unregister).with(:before, @action) + @action.unregister + end + + it "unregisters itself with MSpec for the :after event" do + expect(MSpec).to receive(:unregister).with(:after, @action) + @action.unregister + end + + it "unregisters itself with MSpec for the :exception event" do + expect(MSpec).to receive(:unregister).with(:exception, @action) + @action.unregister + end + + it "unregisters itself with MSpec for the :finish event" do + expect(MSpec).to receive(:unregister).with(:finish, @action) + @action.unregister + end +end diff --git a/spec/mspec/spec/runner/actions/taglist_spec.rb b/spec/mspec/spec/runner/actions/taglist_spec.rb new file mode 100644 index 0000000000..b6a5400f7d --- /dev/null +++ b/spec/mspec/spec/runner/actions/taglist_spec.rb @@ -0,0 +1,152 @@ +require File.dirname(__FILE__) + '/../../spec_helper' +require 'mspec/runner/actions/taglist' +require 'mspec/runner/mspec' +require 'mspec/runner/example' +require 'mspec/runner/tag' + +RSpec.describe TagListAction, "#include?" do + it "returns true" do + expect(TagListAction.new.include?(:anything)).to be_truthy + end +end + +RSpec.describe TagListAction, "#===" do + before :each do + tag = SpecTag.new "fails:description" + allow(MSpec).to receive(:read_tags).and_return([tag]) + @filter = double("MatchFilter").as_null_object + allow(MatchFilter).to receive(:new).and_return(@filter) + @action = TagListAction.new + @action.load + end + + it "returns true if filter === string returns true" do + expect(@filter).to receive(:===).with("str").and_return(true) + expect(@action.===("str")).to be_truthy + end + + it "returns false if filter === string returns false" do + expect(@filter).to receive(:===).with("str").and_return(false) + expect(@action.===("str")).to be_falsey + end +end + +RSpec.describe TagListAction, "#start" do + before :each do + @stdout = $stdout + $stdout = IOStub.new + end + + after :each do + $stdout = @stdout + end + + it "prints a banner for specific tags" do + action = TagListAction.new ["fails", "unstable"] + action.start + expect($stdout).to eq("\nListing specs tagged with 'fails', 'unstable'\n\n") + end + + it "prints a banner for all tags" do + action = TagListAction.new + action.start + expect($stdout).to eq("\nListing all tagged specs\n\n") + end +end + +RSpec.describe TagListAction, "#load" do + before :each do + @t1 = SpecTag.new "fails:I fail" + @t2 = SpecTag.new "unstable:I'm unstable" + end + + it "creates a MatchFilter for matching tags" do + expect(MSpec).to receive(:read_tags).with(["fails"]).and_return([@t1]) + expect(MatchFilter).to receive(:new).with(nil, "I fail") + TagListAction.new(["fails"]).load + end + + it "creates a MatchFilter for all tags" do + expect(MSpec).to receive(:read_tags).and_return([@t1, @t2]) + expect(MatchFilter).to receive(:new).with(nil, "I fail", "I'm unstable") + TagListAction.new.load + end + + it "does not create a MatchFilter if there are no matching tags" do + allow(MSpec).to receive(:read_tags).and_return([]) + expect(MatchFilter).not_to receive(:new) + TagListAction.new(["fails"]).load + end +end + +RSpec.describe TagListAction, "#after" do + before :each do + @stdout = $stdout + $stdout = IOStub.new + + @state = double("ExampleState") + allow(@state).to receive(:description).and_return("str") + + @action = TagListAction.new + end + + after :each do + $stdout = @stdout + end + + it "prints nothing if the filter does not match" do + expect(@action).to receive(:===).with("str").and_return(false) + @action.after(@state) + expect($stdout).to eq("") + end + + it "prints the example description if the filter matches" do + expect(@action).to receive(:===).with("str").and_return(true) + @action.after(@state) + expect($stdout).to eq("str\n") + end +end + +RSpec.describe TagListAction, "#register" do + before :each do + allow(MSpec).to receive(:register) + @action = TagListAction.new + end + + it "registers itself with MSpec for the :start event" do + expect(MSpec).to receive(:register).with(:start, @action) + @action.register + end + + it "registers itself with MSpec for the :load event" do + expect(MSpec).to receive(:register).with(:load, @action) + @action.register + end + + it "registers itself with MSpec for the :after event" do + expect(MSpec).to receive(:register).with(:after, @action) + @action.register + end +end + +RSpec.describe TagListAction, "#unregister" do + before :each do + allow(MSpec).to receive(:unregister) + @action = TagListAction.new + end + + it "unregisters itself with MSpec for the :start event" do + expect(MSpec).to receive(:unregister).with(:start, @action) + @action.unregister + end + + it "unregisters itself with MSpec for the :load event" do + expect(MSpec).to receive(:unregister).with(:load, @action) + @action.unregister + end + + it "unregisters itself with MSpec for the :after event" do + expect(MSpec).to receive(:unregister).with(:after, @action) + @action.unregister + end +end diff --git a/spec/mspec/spec/runner/actions/tagpurge_spec.rb b/spec/mspec/spec/runner/actions/tagpurge_spec.rb new file mode 100644 index 0000000000..37df0afd5a --- /dev/null +++ b/spec/mspec/spec/runner/actions/tagpurge_spec.rb @@ -0,0 +1,154 @@ +require File.dirname(__FILE__) + '/../../spec_helper' +require 'mspec/runner/actions/tagpurge' +require 'mspec/runner/mspec' +require 'mspec/runner/example' +require 'mspec/runner/tag' + +RSpec.describe TagPurgeAction, "#start" do + before :each do + @stdout = $stdout + $stdout = IOStub.new + end + + after :each do + $stdout = @stdout + end + + it "prints a banner" do + action = TagPurgeAction.new + action.start + expect($stdout).to eq("\nRemoving tags not matching any specs\n\n") + end +end + +RSpec.describe TagPurgeAction, "#load" do + before :each do + @t1 = SpecTag.new "fails:I fail" + @t2 = SpecTag.new "unstable:I'm unstable" + end + + it "creates a MatchFilter for all tags" do + expect(MSpec).to receive(:read_tags).and_return([@t1, @t2]) + expect(MatchFilter).to receive(:new).with(nil, "I fail", "I'm unstable") + TagPurgeAction.new.load + end +end + +RSpec.describe TagPurgeAction, "#after" do + before :each do + @state = double("ExampleState") + allow(@state).to receive(:description).and_return("str") + + @action = TagPurgeAction.new + end + + it "does not save the description if the filter does not match" do + expect(@action).to receive(:===).with("str").and_return(false) + @action.after @state + expect(@action.matching).to eq([]) + end + + it "saves the description if the filter matches" do + expect(@action).to receive(:===).with("str").and_return(true) + @action.after @state + expect(@action.matching).to eq(["str"]) + end +end + +RSpec.describe TagPurgeAction, "#unload" do + before :each do + @stdout = $stdout + $stdout = IOStub.new + + @t1 = SpecTag.new "fails:I fail" + @t2 = SpecTag.new "unstable:I'm unstable" + @t3 = SpecTag.new "fails:I'm unstable" + + allow(MSpec).to receive(:read_tags).and_return([@t1, @t2, @t3]) + allow(MSpec).to receive(:write_tags) + + @state = double("ExampleState") + allow(@state).to receive(:description).and_return("I'm unstable") + + @action = TagPurgeAction.new + @action.load + @action.after @state + end + + after :each do + $stdout = @stdout + end + + it "does not rewrite any tags if there were no tags for the specs" do + expect(MSpec).to receive(:read_tags).and_return([]) + expect(MSpec).to receive(:delete_tags) + expect(MSpec).not_to receive(:write_tags) + + @action.load + @action.after @state + @action.unload + + expect($stdout).to eq("") + end + + it "rewrites tags that were matched" do + expect(MSpec).to receive(:write_tags).with([@t2, @t3]) + @action.unload + end + + it "prints tags that were not matched" do + @action.unload + expect($stdout).to eq("I fail\n") + end +end + +RSpec.describe TagPurgeAction, "#unload" do + before :each do + @stdout = $stdout + $stdout = IOStub.new + + allow(MSpec).to receive(:read_tags).and_return([]) + + @state = double("ExampleState") + allow(@state).to receive(:description).and_return("I'm unstable") + + @action = TagPurgeAction.new + @action.load + @action.after @state + end + + after :each do + $stdout = @stdout + end + + it "deletes the tag file if no tags were found" do + expect(MSpec).not_to receive(:write_tags) + expect(MSpec).to receive(:delete_tags) + @action.unload + expect($stdout).to eq("") + end +end + +RSpec.describe TagPurgeAction, "#register" do + before :each do + allow(MSpec).to receive(:register) + @action = TagPurgeAction.new + end + + it "registers itself with MSpec for the :unload event" do + expect(MSpec).to receive(:register).with(:unload, @action) + @action.register + end +end + +RSpec.describe TagPurgeAction, "#unregister" do + before :each do + allow(MSpec).to receive(:unregister) + @action = TagPurgeAction.new + end + + it "unregisters itself with MSpec for the :unload event" do + expect(MSpec).to receive(:unregister).with(:unload, @action) + @action.unregister + end +end diff --git a/spec/mspec/spec/runner/actions/tally_spec.rb b/spec/mspec/spec/runner/actions/tally_spec.rb new file mode 100644 index 0000000000..d80ab1164a --- /dev/null +++ b/spec/mspec/spec/runner/actions/tally_spec.rb @@ -0,0 +1,355 @@ +require File.dirname(__FILE__) + '/../../spec_helper' +require 'mspec/expectations/expectations' +require 'mspec/runner/actions/tally' +require 'mspec/runner/mspec' +require 'mspec/runner/example' + +RSpec.describe Tally, "#files!" do + before :each do + @tally = Tally.new + end + + it "increments the count returned by #files" do + @tally.files! 3 + expect(@tally.files).to eq(3) + @tally.files! + expect(@tally.files).to eq(4) + end +end + +RSpec.describe Tally, "#examples!" do + before :each do + @tally = Tally.new + end + + it "increments the count returned by #examples" do + @tally.examples! 2 + expect(@tally.examples).to eq(2) + @tally.examples! 2 + expect(@tally.examples).to eq(4) + end +end + +RSpec.describe Tally, "#expectations!" do + before :each do + @tally = Tally.new + end + + it "increments the count returned by #expectations" do + @tally.expectations! + expect(@tally.expectations).to eq(1) + @tally.expectations! 3 + expect(@tally.expectations).to eq(4) + end +end + +RSpec.describe Tally, "#failures!" do + before :each do + @tally = Tally.new + end + + it "increments the count returned by #failures" do + @tally.failures! 1 + expect(@tally.failures).to eq(1) + @tally.failures! + expect(@tally.failures).to eq(2) + end +end + +RSpec.describe Tally, "#errors!" do + before :each do + @tally = Tally.new + end + + it "increments the count returned by #errors" do + @tally.errors! + expect(@tally.errors).to eq(1) + @tally.errors! 2 + expect(@tally.errors).to eq(3) + end +end + +RSpec.describe Tally, "#guards!" do + before :each do + @tally = Tally.new + end + + it "increments the count returned by #guards" do + @tally.guards! + expect(@tally.guards).to eq(1) + @tally.guards! 2 + expect(@tally.guards).to eq(3) + end +end + +RSpec.describe Tally, "#file" do + before :each do + @tally = Tally.new + end + + it "returns a formatted string of the number of #files" do + expect(@tally.file).to eq("0 files") + @tally.files! + expect(@tally.file).to eq("1 file") + @tally.files! + expect(@tally.file).to eq("2 files") + end +end + +RSpec.describe Tally, "#example" do + before :each do + @tally = Tally.new + end + + it "returns a formatted string of the number of #examples" do + expect(@tally.example).to eq("0 examples") + @tally.examples! + expect(@tally.example).to eq("1 example") + @tally.examples! + expect(@tally.example).to eq("2 examples") + end +end + +RSpec.describe Tally, "#expectation" do + before :each do + @tally = Tally.new + end + + it "returns a formatted string of the number of #expectations" do + expect(@tally.expectation).to eq("0 expectations") + @tally.expectations! + expect(@tally.expectation).to eq("1 expectation") + @tally.expectations! + expect(@tally.expectation).to eq("2 expectations") + end +end + +RSpec.describe Tally, "#failure" do + before :each do + @tally = Tally.new + end + + it "returns a formatted string of the number of #failures" do + expect(@tally.failure).to eq("0 failures") + @tally.failures! + expect(@tally.failure).to eq("1 failure") + @tally.failures! + expect(@tally.failure).to eq("2 failures") + end +end + +RSpec.describe Tally, "#error" do + before :each do + @tally = Tally.new + end + + it "returns a formatted string of the number of #errors" do + expect(@tally.error).to eq("0 errors") + @tally.errors! + expect(@tally.error).to eq("1 error") + @tally.errors! + expect(@tally.error).to eq("2 errors") + end +end + +RSpec.describe Tally, "#guard" do + before :each do + @tally = Tally.new + end + + it "returns a formatted string of the number of #guards" do + expect(@tally.guard).to eq("0 guards") + @tally.guards! + expect(@tally.guard).to eq("1 guard") + @tally.guards! + expect(@tally.guard).to eq("2 guards") + end +end + +RSpec.describe Tally, "#format" do + before :each do + @tally = Tally.new + end + + after :each do + MSpec.clear_modes + end + + it "returns a formatted string of counts" do + @tally.files! + @tally.examples! 2 + @tally.expectations! 4 + @tally.errors! + @tally.tagged! + expect(@tally.format).to eq("1 file, 2 examples, 4 expectations, 0 failures, 1 error, 1 tagged") + end + + it "includes guards if MSpec is in verify mode" do + MSpec.register_mode :verify + @tally.files! + @tally.examples! 2 + @tally.expectations! 4 + @tally.errors! + @tally.tagged! + @tally.guards! + expect(@tally.format).to eq( + "1 file, 2 examples, 4 expectations, 0 failures, 1 error, 1 tagged, 1 guard" + ) + end + + it "includes guards if MSpec is in report mode" do + MSpec.register_mode :report + @tally.files! + @tally.examples! 2 + @tally.expectations! 4 + @tally.errors! + @tally.tagged! + @tally.guards! 2 + expect(@tally.format).to eq( + "1 file, 2 examples, 4 expectations, 0 failures, 1 error, 1 tagged, 2 guards" + ) + end + + it "includes guards if MSpec is in report_on mode" do + MSpec.register_mode :report_on + @tally.files! + @tally.examples! 2 + @tally.expectations! 4 + @tally.errors! + @tally.guards! 2 + expect(@tally.format).to eq( + "1 file, 2 examples, 4 expectations, 0 failures, 1 error, 0 tagged, 2 guards" + ) + end +end + +RSpec.describe TallyAction, "#counter" do + before :each do + @tally = TallyAction.new + @state = ExampleState.new("describe", "it") + end + + it "returns the Tally object" do + expect(@tally.counter).to be_kind_of(Tally) + end +end + +RSpec.describe TallyAction, "#load" do + before :each do + @tally = TallyAction.new + @state = ExampleState.new("describe", "it") + end + + it "increments the count returned by Tally#files" do + @tally.load + expect(@tally.counter.files).to eq(1) + end +end + +RSpec.describe TallyAction, "#expectation" do + before :each do + @tally = TallyAction.new + @state = ExampleState.new("describe", "it") + end + + it "increments the count returned by Tally#expectations" do + @tally.expectation @state + expect(@tally.counter.expectations).to eq(1) + end +end + +RSpec.describe TallyAction, "#example" do + before :each do + @tally = TallyAction.new + @state = ExampleState.new("describe", "it") + end + + it "increments counts returned by Tally#examples" do + @tally.example @state, nil + expect(@tally.counter.examples).to eq(1) + expect(@tally.counter.expectations).to eq(0) + expect(@tally.counter.failures).to eq(0) + expect(@tally.counter.errors).to eq(0) + end +end + +RSpec.describe TallyAction, "#exception" do + before :each do + @tally = TallyAction.new + @state = ExampleState.new("describe", "it") + end + + it "increments counts returned by Tally#failures" do + exc = ExceptionState.new nil, nil, SpecExpectationNotMetError.new("Failed!") + @tally.exception exc + expect(@tally.counter.examples).to eq(0) + expect(@tally.counter.expectations).to eq(0) + expect(@tally.counter.failures).to eq(1) + expect(@tally.counter.errors).to eq(0) + end +end + +RSpec.describe TallyAction, "#exception" do + before :each do + @tally = TallyAction.new + @state = ExampleState.new("describe", "it") + end + + it "increments counts returned by Tally#errors" do + exc = ExceptionState.new nil, nil, Exception.new("Error!") + @tally.exception exc + expect(@tally.counter.examples).to eq(0) + expect(@tally.counter.expectations).to eq(0) + expect(@tally.counter.failures).to eq(0) + expect(@tally.counter.errors).to eq(1) + end +end + +RSpec.describe TallyAction, "#format" do + before :each do + @tally = TallyAction.new + @state = ExampleState.new("describe", "it") + end + + it "returns a readable string of counts" do + @tally.load + @tally.example @state, nil + @tally.expectation @state + @tally.expectation @state + exc = ExceptionState.new nil, nil, SpecExpectationNotMetError.new("Failed!") + @tally.exception exc + expect(@tally.format).to eq("1 file, 1 example, 2 expectations, 1 failure, 0 errors, 0 tagged") + end +end + +RSpec.describe TallyAction, "#register" do + before :each do + @tally = TallyAction.new + @state = ExampleState.new("describe", "it") + end + + it "registers itself with MSpec for appropriate actions" do + expect(MSpec).to receive(:register).with(:load, @tally) + expect(MSpec).to receive(:register).with(:exception, @tally) + expect(MSpec).to receive(:register).with(:example, @tally) + expect(MSpec).to receive(:register).with(:tagged, @tally) + expect(MSpec).to receive(:register).with(:expectation, @tally) + @tally.register + end +end + +RSpec.describe TallyAction, "#unregister" do + before :each do + @tally = TallyAction.new + @state = ExampleState.new("describe", "it") + end + + it "unregisters itself with MSpec for appropriate actions" do + expect(MSpec).to receive(:unregister).with(:load, @tally) + expect(MSpec).to receive(:unregister).with(:exception, @tally) + expect(MSpec).to receive(:unregister).with(:example, @tally) + expect(MSpec).to receive(:unregister).with(:tagged, @tally) + expect(MSpec).to receive(:unregister).with(:expectation, @tally) + @tally.unregister + end +end diff --git a/spec/mspec/spec/runner/actions/timer_spec.rb b/spec/mspec/spec/runner/actions/timer_spec.rb new file mode 100644 index 0000000000..28a317177b --- /dev/null +++ b/spec/mspec/spec/runner/actions/timer_spec.rb @@ -0,0 +1,44 @@ +require File.dirname(__FILE__) + '/../../spec_helper' +require 'mspec/runner/actions/timer' +require 'mspec/runner/mspec' +require 'time' + +RSpec.describe TimerAction do + before :each do + @timer = TimerAction.new + @start_time = Time.utc(2009, 3, 30, 14, 5, 19) + @stop_time = Time.utc(2009, 3, 30, 14, 5, 52) + end + + it "responds to #start by recording the current time" do + expect(Time).to receive(:now) + @timer.start + end + + it "responds to #finish by recording the current time" do + expect(Time).to receive(:now) + @timer.finish + end + + it "responds to #elapsed by returning the difference between stop and start" do + allow(Time).to receive(:now).and_return(@start_time) + @timer.start + allow(Time).to receive(:now).and_return(@stop_time) + @timer.finish + expect(@timer.elapsed).to eq(33) + end + + it "responds to #format by returning a readable string of elapsed time" do + allow(Time).to receive(:now).and_return(@start_time) + @timer.start + allow(Time).to receive(:now).and_return(@stop_time) + @timer.finish + expect(@timer.format).to eq("Finished in 33.000000 seconds") + end + + it "responds to #register by registering itself with MSpec for appropriate actions" do + expect(MSpec).to receive(:register).with(:start, @timer) + expect(MSpec).to receive(:register).with(:finish, @timer) + @timer.register + end +end diff --git a/spec/mspec/spec/runner/context_spec.rb b/spec/mspec/spec/runner/context_spec.rb new file mode 100644 index 0000000000..9ebc708c0c --- /dev/null +++ b/spec/mspec/spec/runner/context_spec.rb @@ -0,0 +1,1028 @@ +require 'spec_helper' +require 'mspec/expectations/expectations' +require 'mspec/matchers/base' +require 'mspec/runner/mspec' +require 'mspec/mocks/mock' +require 'mspec/runner/context' +require 'mspec/runner/example' + +RSpec.describe ContextState, "#describe" do + before :each do + @state = ContextState.new "C#m" + @proc = proc { ScratchPad.record :a } + ScratchPad.clear + end + + it "evaluates the passed block" do + @state.describe(&@proc) + expect(ScratchPad.recorded).to eq(:a) + end + + it "evaluates the passed block via #protect" do + expect(@state).to receive(:protect).with("C#m", @proc, false) + @state.describe(&@proc) + end + + it "registers #parent as the current MSpec ContextState" do + parent = ContextState.new "" + @state.parent = parent + expect(MSpec).to receive(:register_current).with(parent) + @state.describe { } + end + + it "registers self with MSpec when #shared? is true" do + state = ContextState.new "something shared", :shared => true + expect(MSpec).to receive(:register_shared).with(state) + state.describe { } + end +end + +RSpec.describe ContextState, "#shared?" do + it "returns false when the ContextState is not shared" do + expect(ContextState.new("").shared?).to be_falsey + end + + it "returns true when the ContextState is shared" do + expect(ContextState.new("", {:shared => true}).shared?).to be_truthy + end +end + +RSpec.describe ContextState, "#to_s" do + it "returns a description string for self when passed a Module" do + expect(ContextState.new(Object).to_s).to eq("Object") + end + + it "returns a description string for self when passed a String" do + expect(ContextState.new("SomeClass").to_s).to eq("SomeClass") + end +end + +RSpec.describe ContextState, "#description" do + before :each do + @state = ContextState.new "when empty" + @parent = ContextState.new "Toplevel" + end + + it "returns a composite description string from self and all parents" do + expect(@parent.description).to eq("Toplevel") + expect(@state.description).to eq("when empty") + @state.parent = @parent + expect(@state.description).to eq("Toplevel when empty") + end +end + +RSpec.describe ContextState, "#it" do + before :each do + @state = ContextState.new "" + @proc = lambda {|*| } + + @ex = ExampleState.new("", "", &@proc) + end + + it "creates an ExampleState instance for the block" do + expect(ExampleState).to receive(:new).with(@state, "it", @proc).and_return(@ex) + @state.describe(&@proc) + @state.it("it", &@proc) + end + + it "calls registered :add actions" do + expect(ExampleState).to receive(:new).with(@state, "it", @proc).and_return(@ex) + + add_action = double("add") + expect(add_action).to receive(:add).with(@ex) { ScratchPad.record :add } + MSpec.register :add, add_action + + @state.it("it", &@proc) + expect(ScratchPad.recorded).to eq(:add) + MSpec.unregister :add, add_action + end +end + +RSpec.describe ContextState, "#examples" do + before :each do + @state = ContextState.new "" + end + + it "returns a list of all examples in this ContextState" do + @state.it("first") { } + @state.it("second") { } + expect(@state.examples.size).to eq(2) + end +end + +RSpec.describe ContextState, "#before" do + before :each do + @state = ContextState.new "" + @proc = lambda {|*| } + end + + it "records the block for :each" do + @state.before(:each, &@proc) + expect(@state.before(:each)).to eq([@proc]) + end + + it "records the block for :all" do + @state.before(:all, &@proc) + expect(@state.before(:all)).to eq([@proc]) + end +end + +RSpec.describe ContextState, "#after" do + before :each do + @state = ContextState.new "" + @proc = lambda {|*| } + end + + it "records the block for :each" do + @state.after(:each, &@proc) + expect(@state.after(:each)).to eq([@proc]) + end + + it "records the block for :all" do + @state.after(:all, &@proc) + expect(@state.after(:all)).to eq([@proc]) + end +end + +RSpec.describe ContextState, "#pre" do + before :each do + @a = lambda {|*| } + @b = lambda {|*| } + @c = lambda {|*| } + + parent = ContextState.new "" + parent.before(:each, &@c) + parent.before(:all, &@c) + + @state = ContextState.new "" + @state.parent = parent + end + + it "returns before(:each) actions in the order they were defined" do + @state.before(:each, &@a) + @state.before(:each, &@b) + expect(@state.pre(:each)).to eq([@c, @a, @b]) + end + + it "returns before(:all) actions in the order they were defined" do + @state.before(:all, &@a) + @state.before(:all, &@b) + expect(@state.pre(:all)).to eq([@c, @a, @b]) + end +end + +RSpec.describe ContextState, "#post" do + before :each do + @a = lambda {|*| } + @b = lambda {|*| } + @c = lambda {|*| } + + parent = ContextState.new "" + parent.after(:each, &@c) + parent.after(:all, &@c) + + @state = ContextState.new "" + @state.parent = parent + end + + it "returns after(:each) actions in the reverse order they were defined" do + @state.after(:each, &@a) + @state.after(:each, &@b) + expect(@state.post(:each)).to eq([@b, @a, @c]) + end + + it "returns after(:all) actions in the reverse order they were defined" do + @state.after(:all, &@a) + @state.after(:all, &@b) + expect(@state.post(:all)).to eq([@b, @a, @c]) + end +end + +RSpec.describe ContextState, "#protect" do + before :each do + ScratchPad.record [] + @a = lambda {|*| ScratchPad << :a } + @b = lambda {|*| ScratchPad << :b } + @c = lambda {|*| raise Exception, "Fail!" } + end + + it "returns true and does execute any blocks if check and MSpec.mode?(:pretend) are true" do + expect(MSpec).to receive(:mode?).with(:pretend).and_return(true) + expect(ContextState.new("").protect("message", [@a, @b])).to be_truthy + expect(ScratchPad.recorded).to eq([]) + end + + it "executes the blocks if MSpec.mode?(:pretend) is false" do + expect(MSpec).to receive(:mode?).with(:pretend).and_return(false) + ContextState.new("").protect("message", [@a, @b]) + expect(ScratchPad.recorded).to eq([:a, :b]) + end + + it "executes the blocks if check is false" do + ContextState.new("").protect("message", [@a, @b], false) + expect(ScratchPad.recorded).to eq([:a, :b]) + end + + it "returns true if none of the blocks raise an exception" do + expect(ContextState.new("").protect("message", [@a, @b])).to be_truthy + end + + it "returns false if any of the blocks raise an exception" do + expect(ContextState.new("").protect("message", [@a, @c, @b])).to be_falsey + end +end + +RSpec.describe ContextState, "#parent=" do + before :each do + @state = ContextState.new "" + @parent = double("describe") + allow(@parent).to receive(:parent).and_return(nil) + allow(@parent).to receive(:child) + end + + it "does not set self as a child of parent if shared" do + expect(@parent).not_to receive(:child) + state = ContextState.new "", :shared => true + state.parent = @parent + end + + it "does not set parents if shared" do + state = ContextState.new "", :shared => true + state.parent = @parent + expect(state.parents).to eq([state]) + end + + it "sets self as a child of parent" do + expect(@parent).to receive(:child).with(@state) + @state.parent = @parent + end + + it "creates the list of parents" do + @state.parent = @parent + expect(@state.parents).to eq([@parent, @state]) + end +end + +RSpec.describe ContextState, "#parent" do + before :each do + @state = ContextState.new "" + @parent = double("describe") + allow(@parent).to receive(:parent).and_return(nil) + allow(@parent).to receive(:child) + end + + it "returns nil if parent has not been set" do + expect(@state.parent).to be_nil + end + + it "returns the parent" do + @state.parent = @parent + expect(@state.parent).to eq(@parent) + end +end + +RSpec.describe ContextState, "#parents" do + before :each do + @first = ContextState.new "" + @second = ContextState.new "" + @parent = double("describe") + allow(@parent).to receive(:parent).and_return(nil) + allow(@parent).to receive(:child) + end + + it "returns a list of all enclosing ContextState instances" do + @first.parent = @parent + @second.parent = @first + expect(@second.parents).to eq([@parent, @first, @second]) + end +end + +RSpec.describe ContextState, "#child" do + before :each do + @first = ContextState.new "" + @second = ContextState.new "" + @parent = double("describe") + allow(@parent).to receive(:parent).and_return(nil) + allow(@parent).to receive(:child) + end + + it "adds the ContextState to the list of contained ContextStates" do + @first.child @second + expect(@first.children).to eq([@second]) + end +end + +RSpec.describe ContextState, "#children" do + before :each do + @parent = ContextState.new "" + @first = ContextState.new "" + @second = ContextState.new "" + end + + it "returns the list of directly contained ContextStates" do + @first.parent = @parent + @second.parent = @first + expect(@parent.children).to eq([@first]) + expect(@first.children).to eq([@second]) + end +end + +RSpec.describe ContextState, "#state" do + before :each do + MSpec.store :before, [] + MSpec.store :after, [] + + @state = ContextState.new "" + end + + it "returns nil if no spec is being executed" do + expect(@state.state).to eq(nil) + end + + it "returns a ExampleState instance if an example is being executed" do + ScratchPad.record @state + @state.describe { } + @state.it("") { ScratchPad.record ScratchPad.recorded.state } + @state.process + expect(@state.state).to eq(nil) + expect(ScratchPad.recorded).to be_kind_of(ExampleState) + end +end + +RSpec.describe ContextState, "#process" do + before :each do + MSpec.store :before, [] + MSpec.store :after, [] + allow(MSpec).to receive(:register_current) + + @state = ContextState.new "" + @state.describe { } + + @a = lambda {|*| ScratchPad << :a } + @b = lambda {|*| ScratchPad << :b } + ScratchPad.record [] + end + + it "calls each before(:all) block" do + @state.before(:all, &@a) + @state.before(:all, &@b) + @state.it("") { } + @state.process + expect(ScratchPad.recorded).to eq([:a, :b]) + end + + it "calls each after(:all) block" do + @state.after(:all, &@a) + @state.after(:all, &@b) + @state.it("") { } + @state.process + expect(ScratchPad.recorded).to eq([:b, :a]) + end + + it "calls each it block" do + @state.it("one", &@a) + @state.it("two", &@b) + @state.process + expect(ScratchPad.recorded).to eq([:a, :b]) + end + + it "does not call the #it block if #filtered? returns true" do + @state.it("one", &@a) + @state.it("two", &@b) + allow(@state.examples.first).to receive(:filtered?).and_return(true) + @state.process + expect(ScratchPad.recorded).to eq([:b]) + end + + it "calls each before(:each) block" do + @state.before(:each, &@a) + @state.before(:each, &@b) + @state.it("") { } + @state.process + expect(ScratchPad.recorded).to eq([:a, :b]) + end + + it "calls each after(:each) block" do + @state.after(:each, &@a) + @state.after(:each, &@b) + @state.it("") { } + @state.process + expect(ScratchPad.recorded).to eq([:b, :a]) + end + + it "calls Mock.cleanup for each it block" do + @state.it("") { } + @state.it("") { } + expect(Mock).to receive(:cleanup).twice + @state.process + end + + it "calls Mock.verify_count for each it block" do + @state.it("") { } + @state.it("") { } + expect(Mock).to receive(:verify_count).twice + @state.process + end + + it "calls the describe block" do + ScratchPad.record [] + @state.describe { ScratchPad << :a } + @state.process + expect(ScratchPad.recorded).to eq([:a]) + end + + it "creates a new ExampleState instance for each example" do + ScratchPad.record @state + @state.describe { } + @state.it("it") { ScratchPad.record ScratchPad.recorded.state } + @state.process + expect(ScratchPad.recorded).to be_kind_of(ExampleState) + end + + it "clears the expectations flag before evaluating the #it block" do + MSpec.clear_expectations + expect(MSpec).to receive(:clear_expectations) + @state.it("it") { ScratchPad.record MSpec.expectation? } + @state.process + expect(ScratchPad.recorded).to be_falsey + end + + it "shuffles the spec list if MSpec.randomize? is true" do + MSpec.randomize = true + begin + expect(MSpec).to receive(:shuffle) + @state.it("") { } + @state.process + ensure + MSpec.randomize = false + end + end + + it "sets the current MSpec ContextState" do + expect(MSpec).to receive(:register_current).with(@state) + @state.process + end + + it "resets the current MSpec ContextState to nil when there are examples" do + expect(MSpec).to receive(:register_current).with(nil) + @state.it("") { } + @state.process + end + + it "resets the current MSpec ContextState to nil when there are no examples" do + expect(MSpec).to receive(:register_current).with(nil) + @state.process + end + + it "call #process on children when there are examples" do + child = ContextState.new "" + expect(child).to receive(:process) + @state.child child + @state.it("") { } + @state.process + end + + it "call #process on children when there are no examples" do + child = ContextState.new "" + expect(child).to receive(:process) + @state.child child + @state.process + end +end + +RSpec.describe ContextState, "#process" do + before :each do + MSpec.store :exception, [] + + @state = ContextState.new "" + @state.describe { } + + action = double("action") + def action.exception(exc) + ScratchPad.record :exception if exc.exception.is_a? SpecExpectationNotFoundError + end + MSpec.register :exception, action + + MSpec.clear_expectations + ScratchPad.clear + end + + after :each do + MSpec.store :exception, nil + end + + it "raises an SpecExpectationNotFoundError if an #it block does not contain an expectation" do + @state.it("it") { } + @state.process + expect(ScratchPad.recorded).to eq(:exception) + end + + it "does not raise an SpecExpectationNotFoundError if an #it block does contain an expectation" do + @state.it("it") { MSpec.expectation } + @state.process + expect(ScratchPad.recorded).to be_nil + end + + it "does not raise an SpecExpectationNotFoundError if the #it block causes a failure" do + @state.it("it") { raise Exception, "Failed!" } + @state.process + expect(ScratchPad.recorded).to be_nil + end +end + +RSpec.describe ContextState, "#process" do + before :each do + MSpec.store :example, [] + + @state = ContextState.new "" + @state.describe { } + + example = double("example") + def example.example(state, spec) + ScratchPad << state << spec + end + MSpec.register :example, example + + ScratchPad.record [] + end + + after :each do + MSpec.store :example, nil + end + + it "calls registered :example actions with the current ExampleState and block" do + @state.it("") { MSpec.expectation } + @state.process + + expect(ScratchPad.recorded.first).to be_kind_of(ExampleState) + expect(ScratchPad.recorded.last).to be_kind_of(Proc) + end + + it "does not call registered example actions if the example has no block" do + @state.it("empty example") + @state.process + expect(ScratchPad.recorded).to eq([]) + end +end + +RSpec.describe ContextState, "#process" do + before :each do + MSpec.store :before, [] + MSpec.store :after, [] + + @state = ContextState.new "" + @state.describe { } + @state.it("") { MSpec.expectation } + end + + after :each do + MSpec.store :before, nil + MSpec.store :after, nil + end + + it "calls registered :before actions with the current ExampleState instance" do + before = double("before") + expect(before).to receive(:before) { + ScratchPad.record :before + @spec_state = @state.state + } + MSpec.register :before, before + @state.process + expect(ScratchPad.recorded).to eq(:before) + expect(@spec_state).to be_kind_of(ExampleState) + end + + it "calls registered :after actions with the current ExampleState instance" do + after = double("after") + expect(after).to receive(:after) { + ScratchPad.record :after + @spec_state = @state.state + } + MSpec.register :after, after + @state.process + expect(ScratchPad.recorded).to eq(:after) + expect(@spec_state).to be_kind_of(ExampleState) + end +end + +RSpec.describe ContextState, "#process" do + before :each do + MSpec.store :enter, [] + MSpec.store :leave, [] + + @state = ContextState.new "C#m" + @state.describe { } + @state.it("") { MSpec.expectation } + end + + after :each do + MSpec.store :enter, nil + MSpec.store :leave, nil + end + + it "calls registered :enter actions with the current #describe string" do + enter = double("enter") + expect(enter).to receive(:enter).with("C#m") { ScratchPad.record :enter } + MSpec.register :enter, enter + @state.process + expect(ScratchPad.recorded).to eq(:enter) + end + + it "calls registered :leave actions" do + leave = double("leave") + expect(leave).to receive(:leave) { ScratchPad.record :leave } + MSpec.register :leave, leave + @state.process + expect(ScratchPad.recorded).to eq(:leave) + end +end + +RSpec.describe ContextState, "#process when an exception is raised in before(:all)" do + before :each do + MSpec.store :before, [] + MSpec.store :after, [] + + @state = ContextState.new "" + @state.describe { } + + @a = lambda {|*| ScratchPad << :a } + @b = lambda {|*| ScratchPad << :b } + ScratchPad.record [] + + @state.before(:all) { raise Exception, "Fail!" } + end + + after :each do + MSpec.store :before, nil + MSpec.store :after, nil + end + + it "does not call before(:each)" do + @state.before(:each, &@a) + @state.it("") { } + @state.process + expect(ScratchPad.recorded).to eq([]) + end + + it "does not call the it block" do + @state.it("one", &@a) + @state.process + expect(ScratchPad.recorded).to eq([]) + end + + it "does not call after(:each)" do + @state.after(:each, &@a) + @state.it("") { } + @state.process + expect(ScratchPad.recorded).to eq([]) + end + + it "does not call after(:each)" do + @state.after(:all, &@a) + @state.it("") { } + @state.process + expect(ScratchPad.recorded).to eq([]) + end + + it "does not call Mock.verify_count" do + @state.it("") { } + expect(Mock).not_to receive(:verify_count) + @state.process + end + + it "calls Mock.cleanup" do + @state.it("") { } + expect(Mock).to receive(:cleanup) + @state.process + end +end + +RSpec.describe ContextState, "#process when an exception is raised in before(:each)" do + before :each do + MSpec.store :before, [] + MSpec.store :after, [] + + @state = ContextState.new "" + @state.describe { } + + @a = lambda {|*| ScratchPad << :a } + @b = lambda {|*| ScratchPad << :b } + ScratchPad.record [] + + @state.before(:each) { raise Exception, "Fail!" } + end + + after :each do + MSpec.store :before, nil + MSpec.store :after, nil + end + + it "does not call the it block" do + @state.it("one", &@a) + @state.process + expect(ScratchPad.recorded).to eq([]) + end + + it "calls after(:each)" do + @state.after(:each, &@a) + @state.it("") { } + @state.process + expect(ScratchPad.recorded).to eq([:a]) + end + + it "calls Mock.verify_count" do + @state.it("") { } + expect(Mock).to receive(:verify_count) + @state.process + end +end + +RSpec.describe ContextState, "#process in pretend mode" do + before :all do + MSpec.register_mode :pretend + end + + after :all do + MSpec.clear_modes + end + + before :each do + ScratchPad.clear + MSpec.store :before, [] + MSpec.store :after, [] + + @state = ContextState.new "" + @state.describe { } + @state.it("") { } + end + + after :each do + MSpec.store :before, nil + MSpec.store :after, nil + end + + it "calls registered :before actions with the current ExampleState instance" do + before = double("before") + expect(before).to receive(:before) { + ScratchPad.record :before + @spec_state = @state.state + } + MSpec.register :before, before + @state.process + expect(ScratchPad.recorded).to eq(:before) + expect(@spec_state).to be_kind_of(ExampleState) + end + + it "calls registered :after actions with the current ExampleState instance" do + after = double("after") + expect(after).to receive(:after) { + ScratchPad.record :after + @spec_state = @state.state + } + MSpec.register :after, after + @state.process + expect(ScratchPad.recorded).to eq(:after) + expect(@spec_state).to be_kind_of(ExampleState) + end +end + +RSpec.describe ContextState, "#process in pretend mode" do + before :all do + MSpec.register_mode :pretend + end + + after :all do + MSpec.clear_modes + end + + before :each do + MSpec.store :before, [] + MSpec.store :after, [] + + @state = ContextState.new "" + @state.describe { } + + @a = lambda {|*| ScratchPad << :a } + @b = lambda {|*| ScratchPad << :b } + ScratchPad.record [] + end + + it "calls the describe block" do + ScratchPad.record [] + @state.describe { ScratchPad << :a } + @state.process + expect(ScratchPad.recorded).to eq([:a]) + end + + it "does not call any before(:all) block" do + @state.before(:all, &@a) + @state.before(:all, &@b) + @state.it("") { } + @state.process + expect(ScratchPad.recorded).to eq([]) + end + + it "does not call any after(:all) block" do + @state.after(:all, &@a) + @state.after(:all, &@b) + @state.it("") { } + @state.process + expect(ScratchPad.recorded).to eq([]) + end + + it "does not call any it block" do + @state.it("one", &@a) + @state.it("two", &@b) + @state.process + expect(ScratchPad.recorded).to eq([]) + end + + it "does not call any before(:each) block" do + @state.before(:each, &@a) + @state.before(:each, &@b) + @state.it("") { } + @state.process + expect(ScratchPad.recorded).to eq([]) + end + + it "does not call any after(:each) block" do + @state.after(:each, &@a) + @state.after(:each, &@b) + @state.it("") { } + @state.process + expect(ScratchPad.recorded).to eq([]) + end + + it "does not call Mock.cleanup" do + @state.it("") { } + @state.it("") { } + expect(Mock).not_to receive(:cleanup) + @state.process + end +end + +RSpec.describe ContextState, "#process in pretend mode" do + before :all do + MSpec.register_mode :pretend + end + + after :all do + MSpec.clear_modes + end + + before :each do + MSpec.store :enter, [] + MSpec.store :leave, [] + + @state = ContextState.new "" + @state.describe { } + @state.it("") { } + end + + after :each do + MSpec.store :enter, nil + MSpec.store :leave, nil + end + + it "calls registered :enter actions with the current #describe string" do + enter = double("enter") + expect(enter).to receive(:enter) { ScratchPad.record :enter } + MSpec.register :enter, enter + @state.process + expect(ScratchPad.recorded).to eq(:enter) + end + + it "calls registered :leave actions" do + leave = double("leave") + expect(leave).to receive(:leave) { ScratchPad.record :leave } + MSpec.register :leave, leave + @state.process + expect(ScratchPad.recorded).to eq(:leave) + end +end + +RSpec.describe ContextState, "#it_should_behave_like" do + before :each do + @shared_desc = :shared_context + @shared = ContextState.new(@shared_desc, :shared => true) + allow(MSpec).to receive(:retrieve_shared).and_return(@shared) + + @state = ContextState.new "Top level" + @a = lambda {|*| } + @b = lambda {|*| } + end + + it "raises an Exception if unable to find the shared ContextState" do + expect(MSpec).to receive(:retrieve_shared).and_return(nil) + expect { @state.it_should_behave_like :this }.to raise_error(Exception) + end + + describe "for nested ContextState instances" do + before :each do + @nested = ContextState.new "nested context" + @nested.parents.unshift @shared + + @shared.children << @nested + + @nested_dup = @nested.dup + allow(@nested).to receive(:dup).and_return(@nested_dup) + end + + it "duplicates the nested ContextState" do + @state.it_should_behave_like @shared_desc + expect(@state.children.first).to equal(@nested_dup) + end + + it "sets the parent of the nested ContextState to the containing ContextState" do + @state.it_should_behave_like @shared_desc + expect(@nested_dup.parent).to equal(@state) + end + + it "sets the context for nested examples to the nested ContextState's dup" do + @shared.it "an example", &@a + @shared.it "another example", &@b + @state.it_should_behave_like @shared_desc + @nested_dup.examples.each { |x| expect(x.context).to equal(@nested_dup) } + end + + it "omits the shored ContextState's description" do + @nested.it "an example", &@a + @nested.it "another example", &@b + @state.it_should_behave_like @shared_desc + + expect(@nested_dup.description).to eq("Top level nested context") + expect(@nested_dup.examples.first.description).to eq("Top level nested context an example") + expect(@nested_dup.examples.last.description).to eq("Top level nested context another example") + end + end + + it "adds duped examples from the shared ContextState" do + @shared.it "some method", &@a + ex_dup = @shared.examples.first.dup + allow(@shared.examples.first).to receive(:dup).and_return(ex_dup) + + @state.it_should_behave_like @shared_desc + expect(@state.examples).to eq([ex_dup]) + end + + it "sets the context for examples to the containing ContextState" do + @shared.it "an example", &@a + @shared.it "another example", &@b + @state.it_should_behave_like @shared_desc + @state.examples.each { |x| expect(x.context).to equal(@state) } + end + + it "adds before(:all) blocks from the shared ContextState" do + @shared.before :all, &@a + @shared.before :all, &@b + @state.it_should_behave_like @shared_desc + expect(@state.before(:all)).to include(*@shared.before(:all)) + end + + it "adds before(:each) blocks from the shared ContextState" do + @shared.before :each, &@a + @shared.before :each, &@b + @state.it_should_behave_like @shared_desc + expect(@state.before(:each)).to include(*@shared.before(:each)) + end + + it "adds after(:each) blocks from the shared ContextState" do + @shared.after :each, &@a + @shared.after :each, &@b + @state.it_should_behave_like @shared_desc + expect(@state.after(:each)).to include(*@shared.after(:each)) + end + + it "adds after(:all) blocks from the shared ContextState" do + @shared.after :all, &@a + @shared.after :all, &@b + @state.it_should_behave_like @shared_desc + expect(@state.after(:all)).to include(*@shared.after(:all)) + end +end + +RSpec.describe ContextState, "#filter_examples" do + before :each do + @state = ContextState.new "" + @state.it("one") { } + @state.it("two") { } + end + + it "removes examples that are filtered" do + allow(@state.examples.first).to receive(:filtered?).and_return(true) + expect(@state.examples.size).to eq(2) + @state.filter_examples + expect(@state.examples.size).to eq(1) + end + + it "returns true if there are remaining examples to evaluate" do + allow(@state.examples.first).to receive(:filtered?).and_return(true) + expect(@state.filter_examples).to be_truthy + end + + it "returns false if there are no remaining examples to evaluate" do + allow(@state.examples.first).to receive(:filtered?).and_return(true) + allow(@state.examples.last).to receive(:filtered?).and_return(true) + expect(@state.filter_examples).to be_falsey + end +end diff --git a/spec/mspec/spec/runner/example_spec.rb b/spec/mspec/spec/runner/example_spec.rb new file mode 100644 index 0000000000..8bac166da8 --- /dev/null +++ b/spec/mspec/spec/runner/example_spec.rb @@ -0,0 +1,117 @@ +require 'spec_helper' +require 'mspec/matchers/base' +require 'mspec/runner/mspec' +require 'mspec/mocks/mock' +require 'mspec/runner/example' + +RSpec.describe ExampleState do + it "is initialized with the ContextState, #it string, and #it block" do + prc = lambda { } + context = ContextState.new "" + expect(ExampleState.new(context, "does", prc)).to be_kind_of(ExampleState) + end +end + +RSpec.describe ExampleState, "#describe" do + before :each do + @context = ContextState.new "Object#to_s" + @state = ExampleState.new @context, "it" + end + + it "returns the ContextState#description" do + expect(@state.describe).to eq(@context.description) + end +end + +RSpec.describe ExampleState, "#it" do + before :each do + @state = ExampleState.new ContextState.new("describe"), "it" + end + + it "returns the argument to the #it block" do + expect(@state.it).to eq("it") + end +end + +RSpec.describe ExampleState, "#context=" do + before :each do + @state = ExampleState.new ContextState.new("describe"), "it" + @context = ContextState.new "New#context" + end + + it "sets the containing ContextState" do + @state.context = @context + expect(@state.context).to eq(@context) + end + + it "resets the description" do + expect(@state.description).to eq("describe it") + @state.context = @context + expect(@state.description).to eq("New#context it") + end +end + +RSpec.describe ExampleState, "#example" do + before :each do + @proc = lambda { } + @state = ExampleState.new ContextState.new("describe"), "it", @proc + end + + it "returns the #it block" do + expect(@state.example).to eq(@proc) + end +end + +RSpec.describe ExampleState, "#filtered?" do + before :each do + MSpec.store :include, [] + MSpec.store :exclude, [] + + @state = ExampleState.new ContextState.new("describe"), "it" + @filter = double("filter") + end + + after :each do + MSpec.store :include, [] + MSpec.store :exclude, [] + end + + it "returns false if MSpec include filters list is empty" do + expect(@state.filtered?).to eq(false) + end + + it "returns false if MSpec include filters match this spec" do + expect(@filter).to receive(:===).and_return(true) + MSpec.register :include, @filter + expect(@state.filtered?).to eq(false) + end + + it "returns true if MSpec include filters do not match this spec" do + expect(@filter).to receive(:===).and_return(false) + MSpec.register :include, @filter + expect(@state.filtered?).to eq(true) + end + + it "returns false if MSpec exclude filters list is empty" do + expect(@state.filtered?).to eq(false) + end + + it "returns false if MSpec exclude filters do not match this spec" do + expect(@filter).to receive(:===).and_return(false) + MSpec.register :exclude, @filter + expect(@state.filtered?).to eq(false) + end + + it "returns true if MSpec exclude filters match this spec" do + expect(@filter).to receive(:===).and_return(true) + MSpec.register :exclude, @filter + expect(@state.filtered?).to eq(true) + end + + it "returns true if MSpec include and exclude filters match this spec" do + expect(@filter).to receive(:===).twice.and_return(true) + MSpec.register :include, @filter + MSpec.register :exclude, @filter + expect(@state.filtered?).to eq(true) + end +end diff --git a/spec/mspec/spec/runner/exception_spec.rb b/spec/mspec/spec/runner/exception_spec.rb new file mode 100644 index 0000000000..a77a2c9cf4 --- /dev/null +++ b/spec/mspec/spec/runner/exception_spec.rb @@ -0,0 +1,146 @@ +require 'spec_helper' +require 'mspec/expectations/expectations' +require 'mspec/runner/example' +require 'mspec/runner/exception' +require 'mspec/utils/script' + +RSpec.describe ExceptionState, "#initialize" do + it "takes a state, location (e.g. before :each), and exception" do + context = ContextState.new "Class#method" + state = ExampleState.new context, "does something" + exc = Exception.new "Fail!" + expect(ExceptionState.new(state, "location", exc)).to be_kind_of(ExceptionState) + end +end + +RSpec.describe ExceptionState, "#description" do + before :each do + context = ContextState.new "Class#method" + @state = ExampleState.new context, "does something" + end + + it "returns the state description if state was not nil" do + exc = ExceptionState.new(@state, nil, nil) + expect(exc.description).to eq("Class#method does something") + end + + it "returns the location if it is not nil and description is nil" do + exc = ExceptionState.new(nil, "location", nil) + expect(exc.description).to eq("An exception occurred during: location") + end + + it "returns both description and location if neither are nil" do + exc = ExceptionState.new(@state, "location", nil) + expect(exc.description).to eq("An exception occurred during: location\nClass#method does something") + end +end + +RSpec.describe ExceptionState, "#describe" do + before :each do + context = ContextState.new "Class#method" + @state = ExampleState.new context, "does something" + end + + it "returns the ExampleState#describe string if created with a non-nil state" do + expect(ExceptionState.new(@state, nil, nil).describe).to eq(@state.describe) + end + + it "returns an empty string if created with a nil state" do + expect(ExceptionState.new(nil, nil, nil).describe).to eq("") + end +end + +RSpec.describe ExceptionState, "#it" do + before :each do + context = ContextState.new "Class#method" + @state = ExampleState.new context, "does something" + end + + it "returns the ExampleState#it string if created with a non-nil state" do + expect(ExceptionState.new(@state, nil, nil).it).to eq(@state.it) + end + + it "returns an empty string if created with a nil state" do + expect(ExceptionState.new(nil, nil, nil).it).to eq("") + end +end + +RSpec.describe ExceptionState, "#failure?" do + before :each do + @state = ExampleState.new ContextState.new("C#m"), "works" + end + + it "returns true if the exception is an SpecExpectationNotMetError" do + exc = ExceptionState.new @state, "", SpecExpectationNotMetError.new("Fail!") + expect(exc.failure?).to be_truthy + end + + it "returns true if the exception is an SpecExpectationNotFoundError" do + exc = ExceptionState.new @state, "", SpecExpectationNotFoundError.new("Fail!") + expect(exc.failure?).to be_truthy + end + + it "returns false if the exception is not an SpecExpectationNotMetError or an SpecExpectationNotFoundError" do + exc = ExceptionState.new @state, "", Exception.new("Fail!") + expect(exc.failure?).to be_falsey + end +end + +RSpec.describe ExceptionState, "#message" do + before :each do + @state = ExampleState.new ContextState.new("C#m"), "works" + end + + it "returns <No message> if the exception message is empty" do + exc = ExceptionState.new @state, "", Exception.new("") + expect(exc.message).to eq("Exception: <No message>") + end + + it "returns the message without exception class when the exception is an SpecExpectationNotMetError" do + exc = ExceptionState.new @state, "", SpecExpectationNotMetError.new("Fail!") + expect(exc.message).to eq("Fail!") + end + + it "returns SpecExpectationNotFoundError#message when the exception is an SpecExpectationNotFoundError" do + e = SpecExpectationNotFoundError.new + exc = ExceptionState.new @state, "", e + expect(exc.message).to eq(e.message) + end + + it "returns the message with exception class when the exception is not an SpecExpectationNotMetError or an SpecExpectationNotFoundError" do + exc = ExceptionState.new @state, "", Exception.new("Fail!") + expect(exc.message).to eq("Exception: Fail!") + end +end + +RSpec.describe ExceptionState, "#backtrace" do + before :each do + @state = ExampleState.new ContextState.new("C#m"), "works" + begin + raise Exception + rescue Exception => @exception + @exc = ExceptionState.new @state, "", @exception + end + end + + after :each do + $MSPEC_DEBUG = nil + end + + it "returns a string representation of the exception backtrace" do + expect(@exc.backtrace).to be_kind_of(String) + end + + it "does not filter files from the backtrace if $MSPEC_DEBUG is true" do + $MSPEC_DEBUG = true + expect(@exc.backtrace).to eq(@exception.backtrace.join("\n")) + end + + it "filters files matching config[:backtrace_filter]" do + MSpecScript.set :backtrace_filter, %r[mspec/lib] + $MSPEC_DEBUG = nil + @exc.backtrace.split("\n").each do |line| + expect(line).not_to match(%r[mspec/lib]) + end + end +end diff --git a/spec/mspec/spec/runner/filters/a.yaml b/spec/mspec/spec/runner/filters/a.yaml new file mode 100644 index 0000000000..1940e3cba6 --- /dev/null +++ b/spec/mspec/spec/runner/filters/a.yaml @@ -0,0 +1,4 @@ +--- +A#: +- a +- aa diff --git a/spec/mspec/spec/runner/filters/b.yaml b/spec/mspec/spec/runner/filters/b.yaml new file mode 100644 index 0000000000..a24bdb2f19 --- /dev/null +++ b/spec/mspec/spec/runner/filters/b.yaml @@ -0,0 +1,11 @@ +--- +B.: +- b +- bb +B::C#: +- b! +- b= +- b? +- "-" +- "[]" +- "[]=" diff --git a/spec/mspec/spec/runner/filters/match_spec.rb b/spec/mspec/spec/runner/filters/match_spec.rb new file mode 100644 index 0000000000..970da00446 --- /dev/null +++ b/spec/mspec/spec/runner/filters/match_spec.rb @@ -0,0 +1,34 @@ +require File.dirname(__FILE__) + '/../../spec_helper' +require 'mspec/runner/mspec' +require 'mspec/runner/filters/match' + +RSpec.describe MatchFilter, "#===" do + before :each do + @filter = MatchFilter.new nil, 'a', 'b', 'c' + end + + it "returns true if the argument matches any of the #initialize strings" do + expect(@filter.===('aaa')).to eq(true) + expect(@filter.===('bccb')).to eq(true) + end + + it "returns false if the argument matches none of the #initialize strings" do + expect(@filter.===('d')).to eq(false) + end +end + +RSpec.describe MatchFilter, "#register" do + it "registers itself with MSpec for the designated action list" do + filter = MatchFilter.new :include + expect(MSpec).to receive(:register).with(:include, filter) + filter.register + end +end + +RSpec.describe MatchFilter, "#unregister" do + it "unregisters itself with MSpec for the designated action list" do + filter = MatchFilter.new :exclude + expect(MSpec).to receive(:unregister).with(:exclude, filter) + filter.unregister + end +end diff --git a/spec/mspec/spec/runner/filters/profile_spec.rb b/spec/mspec/spec/runner/filters/profile_spec.rb new file mode 100644 index 0000000000..25f5e07aef --- /dev/null +++ b/spec/mspec/spec/runner/filters/profile_spec.rb @@ -0,0 +1,117 @@ +require File.dirname(__FILE__) + '/../../spec_helper' +require 'mspec/runner/mspec' +require 'mspec/runner/filters/profile' + +RSpec.describe ProfileFilter, "#find" do + before :each do + @filter = ProfileFilter.new nil + allow(File).to receive(:exist?).and_return(false) + @file = "rails.yaml" + end + + it "attempts to locate the file through the expanded path name" do + expect(File).to receive(:expand_path).with(@file).and_return(@file) + expect(File).to receive(:exist?).with(@file).and_return(true) + expect(@filter.find(@file)).to eq(@file) + end + + it "attempts to locate the file in 'spec/profiles'" do + path = File.join "spec/profiles", @file + expect(File).to receive(:exist?).with(path).and_return(true) + expect(@filter.find(@file)).to eq(path) + end + + it "attempts to locate the file in 'spec'" do + path = File.join "spec", @file + expect(File).to receive(:exist?).with(path).and_return(true) + expect(@filter.find(@file)).to eq(path) + end + + it "attempts to locate the file in 'profiles'" do + path = File.join "profiles", @file + expect(File).to receive(:exist?).with(path).and_return(true) + expect(@filter.find(@file)).to eq(path) + end + + it "attempts to locate the file in '.'" do + path = File.join ".", @file + expect(File).to receive(:exist?).with(path).and_return(true) + expect(@filter.find(@file)).to eq(path) + end +end + +RSpec.describe ProfileFilter, "#parse" do + before :each do + @filter = ProfileFilter.new nil + @file = File.open(File.dirname(__FILE__) + "/b.yaml", "r") + end + + after :each do + @file.close + end + + it "creates a Hash of the contents of the YAML file" do + expect(@filter.parse(@file)).to eq({ + "B." => ["b", "bb"], + "B::C#" => ["b!", "b=", "b?", "-", "[]", "[]="] + }) + end +end + +RSpec.describe ProfileFilter, "#load" do + before :each do + @filter = ProfileFilter.new nil + @files = [ + File.dirname(__FILE__) + "/a.yaml", + File.dirname(__FILE__) + "/b.yaml" + ] + end + + it "generates a composite hash from multiple YAML files" do + expect(@filter.load(*@files)).to eq({ + "A#" => ["a", "aa"], + "B." => ["b", "bb"], + "B::C#" => ["b!", "b=", "b?", "-", "[]", "[]="] + }) + end +end + +RSpec.describe ProfileFilter, "#===" do + before :each do + @filter = ProfileFilter.new nil + allow(@filter).to receive(:load).and_return({ "A#" => ["[]=", "a", "a!", "a?", "aa="]}) + @filter.send :initialize, nil + end + + it "returns true if the spec description is for a method in the profile" do + expect(@filter.===("The A#[]= method")).to eq(true) + expect(@filter.===("A#a returns")).to eq(true) + expect(@filter.===("A#a! replaces")).to eq(true) + expect(@filter.===("A#a? returns")).to eq(true) + expect(@filter.===("A#aa= raises")).to eq(true) + end + + it "returns false if the spec description is for a method not in the profile" do + expect(@filter.===("The A#[] method")).to eq(false) + expect(@filter.===("B#a returns")).to eq(false) + expect(@filter.===("A.a! replaces")).to eq(false) + expect(@filter.===("AA#a? returns")).to eq(false) + expect(@filter.===("A#aa raises")).to eq(false) + end +end + +RSpec.describe ProfileFilter, "#register" do + it "registers itself with MSpec for the designated action list" do + filter = ProfileFilter.new :include + expect(MSpec).to receive(:register).with(:include, filter) + filter.register + end +end + +RSpec.describe ProfileFilter, "#unregister" do + it "unregisters itself with MSpec for the designated action list" do + filter = ProfileFilter.new :exclude + expect(MSpec).to receive(:unregister).with(:exclude, filter) + filter.unregister + end +end diff --git a/spec/mspec/spec/runner/filters/regexp_spec.rb b/spec/mspec/spec/runner/filters/regexp_spec.rb new file mode 100644 index 0000000000..1d1d3554f6 --- /dev/null +++ b/spec/mspec/spec/runner/filters/regexp_spec.rb @@ -0,0 +1,31 @@ +require File.dirname(__FILE__) + '/../../spec_helper' +require 'mspec/runner/mspec' +require 'mspec/runner/filters/regexp' + +RSpec.describe MatchFilter, "#===" do + before :each do + @filter = RegexpFilter.new nil, 'a(b|c)', 'b[^ab]', 'cc?' + end + + it "returns true if the argument matches any of the #initialize strings" do + expect(@filter.===('ab')).to eq(true) + expect(@filter.===('bc suffix')).to eq(true) + expect(@filter.===('prefix cc')).to eq(true) + end + + it "returns false if the argument matches none of the #initialize strings" do + expect(@filter.===('aa')).to eq(false) + expect(@filter.===('ba')).to eq(false) + expect(@filter.===('prefix d suffix')).to eq(false) + end +end + +RSpec.describe RegexpFilter, "#to_regexp" do + before :each do + @filter = RegexpFilter.new nil + end + + it "converts its arguments to Regexp instances" do + expect(@filter.send(:to_regexp, 'a(b|c)', 'b[^ab]', 'cc?')).to eq([/a(b|c)/, /b[^ab]/, /cc?/]) + end +end diff --git a/spec/mspec/spec/runner/filters/tag_spec.rb b/spec/mspec/spec/runner/filters/tag_spec.rb new file mode 100644 index 0000000000..356175a754 --- /dev/null +++ b/spec/mspec/spec/runner/filters/tag_spec.rb @@ -0,0 +1,92 @@ +require File.dirname(__FILE__) + '/../../spec_helper' +require 'mspec/runner/mspec' +require 'mspec/runner/filters/match' +require 'mspec/runner/filters/tag' + +RSpec.describe TagFilter, "#load" do + before :each do + @match = double("match filter").as_null_object + @filter = TagFilter.new :include, "tag", "key" + @tag = SpecTag.new "tag(comment):description" + allow(MSpec).to receive(:read_tags).and_return([@tag]) + allow(MSpec).to receive(:register) + end + + it "loads tags from the tag file" do + expect(MSpec).to receive(:read_tags).with(["tag", "key"]).and_return([]) + @filter.load + end + + + it "registers itself with MSpec for the :include action" do + filter = TagFilter.new(:include) + expect(MSpec).to receive(:register).with(:include, filter) + filter.load + end + + it "registers itself with MSpec for the :exclude action" do + filter = TagFilter.new(:exclude) + expect(MSpec).to receive(:register).with(:exclude, filter) + filter.load + end +end + +RSpec.describe TagFilter, "#unload" do + before :each do + @filter = TagFilter.new :include, "tag", "key" + @tag = SpecTag.new "tag(comment):description" + allow(MSpec).to receive(:read_tags).and_return([@tag]) + allow(MSpec).to receive(:register) + end + + it "unregisters itself" do + @filter.load + expect(MSpec).to receive(:unregister).with(:include, @filter) + @filter.unload + end +end + +RSpec.describe TagFilter, "#register" do + before :each do + allow(MSpec).to receive(:register) + end + + it "registers itself with MSpec for the :load, :unload actions" do + filter = TagFilter.new(nil) + expect(MSpec).to receive(:register).with(:load, filter) + expect(MSpec).to receive(:register).with(:unload, filter) + filter.register + end +end + +RSpec.describe TagFilter, "#unregister" do + before :each do + allow(MSpec).to receive(:unregister) + end + + it "unregisters itself with MSpec for the :load, :unload actions" do + filter = TagFilter.new(nil) + expect(MSpec).to receive(:unregister).with(:load, filter) + expect(MSpec).to receive(:unregister).with(:unload, filter) + filter.unregister + end +end + +RSpec.describe TagFilter, "#===" do + before :each do + @filter = TagFilter.new nil, "tag", "key" + @tag = SpecTag.new "tag(comment):description" + allow(MSpec).to receive(:read_tags).and_return([@tag]) + allow(MSpec).to receive(:register) + @filter.load + end + + it "returns true if the argument matches any of the descriptions" do + expect(@filter.===('description')).to eq(true) + end + + it "returns false if the argument matches none of the descriptions" do + expect(@filter.===('descriptionA')).to eq(false) + expect(@filter.===('adescription')).to eq(false) + end +end diff --git a/spec/mspec/spec/runner/formatters/describe_spec.rb b/spec/mspec/spec/runner/formatters/describe_spec.rb new file mode 100644 index 0000000000..55f497aca6 --- /dev/null +++ b/spec/mspec/spec/runner/formatters/describe_spec.rb @@ -0,0 +1,67 @@ +require File.dirname(__FILE__) + '/../../spec_helper' +require 'mspec/runner/formatters/describe' +require 'mspec/runner/example' + +RSpec.describe DescribeFormatter, "#finish" do + before :each do + allow(MSpec).to receive(:register) + allow(MSpec).to receive(:unregister) + + @timer = double("timer").as_null_object + allow(TimerAction).to receive(:new).and_return(@timer) + allow(@timer).to receive(:format).and_return("Finished in 2.0 seconds") + + $stdout = @out = IOStub.new + context = ContextState.new "Class#method" + @state = ExampleState.new(context, "runs") + + @formatter = DescribeFormatter.new + @formatter.register + + @tally = @formatter.tally + @counter = @tally.counter + + @counter.files! + @counter.examples! + @counter.expectations! 2 + end + + after :each do + $stdout = STDOUT + end + + it "prints a summary of elapsed time" do + @formatter.finish + expect(@out).to match(/^Finished in 2.0 seconds$/) + end + + it "prints a tally of counts" do + @formatter.finish + expect(@out).to match(/^1 file, 1 example, 2 expectations, 0 failures, 0 errors, 0 tagged$/) + end + + it "does not print exceptions" do + @formatter.finish + expect(@out).to eq(%[ + +Finished in 2.0 seconds + +1 file, 1 example, 2 expectations, 0 failures, 0 errors, 0 tagged +]) + end + + it "prints a summary of failures and errors for each describe block" do + exc = ExceptionState.new @state, nil, MSpecExampleError.new("broken") + allow(exc).to receive(:backtrace).and_return("path/to/some/file.rb:35:in method") + @formatter.exception exc + @formatter.finish + expect(@out).to eq(%[ + +Class#method 0 failures, 1 error + +Finished in 2.0 seconds + +1 file, 1 example, 2 expectations, 0 failures, 0 errors, 0 tagged +]) + end +end diff --git a/spec/mspec/spec/runner/formatters/dotted_spec.rb b/spec/mspec/spec/runner/formatters/dotted_spec.rb new file mode 100644 index 0000000000..336b1227ed --- /dev/null +++ b/spec/mspec/spec/runner/formatters/dotted_spec.rb @@ -0,0 +1,284 @@ +require File.dirname(__FILE__) + '/../../spec_helper' +require 'mspec/runner/formatters/dotted' +require 'mspec/runner/mspec' +require 'mspec/runner/example' +require 'mspec/utils/script' + +RSpec.describe DottedFormatter, "#initialize" do + it "permits zero arguments" do + DottedFormatter.new + end + + it "accepts one argument" do + DottedFormatter.new nil + end +end + +RSpec.describe DottedFormatter, "#register" do + before :each do + @formatter = DottedFormatter.new + allow(MSpec).to receive(:register) + end + + it "registers self with MSpec for appropriate actions" do + expect(MSpec).to receive(:register).with(:exception, @formatter) + expect(MSpec).to receive(:register).with(:before, @formatter) + expect(MSpec).to receive(:register).with(:after, @formatter) + expect(MSpec).to receive(:register).with(:finish, @formatter) + @formatter.register + end + + it "creates TimerAction and TallyAction" do + timer = double("timer") + tally = double("tally") + expect(timer).to receive(:register) + expect(tally).to receive(:register) + expect(tally).to receive(:counter) + expect(TimerAction).to receive(:new).and_return(timer) + expect(TallyAction).to receive(:new).and_return(tally) + @formatter.register + end +end + +RSpec.describe DottedFormatter, "#print" do + before :each do + $stdout = IOStub.new + end + + after :each do + $stdout = STDOUT + end + + it "writes to $stdout by default" do + formatter = DottedFormatter.new + formatter.print "begonias" + expect($stdout).to eq("begonias") + end + + it "writes to the file specified when the formatter was created" do + out = IOStub.new + expect(File).to receive(:open).with("some/file", "w").and_return(out) + formatter = DottedFormatter.new "some/file" + formatter.print "begonias" + expect(out).to eq("begonias") + end + + it "flushes the IO output" do + expect($stdout).to receive(:flush) + formatter = DottedFormatter.new + formatter.print "begonias" + end +end + +RSpec.describe DottedFormatter, "#exception" do + before :each do + @formatter = DottedFormatter.new + @failure = ExceptionState.new nil, nil, SpecExpectationNotMetError.new("failed") + @error = ExceptionState.new nil, nil, MSpecExampleError.new("boom!") + end + + it "sets the #failure? flag" do + @formatter.exception @failure + expect(@formatter.failure?).to be_truthy + @formatter.exception @error + expect(@formatter.failure?).to be_falsey + end + + it "sets the #exception? flag" do + @formatter.exception @error + expect(@formatter.exception?).to be_truthy + @formatter.exception @failure + expect(@formatter.exception?).to be_truthy + end + + it "adds the exception to the list of exceptions" do + expect(@formatter.exceptions).to eq([]) + @formatter.exception @error + @formatter.exception @failure + expect(@formatter.exceptions).to eq([@error, @failure]) + end +end + +RSpec.describe DottedFormatter, "#exception?" do + before :each do + @formatter = DottedFormatter.new + @failure = ExceptionState.new nil, nil, SpecExpectationNotMetError.new("failed") + @error = ExceptionState.new nil, nil, MSpecExampleError.new("boom!") + end + + it "returns false if there have been no exceptions" do + expect(@formatter.exception?).to be_falsey + end + + it "returns true if any exceptions are errors" do + @formatter.exception @failure + @formatter.exception @error + expect(@formatter.exception?).to be_truthy + end + + it "returns true if all exceptions are failures" do + @formatter.exception @failure + @formatter.exception @failure + expect(@formatter.exception?).to be_truthy + end + + it "returns true if all exceptions are errors" do + @formatter.exception @error + @formatter.exception @error + expect(@formatter.exception?).to be_truthy + end +end + +RSpec.describe DottedFormatter, "#failure?" do + before :each do + @formatter = DottedFormatter.new + @failure = ExceptionState.new nil, nil, SpecExpectationNotMetError.new("failed") + @error = ExceptionState.new nil, nil, MSpecExampleError.new("boom!") + end + + it "returns false if there have been no exceptions" do + expect(@formatter.failure?).to be_falsey + end + + it "returns false if any exceptions are errors" do + @formatter.exception @failure + @formatter.exception @error + expect(@formatter.failure?).to be_falsey + end + + it "returns true if all exceptions are failures" do + @formatter.exception @failure + @formatter.exception @failure + expect(@formatter.failure?).to be_truthy + end +end + +RSpec.describe DottedFormatter, "#before" do + before :each do + @state = ExampleState.new ContextState.new("describe"), "it" + @formatter = DottedFormatter.new + @formatter.exception ExceptionState.new(nil, nil, SpecExpectationNotMetError.new("Failed!")) + end + + it "resets the #failure? flag to false" do + expect(@formatter.failure?).to be_truthy + @formatter.before @state + expect(@formatter.failure?).to be_falsey + end + + it "resets the #exception? flag to false" do + expect(@formatter.exception?).to be_truthy + @formatter.before @state + expect(@formatter.exception?).to be_falsey + end +end + +RSpec.describe DottedFormatter, "#after" do + before :each do + $stdout = @out = IOStub.new + @formatter = DottedFormatter.new + @state = ExampleState.new ContextState.new("describe"), "it" + end + + after :each do + $stdout = STDOUT + end + + it "prints a '.' if there was no exception raised" do + @formatter.after(@state) + expect(@out).to eq(".") + end + + it "prints an 'F' if there was an expectation failure" do + exc = SpecExpectationNotMetError.new "failed" + @formatter.exception ExceptionState.new(@state, nil, exc) + @formatter.after(@state) + expect(@out).to eq("F") + end + + it "prints an 'E' if there was an exception other than expectation failure" do + exc = MSpecExampleError.new("boom!") + @formatter.exception ExceptionState.new(@state, nil, exc) + @formatter.after(@state) + expect(@out).to eq("E") + end + + it "prints an 'E' if there are mixed exceptions and exepctation failures" do + exc = SpecExpectationNotMetError.new "failed" + @formatter.exception ExceptionState.new(@state, nil, exc) + exc = MSpecExampleError.new("boom!") + @formatter.exception ExceptionState.new(@state, nil, exc) + @formatter.after(@state) + expect(@out).to eq("E") + end +end + +RSpec.describe DottedFormatter, "#finish" do + before :each do + @tally = double("tally").as_null_object + allow(TallyAction).to receive(:new).and_return(@tally) + @timer = double("timer").as_null_object + allow(TimerAction).to receive(:new).and_return(@timer) + + $stdout = @out = IOStub.new + context = ContextState.new "Class#method" + @state = ExampleState.new(context, "runs") + allow(MSpec).to receive(:register) + @formatter = DottedFormatter.new + @formatter.register + end + + after :each do + $stdout = STDOUT + end + + it "prints a failure message for an exception" do + exc = ExceptionState.new @state, nil, MSpecExampleError.new("broken") + @formatter.exception exc + @formatter.after @state + @formatter.finish + expect(@out).to match(/^1\)\nClass#method runs ERROR$/) + end + + it "prints a backtrace for an exception" do + exc = ExceptionState.new @state, nil, MSpecExampleError.new("broken") + allow(exc).to receive(:backtrace).and_return("path/to/some/file.rb:35:in method") + @formatter.exception exc + @formatter.after @state + @formatter.finish + expect(@out).to match(%r[path/to/some/file.rb:35:in method$]) + end + + it "prints a summary of elapsed time" do + expect(@timer).to receive(:format).and_return("Finished in 2.0 seconds") + @formatter.finish + expect(@out).to match(/^Finished in 2.0 seconds$/) + end + + it "prints a tally of counts" do + expect(@tally).to receive(:format).and_return("1 example, 0 failures") + @formatter.finish + expect(@out).to match(/^1 example, 0 failures$/) + end + + it "prints errors, backtraces, elapsed time, and tallies" do + exc = ExceptionState.new @state, nil, MSpecExampleError.new("broken") + allow(exc).to receive(:backtrace).and_return("path/to/some/file.rb:35:in method") + @formatter.exception exc + expect(@timer).to receive(:format).and_return("Finished in 2.0 seconds") + expect(@tally).to receive(:format).and_return("1 example, 1 failure") + @formatter.after @state + @formatter.finish + expect(@out).to eq(%[E + +1) +Class#method runs ERROR +MSpecExampleError: broken +path/to/some/file.rb:35:in method + +Finished in 2.0 seconds + +1 example, 1 failure +]) + end +end diff --git a/spec/mspec/spec/runner/formatters/file_spec.rb b/spec/mspec/spec/runner/formatters/file_spec.rb new file mode 100644 index 0000000000..ae11d60845 --- /dev/null +++ b/spec/mspec/spec/runner/formatters/file_spec.rb @@ -0,0 +1,84 @@ +require File.dirname(__FILE__) + '/../../spec_helper' +require 'mspec/runner/formatters/file' +require 'mspec/runner/mspec' +require 'mspec/runner/example' + +RSpec.describe FileFormatter, "#register" do + before :each do + @formatter = FileFormatter.new + allow(MSpec).to receive(:register) + allow(MSpec).to receive(:unregister) + end + + it "registers self with MSpec for :load, :unload actions" do + expect(MSpec).to receive(:register).with(:load, @formatter) + expect(MSpec).to receive(:register).with(:unload, @formatter) + @formatter.register + end + + it "unregisters self with MSpec for :before, :after actions" do + expect(MSpec).to receive(:unregister).with(:before, @formatter) + expect(MSpec).to receive(:unregister).with(:after, @formatter) + @formatter.register + end +end + +RSpec.describe FileFormatter, "#load" do + before :each do + @state = ExampleState.new ContextState.new("describe"), "it" + @formatter = FileFormatter.new + @formatter.exception ExceptionState.new(nil, nil, SpecExpectationNotMetError.new("Failed!")) + end + + it "resets the #failure? flag to false" do + expect(@formatter.failure?).to be_truthy + @formatter.load @state + expect(@formatter.failure?).to be_falsey + end + + it "resets the #exception? flag to false" do + expect(@formatter.exception?).to be_truthy + @formatter.load @state + expect(@formatter.exception?).to be_falsey + end +end + +RSpec.describe FileFormatter, "#unload" do + before :each do + $stdout = @out = IOStub.new + @formatter = FileFormatter.new + @state = ExampleState.new ContextState.new("describe"), "it" + end + + after :each do + $stdout = STDOUT + end + + it "prints a '.' if there was no exception raised" do + @formatter.unload(@state) + expect(@out).to eq(".") + end + + it "prints an 'F' if there was an expectation failure" do + exc = SpecExpectationNotMetError.new "failed" + @formatter.exception ExceptionState.new(@state, nil, exc) + @formatter.unload(@state) + expect(@out).to eq("F") + end + + it "prints an 'E' if there was an exception other than expectation failure" do + exc = MSpecExampleError.new("boom!") + @formatter.exception ExceptionState.new(@state, nil, exc) + @formatter.unload(@state) + expect(@out).to eq("E") + end + + it "prints an 'E' if there are mixed exceptions and exepctation failures" do + exc = SpecExpectationNotMetError.new "failed" + @formatter.exception ExceptionState.new(@state, nil, exc) + exc = MSpecExampleError.new("boom!") + @formatter.exception ExceptionState.new(@state, nil, exc) + @formatter.unload(@state) + expect(@out).to eq("E") + end +end diff --git a/spec/mspec/spec/runner/formatters/html_spec.rb b/spec/mspec/spec/runner/formatters/html_spec.rb new file mode 100644 index 0000000000..ed973ad93f --- /dev/null +++ b/spec/mspec/spec/runner/formatters/html_spec.rb @@ -0,0 +1,220 @@ +require File.dirname(__FILE__) + '/../../spec_helper' +require 'mspec/guards/guard' +require 'mspec/runner/formatters/html' +require 'mspec/runner/mspec' +require 'mspec/runner/example' +require 'mspec/utils/script' +require 'mspec/helpers' + +RSpec.describe HtmlFormatter do + before :each do + @formatter = HtmlFormatter.new + end + + it "responds to #register by registering itself with MSpec for appropriate actions" do + allow(MSpec).to receive(:register) + expect(MSpec).to receive(:register).with(:start, @formatter) + expect(MSpec).to receive(:register).with(:enter, @formatter) + expect(MSpec).to receive(:register).with(:leave, @formatter) + @formatter.register + end +end + +RSpec.describe HtmlFormatter, "#start" do + before :each do + $stdout = @out = IOStub.new + @formatter = HtmlFormatter.new + end + + after :each do + $stdout = STDOUT + end + + it "prints the HTML head" do + @formatter.start + ruby_engine = RUBY_ENGINE + expect(ruby_engine).to match(/^#{ruby_engine}/) + expect(@out).to eq(%[<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" + "http://www.w3.org/TR/html4/strict.dtd"> +<html> +<head> +<title>Spec Output For #{ruby_engine} (#{RUBY_VERSION})</title> +<style type="text/css"> +ul { + list-style: none; +} +.fail { + color: red; +} +.pass { + color: green; +} +#details :target { + background-color: #ffffe0; +} +</style> +</head> +<body> +]) + end +end + +RSpec.describe HtmlFormatter, "#enter" do + before :each do + $stdout = @out = IOStub.new + @formatter = HtmlFormatter.new + end + + after :each do + $stdout = STDOUT + end + + it "prints the #describe string" do + @formatter.enter "describe" + expect(@out).to eq("<div><p>describe</p>\n<ul>\n") + end +end + +RSpec.describe HtmlFormatter, "#leave" do + before :each do + $stdout = @out = IOStub.new + @formatter = HtmlFormatter.new + end + + after :each do + $stdout = STDOUT + end + + it "prints the closing tags for the #describe string" do + @formatter.leave + expect(@out).to eq("</ul>\n</div>\n") + end +end + +RSpec.describe HtmlFormatter, "#exception" do + before :each do + $stdout = @out = IOStub.new + @formatter = HtmlFormatter.new + @formatter.register + @state = ExampleState.new ContextState.new("describe"), "it" + end + + after :each do + $stdout = STDOUT + end + + it "prints the #it string once for each exception raised" do + exc = ExceptionState.new @state, nil, SpecExpectationNotMetError.new("disappointing") + @formatter.exception exc + exc = ExceptionState.new @state, nil, MSpecExampleError.new("painful") + @formatter.exception exc + expect(@out).to eq(%[<li class="fail">- it (<a href="#details-1">FAILED - 1</a>)</li> +<li class="fail">- it (<a href="#details-2">ERROR - 2</a>)</li> +]) + end +end + +RSpec.describe HtmlFormatter, "#after" do + before :each do + $stdout = @out = IOStub.new + @formatter = HtmlFormatter.new + @formatter.register + @state = ExampleState.new ContextState.new("describe"), "it" + end + + after :each do + $stdout = STDOUT + end + + it "prints the #it once when there are no exceptions raised" do + @formatter.after @state + expect(@out).to eq(%[<li class="pass">- it</li>\n]) + end + + it "does not print any output if an exception is raised" do + exc = ExceptionState.new @state, nil, SpecExpectationNotMetError.new("disappointing") + @formatter.exception exc + out = @out.dup + @formatter.after @state + expect(@out).to eq(out) + end +end + +RSpec.describe HtmlFormatter, "#finish" do + before :each do + @tally = double("tally").as_null_object + allow(TallyAction).to receive(:new).and_return(@tally) + @timer = double("timer").as_null_object + allow(TimerAction).to receive(:new).and_return(@timer) + + @out = tmp("HtmlFormatter") + + context = ContextState.new "describe" + @state = ExampleState.new(context, "it") + allow(MSpec).to receive(:register) + @formatter = HtmlFormatter.new(@out) + @formatter.register + @exception = MSpecExampleError.new("broken") + allow(@exception).to receive(:backtrace).and_return(["file.rb:1", "file.rb:2"]) + end + + after :each do + rm_r @out + end + + it "prints a failure message for an exception" do + exc = ExceptionState.new @state, nil, @exception + @formatter.exception exc + @formatter.finish + output = File.read(@out) + expect(output).to include "<p>describe it ERROR</p>" + end + + it "prints a backtrace for an exception" do + exc = ExceptionState.new @state, nil, @exception + allow(exc).to receive(:backtrace).and_return("path/to/some/file.rb:35:in method") + @formatter.exception exc + @formatter.finish + output = File.read(@out) + expect(output).to match(%r[<pre>.*path/to/some/file.rb:35:in method.*</pre>]m) + end + + it "prints a summary of elapsed time" do + expect(@timer).to receive(:format).and_return("Finished in 2.0 seconds") + @formatter.finish + output = File.read(@out) + expect(output).to include "<p>Finished in 2.0 seconds</p>\n" + end + + it "prints a tally of counts" do + expect(@tally).to receive(:format).and_return("1 example, 0 failures") + @formatter.finish + output = File.read(@out) + expect(output).to include '<p class="pass">1 example, 0 failures</p>' + end + + it "prints errors, backtraces, elapsed time, and tallies" do + exc = ExceptionState.new @state, nil, @exception + allow(exc).to receive(:backtrace).and_return("path/to/some/file.rb:35:in method") + @formatter.exception exc + + expect(@timer).to receive(:format).and_return("Finished in 2.0 seconds") + expect(@tally).to receive(:format).and_return("1 example, 1 failures") + @formatter.finish + output = File.read(@out) + expect(output).to eq(%[<li class=\"fail\">- it (<a href=\"#details-1\">ERROR - 1</a>)</li> +<hr> +<ol id="details"> +<li id="details-1"><p>describe it ERROR</p> +<p>MSpecExampleError: broken</p> +<pre> +path/to/some/file.rb:35:in method</pre> +</li> +</ol> +<p>Finished in 2.0 seconds</p> +<p class="fail">1 example, 1 failures</p> +</body> +</html> +]) + end +end diff --git a/spec/mspec/spec/runner/formatters/junit_spec.rb b/spec/mspec/spec/runner/formatters/junit_spec.rb new file mode 100644 index 0000000000..3b3da73849 --- /dev/null +++ b/spec/mspec/spec/runner/formatters/junit_spec.rb @@ -0,0 +1,159 @@ +# -*- coding: utf-8 -*- +require File.dirname(__FILE__) + '/../../spec_helper' +require 'mspec/runner/formatters/junit' +require 'mspec/runner/example' +require 'mspec/helpers' + +RSpec.describe JUnitFormatter, "#initialize" do + it "permits zero arguments" do + expect { JUnitFormatter.new }.not_to raise_error + end + + it "accepts one argument" do + expect { JUnitFormatter.new nil }.not_to raise_error + end +end + +RSpec.describe JUnitFormatter, "#print" do + before :each do + $stdout = IOStub.new + @out = IOStub.new + allow(File).to receive(:open).and_return(@out) + @formatter = JUnitFormatter.new "some/file" + end + + after :each do + $stdout = STDOUT + end + + it "writes to $stdout if #switch has not been called" do + @formatter.print "begonias" + expect($stdout).to eq("begonias") + expect(@out).to eq("") + end + + it "writes to the file passed to #initialize once #switch has been called" do + @formatter.switch + @formatter.print "begonias" + expect($stdout).to eq("") + expect(@out).to eq("begonias") + end + + it "writes to $stdout once #switch is called if no file was passed to #initialize" do + formatter = JUnitFormatter.new + formatter.switch + formatter.print "begonias" + expect($stdout).to eq("begonias") + expect(@out).to eq("") + end +end + +RSpec.describe JUnitFormatter, "#finish" do + before :each do + @tally = double("tally").as_null_object + @counter = double("counter").as_null_object + allow(@tally).to receive(:counter).and_return(@counter) + allow(TallyAction).to receive(:new).and_return(@tally) + + @timer = double("timer").as_null_object + allow(TimerAction).to receive(:new).and_return(@timer) + + @out = tmp("JUnitFormatter") + + context = ContextState.new "describe" + @state = ExampleState.new(context, "it") + + @formatter = JUnitFormatter.new(@out) + allow(@formatter).to receive(:backtrace).and_return("") + allow(MSpec).to receive(:register) + @formatter.register + + exc = ExceptionState.new @state, nil, MSpecExampleError.new("broken") + allow(exc).to receive(:backtrace).and_return("path/to/some/file.rb:35:in method") + @formatter.exception exc + @formatter.after @state + end + + after :each do + rm_r @out + end + + it "calls #switch" do + expect(@formatter).to receive(:switch).and_call_original + @formatter.finish + end + + it "outputs a failure message and backtrace" do + @formatter.finish + output = File.read(@out) + expect(output).to include 'message="error in describe it" type="error"' + expect(output).to include "MSpecExampleError: broken\n" + expect(output).to include "path/to/some/file.rb:35:in method" + end + + it "encodes message and backtrace in latin1 for jenkins" do + exc = ExceptionState.new @state, nil, MSpecExampleError.new("broken…") + allow(exc).to receive(:backtrace).and_return("path/to/some/file.rb:35:in methød") + @formatter.exception exc + @formatter.finish + output = File.binread(@out) + expect(output).to match(/MSpecExampleError: broken((\.\.\.)|\?)\n/) + expect(output).to match(/path\/to\/some\/file\.rb:35:in meth(\?|o)d/) + end + + it "outputs an elapsed time" do + expect(@timer).to receive(:elapsed).and_return(4.2) + @formatter.finish + output = File.read(@out) + expect(output).to include 'time="4.2"' + end + + it "outputs overall elapsed time" do + expect(@timer).to receive(:elapsed).and_return(4.2) + @formatter.finish + output = File.read(@out) + expect(output).to include 'timeCount="4.2"' + end + + it "outputs the number of examples as test count" do + expect(@counter).to receive(:examples).and_return(9) + @formatter.finish + output = File.read(@out) + expect(output).to include 'tests="9"' + end + + it "outputs overall number of examples as test count" do + expect(@counter).to receive(:examples).and_return(9) + @formatter.finish + output = File.read(@out) + expect(output).to include 'testCount="9"' + end + + it "outputs a failure count" do + expect(@counter).to receive(:failures).and_return(2) + @formatter.finish + output = File.read(@out) + expect(output).to include 'failureCount="2"' + end + + it "outputs overall failure count" do + expect(@counter).to receive(:failures).and_return(2) + @formatter.finish + output = File.read(@out) + expect(output).to include 'failures="2"' + end + + it "outputs an error count" do + expect(@counter).to receive(:errors).and_return(1) + @formatter.finish + output = File.read(@out) + expect(output).to include 'errors="1"' + end + + it "outputs overall error count" do + expect(@counter).to receive(:errors).and_return(1) + @formatter.finish + output = File.read(@out) + expect(output).to include 'errorCount="1"' + end +end diff --git a/spec/mspec/spec/runner/formatters/method_spec.rb b/spec/mspec/spec/runner/formatters/method_spec.rb new file mode 100644 index 0000000000..02bf47d538 --- /dev/null +++ b/spec/mspec/spec/runner/formatters/method_spec.rb @@ -0,0 +1,177 @@ +require File.dirname(__FILE__) + '/../../spec_helper' +require 'mspec/runner/formatters/method' +require 'mspec/runner/mspec' +require 'mspec/runner/example' +require 'mspec/utils/script' + +RSpec.describe MethodFormatter, "#method_type" do + before :each do + @formatter = MethodFormatter.new + end + + it "returns 'class' if the separator is '.' or '::'" do + expect(@formatter.method_type('.')).to eq("class") + expect(@formatter.method_type('::')).to eq("class") + end + + it "returns 'instance' if the separator is '#'" do + expect(@formatter.method_type('#')).to eq("instance") + end + + it "returns 'unknown' for all other cases" do + expect(@formatter.method_type(nil)).to eq("unknown") + end +end + +RSpec.describe MethodFormatter, "#before" do + before :each do + @formatter = MethodFormatter.new + allow(MSpec).to receive(:register) + @formatter.register + end + + it "resets the tally counters to 0" do + @formatter.tally.counter.examples = 3 + @formatter.tally.counter.expectations = 4 + @formatter.tally.counter.failures = 2 + @formatter.tally.counter.errors = 1 + + state = ExampleState.new ContextState.new("describe"), "it" + @formatter.before state + expect(@formatter.tally.counter.examples).to eq(0) + expect(@formatter.tally.counter.expectations).to eq(0) + expect(@formatter.tally.counter.failures).to eq(0) + expect(@formatter.tally.counter.errors).to eq(0) + end + + it "records the class, method if available" do + state = ExampleState.new ContextState.new("Some#method"), "it" + @formatter.before state + key = "Some#method" + expect(@formatter.methods.keys).to include(key) + h = @formatter.methods[key] + expect(h[:class]).to eq("Some") + expect(h[:method]).to eq("method") + expect(h[:description]).to eq("Some#method it") + end + + it "does not record class, method unless both are available" do + state = ExampleState.new ContextState.new("Some method"), "it" + @formatter.before state + key = "Some method" + expect(@formatter.methods.keys).to include(key) + h = @formatter.methods[key] + expect(h[:class]).to eq("") + expect(h[:method]).to eq("") + expect(h[:description]).to eq("Some method it") + end + + it "sets the method type to unknown if class and method are not available" do + state = ExampleState.new ContextState.new("Some method"), "it" + @formatter.before state + key = "Some method" + h = @formatter.methods[key] + expect(h[:type]).to eq("unknown") + end + + it "sets the method type based on the class, method separator" do + [["C#m", "instance"], ["C.m", "class"], ["C::m", "class"]].each do |k, t| + state = ExampleState.new ContextState.new(k), "it" + @formatter.before state + h = @formatter.methods[k] + expect(h[:type]).to eq(t) + end + end + + it "clears the list of exceptions" do + state = ExampleState.new ContextState.new("describe"), "it" + @formatter.exceptions << "stuff" + @formatter.before state + expect(@formatter.exceptions).to be_empty + end +end + +RSpec.describe MethodFormatter, "#after" do + before :each do + @formatter = MethodFormatter.new + allow(MSpec).to receive(:register) + @formatter.register + end + + it "sets the tally counts" do + state = ExampleState.new ContextState.new("Some#method"), "it" + @formatter.before state + + @formatter.tally.counter.examples = 3 + @formatter.tally.counter.expectations = 4 + @formatter.tally.counter.failures = 2 + @formatter.tally.counter.errors = 1 + + @formatter.after state + h = @formatter.methods["Some#method"] + expect(h[:examples]).to eq(3) + expect(h[:expectations]).to eq(4) + expect(h[:failures]).to eq(2) + expect(h[:errors]).to eq(1) + end + + it "renders the list of exceptions" do + state = ExampleState.new ContextState.new("Some#method"), "it" + @formatter.before state + + exc = SpecExpectationNotMetError.new "failed" + @formatter.exception ExceptionState.new(state, nil, exc) + @formatter.exception ExceptionState.new(state, nil, exc) + + @formatter.after state + h = @formatter.methods["Some#method"] + expect(h[:exceptions]).to eq([ + %[failed\n\n], + %[failed\n\n] + ]) + end +end + +RSpec.describe MethodFormatter, "#after" do + before :each do + $stdout = IOStub.new + context = ContextState.new "Class#method" + @state = ExampleState.new(context, "runs") + @formatter = MethodFormatter.new + allow(MSpec).to receive(:register) + @formatter.register + end + + after :each do + $stdout = STDOUT + end + + it "prints a summary of the results of an example in YAML format" do + @formatter.before @state + @formatter.tally.counter.examples = 3 + @formatter.tally.counter.expectations = 4 + @formatter.tally.counter.failures = 2 + @formatter.tally.counter.errors = 1 + + exc = SpecExpectationNotMetError.new "failed" + @formatter.exception ExceptionState.new(@state, nil, exc) + @formatter.exception ExceptionState.new(@state, nil, exc) + + @formatter.after @state + @formatter.finish + expect($stdout).to eq(%[--- +"Class#method": + class: "Class" + method: "method" + type: instance + description: "Class#method runs" + examples: 3 + expectations: 4 + failures: 2 + errors: 1 + exceptions: + - "failed\\n\\n" + - "failed\\n\\n" +]) + end +end diff --git a/spec/mspec/spec/runner/formatters/multi_spec.rb b/spec/mspec/spec/runner/formatters/multi_spec.rb new file mode 100644 index 0000000000..2d13c05836 --- /dev/null +++ b/spec/mspec/spec/runner/formatters/multi_spec.rb @@ -0,0 +1,68 @@ +require File.dirname(__FILE__) + '/../../spec_helper' +require 'mspec/runner/formatters/dotted' +require 'mspec/runner/formatters/multi' +require 'mspec/runner/example' +require 'yaml' + +RSpec.describe MultiFormatter, "#aggregate_results" do + before :each do + @stdout, $stdout = $stdout, IOStub.new + + @file = double("file").as_null_object + + allow(File).to receive(:delete) + allow(File).to receive(:read) + + @hash = { "files"=>1, "examples"=>1, "expectations"=>2, "failures"=>0, "errors"=>0 } + allow(YAML).to receive(:load).and_return(@hash) + + @formatter = DottedFormatter.new.extend(MultiFormatter) + allow(@formatter.timer).to receive(:format).and_return("Finished in 42 seconds") + end + + after :each do + $stdout = @stdout + end + + it "outputs a summary without errors" do + @formatter.aggregate_results(["a", "b"]) + @formatter.finish + expect($stdout).to eq(%[ + +Finished in 42 seconds + +2 files, 2 examples, 4 expectations, 0 failures, 0 errors, 0 tagged +]) + end + + it "outputs a summary with errors" do + @hash["exceptions"] = [ + "Some#method works real good FAILED\nExpected real good\n to equal fail\n\nfoo.rb:1\nfoo.rb:2", + "Some#method never fails ERROR\nExpected 5\n to equal 3\n\nfoo.rb:1\nfoo.rb:2" + ] + @formatter.aggregate_results(["a"]) + @formatter.finish + expect($stdout).to eq(%[ + +1) +Some#method works real good FAILED +Expected real good + to equal fail + +foo.rb:1 +foo.rb:2 + +2) +Some#method never fails ERROR +Expected 5 + to equal 3 + +foo.rb:1 +foo.rb:2 + +Finished in 42 seconds + +1 file, 1 example, 2 expectations, 0 failures, 0 errors, 0 tagged +]) + end +end diff --git a/spec/mspec/spec/runner/formatters/specdoc_spec.rb b/spec/mspec/spec/runner/formatters/specdoc_spec.rb new file mode 100644 index 0000000000..54b5e2cf0d --- /dev/null +++ b/spec/mspec/spec/runner/formatters/specdoc_spec.rb @@ -0,0 +1,106 @@ +require File.dirname(__FILE__) + '/../../spec_helper' +require 'mspec/runner/formatters/specdoc' +require 'mspec/runner/example' + +RSpec.describe SpecdocFormatter do + before :each do + @formatter = SpecdocFormatter.new + end + + it "responds to #register by registering itself with MSpec for appropriate actions" do + allow(MSpec).to receive(:register) + expect(MSpec).to receive(:register).with(:enter, @formatter) + @formatter.register + end +end + +RSpec.describe SpecdocFormatter, "#enter" do + before :each do + $stdout = @out = IOStub.new + @formatter = SpecdocFormatter.new + end + + after :each do + $stdout = STDOUT + end + + it "prints the #describe string" do + @formatter.enter("describe") + expect(@out).to eq("\ndescribe\n") + end +end + +RSpec.describe SpecdocFormatter, "#before" do + before :each do + $stdout = @out = IOStub.new + @formatter = SpecdocFormatter.new + @state = ExampleState.new ContextState.new("describe"), "it" + end + + after :each do + $stdout = STDOUT + end + + it "prints the #it string" do + @formatter.before @state + expect(@out).to eq("- it") + end + + it "resets the #exception? flag" do + exc = ExceptionState.new @state, nil, SpecExpectationNotMetError.new("disappointing") + @formatter.exception exc + expect(@formatter.exception?).to be_truthy + @formatter.before @state + expect(@formatter.exception?).to be_falsey + end +end + +RSpec.describe SpecdocFormatter, "#exception" do + before :each do + $stdout = @out = IOStub.new + @formatter = SpecdocFormatter.new + context = ContextState.new "describe" + @state = ExampleState.new context, "it" + end + + after :each do + $stdout = STDOUT + end + + it "prints 'ERROR' if an exception is not an SpecExpectationNotMetError" do + exc = ExceptionState.new @state, nil, MSpecExampleError.new("painful") + @formatter.exception exc + expect(@out).to eq(" (ERROR - 1)") + end + + it "prints 'FAILED' if an exception is an SpecExpectationNotMetError" do + exc = ExceptionState.new @state, nil, SpecExpectationNotMetError.new("disappointing") + @formatter.exception exc + expect(@out).to eq(" (FAILED - 1)") + end + + it "prints the #it string if an exception has already been raised" do + exc = ExceptionState.new @state, nil, SpecExpectationNotMetError.new("disappointing") + @formatter.exception exc + exc = ExceptionState.new @state, nil, MSpecExampleError.new("painful") + @formatter.exception exc + expect(@out).to eq(" (FAILED - 1)\n- it (ERROR - 2)") + end +end + +RSpec.describe SpecdocFormatter, "#after" do + before :each do + $stdout = @out = IOStub.new + @formatter = SpecdocFormatter.new + @state = ExampleState.new "describe", "it" + end + + after :each do + $stdout = STDOUT + end + + it "prints a newline character" do + @formatter.after @state + expect(@out).to eq("\n") + end +end diff --git a/spec/mspec/spec/runner/formatters/spinner_spec.rb b/spec/mspec/spec/runner/formatters/spinner_spec.rb new file mode 100644 index 0000000000..5c93d38822 --- /dev/null +++ b/spec/mspec/spec/runner/formatters/spinner_spec.rb @@ -0,0 +1,83 @@ +require File.dirname(__FILE__) + '/../../spec_helper' +require 'mspec/runner/formatters/spinner' +require 'mspec/runner/mspec' +require 'mspec/runner/example' + +RSpec.describe SpinnerFormatter, "#initialize" do + it "permits zero arguments" do + SpinnerFormatter.new + end + + it "accepts one argument" do + SpinnerFormatter.new nil + end +end + +RSpec.describe SpinnerFormatter, "#register" do + before :each do + @formatter = SpinnerFormatter.new + allow(MSpec).to receive(:register) + end + + it "registers self with MSpec for appropriate actions" do + expect(MSpec).to receive(:register).with(:start, @formatter) + expect(MSpec).to receive(:register).with(:unload, @formatter) + expect(MSpec).to receive(:register).with(:after, @formatter) + expect(MSpec).to receive(:register).with(:finish, @formatter) + @formatter.register + end + + it "creates TimerAction and TallyAction" do + timer = double("timer") + tally = double("tally") + expect(timer).to receive(:register) + expect(tally).to receive(:register) + expect(tally).to receive(:counter) + expect(TimerAction).to receive(:new).and_return(timer) + expect(TallyAction).to receive(:new).and_return(tally) + @formatter.register + end +end + +RSpec.describe SpinnerFormatter, "#print" do + after :each do + $stdout = STDOUT + end + + it "ignores the argument to #initialize and writes to $stdout" do + $stdout = IOStub.new + formatter = SpinnerFormatter.new "some/file" + formatter.print "begonias" + expect($stdout).to eq("begonias") + end +end + +RSpec.describe SpinnerFormatter, "#after" do + before :each do + $stdout = IOStub.new + MSpec.store(:files, ["a", "b", "c", "d"]) + @formatter = SpinnerFormatter.new + @formatter.register + @state = ExampleState.new("describe", "it") + end + + after :each do + $stdout = STDOUT + end + + it "updates the spinner" do + @formatter.start + @formatter.after @state + @formatter.unload + + if ENV["TERM"] != "dumb" + green = "\e[0;32m" + reset = "\e[0m" + end + + output = "\r[/ | 0% | 00:00:00] #{green} 0F #{green} 0E#{reset} " \ + "\r[- | 0% | 00:00:00] #{green} 0F #{green} 0E#{reset} " \ + "\r[\\ | ========== 25% | 00:00:00] #{green} 0F #{green} 0E#{reset} " + expect($stdout).to eq(output) + end +end diff --git a/spec/mspec/spec/runner/formatters/summary_spec.rb b/spec/mspec/spec/runner/formatters/summary_spec.rb new file mode 100644 index 0000000000..c87d940042 --- /dev/null +++ b/spec/mspec/spec/runner/formatters/summary_spec.rb @@ -0,0 +1,26 @@ +require File.dirname(__FILE__) + '/../../spec_helper' +require 'mspec/runner/formatters/summary' +require 'mspec/runner/example' + +RSpec.describe SummaryFormatter, "#after" do + before :each do + $stdout = @out = IOStub.new + @formatter = SummaryFormatter.new + @formatter.register + context = ContextState.new "describe" + @state = ExampleState.new(context, "it") + end + + after :each do + $stdout = STDOUT + end + + it "does not print anything" do + exc = ExceptionState.new @state, nil, SpecExpectationNotMetError.new("disappointing") + @formatter.exception exc + exc = ExceptionState.new @state, nil, MSpecExampleError.new("painful") + @formatter.exception exc + @formatter.after(@state) + expect(@out).to eq("") + end +end diff --git a/spec/mspec/spec/runner/formatters/unit_spec.rb b/spec/mspec/spec/runner/formatters/unit_spec.rb new file mode 100644 index 0000000000..d349e6871d --- /dev/null +++ b/spec/mspec/spec/runner/formatters/unit_spec.rb @@ -0,0 +1,73 @@ +require File.dirname(__FILE__) + '/../../spec_helper' +require 'mspec/runner/formatters/unit' +require 'mspec/runner/example' +require 'mspec/utils/script' + +RSpec.describe UnitdiffFormatter, "#finish" do + before :each do + @tally = double("tally").as_null_object + allow(TallyAction).to receive(:new).and_return(@tally) + @timer = double("timer").as_null_object + allow(TimerAction).to receive(:new).and_return(@timer) + + $stdout = @out = IOStub.new + context = ContextState.new "describe" + @state = ExampleState.new(context, "it") + allow(MSpec).to receive(:register) + @formatter = UnitdiffFormatter.new + @formatter.register + end + + after :each do + $stdout = STDOUT + end + + it "prints a failure message for an exception" do + exc = ExceptionState.new @state, nil, MSpecExampleError.new("broken") + @formatter.exception exc + @formatter.after @state + @formatter.finish + expect(@out).to match(/^1\)\ndescribe it ERROR$/) + end + + it "prints a backtrace for an exception" do + exc = ExceptionState.new @state, nil, Exception.new("broken") + allow(exc).to receive(:backtrace).and_return("path/to/some/file.rb:35:in method") + @formatter.exception exc + @formatter.finish + expect(@out).to match(%r[path/to/some/file.rb:35:in method$]) + end + + it "prints a summary of elapsed time" do + expect(@timer).to receive(:format).and_return("Finished in 2.0 seconds") + @formatter.finish + expect(@out).to match(/^Finished in 2.0 seconds$/) + end + + it "prints a tally of counts" do + expect(@tally).to receive(:format).and_return("1 example, 0 failures") + @formatter.finish + expect(@out).to match(/^1 example, 0 failures$/) + end + + it "prints errors, backtraces, elapsed time, and tallies" do + exc = ExceptionState.new @state, nil, Exception.new("broken") + allow(exc).to receive(:backtrace).and_return("path/to/some/file.rb:35:in method") + @formatter.exception exc + @formatter.after @state + expect(@timer).to receive(:format).and_return("Finished in 2.0 seconds") + expect(@tally).to receive(:format).and_return("1 example, 0 failures") + @formatter.finish + expect(@out).to eq(%[E + +Finished in 2.0 seconds + +1) +describe it ERROR +Exception: broken: +path/to/some/file.rb:35:in method + +1 example, 0 failures +]) + end +end diff --git a/spec/mspec/spec/runner/formatters/yaml_spec.rb b/spec/mspec/spec/runner/formatters/yaml_spec.rb new file mode 100644 index 0000000000..2e334fdbb9 --- /dev/null +++ b/spec/mspec/spec/runner/formatters/yaml_spec.rb @@ -0,0 +1,134 @@ +require File.dirname(__FILE__) + '/../../spec_helper' +require 'mspec/runner/formatters/yaml' +require 'mspec/runner/example' +require 'mspec/helpers' + +RSpec.describe YamlFormatter, "#initialize" do + it "permits zero arguments" do + YamlFormatter.new + end + + it "accepts one argument" do + YamlFormatter.new nil + end +end + +RSpec.describe YamlFormatter, "#print" do + before :each do + $stdout = IOStub.new + @out = IOStub.new + allow(File).to receive(:open).and_return(@out) + @formatter = YamlFormatter.new "some/file" + end + + after :each do + $stdout = STDOUT + end + + it "writes to $stdout if #switch has not been called" do + @formatter.print "begonias" + expect($stdout).to eq("begonias") + expect(@out).to eq("") + end + + it "writes to the file passed to #initialize once #switch has been called" do + @formatter.switch + @formatter.print "begonias" + expect($stdout).to eq("") + expect(@out).to eq("begonias") + end + + it "writes to $stdout once #switch is called if no file was passed to #initialize" do + formatter = YamlFormatter.new + formatter.switch + formatter.print "begonias" + expect($stdout).to eq("begonias") + expect(@out).to eq("") + end +end + +RSpec.describe YamlFormatter, "#finish" do + before :each do + @tally = double("tally").as_null_object + @counter = double("counter").as_null_object + allow(@tally).to receive(:counter).and_return(@counter) + allow(TallyAction).to receive(:new).and_return(@tally) + + @timer = double("timer").as_null_object + allow(TimerAction).to receive(:new).and_return(@timer) + + @out = tmp("YamlFormatter") + + context = ContextState.new "describe" + @state = ExampleState.new(context, "it") + + @formatter = YamlFormatter.new(@out) + allow(@formatter).to receive(:backtrace).and_return("") + allow(MSpec).to receive(:register) + @formatter.register + + exc = ExceptionState.new @state, nil, MSpecExampleError.new("broken") + allow(exc).to receive(:backtrace).and_return("path/to/some/file.rb:35:in method") + @formatter.exception exc + @formatter.after @state + end + + after :each do + rm_r @out + end + + it "calls #switch" do + expect(@formatter).to receive(:switch).and_call_original + @formatter.finish + end + + it "outputs a failure message and backtrace" do + @formatter.finish + output = File.read(@out) + expect(output).to include "describe it ERROR" + expect(output).to include "MSpecExampleError: broken\\n" + expect(output).to include "path/to/some/file.rb:35:in method" + end + + it "outputs an elapsed time" do + expect(@timer).to receive(:elapsed).and_return(4.2) + @formatter.finish + output = File.read(@out) + expect(output).to include "time: 4.2" + end + + it "outputs a file count" do + expect(@counter).to receive(:files).and_return(3) + @formatter.finish + output = File.read(@out) + expect(output).to include "files: 3" + end + + it "outputs an example count" do + expect(@counter).to receive(:examples).and_return(3) + @formatter.finish + output = File.read(@out) + expect(output).to include "examples: 3" + end + + it "outputs an expectation count" do + expect(@counter).to receive(:expectations).and_return(9) + @formatter.finish + output = File.read(@out) + expect(output).to include "expectations: 9" + end + + it "outputs a failure count" do + expect(@counter).to receive(:failures).and_return(2) + @formatter.finish + output = File.read(@out) + expect(output).to include "failures: 2" + end + + it "outputs an error count" do + expect(@counter).to receive(:errors).and_return(1) + @formatter.finish + output = File.read(@out) + expect(output).to include "errors: 1" + end +end diff --git a/spec/mspec/spec/runner/mspec_spec.rb b/spec/mspec/spec/runner/mspec_spec.rb new file mode 100644 index 0000000000..4af01806c0 --- /dev/null +++ b/spec/mspec/spec/runner/mspec_spec.rb @@ -0,0 +1,597 @@ +require 'spec_helper' +require 'mspec/expectations/expectations' +require 'mspec/helpers/tmp' +require 'mspec/helpers/fs' +require 'mspec/matchers/base' +require 'mspec/runner/mspec' +require 'mspec/runner/example' + +RSpec.describe MSpec, ".register_files" do + it "records which spec files to run" do + MSpec.register_files [:one, :two, :three] + expect(MSpec.files_array).to eq([:one, :two, :three]) + end +end + +RSpec.describe MSpec, ".register_mode" do + before :each do + MSpec.clear_modes + end + + it "sets execution mode flags" do + MSpec.register_mode :verify + expect(MSpec.retrieve(:modes)).to eq([:verify]) + end +end + +RSpec.describe MSpec, ".register_tags_patterns" do + it "records the patterns for generating a tag file from a spec file" do + MSpec.register_tags_patterns [[/spec\/ruby/, "spec/tags"], [/frozen/, "ruby"]] + expect(MSpec.retrieve(:tags_patterns)).to eq([[/spec\/ruby/, "spec/tags"], [/frozen/, "ruby"]]) + end +end + +RSpec.describe MSpec, ".register_exit" do + before :each do + MSpec.store :exit, 0 + end + + it "records the exit code" do + expect(MSpec.exit_code).to eq(0) + MSpec.register_exit 1 + expect(MSpec.exit_code).to eq(1) + end +end + +RSpec.describe MSpec, ".exit_code" do + it "retrieves the code set with .register_exit" do + MSpec.register_exit 99 + expect(MSpec.exit_code).to eq(99) + end +end + +RSpec.describe MSpec, ".store" do + it "records data for MSpec settings" do + MSpec.store :anything, :value + expect(MSpec.retrieve(:anything)).to eq(:value) + end +end + +RSpec.describe MSpec, ".retrieve" do + it "accesses .store'd data" do + MSpec.register :retrieve, :first + expect(MSpec.retrieve(:retrieve)).to eq([:first]) + end +end + +RSpec.describe MSpec, ".randomize" do + it "sets the flag to randomize spec execution order" do + expect(MSpec.randomize?).to eq(false) + MSpec.randomize = true + expect(MSpec.randomize?).to eq(true) + MSpec.randomize = false + expect(MSpec.randomize?).to eq(false) + end +end + +RSpec.describe MSpec, ".register" do + it "is the gateway behind the register(symbol, action) facility" do + MSpec.register :bonus, :first + MSpec.register :bonus, :second + MSpec.register :bonus, :second + expect(MSpec.retrieve(:bonus)).to eq([:first, :second]) + end +end + +RSpec.describe MSpec, ".unregister" do + it "is the gateway behind the unregister(symbol, actions) facility" do + MSpec.register :unregister, :first + MSpec.register :unregister, :second + MSpec.unregister :unregister, :second + expect(MSpec.retrieve(:unregister)).to eq([:first]) + end +end + +RSpec.describe MSpec, ".protect" do + before :each do + MSpec.clear_current + @cs = ContextState.new "C#m" + @cs.parent = MSpec.current + + @es = ExampleState.new @cs, "runs" + ScratchPad.record Exception.new("Sharp!") + end + + it "returns true if no exception is raised" do + expect(MSpec.protect("passed") { 1 }).to be_truthy + end + + it "returns false if an exception is raised" do + expect(MSpec.protect("testing") { raise ScratchPad.recorded }).to be_falsey + end + + it "rescues any exceptions raised when evaluating the block argument" do + MSpec.protect("") { raise Exception, "Now you see me..." } + end + + it "does not rescue SystemExit" do + begin + MSpec.protect("") { exit 1 } + rescue SystemExit + ScratchPad.record :system_exit + end + expect(ScratchPad.recorded).to eq(:system_exit) + end + + it "calls all the exception actions" do + exc = ExceptionState.new @es, "testing", ScratchPad.recorded + allow(ExceptionState).to receive(:new).and_return(exc) + action = double("exception") + expect(action).to receive(:exception).with(exc) + MSpec.register :exception, action + MSpec.protect("testing") { raise ScratchPad.recorded } + MSpec.unregister :exception, action + end + + it "registers a non-zero exit code when an exception is raised" do + expect(MSpec).to receive(:register_exit).with(1) + MSpec.protect("testing") { raise ScratchPad.recorded } + end +end + +RSpec.describe MSpec, ".register_current" do + before :each do + MSpec.clear_current + end + + it "sets the value returned by MSpec.current" do + expect(MSpec.current).to be_nil + MSpec.register_current :a + expect(MSpec.current).to eq(:a) + end +end + +RSpec.describe MSpec, ".clear_current" do + it "sets the value returned by MSpec.current to nil" do + MSpec.register_current :a + expect(MSpec.current).not_to be_nil + MSpec.clear_current + expect(MSpec.current).to be_nil + end +end + +RSpec.describe MSpec, ".current" do + before :each do + MSpec.clear_current + end + + it "returns nil if no ContextState has been registered" do + expect(MSpec.current).to be_nil + end + + it "returns the most recently registered ContextState" do + first = ContextState.new "" + second = ContextState.new "" + MSpec.register_current first + expect(MSpec.current).to eq(first) + MSpec.register_current second + expect(MSpec.current).to eq(second) + end +end + +RSpec.describe MSpec, ".actions" do + before :each do + MSpec.store :start, [] + ScratchPad.record [] + start_one = double("one") + allow(start_one).to receive(:start) { ScratchPad << :one } + start_two = double("two") + allow(start_two).to receive(:start) { ScratchPad << :two } + MSpec.register :start, start_one + MSpec.register :start, start_two + end + + it "does not attempt to run any actions if none have been registered" do + MSpec.store :finish, nil + expect { MSpec.actions :finish }.not_to raise_error + end + + it "runs each action registered as a start action" do + MSpec.actions :start + expect(ScratchPad.recorded).to eq([:one, :two]) + end +end + +RSpec.describe MSpec, ".mode?" do + before :each do + MSpec.clear_modes + end + + it "returns true if the mode has been set" do + expect(MSpec.mode?(:verify)).to eq(false) + MSpec.register_mode :verify + expect(MSpec.mode?(:verify)).to eq(true) + end +end + +RSpec.describe MSpec, ".clear_modes" do + it "clears all registered modes" do + MSpec.register_mode(:pretend) + MSpec.register_mode(:verify) + + expect(MSpec.mode?(:pretend)).to eq(true) + expect(MSpec.mode?(:verify)).to eq(true) + + MSpec.clear_modes + + expect(MSpec.mode?(:pretend)).to eq(false) + expect(MSpec.mode?(:verify)).to eq(false) + end +end + +RSpec.describe MSpec, ".guarded?" do + before :each do + MSpec.instance_variable_set :@guarded, [] + end + + it "returns false if no guard has run" do + expect(MSpec.guarded?).to eq(false) + end + + it "returns true if a single guard has run" do + MSpec.guard + expect(MSpec.guarded?).to eq(true) + end + + it "returns true if more than one guard has run" do + MSpec.guard + MSpec.guard + expect(MSpec.guarded?).to eq(true) + end + + it "returns true until all guards have finished" do + MSpec.guard + MSpec.guard + expect(MSpec.guarded?).to eq(true) + MSpec.unguard + expect(MSpec.guarded?).to eq(true) + MSpec.unguard + expect(MSpec.guarded?).to eq(false) + end +end + +RSpec.describe MSpec, ".describe" do + before :each do + MSpec.clear_current + @cs = ContextState.new "" + allow(ContextState).to receive(:new).and_return(@cs) + allow(MSpec).to receive(:current).and_return(nil) + allow(MSpec).to receive(:register_current) + end + + it "creates a new ContextState for the block" do + expect(ContextState).to receive(:new).and_return(@cs) + MSpec.describe(Object) { } + end + + it "accepts an optional second argument" do + expect(ContextState).to receive(:new).and_return(@cs) + MSpec.describe(Object, "msg") { } + end + + it "registers the newly created ContextState" do + expect(MSpec).to receive(:register_current).with(@cs).twice + MSpec.describe(Object) { } + end + + it "invokes the ContextState#describe method" do + expect(@cs).to receive(:describe) + MSpec.describe(Object, "msg") {} + end +end + +RSpec.describe MSpec, ".process" do + before :each do + allow(MSpec).to receive(:files) + MSpec.store :start, [] + MSpec.store :finish, [] + allow(STDOUT).to receive(:puts) + end + + it "prints the RUBY_DESCRIPTION" do + expect(STDOUT).to receive(:puts).with(RUBY_DESCRIPTION) + MSpec.process + end + + it "calls all start actions" do + start = double("start") + allow(start).to receive(:start) { ScratchPad.record :start } + MSpec.register :start, start + MSpec.process + expect(ScratchPad.recorded).to eq(:start) + end + + it "calls all finish actions" do + finish = double("finish") + allow(finish).to receive(:finish) { ScratchPad.record :finish } + MSpec.register :finish, finish + MSpec.process + expect(ScratchPad.recorded).to eq(:finish) + end + + it "calls the files method" do + expect(MSpec).to receive(:files) + MSpec.process + end +end + +RSpec.describe MSpec, ".files" do + before :each do + MSpec.store :load, [] + MSpec.store :unload, [] + MSpec.register_files [:one, :two, :three] + allow(Kernel).to receive(:load) + end + + it "calls load actions before each file" do + load = double("load") + allow(load).to receive(:load) { ScratchPad.record :load } + MSpec.register :load, load + MSpec.files + expect(ScratchPad.recorded).to eq(:load) + end + + it "shuffles the file list if .randomize? is true" do + MSpec.randomize = true + expect(MSpec).to receive(:shuffle) + MSpec.files + MSpec.randomize = false + end + + it "registers the current file" do + load = double("load") + files = [] + allow(load).to receive(:load) { files << MSpec.file } + MSpec.register :load, load + MSpec.files + expect(files).to eq([:one, :two, :three]) + end +end + +RSpec.describe MSpec, ".shuffle" do + before :each do + @base = (0..100).to_a + @list = @base.clone + MSpec.shuffle @list + end + + it "does not alter the elements in the list" do + @base.each do |elt| + expect(@list).to include(elt) + end + end + + it "changes the order of the list" do + # obviously, this spec has a certain probability + # of failing. If it fails, run it again. + expect(@list).not_to eq(@base) + end +end + +RSpec.describe MSpec, ".tags_file" do + before :each do + MSpec.store :file, "path/to/spec/something/some_spec.rb" + MSpec.store :tags_patterns, nil + end + + it "returns the default tags file for the current spec file" do + expect(MSpec.tags_file).to eq("path/to/spec/tags/something/some_tags.txt") + end + + it "returns the tags file for the current spec file with custom tags_patterns" do + MSpec.register_tags_patterns [[/^(.*)\/spec/, '\1/tags'], [/_spec.rb/, "_tags.txt"]] + expect(MSpec.tags_file).to eq("path/to/tags/something/some_tags.txt") + end + + it "performs multiple substitutions" do + MSpec.register_tags_patterns [ + [%r(/spec/something/), "/spec/other/"], + [%r(/spec/), "/spec/tags/"], + [/_spec.rb/, "_tags.txt"] + ] + expect(MSpec.tags_file).to eq("path/to/spec/tags/other/some_tags.txt") + end + + it "handles cases where no substitution is performed" do + MSpec.register_tags_patterns [[/nothing/, "something"]] + expect(MSpec.tags_file).to eq("path/to/spec/something/some_spec.rb") + end +end + +RSpec.describe MSpec, ".read_tags" do + before :each do + allow(MSpec).to receive(:tags_file).and_return(File.dirname(__FILE__) + '/tags.txt') + end + + it "returns a list of tag instances for matching tag names found" do + one = SpecTag.new "fail(broken):Some#method? works" + expect(MSpec.read_tags(["fail", "pass"])).to eq([one]) + end + + it "returns [] if no tags names match" do + expect(MSpec.read_tags("super")).to eq([]) + end +end + +RSpec.describe MSpec, ".read_tags" do + before :each do + @tag = SpecTag.new "fails:Some#method" + File.open(tmp("tags.txt", false), "w") do |f| + f.puts "" + f.puts @tag + f.puts "" + end + allow(MSpec).to receive(:tags_file).and_return(tmp("tags.txt", false)) + end + + it "does not return a tag object for empty lines" do + expect(MSpec.read_tags(["fails"])).to eq([@tag]) + end +end + +RSpec.describe MSpec, ".write_tags" do + before :each do + FileUtils.cp File.dirname(__FILE__) + "/tags.txt", tmp("tags.txt", false) + allow(MSpec).to receive(:tags_file).and_return(tmp("tags.txt", false)) + @tag1 = SpecTag.new "check(broken):Tag#rewrite works" + @tag2 = SpecTag.new "broken:Tag#write_tags fails" + end + + after :all do + rm_r tmp("tags.txt", false) + end + + it "overwrites the tags in the tag file" do + expect(IO.read(tmp("tags.txt", false))).to eq(%[fail(broken):Some#method? works +incomplete(20%):The#best method ever +benchmark(0.01825):The#fastest method today +extended():\"Multi-line\\ntext\\ntag\" +]) + MSpec.write_tags [@tag1, @tag2] + expect(IO.read(tmp("tags.txt", false))).to eq(%[check(broken):Tag#rewrite works +broken:Tag#write_tags fails +]) + end +end + +RSpec.describe MSpec, ".write_tag" do + before :each do + allow(FileUtils).to receive(:mkdir_p) + allow(MSpec).to receive(:tags_file).and_return(tmp("tags.txt", false)) + @tag = SpecTag.new "fail(broken):Some#method works" + end + + after :all do + rm_r tmp("tags.txt", false) + end + + it "writes a tag to the tags file for the current spec file" do + MSpec.write_tag @tag + expect(IO.read(tmp("tags.txt", false))).to eq("fail(broken):Some#method works\n") + end + + it "does not write a duplicate tag" do + File.open(tmp("tags.txt", false), "w") { |f| f.puts @tag } + MSpec.write_tag @tag + expect(IO.read(tmp("tags.txt", false))).to eq("fail(broken):Some#method works\n") + end +end + +RSpec.describe MSpec, ".delete_tag" do + before :each do + FileUtils.cp File.dirname(__FILE__) + "/tags.txt", tmp("tags.txt", false) + allow(MSpec).to receive(:tags_file).and_return(tmp("tags.txt", false)) + @tag = SpecTag.new "fail(Comments don't matter):Some#method? works" + end + + after :each do + rm_r tmp("tags.txt", false) + end + + it "deletes the tag if it exists" do + expect(MSpec.delete_tag(@tag)).to eq(true) + expect(IO.read(tmp("tags.txt", false))).to eq(%[incomplete(20%):The#best method ever +benchmark(0.01825):The#fastest method today +extended():\"Multi-line\\ntext\\ntag\" +]) + end + + it "deletes a tag with escaped newlines" do + expect(MSpec.delete_tag(SpecTag.new('extended:"Multi-line\ntext\ntag"'))).to eq(true) + expect(IO.read(tmp("tags.txt", false))).to eq(%[fail(broken):Some#method? works +incomplete(20%):The#best method ever +benchmark(0.01825):The#fastest method today +]) + end + + it "does not change the tags file contents if the tag doesn't exist" do + @tag.tag = "failed" + expect(MSpec.delete_tag(@tag)).to eq(false) + expect(IO.read(tmp("tags.txt", false))).to eq(%[fail(broken):Some#method? works +incomplete(20%):The#best method ever +benchmark(0.01825):The#fastest method today +extended():\"Multi-line\\ntext\\ntag\" +]) + end + + it "deletes the tag file if it is empty" do + expect(MSpec.delete_tag(@tag)).to eq(true) + expect(MSpec.delete_tag(SpecTag.new("incomplete:The#best method ever"))).to eq(true) + expect(MSpec.delete_tag(SpecTag.new("benchmark:The#fastest method today"))).to eq(true) + expect(MSpec.delete_tag(SpecTag.new('extended:"Multi-line\ntext\ntag"'))).to eq(true) + expect(File.exist?(tmp("tags.txt", false))).to eq(false) + end +end + +RSpec.describe MSpec, ".delete_tags" do + before :each do + @tags = tmp("tags.txt", false) + FileUtils.cp File.dirname(__FILE__) + "/tags.txt", @tags + allow(MSpec).to receive(:tags_file).and_return(@tags) + end + + it "deletes the tag file" do + MSpec.delete_tags + expect(File.exist?(@tags)).to be_falsey + end +end + +RSpec.describe MSpec, ".expectation" do + it "sets the flag that an expectation has been reported" do + MSpec.clear_expectations + expect(MSpec.expectation?).to be_falsey + MSpec.expectation + expect(MSpec.expectation?).to be_truthy + end +end + +RSpec.describe MSpec, ".expectation?" do + it "returns true if an expectation has been reported" do + MSpec.expectation + expect(MSpec.expectation?).to be_truthy + end + + it "returns false if an expectation has not been reported" do + MSpec.clear_expectations + expect(MSpec.expectation?).to be_falsey + end +end + +RSpec.describe MSpec, ".clear_expectations" do + it "clears the flag that an expectation has been reported" do + MSpec.expectation + expect(MSpec.expectation?).to be_truthy + MSpec.clear_expectations + expect(MSpec.expectation?).to be_falsey + end +end + +RSpec.describe MSpec, ".register_shared" do + it "stores a shared ContextState by description" do + parent = ContextState.new "container" + state = ContextState.new "shared" + state.parent = parent + prc = lambda { } + state.describe(&prc) + MSpec.register_shared(state) + expect(MSpec.retrieve(:shared)["shared"]).to eq(state) + end +end + +RSpec.describe MSpec, ".retrieve_shared" do + it "retrieves the shared ContextState matching description" do + state = ContextState.new "" + MSpec.retrieve(:shared)["shared"] = state + expect(MSpec.retrieve_shared(:shared)).to eq(state) + end +end diff --git a/spec/mspec/spec/runner/shared_spec.rb b/spec/mspec/spec/runner/shared_spec.rb new file mode 100644 index 0000000000..153b8f0698 --- /dev/null +++ b/spec/mspec/spec/runner/shared_spec.rb @@ -0,0 +1,90 @@ +require 'spec_helper' +require 'mspec/runner/shared' +require 'mspec/runner/context' +require 'mspec/runner/example' + +RSpec.describe Object, "#it_behaves_like" do + before :each do + ScratchPad.clear + + MSpec.setup_env + + @state = ContextState.new "Top level" + @state.instance_variable_set :@parsed, true + @state.singleton_class.send(:public, :it_behaves_like) + + @shared = ContextState.new :shared_spec, :shared => true + allow(MSpec).to receive(:retrieve_shared).and_return(@shared) + end + + it "creates @method set to the name of the aliased method" do + @shared.it("an example") { ScratchPad.record @method } + @state.it_behaves_like :shared_spec, :some_method + @state.process + expect(ScratchPad.recorded).to eq(:some_method) + end + + it "creates @object if the passed object" do + object = Object.new + @shared.it("an example") { ScratchPad.record @object } + @state.it_behaves_like :shared_spec, :some_method, object + @state.process + expect(ScratchPad.recorded).to eq(object) + end + + it "creates @object if the passed false" do + object = false + @shared.it("an example") { ScratchPad.record @object } + @state.it_behaves_like :shared_spec, :some_method, object + @state.process + expect(ScratchPad.recorded).to eq(object) + end + + it "sends :it_should_behave_like" do + expect(@state).to receive(:it_should_behave_like) + @state.it_behaves_like :shared_spec, :some_method + end + + describe "with multiple shared contexts" do + before :each do + @obj = Object.new + @obj2 = Object.new + + @state2 = ContextState.new "Second top level" + @state2.instance_variable_set :@parsed, true + @state2.singleton_class.send(:public, :it_behaves_like) + end + + it "ensures the shared spec state is distinct" do + @shared.it("an example") { ScratchPad.record [@method, @object] } + + @state.it_behaves_like :shared_spec, :some_method, @obj + + @state.process + expect(ScratchPad.recorded).to eq([:some_method, @obj]) + + @state2.it_behaves_like :shared_spec, :another_method, @obj2 + + @state2.process + expect(ScratchPad.recorded).to eq([:another_method, @obj2]) + end + + it "ensures the shared spec state is distinct for nested shared specs" do + nested = ContextState.new "nested context" + nested.instance_variable_set :@parsed, true + nested.parent = @shared + + nested.it("another example") { ScratchPad.record [:shared, @method, @object] } + + @state.it_behaves_like :shared_spec, :some_method, @obj + + @state.process + expect(ScratchPad.recorded).to eq([:shared, :some_method, @obj]) + + @state2.it_behaves_like :shared_spec, :another_method, @obj2 + + @state2.process + expect(ScratchPad.recorded).to eq([:shared, :another_method, @obj2]) + end + end +end diff --git a/spec/mspec/spec/runner/tag_spec.rb b/spec/mspec/spec/runner/tag_spec.rb new file mode 100644 index 0000000000..bda9ac4280 --- /dev/null +++ b/spec/mspec/spec/runner/tag_spec.rb @@ -0,0 +1,123 @@ +require 'spec_helper' +require 'mspec/runner/tag' + +RSpec.describe SpecTag do + it "accepts an optional string to parse into fields" do + tag = SpecTag.new "tag(comment):description" + expect(tag.tag).to eq("tag") + expect(tag.comment).to eq("comment") + expect(tag.description).to eq("description") + end +end + +RSpec.describe SpecTag, "#parse" do + before :each do + @tag = SpecTag.new + end + + it "accepts 'tag(comment):description'" do + @tag.parse "tag(I'm real):Some#method returns a value" + expect(@tag.tag).to eq("tag") + expect(@tag.comment).to eq("I'm real") + expect(@tag.description).to eq("Some#method returns a value") + end + + it "accepts 'tag:description'" do + @tag.parse "tag:Another#method" + expect(@tag.tag).to eq("tag") + expect(@tag.comment).to eq(nil) + expect(@tag.description).to eq("Another#method") + end + + it "accepts 'tag():description'" do + @tag.parse "tag():Another#method" + expect(@tag.tag).to eq("tag") + expect(@tag.comment).to eq(nil) + expect(@tag.description).to eq("Another#method") + end + + it "accepts 'tag:'" do + @tag.parse "tag:" + expect(@tag.tag).to eq("tag") + expect(@tag.comment).to eq(nil) + expect(@tag.description).to eq("") + end + + it "accepts 'tag(bug:555):Another#method'" do + @tag.parse "tag(bug:555):Another#method" + expect(@tag.tag).to eq("tag") + expect(@tag.comment).to eq("bug:555") + expect(@tag.description).to eq("Another#method") + end + + it "accepts 'tag(http://someplace.com/neato):Another#method'" do + @tag.parse "tag(http://someplace.com/neato):Another#method" + expect(@tag.tag).to eq("tag") + expect(@tag.comment).to eq("http://someplace.com/neato") + expect(@tag.description).to eq("Another#method") + end + + it "accepts 'tag(comment):\"Multi-line\\ntext\"'" do + @tag.parse 'tag(comment):"Multi-line\ntext"' + expect(@tag.tag).to eq("tag") + expect(@tag.comment).to eq("comment") + expect(@tag.description).to eq("Multi-line\ntext") + end + + it "ignores '#anything'" do + @tag.parse "# this could be a comment" + expect(@tag.tag).to eq(nil) + expect(@tag.comment).to eq(nil) + expect(@tag.description).to eq(nil) + end +end + +RSpec.describe SpecTag, "#to_s" do + it "formats itself as 'tag(comment):description'" do + txt = "tag(comment):description" + tag = SpecTag.new txt + expect(tag.tag).to eq("tag") + expect(tag.comment).to eq("comment") + expect(tag.description).to eq("description") + expect(tag.to_s).to eq(txt) + end + + it "formats itself as 'tag:description" do + txt = "tag:description" + tag = SpecTag.new txt + expect(tag.tag).to eq("tag") + expect(tag.comment).to eq(nil) + expect(tag.description).to eq("description") + expect(tag.to_s).to eq(txt) + end + + it "formats itself as 'tag(comment):\"multi-line\\ntext\\ntag\"'" do + txt = 'tag(comment):"multi-line\ntext\ntag"' + tag = SpecTag.new txt + expect(tag.tag).to eq("tag") + expect(tag.comment).to eq("comment") + expect(tag.description).to eq("multi-line\ntext\ntag") + expect(tag.to_s).to eq(txt) + end +end + +RSpec.describe SpecTag, "#==" do + it "returns true if the tags have the same fields" do + one = SpecTag.new "tag(this):unicorn" + two = SpecTag.new "tag(this):unicorn" + expect(one.==(two)).to eq(true) + expect([one].==([two])).to eq(true) + end +end + +RSpec.describe SpecTag, "#unescape" do + it "replaces \\n by LF when the description is quoted" do + tag = SpecTag.new 'tag:"desc with\nnew line"' + expect(tag.description).to eq("desc with\nnew line") + end + + it "does not replaces \\n by LF when the description is not quoted " do + tag = SpecTag.new 'tag:desc with\nnew line' + expect(tag.description).to eq("desc with\\nnew line") + end +end diff --git a/spec/mspec/spec/runner/tags.txt b/spec/mspec/spec/runner/tags.txt new file mode 100644 index 0000000000..f4eb6ad034 --- /dev/null +++ b/spec/mspec/spec/runner/tags.txt @@ -0,0 +1,4 @@ +fail(broken):Some#method? works +incomplete(20%):The#best method ever +benchmark(0.01825):The#fastest method today +extended():"Multi-line\ntext\ntag" diff --git a/spec/mspec/spec/spec_helper.rb b/spec/mspec/spec/spec_helper.rb new file mode 100644 index 0000000000..5cabfe5626 --- /dev/null +++ b/spec/mspec/spec/spec_helper.rb @@ -0,0 +1,70 @@ +RSpec.configure do |config| + config.disable_monkey_patching! + config.raise_errors_for_deprecations! +end + +require 'mspec' + +# Remove this when MRI has intelligent warnings +$VERBOSE = nil unless $VERBOSE + +class MOSConfig < Hash + def initialize + self[:loadpath] = [] + self[:requires] = [] + self[:flags] = [] + self[:options] = [] + self[:includes] = [] + self[:excludes] = [] + self[:patterns] = [] + self[:xpatterns] = [] + self[:tags] = [] + self[:xtags] = [] + self[:atags] = [] + self[:astrings] = [] + self[:target] = 'ruby' + self[:command] = nil + self[:ltags] = [] + self[:files] = [] + self[:launch] = [] + end +end + +def new_option + config = MOSConfig.new + return MSpecOptions.new("spec", 20, config), config +end + +# Just to have an exception name output not be "Exception" +class MSpecExampleError < Exception +end + +def hide_deprecation_warnings + allow(MSpec).to receive(:deprecate) +end + +def run_mspec(command, args) + cwd = Dir.pwd + command = " #{command}" unless command.start_with?('-') + cmd = "#{cwd}/bin/mspec#{command} -B spec/fixtures/config.mspec #{args}" + out = `#{cmd} 2>&1` + ret = $? + out = out.sub(/\A\$.+\n/, '') # Remove printed command line + out = out.sub(RUBY_DESCRIPTION, "RUBY_DESCRIPTION") + out = out.gsub(/\d+\.\d{6}/, "D.DDDDDD") # Specs total time + out = out.gsub(/\d{2}:\d{2}:\d{2}/, "00:00:00") # Progress bar time + out = out.gsub(cwd, "CWD") + return out, ret +end + +def ensure_mspec_method(method) + file, _line = method.source_location + expect(file).to start_with(File.expand_path('../../lib/mspec', __FILE__ )) +end + +PublicMSpecMatchers = Class.new { + include MSpecMatchers + public :raise_error +}.new + +BACKTRACE_QUOTE = RUBY_VERSION >= "3.4" ? "'" : "`" diff --git a/spec/mspec/spec/utils/deprecate_spec.rb b/spec/mspec/spec/utils/deprecate_spec.rb new file mode 100644 index 0000000000..73eaf7d04e --- /dev/null +++ b/spec/mspec/spec/utils/deprecate_spec.rb @@ -0,0 +1,17 @@ +require 'spec_helper' +require 'mspec/utils/deprecate' + +RSpec.describe MSpec, "#deprecate" do + it "warns when using a deprecated method" do + warning = nil + allow($stderr).to receive(:puts) { |str| warning = str } + MSpec.deprecate(:some_method, :other_method) + expect(warning).to start_with(<<-EOS.chomp) + +some_method is deprecated, use other_method instead. +from +EOS + expect(warning).to include(__FILE__) + expect(warning).to include('8') + end +end diff --git a/spec/mspec/spec/utils/fixtures/this_file_raises.rb b/spec/mspec/spec/utils/fixtures/this_file_raises.rb new file mode 100644 index 0000000000..8e37a587bf --- /dev/null +++ b/spec/mspec/spec/utils/fixtures/this_file_raises.rb @@ -0,0 +1 @@ +raise "This is a BAD file" diff --git a/spec/mspec/spec/utils/fixtures/this_file_raises2.rb b/spec/mspec/spec/utils/fixtures/this_file_raises2.rb new file mode 100644 index 0000000000..8efc10199a --- /dev/null +++ b/spec/mspec/spec/utils/fixtures/this_file_raises2.rb @@ -0,0 +1 @@ +raise "This is a BAD file 2" diff --git a/spec/mspec/spec/utils/name_map_spec.rb b/spec/mspec/spec/utils/name_map_spec.rb new file mode 100644 index 0000000000..a42dc9ffec --- /dev/null +++ b/spec/mspec/spec/utils/name_map_spec.rb @@ -0,0 +1,187 @@ +require 'spec_helper' +require 'mspec/utils/name_map' + +module NameMapSpecs + class A + A = self + + def self.a; end + def a; end + def c; end + + class B + def b; end + end + end + + class Error + end + + class Fixnum + def f; end + end + + autoload :BadFile, "#{__dir__}/fixtures/this_file_raises.rb" + autoload :BadFile2, "#{__dir__}/fixtures/this_file_raises2.rb" + + def self.n; end + def n; end +end + +RSpec.describe NameMap, "#exception?" do + before :each do + @map = NameMap.new + end + + it "returns true if the constant is Errno" do + expect(@map.exception?("Errno")).to eq(true) + end + + it "returns true if the constant is a kind of Exception" do + expect(@map.exception?("Errno::EBADF")).to eq(true) + expect(@map.exception?("LoadError")).to eq(true) + expect(@map.exception?("SystemExit")).to eq(true) + end + + it "returns false if the constant is not a kind of Exception" do + expect(@map.exception?("NameMapSpecs::Error")).to eq(false) + expect(@map.exception?("NameMapSpecs")).to eq(false) + end + + it "returns false if the constant does not exist" do + expect(@map.exception?("Nonexistent")).to eq(false) + end +end + +RSpec.describe NameMap, "#class_or_module" do + before :each do + @map = NameMap.new true + end + + it "returns the constant specified by the string" do + expect(@map.class_or_module("NameMapSpecs")).to eq(NameMapSpecs) + end + + it "returns the constant specified by the 'A::B' string" do + expect(@map.class_or_module("NameMapSpecs::A")).to eq(NameMapSpecs::A) + end + + it "returns nil if the constant is not a class or module" do + expect(@map.class_or_module("Float::MAX")).to eq(nil) + end + + it "returns nil if the constant is in the set of excluded constants" do + excluded = %w[ + MSpecScript + MkSpec + NameMap + ] + + excluded.each do |const| + expect(@map.class_or_module(const)).to eq(nil) + end + end + + it "returns nil if the constant does not exist" do + expect(@map.class_or_module("Heaven")).to eq(nil) + expect(@map.class_or_module("Hell")).to eq(nil) + expect(@map.class_or_module("Bush::Brain")).to eq(nil) + end + + it "returns nil if accessing the constant raises RuntimeError" do + expect { NameMapSpecs::BadFile }.to raise_error(RuntimeError) + expect(@map.class_or_module("NameMapSpecs::BadFile")).to eq(nil) + end + + it "returns nil if accessing the constant raises RuntimeError when not triggering the autoload before" do + expect(@map.class_or_module("NameMapSpecs::BadFile2")).to eq(nil) + end +end + +RSpec.describe NameMap, "#dir_name" do + before :each do + @map = NameMap.new + end + + it "returns a directory name from the base name and constant" do + expect(@map.dir_name("NameMapSpecs", 'spec/core')).to eq('spec/core/namemapspecs') + end + + it "returns a directory name from the components in the constants name" do + expect(@map.dir_name("NameMapSpecs::A", 'spec')).to eq('spec/namemapspecs/a') + expect(@map.dir_name("NameMapSpecs::A::B", 'spec')).to eq('spec/namemapspecs/a/b') + end + + it "returns a directory name without 'class' for constants like TrueClass" do + expect(@map.dir_name("TrueClass", 'spec')).to eq('spec/true') + expect(@map.dir_name("FalseClass", 'spec')).to eq('spec/false') + end + + it "returns 'exception' for the directory name of any Exception subclass" do + expect(@map.dir_name("SystemExit", 'spec')).to eq('spec/exception') + expect(@map.dir_name("Errno::EBADF", 'spec')).to eq('spec/exception') + end + + it "returns 'class' for Class" do + expect(@map.dir_name("Class", 'spec')).to eq('spec/class') + end +end + +# These specs do not cover all the mappings, but only describe how the +# name is derived when the hash item maps to a single value, a hash with +# a specific item, or a hash with a :default item. +RSpec.describe NameMap, "#file_name" do + before :each do + @map = NameMap.new + end + + it "returns the name of the spec file based on the constant and method" do + expect(@map.file_name("[]=", "Array")).to eq("element_set_spec.rb") + end + + it "returns the name of the spec file based on the special entry for the method" do + expect(@map.file_name("~", "Regexp")).to eq("match_spec.rb") + expect(@map.file_name("~", "Integer")).to eq("complement_spec.rb") + end + + it "returns the name of the spec file based on the default entry for the method" do + expect(@map.file_name("<<", "NameMapSpecs")).to eq("append_spec.rb") + end + + it "uses the last component of the constant to look up the method name" do + expect(@map.file_name("^", "NameMapSpecs::Integer")).to eq("bit_xor_spec.rb") + end +end + +RSpec.describe NameMap, "#namespace" do + before :each do + @map = NameMap.new + end + + it "prepends the module to the constant name" do + expect(@map.namespace("SubModule", Integer)).to eq("SubModule::Integer") + end + + it "does not prepend Object, Class, or Module to the constant name" do + expect(@map.namespace("Object", String)).to eq("String") + expect(@map.namespace("Module", Integer)).to eq("Integer") + expect(@map.namespace("Class", Float)).to eq("Float") + end +end + +RSpec.describe NameMap, "#map" do + before :each do + @map = NameMap.new + end + + it "flattens an object hierarchy into a single Hash" do + expect(@map.map({}, [NameMapSpecs])).to eq({ + "NameMapSpecs." => ["n"], + "NameMapSpecs#" => ["n"], + "NameMapSpecs::A." => ["a"], + "NameMapSpecs::A#" => ["a", "c"], + "NameMapSpecs::A::B#" => ["b"], + "NameMapSpecs::Fixnum#" => ["f"] + }) + end +end diff --git a/spec/mspec/spec/utils/options_spec.rb b/spec/mspec/spec/utils/options_spec.rb new file mode 100644 index 0000000000..2e3925f579 --- /dev/null +++ b/spec/mspec/spec/utils/options_spec.rb @@ -0,0 +1,1302 @@ +require 'spec_helper' +require 'mspec/utils/options' +require 'mspec/version' +require 'mspec/guards/guard' +require 'mspec/runner/mspec' +require 'mspec/runner/formatters' + +RSpec.describe MSpecOption, ".new" do + before :each do + @opt = MSpecOption.new("-a", "--bdc", "ARG", "desc", :block) + end + + it "sets the short attribute" do + expect(@opt.short).to eq("-a") + end + + it "sets the long attribute" do + expect(@opt.long).to eq("--bdc") + end + + it "sets the arg attribute" do + expect(@opt.arg).to eq("ARG") + end + + it "sets the description attribute" do + expect(@opt.description).to eq("desc") + end + + it "sets the block attribute" do + expect(@opt.block).to eq(:block) + end +end + +RSpec.describe MSpecOption, "#arg?" do + it "returns true if arg attribute is not nil" do + expect(MSpecOption.new(nil, nil, "ARG", nil, nil).arg?).to be_truthy + end + + it "returns false if arg attribute is nil" do + expect(MSpecOption.new(nil, nil, nil, nil, nil).arg?).to be_falsey + end +end + +RSpec.describe MSpecOption, "#match?" do + before :each do + @opt = MSpecOption.new("-a", "--bdc", "ARG", "desc", :block) + end + + it "returns true if the argument matches the short option" do + expect(@opt.match?("-a")).to be_truthy + end + + it "returns true if the argument matches the long option" do + expect(@opt.match?("--bdc")).to be_truthy + end + + it "returns false if the argument matches neither the short nor long option" do + expect(@opt.match?("-b")).to be_falsey + expect(@opt.match?("-abdc")).to be_falsey + end +end + +RSpec.describe MSpecOptions, ".new" do + before :each do + @opt = MSpecOptions.new("cmd", 20, :config) + end + + it "sets the banner attribute" do + expect(@opt.banner).to eq("cmd") + end + + it "sets the config attribute" do + expect(@opt.config).to eq(:config) + end + + it "sets the width attribute" do + expect(@opt.width).to eq(20) + end + + it "sets the default width attribute" do + expect(MSpecOptions.new.width).to eq(30) + end +end + +RSpec.describe MSpecOptions, "#on" do + before :each do + @opt = MSpecOptions.new + end + + it "adds a short option" do + expect(@opt).to receive(:add).with("-a", nil, nil, "desc", nil) + @opt.on("-a", "desc") + end + + it "adds a short option taking an argument" do + expect(@opt).to receive(:add).with("-a", nil, "ARG", "desc", nil) + @opt.on("-a", "ARG", "desc") + end + + it "adds a long option" do + expect(@opt).to receive(:add).with("-a", nil, nil, "desc", nil) + @opt.on("-a", "desc") + end + + it "adds a long option taking an argument" do + expect(@opt).to receive(:add).with("-a", nil, nil, "desc", nil) + @opt.on("-a", "desc") + end + + it "adds a short and long option" do + expect(@opt).to receive(:add).with("-a", nil, nil, "desc", nil) + @opt.on("-a", "desc") + end + + it "adds a short and long option taking an argument" do + expect(@opt).to receive(:add).with("-a", nil, nil, "desc", nil) + @opt.on("-a", "desc") + end + + it "raises MSpecOptions::OptionError if pass less than 2 arguments" do + expect { @opt.on }.to raise_error(MSpecOptions::OptionError) + expect { @opt.on "" }.to raise_error(MSpecOptions::OptionError) + end +end + +RSpec.describe MSpecOptions, "#add" do + before :each do + @opt = MSpecOptions.new "cmd", 20 + @prc = lambda { } + end + + it "adds documentation for an option" do + expect(@opt).to receive(:doc).with(" -t, --typo ARG Correct typo ARG") + @opt.add("-t", "--typo", "ARG", "Correct typo ARG", @prc) + end + + it "leaves spaces in the documentation for a missing short option" do + expect(@opt).to receive(:doc).with(" --typo ARG Correct typo ARG") + @opt.add(nil, "--typo", "ARG", "Correct typo ARG", @prc) + end + + it "handles a short option with argument but no long argument" do + expect(@opt).to receive(:doc).with(" -t ARG Correct typo ARG") + @opt.add("-t", nil, "ARG", "Correct typo ARG", @prc) + end + + it "registers an option" do + option = MSpecOption.new "-t", "--typo", "ARG", "Correct typo ARG", @prc + expect(MSpecOption).to receive(:new).with( + "-t", "--typo", "ARG", "Correct typo ARG", @prc).and_return(option) + @opt.add("-t", "--typo", "ARG", "Correct typo ARG", @prc) + expect(@opt.options).to eq([option]) + end +end + +RSpec.describe MSpecOptions, "#match?" do + before :each do + @opt = MSpecOptions.new + end + + it "returns the MSpecOption instance matching the argument" do + @opt.on "-a", "--abdc", "desc" + option = @opt.match? "-a" + expect(@opt.match?("--abdc")).to be(option) + expect(option).to be_kind_of(MSpecOption) + expect(option.short).to eq("-a") + expect(option.long).to eq("--abdc") + expect(option.description).to eq("desc") + end +end + +RSpec.describe MSpecOptions, "#process" do + before :each do + @opt = MSpecOptions.new + ScratchPad.clear + end + + it "calls the on_extra block if the argument does not match any option" do + @opt.on_extra { ScratchPad.record :extra } + @opt.process ["-a"], "-a", "-a", nil + expect(ScratchPad.recorded).to eq(:extra) + end + + it "returns the matching option" do + @opt.on "-a", "ARG", "desc" + option = @opt.process [], "-a", "-a", "ARG" + expect(option).to be_kind_of(MSpecOption) + expect(option.short).to eq("-a") + expect(option.arg).to eq("ARG") + expect(option.description).to eq("desc") + end + + it "raises an MSpecOptions::ParseError if arg is nil and there are no more entries in argv" do + @opt.on "-a", "ARG", "desc" + expect { @opt.process [], "-a", "-a", nil }.to raise_error(MSpecOptions::ParseError) + end + + it "fetches the argument for the option from argv if arg is nil" do + @opt.on("-a", "ARG", "desc") { |o| ScratchPad.record o } + @opt.process ["ARG"], "-a", "-a", nil + expect(ScratchPad.recorded).to eq("ARG") + end + + it "calls the option's block" do + @opt.on("-a", "ARG", "desc") { ScratchPad.record :option } + @opt.process [], "-a", "-a", "ARG" + expect(ScratchPad.recorded).to eq(:option) + end + + it "does not call the option's block if it is nil" do + @opt.on "-a", "ARG", "desc" + expect { @opt.process [], "-a", "-a", "ARG" }.not_to raise_error + end +end + +RSpec.describe MSpecOptions, "#split" do + before :each do + @opt = MSpecOptions.new + end + + it "breaks a string at the nth character" do + opt, arg, rest = @opt.split "-bdc", 2 + expect(opt).to eq("-b") + expect(arg).to eq("dc") + expect(rest).to eq("dc") + end + + it "returns nil for arg if there are no characters left" do + opt, arg, rest = @opt.split "-b", 2 + expect(opt).to eq("-b") + expect(arg).to eq(nil) + expect(rest).to eq("") + end +end + +RSpec.describe MSpecOptions, "#parse" do + before :each do + @opt = MSpecOptions.new + @prc = lambda { ScratchPad.record :parsed } + @arg_prc = lambda { |o| ScratchPad.record [:parsed, o] } + ScratchPad.clear + end + + it "parses a short option" do + @opt.on "-a", "desc", &@prc + @opt.parse ["-a"] + expect(ScratchPad.recorded).to eq(:parsed) + end + + it "parse a long option" do + @opt.on "--abdc", "desc", &@prc + @opt.parse ["--abdc"] + expect(ScratchPad.recorded).to eq(:parsed) + end + + it "parses a short option group" do + @opt.on "-a", "ARG", "desc", &@arg_prc + @opt.parse ["-a", "ARG"] + expect(ScratchPad.recorded).to eq([:parsed, "ARG"]) + end + + it "parses a short option with an argument" do + @opt.on "-a", "ARG", "desc", &@arg_prc + @opt.parse ["-a", "ARG"] + expect(ScratchPad.recorded).to eq([:parsed, "ARG"]) + end + + it "parses a short option with connected argument" do + @opt.on "-a", "ARG", "desc", &@arg_prc + @opt.parse ["-aARG"] + expect(ScratchPad.recorded).to eq([:parsed, "ARG"]) + end + + it "parses a long option with an argument" do + @opt.on "--abdc", "ARG", "desc", &@arg_prc + @opt.parse ["--abdc", "ARG"] + expect(ScratchPad.recorded).to eq([:parsed, "ARG"]) + end + + it "parses a long option with an '=' argument" do + @opt.on "--abdc", "ARG", "desc", &@arg_prc + @opt.parse ["--abdc=ARG"] + expect(ScratchPad.recorded).to eq([:parsed, "ARG"]) + end + + it "parses a short option group with the final option taking an argument" do + ScratchPad.record [] + @opt.on("-a", "desc") { |o| ScratchPad << :a } + @opt.on("-b", "ARG", "desc") { |o| ScratchPad << [:b, o] } + @opt.parse ["-ab", "ARG"] + expect(ScratchPad.recorded).to eq([:a, [:b, "ARG"]]) + end + + it "parses a short option group with a connected argument" do + ScratchPad.record [] + @opt.on("-a", "desc") { |o| ScratchPad << :a } + @opt.on("-b", "ARG", "desc") { |o| ScratchPad << [:b, o] } + @opt.on("-c", "desc") { |o| ScratchPad << :c } + @opt.parse ["-acbARG"] + expect(ScratchPad.recorded).to eq([:a, :c, [:b, "ARG"]]) + end + + it "returns the unprocessed entries" do + @opt.on "-a", "ARG", "desc", &@arg_prc + expect(@opt.parse(["abdc", "-a", "ilny"])).to eq(["abdc"]) + end + + it "calls the on_extra handler with unrecognized options" do + ScratchPad.record [] + @opt.on_extra { |o| ScratchPad << o } + @opt.on "-a", "desc" + @opt.parse ["-a", "-b"] + expect(ScratchPad.recorded).to eq(["-b"]) + end + + it "does not attempt to call the block if it is nil" do + @opt.on "-a", "ARG", "desc" + expect(@opt.parse(["-a", "ARG"])).to eq([]) + end + + it "raises MSpecOptions::ParseError if passed an unrecognized option" do + expect(@opt).to receive(:raise).with(MSpecOptions::ParseError, an_instance_of(String)) + allow(@opt).to receive(:puts) + allow(@opt).to receive(:exit) + @opt.parse "-u" + end +end + +RSpec.describe MSpecOptions, "#banner=" do + before :each do + @opt = MSpecOptions.new + end + + it "sets the banner attribute" do + expect(@opt.banner).to eq("") + @opt.banner = "banner" + expect(@opt.banner).to eq("banner") + end +end + +RSpec.describe MSpecOptions, "#width=" do + before :each do + @opt = MSpecOptions.new + end + + it "sets the width attribute" do + expect(@opt.width).to eq(30) + @opt.width = 20 + expect(@opt.width).to eq(20) + end +end + +RSpec.describe MSpecOptions, "#config=" do + before :each do + @opt = MSpecOptions.new + end + + it "sets the config attribute" do + expect(@opt.config).to be_nil + @opt.config = :config + expect(@opt.config).to eq(:config) + end +end + +RSpec.describe MSpecOptions, "#doc" do + before :each do + @opt = MSpecOptions.new "command" + end + + it "adds text to be displayed with #to_s" do + @opt.doc "Some message" + @opt.doc "Another message" + expect(@opt.to_s).to eq <<-EOD +command + +Some message +Another message +EOD + end +end + +RSpec.describe MSpecOptions, "#version" do + before :each do + @opt = MSpecOptions.new + ScratchPad.clear + end + + it "installs a basic -v, --version option" do + expect(@opt).to receive(:puts) + expect(@opt).to receive(:exit) + @opt.version "1.0.0" + @opt.parse "-v" + end + + it "accepts a block instead of using the default block" do + @opt.version("1.0.0") { |o| ScratchPad.record :version } + @opt.parse "-v" + expect(ScratchPad.recorded).to eq(:version) + end +end + +RSpec.describe MSpecOptions, "#help" do + before :each do + @opt = MSpecOptions.new + ScratchPad.clear + end + + it "installs a basic -h, --help option" do + expect(@opt).to receive(:puts) + expect(@opt).to receive(:exit).with(1) + @opt.help + @opt.parse "-h" + end + + it "accepts a block instead of using the default block" do + @opt.help { |o| ScratchPad.record :help } + @opt.parse "-h" + expect(ScratchPad.recorded).to eq(:help) + end +end + +RSpec.describe MSpecOptions, "#on_extra" do + before :each do + @opt = MSpecOptions.new + ScratchPad.clear + end + + it "registers a block to be called when an option is not recognized" do + @opt.on_extra { ScratchPad.record :extra } + @opt.parse "-g" + expect(ScratchPad.recorded).to eq(:extra) + end +end + +RSpec.describe MSpecOptions, "#to_s" do + before :each do + @opt = MSpecOptions.new "command" + end + + it "returns the banner and descriptive strings for all registered options" do + @opt.on "-t", "--this ARG", "Adds this ARG to the list" + expect(@opt.to_s).to eq <<-EOD +command + + -t, --this ARG Adds this ARG to the list +EOD + end +end + +RSpec.describe "The -B, --config FILE option" do + before :each do + @options, @config = new_option + end + + it "is enabled with #configure { }" do + expect(@options).to receive(:on).with("-B", "--config", "FILE", + an_instance_of(String)) + @options.configure {} + end + + it "calls the passed block" do + ["-B", "--config"].each do |opt| + ScratchPad.clear + + @options.configure { |x| ScratchPad.record x } + @options.parse [opt, "file"] + expect(ScratchPad.recorded).to eq("file") + end + end +end + +RSpec.describe "The -C, --chdir DIR option" do + before :each do + @options, @config = new_option + @options.chdir + end + + it "is enabled with #chdir" do + expect(@options).to receive(:on).with("-C", "--chdir", "DIR", + an_instance_of(String)) + @options.chdir + end + + it "changes the working directory to DIR" do + expect(Dir).to receive(:chdir).with("dir").twice + ["-C", "--chdir"].each do |opt| + @options.parse [opt, "dir"] + end + end +end + +RSpec.describe "The --prefix STR option" do + before :each do + @options, @config = new_option + end + + it "is enabled with #prefix" do + expect(@options).to receive(:on).with("--prefix", "STR", + an_instance_of(String)) + @options.prefix + end + + it "sets the prefix config value" do + @options.prefix + @options.parse ["--prefix", "some/dir"] + expect(@config[:prefix]).to eq("some/dir") + end +end + +RSpec.describe "The -t, --target TARGET option" do + before :each do + @options, @config = new_option + @options.targets + end + + it "is enabled with #targets" do + allow(@options).to receive(:on) + expect(@options).to receive(:on).with("-t", "--target", "TARGET", + an_instance_of(String)) + @options.targets + end + + it "sets the target to 'ruby' and flags to verbose with TARGET 'r' or 'ruby'" do + ["-t", "--target"].each do |opt| + ["r", "ruby"].each do |t| + @config[:target] = nil + @options.parse [opt, t] + expect(@config[:target]).to eq("ruby") + end + end + end + + it "sets the target to 'jruby' with TARGET 'j' or 'jruby'" do + ["-t", "--target"].each do |opt| + ["j", "jruby"].each do |t| + @config[:target] = nil + @options.parse [opt, t] + expect(@config[:target]).to eq("jruby") + end + end + end + + it "sets the target to 'shotgun/rubinius' with TARGET 'x' or 'rubinius'" do + ["-t", "--target"].each do |opt| + ["x", "rubinius"].each do |t| + @config[:target] = nil + @options.parse [opt, t] + expect(@config[:target]).to eq("./bin/rbx") + end + end + end + + it "set the target to 'rbx' with TARGET 'rbx'" do + ["-t", "--target"].each do |opt| + ["X", "rbx"].each do |t| + @config[:target] = nil + @options.parse [opt, t] + expect(@config[:target]).to eq("rbx") + end + end + end + + it "sets the target to 'maglev' with TARGET 'm' or 'maglev'" do + ["-t", "--target"].each do |opt| + ["m", "maglev"].each do |t| + @config[:target] = nil + @options.parse [opt, t] + expect(@config[:target]).to eq("maglev-ruby") + end + end + end + + it "sets the target to 'topaz' with TARGET 't' or 'topaz'" do + ["-t", "--target"].each do |opt| + ["t", "topaz"].each do |t| + @config[:target] = nil + @options.parse [opt, t] + expect(@config[:target]).to eq("topaz") + end + end + end + + it "sets the target to TARGET" do + ["-t", "--target"].each do |opt| + @config[:target] = nil + @options.parse [opt, "whateva"] + expect(@config[:target]).to eq("whateva") + end + end +end + +RSpec.describe "The -T, --target-opt OPT option" do + before :each do + @options, @config = new_option + @options.targets + end + + it "is enabled with #targets" do + allow(@options).to receive(:on) + expect(@options).to receive(:on).with("-T", "--target-opt", "OPT", + an_instance_of(String)) + @options.targets + end + + it "adds OPT to flags" do + ["-T", "--target-opt"].each do |opt| + @config[:flags].delete "--whateva" + @options.parse [opt, "--whateva"] + expect(@config[:flags]).to include("--whateva") + end + end +end + +RSpec.describe "The -I, --include DIR option" do + before :each do + @options, @config = new_option + @options.targets + end + + it "is enabled with #targets" do + allow(@options).to receive(:on) + expect(@options).to receive(:on).with("-I", "--include", "DIR", + an_instance_of(String)) + @options.targets + end + + it "add DIR to the load path" do + ["-I", "--include"].each do |opt| + @config[:loadpath].delete "-Ipackage" + @options.parse [opt, "package"] + expect(@config[:loadpath]).to include("-Ipackage") + end + end +end + +RSpec.describe "The -r, --require LIBRARY option" do + before :each do + @options, @config = new_option + @options.targets + end + + it "is enabled with #targets" do + allow(@options).to receive(:on) + expect(@options).to receive(:on).with("-r", "--require", "LIBRARY", + an_instance_of(String)) + @options.targets + end + + it "adds LIBRARY to the requires list" do + ["-r", "--require"].each do |opt| + @config[:requires].delete "-rlibrick" + @options.parse [opt, "librick"] + expect(@config[:requires]).to include("-rlibrick") + end + end +end + +RSpec.describe "The -f, --format FORMAT option" do + before :each do + @options, @config = new_option + @options.formatters + end + + it "is enabled with #formatters" do + allow(@options).to receive(:on) + expect(@options).to receive(:on).with("-f", "--format", "FORMAT", + an_instance_of(String)) + @options.formatters + end + + it "sets the SpecdocFormatter with FORMAT 's' or 'specdoc'" do + ["-f", "--format"].each do |opt| + ["s", "specdoc"].each do |f| + @config[:formatter] = nil + @options.parse [opt, f] + expect(@config[:formatter]).to eq(SpecdocFormatter) + end + end + end + + it "sets the HtmlFormatter with FORMAT 'h' or 'html'" do + ["-f", "--format"].each do |opt| + ["h", "html"].each do |f| + @config[:formatter] = nil + @options.parse [opt, f] + expect(@config[:formatter]).to eq(HtmlFormatter) + end + end + end + + it "sets the DottedFormatter with FORMAT 'd', 'dot' or 'dotted'" do + ["-f", "--format"].each do |opt| + ["d", "dot", "dotted"].each do |f| + @config[:formatter] = nil + @options.parse [opt, f] + expect(@config[:formatter]).to eq(DottedFormatter) + end + end + end + + it "sets the DescribeFormatter with FORMAT 'b' or 'describe'" do + ["-f", "--format"].each do |opt| + ["b", "describe"].each do |f| + @config[:formatter] = nil + @options.parse [opt, f] + expect(@config[:formatter]).to eq(DescribeFormatter) + end + end + end + + it "sets the FileFormatter with FORMAT 'f', 'file'" do + ["-f", "--format"].each do |opt| + ["f", "file"].each do |f| + @config[:formatter] = nil + @options.parse [opt, f] + expect(@config[:formatter]).to eq(FileFormatter) + end + end + end + + it "sets the UnitdiffFormatter with FORMAT 'u', 'unit', or 'unitdiff'" do + ["-f", "--format"].each do |opt| + ["u", "unit", "unitdiff"].each do |f| + @config[:formatter] = nil + @options.parse [opt, f] + expect(@config[:formatter]).to eq(UnitdiffFormatter) + end + end + end + + it "sets the SummaryFormatter with FORMAT 'm' or 'summary'" do + ["-f", "--format"].each do |opt| + ["m", "summary"].each do |f| + @config[:formatter] = nil + @options.parse [opt, f] + expect(@config[:formatter]).to eq(SummaryFormatter) + end + end + end + + it "sets the SpinnerFormatter with FORMAT 'a', '*', or 'spin'" do + ["-f", "--format"].each do |opt| + ["a", "*", "spin"].each do |f| + @config[:formatter] = nil + @options.parse [opt, f] + expect(@config[:formatter]).to eq(SpinnerFormatter) + end + end + end + + it "sets the MethodFormatter with FORMAT 't' or 'method'" do + ["-f", "--format"].each do |opt| + ["t", "method"].each do |f| + @config[:formatter] = nil + @options.parse [opt, f] + expect(@config[:formatter]).to eq(MethodFormatter) + end + end + end + + it "sets the YamlFormatter with FORMAT 'y' or 'yaml'" do + ["-f", "--format"].each do |opt| + ["y", "yaml"].each do |f| + @config[:formatter] = nil + @options.parse [opt, f] + expect(@config[:formatter]).to eq(YamlFormatter) + end + end + end + + it "sets the JUnitFormatter with FORMAT 'j' or 'junit'" do + ["-f", "--format"].each do |opt| + ["j", "junit"].each do |f| + @config[:formatter] = nil + @options.parse [opt, f] + expect(@config[:formatter]).to eq(JUnitFormatter) + end + end + end +end + +RSpec.describe "The -o, --output FILE option" do + before :each do + @options, @config = new_option + @options.formatters + end + + it "is enabled with #formatters" do + allow(@options).to receive(:on) + expect(@options).to receive(:on).with("-o", "--output", "FILE", + an_instance_of(String)) + @options.formatters + end + + it "sets the output to FILE" do + ["-o", "--output"].each do |opt| + @config[:output] = nil + @options.parse [opt, "some/file"] + expect(@config[:output]).to eq("some/file") + end + end +end + +RSpec.describe "The -e, --example STR" do + before :each do + @options, @config = new_option + @options.filters + end + + it "is enabled with #filters" do + allow(@options).to receive(:on) + expect(@options).to receive(:on).with("-e", "--example", "STR", + an_instance_of(String)) + @options.filters + end + + it "adds STR to the includes list" do + ["-e", "--example"].each do |opt| + @config[:includes] = [] + @options.parse [opt, "this spec"] + expect(@config[:includes]).to include("this spec") + end + end +end + +RSpec.describe "The -E, --exclude STR" do + before :each do + @options, @config = new_option + @options.filters + end + + it "is enabled with #filters" do + allow(@options).to receive(:on) + expect(@options).to receive(:on).with("-E", "--exclude", "STR", + an_instance_of(String)) + @options.filters + end + + it "adds STR to the excludes list" do + ["-E", "--exclude"].each do |opt| + @config[:excludes] = [] + @options.parse [opt, "this spec"] + expect(@config[:excludes]).to include("this spec") + end + end +end + +RSpec.describe "The -p, --pattern PATTERN" do + before :each do + @options, @config = new_option + @options.filters + end + + it "is enabled with #filters" do + allow(@options).to receive(:on) + expect(@options).to receive(:on).with("-p", "--pattern", "PATTERN", + an_instance_of(String)) + @options.filters + end + + it "adds PATTERN to the included patterns list" do + ["-p", "--pattern"].each do |opt| + @config[:patterns] = [] + @options.parse [opt, "this spec"] + expect(@config[:patterns]).to include(/this spec/) + end + end +end + +RSpec.describe "The -P, --excl-pattern PATTERN" do + before :each do + @options, @config = new_option + @options.filters + end + + it "is enabled with #filters" do + allow(@options).to receive(:on) + expect(@options).to receive(:on).with("-P", "--excl-pattern", "PATTERN", + an_instance_of(String)) + @options.filters + end + + it "adds PATTERN to the excluded patterns list" do + ["-P", "--excl-pattern"].each do |opt| + @config[:xpatterns] = [] + @options.parse [opt, "this spec"] + expect(@config[:xpatterns]).to include(/this spec/) + end + end +end + +RSpec.describe "The -g, --tag TAG" do + before :each do + @options, @config = new_option + @options.filters + end + + it "is enabled with #filters" do + allow(@options).to receive(:on) + expect(@options).to receive(:on).with("-g", "--tag", "TAG", + an_instance_of(String)) + @options.filters + end + + it "adds TAG to the included tags list" do + ["-g", "--tag"].each do |opt| + @config[:tags] = [] + @options.parse [opt, "this spec"] + expect(@config[:tags]).to include("this spec") + end + end +end + +RSpec.describe "The -G, --excl-tag TAG" do + before :each do + @options, @config = new_option + @options.filters + end + + it "is enabled with #filters" do + allow(@options).to receive(:on) + expect(@options).to receive(:on).with("-G", "--excl-tag", "TAG", + an_instance_of(String)) + @options.filters + end + + it "adds TAG to the excluded tags list" do + ["-G", "--excl-tag"].each do |opt| + @config[:xtags] = [] + @options.parse [opt, "this spec"] + expect(@config[:xtags]).to include("this spec") + end + end +end + +RSpec.describe "The -w, --profile FILE option" do + before :each do + @options, @config = new_option + @options.filters + end + + it "is enabled with #filters" do + allow(@options).to receive(:on) + expect(@options).to receive(:on).with("-w", "--profile", "FILE", + an_instance_of(String)) + @options.filters + end + + it "adds FILE to the included profiles list" do + ["-w", "--profile"].each do |opt| + @config[:profiles] = [] + @options.parse [opt, "spec/profiles/rails.yaml"] + expect(@config[:profiles]).to include("spec/profiles/rails.yaml") + end + end +end + +RSpec.describe "The -W, --excl-profile FILE option" do + before :each do + @options, @config = new_option + @options.filters + end + + it "is enabled with #filters" do + allow(@options).to receive(:on) + expect(@options).to receive(:on).with("-W", "--excl-profile", "FILE", + an_instance_of(String)) + @options.filters + end + + it "adds FILE to the excluded profiles list" do + ["-W", "--excl-profile"].each do |opt| + @config[:xprofiles] = [] + @options.parse [opt, "spec/profiles/rails.yaml"] + expect(@config[:xprofiles]).to include("spec/profiles/rails.yaml") + end + end +end + +RSpec.describe "The -Z, --dry-run option" do + before :each do + @options, @config = new_option + @options.pretend + end + + it "is enabled with #pretend" do + expect(@options).to receive(:on).with("-Z", "--dry-run", an_instance_of(String)) + @options.pretend + end + + it "registers the MSpec pretend mode" do + expect(MSpec).to receive(:register_mode).with(:pretend).twice + ["-Z", "--dry-run"].each do |opt| + @options.parse opt + end + end +end + +RSpec.describe "The --unguarded option" do + before :each do + @options, @config = new_option + @options.unguarded + end + + it "is enabled with #unguarded" do + allow(@options).to receive(:on) + expect(@options).to receive(:on).with("--unguarded", an_instance_of(String)) + @options.unguarded + end + + it "registers the MSpec unguarded mode" do + expect(MSpec).to receive(:register_mode).with(:unguarded) + @options.parse "--unguarded" + end +end + +RSpec.describe "The --no-ruby_guard option" do + before :each do + @options, @config = new_option + @options.unguarded + end + + it "is enabled with #unguarded" do + allow(@options).to receive(:on) + expect(@options).to receive(:on).with("--no-ruby_bug", an_instance_of(String)) + @options.unguarded + end + + it "registers the MSpec no_ruby_bug mode" do + expect(MSpec).to receive(:register_mode).with(:no_ruby_bug) + @options.parse "--no-ruby_bug" + end +end + +RSpec.describe "The -H, --random option" do + before :each do + @options, @config = new_option + @options.randomize + end + + it "is enabled with #randomize" do + expect(@options).to receive(:on).with("-H", "--random", an_instance_of(String)) + @options.randomize + end + + it "registers the MSpec randomize mode" do + expect(MSpec).to receive(:randomize=).twice + ["-H", "--random"].each do |opt| + @options.parse opt + end + end +end + +RSpec.describe "The -R, --repeat option" do + before :each do + @options, @config = new_option + @options.repeat + end + + it "is enabled with #repeat" do + expect(@options).to receive(:on).with("-R", "--repeat", "NUMBER", an_instance_of(String)) + @options.repeat + end + + it "registers the MSpec repeat mode" do + ["-R", "--repeat"].each do |opt| + MSpec.repeat = 1 + @options.parse [opt, "10"] + repeat_count = 0 + MSpec.repeat do + repeat_count += 1 + end + expect(repeat_count).to eq(10) + end + end +end + +RSpec.describe "The -V, --verbose option" do + before :each do + @options, @config = new_option + @options.verbose + end + + it "is enabled with #verbose" do + allow(@options).to receive(:on) + expect(@options).to receive(:on).with("-V", "--verbose", an_instance_of(String)) + @options.verbose + end + + it "registers a verbose output object with MSpec" do + expect(MSpec).to receive(:register).with(:start, anything()).twice + expect(MSpec).to receive(:register).with(:load, anything()).twice + ["-V", "--verbose"].each do |opt| + @options.parse opt + end + end +end + +RSpec.describe "The -m, --marker MARKER option" do + before :each do + @options, @config = new_option + @options.verbose + end + + it "is enabled with #verbose" do + allow(@options).to receive(:on) + expect(@options).to receive(:on).with("-m", "--marker", "MARKER", + an_instance_of(String)) + @options.verbose + end + + it "registers a marker output object with MSpec" do + expect(MSpec).to receive(:register).with(:load, anything()).twice + ["-m", "--marker"].each do |opt| + @options.parse [opt, ","] + end + end +end + +RSpec.describe "The --int-spec option" do + before :each do + @options, @config = new_option + @options.interrupt + end + + it "is enabled with #interrupt" do + expect(@options).to receive(:on).with("--int-spec", an_instance_of(String)) + @options.interrupt + end + + it "sets the abort config option to false to only abort the running spec with ^C" do + @config[:abort] = true + @options.parse "--int-spec" + expect(@config[:abort]).to eq(false) + end +end + +RSpec.describe "The -Y, --verify option" do + before :each do + @options, @config = new_option + @options.verify + end + + it "is enabled with #interrupt" do + allow(@options).to receive(:on) + expect(@options).to receive(:on).with("-Y", "--verify", an_instance_of(String)) + @options.verify + end + + it "sets the MSpec mode to :verify" do + expect(MSpec).to receive(:register_mode).with(:verify).twice + ["-Y", "--verify"].each do |m| + @options.parse m + end + end +end + +RSpec.describe "The -O, --report option" do + before :each do + @options, @config = new_option + @options.verify + end + + it "is enabled with #interrupt" do + allow(@options).to receive(:on) + expect(@options).to receive(:on).with("-O", "--report", an_instance_of(String)) + @options.verify + end + + it "sets the MSpec mode to :report" do + expect(MSpec).to receive(:register_mode).with(:report).twice + ["-O", "--report"].each do |m| + @options.parse m + end + end +end + +RSpec.describe "The --report-on GUARD option" do + before :each do + allow(MSpec).to receive(:register_mode) + + @options, @config = new_option + @options.verify + + SpecGuard.clear_guards + end + + after :each do + SpecGuard.clear_guards + end + + it "is enabled with #interrupt" do + allow(@options).to receive(:on) + expect(@options).to receive(:on).with("--report-on", "GUARD", an_instance_of(String)) + @options.verify + end + + it "sets the MSpec mode to :report_on" do + expect(MSpec).to receive(:register_mode).with(:report_on) + @options.parse ["--report-on", "ruby_bug"] + end + + it "converts the guard name to a symbol" do + name = double("ruby_bug") + expect(name).to receive(:to_sym) + @options.parse ["--report-on", name] + end + + it "saves the name of the guard" do + @options.parse ["--report-on", "ruby_bug"] + expect(SpecGuard.guards).to eq([:ruby_bug]) + end +end + +RSpec.describe "The -K, --action-tag TAG option" do + before :each do + @options, @config = new_option + @options.action_filters + end + + it "is enabled with #action_filters" do + allow(@options).to receive(:on) + expect(@options).to receive(:on).with("-K", "--action-tag", "TAG", + an_instance_of(String)) + @options.action_filters + end + + it "adds TAG to the list of tags that trigger actions" do + ["-K", "--action-tag"].each do |opt| + @config[:atags] = [] + @options.parse [opt, "action-tag"] + expect(@config[:atags]).to include("action-tag") + end + end +end + +RSpec.describe "The -S, --action-string STR option" do + before :each do + @options, @config = new_option + @options.action_filters + end + + it "is enabled with #action_filters" do + allow(@options).to receive(:on) + expect(@options).to receive(:on).with("-S", "--action-string", "STR", + an_instance_of(String)) + @options.action_filters + end + + it "adds STR to the list of spec descriptions that trigger actions" do + ["-S", "--action-string"].each do |opt| + @config[:astrings] = [] + @options.parse [opt, "action-str"] + expect(@config[:astrings]).to include("action-str") + end + end +end + +RSpec.describe "The -d, --debug option" do + before :each do + @options, @config = new_option + @options.debug + end + + after :each do + $MSPEC_DEBUG = nil + end + + it "is enabled with #debug" do + allow(@options).to receive(:on) + expect(@options).to receive(:on).with("-d", "--debug", an_instance_of(String)) + @options.debug + end + + it "sets $MSPEC_DEBUG to true" do + ["-d", "--debug"].each do |opt| + expect($MSPEC_DEBUG).not_to be_truthy + @options.parse opt + expect($MSPEC_DEBUG).to be_truthy + $MSPEC_DEBUG = nil + end + end +end + +RSpec.describe "MSpecOptions#all" do + it "includes all options" do + meth = MSpecOptions.instance_method(:all) + file, line = meth.source_location + contents = File.read(file) + lines = contents.lines + + from = line + to = from + to += 1 until /^\s*end\s*$/ =~ lines[to] + calls = lines[from...to].map(&:strip) + + option_methods = contents.scan(/def (\w+).*\n\s*on\(/).map(&:first) + option_methods[0].sub!("configure", "configure {}") + + expect(calls).to eq(option_methods) + end +end diff --git a/spec/mspec/spec/utils/script_spec.rb b/spec/mspec/spec/utils/script_spec.rb new file mode 100644 index 0000000000..c35bda8b47 --- /dev/null +++ b/spec/mspec/spec/utils/script_spec.rb @@ -0,0 +1,470 @@ +require 'spec_helper' +require 'mspec/utils/script' +require 'mspec/runner/mspec' +require 'mspec/runner/filters' +require 'mspec/runner/actions/filter' + +RSpec.describe MSpecScript, ".config" do + it "returns a Hash" do + expect(MSpecScript.config).to be_kind_of(Hash) + end +end + +RSpec.describe MSpecScript, ".set" do + it "sets the config hash key, value" do + MSpecScript.set :a, 10 + expect(MSpecScript.config[:a]).to eq(10) + end +end + +RSpec.describe MSpecScript, ".get" do + it "gets the config hash value for a key" do + MSpecScript.set :a, 10 + expect(MSpecScript.get(:a)).to eq(10) + end +end + +RSpec.describe MSpecScript, "#config" do + it "returns the MSpecScript config hash" do + MSpecScript.set :b, 5 + expect(MSpecScript.new.config[:b]).to eq(5) + end + + it "returns the MSpecScript config hash from subclasses" do + class MSSClass < MSpecScript; end + MSpecScript.set :b, 5 + expect(MSSClass.new.config[:b]).to eq(5) + end +end + +RSpec.describe MSpecScript, "#load_default" do + before :all do + @verbose = $VERBOSE + $VERBOSE = nil + end + + after :all do + $VERBOSE = @verbose + end + + before :each do + @version = RUBY_VERSION + if Object.const_defined? :RUBY_ENGINE + @engine = Object.const_get :RUBY_ENGINE + end + @script = MSpecScript.new + allow(MSpecScript).to receive(:new).and_return(@script) + end + + after :each do + Object.const_set :RUBY_VERSION, @version + Object.const_set :RUBY_ENGINE, @engine if @engine + end + + it "attempts to load 'default.mspec'" do + allow(@script).to receive(:try_load) + expect(@script).to receive(:try_load).with('default.mspec').and_return(true) + @script.load_default + end + + it "attempts to load a config file based on RUBY_ENGINE and RUBY_VERSION" do + Object.const_set :RUBY_ENGINE, "ybur" + Object.const_set :RUBY_VERSION, "1.8.9" + default = "ybur.1.8.mspec" + expect(@script).to receive(:try_load).with('default.mspec').and_return(false) + expect(@script).to receive(:try_load).with(default) + expect(@script).to receive(:try_load).with('ybur.mspec') + @script.load_default + end +end + +RSpec.describe MSpecScript, ".main" do + before :each do + @script = double("MSpecScript").as_null_object + allow(MSpecScript).to receive(:new).and_return(@script) + # Do not require full mspec as it would conflict with RSpec + expect(MSpecScript).to receive(:require).with('mspec') + end + + it "creates an instance of MSpecScript" do + expect(MSpecScript).to receive(:new).and_return(@script) + MSpecScript.main + end + + it "attempts to load the default config" do + expect(@script).to receive(:load_default) + MSpecScript.main + end + + it "calls the #options method on the script" do + expect(@script).to receive(:options) + MSpecScript.main + end + + it "calls the #signals method on the script" do + expect(@script).to receive(:signals) + MSpecScript.main + end + + it "calls the #register method on the script" do + expect(@script).to receive(:register) + MSpecScript.main + end + + it "calls the #setup_env method on the script" do + expect(@script).to receive(:setup_env) + MSpecScript.main + end + + it "calls the #run method on the script" do + expect(@script).to receive(:run) + MSpecScript.main + end +end + +RSpec.describe MSpecScript, "#initialize" do + before :each do + @config = MSpecScript.new.config + end + + it "sets the default config values" do + expect(@config[:formatter]).to eq(nil) + expect(@config[:includes]).to eq([]) + expect(@config[:excludes]).to eq([]) + expect(@config[:patterns]).to eq([]) + expect(@config[:xpatterns]).to eq([]) + expect(@config[:tags]).to eq([]) + expect(@config[:xtags]).to eq([]) + expect(@config[:atags]).to eq([]) + expect(@config[:astrings]).to eq([]) + expect(@config[:abort]).to eq(true) + expect(@config[:config_ext]).to eq('.mspec') + end +end + +RSpec.describe MSpecScript, "#load" do + before :each do + allow(File).to receive(:exist?).and_return(false) + @script = MSpecScript.new + @file = "default.mspec" + @base = "default" + end + + it "attempts to locate the file through the expanded path name" do + expect(File).to receive(:expand_path).with(@file, ".").and_return(@file) + expect(File).to receive(:exist?).with(@file).and_return(true) + expect(Kernel).to receive(:load).with(@file).and_return(:loaded) + expect(@script.load(@file)).to eq(:loaded) + end + + it "appends config[:config_ext] to the name and attempts to locate the file through the expanded path name" do + expect(File).to receive(:expand_path).with(@base, ".").and_return(@base) + expect(File).to receive(:expand_path).with(@base, "spec").and_return(@base) + expect(File).to receive(:expand_path).with(@file, ".").and_return(@file) + expect(File).to receive(:exist?).with(@base).and_return(false) + expect(File).to receive(:exist?).with(@file).and_return(true) + expect(Kernel).to receive(:load).with(@file).and_return(:loaded) + expect(@script.load(@base)).to eq(:loaded) + end + + it "attempts to locate the file in '.'" do + path = File.expand_path @file, "." + expect(File).to receive(:exist?).with(path).and_return(true) + expect(Kernel).to receive(:load).with(path).and_return(:loaded) + expect(@script.load(@file)).to eq(:loaded) + end + + it "appends config[:config_ext] to the name and attempts to locate the file in '.'" do + path = File.expand_path @file, "." + expect(File).to receive(:exist?).with(path).and_return(true) + expect(Kernel).to receive(:load).with(path).and_return(:loaded) + expect(@script.load(@base)).to eq(:loaded) + end + + it "attempts to locate the file in 'spec'" do + path = File.expand_path @file, "spec" + expect(File).to receive(:exist?).with(path).and_return(true) + expect(Kernel).to receive(:load).with(path).and_return(:loaded) + expect(@script.load(@file)).to eq(:loaded) + end + + it "appends config[:config_ext] to the name and attempts to locate the file in 'spec'" do + path = File.expand_path @file, "spec" + expect(File).to receive(:exist?).with(path).and_return(true) + expect(Kernel).to receive(:load).with(path).and_return(:loaded) + expect(@script.load(@base)).to eq(:loaded) + end + + it "loads a given file only once" do + path = File.expand_path @file, "spec" + expect(File).to receive(:exist?).with(path).and_return(true) + expect(Kernel).to receive(:load).once.with(path).and_return(:loaded) + expect(@script.load(@base)).to eq(:loaded) + expect(@script.load(@base)).to eq(true) + end +end + +RSpec.describe MSpecScript, "#custom_options" do + before :each do + @script = MSpecScript.new + end + + after :each do + end + + it "prints 'None'" do + options = double("options") + expect(options).to receive(:doc).with(" No custom options registered") + @script.custom_options options + end +end + +RSpec.describe MSpecScript, "#register" do + before :each do + @script = MSpecScript.new + + @formatter = double("formatter").as_null_object + @script.config[:formatter] = @formatter + end + + it "creates and registers the formatter" do + expect(@formatter).to receive(:new).and_return(@formatter) + expect(@formatter).to receive(:register) + @script.register + end + + it "does not register the formatter if config[:formatter] is false" do + @script.config[:formatter] = false + @script.register + end + + it "calls #custom_register" do + expect(@script).to receive(:custom_register) + @script.register + end + + it "registers :formatter with the formatter instance" do + allow(@formatter).to receive(:new).and_return(@formatter) + @script.register + expect(MSpec.formatter).to be(@formatter) + end + + it "does not register :formatter if config[:formatter] is false" do + @script.config[:formatter] = false + expect(MSpec).not_to receive(:store) + @script.register + end +end + +RSpec.describe MSpecScript, "#register" do + before :each do + @script = MSpecScript.new + + @formatter = double("formatter").as_null_object + @script.config[:formatter] = @formatter + + @filter = double("filter") + expect(@filter).to receive(:register) + + @ary = ["some", "spec"] + end + + it "creates and registers a MatchFilter for include specs" do + expect(MatchFilter).to receive(:new).with(:include, *@ary).and_return(@filter) + @script.config[:includes] = @ary + @script.register + end + + it "creates and registers a MatchFilter for excluded specs" do + expect(MatchFilter).to receive(:new).with(:exclude, *@ary).and_return(@filter) + @script.config[:excludes] = @ary + @script.register + end + + it "creates and registers a RegexpFilter for include specs" do + expect(RegexpFilter).to receive(:new).with(:include, *@ary).and_return(@filter) + @script.config[:patterns] = @ary + @script.register + end + + it "creates and registers a RegexpFilter for excluded specs" do + expect(RegexpFilter).to receive(:new).with(:exclude, *@ary).and_return(@filter) + @script.config[:xpatterns] = @ary + @script.register + end + + it "creates and registers a TagFilter for include specs" do + expect(TagFilter).to receive(:new).with(:include, *@ary).and_return(@filter) + @script.config[:tags] = @ary + @script.register + end + + it "creates and registers a TagFilter for excluded specs" do + expect(TagFilter).to receive(:new).with(:exclude, *@ary).and_return(@filter) + @script.config[:xtags] = @ary + @script.register + end + + it "creates and registers a ProfileFilter for include specs" do + expect(ProfileFilter).to receive(:new).with(:include, *@ary).and_return(@filter) + @script.config[:profiles] = @ary + @script.register + end + + it "creates and registers a ProfileFilter for excluded specs" do + expect(ProfileFilter).to receive(:new).with(:exclude, *@ary).and_return(@filter) + @script.config[:xprofiles] = @ary + @script.register + end +end + +RSpec.describe MSpecScript, "#signals" do + before :each do + @script = MSpecScript.new + @abort = @script.config[:abort] + end + + after :each do + @script.config[:abort] = @abort + end + + it "traps the INT signal if config[:abort] is true" do + expect(Signal).to receive(:trap).with("INT") + @script.config[:abort] = true + @script.signals + end + + it "does not trap the INT signal if config[:abort] is not true" do + expect(Signal).not_to receive(:trap).with("INT") + @script.config[:abort] = false + @script.signals + end +end + +RSpec.describe MSpecScript, "#entries" do + before :each do + @script = MSpecScript.new + + allow(File).to receive(:realpath).and_return("name") + allow(File).to receive(:file?).and_return(false) + allow(File).to receive(:directory?).and_return(false) + end + + it "returns the pattern in an array if it is a file" do + expect(File).to receive(:realpath).with("file").and_return("file/expanded.rb") + expect(File).to receive(:file?).with("file/expanded.rb").and_return(true) + expect(@script.entries("file")).to eq(["file/expanded.rb"]) + end + + it "returns Dir['pattern/**/*_spec.rb'] if pattern is a directory" do + expect(File).to receive(:directory?).with("name").and_return(true) + allow(File).to receive(:realpath).and_return("name", "name/**/*_spec.rb") + expect(Dir).to receive(:[]).with("name/**/*_spec.rb").and_return(["dir1", "dir2"]) + expect(@script.entries("name")).to eq(["dir1", "dir2"]) + end + + it "aborts if pattern cannot be resolved to a file nor a directory" do + expect(@script).to receive(:abort) + @script.entries("pattern") + end + + describe "with config[:prefix] set" do + before :each do + prefix = "prefix/dir" + @script.config[:prefix] = prefix + @name = prefix + "/name" + end + + it "returns the pattern in an array if it is a file" do + name = "#{@name}.rb" + expect(File).to receive(:realpath).with(name).and_return(name) + expect(File).to receive(:file?).with(name).and_return(true) + expect(@script.entries("name.rb")).to eq([name]) + end + + it "returns Dir['pattern/**/*_spec.rb'] if pattern is a directory" do + allow(File).to receive(:realpath).and_return(@name, @name+"/**/*_spec.rb") + expect(File).to receive(:directory?).with(@name).and_return(true) + expect(Dir).to receive(:[]).with(@name + "/**/*_spec.rb").and_return(["dir1", "dir2"]) + expect(@script.entries("name")).to eq(["dir1", "dir2"]) + end + + it "aborts if pattern cannot be resolved to a file nor a directory" do + expect(@script).to receive(:abort) + @script.entries("pattern") + end + end +end + +RSpec.describe MSpecScript, "#files" do + before :each do + @script = MSpecScript.new + end + + it "accumulates the values returned by #entries" do + expect(@script).to receive(:entries).and_return(["file1"], ["file2"]) + expect(@script.files(["a", "b"])).to eq(["file1", "file2"]) + end + + it "strips a leading '^' and removes the values returned by #entries" do + expect(@script).to receive(:entries).and_return(["file1"], ["file2"], ["file1"]) + expect(@script.files(["a", "b", "^a"])).to eq(["file2"]) + end + + it "processes the array elements in order" do + expect(@script).to receive(:entries).and_return(["file1"], ["file1"], ["file2"]) + expect(@script.files(["^a", "a", "b"])).to eq(["file1", "file2"]) + end +end + +RSpec.describe MSpecScript, "#files" do + before :each do + MSpecScript.set :files, ["file1", "file2"] + + @script = MSpecScript.new + end + + after :each do + MSpecScript.config.delete :files + end + + it "looks up items with leading ':' in the config object" do + expect(@script).to receive(:entries).and_return(["file1"], ["file2"]) + expect(@script.files([":files"])).to eq(["file1", "file2"]) + end + + it "aborts if the config key is not set" do + expect(@script).to receive(:abort).with("Key :all_files not found in mspec config.") + @script.files([":all_files"]) + end +end + +RSpec.describe MSpecScript, "#setup_env" do + before :each do + @script = MSpecScript.new + @options, @config = new_option + allow(@script).to receive(:config).and_return(@config) + end + + after :each do + end + + it "sets MSPEC_RUNNER = '1' in the environment" do + ENV["MSPEC_RUNNER"] = "0" + @script.setup_env + expect(ENV["MSPEC_RUNNER"]).to eq("1") + end + + it "sets RUBY_EXE = config[:target] in the environment" do + ENV["RUBY_EXE"] = nil + @script.setup_env + expect(ENV["RUBY_EXE"]).to eq(@config[:target]) + end + + it "sets RUBY_FLAGS = config[:flags] in the environment" do + ENV["RUBY_FLAGS"] = nil + @config[:flags] = ["-w", "-Q"] + @script.setup_env + expect(ENV["RUBY_FLAGS"]).to eq("-w -Q") + end +end diff --git a/spec/mspec/spec/utils/version_spec.rb b/spec/mspec/spec/utils/version_spec.rb new file mode 100644 index 0000000000..ec367d2a1e --- /dev/null +++ b/spec/mspec/spec/utils/version_spec.rb @@ -0,0 +1,45 @@ +require 'spec_helper' +require 'mspec/utils/version' + +RSpec.describe SpecVersion, "#to_s" do + it "returns the string with which it was initialized" do + expect(SpecVersion.new("1.8").to_s).to eq("1.8") + expect(SpecVersion.new("2.118.9").to_s).to eq("2.118.9") + end +end + +RSpec.describe SpecVersion, "#to_str" do + it "returns the same string as #to_s" do + version = SpecVersion.new("2.118.9") + expect(version.to_str).to eq(version.to_s) + end +end + +RSpec.describe SpecVersion, "#to_i with ceil = false" do + it "returns an integer representation of the version string" do + expect(SpecVersion.new("2.23.10").to_i).to eq(1022310) + end + + it "replaces missing version parts with zeros" do + expect(SpecVersion.new("1.8").to_i).to eq(1010800) + expect(SpecVersion.new("1.8.6").to_i).to eq(1010806) + end +end + +RSpec.describe SpecVersion, "#to_i with ceil = true" do + it "returns an integer representation of the version string" do + expect(SpecVersion.new("1.8.6", true).to_i).to eq(1010806) + end + + it "fills in 9s for missing tiny values" do + expect(SpecVersion.new("1.8", true).to_i).to eq(1010899) + expect(SpecVersion.new("1.8.6", true).to_i).to eq(1010806) + end +end + +RSpec.describe SpecVersion, "#to_int" do + it "returns the same value as #to_i" do + version = SpecVersion.new("4.16.87") + expect(version.to_int).to eq(version.to_i) + end +end diff --git a/spec/mspec/tool/check_require_spec_helper.rb b/spec/mspec/tool/check_require_spec_helper.rb new file mode 100755 index 0000000000..07126e68dc --- /dev/null +++ b/spec/mspec/tool/check_require_spec_helper.rb @@ -0,0 +1,34 @@ +#!/usr/bin/env ruby + +# This script is used to check that each *_spec.rb file has +# a relative_require for spec_helper which should live higher +# up in the ruby/spec repo directory tree. +# +# Prints errors to $stderr and returns a non-zero exit code when +# errors are found. +# +# Related to https://github.com/ruby/spec/pull/992 + +def check_file(fn) + File.foreach(fn) do |line| + return $1 if line =~ /^\s*require_relative\s*['"](.*spec_helper)['"]/ + end + nil +end + +rootdir = ARGV[0] || "." +fglob = File.join(rootdir, "**", "*_spec.rb") +specfiles = Dir.glob(fglob) +raise "No spec files found in #{fglob.inspect}. Give an argument to specify the root-directory of ruby/spec" if specfiles.empty? + +errors = 0 +specfiles.sort.each do |fn| + result = check_file(fn) + if result.nil? + warn "Missing require_relative for *spec_helper for file: #{fn}" + errors += 1 + end +end + +puts "# Found #{errors} files with require_relative spec_helper issues." +exit 1 if errors > 0 diff --git a/spec/mspec/tool/find.rb b/spec/mspec/tool/find.rb new file mode 100755 index 0000000000..322b023f15 --- /dev/null +++ b/spec/mspec/tool/find.rb @@ -0,0 +1,10 @@ +#!/usr/bin/env ruby +Dir.chdir('../rubyspec') do + regexp = Regexp.new(ARGV[0]) + Dir.glob('**/*.rb') do |file| + contents = File.read(file) + if regexp =~ contents + puts file + end + end +end diff --git a/spec/mspec/tool/pull-latest-mspec-spec b/spec/mspec/tool/pull-latest-mspec-spec new file mode 100755 index 0000000000..154a353e64 --- /dev/null +++ b/spec/mspec/tool/pull-latest-mspec-spec @@ -0,0 +1,26 @@ +#!/bin/bash + +# Assumes all commits have been synchronized to https://github.com/ruby/spec +# See spec/mspec/tool/sync/sync-rubyspec.rb + +function sync { + dir="$1" + repo="$2" + short_repo_name="ruby/$(basename "$repo" .git)" + + rm -rf "$dir" + git clone --depth 1 "$repo" "$dir" + commit=$(git -C "$dir" log -n 1 --format='%h') + rm -rf "$dir/.git" + + # Remove CI files to avoid confusion + rm -f "$dir/appveyor.yml" + rm -f "$dir/.travis.yml" + rm -rf "$dir/.github" + + git add "$dir" + git commit -m "Update to ${short_repo_name}@${commit}" +} + +sync spec/mspec https://github.com/ruby/mspec.git +sync spec/ruby https://github.com/ruby/spec.git diff --git a/spec/mspec/tool/remove_old_guards.rb b/spec/mspec/tool/remove_old_guards.rb new file mode 100755 index 0000000000..bc5612c78d --- /dev/null +++ b/spec/mspec/tool/remove_old_guards.rb @@ -0,0 +1,145 @@ +#!/usr/bin/env ruby + +# Removes old version guards in ruby/spec. +# Run it from the ruby/spec repository root. +# The argument is the new minimum supported version. +# +# cd spec +# ../mspec/tool/remove_old_guards.rb <ruby-version> +# +# where <ruby-version> is a version guard with which should be removed +# +# Example: +# tool/remove_old_guards.rb 3.1 +# +# As a result guards like +# ruby_version_is "3.1" do +# # ... +# end +# +# will be removed. + +def dedent(line) + if line.start_with?(" ") + line[2..-1] + else + line + end +end + +def each_spec_file(&block) + Dir["*/**/*.rb"].each(&block) +end + +def each_file(&block) + Dir["**/*"].each { |path| + yield path if File.file?(path) + } +end + +def remove_guards(guard, keep) + each_spec_file do |file| + contents = File.binread(file) + if contents =~ guard + puts file + lines = contents.lines.to_a + while first = lines.find_index { |line| line =~ guard } + comment = first + while comment > 0 and lines[comment-1] =~ /^(\s*)#/ + comment -= 1 + end + indent = lines[first][/^(\s*)/, 1].length + last = (first+1...lines.size).find { |i| + space = lines[i][/^(\s*)end$/, 1] and space.length == indent + } + raise file unless last + if keep + lines[comment..last] = lines[first+1..last-1].map { |l| dedent(l) } + else + if comment > 0 and lines[comment-1] == "\n" + comment -= 1 + elsif lines[last+1] == "\n" + last += 1 + end + lines[comment..last] = [] + end + end + File.binwrite file, lines.join + end + end +end + +def remove_empty_files + each_spec_file do |file| + unless file.include?("fixtures/") + lines = File.readlines(file) + if lines.all? { |line| line.chomp.empty? or line.start_with?('require', '#') } + puts "Removing empty file #{file}" + File.delete(file) + end + end + end +end + +def remove_unused_shared_specs + shared_groups = {} + # Dir["**/shared/**/*.rb"].each do |shared| + each_spec_file do |shared| + next if File.basename(shared) == 'constants.rb' + contents = File.binread(shared) + found = false + contents.scan(/^\s*describe (:[\w_?]+), shared: true do$/) { + shared_groups[$1] = 0 + found = true + } + if !found and shared.include?('shared/') and !shared.include?('fixtures/') and !shared.end_with?('/constants.rb') + puts "no shared describe in #{shared} ?" + end + end + + each_spec_file do |file| + contents = File.binread(file) + contents.scan(/(?:it_behaves_like|it_should_behave_like) (:[\w_?]+)[,\s]/) do + puts $1 unless shared_groups.key?($1) + shared_groups[$1] += 1 + end + end + + shared_groups.each_pair do |group, value| + if value == 0 + puts "Shared describe #{group} seems unused" + elsif value == 1 + puts "Shared describe #{group} seems used only once" if $VERBOSE + end + end +end + +def search(regexp) + each_file do |file| + contents = File.binread(file) + if contents =~ regexp + puts file + contents.each_line do |line| + if line =~ regexp + puts line + end + end + end + end +end + +abort "usage: #{$0} <ruby-version>" if ARGV.empty? + +version = Regexp.escape(ARGV.fetch(0)) +version += "(?:\\.0)?" if version.count(".") < 2 +remove_guards(/ruby_version_is (["'])#{version}\1 do/, true) +remove_guards(/ruby_version_is (["'])[0-9.]*\1 *... *(["'])#{version}\2 do/, false) +remove_guards(/ruby_bug ["']#\d+["'], (["'])[0-9.]*\1 *... *(["'])#{version}\2 do/, true) + +remove_empty_files +remove_unused_shared_specs + +puts "Search:" +search(/(["'])#{version}\1/) +search(/^\s*#.+#{version}/) +search(/RUBY_VERSION_IS_#{version.tr('.', '_')}/) diff --git a/spec/mspec/tool/sync/.gitignore b/spec/mspec/tool/sync/.gitignore new file mode 100644 index 0000000000..e64f1e8542 --- /dev/null +++ b/spec/mspec/tool/sync/.gitignore @@ -0,0 +1,4 @@ +/jruby +/rubinius +/ruby +/truffleruby diff --git a/spec/mspec/tool/sync/sync-rubyspec.rb b/spec/mspec/tool/sync/sync-rubyspec.rb new file mode 100644 index 0000000000..86c43d0dc8 --- /dev/null +++ b/spec/mspec/tool/sync/sync-rubyspec.rb @@ -0,0 +1,254 @@ +# This script is based on commands from the wiki: +# https://github.com/ruby/spec/wiki/Merging-specs-from-JRuby-and-other-sources + +IMPLS = { + truffleruby: { + git: "https://github.com/truffleruby/truffleruby.git", + from_commit: "f10ab6988d", + }, + jruby: { + git: "https://github.com/jruby/jruby.git", + from_commit: "f10ab6988d", + }, + rbx: { + git: "https://github.com/rubinius/rubinius.git", + }, + mri: { + git: "https://github.com/ruby/ruby.git", + }, +} + +MSPEC = ARGV.delete('--mspec') + +CHECK_LAST_MERGE = !MSPEC && ENV['CHECK_LAST_MERGE'] != 'false' +TEST_MASTER = ENV['TEST_MASTER'] != 'false' + +ONLY_FILTER = ENV['ONLY_FILTER'] == 'true' + +MSPEC_REPO = File.expand_path("../../..", __FILE__) +raise MSPEC_REPO if !Dir.exist?(MSPEC_REPO) or !Dir.exist?("#{MSPEC_REPO}/.git") + +# Assuming the rubyspec repo is a sibling of the mspec repo +RUBYSPEC_REPO = File.expand_path("../rubyspec", MSPEC_REPO) +raise RUBYSPEC_REPO unless Dir.exist?(RUBYSPEC_REPO) + +SOURCE_REPO = MSPEC ? MSPEC_REPO : RUBYSPEC_REPO + +# LAST_MERGE is a commit of ruby/spec or ruby/mspec +# which is the spec/mspec commit that was last imported in the Ruby implementation +# (i.e. the commit in "Update to ruby/spec@commit"). +# It is normally automatically computed, but can be manually set when +# e.g. the last update of specs wasn't merged in the Ruby implementation. +LAST_MERGE = ENV["LAST_MERGE"] + +NOW = Time.now + +BRIGHT_RED = "\e[31;1m" +BRIGHT_YELLOW = "\e[33;1m" +RESET = "\e[0m" + +# git filter-branch --subdirectory-filter works fine for our use case +ENV['FILTER_BRANCH_SQUELCH_WARNING'] = '1' + +class RubyImplementation + attr_reader :name + + def initialize(name, data) + @name = name.to_s + @data = data + end + + def git_url + @data[:git] + end + + def repo_name + File.basename(git_url, ".git") + end + + def repo_path + "#{__dir__}/#{repo_name}" + end + + def repo_org + File.basename(File.dirname(git_url)) + end + + def from_commit + from = @data[:from_commit] + "#{from}..." if from + end + + def last_merge_message + message = @data[:merge_message] || "Update to ruby/spec@" + message.gsub!("ruby/spec", "ruby/mspec") if MSPEC + message + end + + def prefix + MSPEC ? "spec/mspec" : "spec/ruby" + end + + def rebased_branch + "#{@name}-rebased" + end +end + +def sh(*args) + puts args.join(' ') + system(*args) + raise unless $?.success? +end + +def branch?(name) + branches = `git branch`.sub('*', '').lines.map(&:strip) + branches.include?(name) +end + +def update_repo(impl) + unless File.directory? impl.repo_name + sh "git", "clone", impl.git_url + end + + Dir.chdir(impl.repo_name) do + puts Dir.pwd + + sh "git", "checkout", "master" + sh "git", "pull" + end +end + +def filter_commits(impl) + Dir.chdir(impl.repo_name) do + date = NOW.strftime("%F") + branch = "#{MSPEC ? :mspec : :specs}-#{date}" + + unless branch?(branch) + sh "git", "checkout", "-b", branch + sh "git", "filter-branch", "-f", "--subdirectory-filter", impl.prefix, *impl.from_commit + sh "git", "push", "-f", SOURCE_REPO, "#{branch}:#{impl.name}" + end + end +end + +def rebase_commits(impl) + Dir.chdir(SOURCE_REPO) do + sh "git", "checkout", "master" + sh "git", "pull" + + rebased = impl.rebased_branch + if branch?(rebased) + last_commit = Time.at(Integer(`git log -n 1 --format='%ct' #{rebased}`)) + days_since_last_commit = (NOW-last_commit) / 86400 + if days_since_last_commit > 7 + abort "#{BRIGHT_RED}#{rebased} exists but last commit is old (#{last_commit}), delete the branch if it was merged#{RESET}" + else + puts "#{BRIGHT_YELLOW}#{rebased} already exists, last commit on #{last_commit}, assuming it correct#{RESET}" + sh "git", "checkout", rebased + end + else + sh "git", "checkout", impl.name + + if LAST_MERGE + last_merge = `git log -n 1 --format='%H %ct' #{LAST_MERGE}` + else + last_merge = `git log --grep='^#{impl.last_merge_message}' -n 1 --format='%H %ct'` + end + last_merge, commit_timestamp = last_merge.split(' ') + + raise "Could not find last merge" unless last_merge + puts "Last merge is #{last_merge}" + + commit_date = Time.at(Integer(commit_timestamp)) + days_since_last_merge = (NOW-commit_date) / 86400 + if CHECK_LAST_MERGE and days_since_last_merge > 60 + raise "#{days_since_last_merge.floor} days since last merge, probably wrong commit" + end + + puts "Checking if the last merge is consistent with upstream files" + rubyspec_commit = `git log -n 1 --format='%s' #{last_merge}`.chomp.split('@', 2)[-1] + sh "git", "checkout", last_merge + sh "git", "diff", "--exit-code", rubyspec_commit, "--", ":!.github" + + puts "Rebasing..." + sh "git", "branch", "-D", rebased if branch?(rebased) + sh "git", "checkout", "-b", rebased, impl.name + sh "git", "rebase", "--onto", "master", last_merge + end + end +end + +def new_commits?(impl) + Dir.chdir(SOURCE_REPO) do + diff = `git diff master #{impl.rebased_branch}` + !diff.empty? + end +end + +def test_new_specs + require "yaml" + Dir.chdir(SOURCE_REPO) do + workflow = YAML.load_file(".github/workflows/ci.yml") + job_name = MSPEC ? "test" : "specs" + versions = workflow.dig("jobs", job_name, "strategy", "matrix", "ruby").map(&:to_s) + versions = versions.grep(/^\d+\./) # Test on MRI + min_version, max_version = versions.minmax + + test_command = MSPEC ? "bundle install && bundle exec rspec" : "../mspec/bin/mspec -j" + + run_test = -> version { + command = "chruby ruby-#{version} && #{test_command}" + sh ENV["SHELL"], "-c", command + } + + run_test[min_version] + run_test[max_version] + run_test["master"] if TEST_MASTER + end +end + +def fast_forward_master(impl) + Dir.chdir(SOURCE_REPO) do + sh "git", "checkout", "master" + sh "git", "merge", "--ff-only", impl.rebased_branch + sh "git", "branch", "--delete", impl.rebased_branch + end +end + +def check_ci + puts + puts <<-EOS + Push to master, and check that the CI passes: + https://github.com/ruby/#{:m if MSPEC}spec/commits/master + + EOS +end + +def main(impls) + impls.each_pair do |impl, data| + impl = RubyImplementation.new(impl, data) + update_repo(impl) + filter_commits(impl) + unless ONLY_FILTER + rebase_commits(impl) + if new_commits?(impl) + test_new_specs + fast_forward_master(impl) + check_ci + else + STDERR.puts "#{BRIGHT_YELLOW}No new commits#{RESET}" + fast_forward_master(impl) + end + end + end +end + +if ARGV == ["all"] + impls = IMPLS +else + args = ARGV.map { |arg| arg.to_sym } + raise ARGV.to_s unless (args - IMPLS.keys).empty? + impls = IMPLS.select { |impl| args.include?(impl) } +end + +main(impls) diff --git a/spec/mspec/tool/tag_from_output.rb b/spec/mspec/tool/tag_from_output.rb new file mode 100755 index 0000000000..41aa70f932 --- /dev/null +++ b/spec/mspec/tool/tag_from_output.rb @@ -0,0 +1,65 @@ +#!/usr/bin/env ruby + +# Adds tags based on error and failures output (e.g., from a CI log), +# without running any spec code. + +tag = ENV["TAG"] || "fails" + +tags_dir = %w[ + spec/tags + spec/tags/ruby +].find { |dir| Dir.exist?("#{dir}/language") } +abort 'Could not find tags directory' unless tags_dir + +output = ARGF.readlines + +# Automatically strip datetime of GitHub Actions +if output.first =~ /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d+Z / + output = output.map { |line| line.split(' ', 2).last } +end + +NUMBER = /^\d+\)$/ +ERROR_OR_FAILED = / (ERROR|FAILED)$/ +SPEC_FILE = /^((?:\/|[CD]:\/).+_spec\.rb)\:\d+/ + +output.slice_before(NUMBER).select { |number, *rest| + number =~ NUMBER and rest.any? { |line| line =~ ERROR_OR_FAILED } +}.each { |number, *rest| + error_line = rest.find { |line| line =~ ERROR_OR_FAILED } + description = error_line.match(ERROR_OR_FAILED).pre_match + + spec_file = rest.find { |line| line =~ SPEC_FILE } + if spec_file + spec_file = spec_file[SPEC_FILE, 1] or raise + else + if error_line =~ /^([\w:]+)[#\.](\w+) / + mod, method = $1, $2 + file = "#{mod.downcase.gsub('::', '/')}/#{method}_spec.rb" + spec_file = ['spec/ruby/core', 'spec/ruby/library', *Dir.glob('spec/ruby/library/*')].find { |dir| + path = "#{dir}/#{file}" + break path if File.exist?(path) + } + end + + unless spec_file + warn "Could not find file for:\n#{error_line}" + next + end + end + + prefix = spec_file.index('spec/ruby/') || spec_file.index('spec/truffle/') + spec_file = spec_file[prefix..-1] + + tags_file = spec_file.sub('spec/ruby/', "#{tags_dir}/").sub('spec/truffle/', "#{tags_dir}/truffle/") + tags_file = tags_file.sub(/_spec\.rb$/, '_tags.txt') + + dir = File.dirname(tags_file) + Dir.mkdir(dir) unless Dir.exist?(dir) + + tag_line = "#{tag}:#{description}" + lines = File.exist?(tags_file) ? File.readlines(tags_file, chomp: true) : [] + unless lines.include?(tag_line) + puts tags_file + File.write(tags_file, (lines + [tag_line]).join("\n") + "\n") + end +} diff --git a/spec/mspec/tool/wrap_with_guard.rb b/spec/mspec/tool/wrap_with_guard.rb new file mode 100755 index 0000000000..5b1bf4d7f7 --- /dev/null +++ b/spec/mspec/tool/wrap_with_guard.rb @@ -0,0 +1,28 @@ +#!/usr/bin/env ruby +# Wrap the passed the files with a guard (e.g., `ruby_version_is ""..."3.0"`). +# Notably if some methods are removed, this is a convenient way to skip such file from a given version. +# Example usage: +# $ spec/mspec/tool/wrap_with_guard.rb 'ruby_version_is ""..."3.0"' spec/ruby/library/set/sortedset/**/*_spec.rb + +guard, *files = ARGV +abort "Usage: #{$0} GUARD FILES..." if files.empty? + +files.each do |file| + contents = File.binread(file) + lines = contents.lines.to_a + + lines = lines.map { |line| line.chomp.empty? ? line : " #{line}" } + + version_line = "#{guard} do\n" + if lines[0] =~ /^\s*require.+spec_helper/ + lines[0] = lines[0].sub(/^ /, '') + lines.insert 1, "\n", version_line + else + warn "Could not find 'require spec_helper' line in #{file}" + lines.insert 0, version_line + end + + lines << "end\n" + + File.binwrite file, lines.join +end |
