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.
- checksums.yaml +7 -0
- data/README.md +205 -0
- data/app/assets/builds/tailwind.css +2 -0
- data/app/assets/images/icon.png +0 -0
- data/app/assets/images/icon.svg +68 -0
- data/app/assets/stylesheets/devmetrics/dashboard.css +476 -0
- data/app/assets/stylesheets/devmetrics_live/application.css +10 -0
- data/app/assets/tailwind/application.css +1 -0
- data/app/channels/application_cable/channel.rb +4 -0
- data/app/channels/application_cable/connection.rb +4 -0
- data/app/channels/devformance/metrics_channel.rb +25 -0
- data/app/controllers/application_controller.rb +4 -0
- data/app/controllers/devformance/application_controller.rb +19 -0
- data/app/controllers/devformance/icons_controller.rb +21 -0
- data/app/controllers/devformance/metrics_controller.rb +41 -0
- data/app/controllers/devformance/playground_controller.rb +89 -0
- data/app/helpers/application_helper.rb +9 -0
- data/app/helpers/metrics_helper.rb +2 -0
- data/app/helpers/playground_helper.rb +2 -0
- data/app/javascript/devformance/channels/consumer.js +2 -0
- data/app/javascript/devformance/channels/index.js +1 -0
- data/app/javascript/devformance/controllers/application.js +9 -0
- data/app/javascript/devformance/controllers/hello_controller.js +7 -0
- data/app/javascript/devformance/controllers/index.js +14 -0
- data/app/javascript/devformance/controllers/metrics_controller.js +364 -0
- data/app/javascript/devformance/controllers/playground_controller.js +33 -0
- data/app/javascript/devmetrics.js +4 -0
- data/app/jobs/application_job.rb +7 -0
- data/app/jobs/devformance/file_runner_job.rb +318 -0
- data/app/mailers/application_mailer.rb +4 -0
- data/app/models/application_record.rb +3 -0
- data/app/models/devformance/file_result.rb +14 -0
- data/app/models/devformance/run.rb +19 -0
- data/app/models/devformance/slow_query.rb +5 -0
- data/app/views/devformance/metrics/index.html.erb +79 -0
- data/app/views/devformance/playground/run.html.erb +63 -0
- data/app/views/layouts/devformance/application.html.erb +856 -0
- data/app/views/layouts/mailer.html.erb +13 -0
- data/app/views/layouts/mailer.text.erb +1 -0
- data/app/views/metrics/index.html.erb +334 -0
- data/app/views/pwa/manifest.json.erb +22 -0
- data/app/views/pwa/service-worker.js +26 -0
- data/config/BUSINESS_LOGIC_PLAN.md +1244 -0
- data/config/application.rb +31 -0
- data/config/boot.rb +4 -0
- data/config/cable.yml +17 -0
- data/config/cache.yml +16 -0
- data/config/credentials.yml.enc +1 -0
- data/config/database.yml +98 -0
- data/config/deploy.yml +116 -0
- data/config/engine_routes.rb +13 -0
- data/config/environment.rb +5 -0
- data/config/environments/development.rb +84 -0
- data/config/environments/production.rb +90 -0
- data/config/environments/test.rb +59 -0
- data/config/importmap.rb +11 -0
- data/config/initializers/assets.rb +7 -0
- data/config/initializers/content_security_policy.rb +25 -0
- data/config/initializers/filter_parameter_logging.rb +8 -0
- data/config/initializers/inflections.rb +16 -0
- data/config/locales/en.yml +31 -0
- data/config/master.key +1 -0
- data/config/puma.rb +41 -0
- data/config/queue.yml +22 -0
- data/config/recurring.yml +15 -0
- data/config/routes.rb +20 -0
- data/config/storage.yml +34 -0
- data/db/migrate/20260317144616_create_slow_queries.rb +13 -0
- data/db/migrate/20260317175630_create_performance_runs.rb +14 -0
- data/db/migrate/20260317195043_add_run_id_to_slow_queries.rb +10 -0
- data/db/migrate/20260319000001_create_devformance_runs.rb +20 -0
- data/db/migrate/20260319000002_create_devformance_file_results.rb +29 -0
- data/db/migrate/20260319000003_add_columns_to_slow_queries.rb +7 -0
- data/lib/devformance/bullet_log_parser.rb +47 -0
- data/lib/devformance/compatibility.rb +12 -0
- data/lib/devformance/coverage_setup.rb +33 -0
- data/lib/devformance/engine.rb +80 -0
- data/lib/devformance/log_writer.rb +29 -0
- data/lib/devformance/run_orchestrator.rb +58 -0
- data/lib/devformance/sql_instrumentor.rb +29 -0
- data/lib/devformance/test_framework/base.rb +43 -0
- data/lib/devformance/test_framework/coverage_helper.rb +76 -0
- data/lib/devformance/test_framework/detector.rb +26 -0
- data/lib/devformance/test_framework/minitest.rb +71 -0
- data/lib/devformance/test_framework/registry.rb +24 -0
- data/lib/devformance/test_framework/rspec.rb +60 -0
- data/lib/devformance/test_helper.rb +42 -0
- data/lib/devformance/version.rb +3 -0
- data/lib/devformance.rb +196 -0
- data/lib/generators/devformance/install/install_generator.rb +73 -0
- data/lib/generators/devformance/install/templates/add_columns_to_slow_queries.rb.erb +7 -0
- data/lib/generators/devformance/install/templates/add_run_id_to_slow_queries.rb.erb +10 -0
- data/lib/generators/devformance/install/templates/create_devformance_file_results.rb.erb +29 -0
- data/lib/generators/devformance/install/templates/create_devformance_runs.rb.erb +20 -0
- data/lib/generators/devformance/install/templates/create_performance_runs.rb.erb +14 -0
- data/lib/generators/devformance/install/templates/create_slow_queries.rb.erb +13 -0
- data/lib/generators/devformance/install/templates/initializer.rb +23 -0
- data/lib/tasks/devformance.rake +45 -0
- data/spec/fixtures/devformance/devformance_run.rb +27 -0
- data/spec/fixtures/devformance/file_result.rb +34 -0
- data/spec/fixtures/devformance/slow_query.rb +11 -0
- data/spec/lib/devmetrics/log_writer_spec.rb +81 -0
- data/spec/lib/devmetrics/run_orchestrator_spec.rb +102 -0
- data/spec/lib/devmetrics/sql_instrumentor_spec.rb +115 -0
- data/spec/models/devmetrics/file_result_spec.rb +87 -0
- data/spec/models/devmetrics/run_spec.rb +66 -0
- data/spec/models/query_log_spec.rb +21 -0
- data/spec/rails_helper.rb +20 -0
- data/spec/requests/devmetrics/metrics_controller_spec.rb +149 -0
- data/spec/requests/devmetrics_pages_spec.rb +12 -0
- data/spec/requests/performance_spec.rb +17 -0
- data/spec/requests/slow_perf_spec.rb +9 -0
- data/spec/spec_helper.rb +114 -0
- data/spec/support/devmetrics_formatter.rb +106 -0
- data/spec/support/devmetrics_metrics.rb +37 -0
- data/spec/support/factory_bot.rb +3 -0
- 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
|
data/lib/devformance.rb
ADDED
|
@@ -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
|