eksa-mination 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.eksa-mination +1 -0
- data/LICENSE +21 -0
- data/README.md +54 -0
- data/bin/eksa-mination +6 -0
- data/lib/calculator.rb +18 -0
- data/lib/eksa-mination/cli.rb +118 -0
- data/lib/eksa-mination/dsl.rb +175 -0
- data/lib/eksa-mination/formatters/base.rb +78 -0
- data/lib/eksa-mination/formatters/documentation.rb +36 -0
- data/lib/eksa-mination/formatters/html.rb +147 -0
- data/lib/eksa-mination/formatters/json.rb +45 -0
- data/lib/eksa-mination/formatters/progress.rb +22 -0
- data/lib/eksa-mination/matchers/be.rb +11 -0
- data/lib/eksa-mination/matchers/be_a.rb +10 -0
- data/lib/eksa-mination/matchers/eq.rb +11 -0
- data/lib/eksa-mination/matchers/falsey.rb +10 -0
- data/lib/eksa-mination/matchers/include.rb +11 -0
- data/lib/eksa-mination/matchers/match.rb +11 -0
- data/lib/eksa-mination/matchers/raise_error.rb +35 -0
- data/lib/eksa-mination/matchers/respond_to.rb +10 -0
- data/lib/eksa-mination/matchers/truthy.rb +10 -0
- data/lib/eksa-mination/matchers.rb +102 -0
- data/lib/eksa-mination/mocks.rb +137 -0
- data/lib/eksa-mination/runner.rb +120 -0
- data/lib/eksa-mination.rb +23 -0
- metadata +66 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: c1b4a71a3c02a2d192423b2b171a1cd6752a712ffee7093e611e33a88a1c6e48
|
|
4
|
+
data.tar.gz: 79f31960ca216c4c3b97d9086b446b495199978046a0d094d62a56faf94df758
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: f45bb698967aa92f5ffa516a882100390da7b743865c11a0b1c7738e0e09ad19dedbbee65ed10ffd5230690ae731f1d80ce88545aecd49d69f7978231ca384f4
|
|
7
|
+
data.tar.gz: 319c76f62089634983380db8e92b3a9049261d022f1666fecb0fa5e0a04292d592239c03e72e105ffdbc8595fd0771c5452749a4f3e95371c2f7960d9c52a0b8
|
data/.eksa-mination
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
--format documentation
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 IshikawaUta
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT,. TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Eksa-Mination ๐
|
|
2
|
+
|
|
3
|
+
A robust, modular, and feature-rich Ruby testing framework inspired by RSpec.
|
|
4
|
+
|
|
5
|
+
Eksa-Mination provides a familiar DSL for writing expressive tests, comprehensive mocking/stubbing capabilities, and powerful reporting toolsโall in a lightweight package.
|
|
6
|
+
|
|
7
|
+
## โจ Features
|
|
8
|
+
|
|
9
|
+
- **Fluent DSL**: `describe`, `it`, `let`, `let!`, `subject`, and `shared_examples`.
|
|
10
|
+
- **Flexible Matchers**: `eq`, `be_a`, `respond_to`, `include`, `match`, `raise_error`, and more.
|
|
11
|
+
- **Advanced Mocking**: Method stubbing (`allow().to receive()`) and **Constant Stubbing** (`stub_const`).
|
|
12
|
+
- **Powerful Filtering**: Run tests by name (`-e`), line number (`file:line`), or **Metadata Tags** (`--tag`).
|
|
13
|
+
- **Rich Reporting**: Built-in formatters for **Progress**, **Documentation**, **JSON**, and **HTML** (Premium Dark Mode).
|
|
14
|
+
- **Configuration**: Automatic loading of defaults from `.eksa-mination`.
|
|
15
|
+
|
|
16
|
+
## ๐ Getting Started
|
|
17
|
+
|
|
18
|
+
### Installation
|
|
19
|
+
Install it as a gem:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
gem install eksa-mination
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Or clone this repository directly to your project.
|
|
26
|
+
|
|
27
|
+
### Running Tests
|
|
28
|
+
By default, Eksa-Mination looks for files matching `spec/**/*_spec.rb`.
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
# Run all tests
|
|
32
|
+
eksa-mination
|
|
33
|
+
|
|
34
|
+
# Run with Documentation formatter
|
|
35
|
+
eksa-mination -f documentation
|
|
36
|
+
|
|
37
|
+
# Run a specific line
|
|
38
|
+
eksa-mination spec/my_spec.rb:15
|
|
39
|
+
|
|
40
|
+
# Filter by Tag
|
|
41
|
+
eksa-mination --tag focus
|
|
42
|
+
|
|
43
|
+
# Generate HTML Report
|
|
44
|
+
eksa-mination -f html
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## ๐ Project Structure
|
|
48
|
+
|
|
49
|
+
- `bin/`: The primary executable.
|
|
50
|
+
- `lib/eksa-mination`: Core logic, matchers, and formatters.
|
|
51
|
+
- `spec/`: Example testing files and usage demonstrations.
|
|
52
|
+
|
|
53
|
+
## ๐ License
|
|
54
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
data/bin/eksa-mination
ADDED
data/lib/calculator.rb
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
require 'optparse'
|
|
2
|
+
|
|
3
|
+
module EksaMination
|
|
4
|
+
class CLI
|
|
5
|
+
def initialize(args)
|
|
6
|
+
@args = load_config_args + args
|
|
7
|
+
@options = {
|
|
8
|
+
pattern: 'spec/**/*_spec.rb'
|
|
9
|
+
}
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def run
|
|
13
|
+
parse_options
|
|
14
|
+
files = discover_files
|
|
15
|
+
|
|
16
|
+
if files.empty?
|
|
17
|
+
puts "No spec files found."
|
|
18
|
+
exit 1
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
files.each do |file|
|
|
22
|
+
load File.expand_path(file)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
if @options[:formatter]
|
|
26
|
+
EksaMination.reporter = @options[:formatter].new
|
|
27
|
+
end
|
|
28
|
+
EksaMination.reporter.use_color = @options[:color] unless @options[:color].nil?
|
|
29
|
+
EksaMination.run(@options)
|
|
30
|
+
|
|
31
|
+
# Exit with status code based on failures
|
|
32
|
+
exit(EksaMination.reporter.instance_variable_get(:@failures).any? ? 1 : 0)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def load_config_args
|
|
38
|
+
if File.exist?('.eksa-mination')
|
|
39
|
+
File.read('.eksa-mination').split(/\s+/)
|
|
40
|
+
else
|
|
41
|
+
[]
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def parse_options
|
|
46
|
+
OptionParser.new do |opts|
|
|
47
|
+
opts.banner = "Usage: eksa-mination [options] [files or directories]"
|
|
48
|
+
opts.separator ""
|
|
49
|
+
opts.separator " **** Output ****"
|
|
50
|
+
opts.separator ""
|
|
51
|
+
|
|
52
|
+
opts.on("-v", "--version", "Display the version.") do
|
|
53
|
+
puts "eksa-mination #{EksaMination::VERSION}"
|
|
54
|
+
exit
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
opts.on("-h", "--help", "You're looking at it.") do
|
|
58
|
+
puts opts
|
|
59
|
+
exit
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
opts.on("-P", "--pattern PATTERN", "Load files matching pattern (default: \"spec/**/*_spec.rb\").") do |p|
|
|
63
|
+
@options[:pattern] = p
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
opts.on("-f", "--format FORMATTER", "Choose a formatter ([p]rogress, [d]ocumentation, [j]son, [h]tml)") do |f|
|
|
67
|
+
case f
|
|
68
|
+
when 'p', 'progress'
|
|
69
|
+
@options[:formatter] = EksaMination::Formatters::Progress
|
|
70
|
+
when 'd', 'documentation'
|
|
71
|
+
@options[:formatter] = EksaMination::Formatters::Documentation
|
|
72
|
+
when 'j', 'json'
|
|
73
|
+
@options[:formatter] = EksaMination::Formatters::JSONFormatter
|
|
74
|
+
when 'h', 'html'
|
|
75
|
+
@options[:formatter] = EksaMination::Formatters::HTML
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
opts.on("--[no-]color", "--[no-]colour", "Enable/disable color output") do |c|
|
|
80
|
+
# logic for color toggle can be added to BaseFormatter
|
|
81
|
+
@options[:color] = c
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
opts.separator ""
|
|
85
|
+
opts.separator " **** Filtering/tags ****"
|
|
86
|
+
opts.separator ""
|
|
87
|
+
opts.on("-e", "--example STRING", "Run examples whose full nested names include STRING") do |s|
|
|
88
|
+
@options[:example] = s
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
opts.on("-t", "--tag TAG", "Run examples with the specified tag") do |t|
|
|
92
|
+
@options[:tag] = t
|
|
93
|
+
end
|
|
94
|
+
end.parse!(@args)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def discover_files
|
|
98
|
+
if @args.empty?
|
|
99
|
+
Dir.glob(@options[:pattern])
|
|
100
|
+
else
|
|
101
|
+
@args.flat_map do |arg|
|
|
102
|
+
path, line = arg.split(':')
|
|
103
|
+
if line
|
|
104
|
+
@options[:line_numbers] ||= {}
|
|
105
|
+
full_path = File.expand_path(path)
|
|
106
|
+
@options[:line_numbers][full_path] ||= []
|
|
107
|
+
@options[:line_numbers][full_path] << line.to_i
|
|
108
|
+
path
|
|
109
|
+
elsif File.directory?(path)
|
|
110
|
+
Dir.glob(File.join(path, "**/*_spec.rb"))
|
|
111
|
+
else
|
|
112
|
+
path
|
|
113
|
+
end
|
|
114
|
+
end.compact.uniq
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
module EksaMination
|
|
2
|
+
module DSL
|
|
3
|
+
def describe(description, *metadata, &block)
|
|
4
|
+
# Check if we are inside another describe
|
|
5
|
+
parent = EksaMination.current_group
|
|
6
|
+
file_path, line_number = block.source_location
|
|
7
|
+
options = process_metadata(metadata)
|
|
8
|
+
group = ExampleGroup.new(description, parent: parent, metadata: options, file_path: file_path, line_number: line_number)
|
|
9
|
+
|
|
10
|
+
EksaMination.current_group = group
|
|
11
|
+
group.instance_eval(&block)
|
|
12
|
+
EksaMination.current_group = parent
|
|
13
|
+
|
|
14
|
+
# Only register top-level groups to the main runner
|
|
15
|
+
EksaMination.register_group(group) unless parent
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def xdescribe(description, *metadata, &block)
|
|
19
|
+
file_path, line_number = block.source_location
|
|
20
|
+
options = process_metadata(metadata).merge(skipped: true)
|
|
21
|
+
group = ExampleGroup.new(description, parent: EksaMination.current_group, metadata: options, skipped: true, file_path: file_path, line_number: line_number)
|
|
22
|
+
EksaMination.register_group(group)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def shared_examples(name, &block)
|
|
26
|
+
EksaMination.shared_examples[name] = block
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def process_metadata(metadata_args)
|
|
32
|
+
options = {}
|
|
33
|
+
metadata_args.each do |arg|
|
|
34
|
+
if arg.is_a?(Symbol)
|
|
35
|
+
options[arg] = true
|
|
36
|
+
elsif arg.is_a?(Hash)
|
|
37
|
+
options.merge!(arg)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
options
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
class ExampleGroup
|
|
45
|
+
attr_reader :description, :examples, :before_hooks, :after_hooks, :let_blocks, :let_bang_blocks, :subject_block, :skipped, :parent, :file_path, :line_number, :metadata
|
|
46
|
+
|
|
47
|
+
def initialize(description, parent: nil, skipped: false, metadata: {}, file_path: nil, line_number: nil)
|
|
48
|
+
@description = description
|
|
49
|
+
@parent = parent
|
|
50
|
+
@examples = []
|
|
51
|
+
@before_hooks = []
|
|
52
|
+
@after_hooks = []
|
|
53
|
+
@let_blocks = {}
|
|
54
|
+
@let_bang_blocks = {}
|
|
55
|
+
@skipped = skipped || (parent && parent.skipped)
|
|
56
|
+
@metadata = (parent ? parent.metadata.merge(metadata) : metadata)
|
|
57
|
+
@file_path = file_path
|
|
58
|
+
@line_number = line_number
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def it(description, *metadata, &block)
|
|
62
|
+
file_path, line_number = block.source_location
|
|
63
|
+
options = process_metadata(metadata)
|
|
64
|
+
@examples << Example.new(description, block, parent: self, metadata: options, file_path: file_path, line_number: line_number)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def xit(description, *metadata, &block)
|
|
68
|
+
file_path, line_number = block.source_location
|
|
69
|
+
options = process_metadata(metadata).merge(skipped: true)
|
|
70
|
+
@examples << Example.new(description, block, parent: self, metadata: options, skipped: true, file_path: file_path, line_number: line_number)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def let(name, &block)
|
|
75
|
+
@let_blocks[name] = block
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def let!(name, &block)
|
|
79
|
+
@let_bang_blocks[name] = block
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def subject(&block)
|
|
83
|
+
@subject_block = block
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def before(&block)
|
|
87
|
+
@before_hooks << block
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def after(&block)
|
|
91
|
+
@after_hooks << block
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def full_description
|
|
95
|
+
@parent ? "#{@parent.full_description} #{@description}" : @description
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Nested groups started from inside this group
|
|
99
|
+
def describe(description, &block)
|
|
100
|
+
file_path, line_number = block.source_location
|
|
101
|
+
group = ExampleGroup.new(description, parent: self, file_path: file_path, line_number: line_number)
|
|
102
|
+
@examples << group
|
|
103
|
+
group.instance_eval(&block)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def it_behaves_like(name, *args)
|
|
107
|
+
block = EksaMination.shared_examples[name]
|
|
108
|
+
raise "Shared examples '#{name}' not found" unless block
|
|
109
|
+
instance_exec(*args, &block)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
private
|
|
113
|
+
|
|
114
|
+
def process_metadata(metadata_args)
|
|
115
|
+
options = {}
|
|
116
|
+
metadata_args.each do |arg|
|
|
117
|
+
if arg.is_a?(Symbol)
|
|
118
|
+
options[arg] = true
|
|
119
|
+
elsif arg.is_a?(Hash)
|
|
120
|
+
options.merge!(arg)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
options
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
class Example
|
|
128
|
+
attr_reader :description, :block, :skipped, :file_path, :line_number, :parent, :metadata
|
|
129
|
+
|
|
130
|
+
def initialize(description, block, parent: nil, metadata: {}, skipped: false, file_path: nil, line_number: nil)
|
|
131
|
+
@description = description
|
|
132
|
+
@block = block
|
|
133
|
+
@parent = parent
|
|
134
|
+
@metadata = (parent ? parent.metadata.merge(metadata) : metadata)
|
|
135
|
+
@skipped = skipped || @metadata[:skipped]
|
|
136
|
+
@file_path = file_path
|
|
137
|
+
@line_number = line_number
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def full_description
|
|
141
|
+
@parent ? "#{@parent.full_description} #{@description}" : @description
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
@groups = []
|
|
146
|
+
@current_group = nil
|
|
147
|
+
@shared_examples = {}
|
|
148
|
+
|
|
149
|
+
def self.register_group(group)
|
|
150
|
+
@groups << group
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def self.groups
|
|
154
|
+
@groups
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def self.shared_examples
|
|
158
|
+
@shared_examples
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def self.current_group
|
|
162
|
+
@current_group
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def self.current_group=(group)
|
|
166
|
+
@current_group = group
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def self.reset!
|
|
170
|
+
@groups = []
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Global monkeypatch
|
|
175
|
+
Object.include(EksaMination::DSL)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
module EksaMination
|
|
2
|
+
module Formatters
|
|
3
|
+
class Base
|
|
4
|
+
COLORS = {
|
|
5
|
+
green: "\e[32m",
|
|
6
|
+
red: "\e[31m",
|
|
7
|
+
yellow: "\e[33m",
|
|
8
|
+
cyan: "\e[36m",
|
|
9
|
+
reset: "\e[0m",
|
|
10
|
+
bold: "\e[1m"
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
attr_reader :failures, :skipped, :example_count, :results
|
|
14
|
+
attr_accessor :use_color
|
|
15
|
+
|
|
16
|
+
def initialize
|
|
17
|
+
@failures = []
|
|
18
|
+
@skipped = []
|
|
19
|
+
@results = []
|
|
20
|
+
@example_count = 0
|
|
21
|
+
@use_color = true
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def group_started(group); end
|
|
25
|
+
def group_finished(group); end
|
|
26
|
+
|
|
27
|
+
def report_success(example)
|
|
28
|
+
@example_count += 1
|
|
29
|
+
@results << { example: example, status: :passed }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def report_failure(example, error)
|
|
33
|
+
@example_count += 1
|
|
34
|
+
@failures << { example: example, error: error }
|
|
35
|
+
@results << { example: example, status: :failed, error: error }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def report_skipped(example)
|
|
39
|
+
@example_count += 1
|
|
40
|
+
@skipped << example
|
|
41
|
+
@results << { example: example, status: :pending }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def summarize
|
|
45
|
+
puts "\n\n"
|
|
46
|
+
if @failures.any?
|
|
47
|
+
puts colorize("Failures:", :red)
|
|
48
|
+
@failures.each_with_index do |failure, i|
|
|
49
|
+
example = failure[:example]
|
|
50
|
+
error = failure[:error]
|
|
51
|
+
puts "\n#{i + 1}) #{example.full_description}"
|
|
52
|
+
if error.respond_to?(:expected) && (error.expected || error.actual)
|
|
53
|
+
puts " Failure/Error: #{colorize(error.failure_message, :red)} #{colorize(error.expected.inspect, :green)} (expected) vs #{colorize(error.actual.inspect, :red)} (actual)"
|
|
54
|
+
else
|
|
55
|
+
puts " #{colorize(error.message, :red)}"
|
|
56
|
+
end
|
|
57
|
+
# Show file:line from backtrace
|
|
58
|
+
bt = error.backtrace.find { |l| l.include?('_spec.rb') } || error.backtrace.first
|
|
59
|
+
puts " # #{bt}"
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
puts "\n"
|
|
64
|
+
color = @failures.any? ? :red : :green
|
|
65
|
+
summary = "#{@example_count} examples, #{@failures.size} failures"
|
|
66
|
+
summary += ", #{@skipped.size} pending" if @skipped.any?
|
|
67
|
+
puts colorize(summary, color)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def colorize(text, color)
|
|
73
|
+
return text unless @use_color
|
|
74
|
+
"#{COLORS[color]}#{text}#{COLORS[:reset]}"
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
require_relative 'base'
|
|
2
|
+
|
|
3
|
+
module EksaMination
|
|
4
|
+
module Formatters
|
|
5
|
+
class Documentation < Base
|
|
6
|
+
def initialize
|
|
7
|
+
super
|
|
8
|
+
@level = 0
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def group_started(group)
|
|
12
|
+
puts " " * @level + group.description
|
|
13
|
+
@level += 1
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def group_finished(group)
|
|
17
|
+
@level -= 1
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def report_success(example)
|
|
21
|
+
super
|
|
22
|
+
puts " " * @level + colorize(example.description, :green)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def report_failure(example, error)
|
|
26
|
+
super
|
|
27
|
+
puts " " * @level + colorize(example.description + " (FAILED)", :red)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def report_skipped(example)
|
|
31
|
+
super
|
|
32
|
+
puts " " * @level + colorize(example.description + " (PENDING)", :yellow)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
require_relative 'base'
|
|
2
|
+
|
|
3
|
+
module EksaMination
|
|
4
|
+
module Formatters
|
|
5
|
+
class HTML < Base
|
|
6
|
+
def summarize
|
|
7
|
+
report_file = "eksa-report.html"
|
|
8
|
+
File.open(report_file, "w") do |f|
|
|
9
|
+
f.puts html_header
|
|
10
|
+
f.puts "<body>"
|
|
11
|
+
f.puts dashboard_section
|
|
12
|
+
f.puts "<div class='container'>"
|
|
13
|
+
f.puts results_section
|
|
14
|
+
f.puts "</div>"
|
|
15
|
+
f.puts footer_section
|
|
16
|
+
f.puts "</body></html>"
|
|
17
|
+
end
|
|
18
|
+
puts "\nHTML report generated: #{colorize(report_file, :cyan)}"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def html_header
|
|
24
|
+
<<~HTML
|
|
25
|
+
<!DOCTYPE html>
|
|
26
|
+
<html>
|
|
27
|
+
<head>
|
|
28
|
+
<title>Eksa-Mination Test Report</title>
|
|
29
|
+
<style>
|
|
30
|
+
:root {
|
|
31
|
+
--bg: #0f172a;
|
|
32
|
+
--card-bg: #1e293b;
|
|
33
|
+
--text: #f1f5f9;
|
|
34
|
+
--success: #10b981;
|
|
35
|
+
--failure: #f43f5e;
|
|
36
|
+
--pending: #f59e0b;
|
|
37
|
+
--accent: #38bdf8;
|
|
38
|
+
}
|
|
39
|
+
body {
|
|
40
|
+
font-family: 'Inter', sans-serif;
|
|
41
|
+
background: var(--bg);
|
|
42
|
+
color: var(--text);
|
|
43
|
+
margin: 0;
|
|
44
|
+
padding: 0;
|
|
45
|
+
}
|
|
46
|
+
.dashboard {
|
|
47
|
+
background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
|
|
48
|
+
padding: 40px 20px;
|
|
49
|
+
text-align: center;
|
|
50
|
+
border-bottom: 1px solid #334155;
|
|
51
|
+
}
|
|
52
|
+
.stats {
|
|
53
|
+
display: flex;
|
|
54
|
+
justify-content: center;
|
|
55
|
+
gap: 30px;
|
|
56
|
+
margin-top: 20px;
|
|
57
|
+
}
|
|
58
|
+
.stat-item {
|
|
59
|
+
background: var(--card-bg);
|
|
60
|
+
padding: 20px 40px;
|
|
61
|
+
border-radius: 12px;
|
|
62
|
+
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
|
|
63
|
+
}
|
|
64
|
+
.stat-value { font-size: 2em; font-weight: bold; }
|
|
65
|
+
.stat-label { font-size: 0.9em; color: #94a3b8; text-transform: uppercase; letter-spacing: 1px; }
|
|
66
|
+
|
|
67
|
+
.container { max-width: 1000px; margin: 40px auto; padding: 0 20px; }
|
|
68
|
+
|
|
69
|
+
.example-card {
|
|
70
|
+
background: var(--card-bg);
|
|
71
|
+
margin-bottom: 15px;
|
|
72
|
+
border-radius: 8px;
|
|
73
|
+
padding: 15px 20px;
|
|
74
|
+
border-left: 4px solid #334155;
|
|
75
|
+
transition: transform 0.2s;
|
|
76
|
+
}
|
|
77
|
+
.example-card:hover { transform: translateX(5px); }
|
|
78
|
+
.example-card.passed { border-left-color: var(--success); }
|
|
79
|
+
.example-card.failed { border-left-color: var(--failure); }
|
|
80
|
+
.example-card.pending { border-left-color: var(--pending); }
|
|
81
|
+
|
|
82
|
+
.description { font-weight: 500; font-size: 1.1em; }
|
|
83
|
+
.location { font-family: monospace; font-size: 0.8em; color: #64748b; margin-top: 5px; }
|
|
84
|
+
.error-message {
|
|
85
|
+
background: #450a0a;
|
|
86
|
+
color: #fecaca;
|
|
87
|
+
padding: 15px;
|
|
88
|
+
border-radius: 6px;
|
|
89
|
+
margin-top: 10px;
|
|
90
|
+
font-family: monospace;
|
|
91
|
+
white-space: pre-wrap;
|
|
92
|
+
font-size: 0.9em;
|
|
93
|
+
}
|
|
94
|
+
</style>
|
|
95
|
+
</head>
|
|
96
|
+
HTML
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def dashboard_section
|
|
100
|
+
<<~HTML
|
|
101
|
+
<div class="dashboard">
|
|
102
|
+
<h1>Eksa-Mination 1.0.0</h1>
|
|
103
|
+
<div class="stats">
|
|
104
|
+
<div class="stat-item">
|
|
105
|
+
<div class="stat-value" style="color: var(--accent)">#{@example_count}</div>
|
|
106
|
+
<div class="stat-label">Examples</div>
|
|
107
|
+
</div>
|
|
108
|
+
<div class="stat-item">
|
|
109
|
+
<div class="stat-value" style="color: var(--success)">#{@example_count - @failures.size - @skipped.size}</div>
|
|
110
|
+
<div class="stat-label">Passed</div>
|
|
111
|
+
</div>
|
|
112
|
+
<div class="stat-item">
|
|
113
|
+
<div class="stat-value" style="color: var(--failure)">#{@failures.size}</div>
|
|
114
|
+
<div class="stat-label">Failed</div>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
HTML
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def results_section
|
|
122
|
+
@results.map do |res|
|
|
123
|
+
example = res[:example]
|
|
124
|
+
status_class = res[:status].to_s
|
|
125
|
+
|
|
126
|
+
error_html = ""
|
|
127
|
+
if res[:status] == :failed
|
|
128
|
+
error = res[:error]
|
|
129
|
+
error_html = "<div class='error-message'>#{error.message}\\n\\n#{error.backtrace.first(5).join("\\n")}</div>"
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
<<~HTML
|
|
133
|
+
<div class="example-card #{status_class}">
|
|
134
|
+
<div class="description">#{example.full_description}</div>
|
|
135
|
+
<div class="location">#{example.file_path}:#{example.line_number}</div>
|
|
136
|
+
#{error_html}
|
|
137
|
+
</div>
|
|
138
|
+
HTML
|
|
139
|
+
end.join("\n")
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def footer_section
|
|
143
|
+
"<div style='text-align: center; color: #475569; margin: 40px 0;'>Generated by Eksa-Mination Testing Framework</div>"
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
require_relative 'base'
|
|
3
|
+
|
|
4
|
+
module EksaMination
|
|
5
|
+
module Formatters
|
|
6
|
+
class JSONFormatter < Base
|
|
7
|
+
def report_success(example); super; end
|
|
8
|
+
def report_failure(example, error); super; end
|
|
9
|
+
def report_skipped(example); super; end
|
|
10
|
+
|
|
11
|
+
def summarize
|
|
12
|
+
data = {
|
|
13
|
+
version: EksaMination::VERSION,
|
|
14
|
+
summary: {
|
|
15
|
+
example_count: @example_count,
|
|
16
|
+
failure_count: @failures.size,
|
|
17
|
+
pending_count: @skipped.size
|
|
18
|
+
},
|
|
19
|
+
examples: @results.map do |res|
|
|
20
|
+
example = res[:example]
|
|
21
|
+
item = {
|
|
22
|
+
description: example.description,
|
|
23
|
+
full_description: example.full_description,
|
|
24
|
+
status: res[:status].to_s,
|
|
25
|
+
file_path: example.file_path,
|
|
26
|
+
line_number: example.line_number
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if res[:status] == :failed
|
|
30
|
+
error = res[:error]
|
|
31
|
+
item[:exception] = {
|
|
32
|
+
class: error.class.name,
|
|
33
|
+
message: error.message,
|
|
34
|
+
backtrace: error.backtrace
|
|
35
|
+
}
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
item
|
|
39
|
+
end
|
|
40
|
+
}
|
|
41
|
+
puts JSON.pretty_generate(data)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
require_relative 'base'
|
|
2
|
+
|
|
3
|
+
module EksaMination
|
|
4
|
+
module Formatters
|
|
5
|
+
class Progress < Base
|
|
6
|
+
def report_success(example)
|
|
7
|
+
super
|
|
8
|
+
print colorize(".", :green)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def report_failure(example, error)
|
|
12
|
+
super
|
|
13
|
+
print colorize("F", :red)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def report_skipped(example)
|
|
17
|
+
super
|
|
18
|
+
print colorize("*", :yellow)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
module EksaMination
|
|
2
|
+
module Matchers
|
|
3
|
+
class BeMatcher
|
|
4
|
+
attr_reader :expected
|
|
5
|
+
def initialize(expected); @expected = expected; end
|
|
6
|
+
def matches?(actual); actual.equal?(@expected); end
|
|
7
|
+
def failure_message; "is not the same object as"; end
|
|
8
|
+
def negative_failure_message; "is the same object as"; end
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
module EksaMination
|
|
2
|
+
module Matchers
|
|
3
|
+
class EqMatcher
|
|
4
|
+
attr_reader :expected
|
|
5
|
+
def initialize(expected); @expected = expected; end
|
|
6
|
+
def matches?(actual); actual == @expected; end
|
|
7
|
+
def failure_message; "is not equal to"; end
|
|
8
|
+
def negative_failure_message; "is equal to"; end
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
module EksaMination
|
|
2
|
+
module Matchers
|
|
3
|
+
class IncludeMatcher
|
|
4
|
+
attr_reader :expected
|
|
5
|
+
def initialize(expected); @expected = expected; end
|
|
6
|
+
def matches?(actual); actual.include?(@expected); end
|
|
7
|
+
def failure_message; "does not include"; end
|
|
8
|
+
def negative_failure_message; "includes"; end
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
module EksaMination
|
|
2
|
+
module Matchers
|
|
3
|
+
class MatchMatcher
|
|
4
|
+
attr_reader :expected
|
|
5
|
+
def initialize(expected); @expected = expected; end
|
|
6
|
+
def matches?(actual); actual =~ @expected; end
|
|
7
|
+
def failure_message; "does not match pattern"; end
|
|
8
|
+
def negative_failure_message; "matches pattern"; end
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
module EksaMination
|
|
2
|
+
module Matchers
|
|
3
|
+
class RaiseErrorMatcher
|
|
4
|
+
attr_reader :expected, :expected_message
|
|
5
|
+
|
|
6
|
+
def initialize(error_class, expected_message)
|
|
7
|
+
@expected = error_class
|
|
8
|
+
@expected_message = expected_message
|
|
9
|
+
@actual_error = nil
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def matches?(block)
|
|
13
|
+
return false unless block.respond_to?(:call)
|
|
14
|
+
begin
|
|
15
|
+
block.call
|
|
16
|
+
false
|
|
17
|
+
rescue @expected => e
|
|
18
|
+
@actual_error = e
|
|
19
|
+
@expected_message ? (e.message == @expected_message) : true
|
|
20
|
+
rescue => e
|
|
21
|
+
@actual_error = e
|
|
22
|
+
false
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def failure_message
|
|
27
|
+
@actual_error ? "raised #{@actual_error.class} (#{@actual_error.message}) instead of #{@expected}" : "did not raise #{@expected}"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def negative_failure_message
|
|
31
|
+
"raised #{@expected}"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
module EksaMination
|
|
2
|
+
module Matchers
|
|
3
|
+
class RespondToMatcher
|
|
4
|
+
attr_reader :expected
|
|
5
|
+
def initialize(expected); @expected = expected; end
|
|
6
|
+
def matches?(actual); actual.respond_to?(@expected); end
|
|
7
|
+
def failure_message; "does not respond to"; end
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
require_relative 'matchers/eq'
|
|
2
|
+
require_relative 'matchers/be'
|
|
3
|
+
require_relative 'matchers/match'
|
|
4
|
+
require_relative 'matchers/include'
|
|
5
|
+
require_relative 'matchers/raise_error'
|
|
6
|
+
require_relative 'matchers/truthy'
|
|
7
|
+
require_relative 'matchers/falsey'
|
|
8
|
+
require_relative 'matchers/be_a'
|
|
9
|
+
require_relative 'matchers/respond_to'
|
|
10
|
+
|
|
11
|
+
module EksaMination
|
|
12
|
+
module Expectations
|
|
13
|
+
def expect(actual = nil, &block)
|
|
14
|
+
Expectation.new(actual || block)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
class Expectation
|
|
19
|
+
def initialize(actual)
|
|
20
|
+
@actual = actual
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def to(matcher)
|
|
24
|
+
# Some matchers (like raise_error) need the block itself
|
|
25
|
+
if matcher.matches?(@actual)
|
|
26
|
+
# Success
|
|
27
|
+
else
|
|
28
|
+
expected = matcher.respond_to?(:expected) ? matcher.expected : nil
|
|
29
|
+
raise MatchError.new(
|
|
30
|
+
"Expected: #{expected.inspect}\nActual: #{@actual.inspect}",
|
|
31
|
+
matcher.failure_message,
|
|
32
|
+
expected,
|
|
33
|
+
@actual
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def not_to(matcher)
|
|
39
|
+
if matcher.matches?(@actual)
|
|
40
|
+
raise MatchError.new("Expected: not to be #{matcher.expected.inspect}\nActual: #{@actual.inspect}", matcher.negative_failure_message)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
class MatchError < StandardError
|
|
46
|
+
attr_reader :detail, :failure_message, :expected, :actual
|
|
47
|
+
|
|
48
|
+
def initialize(detail, failure_message = nil, expected = nil, actual = nil)
|
|
49
|
+
@detail = detail
|
|
50
|
+
@failure_message = failure_message
|
|
51
|
+
@expected = expected
|
|
52
|
+
@actual = actual
|
|
53
|
+
super(detail)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
module Matchers
|
|
58
|
+
def eq(expected)
|
|
59
|
+
EqMatcher.new(expected)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def be(expected)
|
|
63
|
+
BeMatcher.new(expected)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def match(regex)
|
|
67
|
+
MatchMatcher.new(regex)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def include(expected)
|
|
71
|
+
IncludeMatcher.new(expected)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def raise_error(error_class = StandardError, message = nil)
|
|
75
|
+
RaiseErrorMatcher.new(error_class, message)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def be_a(klass)
|
|
79
|
+
BeAMatcher.new(klass)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def respond_to(method)
|
|
83
|
+
RespondToMatcher.new(method)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def be_nil
|
|
87
|
+
be(nil)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def be_truthy
|
|
91
|
+
TruthyMatcher.new
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def be_falsey
|
|
95
|
+
FalseyMatcher.new
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Global monkeypatch
|
|
101
|
+
Object.include(EksaMination::Expectations)
|
|
102
|
+
Object.include(EksaMination::Matchers)
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
module EksaMination
|
|
2
|
+
module Mocks
|
|
3
|
+
def allow(object)
|
|
4
|
+
Proxy.new(object)
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def double(name = "Double")
|
|
8
|
+
Object.new.tap do |obj|
|
|
9
|
+
obj.define_singleton_method(:to_s) { name }
|
|
10
|
+
obj.define_singleton_method(:inspect) { "#<Double:#{name}>" }
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def stub_const(const_name, value)
|
|
15
|
+
stub = ConstantStub.new(const_name, value)
|
|
16
|
+
stub.apply
|
|
17
|
+
EksaMination::Mocks.register_stub(stub)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.reset
|
|
21
|
+
@stubs ||= []
|
|
22
|
+
@stubs.each(&:restore)
|
|
23
|
+
@stubs = []
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.register_stub(stub)
|
|
27
|
+
@stubs ||= []
|
|
28
|
+
@stubs << stub
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
class Proxy
|
|
32
|
+
def initialize(object)
|
|
33
|
+
@object = object
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def to(matcher)
|
|
37
|
+
matcher.set_object(@object)
|
|
38
|
+
matcher
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
class ReceiveMatcher
|
|
43
|
+
def initialize(method_name)
|
|
44
|
+
@method_name = method_name
|
|
45
|
+
@return_value = nil
|
|
46
|
+
@object = nil
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def set_object(object)
|
|
50
|
+
@object = object
|
|
51
|
+
apply_stub
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def and_return(value)
|
|
55
|
+
@return_value = value
|
|
56
|
+
apply_stub if @object
|
|
57
|
+
self
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def apply_stub
|
|
63
|
+
stub = Stub.new(@object, @method_name, @return_value)
|
|
64
|
+
stub.apply
|
|
65
|
+
EksaMination::Mocks.register_stub(stub)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
class Stub
|
|
70
|
+
def initialize(object, method_name, return_value)
|
|
71
|
+
@object = object
|
|
72
|
+
@method_name = method_name
|
|
73
|
+
@return_value = return_value
|
|
74
|
+
@original_method = nil
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def apply
|
|
78
|
+
@original_method = @object.method(@method_name) if @object.respond_to?(@method_name)
|
|
79
|
+
|
|
80
|
+
return_val = @return_value
|
|
81
|
+
|
|
82
|
+
@object.define_singleton_method(@method_name) do |*args, &block|
|
|
83
|
+
return_val
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def restore
|
|
88
|
+
if @original_method
|
|
89
|
+
@object.define_singleton_method(@method_name, &@original_method)
|
|
90
|
+
else
|
|
91
|
+
@object.singleton_class.remove_method(@method_name) rescue nil
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
class ConstantStub
|
|
97
|
+
def initialize(name, value)
|
|
98
|
+
@name = name
|
|
99
|
+
@value = value
|
|
100
|
+
@original_value = nil
|
|
101
|
+
@defined = false
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def apply
|
|
105
|
+
parts = @name.split('::')
|
|
106
|
+
@const_name = parts.pop
|
|
107
|
+
@parent = parts.empty? ? Object : Object.const_get(parts.join('::'))
|
|
108
|
+
|
|
109
|
+
if @parent.const_defined?(@const_name, false)
|
|
110
|
+
@original_value = @parent.const_get(@const_name, false)
|
|
111
|
+
@defined = true
|
|
112
|
+
@parent.send(:remove_const, @const_name)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
@parent.const_set(@const_name, @value)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def restore
|
|
119
|
+
@parent.send(:remove_const, @const_name) rescue nil
|
|
120
|
+
@parent.const_set(@const_name, @original_value) if @defined
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def self.reset_mocks
|
|
126
|
+
Mocks.reset
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
module EksaMination::Matchers
|
|
131
|
+
def receive(method_name)
|
|
132
|
+
EksaMination::Mocks::ReceiveMatcher.new(method_name)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Global monkeypatch
|
|
137
|
+
Object.include(EksaMination::Mocks)
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
module EksaMination
|
|
2
|
+
class Runner
|
|
3
|
+
def initialize(groups, reporter, options = {})
|
|
4
|
+
@groups = groups
|
|
5
|
+
@reporter = reporter
|
|
6
|
+
@options = options
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def run
|
|
10
|
+
@groups.each do |group|
|
|
11
|
+
run_group(group)
|
|
12
|
+
end
|
|
13
|
+
@reporter.summarize
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def run_group(group)
|
|
19
|
+
@reporter.group_started(group)
|
|
20
|
+
begin
|
|
21
|
+
group.examples.each do |example|
|
|
22
|
+
if example.is_a?(ExampleGroup)
|
|
23
|
+
run_group(example) # Recursive for nested groups
|
|
24
|
+
elsif group.skipped || example.skipped
|
|
25
|
+
@reporter.report_skipped(example)
|
|
26
|
+
else
|
|
27
|
+
next unless should_run?(example)
|
|
28
|
+
run_example(group, example)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
ensure
|
|
32
|
+
@reporter.group_finished(group)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def should_run?(example)
|
|
37
|
+
# Match tag filter if provided
|
|
38
|
+
if @options[:tag]
|
|
39
|
+
tag_key = @options[:tag].to_sym
|
|
40
|
+
return false unless example.metadata[tag_key]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Match name filter if provided
|
|
44
|
+
if @options[:example]
|
|
45
|
+
return false unless example.full_description.include?(@options[:example])
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Match line number filter if provided for this file
|
|
49
|
+
if @options[:line_numbers] && @options[:line_numbers][example.file_path]
|
|
50
|
+
return false unless @options[:line_numbers][example.file_path].include?(example.line_number)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
true
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def run_example(group, example)
|
|
57
|
+
# Create an instance to run the hooks and example in
|
|
58
|
+
context = Object.new
|
|
59
|
+
context.extend(EksaMination::Expectations)
|
|
60
|
+
context.extend(EksaMination::Matchers)
|
|
61
|
+
context.extend(EksaMination::Mocks)
|
|
62
|
+
|
|
63
|
+
# Collect all let/subject/hooks from hierarchy
|
|
64
|
+
hierarchy = []
|
|
65
|
+
current = group
|
|
66
|
+
while current
|
|
67
|
+
hierarchy.unshift(current)
|
|
68
|
+
current = current.parent
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Handle let, let!, and subject
|
|
72
|
+
cache = {}
|
|
73
|
+
|
|
74
|
+
hierarchy.each do |g|
|
|
75
|
+
# Define let methods from this group
|
|
76
|
+
g.let_blocks.each do |name, block|
|
|
77
|
+
context.define_singleton_method(name) do
|
|
78
|
+
cache[name] ||= context.instance_eval(&block)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Define let! methods from this group (eagerly evaluated)
|
|
83
|
+
g.let_bang_blocks.each do |name, block|
|
|
84
|
+
context.define_singleton_method(name) do
|
|
85
|
+
cache[name] ||= context.instance_eval(&block)
|
|
86
|
+
end
|
|
87
|
+
# Trigger early
|
|
88
|
+
context.send(name)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Define subject if present in this group
|
|
92
|
+
if g.subject_block
|
|
93
|
+
context.define_singleton_method(:subject) do
|
|
94
|
+
cache[:subject] ||= context.instance_eval(&g.subject_block)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
begin
|
|
100
|
+
# Run before hooks from outside-in
|
|
101
|
+
hierarchy.each do |g|
|
|
102
|
+
g.before_hooks.each { |hook| context.instance_eval(&hook) }
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Execute the test
|
|
106
|
+
context.instance_eval(&example.block)
|
|
107
|
+
@reporter.report_success(example)
|
|
108
|
+
rescue => e
|
|
109
|
+
@reporter.report_failure(example, e)
|
|
110
|
+
ensure
|
|
111
|
+
# Run after hooks from inside-out
|
|
112
|
+
hierarchy.reverse_each do |g|
|
|
113
|
+
g.after_hooks.each { |hook| context.instance_eval(&hook) }
|
|
114
|
+
end
|
|
115
|
+
# Cleanup stubs if we had any
|
|
116
|
+
EksaMination.reset_mocks if EksaMination.respond_to?(:reset_mocks)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
require_relative 'eksa-mination/dsl'
|
|
2
|
+
require_relative 'eksa-mination/matchers'
|
|
3
|
+
require_relative 'eksa-mination/formatters/progress'
|
|
4
|
+
require_relative 'eksa-mination/formatters/documentation'
|
|
5
|
+
require_relative 'eksa-mination/formatters/json'
|
|
6
|
+
require_relative 'eksa-mination/formatters/html'
|
|
7
|
+
require_relative 'eksa-mination/runner'
|
|
8
|
+
require_relative 'eksa-mination/mocks'
|
|
9
|
+
require_relative 'eksa-mination/cli'
|
|
10
|
+
|
|
11
|
+
module EksaMination
|
|
12
|
+
VERSION = "1.0.0"
|
|
13
|
+
|
|
14
|
+
class << self
|
|
15
|
+
attr_accessor :reporter
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
self.reporter = Formatters::Progress.new
|
|
19
|
+
|
|
20
|
+
def self.run(options = {})
|
|
21
|
+
Runner.new(groups, reporter, options).run
|
|
22
|
+
end
|
|
23
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: eksa-mination
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- IshikawaUta
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: Eksa-Mination provides a familiar DSL, comprehensive mocking/stubbing,
|
|
13
|
+
and rich reporting tools in a lightweight package.
|
|
14
|
+
email:
|
|
15
|
+
- komikers09@gmail.com
|
|
16
|
+
executables:
|
|
17
|
+
- eksa-mination
|
|
18
|
+
extensions: []
|
|
19
|
+
extra_rdoc_files: []
|
|
20
|
+
files:
|
|
21
|
+
- ".eksa-mination"
|
|
22
|
+
- LICENSE
|
|
23
|
+
- README.md
|
|
24
|
+
- bin/eksa-mination
|
|
25
|
+
- lib/calculator.rb
|
|
26
|
+
- lib/eksa-mination.rb
|
|
27
|
+
- lib/eksa-mination/cli.rb
|
|
28
|
+
- lib/eksa-mination/dsl.rb
|
|
29
|
+
- lib/eksa-mination/formatters/base.rb
|
|
30
|
+
- lib/eksa-mination/formatters/documentation.rb
|
|
31
|
+
- lib/eksa-mination/formatters/html.rb
|
|
32
|
+
- lib/eksa-mination/formatters/json.rb
|
|
33
|
+
- lib/eksa-mination/formatters/progress.rb
|
|
34
|
+
- lib/eksa-mination/matchers.rb
|
|
35
|
+
- lib/eksa-mination/matchers/be.rb
|
|
36
|
+
- lib/eksa-mination/matchers/be_a.rb
|
|
37
|
+
- lib/eksa-mination/matchers/eq.rb
|
|
38
|
+
- lib/eksa-mination/matchers/falsey.rb
|
|
39
|
+
- lib/eksa-mination/matchers/include.rb
|
|
40
|
+
- lib/eksa-mination/matchers/match.rb
|
|
41
|
+
- lib/eksa-mination/matchers/raise_error.rb
|
|
42
|
+
- lib/eksa-mination/matchers/respond_to.rb
|
|
43
|
+
- lib/eksa-mination/matchers/truthy.rb
|
|
44
|
+
- lib/eksa-mination/mocks.rb
|
|
45
|
+
- lib/eksa-mination/runner.rb
|
|
46
|
+
licenses:
|
|
47
|
+
- MIT
|
|
48
|
+
metadata: {}
|
|
49
|
+
rdoc_options: []
|
|
50
|
+
require_paths:
|
|
51
|
+
- lib
|
|
52
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
53
|
+
requirements:
|
|
54
|
+
- - ">="
|
|
55
|
+
- !ruby/object:Gem::Version
|
|
56
|
+
version: '0'
|
|
57
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - ">="
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '0'
|
|
62
|
+
requirements: []
|
|
63
|
+
rubygems_version: 3.6.7
|
|
64
|
+
specification_version: 4
|
|
65
|
+
summary: A robust and lightweight Ruby testing framework inspired by RSpec.
|
|
66
|
+
test_files: []
|