tng 0.2.7 → 0.2.9

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c30c794fbce7688603e0535f1673b9c755627d090846074a55f49614eb61eb59
4
- data.tar.gz: 487772b58677a33f9f54719bcac234a17f7be7905f0ef00001d1fae259e06704
3
+ metadata.gz: 5f6e0805bc5c9a7dbb6d83b8298fcbc099a14c70e118c02743545a29dfbc8182
4
+ data.tar.gz: 070b0bbc5c54a91b01a2e38113588e7838bb0ff1d8217e0f666695d953a7ea6a
5
5
  SHA512:
6
- metadata.gz: 720a2884c77eb6aa515a9fdcc765554725c08a45afbf7cf278ed03025067d13200550acf86d50edc34334d503fd76d61ef6114d26b5b4d0e6884166f1e74c302
7
- data.tar.gz: b607b077576e8cddc418e58ad2647ea8a1e5e2079ce9ad34420cde5a8b41c6d66d8a1f69f80d105365acb1818be161352b051a1d01e91b2e494b73dd2e5bafd4
6
+ metadata.gz: f46c75ee3f7fa1cc0e22bdb46687c600184e9f2435f8321be71bc1623c33c7a1494326d0cfcd2030017bbe94244f39869870a16f245d9abc3e2ee8b307dedf98
7
+ data.tar.gz: 1c971f70e79e8cf9d9bd99ce779e3d244bee955908aec4ab679fd7a72c491e68a624b2fa56ee8195ecef2e42ba2b3df2828bb7dcc8278caa5374692c68522807
data/README.md CHANGED
@@ -124,9 +124,6 @@ Tng.configure do |config|
124
124
  config.api_key = ENV["TNG_API_KEY"]
125
125
  config.base_url = "https://app.tng.sh/"
126
126
 
127
- config.read_source_code = true
128
- config.read_test_code = true
129
-
130
127
  config.testing_framework = "rspec"
131
128
  config.assertion_style = "expect"
132
129
  config.describe_style = true
@@ -189,10 +186,6 @@ end
189
186
  Tng.configure do |config|
190
187
  config.api_key = ENV["TNG_API_KEY"]
191
188
  config.base_url = "https://app.tng.sh/"
192
-
193
- config.read_source_code = true
194
- config.read_test_code = true
195
-
196
189
  config.testing_framework = "rspec"
197
190
  config.assertion_style = "expect"
198
191
  config.let_style = true
data/bin/tng CHANGED
@@ -130,15 +130,15 @@ class CLI
130
130
  def preprocess_arguments(argv)
131
131
  normalized = []
132
132
  positional_args = []
133
-
133
+
134
134
  argv.each_with_index do |arg, index|
135
135
  case arg
136
136
  when /^(?:--)?(file|f)=(.+)$/
137
- normalized << "--file=#{$2}"
137
+ normalized << "--file=#{::Regexp.last_match(2)}"
138
138
  when /^(?:--)?(method|m)=(.+)$/
139
- normalized << "--method=#{$2}"
139
+ normalized << "--method=#{::Regexp.last_match(2)}"
140
140
  when /^(help|h)=(.+)$/
141
- normalized << "--help=#{$2}"
141
+ normalized << "--help=#{::Regexp.last_match(2)}"
142
142
  when /^--file$/, /^-f$/
143
143
  normalized << arg
144
144
  when /^--method$/, /^-m$/
@@ -156,29 +156,25 @@ class CLI
156
156
  end
157
157
  end
158
158
  end
159
-
159
+
160
160
  if positional_args.length >= 2
161
161
  has_file = normalized.any? { |a| a.match?(/^--file/) }
162
162
  has_method = normalized.any? { |a| a.match?(/^--method/) }
163
-
164
- unless has_file
165
- normalized << "--file=#{positional_args[0]}"
166
- end
167
- unless has_method
168
- normalized << "--method=#{positional_args[1]}"
169
- end
163
+
164
+ normalized << "--file=#{positional_args[0]}" unless has_file
165
+ normalized << "--method=#{positional_args[1]}" unless has_method
170
166
  elsif positional_args.length == 1
171
167
  arg = positional_args[0]
172
168
  has_file = normalized.any? { |a| a.match?(/^--file/) }
173
169
  has_method = normalized.any? { |a| a.match?(/^--method/) }
174
-
175
- if !has_file && (arg.end_with?('.rb') || arg.include?('/'))
170
+
171
+ if !has_file && (arg.end_with?(".rb") || arg.include?("/"))
176
172
  normalized << "--file=#{arg}"
177
- elsif !has_method && !arg.include?('/')
173
+ elsif !has_method && !arg.include?("/")
178
174
  normalized << "--method=#{arg}"
179
175
  end
180
176
  end
181
-
177
+
182
178
  normalized
183
179
  end
184
180
 
@@ -831,7 +827,7 @@ class CLI
831
827
 
832
828
  info_msg = [
833
829
  @pastel.decorate("#{Tng::UI::Theme.icon(:config)} File: #{result[:file_path]}", Tng::UI::Theme.color(:info)),
834
- @pastel.decorate("#{Tng::UI::Theme.icon(:marker)} Run: #{result[:run_command]}", Tng::UI::Theme.color(:warning))
830
+ @pastel.decorate("#{Tng::UI::Theme.icon(:marker)} Run: #{result[:run_command]}", Tng::UI::Theme.color(:secondary))
835
831
  ].join("\n")
836
832
  puts center_text(info_msg)
837
833
  puts
@@ -849,7 +845,7 @@ class CLI
849
845
 
850
846
  info_msg = [
851
847
  @pastel.decorate("#{Tng::UI::Theme.icon(:config)} File: #{result[:file_path]}", Tng::UI::Theme.color(:info)),
852
- @pastel.decorate("#{Tng::UI::Theme.icon(:marker)} Run: #{result[:run_command]}", Tng::UI::Theme.color(:warning))
848
+ @pastel.decorate("#{Tng::UI::Theme.icon(:marker)} Run: #{result[:run_command]}", Tng::UI::Theme.color(:secondary))
853
849
  ].join("\n")
854
850
  puts center_text(info_msg)
855
851
  puts
data/binaries/tng.bundle CHANGED
Binary file
data/binaries/tng.so CHANGED
Binary file
@@ -3,6 +3,7 @@
3
3
  require "rails/generators"
4
4
  require "yaml"
5
5
  require "tng"
6
+ require "pathname"
6
7
 
7
8
  module Tng
8
9
  module Generators
@@ -18,6 +19,7 @@ module Tng
18
19
  @authentication_library = detect_authentication_library
19
20
  @authz_library = detect_authorization_library
20
21
  @factory_library = detect_factory_library
22
+ @test_examples = detect_test_examples
21
23
 
22
24
  initializer_path = "config/initializers/tng.rb"
23
25
 
@@ -61,6 +63,8 @@ module Tng
61
63
  factory_lib = "config.factory_library = \"#{@factory_library}\""
62
64
 
63
65
  framework_specific = generate_framework_specific_config(@test_framework, framework_config)
66
+
67
+ test_examples_config = @test_examples.any? ? @test_examples.inspect : "[]"
64
68
  [
65
69
  "# frozen_string_literal: true",
66
70
  "",
@@ -70,8 +74,6 @@ module Tng
70
74
  " config.api_key = nil",
71
75
  " # You dont need to change this url, unless you will instructed by the CLI.",
72
76
  " config.base_url = \"https://app.tng.sh/\"",
73
- " config.read_source_code = true # Options: true, false",
74
- " config.read_test_code = true # Options: true, false",
75
77
  "",
76
78
  " # Testing Framework",
77
79
  " config.testing_framework = \"#{@test_framework}\" # Options: minitest, rspec",
@@ -80,6 +82,12 @@ module Tng
80
82
  " config.http_mock_library = \"#{framework_config["http_mock_library"]}\" # Options: webmock, vcr, httparty, nil",
81
83
  " #{factory_lib} # Options: factory_bot, factory_girl, fabrication, fabricator, fixtures, active_record",
82
84
  "",
85
+ " # Test Examples",
86
+ " # Example test files for LLM to learn patterns and reduce hallucinations",
87
+ " # Format: [{{\"name\" => \"test_name\", \"path\" => \"spec/models/user_spec.rb\"}}]",
88
+ " # Leave empty [] to auto-detect from project",
89
+ " config.test_examples = #{test_examples_config}",
90
+ "",
83
91
  " # Authentication#{auth_comment}",
84
92
  " #{auth_enabled} # Options: true, false",
85
93
  " #{auth_lib} # Options: devise, clearance, sorcery, nil",
@@ -190,6 +198,156 @@ module Tng
190
198
  "active_record"
191
199
  end
192
200
 
201
+ def detect_test_examples
202
+ rails_root = defined?(Rails) && Rails.root ? Rails.root.to_s : Dir.pwd
203
+ examples = []
204
+
205
+ # Determine test directory based on framework
206
+ test_dir = @test_framework == "rspec" ? "spec" : "test"
207
+
208
+ # Comprehensive exclusion list for build artifacts, caches, dependencies
209
+ exclude_dirs = [
210
+ ".git", "log", "tmp", "vendor", "node_modules",
211
+ "coverage", "public", "storage", "db", "config",
212
+ "lib/tasks", "bin", ".bundle", ".vscode", ".idea"
213
+ ]
214
+
215
+ # Also exclude files larger than 500KB (probably not good examples)
216
+ max_file_size = 500 * 1024 # 500KB
217
+
218
+ # Find all test directories and subdirectories
219
+ test_directories = find_test_directories(rails_root, test_dir, exclude_dirs)
220
+
221
+ # Determine collection strategy based on folder structure
222
+ main_test_dir = File.join(rails_root, test_dir)
223
+ has_subfolders = test_directories.size > 1
224
+
225
+ if has_subfolders
226
+ # If we have subfolders, take 2 files per directory
227
+ collected_files = []
228
+ test_directories.each do |dir_path|
229
+ dir_files = find_valid_test_files_in_directory(dir_path, max_file_size)
230
+ # Take up to 2 files from this directory
231
+ collected_files.concat(dir_files.first(2))
232
+ end
233
+ else
234
+ # If no subfolders, take up to 5 files from main test directory
235
+ collected_files = find_valid_test_files_in_directory(main_test_dir, max_file_size).first(5)
236
+ end
237
+
238
+ # Sort all collected files by modification time (recent first), then by size (smaller files first)
239
+ collected_files.sort_by! { |f| [-File.mtime(f).to_i, File.size(f)] }
240
+
241
+ # Convert to examples format
242
+ collected_files.first(5).each do |test_file|
243
+ examples << {
244
+ "name" => File.basename(test_file),
245
+ "path" => Pathname.new(test_file).relative_path_from(Pathname.new(rails_root)).to_s
246
+ }
247
+ rescue StandardError
248
+ next
249
+ end
250
+
251
+ examples
252
+ rescue StandardError
253
+ # Return empty array if anything goes wrong
254
+ []
255
+ end
256
+
257
+ def find_test_directories(rails_root, test_dir, exclude_dirs)
258
+ directories = []
259
+ main_test_path = File.join(rails_root, test_dir)
260
+
261
+ return [] unless Dir.exist?(main_test_path)
262
+
263
+ # Add main test directory
264
+ directories << main_test_path
265
+
266
+ # Find all subdirectories recursively
267
+ Dir.glob(File.join(main_test_path, "**/*/")).each do |dir|
268
+ # Skip excluded directories
269
+ next if exclude_dirs.any? { |excluded| dir.include?("/#{excluded}/") }
270
+
271
+ # Skip if it's not a directory with actual test files
272
+ next unless test_files?(dir)
273
+
274
+ directories << dir
275
+ end
276
+
277
+ directories.uniq
278
+ end
279
+
280
+ def test_files?(dir_path)
281
+ return false unless Dir.exist?(dir_path)
282
+
283
+ pattern = @test_framework == "rspec" ? "*_spec.rb" : "*_test.rb"
284
+ Dir.glob(File.join(dir_path, pattern)).any?
285
+ end
286
+
287
+ def find_valid_test_files_in_directory(dir_path, max_file_size)
288
+ return [] unless Dir.exist?(dir_path)
289
+
290
+ pattern = @test_framework == "rspec" ? "*_spec.rb" : "*_test.rb"
291
+ files = []
292
+
293
+ Dir.glob(File.join(dir_path, pattern)).each do |test_file|
294
+ next if File.size(test_file) > max_file_size
295
+ next unless valid_test_file?(test_file)
296
+
297
+ files << test_file
298
+ rescue StandardError
299
+ next
300
+ end
301
+
302
+ files
303
+ end
304
+
305
+ def valid_test_file?(file_path)
306
+ return false unless File.exist?(file_path) && File.readable?(file_path)
307
+ return false if File.size(file_path) == 0
308
+
309
+ begin
310
+ # Read first 1KB of the file
311
+ content = File.read(file_path, 1024)
312
+
313
+ # Check for Ruby test indicators based on framework
314
+ test_indicators = if @test_framework == "rspec"
315
+ [
316
+ "describe ", # RSpec describe blocks
317
+ "context ", # RSpec context blocks
318
+ "it ", # RSpec examples
319
+ "specify ", # RSpec specify
320
+ "expect(", # RSpec expectations
321
+ "before(", # RSpec hooks
322
+ "let(", # RSpec let
323
+ "subject ", # RSpec subject
324
+ 'require "spec_helper"', # RSpec spec helper
325
+ 'require "rails_helper"' # RSpec rails helper
326
+ ]
327
+ else # minitest
328
+ [
329
+ "def test_", # Minitest test methods
330
+ "class.*Test", # Test classes
331
+ "assert ", # Assertions
332
+ "refute ", # Refutations
333
+ 'require "test_helper"', # Minitest helper
334
+ 'require "minitest"', # Minitest require
335
+ "MiniTest::Test" # Minitest base class
336
+ ]
337
+ end
338
+
339
+ # Must contain at least one test indicator
340
+ return false unless test_indicators.any? { |indicator| content.include?(indicator) }
341
+
342
+ # Basic Ruby syntax check - should not have obvious non-Ruby content
343
+ return false if content.scan(/#!/).size > 5 || content.include?("<?xml")
344
+
345
+ true
346
+ rescue StandardError
347
+ false
348
+ end
349
+ end
350
+
193
351
  def generate_framework_config(framework)
194
352
  if framework == "minitest"
195
353
  {
@@ -33,32 +33,50 @@ module Tng
33
33
  instance_methods = model_class.public_instance_methods(false)
34
34
  class_methods = model_class.public_methods(false) - Class.public_methods
35
35
 
36
- model_file = model_class.const_source_location(model_class.name.split("::").last)&.first
36
+ model_file = Object.const_source_location(model_class.name)&.first
37
37
 
38
- model_methods = if model_file && File.exist?(model_file)
39
- source_code = File.read(model_file)
40
- result = Prism.parse(source_code)
38
+ if model_file && File.exist?(model_file)
39
+ source_code = File.read(model_file)
40
+ result = Prism.parse(source_code)
41
41
 
42
- defined_methods = []
43
- extract_method_names(result.value, defined_methods)
42
+ defined_methods = []
43
+ synthetic_methods = []
44
+ scopes = []
45
+ validations = []
46
+ extract_method_names(result.value, defined_methods, synthetic_methods, scopes, validations)
44
47
 
45
- filtered_instance_methods = instance_methods.select do |method_name|
46
- method = model_class.instance_method(method_name)
47
- next false unless method.owner == model_class
48
+ filtered_instance_methods = instance_methods.select do |method_name|
49
+ method = model_class.instance_method(method_name)
50
+ next false unless method.owner == model_class
48
51
 
49
- defined_methods.include?(method_name.to_s)
50
- end
52
+ defined_methods.include?(method_name.to_s)
53
+ end
51
54
 
52
- filtered_class_methods = class_methods.select do |method_name|
53
- defined_methods.include?(method_name.to_s)
54
- end
55
+ filtered_class_methods = class_methods.select do |method_name|
56
+ defined_methods.include?(method_name.to_s)
57
+ end
55
58
 
56
- filtered_instance_methods + filtered_class_methods
57
- else
58
- []
59
- end
59
+ # Return array of hashes with type information
60
+ instance_method_hashes = filtered_instance_methods.map do |name|
61
+ { name: name.to_s, type: "instance_method" }
62
+ end
63
+ class_method_hashes = filtered_class_methods.map do |name|
64
+ { name: name.to_s, type: "class_method" }
65
+ end
66
+ synthetic_method_hashes = synthetic_methods.map do |name|
67
+ { name: name.to_s, type: "synthetic" }
68
+ end
69
+ scope_hashes = scopes.map do |name|
70
+ { name: name.to_s, type: "scope" }
71
+ end
72
+ validation_hashes = validations.map do |name|
73
+ { name: name.to_s, type: "validation" }
74
+ end
60
75
 
61
- model_methods.map { |method_name| { name: method_name.to_s } }
76
+ instance_method_hashes + class_method_hashes + synthetic_method_hashes + scope_hashes + validation_hashes
77
+ else
78
+ []
79
+ end
62
80
  rescue NameError => e
63
81
  puts "❌ Could not load model class #{model_name}: #{e.message}"
64
82
  []
@@ -68,7 +86,7 @@ module Tng
68
86
  end
69
87
  end
70
88
 
71
- def self.extract_method_names(node, methods)
89
+ def self.extract_method_names(node, methods, synthetic_methods = [], scopes = [], validations = [])
72
90
  return unless node.is_a?(Prism::Node)
73
91
 
74
92
  case node
@@ -81,18 +99,37 @@ module Tng
81
99
  first_arg = node.arguments.arguments.first
82
100
  if first_arg.is_a?(Prism::SymbolNode)
83
101
  scope_name = first_arg.value
84
- methods << scope_name if scope_name
102
+ scopes << scope_name if scope_name
103
+ end
104
+ # Handle validation macros
105
+ elsif node.name.to_s.start_with?("validate") && node.arguments&.arguments&.any?
106
+ if node.name == :validate
107
+ # Custom validation: validate :method_name -> extract actual method
108
+ node.arguments.arguments.each do |arg|
109
+ if arg.is_a?(Prism::SymbolNode)
110
+ validation_method = arg.value
111
+ validations << validation_method if validation_method
112
+ end
113
+ end
114
+ else
115
+ # Built-in validations: validates :attr, :attr2, ... -> add to validations
116
+ node.arguments.arguments.each do |arg|
117
+ if arg.is_a?(Prism::SymbolNode)
118
+ attr_name = arg.value
119
+ validations << attr_name if attr_name
120
+ end
121
+ end
85
122
  end
86
123
  end
87
124
  when Prism::SingletonClassNode
88
125
  # Methods inside class << self blocks
89
126
  node.body&.child_nodes&.each do |child|
90
- extract_method_names(child, methods)
127
+ extract_method_names(child, methods, synthetic_methods, scopes, validations)
91
128
  end
92
129
  end
93
130
 
94
131
  node.child_nodes.each do |child|
95
- extract_method_names(child, methods)
132
+ extract_method_names(child, methods, synthetic_methods, scopes, validations)
96
133
  end
97
134
  end
98
135
  end
@@ -47,7 +47,7 @@ module Tng
47
47
  end
48
48
 
49
49
  # Fallback to const_source_location if no method source found
50
- service_file ||= service_class.const_source_location(service_class.name.split("::").last)&.first
50
+ service_file ||= Object.const_source_location(service_class.name)&.first
51
51
 
52
52
  service_methods = if service_file && File.exist?(service_file)
53
53
  source_code = File.read(service_file)
@@ -87,13 +87,13 @@ module Tng
87
87
 
88
88
  case type
89
89
  when :controller
90
- Tng.send_request_for_controller(name, file_object[:path], method_info[:name], *config)
90
+ Tng.send_request_for_controller(name, file_object[:path], method_info, *config)
91
91
  when :model
92
- Tng.send_request_for_model(name, file_object[:path], method_info[:name], *config)
92
+ Tng.send_request_for_model(name, file_object[:path], method_info, *config)
93
93
  when :service
94
- Tng.send_request_for_service(name, file_object[:path], method_info[:name], *config)
94
+ Tng.send_request_for_service(name, file_object[:path], method_info, *config)
95
95
  when :other
96
- Tng.send_request_for_other(name, file_object[:path], method_info[:name], *config)
96
+ Tng.send_request_for_other(name, file_object[:path], method_info, *config)
97
97
  end
98
98
  end
99
99
 
@@ -34,6 +34,18 @@ module Tng
34
34
  }
35
35
  end
36
36
  Tng.config[:authentication_entry_points_with_source] = auth_entry_points_with_source
37
+
38
+ # Add source content to test examples for API requests
39
+ if Tng.config[:test_examples]&.any?
40
+ Tng.config[:test_examples] = Tng.config[:test_examples].map do |example|
41
+ next example unless example.is_a?(Hash) && example["path"]
42
+
43
+ example.merge("source" => read_test_file_content(example["path"]))
44
+ rescue StandardError
45
+ example
46
+ end
47
+ end
48
+
37
49
  Tng.config
38
50
  end
39
51
 
@@ -71,6 +83,19 @@ module Tng
71
83
  rescue StandardError
72
84
  nil
73
85
  end
86
+
87
+ def self.read_test_file_content(relative_path)
88
+ return nil unless relative_path
89
+
90
+ rails_root = defined?(Rails) && Rails.root ? Rails.root.to_s : Dir.pwd
91
+ full_path = File.join(rails_root, relative_path)
92
+
93
+ return nil unless File.exist?(full_path) && File.readable?(full_path)
94
+
95
+ File.read(full_path)
96
+ rescue StandardError
97
+ nil
98
+ end
74
99
  end
75
100
  end
76
101
  end
data/lib/tng/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Tng
4
- VERSION = "0.2.7"
4
+ VERSION = "0.2.9"
5
5
  end
data/lib/tng.rb CHANGED
@@ -53,8 +53,6 @@ module Tng
53
53
  @config = {
54
54
  api_key: nil,
55
55
  base_url: "https://app.tng.sh/",
56
- read_source_code: true,
57
- read_test_code: true,
58
56
  testing_framework: "minitest",
59
57
  authentication_enabled: false,
60
58
  authorization_library: nil,
@@ -62,7 +60,8 @@ module Tng
62
60
  authentication_methods: [],
63
61
  mock_library: "minitest/mock",
64
62
  http_mock_library: "webmock",
65
- factory_library: "active_record"
63
+ factory_library: "active_record",
64
+ test_examples: []
66
65
  }
67
66
 
68
67
  def self.configure
@@ -89,22 +88,6 @@ module Tng
89
88
  @config[:base_url]
90
89
  end
91
90
 
92
- def self.read_source_code=(value)
93
- @config[:read_source_code] = value
94
- end
95
-
96
- def self.read_source_code
97
- @config[:read_source_code]
98
- end
99
-
100
- def self.read_test_code=(value)
101
- @config[:read_test_code] = value
102
- end
103
-
104
- def self.read_test_code
105
- @config[:read_test_code]
106
- end
107
-
108
91
  def self.testing_framework=(value)
109
92
  @config[:testing_framework] = value
110
93
  initialize_framework_defaults(value)
@@ -258,6 +241,14 @@ module Tng
258
241
  @config[:subject_style]
259
242
  end
260
243
 
244
+ def self.test_examples=(value)
245
+ @config[:test_examples] = value
246
+ end
247
+
248
+ def self.test_examples
249
+ @config[:test_examples]
250
+ end
251
+
261
252
  def self.authentication_configured?
262
253
  return false unless authentication_enabled
263
254
  return false if authentication_methods.nil? || authentication_methods.empty?
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tng
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.7
4
+ version: 0.2.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - ralucab
@@ -287,7 +287,7 @@ post_install_message: "┌ TNG ────────────────
287
287
  \ │\n│ • bundle exec
288
288
  tng --help - Show help information │\n│ │\n│
289
289
  \ \U0001F4A1 Generate tests for individual methods with precision │\n└────────────────────────────────────────────────────────────
290
- v0.2.7 ┘\n"
290
+ v0.2.9 ┘\n"
291
291
  rdoc_options: []
292
292
  require_paths:
293
293
  - lib