From 24e7bad607eb52a5577fe47e81abea0d911816e2 Mon Sep 17 00:00:00 2001 From: Josh Nichols Date: Tue, 21 Mar 2023 14:37:58 -0400 Subject: [PATCH 1/4] Add rspec as a dependency --- Gemfile.lock | 21 ++++++++++++++++++--- singed.gemspec | 1 + 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 5dd7631..4920b9d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,15 +1,29 @@ PATH remote: . specs: - singed (0.0.1) - colorize (~> 0.8.1) + singed (0.1.1) + colorize stackprof GEM remote: https://rubygems.org/ specs: colorize (0.8.1) + diff-lcs (1.5.0) rake (13.0.6) + rspec (3.12.0) + rspec-core (~> 3.12.0) + rspec-expectations (~> 3.12.0) + rspec-mocks (~> 3.12.0) + rspec-core (3.12.1) + rspec-support (~> 3.12.0) + rspec-expectations (3.12.2) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.12.0) + rspec-mocks (3.12.4) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.12.0) + rspec-support (3.12.0) stackprof (0.2.17) PLATFORMS @@ -17,7 +31,8 @@ PLATFORMS DEPENDENCIES rake (~> 13.0) + rspec singed! BUNDLED WITH - 2.2.30 + 2.4.4 diff --git a/singed.gemspec b/singed.gemspec index 790cd10..ee0ca01 100644 --- a/singed.gemspec +++ b/singed.gemspec @@ -22,6 +22,7 @@ Gem::Specification.new do |spec| spec.add_dependency 'stackprof' spec.add_development_dependency 'rake', '~> 13.0' + spec.add_development_dependency 'rspec' # For more information and examples about making a new gem, checkout our # guide at: https://bundler.io/guides/creating_gem.html From f812db1c41ea26be7602edd144b24834af152ae8 Mon Sep 17 00:00:00 2001 From: Josh Nichols Date: Tue, 21 Mar 2023 14:41:19 -0400 Subject: [PATCH 2/4] rspec --init --- bin/rspec | 27 +++++++++++++ spec/spec_helper.rb | 98 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 125 insertions(+) create mode 100755 bin/rspec create mode 100644 spec/spec_helper.rb diff --git a/bin/rspec b/bin/rspec new file mode 100755 index 0000000..cb53ebe --- /dev/null +++ b/bin/rspec @@ -0,0 +1,27 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'rspec' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) + +bundle_binstub = File.expand_path("bundle", __dir__) + +if File.file?(bundle_binstub) + if File.read(bundle_binstub, 300).include?("This file was generated by Bundler") + load(bundle_binstub) + else + abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. +Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") + end +end + +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("rspec-core", "rspec") diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..5ae5b69 --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,98 @@ +# This file was generated by the `rspec --init` command. Conventionally, all +# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. +# The generated `.rspec` file contains `--require spec_helper` which will cause +# this file to always be loaded, without a need to explicitly require it in any +# files. +# +# Given that it is always loaded, you are encouraged to keep this file as +# light-weight as possible. Requiring heavyweight dependencies from this file +# will add to the boot time of your test suite on EVERY test run, even for an +# individual file that may not need all of that loaded. Instead, consider making +# a separate helper file that requires the additional dependencies and performs +# the additional setup, and require it from the spec files that actually need +# it. +# +# See https://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration +RSpec.configure do |config| + # rspec-expectations config goes here. You can use an alternate + # assertion/expectation library such as wrong or the stdlib/minitest + # assertions if you prefer. + config.expect_with :rspec do |expectations| + # This option will default to `true` in RSpec 4. It makes the `description` + # and `failure_message` of custom matchers include text for helper methods + # defined using `chain`, e.g.: + # be_bigger_than(2).and_smaller_than(4).description + # # => "be bigger than 2 and smaller than 4" + # ...rather than: + # # => "be bigger than 2" + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + # rspec-mocks config goes here. You can use an alternate test double + # library (such as bogus or mocha) by changing the `mock_with` option here. + config.mock_with :rspec do |mocks| + # Prevents you from mocking or stubbing a method that does not exist on + # a real object. This is generally recommended, and will default to + # `true` in RSpec 4. + mocks.verify_partial_doubles = true + end + + # This option will default to `:apply_to_host_groups` in RSpec 4 (and will + # have no way to turn it off -- the option exists only for backwards + # compatibility in RSpec 3). It causes shared context metadata to be + # inherited by the metadata hash of host groups and examples, rather than + # triggering implicit auto-inclusion in groups with matching metadata. + config.shared_context_metadata_behavior = :apply_to_host_groups + +# The settings below are suggested to provide a good initial experience +# with RSpec, but feel free to customize to your heart's content. +=begin + # This allows you to limit a spec run to individual examples or groups + # you care about by tagging them with `:focus` metadata. When nothing + # is tagged with `:focus`, all examples get run. RSpec also provides + # aliases for `it`, `describe`, and `context` that include `:focus` + # metadata: `fit`, `fdescribe` and `fcontext`, respectively. + config.filter_run_when_matching :focus + + # Allows RSpec to persist some state between runs in order to support + # the `--only-failures` and `--next-failure` CLI options. We recommend + # you configure your source control system to ignore this file. + config.example_status_persistence_file_path = "spec/examples.txt" + + # Limits the available syntax to the non-monkey patched syntax that is + # recommended. For more details, see: + # https://relishapp.com/rspec/rspec-core/docs/configuration/zero-monkey-patching-mode + config.disable_monkey_patching! + + # This setting enables warnings. It's recommended, but in some cases may + # be too noisy due to issues in dependencies. + config.warnings = true + + # Many RSpec users commonly either run the entire suite or an individual + # file, and it's useful to allow more verbose output when running an + # individual spec file. + if config.files_to_run.one? + # Use the documentation formatter for detailed output, + # unless a formatter has already been configured + # (e.g. via a command-line flag). + config.default_formatter = "doc" + end + + # Print the 10 slowest examples and example groups at the + # end of the spec run, to help surface which specs are running + # particularly slow. + config.profile_examples = 10 + + # Run specs in random order to surface order dependencies. If you find an + # order dependency and want to debug it, you can fix the order by providing + # the seed, which is printed after each run. + # --seed 1234 + config.order = :random + + # Seed global randomization in this process using the `--seed` CLI option. + # Setting this allows you to use `--seed` to deterministically reproduce + # test failures related to randomization by passing the same `--seed` value + # as the one that triggered the failure. + Kernel.srand config.seed +=end +end From 9914a4cc6c013682266ebad91e636af186a90e63 Mon Sep 17 00:00:00 2001 From: Josh Nichols Date: Tue, 21 Mar 2023 15:14:32 -0400 Subject: [PATCH 3/4] require singed --- spec/spec_helper.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 5ae5b69..8990a63 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -13,6 +13,9 @@ # it. # # See https://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration + +require 'singed' + RSpec.configure do |config| # rspec-expectations config goes here. You can use an alternate # assertion/expectation library such as wrong or the stdlib/minitest From c870ac6b67b00eed4e2ee20793af2f9ec2d5b297 Mon Sep 17 00:00:00 2001 From: Josh Nichols Date: Tue, 21 Mar 2023 17:43:24 -0400 Subject: [PATCH 4/4] Add rspec for kernel extension. Update README to reflect label usage --- README.md | 2 +- lib/singed/kernel_ext.rb | 6 ++-- spec/singed/kernel_ext_spec.rb | 60 ++++++++++++++++++++++++++++++++++ 3 files changed, 64 insertions(+), 4 deletions(-) create mode 100644 spec/singed/kernel_ext_spec.rb diff --git a/README.md b/README.md index 67f3052..c6047f5 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ Singed.output_directory = "tmp/slowness-exploration" If you are calling it in a loop, or with different variations, you can include a label on the filename: ```ruby -flamegraph(label: "rspec") { +flamegraph("rspec") { # your code here } ``` diff --git a/lib/singed/kernel_ext.rb b/lib/singed/kernel_ext.rb index a5d5cf8..a28b986 100644 --- a/lib/singed/kernel_ext.rb +++ b/lib/singed/kernel_ext.rb @@ -1,15 +1,15 @@ module Kernel - def flamegraph(label = nil, open: true, ignore_gc: false, interval: 1000, &block) + def flamegraph(label = nil, open: true, ignore_gc: false, interval: 1000, io: $stdout, &block) fg = Singed::Flamegraph.new(label: label, ignore_gc: ignore_gc, interval: interval) result = fg.record(&block) fg.save if open # use npx, so we don't have to add it as a dependency - puts "🔥📈 #{'Captured flamegraph, opening with'.colorize(:bold).colorize(:red)}: #{fg.open_command}" + io.puts "🔥📈 #{'Captured flamegraph, opening with'.colorize(:bold).colorize(:red)}: #{fg.open_command}" fg.open else - puts "🔥📈 #{'Captured flamegraph to file'.colorize(:bold).colorize(:red)}: #{fg.filename}" + io.puts "🔥📈 #{'Captured flamegraph to file'.colorize(:bold).colorize(:red)}: #{fg.filename}" end result diff --git a/spec/singed/kernel_ext_spec.rb b/spec/singed/kernel_ext_spec.rb new file mode 100644 index 0000000..70e6bc1 --- /dev/null +++ b/spec/singed/kernel_ext_spec.rb @@ -0,0 +1,60 @@ +describe Kernel, "extension" do + let(:flamegraph) { + instance_double(Singed::Flamegraph) + } + + before do + allow(Singed::Flamegraph).to receive(:new).and_return(flamegraph) + allow(flamegraph).to receive(:record) + allow(flamegraph).to receive(:save) + allow(flamegraph).to receive(:open) + allow(flamegraph).to receive(:open_command) + allow(flamegraph).to receive(:filename) + end + + let(:io) { StringIO.new } + + it "works without any arguments" do + # * except what's needed to test + # note: use Object.new to get the actual flamegraph kernel extension, instead of the rspec-specific flamegraph + Object.new.flamegraph io: io do + end + + expect(Singed::Flamegraph).to have_received(:new).with(label: nil, ignore_gc: false, interval: 1000) + end + + it "works with explicit arguments" do + # note: use Object.new to get the actual flamegraph kernel extension, instead of the rspec-specific flamegraph + Object.new.flamegraph 'yellowjackets', ignore_gc: true, interval: 2000, io: io do + end + + expect(Singed::Flamegraph).to have_received(:new).with(label: 'yellowjackets', ignore_gc: true, interval: 2000) + end + + context "default" do + it "opens" do + Object.new.flamegraph open: true, io: io do + end + + expect(flamegraph).to have_received(:open) + end + end + + context "open: true" do + it "opens" do + Object.new.flamegraph open: true, io: io do + end + + expect(flamegraph).to have_received(:open) + end + end + + context "open: false" do + it "doesn't open" do + Object.new.flamegraph open: false, io: io do + end + + expect(flamegraph).to_not have_received(:open) + end + end +end