igata 0.2.1 → 0.3.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.
@@ -13,7 +13,11 @@ class Igata
13
13
  end
14
14
 
15
15
  def generate
16
- raise MethodNotOverriddenError, "#{self.class}#generate must be implemented"
16
+ class_name = @constant_info.path
17
+ methods = generate_methods
18
+
19
+ template = ERB.new(File.read(template_path("class")), trim_mode: "<>")
20
+ template.result(binding)
17
21
  end
18
22
 
19
23
  private
@@ -30,6 +34,18 @@ class Igata
30
34
  template_content = File.read(template_file)
31
35
  ERB.new(template_content, trim_mode: "<>").result(binding_context)
32
36
  end
37
+
38
+ def generate_methods
39
+ @method_infos.map do |method_info|
40
+ method_name = method_info.name
41
+ branches = method_info.branches
42
+ comparisons = method_info.comparisons
43
+ exceptions = method_info.exceptions
44
+ boundary_values = method_info.boundary_values
45
+ arguments = method_info.arguments
46
+ ERB.new(File.read(template_path("method")), trim_mode: "<>").result(binding)
47
+ end
48
+ end
33
49
  end
34
50
  end
35
51
  end
@@ -5,28 +5,11 @@ require_relative "base"
5
5
  class Igata
6
6
  module Formatters
7
7
  class Minitest < Base
8
- def generate
9
- class_name = @constant_info.path
10
- methods = generate_methods
11
-
12
- template = ERB.new(File.read(template_path("class")), trim_mode: "<>")
13
- template.result(binding)
14
- end
15
-
16
8
  private
17
9
 
18
10
  def templates_dir
19
11
  File.join(__dir__, "templates", "minitest")
20
12
  end
21
-
22
- def generate_methods
23
- @method_infos.map do |method_info|
24
- method_name = method_info.name
25
- branches = method_info.branches
26
- comparisons = method_info.comparisons
27
- ERB.new(File.read(template_path("method")), trim_mode: "<>").result(binding)
28
- end
29
- end
30
13
  end
31
14
  end
32
15
  end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ class Igata
6
+ module Formatters
7
+ class MinitestSpec < Base
8
+ private
9
+
10
+ def templates_dir
11
+ File.join(__dir__, "templates", "minitest_spec")
12
+ end
13
+ end
14
+ end
15
+ end
@@ -5,28 +5,11 @@ require_relative "base"
5
5
  class Igata
6
6
  module Formatters
7
7
  class RSpec < Base
8
- def generate
9
- class_name = @constant_info.path
10
- methods = generate_methods
11
-
12
- template = ERB.new(File.read(template_path("class")), trim_mode: "<>")
13
- template.result(binding)
14
- end
15
-
16
8
  private
17
9
 
18
10
  def templates_dir
19
11
  File.join(__dir__, "templates", "rspec")
20
12
  end
21
-
22
- def generate_methods
23
- @method_infos.map do |method_info|
24
- method_name = method_info.name
25
- branches = method_info.branches
26
- comparisons = method_info.comparisons
27
- ERB.new(File.read(template_path("method")), trim_mode: "<>").result(binding)
28
- end
29
- end
30
13
  end
31
14
  end
32
15
  end
@@ -1,9 +1,45 @@
1
1
  def test_<%= method_name %>
2
+ <% if arguments && !arguments.args.empty? %>
3
+ # Arguments:
4
+ <% arguments.args.each do |arg| %>
5
+ <% if arg.type == :required %>
6
+ # - <%= arg.name %> (required)
7
+ <% elsif arg.type == :optional %>
8
+ # - <%= arg.name %> (optional, default: <%= arg.default %>)
9
+ <% elsif arg.type == :keyword %>
10
+ # - <%= arg.name %>: (keyword, default: <%= arg.default %>)
11
+ <% elsif arg.type == :required_keyword %>
12
+ # - <%= arg.name %>: (required keyword)
13
+ <% elsif arg.type == :rest %>
14
+ # - *<%= arg.name %> (variable length)
15
+ <% elsif arg.type == :keyrest %>
16
+ # - **<%= arg.name %> (keyword variable length)
17
+ <% elsif arg.type == :block %>
18
+ # - &<%= arg.name %> (block)
19
+ <% end %>
20
+ <% end %>
21
+ <% end %>
2
22
  <% if branches && !branches.empty? %>
3
23
  # Branches: <%= branches.map { |b| b.condition ? "#{b.type} (#{b.condition})" : b.type.to_s }.join(", ") %>
4
24
  <% end %>
5
25
  <% if comparisons && !comparisons.empty? %>
6
26
  # Comparisons: <%= comparisons.map { |c| "#{c.operator} (#{c.context})" }.join(", ") %>
27
+ <% end %>
28
+ <% if boundary_values && !boundary_values.empty? %>
29
+ # Boundary value suggestions:
30
+ <% boundary_values.each do |bv| %>
31
+ # <%= bv.comparison.context %>: [<%= bv.description %>]
32
+ <% end %>
33
+ <% end %>
34
+ <% if exceptions && !exceptions.empty? %>
35
+ <% raised = exceptions.select { |e| e.type == :raise } %>
36
+ <% rescued = exceptions.select { |e| e.type == :rescue } %>
37
+ <% if !raised.empty? %>
38
+ # Exceptions raised: <%= raised.map { |e| e.message ? "#{e.exception_class} (\"#{e.message}\")" : e.exception_class }.join(", ") %>
39
+ <% end %>
40
+ <% if !rescued.empty? %>
41
+ # Exceptions rescued: <%= rescued.map { |e| e.exception_class }.uniq.join(", ") %>
42
+ <% end %>
7
43
  <% end %>
8
44
  skip "Not implemented yet"
9
45
  end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+ require "minitest/spec"
5
+
6
+ describe <%= class_name %> do
7
+ <% methods.each_with_index do |method, index| %>
8
+ <%= method %>
9
+ <% if index < methods.length - 1 %>
10
+
11
+ <% end %>
12
+ <% end %>
13
+ end
@@ -0,0 +1,47 @@
1
+ describe "#<%= method_name %>" do
2
+ <% if arguments && !arguments.args.empty? %>
3
+ # Arguments:
4
+ <% arguments.args.each do |arg| %>
5
+ <% if arg.type == :required %>
6
+ # - <%= arg.name %> (required)
7
+ <% elsif arg.type == :optional %>
8
+ # - <%= arg.name %> (optional, default: <%= arg.default %>)
9
+ <% elsif arg.type == :keyword %>
10
+ # - <%= arg.name %>: (keyword, default: <%= arg.default %>)
11
+ <% elsif arg.type == :required_keyword %>
12
+ # - <%= arg.name %>: (required keyword)
13
+ <% elsif arg.type == :rest %>
14
+ # - *<%= arg.name %> (variable length)
15
+ <% elsif arg.type == :keyrest %>
16
+ # - **<%= arg.name %> (keyword variable length)
17
+ <% elsif arg.type == :block %>
18
+ # - &<%= arg.name %> (block)
19
+ <% end %>
20
+ <% end %>
21
+ <% end %>
22
+ <% if branches && !branches.empty? %>
23
+ # Branches: <%= branches.map { |b| b.condition ? "#{b.type} (#{b.condition})" : b.type.to_s }.join(", ") %>
24
+ <% end %>
25
+ <% if comparisons && !comparisons.empty? %>
26
+ # Comparisons: <%= comparisons.map { |c| "#{c.operator} (#{c.context})" }.join(", ") %>
27
+ <% end %>
28
+ <% if boundary_values && !boundary_values.empty? %>
29
+ # Boundary value suggestions:
30
+ <% boundary_values.each do |bv| %>
31
+ # <%= bv.comparison.context %>: [<%= bv.description %>]
32
+ <% end %>
33
+ <% end %>
34
+ <% if exceptions && !exceptions.empty? %>
35
+ <% raised = exceptions.select { |e| e.type == :raise } %>
36
+ <% rescued = exceptions.select { |e| e.type == :rescue } %>
37
+ <% if !raised.empty? %>
38
+ # Exceptions raised: <%= raised.map { |e| e.message ? "#{e.exception_class} (\"#{e.message}\")" : e.exception_class }.join(", ") %>
39
+ <% end %>
40
+ <% if !rescued.empty? %>
41
+ # Exceptions rescued: <%= rescued.map { |e| e.exception_class }.uniq.join(", ") %>
42
+ <% end %>
43
+ <% end %>
44
+ it "works correctly" do
45
+ skip "Not implemented yet"
46
+ end
47
+ end
@@ -1,9 +1,45 @@
1
1
  describe "#<%= method_name %>" do
2
+ <% if arguments && !arguments.args.empty? %>
3
+ # Arguments:
4
+ <% arguments.args.each do |arg| %>
5
+ <% if arg.type == :required %>
6
+ # - <%= arg.name %> (required)
7
+ <% elsif arg.type == :optional %>
8
+ # - <%= arg.name %> (optional, default: <%= arg.default %>)
9
+ <% elsif arg.type == :keyword %>
10
+ # - <%= arg.name %>: (keyword, default: <%= arg.default %>)
11
+ <% elsif arg.type == :required_keyword %>
12
+ # - <%= arg.name %>: (required keyword)
13
+ <% elsif arg.type == :rest %>
14
+ # - *<%= arg.name %> (variable length)
15
+ <% elsif arg.type == :keyrest %>
16
+ # - **<%= arg.name %> (keyword variable length)
17
+ <% elsif arg.type == :block %>
18
+ # - &<%= arg.name %> (block)
19
+ <% end %>
20
+ <% end %>
21
+ <% end %>
2
22
  <% if branches && !branches.empty? %>
3
23
  # Branches: <%= branches.map { |b| b.condition ? "#{b.type} (#{b.condition})" : b.type.to_s }.join(", ") %>
4
24
  <% end %>
5
25
  <% if comparisons && !comparisons.empty? %>
6
26
  # Comparisons: <%= comparisons.map { |c| "#{c.operator} (#{c.context})" }.join(", ") %>
27
+ <% end %>
28
+ <% if boundary_values && !boundary_values.empty? %>
29
+ # Boundary value suggestions:
30
+ <% boundary_values.each do |bv| %>
31
+ # <%= bv.comparison.context %>: [<%= bv.description %>]
32
+ <% end %>
33
+ <% end %>
34
+ <% if exceptions && !exceptions.empty? %>
35
+ <% raised = exceptions.select { |e| e.type == :raise } %>
36
+ <% rescued = exceptions.select { |e| e.type == :rescue } %>
37
+ <% if !raised.empty? %>
38
+ # Exceptions raised: <%= raised.map { |e| e.message ? "#{e.exception_class} (\"#{e.message}\")" : e.exception_class }.join(", ") %>
39
+ <% end %>
40
+ <% if !rescued.empty? %>
41
+ # Exceptions rescued: <%= rescued.map { |e| e.exception_class }.uniq.join(", ") %>
42
+ <% end %>
7
43
  <% end %>
8
44
  it "works correctly" do
9
45
  pending "Not implemented yet"
data/lib/igata/values.rb CHANGED
@@ -9,12 +9,15 @@ class Igata
9
9
  )
10
10
 
11
11
  MethodInfo = Data.define(
12
- :name, # "initialize"
13
- :branches, # Array of BranchInfo (default: [])
14
- :comparisons # Array of ComparisonInfo (default: [])
12
+ :name, # "initialize"
13
+ :branches, # Array of BranchInfo (default: [])
14
+ :comparisons, # Array of ComparisonInfo (default: [])
15
+ :exceptions, # Array of ExceptionInfo (default: [])
16
+ :boundary_values, # Array of BoundaryValueInfo (default: [])
17
+ :arguments # ArgumentInfo (default: nil)
15
18
  ) do
16
- def initialize(name:, branches: [], comparisons: [])
17
- super(name: name, branches: branches, comparisons: comparisons)
19
+ def initialize(name:, branches: [], comparisons: [], exceptions: [], boundary_values: [], arguments: nil)
20
+ super
18
21
  end
19
22
  end
20
23
 
@@ -29,5 +32,32 @@ class Igata
29
32
  :right, # right side expression
30
33
  :context # full expression as string (e.g., "age >= 18")
31
34
  )
35
+
36
+ ExceptionInfo = Data.define(
37
+ :type, # :raise or :rescue
38
+ :exception_class, # exception class name (e.g., "ArgumentError", "StandardError")
39
+ :message, # message for raise (e.g., "Invalid amount")
40
+ :context # full expression as string
41
+ )
42
+
43
+ BoundaryValueInfo = Data.define(
44
+ :comparison, # ComparisonInfo object
45
+ :test_values, # Array of test values (e.g., [17, 18, 19])
46
+ :description # description string (e.g., "Boundary: 17 (below), 18 (boundary), 19 (above)")
47
+ )
48
+
49
+ ArgumentInfo = Data.define(
50
+ :args # Array of ArgDetail
51
+ )
52
+
53
+ ArgDetail = Data.define(
54
+ :name, # "name", "age", "verified", "args", etc.
55
+ :type, # :required, :optional, :keyword, :rest, :keyrest, :block
56
+ :default # default value (for optional/keyword arguments)
57
+ ) do
58
+ def initialize(name:, type:, default: nil)
59
+ super
60
+ end
61
+ end
32
62
  end
33
63
  end
data/lib/igata/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Igata
4
- VERSION = "0.2.1"
4
+ VERSION = "0.3.0"
5
5
  end
data/lib/igata.rb CHANGED
@@ -10,58 +10,98 @@ require_relative "igata/extractors/constant_path"
10
10
  require_relative "igata/extractors/method_names"
11
11
  require_relative "igata/extractors/branch_analyzer"
12
12
  require_relative "igata/extractors/comparison_analyzer"
13
+ require_relative "igata/extractors/exception_analyzer"
14
+ require_relative "igata/extractors/boundary_value_generator"
15
+ require_relative "igata/extractors/argument_extractor"
13
16
  require_relative "igata/formatters/minitest"
14
17
  require_relative "igata/formatters/rspec"
18
+ require_relative "igata/formatters/minitest_spec"
15
19
 
16
20
  class Igata
17
21
  def initialize(source, formatter: :minitest)
18
22
  @source = source
19
- @ast = Kanayago.parse(source)
23
+ parse_result = Kanayago.parse(source)
24
+ @ast = parse_result.ast
25
+ @script_lines = parse_result.script_lines
20
26
  @formatter = formatter
21
27
  end
22
28
 
23
29
  def generate
24
30
  constant_info = Extractors::ConstantPath.extract(@ast)
31
+ return "" unless constant_info
32
+
25
33
  target_node = find_target_class_node(constant_info)
26
34
  method_infos = Extractors::MethodNames.extract(target_node)
27
35
 
36
+ # Add argument information to method_infos
37
+ method_infos = method_infos.map do |method_info|
38
+ arg_info = Extractors::ArgumentExtractor.extract(method_info, @script_lines)
39
+ method_info.with(arguments: arg_info)
40
+ end
41
+
28
42
  formatter_class = resolve_formatter(@formatter)
29
43
  formatter_class.new(constant_info, method_infos).generate
30
44
  end
31
45
 
32
46
  private
33
47
 
48
+ # rubocop:disable Metrics/MethodLength
34
49
  def resolve_formatter(formatter)
35
50
  case formatter
36
51
  when :minitest
37
52
  Formatters::Minitest
38
53
  when :rspec
39
54
  Formatters::RSpec
55
+ when :minitest_spec
56
+ Formatters::MinitestSpec
40
57
  when Class
41
58
  formatter
42
59
  else
43
60
  raise Error, "Unknown formatter: #{formatter}"
44
61
  end
45
62
  end
63
+ # rubocop:enable Metrics/MethodLength
46
64
 
47
65
  def find_target_class_node(constant_info)
66
+ # First, find the actual ClassNode if @ast.body is a BlockNode
67
+ root_class_node = find_root_class_node(@ast.body)
68
+
48
69
  if constant_info.nested
49
70
  # When nested is true, recursively find the deepest (innermost) class
50
71
  # - class User; class Profile; end; end
51
72
  # - class App::User; class Profile; end; end (mixed pattern)
52
73
  # - class App::Model; class Admin::User; class Profile; end; end; end (3+ levels)
53
- find_deepest_class_node(@ast.body)
74
+ find_deepest_class_node(root_class_node)
54
75
  else
55
- # When nested is false, @ast.body itself is the target
76
+ # When nested is false, root_class_node itself is the target
56
77
  # - class User
57
78
  # - class User::Profile
58
- @ast.body
79
+ root_class_node
80
+ end
81
+ end
82
+
83
+ # rubocop:disable Lint/DuplicateBranch
84
+ def find_root_class_node(node)
85
+ if node.is_a?(Kanayago::ClassNode) || node.is_a?(Kanayago::ModuleNode)
86
+ node
87
+ elsif node.is_a?(Kanayago::BlockNode) && node.respond_to?(:find)
88
+ node.find { |n| n.is_a?(Kanayago::ClassNode) || n.is_a?(Kanayago::ModuleNode) }
89
+ else
90
+ node
59
91
  end
60
92
  end
93
+ # rubocop:enable Lint/DuplicateBranch
61
94
 
62
- def find_deepest_class_node(parent_node)
95
+ def find_deepest_class_node(parent_node) # rubocop:disable Metrics/CyclomaticComplexity
63
96
  # Find direct child class/module under the current node
64
- direct_child = parent_node.body.body.find { |node| node.is_a?(Kanayago::ClassNode) || node.is_a?(Kanayago::ModuleNode) }
97
+ return nil unless parent_node.respond_to?(:body)
98
+ return nil unless parent_node.body.respond_to?(:body)
99
+
100
+ class_body = parent_node.body.body
101
+ # For empty classes, class_body is BeginNode which doesn't have find
102
+ return nil unless class_body.respond_to?(:find)
103
+
104
+ direct_child = class_body.find { |node| node.is_a?(Kanayago::ClassNode) || node.is_a?(Kanayago::ModuleNode) }
65
105
  return nil unless direct_child
66
106
 
67
107
  # Check if there's deeper nesting in the child node
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: igata
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - S-H-GAMELINKS
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - "~>"
17
17
  - !ruby/object:Gem::Version
18
- version: 0.3.0
18
+ version: 0.6.1
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
- version: 0.3.0
25
+ version: 0.6.1
26
26
  description: Generate test code from AST node produces by Ruby's Parser
27
27
  email:
28
28
  - gamelinks007@gmail.com
@@ -37,18 +37,25 @@ files:
37
37
  - QUICKSTART.md
38
38
  - README.md
39
39
  - Rakefile
40
+ - USAGE.md
40
41
  - exe/igata
41
42
  - lib/igata.rb
42
43
  - lib/igata/error.rb
44
+ - lib/igata/extractors/argument_extractor.rb
45
+ - lib/igata/extractors/boundary_value_generator.rb
43
46
  - lib/igata/extractors/branch_analyzer.rb
44
47
  - lib/igata/extractors/comparison_analyzer.rb
45
48
  - lib/igata/extractors/constant_path.rb
49
+ - lib/igata/extractors/exception_analyzer.rb
46
50
  - lib/igata/extractors/method_names.rb
47
51
  - lib/igata/formatters/base.rb
48
52
  - lib/igata/formatters/minitest.rb
53
+ - lib/igata/formatters/minitest_spec.rb
49
54
  - lib/igata/formatters/rspec.rb
50
55
  - lib/igata/formatters/templates/minitest/class.erb
51
56
  - lib/igata/formatters/templates/minitest/method.erb
57
+ - lib/igata/formatters/templates/minitest_spec/class.erb
58
+ - lib/igata/formatters/templates/minitest_spec/method.erb
52
59
  - lib/igata/formatters/templates/rspec/class.erb
53
60
  - lib/igata/formatters/templates/rspec/method.erb
54
61
  - lib/igata/values.rb