rspec_power 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.
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "csv"
4
+ require "json"
5
+ require "time"
6
+ require "fileutils"
7
+
8
+ module RSpecPower
9
+ module DbDumpHelpers
10
+ DEFAULT_EXCLUDED_TABLES = %w[schema_migrations ar_internal_metadata].freeze
11
+
12
+ def dump_database_on_failure(example, options = {})
13
+ return unless defined?(::ActiveRecord)
14
+
15
+ connection = ::ActiveRecord::Base.connection
16
+
17
+ base_dir = resolve_base_dir(options[:dir])
18
+ spec_label = sanitize_label(example.full_description)
19
+ timestamp = Time.now.strftime("%Y%m%d-%H%M%S")
20
+
21
+ tables = resolve_tables(connection, options)
22
+ non_empty_tables = tables.select { |t| table_has_rows?(connection, t) }
23
+
24
+ return nil if non_empty_tables.empty?
25
+
26
+ out_dir = File.join(base_dir, "#{timestamp}_#{spec_label}")
27
+ FileUtils.mkdir_p(out_dir)
28
+
29
+ non_empty_tables.each do |table|
30
+ csv_path = File.join(out_dir, "#{table}.csv")
31
+ dump_table_to_csv(connection, table, csv_path)
32
+ end
33
+
34
+ write_metadata(out_dir, example, non_empty_tables)
35
+
36
+ puts "[rspec_power] DB dump written to: #{out_dir}"
37
+ puts "[rspec_power] Tables: #{non_empty_tables.join(", ")}" unless non_empty_tables.empty?
38
+
39
+ out_dir
40
+ rescue => e
41
+ warn "[rspec_power] Failed to dump DB on failure: #{e.class}: #{e.message}"
42
+ nil
43
+ end
44
+
45
+ private
46
+
47
+ def resolve_base_dir(custom_dir)
48
+ return custom_dir if custom_dir && !custom_dir.to_s.strip.empty?
49
+
50
+ base = if defined?(Rails) && Rails.respond_to?(:root) && Rails.root
51
+ Rails.root.join("tmp", "rspec_power", "db_failures").to_s
52
+ else
53
+ File.join(Dir.pwd, "tmp", "rspec_power", "db_failures")
54
+ end
55
+ FileUtils.mkdir_p(base)
56
+ base
57
+ end
58
+
59
+ def resolve_tables(connection, options)
60
+ specified = Array(options[:tables] || options[:only]).map(&:to_s)
61
+ excluded = Array(options[:except] || options[:exclude]).map(&:to_s)
62
+
63
+ all = connection.tables - DEFAULT_EXCLUDED_TABLES
64
+ list = specified.empty? ? all : (all & specified)
65
+ list - excluded
66
+ end
67
+
68
+ def table_has_rows?(connection, table)
69
+ qt = connection.quote_table_name(table)
70
+ sql = "SELECT 1 FROM #{qt} LIMIT 1"
71
+ !connection.select_value(sql).nil?
72
+ rescue
73
+ false
74
+ end
75
+
76
+ def dump_table_to_csv(connection, table, csv_path)
77
+ qt = connection.quote_table_name(table)
78
+ primary_key = safe_primary_key(connection, table)
79
+
80
+ sql = if primary_key
81
+ qc = connection.quote_column_name(primary_key)
82
+ "SELECT * FROM #{qt} ORDER BY #{qc} ASC"
83
+ else
84
+ "SELECT * FROM #{qt}"
85
+ end
86
+
87
+ result = connection.exec_query(sql)
88
+
89
+ CSV.open(csv_path, "w") do |csv|
90
+ csv << result.columns
91
+ result.rows.each { |row| csv << row }
92
+ end
93
+ end
94
+
95
+ def safe_primary_key(connection, table)
96
+ connection.primary_key(table)
97
+ rescue
98
+ nil
99
+ end
100
+
101
+ def sanitize_label(label)
102
+ label.to_s.gsub(/[^a-zA-Z0-9\-_]+/, "_")[0, 120]
103
+ end
104
+
105
+ def write_metadata(out_dir, example, tables)
106
+ meta = {
107
+ "spec" => example.full_description,
108
+ "id" => example.id,
109
+ "file_path" => example.metadata[:file_path],
110
+ "exception" => example.exception&.message,
111
+ "created_at" => Time.now.utc.iso8601,
112
+ "tables" => tables
113
+ }
114
+ File.write(File.join(out_dir, "metadata.json"), JSON.pretty_generate(meta))
115
+ rescue
116
+ # Fallback to plain text if JSON isn't available for some reason
117
+ File.write(
118
+ File.join(out_dir, "metadata.txt"),
119
+ [
120
+ "spec: #{example.full_description}",
121
+ "id: #{example.id}",
122
+ "file_path: #{example.metadata[:file_path]}",
123
+ "exception: #{example.exception&.message}",
124
+ "created_at: #{Time.now.utc.iso8601}",
125
+ "tables: #{tables.join(', ')}"
126
+ ].join("\n")
127
+ )
128
+ end
129
+ end
130
+ end
131
+
132
+ RSpec.shared_context "rspec_power::db_dump:on_fail" do
133
+ include RSpecPower::DbDumpHelpers
134
+
135
+ after(:each) do |example|
136
+ dump_meta = example.metadata[:with_dump_db_on_fail]
137
+ dump_meta = example.metadata[:dump_db_on_fail] if dump_meta.nil?
138
+ next unless dump_meta
139
+ next unless example.exception
140
+
141
+ options = dump_meta.is_a?(Hash) ? dump_meta : {}
142
+ dump_database_on_failure(example, options)
143
+ end
144
+ end
@@ -0,0 +1,5 @@
1
+ module RSpecPower
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace RSpecPower
4
+ end
5
+ end
@@ -0,0 +1,30 @@
1
+ module RSpecPower
2
+ module EnvHelpers
3
+ def with_test_env(overrides = {})
4
+ old_values = overrides.each_with_object({}) do |(key, _), memo|
5
+ memo[key] = ENV.key?(key) ? ENV[key] : :__undefined__
6
+ end
7
+
8
+ # apply overrides (stringify keys just in case)
9
+ overrides.each { |k, v| ENV[k.to_s] = v }
10
+
11
+ yield
12
+ ensure
13
+ # restore old values
14
+ old_values.each do |key, val|
15
+ if val == :__undefined__
16
+ ENV.delete(key)
17
+ else
18
+ ENV[key] = val
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+
25
+ RSpec.shared_context "rspec_power::env:override" do
26
+ around(:each) do |example|
27
+ overrides = example.metadata[:with_env] || {}
28
+ with_test_env(overrides) { example.run }
29
+ end
30
+ end
@@ -0,0 +1,21 @@
1
+ module RSpecPower
2
+ module I18nHelpers
3
+ def with_locale(locale)
4
+ old = I18n.locale
5
+ I18n.locale = locale
6
+ yield
7
+ ensure
8
+ I18n.locale = old
9
+ end
10
+ end
11
+ end
12
+
13
+ RSpec.shared_context "rspec_power::i18n:dynamic" do
14
+ around(:each) do |example|
15
+ if example.metadata.key?(:with_locale)
16
+ with_locale(example.metadata[:with_locale]) { example.run }
17
+ else
18
+ example.run
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,112 @@
1
+ require "logger"
2
+ require "active_support/tagged_logging" if defined?(ActiveSupport::TaggedLogging)
3
+ require "active_support/logger" if defined?(ActiveSupport::Logger)
4
+ require "active_record" if defined?(ActiveRecord)
5
+ require "active_support/log_subscriber" if defined?(ActiveSupport::LogSubscriber)
6
+
7
+ module RSpecPower
8
+ module LoggingHelpers
9
+ class << self
10
+ def logger=(new_logger)
11
+ @logger = new_logger
12
+ global_loggables.each { |l| l.logger = new_logger }
13
+ end
14
+
15
+ def logger
16
+ return @logger if defined?(@logger)
17
+ @logger = if defined?(ActiveSupport::TaggedLogging)
18
+ ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new($stdout))
19
+ elsif defined?(ActiveSupport::Logger)
20
+ ActiveSupport::Logger.new($stdout)
21
+ else
22
+ Logger.new($stdout)
23
+ end
24
+ end
25
+
26
+ def global_loggables
27
+ @global_loggables ||= []
28
+ end
29
+
30
+ def swap_logger(targets)
31
+ targets.map do |target|
32
+ old_logger = target.logger
33
+ begin
34
+ target.logger = logger
35
+ rescue NoMethodError
36
+ # Some classes expose .logger= only via class_attribute; try via Rails.logger
37
+ # but we still continue to next target to avoid halting.
38
+ end
39
+ old_logger
40
+ end
41
+ end
42
+
43
+ def restore_logger(old_loggers, targets)
44
+ targets.each_with_index { |t, i| t.logger = old_loggers[i] }
45
+ end
46
+
47
+ def all_loggables
48
+ base = [
49
+ (::Rails if defined?(::Rails)),
50
+ (::ActiveSupport::LogSubscriber if defined?(::ActiveSupport::LogSubscriber)),
51
+ (::ActiveRecord::Base if defined?(::ActiveRecord::Base)),
52
+ (::ActionController::Base if defined?(::ActionController::Base)),
53
+ (::ActiveJob::Base if defined?(::ActiveJob::Base)),
54
+ (::ActionView::Base if defined?(::ActionView::Base)),
55
+ (::ActionMailer::Base if defined?(::ActionMailer::Base)),
56
+ (::ActionCable if defined?(::ActionCable))
57
+ ].compact
58
+
59
+ # Include all log subscribers for controller/action_view etc.
60
+ if defined?(::ActiveSupport::LogSubscriber)
61
+ ObjectSpace.each_object(Class).select { |c| c < ::ActiveSupport::LogSubscriber }.each do |subscriber|
62
+ base << subscriber if subscriber.respond_to?(:logger)
63
+ end
64
+ end
65
+
66
+ base.select { |l| l.respond_to?(:logger) }
67
+ end
68
+
69
+ def ar_loggables
70
+ @ar_loggables ||= [
71
+ ::ActiveRecord::Base,
72
+ ::ActiveSupport::LogSubscriber
73
+ ]
74
+ end
75
+ end
76
+
77
+ def with_logging
78
+ old = LoggingHelpers.swap_logger(LoggingHelpers.all_loggables)
79
+ yield
80
+ ensure
81
+ LoggingHelpers.restore_logger(old, LoggingHelpers.all_loggables)
82
+ end
83
+
84
+ def with_ar_logging
85
+ old = LoggingHelpers.swap_logger(LoggingHelpers.ar_loggables)
86
+ yield
87
+ ensure
88
+ LoggingHelpers.restore_logger(old, LoggingHelpers.ar_loggables)
89
+ end
90
+ end
91
+ end
92
+
93
+ RSpec.shared_context "rspec_power::logging:verbose" do
94
+ around(:each) do |ex|
95
+ if ex.metadata[:with_log] == true || ex.metadata[:with_log] == :all ||
96
+ ex.metadata[:with_logs] == true || ex.metadata[:with_logs] == :all
97
+ with_logging(&ex)
98
+ else
99
+ ex.call
100
+ end
101
+ end
102
+ end
103
+
104
+ RSpec.shared_context "rspec_power::logging:active_record" do
105
+ around(:each) do |ex|
106
+ if ex.metadata[:with_log_ar]
107
+ with_ar_logging(&ex)
108
+ else
109
+ ex.call
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,32 @@
1
+ module RSpecPower
2
+ module PerformanceHelpers
3
+ # Ensures the given block completes within the specified duration (milliseconds).
4
+ # Raises RSpec::Expectations::ExpectationNotMetError if the threshold is exceeded.
5
+ def with_maximum_execution_time(max_duration_ms)
6
+ raise ArgumentError, "with_maximum_execution_time requires a block" unless block_given?
7
+
8
+ max_ms = max_duration_ms.to_f
9
+ start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
10
+ yield
11
+ ensure
12
+ elapsed_ms = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time) * 1000.0
13
+ if elapsed_ms > max_ms
14
+ formatted_elapsed = format("%.3f", elapsed_ms)
15
+ formatted_limit = format("%.3f", max_ms)
16
+ raise RSpec::Expectations::ExpectationNotMetError,
17
+ "Execution time exceeded: #{formatted_elapsed}ms > #{formatted_limit}ms"
18
+ end
19
+ end
20
+ end
21
+ end
22
+
23
+ RSpec.shared_context "rspec_power::performance:maximum_execution_time" do
24
+ around(:each) do |example|
25
+ threshold = example.metadata[:with_maximum_execution_time]
26
+ if threshold
27
+ with_maximum_execution_time(threshold) { example.run }
28
+ else
29
+ example.run
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,125 @@
1
+ module RSpecPower
2
+ module RequestDumpHelpers
3
+ def dump_state_after_example(example, what_items)
4
+ items_to_dump = Array(what_items).map(&:to_sym)
5
+
6
+ puts "\n[rspec_power] Dump after example: #{example.full_description}"
7
+
8
+ if items_to_dump.include?(:session)
9
+ value = fetch_session_value
10
+ puts "[rspec_power] session: #{safe_inspect(value)}"
11
+ end
12
+
13
+ if items_to_dump.include?(:cookies)
14
+ value = fetch_cookies_value
15
+ puts "[rspec_power] cookies: #{safe_inspect(value)}"
16
+ end
17
+
18
+ if items_to_dump.include?(:flash)
19
+ value = fetch_flash_value
20
+ puts "[rspec_power] flash: #{safe_inspect(value)}"
21
+ end
22
+
23
+ if items_to_dump.include?(:headers)
24
+ value = fetch_headers_value
25
+ puts "[rspec_power] headers: #{safe_inspect(value)}"
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def safe_inspect(value)
32
+ case value
33
+ when Hash
34
+ begin
35
+ value.transform_keys { |k| k.to_s }.inspect
36
+ rescue
37
+ value.inspect
38
+ end
39
+ else
40
+ value.inspect
41
+ end
42
+ end
43
+
44
+ def fetch_session_value
45
+ if respond_to?(:session)
46
+ coerce_to_hash(session)
47
+ elsif defined?(request) && request.respond_to?(:session)
48
+ coerce_to_hash(request.session)
49
+ elsif defined?(controller) && controller.respond_to?(:session)
50
+ coerce_to_hash(controller.session)
51
+ else
52
+ :unavailable
53
+ end
54
+ rescue => e
55
+ "unavailable (#{e.class}: #{e.message})"
56
+ end
57
+
58
+ def fetch_cookies_value
59
+ if respond_to?(:cookies)
60
+ coerce_to_hash(cookies)
61
+ elsif defined?(request) && request.respond_to?(:cookie_jar)
62
+ coerce_to_hash(request.cookie_jar)
63
+ elsif defined?(response) && response.respond_to?(:cookies)
64
+ coerce_to_hash(response.cookies)
65
+ else
66
+ :unavailable
67
+ end
68
+ rescue => e
69
+ "unavailable (#{e.class}: #{e.message})"
70
+ end
71
+
72
+ def fetch_flash_value
73
+ if respond_to?(:flash)
74
+ coerce_to_hash(flash)
75
+ elsif defined?(controller) && controller.respond_to?(:flash)
76
+ coerce_to_hash(controller.flash)
77
+ else
78
+ :unavailable
79
+ end
80
+ rescue => e
81
+ "unavailable (#{e.class}: #{e.message})"
82
+ end
83
+
84
+ def fetch_headers_value
85
+ if defined?(request) && request.respond_to?(:headers)
86
+ coerce_to_hash(request.headers)
87
+ elsif defined?(response) && response.respond_to?(:request) && response.request.respond_to?(:headers)
88
+ coerce_to_hash(response.request.headers)
89
+ elsif defined?(controller) && controller.respond_to?(:request) && controller.request.respond_to?(:headers)
90
+ coerce_to_hash(controller.request.headers)
91
+ else
92
+ :unavailable
93
+ end
94
+ rescue => e
95
+ "unavailable (#{e.class}: #{e.message})"
96
+ end
97
+
98
+ def coerce_to_hash(obj)
99
+ return {} if obj.nil?
100
+ return obj.to_hash if obj.respond_to?(:to_hash)
101
+ return obj.to_h if obj.respond_to?(:to_h)
102
+ if defined?(ActionDispatch::Request::Session) && obj.is_a?(ActionDispatch::Request::Session)
103
+ return obj.to_hash
104
+ end
105
+ obj.inspect
106
+ end
107
+ end
108
+ end
109
+
110
+ RSpec.shared_context "rspec_power::request_dump:after" do
111
+ include RSpecPower::RequestDumpHelpers
112
+
113
+ after(:each) do |example|
114
+ dump_meta = example.metadata[:with_request_dump]
115
+ next unless dump_meta
116
+
117
+ what = if dump_meta.is_a?(Hash) && dump_meta[:what]
118
+ dump_meta[:what]
119
+ else
120
+ [ :session, :cookies, :flash, :headers ]
121
+ end
122
+
123
+ dump_state_after_example(example, what)
124
+ end
125
+ end
@@ -0,0 +1,105 @@
1
+ module RSpecPower
2
+ module ActiveRecordHelpers
3
+ # Fails the example if any SQL is executed within the provided block.
4
+ # Ignores cached, schema, and transaction events to avoid false positives.
5
+ def expect_no_sql
6
+ raise ArgumentError, "expect_no_sql requires a block" unless block_given?
7
+
8
+ executed_sql_statements = capture_executed_sql { yield }
9
+
10
+ if executed_sql_statements.any?
11
+ message = "Expected no SQL to be executed, but #{executed_sql_statements.length} statement(s) were run.\n" \
12
+ + executed_sql_statements.map { |sql| " - #{sql}" }.join("\n")
13
+ raise RSpec::Expectations::ExpectationNotMetError, message
14
+ end
15
+ end
16
+
17
+ # Passes only if at least one SQL statement is executed within the block.
18
+ # Ignores cached, schema, and transaction events to avoid false positives.
19
+ def expect_sql
20
+ raise ArgumentError, "expect_sql requires a block" unless block_given?
21
+
22
+ executed_sql_statements = capture_executed_sql { yield }
23
+
24
+ if executed_sql_statements.empty?
25
+ raise RSpec::Expectations::ExpectationNotMetError,
26
+ "Expected some SQL to be executed, but none was run"
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ # Subscribes to ActiveRecord SQL notifications and captures relevant statements
33
+ # executed within the given block. Returns an Array of SQL strings.
34
+ def capture_executed_sql
35
+ raise ArgumentError, "capture_executed_sql requires a block" unless block_given?
36
+
37
+ executed_sql_statements = []
38
+
39
+ notification_callback = lambda do |_name, _started, _finished, _unique_id, payload|
40
+ event_name = payload[:name].to_s
41
+ is_cached_event = payload[:cached]
42
+
43
+ # Skip noise that doesn't represent user-level SQL
44
+ next if is_cached_event
45
+ next if event_name == "SCHEMA" || event_name == "TRANSACTION"
46
+
47
+ executed_sql_statements << payload[:sql]
48
+ end
49
+
50
+ ActiveSupport::Notifications.subscribed(notification_callback, "sql.active_record") do
51
+ yield
52
+ end
53
+
54
+ executed_sql_statements
55
+ end
56
+ end
57
+ end
58
+
59
+ RSpec.shared_context "rspec_power::sql:none" do
60
+ around(:each) do |example|
61
+ executed_sql = []
62
+
63
+ callback = lambda do |_name, _started, _finished, _unique_id, payload|
64
+ event_name = payload[:name].to_s
65
+ cached = payload[:cached]
66
+ next if cached
67
+ next if event_name == "SCHEMA" || event_name == "TRANSACTION"
68
+
69
+ executed_sql << payload[:sql]
70
+ end
71
+
72
+ ActiveSupport::Notifications.subscribed(callback, "sql.active_record") do
73
+ example.run
74
+ end
75
+
76
+ if executed_sql.any?
77
+ message = "Expected no SQL to be executed, but #{executed_sql.length} statement(s) were run.\n" \
78
+ + executed_sql.map { |sql| " - #{sql}" }.join("\n")
79
+ raise RSpec::Expectations::ExpectationNotMetError, message
80
+ end
81
+ end
82
+ end
83
+
84
+ RSpec.shared_context "rspec_power::sql:must" do
85
+ around(:each) do |example|
86
+ executed_sql = []
87
+
88
+ callback = lambda do |_name, _started, _finished, _unique_id, payload|
89
+ event_name = payload[:name].to_s
90
+ cached = payload[:cached]
91
+ next if cached
92
+ next if event_name == "SCHEMA" || event_name == "TRANSACTION"
93
+
94
+ executed_sql << payload[:sql]
95
+ end
96
+
97
+ ActiveSupport::Notifications.subscribed(callback, "sql.active_record") do
98
+ example.run
99
+ end
100
+
101
+ if executed_sql.empty?
102
+ raise RSpec::Expectations::ExpectationNotMetError, "Expected some SQL to be executed, but none was run"
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,37 @@
1
+ require "active_support/testing/time_helpers"
2
+ require "time"
3
+
4
+ module RSpecPower
5
+ module TimeHelpers
6
+ include ActiveSupport::Testing::TimeHelpers
7
+
8
+ def with_time_zone(zone)
9
+ if defined?(ActiveSupport::TimeZone)
10
+ ::Time.use_zone(zone) { yield }
11
+ else
12
+ yield
13
+ end
14
+ end
15
+ end
16
+ end
17
+
18
+ RSpec.shared_context "rspec_power::time:freeze" do
19
+ around(:each) do |example|
20
+ if ts = example.metadata[:with_time_freeze]
21
+ ts = Time.parse(ts) if ts.is_a?(String)
22
+ travel_to(ts) { example.run }
23
+ else
24
+ example.run
25
+ end
26
+ end
27
+ end
28
+
29
+ RSpec.shared_context "rspec_power::time:zone" do
30
+ around(:each) do |example|
31
+ if zone = example.metadata[:with_time_zone]
32
+ with_time_zone(zone) { example.run }
33
+ else
34
+ example.run
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,3 @@
1
+ module RSpecPower
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,61 @@
1
+ require "rspec_power/version"
2
+ require "rspec_power/engine"
3
+ require "rspec_power/logging"
4
+ require "rspec_power/env"
5
+ require "rspec_power/i18n"
6
+ require "rspec_power/time"
7
+ require "rspec_power/request_dump"
8
+ require "rspec_power/db_dump"
9
+ require "rspec_power/ci"
10
+ require "rspec_power/sql"
11
+ require "rspec_power/performance"
12
+ require "rspec_power/benchmark"
13
+
14
+ RSpec.configure do |config|
15
+ # Logging
16
+ config.include RSpecPower::LoggingHelpers
17
+ config.include_context "rspec_power::logging:verbose", with_log: true
18
+ config.include_context "rspec_power::logging:verbose", with_logs: true
19
+ config.include_context "rspec_power::logging:active_record", with_log_ar: true
20
+
21
+ # Environment variable overrides
22
+ config.include RSpecPower::EnvHelpers
23
+ config.include_context "rspec_power::env:override", :with_env
24
+
25
+ # I18n
26
+ config.include RSpecPower::I18nHelpers
27
+ config.include_context "rspec_power::i18n:dynamic", :with_locale
28
+
29
+ # Time manipulation
30
+ config.include RSpecPower::TimeHelpers
31
+ config.include_context "rspec_power::time:freeze", :with_time_freeze
32
+ config.include_context "rspec_power::time:zone", :with_time_zone
33
+
34
+ # CI-only guards
35
+ # Prefer new tags with "with_" prefix; keep old ones for compatibility
36
+ config.include_context "rspec_power::ci:only", :with_ci_only
37
+ config.include_context "rspec_power::ci:skip", :with_skip_ci
38
+ config.include_context "rspec_power::ci:only", :ci_only
39
+ config.include_context "rspec_power::ci:skip", :skip_ci
40
+
41
+ # SQL guards
42
+ config.include RSpecPower::ActiveRecordHelpers
43
+ config.include_context "rspec_power::sql:none", :with_no_sql_queries
44
+ config.include_context "rspec_power::sql:must", :with_sql_queries
45
+
46
+ # Performance
47
+ config.include RSpecPower::PerformanceHelpers
48
+ config.include_context "rspec_power::performance:maximum_execution_time", :with_maximum_execution_time
49
+
50
+ # Benchmark
51
+ config.include_context "rspec_power::benchmark:run", :with_benchmark
52
+
53
+ # Request dump helpers (session/cookies/flash/headers)
54
+ config.include RSpecPower::RequestDumpHelpers
55
+ config.include_context "rspec_power::request_dump:after", :with_request_dump
56
+
57
+ # DB dump on failure
58
+ # Prefer :with_dump_db_on_fail; keep :dump_db_on_fail for backward compatibility
59
+ config.include_context "rspec_power::db_dump:on_fail", :with_dump_db_on_fail
60
+ config.include_context "rspec_power::db_dump:on_fail", :dump_db_on_fail
61
+ end