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 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
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative '../lib/eksa-mination'
4
+
5
+ # Start the CLI
6
+ EksaMination::CLI.new(ARGV).run
data/lib/calculator.rb ADDED
@@ -0,0 +1,18 @@
1
+ class Calculator
2
+ def add(a, b)
3
+ a + b
4
+ end
5
+
6
+ def subtract(a, b)
7
+ a - b
8
+ end
9
+
10
+ def multiply(a, b)
11
+ a * b
12
+ end
13
+
14
+ def divide(a, b)
15
+ raise "Division by zero" if b == 0
16
+ a.to_f / b
17
+ end
18
+ end
@@ -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,10 @@
1
+ module EksaMination
2
+ module Matchers
3
+ class BeAMatcher
4
+ attr_reader :expected
5
+ def initialize(expected); @expected = expected; end
6
+ def matches?(actual); actual.is_a?(@expected); end
7
+ def failure_message; "is not a kind of"; end
8
+ end
9
+ end
10
+ 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,10 @@
1
+ module EksaMination
2
+ module Matchers
3
+ class FalseyMatcher
4
+ def expected; "falsey"; end
5
+ def matches?(actual); !actual; end
6
+ def failure_message; "is not falsey"; end
7
+ def negative_failure_message; "is falsey"; end
8
+ end
9
+ end
10
+ 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,10 @@
1
+ module EksaMination
2
+ module Matchers
3
+ class TruthyMatcher
4
+ def expected; "truthy"; end
5
+ def matches?(actual); !!actual; end
6
+ def failure_message; "is not truthy"; end
7
+ def negative_failure_message; "is truthy"; 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: []