├── .travis.yml ├── .gitignore ├── bin ├── setup └── console ├── Gemfile ├── Rakefile ├── test ├── lib │ ├── minitest │ │ ├── autorun.rb │ │ ├── mock.rb │ │ └── unit.rb │ ├── find_executable.rb │ ├── test │ │ ├── unit │ │ │ ├── testcase.rb │ │ │ └── assertions.rb │ │ └── unit.rb │ ├── leakchecker.rb │ └── envutil.rb └── matrix │ ├── test_vector.rb │ └── test_matrix.rb ├── CHANGELOG.md ├── matrix.gemspec ├── LICENSE.txt ├── README-DEFAULT-GEM.md ├── README.md └── lib └── matrix ├── lup_decomposition.rb └── eigenvalue_decomposition.rb /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: ruby 3 | rvm: 4 | - ruby-head 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } 4 | 5 | # Specify your gem's dependencies in matrix.gemspec 6 | gemspec 7 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rake/testtask" 3 | 4 | Rake::TestTask.new(:test) do |t| 5 | t.libs << "test" << "test/lib" 6 | t.libs << "lib" 7 | t.test_files = FileList['test/**/test_*.rb'] 8 | end 9 | 10 | task :default => [:test] 11 | -------------------------------------------------------------------------------- /test/lib/minitest/autorun.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # frozen_string_literal: false 3 | 4 | begin 5 | require 'rubygems' 6 | gem 'minitest' 7 | rescue Gem::LoadError 8 | # do nothing 9 | end 10 | 11 | require 'minitest/unit' 12 | require 'minitest/mock' 13 | 14 | MiniTest::Unit.autorun 15 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | List of new features: 4 | 5 | ## v1.0.0 / Ruby 2.6 6 | 7 | * Matrix#antisymmetric? / #skew_symmetric? 8 | 9 | * Matrix#map! / #collect! 10 | 11 | * Matrix#[]= 12 | 13 | * Vector#map! / #collect! 14 | 15 | * Vector#[]= 16 | 17 | ## Ruby 2.5 18 | 19 | * Matrix.combine and Matrix#combine 20 | * Matrix#hadamard_product and Matrix#entrywise_product 21 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "matrix" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start(__FILE__) 15 | -------------------------------------------------------------------------------- /test/lib/find_executable.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: false 2 | require "rbconfig" 3 | 4 | module EnvUtil 5 | def find_executable(cmd, *args) 6 | exts = RbConfig::CONFIG["EXECUTABLE_EXTS"].split | [RbConfig::CONFIG["EXEEXT"]] 7 | ENV["PATH"].split(File::PATH_SEPARATOR).each do |path| 8 | next if path.empty? 9 | path = File.join(path, cmd) 10 | exts.each do |ext| 11 | cmdline = [path + ext, *args] 12 | begin 13 | return cmdline if yield(IO.popen(cmdline, "r", err: [:child, :out], &:read)) 14 | rescue 15 | next 16 | end 17 | end 18 | end 19 | nil 20 | end 21 | module_function :find_executable 22 | end 23 | -------------------------------------------------------------------------------- /matrix.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Gem::Specification.new do |spec| 4 | spec.name = "matrix" 5 | spec.version = "0.1.0" 6 | spec.authors = ["Marc-Andre Lafortune"] 7 | spec.email = ["ruby-core@marc-andre.ca"] 8 | 9 | spec.summary = %q{An implementation of Matrix and Vector classes.} 10 | spec.description = %q{An implementation of Matrix and Vector classes.} 11 | spec.homepage = "https://github.com/ruby/matrix" 12 | spec.license = "BSD-2-Clause" 13 | 14 | spec.files = [".gitignore", ".travis.yml", "Gemfile", "LICENSE.txt", "README.md", "Rakefile", "bin/console", "bin/setup", "lib/matrix.rb", "matrix.gemspec"] 15 | spec.bindir = "exe" 16 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 17 | spec.require_paths = ["lib"] 18 | 19 | spec.add_development_dependency "bundler" 20 | spec.add_development_dependency "rake" 21 | end 22 | -------------------------------------------------------------------------------- /test/lib/test/unit/testcase.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: false 2 | require 'test/unit/assertions' 3 | 4 | module Test 5 | module Unit 6 | # remove silly TestCase class 7 | remove_const(:TestCase) if defined?(self::TestCase) 8 | 9 | class TestCase < MiniTest::Unit::TestCase # :nodoc: all 10 | include Assertions 11 | 12 | def on_parallel_worker? 13 | false 14 | end 15 | 16 | def run runner 17 | @options = runner.options 18 | super runner 19 | end 20 | 21 | def self.test_order 22 | :sorted 23 | end 24 | 25 | def self.method_added(name) 26 | super 27 | return unless name.to_s.start_with?("test_") 28 | @test_methods ||= {} 29 | if @test_methods[name] 30 | warn "test/unit warning: method #{ self }##{ name } is redefined" 31 | end 32 | @test_methods[name] = true 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (C) 1993-2013 Yukihiro Matsumoto. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions 5 | are met: 6 | 1. Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | 2. Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 14 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 15 | ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 16 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 17 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 18 | OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 19 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 20 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 21 | OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 22 | SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /README-DEFAULT-GEM.md: -------------------------------------------------------------------------------- 1 | # Ruby Default Gem Info 2 | 3 | The **matrix** library is a default gem, which means it comes pre-installed with Ruby. Unless you need recent features, you can simply `require "matrix"` directly, no need to install it. 4 | 5 | ## Installation from rubygems.org 6 | 7 | Ruby ships with a locked version of this gem. You can find out which version of **matrix** is included in your version of Ruby on [stdgems.org/matrix](https://stdgems.org/matrix). To get all bug fixes and the latest features you have to install this library as a gem. This can be done by adding the following line to your application's Gemfile or gem's gemspec: 8 | 9 | ```ruby 10 | gem "matrix" 11 | ``` 12 | 13 | And then execute: 14 | 15 | $ bundle 16 | 17 | To make sure the gem takes over the builtin library, run `bundle exec ...` (or call `gem 'matrix' in your code explicitly). 18 | 19 | ## Usage 20 | 21 | See the [README](README.md) file for instructions how to use this gem. 22 | 23 | ## Contributing 24 | 25 | Bug reports and pull requests are welcome on GitHub at https://github.com/ruby/matrix. 26 | 27 | ## Development 28 | 29 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 30 | 31 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Matrix [![Version](https://badge.fury.io/rb/matrix.svg)](https://badge.fury.io/rb/matrix) [![Default Gem](https://img.shields.io/badge/stdgem-default-9c1260.svg)](https://stdgems.org/matrix/) [![Travis](https://travis-ci.com/ruby/matrix.svg)](https://travis-ci.com/ruby/matrix) 2 | 3 | An implementation of `Matrix` and `Vector` classes. 4 | 5 | The `Matrix` class represents a mathematical matrix. It provides methods for creating matrices, operating on them arithmetically and algebraically, and determining their mathematical properties (trace, rank, inverse, determinant, eigensystem, etc.). 6 | 7 | The `Vector` class represents a mathematical vector, which is useful in its own right, and also constitutes a row or column of a `Matrix`. 8 | 9 | ## Installation 10 | 11 | The `matrix` library comes pre-installed with Ruby. Unless you need recent features, you can simply `require 'matrix'` directly, no need to install it. 12 | 13 | If you need features that are more recent than the version of Ruby you want to support (check the [CHANGELOG](CHANGELOG.md)), you must use the gem. To do this, add this line to your application's Gemfile or gem's gemspec: 14 | 15 | ```ruby 16 | gem 'matrix' 17 | ``` 18 | 19 | And then execute: 20 | 21 | $ bundle 22 | 23 | To make sure the gem takes over the builtin library, be to call `bundle exec ...` (or to call `gem 'matrix' explicitly). 24 | 25 | ## Usage 26 | 27 | ```ruby 28 | require 'matrix' 29 | m = Matrix[[1, 2], [3, 4]] 30 | m.determinant # => -2 31 | ``` 32 | 33 | ## Development 34 | 35 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 36 | 37 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 38 | 39 | ## Contributing 40 | 41 | Bug reports and pull requests are welcome on GitHub at https://github.com/ruby/matrix. 42 | 43 | ## License 44 | 45 | The gem is available as open source under the terms of the [2-Clause BSD License](https://opensource.org/licenses/BSD-2-Clause). 46 | -------------------------------------------------------------------------------- /test/lib/leakchecker.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: false 2 | class LeakChecker 3 | def initialize 4 | @fd_info = find_fds 5 | @tempfile_info = find_tempfiles 6 | @thread_info = find_threads 7 | @env_info = find_env 8 | end 9 | 10 | def check(test_name) 11 | leaks = [ 12 | check_fd_leak(test_name), 13 | check_thread_leak(test_name), 14 | check_tempfile_leak(test_name), 15 | check_env(test_name) 16 | ] 17 | GC.start if leaks.any? 18 | end 19 | 20 | def find_fds 21 | if IO.respond_to?(:console) and (m = IO.method(:console)).arity.nonzero? 22 | m[:close] 23 | end 24 | fd_dir = "/proc/self/fd" 25 | if File.directory?(fd_dir) 26 | fds = Dir.open(fd_dir) {|d| 27 | a = d.grep(/\A\d+\z/, &:to_i) 28 | if d.respond_to? :fileno 29 | a -= [d.fileno] 30 | end 31 | a 32 | } 33 | fds.sort 34 | else 35 | [] 36 | end 37 | end 38 | 39 | def check_fd_leak(test_name) 40 | leaked = false 41 | live1 = @fd_info 42 | live2 = find_fds 43 | fd_closed = live1 - live2 44 | if !fd_closed.empty? 45 | fd_closed.each {|fd| 46 | puts "Closed file descriptor: #{test_name}: #{fd}" 47 | } 48 | end 49 | fd_leaked = live2 - live1 50 | if !fd_leaked.empty? 51 | leaked = true 52 | h = {} 53 | ObjectSpace.each_object(IO) {|io| 54 | inspect = io.inspect 55 | begin 56 | autoclose = io.autoclose? 57 | fd = io.fileno 58 | rescue IOError # closed IO object 59 | next 60 | end 61 | (h[fd] ||= []) << [io, autoclose, inspect] 62 | } 63 | fd_leaked.each {|fd| 64 | str = '' 65 | if h[fd] 66 | str << ' :' 67 | h[fd].map {|io, autoclose, inspect| 68 | s = ' ' + inspect 69 | s << "(not-autoclose)" if !autoclose 70 | s 71 | }.sort.each {|s| 72 | str << s 73 | } 74 | end 75 | puts "Leaked file descriptor: #{test_name}: #{fd}#{str}" 76 | } 77 | #system("lsof -p #$$") if !fd_leaked.empty? 78 | h.each {|fd, list| 79 | next if list.length <= 1 80 | if 1 < list.count {|io, autoclose, inspect| autoclose } 81 | str = list.map {|io, autoclose, inspect| " #{inspect}" + (autoclose ? "(autoclose)" : "") }.sort.join 82 | puts "Multiple autoclose IO object for a file descriptor:#{str}" 83 | end 84 | } 85 | end 86 | @fd_info = live2 87 | return leaked 88 | end 89 | 90 | def extend_tempfile_counter 91 | return if defined? LeakChecker::TempfileCounter 92 | m = Module.new { 93 | @count = 0 94 | class << self 95 | attr_accessor :count 96 | end 97 | 98 | def new(data) 99 | LeakChecker::TempfileCounter.count += 1 100 | super(data) 101 | end 102 | } 103 | LeakChecker.const_set(:TempfileCounter, m) 104 | 105 | class << Tempfile::Remover 106 | prepend LeakChecker::TempfileCounter 107 | end 108 | end 109 | 110 | def find_tempfiles(prev_count=-1) 111 | return [prev_count, []] unless defined? Tempfile 112 | extend_tempfile_counter 113 | count = TempfileCounter.count 114 | if prev_count == count 115 | [prev_count, []] 116 | else 117 | tempfiles = ObjectSpace.each_object(Tempfile).find_all {|t| 118 | t.instance_variable_defined?(:@tmpfile) and t.path 119 | } 120 | [count, tempfiles] 121 | end 122 | end 123 | 124 | def check_tempfile_leak(test_name) 125 | return false unless defined? Tempfile 126 | count1, initial_tempfiles = @tempfile_info 127 | count2, current_tempfiles = find_tempfiles(count1) 128 | leaked = false 129 | tempfiles_leaked = current_tempfiles - initial_tempfiles 130 | if !tempfiles_leaked.empty? 131 | leaked = true 132 | list = tempfiles_leaked.map {|t| t.inspect }.sort 133 | list.each {|str| 134 | puts "Leaked tempfile: #{test_name}: #{str}" 135 | } 136 | tempfiles_leaked.each {|t| t.close! } 137 | end 138 | @tempfile_info = [count2, initial_tempfiles] 139 | return leaked 140 | end 141 | 142 | def find_threads 143 | Thread.list.find_all {|t| 144 | t != Thread.current && t.alive? 145 | } 146 | end 147 | 148 | def check_thread_leak(test_name) 149 | live1 = @thread_info 150 | live2 = find_threads 151 | thread_finished = live1 - live2 152 | leaked = false 153 | if !thread_finished.empty? 154 | list = thread_finished.map {|t| t.inspect }.sort 155 | list.each {|str| 156 | puts "Finished thread: #{test_name}: #{str}" 157 | } 158 | end 159 | thread_leaked = live2 - live1 160 | if !thread_leaked.empty? 161 | leaked = true 162 | list = thread_leaked.map {|t| t.inspect }.sort 163 | list.each {|str| 164 | puts "Leaked thread: #{test_name}: #{str}" 165 | } 166 | end 167 | @thread_info = live2 168 | return leaked 169 | end 170 | 171 | def find_env 172 | ENV.to_h 173 | end 174 | 175 | def check_env(test_name) 176 | old_env = @env_info 177 | new_env = ENV.to_h 178 | return false if old_env == new_env 179 | (old_env.keys | new_env.keys).sort.each {|k| 180 | if old_env.has_key?(k) 181 | if new_env.has_key?(k) 182 | if old_env[k] != new_env[k] 183 | puts "Environment variable changed: #{test_name} : #{k.inspect} changed : #{old_env[k].inspect} -> #{new_env[k].inspect}" 184 | end 185 | else 186 | puts "Environment variable changed: #{test_name} : #{k.inspect} deleted" 187 | end 188 | else 189 | if new_env.has_key?(k) 190 | puts "Environment variable changed: #{test_name} : #{k.inspect} added" 191 | else 192 | flunk "unreachable" 193 | end 194 | end 195 | } 196 | @env_info = new_env 197 | return true 198 | end 199 | 200 | def puts(*a) 201 | MiniTest::Unit.output.puts(*a) 202 | end 203 | end 204 | -------------------------------------------------------------------------------- /lib/matrix/lup_decomposition.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: false 2 | class Matrix 3 | # Adapted from JAMA: http://math.nist.gov/javanumerics/jama/ 4 | 5 | # 6 | # For an m-by-n matrix A with m >= n, the LU decomposition is an m-by-n 7 | # unit lower triangular matrix L, an n-by-n upper triangular matrix U, 8 | # and a m-by-m permutation matrix P so that L*U = P*A. 9 | # If m < n, then L is m-by-m and U is m-by-n. 10 | # 11 | # The LUP decomposition with pivoting always exists, even if the matrix is 12 | # singular, so the constructor will never fail. The primary use of the 13 | # LU decomposition is in the solution of square systems of simultaneous 14 | # linear equations. This will fail if singular? returns true. 15 | # 16 | 17 | class LUPDecomposition 18 | # Returns the lower triangular factor +L+ 19 | 20 | include Matrix::ConversionHelper 21 | 22 | def l 23 | Matrix.build(@row_count, [@column_count, @row_count].min) do |i, j| 24 | if (i > j) 25 | @lu[i][j] 26 | elsif (i == j) 27 | 1 28 | else 29 | 0 30 | end 31 | end 32 | end 33 | 34 | # Returns the upper triangular factor +U+ 35 | 36 | def u 37 | Matrix.build([@column_count, @row_count].min, @column_count) do |i, j| 38 | if (i <= j) 39 | @lu[i][j] 40 | else 41 | 0 42 | end 43 | end 44 | end 45 | 46 | # Returns the permutation matrix +P+ 47 | 48 | def p 49 | rows = Array.new(@row_count){Array.new(@row_count, 0)} 50 | @pivots.each_with_index{|p, i| rows[i][p] = 1} 51 | Matrix.send :new, rows, @row_count 52 | end 53 | 54 | # Returns +L+, +U+, +P+ in an array 55 | 56 | def to_ary 57 | [l, u, p] 58 | end 59 | alias_method :to_a, :to_ary 60 | 61 | # Returns the pivoting indices 62 | 63 | attr_reader :pivots 64 | 65 | # Returns +true+ if +U+, and hence +A+, is singular. 66 | 67 | def singular? 68 | @column_count.times do |j| 69 | if (@lu[j][j] == 0) 70 | return true 71 | end 72 | end 73 | false 74 | end 75 | 76 | # Returns the determinant of +A+, calculated efficiently 77 | # from the factorization. 78 | 79 | def det 80 | if (@row_count != @column_count) 81 | Matrix.Raise Matrix::ErrDimensionMismatch 82 | end 83 | d = @pivot_sign 84 | @column_count.times do |j| 85 | d *= @lu[j][j] 86 | end 87 | d 88 | end 89 | alias_method :determinant, :det 90 | 91 | # Returns +m+ so that A*m = b, 92 | # or equivalently so that L*U*m = P*b 93 | # +b+ can be a Matrix or a Vector 94 | 95 | def solve b 96 | if (singular?) 97 | Matrix.Raise Matrix::ErrNotRegular, "Matrix is singular." 98 | end 99 | if b.is_a? Matrix 100 | if (b.row_count != @row_count) 101 | Matrix.Raise Matrix::ErrDimensionMismatch 102 | end 103 | 104 | # Copy right hand side with pivoting 105 | nx = b.column_count 106 | m = @pivots.map{|row| b.row(row).to_a} 107 | 108 | # Solve L*Y = P*b 109 | @column_count.times do |k| 110 | (k+1).upto(@column_count-1) do |i| 111 | nx.times do |j| 112 | m[i][j] -= m[k][j]*@lu[i][k] 113 | end 114 | end 115 | end 116 | # Solve U*m = Y 117 | (@column_count-1).downto(0) do |k| 118 | nx.times do |j| 119 | m[k][j] = m[k][j].quo(@lu[k][k]) 120 | end 121 | k.times do |i| 122 | nx.times do |j| 123 | m[i][j] -= m[k][j]*@lu[i][k] 124 | end 125 | end 126 | end 127 | Matrix.send :new, m, nx 128 | else # same algorithm, specialized for simpler case of a vector 129 | b = convert_to_array(b) 130 | if (b.size != @row_count) 131 | Matrix.Raise Matrix::ErrDimensionMismatch 132 | end 133 | 134 | # Copy right hand side with pivoting 135 | m = b.values_at(*@pivots) 136 | 137 | # Solve L*Y = P*b 138 | @column_count.times do |k| 139 | (k+1).upto(@column_count-1) do |i| 140 | m[i] -= m[k]*@lu[i][k] 141 | end 142 | end 143 | # Solve U*m = Y 144 | (@column_count-1).downto(0) do |k| 145 | m[k] = m[k].quo(@lu[k][k]) 146 | k.times do |i| 147 | m[i] -= m[k]*@lu[i][k] 148 | end 149 | end 150 | Vector.elements(m, false) 151 | end 152 | end 153 | 154 | def initialize a 155 | raise TypeError, "Expected Matrix but got #{a.class}" unless a.is_a?(Matrix) 156 | # Use a "left-looking", dot-product, Crout/Doolittle algorithm. 157 | @lu = a.to_a 158 | @row_count = a.row_count 159 | @column_count = a.column_count 160 | @pivots = Array.new(@row_count) 161 | @row_count.times do |i| 162 | @pivots[i] = i 163 | end 164 | @pivot_sign = 1 165 | lu_col_j = Array.new(@row_count) 166 | 167 | # Outer loop. 168 | 169 | @column_count.times do |j| 170 | 171 | # Make a copy of the j-th column to localize references. 172 | 173 | @row_count.times do |i| 174 | lu_col_j[i] = @lu[i][j] 175 | end 176 | 177 | # Apply previous transformations. 178 | 179 | @row_count.times do |i| 180 | lu_row_i = @lu[i] 181 | 182 | # Most of the time is spent in the following dot product. 183 | 184 | kmax = [i, j].min 185 | s = 0 186 | kmax.times do |k| 187 | s += lu_row_i[k]*lu_col_j[k] 188 | end 189 | 190 | lu_row_i[j] = lu_col_j[i] -= s 191 | end 192 | 193 | # Find pivot and exchange if necessary. 194 | 195 | p = j 196 | (j+1).upto(@row_count-1) do |i| 197 | if (lu_col_j[i].abs > lu_col_j[p].abs) 198 | p = i 199 | end 200 | end 201 | if (p != j) 202 | @column_count.times do |k| 203 | t = @lu[p][k]; @lu[p][k] = @lu[j][k]; @lu[j][k] = t 204 | end 205 | k = @pivots[p]; @pivots[p] = @pivots[j]; @pivots[j] = k 206 | @pivot_sign = -@pivot_sign 207 | end 208 | 209 | # Compute multipliers. 210 | 211 | if (j < @row_count && @lu[j][j] != 0) 212 | (j+1).upto(@row_count-1) do |i| 213 | @lu[i][j] = @lu[i][j].quo(@lu[j][j]) 214 | end 215 | end 216 | end 217 | end 218 | end 219 | end 220 | -------------------------------------------------------------------------------- /test/lib/minitest/mock.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # frozen_string_literal: false 3 | 4 | class MockExpectationError < StandardError; end # :nodoc: 5 | 6 | ## 7 | # A simple and clean mock object framework. 8 | 9 | module MiniTest # :nodoc: 10 | 11 | ## 12 | # All mock objects are an instance of Mock 13 | 14 | class Mock 15 | alias :__respond_to? :respond_to? 16 | 17 | skip_methods = %w(object_id respond_to_missing? inspect === to_s) 18 | 19 | instance_methods.each do |m| 20 | undef_method m unless skip_methods.include?(m.to_s) || m =~ /^__/ 21 | end 22 | 23 | def initialize # :nodoc: 24 | @expected_calls = Hash.new { |calls, name| calls[name] = [] } 25 | @actual_calls = Hash.new { |calls, name| calls[name] = [] } 26 | end 27 | 28 | ## 29 | # Expect that method +name+ is called, optionally with +args+ or a 30 | # +blk+, and returns +retval+. 31 | # 32 | # @mock.expect(:meaning_of_life, 42) 33 | # @mock.meaning_of_life # => 42 34 | # 35 | # @mock.expect(:do_something_with, true, [some_obj, true]) 36 | # @mock.do_something_with(some_obj, true) # => true 37 | # 38 | # @mock.expect(:do_something_else, true) do |a1, a2| 39 | # a1 == "buggs" && a2 == :bunny 40 | # end 41 | # 42 | # +args+ is compared to the expected args using case equality (ie, the 43 | # '===' operator), allowing for less specific expectations. 44 | # 45 | # @mock.expect(:uses_any_string, true, [String]) 46 | # @mock.uses_any_string("foo") # => true 47 | # @mock.verify # => true 48 | # 49 | # @mock.expect(:uses_one_string, true, ["foo"] 50 | # @mock.uses_one_string("bar") # => true 51 | # @mock.verify # => raises MockExpectationError 52 | 53 | def expect(name, retval, args=[], &blk) 54 | if block_given? 55 | raise ArgumentError, "args ignored when block given" unless args.empty? 56 | @expected_calls[name] << { :retval => retval, :block => blk } 57 | else 58 | raise ArgumentError, "args must be an array" unless Array === args 59 | @expected_calls[name] << { :retval => retval, :args => args } 60 | end 61 | self 62 | end 63 | 64 | def __call name, data # :nodoc: 65 | case data 66 | when Hash then 67 | "#{name}(#{data[:args].inspect[1..-2]}) => #{data[:retval].inspect}" 68 | else 69 | data.map { |d| __call name, d }.join ", " 70 | end 71 | end 72 | 73 | ## 74 | # Verify that all methods were called as expected. Raises 75 | # +MockExpectationError+ if the mock object was not called as 76 | # expected. 77 | 78 | def verify 79 | @expected_calls.each do |name, calls| 80 | calls.each do |expected| 81 | msg1 = "expected #{__call name, expected}" 82 | msg2 = "#{msg1}, got [#{__call name, @actual_calls[name]}]" 83 | 84 | raise MockExpectationError, msg2 if 85 | @actual_calls.has_key?(name) and 86 | not @actual_calls[name].include?(expected) 87 | 88 | raise MockExpectationError, msg1 unless 89 | @actual_calls.has_key?(name) and 90 | @actual_calls[name].include?(expected) 91 | end 92 | end 93 | true 94 | end 95 | 96 | def method_missing(sym, *args) # :nodoc: 97 | unless @expected_calls.has_key?(sym) then 98 | raise NoMethodError, "unmocked method %p, expected one of %p" % 99 | [sym, @expected_calls.keys.sort_by(&:to_s)] 100 | end 101 | 102 | index = @actual_calls[sym].length 103 | expected_call = @expected_calls[sym][index] 104 | 105 | unless expected_call then 106 | raise MockExpectationError, "No more expects available for %p: %p" % 107 | [sym, args] 108 | end 109 | 110 | expected_args, retval, val_block = 111 | expected_call.values_at(:args, :retval, :block) 112 | 113 | if val_block then 114 | raise MockExpectationError, "mocked method %p failed block w/ %p" % 115 | [sym, args] unless val_block.call(args) 116 | 117 | # keep "verify" happy 118 | @actual_calls[sym] << expected_call 119 | return retval 120 | end 121 | 122 | if expected_args.size != args.size then 123 | raise ArgumentError, "mocked method %p expects %d arguments, got %d" % 124 | [sym, expected_args.size, args.size] 125 | end 126 | 127 | fully_matched = expected_args.zip(args).all? { |mod, a| 128 | mod === a or mod == a 129 | } 130 | 131 | unless fully_matched then 132 | raise MockExpectationError, "mocked method %p called with unexpected arguments %p" % 133 | [sym, args] 134 | end 135 | 136 | @actual_calls[sym] << { 137 | :retval => retval, 138 | :args => expected_args.zip(args).map { |mod, a| mod === a ? mod : a } 139 | } 140 | 141 | retval 142 | end 143 | 144 | def respond_to?(sym, include_private = false) # :nodoc: 145 | return true if @expected_calls.has_key?(sym.to_sym) 146 | return __respond_to?(sym, include_private) 147 | end 148 | end 149 | end 150 | 151 | class Object # :nodoc: 152 | 153 | ## 154 | # Add a temporary stubbed method replacing +name+ for the duration 155 | # of the +block+. If +val_or_callable+ responds to #call, then it 156 | # returns the result of calling it, otherwise returns the value 157 | # as-is. Cleans up the stub at the end of the +block+. The method 158 | # +name+ must exist before stubbing. 159 | # 160 | # def test_stale_eh 161 | # obj_under_test = Something.new 162 | # refute obj_under_test.stale? 163 | # 164 | # Time.stub :now, Time.at(0) do 165 | # assert obj_under_test.stale? 166 | # end 167 | # end 168 | 169 | def stub name, val_or_callable, &block 170 | new_name = "__minitest_stub__#{name}" 171 | 172 | metaclass = class << self; self; end 173 | 174 | if respond_to? name and not methods.map(&:to_s).include? name.to_s then 175 | metaclass.send :define_method, name do |*args| 176 | super(*args) 177 | end 178 | end 179 | 180 | metaclass.send :alias_method, new_name, name 181 | 182 | metaclass.send :define_method, name do |*args| 183 | if val_or_callable.respond_to? :call then 184 | val_or_callable.call(*args) 185 | else 186 | val_or_callable 187 | end 188 | end 189 | 190 | yield self 191 | ensure 192 | metaclass.send :undef_method, name 193 | metaclass.send :alias_method, name, new_name 194 | metaclass.send :undef_method, new_name 195 | end 196 | end 197 | -------------------------------------------------------------------------------- /test/lib/envutil.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: us-ascii -*- 2 | # frozen_string_literal: false 3 | require "open3" 4 | require "timeout" 5 | require_relative "find_executable" 6 | begin 7 | require 'rbconfig' 8 | rescue LoadError 9 | end 10 | begin 11 | require "rbconfig/sizeof" 12 | rescue LoadError 13 | end 14 | 15 | module EnvUtil 16 | def rubybin 17 | if ruby = ENV["RUBY"] 18 | return ruby 19 | end 20 | ruby = "ruby" 21 | exeext = RbConfig::CONFIG["EXEEXT"] 22 | rubyexe = (ruby + exeext if exeext and !exeext.empty?) 23 | 3.times do 24 | if File.exist? ruby and File.executable? ruby and !File.directory? ruby 25 | return File.expand_path(ruby) 26 | end 27 | if rubyexe and File.exist? rubyexe and File.executable? rubyexe 28 | return File.expand_path(rubyexe) 29 | end 30 | ruby = File.join("..", ruby) 31 | end 32 | if defined?(RbConfig.ruby) 33 | RbConfig.ruby 34 | else 35 | "ruby" 36 | end 37 | end 38 | module_function :rubybin 39 | 40 | LANG_ENVS = %w"LANG LC_ALL LC_CTYPE" 41 | 42 | DEFAULT_SIGNALS = Signal.list 43 | DEFAULT_SIGNALS.delete("TERM") if /mswin|mingw/ =~ RUBY_PLATFORM 44 | 45 | class << self 46 | attr_accessor :subprocess_timeout_scale 47 | end 48 | 49 | def invoke_ruby(args, stdin_data = "", capture_stdout = false, capture_stderr = false, 50 | encoding: nil, timeout: 10, reprieve: 1, timeout_error: Timeout::Error, 51 | stdout_filter: nil, stderr_filter: nil, 52 | signal: :TERM, 53 | rubybin: EnvUtil.rubybin, 54 | **opt) 55 | if scale = EnvUtil.subprocess_timeout_scale 56 | timeout *= scale if timeout 57 | reprieve *= scale if reprieve 58 | end 59 | in_c, in_p = IO.pipe 60 | out_p, out_c = IO.pipe if capture_stdout 61 | err_p, err_c = IO.pipe if capture_stderr && capture_stderr != :merge_to_stdout 62 | opt[:in] = in_c 63 | opt[:out] = out_c if capture_stdout 64 | opt[:err] = capture_stderr == :merge_to_stdout ? out_c : err_c if capture_stderr 65 | if encoding 66 | out_p.set_encoding(encoding) if out_p 67 | err_p.set_encoding(encoding) if err_p 68 | end 69 | c = "C" 70 | child_env = {} 71 | LANG_ENVS.each {|lc| child_env[lc] = c} 72 | if Array === args and Hash === args.first 73 | child_env.update(args.shift) 74 | end 75 | args = [args] if args.kind_of?(String) 76 | pid = spawn(child_env, rubybin, *args, **opt) 77 | in_c.close 78 | out_c.close if capture_stdout 79 | err_c.close if capture_stderr && capture_stderr != :merge_to_stdout 80 | if block_given? 81 | return yield in_p, out_p, err_p, pid 82 | else 83 | th_stdout = Thread.new { out_p.read } if capture_stdout 84 | th_stderr = Thread.new { err_p.read } if capture_stderr && capture_stderr != :merge_to_stdout 85 | in_p.write stdin_data.to_str unless stdin_data.empty? 86 | in_p.close 87 | if (!th_stdout || th_stdout.join(timeout)) && (!th_stderr || th_stderr.join(timeout)) 88 | timeout_error = nil 89 | else 90 | signals = Array(signal).select do |sig| 91 | DEFAULT_SIGNALS[sig.to_s] or 92 | DEFAULT_SIGNALS[Signal.signame(sig)] rescue false 93 | end 94 | signals |= [:ABRT, :KILL] 95 | case pgroup = opt[:pgroup] 96 | when 0, true 97 | pgroup = -pid 98 | when nil, false 99 | pgroup = pid 100 | end 101 | while signal = signals.shift 102 | begin 103 | Process.kill signal, pgroup 104 | rescue Errno::EINVAL 105 | next 106 | rescue Errno::ESRCH 107 | break 108 | end 109 | if signals.empty? or !reprieve 110 | Process.wait(pid) 111 | else 112 | begin 113 | Timeout.timeout(reprieve) {Process.wait(pid)} 114 | rescue Timeout::Error 115 | end 116 | end 117 | end 118 | status = $? 119 | end 120 | stdout = th_stdout.value if capture_stdout 121 | stderr = th_stderr.value if capture_stderr && capture_stderr != :merge_to_stdout 122 | out_p.close if capture_stdout 123 | err_p.close if capture_stderr && capture_stderr != :merge_to_stdout 124 | status ||= Process.wait2(pid)[1] 125 | stdout = stdout_filter.call(stdout) if stdout_filter 126 | stderr = stderr_filter.call(stderr) if stderr_filter 127 | if timeout_error 128 | bt = caller_locations 129 | msg = "execution of #{bt.shift.label} expired" 130 | msg = Test::Unit::Assertions::FailDesc[status, msg, [stdout, stderr].join("\n")].() 131 | raise timeout_error, msg, bt.map(&:to_s) 132 | end 133 | return stdout, stderr, status 134 | end 135 | ensure 136 | [th_stdout, th_stderr].each do |th| 137 | th.kill if th 138 | end 139 | [in_c, in_p, out_c, out_p, err_c, err_p].each do |io| 140 | io&.close 141 | end 142 | [th_stdout, th_stderr].each do |th| 143 | th.join if th 144 | end 145 | end 146 | module_function :invoke_ruby 147 | 148 | alias rubyexec invoke_ruby 149 | class << self 150 | alias rubyexec invoke_ruby 151 | end 152 | 153 | def verbose_warning 154 | class << (stderr = "") 155 | alias write << 156 | end 157 | stderr, $stderr, verbose, $VERBOSE = $stderr, stderr, $VERBOSE, true 158 | yield stderr 159 | return $stderr 160 | ensure 161 | stderr, $stderr, $VERBOSE = $stderr, stderr, verbose 162 | end 163 | module_function :verbose_warning 164 | 165 | def default_warning 166 | verbose, $VERBOSE = $VERBOSE, false 167 | yield 168 | ensure 169 | $VERBOSE = verbose 170 | end 171 | module_function :default_warning 172 | 173 | def suppress_warning 174 | verbose, $VERBOSE = $VERBOSE, nil 175 | yield 176 | ensure 177 | $VERBOSE = verbose 178 | end 179 | module_function :suppress_warning 180 | 181 | def under_gc_stress(stress = true) 182 | stress, GC.stress = GC.stress, stress 183 | yield 184 | ensure 185 | GC.stress = stress 186 | end 187 | module_function :under_gc_stress 188 | 189 | def with_default_external(enc) 190 | verbose, $VERBOSE = $VERBOSE, nil 191 | origenc, Encoding.default_external = Encoding.default_external, enc 192 | $VERBOSE = verbose 193 | yield 194 | ensure 195 | verbose, $VERBOSE = $VERBOSE, nil 196 | Encoding.default_external = origenc 197 | $VERBOSE = verbose 198 | end 199 | module_function :with_default_external 200 | 201 | def with_default_internal(enc) 202 | verbose, $VERBOSE = $VERBOSE, nil 203 | origenc, Encoding.default_internal = Encoding.default_internal, enc 204 | $VERBOSE = verbose 205 | yield 206 | ensure 207 | verbose, $VERBOSE = $VERBOSE, nil 208 | Encoding.default_internal = origenc 209 | $VERBOSE = verbose 210 | end 211 | module_function :with_default_internal 212 | 213 | def labeled_module(name, &block) 214 | Module.new do 215 | singleton_class.class_eval {define_method(:to_s) {name}; alias inspect to_s} 216 | class_eval(&block) if block 217 | end 218 | end 219 | module_function :labeled_module 220 | 221 | def labeled_class(name, superclass = Object, &block) 222 | Class.new(superclass) do 223 | singleton_class.class_eval {define_method(:to_s) {name}; alias inspect to_s} 224 | class_eval(&block) if block 225 | end 226 | end 227 | module_function :labeled_class 228 | 229 | if /darwin/ =~ RUBY_PLATFORM 230 | DIAGNOSTIC_REPORTS_PATH = File.expand_path("~/Library/Logs/DiagnosticReports") 231 | DIAGNOSTIC_REPORTS_TIMEFORMAT = '%Y-%m-%d-%H%M%S' 232 | @ruby_install_name = RbConfig::CONFIG['RUBY_INSTALL_NAME'] 233 | 234 | def self.diagnostic_reports(signame, pid, now) 235 | return unless %w[ABRT QUIT SEGV ILL TRAP].include?(signame) 236 | cmd = File.basename(rubybin) 237 | cmd = @ruby_install_name if "ruby-runner#{RbConfig::CONFIG["EXEEXT"]}" == cmd 238 | path = DIAGNOSTIC_REPORTS_PATH 239 | timeformat = DIAGNOSTIC_REPORTS_TIMEFORMAT 240 | pat = "#{path}/#{cmd}_#{now.strftime(timeformat)}[-_]*.crash" 241 | first = true 242 | 30.times do 243 | first ? (first = false) : sleep(0.1) 244 | Dir.glob(pat) do |name| 245 | log = File.read(name) rescue next 246 | if /\AProcess:\s+#{cmd} \[#{pid}\]$/ =~ log 247 | File.unlink(name) 248 | File.unlink("#{path}/.#{File.basename(name)}.plist") rescue nil 249 | return log 250 | end 251 | end 252 | end 253 | nil 254 | end 255 | else 256 | def self.diagnostic_reports(signame, pid, now) 257 | end 258 | end 259 | 260 | def self.gc_stress_to_class? 261 | unless defined?(@gc_stress_to_class) 262 | _, _, status = invoke_ruby(["-e""exit GC.respond_to?(:add_stress_to_class)"]) 263 | @gc_stress_to_class = status.success? 264 | end 265 | @gc_stress_to_class 266 | end 267 | end 268 | 269 | if defined?(RbConfig) 270 | module RbConfig 271 | @ruby = EnvUtil.rubybin 272 | class << self 273 | undef ruby if method_defined?(:ruby) 274 | attr_reader :ruby 275 | end 276 | dir = File.dirname(ruby) 277 | name = File.basename(ruby, CONFIG['EXEEXT']) 278 | CONFIG['bindir'] = dir 279 | CONFIG['ruby_install_name'] = name 280 | CONFIG['RUBY_INSTALL_NAME'] = name 281 | Gem::ConfigMap[:bindir] = dir if defined?(Gem::ConfigMap) 282 | end 283 | end 284 | -------------------------------------------------------------------------------- /test/matrix/test_vector.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: false 2 | require 'test/unit' 3 | require 'matrix' 4 | 5 | class TestVector < Test::Unit::TestCase 6 | def setup 7 | @v1 = Vector[1,2,3] 8 | @v2 = Vector[1,2,3] 9 | @v3 = @v1.clone 10 | @v4 = Vector[1.0, 2.0, 3.0] 11 | @w1 = Vector[2,3,4] 12 | end 13 | 14 | def test_zero 15 | assert_equal Vector[0, 0, 0, 0], Vector.zero(4) 16 | assert_equal Vector[], Vector.zero(0) 17 | assert_raise(ArgumentError) { Vector.zero(-1) } 18 | assert Vector[0, 0, 0, 0].zero? 19 | end 20 | 21 | def test_basis 22 | assert_equal(Vector[1, 0, 0], Vector.basis(size: 3, index: 0)) 23 | assert_raise(ArgumentError) { Vector.basis(size: -1, index: 2) } 24 | assert_raise(ArgumentError) { Vector.basis(size: 4, index: -1) } 25 | assert_raise(ArgumentError) { Vector.basis(size: 3, index: 3) } 26 | assert_raise(ArgumentError) { Vector.basis(size: 3) } 27 | assert_raise(ArgumentError) { Vector.basis(index: 3) } 28 | end 29 | 30 | def test_get_element 31 | assert_equal(@v1[0..], [1, 2, 3]) 32 | assert_equal(@v1[0..1], [1, 2]) 33 | assert_equal(@v1[2], 3) 34 | assert_equal(@v1[4], nil) 35 | end 36 | 37 | def test_set_element 38 | 39 | assert_block do 40 | v = Vector[5, 6, 7, 8, 9] 41 | v[1..2] = Vector[1, 2] 42 | v == Vector[5, 1, 2, 8, 9] 43 | end 44 | 45 | assert_block do 46 | v = Vector[6, 7, 8] 47 | v[1..2] = Matrix[[1, 3]] 48 | v == Vector[6, 1, 3] 49 | end 50 | 51 | assert_block do 52 | v = Vector[1, 2, 3, 4, 5, 6] 53 | v[0..2] = 8 54 | v == Vector[8, 8, 8, 4, 5, 6] 55 | end 56 | 57 | assert_block do 58 | v = Vector[1, 3, 4, 5] 59 | v[2] = 5 60 | v == Vector[1, 3, 5, 5] 61 | end 62 | 63 | assert_block do 64 | v = Vector[2, 3, 5] 65 | v[-2] = 13 66 | v == Vector[2, 13, 5] 67 | end 68 | 69 | assert_block do 70 | v = Vector[4, 8, 9, 11, 30] 71 | v[1..-2] = Vector[1, 2, 3] 72 | v == Vector[4, 1, 2, 3, 30] 73 | end 74 | 75 | assert_raise(IndexError) {Vector[1, 3, 4, 5][5..6] = 17} 76 | assert_raise(IndexError) {Vector[1, 3, 4, 5][6] = 17} 77 | assert_raise(Matrix::ErrDimensionMismatch) {Vector[1, 3, 4, 5][0..2] = Matrix[[1], [2], [3]]} 78 | assert_raise(ArgumentError) {Vector[1, 2, 3, 4, 5, 6][0..2] = Vector[1, 2, 3, 4, 5, 6]} 79 | assert_raise(FrozenError) { Vector[7, 8, 9].freeze[0..1] = 5} 80 | end 81 | 82 | def test_map! 83 | v1 = Vector[1, 2, 3] 84 | v2 = Vector[1, 3, 5].freeze 85 | v3 = Vector[].freeze 86 | assert_equal Vector[1, 4, 9], v1.map!{|e| e ** 2} 87 | assert_equal v1, v1.map!{|e| e - 8} 88 | assert_raise(FrozenError) { v2.map!{|e| e + 2 }} 89 | assert_raise(FrozenError){ v3.map!{} } 90 | end 91 | 92 | def test_freeze 93 | v = Vector[1,2,3] 94 | f = v.freeze 95 | assert_equal true, f.frozen? 96 | assert v.equal?(f) 97 | assert v.equal?(f.freeze) 98 | assert_raise(FrozenError){ v[1] = 56 } 99 | assert_equal v.dup, v 100 | end 101 | 102 | def test_clone 103 | a = Vector[4] 104 | def a.foo 105 | 42 106 | end 107 | 108 | v = a.clone 109 | v[0] = 2 110 | assert_equal a, v * 2 111 | assert_equal 42, v.foo 112 | 113 | a.freeze 114 | v = a.clone 115 | assert v.frozen? 116 | assert_equal 42, v.foo 117 | end 118 | 119 | def test_dup 120 | a = Vector[4] 121 | def a.foo 122 | 42 123 | end 124 | a.freeze 125 | 126 | v = a.dup 127 | v[0] = 2 128 | assert_equal a, v * 2 129 | assert !v.respond_to?(:foo) 130 | end 131 | 132 | def test_identity 133 | assert_same @v1, @v1 134 | assert_not_same @v1, @v2 135 | assert_not_same @v1, @v3 136 | assert_not_same @v1, @v4 137 | assert_not_same @v1, @w1 138 | end 139 | 140 | def test_equality 141 | assert_equal @v1, @v1 142 | assert_equal @v1, @v2 143 | assert_equal @v1, @v3 144 | assert_equal @v1, @v4 145 | assert_not_equal @v1, @w1 146 | end 147 | 148 | def test_hash_equality 149 | assert @v1.eql?(@v1) 150 | assert @v1.eql?(@v2) 151 | assert @v1.eql?(@v3) 152 | assert !@v1.eql?(@v4) 153 | assert !@v1.eql?(@w1) 154 | 155 | hash = { @v1 => :value } 156 | assert hash.key?(@v1) 157 | assert hash.key?(@v2) 158 | assert hash.key?(@v3) 159 | assert !hash.key?(@v4) 160 | assert !hash.key?(@w1) 161 | end 162 | 163 | def test_hash 164 | assert_equal @v1.hash, @v1.hash 165 | assert_equal @v1.hash, @v2.hash 166 | assert_equal @v1.hash, @v3.hash 167 | end 168 | 169 | def test_aref 170 | assert_equal(1, @v1[0]) 171 | assert_equal(2, @v1[1]) 172 | assert_equal(3, @v1[2]) 173 | assert_equal(3, @v1[-1]) 174 | assert_equal(nil, @v1[3]) 175 | end 176 | 177 | def test_size 178 | assert_equal(3, @v1.size) 179 | end 180 | 181 | def test_each2 182 | a = [] 183 | @v1.each2(@v4) {|x, y| a << [x, y] } 184 | assert_equal([[1,1.0],[2,2.0],[3,3.0]], a) 185 | end 186 | 187 | def test_collect 188 | a = @v1.collect {|x| x + 1 } 189 | assert_equal(Vector[2,3,4], a) 190 | end 191 | 192 | def test_collect2 193 | a = @v1.collect2(@v4) {|x, y| x + y } 194 | assert_equal([2.0,4.0,6.0], a) 195 | end 196 | 197 | def test_map2 198 | a = @v1.map2(@v4) {|x, y| x + y } 199 | assert_equal(Vector[2.0,4.0,6.0], a) 200 | end 201 | 202 | def test_independent? 203 | assert(Vector.independent?(@v1, @w1)) 204 | assert( 205 | Vector.independent?( 206 | Vector.basis(size: 3, index: 0), 207 | Vector.basis(size: 3, index: 1), 208 | Vector.basis(size: 3, index: 2), 209 | ) 210 | ) 211 | 212 | refute(Vector.independent?(@v1, Vector[2,4,6])) 213 | refute(Vector.independent?(Vector[2,4], Vector[1,3], Vector[5,6])) 214 | 215 | assert_raise(TypeError) { Vector.independent?(@v1, 3) } 216 | assert_raise(Vector::ErrDimensionMismatch) { Vector.independent?(@v1, Vector[2,4]) } 217 | 218 | assert(@v1.independent?(Vector[1,2,4], Vector[1,3,4])) 219 | end 220 | 221 | def test_mul 222 | assert_equal(Vector[2,4,6], @v1 * 2) 223 | assert_equal(Matrix[[1, 4, 9], [2, 8, 18], [3, 12, 27]], @v1 * Matrix[[1,4,9]]) 224 | assert_raise(Matrix::ErrOperationNotDefined) { @v1 * Vector[1,4,9] } 225 | o = Object.new 226 | def o.coerce(x) 227 | [1, 1] 228 | end 229 | assert_equal(1, Vector[1, 2, 3] * o) 230 | end 231 | 232 | def test_add 233 | assert_equal(Vector[2,4,6], @v1 + @v1) 234 | assert_equal(Matrix[[2],[6],[12]], @v1 + Matrix[[1],[4],[9]]) 235 | o = Object.new 236 | def o.coerce(x) 237 | [1, 1] 238 | end 239 | assert_equal(2, Vector[1, 2, 3] + o) 240 | end 241 | 242 | def test_sub 243 | assert_equal(Vector[0,0,0], @v1 - @v1) 244 | assert_equal(Matrix[[0],[-2],[-6]], @v1 - Matrix[[1],[4],[9]]) 245 | o = Object.new 246 | def o.coerce(x) 247 | [1, 1] 248 | end 249 | assert_equal(0, Vector[1, 2, 3] - o) 250 | end 251 | 252 | def test_uplus 253 | assert_equal(@v1, +@v1) 254 | end 255 | 256 | def test_negate 257 | assert_equal(Vector[-1, -2, -3], -@v1) 258 | assert_equal(@v1, -(-@v1)) 259 | end 260 | 261 | def test_inner_product 262 | assert_equal(1+4+9, @v1.inner_product(@v1)) 263 | assert_equal(1+4+9, @v1.dot(@v1)) 264 | end 265 | 266 | def test_r 267 | assert_equal(5, Vector[3, 4].r) 268 | end 269 | 270 | def test_round 271 | assert_equal(Vector[1.234, 2.345, 3.40].round(2), Vector[1.23, 2.35, 3.4]) 272 | end 273 | 274 | def test_covector 275 | assert_equal(Matrix[[1,2,3]], @v1.covector) 276 | end 277 | 278 | def test_to_s 279 | assert_equal("Vector[1, 2, 3]", @v1.to_s) 280 | end 281 | 282 | def test_to_matrix 283 | assert_equal Matrix[[1], [2], [3]], @v1.to_matrix 284 | end 285 | 286 | def test_inspect 287 | assert_equal("Vector[1, 2, 3]", @v1.inspect) 288 | end 289 | 290 | def test_magnitude 291 | assert_in_epsilon(3.7416573867739413, @v1.norm) 292 | assert_in_epsilon(3.7416573867739413, @v4.norm) 293 | end 294 | 295 | def test_complex_magnitude 296 | bug6966 = '[ruby-dev:46100]' 297 | v = Vector[Complex(0,1), 0] 298 | assert_equal(1.0, v.norm, bug6966) 299 | end 300 | 301 | def test_rational_magnitude 302 | v = Vector[Rational(1,2), 0] 303 | assert_equal(0.5, v.norm) 304 | end 305 | 306 | def test_cross_product 307 | v = Vector[1, 0, 0].cross_product Vector[0, 1, 0] 308 | assert_equal(Vector[0, 0, 1], v) 309 | v2 = Vector[1, 2].cross_product 310 | assert_equal(Vector[-2, 1], v2) 311 | v3 = Vector[3, 5, 2, 1].cross(Vector[4, 3, 1, 8], Vector[2, 9, 4, 3]) 312 | assert_equal(Vector[16, -65, 139, -1], v3) 313 | assert_equal Vector[0, 0, 0, 1], 314 | Vector[1, 0, 0, 0].cross(Vector[0, 1, 0, 0], Vector[0, 0, 1, 0]) 315 | assert_equal Vector[0, 0, 0, 0, 1], 316 | Vector[1, 0, 0, 0, 0].cross(Vector[0, 1, 0, 0, 0], Vector[0, 0, 1, 0, 0], Vector[0, 0, 0, 1, 0]) 317 | assert_raise(Vector::ErrDimensionMismatch) { Vector[1, 2, 3].cross_product(Vector[1, 4]) } 318 | assert_raise(TypeError) { Vector[1, 2, 3].cross_product(42) } 319 | assert_raise(ArgumentError) { Vector[1, 2].cross_product(Vector[2, -1]) } 320 | assert_raise(Vector::ErrOperationNotDefined) { Vector[1].cross_product } 321 | end 322 | 323 | def test_angle_with 324 | assert_in_epsilon(Math::PI, Vector[1, 0].angle_with(Vector[-1, 0])) 325 | assert_in_epsilon(Math::PI/2, Vector[1, 0].angle_with(Vector[0, -1])) 326 | assert_in_epsilon(Math::PI/4, Vector[2, 2].angle_with(Vector[0, 1])) 327 | assert_in_delta(0.0, Vector[1, 1].angle_with(Vector[1, 1]), 0.00001) 328 | assert_equal(Vector[6, 6].angle_with(Vector[7, 7]), 0.0) 329 | assert_equal(Vector[6, 6].angle_with(Vector[-7, -7]), Math::PI) 330 | 331 | assert_raise(Vector::ZeroVectorError) { Vector[1, 1].angle_with(Vector[0, 0]) } 332 | assert_raise(Vector::ZeroVectorError) { Vector[0, 0].angle_with(Vector[1, 1]) } 333 | assert_raise(Matrix::ErrDimensionMismatch) { Vector[1, 2, 3].angle_with(Vector[0, 1]) } 334 | end 335 | end 336 | -------------------------------------------------------------------------------- /lib/matrix/eigenvalue_decomposition.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: false 2 | class Matrix 3 | # Adapted from JAMA: http://math.nist.gov/javanumerics/jama/ 4 | 5 | # Eigenvalues and eigenvectors of a real matrix. 6 | # 7 | # Computes the eigenvalues and eigenvectors of a matrix A. 8 | # 9 | # If A is diagonalizable, this provides matrices V and D 10 | # such that A = V*D*V.inv, where D is the diagonal matrix with entries 11 | # equal to the eigenvalues and V is formed by the eigenvectors. 12 | # 13 | # If A is symmetric, then V is orthogonal and thus A = V*D*V.t 14 | 15 | class EigenvalueDecomposition 16 | 17 | # Constructs the eigenvalue decomposition for a square matrix +A+ 18 | # 19 | def initialize(a) 20 | # @d, @e: Arrays for internal storage of eigenvalues. 21 | # @v: Array for internal storage of eigenvectors. 22 | # @h: Array for internal storage of nonsymmetric Hessenberg form. 23 | raise TypeError, "Expected Matrix but got #{a.class}" unless a.is_a?(Matrix) 24 | @size = a.row_count 25 | @d = Array.new(@size, 0) 26 | @e = Array.new(@size, 0) 27 | 28 | if (@symmetric = a.symmetric?) 29 | @v = a.to_a 30 | tridiagonalize 31 | diagonalize 32 | else 33 | @v = Array.new(@size) { Array.new(@size, 0) } 34 | @h = a.to_a 35 | @ort = Array.new(@size, 0) 36 | reduce_to_hessenberg 37 | hessenberg_to_real_schur 38 | end 39 | end 40 | 41 | # Returns the eigenvector matrix +V+ 42 | # 43 | def eigenvector_matrix 44 | Matrix.send(:new, build_eigenvectors.transpose) 45 | end 46 | alias_method :v, :eigenvector_matrix 47 | 48 | # Returns the inverse of the eigenvector matrix +V+ 49 | # 50 | def eigenvector_matrix_inv 51 | r = Matrix.send(:new, build_eigenvectors) 52 | r = r.transpose.inverse unless @symmetric 53 | r 54 | end 55 | alias_method :v_inv, :eigenvector_matrix_inv 56 | 57 | # Returns the eigenvalues in an array 58 | # 59 | def eigenvalues 60 | values = @d.dup 61 | @e.each_with_index{|imag, i| values[i] = Complex(values[i], imag) unless imag == 0} 62 | values 63 | end 64 | 65 | # Returns an array of the eigenvectors 66 | # 67 | def eigenvectors 68 | build_eigenvectors.map{|ev| Vector.send(:new, ev)} 69 | end 70 | 71 | # Returns the block diagonal eigenvalue matrix +D+ 72 | # 73 | def eigenvalue_matrix 74 | Matrix.diagonal(*eigenvalues) 75 | end 76 | alias_method :d, :eigenvalue_matrix 77 | 78 | # Returns [eigenvector_matrix, eigenvalue_matrix, eigenvector_matrix_inv] 79 | # 80 | def to_ary 81 | [v, d, v_inv] 82 | end 83 | alias_method :to_a, :to_ary 84 | 85 | 86 | private def build_eigenvectors 87 | # JAMA stores complex eigenvectors in a strange way 88 | # See http://web.archive.org/web/20111016032731/http://cio.nist.gov/esd/emaildir/lists/jama/msg01021.html 89 | @e.each_with_index.map do |imag, i| 90 | if imag == 0 91 | Array.new(@size){|j| @v[j][i]} 92 | elsif imag > 0 93 | Array.new(@size){|j| Complex(@v[j][i], @v[j][i+1])} 94 | else 95 | Array.new(@size){|j| Complex(@v[j][i-1], -@v[j][i])} 96 | end 97 | end 98 | end 99 | 100 | # Complex scalar division. 101 | 102 | private def cdiv(xr, xi, yr, yi) 103 | if (yr.abs > yi.abs) 104 | r = yi/yr 105 | d = yr + r*yi 106 | [(xr + r*xi)/d, (xi - r*xr)/d] 107 | else 108 | r = yr/yi 109 | d = yi + r*yr 110 | [(r*xr + xi)/d, (r*xi - xr)/d] 111 | end 112 | end 113 | 114 | 115 | # Symmetric Householder reduction to tridiagonal form. 116 | 117 | private def tridiagonalize 118 | 119 | # This is derived from the Algol procedures tred2 by 120 | # Bowdler, Martin, Reinsch, and Wilkinson, Handbook for 121 | # Auto. Comp., Vol.ii-Linear Algebra, and the corresponding 122 | # Fortran subroutine in EISPACK. 123 | 124 | @size.times do |j| 125 | @d[j] = @v[@size-1][j] 126 | end 127 | 128 | # Householder reduction to tridiagonal form. 129 | 130 | (@size-1).downto(0+1) do |i| 131 | 132 | # Scale to avoid under/overflow. 133 | 134 | scale = 0.0 135 | h = 0.0 136 | i.times do |k| 137 | scale = scale + @d[k].abs 138 | end 139 | if (scale == 0.0) 140 | @e[i] = @d[i-1] 141 | i.times do |j| 142 | @d[j] = @v[i-1][j] 143 | @v[i][j] = 0.0 144 | @v[j][i] = 0.0 145 | end 146 | else 147 | 148 | # Generate Householder vector. 149 | 150 | i.times do |k| 151 | @d[k] /= scale 152 | h += @d[k] * @d[k] 153 | end 154 | f = @d[i-1] 155 | g = Math.sqrt(h) 156 | if (f > 0) 157 | g = -g 158 | end 159 | @e[i] = scale * g 160 | h -= f * g 161 | @d[i-1] = f - g 162 | i.times do |j| 163 | @e[j] = 0.0 164 | end 165 | 166 | # Apply similarity transformation to remaining columns. 167 | 168 | i.times do |j| 169 | f = @d[j] 170 | @v[j][i] = f 171 | g = @e[j] + @v[j][j] * f 172 | (j+1).upto(i-1) do |k| 173 | g += @v[k][j] * @d[k] 174 | @e[k] += @v[k][j] * f 175 | end 176 | @e[j] = g 177 | end 178 | f = 0.0 179 | i.times do |j| 180 | @e[j] /= h 181 | f += @e[j] * @d[j] 182 | end 183 | hh = f / (h + h) 184 | i.times do |j| 185 | @e[j] -= hh * @d[j] 186 | end 187 | i.times do |j| 188 | f = @d[j] 189 | g = @e[j] 190 | j.upto(i-1) do |k| 191 | @v[k][j] -= (f * @e[k] + g * @d[k]) 192 | end 193 | @d[j] = @v[i-1][j] 194 | @v[i][j] = 0.0 195 | end 196 | end 197 | @d[i] = h 198 | end 199 | 200 | # Accumulate transformations. 201 | 202 | 0.upto(@size-1-1) do |i| 203 | @v[@size-1][i] = @v[i][i] 204 | @v[i][i] = 1.0 205 | h = @d[i+1] 206 | if (h != 0.0) 207 | 0.upto(i) do |k| 208 | @d[k] = @v[k][i+1] / h 209 | end 210 | 0.upto(i) do |j| 211 | g = 0.0 212 | 0.upto(i) do |k| 213 | g += @v[k][i+1] * @v[k][j] 214 | end 215 | 0.upto(i) do |k| 216 | @v[k][j] -= g * @d[k] 217 | end 218 | end 219 | end 220 | 0.upto(i) do |k| 221 | @v[k][i+1] = 0.0 222 | end 223 | end 224 | @size.times do |j| 225 | @d[j] = @v[@size-1][j] 226 | @v[@size-1][j] = 0.0 227 | end 228 | @v[@size-1][@size-1] = 1.0 229 | @e[0] = 0.0 230 | end 231 | 232 | 233 | # Symmetric tridiagonal QL algorithm. 234 | 235 | private def diagonalize 236 | # This is derived from the Algol procedures tql2, by 237 | # Bowdler, Martin, Reinsch, and Wilkinson, Handbook for 238 | # Auto. Comp., Vol.ii-Linear Algebra, and the corresponding 239 | # Fortran subroutine in EISPACK. 240 | 241 | 1.upto(@size-1) do |i| 242 | @e[i-1] = @e[i] 243 | end 244 | @e[@size-1] = 0.0 245 | 246 | f = 0.0 247 | tst1 = 0.0 248 | eps = Float::EPSILON 249 | @size.times do |l| 250 | 251 | # Find small subdiagonal element 252 | 253 | tst1 = [tst1, @d[l].abs + @e[l].abs].max 254 | m = l 255 | while (m < @size) do 256 | if (@e[m].abs <= eps*tst1) 257 | break 258 | end 259 | m+=1 260 | end 261 | 262 | # If m == l, @d[l] is an eigenvalue, 263 | # otherwise, iterate. 264 | 265 | if (m > l) 266 | iter = 0 267 | begin 268 | iter = iter + 1 # (Could check iteration count here.) 269 | 270 | # Compute implicit shift 271 | 272 | g = @d[l] 273 | p = (@d[l+1] - g) / (2.0 * @e[l]) 274 | r = Math.hypot(p, 1.0) 275 | if (p < 0) 276 | r = -r 277 | end 278 | @d[l] = @e[l] / (p + r) 279 | @d[l+1] = @e[l] * (p + r) 280 | dl1 = @d[l+1] 281 | h = g - @d[l] 282 | (l+2).upto(@size-1) do |i| 283 | @d[i] -= h 284 | end 285 | f += h 286 | 287 | # Implicit QL transformation. 288 | 289 | p = @d[m] 290 | c = 1.0 291 | c2 = c 292 | c3 = c 293 | el1 = @e[l+1] 294 | s = 0.0 295 | s2 = 0.0 296 | (m-1).downto(l) do |i| 297 | c3 = c2 298 | c2 = c 299 | s2 = s 300 | g = c * @e[i] 301 | h = c * p 302 | r = Math.hypot(p, @e[i]) 303 | @e[i+1] = s * r 304 | s = @e[i] / r 305 | c = p / r 306 | p = c * @d[i] - s * g 307 | @d[i+1] = h + s * (c * g + s * @d[i]) 308 | 309 | # Accumulate transformation. 310 | 311 | @size.times do |k| 312 | h = @v[k][i+1] 313 | @v[k][i+1] = s * @v[k][i] + c * h 314 | @v[k][i] = c * @v[k][i] - s * h 315 | end 316 | end 317 | p = -s * s2 * c3 * el1 * @e[l] / dl1 318 | @e[l] = s * p 319 | @d[l] = c * p 320 | 321 | # Check for convergence. 322 | 323 | end while (@e[l].abs > eps*tst1) 324 | end 325 | @d[l] = @d[l] + f 326 | @e[l] = 0.0 327 | end 328 | 329 | # Sort eigenvalues and corresponding vectors. 330 | 331 | 0.upto(@size-2) do |i| 332 | k = i 333 | p = @d[i] 334 | (i+1).upto(@size-1) do |j| 335 | if (@d[j] < p) 336 | k = j 337 | p = @d[j] 338 | end 339 | end 340 | if (k != i) 341 | @d[k] = @d[i] 342 | @d[i] = p 343 | @size.times do |j| 344 | p = @v[j][i] 345 | @v[j][i] = @v[j][k] 346 | @v[j][k] = p 347 | end 348 | end 349 | end 350 | end 351 | 352 | # Nonsymmetric reduction to Hessenberg form. 353 | 354 | private def reduce_to_hessenberg 355 | # This is derived from the Algol procedures orthes and ortran, 356 | # by Martin and Wilkinson, Handbook for Auto. Comp., 357 | # Vol.ii-Linear Algebra, and the corresponding 358 | # Fortran subroutines in EISPACK. 359 | 360 | low = 0 361 | high = @size-1 362 | 363 | (low+1).upto(high-1) do |m| 364 | 365 | # Scale column. 366 | 367 | scale = 0.0 368 | m.upto(high) do |i| 369 | scale = scale + @h[i][m-1].abs 370 | end 371 | if (scale != 0.0) 372 | 373 | # Compute Householder transformation. 374 | 375 | h = 0.0 376 | high.downto(m) do |i| 377 | @ort[i] = @h[i][m-1]/scale 378 | h += @ort[i] * @ort[i] 379 | end 380 | g = Math.sqrt(h) 381 | if (@ort[m] > 0) 382 | g = -g 383 | end 384 | h -= @ort[m] * g 385 | @ort[m] = @ort[m] - g 386 | 387 | # Apply Householder similarity transformation 388 | # @h = (I-u*u'/h)*@h*(I-u*u')/h) 389 | 390 | m.upto(@size-1) do |j| 391 | f = 0.0 392 | high.downto(m) do |i| 393 | f += @ort[i]*@h[i][j] 394 | end 395 | f = f/h 396 | m.upto(high) do |i| 397 | @h[i][j] -= f*@ort[i] 398 | end 399 | end 400 | 401 | 0.upto(high) do |i| 402 | f = 0.0 403 | high.downto(m) do |j| 404 | f += @ort[j]*@h[i][j] 405 | end 406 | f = f/h 407 | m.upto(high) do |j| 408 | @h[i][j] -= f*@ort[j] 409 | end 410 | end 411 | @ort[m] = scale*@ort[m] 412 | @h[m][m-1] = scale*g 413 | end 414 | end 415 | 416 | # Accumulate transformations (Algol's ortran). 417 | 418 | @size.times do |i| 419 | @size.times do |j| 420 | @v[i][j] = (i == j ? 1.0 : 0.0) 421 | end 422 | end 423 | 424 | (high-1).downto(low+1) do |m| 425 | if (@h[m][m-1] != 0.0) 426 | (m+1).upto(high) do |i| 427 | @ort[i] = @h[i][m-1] 428 | end 429 | m.upto(high) do |j| 430 | g = 0.0 431 | m.upto(high) do |i| 432 | g += @ort[i] * @v[i][j] 433 | end 434 | # Double division avoids possible underflow 435 | g = (g / @ort[m]) / @h[m][m-1] 436 | m.upto(high) do |i| 437 | @v[i][j] += g * @ort[i] 438 | end 439 | end 440 | end 441 | end 442 | end 443 | 444 | # Nonsymmetric reduction from Hessenberg to real Schur form. 445 | 446 | private def hessenberg_to_real_schur 447 | 448 | # This is derived from the Algol procedure hqr2, 449 | # by Martin and Wilkinson, Handbook for Auto. Comp., 450 | # Vol.ii-Linear Algebra, and the corresponding 451 | # Fortran subroutine in EISPACK. 452 | 453 | # Initialize 454 | 455 | nn = @size 456 | n = nn-1 457 | low = 0 458 | high = nn-1 459 | eps = Float::EPSILON 460 | exshift = 0.0 461 | p = q = r = s = z = 0 462 | 463 | # Store roots isolated by balanc and compute matrix norm 464 | 465 | norm = 0.0 466 | nn.times do |i| 467 | if (i < low || i > high) 468 | @d[i] = @h[i][i] 469 | @e[i] = 0.0 470 | end 471 | ([i-1, 0].max).upto(nn-1) do |j| 472 | norm = norm + @h[i][j].abs 473 | end 474 | end 475 | 476 | # Outer loop over eigenvalue index 477 | 478 | iter = 0 479 | while (n >= low) do 480 | 481 | # Look for single small sub-diagonal element 482 | 483 | l = n 484 | while (l > low) do 485 | s = @h[l-1][l-1].abs + @h[l][l].abs 486 | if (s == 0.0) 487 | s = norm 488 | end 489 | if (@h[l][l-1].abs < eps * s) 490 | break 491 | end 492 | l-=1 493 | end 494 | 495 | # Check for convergence 496 | # One root found 497 | 498 | if (l == n) 499 | @h[n][n] = @h[n][n] + exshift 500 | @d[n] = @h[n][n] 501 | @e[n] = 0.0 502 | n-=1 503 | iter = 0 504 | 505 | # Two roots found 506 | 507 | elsif (l == n-1) 508 | w = @h[n][n-1] * @h[n-1][n] 509 | p = (@h[n-1][n-1] - @h[n][n]) / 2.0 510 | q = p * p + w 511 | z = Math.sqrt(q.abs) 512 | @h[n][n] = @h[n][n] + exshift 513 | @h[n-1][n-1] = @h[n-1][n-1] + exshift 514 | x = @h[n][n] 515 | 516 | # Real pair 517 | 518 | if (q >= 0) 519 | if (p >= 0) 520 | z = p + z 521 | else 522 | z = p - z 523 | end 524 | @d[n-1] = x + z 525 | @d[n] = @d[n-1] 526 | if (z != 0.0) 527 | @d[n] = x - w / z 528 | end 529 | @e[n-1] = 0.0 530 | @e[n] = 0.0 531 | x = @h[n][n-1] 532 | s = x.abs + z.abs 533 | p = x / s 534 | q = z / s 535 | r = Math.sqrt(p * p+q * q) 536 | p /= r 537 | q /= r 538 | 539 | # Row modification 540 | 541 | (n-1).upto(nn-1) do |j| 542 | z = @h[n-1][j] 543 | @h[n-1][j] = q * z + p * @h[n][j] 544 | @h[n][j] = q * @h[n][j] - p * z 545 | end 546 | 547 | # Column modification 548 | 549 | 0.upto(n) do |i| 550 | z = @h[i][n-1] 551 | @h[i][n-1] = q * z + p * @h[i][n] 552 | @h[i][n] = q * @h[i][n] - p * z 553 | end 554 | 555 | # Accumulate transformations 556 | 557 | low.upto(high) do |i| 558 | z = @v[i][n-1] 559 | @v[i][n-1] = q * z + p * @v[i][n] 560 | @v[i][n] = q * @v[i][n] - p * z 561 | end 562 | 563 | # Complex pair 564 | 565 | else 566 | @d[n-1] = x + p 567 | @d[n] = x + p 568 | @e[n-1] = z 569 | @e[n] = -z 570 | end 571 | n -= 2 572 | iter = 0 573 | 574 | # No convergence yet 575 | 576 | else 577 | 578 | # Form shift 579 | 580 | x = @h[n][n] 581 | y = 0.0 582 | w = 0.0 583 | if (l < n) 584 | y = @h[n-1][n-1] 585 | w = @h[n][n-1] * @h[n-1][n] 586 | end 587 | 588 | # Wilkinson's original ad hoc shift 589 | 590 | if (iter == 10) 591 | exshift += x 592 | low.upto(n) do |i| 593 | @h[i][i] -= x 594 | end 595 | s = @h[n][n-1].abs + @h[n-1][n-2].abs 596 | x = y = 0.75 * s 597 | w = -0.4375 * s * s 598 | end 599 | 600 | # MATLAB's new ad hoc shift 601 | 602 | if (iter == 30) 603 | s = (y - x) / 2.0 604 | s *= s + w 605 | if (s > 0) 606 | s = Math.sqrt(s) 607 | if (y < x) 608 | s = -s 609 | end 610 | s = x - w / ((y - x) / 2.0 + s) 611 | low.upto(n) do |i| 612 | @h[i][i] -= s 613 | end 614 | exshift += s 615 | x = y = w = 0.964 616 | end 617 | end 618 | 619 | iter = iter + 1 # (Could check iteration count here.) 620 | 621 | # Look for two consecutive small sub-diagonal elements 622 | 623 | m = n-2 624 | while (m >= l) do 625 | z = @h[m][m] 626 | r = x - z 627 | s = y - z 628 | p = (r * s - w) / @h[m+1][m] + @h[m][m+1] 629 | q = @h[m+1][m+1] - z - r - s 630 | r = @h[m+2][m+1] 631 | s = p.abs + q.abs + r.abs 632 | p /= s 633 | q /= s 634 | r /= s 635 | if (m == l) 636 | break 637 | end 638 | if (@h[m][m-1].abs * (q.abs + r.abs) < 639 | eps * (p.abs * (@h[m-1][m-1].abs + z.abs + 640 | @h[m+1][m+1].abs))) 641 | break 642 | end 643 | m-=1 644 | end 645 | 646 | (m+2).upto(n) do |i| 647 | @h[i][i-2] = 0.0 648 | if (i > m+2) 649 | @h[i][i-3] = 0.0 650 | end 651 | end 652 | 653 | # Double QR step involving rows l:n and columns m:n 654 | 655 | m.upto(n-1) do |k| 656 | notlast = (k != n-1) 657 | if (k != m) 658 | p = @h[k][k-1] 659 | q = @h[k+1][k-1] 660 | r = (notlast ? @h[k+2][k-1] : 0.0) 661 | x = p.abs + q.abs + r.abs 662 | next if x == 0 663 | p /= x 664 | q /= x 665 | r /= x 666 | end 667 | s = Math.sqrt(p * p + q * q + r * r) 668 | if (p < 0) 669 | s = -s 670 | end 671 | if (s != 0) 672 | if (k != m) 673 | @h[k][k-1] = -s * x 674 | elsif (l != m) 675 | @h[k][k-1] = -@h[k][k-1] 676 | end 677 | p += s 678 | x = p / s 679 | y = q / s 680 | z = r / s 681 | q /= p 682 | r /= p 683 | 684 | # Row modification 685 | 686 | k.upto(nn-1) do |j| 687 | p = @h[k][j] + q * @h[k+1][j] 688 | if (notlast) 689 | p += r * @h[k+2][j] 690 | @h[k+2][j] = @h[k+2][j] - p * z 691 | end 692 | @h[k][j] = @h[k][j] - p * x 693 | @h[k+1][j] = @h[k+1][j] - p * y 694 | end 695 | 696 | # Column modification 697 | 698 | 0.upto([n, k+3].min) do |i| 699 | p = x * @h[i][k] + y * @h[i][k+1] 700 | if (notlast) 701 | p += z * @h[i][k+2] 702 | @h[i][k+2] = @h[i][k+2] - p * r 703 | end 704 | @h[i][k] = @h[i][k] - p 705 | @h[i][k+1] = @h[i][k+1] - p * q 706 | end 707 | 708 | # Accumulate transformations 709 | 710 | low.upto(high) do |i| 711 | p = x * @v[i][k] + y * @v[i][k+1] 712 | if (notlast) 713 | p += z * @v[i][k+2] 714 | @v[i][k+2] = @v[i][k+2] - p * r 715 | end 716 | @v[i][k] = @v[i][k] - p 717 | @v[i][k+1] = @v[i][k+1] - p * q 718 | end 719 | end # (s != 0) 720 | end # k loop 721 | end # check convergence 722 | end # while (n >= low) 723 | 724 | # Backsubstitute to find vectors of upper triangular form 725 | 726 | if (norm == 0.0) 727 | return 728 | end 729 | 730 | (nn-1).downto(0) do |k| 731 | p = @d[k] 732 | q = @e[k] 733 | 734 | # Real vector 735 | 736 | if (q == 0) 737 | l = k 738 | @h[k][k] = 1.0 739 | (k-1).downto(0) do |i| 740 | w = @h[i][i] - p 741 | r = 0.0 742 | l.upto(k) do |j| 743 | r += @h[i][j] * @h[j][k] 744 | end 745 | if (@e[i] < 0.0) 746 | z = w 747 | s = r 748 | else 749 | l = i 750 | if (@e[i] == 0.0) 751 | if (w != 0.0) 752 | @h[i][k] = -r / w 753 | else 754 | @h[i][k] = -r / (eps * norm) 755 | end 756 | 757 | # Solve real equations 758 | 759 | else 760 | x = @h[i][i+1] 761 | y = @h[i+1][i] 762 | q = (@d[i] - p) * (@d[i] - p) + @e[i] * @e[i] 763 | t = (x * s - z * r) / q 764 | @h[i][k] = t 765 | if (x.abs > z.abs) 766 | @h[i+1][k] = (-r - w * t) / x 767 | else 768 | @h[i+1][k] = (-s - y * t) / z 769 | end 770 | end 771 | 772 | # Overflow control 773 | 774 | t = @h[i][k].abs 775 | if ((eps * t) * t > 1) 776 | i.upto(k) do |j| 777 | @h[j][k] = @h[j][k] / t 778 | end 779 | end 780 | end 781 | end 782 | 783 | # Complex vector 784 | 785 | elsif (q < 0) 786 | l = n-1 787 | 788 | # Last vector component imaginary so matrix is triangular 789 | 790 | if (@h[n][n-1].abs > @h[n-1][n].abs) 791 | @h[n-1][n-1] = q / @h[n][n-1] 792 | @h[n-1][n] = -(@h[n][n] - p) / @h[n][n-1] 793 | else 794 | cdivr, cdivi = cdiv(0.0, -@h[n-1][n], @h[n-1][n-1]-p, q) 795 | @h[n-1][n-1] = cdivr 796 | @h[n-1][n] = cdivi 797 | end 798 | @h[n][n-1] = 0.0 799 | @h[n][n] = 1.0 800 | (n-2).downto(0) do |i| 801 | ra = 0.0 802 | sa = 0.0 803 | l.upto(n) do |j| 804 | ra = ra + @h[i][j] * @h[j][n-1] 805 | sa = sa + @h[i][j] * @h[j][n] 806 | end 807 | w = @h[i][i] - p 808 | 809 | if (@e[i] < 0.0) 810 | z = w 811 | r = ra 812 | s = sa 813 | else 814 | l = i 815 | if (@e[i] == 0) 816 | cdivr, cdivi = cdiv(-ra, -sa, w, q) 817 | @h[i][n-1] = cdivr 818 | @h[i][n] = cdivi 819 | else 820 | 821 | # Solve complex equations 822 | 823 | x = @h[i][i+1] 824 | y = @h[i+1][i] 825 | vr = (@d[i] - p) * (@d[i] - p) + @e[i] * @e[i] - q * q 826 | vi = (@d[i] - p) * 2.0 * q 827 | if (vr == 0.0 && vi == 0.0) 828 | vr = eps * norm * (w.abs + q.abs + 829 | x.abs + y.abs + z.abs) 830 | end 831 | cdivr, cdivi = cdiv(x*r-z*ra+q*sa, x*s-z*sa-q*ra, vr, vi) 832 | @h[i][n-1] = cdivr 833 | @h[i][n] = cdivi 834 | if (x.abs > (z.abs + q.abs)) 835 | @h[i+1][n-1] = (-ra - w * @h[i][n-1] + q * @h[i][n]) / x 836 | @h[i+1][n] = (-sa - w * @h[i][n] - q * @h[i][n-1]) / x 837 | else 838 | cdivr, cdivi = cdiv(-r-y*@h[i][n-1], -s-y*@h[i][n], z, q) 839 | @h[i+1][n-1] = cdivr 840 | @h[i+1][n] = cdivi 841 | end 842 | end 843 | 844 | # Overflow control 845 | 846 | t = [@h[i][n-1].abs, @h[i][n].abs].max 847 | if ((eps * t) * t > 1) 848 | i.upto(n) do |j| 849 | @h[j][n-1] = @h[j][n-1] / t 850 | @h[j][n] = @h[j][n] / t 851 | end 852 | end 853 | end 854 | end 855 | end 856 | end 857 | 858 | # Vectors of isolated roots 859 | 860 | nn.times do |i| 861 | if (i < low || i > high) 862 | i.upto(nn-1) do |j| 863 | @v[i][j] = @h[i][j] 864 | end 865 | end 866 | end 867 | 868 | # Back transformation to get eigenvectors of original matrix 869 | 870 | (nn-1).downto(low) do |j| 871 | low.upto(high) do |i| 872 | z = 0.0 873 | low.upto([j, high].min) do |k| 874 | z += @v[i][k] * @h[k][j] 875 | end 876 | @v[i][j] = z 877 | end 878 | end 879 | end 880 | 881 | end 882 | end 883 | -------------------------------------------------------------------------------- /test/matrix/test_matrix.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: false 2 | require 'test/unit' 3 | require 'matrix' 4 | 5 | class SubMatrix < Matrix 6 | end 7 | 8 | class TestMatrix < Test::Unit::TestCase 9 | def setup 10 | @m1 = Matrix[[1,2,3], [4,5,6]] 11 | @m2 = Matrix[[1,2,3], [4,5,6]] 12 | @m3 = @m1.clone 13 | @m4 = Matrix[[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]] 14 | @n1 = Matrix[[2,3,4], [5,6,7]] 15 | @c1 = Matrix[[Complex(1,2), Complex(0,1), 0], [1, 2, 3]] 16 | @e1 = Matrix.empty(2,0) 17 | @e2 = Matrix.empty(0,3) 18 | @a3 = Matrix[[4, 1, -3], [0, 3, 7], [11, -4, 2]] 19 | @a5 = Matrix[[2, 0, 9, 3, 9], [8, 7, 0, 1, 9], [7, 5, 6, 6, 5], [0, 7, 8, 3, 0], [7, 8, 2, 3, 1]] 20 | @b3 = Matrix[[-7, 7, -10], [9, -3, -2], [-1, 3, 9]] 21 | end 22 | 23 | def test_matrix 24 | assert_equal(1, @m1[0, 0]) 25 | assert_equal(2, @m1[0, 1]) 26 | assert_equal(3, @m1[0, 2]) 27 | assert_equal(4, @m1[1, 0]) 28 | assert_equal(5, @m1[1, 1]) 29 | assert_equal(6, @m1[1, 2]) 30 | end 31 | 32 | def test_identity 33 | assert_same @m1, @m1 34 | assert_not_same @m1, @m2 35 | assert_not_same @m1, @m3 36 | assert_not_same @m1, @m4 37 | assert_not_same @m1, @n1 38 | end 39 | 40 | def test_equality 41 | assert_equal @m1, @m1 42 | assert_equal @m1, @m2 43 | assert_equal @m1, @m3 44 | assert_equal @m1, @m4 45 | assert_not_equal @m1, @n1 46 | end 47 | 48 | def test_hash_equality 49 | assert @m1.eql?(@m1) 50 | assert @m1.eql?(@m2) 51 | assert @m1.eql?(@m3) 52 | assert !@m1.eql?(@m4) 53 | assert !@m1.eql?(@n1) 54 | 55 | hash = { @m1 => :value } 56 | assert hash.key?(@m1) 57 | assert hash.key?(@m2) 58 | assert hash.key?(@m3) 59 | assert !hash.key?(@m4) 60 | assert !hash.key?(@n1) 61 | end 62 | 63 | def test_hash 64 | assert_equal @m1.hash, @m1.hash 65 | assert_equal @m1.hash, @m2.hash 66 | assert_equal @m1.hash, @m3.hash 67 | end 68 | 69 | def test_uplus 70 | assert_equal(@m1, +@m1) 71 | end 72 | 73 | def test_negate 74 | assert_equal(Matrix[[-1, -2, -3], [-4, -5, -6]], -@m1) 75 | assert_equal(@m1, -(-@m1)) 76 | end 77 | 78 | def test_rank 79 | [ 80 | [[0]], 81 | [[0], [0]], 82 | [[0, 0], [0, 0]], 83 | [[0, 0], [0, 0], [0, 0]], 84 | [[0, 0, 0]], 85 | [[0, 0, 0], [0, 0, 0]], 86 | [[0, 0, 0], [0, 0, 0], [0, 0, 0]], 87 | [[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]], 88 | ].each do |rows| 89 | assert_equal 0, Matrix[*rows].rank 90 | end 91 | 92 | [ 93 | [[1], [0]], 94 | [[1, 0], [0, 0]], 95 | [[1, 0], [1, 0]], 96 | [[0, 0], [1, 0]], 97 | [[1, 0], [0, 0], [0, 0]], 98 | [[0, 0], [1, 0], [0, 0]], 99 | [[0, 0], [0, 0], [1, 0]], 100 | [[1, 0], [1, 0], [0, 0]], 101 | [[0, 0], [1, 0], [1, 0]], 102 | [[1, 0], [1, 0], [1, 0]], 103 | [[1, 0, 0]], 104 | [[1, 0, 0], [0, 0, 0]], 105 | [[0, 0, 0], [1, 0, 0]], 106 | [[1, 0, 0], [1, 0, 0]], 107 | [[1, 0, 0], [1, 0, 0]], 108 | [[1, 0, 0], [0, 0, 0], [0, 0, 0]], 109 | [[0, 0, 0], [1, 0, 0], [0, 0, 0]], 110 | [[0, 0, 0], [0, 0, 0], [1, 0, 0]], 111 | [[1, 0, 0], [1, 0, 0], [0, 0, 0]], 112 | [[0, 0, 0], [1, 0, 0], [1, 0, 0]], 113 | [[1, 0, 0], [0, 0, 0], [1, 0, 0]], 114 | [[1, 0, 0], [1, 0, 0], [1, 0, 0]], 115 | [[1, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]], 116 | [[1, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]], 117 | [[1, 0, 0], [1, 0, 0], [0, 0, 0], [0, 0, 0]], 118 | [[1, 0, 0], [0, 0, 0], [1, 0, 0], [0, 0, 0]], 119 | [[1, 0, 0], [0, 0, 0], [0, 0, 0], [1, 0, 0]], 120 | [[1, 0, 0], [1, 0, 0], [1, 0, 0], [0, 0, 0]], 121 | [[1, 0, 0], [0, 0, 0], [1, 0, 0], [1, 0, 0]], 122 | [[1, 0, 0], [1, 0, 0], [0, 0, 0], [1, 0, 0]], 123 | [[1, 0, 0], [1, 0, 0], [1, 0, 0], [1, 0, 0]], 124 | 125 | [[1]], 126 | [[1], [1]], 127 | [[1, 1]], 128 | [[1, 1], [1, 1]], 129 | [[1, 1], [1, 1], [1, 1]], 130 | [[1, 1, 1]], 131 | [[1, 1, 1], [1, 1, 1]], 132 | [[1, 1, 1], [1, 1, 1], [1, 1, 1]], 133 | [[1, 1, 1], [1, 1, 1], [1, 1, 1], [1, 1, 1]], 134 | ].each do |rows| 135 | matrix = Matrix[*rows] 136 | assert_equal 1, matrix.rank 137 | assert_equal 1, matrix.transpose.rank 138 | end 139 | 140 | [ 141 | [[1, 0], [0, 1]], 142 | [[1, 0], [0, 1], [0, 0]], 143 | [[1, 0], [0, 1], [0, 1]], 144 | [[1, 0], [0, 1], [1, 1]], 145 | [[1, 0, 0], [0, 1, 0]], 146 | [[1, 0, 0], [0, 0, 1]], 147 | [[1, 0, 0], [0, 1, 0], [0, 0, 0]], 148 | [[1, 0, 0], [0, 0, 1], [0, 0, 0]], 149 | 150 | [[1, 0, 0], [0, 0, 0], [0, 1, 0]], 151 | [[1, 0, 0], [0, 0, 0], [0, 0, 1]], 152 | 153 | [[1, 0], [1, 1]], 154 | [[1, 2], [1, 1]], 155 | [[1, 2], [0, 1], [1, 1]], 156 | ].each do |rows| 157 | m = Matrix[*rows] 158 | assert_equal 2, m.rank 159 | assert_equal 2, m.transpose.rank 160 | end 161 | 162 | [ 163 | [[1, 0, 0], [0, 1, 0], [0, 0, 1]], 164 | [[1, 1, 0], [0, 1, 1], [1, 0, 1]], 165 | [[1, 1, 0], [0, 1, 1], [1, 0, 1]], 166 | [[1, 1, 0], [0, 1, 1], [1, 0, 1], [0, 0, 0]], 167 | [[1, 1, 0], [0, 1, 1], [1, 0, 1], [1, 1, 1]], 168 | [[1, 1, 1], [1, 1, 2], [1, 3, 1], [4, 1, 1]], 169 | ].each do |rows| 170 | m = Matrix[*rows] 171 | assert_equal 3, m.rank 172 | assert_equal 3, m.transpose.rank 173 | end 174 | end 175 | 176 | def test_inverse 177 | assert_equal(Matrix.empty(0, 0), Matrix.empty.inverse) 178 | assert_equal(Matrix[[-1, 1], [0, -1]], Matrix[[-1, -1], [0, -1]].inverse) 179 | assert_raise(ExceptionForMatrix::ErrDimensionMismatch) { @m1.inverse } 180 | end 181 | 182 | def test_determinant 183 | assert_equal(0, Matrix[[0,0],[0,0]].determinant) 184 | assert_equal(45, Matrix[[7,6], [3,9]].determinant) 185 | assert_equal(-18, Matrix[[2,0,1],[0,-2,2],[1,2,3]].determinant) 186 | assert_equal(-7, Matrix[[0,0,1],[0,7,6],[1,3,9]].determinant) 187 | assert_equal(42, Matrix[[7,0,1,0,12],[8,1,1,9,1],[4,0,0,-7,17],[-1,0,0,-4,8],[10,1,1,8,6]].determinant) 188 | end 189 | 190 | def test_new_matrix 191 | assert_raise(TypeError) { Matrix[Object.new] } 192 | o = Object.new 193 | def o.to_ary; [1,2,3]; end 194 | assert_equal(@m1, Matrix[o, [4,5,6]]) 195 | end 196 | 197 | def test_round 198 | a = Matrix[[1.0111, 2.32320, 3.04343], [4.81, 5.0, 6.997]] 199 | b = Matrix[[1.01, 2.32, 3.04], [4.81, 5.0, 7.0]] 200 | assert_equal(a.round(2), b) 201 | end 202 | 203 | def test_rows 204 | assert_equal(@m1, Matrix.rows([[1, 2, 3], [4, 5, 6]])) 205 | end 206 | 207 | def test_rows_copy 208 | rows1 = [[1], [1]] 209 | rows2 = [[1], [1]] 210 | 211 | m1 = Matrix.rows(rows1, copy = false) 212 | m2 = Matrix.rows(rows2, copy = true) 213 | 214 | rows1.uniq! 215 | rows2.uniq! 216 | 217 | assert_equal([[1]], m1.to_a) 218 | assert_equal([[1], [1]], m2.to_a) 219 | end 220 | 221 | def test_to_matrix 222 | assert @m1.equal? @m1.to_matrix 223 | end 224 | 225 | def test_columns 226 | assert_equal(@m1, Matrix.columns([[1, 4], [2, 5], [3, 6]])) 227 | end 228 | 229 | def test_diagonal 230 | assert_equal(Matrix.empty(0, 0), Matrix.diagonal( )) 231 | assert_equal(Matrix[[3,0,0],[0,2,0],[0,0,1]], Matrix.diagonal(3, 2, 1)) 232 | assert_equal(Matrix[[4,0,0,0],[0,3,0,0],[0,0,2,0],[0,0,0,1]], Matrix.diagonal(4, 3, 2, 1)) 233 | end 234 | 235 | def test_scalar 236 | assert_equal(Matrix.empty(0, 0), Matrix.scalar(0, 1)) 237 | assert_equal(Matrix[[2,0,0],[0,2,0],[0,0,2]], Matrix.scalar(3, 2)) 238 | assert_equal(Matrix[[2,0,0,0],[0,2,0,0],[0,0,2,0],[0,0,0,2]], Matrix.scalar(4, 2)) 239 | end 240 | 241 | def test_identity2 242 | assert_equal(Matrix[[1,0,0],[0,1,0],[0,0,1]], Matrix.identity(3)) 243 | assert_equal(Matrix[[1,0,0],[0,1,0],[0,0,1]], Matrix.unit(3)) 244 | assert_equal(Matrix[[1,0,0],[0,1,0],[0,0,1]], Matrix.I(3)) 245 | assert_equal(Matrix[[1,0,0,0],[0,1,0,0],[0,0,1,0],[0,0,0,1]], Matrix.identity(4)) 246 | end 247 | 248 | def test_zero 249 | assert_equal(Matrix[[0,0,0],[0,0,0],[0,0,0]], Matrix.zero(3)) 250 | assert_equal(Matrix[[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0]], Matrix.zero(4)) 251 | assert_equal(Matrix[[0]], Matrix.zero(1)) 252 | end 253 | 254 | def test_row_vector 255 | assert_equal(Matrix[[1,2,3,4]], Matrix.row_vector([1,2,3,4])) 256 | end 257 | 258 | def test_column_vector 259 | assert_equal(Matrix[[1],[2],[3],[4]], Matrix.column_vector([1,2,3,4])) 260 | end 261 | 262 | def test_empty 263 | m = Matrix.empty(2, 0) 264 | assert_equal(Matrix[ [], [] ], m) 265 | n = Matrix.empty(0, 3) 266 | assert_equal(Matrix.columns([ [], [], [] ]), n) 267 | assert_equal(Matrix[[0, 0, 0], [0, 0, 0]], m * n) 268 | end 269 | 270 | def test_row 271 | assert_equal(Vector[1, 2, 3], @m1.row(0)) 272 | assert_equal(Vector[4, 5, 6], @m1.row(1)) 273 | a = []; @m1.row(0) {|x| a << x } 274 | assert_equal([1, 2, 3], a) 275 | end 276 | 277 | def test_column 278 | assert_equal(Vector[1, 4], @m1.column(0)) 279 | assert_equal(Vector[2, 5], @m1.column(1)) 280 | assert_equal(Vector[3, 6], @m1.column(2)) 281 | a = []; @m1.column(0) {|x| a << x } 282 | assert_equal([1, 4], a) 283 | end 284 | 285 | def test_collect 286 | m1 = Matrix.zero(2,2) 287 | m2 = Matrix.build(3,4){|row, col| 1} 288 | 289 | assert_equal(Matrix[[5, 5, 5, 5], [5, 5, 5, 5], [5, 5, 5, 5]], m2.collect{|e| e * 5}) 290 | assert_equal(Matrix[[7, 0],[0, 7]], m1.collect(:diagonal){|e| e + 7}) 291 | assert_equal(Matrix[[0, 5],[5, 0]], m1.collect(:off_diagonal){|e| e + 5}) 292 | assert_equal(Matrix[[8, 1, 1, 1], [8, 8, 1, 1], [8, 8, 8, 1]], m2.collect(:lower){|e| e + 7}) 293 | assert_equal(Matrix[[1, 1, 1, 1], [-11, 1, 1, 1], [-11, -11, 1, 1]], m2.collect(:strict_lower){|e| e - 12}) 294 | assert_equal(Matrix[[1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1]], m2.collect(:strict_upper){|e| e ** 2}) 295 | assert_equal(Matrix[[-1, -1, -1, -1], [1, -1, -1, -1], [1, 1, -1, -1]], m2.collect(:upper){|e| -e}) 296 | assert_raise(ArgumentError) {m1.collect(:test){|e| e + 7}} 297 | assert_not_equal(m2, m2.collect {|e| e * 2 }) 298 | end 299 | 300 | def test_minor 301 | assert_equal(Matrix[[1, 2], [4, 5]], @m1.minor(0..1, 0..1)) 302 | assert_equal(Matrix[[2], [5]], @m1.minor(0..1, 1..1)) 303 | assert_equal(Matrix[[4, 5]], @m1.minor(1..1, 0..1)) 304 | assert_equal(Matrix[[1, 2], [4, 5]], @m1.minor(0, 2, 0, 2)) 305 | assert_equal(Matrix[[4, 5]], @m1.minor(1, 1, 0, 2)) 306 | assert_equal(Matrix[[2], [5]], @m1.minor(0, 2, 1, 1)) 307 | assert_raise(ArgumentError) { @m1.minor(0) } 308 | end 309 | 310 | def test_first_minor 311 | assert_equal(Matrix.empty(0, 0), Matrix[[1]].first_minor(0, 0)) 312 | assert_equal(Matrix.empty(0, 2), Matrix[[1, 4, 2]].first_minor(0, 1)) 313 | assert_equal(Matrix[[1, 3]], @m1.first_minor(1, 1)) 314 | assert_equal(Matrix[[4, 6]], @m1.first_minor(0, 1)) 315 | assert_equal(Matrix[[1, 2]], @m1.first_minor(1, 2)) 316 | assert_raise(RuntimeError) { Matrix.empty(0, 0).first_minor(0, 0) } 317 | assert_raise(ArgumentError) { @m1.first_minor(4, 0) } 318 | assert_raise(ArgumentError) { @m1.first_minor(0, -1) } 319 | assert_raise(ArgumentError) { @m1.first_minor(-1, 4) } 320 | end 321 | 322 | def test_cofactor 323 | assert_equal(1, Matrix[[1]].cofactor(0, 0)) 324 | assert_equal(9, Matrix[[7,6],[3,9]].cofactor(0, 0)) 325 | assert_equal(0, Matrix[[0,0],[0,0]].cofactor(0, 0)) 326 | assert_equal(3, Matrix[[0,0,1],[0,7,6],[1,3,9]].cofactor(1, 0)) 327 | assert_equal(-21, Matrix[[7,0,1,0,12],[8,1,1,9,1],[4,0,0,-7,17],[-1,0,0,-4,8],[10,1,1,8,6]].cofactor(2, 3)) 328 | assert_raise(RuntimeError) { Matrix.empty(0, 0).cofactor(0, 0) } 329 | assert_raise(ArgumentError) { Matrix[[0,0],[0,0]].cofactor(-1, 4) } 330 | assert_raise(ExceptionForMatrix::ErrDimensionMismatch) { Matrix[[2,0,1],[0,-2,2]].cofactor(0, 0) } 331 | end 332 | 333 | def test_adjugate 334 | assert_equal(Matrix.empty, Matrix.empty.adjugate) 335 | assert_equal(Matrix[[1]], Matrix[[5]].adjugate) 336 | assert_equal(Matrix[[9,-6],[-3,7]], Matrix[[7,6],[3,9]].adjugate) 337 | assert_equal(Matrix[[45,3,-7],[6,-1,0],[-7,0,0]], Matrix[[0,0,1],[0,7,6],[1,3,9]].adjugate) 338 | assert_equal(Matrix.identity(5), (@a5.adjugate * @a5) / @a5.det) 339 | assert_equal(Matrix.I(3), Matrix.I(3).adjugate) 340 | assert_equal((@a3 * @b3).adjugate, @b3.adjugate * @a3.adjugate) 341 | assert_equal(4**(@a3.row_count-1) * @a3.adjugate, (4 * @a3).adjugate) 342 | assert_raise(ExceptionForMatrix::ErrDimensionMismatch) { @m1.adjugate } 343 | end 344 | 345 | def test_laplace_expansion 346 | assert_equal(1, Matrix[[1]].laplace_expansion(row: 0)) 347 | assert_equal(45, Matrix[[7,6], [3,9]].laplace_expansion(row: 1)) 348 | assert_equal(0, Matrix[[0,0],[0,0]].laplace_expansion(column: 0)) 349 | assert_equal(-7, Matrix[[0,0,1],[0,7,6],[1,3,9]].laplace_expansion(column: 2)) 350 | 351 | assert_equal(Vector[3, -2], Matrix[[Vector[1, 0], Vector[0, 1]], [2, 3]].laplace_expansion(row: 0)) 352 | 353 | assert_raise(ExceptionForMatrix::ErrDimensionMismatch) { @m1.laplace_expansion(row: 1) } 354 | assert_raise(ArgumentError) { Matrix[[7,6], [3,9]].laplace_expansion() } 355 | assert_raise(ArgumentError) { Matrix[[7,6], [3,9]].laplace_expansion(foo: 1) } 356 | assert_raise(ArgumentError) { Matrix[[7,6], [3,9]].laplace_expansion(row: 1, column: 1) } 357 | assert_raise(ArgumentError) { Matrix[[7,6], [3,9]].laplace_expansion(row: 2) } 358 | assert_raise(ArgumentError) { Matrix[[0,0,1],[0,7,6],[1,3,9]].laplace_expansion(column: -1) } 359 | 360 | assert_raise(RuntimeError) { Matrix.empty(0, 0).laplace_expansion(row: 0) } 361 | end 362 | 363 | def test_regular? 364 | assert(Matrix[[1, 0], [0, 1]].regular?) 365 | assert(Matrix[[1, 0, 0], [0, 1, 0], [0, 0, 1]].regular?) 366 | assert(!Matrix[[1, 0, 0], [0, 0, 1], [0, 0, 1]].regular?) 367 | end 368 | 369 | def test_singular? 370 | assert(!Matrix[[1, 0], [0, 1]].singular?) 371 | assert(!Matrix[[1, 0, 0], [0, 1, 0], [0, 0, 1]].singular?) 372 | assert(Matrix[[1, 0, 0], [0, 0, 1], [0, 0, 1]].singular?) 373 | end 374 | 375 | def test_square? 376 | assert(Matrix[[1, 0], [0, 1]].square?) 377 | assert(Matrix[[1, 0, 0], [0, 1, 0], [0, 0, 1]].square?) 378 | assert(Matrix[[1, 0, 0], [0, 0, 1], [0, 0, 1]].square?) 379 | assert(!Matrix[[1, 0, 0], [0, 1, 0]].square?) 380 | end 381 | 382 | def test_mul 383 | assert_equal(Matrix[[2,4],[6,8]], Matrix[[2,4],[6,8]] * Matrix.I(2)) 384 | assert_equal(Matrix[[4,8],[12,16]], Matrix[[2,4],[6,8]] * 2) 385 | assert_equal(Matrix[[4,8],[12,16]], 2 * Matrix[[2,4],[6,8]]) 386 | assert_equal(Matrix[[14,32],[32,77]], @m1 * @m1.transpose) 387 | assert_equal(Matrix[[17,22,27],[22,29,36],[27,36,45]], @m1.transpose * @m1) 388 | assert_equal(Vector[14,32], @m1 * Vector[1,2,3]) 389 | o = Object.new 390 | def o.coerce(m) 391 | [m, m.transpose] 392 | end 393 | assert_equal(Matrix[[14,32],[32,77]], @m1 * o) 394 | end 395 | 396 | def test_add 397 | assert_equal(Matrix[[6,0],[-4,12]], Matrix.scalar(2,5) + Matrix[[1,0],[-4,7]]) 398 | assert_equal(Matrix[[3,5,7],[9,11,13]], @m1 + @n1) 399 | assert_equal(Matrix[[3,5,7],[9,11,13]], @n1 + @m1) 400 | assert_equal(Matrix[[2],[4],[6]], Matrix[[1],[2],[3]] + Vector[1,2,3]) 401 | assert_raise(Matrix::ErrOperationNotDefined) { @m1 + 1 } 402 | o = Object.new 403 | def o.coerce(m) 404 | [m, m] 405 | end 406 | assert_equal(Matrix[[2,4,6],[8,10,12]], @m1 + o) 407 | end 408 | 409 | def test_sub 410 | assert_equal(Matrix[[4,0],[4,-2]], Matrix.scalar(2,5) - Matrix[[1,0],[-4,7]]) 411 | assert_equal(Matrix[[-1,-1,-1],[-1,-1,-1]], @m1 - @n1) 412 | assert_equal(Matrix[[1,1,1],[1,1,1]], @n1 - @m1) 413 | assert_equal(Matrix[[0],[0],[0]], Matrix[[1],[2],[3]] - Vector[1,2,3]) 414 | assert_raise(Matrix::ErrOperationNotDefined) { @m1 - 1 } 415 | o = Object.new 416 | def o.coerce(m) 417 | [m, m] 418 | end 419 | assert_equal(Matrix[[0,0,0],[0,0,0]], @m1 - o) 420 | end 421 | 422 | def test_div 423 | assert_equal(Matrix[[0,1,1],[2,2,3]], @m1 / 2) 424 | assert_equal(Matrix[[1,1],[1,1]], Matrix[[2,2],[2,2]] / Matrix.scalar(2,2)) 425 | o = Object.new 426 | def o.coerce(m) 427 | [m, Matrix.scalar(2,2)] 428 | end 429 | assert_equal(Matrix[[1,1],[1,1]], Matrix[[2,2],[2,2]] / o) 430 | end 431 | 432 | def test_hadamard_product 433 | assert_equal(Matrix[[1,4], [9,16]], Matrix[[1,2], [3,4]].hadamard_product(Matrix[[1,2], [3,4]])) 434 | assert_equal(Matrix[[2, 6, 12], [20, 30, 42]], @m1.hadamard_product(@n1)) 435 | o = Object.new 436 | def o.to_matrix 437 | Matrix[[1, 2, 3], [-1, 0, 1]] 438 | end 439 | assert_equal(Matrix[[1, 4, 9], [-4, 0, 6]], @m1.hadamard_product(o)) 440 | e = Matrix.empty(3, 0) 441 | assert_equal(e, e.hadamard_product(e)) 442 | e = Matrix.empty(0, 3) 443 | assert_equal(e, e.hadamard_product(e)) 444 | end 445 | 446 | def test_exp 447 | assert_equal(Matrix[[67,96],[48,99]], Matrix[[7,6],[3,9]] ** 2) 448 | assert_equal(Matrix.I(5), Matrix.I(5) ** -1) 449 | assert_raise(Matrix::ErrOperationNotDefined) { Matrix.I(5) ** Object.new } 450 | end 451 | 452 | def test_det 453 | assert_equal(Matrix.instance_method(:determinant), Matrix.instance_method(:det)) 454 | end 455 | 456 | def test_rank2 457 | assert_equal(2, Matrix[[7,6],[3,9]].rank) 458 | assert_equal(0, Matrix[[0,0],[0,0]].rank) 459 | assert_equal(3, Matrix[[0,0,1],[0,7,6],[1,3,9]].rank) 460 | assert_equal(1, Matrix[[0,1],[0,1],[0,1]].rank) 461 | assert_equal(2, @m1.rank) 462 | end 463 | 464 | def test_trace 465 | assert_equal(1+5+9, Matrix[[1,2,3],[4,5,6],[7,8,9]].trace) 466 | end 467 | 468 | def test_transpose 469 | assert_equal(Matrix[[1,4],[2,5],[3,6]], @m1.transpose) 470 | end 471 | 472 | def test_conjugate 473 | assert_equal(Matrix[[Complex(1,-2), Complex(0,-1), 0], [1, 2, 3]], @c1.conjugate) 474 | end 475 | 476 | def test_eigensystem 477 | m = Matrix[[1, 2], [3, 4]] 478 | v, d, v_inv = m.eigensystem 479 | assert(d.diagonal?) 480 | assert_equal(v.inv, v_inv) 481 | assert_equal((v * d * v_inv).round(5), m) 482 | end 483 | 484 | def test_imaginary 485 | assert_equal(Matrix[[2, 1, 0], [0, 0, 0]], @c1.imaginary) 486 | end 487 | 488 | def test_lup 489 | m = Matrix[[1, 2], [3, 4]] 490 | l, u, p = m.lup 491 | assert(l.lower_triangular?) 492 | assert(u.upper_triangular?) 493 | assert(p.permutation?) 494 | assert(l * u == p * m) 495 | assert_equal(m.lup.solve([2, 5]), Vector[1, Rational(1,2)]) 496 | end 497 | 498 | def test_real 499 | assert_equal(Matrix[[1, 0, 0], [1, 2, 3]], @c1.real) 500 | end 501 | 502 | def test_rect 503 | assert_equal([Matrix[[1, 0, 0], [1, 2, 3]], Matrix[[2, 1, 0], [0, 0, 0]]], @c1.rect) 504 | end 505 | 506 | def test_row_vectors 507 | assert_equal([Vector[1,2,3], Vector[4,5,6]], @m1.row_vectors) 508 | end 509 | 510 | def test_column_vectors 511 | assert_equal([Vector[1,4], Vector[2,5], Vector[3,6]], @m1.column_vectors) 512 | end 513 | 514 | def test_to_s 515 | assert_equal("Matrix[[1, 2, 3], [4, 5, 6]]", @m1.to_s) 516 | assert_equal("Matrix.empty(0, 0)", Matrix[].to_s) 517 | assert_equal("Matrix.empty(1, 0)", Matrix[[]].to_s) 518 | end 519 | 520 | def test_inspect 521 | assert_equal("Matrix[[1, 2, 3], [4, 5, 6]]", @m1.inspect) 522 | assert_equal("Matrix.empty(0, 0)", Matrix[].inspect) 523 | assert_equal("Matrix.empty(1, 0)", Matrix[[]].inspect) 524 | end 525 | 526 | def test_scalar_add 527 | s1 = @m1.coerce(1).first 528 | assert_equal(Matrix[[1]], (s1 + 0) * Matrix[[1]]) 529 | assert_raise(Matrix::ErrOperationNotDefined) { s1 + Vector[0] } 530 | assert_raise(Matrix::ErrOperationNotDefined) { s1 + Matrix[[0]] } 531 | o = Object.new 532 | def o.coerce(x) 533 | [1, 1] 534 | end 535 | assert_equal(2, s1 + o) 536 | end 537 | 538 | def test_scalar_sub 539 | s1 = @m1.coerce(1).first 540 | assert_equal(Matrix[[1]], (s1 - 0) * Matrix[[1]]) 541 | assert_raise(Matrix::ErrOperationNotDefined) { s1 - Vector[0] } 542 | assert_raise(Matrix::ErrOperationNotDefined) { s1 - Matrix[[0]] } 543 | o = Object.new 544 | def o.coerce(x) 545 | [1, 1] 546 | end 547 | assert_equal(0, s1 - o) 548 | end 549 | 550 | def test_scalar_mul 551 | s1 = @m1.coerce(1).first 552 | assert_equal(Matrix[[1]], (s1 * 1) * Matrix[[1]]) 553 | assert_equal(Vector[2], s1 * Vector[2]) 554 | assert_equal(Matrix[[2]], s1 * Matrix[[2]]) 555 | o = Object.new 556 | def o.coerce(x) 557 | [1, 1] 558 | end 559 | assert_equal(1, s1 * o) 560 | end 561 | 562 | def test_scalar_div 563 | s1 = @m1.coerce(1).first 564 | assert_equal(Matrix[[1]], (s1 / 1) * Matrix[[1]]) 565 | assert_raise(Matrix::ErrOperationNotDefined) { s1 / Vector[0] } 566 | assert_equal(Matrix[[Rational(1,2)]], s1 / Matrix[[2]]) 567 | o = Object.new 568 | def o.coerce(x) 569 | [1, 1] 570 | end 571 | assert_equal(1, s1 / o) 572 | end 573 | 574 | def test_scalar_pow 575 | s1 = @m1.coerce(1).first 576 | assert_equal(Matrix[[1]], (s1 ** 1) * Matrix[[1]]) 577 | assert_raise(Matrix::ErrOperationNotDefined) { s1 ** Vector[0] } 578 | assert_raise(Matrix::ErrOperationNotImplemented) { s1 ** Matrix[[1]] } 579 | o = Object.new 580 | def o.coerce(x) 581 | [1, 1] 582 | end 583 | assert_equal(1, s1 ** o) 584 | end 585 | 586 | def test_hstack 587 | assert_equal Matrix[[1,2,3,2,3,4,1,2,3], [4,5,6,5,6,7,4,5,6]], 588 | @m1.hstack(@n1, @m1) 589 | # Error checking: 590 | assert_raise(TypeError) { @m1.hstack(42) } 591 | assert_raise(TypeError) { Matrix.hstack(42, @m1) } 592 | assert_raise(Matrix::ErrDimensionMismatch) { @m1.hstack(Matrix.identity(3)) } 593 | assert_raise(Matrix::ErrDimensionMismatch) { @e1.hstack(@e2) } 594 | # Corner cases: 595 | assert_equal @m1, @m1.hstack 596 | assert_equal @e1, @e1.hstack(@e1) 597 | assert_equal Matrix.empty(0,6), @e2.hstack(@e2) 598 | assert_equal SubMatrix, SubMatrix.hstack(@e1).class 599 | # From Vectors: 600 | assert_equal Matrix[[1, 3],[2, 4]], Matrix.hstack(Vector[1,2], Vector[3, 4]) 601 | end 602 | 603 | def test_vstack 604 | assert_equal Matrix[[1,2,3], [4,5,6], [2,3,4], [5,6,7], [1,2,3], [4,5,6]], 605 | @m1.vstack(@n1, @m1) 606 | # Error checking: 607 | assert_raise(TypeError) { @m1.vstack(42) } 608 | assert_raise(TypeError) { Matrix.vstack(42, @m1) } 609 | assert_raise(Matrix::ErrDimensionMismatch) { @m1.vstack(Matrix.identity(2)) } 610 | assert_raise(Matrix::ErrDimensionMismatch) { @e1.vstack(@e2) } 611 | # Corner cases: 612 | assert_equal @m1, @m1.vstack 613 | assert_equal Matrix.empty(4,0), @e1.vstack(@e1) 614 | assert_equal @e2, @e2.vstack(@e2) 615 | assert_equal SubMatrix, SubMatrix.vstack(@e1).class 616 | # From Vectors: 617 | assert_equal Matrix[[1],[2],[3]], Matrix.vstack(Vector[1,2], Vector[3]) 618 | end 619 | 620 | def test_combine 621 | x = Matrix[[6, 6], [4, 4]] 622 | y = Matrix[[1, 2], [3, 4]] 623 | assert_equal Matrix[[5, 4], [1, 0]], Matrix.combine(x, y) {|a, b| a - b} 624 | assert_equal Matrix[[5, 4], [1, 0]], x.combine(y) {|a, b| a - b} 625 | # Without block 626 | assert_equal Matrix[[5, 4], [1, 0]], Matrix.combine(x, y).each {|a, b| a - b} 627 | # With vectors 628 | assert_equal Matrix[[111], [222]], Matrix.combine(Matrix[[1], [2]], Vector[10,20], Vector[100,200], &:sum) 629 | # Basic checks 630 | assert_raise(Matrix::ErrDimensionMismatch) { @m1.combine(x) { raise } } 631 | # Edge cases 632 | assert_equal Matrix.empty, Matrix.combine{ raise } 633 | assert_equal Matrix.empty(3,0), Matrix.combine(Matrix.empty(3,0), Matrix.empty(3,0)) { raise } 634 | assert_equal Matrix.empty(0,3), Matrix.combine(Matrix.empty(0,3), Matrix.empty(0,3)) { raise } 635 | end 636 | 637 | def test_set_element 638 | src = Matrix[ 639 | [1, 2, 3, 4], 640 | [5, 6, 7, 8], 641 | [9, 10, 11, 12], 642 | ] 643 | rows = { 644 | range: [1..2, 1...3, 1..-1, -2..2, 1.., 1..., -2.., -2...], 645 | int: [2, -1], 646 | invalid: [-4, 4, -4..2, 2..-4, 0...0, 2..0, -4..], 647 | } 648 | columns = { 649 | range: [2..3, 2...4, 2..-1, -2..3, 2.., 2..., -2..., -2..], 650 | int: [3, -1], 651 | invalid: [-5, 5, -5..2, 2..-5, 0...0, -5..], 652 | } 653 | values = { 654 | element: 42, 655 | matrix: Matrix[[20, 21], [22, 23]], 656 | vector: Vector[30, 31], 657 | row: Matrix[[60, 61]], 658 | column: Matrix[[50], [51]], 659 | mismatched_matrix: Matrix.identity(3), 660 | mismatched_vector: Vector[0, 1, 2, 3], 661 | } 662 | solutions = { 663 | [:int, :int] => { 664 | element: Matrix[[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 42]], 665 | }, 666 | [:range , :int] => { 667 | element: Matrix[[1, 2, 3, 4], [5, 6, 7, 42], [9, 10, 11, 42]], 668 | column: Matrix[[1, 2, 3, 4], [5, 6, 7, 50], [9, 10, 11, 51]], 669 | vector: Matrix[[1, 2, 3, 4], [5, 6, 7, 30], [9, 10, 11, 31]], 670 | }, 671 | [:int, :range] => { 672 | element: Matrix[[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 42, 42]], 673 | row: Matrix[[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 60, 61]], 674 | vector: Matrix[[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 30, 31]], 675 | }, 676 | [:range , :range] => { 677 | element: Matrix[[1, 2, 3, 4], [5, 6, 42, 42], [9, 10, 42, 42]], 678 | matrix: Matrix[[1, 2, 3, 4], [5, 6, 20, 21], [9, 10, 22, 23]], 679 | }, 680 | } 681 | solutions.default = Hash.new(IndexError) 682 | 683 | rows.each do |row_style, row_arguments| 684 | row_arguments.each do |row_argument| 685 | columns.each do |column_style, column_arguments| 686 | column_arguments.each do |column_argument| 687 | values.each do |value_type, value| 688 | expected = solutions[[row_style, column_style]][value_type] || Matrix::ErrDimensionMismatch 689 | 690 | result = src.clone 691 | begin 692 | result[row_argument, column_argument] = value 693 | assert_equal expected, result, 694 | "m[#{row_argument.inspect}][#{column_argument.inspect}] = #{value.inspect} failed" 695 | rescue Exception => e 696 | raise if e.class != expected 697 | end 698 | end 699 | end 700 | end 701 | end 702 | end 703 | end 704 | 705 | def test_map! 706 | m1 = Matrix.zero(2,2) 707 | m2 = Matrix.build(3,4){|row, col| 1} 708 | m3 = Matrix.zero(3,5).freeze 709 | m4 = Matrix.empty.freeze 710 | 711 | assert_equal Matrix[[5, 5, 5, 5], [5, 5, 5, 5], [5, 5, 5, 5]], m2.map!{|e| e * 5} 712 | assert_equal Matrix[[7, 0],[0, 7]], m1.map!(:diagonal){|e| e + 7} 713 | assert_equal Matrix[[7, 5],[5, 7]], m1.map!(:off_diagonal){|e| e + 5} 714 | assert_equal Matrix[[12, 5, 5, 5], [12, 12, 5, 5], [12, 12, 12, 5]], m2.map!(:lower){|e| e + 7} 715 | assert_equal Matrix[[12, 5, 5, 5], [0, 12, 5, 5], [0, 0, 12, 5]], m2.map!(:strict_lower){|e| e - 12} 716 | assert_equal Matrix[[12, 25, 25, 25], [0, 12, 25, 25], [0, 0, 12, 25]], m2.map!(:strict_upper){|e| e ** 2} 717 | assert_equal Matrix[[-12, -25, -25, -25], [0, -12, -25, -25], [0, 0, -12, -25]], m2.map!(:upper){|e| -e} 718 | assert_equal m1, m1.map!{|e| e ** 2 } 719 | assert_equal m2, m2.map!(:lower){ |e| e - 3 } 720 | assert_raise(ArgumentError) {m1.map!(:test){|e| e + 7}} 721 | assert_raise(FrozenError) { m3.map!{|e| e * 2} } 722 | assert_raise(FrozenError) { m4.map!{} } 723 | end 724 | 725 | def test_freeze 726 | m = Matrix[[1, 2, 3],[4, 5, 6]] 727 | f = m.freeze 728 | assert_equal true, f.frozen? 729 | assert m.equal?(f) 730 | assert m.equal?(f.freeze) 731 | assert_raise(FrozenError){ m[0, 1] = 56 } 732 | assert_equal m.dup, m 733 | end 734 | 735 | def test_clone 736 | a = Matrix[[4]] 737 | def a.foo 738 | 42 739 | end 740 | 741 | m = a.clone 742 | m[0, 0] = 2 743 | assert_equal a, m * 2 744 | assert_equal 42, m.foo 745 | 746 | a.freeze 747 | m = a.clone 748 | assert m.frozen? 749 | assert_equal 42, m.foo 750 | end 751 | 752 | def test_dup 753 | a = Matrix[[4]] 754 | def a.foo 755 | 42 756 | end 757 | a.freeze 758 | 759 | m = a.dup 760 | m[0, 0] = 2 761 | assert_equal a, m * 2 762 | assert !m.respond_to?(:foo) 763 | end 764 | 765 | def test_eigenvalues_and_eigenvectors_symmetric 766 | m = Matrix[ 767 | [8, 1], 768 | [1, 8] 769 | ] 770 | values = m.eigensystem.eigenvalues 771 | assert_in_epsilon(7.0, values[0]) 772 | assert_in_epsilon(9.0, values[1]) 773 | vectors = m.eigensystem.eigenvectors 774 | assert_in_epsilon(-vectors[0][0], vectors[0][1]) 775 | assert_in_epsilon(vectors[1][0], vectors[1][1]) 776 | end 777 | 778 | def test_eigenvalues_and_eigenvectors_nonsymmetric 779 | m = Matrix[ 780 | [8, 1], 781 | [4, 5] 782 | ] 783 | values = m.eigensystem.eigenvalues 784 | assert_in_epsilon(9.0, values[0]) 785 | assert_in_epsilon(4.0, values[1]) 786 | vectors = m.eigensystem.eigenvectors 787 | assert_in_epsilon(vectors[0][0], vectors[0][1]) 788 | assert_in_epsilon(-4 * vectors[1][0], vectors[1][1]) 789 | end 790 | end 791 | -------------------------------------------------------------------------------- /test/lib/test/unit/assertions.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: false 2 | require 'minitest/unit' 3 | require 'pp' 4 | 5 | module Test 6 | module Unit 7 | module Assertions 8 | include MiniTest::Assertions 9 | 10 | def mu_pp(obj) #:nodoc: 11 | obj.pretty_inspect.chomp 12 | end 13 | 14 | MINI_DIR = File.join(File.dirname(File.dirname(File.expand_path(__FILE__))), "minitest") #:nodoc: 15 | 16 | # :call-seq: 17 | # assert(test, [failure_message]) 18 | # 19 | #Tests if +test+ is true. 20 | # 21 | #+msg+ may be a String or a Proc. If +msg+ is a String, it will be used 22 | #as the failure message. Otherwise, the result of calling +msg+ will be 23 | #used as the message if the assertion fails. 24 | # 25 | #If no +msg+ is given, a default message will be used. 26 | # 27 | # assert(false, "This was expected to be true") 28 | def assert(test, *msgs) 29 | case msg = msgs.first 30 | when String, Proc 31 | when nil 32 | msgs.shift 33 | else 34 | bt = caller.reject { |s| s.start_with?(MINI_DIR) } 35 | raise ArgumentError, "assertion message must be String or Proc, but #{msg.class} was given.", bt 36 | end unless msgs.empty? 37 | super 38 | end 39 | 40 | # :call-seq: 41 | # assert_block( failure_message = nil ) 42 | # 43 | #Tests the result of the given block. If the block does not return true, 44 | #the assertion will fail. The optional +failure_message+ argument is the same as in 45 | #Assertions#assert. 46 | # 47 | # assert_block do 48 | # [1, 2, 3].any? { |num| num < 1 } 49 | # end 50 | def assert_block(*msgs) 51 | assert yield, *msgs 52 | end 53 | 54 | # :call-seq: 55 | # assert_raise( *args, &block ) 56 | # 57 | #Tests if the given block raises an exception. Acceptable exception 58 | #types may be given as optional arguments. If the last argument is a 59 | #String, it will be used as the error message. 60 | # 61 | # assert_raise do #Fails, no Exceptions are raised 62 | # end 63 | # 64 | # assert_raise NameError do 65 | # puts x #Raises NameError, so assertion succeeds 66 | # end 67 | def assert_raise(*exp, &b) 68 | case exp.last 69 | when String, Proc 70 | msg = exp.pop 71 | end 72 | 73 | begin 74 | yield 75 | rescue MiniTest::Skip => e 76 | return e if exp.include? MiniTest::Skip 77 | raise e 78 | rescue Exception => e 79 | expected = exp.any? { |ex| 80 | if ex.instance_of? Module then 81 | e.kind_of? ex 82 | else 83 | e.instance_of? ex 84 | end 85 | } 86 | 87 | assert expected, proc { 88 | exception_details(e, message(msg) {"#{mu_pp(exp)} exception expected, not"}.call) 89 | } 90 | 91 | return e 92 | end 93 | 94 | exp = exp.first if exp.size == 1 95 | 96 | flunk(message(msg) {"#{mu_pp(exp)} expected but nothing was raised"}) 97 | end 98 | 99 | def assert_raises(*exp, &b) 100 | raise NoMethodError, "use assert_raise", caller 101 | end 102 | 103 | # :call-seq: 104 | # assert_raise_with_message(exception, expected, msg = nil, &block) 105 | # 106 | #Tests if the given block raises an exception with the expected 107 | #message. 108 | # 109 | # assert_raise_with_message(RuntimeError, "foo") do 110 | # nil #Fails, no Exceptions are raised 111 | # end 112 | # 113 | # assert_raise_with_message(RuntimeError, "foo") do 114 | # raise ArgumentError, "foo" #Fails, different Exception is raised 115 | # end 116 | # 117 | # assert_raise_with_message(RuntimeError, "foo") do 118 | # raise "bar" #Fails, RuntimeError is raised but the message differs 119 | # end 120 | # 121 | # assert_raise_with_message(RuntimeError, "foo") do 122 | # raise "foo" #Raises RuntimeError with the message, so assertion succeeds 123 | # end 124 | def assert_raise_with_message(exception, expected, msg = nil, &block) 125 | case expected 126 | when String 127 | assert = :assert_equal 128 | when Regexp 129 | assert = :assert_match 130 | else 131 | raise TypeError, "Expected #{expected.inspect} to be a kind of String or Regexp, not #{expected.class}" 132 | end 133 | 134 | ex = m = nil 135 | EnvUtil.with_default_internal(expected.encoding) do 136 | ex = assert_raise(exception, msg || proc {"Exception(#{exception}) with message matches to #{expected.inspect}"}) do 137 | yield 138 | end 139 | m = ex.message 140 | end 141 | msg = message(msg, "") {"Expected Exception(#{exception}) was raised, but the message doesn't match"} 142 | 143 | if assert == :assert_equal 144 | assert_equal(expected, m, msg) 145 | else 146 | msg = message(msg) { "Expected #{mu_pp expected} to match #{mu_pp m}" } 147 | assert expected =~ m, msg 148 | block.binding.eval("proc{|_|$~=_}").call($~) 149 | end 150 | ex 151 | end 152 | 153 | # :call-seq: 154 | # assert_nothing_raised( *args, &block ) 155 | # 156 | #If any exceptions are given as arguments, the assertion will 157 | #fail if one of those exceptions are raised. Otherwise, the test fails 158 | #if any exceptions are raised. 159 | # 160 | #The final argument may be a failure message. 161 | # 162 | # assert_nothing_raised RuntimeError do 163 | # raise Exception #Assertion passes, Exception is not a RuntimeError 164 | # end 165 | # 166 | # assert_nothing_raised do 167 | # raise Exception #Assertion fails 168 | # end 169 | def assert_nothing_raised(*args) 170 | self._assertions += 1 171 | if Module === args.last 172 | msg = nil 173 | else 174 | msg = args.pop 175 | end 176 | begin 177 | line = __LINE__; yield 178 | rescue MiniTest::Skip 179 | raise 180 | rescue Exception => e 181 | bt = e.backtrace 182 | as = e.instance_of?(MiniTest::Assertion) 183 | if as 184 | ans = /\A#{Regexp.quote(__FILE__)}:#{line}:in /o 185 | bt.reject! {|ln| ans =~ ln} 186 | end 187 | if ((args.empty? && !as) || 188 | args.any? {|a| a.instance_of?(Module) ? e.is_a?(a) : e.class == a }) 189 | msg = message(msg) { "Exception raised:\n<#{mu_pp(e)}>" } 190 | raise MiniTest::Assertion, msg.call, bt 191 | else 192 | raise 193 | end 194 | end 195 | end 196 | 197 | # :call-seq: 198 | # assert_nothing_thrown( failure_message = nil, &block ) 199 | # 200 | #Fails if the given block uses a call to Kernel#throw, and 201 | #returns the result of the block otherwise. 202 | # 203 | #An optional failure message may be provided as the final argument. 204 | # 205 | # assert_nothing_thrown "Something was thrown!" do 206 | # throw :problem? 207 | # end 208 | def assert_nothing_thrown(msg=nil) 209 | begin 210 | ret = yield 211 | rescue ArgumentError => error 212 | raise error if /\Auncaught throw (.+)\z/m !~ error.message 213 | msg = message(msg) { "<#{$1}> was thrown when nothing was expected" } 214 | flunk(msg) 215 | end 216 | assert(true, "Expected nothing to be thrown") 217 | ret 218 | end 219 | 220 | # :call-seq: 221 | # assert_throw( tag, failure_message = nil, &block ) 222 | # 223 | #Fails unless the given block throws +tag+, returns the caught 224 | #value otherwise. 225 | # 226 | #An optional failure message may be provided as the final argument. 227 | # 228 | # tag = Object.new 229 | # assert_throw(tag, "#{tag} was not thrown!") do 230 | # throw tag 231 | # end 232 | def assert_throw(tag, msg = nil) 233 | ret = catch(tag) do 234 | begin 235 | yield(tag) 236 | rescue UncaughtThrowError => e 237 | thrown = e.tag 238 | end 239 | msg = message(msg) { 240 | "Expected #{mu_pp(tag)} to have been thrown"\ 241 | "#{%Q[, not #{thrown}] if thrown}" 242 | } 243 | assert(false, msg) 244 | end 245 | assert(true) 246 | ret 247 | end 248 | 249 | # :call-seq: 250 | # assert_equal( expected, actual, failure_message = nil ) 251 | # 252 | #Tests if +expected+ is equal to +actual+. 253 | # 254 | #An optional failure message may be provided as the final argument. 255 | def assert_equal(exp, act, msg = nil) 256 | msg = message(msg) { 257 | exp_str = mu_pp(exp) 258 | act_str = mu_pp(act) 259 | exp_comment = '' 260 | act_comment = '' 261 | if exp_str == act_str 262 | if (exp.is_a?(String) && act.is_a?(String)) || 263 | (exp.is_a?(Regexp) && act.is_a?(Regexp)) 264 | exp_comment = " (#{exp.encoding})" 265 | act_comment = " (#{act.encoding})" 266 | elsif exp.is_a?(Float) && act.is_a?(Float) 267 | exp_str = "%\#.#{Float::DIG+2}g" % exp 268 | act_str = "%\#.#{Float::DIG+2}g" % act 269 | elsif exp.is_a?(Time) && act.is_a?(Time) 270 | if exp.subsec * 1000_000_000 == exp.nsec 271 | exp_comment = " (#{exp.nsec}[ns])" 272 | else 273 | exp_comment = " (subsec=#{exp.subsec})" 274 | end 275 | if act.subsec * 1000_000_000 == act.nsec 276 | act_comment = " (#{act.nsec}[ns])" 277 | else 278 | act_comment = " (subsec=#{act.subsec})" 279 | end 280 | elsif exp.class != act.class 281 | # a subclass of Range, for example. 282 | exp_comment = " (#{exp.class})" 283 | act_comment = " (#{act.class})" 284 | end 285 | elsif !Encoding.compatible?(exp_str, act_str) 286 | if exp.is_a?(String) && act.is_a?(String) 287 | exp_str = exp.dump 288 | act_str = act.dump 289 | exp_comment = " (#{exp.encoding})" 290 | act_comment = " (#{act.encoding})" 291 | else 292 | exp_str = exp_str.dump 293 | act_str = act_str.dump 294 | end 295 | end 296 | "<#{exp_str}>#{exp_comment} expected but was\n<#{act_str}>#{act_comment}" 297 | } 298 | assert(exp == act, msg) 299 | end 300 | 301 | # :call-seq: 302 | # assert_not_nil( expression, failure_message = nil ) 303 | # 304 | #Tests if +expression+ is not nil. 305 | # 306 | #An optional failure message may be provided as the final argument. 307 | def assert_not_nil(exp, msg=nil) 308 | msg = message(msg) { "<#{mu_pp(exp)}> expected to not be nil" } 309 | assert(!exp.nil?, msg) 310 | end 311 | 312 | # :call-seq: 313 | # assert_not_equal( expected, actual, failure_message = nil ) 314 | # 315 | #Tests if +expected+ is not equal to +actual+. 316 | # 317 | #An optional failure message may be provided as the final argument. 318 | def assert_not_equal(exp, act, msg=nil) 319 | msg = message(msg) { "<#{mu_pp(exp)}> expected to be != to\n<#{mu_pp(act)}>" } 320 | assert(exp != act, msg) 321 | end 322 | 323 | # :call-seq: 324 | # assert_no_match( regexp, string, failure_message = nil ) 325 | # 326 | #Tests if the given Regexp does not match a given String. 327 | # 328 | #An optional failure message may be provided as the final argument. 329 | def assert_no_match(regexp, string, msg=nil) 330 | assert_instance_of(Regexp, regexp, "The first argument to assert_no_match should be a Regexp.") 331 | self._assertions -= 1 332 | msg = message(msg) { "<#{mu_pp(regexp)}> expected to not match\n<#{mu_pp(string)}>" } 333 | assert(regexp !~ string, msg) 334 | end 335 | 336 | # :call-seq: 337 | # assert_not_same( expected, actual, failure_message = nil ) 338 | # 339 | #Tests if +expected+ is not the same object as +actual+. 340 | #This test uses Object#equal? to test equality. 341 | # 342 | #An optional failure message may be provided as the final argument. 343 | # 344 | # assert_not_same("x", "x") #Succeeds 345 | def assert_not_same(expected, actual, message="") 346 | msg = message(msg) { build_message(message, < 348 | with id expected to not be equal\\? to 349 | 350 | with id . 351 | EOT 352 | assert(!actual.equal?(expected), msg) 353 | end 354 | 355 | # :call-seq: 356 | # assert_respond_to( object, method, failure_message = nil ) 357 | # 358 | #Tests if the given Object responds to +method+. 359 | # 360 | #An optional failure message may be provided as the final argument. 361 | # 362 | # assert_respond_to("hello", :reverse) #Succeeds 363 | # assert_respond_to("hello", :does_not_exist) #Fails 364 | def assert_respond_to(obj, (meth, *priv), msg = nil) 365 | unless priv.empty? 366 | msg = message(msg) { 367 | "Expected #{mu_pp(obj)} (#{obj.class}) to respond to ##{meth}#{" privately" if priv[0]}" 368 | } 369 | return assert obj.respond_to?(meth, *priv), msg 370 | end 371 | #get rid of overcounting 372 | if caller_locations(1, 1)[0].path.start_with?(MINI_DIR) 373 | return if obj.respond_to?(meth) 374 | end 375 | super(obj, meth, msg) 376 | end 377 | 378 | # :call-seq: 379 | # assert_not_respond_to( object, method, failure_message = nil ) 380 | # 381 | #Tests if the given Object does not respond to +method+. 382 | # 383 | #An optional failure message may be provided as the final argument. 384 | # 385 | # assert_not_respond_to("hello", :reverse) #Fails 386 | # assert_not_respond_to("hello", :does_not_exist) #Succeeds 387 | def assert_not_respond_to(obj, (meth, *priv), msg = nil) 388 | unless priv.empty? 389 | msg = message(msg) { 390 | "Expected #{mu_pp(obj)} (#{obj.class}) to not respond to ##{meth}#{" privately" if priv[0]}" 391 | } 392 | return assert !obj.respond_to?(meth, *priv), msg 393 | end 394 | #get rid of overcounting 395 | if caller_locations(1, 1)[0].path.start_with?(MINI_DIR) 396 | return unless obj.respond_to?(meth) 397 | end 398 | refute_respond_to(obj, meth, msg) 399 | end 400 | 401 | # :call-seq: 402 | # assert_send( +send_array+, failure_message = nil ) 403 | # 404 | # Passes if the method send returns a true value. 405 | # 406 | # +send_array+ is composed of: 407 | # * A receiver 408 | # * A method 409 | # * Arguments to the method 410 | # 411 | # Example: 412 | # assert_send(["Hello world", :include?, "Hello"]) # -> pass 413 | # assert_send(["Hello world", :include?, "Goodbye"]) # -> fail 414 | def assert_send send_ary, m = nil 415 | recv, msg, *args = send_ary 416 | m = message(m) { 417 | if args.empty? 418 | argsstr = "" 419 | else 420 | (argsstr = mu_pp(args)).sub!(/\A\[(.*)\]\z/m, '(\1)') 421 | end 422 | "Expected #{mu_pp(recv)}.#{msg}#{argsstr} to return true" 423 | } 424 | assert recv.__send__(msg, *args), m 425 | end 426 | 427 | # :call-seq: 428 | # assert_not_send( +send_array+, failure_message = nil ) 429 | # 430 | # Passes if the method send doesn't return a true value. 431 | # 432 | # +send_array+ is composed of: 433 | # * A receiver 434 | # * A method 435 | # * Arguments to the method 436 | # 437 | # Example: 438 | # assert_not_send([[1, 2], :member?, 1]) # -> fail 439 | # assert_not_send([[1, 2], :member?, 4]) # -> pass 440 | def assert_not_send send_ary, m = nil 441 | recv, msg, *args = send_ary 442 | m = message(m) { 443 | if args.empty? 444 | argsstr = "" 445 | else 446 | (argsstr = mu_pp(args)).sub!(/\A\[(.*)\]\z/m, '(\1)') 447 | end 448 | "Expected #{mu_pp(recv)}.#{msg}#{argsstr} to return false" 449 | } 450 | assert !recv.__send__(msg, *args), m 451 | end 452 | 453 | ms = instance_methods(true).map {|sym| sym.to_s } 454 | ms.grep(/\Arefute_/) do |m| 455 | mname = ('assert_not_' << m.to_s[/.*?_(.*)/, 1]) 456 | alias_method(mname, m) unless ms.include? mname 457 | end 458 | alias assert_include assert_includes 459 | alias assert_not_include assert_not_includes 460 | 461 | def assert_all?(obj, m = nil, &blk) 462 | failed = [] 463 | obj.each do |*a, &b| 464 | unless blk.call(*a, &b) 465 | failed << (a.size > 1 ? a : a[0]) 466 | end 467 | end 468 | assert(failed.empty?, message(m) {failed.pretty_inspect}) 469 | end 470 | 471 | def assert_not_all?(obj, m = nil, &blk) 472 | failed = [] 473 | obj.each do |*a, &b| 474 | if blk.call(*a, &b) 475 | failed << a.size > 1 ? a : a[0] 476 | end 477 | end 478 | assert(failed.empty?, message(m) {failed.pretty_inspect}) 479 | end 480 | 481 | # compatibility with test-unit 482 | alias pend skip 483 | 484 | def prepare_syntax_check(code, fname = caller_locations(2, 1)[0], mesg = fname.to_s, verbose: nil) 485 | code = code.dup.force_encoding(Encoding::UTF_8) 486 | verbose, $VERBOSE = $VERBOSE, verbose 487 | case 488 | when Array === fname 489 | fname, line = *fname 490 | when defined?(fname.path) && defined?(fname.lineno) 491 | fname, line = fname.path, fname.lineno 492 | else 493 | line = 1 494 | end 495 | yield(code, fname, line, mesg) 496 | ensure 497 | $VERBOSE = verbose 498 | end 499 | 500 | def assert_valid_syntax(code, *args) 501 | prepare_syntax_check(code, *args) do |src, fname, line, mesg| 502 | yield if defined?(yield) 503 | assert_nothing_raised(SyntaxError, mesg) do 504 | RubyVM::InstructionSequence.compile(src, fname, fname, line) 505 | end 506 | end 507 | end 508 | 509 | def assert_syntax_error(code, error, *args) 510 | prepare_syntax_check(code, *args) do |src, fname, line, mesg| 511 | yield if defined?(yield) 512 | e = assert_raise(SyntaxError, mesg) do 513 | RubyVM::InstructionSequence.compile(src, fname, fname, line) 514 | end 515 | assert_match(error, e.message, mesg) 516 | end 517 | end 518 | 519 | def assert_normal_exit(testsrc, message = '', child_env: nil, **opt) 520 | assert_valid_syntax(testsrc, caller_locations(1, 1)[0]) 521 | if child_env 522 | child_env = [child_env] 523 | else 524 | child_env = [] 525 | end 526 | out, _, status = EnvUtil.invoke_ruby(child_env + %W'-W0', testsrc, true, :merge_to_stdout, **opt) 527 | assert !status.signaled?, FailDesc[status, message, out] 528 | end 529 | 530 | FailDesc = proc do |status, message = "", out = ""| 531 | pid = status.pid 532 | now = Time.now 533 | faildesc = proc do 534 | if signo = status.termsig 535 | signame = Signal.signame(signo) 536 | sigdesc = "signal #{signo}" 537 | end 538 | log = EnvUtil.diagnostic_reports(signame, pid, now) 539 | if signame 540 | sigdesc = "SIG#{signame} (#{sigdesc})" 541 | end 542 | if status.coredump? 543 | sigdesc << " (core dumped)" 544 | end 545 | full_message = '' 546 | message = message.call if Proc === message 547 | if message and !message.empty? 548 | full_message << message << "\n" 549 | end 550 | full_message << "pid #{pid}" 551 | full_message << " exit #{status.exitstatus}" if status.exited? 552 | full_message << " killed by #{sigdesc}" if sigdesc 553 | if out and !out.empty? 554 | full_message << "\n#{out.b.gsub(/^/, '| ')}" 555 | full_message << "\n" if /\n\z/ !~ full_message 556 | end 557 | if log 558 | full_message << "\n#{log.b.gsub(/^/, '| ')}" 559 | end 560 | full_message 561 | end 562 | faildesc 563 | end 564 | 565 | def assert_in_out_err(args, test_stdin = "", test_stdout = [], test_stderr = [], message = nil, 566 | success: nil, **opt) 567 | stdout, stderr, status = EnvUtil.invoke_ruby(args, test_stdin, true, true, **opt) 568 | if signo = status.termsig 569 | EnvUtil.diagnostic_reports(Signal.signame(signo), status.pid, Time.now) 570 | end 571 | if block_given? 572 | raise "test_stdout ignored, use block only or without block" if test_stdout != [] 573 | raise "test_stderr ignored, use block only or without block" if test_stderr != [] 574 | yield(stdout.lines.map {|l| l.chomp }, stderr.lines.map {|l| l.chomp }, status) 575 | else 576 | all_assertions(message) do |a| 577 | [["stdout", test_stdout, stdout], ["stderr", test_stderr, stderr]].each do |key, exp, act| 578 | a.for(key) do 579 | if exp.is_a?(Regexp) 580 | assert_match(exp, act) 581 | elsif exp.all? {|e| String === e} 582 | assert_equal(exp, act.lines.map {|l| l.chomp }) 583 | else 584 | assert_pattern_list(exp, act) 585 | end 586 | end 587 | end 588 | unless success.nil? 589 | a.for("success?") do 590 | if success 591 | assert_predicate(status, :success?) 592 | else 593 | assert_not_predicate(status, :success?) 594 | end 595 | end 596 | end 597 | end 598 | status 599 | end 600 | end 601 | 602 | def assert_ruby_status(args, test_stdin="", message=nil, **opt) 603 | out, _, status = EnvUtil.invoke_ruby(args, test_stdin, true, :merge_to_stdout, **opt) 604 | desc = FailDesc[status, message, out] 605 | assert(!status.signaled?, desc) 606 | message ||= "ruby exit status is not success:" 607 | assert(status.success?, desc) 608 | end 609 | 610 | ABORT_SIGNALS = Signal.list.values_at(*%w"ILL ABRT BUS SEGV TERM") 611 | 612 | def assert_separately(args, file = nil, line = nil, src, ignore_stderr: nil, **opt) 613 | unless file and line 614 | loc, = caller_locations(1,1) 615 | file ||= loc.path 616 | line ||= loc.lineno 617 | end 618 | src = < marshal_error 638 | ignore_stderr = nil 639 | end 640 | if res 641 | if bt = res.backtrace 642 | bt.each do |l| 643 | l.sub!(/\A-:(\d+)/){"#{file}:#{line + $1.to_i}"} 644 | end 645 | bt.concat(caller) 646 | else 647 | res.set_backtrace(caller) 648 | end 649 | raise res unless SystemExit === res 650 | end 651 | 652 | # really is it succeed? 653 | unless ignore_stderr 654 | # the body of assert_separately must not output anything to detect error 655 | assert(stderr.empty?, FailDesc[status, "assert_separately failed with error message", stderr]) 656 | end 657 | assert(status.success?, FailDesc[status, "assert_separately failed", stderr]) 658 | raise marshal_error if marshal_error 659 | end 660 | 661 | def assert_warning(pat, msg = nil) 662 | stderr = EnvUtil.verbose_warning { 663 | EnvUtil.with_default_internal(pat.encoding) { 664 | yield 665 | } 666 | } 667 | msg = message(msg) {diff pat, stderr} 668 | assert(pat === stderr, msg) 669 | end 670 | 671 | def assert_warn(*args) 672 | assert_warning(*args) {$VERBOSE = false; yield} 673 | end 674 | 675 | def assert_no_memory_leak(args, prepare, code, message=nil, limit: 2.0, rss: false, **opt) 676 | require_relative '../../memory_status' 677 | raise MiniTest::Skip, "unsupported platform" unless defined?(Memory::Status) 678 | 679 | token = "\e[7;1m#{$$.to_s}:#{Time.now.strftime('%s.%L')}:#{rand(0x10000).to_s(16)}:\e[m" 680 | token_dump = token.dump 681 | token_re = Regexp.quote(token) 682 | envs = args.shift if Array === args and Hash === args.first 683 | args = [ 684 | "--disable=gems", 685 | "-r", File.expand_path("../../../memory_status", __FILE__), 686 | *args, 687 | "-v", "-", 688 | ] 689 | if defined? Memory::NO_MEMORY_LEAK_ENVS then 690 | envs ||= {} 691 | newenvs = envs.merge(Memory::NO_MEMORY_LEAK_ENVS) { |_, _, _| break } 692 | envs = newenvs if newenvs 693 | end 694 | args.unshift(envs) if envs 695 | cmd = [ 696 | 'END {STDERR.puts '"#{token_dump}"'"FINAL=#{Memory::Status.new}"}', 697 | prepare, 698 | 'STDERR.puts('"#{token_dump}"'"START=#{$initial_status = Memory::Status.new}")', 699 | '$initial_size = $initial_status.size', 700 | code, 701 | 'GC.start', 702 | ].join("\n") 703 | _, err, status = EnvUtil.invoke_ruby(args, cmd, true, true, **opt) 704 | before = err.sub!(/^#{token_re}START=(\{.*\})\n/, '') && Memory::Status.parse($1) 705 | after = err.sub!(/^#{token_re}FINAL=(\{.*\})\n/, '') && Memory::Status.parse($1) 706 | assert(status.success?, FailDesc[status, message, err]) 707 | ([:size, (rss && :rss)] & after.members).each do |n| 708 | b = before[n] 709 | a = after[n] 710 | next unless a > 0 and b > 0 711 | assert_operator(a.fdiv(b), :<, limit, message(message) {"#{n}: #{b} => #{a}"}) 712 | end 713 | rescue LoadError 714 | skip 715 | end 716 | 717 | def assert_is_minus_zero(f) 718 | assert(1.0/f == -Float::INFINITY, "#{f} is not -0.0") 719 | end 720 | 721 | def assert_file 722 | AssertFile 723 | end 724 | 725 | # pattern_list is an array which contains regexp and :*. 726 | # :* means any sequence. 727 | # 728 | # pattern_list is anchored. 729 | # Use [:*, regexp, :*] for non-anchored match. 730 | def assert_pattern_list(pattern_list, actual, message=nil) 731 | rest = actual 732 | anchored = true 733 | pattern_list.each_with_index {|pattern, i| 734 | if pattern == :* 735 | anchored = false 736 | else 737 | if anchored 738 | match = /\A#{pattern}/.match(rest) 739 | else 740 | match = pattern.match(rest) 741 | end 742 | unless match 743 | msg = message(msg) { 744 | expect_msg = "Expected #{mu_pp pattern}\n" 745 | if /\n[^\n]/ =~ rest 746 | actual_mesg = "to match\n" 747 | rest.scan(/.*\n+/) { 748 | actual_mesg << ' ' << $&.inspect << "+\n" 749 | } 750 | actual_mesg.sub!(/\+\n\z/, '') 751 | else 752 | actual_mesg = "to match #{mu_pp rest}" 753 | end 754 | actual_mesg << "\nafter #{i} patterns with #{actual.length - rest.length} characters" 755 | expect_msg + actual_mesg 756 | } 757 | assert false, msg 758 | end 759 | rest = match.post_match 760 | anchored = true 761 | end 762 | } 763 | if anchored 764 | assert_equal("", rest) 765 | end 766 | end 767 | 768 | # threads should respond to shift method. 769 | # Array can be used. 770 | def assert_join_threads(threads, message = nil) 771 | errs = [] 772 | values = [] 773 | while th = threads.shift 774 | begin 775 | values << th.value 776 | rescue Exception 777 | errs << [th, $!] 778 | end 779 | end 780 | if !errs.empty? 781 | msg = "exceptions on #{errs.length} threads:\n" + 782 | errs.map {|t, err| 783 | "#{t.inspect}:\n" + 784 | err.backtrace.map.with_index {|line, i| 785 | if i == 0 786 | "#{line}: #{err.message} (#{err.class})" 787 | else 788 | "\tfrom #{line}" 789 | end 790 | }.join("\n") 791 | }.join("\n---\n") 792 | if message 793 | msg = "#{message}\n#{msg}" 794 | end 795 | raise MiniTest::Assertion, msg 796 | end 797 | values 798 | end 799 | 800 | class << (AssertFile = Struct.new(:failure_message).new) 801 | include Assertions 802 | def assert_file_predicate(predicate, *args) 803 | if /\Anot_/ =~ predicate 804 | predicate = $' 805 | neg = " not" 806 | end 807 | result = File.__send__(predicate, *args) 808 | result = !result if neg 809 | mesg = "Expected file " << args.shift.inspect 810 | mesg << "#{neg} to be #{predicate}" 811 | mesg << mu_pp(args).sub(/\A\[(.*)\]\z/m, '(\1)') unless args.empty? 812 | mesg << " #{failure_message}" if failure_message 813 | assert(result, mesg) 814 | end 815 | alias method_missing assert_file_predicate 816 | 817 | def for(message) 818 | clone.tap {|a| a.failure_message = message} 819 | end 820 | end 821 | 822 | class AllFailures 823 | attr_reader :failures 824 | 825 | def initialize 826 | @count = 0 827 | @failures = {} 828 | end 829 | 830 | def for(key) 831 | @count += 1 832 | yield 833 | rescue Exception => e 834 | @failures[key] = [@count, e] 835 | end 836 | 837 | def message 838 | i = 0 839 | total = @count.to_s 840 | fmt = "%#{total.size}d" 841 | @failures.map {|k, (n, v)| 842 | "\n#{i+=1}. [#{fmt%n}/#{total}] Assertion for #{k.inspect}\n#{v.message.b.gsub(/^/, ' | ')}" 843 | }.join("\n") 844 | end 845 | 846 | def pass? 847 | @failures.empty? 848 | end 849 | end 850 | 851 | def assert_all_assertions(msg = nil) 852 | all = AllFailures.new 853 | yield all 854 | ensure 855 | assert(all.pass?, message(msg) {all.message.chomp(".")}) 856 | end 857 | alias all_assertions assert_all_assertions 858 | 859 | def build_message(head, template=nil, *arguments) #:nodoc: 860 | template &&= template.chomp 861 | template.gsub(/\G((?:[^\\]|\\.)*?)(\\)?\?/) { $1 + ($2 ? "?" : mu_pp(arguments.shift)) } 862 | end 863 | 864 | def message(msg = nil, *args, &default) # :nodoc: 865 | if Proc === msg 866 | super(nil, *args) do 867 | ary = [msg.call, (default.call if default)].compact.reject(&:empty?) 868 | if 1 < ary.length 869 | ary[0...-1] = ary[0...-1].map {|str| str.sub(/(?$()]/ ? s.inspect : s }.join " " 67 | @options = options 68 | end 69 | 70 | private 71 | def setup_options(opts, options) 72 | opts.separator 'minitest options:' 73 | opts.version = MiniTest::Unit::VERSION 74 | 75 | opts.on '-h', '--help', 'Display this help.' do 76 | puts opts 77 | exit 78 | end 79 | 80 | opts.on '-s', '--seed SEED', Integer, "Sets random seed" do |m| 81 | options[:seed] = m 82 | end 83 | 84 | opts.on '-v', '--verbose', "Verbose. Show progress processing files." do 85 | options[:verbose] = true 86 | self.verbose = options[:verbose] 87 | end 88 | 89 | opts.on '-n', '--name PATTERN', "Filter test method names on pattern: /REGEXP/, !/REGEXP/ or STRING" do |a| 90 | (options[:filter] ||= []) << a 91 | end 92 | 93 | opts.on '--test-order=random|alpha|sorted', [:random, :alpha, :sorted] do |a| 94 | MiniTest::Unit::TestCase.test_order = a 95 | end 96 | end 97 | 98 | def non_options(files, options) 99 | filter = options[:filter] 100 | if filter 101 | pos_pat = /\A\/(.*)\/\z/ 102 | neg_pat = /\A!\/(.*)\/\z/ 103 | negative, positive = filter.partition {|s| neg_pat =~ s} 104 | if positive.empty? 105 | filter = nil 106 | elsif negative.empty? and positive.size == 1 and pos_pat !~ positive[0] 107 | filter = positive[0] 108 | else 109 | filter = Regexp.union(*positive.map! {|s| Regexp.new(s[pos_pat, 1] || "\\A#{Regexp.quote(s)}\\z")}) 110 | end 111 | unless negative.empty? 112 | negative = Regexp.union(*negative.map! {|s| Regexp.new(s[neg_pat, 1])}) 113 | filter = /\A(?=.*#{filter})(?!.*#{negative})/ 114 | end 115 | if Regexp === filter 116 | # bypass conversion in minitest 117 | def filter.=~(other) # :nodoc: 118 | super unless Regexp === other 119 | end 120 | end 121 | options[:filter] = filter 122 | end 123 | true 124 | end 125 | end 126 | 127 | module Parallel # :nodoc: all 128 | def process_args(args = []) 129 | return @options if @options 130 | options = super 131 | if @options[:parallel] 132 | @files = args 133 | end 134 | options 135 | end 136 | 137 | def non_options(files, options) 138 | @jobserver = nil 139 | if !options[:parallel] and 140 | /(?:\A|\s)--jobserver-(?:auth|fds)=(\d+),(\d+)/ =~ ENV["MAKEFLAGS"] 141 | begin 142 | r = IO.for_fd($1.to_i(10), "rb", autoclose: false) 143 | w = IO.for_fd($2.to_i(10), "wb", autoclose: false) 144 | rescue 145 | r.close if r 146 | nil 147 | else 148 | @jobserver = [r, w] 149 | options[:parallel] ||= 1 150 | end 151 | end 152 | super 153 | end 154 | 155 | def status(*args) 156 | result = super 157 | raise @interrupt if @interrupt 158 | result 159 | end 160 | 161 | private 162 | def setup_options(opts, options) 163 | super 164 | 165 | opts.separator "parallel test options:" 166 | 167 | options[:retry] = true 168 | 169 | opts.on '-j N', '--jobs N', /\A(t)?(\d+)\z/, "Allow run tests with N jobs at once" do |_, t, a| 170 | options[:testing] = true & t # For testing 171 | options[:parallel] = a.to_i 172 | end 173 | 174 | opts.on '--separate', "Restart job process after one testcase has done" do 175 | options[:parallel] ||= 1 176 | options[:separate] = true 177 | end 178 | 179 | opts.on '--retry', "Retry running testcase when --jobs specified" do 180 | options[:retry] = true 181 | end 182 | 183 | opts.on '--no-retry', "Disable --retry" do 184 | options[:retry] = false 185 | end 186 | 187 | opts.on '--ruby VAL', "Path to ruby; It'll have used at -j option" do |a| 188 | options[:ruby] = a.split(/ /).reject(&:empty?) 189 | end 190 | end 191 | 192 | class Worker 193 | def self.launch(ruby,args=[]) 194 | io = IO.popen([*ruby, "-W1", 195 | "#{File.dirname(__FILE__)}/unit/parallel.rb", 196 | *args], "rb+") 197 | new(io, io.pid, :waiting) 198 | end 199 | 200 | attr_reader :quit_called 201 | 202 | def initialize(io, pid, status) 203 | @io = io 204 | @pid = pid 205 | @status = status 206 | @file = nil 207 | @real_file = nil 208 | @loadpath = [] 209 | @hooks = {} 210 | @quit_called = false 211 | end 212 | 213 | def puts(*args) 214 | @io.puts(*args) 215 | end 216 | 217 | def run(task,type) 218 | @file = File.basename(task, ".rb") 219 | @real_file = task 220 | begin 221 | puts "loadpath #{[Marshal.dump($:-@loadpath)].pack("m0")}" 222 | @loadpath = $:.dup 223 | puts "run #{task} #{type}" 224 | @status = :prepare 225 | rescue Errno::EPIPE 226 | died 227 | rescue IOError 228 | raise unless /stream closed|closed stream/ =~ $!.message 229 | died 230 | end 231 | end 232 | 233 | def hook(id,&block) 234 | @hooks[id] ||= [] 235 | @hooks[id] << block 236 | self 237 | end 238 | 239 | def read 240 | res = (@status == :quit) ? @io.read : @io.gets 241 | res && res.chomp 242 | end 243 | 244 | def close 245 | @io.close unless @io.closed? 246 | self 247 | rescue IOError 248 | end 249 | 250 | def quit 251 | return if @io.closed? 252 | @quit_called = true 253 | @io.puts "quit" 254 | end 255 | 256 | def kill 257 | Process.kill(:KILL, @pid) 258 | rescue Errno::ESRCH 259 | end 260 | 261 | def died(*additional) 262 | @status = :quit 263 | @io.close 264 | status = $? 265 | if status and status.signaled? 266 | additional[0] ||= SignalException.new(status.termsig) 267 | end 268 | 269 | call_hook(:dead,*additional) 270 | end 271 | 272 | def to_s 273 | if @file and @status != :ready 274 | "#{@pid}=#{@file}" 275 | else 276 | "#{@pid}:#{@status.to_s.ljust(7)}" 277 | end 278 | end 279 | 280 | attr_reader :io, :pid 281 | attr_accessor :status, :file, :real_file, :loadpath 282 | 283 | private 284 | 285 | def call_hook(id,*additional) 286 | @hooks[id] ||= [] 287 | @hooks[id].each{|hook| hook[self,additional] } 288 | self 289 | end 290 | 291 | end 292 | 293 | def after_worker_down(worker, e=nil, c=false) 294 | return unless @options[:parallel] 295 | return if @interrupt 296 | if @jobserver 297 | @jobserver[1] << @job_tokens 298 | @job_tokens.clear 299 | end 300 | warn e if e 301 | real_file = worker.real_file and warn "running file: #{real_file}" 302 | @need_quit = true 303 | warn "" 304 | warn "Some worker was crashed. It seems ruby interpreter's bug" 305 | warn "or, a bug of test/unit/parallel.rb. try again without -j" 306 | warn "option." 307 | warn "" 308 | STDERR.flush 309 | exit c 310 | end 311 | 312 | def after_worker_quit(worker) 313 | return unless @options[:parallel] 314 | return if @interrupt 315 | worker.close 316 | if @jobserver and !@job_tokens.empty? 317 | @jobserver[1] << @job_tokens.slice!(0) 318 | end 319 | @workers.delete(worker) 320 | @dead_workers << worker 321 | @ios = @workers.map(&:io) 322 | end 323 | 324 | def launch_worker 325 | begin 326 | worker = Worker.launch(@options[:ruby], @run_options) 327 | rescue => e 328 | abort "ERROR: Failed to launch job process - #{e.class}: #{e.message}" 329 | end 330 | worker.hook(:dead) do |w,info| 331 | after_worker_quit w 332 | after_worker_down w, *info if !info.empty? && !worker.quit_called 333 | end 334 | @workers << worker 335 | @ios << worker.io 336 | @workers_hash[worker.io] = worker 337 | worker 338 | end 339 | 340 | def delete_worker(worker) 341 | @workers_hash.delete worker.io 342 | @workers.delete worker 343 | @ios.delete worker.io 344 | end 345 | 346 | def quit_workers 347 | return if @workers.empty? 348 | @workers.reject! do |worker| 349 | begin 350 | Timeout.timeout(1) do 351 | worker.quit 352 | end 353 | rescue Errno::EPIPE 354 | rescue Timeout::Error 355 | end 356 | worker.close 357 | end 358 | 359 | return if @workers.empty? 360 | begin 361 | Timeout.timeout(0.2 * @workers.size) do 362 | Process.waitall 363 | end 364 | rescue Timeout::Error 365 | @workers.each do |worker| 366 | worker.kill 367 | end 368 | @worker.clear 369 | end 370 | end 371 | 372 | FakeClass = Struct.new(:name) 373 | def fake_class(name) 374 | (@fake_classes ||= {})[name] ||= FakeClass.new(name) 375 | end 376 | 377 | def deal(io, type, result, rep, shutting_down = false) 378 | worker = @workers_hash[io] 379 | cmd = worker.read 380 | cmd.sub!(/\A\.+/, '') if cmd # read may return nil 381 | case cmd 382 | when '' 383 | # just only dots, ignore 384 | when /^okay$/ 385 | worker.status = :running 386 | when /^ready(!)?$/ 387 | bang = $1 388 | worker.status = :ready 389 | 390 | unless task = @tasks.shift 391 | worker.quit 392 | return nil 393 | end 394 | if @options[:separate] and not bang 395 | worker.quit 396 | worker = add_worker 397 | end 398 | worker.run(task, type) 399 | @test_count += 1 400 | 401 | jobs_status(worker) 402 | when /^done (.+?)$/ 403 | begin 404 | r = Marshal.load($1.unpack("m")[0]) 405 | rescue 406 | print "unknown object: #{$1.unpack("m")[0].dump}" 407 | return true 408 | end 409 | result << r[0..1] unless r[0..1] == [nil,nil] 410 | rep << {file: worker.real_file, report: r[2], result: r[3], testcase: r[5]} 411 | $:.push(*r[4]).uniq! 412 | jobs_status(worker) if @options[:job_status] == :replace 413 | return true 414 | when /^record (.+?)$/ 415 | begin 416 | r = Marshal.load($1.unpack("m")[0]) 417 | rescue => e 418 | print "unknown record: #{e.message} #{$1.unpack("m")[0].dump}" 419 | return true 420 | end 421 | record(fake_class(r[0]), *r[1..-1]) 422 | when /^p (.+?)$/ 423 | del_jobs_status 424 | print $1.unpack("m")[0] 425 | jobs_status(worker) if @options[:job_status] == :replace 426 | when /^after (.+?)$/ 427 | @warnings << Marshal.load($1.unpack("m")[0]) 428 | when /^bye (.+?)$/ 429 | after_worker_down worker, Marshal.load($1.unpack("m")[0]) 430 | when /^bye$/, nil 431 | if shutting_down || worker.quit_called 432 | after_worker_quit worker 433 | else 434 | after_worker_down worker 435 | end 436 | else 437 | print "unknown command: #{cmd.dump}\n" 438 | end 439 | return false 440 | end 441 | 442 | def _run_parallel suites, type, result 443 | if @options[:parallel] < 1 444 | warn "Error: parameter of -j option should be greater than 0." 445 | return 446 | end 447 | 448 | # Require needed things for parallel running 449 | require 'thread' 450 | require 'timeout' 451 | @tasks = @files.dup # Array of filenames. 452 | @need_quit = false 453 | @dead_workers = [] # Array of dead workers. 454 | @warnings = [] 455 | @total_tests = @tasks.size.to_s(10) 456 | rep = [] # FIXME: more good naming 457 | 458 | @workers = [] # Array of workers. 459 | @workers_hash = {} # out-IO => worker 460 | @ios = [] # Array of worker IOs 461 | @job_tokens = String.new(encoding: Encoding::ASCII_8BIT) if @jobserver 462 | begin 463 | [@tasks.size, @options[:parallel]].min.times {launch_worker} 464 | 465 | while _io = IO.select(@ios)[0] 466 | break if _io.any? do |io| 467 | @need_quit or 468 | (deal(io, type, result, rep).nil? and 469 | !@workers.any? {|x| [:running, :prepare].include? x.status}) 470 | end 471 | if @jobserver and @job_tokens and !@tasks.empty? and !@workers.any? {|x| x.status == :ready} 472 | t = @jobserver[0].read_nonblock([@tasks.size, @options[:parallel]].min, exception: false) 473 | if String === t 474 | @job_tokens << t 475 | t.size.times {launch_worker} 476 | end 477 | end 478 | end 479 | rescue Interrupt => ex 480 | @interrupt = ex 481 | return result 482 | ensure 483 | if @interrupt 484 | @ios.select!{|x| @workers_hash[x].status == :running } 485 | while !@ios.empty? && (__io = IO.select(@ios,[],[],10)) 486 | __io[0].reject! {|io| deal(io, type, result, rep, true)} 487 | end 488 | end 489 | 490 | quit_workers 491 | 492 | unless @interrupt || !@options[:retry] || @need_quit 493 | parallel = @options[:parallel] 494 | @options[:parallel] = false 495 | suites, rep = rep.partition {|r| r[:testcase] && r[:file] && r[:report].any? {|e| !e[2].is_a?(MiniTest::Skip)}} 496 | suites.map {|r| r[:file]}.uniq.each {|file| require file} 497 | suites.map! {|r| eval("::"+r[:testcase])} 498 | del_status_line or puts 499 | unless suites.empty? 500 | puts "\n""Retrying..." 501 | _run_suites(suites, type) 502 | end 503 | @options[:parallel] = parallel 504 | end 505 | unless @options[:retry] 506 | del_status_line or puts 507 | end 508 | unless rep.empty? 509 | rep.each do |r| 510 | r[:report].each do |f| 511 | puke(*f) if f 512 | end 513 | end 514 | if @options[:retry] 515 | @errors += rep.map{|x| x[:result][0] }.inject(:+) 516 | @failures += rep.map{|x| x[:result][1] }.inject(:+) 517 | @skips += rep.map{|x| x[:result][2] }.inject(:+) 518 | end 519 | end 520 | unless @warnings.empty? 521 | warn "" 522 | @warnings.uniq! {|w| w[1].message} 523 | @warnings.each do |w| 524 | warn "#{w[0]}: #{w[1].message} (#{w[1].class})" 525 | end 526 | warn "" 527 | end 528 | end 529 | end 530 | 531 | def _run_suites suites, type 532 | _prepare_run(suites, type) 533 | @interrupt = nil 534 | result = [] 535 | GC.start 536 | if @options[:parallel] 537 | _run_parallel suites, type, result 538 | else 539 | suites.each {|suite| 540 | begin 541 | result << _run_suite(suite, type) 542 | rescue Interrupt => e 543 | @interrupt = e 544 | break 545 | end 546 | } 547 | end 548 | del_status_line 549 | result 550 | end 551 | end 552 | 553 | module Skipping # :nodoc: all 554 | def failed(s) 555 | super if !s or @options[:hide_skip] 556 | end 557 | 558 | private 559 | def setup_options(opts, options) 560 | super 561 | 562 | opts.separator "skipping options:" 563 | 564 | options[:hide_skip] = true 565 | 566 | opts.on '-q', '--hide-skip', 'Hide skipped tests' do 567 | options[:hide_skip] = true 568 | end 569 | 570 | opts.on '--show-skip', 'Show skipped tests' do 571 | options[:hide_skip] = false 572 | end 573 | end 574 | 575 | private 576 | def _run_suites(suites, type) 577 | result = super 578 | report.reject!{|r| r.start_with? "Skipped:" } if @options[:hide_skip] 579 | report.sort_by!{|r| r.start_with?("Skipped:") ? 0 : \ 580 | (r.start_with?("Failure:") ? 1 : 2) } 581 | failed(nil) 582 | result 583 | end 584 | end 585 | 586 | module Statistics 587 | def update_list(list, rec, max) 588 | if i = list.empty? ? 0 : list.bsearch_index {|*a| yield(*a)} 589 | list[i, 0] = [rec] 590 | list[max..-1] = [] if list.size >= max 591 | end 592 | end 593 | 594 | def record(suite, method, assertions, time, error) 595 | if @options.values_at(:longest, :most_asserted).any? 596 | @tops ||= {} 597 | rec = [suite.name, method, assertions, time, error] 598 | if max = @options[:longest] 599 | update_list(@tops[:longest] ||= [], rec, max) {|_,_,_,t,_|t 0 658 | end 659 | $stdout.flush if flush 660 | @status_line_size = 0 661 | end 662 | 663 | def add_status(line) 664 | @status_line_size ||= 0 665 | if @options[:job_status] == :replace 666 | line = line[0...(terminal_width-@status_line_size)] 667 | end 668 | print line 669 | @status_line_size += line.size 670 | end 671 | 672 | def jobs_status(worker) 673 | return if !@options[:job_status] or @options[:verbose] 674 | if @options[:job_status] == :replace 675 | status_line = @workers.map(&:to_s).join(" ") 676 | else 677 | status_line = worker.to_s 678 | end 679 | update_status(status_line) or (puts; nil) 680 | end 681 | 682 | def del_jobs_status 683 | return unless @options[:job_status] == :replace && @status_line_size.nonzero? 684 | del_status_line 685 | end 686 | 687 | def output 688 | (@output ||= nil) || super 689 | end 690 | 691 | def _prepare_run(suites, type) 692 | options[:job_status] ||= :replace if @tty && !@verbose 693 | case options[:color] 694 | when :always 695 | color = true 696 | when :auto, nil 697 | color = (@tty || @options[:job_status] == :replace) && /dumb/ !~ ENV["TERM"] 698 | else 699 | color = false 700 | end 701 | if color 702 | # dircolors-like style 703 | colors = (colors = ENV['TEST_COLORS']) ? Hash[colors.scan(/(\w+)=([^:\n]*)/)] : {} 704 | begin 705 | File.read(File.join(__dir__, "../../colors")).scan(/(\w+)=([^:\n]*)/) do |n, c| 706 | colors[n] ||= c 707 | end 708 | rescue 709 | end 710 | @passed_color = "\e[;#{colors["pass"] || "32"}m" 711 | @failed_color = "\e[;#{colors["fail"] || "31"}m" 712 | @skipped_color = "\e[;#{colors["skip"] || "33"}m" 713 | @reset_color = "\e[m" 714 | else 715 | @passed_color = @failed_color = @skipped_color = @reset_color = "" 716 | end 717 | if color or @options[:job_status] == :replace 718 | @verbose = !options[:parallel] 719 | end 720 | @output = Output.new(self) unless @options[:testing] 721 | filter = options[:filter] 722 | type = "#{type}_methods" 723 | total = if filter 724 | suites.inject(0) {|n, suite| n + suite.send(type).grep(filter).size} 725 | else 726 | suites.inject(0) {|n, suite| n + suite.send(type).size} 727 | end 728 | @test_count = 0 729 | @total_tests = total.to_s(10) 730 | end 731 | 732 | def new_test(s) 733 | @test_count += 1 734 | update_status(s) 735 | end 736 | 737 | def update_status(s) 738 | count = @test_count.to_s(10).rjust(@total_tests.size) 739 | del_status_line(false) 740 | print(@passed_color) 741 | add_status("[#{count}/#{@total_tests}]") 742 | print(@reset_color) 743 | add_status(" #{s}") 744 | $stdout.print "\r" if @options[:job_status] == :replace and !@verbose 745 | $stdout.flush 746 | end 747 | 748 | def _print(s); $stdout.print(s); end 749 | def succeed; del_status_line; end 750 | 751 | def failed(s) 752 | return if s and @options[:job_status] != :replace 753 | sep = "\n" 754 | @report_count ||= 0 755 | report.each do |msg| 756 | if msg.start_with? "Skipped:" 757 | if @options[:hide_skip] 758 | del_status_line 759 | next 760 | end 761 | color = @skipped_color 762 | else 763 | color = @failed_color 764 | end 765 | msg = msg.split(/$/, 2) 766 | $stdout.printf("%s%s%3d) %s%s%s\n", 767 | sep, color, @report_count += 1, 768 | msg[0], @reset_color, msg[1]) 769 | sep = nil 770 | end 771 | report.clear 772 | end 773 | 774 | def initialize 775 | super 776 | @tty = $stdout.tty? 777 | end 778 | 779 | def run(*args) 780 | result = super 781 | puts "\nruby -v: #{RUBY_DESCRIPTION}" 782 | result 783 | end 784 | 785 | private 786 | def setup_options(opts, options) 787 | super 788 | 789 | opts.separator "status line options:" 790 | 791 | options[:job_status] = nil 792 | 793 | opts.on '--jobs-status [TYPE]', [:normal, :replace, :none], 794 | "Show status of jobs every file; Disabled when --jobs isn't specified." do |type| 795 | options[:job_status] = (type || :normal if type != :none) 796 | end 797 | 798 | opts.on '--color[=WHEN]', 799 | [:always, :never, :auto], 800 | "colorize the output. WHEN defaults to 'always'", "or can be 'never' or 'auto'." do |c| 801 | options[:color] = c || :always 802 | end 803 | 804 | opts.on '--tty[=WHEN]', 805 | [:yes, :no], 806 | "force to output tty control. WHEN defaults to 'yes'", "or can be 'no'." do |c| 807 | @tty = c != :no 808 | end 809 | end 810 | 811 | class Output < Struct.new(:runner) # :nodoc: all 812 | def puts(*a) $stdout.puts(*a) unless a.empty? end 813 | def respond_to_missing?(*a) $stdout.respond_to?(*a) end 814 | def method_missing(*a, &b) $stdout.__send__(*a, &b) end 815 | 816 | def print(s) 817 | case s 818 | when /\A(.*\#.*) = \z/ 819 | runner.new_test($1) 820 | when /\A(.* s) = \z/ 821 | runner.add_status(" = #$1") 822 | when /\A\.+\z/ 823 | runner.succeed 824 | when /\A[EFS]\z/ 825 | runner.failed(s) 826 | else 827 | $stdout.print(s) 828 | end 829 | end 830 | end 831 | end 832 | 833 | module LoadPathOption # :nodoc: all 834 | def non_options(files, options) 835 | begin 836 | require "rbconfig" 837 | rescue LoadError 838 | warn "#{caller(1)[0]}: warning: Parallel running disabled because can't get path to ruby; run specify with --ruby argument" 839 | options[:parallel] = nil 840 | else 841 | options[:ruby] ||= [RbConfig.ruby] 842 | end 843 | 844 | super 845 | end 846 | 847 | def setup_options(parser, options) 848 | super 849 | parser.separator "load path options:" 850 | parser.on '-Idirectory', 'Add library load path' do |dirs| 851 | dirs.split(':').each { |d| $LOAD_PATH.unshift d } 852 | end 853 | end 854 | end 855 | 856 | module GlobOption # :nodoc: all 857 | @@testfile_prefix = "test" 858 | @@testfile_suffix = "test" 859 | 860 | def setup_options(parser, options) 861 | super 862 | parser.separator "globbing options:" 863 | parser.on '-b', '--basedir=DIR', 'Base directory of test suites.' do |dir| 864 | options[:base_directory] = dir 865 | end 866 | parser.on '-x', '--exclude REGEXP', 'Exclude test files on pattern.' do |pattern| 867 | (options[:reject] ||= []) << pattern 868 | end 869 | end 870 | 871 | def non_options(files, options) 872 | paths = [options.delete(:base_directory), nil].uniq 873 | if reject = options.delete(:reject) 874 | reject_pat = Regexp.union(reject.map {|r| %r"#{r}"}) 875 | end 876 | files.map! {|f| 877 | f = f.tr(File::ALT_SEPARATOR, File::SEPARATOR) if File::ALT_SEPARATOR 878 | ((paths if /\A\.\.?(?:\z|\/)/ !~ f) || [nil]).any? do |prefix| 879 | if prefix 880 | path = f.empty? ? prefix : "#{prefix}/#{f}" 881 | else 882 | next if f.empty? 883 | path = f 884 | end 885 | if !(match = (Dir["#{path}/**/#{@@testfile_prefix}_*.rb"] + Dir["#{path}/**/*_#{@@testfile_suffix}.rb"]).uniq).empty? 886 | if reject 887 | match.reject! {|n| 888 | n[(prefix.length+1)..-1] if prefix 889 | reject_pat =~ n 890 | } 891 | end 892 | break match 893 | elsif !reject or reject_pat !~ f and File.exist? path 894 | break path 895 | end 896 | end or 897 | raise ArgumentError, "file not found: #{f}" 898 | } 899 | files.flatten! 900 | super(files, options) 901 | end 902 | end 903 | 904 | module GCStressOption # :nodoc: all 905 | def setup_options(parser, options) 906 | super 907 | parser.separator "GC options:" 908 | parser.on '--[no-]gc-stress', 'Set GC.stress as true' do |flag| 909 | options[:gc_stress] = flag 910 | end 911 | end 912 | 913 | def non_options(files, options) 914 | if options.delete(:gc_stress) 915 | MiniTest::Unit::TestCase.class_eval do 916 | oldrun = instance_method(:run) 917 | define_method(:run) do |runner| 918 | begin 919 | gc_stress, GC.stress = GC.stress, true 920 | oldrun.bind(self).call(runner) 921 | ensure 922 | GC.stress = gc_stress 923 | end 924 | end 925 | end 926 | end 927 | super 928 | end 929 | end 930 | 931 | module RequireFiles # :nodoc: all 932 | def non_options(files, options) 933 | return false if !super 934 | errors = {} 935 | result = false 936 | files.each {|f| 937 | d = File.dirname(path = File.realpath(f)) 938 | unless $:.include? d 939 | $: << d 940 | end 941 | begin 942 | require path unless options[:parallel] 943 | result = true 944 | rescue LoadError 945 | next if errors[$!.message] 946 | errors[$!.message] = true 947 | puts "#{f}: #{$!}" 948 | end 949 | } 950 | result 951 | end 952 | end 953 | 954 | module RepeatOption # :nodoc: all 955 | def setup_options(parser, options) 956 | super 957 | options[:repeat_count] = nil 958 | parser.separator "repeat options:" 959 | parser.on '--repeat-count=NUM', "Number of times to repeat", Integer do |n| 960 | options[:repeat_count] = n 961 | end 962 | end 963 | 964 | def _run_anything(type) 965 | @repeat_count = @options[:repeat_count] 966 | super 967 | end 968 | end 969 | 970 | module ExcludesOption # :nodoc: all 971 | class ExcludedMethods < Struct.new(:excludes) 972 | def exclude(name, reason) 973 | excludes[name] = reason 974 | end 975 | 976 | def exclude_from(klass) 977 | excludes = self.excludes 978 | pattern = excludes.keys.grep(Regexp).tap {|k| 979 | break (Regexp.new(k.join('|')) unless k.empty?) 980 | } 981 | klass.class_eval do 982 | public_instance_methods(false).each do |method| 983 | if excludes[method] or (pattern and pattern =~ method) 984 | remove_method(method) 985 | end 986 | end 987 | public_instance_methods(true).each do |method| 988 | if excludes[method] or (pattern and pattern =~ method) 989 | undef_method(method) 990 | end 991 | end 992 | end 993 | end 994 | 995 | def self.load(dirs, name) 996 | return unless dirs and name 997 | instance = nil 998 | dirs.each do |dir| 999 | path = File.join(dir, name.gsub(/::/, '/') + ".rb") 1000 | begin 1001 | src = File.read(path) 1002 | rescue Errno::ENOENT 1003 | nil 1004 | else 1005 | instance ||= new({}) 1006 | instance.instance_eval(src, path) 1007 | end 1008 | end 1009 | instance 1010 | end 1011 | end 1012 | 1013 | def setup_options(parser, options) 1014 | super 1015 | if excludes = ENV["EXCLUDES"] 1016 | excludes = excludes.split(File::PATH_SEPARATOR) 1017 | end 1018 | options[:excludes] = excludes || [] 1019 | parser.separator "excludes options:" 1020 | parser.on '-X', '--excludes-dir DIRECTORY', "Directory name of exclude files" do |d| 1021 | options[:excludes].concat d.split(File::PATH_SEPARATOR) 1022 | end 1023 | end 1024 | 1025 | def _run_suite(suite, type) 1026 | if ex = ExcludedMethods.load(@options[:excludes], suite.name) 1027 | ex.exclude_from(suite) 1028 | end 1029 | super 1030 | end 1031 | end 1032 | 1033 | module SubprocessOption 1034 | def setup_options(parser, options) 1035 | super 1036 | parser.separator "subprocess options:" 1037 | parser.on '--subprocess-timeout-scale NUM', "Scale subprocess timeout", Float do |scale| 1038 | raise OptionParser::InvalidArgument, "timeout scale must be positive" unless scale > 0 1039 | options[:timeout_scale] = scale 1040 | end 1041 | if scale = options[:timeout_scale] or 1042 | (scale = ENV["RUBY_TEST_SUBPROCESS_TIMEOUT_SCALE"] and (scale = scale.to_f) > 0) 1043 | EnvUtil.subprocess_timeout_scale = scale 1044 | end 1045 | end 1046 | end 1047 | 1048 | class Runner < MiniTest::Unit # :nodoc: all 1049 | include Test::Unit::Options 1050 | include Test::Unit::StatusLine 1051 | include Test::Unit::Parallel 1052 | include Test::Unit::Statistics 1053 | include Test::Unit::Skipping 1054 | include Test::Unit::GlobOption 1055 | include Test::Unit::RepeatOption 1056 | include Test::Unit::LoadPathOption 1057 | include Test::Unit::GCStressOption 1058 | include Test::Unit::ExcludesOption 1059 | include Test::Unit::SubprocessOption 1060 | include Test::Unit::RunCount 1061 | 1062 | class << self; undef autorun; end 1063 | 1064 | @@stop_auto_run = false 1065 | def self.autorun 1066 | at_exit { 1067 | Test::Unit::RunCount.run_once { 1068 | exit(Test::Unit::Runner.new.run(ARGV) || true) 1069 | } unless @@stop_auto_run 1070 | } unless @@installed_at_exit 1071 | @@installed_at_exit = true 1072 | end 1073 | 1074 | alias mini_run_suite _run_suite 1075 | 1076 | # Overriding of MiniTest::Unit#puke 1077 | def puke klass, meth, e 1078 | # TODO: 1079 | # this overriding is for minitest feature that skip messages are 1080 | # hidden when not verbose (-v), note this is temporally. 1081 | n = report.size 1082 | rep = super 1083 | if MiniTest::Skip === e and /no message given\z/ =~ e.message 1084 | report.slice!(n..-1) 1085 | rep = "." 1086 | end 1087 | rep 1088 | end 1089 | end 1090 | 1091 | class AutoRunner # :nodoc: all 1092 | class Runner < Test::Unit::Runner 1093 | include Test::Unit::RequireFiles 1094 | end 1095 | 1096 | attr_accessor :to_run, :options 1097 | 1098 | def initialize(force_standalone = false, default_dir = nil, argv = ARGV) 1099 | @force_standalone = force_standalone 1100 | @runner = Runner.new do |files, options| 1101 | options[:base_directory] ||= default_dir 1102 | files << default_dir if files.empty? and default_dir 1103 | @to_run = files 1104 | yield self if block_given? 1105 | files 1106 | end 1107 | Runner.runner = @runner 1108 | @options = @runner.option_parser 1109 | if @force_standalone 1110 | @options.banner.sub!(/\[options\]/, '\& tests...') 1111 | end 1112 | @argv = argv 1113 | end 1114 | 1115 | def process_args(*args) 1116 | @runner.process_args(*args) 1117 | !@to_run.empty? 1118 | end 1119 | 1120 | def run 1121 | if @force_standalone and not process_args(@argv) 1122 | abort @options.banner 1123 | end 1124 | @runner.run(@argv) || true 1125 | end 1126 | 1127 | def self.run(*args) 1128 | new(*args).run 1129 | end 1130 | end 1131 | 1132 | class ProxyError < StandardError # :nodoc: all 1133 | def initialize(ex) 1134 | @message = ex.message 1135 | @backtrace = ex.backtrace 1136 | end 1137 | 1138 | attr_accessor :message, :backtrace 1139 | end 1140 | end 1141 | end 1142 | 1143 | module MiniTest # :nodoc: all 1144 | class Unit 1145 | end 1146 | end 1147 | 1148 | class MiniTest::Unit::TestCase # :nodoc: all 1149 | test_order = self.test_order 1150 | class << self 1151 | attr_writer :test_order 1152 | undef test_order 1153 | end 1154 | def self.test_order 1155 | defined?(@test_order) ? @test_order : superclass.test_order 1156 | end 1157 | self.test_order = test_order 1158 | undef run_test 1159 | RUN_TEST_TRACE = "#{__FILE__}:#{__LINE__+3}:in `run_test'".freeze 1160 | def run_test(name) 1161 | progname, $0 = $0, "#{$0}: #{self.class}##{name}" 1162 | self.__send__(name) 1163 | ensure 1164 | $@.delete(RUN_TEST_TRACE) if $@ 1165 | $0 = progname 1166 | end 1167 | end 1168 | 1169 | Test::Unit::Runner.autorun 1170 | -------------------------------------------------------------------------------- /test/lib/minitest/unit.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # frozen_string_literal: false 3 | 4 | require "optparse" 5 | require "rbconfig" 6 | require "leakchecker" 7 | 8 | ## 9 | # Minimal (mostly drop-in) replacement for test-unit. 10 | # 11 | # :include: README.txt 12 | 13 | module MiniTest 14 | 15 | def self.const_missing name # :nodoc: 16 | case name 17 | when :MINI_DIR then 18 | msg = "MiniTest::MINI_DIR was removed. Don't violate other's internals." 19 | warn "WAR\NING: #{msg}" 20 | warn "WAR\NING: Used by #{caller.first}." 21 | const_set :MINI_DIR, "bad value" 22 | else 23 | super 24 | end 25 | end 26 | 27 | ## 28 | # Assertion base class 29 | 30 | class Assertion < Exception; end 31 | 32 | ## 33 | # Assertion raised when skipping a test 34 | 35 | class Skip < Assertion; end 36 | 37 | class << self 38 | ## 39 | # Filter object for backtraces. 40 | 41 | attr_accessor :backtrace_filter 42 | end 43 | 44 | class BacktraceFilter # :nodoc: 45 | def filter bt 46 | return ["No backtrace"] unless bt 47 | 48 | new_bt = [] 49 | 50 | unless $DEBUG then 51 | bt.each do |line| 52 | break if line =~ /lib\/minitest/ 53 | new_bt << line 54 | end 55 | 56 | new_bt = bt.reject { |line| line =~ /lib\/minitest/ } if new_bt.empty? 57 | new_bt = bt.dup if new_bt.empty? 58 | else 59 | new_bt = bt.dup 60 | end 61 | 62 | new_bt 63 | end 64 | end 65 | 66 | self.backtrace_filter = BacktraceFilter.new 67 | 68 | def self.filter_backtrace bt # :nodoc: 69 | backtrace_filter.filter bt 70 | end 71 | 72 | ## 73 | # MiniTest Assertions. All assertion methods accept a +msg+ which is 74 | # printed if the assertion fails. 75 | 76 | module Assertions 77 | ## 78 | # Returns the diff command to use in #diff. Tries to intelligently 79 | # figure out what diff to use. 80 | 81 | def self.diff 82 | @diff = if (RbConfig::CONFIG['host_os'] =~ /mswin|mingw/ && 83 | system("diff.exe", __FILE__, __FILE__)) then 84 | "diff.exe -u" 85 | elsif Minitest::Unit::Guard.maglev? then # HACK 86 | "diff -u" 87 | elsif system("gdiff", __FILE__, __FILE__) 88 | "gdiff -u" # solaris and kin suck 89 | elsif system("diff", __FILE__, __FILE__) 90 | "diff -u" 91 | else 92 | nil 93 | end unless defined? @diff 94 | 95 | @diff 96 | end 97 | 98 | ## 99 | # Set the diff command to use in #diff. 100 | 101 | def self.diff= o 102 | @diff = o 103 | end 104 | 105 | ## 106 | # Returns a diff between +exp+ and +act+. If there is no known 107 | # diff command or if it doesn't make sense to diff the output 108 | # (single line, short output), then it simply returns a basic 109 | # comparison between the two. 110 | 111 | def diff exp, act 112 | require "tempfile" 113 | 114 | expect = mu_pp_for_diff exp 115 | butwas = mu_pp_for_diff act 116 | result = nil 117 | 118 | need_to_diff = 119 | MiniTest::Assertions.diff && 120 | (expect.include?("\n") || 121 | butwas.include?("\n") || 122 | expect.size > 30 || 123 | butwas.size > 30 || 124 | expect == butwas) 125 | 126 | return "Expected: #{mu_pp exp}\n Actual: #{mu_pp act}" unless 127 | need_to_diff 128 | 129 | tempfile_a = nil 130 | tempfile_b = nil 131 | 132 | Tempfile.open("expect") do |a| 133 | tempfile_a = a 134 | a.puts expect 135 | a.flush 136 | 137 | Tempfile.open("butwas") do |b| 138 | tempfile_b = b 139 | b.puts butwas 140 | b.flush 141 | 142 | result = `#{MiniTest::Assertions.diff} #{a.path} #{b.path}` 143 | result.sub!(/^\-\-\- .+/, "--- expected") 144 | result.sub!(/^\+\+\+ .+/, "+++ actual") 145 | 146 | if result.empty? then 147 | klass = exp.class 148 | result = [ 149 | "No visible difference in the #{klass}#inspect output.\n", 150 | "You should look at the implementation of #== on ", 151 | "#{klass} or its members.\n", 152 | expect, 153 | ].join 154 | end 155 | end 156 | end 157 | 158 | result 159 | ensure 160 | tempfile_a.close! if tempfile_a 161 | tempfile_b.close! if tempfile_b 162 | end 163 | 164 | ## 165 | # This returns a human-readable version of +obj+. By default 166 | # #inspect is called. You can override this to use #pretty_print 167 | # if you want. 168 | 169 | def mu_pp obj 170 | s = obj.inspect 171 | s = s.encode Encoding.default_external if defined? Encoding 172 | s 173 | end 174 | 175 | ## 176 | # This returns a diff-able human-readable version of +obj+. This 177 | # differs from the regular mu_pp because it expands escaped 178 | # newlines and makes hex-values generic (like object_ids). This 179 | # uses mu_pp to do the first pass and then cleans it up. 180 | 181 | def mu_pp_for_diff obj 182 | mu_pp(obj).gsub(/\\n/, "\n").gsub(/:0x[a-fA-F0-9]{4,}/m, ':0xXXXXXX') 183 | end 184 | 185 | def _assertions= n # :nodoc: 186 | @_assertions = n 187 | end 188 | 189 | def _assertions # :nodoc: 190 | @_assertions ||= 0 191 | end 192 | 193 | ## 194 | # Fails unless +test+ is a true value. 195 | 196 | def assert test, msg = nil 197 | msg ||= "Failed assertion, no message given." 198 | self._assertions += 1 199 | unless test then 200 | msg = msg.call if Proc === msg 201 | raise MiniTest::Assertion, msg 202 | end 203 | true 204 | end 205 | 206 | ## 207 | # Fails unless +obj+ is empty. 208 | 209 | def assert_empty obj, msg = nil 210 | msg = message(msg) { "Expected #{mu_pp(obj)} to be empty" } 211 | assert_respond_to obj, :empty? 212 | assert obj.empty?, msg 213 | end 214 | 215 | ## 216 | # Fails unless exp == act printing the difference between 217 | # the two, if possible. 218 | # 219 | # If there is no visible difference but the assertion fails, you 220 | # should suspect that your #== is buggy, or your inspect output is 221 | # missing crucial details. 222 | # 223 | # For floats use assert_in_delta. 224 | # 225 | # See also: MiniTest::Assertions.diff 226 | 227 | def assert_equal exp, act, msg = nil 228 | msg = message(msg, "") { diff exp, act } 229 | assert exp == act, msg 230 | end 231 | 232 | ## 233 | # For comparing Floats. Fails unless +exp+ and +act+ are within +delta+ 234 | # of each other. 235 | # 236 | # assert_in_delta Math::PI, (22.0 / 7.0), 0.01 237 | 238 | def assert_in_delta exp, act, delta = 0.001, msg = nil 239 | n = (exp - act).abs 240 | msg = message(msg) { 241 | "Expected |#{exp} - #{act}| (#{n}) to be <= #{delta}" 242 | } 243 | assert delta >= n, msg 244 | end 245 | 246 | ## 247 | # For comparing Floats. Fails unless +exp+ and +act+ have a relative 248 | # error less than +epsilon+. 249 | 250 | def assert_in_epsilon a, b, epsilon = 0.001, msg = nil 251 | assert_in_delta a, b, [a.abs, b.abs].min * epsilon, msg 252 | end 253 | 254 | ## 255 | # Fails unless +collection+ includes +obj+. 256 | 257 | def assert_includes collection, obj, msg = nil 258 | msg = message(msg) { 259 | "Expected #{mu_pp(collection)} to include #{mu_pp(obj)}" 260 | } 261 | assert_respond_to collection, :include? 262 | assert collection.include?(obj), msg 263 | end 264 | 265 | ## 266 | # Fails unless +obj+ is an instance of +cls+. 267 | 268 | def assert_instance_of cls, obj, msg = nil 269 | msg = message(msg) { 270 | "Expected #{mu_pp(obj)} to be an instance of #{cls}, not #{obj.class}" 271 | } 272 | 273 | assert obj.instance_of?(cls), msg 274 | end 275 | 276 | ## 277 | # Fails unless +obj+ is a kind of +cls+. 278 | 279 | def assert_kind_of cls, obj, msg = nil # TODO: merge with instance_of 280 | msg = message(msg) { 281 | "Expected #{mu_pp(obj)} to be a kind of #{cls}, not #{obj.class}" } 282 | 283 | assert obj.kind_of?(cls), msg 284 | end 285 | 286 | ## 287 | # Fails unless +matcher+ =~ +obj+. 288 | 289 | def assert_match matcher, obj, msg = nil 290 | msg = message(msg) { "Expected #{mu_pp matcher} to match #{mu_pp obj}" } 291 | assert_respond_to matcher, :"=~" 292 | matcher = Regexp.new Regexp.escape matcher if String === matcher 293 | assert matcher =~ obj, msg 294 | end 295 | 296 | ## 297 | # Fails unless +obj+ is nil 298 | 299 | def assert_nil obj, msg = nil 300 | msg = message(msg) { "Expected #{mu_pp(obj)} to be nil" } 301 | assert obj.nil?, msg 302 | end 303 | 304 | ## 305 | # For testing with binary operators. 306 | # 307 | # assert_operator 5, :<=, 4 308 | 309 | def assert_operator o1, op, o2 = (predicate = true; nil), msg = nil 310 | return assert_predicate o1, op, msg if predicate 311 | msg = message(msg) { "Expected #{mu_pp(o1)} to be #{op} #{mu_pp(o2)}" } 312 | assert o1.__send__(op, o2), msg 313 | end 314 | 315 | ## 316 | # Fails if stdout or stderr do not output the expected results. 317 | # Pass in nil if you don't care about that streams output. Pass in 318 | # "" if you require it to be silent. Pass in a regexp if you want 319 | # to pattern match. 320 | # 321 | # NOTE: this uses #capture_io, not #capture_subprocess_io. 322 | # 323 | # See also: #assert_silent 324 | 325 | def assert_output stdout = nil, stderr = nil 326 | out, err = capture_io do 327 | yield 328 | end 329 | 330 | err_msg = Regexp === stderr ? :assert_match : :assert_equal if stderr 331 | out_msg = Regexp === stdout ? :assert_match : :assert_equal if stdout 332 | 333 | y = send err_msg, stderr, err, "In stderr" if err_msg 334 | x = send out_msg, stdout, out, "In stdout" if out_msg 335 | 336 | (!stdout || x) && (!stderr || y) 337 | end 338 | 339 | ## 340 | # For testing with predicates. 341 | # 342 | # assert_predicate str, :empty? 343 | # 344 | # This is really meant for specs and is front-ended by assert_operator: 345 | # 346 | # str.must_be :empty? 347 | 348 | def assert_predicate o1, op, msg = nil 349 | msg = message(msg) { "Expected #{mu_pp(o1)} to be #{op}" } 350 | assert o1.__send__(op), msg 351 | end 352 | 353 | ## 354 | # Fails unless the block raises one of +exp+. Returns the 355 | # exception matched so you can check the message, attributes, etc. 356 | 357 | def assert_raises *exp 358 | msg = "#{exp.pop}.\n" if String === exp.last 359 | 360 | begin 361 | yield 362 | rescue MiniTest::Skip => e 363 | return e if exp.include? MiniTest::Skip 364 | raise e 365 | rescue Exception => e 366 | expected = exp.any? { |ex| 367 | if ex.instance_of? Module then 368 | e.kind_of? ex 369 | else 370 | e.instance_of? ex 371 | end 372 | } 373 | 374 | assert expected, proc { 375 | exception_details(e, "#{msg}#{mu_pp(exp)} exception expected, not") 376 | } 377 | 378 | return e 379 | end 380 | 381 | exp = exp.first if exp.size == 1 382 | 383 | flunk "#{msg}#{mu_pp(exp)} expected but nothing was raised." 384 | end 385 | 386 | ## 387 | # Fails unless +obj+ responds to +meth+. 388 | 389 | def assert_respond_to obj, meth, msg = nil 390 | msg = message(msg) { 391 | "Expected #{mu_pp(obj)} (#{obj.class}) to respond to ##{meth}" 392 | } 393 | assert obj.respond_to?(meth), msg 394 | end 395 | 396 | ## 397 | # Fails unless +exp+ and +act+ are #equal? 398 | 399 | def assert_same exp, act, msg = nil 400 | msg = message(msg) { 401 | data = [mu_pp(act), act.object_id, mu_pp(exp), exp.object_id] 402 | "Expected %s (oid=%d) to be the same as %s (oid=%d)" % data 403 | } 404 | assert exp.equal?(act), msg 405 | end 406 | 407 | ## 408 | # +send_ary+ is a receiver, message and arguments. 409 | # 410 | # Fails unless the call returns a true value 411 | # TODO: I should prolly remove this from specs 412 | 413 | def assert_send send_ary, m = nil 414 | recv, msg, *args = send_ary 415 | m = message(m) { 416 | "Expected #{mu_pp(recv)}.#{msg}(*#{mu_pp(args)}) to return true" } 417 | assert recv.__send__(msg, *args), m 418 | end 419 | 420 | ## 421 | # Fails if the block outputs anything to stderr or stdout. 422 | # 423 | # See also: #assert_output 424 | 425 | def assert_silent 426 | assert_output "", "" do 427 | yield 428 | end 429 | end 430 | 431 | ## 432 | # Fails unless the block throws +sym+ 433 | 434 | def assert_throws sym, msg = nil 435 | default = "Expected #{mu_pp(sym)} to have been thrown" 436 | caught = true 437 | catch(sym) do 438 | begin 439 | yield 440 | rescue ThreadError => e # wtf?!? 1.8 + threads == suck 441 | default += ", not \:#{e.message[/uncaught throw \`(\w+?)\'/, 1]}" 442 | rescue ArgumentError => e # 1.9 exception 443 | default += ", not #{e.message.split(/ /).last}" 444 | rescue NameError => e # 1.8 exception 445 | default += ", not #{e.name.inspect}" 446 | end 447 | caught = false 448 | end 449 | 450 | assert caught, message(msg) { default } 451 | end 452 | 453 | ## 454 | # Captures $stdout and $stderr into strings: 455 | # 456 | # out, err = capture_io do 457 | # puts "Some info" 458 | # warn "You did a bad thing" 459 | # end 460 | # 461 | # assert_match %r%info%, out 462 | # assert_match %r%bad%, err 463 | # 464 | # NOTE: For efficiency, this method uses StringIO and does not 465 | # capture IO for subprocesses. Use #capture_subprocess_io for 466 | # that. 467 | 468 | def capture_io 469 | require 'stringio' 470 | 471 | captured_stdout, captured_stderr = StringIO.new, StringIO.new 472 | 473 | synchronize do 474 | orig_stdout, orig_stderr = $stdout, $stderr 475 | $stdout, $stderr = captured_stdout, captured_stderr 476 | 477 | begin 478 | yield 479 | ensure 480 | $stdout = orig_stdout 481 | $stderr = orig_stderr 482 | end 483 | end 484 | 485 | return captured_stdout.string, captured_stderr.string 486 | end 487 | 488 | ## 489 | # Captures $stdout and $stderr into strings, using Tempfile to 490 | # ensure that subprocess IO is captured as well. 491 | # 492 | # out, err = capture_subprocess_io do 493 | # system "echo Some info" 494 | # system "echo You did a bad thing 1>&2" 495 | # end 496 | # 497 | # assert_match %r%info%, out 498 | # assert_match %r%bad%, err 499 | # 500 | # NOTE: This method is approximately 10x slower than #capture_io so 501 | # only use it when you need to test the output of a subprocess. 502 | 503 | def capture_subprocess_io 504 | require 'tempfile' 505 | 506 | captured_stdout, captured_stderr = Tempfile.new("out"), Tempfile.new("err") 507 | 508 | synchronize do 509 | orig_stdout, orig_stderr = $stdout.dup, $stderr.dup 510 | $stdout.reopen captured_stdout 511 | $stderr.reopen captured_stderr 512 | 513 | begin 514 | yield 515 | 516 | $stdout.rewind 517 | $stderr.rewind 518 | 519 | [captured_stdout.read, captured_stderr.read] 520 | ensure 521 | $stdout.reopen orig_stdout 522 | $stderr.reopen orig_stderr 523 | orig_stdout.close 524 | orig_stderr.close 525 | captured_stdout.close! 526 | captured_stderr.close! 527 | end 528 | end 529 | end 530 | 531 | ## 532 | # Returns details for exception +e+ 533 | 534 | def exception_details e, msg 535 | [ 536 | "#{msg}", 537 | "Class: <#{e.class}>", 538 | "Message: <#{e.message.inspect}>", 539 | "---Backtrace---", 540 | "#{MiniTest::filter_backtrace(e.backtrace).join("\n")}", 541 | "---------------", 542 | ].join "\n" 543 | end 544 | 545 | ## 546 | # Fails with +msg+ 547 | 548 | def flunk msg = nil 549 | msg ||= "Epic Fail!" 550 | assert false, msg 551 | end 552 | 553 | ## 554 | # Returns a proc that will output +msg+ along with the default message. 555 | 556 | def message msg = nil, ending = ".", &default 557 | proc { 558 | msg = msg.call.chomp(".") if Proc === msg 559 | custom_message = "#{msg}.\n" unless msg.nil? or msg.to_s.empty? 560 | "#{custom_message}#{default.call}#{ending}" 561 | } 562 | end 563 | 564 | ## 565 | # used for counting assertions 566 | 567 | def pass msg = nil 568 | assert true 569 | end 570 | 571 | ## 572 | # Fails if +test+ is a true value 573 | 574 | def refute test, msg = nil 575 | msg ||= "Failed refutation, no message given" 576 | not assert(! test, msg) 577 | end 578 | 579 | ## 580 | # Fails if +obj+ is empty. 581 | 582 | def refute_empty obj, msg = nil 583 | msg = message(msg) { "Expected #{mu_pp(obj)} to not be empty" } 584 | assert_respond_to obj, :empty? 585 | refute obj.empty?, msg 586 | end 587 | 588 | ## 589 | # Fails if exp == act. 590 | # 591 | # For floats use refute_in_delta. 592 | 593 | def refute_equal exp, act, msg = nil 594 | msg = message(msg) { 595 | "Expected #{mu_pp(act)} to not be equal to #{mu_pp(exp)}" 596 | } 597 | refute exp == act, msg 598 | end 599 | 600 | ## 601 | # For comparing Floats. Fails if +exp+ is within +delta+ of +act+. 602 | # 603 | # refute_in_delta Math::PI, (22.0 / 7.0) 604 | 605 | def refute_in_delta exp, act, delta = 0.001, msg = nil 606 | n = (exp - act).abs 607 | msg = message(msg) { 608 | "Expected |#{exp} - #{act}| (#{n}) to not be <= #{delta}" 609 | } 610 | refute delta >= n, msg 611 | end 612 | 613 | ## 614 | # For comparing Floats. Fails if +exp+ and +act+ have a relative error 615 | # less than +epsilon+. 616 | 617 | def refute_in_epsilon a, b, epsilon = 0.001, msg = nil 618 | refute_in_delta a, b, a * epsilon, msg 619 | end 620 | 621 | ## 622 | # Fails if +collection+ includes +obj+. 623 | 624 | def refute_includes collection, obj, msg = nil 625 | msg = message(msg) { 626 | "Expected #{mu_pp(collection)} to not include #{mu_pp(obj)}" 627 | } 628 | assert_respond_to collection, :include? 629 | refute collection.include?(obj), msg 630 | end 631 | 632 | ## 633 | # Fails if +obj+ is an instance of +cls+. 634 | 635 | def refute_instance_of cls, obj, msg = nil 636 | msg = message(msg) { 637 | "Expected #{mu_pp(obj)} to not be an instance of #{cls}" 638 | } 639 | refute obj.instance_of?(cls), msg 640 | end 641 | 642 | ## 643 | # Fails if +obj+ is a kind of +cls+. 644 | 645 | def refute_kind_of cls, obj, msg = nil # TODO: merge with instance_of 646 | msg = message(msg) { "Expected #{mu_pp(obj)} to not be a kind of #{cls}" } 647 | refute obj.kind_of?(cls), msg 648 | end 649 | 650 | ## 651 | # Fails if +matcher+ =~ +obj+. 652 | 653 | def refute_match matcher, obj, msg = nil 654 | msg = message(msg) {"Expected #{mu_pp matcher} to not match #{mu_pp obj}"} 655 | assert_respond_to matcher, :"=~" 656 | matcher = Regexp.new Regexp.escape matcher if String === matcher 657 | refute matcher =~ obj, msg 658 | end 659 | 660 | ## 661 | # Fails if +obj+ is nil. 662 | 663 | def refute_nil obj, msg = nil 664 | msg = message(msg) { "Expected #{mu_pp(obj)} to not be nil" } 665 | refute obj.nil?, msg 666 | end 667 | 668 | ## 669 | # Fails if +o1+ is not +op+ +o2+. Eg: 670 | # 671 | # refute_operator 1, :>, 2 #=> pass 672 | # refute_operator 1, :<, 2 #=> fail 673 | 674 | def refute_operator o1, op, o2 = (predicate = true; nil), msg = nil 675 | return refute_predicate o1, op, msg if predicate 676 | msg = message(msg) { "Expected #{mu_pp(o1)} to not be #{op} #{mu_pp(o2)}"} 677 | refute o1.__send__(op, o2), msg 678 | end 679 | 680 | ## 681 | # For testing with predicates. 682 | # 683 | # refute_predicate str, :empty? 684 | # 685 | # This is really meant for specs and is front-ended by refute_operator: 686 | # 687 | # str.wont_be :empty? 688 | 689 | def refute_predicate o1, op, msg = nil 690 | msg = message(msg) { "Expected #{mu_pp(o1)} to not be #{op}" } 691 | refute o1.__send__(op), msg 692 | end 693 | 694 | ## 695 | # Fails if +obj+ responds to the message +meth+. 696 | 697 | def refute_respond_to obj, meth, msg = nil 698 | msg = message(msg) { "Expected #{mu_pp(obj)} to not respond to #{meth}" } 699 | 700 | refute obj.respond_to?(meth), msg 701 | end 702 | 703 | ## 704 | # Fails if +exp+ is the same (by object identity) as +act+. 705 | 706 | def refute_same exp, act, msg = nil 707 | msg = message(msg) { 708 | data = [mu_pp(act), act.object_id, mu_pp(exp), exp.object_id] 709 | "Expected %s (oid=%d) to not be the same as %s (oid=%d)" % data 710 | } 711 | refute exp.equal?(act), msg 712 | end 713 | 714 | ## 715 | # Skips the current test. Gets listed at the end of the run but 716 | # doesn't cause a failure exit code. 717 | 718 | def skip msg = nil, bt = caller 719 | msg ||= "Skipped, no message given" 720 | @skip = true 721 | raise MiniTest::Skip, msg, bt 722 | end 723 | 724 | ## 725 | # Was this testcase skipped? Meant for #teardown. 726 | 727 | def skipped? 728 | defined?(@skip) and @skip 729 | end 730 | 731 | ## 732 | # Takes a block and wraps it with the runner's shared mutex. 733 | 734 | def synchronize 735 | Minitest::Unit.runner.synchronize do 736 | yield 737 | end 738 | end 739 | end 740 | 741 | class Unit # :nodoc: 742 | VERSION = "4.7.5" # :nodoc: 743 | 744 | attr_accessor :report, :failures, :errors, :skips # :nodoc: 745 | attr_accessor :assertion_count # :nodoc: 746 | attr_writer :test_count # :nodoc: 747 | attr_accessor :start_time # :nodoc: 748 | attr_accessor :help # :nodoc: 749 | attr_accessor :verbose # :nodoc: 750 | attr_writer :options # :nodoc: 751 | 752 | ## 753 | # :attr: 754 | # 755 | # if true, installs an "INFO" signal handler (only available to BSD and 756 | # OS X users) which prints diagnostic information about the test run. 757 | # 758 | # This is auto-detected by default but may be overridden by custom 759 | # runners. 760 | 761 | attr_accessor :info_signal 762 | 763 | ## 764 | # Lazy accessor for options. 765 | 766 | def options 767 | @options ||= {} 768 | end 769 | 770 | @@installed_at_exit ||= false 771 | @@out = $stdout 772 | @@after_tests = [] 773 | 774 | ## 775 | # A simple hook allowing you to run a block of code after _all_ of 776 | # the tests are done. Eg: 777 | # 778 | # MiniTest::Unit.after_tests { p $debugging_info } 779 | 780 | def self.after_tests &block 781 | @@after_tests << block 782 | end 783 | 784 | ## 785 | # Registers MiniTest::Unit to run tests at process exit 786 | 787 | def self.autorun 788 | at_exit { 789 | # don't run if there was a non-exit exception 790 | next if $! and not $!.kind_of? SystemExit 791 | 792 | # the order here is important. The at_exit handler must be 793 | # installed before anyone else gets a chance to install their 794 | # own, that way we can be assured that our exit will be last 795 | # to run (at_exit stacks). 796 | exit_code = nil 797 | 798 | at_exit { 799 | @@after_tests.reverse_each(&:call) 800 | exit false if exit_code && exit_code != 0 801 | } 802 | 803 | exit_code = MiniTest::Unit.new.run ARGV 804 | } unless @@installed_at_exit 805 | @@installed_at_exit = true 806 | end 807 | 808 | ## 809 | # Returns the stream to use for output. 810 | 811 | def self.output 812 | @@out 813 | end 814 | 815 | ## 816 | # Sets MiniTest::Unit to write output to +stream+. $stdout is the default 817 | # output 818 | 819 | def self.output= stream 820 | @@out = stream 821 | end 822 | 823 | ## 824 | # Tells MiniTest::Unit to delegate to +runner+, an instance of a 825 | # MiniTest::Unit subclass, when MiniTest::Unit#run is called. 826 | 827 | def self.runner= runner 828 | @@runner = runner 829 | end 830 | 831 | ## 832 | # Returns the MiniTest::Unit subclass instance that will be used 833 | # to run the tests. A MiniTest::Unit instance is the default 834 | # runner. 835 | 836 | def self.runner 837 | @@runner ||= self.new 838 | end 839 | 840 | ## 841 | # Return all plugins' run methods (methods that start with "run_"). 842 | 843 | def self.plugins 844 | @@plugins ||= (["run_tests"] + 845 | public_instance_methods(false). 846 | grep(/^run_/).map { |s| s.to_s }).uniq 847 | end 848 | 849 | ## 850 | # Return the IO for output. 851 | 852 | def output 853 | self.class.output 854 | end 855 | 856 | def puts *a # :nodoc: 857 | output.puts(*a) 858 | end 859 | 860 | def print *a # :nodoc: 861 | output.print(*a) 862 | end 863 | 864 | def test_count # :nodoc: 865 | @test_count ||= 0 866 | end 867 | 868 | ## 869 | # Runner for a given +type+ (eg, test vs bench). 870 | 871 | def _run_anything type 872 | suites = TestCase.send "#{type}_suites" 873 | return if suites.empty? 874 | 875 | puts 876 | puts "# Running #{type}s:" 877 | puts 878 | 879 | @test_count, @assertion_count = 0, 0 880 | test_count = assertion_count = 0 881 | sync = output.respond_to? :"sync=" # stupid emacs 882 | old_sync, output.sync = output.sync, true if sync 883 | 884 | count = 0 885 | begin 886 | start = Time.now 887 | 888 | results = _run_suites suites, type 889 | 890 | @test_count = results.inject(0) { |sum, (tc, _)| sum + tc } 891 | @assertion_count = results.inject(0) { |sum, (_, ac)| sum + ac } 892 | test_count += @test_count 893 | assertion_count += @assertion_count 894 | t = Time.now - start 895 | count += 1 896 | unless @repeat_count 897 | puts 898 | puts 899 | end 900 | puts "Finished%s %ss in %.6fs, %.4f tests/s, %.4f assertions/s.\n" % 901 | [(@repeat_count ? "(#{count}/#{@repeat_count}) " : ""), type, 902 | t, @test_count.fdiv(t), @assertion_count.fdiv(t)] 903 | end while @repeat_count && count < @repeat_count && report.empty? 904 | 905 | output.sync = old_sync if sync 906 | 907 | report.each_with_index do |msg, i| 908 | puts "\n%3d) %s" % [i + 1, msg] 909 | end 910 | 911 | puts 912 | @test_count = test_count 913 | @assertion_count = assertion_count 914 | 915 | status 916 | end 917 | 918 | ## 919 | # Runs all the +suites+ for a given +type+. 920 | # 921 | 922 | def _run_suites suites, type 923 | suites.map { |suite| _run_suite suite, type } 924 | end 925 | 926 | ## 927 | # Run a single +suite+ for a given +type+. 928 | 929 | def _run_suite suite, type 930 | header = "#{type}_suite_header" 931 | puts send(header, suite) if respond_to? header 932 | 933 | filter = options[:filter] || '/./' 934 | filter = Regexp.new $1 if filter =~ /\/(.*)\// 935 | 936 | all_test_methods = suite.send "#{type}_methods" 937 | 938 | filtered_test_methods = all_test_methods.find_all { |m| 939 | filter === m || filter === "#{suite}##{m}" 940 | } 941 | 942 | leakchecker = LeakChecker.new 943 | 944 | assertions = filtered_test_methods.map { |method| 945 | inst = suite.new method 946 | inst._assertions = 0 947 | 948 | print "#{suite}##{method} = " if @verbose 949 | 950 | start_time = Time.now if @verbose 951 | result = inst.run self 952 | 953 | print "%.2f s = " % (Time.now - start_time) if @verbose 954 | print result 955 | puts if @verbose 956 | $stdout.flush 957 | 958 | leakchecker.check("#{inst.class}\##{inst.__name__}") 959 | 960 | inst._assertions 961 | } 962 | 963 | return assertions.size, assertions.inject(0) { |sum, n| sum + n } 964 | end 965 | 966 | ## 967 | # Record the result of a single test. Makes it very easy to gather 968 | # information. Eg: 969 | # 970 | # class StatisticsRecorder < MiniTest::Unit 971 | # def record suite, method, assertions, time, error 972 | # # ... record the results somewhere ... 973 | # end 974 | # end 975 | # 976 | # MiniTest::Unit.runner = StatisticsRecorder.new 977 | # 978 | # NOTE: record might be sent more than once per test. It will be 979 | # sent once with the results from the test itself. If there is a 980 | # failure or error in teardown, it will be sent again with the 981 | # error or failure. 982 | 983 | def record suite, method, assertions, time, error 984 | end 985 | 986 | def location e # :nodoc: 987 | last_before_assertion = "" 988 | e.backtrace.reverse_each do |s| 989 | break if s =~ /in .(assert|refute|flunk|pass|fail|raise|must|wont)/ 990 | last_before_assertion = s 991 | end 992 | last_before_assertion.sub(/:in .*$/, '') 993 | end 994 | 995 | ## 996 | # Writes status for failed test +meth+ in +klass+ which finished with 997 | # exception +e+ 998 | 999 | def puke klass, meth, e 1000 | e = case e 1001 | when MiniTest::Skip then 1002 | @skips += 1 1003 | return "S" unless @verbose 1004 | "Skipped:\n#{klass}##{meth} [#{location e}]:\n#{e.message}\n" 1005 | when MiniTest::Assertion then 1006 | @failures += 1 1007 | "Failure:\n#{klass}##{meth} [#{location e}]:\n#{e.message}\n" 1008 | else 1009 | @errors += 1 1010 | bt = MiniTest::filter_backtrace(e.backtrace).join "\n " 1011 | "Error:\n#{klass}##{meth}:\n#{e.class}: #{e.message.b}\n #{bt}\n" 1012 | end 1013 | @report << e 1014 | e[0, 1] 1015 | end 1016 | 1017 | def initialize # :nodoc: 1018 | @report = [] 1019 | @errors = @failures = @skips = 0 1020 | @verbose = false 1021 | @mutex = Thread::Mutex.new 1022 | @info_signal = Signal.list['INFO'] 1023 | @repeat_count = nil 1024 | end 1025 | 1026 | def synchronize # :nodoc: 1027 | if @mutex then 1028 | @mutex.synchronize { yield } 1029 | else 1030 | yield 1031 | end 1032 | end 1033 | 1034 | def process_args args = [] # :nodoc: 1035 | options = {} 1036 | orig_args = args.dup 1037 | 1038 | OptionParser.new do |opts| 1039 | opts.banner = 'minitest options:' 1040 | opts.version = MiniTest::Unit::VERSION 1041 | 1042 | opts.on '-h', '--help', 'Display this help.' do 1043 | puts opts 1044 | exit 1045 | end 1046 | 1047 | opts.on '-s', '--seed SEED', Integer, "Sets random seed" do |m| 1048 | options[:seed] = m.to_i 1049 | end 1050 | 1051 | opts.on '-v', '--verbose', "Verbose. Show progress processing files." do 1052 | options[:verbose] = true 1053 | end 1054 | 1055 | opts.on '-n', '--name PATTERN', "Filter test names on pattern (e.g. /foo/)" do |a| 1056 | options[:filter] = a 1057 | end 1058 | 1059 | opts.parse! args 1060 | orig_args -= args 1061 | end 1062 | 1063 | unless options[:seed] then 1064 | srand 1065 | options[:seed] = srand % 0xFFFF 1066 | orig_args << "--seed" << options[:seed].to_s 1067 | end 1068 | 1069 | srand options[:seed] 1070 | 1071 | self.verbose = options[:verbose] 1072 | @help = orig_args.map { |s| s =~ /[\s|&<>$()]/ ? s.inspect : s }.join " " 1073 | 1074 | options 1075 | end 1076 | 1077 | ## 1078 | # Begins the full test run. Delegates to +runner+'s #_run method. 1079 | 1080 | def run args = [] 1081 | self.class.runner._run(args) 1082 | end 1083 | 1084 | ## 1085 | # Top level driver, controls all output and filtering. 1086 | 1087 | def _run args = [] 1088 | args = process_args args # ARGH!! blame test/unit process_args 1089 | self.options.merge! args 1090 | 1091 | puts "Run options: #{help}" 1092 | 1093 | self.class.plugins.each do |plugin| 1094 | send plugin 1095 | break unless report.empty? 1096 | end 1097 | 1098 | return failures + errors if self.test_count > 0 # or return nil... 1099 | rescue Interrupt 1100 | abort 'Interrupted' 1101 | end 1102 | 1103 | ## 1104 | # Runs test suites matching +filter+. 1105 | 1106 | def run_tests 1107 | _run_anything :test 1108 | end 1109 | 1110 | ## 1111 | # Writes status to +io+ 1112 | 1113 | def status io = self.output 1114 | format = "%d tests, %d assertions, %d failures, %d errors, %d skips" 1115 | io.puts format % [test_count, assertion_count, failures, errors, skips] 1116 | end 1117 | 1118 | ## 1119 | # Provides a simple set of guards that you can use in your tests 1120 | # to skip execution if it is not applicable. These methods are 1121 | # mixed into TestCase as both instance and class methods so you 1122 | # can use them inside or outside of the test methods. 1123 | # 1124 | # def test_something_for_mri 1125 | # skip "bug 1234" if jruby? 1126 | # # ... 1127 | # end 1128 | # 1129 | # if windows? then 1130 | # # ... lots of test methods ... 1131 | # end 1132 | 1133 | module Guard 1134 | 1135 | ## 1136 | # Is this running on jruby? 1137 | 1138 | def jruby? platform = RUBY_PLATFORM 1139 | "java" == platform 1140 | end 1141 | 1142 | ## 1143 | # Is this running on mri? 1144 | 1145 | def maglev? platform = defined?(RUBY_ENGINE) && RUBY_ENGINE 1146 | "maglev" == platform 1147 | end 1148 | 1149 | module_function :maglev? 1150 | 1151 | ## 1152 | # Is this running on mri? 1153 | 1154 | def mri? platform = RUBY_DESCRIPTION 1155 | /^ruby/ =~ platform 1156 | end 1157 | 1158 | ## 1159 | # Is this running on rubinius? 1160 | 1161 | def rubinius? platform = defined?(RUBY_ENGINE) && RUBY_ENGINE 1162 | "rbx" == platform 1163 | end 1164 | 1165 | ## 1166 | # Is this running on windows? 1167 | 1168 | def windows? platform = RUBY_PLATFORM 1169 | /mswin|mingw/ =~ platform 1170 | end 1171 | end 1172 | 1173 | ## 1174 | # Provides before/after hooks for setup and teardown. These are 1175 | # meant for library writers, NOT for regular test authors. See 1176 | # #before_setup for an example. 1177 | 1178 | module LifecycleHooks 1179 | ## 1180 | # Runs before every test, after setup. This hook is meant for 1181 | # libraries to extend minitest. It is not meant to be used by 1182 | # test developers. 1183 | # 1184 | # See #before_setup for an example. 1185 | 1186 | def after_setup; end 1187 | 1188 | ## 1189 | # Runs before every test, before setup. This hook is meant for 1190 | # libraries to extend minitest. It is not meant to be used by 1191 | # test developers. 1192 | # 1193 | # As a simplistic example: 1194 | # 1195 | # module MyMinitestPlugin 1196 | # def before_setup 1197 | # super 1198 | # # ... stuff to do before setup is run 1199 | # end 1200 | # 1201 | # def after_setup 1202 | # # ... stuff to do after setup is run 1203 | # super 1204 | # end 1205 | # 1206 | # def before_teardown 1207 | # super 1208 | # # ... stuff to do before teardown is run 1209 | # end 1210 | # 1211 | # def after_teardown 1212 | # # ... stuff to do after teardown is run 1213 | # super 1214 | # end 1215 | # end 1216 | # 1217 | # class MiniTest::Unit::TestCase 1218 | # include MyMinitestPlugin 1219 | # end 1220 | 1221 | def before_setup; end 1222 | 1223 | ## 1224 | # Runs after every test, before teardown. This hook is meant for 1225 | # libraries to extend minitest. It is not meant to be used by 1226 | # test developers. 1227 | # 1228 | # See #before_setup for an example. 1229 | 1230 | def before_teardown; end 1231 | 1232 | ## 1233 | # Runs after every test, after teardown. This hook is meant for 1234 | # libraries to extend minitest. It is not meant to be used by 1235 | # test developers. 1236 | # 1237 | # See #before_setup for an example. 1238 | 1239 | def after_teardown; end 1240 | end 1241 | 1242 | ## 1243 | # Subclass TestCase to create your own tests. Typically you'll want a 1244 | # TestCase subclass per implementation class. 1245 | # 1246 | # See MiniTest::Assertions 1247 | 1248 | class TestCase 1249 | include LifecycleHooks 1250 | include Guard 1251 | extend Guard 1252 | 1253 | attr_reader :__name__ # :nodoc: 1254 | 1255 | PASSTHROUGH_EXCEPTIONS = [NoMemoryError, SignalException, 1256 | Interrupt, SystemExit] # :nodoc: 1257 | 1258 | ## 1259 | # Runs the tests reporting the status to +runner+ 1260 | 1261 | def run runner 1262 | trap "INFO" do 1263 | runner.report.each_with_index do |msg, i| 1264 | warn "\n%3d) %s" % [i + 1, msg] 1265 | end 1266 | warn '' 1267 | time = runner.start_time ? Time.now - runner.start_time : 0 1268 | warn "Current Test: %s#%s %.2fs" % [self.class, self.__name__, time] 1269 | runner.status $stderr 1270 | end if runner.info_signal 1271 | 1272 | start_time = Time.now 1273 | 1274 | result = "" 1275 | begin 1276 | @passed = nil 1277 | self.before_setup 1278 | self.setup 1279 | self.after_setup 1280 | self.run_test self.__name__ 1281 | result = "." unless io? 1282 | time = Time.now - start_time 1283 | runner.record self.class, self.__name__, self._assertions, time, nil 1284 | @passed = true 1285 | rescue *PASSTHROUGH_EXCEPTIONS 1286 | raise 1287 | rescue Exception => e 1288 | @passed = Skip === e 1289 | time = Time.now - start_time 1290 | runner.record self.class, self.__name__, self._assertions, time, e 1291 | result = runner.puke self.class, self.__name__, e 1292 | ensure 1293 | %w{ before_teardown teardown after_teardown }.each do |hook| 1294 | begin 1295 | self.send hook 1296 | rescue *PASSTHROUGH_EXCEPTIONS 1297 | raise 1298 | rescue Exception => e 1299 | @passed = false 1300 | runner.record self.class, self.__name__, self._assertions, time, e 1301 | result = runner.puke self.class, self.__name__, e 1302 | end 1303 | end 1304 | trap 'INFO', 'DEFAULT' if runner.info_signal 1305 | end 1306 | result 1307 | end 1308 | 1309 | alias :run_test :__send__ 1310 | 1311 | def initialize name # :nodoc: 1312 | @__name__ = name 1313 | @__io__ = nil 1314 | @passed = nil 1315 | @@current = self # FIX: make thread local 1316 | end 1317 | 1318 | def self.current # :nodoc: 1319 | @@current # FIX: make thread local 1320 | end 1321 | 1322 | ## 1323 | # Return the output IO object 1324 | 1325 | def io 1326 | @__io__ = true 1327 | MiniTest::Unit.output 1328 | end 1329 | 1330 | ## 1331 | # Have we hooked up the IO yet? 1332 | 1333 | def io? 1334 | @__io__ 1335 | end 1336 | 1337 | def self.reset # :nodoc: 1338 | @@test_suites = {} 1339 | end 1340 | 1341 | reset 1342 | 1343 | ## 1344 | # Make diffs for this TestCase use #pretty_inspect so that diff 1345 | # in assert_equal can be more details. NOTE: this is much slower 1346 | # than the regular inspect but much more usable for complex 1347 | # objects. 1348 | 1349 | def self.make_my_diffs_pretty! 1350 | require 'pp' 1351 | 1352 | define_method :mu_pp do |o| 1353 | o.pretty_inspect 1354 | end 1355 | end 1356 | 1357 | def self.inherited klass # :nodoc: 1358 | @@test_suites[klass] = true 1359 | super 1360 | end 1361 | 1362 | def self.test_order # :nodoc: 1363 | :random 1364 | end 1365 | 1366 | def self.test_suites # :nodoc: 1367 | @@test_suites.keys.sort_by { |ts| ts.name.to_s } 1368 | end 1369 | 1370 | def self.test_methods # :nodoc: 1371 | methods = public_instance_methods(true).grep(/^test/).map { |m| m.to_s } 1372 | 1373 | case self.test_order 1374 | when :parallel 1375 | max = methods.size 1376 | ParallelEach.new methods.sort.sort_by { rand max } 1377 | when :random then 1378 | max = methods.size 1379 | methods.sort.sort_by { rand max } 1380 | when :alpha, :sorted then 1381 | methods.sort 1382 | else 1383 | raise "Unknown test_order: #{self.test_order.inspect}" 1384 | end 1385 | end 1386 | 1387 | ## 1388 | # Returns true if the test passed. 1389 | 1390 | def passed? 1391 | @passed 1392 | end 1393 | 1394 | ## 1395 | # Runs before every test. Use this to set up before each test 1396 | # run. 1397 | 1398 | def setup; end 1399 | 1400 | ## 1401 | # Runs after every test. Use this to clean up after each test 1402 | # run. 1403 | 1404 | def teardown; end 1405 | 1406 | include MiniTest::Assertions 1407 | end # class TestCase 1408 | end # class Unit 1409 | 1410 | Test = Unit::TestCase 1411 | end # module MiniTest 1412 | 1413 | Minitest = MiniTest # :nodoc: because ugh... I typo this all the time 1414 | --------------------------------------------------------------------------------