├── .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 [](https://badge.fury.io/rb/matrix) [](https://stdgems.org/matrix/) [](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