ruby_memcheck 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7d7e2556e802cceebbd1607895ed319b54037e2a945586f8fe65e9f6bda0928c
4
- data.tar.gz: 3cba76f62de7ad396458d651a272d660456e362a85c1c6a67ac6ae688b3e4c30
3
+ metadata.gz: 149d43766feab55baa7f6822767ec4d68ae465668fc6183c8ad9cca7b4bd7955
4
+ data.tar.gz: 1d97e5ac70355a3b14f14e7e7acbba925b8d4e43a1907e5ceb0af7de29a60732
5
5
  SHA512:
6
- metadata.gz: 6e849c1bcfc9947293939452298c344acd71136dd7b617bb3edfc07c7b8122a9bc366e1e57a83d86293ed284569121185d465fd9e47959a9e9e0b6579b212022
7
- data.tar.gz: 3131ae9a995bcd2e9f9fcab7f7bfb8b99c3449eadb12d9bfad0366681971d3c3fa7234e194fbf7e5c9a41583d18814cc59aad7802836ba1d075e0d6893be0e1d
6
+ metadata.gz: f1953d6feaba3701b695d0c8d84086efa586ce05e2da518b4e68e8e8b5f8a615e35360ce36a32e5f61a489bc3bb88517b48353a644a72cb2f902edb6ff34e633
7
+ data.tar.gz: a95a1a75f17ddafb64a2e767d0a32ce5aa0b941e5bd4522a9ab5b3bbe5fc647c49579014fe09571f27b64e543521d6327b3dbde4e066044005696edc13442a39
data/.rubocop.yml CHANGED
@@ -2,6 +2,7 @@ inherit_gem:
2
2
  rubocop-shopify: rubocop.yml
3
3
 
4
4
  AllCops:
5
+ TargetRubyVersion: 2.6
5
6
  SuggestExtensions: false
6
7
 
7
8
  Style/GlobalVars:
data/README.md CHANGED
@@ -9,7 +9,7 @@ This gem provides a sane way to use Valgrind's memcheck on your native extension
9
9
  1. [How does it work?](#how-does-it-work)
10
10
  1. [Limitations](#limitations)
11
11
  1. [Installation](#installation)
12
- 1. [Usage](#usage)
12
+ 1. [Setup](#setup)
13
13
 
14
14
  ## What is this gem?
15
15
 
@@ -38,15 +38,13 @@ Because of the aggressive heuristics used to filter out false-positives, there a
38
38
 
39
39
  ## Installation
40
40
 
41
- Add this line to your application's Gemfile:
42
-
43
- ```ruby
44
- gem "ruby_memcheck"
41
+ ```
42
+ gem install ruby_memcheck
45
43
  ```
46
44
 
47
- ## Usage
45
+ ## Setup
48
46
 
49
- The easiest way to use this gem is to use it on your test suite using rake.
47
+ The easiest way to use this gem is to use it on your test suite (minitest or RSpec) using rake.
50
48
 
51
49
  0. Install Valgrind.
52
50
  1. In your Rakefile, require this gem.
@@ -54,43 +52,69 @@ The easiest way to use this gem is to use it on your test suite using rake.
54
52
  ```ruby
55
53
  require "ruby_memcheck"
56
54
  ```
55
+
56
+ - **For RSpec:** If you're using RSpec, also add the following require.
57
+
58
+ ```ruby
59
+ require "ruby_memcheck/rspec/rake_task"
60
+ ```
61
+
57
62
  1. Configure the gem by calling `RubyMemcheck.config`. You must pass it your binary name. This is the same value you passed into `create_makefile` in your `extconf.rb` file. Make sure this value is correct or it will filter out almost everything as a false-positive!
58
63
 
59
64
  ```ruby
60
65
  RubyMemcheck.config(binary_name: "your_binary_name")
61
66
  ```
62
- 1. Locate your test task(s) in your Rakefile. You can identify it with a call to `Rake::TestTask.new`.
63
- 1. Create a namespace under the test task and create a `RubyMemcheck::TestTask` with the same configuration.
67
+ 1. Setup the test task for your test framework.
68
+ - **minitest**
69
+
70
+ Locate your test task(s) in your Rakefile. You can identify it with a call to `Rake::TestTask.new`.
64
71
 
65
- For example, if your Rakefile looked like this before:
72
+ Create a namespace under the test task and create a `RubyMemcheck::TestTask` with the same configuration.
66
73
 
67
- ```ruby
68
- Rake::TestTask.new(test: :compile) do |t|
69
- t.libs << "test"
70
- t.test_files = FileList["test/unit/**/*_test.rb"]
71
- end
72
- ```
74
+ For example, if your Rakefile looked like this before:
73
75
 
74
- You can change it to look like this:
76
+ ```ruby
77
+ Rake::TestTask.new(test: :compile) do |t|
78
+ t.libs << "test"
79
+ t.test_files = FileList["test/unit/**/*_test.rb"]
80
+ end
81
+ ```
75
82
 
76
- ```ruby
77
- test_config = lambda do |t|
78
- t.libs << "test"
79
- t.test_files = FileList["test/**/*_test.rb"]
80
- end
81
- Rake::TestTask.new(test: :compile, &test_config)
82
- namespace :test do
83
- RubyMemcheck::TestTask.new(valgrind: :compile, &test_config)
84
- end
85
- ```
86
- 1. At the top of your `test_helper.rb`/`spec_helper.rb` (or whatever file sets up your test suite), add this line:
83
+ You can change it to look like this:
87
84
 
88
- ```ruby
89
- at_exit { GC.start }
90
- ```
85
+ ```ruby
86
+ test_config = lambda do |t|
87
+ t.libs << "test"
88
+ t.test_files = FileList["test/**/*_test.rb"]
89
+ end
90
+ Rake::TestTask.new(test: :compile, &test_config)
91
+ namespace :test do
92
+ RubyMemcheck::TestTask.new(valgrind: :compile, &test_config)
93
+ end
94
+ ```
95
+
96
+ - **RSpec**
97
+
98
+ Locate your rake task(s) in your Rakefile. You can identify it with a call to `RSpec::Core::RakeTask.new`.
99
+
100
+ Create a namespace under the test task and create a `RubyMemcheck::RSpec::RakeTask` with the same configuration.
101
+
102
+ For example, if your Rakefile looked like this before:
103
+
104
+ ```ruby
105
+ RubyMemcheck::RSpec::RakeTask.new(spec: :compile)
106
+ ```
107
+
108
+ You can change it to look like this:
109
+
110
+ ```ruby
111
+ RubyMemcheck::RSpec::RakeTask.new(spec: :compile)
112
+ namespace :spec do
113
+ RubyMemcheck::RSpec::RakeTask.new(valgrind: :compile)
114
+ end
115
+ ```
91
116
 
92
- Place this line as close to the top of the file as possible, before any requires in the file (especially before the call to `require "minitest/autorun"`). This will ensure that the Garbage Collector is ran before Ruby shuts down. This will reduce the number of false-positives.
93
- 1. You're ready to run your test suite with Valgrind using `rake test:valgrind`! Note that this will take a while to run because Valgrind will make Ruby significantly slower.
117
+ 1. You're ready to run your test suite with Valgrind using `rake test:valgrind` or `rake spec:valgrind`! Note that this will take a while to run because Valgrind will make Ruby significantly slower.
94
118
  1. (Optional) If you find false-positives in the output, you can create suppression files in a `suppressions` directory in the root directory of your gem. In this directory, you can create [Valgrind suppression files](https://wiki.wxwidgets.org/Valgrind_Suppression_File_Howto). The most basic suppression file is `your_binary_name_ruby.supp`. If you want some suppressions for only specific versions of Ruby, you can add the Ruby version to the filename. For example, `your_binary_name_ruby-3.supp` will suppress for any Rubies with a major version of 3 (e.g. 3.0.0, 3.1.1, etc.), while suppression file `your_binary_name_ruby-3.1.supp` will only be used for Ruby with a major and minor version of 3.1 (e.g. 3.1.0, 3.1.1, etc.).
95
119
 
96
120
  ## License
@@ -6,26 +6,33 @@ module RubyMemcheck
6
6
  DEFAULT_VALGRIND_OPTIONS = [
7
7
  "--num-callers=50",
8
8
  "--error-limit=no",
9
+ "--trace-children=yes",
9
10
  "--undef-value-errors=no",
10
11
  "--leak-check=full",
11
12
  "--show-leak-kinds=definite",
12
13
  ].freeze
13
14
  DEFAULT_VALGRIND_SUPPRESSIONS_DIR = "suppressions"
14
15
  DEFAULT_SKIPPED_RUBY_FUNCTIONS = [
16
+ /\Aeval_string_with_cref\z/,
17
+ /\Arb_add_method_cfunc\z/,
15
18
  /\Arb_check_funcall/,
19
+ /\Arb_class_boot\z/,
16
20
  /\Arb_enc_raise\z/,
17
21
  /\Arb_exc_raise\z/,
22
+ /\Arb_extend_object\z/,
18
23
  /\Arb_funcall/,
19
24
  /\Arb_intern/,
20
25
  /\Arb_ivar_set\z/,
26
+ /\Arb_module_new\z/,
21
27
  /\Arb_raise\z/,
22
28
  /\Arb_rescue/,
23
29
  /\Arb_respond_to\z/,
24
30
  /\Arb_yield/,
25
31
  ].freeze
26
32
 
27
- attr_reader :binary_name, :ruby, :valgrind_options, :valgrind,
28
- :skipped_ruby_functions, :valgrind_xml_file, :output_io
33
+ attr_reader :binary_name, :ruby, :valgrind, :valgrind_options, :valgrind_suppressions_dir,
34
+ :valgrind_generate_suppressions, :skipped_ruby_functions, :valgrind_xml_dir, :output_io
35
+ alias_method :valgrind_generate_suppressions?, :valgrind_generate_suppressions
29
36
 
30
37
  def initialize(
31
38
  binary_name:,
@@ -33,30 +40,43 @@ module RubyMemcheck
33
40
  valgrind: DEFAULT_VALGRIND,
34
41
  valgrind_options: DEFAULT_VALGRIND_OPTIONS,
35
42
  valgrind_suppressions_dir: DEFAULT_VALGRIND_SUPPRESSIONS_DIR,
43
+ valgrind_generate_suppressions: false,
36
44
  skipped_ruby_functions: DEFAULT_SKIPPED_RUBY_FUNCTIONS,
37
- valgrind_xml_file: Tempfile.new,
45
+ valgrind_xml_dir: Dir.mktmpdir,
38
46
  output_io: $stderr
39
47
  )
40
48
  @binary_name = binary_name
41
49
  @ruby = ruby
42
50
  @valgrind = valgrind
43
- @valgrind_options =
44
- valgrind_options +
45
- get_valgrind_suppression_files(valgrind_suppressions_dir).map { |f| "--suppressions=#{f}" }
51
+ @valgrind_options = valgrind_options
52
+ @valgrind_suppressions_dir = File.expand_path(valgrind_suppressions_dir)
53
+ @valgrind_generate_suppressions = valgrind_generate_suppressions
46
54
  @skipped_ruby_functions = skipped_ruby_functions
47
55
  @output_io = output_io
48
56
 
49
- if valgrind_xml_file
50
- @valgrind_xml_file = valgrind_xml_file
57
+ if valgrind_xml_dir
58
+ valgrind_xml_dir = File.expand_path(valgrind_xml_dir)
59
+ FileUtils.mkdir_p(valgrind_xml_dir)
60
+ @valgrind_xml_dir = valgrind_xml_dir
51
61
  @valgrind_options += [
52
62
  "--xml=yes",
53
- "--xml-file=#{valgrind_xml_file.path}",
63
+ # %p will be replaced with the PID
64
+ # This prevents forking and shelling out from generating a corrupted XML
65
+ # See --log-file from https://valgrind.org/docs/manual/manual-core.html
66
+ "--xml-file=#{File.join(valgrind_xml_dir, "%p.out")}",
54
67
  ]
55
68
  end
56
69
  end
57
70
 
58
71
  def command(*args)
59
- "#{valgrind} #{valgrind_options.join(" ")} #{ruby} #{args.join(" ")}"
72
+ [
73
+ valgrind,
74
+ valgrind_options,
75
+ get_valgrind_suppression_files(valgrind_suppressions_dir).map { |f| "--suppressions=#{f}" },
76
+ valgrind_generate_suppressions ? "--gen-suppressions=all" : "",
77
+ ruby,
78
+ args,
79
+ ].flatten.join(" ")
60
80
  end
61
81
 
62
82
  def skip_stack?(stack)
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rspec/core/rake_task"
4
+
5
+ module RubyMemcheck
6
+ module RSpec
7
+ class RakeTask < ::RSpec::Core::RakeTask
8
+ include TestTaskReporter
9
+
10
+ attr_reader :configuration
11
+
12
+ def initialize(*args)
13
+ @configuration =
14
+ if !args.empty? && args[0].is_a?(Configuration)
15
+ args.shift
16
+ else
17
+ RubyMemcheck.default_configuration
18
+ end
19
+
20
+ super
21
+ end
22
+
23
+ def run_task(verbose)
24
+ error = nil
25
+
26
+ begin
27
+ # RSpec::Core::RakeTask#run_task calls Kernel.exit on failure
28
+ super
29
+ rescue SystemExit => e
30
+ error = e
31
+ end
32
+
33
+ report_valgrind_errors
34
+
35
+ raise error if error
36
+ end
37
+
38
+ private
39
+
40
+ def spec_command
41
+ # First part of command is Ruby
42
+ args = super.split(" ")[1..]
43
+
44
+ configuration.command(args)
45
+ end
46
+ end
47
+ end
48
+ end
@@ -9,8 +9,6 @@ module RubyMemcheck
9
9
  end
10
10
 
11
11
  def to_s
12
- return "" if root.nil?
13
-
14
12
  str = StringIO.new
15
13
  str << "{\n"
16
14
  str << " #{root.at_xpath("sname").content}\n"
@@ -2,9 +2,9 @@
2
2
 
3
3
  module RubyMemcheck
4
4
  class TestTask < Rake::TestTask
5
- VALGRIND_REPORT_MSG = "Valgrind reported errors (e.g. memory leak or use-after-free)"
5
+ include TestTaskReporter
6
6
 
7
- attr_reader :configuration, :errors
7
+ attr_reader :configuration
8
8
 
9
9
  def initialize(*args)
10
10
  @configuration =
@@ -20,39 +20,10 @@ module RubyMemcheck
20
20
  def ruby(*args, **options, &block)
21
21
  command = configuration.command(args)
22
22
  sh(command, **options) do |ok, res|
23
- if configuration.valgrind_xml_file
24
- parse_valgrind_output
25
- unless errors.empty?
26
- output_valgrind_errors
27
- raise VALGRIND_REPORT_MSG
28
- end
29
- end
23
+ report_valgrind_errors
30
24
 
31
25
  yield ok, res if block_given?
32
26
  end
33
27
  end
34
-
35
- private
36
-
37
- def parse_valgrind_output
38
- require "nokogiri"
39
-
40
- @errors = []
41
-
42
- Nokogiri::XML::Reader(File.open(configuration.valgrind_xml_file.to_path)).each do |node|
43
- next unless node.name == "error" && node.node_type == Nokogiri::XML::Reader::TYPE_ELEMENT
44
- error_xml = Nokogiri::XML::Document.parse(node.outer_xml).root
45
- error = ValgrindError.new(configuration, error_xml)
46
- next if error.skip?
47
- @errors << error
48
- end
49
- end
50
-
51
- def output_valgrind_errors
52
- @errors.each do |error|
53
- configuration.output_io.puts error
54
- configuration.output_io.puts
55
- end
56
- end
57
28
  end
58
29
  end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyMemcheck
4
+ module TestTaskReporter
5
+ VALGRIND_REPORT_MSG = "Valgrind reported errors (e.g. memory leak or use-after-free)"
6
+
7
+ attr_reader :errors
8
+
9
+ private
10
+
11
+ def report_valgrind_errors
12
+ if configuration.valgrind_xml_dir
13
+ parse_valgrind_output
14
+ remove_valgrind_xml_files
15
+
16
+ unless errors.empty?
17
+ output_valgrind_errors
18
+ raise VALGRIND_REPORT_MSG
19
+ end
20
+ end
21
+ end
22
+
23
+ def parse_valgrind_output
24
+ require "nokogiri"
25
+
26
+ @errors = []
27
+
28
+ Dir[File.join(configuration.valgrind_xml_dir, "*")].each do |file|
29
+ Nokogiri::XML::Reader(File.open(file)).each do |node|
30
+ next unless node.name == "error" && node.node_type == Nokogiri::XML::Reader::TYPE_ELEMENT
31
+ error_xml = Nokogiri::XML::Document.parse(node.outer_xml).root
32
+ error = ValgrindError.new(configuration, error_xml)
33
+ next if error.skip?
34
+ @errors << error
35
+ end
36
+ end
37
+ end
38
+
39
+ def output_valgrind_errors
40
+ @errors.each do |error|
41
+ configuration.output_io.puts error
42
+ configuration.output_io.puts
43
+ end
44
+ end
45
+
46
+ def remove_valgrind_xml_files
47
+ FileUtils.rm_rf(configuration.valgrind_xml_dir)
48
+ end
49
+ end
50
+ end
@@ -2,6 +2,9 @@
2
2
 
3
3
  module RubyMemcheck
4
4
  class ValgrindError
5
+ SUPPRESSION_NOT_CONFIGURED_ERROR_MSG =
6
+ "Please enable suppressions by configuring with valgrind_generate_suppressions set to true"
7
+
5
8
  attr_reader :kind, :msg, :stack, :suppression
6
9
 
7
10
  def initialize(configuration, error)
@@ -14,7 +17,13 @@ module RubyMemcheck
14
17
  end
15
18
  @stack = Stack.new(configuration, error.at_xpath("stack"))
16
19
  @configuration = configuration
17
- @suppression = Suppression.new(configuration, error.at_xpath("suppression"))
20
+
21
+ suppression_node = error.at_xpath("suppression")
22
+ if configuration.valgrind_generate_suppressions?
23
+ @suppression = Suppression.new(configuration, suppression_node)
24
+ elsif suppression_node
25
+ raise SUPPRESSION_NOT_CONFIGURED_ERROR_MSG
26
+ end
18
27
  end
19
28
 
20
29
  def skip?
@@ -35,7 +44,7 @@ module RubyMemcheck
35
44
  " #{frame}\n"
36
45
  end
37
46
  end
38
- str << suppression.to_s
47
+ str << suppression.to_s if suppression
39
48
  str.string
40
49
  end
41
50
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubyMemcheck
4
- VERSION = "0.2.1"
4
+ VERSION = "0.3.0"
5
5
  end
data/lib/ruby_memcheck.rb CHANGED
@@ -6,6 +6,7 @@ require "rake/testtask"
6
6
  require "ruby_memcheck/configuration"
7
7
  require "ruby_memcheck/frame"
8
8
  require "ruby_memcheck/stack"
9
+ require "ruby_memcheck/test_task_reporter"
9
10
  require "ruby_memcheck/test_task"
10
11
  require "ruby_memcheck/valgrind_error"
11
12
  require "ruby_memcheck/suppression"
@@ -27,8 +27,10 @@ Gem::Specification.new do |spec|
27
27
  spec.add_dependency("nokogiri")
28
28
 
29
29
  spec.add_development_dependency("minitest", "~> 5.0")
30
+ spec.add_development_dependency("minitest-parallel_fork", "~> 1.2")
30
31
  spec.add_development_dependency("rake", "~> 13.0")
31
32
  spec.add_development_dependency("rake-compiler", "~> 1.1")
33
+ spec.add_development_dependency("rspec-core")
32
34
  spec.add_development_dependency("rubocop", "~> 1.22")
33
35
  spec.add_development_dependency("rubocop-shopify", "~> 2.3")
34
36
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby_memcheck
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter Zhu
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-10-22 00:00:00.000000000 Z
11
+ date: 2021-11-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: nokogiri
@@ -38,6 +38,20 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: '5.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: minitest-parallel_fork
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.2'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.2'
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: rake
43
57
  requirement: !ruby/object:Gem::Requirement
@@ -66,6 +80,20 @@ dependencies:
66
80
  - - "~>"
67
81
  - !ruby/object:Gem::Version
68
82
  version: '1.1'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rspec-core
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
69
97
  - !ruby/object:Gem::Dependency
70
98
  name: rubocop
71
99
  requirement: !ruby/object:Gem::Requirement
@@ -112,9 +140,11 @@ files:
112
140
  - lib/ruby_memcheck.rb
113
141
  - lib/ruby_memcheck/configuration.rb
114
142
  - lib/ruby_memcheck/frame.rb
143
+ - lib/ruby_memcheck/rspec/rake_task.rb
115
144
  - lib/ruby_memcheck/stack.rb
116
145
  - lib/ruby_memcheck/suppression.rb
117
146
  - lib/ruby_memcheck/test_task.rb
147
+ - lib/ruby_memcheck/test_task_reporter.rb
118
148
  - lib/ruby_memcheck/valgrind_error.rb
119
149
  - lib/ruby_memcheck/version.rb
120
150
  - ruby_memcheck.gemspec
@@ -138,7 +168,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
138
168
  - !ruby/object:Gem::Version
139
169
  version: '0'
140
170
  requirements: []
141
- rubygems_version: 3.3.0.dev
171
+ rubygems_version: 3.2.22
142
172
  signing_key:
143
173
  specification_version: 4
144
174
  summary: Use Valgrind memcheck without going crazy