ruby_memcheck 0.1.1 → 1.0.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: 2ec5f6d45b73b374080c51c74cf3e771ee605e3b32750c61f6a48ecc4a8ce794
4
- data.tar.gz: 4202115ba33f703884c6e8574ceacc20900c2fc530c0e54fb69557b29378cef1
3
+ metadata.gz: 751718a23aee213973e93656dba653e4e253fd3f83618007c39e809827273ac5
4
+ data.tar.gz: 55d6196fb48e0c18fabb5c87536c2c4114d7a1c1a2b04128ec2c707389934de0
5
5
  SHA512:
6
- metadata.gz: 6ab4a3e446597018407ba9cc6ffe0558e9da050f25c2cbef0ab070bb52d412941f71c12e93955d66e9b6e79340178ff4faa9dcf4017bdfcef8c339493ec547ed
7
- data.tar.gz: 6a25b3a25a8e0d093fca6f87a0f5bb508ee3f54fb9d853935ca6bd6e036e9635faff5e4e0fbf47eb3784a79f6bdc0ec6751691fedc3c3162562599abb23e7e9a
6
+ metadata.gz: 861d0639b3d06d124f4020cb372472954018ad113ba4864b9367be6c6d167a338297d18c7c4adbcae767ba4a30e8925402bd7faa5a97841a513914ce41e10997
7
+ data.tar.gz: e0dc1e236947746baef93281f00ec8129f651b7e560e56d2dffc5c188f14e5b32b39adc204ecea2da51c1e9ed341f1c00ff55b4b82521bb2b7b54085d5bfddfc
data/.rubocop.yml CHANGED
@@ -2,4 +2,9 @@ inherit_gem:
2
2
  rubocop-shopify: rubocop.yml
3
3
 
4
4
  AllCops:
5
+ TargetRubyVersion: 2.6
5
6
  SuggestExtensions: false
7
+
8
+ Style/GlobalVars:
9
+ Exclude:
10
+ - test/ruby_memcheck/ext/extconf.rb
data/README.md CHANGED
@@ -9,11 +9,14 @@ 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
+ 1. [Configuration](#configuration)
14
+ 1. [Suppression files](#suppression-files)
15
+ 1. [License](#license)
13
16
 
14
17
  ## What is this gem?
15
18
 
16
- Valgrind's memcheck is a great tool to find and debug memory issues (e.g. memory leak, use-after-free, etc.). However, it doesn't work well on Ruby because Ruby does not free all of the memory it allocates during shutdown. This results in Valgrind reporting thousands (or more) false-positives, making it very difficult for Valgrind to actually be useful. This gem solves the problem by using heuristics to filter out false-positives.
19
+ Valgrind's memcheck is a great tool to find and debug memory issues (e.g. memory leak, use-after-free, etc.). However, it doesn't work well on Ruby because Ruby does not free all of the memory it allocates during shutdown. This results in Valgrind reporting thousands (or more) false positives, making it very difficult for Valgrind to actually be useful. This gem solves the problem by using heuristics to filter out false positives.
17
20
 
18
21
  ### Who should use this gem?
19
22
 
@@ -21,11 +24,11 @@ Only gems with native extensions can use this gem. If your gem is written in pla
21
24
 
22
25
  ### How does it work?
23
26
 
24
- This gem runs Valgrind with the `--xml` option to generate a XML of all the errors. It will then parse the XML and use various heuristics based on the type of the error and the stack trace to filter out errors that are false-positives.
27
+ This gem runs Valgrind with the `--xml` option to generate an XML of all the errors. It will then parse the XML and use various heuristics based on the type of the error and the stack trace to filter out errors that are false positives.
25
28
 
26
29
  ### Limitations
27
30
 
28
- Because of the aggressive heuristics used to filter out false-positives, there are various limitations of what this gem can detect.
31
+ Because of the aggressive heuristics used to filter out false positives, there are various limitations of what this gem can detect.
29
32
 
30
33
  1. This gem is only expected to work on Linux.
31
34
  1. It will not find memory leaks in Ruby. It filters out everything in Ruby.
@@ -38,15 +41,13 @@ Because of the aggressive heuristics used to filter out false-positives, there a
38
41
 
39
42
  ## Installation
40
43
 
41
- Add this line to your application's Gemfile:
42
-
43
- ```ruby
44
- gem "ruby_memcheck"
44
+ ```
45
+ gem install ruby_memcheck
45
46
  ```
46
47
 
47
- ## Usage
48
+ ## Setup
48
49
 
49
- The easiest way to use this gem is to use it on your test suite using rake.
50
+ The easiest way to use this gem is to use it on your test suite (minitest or RSpec) using rake.
50
51
 
51
52
  0. Install Valgrind.
52
53
  1. In your Rakefile, require this gem.
@@ -54,43 +55,110 @@ The easiest way to use this gem is to use it on your test suite using rake.
54
55
  ```ruby
55
56
  require "ruby_memcheck"
56
57
  ```
58
+
59
+ - **For RSpec:** If you're using RSpec, also add the following require.
60
+
61
+ ```ruby
62
+ require "ruby_memcheck/rspec/rake_task"
63
+ ```
64
+
57
65
  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
66
 
59
67
  ```ruby
60
68
  RubyMemcheck.config(binary_name: "your_binary_name")
61
69
  ```
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.
70
+ 1. Setup the test task for your test framework.
71
+ - **minitest**
72
+
73
+ Locate your test task(s) in your Rakefile. You can identify it with a call to `Rake::TestTask.new`.
64
74
 
65
- For example, if your Rakefile looked like this before:
75
+ Create a namespace under the test task and create a `RubyMemcheck::TestTask` with the same configuration.
66
76
 
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
- ```
77
+ For example, if your Rakefile looked like this before:
73
78
 
74
- You can change it to look like this:
79
+ ```ruby
80
+ Rake::TestTask.new(test: :compile) do |t|
81
+ t.libs << "test"
82
+ t.test_files = FileList["test/unit/**/*_test.rb"]
83
+ end
84
+ ```
75
85
 
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. In your `test_helper.rb`/`spec_helper.rb` (or whatever file sets up your test suite), add this line:
86
+ You can change it to look like this:
87
87
 
88
- ```ruby
89
- END { GC.start }
90
- ```
88
+ ```ruby
89
+ test_config = lambda do |t|
90
+ t.libs << "test"
91
+ t.test_files = FileList["test/**/*_test.rb"]
92
+ end
93
+ Rake::TestTask.new(test: :compile, &test_config)
94
+ namespace :test do
95
+ RubyMemcheck::TestTask.new(valgrind: :compile, &test_config)
96
+ end
97
+ ```
98
+
99
+ - **RSpec**
100
+
101
+ Locate your rake task(s) in your Rakefile. You can identify it with a call to `RSpec::Core::RakeTask.new`.
102
+
103
+ Create a namespace under the test task and create a `RubyMemcheck::RSpec::RakeTask` with the same configuration.
104
+
105
+ For example, if your Rakefile looked like this before:
106
+
107
+ ```ruby
108
+ RubyMemcheck::RSpec::RakeTask.new(spec: :compile)
109
+ ```
110
+
111
+ You can change it to look like this:
112
+
113
+ ```ruby
114
+ RubyMemcheck::RSpec::RakeTask.new(spec: :compile)
115
+ namespace :spec do
116
+ RubyMemcheck::RSpec::RakeTask.new(valgrind: :compile)
117
+ end
118
+ ```
119
+
120
+ 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.
121
+ 1. (Optional) If you find false positives in the output, you can create Valgrind suppression files. See the [`Suppression files`](#suppression-files) section for more details.
122
+
123
+ ## Configuration
124
+
125
+ When you run `RubyMemcheck.config`, you are creating a default `RubyMemcheck::Configuration`. By default, the Rake tasks for minitest and RSpec will use this configuration. You can also manually pass in a `Configuration` object as the first argument to the constructor of `RubyMemcheck::TestTask` or `RubyMemcheck::RSpec::RakeTask` to use a different `Configuration` object rather than the default one.
126
+
127
+ `RubyMemcheck::Configuration` accepts a variety of keyword arguments. Here are all the arguments:
128
+
129
+ - `binary_name`: Required. The binary name of your native extension gem. This is the same value you passed into `create_makefile` in your `extconf.rb` file.
130
+ - `ruby`: Optional. The command to run to invoke Ruby. Defaults to the Ruby that is currently being used.
131
+ - `valgrind`: Optional. The command to run to invoke Valgrind. Defaults to the string `"valgrind"`.
132
+ - `valgrind_options`: Optional. Array of options to pass into Valgrind. This is only present as an escape hatch, so avoid using it. This may be deprecated or removed in future versions.
133
+ - `valgrind_suppressions_dir`: Optional. The string path of the directory that stores suppression files for Valgrind. See the [`Suppression files`](#suppression-files) section for more details. Defaults to `suppressions`.
134
+ - `valgrind_generate_suppressions`: Optional. Whether suppressions should also be outputted along with the errors. the [`Suppression files`](#suppression-files) section for more details. Defaults to `false`.
135
+ - `skipped_ruby_functions`: Optional. Ruby functions that are ignored because they are considered a call back into Ruby. This is only present as an escape hatch, so avoid using it. If you find another Ruby function that is a false positive because it calls back into Ruby, please send a patch into this repo. Otherwise, use a Valgrind suppression file.
136
+ - `valgrind_xml_dir`: Optional. The directory to store temporary XML files for Valgrind. It defaults to a temporary directory. This is present for development debugging, so you shouldn't have to use it.
137
+ - `output_io`: Optional. The `IO` object to output Valgrind errors to. Defaults to standard error.
138
+
139
+ ## Suppression files
140
+
141
+ 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).
142
+
143
+ 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.).
144
+
145
+ ## Success stories
146
+
147
+ Let's celebrate wins from this gem! If this gem was useful for you, please share your story below too!
91
148
 
92
- 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.
149
+ - [`liquid-c`](https://github.com/Shopify/liquid-c):
150
+ - Found 2 memory leaks: [#157](https://github.com/Shopify/liquid-c/pull/157), [#161](https://github.com/Shopify/liquid-c/pull/161)
151
+ - Running on CI: [#162](https://github.com/Shopify/liquid-c/pull/162)
152
+ - [`nokogiri`](https://github.com/sparklemotion/nokogiri):
153
+ - Found 5 memory leaks: [4 in #2345](https://github.com/sparklemotion/nokogiri/pull/2345), [#2347](https://github.com/sparklemotion/nokogiri/pull/2347)
154
+ - CI is WIP: [#2344](https://github.com/sparklemotion/nokogiri/pull/2344)
155
+ - [`rotoscope`](https://github.com/Shopify/rotoscope):
156
+ - Found a [memory leak in Ruby TracePoint](https://bugs.ruby-lang.org/issues/18264)
157
+ - Running on CI: [#89](https://github.com/Shopify/rotoscope/pull/89)
158
+ - [`protobuf`](https://github.com/protocolbuffers/protobuf):
159
+ - Found 1 memory leak: [#9150](https://github.com/protocolbuffers/protobuf/pull/9150)
160
+ - [`gRPC`](https://github.com/grpc/grpc):
161
+ - Found 1 memory leak: [#27900](https://github.com/grpc/grpc/pull/27900)
94
162
 
95
163
  ## License
96
164
 
@@ -6,87 +6,91 @@ 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
14
+ DEFAULT_VALGRIND_SUPPRESSIONS_DIR = "suppressions"
13
15
  DEFAULT_SKIPPED_RUBY_FUNCTIONS = [
16
+ /\Aeval_string_with_cref\z/,
17
+ /\Arb_add_method_cfunc\z/,
14
18
  /\Arb_check_funcall/,
19
+ /\Arb_class_boot\z/, # Called for all the different ways to create a Class
15
20
  /\Arb_enc_raise\z/,
21
+ /\Arb_exc_raise\z/,
22
+ /\Arb_extend_object\z/,
16
23
  /\Arb_funcall/,
24
+ /\Arb_intern/,
17
25
  /\Arb_ivar_set\z/,
26
+ /\Arb_module_new\z/,
18
27
  /\Arb_raise\z/,
19
28
  /\Arb_rescue/,
20
29
  /\Arb_respond_to\z/,
30
+ /\Arb_thread_create\z/, # Threads are relased to a cache, so they may be reported as a leak
21
31
  /\Arb_yield/,
22
32
  ].freeze
23
33
 
24
- attr_reader :binary_name, :ruby, :valgrind_options, :valgrind,
25
- :skipped_ruby_functions, :valgrind_xml_file, :output_io
34
+ attr_reader :binary_name, :ruby, :valgrind, :valgrind_options, :valgrind_suppressions_dir,
35
+ :valgrind_generate_suppressions, :skipped_ruby_functions, :valgrind_xml_dir, :output_io
36
+ alias_method :valgrind_generate_suppressions?, :valgrind_generate_suppressions
26
37
 
27
38
  def initialize(
28
39
  binary_name:,
29
40
  ruby: FileUtils::RUBY,
30
41
  valgrind: DEFAULT_VALGRIND,
31
42
  valgrind_options: DEFAULT_VALGRIND_OPTIONS,
43
+ valgrind_suppressions_dir: DEFAULT_VALGRIND_SUPPRESSIONS_DIR,
44
+ valgrind_generate_suppressions: false,
32
45
  skipped_ruby_functions: DEFAULT_SKIPPED_RUBY_FUNCTIONS,
33
- valgrind_xml_file: Tempfile.new,
46
+ valgrind_xml_dir: Dir.mktmpdir,
34
47
  output_io: $stderr
35
48
  )
36
49
  @binary_name = binary_name
37
50
  @ruby = ruby
38
51
  @valgrind = valgrind
39
52
  @valgrind_options = valgrind_options
53
+ @valgrind_suppressions_dir = File.expand_path(valgrind_suppressions_dir)
54
+ @valgrind_generate_suppressions = valgrind_generate_suppressions
40
55
  @skipped_ruby_functions = skipped_ruby_functions
41
56
  @output_io = output_io
42
57
 
43
- if valgrind_xml_file
44
- @valgrind_xml_file = valgrind_xml_file
58
+ if valgrind_xml_dir
59
+ valgrind_xml_dir = File.expand_path(valgrind_xml_dir)
60
+ FileUtils.mkdir_p(valgrind_xml_dir)
61
+ @valgrind_xml_dir = valgrind_xml_dir
45
62
  @valgrind_options += [
46
63
  "--xml=yes",
47
- "--xml-file=#{valgrind_xml_file.path}",
64
+ # %p will be replaced with the PID
65
+ # This prevents forking and shelling out from generating a corrupted XML
66
+ # See --log-file from https://valgrind.org/docs/manual/manual-core.html
67
+ "--xml-file=#{File.join(valgrind_xml_dir, "%p.out")}",
48
68
  ]
49
69
  end
50
70
  end
51
71
 
52
72
  def command(*args)
53
- "#{valgrind} #{valgrind_options.join(" ")} #{ruby} #{args.join(" ")}"
73
+ [
74
+ valgrind,
75
+ valgrind_options,
76
+ get_valgrind_suppression_files(valgrind_suppressions_dir).map { |f| "--suppressions=#{f}" },
77
+ valgrind_generate_suppressions ? "--gen-suppressions=all" : "",
78
+ ruby,
79
+ args,
80
+ ].flatten.join(" ")
54
81
  end
55
82
 
56
- def skip_stack?(stack)
57
- in_binary = false
83
+ private
58
84
 
59
- stack.frames.each do |frame|
60
- fn = frame.fn
85
+ def get_valgrind_suppression_files(dir)
86
+ full_ruby_version = "#{RUBY_ENGINE}-#{RUBY_VERSION}.#{RUBY_PATCHLEVEL}"
87
+ versions = [full_ruby_version]
88
+ (0..3).reverse_each { |i| versions << full_ruby_version.split(".")[0, i].join(".") }
89
+ versions << RUBY_ENGINE
61
90
 
62
- if frame_in_ruby?(frame) # in ruby
63
- unless in_binary
64
- # Skip this stack because it was called from Ruby
65
- return true if skipped_ruby_functions.any? { |r| r.match?(fn) }
66
- end
67
- elsif frame_in_binary?(frame) # in binary
68
- in_binary = true
69
-
70
- # Skip the Init function
71
- return true if fn == "Init_#{binary_name}"
72
- end
73
- end
74
-
75
- !in_binary
76
- end
77
-
78
- def frame_in_ruby?(frame)
79
- frame.obj == ruby ||
80
- # Hack to fix Ruby built with --enabled-shared
81
- File.basename(frame.obj) == "libruby.so.#{RUBY_VERSION}"
82
- end
83
-
84
- def frame_in_binary?(frame)
85
- if frame.obj
86
- File.basename(frame.obj, ".*") == binary_name
87
- else
88
- false
89
- end
91
+ versions.map do |version|
92
+ Dir[File.join(dir, "#{binary_name}_#{version}.supp")]
93
+ end.flatten
90
94
  end
91
95
  end
92
96
  end
@@ -2,17 +2,29 @@
2
2
 
3
3
  module RubyMemcheck
4
4
  class Frame
5
- attr_reader :fn, :obj, :file, :line, :in_binary
6
- alias_method :in_binary?, :in_binary
5
+ attr_reader :configuration, :fn, :obj, :file, :line
7
6
 
8
7
  def initialize(configuration, frame_xml)
8
+ @configuration = configuration
9
9
  @fn = frame_xml.at_xpath("fn")&.content
10
10
  @obj = frame_xml.at_xpath("obj")&.content
11
11
  # file and line may not be available
12
12
  @file = frame_xml.at_xpath("file")&.content
13
13
  @line = frame_xml.at_xpath("line")&.content
14
+ end
15
+
16
+ def in_ruby?
17
+ obj == configuration.ruby ||
18
+ # Hack to fix Ruby built with --enabled-shared
19
+ File.basename(obj) == "libruby.so.#{RUBY_VERSION}"
20
+ end
14
21
 
15
- @in_binary = configuration.frame_in_binary?(self)
22
+ def in_binary?
23
+ if obj
24
+ File.basename(obj, ".*") == configuration.binary_name
25
+ else
26
+ false
27
+ end
16
28
  end
17
29
 
18
30
  def to_s
@@ -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
@@ -2,10 +2,38 @@
2
2
 
3
3
  module RubyMemcheck
4
4
  class Stack
5
- attr_reader :frames
5
+ attr_reader :configuration, :frames
6
6
 
7
7
  def initialize(configuration, stack_xml)
8
+ @configuration = configuration
8
9
  @frames = stack_xml.xpath("frame").map { |frame| Frame.new(configuration, frame) }
9
10
  end
11
+
12
+ def skip?
13
+ in_binary = false
14
+
15
+ frames.each do |frame|
16
+ fn = frame.fn
17
+
18
+ if frame.in_ruby?
19
+ # If a stack from from the binary was encountered first, then this
20
+ # memory leak did not occur from Ruby
21
+ unless in_binary
22
+ # Skip this stack because it was called from Ruby
23
+ return true if configuration.skipped_ruby_functions.any? { |r| r.match?(fn) }
24
+ end
25
+ elsif frame.in_binary?
26
+ in_binary = true
27
+
28
+ # Skip the Init function because it is only ever called once, so
29
+ # leaks in it cannot cause memory bloat
30
+ return true if fn == "Init_#{configuration.binary_name}"
31
+ end
32
+ end
33
+
34
+ # Skip if the stack was never in the binary because it is very likely
35
+ # not a leak in the native gem
36
+ !in_binary
37
+ end
10
38
  end
11
39
  end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyMemcheck
4
+ class Suppression
5
+ attr_reader :root
6
+
7
+ def initialize(configuration, suppression_node)
8
+ @root = suppression_node
9
+ end
10
+
11
+ def to_s
12
+ str = StringIO.new
13
+ str << "{\n"
14
+ str << " #{root.at_xpath("sname").content}\n"
15
+ str << " #{root.at_xpath("skind").content}\n"
16
+ root.xpath("./sframe/fun | ./sframe/obj").each do |frame|
17
+ str << " #{frame.name}:#{frame.content}\n"
18
+ end
19
+ str << "}\n"
20
+ str.string
21
+ end
22
+ end
23
+ end
@@ -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,36 +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 ok && 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
- @errors = []
39
-
40
- xml = Nokogiri::XML(configuration.valgrind_xml_file.read)
41
- xml.xpath("/valgrindoutput/error").each do |error_xml|
42
- error = ValgrindError.new(configuration, error_xml)
43
- next if error.skip?
44
- @errors << error
45
- end
46
- end
47
-
48
- def output_valgrind_errors
49
- @errors.each do |error|
50
- configuration.output_io.puts error
51
- configuration.output_io.puts
52
- end
53
- end
54
28
  end
55
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,7 +2,10 @@
2
2
 
3
3
  module RubyMemcheck
4
4
  class ValgrindError
5
- attr_reader :kind, :msg, :stack
5
+ SUPPRESSION_NOT_CONFIGURED_ERROR_MSG =
6
+ "Please enable suppressions by configuring with valgrind_generate_suppressions set to true"
7
+
8
+ attr_reader :kind, :msg, :stack, :suppression
6
9
 
7
10
  def initialize(configuration, error)
8
11
  @kind = error.at_xpath("kind").content
@@ -14,14 +17,17 @@ module RubyMemcheck
14
17
  end
15
18
  @stack = Stack.new(configuration, error.at_xpath("stack"))
16
19
  @configuration = configuration
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
17
27
  end
18
28
 
19
29
  def skip?
20
- if should_filter?
21
- @configuration.skip_stack?(stack)
22
- else
23
- false
24
- end
30
+ should_filter? && stack.skip?
25
31
  end
26
32
 
27
33
  def to_s
@@ -34,6 +40,7 @@ module RubyMemcheck
34
40
  " #{frame}\n"
35
41
  end
36
42
  end
43
+ str << suppression.to_s if suppression
37
44
  str.string
38
45
  end
39
46
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubyMemcheck
4
- VERSION = "0.1.1"
4
+ VERSION = "1.0.0"
5
5
  end
data/lib/ruby_memcheck.rb CHANGED
@@ -1,14 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "tempfile"
4
- require "nokogiri"
5
4
  require "rake/testtask"
6
5
 
7
6
  require "ruby_memcheck/configuration"
8
7
  require "ruby_memcheck/frame"
9
8
  require "ruby_memcheck/stack"
9
+ require "ruby_memcheck/test_task_reporter"
10
10
  require "ruby_memcheck/test_task"
11
11
  require "ruby_memcheck/valgrind_error"
12
+ require "ruby_memcheck/suppression"
12
13
  require "ruby_memcheck/version"
13
14
 
14
15
  module RubyMemcheck
@@ -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.1.1
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter Zhu
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-10-18 00:00:00.000000000 Z
11
+ date: 2021-11-03 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
@@ -94,7 +122,7 @@ dependencies:
94
122
  - - "~>"
95
123
  - !ruby/object:Gem::Version
96
124
  version: '2.3'
97
- description:
125
+ description:
98
126
  email:
99
127
  - peter@peterzhu.ca
100
128
  executables: []
@@ -112,8 +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
145
+ - lib/ruby_memcheck/suppression.rb
116
146
  - lib/ruby_memcheck/test_task.rb
147
+ - lib/ruby_memcheck/test_task_reporter.rb
117
148
  - lib/ruby_memcheck/valgrind_error.rb
118
149
  - lib/ruby_memcheck/version.rb
119
150
  - ruby_memcheck.gemspec
@@ -122,7 +153,7 @@ licenses:
122
153
  - MIT
123
154
  metadata:
124
155
  homepage_uri: https://github.com/peterzhu2118/ruby_memcheck
125
- post_install_message:
156
+ post_install_message:
126
157
  rdoc_options: []
127
158
  require_paths:
128
159
  - lib
@@ -137,8 +168,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
137
168
  - !ruby/object:Gem::Version
138
169
  version: '0'
139
170
  requirements: []
140
- rubygems_version: 3.2.15
141
- signing_key:
171
+ rubygems_version: 3.2.22
172
+ signing_key:
142
173
  specification_version: 4
143
174
  summary: Use Valgrind memcheck without going crazy
144
175
  test_files: []