test-prof-0.10.2/ 000755 001751 001751 00000000000 13626445505 014035 5 ustar 00pravi pravi 000000 000000 test-prof-0.10.2/CHANGELOG.md 000644 001751 001751 00000036420 13626445505 015653 0 ustar 00pravi pravi 000000 000000 # Change log
## master (unreleased)
## 0.10.2 (2020-01-07) 🎄
- Fix Ruby 2.7 deprecations. ([@lostie][])
## 0.10.1 (2019-10-17)
- Fix AnyFixture DSL when using with Rails 6.1+. ([@palkan][])
- Fix loading `let_it_be` without ActiveRecord present. ([@palkan][])
- Fix compatibility of `before_all` with [`isolator`](https://github.com/palkan/isolator) gem to handle correct usages of non-atomic interactions outside DB transactions. ([@Envek][])
- Updates FactoryProf to show the amount of time taken per factory call. ([@tyleriguchi][])
## 0.10.0 (2019-08-19)
- Use RSpec example ID instead of full description for RubyProf/Stackprof report names. ([@palkan][])
For more complex scenarios feel free to use your own report name generator:
```ruby
# for RubyProf
TestProf::RubyProf::Listener.report_name_generator = ->(example) { "..." }
# for Stackprof
TestProf::StackProf::Listener.report_name_generator = ->(example) { "..." }
```
- Support arrays in `let_it_be` with modifiers. ([@palkan][])
```ruby
# Now you can use modifiers with arrays
let_it_be(:posts, reload: true) { create_pair(:post) }
```
- Refactor `let_it_be` modifiers and allow adding custom modifiers. ([@palkan][])
```ruby
TestProf::LetItBe.config.register_modifier :reload do |record, val|
# ignore when `reload: false`
next record unless val
# ignore non-ActiveRecord objects
next record unless record.is_a?(::ActiveRecord::Base)
record.reload
end
```
- Print warning when `ActiveRecordSharedConnection` is used in the version of Rails
supporting `lock_threads` (5.1+). ([@palkan][])
## 0.9.0 (2019-05-14)
- Add threshold and custom event support to FactoryDoctor. ([@palkan][])
```sh
$ FDOC=1 FDOC_EVENT="sql.rom" FDOC_THRESHOLD=0.1 rspec
```
- Add Fabrication support to FactoryDoctor. ([@palkan][])
- Add `guard` and `top_level` options to `EventProf::Monitor`. ([@palkan][])
For example:
```ruby
TestProf::EventProf.monitor(
Sidekiq::Client,
"sidekiq.inline",
:raw_push,
top_level: true,
guard: ->(*) { Sidekiq::Testing.inline? }
)
```
- Add global `before_all` hooks. ([@danielwaterworth][], [@palkan][])
Now you can run additional code before and after every `before_all` transaction
begins and rollbacks:
```ruby
TestProf::BeforeAll.configure do |config|
config.before(:begin) do
# do something before transaction opens
end
config.after(:rollback) do
# do something after transaction closes
end
end
```
- Add ability to use `let_it_be` aliases with predefined options. ([@danielwaterworth][])
```ruby
TestProf::LetItBe.configure do |config|
config.alias_to :let_it_be_with_refind, refind: true
end
```
- Made FactoryProf measure and report on timing ([@danielwaterworth][])
## 0.8.0 (2019-04-12) 🚀
- **Ruby 2.4+ is requiered** ([@palkan][])
- **RSpec 3.5+ is requiered for RSpec features** ([@palkan][])
- Make `before_all` compatible with [`isolator`](https://github.com/palkan/isolator). ([@palkan][])
- Add `with_logging` and `with_ar_logging` helpers to logging recipe. ([@palkan][])
- Make `before_all` for Active Record `lock_thread` aware. ([@palkan][])
`before_all` can went crazy if you open multiple connections within it
(since it tracks the number of open transactions).
Rails 5+ `lock_thread` feature only locks the connection thread in
`before`/`setup` hook thus making it possible to have multiple connections/transactions
in `before_all` (e.g. performing jobs with Active Job async adapter).
## 0.7.5 (2019-02-22)
- Make `let_it_be` and `before_all` work with `include_context`. ([@palkan][])
Fixes [#117](https://github.com/palkan/test-prof/issues/117)
## 0.7.4 (2019-02-16)
- Add JSON report support for StackProf. ([@palkan][])
- Add ability to specify report/artifact name suffixes. ([@palkan][])
## 0.7.3 (2018-11-07)
- Add a header with the general information on factories usage [#99](https://github.com/palkan/test-prof/issues/99) ([@szemek][])
- Improve test sampling.([@mkldon][])
```bash
$ SAMPLE=10 rake test # runs 10 random test examples
$ SAMPLE_GROUPS=10 rake test # runs 10 random example groups
```
- Extend Event Prof formatter to include the absolute run time and the percentage of the event tim [#100](https://github.com/palkan/test-prof/issues/100) ([@dmagro][])
## 0.7.2 (2018-10-08)
- Add `RSpec/AggregateFailures` support for non-regular 'its' examples. ([@broels][])
## 0.7.1 (2018-08-20)
- Add ability to ignore connection configurations in shared connection.([@palkan][])
Example:
```ruby
# Do not use shared connection for sqlite db
TestProf::ActiveRecordSharedConnection.ignore { |config| config[:adapter] == "sqlite3" }
```
## 0.7.0 (2018-08-12)
- **Ruby 2.3+ is required**. ([@palkan][])
Ruby 2.2 EOL was on 2018-03-31.
- Upgrade RubyProf integration to `ruby-prof >= 0.17`. ([@palkan][])
Use `exclude_common_methods!` instead of the deprecated `eliminate_methods!`.
Add RSpec specific exclusions.
Add ability to specify custom exclusions through `config.custom_exclusions`, e.g.:
```ruby
TestProf::RubyProf.configure do |config|
config.custom_exclusions = {User => %i[save save!]}
end
```
## 0.6.0 (2018-06-29)
### Features
- Add `EventProf.monitor` to instrument arbitrary methods. ([@palkan][])
Add custom instrumetation easily:
```ruby
class Work
def do
# ...
end
end
# Instrument Work#do calls with "my.work" event
TestProf::EventProf.monitor(Work, "my.work", :do)
```
[📝 Docs](https://test-prof.evilmartians.io/#/event_prof?id=profile-arbitrary-methods)
- Adapterize `before_all`. ([@palkan][])
Now it's possible to write your own adapter for `before_all` to manage transactions.
[📝 Docs](https://test-prof.evilmartians.io/#/before_all?id=database-adapters)
- Add `before_all` for Minitest. ([@palkan][])
[📝 Docs](https://test-prof.evilmartians.io/#/before_all?id=minitest-experimental)
### Fixes & Improvements
- Show top `let` declarations per example group in RSpecDissect profiler. ([@palkan][])
The output now includes the following information:
```
Top 5 slowest suites (by `let` time):
FunnelsController (./spec/controllers/funnels_controller_spec.rb:3) – 00:38.532 of 00:43.649 (133)
↳ user – 3
↳ funnel – 2
ApplicantsController (./spec/controllers/applicants_controller_spec.rb:3) – 00:33.252 of 00:41.407 (222)
↳ user – 10
↳ funnel – 5
```
Enabled by default. Disable it with:
```ruby
TestProf::RSpecDissect.configure do |config|
config.let_stats_enabled = false
end
```
- [Fix [#80](https://github.com/palkan/test-prof/issues/80)] Added ability to preserve traits. ([@Vasfed][])
Disabled by default for compatibility. Enable globally by `FactoryDefault.preserve_traits = true` or for single `create_default`: `create_default(:user, preserve_traits: true)`
When enabled - default object will be used only when there's no [traits](https://github.com/thoughtbot/factory_bot/blob/master/GETTING_STARTED.md#traits) in association.
- Add ability to run only `let` or `before` profiler with RSpecDissect. ([@palkan][])
- Collect _raw_ data with StackProf by default. ([@palkan][])
- Refactor `:with_clean_fixture` to clean data once per group. ([@palkan][])
- [Fix [#75](https://github.com/palkan/test-prof/issues/75)] Fix `RSpec/Aggregate` failures with non-regular examples. ([@palkan][])
Do not take into account `xit`, `pending`, `its`, etc. examples,
only consider regular `it`, `specify`, `scenario`, `example`.
## 0.5.0 (2018-04-25)
### Features
- Add events support to TagProf. ([@palkan][])
Example usage:
```sh
TAG_PROF=type TAG_PROF_EVENT=sql.active_record rspec
```
[📝 Docs](https://test-prof.evilmartians.io/#/tag_prof?id=profiling-events)
- Add logging helpers for Rails. ([@palkan][])
Enable verbose logging globally:
```sh
LOG=all rspec
```
Or per example (group):
```ruby
it "does smth weird", :log do
# ...
end
```
[📝 Docs](https://test-prof.evilmartians.io/#/logging)
- Add HTML report for `TagProf`. ([@palkan][])
Generate HTML report by setting `TAG_PROF_FORMAT` to `html`.
- Add ability to track multiple events at the same time with `EventProf`. ([@palkan][])
- Add `AnyFixture` DSL. ([@palkan][])
Example:
```ruby
# Enable DSL
using TestProf::AnyFixture::DSL
# and then you can use `fixture` method (which is just an alias for `TestProf::AnyFixture.register`)
before(:all) { fixture(:account) }
# You can also use it to fetch the record (instead of storing it in instance variable)
let(:account) { fixture(:account) }
```
[📝 Docs](https://test-prof.evilmartians.io/#/any_fixture?id=dsl)
- Add `AnyFixture` usage report. ([@palkan][])
Enable `AnyFixture` usage reporting with `ANYFIXTURE_REPORTING=1` or with:
```ruby
TestProf::AnyFixture.reporting_enabled = true
```
[📝 Docs](https://test-prof.evilmartians.io/#/any_fixture?id=usage-report)
- Add `ActiveRecordSharedConnection` recipe. ([@palkan][])
Force ActiveRecord to use the same connection between threads (to avoid database cleaning in browser tests).
[📝 Docs](https://test-prof.evilmartians.io/#/active_record_shared_connection)
- [#70](https://github.com/palkan/test-prof/pull/70) Add `FactoryAllStub` recipe. ([@palkan][])
[📝 Docs](https://test-prof.evilmartians.io/#/factory_all_stub)
- Add `ActiveRecordRefind` refinement. ([@palkan][])
[📝 Docs](https://test-prof.evilmartians.io/#/any_fixture?id=activerecordrefind)
### Fixes & Improvements
- **Brand new documentatation website: https://test-prof.evilmartians.io/**
- Disable referential integrity when cleaning AnyFixture. ([@palkan][])
## 0.4.9 (2018-03-20)
- [Fix [#64](https://github.com/palkan/test-prof/issues/64)] Fix dependencies requiring for FactoryDefault. ([@palkan][])
- [Fix [#60](https://github.com/palkan/test-prof/issues/60)] Fix RSpecDissect reporter hooks. ([@palkan][])
Consider only `example_failed` and `example_passed` to ensure that the `run_time`
is available.
## 0.4.8 (2018-01-17)
- Add `minitest` 5.11 support. ([@palkan][])
- Fix `spring` detection. ([@palkan][])
Some `spring`-related gems do not check whether Spring is running and load
Spring modules. Thus we have `Spring` defined (and even `Spring.after_fork` defined) but no-op.
Now we require that `Spring::Applcation` is defined in order to rely on Spring.
Possibly fixes [#47](https://github.com/palkan/test-prof/issues/47).
## 0.4.7 (2017-12-25)
- [#57](https://github.com/palkan/test-prof/pull/57) Fix RubyProf Printers Support ([@rabotyaga][])
## 0.4.6 (2017-12-17)
- Upgrade RSpec/AggregateFailures to RuboCop 0.52.0. ([@palkan][])
RuboCop < 0.51.0 is not supported anymore.
- [Fixes [#49](https://github.com/palkan/test-prof/issues/49)] Correctly detect RSpec version in `let_it_be`. ([@desoleary][])
## 0.4.5 (2017-12-09)
- Fix circular require in `lib/factory_doctor/minitest`. ([@palkan][])
## 0.4.4 (2017-11-08)
- [Fixes [#48](https://github.com/palkan/test-prof/issues/48)] Respect RubyProf reports files extensions. ([@palkan][])
## 0.4.3 (2017-10-26)
- [#46](https://github.com/palkan/test-prof/pull/46) Support FactoryBot, which is [former FactoryGirl](https://github.com/thoughtbot/factory_bot/pull/1051),
while maintaining compatibility with latter. ([@Shkrt][])
## 0.4.2 (2017-10-23)
- Fix bug with multiple `before_all` within one group. ([@palkan][])
## 0.4.1 (2017-10-18)
- [#44](https://github.com/palkan/test-prof/pull/44) Support older versions of RSpec. ([@palkan][])
Support RSpec 3.1.0+ in general.
`let_it_be` supports only RSpec 3.3.0+.
RSpecDissect `let` tracking supports only RSpec 3.3.0+.
- [#38](https://github.com/palkan/test-prof/pull/38) Factory Doctor Minitest integration. ([@IDolgirev][])
It is possible now to use Factory Doctor with Minitest
## 0.4.0 (2017-10-03)
### Features:
- [#29](https://github.com/palkan/test-prof/pull/29) EventProf Minitest integration. ([@IDolgirev][])
It is possible now to use Event Prof with Minitest
- [#30](https://github.com/palkan/test-prof/pull/30) Fabrication support for FactoryProf. ([@Shkrt][])
FactoryProf now also accounts objects created by Fabrication gem (in addition to FactoryGirl)
## 0.3.0 (2017-09-21)
### Features:
- Combine RSpecStamp with FactoryDoctor. ([@palkan][])
Automatically mark _bad_ examples with custom tags.
- [#17](https://github.com/palkan/test-prof/pull/17) Combine RSpecStamp with EventProf and RSpecDissect. ([@palkan][])
It is possible now to automatically mark _slow_ examples and groups with custom tags. For example:
```sh
$ EVENT_PROF="sql.active_record" EVENT_PROF_STAMP="slow:sql" rspec ...
```
After running the command above the top 5 slowest example groups would be marked with `slow: :sql` tag.
- [#14](https://github.com/palkan/test-prof/pull/14) RSpecDissect profiler. ([@palkan][])
RSpecDissect tracks how much time do you spend in `before` hooks and memoization helpers (i.e. `let`) in your tests.
- [#13](https://github.com/palkan/test-prof/pull/13) RSpec `let_it_be` method. ([@palkan][])
Just like `let`, but persist the result for the whole group (i.e. `let` + `before_all`).
### Improvements:
- Add ability to specify RubyProf report through `TEST_RUBY_PROF` env variable. ([@palkan][])
- Add ability to specify StackProf raw mode through `TEST_STACK_PROF` env variable. ([@palkan][])
### Changes
- Use RubyProf `FlatPrinter` by default (was `CallStackPrinter`). ([@palkan][])
## 0.2.5 (2017-08-30)
- [#16](https://github.com/palkan/test-prof/pull/16) Support Ruby >= 2.2.0 (was >= 2.3.0). ([@palkan][])
## 0.2.4 (2017-08-29)
- EventProf: Fix regression bug with examples profiling. ([@palkan][])
There was a bug when an event occurs before the example has started (e.g. in `before(:context)` hook).
## 0.2.3 (2017-08-28)
- Minor improvements. ([@palkan][])
## 0.2.2 (2017-08-23)
- Fix time calculation when Time class is monkey-patched. ([@palkan][])
Add `TestProf.now` method which is just a copy of original `Time.now` and use it everywhere.
Fixes [#10](https://github.com/palkan/test-prof/issues/10).
## 0.2.1 (2017-08-19)
- Detect `RSpec` by checking the presence of `RSpec::Core`. ([@palkan][])
Fixes [#8](https://github.com/palkan/test-prof/issues/8).
## 0.2.0 (2017-08-18)
- Ensure output directory exists. ([@danielwestendorf][])
**Change default output dir** to "tmp/test_prof".
Rename `#artefact_path` to `#artifact_path` to be more US-like
Ensure output dir exists in `#artifact_path` method.
- FactoryDoctor: print success message when no bad examples found. ([@palkan][])
## 0.1.1 (2017-08-17)
- AnyFixture: clean tables in reverse order to not fail when foreign keys exist. ([@marshall-lee][])
## 0.1.0 (2017-08-15)
- Initial version. ([@palkan][])
[@palkan]: https://github.com/palkan
[@marshall-lee]: https://github.com/marshall-lee
[@danielwestendorf]: https://github.com/danielwestendorf
[@Shkrt]: https://github.com/Shkrt
[@IDolgirev]: https://github.com/IDolgirev
[@desoleary]: https://github.com/desoleary
[@rabotyaga]: https://github.com/rabotyaga
[@Vasfed]: https://github.com/Vasfed
[@szemek]: https://github.com/szemek
[@mkldon]: https://github.com/mkldon
[@dmagro]: https://github.com/dmagro
[@danielwaterworth]: https://github.com/danielwaterworth
[@Envek]: https://github.com/Envek
[@tyleriguchi]: https://github.com/tyleriguchi
[@lostie]: https://github.com/lostie
test-prof-0.10.2/lib/ 000755 001751 001751 00000000000 13626445505 014603 5 ustar 00pravi pravi 000000 000000 test-prof-0.10.2/lib/minitest/ 000755 001751 001751 00000000000 13626445505 016437 5 ustar 00pravi pravi 000000 000000 test-prof-0.10.2/lib/minitest/event_prof_formatter.rb 000644 001751 001751 00000004663 13626445505 023227 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
require "test_prof/ext/float_duration"
require "test_prof/ext/string_truncate"
module Minitest
module TestProf
class EventProfFormatter # :nodoc:
using ::TestProf::FloatDuration
using ::TestProf::StringTruncate
def initialize(profilers)
@profilers = profilers
@results = []
end
def prepare_results
@profilers.each do |profiler|
total_results(profiler)
by_groups(profiler)
by_examples(profiler)
end
@results.join
end
private
def total_results(profiler)
time_percentage = time_percentage(profiler.total_time, profiler.absolute_run_time)
@results <<
<<~MSG
EventProf results for #{profiler.event}
Total time: #{profiler.total_time.duration} of #{profiler.absolute_run_time.duration} (#{time_percentage}%)
Total events: #{profiler.total_count}
Top #{profiler.top_count} slowest suites (by #{profiler.rank_by}):
MSG
end
def by_groups(profiler)
result = profiler.results
groups = result[:groups]
groups.each do |group|
description = group[:id][:name]
location = group[:id][:location]
time = group[:time]
run_time = group[:run_time]
time_percentage = time_percentage(time, run_time)
@results <<
<<~GROUP
#{description.truncate} (#{location}) – #{time.duration} (#{group[:count]} / #{group[:examples]}) of #{run_time.duration} (#{time_percentage}%)
GROUP
end
end
def by_examples(profiler)
result = profiler.results
examples = result[:examples]
return unless examples
@results << "\nTop #{profiler.top_count} slowest tests (by #{profiler.rank_by}):\n\n"
examples.each do |example|
description = example[:id][:name]
location = example[:id][:location]
time = example[:time]
run_time = example[:run_time]
time_percentage = time_percentage(time, run_time)
@results <<
<<~GROUP
#{description.truncate} (#{location}) – #{time.duration} (#{example[:count]}) of #{run_time.duration} (#{time_percentage}%)
GROUP
end
end
def time_percentage(time, total_time)
(time / total_time * 100).round(2)
end
end
end
end
test-prof-0.10.2/lib/minitest/test_prof_plugin.rb 000644 001751 001751 00000003116 13626445505 022350 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
require "test_prof/event_prof/minitest"
require "test_prof/factory_doctor/minitest"
module Minitest # :nodoc:
module TestProf # :nodoc:
def self.configure_options(options = {})
options.tap do |opts|
opts[:event] = ENV["EVENT_PROF"] if ENV["EVENT_PROF"]
opts[:rank_by] = ENV["EVENT_PROF_RANK"].to_sym if ENV["EVENT_PROF_RANK"]
opts[:top_count] = ENV["EVENT_PROF_TOP"].to_i if ENV["EVENT_PROF_TOP"]
opts[:per_example] = true if ENV["EVENT_PROF_EXAMPLES"]
opts[:fdoc] = true if ENV["FDOC"]
end
end
end
def self.plugin_test_prof_options(opts, options)
opts.on "--event-prof=EVENT", "Collects metrics for specified EVENT" do |event|
options[:event] = event
end
opts.on "--event-prof-rank-by=RANK_BY", "Defines RANK_BY parameter for results" do |rank|
options[:rank_by] = rank.to_sym
end
opts.on "--event-prof-top-count=N", "Limits results with N groups/examples" do |count|
options[:top_count] = count.to_i
end
opts.on "--event-prof-per-example", TrueClass, "Includes examples metrics to results" do |flag|
options[:per_example] = flag
end
opts.on "--factory-doctor", TrueClass, "Enable Factory Doctor for your examples" do |flag|
options[:fdoc] = flag
end
end
def self.plugin_test_prof_init(options)
options = TestProf.configure_options(options)
reporter << TestProf::EventProfReporter.new(options[:io], options) if options[:event]
reporter << TestProf::FactoryDoctorReporter.new(options[:io], options) if options[:fdoc]
end
end
test-prof-0.10.2/lib/minitest/base_reporter.rb 000644 001751 001751 00000003212 13626445505 021616 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
require "minitest"
require "test_prof/logging"
module Minitest
module TestProf
class BaseReporter < AbstractReporter # :nodoc:
include ::TestProf::Logging
attr_accessor :io
def initialize(io = $stdout, _options = {})
@io = io
inject_to_minitest_reporters if defined? Minitest::Reporters
end
def start
end
def prerecord(group, example)
end
def before_test(test)
end
def record(*)
end
def after_test(test)
end
def report
end
private
def location(group, example = nil)
# Minitest::Result (>= 5.11) has `source_location` method
return group.source_location if group.respond_to?(:source_location)
if group.is_a? Class
suite = group.public_instance_methods.select { |mtd| mtd.to_s.match(/^test_/) }
name = suite.find { |mtd| mtd.to_s == example }
group.instance_method(name).source_location
else
suite = group.methods.select { |mtd| mtd.to_s.match(/^test_/) }
name = suite.find { |mtd| mtd.to_s == group.name }
group.method(name).source_location
end
end
def location_with_line_number(group, example = nil)
File.expand_path(location(group, example).join(":")).gsub(Dir.getwd, ".")
end
def location_without_line_number(group, example = nil)
File.expand_path(location(group, example).first).gsub(Dir.getwd, ".")
end
def inject_to_minitest_reporters
Minitest::Reporters.reporters << self if Minitest::Reporters.reporters
end
end
end
end
test-prof-0.10.2/lib/test_prof/ 000755 001751 001751 00000000000 13626445505 016610 5 ustar 00pravi pravi 000000 000000 test-prof-0.10.2/lib/test_prof/factory_all_stub.rb 000644 001751 001751 00000001344 13626445505 022473 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
require "test_prof/factory_bot"
require "test_prof/factory_all_stub/factory_bot_patch"
module TestProf
# FactoryAllStub inject into FactoryBot to make
# all strategies be `build_stubbed` strategy.
module FactoryAllStub
LOCAL_NAME = :__factory_bot_stub_all__
class << self
def init
# Monkey-patch FactoryBot / FactoryGirl
TestProf::FactoryBot::FactoryRunner.prepend(FactoryBotPatch) if
defined?(TestProf::FactoryBot)
end
def enabled?
Thread.current[LOCAL_NAME] == true
end
def enable!
Thread.current[LOCAL_NAME] = true
end
def disable!
Thread.current[LOCAL_NAME] = false
end
end
end
end
test-prof-0.10.2/lib/test_prof/before_all/ 000755 001751 001751 00000000000 13626445505 020702 5 ustar 00pravi pravi 000000 000000 test-prof-0.10.2/lib/test_prof/before_all/adapters/ 000755 001751 001751 00000000000 13626445505 022505 5 ustar 00pravi pravi 000000 000000 test-prof-0.10.2/lib/test_prof/before_all/adapters/active_record.rb 000644 001751 001751 00000002366 13626445505 025652 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
if ::ActiveRecord::VERSION::MAJOR < 4
require "test_prof/ext/active_record_3"
using TestProf::ActiveRecord3Transactions
end
module TestProf
module BeforeAll
module Adapters
# ActiveRecord adapter for `before_all`
module ActiveRecord
class << self
def begin_transaction
::ActiveRecord::Base.connection.begin_transaction(joinable: false)
end
def rollback_transaction
if ::ActiveRecord::Base.connection.open_transactions.zero?
warn "!!! before_all transaction has been already rollbacked and " \
"could work incorrectly"
return
end
::ActiveRecord::Base.connection.rollback_transaction
end
end
end
end
configure do |config|
# Make sure ActiveRecord uses locked thread.
# It only gets locked in `before` / `setup` hook,
# thus using thread in `before_all` (e.g. ActiveJob async adapter)
# might lead to leaking connections
config.before(:begin) do
next unless ::ActiveRecord::Base.connection.pool.respond_to?(:lock_thread=)
::ActiveRecord::Base.connection.pool.lock_thread = true
end
end
end
end
test-prof-0.10.2/lib/test_prof/before_all/isolator.rb 000644 001751 001751 00000000664 13626445505 023071 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
module TestProf
module BeforeAll
# Disable Isolator within before_all blocks
module Isolator
def begin_transaction(*)
::Isolator.transactions_threshold += 1
super
end
def rollback_transaction(*)
super
::Isolator.transactions_threshold -= 1
end
end
end
end
TestProf::BeforeAll.singleton_class.prepend(TestProf::BeforeAll::Isolator)
test-prof-0.10.2/lib/test_prof/factory_default/ 000755 001751 001751 00000000000 13626445505 021763 5 ustar 00pravi pravi 000000 000000 test-prof-0.10.2/lib/test_prof/factory_default/factory_bot_patch.rb 000644 001751 001751 00000000666 13626445505 026012 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
module TestProf
module FactoryDefault # :nodoc: all
module RunnerExt
refine TestProf::FactoryBot::FactoryRunner do
def name
@name
end
def traits
@traits
end
end
end
using RunnerExt
module StrategyExt
def association(runner)
FactoryDefault.get(runner.name, runner.traits) || super
end
end
end
end
test-prof-0.10.2/lib/test_prof/stack_prof.rb 000644 001751 001751 00000010523 13626445505 021271 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
module TestProf
# StackProf wrapper.
#
# Has 2 modes: global and per-example.
#
# Example:
#
# # To activate global profiling you can use env variable
# TEST_STACK_PROF=1 rspec ...
#
# # or in your code
# TestProf::StackProf.run
#
# To profile a specific examples add :sprof tag to it:
#
# it "is doing heavy stuff", :sprof do
# ...
# end
#
module StackProf
# StackProf configuration
class Configuration
FORMATS = %w[html json].freeze
attr_accessor :mode, :interval, :raw, :target, :format
def initialize
@mode = ENV.fetch("TEST_STACK_PROF_MODE", :wall).to_sym
@target = ENV["TEST_STACK_PROF"] == "boot" ? :boot : :suite
@raw = ENV["TEST_STACK_PROF_RAW"] != "0"
@format =
if FORMATS.include?(ENV["TEST_STACK_PROF_FORMAT"])
ENV["TEST_STACK_PROF_FORMAT"]
else
"html"
end
end
def raw?
@raw == true
end
def boot?
target == :boot
end
def suite?
target == :suite
end
end
class << self
include Logging
def config
@config ||= Configuration.new
end
def configure
yield config
end
# Run StackProf and automatically dump
# a report when the process exits or when the application is booted.
def run
return unless profile
@locked = true
log :info, "StackProf#{config.raw? ? " (raw)" : ""} enabled globally: " \
"mode – #{config.mode}, target – #{config.target}"
at_exit { dump("total") } if config.suite?
end
def profile(name = nil)
if locked?
log :warn, <<~MSG
StackProf is activated globally, you cannot generate per-example report.
Make sure you haven's set the TEST_STACK_PROF environmental variable.
MSG
return false
end
return false unless init_stack_prof
options = {
mode: config.mode,
raw: config.raw
}
options[:interval] = config.interval if config.interval
if block_given?
options[:out] = build_path(name)
::StackProf.run(**options) { yield }
else
::StackProf.start(**options)
end
true
end
def dump(name)
::StackProf.stop
path = build_path(name)
::StackProf.results(path)
log :info, "StackProf report generated: #{path}"
return unless config.raw
send("dump_#{config.format}_report", path)
end
private
def build_path(name)
TestProf.artifact_path(
"stack-prof-report-#{config.mode}#{config.raw ? "-raw" : ""}-#{name}.dump"
)
end
def locked?
@locked == true
end
def init_stack_prof
return @initialized if instance_variable_defined?(:@initialized)
@locked = false
@initialized = TestProf.require(
"stackprof",
<<~MSG
Please, install 'stackprof' first:
# Gemfile
gem 'stackprof', '>= 0.2.9', require: false
MSG
) { check_stack_prof_version }
end
def check_stack_prof_version
if Utils.verify_gem_version("stackprof", at_least: "0.2.9")
true
else
log :error, <<~MSG
Please, upgrade 'stackprof' to version >= 0.2.9.
MSG
false
end
end
def dump_html_report(path)
html_path = path.gsub(/\.dump$/, ".html")
log :info, <<~MSG
Run the following command to generate a flame graph report:
stackprof --flamegraph #{path} > #{html_path} && stackprof --flamegraph-viewer=#{html_path}
MSG
end
def dump_json_report(path)
report = ::StackProf::Report.new(
Marshal.load(IO.binread(path)) # rubocop:disable Security/MarshalLoad
)
json_path = path.gsub(/\.dump$/, ".json")
File.write(json_path, JSON.generate(report.data))
log :info, <<~MSG
StackProf JSON report generated: #{json_path}
MSG
end
end
end
end
require "test_prof/stack_prof/rspec" if TestProf.rspec?
# Hook to run StackProf globally
TestProf.activate("TEST_STACK_PROF") do
TestProf::StackProf.run
end
test-prof-0.10.2/lib/test_prof/rspec_dissect.rb 000644 001751 001751 00000006635 13626445505 022001 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
require "test_prof/rspec_stamp"
require "test_prof/logging"
module TestProf
# RSpecDissect tracks how much time do you spend in `before` hooks
# and memoization helpers (i.e. `let`) in your tests.
module RSpecDissect
module ExampleInstrumentation # :nodoc:
def run_before_example(*)
RSpecDissect.track(:before) { super }
end
end
module MemoizedInstrumentation # :nodoc:
def fetch_or_store(id, *)
res = nil
Thread.current[:_rspec_dissect_let_depth] ||= 0
Thread.current[:_rspec_dissect_let_depth] += 1
begin
res = if Thread.current[:_rspec_dissect_let_depth] == 1
RSpecDissect.track(:let, id) { super }
else
super
end
ensure
Thread.current[:_rspec_dissect_let_depth] -= 1
end
res
end
end
# RSpecDisect configuration
class Configuration
MODES = %w[all let before].freeze
attr_accessor :top_count, :let_stats_enabled,
:let_top_count
alias let_stats_enabled? let_stats_enabled
attr_reader :mode
def initialize
@let_stats_enabled = true
@let_top_count = (ENV["RD_PROF_LET_TOP"] || 3).to_i
@top_count = (ENV["RD_PROF_TOP"] || 5).to_i
@stamp = ENV["RD_PROF_STAMP"]
@mode = ENV["RD_PROF"] == "1" ? "all" : ENV["RD_PROF"]
unless MODES.include?(mode)
raise "Unknown RSpecDissect mode: #{mode};" \
"available modes: #{MODES.join(", ")}"
end
RSpecStamp.config.tags = @stamp if stamp?
end
def let?
mode == "all" || mode == "let"
end
def before?
mode == "all" || mode == "before"
end
def stamp?
!@stamp.nil?
end
end
METRICS = %w[before let].freeze
class << self
include Logging
def config
@config ||= Configuration.new
end
def configure
yield config
end
def init
RSpec::Core::Example.prepend(ExampleInstrumentation)
RSpec::Core::MemoizedHelpers::ThreadsafeMemoized.prepend(MemoizedInstrumentation)
RSpec::Core::MemoizedHelpers::NonThreadSafeMemoized.prepend(MemoizedInstrumentation)
@data = {}
METRICS.each do |type|
@data["total_#{type}"] = 0.0
end
reset!
log :info, "RSpecDissect enabled"
end
def track(type, meta = nil)
start = TestProf.now
res = yield
delta = (TestProf.now - start)
type = type.to_s
@data[type][:time] += delta
@data[type][:meta] << meta unless meta.nil?
@data["total_#{type}"] += delta
res
end
def reset!
METRICS.each do |type|
@data[type.to_s] = {time: 0.0, meta: []}
end
end
# Whether we are able to track `let` usage
def memoization_available?
defined?(::RSpec::Core::MemoizedHelpers::ThreadsafeMemoized)
end
def time_for(key)
@data[key.to_s][:time]
end
def meta_for(key)
@data[key.to_s][:meta]
end
def total_time_for(key)
@data["total_#{key}"]
end
end
end
end
require "test_prof/rspec_dissect/collectors/let"
require "test_prof/rspec_dissect/collectors/before"
require "test_prof/rspec_dissect/rspec" if TestProf.rspec?
TestProf.activate("RD_PROF") do
TestProf::RSpecDissect.init
end
test-prof-0.10.2/lib/test_prof/before_all.rb 000644 001751 001751 00000004623 13626445505 021234 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
module TestProf
# `before_all` helper configuration
module BeforeAll
class AdapterMissing < StandardError # :nodoc:
MSG = "Please, provide an adapter for `before_all` " \
"through `TestProf::BeforeAll.adapter = MyAdapter`"
def initialize
super(MSG)
end
end
class << self
attr_accessor :adapter
def begin_transaction
raise AdapterMissing if adapter.nil?
config.run_hooks(:begin) do
adapter.begin_transaction
end
yield
end
def within_transaction
yield
end
def rollback_transaction
raise AdapterMissing if adapter.nil?
config.run_hooks(:rollback) do
adapter.rollback_transaction
end
end
def config
@config ||= Configuration.new
end
def configure
yield config
end
end
class HooksChain # :nodoc:
attr_reader :type, :after, :before
def initialize(type)
@type = type
@before = []
@after = []
end
def run
before.each(&:call)
yield
after.each(&:call)
end
end
class Configuration
HOOKS = %i[begin rollback].freeze
def initialize
@hooks = Hash.new { |h, k| h[k] = HooksChain.new(k) }
end
# Add `before` hook for `begin` or
# `rollback` operation:
#
# config.before(:rollback) { ... }
def before(type, &block)
validate_hook_type!(type)
hooks[type].before << block if block_given?
end
# Add `after` hook for `begin` or
# `rollback` operation:
#
# config.after(:begin) { ... }
def after(type, &block)
validate_hook_type!(type)
hooks[type].after << block if block_given?
end
def run_hooks(type) # :nodoc:
validate_hook_type!(type)
hooks[type].run { yield }
end
private
def validate_hook_type!(type)
return if HOOKS.include?(type)
raise ArgumentError, "Unknown hook type: #{type}. Valid types: #{HOOKS.join(", ")}"
end
attr_reader :hooks
end
end
end
if defined?(::ActiveRecord::Base)
require "test_prof/before_all/adapters/active_record"
TestProf::BeforeAll.adapter = TestProf::BeforeAll::Adapters::ActiveRecord
end
if defined?(::Isolator)
require "test_prof/before_all/isolator"
end
test-prof-0.10.2/lib/test_prof/rspec_stamp/ 000755 001751 001751 00000000000 13626445505 021130 5 ustar 00pravi pravi 000000 000000 test-prof-0.10.2/lib/test_prof/rspec_stamp/parser.rb 000644 001751 001751 00000007654 13626445505 022765 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
require "ripper"
module TestProf
module RSpecStamp
# Parse examples headers
module Parser
# Contains the result of parsing
class Result
attr_accessor :fname, :desc, :desc_const
attr_reader :tags, :htags
def add_tag(v)
@tags ||= []
@tags << v
end
def add_htag(k, v)
@htags ||= []
@htags << [k, v]
end
def remove_tag(tag)
@tags&.delete(tag)
@htags&.delete_if { |(k, _v)| k == tag }
end
end
class << self
# rubocop: disable Metrics/CyclomaticComplexity
# rubocop: disable Metrics/PerceivedComplexity
def parse(code)
sexp = Ripper.sexp(code)
return unless sexp
# sexp has the following format:
# [:program,
# [
# [
# :command,
# [:@ident, "it", [1, 0]],
# [:args_add_block, [ ... ]]
# ]
# ]
# ]
#
# or
#
# [:program,
# [
# [
# :vcall,
# [:@ident, "it", [1, 0]]
# ]
# ]
# ]
res = Result.new
fcall = sexp[1][0][1]
args_block = sexp[1][0][2]
if fcall.first == :fcall
fcall = fcall[1]
elsif fcall.first == :var_ref
res.fname = [parse_const(fcall), sexp[1][0][3][1]].join(".")
args_block = sexp[1][0][4]
end
res.fname ||= fcall[1]
return res if args_block.nil?
args_block = args_block[1] if args_block.first == :arg_paren
args = args_block[1]
if args.first.first == :string_literal
res.desc = parse_literal(args.shift)
elsif args.first.first == :var_ref || args.first.first == :const_path_ref
res.desc_const = parse_const(args.shift)
end
parse_arg(res, args.shift) until args.empty?
res
end
# rubocop: enable Metrics/CyclomaticComplexity
# rubocop: enable Metrics/PerceivedComplexity
private
def parse_arg(res, arg)
if arg.first == :symbol_literal
res.add_tag parse_literal(arg)
elsif arg.first == :bare_assoc_hash
parse_hash(res, arg[1])
end
end
def parse_hash(res, hash_arg)
hash_arg.each do |(_, label, val)|
res.add_htag label[1][0..-2].to_sym, parse_value(val)
end
end
# Expr of the form:
# bool - [:var_ref, [:@kw, "true", [1, 24]]]
# string - [:string_literal, [:string_content, [...]]]
# int - [:@int, "3", [1, 52]]]]
def parse_value(expr)
case expr.first
when :var_ref
expr[1][1] == "true"
when :@int
expr[1].to_i
when :@float
expr[1].to_f
else
parse_literal(expr)
end
end
# Expr of the form:
# [:string_literal, [:string_content, [:@tstring_content, "is", [1, 4]]]]
def parse_literal(expr)
val = expr[1][1][1]
val = val.to_sym if expr[0] == :symbol_literal ||
expr[0] == :assoc_new
val
end
# Expr of the form:
# [:var_ref, [:@const, "User", [1, 9]]]
#
# or
#
# [:const_path_ref, [:const_path_ref, [:var_ref,
# [:@const, "User", [1, 17]]],
# [:@const, "Guest", [1, 23]]],
# [:@const, "Collection", [1, 30]]
def parse_const(expr)
if expr.first == :var_ref
expr[1][1]
elsif expr.first == :@const
expr[1]
elsif expr.first == :const_path_ref
expr[1..-1].map(&method(:parse_const)).join("::")
end
end
end
end
end
end
test-prof-0.10.2/lib/test_prof/rspec_stamp/rspec.rb 000644 001751 001751 00000002642 13626445505 022575 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
module TestProf
module RSpecStamp
class RSpecListener # :nodoc:
include Logging
NOTIFICATIONS = %i[
example_failed
].freeze
def initialize
@failed = 0
@ignored = 0
@total = 0
@examples = Hash.new { |h, k| h[k] = [] }
end
def example_failed(notification)
return if notification.example.pending?
location = notification.example.metadata[:location]
file, line = location.split(":")
@examples[file] << line.to_i
end
def stamp!
stamper = Stamper.new
@examples.each do |file, lines|
stamper.stamp_file(file, lines.uniq)
end
msgs = []
msgs <<
<<~MSG
RSpec Stamp results
Total patches: #{stamper.total}
Total files: #{@examples.keys.size}
Failed patches: #{stamper.failed}
Ignored files: #{stamper.ignored}
MSG
log :info, msgs.join
end
end
end
end
# Register EventProf listener
TestProf.activate("RSTAMP") do
RSpec.configure do |config|
listener = nil
config.before(:suite) do
listener = TestProf::RSpecStamp::RSpecListener.new
config.reporter.register_listener(
listener, *TestProf::RSpecStamp::RSpecListener::NOTIFICATIONS
)
end
config.after(:suite) { listener&.stamp! }
end
end
test-prof-0.10.2/lib/test_prof/ext/ 000755 001751 001751 00000000000 13626445505 017410 5 ustar 00pravi pravi 000000 000000 test-prof-0.10.2/lib/test_prof/ext/active_record_refind.rb 000644 001751 001751 00000001005 13626445505 024071 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
module TestProf
module Ext
# Adds `ActiveRecord::Base#refind` method (through refinement)
module ActiveRecordRefind
refine ActiveRecord::Base do
# Returns new reloaded record.
#
# Unlike `reload` this method returns
# completely re-initialized instance.
#
# We need it to make sure that the state is clean.
def refind
self.class.find(send(self.class.primary_key))
end
end
end
end
end
test-prof-0.10.2/lib/test_prof/ext/string_truncate.rb 000644 001751 001751 00000000662 13626445505 023154 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
module TestProf
# Extend String with #truncate method
module StringTruncate
refine String do
# Truncate to the specified limit
# by replacing middle part with dots
def truncate(limit = 30)
return self unless size > limit
head = ((limit - 3) / 2)
tail = head + 3 - limit
"#{self[0..(head - 1)]}...#{self[tail..-1]}"
end
end
end
end
test-prof-0.10.2/lib/test_prof/ext/float_duration.rb 000644 001751 001751 00000000406 13626445505 022747 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
module TestProf
# Extend Float with #duration method
module FloatDuration
refine Float do
def duration
t = self
format("%02d:%02d.%03d", t / 60, t % 60, t.modulo(1) * 1000)
end
end
end
end
test-prof-0.10.2/lib/test_prof/ext/active_record_3.rb 000644 001751 001751 00000001265 13626445505 022774 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
module TestProf
# Add missing `begin_transaction` and `rollback_transaction` methods
module ActiveRecord3Transactions
refine ::ActiveRecord::ConnectionAdapters::AbstractAdapter do
def begin_transaction(joinable: true)
increment_open_transactions
if open_transactions > 0
create_savepoint
else
begin_db_transaction
end
self.transaction_joinable = joinable
end
def rollback_transaction(*)
if open_transactions > 1
rollback_to_savepoint
else
rollback_db_transaction
end
decrement_open_transactions
end
end
end
end
test-prof-0.10.2/lib/test_prof/ext/array_bsearch_index.rb 000644 001751 001751 00000000531 13626445505 023730 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
module TestProf
# Ruby 2.3 #bsearch_index method (for usage with older Rubies)
# Straightforward and non-optimal implementation,
# just for compatibility
module ArrayBSearchIndex
refine Array do
def bsearch_index(&block)
el = bsearch(&block)
index(el)
end
end
end
end
test-prof-0.10.2/lib/test_prof/ext/factory_bot_strategy.rb 000644 001751 001751 00000001051 13626445505 024167 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
module TestProf
# FactoryBot 5.0 uses strategy classes for associations,
# older versions and top-level invocations use Symbols.
#
# This Refinement should be used FactoryRunner patches to check
# that strategy is :create.
module FactoryBotStrategy
refine Symbol do
def create?
self == :create
end
end
if defined?(::FactoryBot::Strategy::Create)
refine Class do
def create?
self <= ::FactoryBot::Strategy::Create
end
end
end
end
end
test-prof-0.10.2/lib/test_prof/ext/string_parameterize.rb 000644 001751 001751 00000000622 13626445505 024013 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
module TestProf
# Extend String with #parameterize method
module StringParameterize
refine String do
# Replaces special characters in a string with dashes.
def parameterize(separator: "-", preserve_case: false)
gsub(/[^a-z0-9\-_]+/i, separator).tap do |str|
str.downcase! unless preserve_case
end
end
end
end
end
test-prof-0.10.2/lib/test_prof/cops/ 000755 001751 001751 00000000000 13626445505 017554 5 ustar 00pravi pravi 000000 000000 test-prof-0.10.2/lib/test_prof/cops/rspec/ 000755 001751 001751 00000000000 13626445505 020670 5 ustar 00pravi pravi 000000 000000 test-prof-0.10.2/lib/test_prof/cops/rspec/aggregate_failures.rb 000644 001751 001751 00000011254 13626445505 025040 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
require "rubocop"
require "test_prof/utils"
module RuboCop
module Cop
module RSpec
# Rejects and auto-corrects the usage of one-liners examples in favour of
# :aggregate_failures feature.
#
# Example:
#
# # bad
# it { is_expected.to be_success }
# it { is_expected.to have_header('X-TOTAL-PAGES', 10) }
# it { is_expected.to have_header('X-NEXT-PAGE', 2) }
# its(:status) { is_expected.to eq(200) }
#
# # good
# it "returns the second page", :aggregate_failures do
# is_expected.to be_success
# is_expected.to have_header('X-TOTAL-PAGES', 10)
# is_expected.to have_header('X-NEXT-PAGE', 2)
# expect(subject.status).to eq(200)
# end
#
class AggregateFailures < RuboCop::Cop::Cop
# From https://github.com/backus/rubocop-rspec/blob/master/lib/rubocop/rspec/language.rb
GROUP_BLOCKS = %i[
describe context feature example_group
].freeze
EXAMPLE_BLOCKS = %i[
it its specify example scenario
].freeze
class << self
def supported?
return @supported if instance_variable_defined?(:@supported)
@supported = TestProf::Utils.verify_gem_version("rubocop", at_least: "0.51.0")
unless @supported
warn "RSpec/AggregateFailures cop requires RuboCop >= 0.51.0. Skipping"
end
@supported
end
end
def on_block(node)
return unless self.class.supported?
method, _args, body = *node
return unless body&.begin_type?
_receiver, method_name, _object = *method
return unless GROUP_BLOCKS.include?(method_name)
return if check_node(body)
add_offense(
node,
location: :expression,
message: "Use :aggregate_failures instead of several one-liners."
)
end
def autocorrect(node)
_method, _args, body = *node
iter = body.children.each
first_example = loop do
child = iter.next
break child if oneliner?(child)
end
base_indent = " " * first_example.source_range.column
replacements = [
header_from(first_example),
body_from(first_example, base_indent)
]
last_example = nil
loop do
child = iter.next
break unless oneliner?(child)
last_example = child
replacements << body_from(child, base_indent)
end
replacements << "#{base_indent}end"
range = first_example.source_range.begin.join(
last_example.source_range.end
)
replacement = replacements.join("\n")
lambda do |corrector|
corrector.replace(range, replacement)
end
end
private
def check_node(node)
offenders = 0
node.children.each do |child|
if oneliner?(child)
offenders += 1
elsif example_node?(child)
break if offenders > 1
offenders = 0
end
end
offenders < 2
end
def oneliner?(node)
node&.block_type? &&
(node.source.lines.size == 1) &&
example_node?(node)
end
def example_node?(node)
method, _args, _body = *node
_receiver, method_name, _object = *method
EXAMPLE_BLOCKS.include?(method_name)
end
def header_from(node)
method, _args, _body = *node
_receiver, method_name, _object = *method
method_name = :it if method_name == :its
%(#{method_name} "works", :aggregate_failures do)
end
def body_from(node, base_indent = "")
method, _args, body = *node
body_source = method.method_name == :its ? body_from_its(method, body) : body.source
"#{base_indent}#{indent}#{body_source}"
end
def body_from_its(method, body)
subject_attribute = method.arguments.first
expectation = body.method_name
match = body.arguments.first.source
if subject_attribute.array_type?
hash_keys = subject_attribute.values.map(&:value).join(", ")
attribute = "dig(#{hash_keys})"
else
attribute = subject_attribute.value
end
"expect(subject.#{attribute}).#{expectation} #{match}"
end
def indent
@indent ||= " " * (config.for_cop("IndentationWidth")["Width"] || 2)
end
end
end
end
end
test-prof-0.10.2/lib/test_prof/factory_doctor/ 000755 001751 001751 00000000000 13626445505 021631 5 ustar 00pravi pravi 000000 000000 test-prof-0.10.2/lib/test_prof/factory_doctor/rspec.rb 000644 001751 001751 00000007072 13626445505 023300 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
require "test_prof/ext/float_duration"
module TestProf
module FactoryDoctor
class RSpecListener # :nodoc:
include Logging
using FloatDuration
SUCCESS_MESSAGE = 'FactoryDoctor says: "Looks good to me!"'
NOTIFICATIONS = %i[
example_started
example_finished
].freeze
def initialize
@count = 0
@time = 0.0
@example_groups = Hash.new { |h, k| h[k] = [] }
end
def example_started(_notification)
FactoryDoctor.start
end
def example_finished(notification)
FactoryDoctor.stop
return if notification.example.pending?
result = FactoryDoctor.result
return unless result.bad?
group = notification.example.example_group.parent_groups.last
notification.example.metadata.merge!(
factories: result.count,
time: result.time
)
@example_groups[group] << notification.example
@count += 1
@time += result.time
end
def print
return log(:info, SUCCESS_MESSAGE) if @example_groups.empty?
msgs = []
msgs <<
<<~MSG
FactoryDoctor report
Total (potentially) bad examples: #{@count}
Total wasted time: #{@time.duration}
MSG
@example_groups.each do |group, examples|
group_time = examples.sum { |ex| ex.metadata[:time] }
group_count = examples.sum { |ex| ex.metadata[:factories] }
msgs << "#{group.description} (#{group.metadata[:location]}) " \
"(#{pluralize_records(group_count)} created, " \
"#{group_time.duration})\n"
examples.each do |ex|
msgs << " #{ex.description} (#{ex.metadata[:location]}) " \
"– #{pluralize_records(ex.metadata[:factories])} created, "\
"#{ex.metadata[:time].duration}\n"
end
msgs << "\n"
end
log :info, msgs.join
stamp! if FactoryDoctor.stamp?
end
def stamp!
stamper = RSpecStamp::Stamper.new
examples = Hash.new { |h, k| h[k] = [] }
@example_groups.each_value do |bad_examples|
bad_examples.each do |example|
file, line = example.metadata[:location].split(":")
examples[file] << line.to_i
end
end
examples.each do |file, lines|
stamper.stamp_file(file, lines.uniq)
end
msgs = []
msgs <<
<<~MSG
RSpec Stamp results
Total patches: #{stamper.total}
Total files: #{examples.keys.size}
Failed patches: #{stamper.failed}
Ignored files: #{stamper.ignored}
MSG
log :info, msgs.join
end
private
def pluralize_records(count)
return "1 record" if count == 1
"#{count} records"
end
end
end
end
# Register FactoryDoctor listener
TestProf.activate("FDOC") do
TestProf::FactoryDoctor.init
RSpec.configure do |config|
listener = nil
config.before(:suite) do
listener = TestProf::FactoryDoctor::RSpecListener.new
config.reporter.register_listener(
listener, *TestProf::FactoryDoctor::RSpecListener::NOTIFICATIONS
)
end
config.after(:suite) { listener&.print }
end
RSpec.shared_context "factory_doctor:ignore" do
around(:each) { |ex| TestProf::FactoryDoctor.ignore(&ex) }
end
RSpec.configure do |config|
config.include_context "factory_doctor:ignore", fd_ignore: true
end
end
test-prof-0.10.2/lib/test_prof/factory_doctor/minitest.rb 000644 001751 001751 00000004570 13626445505 024020 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
require "minitest/base_reporter"
require "test_prof/ext/float_duration"
module Minitest
module TestProf # :nodoc:
# Add fd_ignore methods
module FactoryDoctorIgnore
def fd_ignore
::TestProf::FactoryDoctor.ignore!
end
end
Minitest::Test.include FactoryDoctorIgnore
class FactoryDoctorReporter < BaseReporter # :nodoc:
using ::TestProf::FloatDuration
SUCCESS_MESSAGE = 'FactoryDoctor says: "Looks good to me!"'
def initialize(io = $stdout, options = {})
super
::TestProf::FactoryDoctor.init
@count = 0
@time = 0.0
@example_groups = Hash.new { |h, k| h[k] = [] }
end
def prerecord(_group, _example)
::TestProf::FactoryDoctor.start
end
def record(example)
::TestProf::FactoryDoctor.stop
return if example.skipped? || ::TestProf::FactoryDoctor.ignore?
result = ::TestProf::FactoryDoctor.result
return unless result.bad?
# Minitest::Result (>= 5.11) has `klass` method
group_name = example.respond_to?(:klass) ? example.klass : example.class.name
group = {
description: group_name,
location: location_without_line_number(example)
}
@example_groups[group] << {
description: example.name.gsub(/^test_(?:\d+_)?/, ""),
location: location_with_line_number(example),
factories: result.count,
time: result.time
}
@count += 1
@time += result.time
end
def report
return log(:info, SUCCESS_MESSAGE) if @example_groups.empty?
msgs = []
msgs <<
<<~MSG
FactoryDoctor report
Total (potentially) bad examples: #{@count}
Total wasted time: #{@time.duration}
MSG
@example_groups.each do |group, examples|
msgs << "#{group[:description]} (#{group[:location]})\n"
examples.each do |ex|
msgs << " #{ex[:description]} (#{ex[:location]}) "\
"– #{pluralize_records(ex[:factories])} created, "\
"#{ex[:time].duration}\n"
end
msgs << "\n"
end
log :info, msgs.join
end
private
def pluralize_records(count)
count == 1 ? "1 record" : "#{count} records"
end
end
end
end
test-prof-0.10.2/lib/test_prof/factory_doctor/fabrication_patch.rb 000644 001751 001751 00000000375 13626445505 025623 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
module TestProf
module FactoryDoctor
# Wrap #run method with FactoryDoctor tracking
module FabricationPatch
def create(*)
FactoryDoctor.within_factory(:create) { super }
end
end
end
end
test-prof-0.10.2/lib/test_prof/factory_doctor/factory_bot_patch.rb 000644 001751 001751 00000000600 13626445505 025644 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
require "test_prof/ext/factory_bot_strategy"
module TestProf
module FactoryDoctor
# Wrap #run method with FactoryDoctor tracking
module FactoryBotPatch
using TestProf::FactoryBotStrategy
def run(strategy = @strategy)
FactoryDoctor.within_factory(strategy.create? ? :create : :other) { super }
end
end
end
end
test-prof-0.10.2/lib/test_prof/tag_prof.rb 000644 001751 001751 00000000410 13626445505 020731 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
require "test_prof/tag_prof/result"
require "test_prof/tag_prof/printers/simple"
require "test_prof/tag_prof/printers/html"
module TestProf
module TagProf # :nodoc:
end
end
require "test_prof/tag_prof/rspec" if TestProf.rspec?
test-prof-0.10.2/lib/test_prof/recipes/ 000755 001751 001751 00000000000 13626445505 020242 5 ustar 00pravi pravi 000000 000000 test-prof-0.10.2/lib/test_prof/recipes/logging.rb 000644 001751 001751 00000006231 13626445505 022217 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
require "test_prof"
module TestProf
module Rails
# Add `with_logging` and `with_ar_logging helpers`
module LoggingHelpers
class << self
attr_writer :logger
def logger
return @logger if instance_variable_defined?(:@logger)
@logger = Logger.new(STDOUT)
end
def ar_loggables
return @ar_loggables if instance_variable_defined?(:@ar_loggables)
@ar_loggables = [
::ActiveRecord::Base,
::ActiveSupport::LogSubscriber
]
end
# rubocop:disable Metrics/CyclomaticComplexity
# rubocop:disable Metrics/PerceivedComplexity
def all_loggables
return @all_loggables if instance_variable_defined?(:@all_loggables)
@all_loggables = [
::ActiveSupport::LogSubscriber,
::Rails,
defined?(::ActiveRecord::Base) && ::ActiveRecord::Base,
defined?(::ActiveJob::Base) && ::ActiveJob::Base,
defined?(::ActionView::Base) && ::ActionView::Base,
defined?(::ActionMailer::Base) && ::ActionMailer::Base,
defined?(::ActionCable::Server::Base.config) && ::ActionCable::Server::Base.config,
defined?(::ActiveStorage) && ::ActiveStorage
].compact
end
# rubocop:enable Metrics/CyclomaticComplexity
# rubocop:enable Metrics/PerceivedComplexity
def swap_logger(loggables)
loggables.map do |loggable|
was_logger = loggable.logger
loggable.logger = logger
was_logger
end
end
def restore_logger(was_loggers, loggables)
loggables.each_with_index do |loggable, i|
loggable.logger = was_loggers[i]
end
end
end
# Enable verbose Rails logging within a block
def with_logging
*loggers = LoggingHelpers.swap_logger(LoggingHelpers.all_loggables)
yield
ensure
LoggingHelpers.restore_logger(loggers, LoggingHelpers.all_loggables)
end
def with_ar_logging
*loggers = LoggingHelpers.swap_logger(LoggingHelpers.ar_loggables)
yield
ensure
LoggingHelpers.restore_logger(loggers, LoggingHelpers.ar_loggables)
end
end
end
end
if TestProf.rspec?
RSpec.shared_context "logging:verbose" do
around(:each) do |ex|
with_logging(&ex)
end
end
RSpec.shared_context "logging:active_record" do
around(:each) do |ex|
with_ar_logging(&ex)
end
end
RSpec.configure do |config|
config.include TestProf::Rails::LoggingHelpers
config.include_context "logging:active_record", log: :ar
config.include_context "logging:verbose", log: true
end
end
TestProf.activate("LOG", "all") do
TestProf.log :info, "Rails verbose logging enabled"
ActiveSupport::LogSubscriber.logger =
Rails.logger =
ActiveRecord::Base.logger = TestProf::Rails::LoggingHelpers.logger
end
TestProf.activate("LOG", "ar") do
TestProf.log :info, "Active Record verbose logging enabled"
ActiveSupport::LogSubscriber.logger =
ActiveRecord::Base.logger = TestProf::Rails::LoggingHelpers.logger
end
test-prof-0.10.2/lib/test_prof/recipes/minitest/ 000755 001751 001751 00000000000 13626445505 022076 5 ustar 00pravi pravi 000000 000000 test-prof-0.10.2/lib/test_prof/recipes/minitest/before_all.rb 000644 001751 001751 00000003431 13626445505 024516 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
require "test_prof/before_all"
module TestProf
module BeforeAll
# Add before_all hook to Minitest: wrap all examples into a transaction and
# store instance variables
module Minitest # :nodoc: all
class Executor
attr_reader :active
alias active? active
def initialize(&block)
@block = block
end
def activate!(test_class)
return if active?
@active = true
@examples_left = test_class.runnable_methods.size
BeforeAll.begin_transaction do
capture!
end
end
def try_deactivate!
@examples_left -= 1
return unless @examples_left.zero?
@active = false
BeforeAll.rollback_transaction
end
def capture!
instance_eval(&@block)
end
def restore_to(test_object)
instance_variables.each do |ivar|
next if ivar == :@block
test_object.instance_variable_set(
ivar,
instance_variable_get(ivar)
)
end
end
end
class << self
def included(base)
base.extend ClassMethods
end
end
module ClassMethods
attr_accessor :before_all_executor
def before_all(&block)
self.before_all_executor = Executor.new(&block)
prepend(Module.new do
def setup
self.class.before_all_executor.activate!(self.class)
self.class.before_all_executor.restore_to(self)
super
end
def teardown
super
self.class.before_all_executor.try_deactivate!
end
end)
end
end
end
end
end
test-prof-0.10.2/lib/test_prof/recipes/minitest/sample.rb 000644 001751 001751 00000003042 13626445505 023703 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
module TestProf
# Add ability to run only a specified number of example groups (randomly selected)
module MinitestSample
# Do not add these classes to resulted sample
CORE_RUNNABLES = [
Minitest::Test,
Minitest::Unit::TestCase,
Minitest::Spec
].freeze
class << self
def suites
# Make sure that sample contains only _real_ suites
Minitest::Runnable.runnables
.reject { |suite| CORE_RUNNABLES.include?(suite) }
end
def sample_groups(sample_size)
saved_suites = suites
Minitest::Runnable.reset
saved_suites.sample(sample_size).each { |r| Minitest::Runnable.runnables << r }
end
def sample_examples(sample_size)
all_examples = suites.flat_map do |runnable|
runnable.runnable_methods.map { |method| [runnable, method] }
end
sample = all_examples.sample(sample_size)
# Filter examples by overriding #runnable_methods for all suites
suites.each do |runnable|
runnable.define_singleton_method(:runnable_methods) do
super() & sample.select { |ex| ex.first.equal?(runnable) }.map(&:last)
end
end
end
end
# Overrides Minitest.run
def run(*)
if ENV["SAMPLE"]
MinitestSample.sample_examples(ENV["SAMPLE"].to_i)
elsif ENV["SAMPLE_GROUPS"]
MinitestSample.sample_groups(ENV["SAMPLE_GROUPS"].to_i)
end
super
end
end
end
Minitest.singleton_class.prepend(TestProf::MinitestSample)
test-prof-0.10.2/lib/test_prof/recipes/active_record_one_love.rb 000644 001751 001751 00000000251 13626445505 025264 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
require_relative "./active_record_shared_connection"
# One ❤️
TestProf::ActiveRecordOneLove = TestProf::ActiveRecordSharedConnection
test-prof-0.10.2/lib/test_prof/recipes/rspec/ 000755 001751 001751 00000000000 13626445505 021356 5 ustar 00pravi pravi 000000 000000 test-prof-0.10.2/lib/test_prof/recipes/rspec/factory_all_stub.rb 000644 001751 001751 00000000537 13626445505 025244 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
require "test_prof/factory_all_stub"
TestProf::FactoryAllStub.init
RSpec.shared_context "factory:stub" do
prepend_before(:all) { TestProf::FactoryAllStub.enable! }
append_after(:all) { TestProf::FactoryAllStub.disable! }
end
RSpec.configure do |config|
config.include_context "factory:stub", factory: :stub
end
test-prof-0.10.2/lib/test_prof/recipes/rspec/before_all.rb 000644 001751 001751 00000001670 13626445505 024001 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
require "test_prof/before_all"
module TestProf
module BeforeAll
# Helper to wrap the whole example group into a transaction
module RSpec
def before_all(&block)
raise ArgumentError, "Block is required!" unless block_given?
return within_before_all(&block) if within_before_all?
@__before_all_activated__ = true
before(:all) do
BeforeAll.begin_transaction do
instance_eval(&block)
end
end
after(:all) do
BeforeAll.rollback_transaction
end
end
def within_before_all(&block)
before(:all) do
BeforeAll.within_transaction do
instance_eval(&block)
end
end
end
def within_before_all?
instance_variable_defined?(:@__before_all_activated__)
end
end
end
end
RSpec::Core::ExampleGroup.extend TestProf::BeforeAll::RSpec
test-prof-0.10.2/lib/test_prof/recipes/rspec/let_it_be.rb 000644 001751 001751 00000007637 13626445505 023646 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
require "test_prof"
require_relative "./before_all"
module TestProf
# Add `#map` to Object as an alias for `then` to use in modifiers
using(Module.new do
refine Object do
def map
yield self
end
end
end)
# Just like `let`, but persist the result for the whole group.
# NOTE: Experimental and magical, for more control use `before_all`.
module LetItBe
class Configuration
# Define an alias for `let_it_be` with the predefined options:
#
# TestProf::LetItBe.configure do |config|
# config.alias_to :let_it_be_reloaded, reload: true
# end
def alias_to(name, **default_args)
LetItBe.define_let_it_be_alias(name, **default_args)
end
def register_modifier(key, &block)
raise ArgumentError, "Modifier #{key} is already defined for let_it_be" if LetItBe.modifiers.key?(key)
LetItBe.modifiers[key] = block
end
end
class << self
def config
@config ||= Configuration.new
end
def configure
yield config
end
def modifiers
@modifiers ||= {}
end
def wrap_with_modifiers(mods, &block)
validate_modifiers! mods
return block if mods.empty?
-> {
instance_eval(&block).map do |record|
mods.inject(record) do |rec, (k, v)|
LetItBe.modifiers.fetch(k).call(rec, v)
end
end
}
end
def module_for(group)
modules[group] ||= begin
Module.new.tap { |mod| group.prepend(mod) }
end
end
private
def modules
@modules ||= {}
end
def validate_modifiers!(mods)
unknown = mods.keys - modifiers.keys
return if unknown.empty?
raise ArgumentError, "Unknown let_it_be modifiers were used: #{unknown.join(", ")}. " \
"Available modifiers are: #{modifiers.keys.join(", ")}"
end
end
# Use uniq prefix for instance variables to avoid collisions
# We want to use the power of Ruby's unicode support)
# And we love cats!)
PREFIX = RUBY_ENGINE == "jruby" ? "@__jruby_is_not_cat_friendly__" : "@😸"
def self.define_let_it_be_alias(name, **default_args)
define_method(name) do |identifier, **options, &blk|
let_it_be(identifier, **default_args.merge(options), &blk)
end
end
def let_it_be(identifier, **options, &block)
initializer = proc do
instance_variable_set(:"#{PREFIX}#{identifier}", instance_exec(&block))
end
if within_before_all?
within_before_all(&initializer)
else
before_all(&initializer)
end
define_let_it_be_methods(identifier, **options)
end
def define_let_it_be_methods(identifier, **modifiers)
let_accessor = LetItBe.wrap_with_modifiers(modifiers) do
instance_variable_get(:"#{PREFIX}#{identifier}")
end
LetItBe.module_for(self).module_eval do
define_method(identifier) do
# Trying to detect the context (couldn't find other way so far)
if /\(:context\)/.match?(@__inspect_output)
instance_variable_get(:"#{PREFIX}#{identifier}")
else
# Fallback to let definition
super()
end
end
end
let(identifier, &let_accessor)
end
end
end
if defined?(::ActiveRecord)
require "test_prof/ext/active_record_refind"
using TestProf::Ext::ActiveRecordRefind
TestProf::LetItBe.configure do |config|
config.register_modifier :reload do |record, val|
next record unless val
next record unless record.is_a?(::ActiveRecord::Base)
record.reload
end
config.register_modifier :refind do |record, val|
next record unless val
next record unless record.is_a?(::ActiveRecord::Base)
record.refind
end
end
end
RSpec::Core::ExampleGroup.extend TestProf::LetItBe
test-prof-0.10.2/lib/test_prof/recipes/rspec/sample.rb 000644 001751 001751 00000002131 13626445505 023161 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
module TestProf
# Instance variable writer for RSpec::Core::World
module RSpecWorldSamplePatch
def filtered_examples=(val)
@filtered_examples = val
end
end
end
if ENV["SAMPLE"]
RSpec::Core::World.include(TestProf::RSpecWorldSamplePatch)
RSpec.configure do |config|
config.before(:suite) do
filtered_examples = RSpec.world.filtered_examples.values.flatten
sample = filtered_examples.sample(ENV["SAMPLE"].to_i)
RSpec.world.filtered_examples = Hash.new do |hash, group|
hash[group] = group.examples & sample
end
end
end
end
if ENV["SAMPLE_GROUPS"]
RSpec::Core::World.include(TestProf::RSpecWorldSamplePatch)
RSpec.configure do |config|
config.before(:suite) do
filtered_groups = RSpec.world.filtered_examples.reject do |_group, examples|
examples.empty?
end.keys
sample = filtered_groups.sample(ENV["SAMPLE_GROUPS"].to_i)
RSpec.world.filtered_examples = Hash.new do |hash, group|
hash[group] = sample.include?(group) ? group.examples : []
end
end
end
end
test-prof-0.10.2/lib/test_prof/recipes/rspec/any_fixture.rb 000644 001751 001751 00000000760 13626445505 024243 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
require "test_prof/any_fixture"
require "test_prof/recipes/rspec/before_all"
RSpec.shared_context "any_fixture:clean" do
extend TestProf::BeforeAll::RSpec
before_all do
TestProf::AnyFixture.clean
end
end
RSpec.configure do |config|
config.include_context "any_fixture:clean", with_clean_fixture: true
config.after(:suite) do
TestProf::AnyFixture.report_stats if TestProf::AnyFixture.reporting_enabled?
TestProf::AnyFixture.reset
end
end
test-prof-0.10.2/lib/test_prof/recipes/rspec/factory_default.rb 000644 001751 001751 00000000274 13626445505 025061 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
require "test_prof/factory_default"
TestProf::FactoryDefault.init
RSpec.configure do |config|
config.after(:each) { TestProf::FactoryDefault.reset }
end
test-prof-0.10.2/lib/test_prof/recipes/active_record_shared_connection.rb 000644 001751 001751 00000003734 13626445505 027154 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
module TestProf
# Forces ActiveRecord to use the same connection between threads
module ActiveRecordSharedConnection # :nodoc: all
class << self
attr_reader :connection
def enable!
self.connection = ActiveRecord::Base.connection
end
def disable!
self.connection = nil
end
def ignore(&block)
raise ArgumentError, "Block is required" unless block_given?
@ignores ||= []
ignores << block
end
def ignored?(config)
!ignores.nil? && ignores.any? { |clbk| clbk.call(config) }
end
private
attr_reader :ignores
def connection=(conn)
@connection = conn
connection.singleton_class.prepend Connection
connection
end
end
module Connection
def shared_lock
@shared_lock ||= Mutex.new
end
def exec_cache(*)
shared_lock.synchronize { super }
end
def exec_no_cache(*)
shared_lock.synchronize { super }
end
def execute(*)
shared_lock.synchronize { super }
end
end
module Ext
def connection
return super if ActiveRecordSharedConnection.ignored?(connection_config)
ActiveRecordSharedConnection.connection || super
end
end
end
end
ActiveSupport.on_load(:active_record) do
if ::ActiveRecord::Base.connection.pool.respond_to?(:lock_thread=)
TestProf.log :warn, "You activated ActiveRecordSharedConnection patch for the Rails version,\n" \
"which has a built-in support for the same functionality.\n" \
"Consider removing it, 'cause this could result in unexpected behaviour.\n\n" \
"Read more in the docs: https://test-prof.evilmartians.io/#/active_record_shared_connection"
end
TestProf::ActiveRecordSharedConnection.enable!
ActiveRecord::Base.singleton_class.prepend TestProf::ActiveRecordSharedConnection::Ext
end
test-prof-0.10.2/lib/test_prof/utils/ 000755 001751 001751 00000000000 13626445505 017750 5 ustar 00pravi pravi 000000 000000 test-prof-0.10.2/lib/test_prof/utils/html_builder.rb 000644 001751 001751 00000000756 13626445505 022757 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
require "json"
module TestProf
module Utils
# Generates static HTML reports with injected data
module HTMLBuilder
class << self
def generate(data:, template:, output:)
template = File.read(TestProf.asset_path(template))
template.sub! "/**REPORT-DATA**/", data.to_json
outpath = TestProf.artifact_path(output)
File.write(outpath, template)
outpath
end
end
end
end
end
test-prof-0.10.2/lib/test_prof/utils/rspec.rb 000644 001751 001751 00000000572 13626445505 021415 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
unless "".respond_to?(:parameterize)
require "test_prof/ext/string_parameterize"
using TestProf::StringParameterize
end
module TestProf
module Utils
module RSpec
class << self
def example_to_filename(example)
::RSpec::Core::Metadata.id_from(example.metadata).parameterize
end
end
end
end
end
test-prof-0.10.2/lib/test_prof/utils/sized_ordered_set.rb 000644 001751 001751 00000002424 13626445505 023774 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
module TestProf
module Utils
# Ordered set with capacity
class SizedOrderedSet
unless [].respond_to?(:bsearch_index)
require "test_prof/ext/array_bsearch_index"
using ArrayBSearchIndex
end
include Enumerable
def initialize(max_size, sort_by: nil, &block)
@max_size = max_size
@comparator =
if block_given?
block
elsif !sort_by.nil?
->(x, y) { x[sort_by] >= y[sort_by] }
else
->(x, y) { x >= y }
end
@data = []
end
def <<(item)
return if data.size == max_size &&
comparator.call(data.last, item)
# Find an index of a smaller element
index = data.bsearch_index { |x| !comparator.call(x, item) }
if index.nil?
data << item
else
data.insert(index, item)
end
data.pop if data.size > max_size
data.size
end
def each(&block)
if block_given?
data.each(&block)
else
data.each
end
end
def size
data.size
end
def to_a
data.dup
end
private
attr_reader :max_size, :data, :comparator
end
end
end
test-prof-0.10.2/lib/test_prof/any_fixture/ 000755 001751 001751 00000000000 13626445505 021145 5 ustar 00pravi pravi 000000 000000 test-prof-0.10.2/lib/test_prof/any_fixture/dsl.rb 000644 001751 001751 00000001053 13626445505 022253 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
module TestProf
module AnyFixture
# Adds "global" `fixture` method (through refinement)
module DSL
# Refine object, 'cause refining modules (Kernel) is vulnerable to prepend:
# - https://bugs.ruby-lang.org/issues/13446
# - Rails added `Kernel.prepend` in 6.1: https://github.com/rails/rails/commit/3124007bd674dcdc9c3b5c6b2964dfb7a1a0733c
refine ::Object do
def fixture(id, &block)
::TestProf::AnyFixture.register(:"#{id}", &block)
end
end
end
end
end
test-prof-0.10.2/lib/test_prof/event_prof/ 000755 001751 001751 00000000000 13626445505 020757 5 ustar 00pravi pravi 000000 000000 test-prof-0.10.2/lib/test_prof/event_prof/rspec.rb 000644 001751 001751 00000010316 13626445505 022421 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
require "test_prof/ext/float_duration"
require "test_prof/ext/string_truncate"
module TestProf
module EventProf
class RSpecListener # :nodoc:
include Logging
using FloatDuration
using StringTruncate
NOTIFICATIONS = %i[
example_group_started
example_group_finished
example_started
example_finished
].freeze
def initialize
@profiler = EventProf.build
log :info, "EventProf enabled (#{@profiler.events.join(", ")})"
end
def example_group_started(notification)
return unless notification.group.top_level?
@profiler.group_started notification.group
end
def example_group_finished(notification)
return unless notification.group.top_level?
@profiler.group_finished notification.group
end
def example_started(notification)
@profiler.example_started notification.example
end
def example_finished(notification)
@profiler.example_finished notification.example
end
def print
@profiler.each(&method(:report))
end
def report(profiler)
result = profiler.results
time_percentage = time_percentage(profiler.total_time, profiler.absolute_run_time)
msgs = []
msgs <<
<<~MSG
EventProf results for #{profiler.event}
Total time: #{profiler.total_time.duration} of #{profiler.absolute_run_time.duration} (#{time_percentage}%)
Total events: #{profiler.total_count}
Top #{profiler.top_count} slowest suites (by #{profiler.rank_by}):
MSG
result[:groups].each do |group|
description = group[:id].top_level_description
location = group[:id].metadata[:location]
time = group[:time]
run_time = group[:run_time]
time_percentage = time_percentage(time, run_time)
msgs <<
<<~GROUP
#{description.truncate} (#{location}) – #{time.duration} (#{group[:count]} / #{group[:examples]}) of #{run_time.duration} (#{time_percentage}%)
GROUP
end
if result[:examples]
msgs << "\nTop #{profiler.top_count} slowest tests (by #{profiler.rank_by}):\n\n"
result[:examples].each do |example|
description = example[:id].description
location = example[:id].metadata[:location]
time = example[:time]
run_time = example[:run_time]
time_percentage = time_percentage(time, run_time)
msgs <<
<<~GROUP
#{description.truncate} (#{location}) – #{time.duration} (#{example[:count]}) of #{run_time.duration} (#{time_percentage}%)
GROUP
end
end
log :info, msgs.join
stamp!(profiler) if EventProf.config.stamp?
end
def stamp!(profiler)
result = profiler.results
stamper = RSpecStamp::Stamper.new
examples = Hash.new { |h, k| h[k] = [] }
(result[:groups].to_a + result.fetch(:examples, []).to_a)
.map { |obj| obj[:id].metadata[:location] }.each do |location|
file, line = location.split(":")
examples[file] << line.to_i
end
examples.each do |file, lines|
stamper.stamp_file(file, lines.uniq)
end
msgs = []
msgs <<
<<~MSG
RSpec Stamp results
Total patches: #{stamper.total}
Total files: #{examples.keys.size}
Failed patches: #{stamper.failed}
Ignored files: #{stamper.ignored}
MSG
log :info, msgs.join
end
def time_percentage(time, total_time)
(time / total_time * 100).round(2)
end
end
end
end
# Register EventProf listener
TestProf.activate("EVENT_PROF") do
TestProf::EventProf::CustomEvents.activate_all(ENV["EVENT_PROF"])
RSpec.configure do |config|
listener = nil
config.before(:suite) do
listener = TestProf::EventProf::RSpecListener.new
config.reporter.register_listener(
listener, *TestProf::EventProf::RSpecListener::NOTIFICATIONS
)
end
config.after(:suite) { listener&.print }
end
end
test-prof-0.10.2/lib/test_prof/event_prof/profiler.rb 000644 001751 001751 00000006547 13626445505 023142 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
module TestProf
module EventProf
class Profiler # :nodoc:
attr_reader :event, :total_count, :total_time, :rank_by, :top_count, :per_example,
:time, :count, :example_time, :example_count, :absolute_run_time
alias per_example? per_example
def initialize(event:, instrumenter:, rank_by: :time, top_count: 5, per_example: false)
@event = event
@rank_by = rank_by
@top_count = top_count
@per_example = per_example
instrumenter.subscribe(event) { |time| track(time) }
@groups = Utils::SizedOrderedSet.new(
top_count, sort_by: rank_by
)
@examples = Utils::SizedOrderedSet.new(
top_count, sort_by: rank_by
)
@total_count = 0
@total_time = 0.0
@absolute_run_time = 0.0
end
def track(time)
return if @current_group.nil?
@total_time += time
@total_count += 1
@time += time
@count += 1
return if @current_example.nil?
@example_time += time
@example_count += 1
end
def group_started(id)
reset_group!
@current_group = id
end
def group_finished(id)
group_run_time = take_time(@group_ts)
@absolute_run_time += group_run_time
data = {
id: id,
time: @time,
run_time: group_run_time,
count: @count,
examples: @total_examples
}
@groups << data unless data[rank_by].zero?
@current_group = nil
end
def example_started(id)
return unless per_example?
reset_example!
@current_example = id
end
def example_finished(id)
@total_examples += 1
return unless per_example?
example_run_time = take_time(@example_ts)
data = {
id: id,
time: @example_time,
run_time: example_run_time,
count: @example_count
}
@examples << data unless data[rank_by].zero?
@current_example = nil
end
def results
results = {
groups: @groups.to_a
}.tap do |data|
next unless per_example?
data[:examples] = @examples.to_a
end
results
end
def take_time(start_ts)
TestProf.now - start_ts
end
private
def reset_group!
@time = 0.0
@count = 0
@total_examples = 0
@group_ts = TestProf.now
end
def reset_example!
@example_count = 0
@example_time = 0.0
@example_ts = TestProf.now
end
end
# Multiple profilers wrapper
class ProfilersGroup
attr_reader :profilers
def initialize(event:, **options)
events = event.split(",")
@profilers = events.map do |ev|
Profiler.new(event: ev, **options)
end
end
def each(&block)
if block_given?
@profilers.each(&block)
else
@profilers.each
end
end
def events
@profilers.map(&:event)
end
%i[group_started group_finished example_started example_finished].each do |name|
class_eval <<-CODE, __FILE__, __LINE__ + 1
def #{name}(id)
@profilers.each { |profiler| profiler.#{name}(id) }
end
CODE
end
end
end
end
test-prof-0.10.2/lib/test_prof/event_prof/custom_events/ 000755 001751 001751 00000000000 13626445505 023655 5 ustar 00pravi pravi 000000 000000 test-prof-0.10.2/lib/test_prof/event_prof/custom_events/factory_create.rb 000644 001751 001751 00000001614 13626445505 027176 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
require "test_prof/factory_bot"
require "test_prof/ext/factory_bot_strategy"
using TestProf::FactoryBotStrategy
TestProf::EventProf::CustomEvents.register("factory.create") do
if defined?(TestProf::FactoryBot) || defined?(Fabricate)
if defined?(TestProf::FactoryBot)
TestProf::EventProf.monitor(
TestProf::FactoryBot::FactoryRunner,
"factory.create",
:run,
top_level: true,
guard: ->(strategy = @strategy) { strategy.create? }
)
end
if defined?(Fabricate)
TestProf::EventProf.monitor(
Fabricate.singleton_class,
"factory.create",
:create,
top_level: true
)
end
else
TestProf.log(
:error,
<<~MSG
Failed to load factory_bot / factory_girl / fabrication.
Make sure that any of them is in your Gemfile.
MSG
)
end
end
test-prof-0.10.2/lib/test_prof/event_prof/custom_events/sidekiq_inline.rb 000644 001751 001751 00000000662 13626445505 027175 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
TestProf::EventProf::CustomEvents.register("sidekiq.inline") do
if TestProf.require(
"sidekiq/testing",
<<~MSG
Failed to load Sidekiq.
Make sure that "sidekiq" gem is in your Gemfile.
MSG
)
TestProf::EventProf.monitor(
Sidekiq::Client,
"sidekiq.inline",
:raw_push,
top_level: true,
guard: ->(*) { Sidekiq::Testing.inline? }
)
end
end
test-prof-0.10.2/lib/test_prof/event_prof/custom_events/sidekiq_jobs.rb 000644 001751 001751 00000000753 13626445505 026655 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
TestProf::EventProf::CustomEvents.register("sidekiq.jobs") do
if TestProf.require(
"sidekiq/testing",
<<~MSG
Failed to load Sidekiq.
Make sure that "sidekiq" gem is in your Gemfile.
MSG
)
TestProf::EventProf.monitor(
Sidekiq::Client,
"sidekiq.jobs",
:raw_push,
guard: ->(*) { Sidekiq::Testing.inline? }
)
TestProf::EventProf.configure do |config|
config.rank_by = :count
end
end
end
test-prof-0.10.2/lib/test_prof/event_prof/minitest.rb 000644 001751 001751 00000004147 13626445505 023146 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
require "minitest/base_reporter"
require "minitest/event_prof_formatter"
module Minitest
module TestProf
class EventProfReporter < BaseReporter # :nodoc:
def initialize(io = $stdout, options = {})
super
@profiler = configure_profiler(options)
log :info, "EventProf enabled (#{@profiler.events.join(", ")})"
@formatter = EventProfFormatter.new(@profiler)
@current_group = nil
@current_example = nil
end
def prerecord(group, example)
change_current_group(group, example) unless @current_group
track_current_example(group, example)
end
def before_test(test)
prerecord(test.class, test.name)
end
def record(*)
@profiler.example_finished(@current_example)
end
def report
@profiler.group_finished(@current_group)
result = @formatter.prepare_results
log :info, result
end
private
def track_current_example(group, example)
unless @current_group[:name] == group.name
@profiler.group_finished(@current_group)
change_current_group(group, example)
end
@current_example = {
name: example.gsub(/^test_(?:\d+_)?/, ""),
location: location_with_line_number(group, example)
}
@profiler.example_started(@current_example)
end
def change_current_group(group, example)
@current_group = {
name: group.name,
location: location_without_line_number(group, example)
}
@profiler.group_started(@current_group)
end
def configure_profiler(options)
::TestProf::EventProf.configure do |config|
config.event = options[:event]
config.rank_by = options[:rank_by] if options[:rank_by]
config.top_count = options[:top_count] if options[:top_count]
config.per_example = options[:per_example] if options[:per_example]
::TestProf::EventProf::CustomEvents.activate_all config.event
end
::TestProf::EventProf.build
end
end
end
end
test-prof-0.10.2/lib/test_prof/event_prof/custom_events.rb 000644 001751 001751 00000001614 13626445505 024204 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
module TestProf
module EventProf
# Registers and activates custom events (which require patches).
module CustomEvents
class << self
def register(event, &block)
raise ArgumentError, "Block is required!" unless block_given?
registrations[event] = block
end
def activate_all(events)
events = events.split(",")
events.each { |event| try_activate(event) }
end
def try_activate(event)
return unless registrations.key?(event)
registrations.delete(event).call
end
private
def registrations
@registrations ||= {}
end
end
end
end
end
require "test_prof/event_prof/custom_events/factory_create"
require "test_prof/event_prof/custom_events/sidekiq_inline"
require "test_prof/event_prof/custom_events/sidekiq_jobs"
test-prof-0.10.2/lib/test_prof/event_prof/instrumentations/ 000755 001751 001751 00000000000 13626445505 024405 5 ustar 00pravi pravi 000000 000000 test-prof-0.10.2/lib/test_prof/event_prof/instrumentations/active_support.rb 000644 001751 001751 00000001075 13626445505 030004 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
module TestProf::EventProf
module Instrumentations
# Wrapper over ActiveSupport::Notifications
module ActiveSupport
class << self
def subscribe(event)
raise ArgumentError, "Block is required!" unless block_given?
::ActiveSupport::Notifications.subscribe(event) do |_event, start, finish, *_args|
yield (finish - start)
end
end
def instrument(event)
::ActiveSupport::Notifications.instrument(event) { yield }
end
end
end
end
end
test-prof-0.10.2/lib/test_prof/event_prof/monitor.rb 000644 001751 001751 00000002660 13626445505 022777 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
module TestProf
module EventProf
# Wrap methods with instrumentation
module Monitor
class BaseTracker
attr_reader :event
def initialize(event)
@event = event
end
def track
TestProf::EventProf.instrumenter.instrument(event) { yield }
end
end
class TopLevelTracker < BaseTracker
attr_reader :id
def initialize(event)
super
@id = :"event_prof_monitor_#{event}"
Thread.current[id] = 0
end
def track
Thread.current[id] += 1
res = nil
begin
res =
if Thread.current[id] == 1
super { yield }
else
yield
end
ensure
Thread.current[id] -= 1
end
res
end
end
class << self
def call(mod, event, *mids, guard: nil, top_level: false)
tracker = top_level ? TopLevelTracker.new(event) : BaseTracker.new(event)
patch = Module.new do
mids.each do |mid|
define_method(mid) do |*args, &block|
next super(*args, &block) unless guard.nil? || instance_exec(*args, &guard)
tracker.track { super(*args, &block) }
end
end
end
mod.prepend(patch)
end
end
end
end
end
test-prof-0.10.2/lib/test_prof/rspec_dissect/ 000755 001751 001751 00000000000 13626445505 021442 5 ustar 00pravi pravi 000000 000000 test-prof-0.10.2/lib/test_prof/rspec_dissect/rspec.rb 000644 001751 001751 00000006123 13626445505 023105 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
require "test_prof/ext/float_duration"
module TestProf
module RSpecDissect
class Listener # :nodoc:
include Logging
using FloatDuration
NOTIFICATIONS = %i[
example_group_finished
example_finished
].freeze
def initialize
@collectors = []
if RSpecDissect.config.let?
collectors << Collectors::Let.new(top_count: RSpecDissect.config.top_count)
end
if RSpecDissect.config.before?
collectors << Collectors::Before.new(top_count: RSpecDissect.config.top_count)
end
@examples_count = 0
@examples_time = 0.0
@total_examples_time = 0.0
end
def example_finished(notification)
@examples_count += 1
@examples_time += notification.example.execution_result.run_time
end
def example_group_finished(notification)
return unless notification.group.top_level?
data = {}
data[:total] = @examples_time
data[:count] = @examples_count
data[:desc] = notification.group.top_level_description
data[:loc] = notification.group.metadata[:location]
collectors.each { |c| c.populate!(data) }
collectors.each { |c| c << data }
@total_examples_time += @examples_time
@examples_count = 0
@examples_time = 0.0
RSpecDissect.reset!
end
def print
msgs = []
msgs <<
<<~MSG
RSpecDissect report
Total time: #{@total_examples_time.duration}
MSG
collectors.each do |c|
msgs << c.total_time_message
end
msgs << "\n"
collectors.each do |c|
msgs << c.print_results
end
log :info, msgs.join
stamp! if RSpecDissect.config.stamp?
end
def stamp!
stamper = RSpecStamp::Stamper.new
examples = Hash.new { |h, k| h[k] = [] }
all_results = collectors.inject([]) { |acc, c| acc + c.results.to_a }
all_results
.map { |obj| obj[:loc] }.each do |location|
file, line = location.split(":")
examples[file] << line.to_i
end
examples.each do |file, lines|
stamper.stamp_file(file, lines.uniq)
end
msgs = []
msgs <<
<<~MSG
RSpec Stamp results
Total patches: #{stamper.total}
Total files: #{examples.keys.size}
Failed patches: #{stamper.failed}
Ignored files: #{stamper.ignored}
MSG
log :info, msgs.join
end
private
attr_reader :collectors
def top_count
RSpecDissect.config.top_count
end
end
end
end
# Register RSpecDissect listener
TestProf.activate("RD_PROF") do
RSpec.configure do |config|
listener = nil
config.before(:suite) do
listener = TestProf::RSpecDissect::Listener.new
config.reporter.register_listener(
listener, *TestProf::RSpecDissect::Listener::NOTIFICATIONS
)
end
config.after(:suite) { listener&.print }
end
end
test-prof-0.10.2/lib/test_prof/rspec_dissect/collectors/ 000755 001751 001751 00000000000 13626445505 023613 5 ustar 00pravi pravi 000000 000000 test-prof-0.10.2/lib/test_prof/rspec_dissect/collectors/before.rb 000644 001751 001751 00000000514 13626445505 025402 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
require_relative "./base"
module TestProf
module RSpecDissect
module Collectors # :nodoc: all
class Before < Base
def initialize(params)
super(name: :before, **params)
end
def print_name
"before(:each)"
end
end
end
end
end
test-prof-0.10.2/lib/test_prof/rspec_dissect/collectors/base.rb 000644 001751 001751 00000002770 13626445505 025060 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
require "test_prof/utils/sized_ordered_set"
require "test_prof/ext/float_duration"
require "test_prof/ext/string_truncate"
module TestProf # :nodoc: all
using FloatDuration
using StringTruncate
module RSpecDissect
module Collectors
class Base
attr_reader :results, :name, :top_count
def initialize(name:, top_count:)
@name = name
@top_count = top_count
@results = Utils::SizedOrderedSet.new(
top_count, sort_by: name
)
end
def populate!(data)
data[name] = RSpecDissect.time_for(name)
end
def <<(data)
results << data
end
def total_time
RSpecDissect.total_time_for(name)
end
def total_time_message
"\nTotal `#{print_name}` time: #{total_time.duration}"
end
def print_name
name
end
def print_result_header
<<~MSG
Top #{top_count} slowest suites (by `#{print_name}` time):
MSG
end
def print_group_result(group)
<<~GROUP
#{group[:desc].truncate} (#{group[:loc]}) – #{group[name].duration} of #{group[:total].duration} (#{group[:count]})
GROUP
end
def print_results
msgs = [print_result_header]
results.each do |group|
msgs << print_group_result(group)
end
msgs.join
end
end
end
end
end
test-prof-0.10.2/lib/test_prof/rspec_dissect/collectors/let.rb 000644 001751 001751 00000001663 13626445505 024732 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
require_relative "./base"
module TestProf
module RSpecDissect
module Collectors # :nodoc: all
class Let < Base
def initialize(params)
super(name: :let, **params)
end
def populate!(data)
super
data[:let_calls] = RSpecDissect.meta_for(name)
end
def print_results
return unless RSpecDissect.memoization_available?
super
end
def print_group_result(group)
return super unless RSpecDissect.config.let_stats_enabled?
msgs = [super]
group[:let_calls]
.group_by(&:itself)
.map { |id, calls| [id, -calls.size] }
.sort_by(&:last)
.take(RSpecDissect.config.let_top_count)
.each do |(id, size)|
msgs << " ↳ #{id} – #{-size}\n"
end
msgs.join
end
end
end
end
end
test-prof-0.10.2/lib/test_prof/logging.rb 000644 001751 001751 00000001041 13626445505 020557 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
module TestProf
# Helper for output printing
module Logging
COLORS = {
info: "\e[34m", # blue
warn: "\e[33m", # yellow
error: "\e[31m" # red
}.freeze
def log(level, msg)
TestProf.config.output.puts(build_log_msg(level, msg))
end
def build_log_msg(level, msg)
colorize(level, "[TEST PROF #{level.to_s.upcase}] #{msg}")
end
def colorize(level, msg)
return msg unless TestProf.config.color?
"#{COLORS[level]}#{msg}\e[0m"
end
end
end
test-prof-0.10.2/lib/test_prof/factory_prof/ 000755 001751 001751 00000000000 13626445505 021305 5 ustar 00pravi pravi 000000 000000 test-prof-0.10.2/lib/test_prof/factory_prof/fabrication_patch.rb 000644 001751 001751 00000000417 13626445505 025274 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
module TestProf
module FactoryProf
# Wrap #run method with FactoryProf tracking
module FabricationPatch
def create(name, overrides = {})
FactoryBuilders::Fabrication.track(name) { super }
end
end
end
end
test-prof-0.10.2/lib/test_prof/factory_prof/printers/ 000755 001751 001751 00000000000 13626445505 023153 5 ustar 00pravi pravi 000000 000000 test-prof-0.10.2/lib/test_prof/factory_prof/printers/flamegraph.rb 000644 001751 001751 00000003252 13626445505 025610 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
require "test_prof/utils/html_builder"
module TestProf::FactoryProf
module Printers
module Flamegraph # :nodoc: all
TEMPLATE = "flamegraph.template.html"
OUTPUT_NAME = "factory-flame.html"
class << self
include TestProf::Logging
def dump(result)
return log(:info, "No factories detected") if result.raw_stats == {}
report_data = {
total_stacks: result.stacks.size,
total: result.total_count
}
report_data[:roots] = convert_stacks(result)
path = TestProf::Utils::HTMLBuilder.generate(
data: report_data,
template: TEMPLATE,
output: OUTPUT_NAME
)
log :info, "FactoryFlame report generated: #{path}"
end
def convert_stacks(result)
res = []
paths = {}
result.stacks.each do |stack|
parent = nil
path = ""
stack.each do |sample|
path = "#{path}/#{sample}"
if paths[path]
node = paths[path]
node[:value] += 1
else
node = {
name: sample,
value: 1,
total: result.raw_stats.fetch(sample)[:total_count]
}
paths[path] = node
if parent.nil?
res << node
else
parent[:children] ||= []
parent[:children] << node
end
end
parent = node
end
end
res
end
end
end
end
end
test-prof-0.10.2/lib/test_prof/factory_prof/printers/simple.rb 000644 001751 001751 00000002557 13626445505 025002 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
module TestProf::FactoryProf
module Printers
module Simple # :nodoc: all
class << self
include TestProf::Logging
def dump(result)
return log(:info, "No factories detected") if result.raw_stats == {}
msgs = []
total_count = result.stats.sum { |stat| stat[:total_count] }
total_top_level_count = result.stats.sum { |stat| stat[:top_level_count] }
total_time = result.stats.sum { |stat| stat[:top_level_time] }
total_uniq_factories = result.stats.map { |stat| stat[:name] }.uniq.count
msgs <<
<<~MSG
Factories usage
Total: #{total_count}
Total top-level: #{total_top_level_count}
Total time: #{format("%.4f", total_time)}s
Total uniq factories: #{total_uniq_factories}
total top-level total time time per call top-level time name
MSG
result.stats.each do |stat|
time_per_call = stat[:total_time] / stat[:total_count]
msgs << format("%8d %11d %13.4fs %17.4fs %18.4fs %18s", stat[:total_count], stat[:top_level_count], stat[:total_time], time_per_call, stat[:top_level_time], stat[:name])
end
log :info, msgs.join("\n")
end
end
end
end
end
test-prof-0.10.2/lib/test_prof/factory_prof/factory_builders/ 000755 001751 001751 00000000000 13626445505 024645 5 ustar 00pravi pravi 000000 000000 test-prof-0.10.2/lib/test_prof/factory_prof/factory_builders/factory_bot.rb 000644 001751 001751 00000001430 13626445505 027503 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
require "test_prof/factory_prof/factory_bot_patch"
require "test_prof/factory_bot"
require "test_prof/ext/factory_bot_strategy"
module TestProf
module FactoryProf
module FactoryBuilders
# implementation of #patch and #track methods
# to provide unified interface for all factory-building gems
class FactoryBot
using TestProf::FactoryBotStrategy
# Monkey-patch FactoryBot / FactoryGirl
def self.patch
TestProf::FactoryBot::FactoryRunner.prepend(FactoryBotPatch) if
defined? TestProf::FactoryBot
end
def self.track(strategy, factory, &block)
return yield unless strategy.create?
FactoryProf.track(factory, &block)
end
end
end
end
end
test-prof-0.10.2/lib/test_prof/factory_prof/factory_builders/fabrication.rb 000644 001751 001751 00000001142 13626445505 027451 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
require "test_prof/factory_prof/fabrication_patch"
module TestProf
module FactoryProf
module FactoryBuilders
# implementation of #patch and #track methods
# to provide unified interface for all factory-building gems
class Fabrication
# Monkey-patch Fabrication
def self.patch
TestProf.require "fabrication" do
::Fabricate.singleton_class.prepend(FabricationPatch)
end
end
def self.track(factory, &block)
FactoryProf.track(factory, &block)
end
end
end
end
end
test-prof-0.10.2/lib/test_prof/factory_prof/factory_bot_patch.rb 000644 001751 001751 00000000425 13626445505 025325 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
module TestProf
module FactoryProf
# Wrap #run method with FactoryProf tracking
module FactoryBotPatch
def run(strategy = @strategy)
FactoryBuilders::FactoryBot.track(strategy, @name) { super }
end
end
end
end
test-prof-0.10.2/lib/test_prof/version.rb 000644 001751 001751 00000000110 13626445505 020612 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
module TestProf
VERSION = "0.10.2"
end
test-prof-0.10.2/lib/test_prof/factory_doctor.rb 000644 001751 001751 00000007143 13626445505 022163 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
require "test_prof/factory_bot"
require "test_prof/factory_doctor/factory_bot_patch"
require "test_prof/factory_doctor/fabrication_patch"
module TestProf
# FactoryDoctor is a tool that helps you identify
# tests that perform unnecessary database queries.
module FactoryDoctor
class Result # :nodoc:
attr_reader :count, :time, :queries_count
def initialize(count, time, queries_count)
@count = count
@time = time
@queries_count = queries_count
end
def bad?
count > 0 && queries_count.zero? && time >= FactoryDoctor.config.threshold
end
end
IGNORED_QUERIES_PATTERN = %r{(
pg_table|
pg_attribute|
pg_namespace|
show\stables|
pragma|
sqlite_master/rollback|
\ATRUNCATE TABLE|
\AALTER TABLE|
\ABEGIN|
\ACOMMIT|
\AROLLBACK|
\ARELEASE|
\ASAVEPOINT
)}xi.freeze
class Configuration
attr_accessor :event, :threshold
def initialize
# event to track for DB interactions
@event = ENV.fetch("FDOC_EVENT", "sql.active_record")
# consider result good if time wasted less then threshold
@threshold = ENV.fetch("FDOC_THRESHOLD", "0.01").to_f
end
end
class << self
include TestProf::Logging
attr_reader :count, :time, :queries_count
def config
@config ||= Configuration.new
end
def configure
yield config
end
# Patch factory lib, init counters
def init
reset!
@running = false
log :info, "FactoryDoctor enabled (event: \"#{config.event}\", threshold: #{config.threshold})"
# Monkey-patch FactoryBot / FactoryGirl
TestProf::FactoryBot::FactoryRunner.prepend(FactoryBotPatch) if
defined?(TestProf::FactoryBot)
# Monkey-patch Fabrication
::Fabricate.singleton_class.prepend(FabricationPatch) if
defined?(::Fabricate)
subscribe!
@stamp = ENV["FDOC_STAMP"]
RSpecStamp.config.tags = @stamp if stamp?
end
def stamp?
!@stamp.nil?
end
def start
reset!
@running = true
end
def stop
@running = false
end
def result
Result.new(count, time, queries_count)
end
# Do not analyze code within the block
def ignore
@ignored = true
res = yield
ensure
@ignored = false
res
end
def ignore!
@ignored = true
end
def ignore?
@ignored == true
end
def within_factory(strategy)
return yield if ignore? || !running? || (strategy != :create)
begin
ts = TestProf.now if @depth.zero?
@depth += 1
@count += 1
yield
ensure
@depth -= 1
@time += (TestProf.now - ts) if @depth.zero?
end
end
private
def reset!
@depth = 0
@time = 0.0
@count = 0
@queries_count = 0
@ignored = false
end
def subscribe!
::ActiveSupport::Notifications.subscribe(config.event) do |_name, _start, _finish, _id, query|
next if ignore? || !running? || within_factory?
next if IGNORED_QUERIES_PATTERN.match?(query[:sql])
@queries_count += 1
end
end
def within_factory?
@depth > 0
end
def running?
@running == true
end
end
end
end
require "test_prof/factory_doctor/rspec" if TestProf.rspec?
require "test_prof/factory_doctor/minitest" if TestProf.minitest?
test-prof-0.10.2/lib/test_prof/rspec_stamp.rb 000644 001751 001751 00000010402 13626445505 021452 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
require "test_prof/logging"
require "test_prof/rspec_stamp/parser"
module TestProf
# Mark RSpec examples with provided tags
module RSpecStamp
EXAMPLE_RXP = /(\s*)(\w+\s*(?:.*)\s*)(do|{)/.freeze
# RSpecStamp configuration
class Configuration
attr_reader :tags
attr_accessor :ignore_files, :dry_run
def initialize
@ignore_files = [%r{spec/support}]
@dry_run = ENV["RSTAMP_DRY_RUN"] == "1"
self.tags = ENV["RSTAMP"]
end
def dry_run?
@dry_run == true
end
def tags=(val)
@tags = if val.is_a?(String)
parse_tags(val)
else
val
end
end
private
def parse_tags(str)
str.split(/\s*,\s*/).each_with_object([]) do |tag, acc|
k, v = tag.split(":")
acc << if v.nil?
k.to_sym
else
Hash[k.to_sym, v.to_sym]
end
end
end
end
# Stamper collects statistics about applying tags
# to examples.
class Stamper
include TestProf::Logging
attr_reader :total, :failed, :ignored
def initialize
@total = 0
@failed = 0
@ignored = 0
end
def stamp_file(file, lines)
@total += lines.size
return if ignored?(file)
log :info, "(dry-run) Patching #{file}" if dry_run?
code = File.readlines(file)
@failed += RSpecStamp.apply_tags(code, lines, RSpecStamp.config.tags)
File.write(file, code.join) unless dry_run?
end
private
def ignored?(file)
ignored = RSpecStamp.config.ignore_files.find do |pattern|
file =~ pattern
end
return unless ignored
log :warn, "Ignore stamping file: #{file}"
@ignored += 1
end
def dry_run?
RSpecStamp.config.dry_run?
end
end
class << self
include TestProf::Logging
def config
@config ||= Configuration.new
end
def configure
yield config
end
# Accepts source code (as array of lines),
# line numbers (of example to apply tags)
# and an array of tags.
def apply_tags(code, lines, tags)
failed = 0
lines.each do |line|
unless stamp_example(code[line - 1], tags)
failed += 1
log :warn, "Failed to stamp: #{code[line - 1]}"
end
end
failed
end
private
# rubocop: disable Metrics/CyclomaticComplexity
# rubocop: disable Metrics/PerceivedComplexity
def stamp_example(example, tags)
matches = example.match(EXAMPLE_RXP)
return false unless matches
code = matches[2]
block = matches[3]
parsed = Parser.parse(code)
return false unless parsed
desc = parsed.desc_const || quote(parsed.desc || "works")
tags.each do |t|
if t.is_a?(Hash)
t.each_key do |k|
parsed.remove_tag(k)
parsed.add_htag(k, t[k])
end
else
parsed.remove_tag(t)
parsed.add_tag(t)
end
end
need_parens = block == "{"
tags_str = parsed.tags.map { |t| t.is_a?(Symbol) ? ":#{t}" : t }.join(", ") unless
parsed.tags.nil? || parsed.tags.empty?
unless parsed.htags.nil? || parsed.htags.empty?
htags_str = parsed.htags.map do |(k, v)|
vstr = v.is_a?(Symbol) ? ":#{v}" : quote(v)
"#{k}: #{vstr}"
end
end
replacement = "\\1#{parsed.fname}#{need_parens ? "(" : " "}"\
"#{[desc, tags_str, htags_str].compact.join(", ")}"\
"#{need_parens ? ") " : " "}\\3"
if config.dry_run?
log :info, "Patched: #{example.sub(EXAMPLE_RXP, replacement)}"
else
example.sub!(EXAMPLE_RXP, replacement)
end
true
end
# rubocop: enable Metrics/CyclomaticComplexity
# rubocop: enable Metrics/PerceivedComplexity
def quote(str)
return str unless str.is_a?(String)
if str.include?("'")
"\"#{str}\""
else
"'#{str}'"
end
end
end
end
end
require "test_prof/rspec_stamp/rspec" if TestProf.rspec?
test-prof-0.10.2/lib/test_prof/event_prof.rb 000644 001751 001751 00000005750 13626445505 021313 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
require "test_prof/rspec_stamp"
require "test_prof/event_prof/profiler"
require "test_prof/event_prof/instrumentations/active_support"
require "test_prof/event_prof/monitor"
require "test_prof/utils/sized_ordered_set"
module TestProf
# EventProf profiles your tests and suites against custom events,
# such as ActiveSupport::Notifacations.
#
# It works very similar to `rspec --profile` but can track arbitrary events.
#
# Example:
#
# # Collect SQL queries stats for every suite and example
# EVENT_PROF='sql.active_record' rspec ...
#
# By default it collects information only about top-level groups (aka suites),
# but you can also profile individual examples. Just set the configuration option:
#
# TestProf::EventProf.configure do |config|
# config.per_example = true
# end
#
# Or provide the EVENT_PROF_EXAMPLES=1 env variable.
module EventProf
# EventProf configuration
class Configuration
# Map of supported instrumenters
INSTRUMENTERS = {
active_support: "ActiveSupport"
}.freeze
attr_accessor :instrumenter, :top_count, :per_example,
:rank_by, :event
def initialize
@event = ENV["EVENT_PROF"]
@instrumenter = :active_support
@top_count = (ENV["EVENT_PROF_TOP"] || 5).to_i
@per_example = ENV["EVENT_PROF_EXAMPLES"] == "1"
@rank_by = (ENV["EVENT_PROF_RANK"] || :time).to_sym
@stamp = ENV["EVENT_PROF_STAMP"]
RSpecStamp.config.tags = @stamp if stamp?
end
def stamp?
!@stamp.nil?
end
def per_example?
per_example == true
end
def resolve_instrumenter
return instrumenter if instrumenter.is_a?(Module)
raise ArgumentError, "Unknown instrumenter: #{instrumenter}" unless
INSTRUMENTERS.key?(instrumenter)
Instrumentations.const_get(INSTRUMENTERS[instrumenter])
end
end
class << self
def config
@config ||= Configuration.new
end
def configure
yield config
end
# Returns new configured instance of profilers group
def build(event = config.event)
ProfilersGroup.new(
event: event,
instrumenter: instrumenter,
rank_by: config.rank_by,
top_count: config.top_count,
per_example: config.per_example?
)
end
def instrumenter
@instrumenter ||= config.resolve_instrumenter
end
# Instrument specified module methods.
# Wraps them with `instrumenter.instrument(event) { ... }`.
#
# Use it to profile arbitrary methods:
#
# TestProf::EventProf.monitor(MyModule, "my_module.call", :call)
def monitor(mod, event, *mids, **kwargs)
Monitor.call(mod, event, *mids, **kwargs)
end
end
end
end
require "test_prof/event_prof/custom_events"
require "test_prof/event_prof/rspec" if TestProf.rspec?
require "test_prof/event_prof/minitest" if TestProf.minitest?
test-prof-0.10.2/lib/test_prof/utils.rb 000644 001751 001751 00000001354 13626445505 020300 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
module TestProf
module Utils # :nodoc:
class << self
# Verify that loaded gem has correct version
def verify_gem_version(gem_name, at_least: nil, at_most: nil)
raise ArgumentError, "Please, provide `at_least` or `at_most` argument" if
at_least.nil? && at_most.nil?
spec = Gem.loaded_specs[gem_name]
version = spec.version if spec
return false if version.nil?
supported_version?(version, at_least, at_most)
end
def supported_version?(gem_version, at_least, at_most)
(at_least.nil? || Gem::Version.new(at_least) <= gem_version) &&
(at_most.nil? || Gem::Version.new(at_most) >= gem_version)
end
end
end
end
test-prof-0.10.2/lib/test_prof/factory_bot.rb 000644 001751 001751 00000000461 13626445505 021451 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
module TestProf # :nodoc: all
FACTORY_GIRL_NAMES = {"factory_bot" => "::FactoryBot", "factory_girl" => "::FactoryGirl"}.freeze
FACTORY_GIRL_NAMES.find do |name, cname|
TestProf.require(name) do
TestProf::FactoryBot = Object.const_get(cname)
end
end
end
test-prof-0.10.2/lib/test_prof/ruby_prof/ 000755 001751 001751 00000000000 13626445505 020617 5 ustar 00pravi pravi 000000 000000 test-prof-0.10.2/lib/test_prof/ruby_prof/rspec_exclusions.rb 000644 001751 001751 00000004255 13626445505 024542 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
module TestProf
module RubyProf
# Generates the list of RSpec (framework internal) methods
# to exclude from profiling
module RSpecExclusions
module_function
def generate
{
RSpec::Core::Runner => %i[
run
run_specs
],
RSpec::Core::ExampleGroup => %i[
run
run_examples
],
RSpec::Core::ExampleGroup.singleton_class => %i[
run
run_examples
],
RSpec::Core::Example => %i[
run
with_around_and_singleton_context_hooks
with_around_example_hooks
instance_exec
run_before_example
],
RSpec::Core::Example.singleton_class => %i[
run
with_around_and_singleton_context_hooks
with_around_example_hooks
],
RSpec::Core::Example::Procsy => [
:call
],
RSpec::Core::Hooks::HookCollections => %i[
run
run_around_example_hooks_for
run_example_hooks_for
run_owned_hooks_for
],
RSpec::Core::Hooks::BeforeHook => [
:run
],
RSpec::Core::Hooks::AroundHook => [
:execute_with
],
RSpec::Core::Configuration => [
:with_suite_hooks
],
RSpec::Core::Reporter => [
:report
]
}.tap do |data|
if defined?(RSpec::Support::ReentrantMutex)
data[RSpec::Support::ReentrantMutex] = [
:synchronize
]
end
if defined?(RSpec::Core::MemoizedHelpers::ThreadsafeMemoized)
data.merge!(
RSpec::Core::MemoizedHelpers::ThreadsafeMemoized => [
:fetch_or_store
],
RSpec::Core::MemoizedHelpers::NonThreadSafeMemoized => [
:fetch_or_store
],
RSpec::Core::MemoizedHelpers::ContextHookMemoized => [
:fetch_or_store
]
)
end
end
end
end
end
end
test-prof-0.10.2/lib/test_prof/ruby_prof/rspec.rb 000644 001751 001751 00000002273 13626445505 022264 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
require "test_prof/utils/rspec"
module TestProf
module RubyProf
# Reporter for RSpec to profile specific examples with RubyProf
class Listener # :nodoc:
class << self
attr_accessor :report_name_generator
end
self.report_name_generator = Utils::RSpec.method(:example_to_filename)
NOTIFICATIONS = %i[
example_started
example_finished
].freeze
def example_started(notification)
return unless profile?(notification.example)
notification.example.metadata[:rprof_report] =
TestProf::RubyProf.profile
end
def example_finished(notification)
return unless profile?(notification.example)
notification.example.metadata[:rprof_report]&.dump(
self.class.report_name_generator.call(notification.example)
)
end
private
def profile?(example)
example.metadata.key?(:rprof)
end
end
end
end
RSpec.configure do |config|
config.before(:suite) do
listener = TestProf::RubyProf::Listener.new
config.reporter.register_listener(
listener, *TestProf::RubyProf::Listener::NOTIFICATIONS
)
end
end
test-prof-0.10.2/lib/test_prof/stack_prof/ 000755 001751 001751 00000000000 13626445505 020743 5 ustar 00pravi pravi 000000 000000 test-prof-0.10.2/lib/test_prof/stack_prof/rspec.rb 000644 001751 001751 00000002623 13626445505 022407 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
require "test_prof/utils/rspec"
module TestProf
module StackProf
# Reporter for RSpec to profile specific examples with StackProf
class Listener # :nodoc:
class << self
attr_accessor :report_name_generator
end
self.report_name_generator = Utils::RSpec.method(:example_to_filename)
NOTIFICATIONS = %i[
example_started
example_finished
].freeze
def example_started(notification)
return unless profile?(notification.example)
notification.example.metadata[:sprof_report] = TestProf::StackProf.profile
end
def example_finished(notification)
return unless profile?(notification.example)
return unless notification.example.metadata[:sprof_report] == false
TestProf::StackProf.dump(
self.class.report_name_generator.call(notification.example)
)
end
private
def profile?(example)
example.metadata.key?(:sprof)
end
end
end
end
RSpec.configure do |config|
config.before(:suite) do
listener = TestProf::StackProf::Listener.new
config.reporter.register_listener(
listener, *TestProf::StackProf::Listener::NOTIFICATIONS
)
end
end
# Handle boot profiling
RSpec.configure do |config|
config.append_before(:suite) do
TestProf::StackProf.dump("boot") if TestProf::StackProf.config.boot?
end
end
test-prof-0.10.2/lib/test_prof/factory_prof.rb 000644 001751 001751 00000006725 13626445505 021644 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
require "test_prof/factory_prof/printers/simple"
require "test_prof/factory_prof/printers/flamegraph"
require "test_prof/factory_prof/factory_builders/factory_bot"
require "test_prof/factory_prof/factory_builders/fabrication"
module TestProf
# FactoryProf collects "factory stacks" that can be used to build
# flamegraphs or detect most popular factories
module FactoryProf
FACTORY_BUILDERS = [FactoryBuilders::FactoryBot,
FactoryBuilders::Fabrication].freeze
# FactoryProf configuration
class Configuration
attr_accessor :mode
def initialize
@mode = ENV["FPROF"] == "flamegraph" ? :flamegraph : :simple
end
# Whether we want to generate flamegraphs
def flamegraph?
@mode == :flamegraph
end
end
class Result # :nodoc:
attr_reader :stacks, :raw_stats
def initialize(stacks, raw_stats)
@stacks = stacks
@raw_stats = raw_stats
end
# Returns sorted stats
def stats
@stats ||= @raw_stats.values
.sort_by { |el| -el[:total_count] }
end
def total_count
@total_count ||= @raw_stats.values.sum { |v| v[:total_count] }
end
def total_time
@total_time ||= @raw_stats.values.sum { |v| v[:total_time] }
end
private
def sorted_stats(key)
@raw_stats.values
.map { |el| [el[:name], el[key]] }
.sort_by { |el| -el[1] }
end
end
class << self
include TestProf::Logging
def config
@config ||= Configuration.new
end
def configure
yield config
end
# Patch factory lib, init vars
def init
@running = false
log :info, "FactoryProf enabled (#{config.mode} mode)"
FACTORY_BUILDERS.each(&:patch)
end
# Inits FactoryProf and setups at exit hook,
# then runs
def run
init
printer = config.flamegraph? ? Printers::Flamegraph : Printers::Simple
at_exit { printer.dump(result) }
start
end
def start
reset!
@running = true
end
def stop
@running = false
end
def result
Result.new(@stacks, @stats)
end
def track(factory)
return yield unless running?
@depth += 1
@current_stack << factory if config.flamegraph?
@stats[factory][:total_count] += 1
@stats[factory][:top_level_count] += 1 if @depth == 1
t1 = TestProf.now
begin
yield
ensure
t2 = TestProf.now
elapsed = t2 - t1
@stats[factory][:total_time] += elapsed
@stats[factory][:top_level_time] += elapsed if @depth == 1
@depth -= 1
flush_stack if @depth.zero?
end
end
private
def reset!
@stacks = [] if config.flamegraph?
@depth = 0
@stats = Hash.new do |h, k|
h[k] = {
name: k,
total_count: 0,
top_level_count: 0,
total_time: 0.0,
top_level_time: 0.0
}
end
flush_stack
end
def flush_stack
return unless config.flamegraph?
@stacks << @current_stack unless @current_stack.nil? || @current_stack.empty?
@current_stack = []
end
def running?
@running == true
end
end
end
end
TestProf.activate("FPROF") do
TestProf::FactoryProf.run
end
test-prof-0.10.2/lib/test_prof/any_fixture.rb 000644 001751 001751 00000006434 13626445505 021501 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
require "test_prof/ext/float_duration"
module TestProf
# Make DB fixtures from blocks.
module AnyFixture
INSERT_RXP = /^INSERT INTO ([\S]+)/.freeze
using FloatDuration
class Cache # :nodoc:
attr_reader :store, :stats
def initialize
@store = {}
@stats = {}
end
def fetch(key)
if store.key?(key)
stats[key][:hit] += 1
return store[key]
end
return unless block_given?
ts = TestProf.now
store[key] = yield
stats[key] = {time: TestProf.now - ts, hit: 0}
store[key]
end
def clear
store.clear
stats.clear
end
end
class << self
include Logging
attr_accessor :reporting_enabled
def reporting_enabled?
reporting_enabled == true
end
# Register a block of code as a fixture,
# returns the result of the block execution
def register(id)
cache.fetch(id) do
ActiveSupport::Notifications.subscribed(method(:subscriber), "sql.active_record") do
yield
end
end
end
# Clean all affected tables (but do not reset cache)
def clean
disable_referential_integrity do
tables_cache.keys.reverse_each do |table|
ActiveRecord::Base.connection.execute %(
DELETE FROM #{table}
)
end
end
end
# Reset all information and clean tables
def reset
clean
tables_cache.clear
cache.clear
end
def subscriber(_event, _start, _finish, _id, data)
matches = data.fetch(:sql).match(INSERT_RXP)
tables_cache[matches[1]] = true if matches
end
def report_stats
msgs = []
msgs <<
<<~MSG
AnyFixture usage stats:
MSG
first_column = cache.stats.keys.map(&:size).max + 2
msgs << format(
"%#{first_column}s %12s %9s %12s",
"key", "build time", "hit count", "saved time"
)
msgs << ""
total_spent = 0.0
total_saved = 0.0
total_miss = 0.0
cache.stats.to_a.sort_by { |(_, v)| -v[:hit] }.each do |(key, stats)|
total_spent += stats[:time]
saved = stats[:time] * stats[:hit]
total_saved += saved
total_miss += stats[:time] if stats[:hit].zero?
msgs << format(
"%#{first_column}s %12s %9d %12s",
key, stats[:time].duration, stats[:hit],
saved.duration
)
end
msgs <<
<<~MSG
Total time spent: #{total_spent.duration}
Total time saved: #{total_saved.duration}
Total time wasted: #{total_miss.duration}
MSG
log :info, msgs.join("\n")
end
private
def cache
@cache ||= Cache.new
end
def tables_cache
@tables_cache ||= {}
end
def disable_referential_integrity
connection = ActiveRecord::Base.connection
return yield unless connection.respond_to?(:disable_referential_integrity)
connection.disable_referential_integrity { yield }
end
end
self.reporting_enabled = ENV["ANYFIXTURE_REPORT"] == "1"
end
end
test-prof-0.10.2/lib/test_prof/rubocop.rb 000644 001751 001751 00000000121 13626445505 020600 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
require "test_prof/cops/rspec/aggregate_failures"
test-prof-0.10.2/lib/test_prof/ruby_prof.rb 000644 001751 001751 00000015236 13626445505 021153 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
module TestProf
# RubyProf wrapper.
#
# Has 2 modes: global and per-example.
#
# Example:
#
# # To activate global profiling you can use env variable
# TEST_RUBY_PROF=1 rspec ...
#
# # or in your code
# TestProf::RubyProf.run
#
# To profile a specific examples add :rprof tag to it:
#
# it "is doing heavy stuff", :rprof do
# ...
# end
#
module RubyProf
# RubyProf configuration
class Configuration
PRINTERS = {
"flat" => "FlatPrinter",
"flat_wln" => "FlatPrinterWithLineNumbers",
"graph" => "GraphPrinter",
"graph_html" => "GraphHtmlPrinter",
"dot" => "DotPrinter",
"." => "DotPrinter",
"call_stack" => "CallStackPrinter",
"call_tree" => "CallTreePrinter",
"multi" => "MultiPrinter"
}.freeze
# Mapping from printer to report file extension
# NOTE: txt is not included and considered default
PRINTER_EXTENSTION = {
"graph_html" => "html",
"dot" => "dot",
"." => "dot",
"call_stack" => "html"
}.freeze
LOGFILE_PREFIX = "ruby-prof-report"
attr_accessor :printer, :mode, :min_percent,
:include_threads, :exclude_common_methods,
:test_prof_exclusions_enabled,
:custom_exclusions
def initialize
@printer = ENV["TEST_RUBY_PROF"].to_sym if PRINTERS.key?(ENV["TEST_RUBY_PROF"])
@printer ||= ENV.fetch("TEST_RUBY_PROF_PRINTER", :flat).to_sym
@mode = ENV.fetch("TEST_RUBY_PROF_MODE", :wall).to_sym
@min_percent = 1
@include_threads = false
@exclude_common_methods = true
@test_prof_exclusions_enabled = true
@custom_exclusions = {}
end
def include_threads?
include_threads == true
end
def exclude_common_methods?
exclude_common_methods == true
end
def test_prof_exclusions_enabled?
@test_prof_exclusions_enabled == true
end
# Returns an array of printer type (ID) and class.
def resolve_printer
return ["custom", printer] if printer.is_a?(Module)
type = printer.to_s
raise ArgumentError, "Unknown printer: #{type}" unless
PRINTERS.key?(type)
[type, ::RubyProf.const_get(PRINTERS[type])]
end
end
# Wrapper over RubyProf profiler and printer
class Report
include TestProf::Logging
def initialize(profiler)
@profiler = profiler
end
# Stop profiling and generate the report
# using provided name.
def dump(name)
result = @profiler.stop
printer_type, printer_class = config.resolve_printer
if %w[call_tree multi].include?(printer_type)
path = TestProf.create_artifact_dir
printer_class.new(result).print(
path: path,
profile: "#{RubyProf::Configuration::LOGFILE_PREFIX}-#{printer_type}-" \
"#{config.mode}-#{name}",
min_percent: config.min_percent
)
else
path = build_path name, printer_type
File.open(path, "w") do |f|
printer_class.new(result).print(f, min_percent: config.min_percent)
end
end
log :info, "RubyProf report generated: #{path}"
end
private
def build_path(name, printer)
TestProf.artifact_path(
"#{RubyProf::Configuration::LOGFILE_PREFIX}-#{printer}-#{config.mode}-#{name}" \
".#{RubyProf::Configuration::PRINTER_EXTENSTION.fetch(printer, "txt")}"
)
end
def config
RubyProf.config
end
end
class << self
include Logging
def config
@config ||= Configuration.new
end
def configure
yield config
end
# Run RubyProf and automatically dump
# a report when the process exits.
#
# Use this method to profile the whole run.
def run
report = profile
return unless report
@locked = true
log :info, "RubyProf enabled globally"
at_exit { report.dump("total") }
end
def profile
if locked?
log :warn, <<~MSG
RubyProf is activated globally, you cannot generate per-example report.
Make sure you haven's set the TEST_RUBY_PROF environmental variable.
MSG
return
end
return unless init_ruby_prof
options = {
merge_fibers: true
}
options[:include_threads] = [Thread.current] unless
config.include_threads?
profiler = ::RubyProf::Profile.new(options)
profiler.exclude_common_methods! if config.exclude_common_methods?
if config.test_prof_exclusions_enabled?
# custom test-prof exclusions
exclude_rspec_methods(profiler)
# custom global exclusions
exclude_common_methods(profiler)
end
config.custom_exclusions.each do |klass, mids|
profiler.exclude_methods! klass, *mids
end
profiler.start
Report.new(profiler)
end
private
def locked?
@locked == true
end
def init_ruby_prof
return @initialized if instance_variable_defined?(:@initialized)
ENV["RUBY_PROF_MEASURE_MODE"] = config.mode.to_s
@initialized = TestProf.require(
"ruby-prof",
<<~MSG
Please, install 'ruby-prof' first:
# Gemfile
gem 'ruby-prof', '>= 0.16.0', require: false
MSG
) { check_ruby_prof_version }
end
def check_ruby_prof_version
if Utils.verify_gem_version("ruby-prof", at_least: "0.17.0")
true
else
log :error, <<~MGS
Please, upgrade 'ruby-prof' to version >= 0.17.0.
MGS
false
end
end
def exclude_rspec_methods(profiler)
return unless TestProf.rspec?
RSpecExclusions.generate.each do |klass, mids|
profiler.exclude_methods!(klass, *mids)
end
end
def exclude_common_methods(profiler)
profiler.exclude_methods!(
TSort,
:tsort_each
)
profiler.exclude_methods!(
TSort.singleton_class,
:tsort_each, :each_strongly_connected_component,
:each_strongly_connected_component_from
)
profiler.exclude_methods!(
BasicObject,
:instance_exec
)
end
end
end
end
if TestProf.rspec?
require "test_prof/ruby_prof/rspec"
require "test_prof/ruby_prof/rspec_exclusions"
end
# Hook to run RubyProf globally
TestProf.activate("TEST_RUBY_PROF") do
TestProf::RubyProf.run
end
test-prof-0.10.2/lib/test_prof/tag_prof/ 000755 001751 001751 00000000000 13626445505 020411 5 ustar 00pravi pravi 000000 000000 test-prof-0.10.2/lib/test_prof/tag_prof/rspec.rb 000644 001751 001751 00000004012 13626445505 022047 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
module TestProf
module TagProf
class RSpecListener # :nodoc:
include Logging
NOTIFICATIONS = %i[
example_started
example_finished
].freeze
attr_reader :result, :printer
def initialize
@printer = ENV["TAG_PROF_FORMAT"] == "html" ? Printers::HTML : Printers::Simple
@result =
if ENV["TAG_PROF_EVENT"].nil?
Result.new ENV["TAG_PROF"].to_sym
else
require "test_prof/event_prof"
@events_profiler = EventProf.build(ENV["TAG_PROF_EVENT"])
Result.new ENV["TAG_PROF"].to_sym, @events_profiler.events
end
log :info, "TagProf enabled (#{result.tag})"
end
def example_started(_notification)
@ts = TestProf.now
# enable event profiling
@events_profiler&.group_started(true)
end
def example_finished(notification)
tag = notification.example.metadata.fetch(result.tag, :__unknown__)
result.track(tag, time: TestProf.now - @ts, events: fetch_events_data)
# reset and disable event profilers
@events_profiler&.group_started(nil)
end
def report
printer.dump(result)
end
private
def fetch_events_data
return {} unless @events_profiler
Hash[
@events_profiler.profilers.map do |profiler|
[profiler.event, profiler.time]
end
]
end
end
end
end
# Register TagProf listener
TestProf.activate("TAG_PROF") do
RSpec.configure do |config|
listener = nil
config.before(:suite) do
listener = TestProf::TagProf::RSpecListener.new
config.reporter.register_listener(
listener, *TestProf::TagProf::RSpecListener::NOTIFICATIONS
)
end
config.after(:suite) { listener&.report }
end
end
# Activate custom events
TestProf.activate("TAG_PROF_EVENT") do
require "test_prof/event_prof"
TestProf::EventProf::CustomEvents.activate_all(ENV["TAG_PROF_EVENT"])
end
test-prof-0.10.2/lib/test_prof/tag_prof/result.rb 000644 001751 001751 00000001472 13626445505 022260 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
module TestProf
module TagProf # :nodoc:
# Object holding all the stats for tags
class Result
attr_reader :tag, :data, :events
def initialize(tag, events = [])
@tag = tag
@events = events
@data = Hash.new do |h, k|
h[k] = {value: k, count: 0, time: 0.0}
h[k].merge!(Hash[events.map { |event| [event, 0.0] }]) unless
events.empty?
h[k]
end
end
def track(tag, time:, events: {})
data[tag][:count] += 1
data[tag][:time] += time
events.each do |k, v|
data[tag][k] += v
end
end
def to_json(*args)
{
tag: tag,
data: data.values,
events: events
}.to_json(*args)
end
end
end
end
test-prof-0.10.2/lib/test_prof/tag_prof/printers/ 000755 001751 001751 00000000000 13626445505 022257 5 ustar 00pravi pravi 000000 000000 test-prof-0.10.2/lib/test_prof/tag_prof/printers/html.rb 000644 001751 001751 00000001006 13626445505 023545 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
module TestProf::TagProf
module Printers
module HTML # :nodoc: all
TEMPLATE = "tagprof.template.html"
OUTPUT_NAME = "tag-prof.html"
class << self
include TestProf::Logging
def dump(result)
path = TestProf::Utils::HTMLBuilder.generate(
data: result,
template: TEMPLATE,
output: OUTPUT_NAME
)
log :info, "TagProf report generated: #{path}"
end
end
end
end
end
test-prof-0.10.2/lib/test_prof/tag_prof/printers/simple.rb 000644 001751 001751 00000003555 13626445505 024105 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
require "test_prof/ext/float_duration"
module TestProf::TagProf
module Printers
module Simple # :nodoc: all
class << self
include TestProf::Logging
using TestProf::FloatDuration
def dump(result)
msgs = []
msgs <<
<<~MSG
TagProf report for #{result.tag}
MSG
header = []
header << format(
"%15s %12s ",
result.tag, "time"
)
events_format = nil
unless result.events.empty?
events_format = result.events.map { |event| "%#{event.size + 2}s " }.join
header << format(
events_format,
*result.events
)
end
header << format(
"%6s %6s %6s %12s",
"total", "%total", "%time", "avg"
)
msgs << header.join
msgs << ""
total = result.data.values.inject(0) { |acc, v| acc + v[:count] }
total_time = result.data.values.inject(0) { |acc, v| acc + v[:time] }
result.data.values.sort_by { |v| -v[:time] }.each do |tag|
line = []
line << format(
"%15s %12s ",
tag[:value], tag[:time].duration
)
unless result.events.empty?
line << format(
events_format,
*result.events.map { |event| tag[event].duration }
)
end
line << format(
"%6d %6.2f %6.2f %12s",
tag[:count],
100 * tag[:count].to_f / total,
100 * tag[:time] / total_time,
(tag[:time] / tag[:count]).duration
)
msgs << line.join
end
log :info, msgs.join("\n")
end
end
end
end
end
test-prof-0.10.2/lib/test_prof/factory_all_stub/ 000755 001751 001751 00000000000 13626445505 022144 5 ustar 00pravi pravi 000000 000000 test-prof-0.10.2/lib/test_prof/factory_all_stub/factory_bot_patch.rb 000644 001751 001751 00000000441 13626445505 026162 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
module TestProf
module FactoryAllStub
# Wrap #run method to override strategy
module FactoryBotPatch
def run(_strategy = @strategy)
return super unless FactoryAllStub.enabled?
super(:build_stubbed)
end
end
end
end
test-prof-0.10.2/lib/test_prof/factory_default.rb 000644 001751 001751 00000003467 13626445505 022322 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
require "test_prof/factory_bot"
require "test_prof/factory_default/factory_bot_patch"
module TestProf
# FactoryDefault allows use to re-use associated objects
# in factories implicilty
module FactoryDefault
module DefaultSyntax # :nodoc:
def create_default(name, *args, &block)
options = args.extract_options!
preserve = options.delete(:preserve_traits)
obj = TestProf::FactoryBot.create(name, *args, options, &block)
set_factory_default(name, obj, preserve_traits: preserve)
end
def set_factory_default(name, obj, preserve_traits: nil)
FactoryDefault.register(name, obj, preserve_traits: preserve_traits)
end
end
class << self
attr_accessor :preserve_traits
def init
TestProf::FactoryBot::Syntax::Methods.include DefaultSyntax
TestProf::FactoryBot.extend DefaultSyntax
TestProf::FactoryBot::Strategy::Create.prepend StrategyExt
TestProf::FactoryBot::Strategy::Build.prepend StrategyExt
TestProf::FactoryBot::Strategy::Stub.prepend StrategyExt
@store = {}
# default is false to retain backward compatibility
@preserve_traits = false
end
def register(name, obj, **options)
options[:preserve_traits] = true if FactoryDefault.preserve_traits
store[name] = {object: obj, **options}
obj
end
def get(name, traits = nil)
record = store[name]
return unless record
if traits && !traits.empty?
return if FactoryDefault.preserve_traits || record[:preserve_traits]
end
record[:object]
end
def remove(name)
store.delete(name)
end
def reset
@store.clear
end
private
attr_reader :store
end
end
end
test-prof-0.10.2/lib/test-prof.rb 000644 001751 001751 00000000063 13626445505 017052 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
require "test_prof"
test-prof-0.10.2/lib/test_prof.rb 000644 001751 001751 00000007655 13626445505 017152 0 ustar 00pravi pravi 000000 000000 # frozen_string_literal: true
require "fileutils"
require "test_prof/version"
require "test_prof/logging"
require "test_prof/utils"
# Ruby applications tests profiling tools.
#
# Contains tools to analyze factories usage, integrate with Ruby profilers,
# profile your examples using ActiveSupport notifications (if any) and
# statically analyze your code with custom RuboCop cops.
#
# Example usage:
#
# require 'test_prof'
#
# # Activate a tool by providing environment variable, e.g.
# TEST_RUBY_PROF=1 rspec ...
#
# # or manually in your code
# TestProf::RubyProf.run
#
# See other modules for more examples.
module TestProf
class << self
include Logging
def config
@config ||= Configuration.new
end
def configure
yield config
end
# Returns true if we're inside RSpec
def rspec?
defined?(RSpec::Core)
end
# Returns true if we're inside Minitest
def minitest?
defined?(Minitest)
end
# Returns the current process time
def now
Process.clock_gettime(Process::CLOCK_MONOTONIC)
end
# Require gem and shows a custom
# message if it fails to load
def require(gem_name, msg = nil)
Kernel.require gem_name
block_given? ? yield : true
rescue LoadError
log(:error, msg) if msg
false
end
# Run block only if provided env var is present and
# equal to the provided value (if any).
# Contains workaround for applications using Spring.
def activate(env_var, val = nil)
if defined?(::Spring::Application)
notify_spring_detected
::Spring.after_fork do
activate!(env_var, val) do
notify_spring_activate env_var
yield
end
end
else
activate!(env_var, val) { yield }
end
end
# Return absolute path to asset
def asset_path(filename)
::File.expand_path(filename, ::File.join(::File.dirname(__FILE__), "..", "assets"))
end
# Return a path to store artifact
def artifact_path(filename)
create_artifact_dir
with_timestamps(
::File.join(
config.output_dir,
with_report_suffix(
filename
)
)
)
end
def create_artifact_dir
FileUtils.mkdir_p(config.output_dir)[0]
end
private
def activate!(env_var, val)
yield if ENV[env_var] && (val.nil? || val === ENV[env_var])
end
def with_timestamps(path)
return path unless config.timestamps?
timestamps = "-#{now.to_i}"
"#{path.sub(/\.\w+$/, "")}#{timestamps}#{::File.extname(path)}"
end
def with_report_suffix(path)
return path if config.report_suffix.nil?
"#{path.sub(/\.\w+$/, "")}-#{config.report_suffix}#{::File.extname(path)}"
end
def notify_spring_detected
return if instance_variable_defined?(:@spring_notified)
log :info, "Spring detected"
@spring_notified = true
end
def notify_spring_activate(env_var)
log :info, "Activating #{env_var} with `Spring.after_fork`"
end
end
# TestProf configuration
class Configuration
attr_accessor :output, # IO to write output messages.
:color, # Whether to colorize output or not
:output_dir, # Directory to store artifacts
:timestamps, # Whether to use timestamped names for artifacts,
:report_suffix # Custom suffix for reports/artifacts
def initialize
@output = $stdout
@color = true
@output_dir = "tmp/test_prof"
@timestamps = false
@report_suffix = ENV["TEST_PROF_REPORT"]
end
def color?
color == true
end
def timestamps?
timestamps == true
end
end
end
require "test_prof/ruby_prof"
require "test_prof/stack_prof"
require "test_prof/event_prof"
require "test_prof/factory_doctor"
require "test_prof/factory_prof"
require "test_prof/rspec_stamp"
require "test_prof/tag_prof"
require "test_prof/rspec_dissect"
require "test_prof/factory_all_stub"
test-prof-0.10.2/README.md 000644 001751 001751 00000011546 13626445505 015323 0 ustar 00pravi pravi 000000 000000 [](http://cultofmartians.com)
[](https://rubygems.org/gems/test-prof) [](https://github.com/palkan/test-prof/actions)
[](https://github.com/palkan/test-prof/actions)
[](https://www.codetriage.com/palkan/test-prof)
[](https://test-prof.evilmartians.io)
# Ruby Tests Profiling Toolbox
TestProf is a collection of different tools to analyze your test suite performance.
Why does test suite performance matter? First of all, testing is a part of a developer's feedback loop (see [@searls](https://github.com/searls) [talk](https://vimeo.com/145917204)) and, secondly, it is a part of a deployment cycle.
Simply speaking, slow tests waste your time making you less productive.
TestProf toolbox aims to help you identify bottlenecks in your test suite. It contains:
- Plug'n'Play integrations for general Ruby profilers ([`ruby-prof`](https://github.com/ruby-prof/ruby-prof), [`stackprof`](https://github.com/tmm1/stackprof))
- Factories usage analyzers and profilers
- ActiveSupport-backed profilers
- RSpec and minitest [helpers](https://test-prof.evilmartians.io/#/?id=recipes) to write faster tests
- RuboCop cops
- etc.
📑 [Documentation](https://test-prof.evilmartians.io)