rspec-activerecord-formatter 1.0.3 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: eb680f42fdf9b7a605517cf7a577b17a494b0d8125e06616891f5aab77a359b5
4
- data.tar.gz: 984a2d0ab39d672bfc42d1cdfd8c6aa73435227a9745b1d672e709818d0f8a26
3
+ metadata.gz: 3eaf70a87ed79a750607124b849bc8f75325f3cbd8b27c6c6c378008bec7d058
4
+ data.tar.gz: 339da336745315f324dcac1a71ae2ec9800623b250c1f1cbb3e2bf7f7c1c2461
5
5
  SHA512:
6
- metadata.gz: 218ab5de04b4b8f01d80cd66b83c4a96dd7ecab0019660db3279ea593e62410c1c6ca58370d811e2b1be208123106c7e1124bf8e7d13cf133f0e0162733a4bb7
7
- data.tar.gz: ec002a337aed2d08b180504e36c6d284e920b73f26371b3bf8cdd3ffd5d95ec70f751e8d22b10620c0b4291cdc705a2ef936b8973df5a23f2ed8522c2e89a44c
6
+ metadata.gz: 671d5e6dc1b7a4abf41c831a26fe6d7f0e599babfb95bcdca2cc5e08e9704bfef855fc9327a349bbd5f5ad9e81032f529ca50d8cb09344589c35bd04c63a22ca
7
+ data.tar.gz: 27dd9d446ebf122ce2ef1fb442440a36965557e99f34e4fe730bcaa18607470b23dbb24dc0e0a8405961ea582bc9920817e9ac193d1dff33b19690a32b5f4cd1
data/.gitignore CHANGED
@@ -1,3 +1,4 @@
1
1
  .DS_Store
2
2
  Gemfile.lock
3
3
  coverage
4
+ *.gem
data/README.md CHANGED
@@ -17,7 +17,7 @@ Installation
17
17
  Normal bundle stuff. You'll almost certainly need to add this gem to your
18
18
  bundle at least temporarily, since you'll be running rspec using bundler.
19
19
 
20
- gem 'rspec-activerecord-formatter'
20
+ gem 'rspec-activerecord-formatter', require: false
21
21
 
22
22
 
23
23
  Usage
@@ -25,15 +25,16 @@ Usage
25
25
 
26
26
  The current answer is to change your `.rspec` initializer to include the following:
27
27
 
28
+ --require rspec-activerecord-formatter
28
29
  --require rails_helper
29
- --format ActiveRecordFormatter
30
+ --format ActiveRecordDocumentationFormatter
30
31
 
31
32
  We have to include the rails_helper so that ActiveRecord is loaded prior to trying to load the
32
33
  formatter. That way, we can hook into AR creations.
33
34
 
34
35
  You can also run the formatter as a one-off option to rspec:
35
36
 
36
- rspec path/to/example_spec.rb --require rails_helper --format ActiveRecordFormatter
37
+ rspec path/to/example_spec.rb --require rspec-activerecord-formatter --require rails_helper --format ActiveRecordDocumentationFormatter
37
38
 
38
39
  Once you set the formatter, you should now see the number of objects created and total queries
39
40
  for each of your tests:
@@ -44,12 +45,17 @@ You'll also get a summary at the end of your test run:
44
45
 
45
46
  ![Test summary.](https://github.com/jmmastey/rspec-activerecord-formatter/raw/master/doc/images/demo_1.png "Test summary.")
46
47
 
48
+ If you want less granularity, you can also use the progress formatter:
49
+
50
+ --require rspec-activerecord-formatter
51
+ --require rails_helper
52
+ --format ActiveRecordProgressFormatter
53
+
47
54
  Next Steps
48
55
  ------------
49
56
  * The method I was using to count AR objects doesn't work well with DatabaseCleaner when not explicitly wiring the library into `before` blocks.
50
57
  I'd like to be able to go back to a method other than scanning for `INSERT INTO` strings.
51
58
  * Configuration, especially formatting the output to optionally outdent the counts.
52
- * Add a `--profile`-like behavior to output the most offending tests.
53
59
  * The current dependency versions are a vague guess. They can and should clearly be more lenient.
54
60
  * I dunno, tests.
55
61
 
@@ -1 +1,6 @@
1
+ require "rspec/activerecord/helpers/collector"
2
+ require "rspec/activerecord/helpers/report"
3
+ require 'rspec/activerecord/base'
4
+ require 'rspec/activerecord/progress_formatter'
5
+ require 'rspec/activerecord/documentation_formatter'
1
6
  require 'rspec/activerecord/formatter'
@@ -0,0 +1,43 @@
1
+ class ActiveRecordFormatterBase
2
+ attr_reader :colorizer, :summary, :collector
3
+
4
+ def initialize(summary, collector)
5
+ @colorizer = ::RSpec::Core::Formatters::ConsoleCodes
6
+ @summary = summary
7
+ @collector = collector
8
+ end
9
+
10
+ def colorized_summary
11
+ formatted = "\nFinished in #{summary.formatted_duration} " \
12
+ "(files took #{summary.formatted_load_time} to load)\n" \
13
+ "#{colorized_expanded_totals}\n"
14
+
15
+ unless summary.failed_examples.empty?
16
+ formatted << summary.colorized_rerun_commands(colorizer) << "\n"
17
+ end
18
+
19
+ formatted
20
+ end
21
+
22
+ private
23
+
24
+ def colorized_expanded_totals
25
+ if summary.failure_count > 0
26
+ colorizer.wrap(expanded_totals_line, RSpec.configuration.failure_color)
27
+ elsif summary.pending_count > 0
28
+ colorizer.wrap(expanded_totals_line, RSpec.configuration.pending_color)
29
+ else
30
+ colorizer.wrap(expanded_totals_line, RSpec.configuration.success_color)
31
+ end
32
+ end
33
+
34
+ def expanded_totals_line
35
+ summary_text = ::RSpec::Core::Formatters::Helpers.pluralize(summary.example_count, "example")
36
+ summary_text << ", " << ::RSpec::Core::Formatters::Helpers.pluralize(summary.failure_count, "failure")
37
+ summary_text << ", #{summary.pending_count} pending" if summary.pending_count > 0
38
+ summary_text << ", #{collector.total_objects} AR objects"
39
+ summary_text << ", #{collector.total_queries} AR queries"
40
+
41
+ summary_text
42
+ end
43
+ end
@@ -0,0 +1,58 @@
1
+ class ActiveRecordDocumentationFormatter < ::RSpec::Core::Formatters::DocumentationFormatter
2
+ attr_reader :collector, :colorizer, :report
3
+
4
+ ::RSpec::Core::Formatters.register self, :start, :dump_summary,
5
+ :example_started, :example_group_started,
6
+ :example_group_finished
7
+
8
+ def initialize(output)
9
+ super
10
+
11
+ @colorizer = ::RSpec::Core::Formatters::ConsoleCodes
12
+ @collector = ActiveRecordFormatterHelpers::Collector.new
13
+ @report = ActiveRecordFormatterHelpers::Report.new(collector)
14
+ end
15
+
16
+ def start(_start_notification)
17
+ output.puts "Recording and reporting ActiveRecord select and creation counts."
18
+ super
19
+ end
20
+
21
+ def example_group_started(example_group)
22
+ collector.group_started(example_group.group)
23
+ super
24
+ end
25
+
26
+ def example_group_finished(example_group)
27
+ collector.group_finished(example_group.group)
28
+ super
29
+ end
30
+
31
+ def example_started(example)
32
+ collector.reset_example(example)
33
+ end
34
+
35
+ def dump_summary(summary)
36
+ base = ActiveRecordFormatterBase.new(summary, collector)
37
+ output.puts base.colorized_summary
38
+
39
+ output.puts "\nOutputting Detailed Profile Data to #{report.report_path}"
40
+ report.write
41
+ end
42
+
43
+ protected
44
+
45
+ def passed_output(example)
46
+ "#{current_indentation}#{example_counts}" +
47
+ colorizer.wrap(example.description.strip, :success)
48
+ end
49
+
50
+ def failure_output(example)
51
+ "#{current_indentation}#{example_counts}" +
52
+ colorizer.wrap("#{example.description.strip} (FAILED - #{next_failure_index})", :failure)
53
+ end
54
+
55
+ def example_counts(suffix: " ")
56
+ "(%02d, %02d)#{suffix}" % [collector.objects_count, collector.query_count]
57
+ end
58
+ end
@@ -1,81 +1 @@
1
- require_relative "helpers/collector"
2
-
3
- class ActiveRecordFormatter < ::RSpec::Core::Formatters::DocumentationFormatter
4
- attr_reader :collector, :colorizer, :configuration
5
-
6
- ::RSpec::Core::Formatters.register self, :start, :dump_summary,
7
- :example_started, :example_passed, :example_failed
8
-
9
- def initialize(output)
10
- super
11
-
12
- @colorizer = ::RSpec::Core::Formatters::ConsoleCodes
13
- @collector = ActiveRecordFormatterHelpers::Collector.new
14
- end
15
-
16
- def start(start_notification)
17
- output.puts "Recording and reporting ActiveRecord select and creation counts."
18
- end
19
-
20
- def example_started(example)
21
- collector.reset_example
22
- end
23
-
24
- def dump_summary(summary)
25
- formatted = "\nFinished in #{summary.formatted_duration} " \
26
- "(files took #{summary.formatted_load_time} to load)\n" \
27
- "#{colorized_expanded_totals(summary)}\n"
28
-
29
- unless summary.failed_examples.empty?
30
- formatted << summary.colorized_rerun_commands(colorizer) << "\n"
31
- end
32
-
33
- output.puts formatted
34
- output.puts record_type_summaries
35
- end
36
-
37
- def record_type_summaries
38
- summary = ["", "Summary of Queries"]
39
- collector.most_common_query_names.each do |name, count|
40
- summary << "%-4s %s" % [count, name]
41
- end
42
-
43
- summary.join("\n")
44
- end
45
-
46
- protected
47
-
48
- def passed_output(example)
49
- "#{current_indentation}#{example_counts}" +
50
- colorizer.wrap(example.description.strip, :success)
51
- end
52
-
53
- def failure_output(example)
54
- "#{current_indentation}#{example_counts}" +
55
- colorizer.wrap("#{example.description.strip} (FAILED - #{next_failure_index})", :failure)
56
- end
57
-
58
- def example_counts(suffix: " ")
59
- "(%02d, %02d)#{suffix}" % [collector.objects_count, collector.query_count]
60
- end
61
-
62
- def colorized_expanded_totals(summary)
63
- if summary.failure_count > 0
64
- colorizer.wrap(expanded_totals_line(summary), RSpec.configuration.failure_color)
65
- elsif summary.pending_count > 0
66
- colorizer.wrap(expanded_totals_line(summary), RSpec.configuration.pending_color)
67
- else
68
- colorizer.wrap(expanded_totals_line(summary), RSpec.configuration.success_color)
69
- end
70
- end
71
-
72
- def expanded_totals_line(summary)
73
- summary_text = ::RSpec::Core::Formatters::Helpers.pluralize(summary.example_count, "example")
74
- summary_text << ", " << ::RSpec::Core::Formatters::Helpers.pluralize(summary.failure_count, "failure")
75
- summary_text << ", #{summary.pending_count} pending" if summary.pending_count > 0
76
- summary_text << ", #{collector.total_objects} AR objects"
77
- summary_text << ", #{collector.total_queries} AR queries"
78
-
79
- summary_text
80
- end
81
- end
1
+ ActiveRecordFormatter = ActiveRecordDocumentationFormatter
@@ -2,15 +2,21 @@ require 'active_support/notifications'
2
2
 
3
3
  module ActiveRecordFormatterHelpers
4
4
  class Collector
5
- attr_reader :query_count, :objects_count, :total_queries, :total_objects, :query_names
5
+ attr_reader :query_count, :objects_count, :total_queries, :total_objects,
6
+ :query_names, :active_groups, :group_counts, :groups_encountered
7
+
6
8
  SKIP_QUERIES = ["SELECT tablename FROM pg_tables", "select sum(ct) from (select count(*) ct from"]
7
9
 
8
10
  def initialize
11
+ #@unnamed_queries = []
9
12
  @query_count = 0
13
+ @groups_encountered = 0
10
14
  @objects_count = 0
11
15
  @total_queries = 0
12
16
  @total_objects = 0
13
17
  @query_names = Hash.new(0)
18
+ @group_counts = Hash.new(0)
19
+ @active_groups = []
14
20
 
15
21
  ActiveSupport::Notifications.subscribe("sql.active_record", method(:record_query))
16
22
  end
@@ -18,33 +24,126 @@ module ActiveRecordFormatterHelpers
18
24
  def record_query(*_unused, data)
19
25
  return if SKIP_QUERIES.any? { |q| data[:sql].index(q) == 0 }
20
26
 
21
- @query_count += 1
22
- @total_queries += 1
23
-
24
- if query_is_an_insert?(data[:sql])
25
- @objects_count += 1
26
- @total_objects += 1
27
- end
28
-
29
- name = data[:name] || "Unnamed"
30
- @query_names[name] += 1
27
+ inc_query
28
+ inc_object if query_is_an_insert?(data[:sql])
29
+ inc_query_name(data)
31
30
  end
32
31
 
33
32
  def most_common_query_names
34
- @query_names.sort_by(&:last).reverse
33
+ query_names.sort_by(&:last).reverse
34
+ end
35
+
36
+ def most_expensive_groups
37
+ group_counts.sort_by(&:last).reverse
35
38
  end
36
39
 
37
- def reset_example
40
+ def reset_example(_)
38
41
  @query_count = 0
39
42
  @objects_count = 0
40
43
  end
41
44
 
45
+ def group_started(group)
46
+ @groups_encountered += 1
47
+
48
+ return unless group.parent_groups.length > 1
49
+
50
+ active_groups.push(group_path(group))
51
+ end
52
+
53
+ def group_finished(group)
54
+ active_groups.delete(group_path(group))
55
+ end
56
+
42
57
  protected
43
58
 
59
+ def inc_object
60
+ @objects_count += 1
61
+ @total_objects += 1
62
+
63
+ active_groups.each do |group|
64
+ @group_counts[group] += 1
65
+ end
66
+ end
67
+
68
+ def inc_query
69
+ @query_count += 1
70
+ @total_queries += 1
71
+ end
72
+
73
+ def inc_query_name(data)
74
+ name = data[:name] || "Unnamed"
75
+
76
+ # In older versions of Rails, insert statements are just counted as SQL
77
+ # queries, which means that all the queries are just bunchedup at the top.
78
+ # Makes this data pretty useless. So anyway, try to suss out a name for
79
+ # at least those insertions (which are much more frequent than, say,
80
+ # updates in a test suite anyway).
81
+ if data[:name] == "SQL" && query_is_an_insert?(data[:sql])
82
+ table = data[:sql].scan(/INSERT INTO "(\w+)"/).first.first
83
+ name = "#{table} Create"
84
+ elsif query_is_a_full_table_delete?(data[:sql])
85
+ table = data[:sql].scan(/DELETE FROM "(\w+)"/).first.first
86
+ name = "Full Delete Table"
87
+ # TODO: truncate table
88
+ elsif query_is_transaction_management?(data[:sql])
89
+ name = "Transaction Management"
90
+ elsif query_is_schema_detection?(data[:sql])
91
+ name = "SCHEMA"
92
+ elsif query_is_trigger_management?(data[:sql])
93
+ name = "Trigger Management"
94
+ elsif query_refreshes_materialized_view?(data[:sql])
95
+ name = "Refresh Materialized View"
96
+ end
97
+
98
+
99
+ # In older versions of Rails, insert statements are just counted as SQL
100
+ # queries, which means that all the queries are just bunchedup at the top.
101
+ # Makes this data pretty useless. So anyway, try to suss out a name for
102
+ # at least those insertions (which are much more frequent than, say,
103
+ # updates in a test suite anyway).
104
+ #if data[:name].nil? && query_is_a_delete?(data[:sql])
105
+ # table = data[:sql].scan(/DELETE FROM "(\w+)"/).first.first
106
+ # name = "#{table} Delete"
107
+ #end
108
+
109
+ #@unnamed_queries << data if name == "Unnamed"
110
+
111
+ query_names[name] += 1
112
+ end
113
+
114
+ def group_path(group)
115
+ group.parent_groups.reverse.map(&:description).join(' ')
116
+ end
117
+
44
118
  # TODO: what happens if we try to create many records at once?
45
119
  # TODO: are there any false positives we need to worry about? false negatives?
46
120
  def query_is_an_insert?(query)
47
- query =~ /INSERT INTO/
121
+ query =~ /^INSERT INTO/
122
+ end
123
+
124
+ def query_is_a_delete?(query)
125
+ query =~ /^DELETE FROM/
126
+ end
127
+
128
+ def query_is_a_full_table_delete?(query)
129
+ query =~ /^DELETE FROM [a-z_\."]*;$/i
130
+ end
131
+
132
+ def query_is_transaction_management?(query)
133
+ query =~ /^(COMMIT|BEGIN|ROLLBACK|SAVEPOINT|RELEASE SAVEPOINT)/
134
+ end
135
+
136
+ def query_is_schema_detection?(query)
137
+ query =~ /SELECT .* FROM pg_tables/m ||
138
+ query =~ /SELECT .* FROM information_schema.views/
139
+ end
140
+
141
+ def query_is_trigger_management?(query)
142
+ query =~ /(DISABLE|ENABLE) TRIGGER ALL/m
143
+ end
144
+
145
+ def query_refreshes_materialized_view?(query)
146
+ query =~ /REFRESH MATERIALIZED VIEW/m
48
147
  end
49
148
  end
50
149
  end
@@ -0,0 +1,45 @@
1
+ module ActiveRecordFormatterHelpers
2
+ class Report
3
+ attr_reader :report_path, :default_path, :report_dir, :collector
4
+
5
+ def initialize(collector)
6
+ @collector = collector
7
+
8
+ @report_dir = Rails.root.join("tmp", "profile")
9
+ @report_path = report_dir.join(timestamped_filename)
10
+ @default_path = report_dir.join('ar_most_recent.txt')
11
+ end
12
+
13
+ def write
14
+ write_file(file_path: report_path)
15
+ write_file(file_path: default_path)
16
+ end
17
+
18
+ private
19
+
20
+ def timestamped_filename
21
+ Time.now.strftime("ar_%Y_%m_%d_%H_%m_%S.txt")
22
+ end
23
+
24
+ def write_file(file_path:)
25
+ Dir.mkdir(report_dir) unless File.exists?(report_dir)
26
+
27
+ File.open(file_path, "wb") do |f|
28
+ f.puts "#{collector.total_objects} AR objects, #{collector.total_queries} AR queries"
29
+ f.puts "#{collector.groups_encountered} example groups executed"
30
+
31
+ f.puts ""
32
+ f.puts "Worst Example Groups by Object Creation"
33
+ collector.most_expensive_groups.first(50).each do |name, count|
34
+ f.puts "%-5s %s" % [count, name]
35
+ end
36
+
37
+ f.puts ""
38
+ f.puts "Most Common Queries"
39
+ collector.most_common_query_names.first(50).each do |name, count|
40
+ f.puts "%-5s %s" % [count, name]
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,44 @@
1
+ require_relative "helpers/collector"
2
+ require_relative "helpers/report"
3
+
4
+ class ActiveRecordProgressFormatter < ::RSpec::Core::Formatters::ProgressFormatter
5
+ attr_reader :collector, :colorizer, :report
6
+
7
+ ::RSpec::Core::Formatters.register self, :start, :dump_summary,
8
+ :example_started, :example_group_started,
9
+ :example_group_finished
10
+
11
+ def initialize(output)
12
+ super
13
+
14
+ @colorizer = ::RSpec::Core::Formatters::ConsoleCodes
15
+ @collector = ActiveRecordFormatterHelpers::Collector.new
16
+ @report = ActiveRecordFormatterHelpers::Report.new(collector)
17
+ end
18
+
19
+ def start(_start_notification)
20
+ output.puts "Recording and reporting ActiveRecord select and creation counts."
21
+ super
22
+ end
23
+
24
+ def example_group_started(example_group)
25
+ collector.group_started(example_group.group)
26
+ super
27
+ end
28
+
29
+ def example_group_finished(example_group)
30
+ collector.group_finished(example_group.group)
31
+ end
32
+
33
+ def example_started(example)
34
+ collector.reset_example(example)
35
+ end
36
+
37
+ def dump_summary(summary)
38
+ base = ActiveRecordFormatterBase.new(summary, collector)
39
+ output.puts base.colorized_summary
40
+
41
+ output.puts "\nOutputting Detailed Profile Data to #{report.report_path}"
42
+ report.write
43
+ end
44
+ end
@@ -6,7 +6,7 @@ $LOAD_PATH << lib_dir unless $LOAD_PATH.include?(lib_dir)
6
6
  Gem::Specification.new do |gem|
7
7
 
8
8
  gem.name = "rspec-activerecord-formatter"
9
- gem.version = "1.0.3"
9
+ gem.version = "2.1.0"
10
10
 
11
11
  gem.summary = "Adds object creations and queries to Rspec output."
12
12
  gem.description = "Creates a new formatter for ActiveRecord that can help you diagnose performance issues in RSpec"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rspec-activerecord-formatter
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.3
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joseph Mastey
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-04-07 00:00:00.000000000 Z
11
+ date: 2020-11-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -72,8 +72,12 @@ files:
72
72
  - doc/images/demo_1.png
73
73
  - doc/images/demo_2.png
74
74
  - lib/rspec-activerecord-formatter.rb
75
+ - lib/rspec/activerecord/base.rb
76
+ - lib/rspec/activerecord/documentation_formatter.rb
75
77
  - lib/rspec/activerecord/formatter.rb
76
78
  - lib/rspec/activerecord/helpers/collector.rb
79
+ - lib/rspec/activerecord/helpers/report.rb
80
+ - lib/rspec/activerecord/progress_formatter.rb
77
81
  - rspec-activerecord-formatter.gemspec
78
82
  - spec/collector_spec.rb
79
83
  - spec/formatter_spec.rb
@@ -82,7 +86,7 @@ homepage: http://github.com/jmmastey/rspec-activerecord-formatter
82
86
  licenses:
83
87
  - MIT
84
88
  metadata: {}
85
- post_install_message:
89
+ post_install_message:
86
90
  rdoc_options: []
87
91
  require_paths:
88
92
  - lib
@@ -97,9 +101,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
97
101
  - !ruby/object:Gem::Version
98
102
  version: '0'
99
103
  requirements: []
100
- rubyforge_project:
101
- rubygems_version: 2.7.3
102
- signing_key:
104
+ rubygems_version: 3.1.4
105
+ signing_key:
103
106
  specification_version: 4
104
107
  summary: Adds object creations and queries to Rspec output.
105
108
  test_files: []