ace-test-runner 0.18.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.
Files changed (60) hide show
  1. checksums.yaml +7 -0
  2. data/.ace-defaults/test/runner.yml +35 -0
  3. data/.ace-defaults/test/suite.yml +31 -0
  4. data/.ace-defaults/test-runner/config.yml +61 -0
  5. data/CHANGELOG.md +626 -0
  6. data/LICENSE +21 -0
  7. data/README.md +42 -0
  8. data/Rakefile +14 -0
  9. data/exe/ace-test +26 -0
  10. data/exe/ace-test-suite +149 -0
  11. data/lib/ace/test_runner/atoms/command_builder.rb +165 -0
  12. data/lib/ace/test_runner/atoms/lazy_loader.rb +62 -0
  13. data/lib/ace/test_runner/atoms/line_number_resolver.rb +86 -0
  14. data/lib/ace/test_runner/atoms/report_directory_resolver.rb +48 -0
  15. data/lib/ace/test_runner/atoms/report_path_resolver.rb +67 -0
  16. data/lib/ace/test_runner/atoms/result_parser.rb +254 -0
  17. data/lib/ace/test_runner/atoms/test_detector.rb +114 -0
  18. data/lib/ace/test_runner/atoms/test_folder_detector.rb +53 -0
  19. data/lib/ace/test_runner/atoms/test_type_detector.rb +83 -0
  20. data/lib/ace/test_runner/atoms/timestamp_generator.rb +103 -0
  21. data/lib/ace/test_runner/cli/commands/test.rb +326 -0
  22. data/lib/ace/test_runner/cli.rb +16 -0
  23. data/lib/ace/test_runner/formatters/base_formatter.rb +102 -0
  24. data/lib/ace/test_runner/formatters/json_formatter.rb +90 -0
  25. data/lib/ace/test_runner/formatters/markdown_formatter.rb +91 -0
  26. data/lib/ace/test_runner/formatters/progress_file_formatter.rb +164 -0
  27. data/lib/ace/test_runner/formatters/progress_formatter.rb +328 -0
  28. data/lib/ace/test_runner/models/test_configuration.rb +165 -0
  29. data/lib/ace/test_runner/models/test_failure.rb +95 -0
  30. data/lib/ace/test_runner/models/test_group.rb +105 -0
  31. data/lib/ace/test_runner/models/test_report.rb +145 -0
  32. data/lib/ace/test_runner/models/test_result.rb +86 -0
  33. data/lib/ace/test_runner/molecules/cli_argument_parser.rb +263 -0
  34. data/lib/ace/test_runner/molecules/config_loader.rb +162 -0
  35. data/lib/ace/test_runner/molecules/deprecation_fixer.rb +204 -0
  36. data/lib/ace/test_runner/molecules/failed_package_reporter.rb +100 -0
  37. data/lib/ace/test_runner/molecules/failure_analyzer.rb +249 -0
  38. data/lib/ace/test_runner/molecules/in_process_runner.rb +249 -0
  39. data/lib/ace/test_runner/molecules/package_resolver.rb +106 -0
  40. data/lib/ace/test_runner/molecules/pattern_resolver.rb +146 -0
  41. data/lib/ace/test_runner/molecules/rake_integration.rb +218 -0
  42. data/lib/ace/test_runner/molecules/report_storage.rb +303 -0
  43. data/lib/ace/test_runner/molecules/smart_test_executor.rb +107 -0
  44. data/lib/ace/test_runner/molecules/test_executor.rb +162 -0
  45. data/lib/ace/test_runner/organisms/agent_reporter.rb +384 -0
  46. data/lib/ace/test_runner/organisms/report_generator.rb +151 -0
  47. data/lib/ace/test_runner/organisms/sequential_group_executor.rb +185 -0
  48. data/lib/ace/test_runner/organisms/test_orchestrator.rb +648 -0
  49. data/lib/ace/test_runner/rake_task.rb +90 -0
  50. data/lib/ace/test_runner/suite/display_helpers.rb +117 -0
  51. data/lib/ace/test_runner/suite/display_manager.rb +204 -0
  52. data/lib/ace/test_runner/suite/duration_estimator.rb +50 -0
  53. data/lib/ace/test_runner/suite/orchestrator.rb +120 -0
  54. data/lib/ace/test_runner/suite/process_monitor.rb +268 -0
  55. data/lib/ace/test_runner/suite/result_aggregator.rb +176 -0
  56. data/lib/ace/test_runner/suite/simple_display_manager.rb +122 -0
  57. data/lib/ace/test_runner/suite.rb +22 -0
  58. data/lib/ace/test_runner/version.rb +7 -0
  59. data/lib/ace/test_runner.rb +69 -0
  60. metadata +246 -0
@@ -0,0 +1,162 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+ require "ostruct"
5
+ require "ace/support/config"
6
+
7
+ module Ace
8
+ module TestRunner
9
+ module Molecules
10
+ # Load configuration using Ace::Support::Config.create() API
11
+ # Follows ADR-022: Configuration Default and Override Pattern
12
+ #
13
+ # Configuration priority (highest to lowest):
14
+ # 1. CLI options (handled by merge_with_options)
15
+ # 2. Explicit config_path if provided
16
+ # 3. Project config: .ace/test/runner.yml (nearest wins via cascade)
17
+ # 4. User config: ~/.ace/test/runner.yml
18
+ # 5. Gem defaults: ace-test-runner/.ace-defaults/test-runner/config.yml
19
+ class ConfigLoader
20
+ # Load gem defaults for direct access (used by tests)
21
+ # @return [Hash] Default configuration with symbol keys
22
+ def self.load_gem_defaults
23
+ gem_root = Gem.loaded_specs["ace-test-runner"]&.gem_dir ||
24
+ File.expand_path("../../../..", __dir__)
25
+
26
+ resolver = Ace::Support::Config.create(
27
+ config_dir: ".ace",
28
+ defaults_dir: ".ace-defaults",
29
+ gem_path: gem_root
30
+ )
31
+
32
+ config = resolver.resolve_namespace("test-runner").data
33
+ deep_symbolize_keys(config)
34
+ end
35
+
36
+ # Reset method for test isolation (no-op since we don't cache at class level)
37
+ def self.reset_gem_defaults!
38
+ # No-op: Ace::Support::Config.create() is called fresh each time
39
+ end
40
+
41
+ def load(config_path = nil)
42
+ gem_root = Gem.loaded_specs["ace-test-runner"]&.gem_dir ||
43
+ File.expand_path("../../../..", __dir__)
44
+
45
+ resolver = Ace::Support::Config.create(
46
+ config_dir: ".ace",
47
+ defaults_dir: ".ace-defaults",
48
+ gem_path: gem_root
49
+ )
50
+
51
+ # Get merged config from cascade
52
+ config = resolver.resolve_file(["test-runner/config.yml", "test/runner.yml"]).data
53
+
54
+ # If explicit config_path provided, merge it on top
55
+ if config_path && File.exist?(config_path)
56
+ user_config = load_from_file(config_path)
57
+ config = Ace::Support::Config::Atoms::DeepMerger.merge(config, user_config)
58
+ end
59
+
60
+ # Convert to symbol keys for backward compatibility
61
+ config = deep_symbolize_keys(config)
62
+
63
+ validate_config(config)
64
+ normalize_config(config)
65
+ rescue => e
66
+ warn "Warning: Could not load ace-test-runner config: #{e.message}" if ENV["DEBUG"]
67
+ # Return minimal valid config on error
68
+ normalize_config({version: 1})
69
+ end
70
+
71
+ def merge_with_options(config, options)
72
+ merged = deep_copy(config)
73
+
74
+ # Override defaults with command-line options
75
+ if options[:format]
76
+ merged[:defaults] ||= {}
77
+ merged[:defaults][:reporter] = options[:format]
78
+ end
79
+
80
+ if options.key?(:color)
81
+ merged[:defaults] ||= {}
82
+ merged[:defaults][:color] = options[:color]
83
+ end
84
+
85
+ if options.key?(:fail_fast)
86
+ merged[:defaults] ||= {}
87
+ merged[:defaults][:fail_fast] = options[:fail_fast]
88
+ end
89
+
90
+ if options[:report_dir]
91
+ merged[:defaults] ||= {}
92
+ merged[:defaults][:report_dir] = options[:report_dir]
93
+ end
94
+
95
+ # Override failure limits with command-line options
96
+ if options[:max_display]
97
+ merged[:failure_limits] ||= {}
98
+ merged[:failure_limits][:max_display] = options[:max_display]
99
+ end
100
+
101
+ OpenStruct.new(merged)
102
+ end
103
+
104
+ private
105
+
106
+ def load_from_file(path)
107
+ YAML.safe_load_file(path, permitted_classes: [], aliases: true) || {}
108
+ rescue => e
109
+ warn "Warning: Failed to load config from #{path}: #{e.message}"
110
+ {}
111
+ end
112
+
113
+ # Recursively convert string keys to symbols
114
+ def deep_symbolize_keys(obj)
115
+ self.class.deep_symbolize_keys(obj)
116
+ end
117
+
118
+ # Class method version for use in self.load_gem_defaults
119
+ def self.deep_symbolize_keys(obj)
120
+ case obj
121
+ when Hash
122
+ obj.each_with_object({}) do |(key, value), result|
123
+ result[key.to_sym] = deep_symbolize_keys(value)
124
+ end
125
+ when Array
126
+ obj.map { |item| deep_symbolize_keys(item) }
127
+ else
128
+ obj
129
+ end
130
+ end
131
+
132
+ def validate_config(config)
133
+ unless config[:version]
134
+ warn "Warning: Configuration missing version field, assuming version 1"
135
+ config[:version] = 1
136
+ end
137
+
138
+ if config[:version] > 1
139
+ warn "Warning: Configuration version #{config[:version]} is newer than supported version 1"
140
+ end
141
+
142
+ config
143
+ end
144
+
145
+ def normalize_config(config)
146
+ # Ensure all sections exist as hashes (defaults already merged in load)
147
+ config[:patterns] ||= {}
148
+ config[:groups] ||= {}
149
+ config[:defaults] ||= {}
150
+ config[:failure_limits] ||= {}
151
+ config[:execution] ||= {}
152
+
153
+ config
154
+ end
155
+
156
+ def deep_copy(obj)
157
+ Marshal.load(Marshal.dump(obj))
158
+ end
159
+ end
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,204 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ace
4
+ module TestRunner
5
+ module Molecules
6
+ # Identifies and suggests fixes for deprecated code patterns
7
+ class DeprecationFixer
8
+ DEPRECATION_PATTERNS = [
9
+ {
10
+ pattern: /must_equal/,
11
+ replacement: "must_be :==",
12
+ description: "Replace must_equal with must_be :=="
13
+ },
14
+ {
15
+ pattern: /wont_equal/,
16
+ replacement: "wont_be :==",
17
+ description: "Replace wont_equal with wont_be :=="
18
+ },
19
+ {
20
+ pattern: /must_be_nil/,
21
+ replacement: "must_be_nil",
22
+ description: "Use must_be_nil (no change needed)"
23
+ },
24
+ {
25
+ pattern: /\.must_be\s+:([<>]=?)/,
26
+ replacement: 'must_be :\1',
27
+ description: "Comparison operators should use symbols"
28
+ },
29
+ {
30
+ pattern: /assert_equal\s+nil,/,
31
+ replacement: "assert_nil",
32
+ description: "Use assert_nil instead of assert_equal nil"
33
+ },
34
+ {
35
+ pattern: /refute_equal\s+nil,/,
36
+ replacement: "refute_nil",
37
+ description: "Use refute_nil instead of refute_equal nil"
38
+ }
39
+ ].freeze
40
+
41
+ def find_deprecations(content)
42
+ deprecations = []
43
+
44
+ content.lines.each_with_index do |line, index|
45
+ DEPRECATION_PATTERNS.each do |pattern_info|
46
+ if line.match?(pattern_info[:pattern])
47
+ deprecations << {
48
+ line_number: index + 1,
49
+ line_content: line.strip,
50
+ pattern: pattern_info[:pattern].source,
51
+ suggestion: pattern_info[:description],
52
+ replacement: pattern_info[:replacement]
53
+ }
54
+ end
55
+ end
56
+ end
57
+
58
+ deprecations
59
+ end
60
+
61
+ def fix_file(file_path, dry_run: false)
62
+ unless File.exist?(file_path)
63
+ return {success: false, error: "File not found: #{file_path}"}
64
+ end
65
+
66
+ original_content = File.read(file_path)
67
+ fixed_content = apply_fixes(original_content)
68
+
69
+ if original_content == fixed_content
70
+ return {
71
+ success: true,
72
+ changes: 0,
73
+ message: "No deprecations found"
74
+ }
75
+ end
76
+
77
+ unless dry_run
78
+ File.write(file_path, fixed_content)
79
+ end
80
+
81
+ {
82
+ success: true,
83
+ changes: count_changes(original_content, fixed_content),
84
+ message: dry_run ? "Would fix deprecations (dry run)" : "Fixed deprecations",
85
+ diff: generate_diff(original_content, fixed_content)
86
+ }
87
+ end
88
+
89
+ def fix_deprecations_in_output(test_output)
90
+ fixes = []
91
+
92
+ # Look for deprecation warnings in test output
93
+ test_output.scan(/DEPRECATION WARNING: (.+)/) do |warning|
94
+ fix = analyze_warning(warning.first)
95
+ fixes << fix if fix
96
+ end
97
+
98
+ fixes.uniq
99
+ end
100
+
101
+ def generate_fix_report(deprecations)
102
+ return "No deprecations found." if deprecations.empty?
103
+
104
+ lines = ["# Deprecation Fixes Required", ""]
105
+
106
+ grouped = deprecations.group_by { |d| d[:file] || "Unknown" }
107
+
108
+ grouped.each do |file, file_deprecations|
109
+ lines << "## File: #{file}"
110
+ lines << ""
111
+
112
+ file_deprecations.each do |dep|
113
+ lines << "- Line #{dep[:line_number]}: #{dep[:suggestion]}"
114
+ if dep[:replacement]
115
+ lines << " Replace: `#{dep[:line_content]}`"
116
+ lines << " With: `#{apply_single_fix(dep[:line_content], dep)}`"
117
+ end
118
+ end
119
+
120
+ lines << ""
121
+ end
122
+
123
+ lines.join("\n")
124
+ end
125
+
126
+ private
127
+
128
+ def apply_fixes(content)
129
+ fixed_content = content.dup
130
+
131
+ DEPRECATION_PATTERNS.each do |pattern_info|
132
+ fixed_content.gsub!(pattern_info[:pattern], pattern_info[:replacement])
133
+ end
134
+
135
+ fixed_content
136
+ end
137
+
138
+ def apply_single_fix(line, deprecation_info)
139
+ return line unless deprecation_info[:replacement]
140
+
141
+ pattern = Regexp.new(deprecation_info[:pattern])
142
+ line.gsub(pattern, deprecation_info[:replacement])
143
+ end
144
+
145
+ def count_changes(original, fixed)
146
+ original_lines = original.lines
147
+ fixed_lines = fixed.lines
148
+
149
+ changes = 0
150
+ [original_lines.length, fixed_lines.length].max.times do |i|
151
+ if original_lines[i] != fixed_lines[i]
152
+ changes += 1
153
+ end
154
+ end
155
+
156
+ changes
157
+ end
158
+
159
+ def generate_diff(original, fixed)
160
+ # Simple diff generation
161
+ diff_lines = []
162
+ original_lines = original.lines
163
+ fixed_lines = fixed.lines
164
+
165
+ [original_lines.length, fixed_lines.length].max.times do |i|
166
+ if original_lines[i] != fixed_lines[i]
167
+ diff_lines << "- #{original_lines[i]}" if original_lines[i]
168
+ diff_lines << "+ #{fixed_lines[i]}" if fixed_lines[i]
169
+ end
170
+ end
171
+
172
+ diff_lines.join
173
+ end
174
+
175
+ def analyze_warning(warning_text)
176
+ # Extract file and line information from warning if available
177
+ if warning_text =~ /(.+):(\d+):\s*(.+)/
178
+ {
179
+ file: $1,
180
+ line: $2.to_i,
181
+ message: $3,
182
+ suggestion: find_fix_for_warning($3)
183
+ }
184
+ else
185
+ {
186
+ message: warning_text,
187
+ suggestion: find_fix_for_warning(warning_text)
188
+ }
189
+ end
190
+ end
191
+
192
+ def find_fix_for_warning(warning_text)
193
+ DEPRECATION_PATTERNS.each do |pattern_info|
194
+ if warning_text.match?(pattern_info[:pattern])
195
+ return pattern_info[:description]
196
+ end
197
+ end
198
+
199
+ "Review deprecation warning and update code accordingly"
200
+ end
201
+ end
202
+ end
203
+ end
204
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+ require_relative "../atoms/report_path_resolver"
5
+
6
+ module Ace
7
+ module TestRunner
8
+ module Molecules
9
+ module FailedPackageReporter
10
+ module_function
11
+
12
+ def format_for_display(package)
13
+ path = extract_path(package)
14
+ report_root = extract_report_root(package)
15
+ package_name = extract_name(package) || File.basename(path)
16
+ report_path = Atoms::ReportPathResolver.call(
17
+ path,
18
+ report_root: report_root,
19
+ package_name: package_name
20
+ )
21
+
22
+ if report_path
23
+ begin
24
+ relative_path = Pathname.new(report_path).relative_path_from(Dir.pwd)
25
+ " → See #{relative_path}"
26
+ rescue => e
27
+ warn "Failed to calculate relative path: #{e.message}" if debug_mode?
28
+ " → See #{report_path}"
29
+ end
30
+ else
31
+ reports_path = fallback_reports_path(path, report_root, package_name)
32
+ begin
33
+ relative_path = Pathname.new(reports_path).relative_path_from(Dir.pwd)
34
+ " → Check #{relative_path}/ for details"
35
+ rescue => e
36
+ warn "Failed to calculate relative path: #{e.message}" if debug_mode?
37
+ " → Check #{reports_path}/ for details"
38
+ end
39
+ end
40
+ end
41
+
42
+ def format_for_markdown(package)
43
+ path = extract_path(package)
44
+ report_root = extract_report_root(package)
45
+ package_name = extract_name(package) || File.basename(path)
46
+ report_path = Atoms::ReportPathResolver.call(
47
+ path,
48
+ report_root: report_root,
49
+ package_name: package_name
50
+ )
51
+
52
+ if report_path
53
+ relative_report_path = relative_or_absolute(report_path)
54
+ "- Report: `#{relative_report_path}`"
55
+ else
56
+ reports_path = fallback_reports_path(path, report_root, package_name)
57
+ relative_reports_path = relative_or_absolute(reports_path)
58
+ "- Report: Check `#{relative_reports_path}/` for details"
59
+ end
60
+ end
61
+
62
+ class << self
63
+ private
64
+
65
+ def extract_path(package)
66
+ package[:path] || package["path"]
67
+ end
68
+
69
+ def extract_name(package)
70
+ package[:name] || package["name"]
71
+ end
72
+
73
+ def extract_report_root(package)
74
+ package[:report_root] || package["report_root"]
75
+ end
76
+
77
+ def fallback_reports_path(path, report_root, package_name)
78
+ if report_root
79
+ short_name = package_name.to_s.sub(/\Aace-/, "")
80
+ return File.join(report_root, short_name) unless short_name.empty?
81
+ end
82
+
83
+ File.join(path, "test-reports")
84
+ end
85
+
86
+ def debug_mode?
87
+ ENV["DEBUG"]
88
+ end
89
+
90
+ def relative_or_absolute(path)
91
+ Pathname.new(path).relative_path_from(Dir.pwd).to_s
92
+ rescue => e
93
+ warn "Failed to calculate relative path: #{e.message}" if debug_mode?
94
+ path
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end