devformance 0.1.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 (117) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +205 -0
  3. data/app/assets/builds/tailwind.css +2 -0
  4. data/app/assets/images/icon.png +0 -0
  5. data/app/assets/images/icon.svg +68 -0
  6. data/app/assets/stylesheets/devmetrics/dashboard.css +476 -0
  7. data/app/assets/stylesheets/devmetrics_live/application.css +10 -0
  8. data/app/assets/tailwind/application.css +1 -0
  9. data/app/channels/application_cable/channel.rb +4 -0
  10. data/app/channels/application_cable/connection.rb +4 -0
  11. data/app/channels/devformance/metrics_channel.rb +25 -0
  12. data/app/controllers/application_controller.rb +4 -0
  13. data/app/controllers/devformance/application_controller.rb +19 -0
  14. data/app/controllers/devformance/icons_controller.rb +21 -0
  15. data/app/controllers/devformance/metrics_controller.rb +41 -0
  16. data/app/controllers/devformance/playground_controller.rb +89 -0
  17. data/app/helpers/application_helper.rb +9 -0
  18. data/app/helpers/metrics_helper.rb +2 -0
  19. data/app/helpers/playground_helper.rb +2 -0
  20. data/app/javascript/devformance/channels/consumer.js +2 -0
  21. data/app/javascript/devformance/channels/index.js +1 -0
  22. data/app/javascript/devformance/controllers/application.js +9 -0
  23. data/app/javascript/devformance/controllers/hello_controller.js +7 -0
  24. data/app/javascript/devformance/controllers/index.js +14 -0
  25. data/app/javascript/devformance/controllers/metrics_controller.js +364 -0
  26. data/app/javascript/devformance/controllers/playground_controller.js +33 -0
  27. data/app/javascript/devmetrics.js +4 -0
  28. data/app/jobs/application_job.rb +7 -0
  29. data/app/jobs/devformance/file_runner_job.rb +318 -0
  30. data/app/mailers/application_mailer.rb +4 -0
  31. data/app/models/application_record.rb +3 -0
  32. data/app/models/devformance/file_result.rb +14 -0
  33. data/app/models/devformance/run.rb +19 -0
  34. data/app/models/devformance/slow_query.rb +5 -0
  35. data/app/views/devformance/metrics/index.html.erb +79 -0
  36. data/app/views/devformance/playground/run.html.erb +63 -0
  37. data/app/views/layouts/devformance/application.html.erb +856 -0
  38. data/app/views/layouts/mailer.html.erb +13 -0
  39. data/app/views/layouts/mailer.text.erb +1 -0
  40. data/app/views/metrics/index.html.erb +334 -0
  41. data/app/views/pwa/manifest.json.erb +22 -0
  42. data/app/views/pwa/service-worker.js +26 -0
  43. data/config/BUSINESS_LOGIC_PLAN.md +1244 -0
  44. data/config/application.rb +31 -0
  45. data/config/boot.rb +4 -0
  46. data/config/cable.yml +17 -0
  47. data/config/cache.yml +16 -0
  48. data/config/credentials.yml.enc +1 -0
  49. data/config/database.yml +98 -0
  50. data/config/deploy.yml +116 -0
  51. data/config/engine_routes.rb +13 -0
  52. data/config/environment.rb +5 -0
  53. data/config/environments/development.rb +84 -0
  54. data/config/environments/production.rb +90 -0
  55. data/config/environments/test.rb +59 -0
  56. data/config/importmap.rb +11 -0
  57. data/config/initializers/assets.rb +7 -0
  58. data/config/initializers/content_security_policy.rb +25 -0
  59. data/config/initializers/filter_parameter_logging.rb +8 -0
  60. data/config/initializers/inflections.rb +16 -0
  61. data/config/locales/en.yml +31 -0
  62. data/config/master.key +1 -0
  63. data/config/puma.rb +41 -0
  64. data/config/queue.yml +22 -0
  65. data/config/recurring.yml +15 -0
  66. data/config/routes.rb +20 -0
  67. data/config/storage.yml +34 -0
  68. data/db/migrate/20260317144616_create_slow_queries.rb +13 -0
  69. data/db/migrate/20260317175630_create_performance_runs.rb +14 -0
  70. data/db/migrate/20260317195043_add_run_id_to_slow_queries.rb +10 -0
  71. data/db/migrate/20260319000001_create_devformance_runs.rb +20 -0
  72. data/db/migrate/20260319000002_create_devformance_file_results.rb +29 -0
  73. data/db/migrate/20260319000003_add_columns_to_slow_queries.rb +7 -0
  74. data/lib/devformance/bullet_log_parser.rb +47 -0
  75. data/lib/devformance/compatibility.rb +12 -0
  76. data/lib/devformance/coverage_setup.rb +33 -0
  77. data/lib/devformance/engine.rb +80 -0
  78. data/lib/devformance/log_writer.rb +29 -0
  79. data/lib/devformance/run_orchestrator.rb +58 -0
  80. data/lib/devformance/sql_instrumentor.rb +29 -0
  81. data/lib/devformance/test_framework/base.rb +43 -0
  82. data/lib/devformance/test_framework/coverage_helper.rb +76 -0
  83. data/lib/devformance/test_framework/detector.rb +26 -0
  84. data/lib/devformance/test_framework/minitest.rb +71 -0
  85. data/lib/devformance/test_framework/registry.rb +24 -0
  86. data/lib/devformance/test_framework/rspec.rb +60 -0
  87. data/lib/devformance/test_helper.rb +42 -0
  88. data/lib/devformance/version.rb +3 -0
  89. data/lib/devformance.rb +196 -0
  90. data/lib/generators/devformance/install/install_generator.rb +73 -0
  91. data/lib/generators/devformance/install/templates/add_columns_to_slow_queries.rb.erb +7 -0
  92. data/lib/generators/devformance/install/templates/add_run_id_to_slow_queries.rb.erb +10 -0
  93. data/lib/generators/devformance/install/templates/create_devformance_file_results.rb.erb +29 -0
  94. data/lib/generators/devformance/install/templates/create_devformance_runs.rb.erb +20 -0
  95. data/lib/generators/devformance/install/templates/create_performance_runs.rb.erb +14 -0
  96. data/lib/generators/devformance/install/templates/create_slow_queries.rb.erb +13 -0
  97. data/lib/generators/devformance/install/templates/initializer.rb +23 -0
  98. data/lib/tasks/devformance.rake +45 -0
  99. data/spec/fixtures/devformance/devformance_run.rb +27 -0
  100. data/spec/fixtures/devformance/file_result.rb +34 -0
  101. data/spec/fixtures/devformance/slow_query.rb +11 -0
  102. data/spec/lib/devmetrics/log_writer_spec.rb +81 -0
  103. data/spec/lib/devmetrics/run_orchestrator_spec.rb +102 -0
  104. data/spec/lib/devmetrics/sql_instrumentor_spec.rb +115 -0
  105. data/spec/models/devmetrics/file_result_spec.rb +87 -0
  106. data/spec/models/devmetrics/run_spec.rb +66 -0
  107. data/spec/models/query_log_spec.rb +21 -0
  108. data/spec/rails_helper.rb +20 -0
  109. data/spec/requests/devmetrics/metrics_controller_spec.rb +149 -0
  110. data/spec/requests/devmetrics_pages_spec.rb +12 -0
  111. data/spec/requests/performance_spec.rb +17 -0
  112. data/spec/requests/slow_perf_spec.rb +9 -0
  113. data/spec/spec_helper.rb +114 -0
  114. data/spec/support/devmetrics_formatter.rb +106 -0
  115. data/spec/support/devmetrics_metrics.rb +37 -0
  116. data/spec/support/factory_bot.rb +3 -0
  117. metadata +200 -0
@@ -0,0 +1,71 @@
1
+ require_relative "base"
2
+
3
+ module Devformance
4
+ module TestFramework
5
+ class Minitest < Base
6
+ def file_pattern
7
+ "test/integration/*_test.rb"
8
+ end
9
+
10
+ def discover_files
11
+ @discover_files ||= Dir.glob(root_path.join(file_pattern)).sort
12
+ end
13
+
14
+ def run_command(file_path, coverage: false)
15
+ [ "bundle", "exec", "rails", "test", "-vc", file_path ]
16
+ end
17
+
18
+ def runner_command
19
+ "bundle exec rails test -vc"
20
+ end
21
+
22
+ def parse_summary(output)
23
+ if output =~ /(\d+)\s+tests?,\s+(\d+)\s+assertions?,\s+(\d+)\s+failures?(?:,\s+(\d+)\s+errors?)?(?:,\s+(\d+)\s+skips?)?/m
24
+ tests = $1.to_i
25
+ failures = $3.to_i
26
+ errors = $4.to_i rescue 0
27
+ skips = $5.to_i rescue 0
28
+ passes = tests - failures - errors - skips
29
+ { examples: tests, failures: failures + errors, pending: skips, passes: passes }
30
+ elsif output =~ /(\d+)\s+runs?,\s+(\d+)\s+assertions?,\s+(\d+)\s+failures?(?:,\s+(\d+)\s+errors?)?(?:,\s+(\d+)\s+skips?)?/m
31
+ runs = $1.to_i
32
+ failures = $3.to_i
33
+ errors = $4.to_i rescue 0
34
+ skips = $5.to_i rescue 0
35
+ passes = runs - failures - errors - skips
36
+ { examples: runs, failures: failures + errors, pending: skips, passes: passes }
37
+ else
38
+ { examples: 0, failures: 0, pending: 0, passes: 0 }
39
+ end
40
+ end
41
+
42
+ def classify_line(line)
43
+ if line.match?(/(\d+)\s+tests?,\s+(\d+)\s+assertions?/)
44
+ "summary"
45
+ elsif line.match?(/^\s*\.+\s/) || line.include?("PASS")
46
+ "pass"
47
+ elsif line.match?(/^\s*[FE]+\s/) || line.include?("FAIL") || line.include?("ERROR")
48
+ "fail"
49
+ elsif line.include?("SKIP")
50
+ "pending"
51
+ elsif line.match?(/Line Coverage:\s*([\d.]+)%/)
52
+ "coverage"
53
+ else
54
+ "info"
55
+ end
56
+ end
57
+
58
+ def extract_coverage(line)
59
+ line.match(/Line Coverage:\s*([\d.]+)%/)&.[](1)&.to_f
60
+ end
61
+
62
+ def name
63
+ "Minitest"
64
+ end
65
+
66
+ def test_terminal_command(file_path)
67
+ "bundle exec rails test #{file_path}"
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,24 @@
1
+ require_relative "base"
2
+ require_relative "rspec"
3
+ require_relative "minitest"
4
+ require_relative "detector"
5
+
6
+ module Devformance
7
+ module TestFramework
8
+ class Registry
9
+ FRAMEWORKS = {
10
+ "rspec" => RSpec,
11
+ "minitest" => Minitest,
12
+ "test" => Minitest
13
+ }.freeze
14
+
15
+ def self.for_name(name)
16
+ FRAMEWORKS[name.to_s.downcase]
17
+ end
18
+
19
+ def self.all_frameworks
20
+ FRAMEWORKS.values.uniq
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,60 @@
1
+ require_relative "base"
2
+
3
+ module Devformance
4
+ module TestFramework
5
+ class RSpec < Base
6
+ def file_pattern
7
+ "spec/**/*_spec.rb"
8
+ end
9
+
10
+ def discover_files
11
+ @discover_files ||= Dir.glob(root_path.join(file_pattern)).sort
12
+ end
13
+
14
+ def run_command(file_path, coverage: false)
15
+ [ "bundle", "exec", "rspec", file_path, "--format", "documentation" ]
16
+ end
17
+
18
+ def runner_command
19
+ "bundle exec rspec"
20
+ end
21
+
22
+ def parse_summary(output)
23
+ if output =~ /(\d+)\s+examples?,\s+(\d+)\s+failures?(?:,\s+(\d+)\s+pending)?/m
24
+ examples = $1.to_i
25
+ failures = $2.to_i
26
+ pending = $3.to_i rescue 0
27
+ { examples: examples, failures: failures, pending: pending, passes: examples - failures - pending }
28
+ else
29
+ { examples: 0, failures: 0, pending: 0, passes: 0 }
30
+ end
31
+ end
32
+
33
+ def classify_line(line)
34
+ if line.match?(/(\d+)\s+examples?,\s+(\d+)\s+failures?/)
35
+ "summary"
36
+ elsif line.match?(/^\s*[·.]\s/) || line.strip.start_with?(".")
37
+ "pass"
38
+ elsif line.match?(/^\s*[F!]\s/) || line.strip.start_with?("F")
39
+ "fail"
40
+ elsif line.match?(/^\s*\*\s/) || line.strip.start_with?("*")
41
+ "pending"
42
+ elsif line.include?("ERROR") || line.include?("Error:")
43
+ "error"
44
+ elsif line.match?(/^\s{3,}/)
45
+ "pass"
46
+ else
47
+ "info"
48
+ end
49
+ end
50
+
51
+ def name
52
+ "RSpec"
53
+ end
54
+
55
+ def test_terminal_command(file_path)
56
+ "bundle exec rspec #{file_path} --format documentation"
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,42 @@
1
+ require "simplecov"
2
+
3
+ module Devformance
4
+ module TestHelper
5
+ def self.setup_coverage!
6
+ return if defined?(@coverage_setup_done)
7
+ return unless ENV["COVERAGE"] == "true" || ENV["SIMPLECOV"] == "true"
8
+
9
+ @coverage_setup_done = true
10
+
11
+ SimpleCov.start do
12
+ add_filter "/vendor/"
13
+ add_filter "/spec/"
14
+
15
+ unless ENV["DEVMETRICS_INCLUDE_TESTS"] == "true"
16
+ add_filter "/test/"
17
+ end
18
+
19
+ add_filter "/config/"
20
+ add_filter "/db/"
21
+
22
+ if defined?(Devformance) && Devformance.configuration
23
+ minimum_coverage Devformance.configuration.coverage_minimum_coverage || 80
24
+ coverage_dir Devformance.configuration.coverage_dir || "coverage"
25
+ end
26
+ end
27
+ end
28
+
29
+ def self.coverage_summary
30
+ return nil unless defined?(SimpleCov) && SimpleCov.result
31
+
32
+ result = SimpleCov.result
33
+ return nil if result.total_lines.zero?
34
+
35
+ {
36
+ overall: result.covered_percent.round(1),
37
+ lines: { covered: result.covered_lines, total: result.total_lines },
38
+ branches: { covered: result.covered_branches, total: result.total_branches }
39
+ }
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,3 @@
1
+ module Devformance
2
+ Version = "0.1.0"
3
+ end
@@ -0,0 +1,196 @@
1
+ begin
2
+ require "devformance/version"
3
+ require "devformance/engine"
4
+ rescue LoadError
5
+ # Running as standalone app - engine/version not available
6
+ end
7
+
8
+ require "cgi"
9
+ require "benchmark"
10
+ require "json"
11
+ require "securerandom"
12
+
13
+ # Optional dependencies — loaded if present in host app
14
+ %w[importmap-rails turbo-rails stimulus-rails propshaft bullet].each do |dep|
15
+ begin
16
+ require dep
17
+ rescue LoadError
18
+ # Optional dependency not available
19
+ end
20
+ end
21
+
22
+ require_relative "devformance/compatibility"
23
+ require_relative "devformance/test_framework/registry"
24
+ require_relative "devformance/test_helper"
25
+
26
+ module Devformance
27
+ class Configuration
28
+ attr_accessor :log_file_path, :slow_query_threshold_ms, :preferred_framework, :coverage_enabled
29
+ attr_accessor :coverage_dir, :coverage_minimum_coverage
30
+
31
+ VALID_FRAMEWORKS = %i[rspec minitest].freeze
32
+
33
+ def initialize
34
+ @log_file_path = "devformance.log"
35
+ @slow_query_threshold_ms = 100
36
+ @preferred_framework = :rspec
37
+ @coverage_enabled = true
38
+ @coverage_dir = "coverage"
39
+ @coverage_minimum_coverage = 80
40
+ end
41
+
42
+ def preferred_framework=(value)
43
+ normalized = value.is_a?(Symbol) ? value : value.to_sym.downcase if value
44
+ if normalized && !VALID_FRAMEWORKS.include?(normalized)
45
+ raise ArgumentError, "Invalid framework: #{value}. Valid options: #{VALID_FRAMEWORKS.join(', ')}"
46
+ end
47
+ @preferred_framework = normalized
48
+ end
49
+
50
+ def framework_adapter
51
+ return nil unless @preferred_framework
52
+
53
+ framework_class = Devformance::TestFramework::Registry.for_name(@preferred_framework.to_s)
54
+ return nil unless framework_class && defined?(Rails)
55
+
56
+ framework_class.new(Rails.root)
57
+ end
58
+
59
+ def coverage?
60
+ @coverage_enabled != false
61
+ end
62
+ end
63
+
64
+ class << self
65
+ def configuration
66
+ @configuration ||= Configuration.new
67
+ end
68
+
69
+ def setup
70
+ yield(configuration)
71
+ end
72
+
73
+ def log_path
74
+ # Use Rails.root if defined (host app), otherwise current dir
75
+ if defined?(Rails) && Rails.root
76
+ Rails.root.join(configuration.log_file_path)
77
+ else
78
+ Pathname.new(configuration.log_file_path)
79
+ end
80
+ end
81
+ end
82
+
83
+ module PerformanceHelpers
84
+ def self.setup_test_run!
85
+ return unless ENV["DEVMETRICS_TRACKING"] == "true"
86
+
87
+ # Clear and initialize the log file
88
+ File.write(Devformance.log_path, "--- Devformance Performance Run: #{Time.current} ---\n")
89
+
90
+ @total_tests = 0
91
+ @passed_tests = 0
92
+ @failed_tests = 0
93
+ @start_time = Time.current
94
+
95
+ # SQL subscriptions
96
+ ActiveSupport::Notifications.subscribe("sql.active_record") do |_, start, finish, _, payload|
97
+ next if payload[:sql] =~ /\A\s*(BEGIN|COMMIT|ROLLBACK|SAVEPOINT|RELEASE|SET|SHOW|pragma)/i
98
+ next if payload[:name]&.match?(/SCHEMA|ActiveRecord/)
99
+
100
+ duration = ((finish - start) * 1000).round(2)
101
+ if duration > Devformance.configuration.slow_query_threshold_ms
102
+ Thread.current[:devformance_slow_queries] ||= []
103
+ Thread.current[:devformance_slow_queries] << { sql: payload[:sql].squish.truncate(100), duration: duration }
104
+ end
105
+ end
106
+
107
+ # Controller processing subscriptions
108
+ ActiveSupport::Notifications.subscribe("process_action.action_controller") do |_, start, finish, _, payload|
109
+ Thread.current[:devformance_current_action] = {
110
+ controller: payload[:controller],
111
+ action: payload[:action],
112
+ duration: ((finish - start) * 1000).round(2),
113
+ db_runtime: payload[:db_runtime]&.round(2)
114
+ }
115
+ end
116
+
117
+ if defined?(Bullet)
118
+ Bullet.enable = true
119
+ Bullet.bullet_logger = true
120
+ end
121
+ end
122
+
123
+ def self.log_example_result(example)
124
+ return unless ENV["DEVMETRICS_TRACKING"] == "true"
125
+ @total_tests += 1
126
+ if example.exception
127
+ @failed_tests += 1
128
+ else
129
+ @passed_tests += 1
130
+ end
131
+
132
+ action_data = Thread.current[:devformance_current_action] || {}
133
+ slow_queries = Thread.current[:devformance_slow_queries] || []
134
+
135
+ n_plus_one_count = 0
136
+ if defined?(Bullet) && Bullet.notification_collector.notifications_present?
137
+ n_plus_one_count = Bullet.notification_collector.collection.size
138
+ end
139
+
140
+ log_row = {
141
+ timestamp: Time.current.strftime("%H:%M:%S"),
142
+ controller: action_data[:controller] || "N/A",
143
+ action: action_data[:action] || "N/A",
144
+ duration_ms: action_data[:duration] || 0.0,
145
+ slow_queries: slow_queries.size,
146
+ n_plus_one_issues: n_plus_one_count,
147
+ status: example.exception ? "FAILED" : "PASSED",
148
+ example: example.full_description.truncate(100)
149
+ }
150
+
151
+ File.open(Devformance.log_path, "a") { |f| f.puts(log_row.to_json) }
152
+
153
+ # Reset state for next example
154
+ Thread.current[:devformance_current_action] = nil
155
+ Thread.current[:devformance_slow_queries] = nil
156
+ Bullet.notification_collector.clear if defined?(Bullet)
157
+ end
158
+
159
+ def self.finish_test_run!
160
+ return unless ENV["DEVMETRICS_TRACKING"] == "true" && @start_time
161
+
162
+ total_duration = (Time.current - @start_time).round(3)
163
+
164
+ summary = {
165
+ type: "SUMMARY",
166
+ total_time_s: total_duration,
167
+ total_tests: @total_tests,
168
+ passed: @passed_tests,
169
+ failed: @failed_tests,
170
+ timestamp: Time.current.to_s
171
+ }
172
+
173
+ File.open(Devformance.log_path, "a") do |f|
174
+ f.puts "\n--- Summary ---"
175
+ f.puts summary.to_json
176
+ end
177
+ end
178
+ end
179
+ end
180
+
181
+ # Auto-configure RSpec if present
182
+ if defined?(RSpec) && RSpec.respond_to?(:configure)
183
+ RSpec.configure do |config|
184
+ config.before(:suite) do
185
+ ::Devformance::PerformanceHelpers.setup_test_run!
186
+ end
187
+
188
+ config.after(:each) do |example|
189
+ ::Devformance::PerformanceHelpers.log_example_result(example)
190
+ end
191
+
192
+ config.after(:suite) do
193
+ ::Devformance::PerformanceHelpers.finish_test_run!
194
+ end
195
+ end
196
+ end
@@ -0,0 +1,73 @@
1
+ require "rails/generators"
2
+ require "rails/generators/migration"
3
+
4
+ module Devformance
5
+ module Generators
6
+ class InstallGenerator < Rails::Generators::Base
7
+ include Rails::Generators::Migration
8
+
9
+ source_root File.expand_path("templates", __dir__)
10
+
11
+ desc "Installs Devformance: copies migrations and creates an initializer."
12
+
13
+ def self.next_migration_number(dir)
14
+ Time.now.utc.strftime("%Y%m%d%H%M%S")
15
+ end
16
+
17
+ def create_initializer
18
+ template "initializer.rb", "config/initializers/devformance.rb"
19
+ end
20
+
21
+ def copy_migrations
22
+ migration_template(
23
+ "create_performance_runs.rb.erb",
24
+ "db/migrate/create_performance_runs.rb"
25
+ )
26
+ sleep 1
27
+ migration_template(
28
+ "create_slow_queries.rb.erb",
29
+ "db/migrate/create_slow_queries.rb"
30
+ )
31
+ sleep 1
32
+ migration_template(
33
+ "add_run_id_to_slow_queries.rb.erb",
34
+ "db/migrate/add_run_id_to_slow_queries.rb"
35
+ )
36
+ sleep 1
37
+ migration_template(
38
+ "add_columns_to_slow_queries.rb.erb",
39
+ "db/migrate/add_columns_to_slow_queries.rb"
40
+ )
41
+ sleep 1
42
+ migration_template(
43
+ "create_devformance_runs.rb.erb",
44
+ "db/migrate/create_devformance_runs.rb"
45
+ )
46
+ sleep 1
47
+ migration_template(
48
+ "create_devformance_file_results.rb.erb",
49
+ "db/migrate/create_devformance_file_results.rb"
50
+ )
51
+ end
52
+
53
+ def mount_instructions
54
+ route_exists = File.read("config/routes.rb").include?("devformance")
55
+ if route_exists
56
+ say "\n Devformance is already mounted in config/routes.rb\n", :green
57
+ else
58
+ say "\n Add this line to your config/routes.rb:\n\n", :yellow
59
+ say " mount ::Devformance::Engine, at: \"/devformance\"\n\n", :cyan
60
+ say " Then visit http://localhost:3000/devformance\n\n", :green
61
+ end
62
+
63
+ say " If you use RSpec, tag your request specs to instrument them:\n\n"
64
+ say " require 'devformance'\n"
65
+ say " RSpec.describe 'Posts API', devformance: true do\n"
66
+ say " ...\n"
67
+ say " end\n\n"
68
+
69
+ say " Then run: bin/rails db:migrate\n", :green
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,7 @@
1
+ class AddColumnsToSlowQueries < ActiveRecord::Migration[7.2]
2
+ def change
3
+ add_column :slow_queries, :sql, :text unless column_exists?(:slow_queries, :sql)
4
+ add_column :slow_queries, :duration_ms, :float unless column_exists?(:slow_queries, :duration_ms)
5
+ add_column :slow_queries, :file_key, :string unless column_exists?(:slow_queries, :file_key)
6
+ end
7
+ end
@@ -0,0 +1,10 @@
1
+ class AddRunIdToSlowQueries < ActiveRecord::Migration[7.2]
2
+ def change
3
+ unless column_exists?(:slow_queries, :run_id)
4
+ add_column :slow_queries, :run_id, :string
5
+ end
6
+ unless index_exists?(:slow_queries, :run_id)
7
+ add_index :slow_queries, :run_id
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,29 @@
1
+ class CreateDevformanceFileResults < ActiveRecord::Migration[7.2]
2
+ def change
3
+ unless table_exists?(:devformance_file_results)
4
+ create_table :devformance_file_results do |t|
5
+ t.string :run_id, null: false
6
+ t.string :file_key, null: false
7
+ t.string :file_path
8
+ t.integer :status, default: 0, null: false
9
+ t.integer :total_tests, default: 0
10
+ t.integer :passed_tests, default: 0
11
+ t.integer :failed_tests, default: 0
12
+ t.integer :slow_query_count, default: 0
13
+ t.integer :n1_count, default: 0
14
+ t.float :coverage
15
+ t.integer :duration_ms
16
+ t.string :log_path
17
+
18
+ t.timestamps
19
+ end
20
+
21
+ unless index_exists?(:devformance_file_results, :run_id)
22
+ add_index :devformance_file_results, :run_id
23
+ end
24
+ unless index_exists?(:devformance_file_results, [:run_id, :file_key])
25
+ add_index :devformance_file_results, [:run_id, :file_key], unique: true
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,20 @@
1
+ class CreateDevformanceRuns < ActiveRecord::Migration[7.2]
2
+ def change
3
+ unless table_exists?(:devformance_runs)
4
+ create_table :devformance_runs do |t|
5
+ t.string :run_id, null: false
6
+ t.integer :status, default: 0, null: false
7
+ t.datetime :started_at
8
+ t.datetime :finished_at
9
+ t.integer :total_files, default: 0
10
+ t.integer :completed_files, default: 0
11
+
12
+ t.timestamps
13
+ end
14
+
15
+ unless index_exists?(:devformance_runs, :run_id)
16
+ add_index :devformance_runs, :run_id, unique: true
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,14 @@
1
+ class CreatePerformanceRuns < ActiveRecord::Migration[7.2]
2
+ def change
3
+ unless table_exists?(:performance_runs)
4
+ create_table :performance_runs do |t|
5
+ t.string :run_id
6
+ t.integer :total_files
7
+ t.integer :completed_files
8
+ t.string :status
9
+
10
+ t.timestamps
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,13 @@
1
+ class CreateSlowQueries < ActiveRecord::Migration[7.2]
2
+ def change
3
+ unless table_exists?(:slow_queries)
4
+ create_table :slow_queries do |t|
5
+ t.string :model_class
6
+ t.integer :line_number
7
+ t.text :fix_suggestion
8
+
9
+ t.timestamps
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,23 @@
1
+ Devformance.setup do |config|
2
+ # Test framework to use for test runs (default: :rspec)
3
+ # Options: :rspec, :minitest
4
+ # config.preferred_framework = :rspec
5
+
6
+ # Slow query threshold in milliseconds (queries above this are logged)
7
+ # config.slow_query_threshold_ms = 100
8
+
9
+ # Coverage tracking (SimpleCov)
10
+ # Enable or disable code coverage collection
11
+ # config.coverage_enabled = true
12
+
13
+ # Directory where coverage reports are stored
14
+ # config.coverage_dir = "coverage"
15
+
16
+ # Minimum coverage percentage (for alerts/warnings)
17
+ # config.coverage_minimum_coverage = 80
18
+ end
19
+
20
+ # Enable coverage when running tests (optional - uncomment if desired)
21
+ # if ENV["COVERAGE"] == "true" || ENV["SIMPLECOV"] == "true"
22
+ # Devformance::TestHelper.setup_coverage!
23
+ # end
@@ -0,0 +1,45 @@
1
+ require_relative "../../lib/devformance/test_helper"
2
+
3
+ namespace :devformance do
4
+ desc "Run tests with coverage"
5
+ task :test_with_coverage, [ :framework ] do |t, args|
6
+ framework = args[:framework] || Devformance.configuration.preferred_framework || "auto"
7
+
8
+ puts "Running Devformance tests with coverage (#{framework})..."
9
+ puts ""
10
+
11
+ ENV["COVERAGE"] = "true"
12
+ ENV["SIMPLECOV"] = "true"
13
+
14
+ Devformance::TestHelper.setup_coverage!
15
+
16
+ if framework == "minitest" || (framework == "auto" && Devformance.configuration.framework_adapter&.name == "Minitest")
17
+ sh "bundle exec rails test -vc"
18
+ else
19
+ sh "bundle exec rspec"
20
+ end
21
+
22
+ summary = Devformance::TestHelper.coverage_summary
23
+ if summary
24
+ puts ""
25
+ puts "Coverage Results:"
26
+ puts " Overall: #{summary[:overall]}%"
27
+ puts " Lines: #{summary[:lines][:covered]}/#{summary[:lines][:total]}"
28
+ end
29
+ end
30
+
31
+ desc "Run tests (auto-detect framework)"
32
+ task :test do
33
+ Rake::Task["devformance:test_with_coverage"].invoke("auto")
34
+ end
35
+
36
+ desc "Run RSpec tests with coverage"
37
+ task :rspec do
38
+ Rake::Task["devformance:test_with_coverage"].invoke("rspec")
39
+ end
40
+
41
+ desc "Run Minitest tests with coverage"
42
+ task :minitest do
43
+ Rake::Task["devformance:test_with_coverage"].invoke("minitest")
44
+ end
45
+ end
@@ -0,0 +1,27 @@
1
+ FactoryBot.define do
2
+ factory :devformance_run, class: "Devformance::Run" do
3
+ run_id { SecureRandom.hex(8) }
4
+ status { :pending }
5
+ started_at { Time.current }
6
+ finished_at { nil }
7
+ total_files { 10 }
8
+ completed_files { 0 }
9
+ end
10
+
11
+ trait :running do
12
+ status { :running }
13
+ completed_files { 3 }
14
+ end
15
+
16
+ trait :completed do
17
+ status { :completed }
18
+ completed_files { 10 }
19
+ finished_at { Time.current }
20
+ end
21
+
22
+ trait :failed do
23
+ status { :failed }
24
+ completed_files { 5 }
25
+ finished_at { Time.current }
26
+ end
27
+ end