test-prof-0.10.2/000755 001751 001751 00000000000 13626445505 014035 5ustar00pravipravi000000 000000 test-prof-0.10.2/CHANGELOG.md000644 001751 001751 00000036420 13626445505 015653 0ustar00pravipravi000000 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 5ustar00pravipravi000000 000000 test-prof-0.10.2/lib/minitest/000755 001751 001751 00000000000 13626445505 016437 5ustar00pravipravi000000 000000 test-prof-0.10.2/lib/minitest/event_prof_formatter.rb000644 001751 001751 00000004663 13626445505 023227 0ustar00pravipravi000000 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.rb000644 001751 001751 00000003116 13626445505 022350 0ustar00pravipravi000000 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.rb000644 001751 001751 00000003212 13626445505 021616 0ustar00pravipravi000000 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 5ustar00pravipravi000000 000000 test-prof-0.10.2/lib/test_prof/factory_all_stub.rb000644 001751 001751 00000001344 13626445505 022473 0ustar00pravipravi000000 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 5ustar00pravipravi000000 000000 test-prof-0.10.2/lib/test_prof/before_all/adapters/000755 001751 001751 00000000000 13626445505 022505 5ustar00pravipravi000000 000000 test-prof-0.10.2/lib/test_prof/before_all/adapters/active_record.rb000644 001751 001751 00000002366 13626445505 025652 0ustar00pravipravi000000 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.rb000644 001751 001751 00000000664 13626445505 023071 0ustar00pravipravi000000 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 5ustar00pravipravi000000 000000 test-prof-0.10.2/lib/test_prof/factory_default/factory_bot_patch.rb000644 001751 001751 00000000666 13626445505 026012 0ustar00pravipravi000000 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.rb000644 001751 001751 00000010523 13626445505 021271 0ustar00pravipravi000000 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.rb000644 001751 001751 00000006635 13626445505 022001 0ustar00pravipravi000000 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.rb000644 001751 001751 00000004623 13626445505 021234 0ustar00pravipravi000000 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 5ustar00pravipravi000000 000000 test-prof-0.10.2/lib/test_prof/rspec_stamp/parser.rb000644 001751 001751 00000007654 13626445505 022765 0ustar00pravipravi000000 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.rb000644 001751 001751 00000002642 13626445505 022575 0ustar00pravipravi000000 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 5ustar00pravipravi000000 000000 test-prof-0.10.2/lib/test_prof/ext/active_record_refind.rb000644 001751 001751 00000001005 13626445505 024071 0ustar00pravipravi000000 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.rb000644 001751 001751 00000000662 13626445505 023154 0ustar00pravipravi000000 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.rb000644 001751 001751 00000000406 13626445505 022747 0ustar00pravipravi000000 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.rb000644 001751 001751 00000001265 13626445505 022774 0ustar00pravipravi000000 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.rb000644 001751 001751 00000000531 13626445505 023730 0ustar00pravipravi000000 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.rb000644 001751 001751 00000001051 13626445505 024167 0ustar00pravipravi000000 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.rb000644 001751 001751 00000000622 13626445505 024013 0ustar00pravipravi000000 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 5ustar00pravipravi000000 000000 test-prof-0.10.2/lib/test_prof/cops/rspec/000755 001751 001751 00000000000 13626445505 020670 5ustar00pravipravi000000 000000 test-prof-0.10.2/lib/test_prof/cops/rspec/aggregate_failures.rb000644 001751 001751 00000011254 13626445505 025040 0ustar00pravipravi000000 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 5ustar00pravipravi000000 000000 test-prof-0.10.2/lib/test_prof/factory_doctor/rspec.rb000644 001751 001751 00000007072 13626445505 023300 0ustar00pravipravi000000 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.rb000644 001751 001751 00000004570 13626445505 024020 0ustar00pravipravi000000 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.rb000644 001751 001751 00000000375 13626445505 025623 0ustar00pravipravi000000 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.rb000644 001751 001751 00000000600 13626445505 025644 0ustar00pravipravi000000 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.rb000644 001751 001751 00000000410 13626445505 020731 0ustar00pravipravi000000 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 5ustar00pravipravi000000 000000 test-prof-0.10.2/lib/test_prof/recipes/logging.rb000644 001751 001751 00000006231 13626445505 022217 0ustar00pravipravi000000 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 5ustar00pravipravi000000 000000 test-prof-0.10.2/lib/test_prof/recipes/minitest/before_all.rb000644 001751 001751 00000003431 13626445505 024516 0ustar00pravipravi000000 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.rb000644 001751 001751 00000003042 13626445505 023703 0ustar00pravipravi000000 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.rb000644 001751 001751 00000000251 13626445505 025264 0ustar00pravipravi000000 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 5ustar00pravipravi000000 000000 test-prof-0.10.2/lib/test_prof/recipes/rspec/factory_all_stub.rb000644 001751 001751 00000000537 13626445505 025244 0ustar00pravipravi000000 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.rb000644 001751 001751 00000001670 13626445505 024001 0ustar00pravipravi000000 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.rb000644 001751 001751 00000007637 13626445505 023646 0ustar00pravipravi000000 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.rb000644 001751 001751 00000002131 13626445505 023161 0ustar00pravipravi000000 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.rb000644 001751 001751 00000000760 13626445505 024243 0ustar00pravipravi000000 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.rb000644 001751 001751 00000000274 13626445505 025061 0ustar00pravipravi000000 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.rb000644 001751 001751 00000003734 13626445505 027154 0ustar00pravipravi000000 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 5ustar00pravipravi000000 000000 test-prof-0.10.2/lib/test_prof/utils/html_builder.rb000644 001751 001751 00000000756 13626445505 022757 0ustar00pravipravi000000 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.rb000644 001751 001751 00000000572 13626445505 021415 0ustar00pravipravi000000 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.rb000644 001751 001751 00000002424 13626445505 023774 0ustar00pravipravi000000 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 5ustar00pravipravi000000 000000 test-prof-0.10.2/lib/test_prof/any_fixture/dsl.rb000644 001751 001751 00000001053 13626445505 022253 0ustar00pravipravi000000 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 5ustar00pravipravi000000 000000 test-prof-0.10.2/lib/test_prof/event_prof/rspec.rb000644 001751 001751 00000010316 13626445505 022421 0ustar00pravipravi000000 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.rb000644 001751 001751 00000006547 13626445505 023142 0ustar00pravipravi000000 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 5ustar00pravipravi000000 000000 test-prof-0.10.2/lib/test_prof/event_prof/custom_events/factory_create.rb000644 001751 001751 00000001614 13626445505 027176 0ustar00pravipravi000000 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.rb000644 001751 001751 00000000662 13626445505 027175 0ustar00pravipravi000000 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.rb000644 001751 001751 00000000753 13626445505 026655 0ustar00pravipravi000000 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.rb000644 001751 001751 00000004147 13626445505 023146 0ustar00pravipravi000000 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.rb000644 001751 001751 00000001614 13626445505 024204 0ustar00pravipravi000000 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 5ustar00pravipravi000000 000000 test-prof-0.10.2/lib/test_prof/event_prof/instrumentations/active_support.rb000644 001751 001751 00000001075 13626445505 030004 0ustar00pravipravi000000 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.rb000644 001751 001751 00000002660 13626445505 022777 0ustar00pravipravi000000 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 5ustar00pravipravi000000 000000 test-prof-0.10.2/lib/test_prof/rspec_dissect/rspec.rb000644 001751 001751 00000006123 13626445505 023105 0ustar00pravipravi000000 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 5ustar00pravipravi000000 000000 test-prof-0.10.2/lib/test_prof/rspec_dissect/collectors/before.rb000644 001751 001751 00000000514 13626445505 025402 0ustar00pravipravi000000 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.rb000644 001751 001751 00000002770 13626445505 025060 0ustar00pravipravi000000 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.rb000644 001751 001751 00000001663 13626445505 024732 0ustar00pravipravi000000 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.rb000644 001751 001751 00000001041 13626445505 020557 0ustar00pravipravi000000 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 5ustar00pravipravi000000 000000 test-prof-0.10.2/lib/test_prof/factory_prof/fabrication_patch.rb000644 001751 001751 00000000417 13626445505 025274 0ustar00pravipravi000000 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 5ustar00pravipravi000000 000000 test-prof-0.10.2/lib/test_prof/factory_prof/printers/flamegraph.rb000644 001751 001751 00000003252 13626445505 025610 0ustar00pravipravi000000 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.rb000644 001751 001751 00000002557 13626445505 025002 0ustar00pravipravi000000 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 5ustar00pravipravi000000 000000 test-prof-0.10.2/lib/test_prof/factory_prof/factory_builders/factory_bot.rb000644 001751 001751 00000001430 13626445505 027503 0ustar00pravipravi000000 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.rb000644 001751 001751 00000001142 13626445505 027451 0ustar00pravipravi000000 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.rb000644 001751 001751 00000000425 13626445505 025325 0ustar00pravipravi000000 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.rb000644 001751 001751 00000000110 13626445505 020612 0ustar00pravipravi000000 000000 # frozen_string_literal: true module TestProf VERSION = "0.10.2" end test-prof-0.10.2/lib/test_prof/factory_doctor.rb000644 001751 001751 00000007143 13626445505 022163 0ustar00pravipravi000000 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.rb000644 001751 001751 00000010402 13626445505 021452 0ustar00pravipravi000000 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.rb000644 001751 001751 00000005750 13626445505 021313 0ustar00pravipravi000000 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.rb000644 001751 001751 00000001354 13626445505 020300 0ustar00pravipravi000000 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.rb000644 001751 001751 00000000461 13626445505 021451 0ustar00pravipravi000000 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 5ustar00pravipravi000000 000000 test-prof-0.10.2/lib/test_prof/ruby_prof/rspec_exclusions.rb000644 001751 001751 00000004255 13626445505 024542 0ustar00pravipravi000000 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.rb000644 001751 001751 00000002273 13626445505 022264 0ustar00pravipravi000000 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 5ustar00pravipravi000000 000000 test-prof-0.10.2/lib/test_prof/stack_prof/rspec.rb000644 001751 001751 00000002623 13626445505 022407 0ustar00pravipravi000000 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.rb000644 001751 001751 00000006725 13626445505 021644 0ustar00pravipravi000000 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.rb000644 001751 001751 00000006434 13626445505 021501 0ustar00pravipravi000000 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.rb000644 001751 001751 00000000121 13626445505 020600 0ustar00pravipravi000000 000000 # frozen_string_literal: true require "test_prof/cops/rspec/aggregate_failures" test-prof-0.10.2/lib/test_prof/ruby_prof.rb000644 001751 001751 00000015236 13626445505 021153 0ustar00pravipravi000000 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 5ustar00pravipravi000000 000000 test-prof-0.10.2/lib/test_prof/tag_prof/rspec.rb000644 001751 001751 00000004012 13626445505 022047 0ustar00pravipravi000000 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.rb000644 001751 001751 00000001472 13626445505 022260 0ustar00pravipravi000000 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 5ustar00pravipravi000000 000000 test-prof-0.10.2/lib/test_prof/tag_prof/printers/html.rb000644 001751 001751 00000001006 13626445505 023545 0ustar00pravipravi000000 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.rb000644 001751 001751 00000003555 13626445505 024105 0ustar00pravipravi000000 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 5ustar00pravipravi000000 000000 test-prof-0.10.2/lib/test_prof/factory_all_stub/factory_bot_patch.rb000644 001751 001751 00000000441 13626445505 026162 0ustar00pravipravi000000 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.rb000644 001751 001751 00000003467 13626445505 022322 0ustar00pravipravi000000 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.rb000644 001751 001751 00000000063 13626445505 017052 0ustar00pravipravi000000 000000 # frozen_string_literal: true require "test_prof" test-prof-0.10.2/lib/test_prof.rb000644 001751 001751 00000007655 13626445505 017152 0ustar00pravipravi000000 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.md000644 001751 001751 00000011546 13626445505 015323 0ustar00pravipravi000000 000000 [![Cult Of Martians](http://cultofmartians.com/assets/badges/badge.svg)](http://cultofmartians.com) [![Gem Version](https://badge.fury.io/rb/test-prof.svg)](https://rubygems.org/gems/test-prof) [![Build](https://github.com/palkan/test-prof/workflows/Build/badge.svg)](https://github.com/palkan/test-prof/actions) [![JRuby Build](https://github.com/palkan/test-prof/workflows/JRuby%20Build/badge.svg)](https://github.com/palkan/test-prof/actions) [![Code Triagers Badge](https://www.codetriage.com/palkan/test-prof/badges/users.svg)](https://www.codetriage.com/palkan/test-prof) [![Documentation](https://img.shields.io/badge/docs-link-brightgreen.svg)](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)

TestProf map

Sponsored by Evil Martians

## Who uses TestProf - [Discourse](https://github.com/discourse/discourse) reduced [~27% of their test suite time](https://twitter.com/samsaffron/status/1125602558024699904) - [Gitlab](https://gitlab.com/gitlab-org/gitlab-ce) reduced [39% of their API tests time](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14370) - [CodeTriage](https://github.com/codetriage/codetriage) - [Dev.to](https://github.com/thepracticaldev/dev.to) - [Open Project](https://github.com/opf/openproject) - [...and others](https://github.com/palkan/test-prof/issues/73) ## Resources - [TestProf: a good doctor for slow Ruby tests](https://evilmartians.com/chronicles/testprof-a-good-doctor-for-slow-ruby-tests) - [TestProf II: Factory therapy for your Ruby tests](https://evilmartians.com/chronicles/testprof-2-factory-therapy-for-your-ruby-tests-rspec-minitest) - Paris.rb, 2018, "99 Problems of Slow Tests" talk [[video](https://www.youtube.com/watch?v=eDMZS_fkRtk), [slides](https://speakerdeck.com/palkan/paris-dot-rb-2018-99-problems-of-slow-tests)] - BalkanRuby, 2018, "Take your slow tests to the doctor" talk [[video](https://www.youtube.com/watch?v=rOcrme82vC8)], [slides](https://speakerdeck.com/palkan/balkanruby-2018-take-your-slow-tests-to-the-doctor)] - RailsClub, Moscow, 2017, "Faster Tests" talk [[video](https://www.youtube.com/watch?v=8S7oHjEiVzs) (RU), [slides](https://speakerdeck.com/palkan/railsclub-moscow-2017-faster-tests)] - RubyConfBy, 2017, "Run Test Run" talk [[video](https://www.youtube.com/watch?v=q52n4p0wkIs), [slides](https://speakerdeck.com/palkan/rubyconfby-minsk-2017-run-test-run)] - [Tips to improve speed of your test suite](https://medium.com/appaloosa-store-engineering/tips-to-improve-speed-of-your-test-suite-8418b485205c) by [Benoit Tigeot](https://github.com/benoittgt) ## Installation Add `test-prof` gem to your application: ```ruby group :test do gem "test-prof" end ``` And that's it) Supported Ruby versions: - Ruby (MRI) >= 2.4.0 (**NOTE:** for Ruby 2.2 use TestProf < 0.7.0 or Ruby 2.3 use TestProf ~> 0.7.0) - JRuby >= 9.1.0.0 (**NOTE:** refinements-dependent features might require 9.2.7+) Supported RSpec version (for RSpec features only): >= 3.5.0 (for older RSpec versions use TestProf < 0.8.0). ## Usage Check out our [docs][]. ## What's next? Have an idea? [Propose](https://github.com/palkan/test-prof/issues/new) a feature request! Already using TestProf? [Share your story!](https://github.com/palkan/test-prof/issues/73) ## License The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT). [docs]: https://test-prof.evilmartians.io ## Security Contact To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. test-prof-0.10.2/test-prof.gemspec000644 001751 001751 00000016060 13626445505 017330 0ustar00pravipravi000000 000000 ######################################################### # This file has been automatically generated by gem2tgz # ######################################################### # -*- encoding: utf-8 -*- # stub: test-prof 0.10.2 ruby lib Gem::Specification.new do |s| s.name = "test-prof".freeze s.version = "0.10.2" s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version= s.metadata = { "bug_tracker_uri" => "http://github.com/palkan/test-prof/issues", "changelog_uri" => "https://github.com/palkan/test-prof/blob/master/CHANGELOG.md", "documentation_uri" => "https://test-prof.evilmartians.io/", "homepage_uri" => "https://test-prof.evilmartians.io/", "source_code_uri" => "http://github.com/palkan/test-prof" } if s.respond_to? :metadata= s.require_paths = ["lib".freeze] s.authors = ["Vladimir Dementyev".freeze] s.date = "2020-01-07" s.description = "\n Ruby applications tests profiling tools.\n\n Contains tools to analyze factories usage, integrate with Ruby profilers,\n profile your examples using ActiveSupport notifications (if any) and\n statically analyze your code with custom RuboCop cops.\n ".freeze s.email = ["dementiev.vm@gmail.com".freeze] s.files = ["CHANGELOG.md".freeze, "LICENSE.txt".freeze, "README.md".freeze, "assets/flamegraph.demo.html".freeze, "assets/flamegraph.template.html".freeze, "assets/src/d3-tip.js".freeze, "assets/src/d3-tip.min.js".freeze, "assets/src/d3.flameGraph.css".freeze, "assets/src/d3.flameGraph.js".freeze, "assets/src/d3.flameGraph.min.css".freeze, "assets/src/d3.flameGraph.min.js".freeze, "assets/src/d3.v4.min.js".freeze, "assets/tagprof.demo.html".freeze, "assets/tagprof.template.html".freeze, "lib/minitest/base_reporter.rb".freeze, "lib/minitest/event_prof_formatter.rb".freeze, "lib/minitest/test_prof_plugin.rb".freeze, "lib/test-prof.rb".freeze, "lib/test_prof.rb".freeze, "lib/test_prof/any_fixture.rb".freeze, "lib/test_prof/any_fixture/dsl.rb".freeze, "lib/test_prof/before_all.rb".freeze, "lib/test_prof/before_all/adapters/active_record.rb".freeze, "lib/test_prof/before_all/isolator.rb".freeze, "lib/test_prof/cops/rspec/aggregate_failures.rb".freeze, "lib/test_prof/event_prof.rb".freeze, "lib/test_prof/event_prof/custom_events.rb".freeze, "lib/test_prof/event_prof/custom_events/factory_create.rb".freeze, "lib/test_prof/event_prof/custom_events/sidekiq_inline.rb".freeze, "lib/test_prof/event_prof/custom_events/sidekiq_jobs.rb".freeze, "lib/test_prof/event_prof/instrumentations/active_support.rb".freeze, "lib/test_prof/event_prof/minitest.rb".freeze, "lib/test_prof/event_prof/monitor.rb".freeze, "lib/test_prof/event_prof/profiler.rb".freeze, "lib/test_prof/event_prof/rspec.rb".freeze, "lib/test_prof/ext/active_record_3.rb".freeze, "lib/test_prof/ext/active_record_refind.rb".freeze, "lib/test_prof/ext/array_bsearch_index.rb".freeze, "lib/test_prof/ext/factory_bot_strategy.rb".freeze, "lib/test_prof/ext/float_duration.rb".freeze, "lib/test_prof/ext/string_parameterize.rb".freeze, "lib/test_prof/ext/string_truncate.rb".freeze, "lib/test_prof/factory_all_stub.rb".freeze, "lib/test_prof/factory_all_stub/factory_bot_patch.rb".freeze, "lib/test_prof/factory_bot.rb".freeze, "lib/test_prof/factory_default.rb".freeze, "lib/test_prof/factory_default/factory_bot_patch.rb".freeze, "lib/test_prof/factory_doctor.rb".freeze, "lib/test_prof/factory_doctor/fabrication_patch.rb".freeze, "lib/test_prof/factory_doctor/factory_bot_patch.rb".freeze, "lib/test_prof/factory_doctor/minitest.rb".freeze, "lib/test_prof/factory_doctor/rspec.rb".freeze, "lib/test_prof/factory_prof.rb".freeze, "lib/test_prof/factory_prof/fabrication_patch.rb".freeze, "lib/test_prof/factory_prof/factory_bot_patch.rb".freeze, "lib/test_prof/factory_prof/factory_builders/fabrication.rb".freeze, "lib/test_prof/factory_prof/factory_builders/factory_bot.rb".freeze, "lib/test_prof/factory_prof/printers/flamegraph.rb".freeze, "lib/test_prof/factory_prof/printers/simple.rb".freeze, "lib/test_prof/logging.rb".freeze, "lib/test_prof/recipes/active_record_one_love.rb".freeze, "lib/test_prof/recipes/active_record_shared_connection.rb".freeze, "lib/test_prof/recipes/logging.rb".freeze, "lib/test_prof/recipes/minitest/before_all.rb".freeze, "lib/test_prof/recipes/minitest/sample.rb".freeze, "lib/test_prof/recipes/rspec/any_fixture.rb".freeze, "lib/test_prof/recipes/rspec/before_all.rb".freeze, "lib/test_prof/recipes/rspec/factory_all_stub.rb".freeze, "lib/test_prof/recipes/rspec/factory_default.rb".freeze, "lib/test_prof/recipes/rspec/let_it_be.rb".freeze, "lib/test_prof/recipes/rspec/sample.rb".freeze, "lib/test_prof/rspec_dissect.rb".freeze, "lib/test_prof/rspec_dissect/collectors/base.rb".freeze, "lib/test_prof/rspec_dissect/collectors/before.rb".freeze, "lib/test_prof/rspec_dissect/collectors/let.rb".freeze, "lib/test_prof/rspec_dissect/rspec.rb".freeze, "lib/test_prof/rspec_stamp.rb".freeze, "lib/test_prof/rspec_stamp/parser.rb".freeze, "lib/test_prof/rspec_stamp/rspec.rb".freeze, "lib/test_prof/rubocop.rb".freeze, "lib/test_prof/ruby_prof.rb".freeze, "lib/test_prof/ruby_prof/rspec.rb".freeze, "lib/test_prof/ruby_prof/rspec_exclusions.rb".freeze, "lib/test_prof/stack_prof.rb".freeze, "lib/test_prof/stack_prof/rspec.rb".freeze, "lib/test_prof/tag_prof.rb".freeze, "lib/test_prof/tag_prof/printers/html.rb".freeze, "lib/test_prof/tag_prof/printers/simple.rb".freeze, "lib/test_prof/tag_prof/result.rb".freeze, "lib/test_prof/tag_prof/rspec.rb".freeze, "lib/test_prof/utils.rb".freeze, "lib/test_prof/utils/html_builder.rb".freeze, "lib/test_prof/utils/rspec.rb".freeze, "lib/test_prof/utils/sized_ordered_set.rb".freeze, "lib/test_prof/version.rb".freeze] s.homepage = "http://github.com/palkan/test-prof".freeze s.licenses = ["MIT".freeze] s.required_ruby_version = Gem::Requirement.new(">= 2.4.0".freeze) s.rubygems_version = "2.7.6.2".freeze s.summary = "Ruby applications tests profiling tools".freeze if s.respond_to? :specification_version then s.specification_version = 4 if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then s.add_development_dependency(%q.freeze, [">= 1.16"]) s.add_development_dependency(%q.freeze, ["~> 0.6"]) s.add_development_dependency(%q.freeze, ["~> 5.9"]) s.add_development_dependency(%q.freeze, ["~> 12.0"]) s.add_development_dependency(%q.freeze, ["~> 3.4"]) s.add_development_dependency(%q.freeze, ["~> 0.77.0"]) else s.add_dependency(%q.freeze, [">= 1.16"]) s.add_dependency(%q.freeze, ["~> 0.6"]) s.add_dependency(%q.freeze, ["~> 5.9"]) s.add_dependency(%q.freeze, ["~> 12.0"]) s.add_dependency(%q.freeze, ["~> 3.4"]) s.add_dependency(%q.freeze, ["~> 0.77.0"]) end else s.add_dependency(%q.freeze, [">= 1.16"]) s.add_dependency(%q.freeze, ["~> 0.6"]) s.add_dependency(%q.freeze, ["~> 5.9"]) s.add_dependency(%q.freeze, ["~> 12.0"]) s.add_dependency(%q.freeze, ["~> 3.4"]) s.add_dependency(%q.freeze, ["~> 0.77.0"]) end end test-prof-0.10.2/LICENSE.txt000644 001751 001751 00000002066 13626445505 015664 0ustar00pravipravi000000 000000 The MIT License (MIT) Copyright (c) 2017-2019 palkan Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.