Repository: Shopify/ruby_memcheck
Branch: main
Commit: f8d1db6ea771
Files: 42
Total size: 57.0 KB
Directory structure:
gitextract_9z716og8/
├── .devcontainer/
│ ├── Dockerfile
│ └── devcontainer.json
├── .github/
│ ├── dependabot.yml
│ └── workflows/
│ ├── cla.yml
│ ├── lint.yml
│ ├── release.yml
│ └── test.yml
├── .gitignore
├── .rubocop.yml
├── .ruby-version
├── Gemfile
├── LICENSE.txt
├── README.md
├── Rakefile
├── exe/
│ └── ruby_memcheck
├── lib/
│ ├── ruby_memcheck/
│ │ ├── configuration.rb
│ │ ├── frame.rb
│ │ ├── rspec/
│ │ │ └── rake_task.rb
│ │ ├── ruby_runner.rb
│ │ ├── stack.rb
│ │ ├── suppression.rb
│ │ ├── test_helper.rb
│ │ ├── test_task.rb
│ │ ├── test_task_reporter.rb
│ │ ├── valgrind_error.rb
│ │ └── version.rb
│ └── ruby_memcheck.rb
├── ruby_memcheck.gemspec
├── suppressions/
│ └── ruby.supp
└── test/
├── ruby_memcheck/
│ ├── ext/
│ │ ├── extconf_one.rb
│ │ ├── extconf_two.rb
│ │ ├── ruby_memcheck_c_test_one.c
│ │ └── ruby_memcheck_c_test_two.c
│ ├── rspec/
│ │ └── rake_task_test.rb
│ ├── ruby_memcheck_suppression_test.rb
│ ├── ruby_runner_test.rb
│ ├── shared_test_task_reporter_tests.rb
│ ├── suppressions/
│ │ └── ruby.supp
│ ├── test_task_test.rb
│ └── valgrind_error_test.rb
├── ruby_memcheck_test.rb
└── test_helper.rb
================================================
FILE CONTENTS
================================================
================================================
FILE: .devcontainer/Dockerfile
================================================
FROM ghcr.io/rails/devcontainer/images/ruby:3.4.7
RUN sudo apt-get update && sudo apt-get install -y valgrind
================================================
FILE: .devcontainer/devcontainer.json
================================================
{
"name": "ruby_memcheck",
"build": {
"dockerfile": "Dockerfile"
},
"features": {
"ghcr.io/devcontainers/features/github-cli:1": {}
}
}
================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
- package-ecosystem: "bundler"
directory: "/"
schedule:
interval: "weekly"
================================================
FILE: .github/workflows/cla.yml
================================================
# .github/workflows/cla.yml
name: Contributor License Agreement (CLA)
on:
pull_request_target:
types: [opened, synchronize]
issue_comment:
types: [created]
jobs:
cla:
runs-on: ubuntu-latest
if: |
(github.event.issue.pull_request
&& !github.event.issue.pull_request.merged_at
&& contains(github.event.comment.body, 'signed')
)
|| (github.event.pull_request && !github.event.pull_request.merged)
steps:
- uses: Shopify/shopify-cla-action@v1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
cla-token: ${{ secrets.CLA_TOKEN }}
================================================
FILE: .github/workflows/lint.yml
================================================
name: Lint
on: [push, pull_request]
jobs:
rubocop:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: ruby/setup-ruby@v1
- run: bundle install --jobs=3 --retry=3 --path=vendor/bundle
- run: bundle exec rubocop
================================================
FILE: .github/workflows/release.yml
================================================
name: Release
on:
release:
types: [published]
jobs:
release:
permissions:
contents: write
id-token: write
environment: release
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
bundler-cache: true
ruby-version: 3.4.4
- uses: rubygems/release-gem@v1
================================================
FILE: .github/workflows/test.yml
================================================
name: Test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
entry:
- { ruby: '3.1', allowed-failure: false }
- { ruby: '3.2', allowed-failure: false }
- { ruby: '3.3', allowed-failure: false }
- { ruby: '3.4', allowed-failure: false }
- { ruby: ruby-head, allowed-failure: false }
name: ruby ${{ matrix.entry.ruby }}
steps:
- uses: actions/checkout@v2
- uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.entry.ruby }}
# - run: sudo apt-get install -y valgrind
# We cannot use Valgrind from apt because we need at least Valgrind 3.20.0
# to support DWARF 5
- run: |
sudo apt-get update
sudo apt-get install -y libc6-dbg
- name: Install Valgrind from source
run: |
wget https://sourceware.org/pub/valgrind/valgrind-3.21.0.tar.bz2
tar xvf valgrind-3.21.0.tar.bz2
cd valgrind-3.21.0
./configure
make
sudo make install
- run: bundle install --jobs=3 --retry=3
- run: bundle exec rake
continue-on-error: ${{ matrix.entry.allowed-failure }}
================================================
FILE: .gitignore
================================================
/.bundle/
/.yardoc
/_yardoc/
/coverage/
/doc/
/pkg/
/spec/reports/
/tmp/
*.so
.DS_Store
================================================
FILE: .rubocop.yml
================================================
inherit_gem:
rubocop-shopify: rubocop.yml
AllCops:
SuggestExtensions: false
Style/GlobalVars:
Exclude:
- test/ruby_memcheck/ext/extconf.rb
Layout/CommentIndentation:
AllowForAlignment: true
================================================
FILE: .ruby-version
================================================
3.4.7
================================================
FILE: Gemfile
================================================
# frozen_string_literal: true
source "https://rubygems.org"
gemspec
================================================
FILE: LICENSE.txt
================================================
The MIT License (MIT)
Copyright 2021-present, Shopify Inc.
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.
================================================
FILE: README.md
================================================
# ruby_memcheck
This gem provides a sane way to use Valgrind's memcheck on your native extension gem.
## Table of contents
1. [What is this gem?](#what-is-this-gem)
1. [Who should use this gem?](#who-should-use-this-gem)
1. [How does it work?](#how-does-it-work)
1. [Limitations](#limitations)
1. [Installation](#installation)
1. [Running a Ruby script](#running-a-ruby-script)
1. [Setup for test suites](#setup-for-test-suites)
1. [Configuration](#configuration)
1. [Suppression files](#suppression-files)
1. [License](#license)
## What is this gem?
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.
### Who should use this gem?
Only gems with native extensions can use this gem. If your gem is written in plain Ruby, this gem is not useful for you.
### How does it work?
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.
For more details, read [this blog post](https://blog.peterzhu.ca/ruby-memcheck/).
### Limitations
Because of the aggressive heuristics used to filter out false positives, there are various limitations of what this gem can detect.
1. This gem is only expected to work on Linux.
1. This gem runs your gem's test suite to find errors and memory leaks. It will only be able to report errors on code paths that are covered by your tests. So make sure your test suite has good coverage!
1. It will not find memory leaks in Ruby. It filters out everything in Ruby.
1. It will not find memory leaks of allocations that occurred in Ruby (even if the memory leak is caused by your native extension).
An example of this is if a string is allocated in Ruby, passed into your native extension, you change the pointer of the string without freeing the contents, so the contents of the string becomes leaked.
1. To filter out false positives, it will only find definite leaks (i.e. memory regions with no pointers to it). It will not find possible leaks (i.e. memory regions with pointers to it).
1. It will not find leaks that occur in the `Init` function of your native extension.
1. It will not find uses of undefined values (e.g. conditional jumps depending on undefined values). This is just a technical limitation that has not been solved yet (contributions welcome!).
## Installation
```
gem install ruby_memcheck
```
## Running a Ruby script
You can run a Ruby script under ruby_memcheck. This will report all memory leaks in all native extensions found in your Ruby script. Simply replace the `ruby` part of your command with `ruby_memcheck`. For example:
```sh
$ ruby_memcheck -e "puts 'Hello world'"
Hello world
```
## Setup for test suites
> **Note**
> If you encounter errors from Valgrind that looks like this:
> ```
> ### unhandled dwarf2 abbrev form code 0x25
> ```
> Then you need a newer version of Valgrind (>= 3.20.0) with DWARF5 support.
> The current versions of Valgrind in Ubuntu Packages is not new enough.
>
> You can install Valgrind from source using the following commands:
> ```
> sudo apt-get install -y libc6-dbg
> wget https://sourceware.org/pub/valgrind/valgrind-3.21.0.tar.bz2
> tar xvf valgrind-3.21.0.tar.bz2
> cd valgrind-3.21.0
> ./configure
> make
> sudo make install
> ```
You can use ruby_memcheck on your test suite (Minitest or RSpec) using rake.
0. Install Valgrind.
1. In your Rakefile, require this gem.
```ruby
require "ruby_memcheck"
```
- **For RSpec:** If you're using RSpec, also add the following require.
```ruby
require "ruby_memcheck/rspec/rake_task"
```
1. Setup the test task for your test framework.
- **minitest**
Locate your test task(s) in your Rakefile. You can identify it with a call to `Rake::TestTask.new`.
Create a namespace under the test task and create a `RubyMemcheck::TestTask` with the same configuration.
For example, if your Rakefile looked like this before:
```ruby
Rake::TestTask.new(test: :compile) do |t|
t.libs << "test"
t.test_files = FileList["test/unit/**/*_test.rb"]
end
```
You can change it to look like this:
```ruby
test_config = lambda do |t|
t.libs << "test"
t.test_files = FileList["test/**/*_test.rb"]
end
Rake::TestTask.new(test: :compile, &test_config)
namespace :test do
RubyMemcheck::TestTask.new(valgrind: :compile, &test_config)
end
```
- **RSpec**
Locate your rake task(s) in your Rakefile. You can identify it with a call to `RSpec::Core::RakeTask.new`.
Create a namespace under the test task and create a `RubyMemcheck::RSpec::RakeTask` with the same configuration.
For example, if your Rakefile looked like this before:
```ruby
RSpec::Core::RakeTask.new(spec: :compile)
```
You can change it to look like this:
```ruby
RSpec::Core::RakeTask.new(spec: :compile)
namespace :spec do
RubyMemcheck::RSpec::RakeTask.new(valgrind: :compile)
end
```
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.
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.
## Configuration
If you want to override any of the default configurations you can call `RubyMemcheck.config` after `require "ruby_memcheck"`. This will create 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.
`RubyMemcheck::Configuration` accepts a variety of keyword arguments. Here are all the arguments:
- `binary_name`: Optional. The name of the only binary to report errors for. Use this if there is too much noise caused by other binaries.
- `ruby`: Optional. The command to run to invoke Ruby. Defaults to the Ruby that is currently being used.
- `valgrind`: Optional. The command to run to invoke Valgrind. Defaults to the string `"valgrind"`.
- `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.
- `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`.
- `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`.
- `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.
- `temp_dir`: Optional. The directory to store temporary files. It defaults to a temporary directory. This is present for development debugging, so you shouldn't have to use it.
- `output_io`: Optional. The `IO` object to output Valgrind errors to. Defaults to standard error.
- `filter_all_errors`: Optional. Whether to filter all kinds of Valgrind errors (not just memory leaks). This feature should only be used if you're encountering a large number of illegal memory accesses coming from Ruby. If you need to use this feature, you may have found a bug inside of Ruby. Consider reporting it to the [Ruby bug tracker](https://bugs.ruby-lang.org/projects/ruby-master/issues/new). Defaults to `false`.
- `use_only_ruby_free_at_exit`: Optional. Use only the [`RUBY_FREE_AT_EXIT`](https://bugs.ruby-lang.org/issues/19993) feature introduced in Ruby 3.3 and disables most of the heuristics inside of ruby_memcheck. Disable this if you want to use the original heuristics. Defaults to `true` for Ruby 3.4 and later, `false` otherwise. Note: while `RUBY_FREE_AT_EXIT` was introduced in Ruby 3.3, there are bugs which prevents it from working well, so it is only enabled by default for Ruby 3.4 and later.
## Suppression files
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 `ruby.supp`. If you want some suppressions for only specific versions of Ruby, you can add the Ruby version to the filename. For example, `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 `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.).
## Success stories
Let's celebrate wins from this gem! If this gem was useful for you, please share your story below too!
- [`liquid-c`](https://github.com/Shopify/liquid-c):
- Found 2 memory leaks: [#157](https://github.com/Shopify/liquid-c/pull/157), [#161](https://github.com/Shopify/liquid-c/pull/161)
- Running on CI: [#162](https://github.com/Shopify/liquid-c/pull/162)
- [`nokogiri`](https://github.com/sparklemotion/nokogiri):
- Found 5 memory leaks: [4 in #2345](https://github.com/sparklemotion/nokogiri/pull/2345), [#2347](https://github.com/sparklemotion/nokogiri/pull/2347)
- Running on CI: [#2344](https://github.com/sparklemotion/nokogiri/pull/2344)
- [`rotoscope`](https://github.com/Shopify/rotoscope):
- Found a [memory leak in Ruby TracePoint](https://bugs.ruby-lang.org/issues/18264)
- Running on CI: [#89](https://github.com/Shopify/rotoscope/pull/89)
- [`protobuf`](https://github.com/protocolbuffers/protobuf):
- Found 1 memory leak: [#9150](https://github.com/protocolbuffers/protobuf/pull/9150)
- [`gRPC`](https://github.com/grpc/grpc):
- Found 1 memory leak: [#27900](https://github.com/grpc/grpc/pull/27900)
- [`wasmtime-rb`](https://github.com/bytecodealliance/wasmtime-rb):
- Found 1 memory leak: [#26](https://github.com/bytecodealliance/wasmtime-rb/pull/26)
- [`yarp`](https://github.com/shopify/yarp):
- Found 6 memory leaks and 1 memory error: [#292](https://github.com/Shopify/yarp/pull/304), [#292](https://github.com/Shopify/yarp/pull/292)
- Running on CI: [#293](https://github.com/Shopify/yarp/pull/293)
- [`libxml2`](https://gitlab.gnome.org/GNOME/libxml2):
- Found 1 memory leak: [memory leak from \`xmlSchemaValidateStream\` in v2.11.x (#530)](https://gitlab.gnome.org/GNOME/libxml2/-/issues/530)
- Running in Nokogiri's CI pipeline: [#2868](https://github.com/sparklemotion/nokogiri/pull/2868)
- [`re2`](https://github.com/mudge/re2):
- Found 8 memory leaks: [#105](https://github.com/mudge/re2/pull/105)
- Running on CI: [#149](https://github.com/mudge/re2/pull/149)
## License
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
================================================
FILE: Rakefile
================================================
# frozen_string_literal: true
require "bundler/gem_tasks"
require "rake/testtask"
require "rake/extensiontask"
Rake::TestTask.new(test: "test:compile") do |t|
t.libs << "test"
t.libs << "lib"
t.test_files = FileList["test/**/*_test.rb"]
end
namespace :test do
Rake::ExtensionTask.new("ruby_memcheck_c_test_one") do |ext|
ext.ext_dir = "test/ruby_memcheck/ext"
ext.lib_dir = "test/ruby_memcheck/ext"
ext.config_script = "extconf_one.rb"
end
Rake::ExtensionTask.new("ruby_memcheck_c_test_two") do |ext|
ext.ext_dir = "test/ruby_memcheck/ext"
ext.lib_dir = "test/ruby_memcheck/ext"
ext.config_script = "extconf_two.rb"
end
end
task default: :test
================================================
FILE: exe/ruby_memcheck
================================================
#!/usr/bin/env ruby
# frozen_string_literal: true
$LOAD_PATH.unshift("#{__dir__}/../lib")
require "ruby_memcheck"
runner = RubyMemcheck::RubyRunner.new
exit(runner.run(*ARGV))
================================================
FILE: lib/ruby_memcheck/configuration.rb
================================================
# frozen_string_literal: true
module RubyMemcheck
class Configuration
DEFAULT_VALGRIND = "valgrind"
DEFAULT_VALGRIND_OPTIONS = [
"--num-callers=50",
"--error-limit=no",
"--trace-children=yes",
"--undef-value-errors=no",
"--leak-check=full",
"--show-leak-kinds=definite",
].freeze
DEFAULT_VALGRIND_SUPPRESSIONS_DIR = "suppressions"
DEFAULT_SKIPPED_RUBY_FUNCTIONS = [
/\Aeval_string_with_cref\z/,
/\Aintern_str\z/, # Same as rb_intern, but sometimes rb_intern is optimized out
/\Arb_add_method_cfunc\z/,
/\Arb_check_funcall/,
/\Arb_class_boot\z/, # Called for all the different ways to create a Class
/\Arb_enc_raise\z/,
/\Arb_exc_raise\z/,
/\Arb_extend_object\z/,
/\Arb_funcall/,
/\Arb_intern/,
/\Arb_ivar_set\z/,
/\Arb_module_new\z/,
/\Arb_raise\z/,
/\Arb_rescue/,
/\Arb_respond_to\z/,
/\Arb_thread_create\z/, # Threads are relased to a cache, so they may be reported as a leak
/\Arb_vm_exec\z/,
/\Arb_yield/,
].freeze
RUBY_FREE_AT_EXIT_SUPPORTED = Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("3.4.0")
attr_reader :binary_name
attr_reader :ruby
attr_reader :valgrind
attr_reader :valgrind_options
attr_reader :valgrind_suppression_files
attr_reader :valgrind_generate_suppressions
attr_reader :skipped_ruby_functions
attr_reader :temp_dir
attr_reader :loaded_features_file
attr_reader :output_io
attr_reader :filter_all_errors
attr_reader :use_only_ruby_free_at_exit
alias_method :valgrind_generate_suppressions?, :valgrind_generate_suppressions
alias_method :filter_all_errors?, :filter_all_errors
alias_method :use_only_ruby_free_at_exit?, :use_only_ruby_free_at_exit
def initialize(
binary_name: nil,
ruby: FileUtils::RUBY,
valgrind: DEFAULT_VALGRIND,
valgrind_options: DEFAULT_VALGRIND_OPTIONS,
valgrind_suppressions_dir: DEFAULT_VALGRIND_SUPPRESSIONS_DIR,
valgrind_generate_suppressions: false,
skipped_ruby_functions: DEFAULT_SKIPPED_RUBY_FUNCTIONS,
temp_dir: Dir.mktmpdir,
output_io: $stderr,
filter_all_errors: false,
use_only_ruby_free_at_exit: RUBY_FREE_AT_EXIT_SUPPORTED
)
@binary_name = binary_name
@ruby = ruby
@valgrind = valgrind
@valgrind_options = valgrind_options
@valgrind_suppression_files =
get_valgrind_suppression_files(File.join(__dir__, "../../suppressions")) +
get_valgrind_suppression_files(valgrind_suppressions_dir)
@valgrind_generate_suppressions = valgrind_generate_suppressions
@skipped_ruby_functions = skipped_ruby_functions
@output_io = output_io
@filter_all_errors = filter_all_errors
temp_dir = File.expand_path(temp_dir)
FileUtils.mkdir_p(temp_dir)
@temp_dir = temp_dir
@valgrind_options += [
"--xml=yes",
# %p will be replaced with the PID
# This prevents forking and shelling out from generating a corrupted XML
# See --log-file from https://valgrind.org/docs/manual/manual-core.html
"--xml-file=#{File.join(temp_dir, "%p.xml")}",
]
@loaded_features_file = Tempfile.create("", @temp_dir)
@use_only_ruby_free_at_exit = use_only_ruby_free_at_exit
end
def command(*args)
[
# On some Rubies, not setting the stack size to be ulimited causes
# Valgrind to report the following error:
# Invalid write of size 1
# reserve_stack (thread_pthread.c:845)
# ruby_init_stack (thread_pthread.c:871)
# main (main.c:48)
"ulimit -s unlimited && ",
# On some distros, and in some Docker containers, the number of file descriptors is set to a
# very high number like 1073741816 that valgrind >= 3.21.0 will error out on:
# --184100:0:libcfile Valgrind: FATAL: Private file creation failed.
# The current file descriptor limit is 1073741804.
# If you are running in Docker please consider
# lowering this limit with the shell built-in limit command.
# --184100:0:libcfile Exiting now.
# See https://bugs.kde.org/show_bug.cgi?id=465435 for background information.
"ulimit -n 8192 && ",
valgrind,
valgrind_options,
valgrind_suppression_files.map { |f| "--suppressions=#{f}" },
valgrind_generate_suppressions ? "--gen-suppressions=all" : "",
ruby,
"-r" + File.expand_path(File.join(__dir__, "test_helper.rb")),
args,
].flatten.join(" ")
end
private
def get_valgrind_suppression_files(dir)
dir = File.expand_path(dir)
full_ruby_version = "#{RUBY_ENGINE}-#{RUBY_VERSION}.#{RUBY_PATCHLEVEL}"
versions = [full_ruby_version]
(0..3).reverse_each { |i| versions << full_ruby_version.split(".")[0, i].join(".") }
versions << RUBY_ENGINE
versions.map do |version|
Dir[File.join(dir, "#{version}.supp")]
end.flatten
end
end
end
================================================
FILE: lib/ruby_memcheck/frame.rb
================================================
# frozen_string_literal: true
module RubyMemcheck
class Frame
attr_reader :configuration, :loaded_binaries, :fn, :obj, :file, :line
def initialize(configuration, loaded_binaries, frame_xml)
@configuration = configuration
@loaded_binaries = loaded_binaries
@fn = frame_xml.at_xpath("fn")&.content
@obj = frame_xml.at_xpath("obj")&.content
# file and line may not be available
@file = frame_xml.at_xpath("file")&.content
@line = frame_xml.at_xpath("line")&.content
end
def in_ruby?
return false unless obj
obj == configuration.ruby ||
# Hack to fix Ruby built with --enabled-shared
File.basename(obj) == "libruby.so.#{RUBY_VERSION}"
end
def in_binary?
return false unless obj
loaded_binaries.include?(obj)
end
def binary_init_func?
return false unless in_binary?
binary_name = File.basename(obj, ".*")
fn == "Init_#{binary_name}"
end
def to_s
if file
"#{fn} (#{file}:#{line})"
elsif fn
"#{fn} (at #{obj})"
else
"<unknown stack frame>"
end
end
end
end
================================================
FILE: lib/ruby_memcheck/rspec/rake_task.rb
================================================
# frozen_string_literal: true
require "rspec/core/rake_task"
module RubyMemcheck
module RSpec
class RakeTask < ::RSpec::Core::RakeTask
attr_reader :configuration
attr_reader :reporter
def initialize(*args)
@configuration =
if !args.empty? && args[0].is_a?(Configuration)
args.shift
else
RubyMemcheck.default_configuration
end
super
end
def run_task(verbose)
error = nil
@reporter = TestTaskReporter.new(configuration)
@reporter.run_ruby_with_valgrind do
# RSpec::Core::RakeTask#run_task calls Kernel.exit on failure
super
rescue SystemExit => e
error = e
end
raise error if error
end
private
def spec_command
# First part of command is Ruby
args = super.split(" ")[1..]
configuration.command(args)
end
end
end
end
================================================
FILE: lib/ruby_memcheck/ruby_runner.rb
================================================
# frozen_string_literal: true
module RubyMemcheck
class RubyRunner
attr_reader :configuration
attr_reader :reporter
def initialize(*args)
@configuration =
if !args.empty? && args[0].is_a?(Configuration)
args.shift
else
RubyMemcheck.default_configuration
end
end
def run(*args, **options)
command = configuration.command(args.map { |a| Shellwords.escape(a) })
@reporter = TestTaskReporter.new(configuration)
@reporter.setup
system(command, options)
exit_code = $CHILD_STATUS.exitstatus
@reporter.report_valgrind_errors
exit_code
end
end
end
================================================
FILE: lib/ruby_memcheck/stack.rb
================================================
# frozen_string_literal: true
module RubyMemcheck
class Stack
attr_reader :configuration, :frames
def initialize(configuration, loaded_binaries, stack_xml)
@configuration = configuration
@frames = stack_xml.xpath("frame").map { |frame| Frame.new(configuration, loaded_binaries, frame) }
end
def skip?
if @configuration.use_only_ruby_free_at_exit?
skip_using_ruby_free_at_exit?
else
skip_using_original_heuristics?
end
end
private
def skip_using_ruby_free_at_exit?
if configuration.binary_name.nil?
false
else
in_binary = false
frames.each do |frame|
if frame.in_binary?
in_binary = true
end
end
!in_binary
end
end
def skip_using_original_heuristics?
in_binary = false
frames.each do |frame|
if frame.in_ruby?
# If a stack from from the binary was encountered first, then this
# memory leak did not occur from Ruby
unless in_binary
# Skip this stack because it was called from Ruby
return true if configuration.skipped_ruby_functions.any? { |r| r.match?(frame.fn) }
end
elsif frame.in_binary?
in_binary = true
# Skip the Init function because it is only ever called once, so
# leaks in it cannot cause memory bloat
return true if frame.binary_init_func?
end
end
# Skip if the stack was never in the binary because it is very likely
# not a leak in the native gem
!in_binary
end
end
end
================================================
FILE: lib/ruby_memcheck/suppression.rb
================================================
# frozen_string_literal: true
module RubyMemcheck
class Suppression
attr_reader :root
def initialize(configuration, suppression_node)
@root = suppression_node
end
def to_s
str = StringIO.new
str << "{\n"
str << " #{root.at_xpath("sname").content}\n"
str << " #{root.at_xpath("skind").content}\n"
root.xpath("./sframe/fun | ./sframe/obj").each do |frame|
str << " #{frame.name}:#{frame.content}\n"
end
str << "}\n"
str.string
end
end
end
================================================
FILE: lib/ruby_memcheck/test_helper.rb
================================================
# frozen_string_literal: true
at_exit do
File.open(ENV["RUBY_MEMCHECK_LOADED_FEATURES_FILE"], "w") do |f|
f.write($LOADED_FEATURES.join("\n"))
end
# We need to remove the @_memoized instance variable from Minitest::Spec
# objects because it holds a hash that contains memoized objects in `let`
# blocks, this can contain objects that will be reported as a memory leak.
if defined?(Minitest::Spec)
require "objspace"
ObjectSpace.each_object(Minitest::Spec) do |obj|
if obj.instance_variable_defined?(:@_memoized)
obj.remove_instance_variable(:@_memoized)
end
end
end
GC.start
end
================================================
FILE: lib/ruby_memcheck/test_task.rb
================================================
# frozen_string_literal: true
module RubyMemcheck
class TestTask < Rake::TestTask
attr_reader :configuration
attr_reader :reporter
def initialize(*args)
@configuration =
if !args.empty? && args[0].is_a?(Configuration)
args.shift
else
RubyMemcheck.default_configuration
end
super
end
def ruby(*args, **options, &block)
command = configuration.command(args)
@reporter = TestTaskReporter.new(configuration)
@reporter.setup
sh(command, **options) do |ok, res|
@reporter.report_valgrind_errors
yield ok, res if block_given?
end
end
end
end
================================================
FILE: lib/ruby_memcheck/test_task_reporter.rb
================================================
# frozen_string_literal: true
module RubyMemcheck
class TestTaskReporter
VALGRIND_REPORT_MSG = "Valgrind reported errors (e.g. memory leak or use-after-free)"
attr_reader :configuration
attr_reader :errors
def initialize(configuration)
@configuration = configuration
@loaded_binaries = nil
end
def run_ruby_with_valgrind(&block)
setup
yield
report_valgrind_errors
end
def setup
ENV["RUBY_MEMCHECK_LOADED_FEATURES_FILE"] = File.expand_path(configuration.loaded_features_file)
ENV["RUBY_MEMCHECK_RUNNING"] = "1"
ENV["RUBY_FREE_AT_EXIT"] = "1"
end
def report_valgrind_errors
parse_valgrind_output
remove_valgrind_xml_files
unless errors.empty?
output_valgrind_errors
raise VALGRIND_REPORT_MSG
end
end
private
def loaded_binaries
return @loaded_binaries if @loaded_binaries
loaded_features = File.readlines(configuration.loaded_features_file, chomp: true)
@loaded_binaries = loaded_features.keep_if do |feat|
# Keep only binaries (ignore Ruby files).
File.extname(feat) == ".so"
end
if configuration.binary_name
@loaded_binaries.keep_if do |feat|
File.basename(feat, ".*") == configuration.binary_name
end
if @loaded_binaries.empty?
raise "The Ruby program executed never loaded a binary called `#{configuration.binary_name}`"
end
end
@loaded_binaries.freeze
end
def valgrind_xml_files
@valgrind_xml_files ||= Dir[File.join(configuration.temp_dir, "*.xml")].freeze
end
def parse_valgrind_output
require "nokogiri"
@errors = []
valgrind_xml_files.each do |file|
reader = Nokogiri::XML::Reader(File.open(file)) do |config| # rubocop:disable Style/SymbolProc
config.huge
end
reader.each do |node|
next unless node.name == "error" && node.node_type == Nokogiri::XML::Reader::TYPE_ELEMENT
error_xml = Nokogiri::XML::Document.parse(node.outer_xml).root
error = ValgrindError.new(configuration, loaded_binaries, error_xml)
next if error.skip?
@errors << error
end
end
end
def remove_valgrind_xml_files
valgrind_xml_files.each do |file|
File.delete(file)
end
end
def output_valgrind_errors
@errors.each do |error|
configuration.output_io.puts error
configuration.output_io.puts
end
end
end
end
================================================
FILE: lib/ruby_memcheck/valgrind_error.rb
================================================
# frozen_string_literal: true
module RubyMemcheck
class ValgrindError
SUPPRESSION_NOT_CONFIGURED_ERROR_MSG =
"Please enable suppressions by configuring with valgrind_generate_suppressions set to true"
attr_reader :kind, :msg, :stack, :suppression
def initialize(configuration, loaded_binaries, error)
@kind = error.at_xpath("kind").content
@msg =
if kind_leak?
error.at_xpath("xwhat/text").content
else
error.at_xpath("what").content
end
@stack = Stack.new(configuration, loaded_binaries, error.at_xpath("stack"))
@configuration = configuration
suppression_node = error.at_xpath("suppression")
if configuration.valgrind_generate_suppressions?
@suppression = Suppression.new(configuration, suppression_node)
elsif suppression_node
raise SUPPRESSION_NOT_CONFIGURED_ERROR_MSG
end
end
def skip?
should_filter? && stack.skip?
end
def to_s
str = StringIO.new
str << "#{msg}\n"
stack.frames.each do |frame|
str << if frame.in_binary?
" *#{frame}\n"
else
" #{frame}\n"
end
end
str << suppression.to_s if suppression
str.string
end
private
def should_filter?
@configuration.filter_all_errors? || kind_leak?
end
def kind_leak?
kind.start_with?("Leak_")
end
end
end
================================================
FILE: lib/ruby_memcheck/version.rb
================================================
# frozen_string_literal: true
module RubyMemcheck
VERSION = "3.0.1"
end
================================================
FILE: lib/ruby_memcheck.rb
================================================
# frozen_string_literal: true
require "English"
require "shellwords"
require "tempfile"
require "rake/testtask"
require "ruby_memcheck/configuration"
require "ruby_memcheck/frame"
require "ruby_memcheck/ruby_runner"
require "ruby_memcheck/stack"
require "ruby_memcheck/test_task_reporter"
require "ruby_memcheck/test_task"
require "ruby_memcheck/valgrind_error"
require "ruby_memcheck/suppression"
require "ruby_memcheck/version"
module RubyMemcheck
class << self
def config(**opts)
@default_configuration = Configuration.new(**opts)
end
def default_configuration
@default_configuration ||= config
end
end
end
================================================
FILE: ruby_memcheck.gemspec
================================================
# frozen_string_literal: true
require_relative "lib/ruby_memcheck/version"
Gem::Specification.new do |spec|
spec.name = "ruby_memcheck"
spec.version = RubyMemcheck::VERSION
spec.authors = ["Peter Zhu"]
spec.email = ["peter@peterzhu.ca"]
spec.summary = "Use Valgrind memcheck without going crazy"
spec.homepage = "https://github.com/Shopify/ruby_memcheck"
spec.license = "MIT"
spec.required_ruby_version = ">= 3.0.0"
spec.metadata["homepage_uri"] = spec.homepage
# Specify which files should be added to the gem when it is released.
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
spec.files = Dir.chdir(File.expand_path("..", __FILE__)) do
%x(git ls-files -z).split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
end
spec.bindir = "exe"
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
spec.require_paths = ["lib"]
spec.add_dependency("nokogiri")
spec.add_development_dependency("minitest", "~> 5.0")
spec.add_development_dependency("minitest-parallel_fork", "~> 2.0")
spec.add_development_dependency("rake", "~> 13.0")
spec.add_development_dependency("rake-compiler", "~> 1.1")
spec.add_development_dependency("rspec-core")
spec.add_development_dependency("rubocop", "~> 1.22")
spec.add_development_dependency("rubocop-shopify", "~> 2.3")
end
================================================
FILE: suppressions/ruby.supp
================================================
{
On platforms where memcpy is safe for overlapped memory, the compiler will sometimes replace memmove with memcpy. Valgrind may report a false positive.
Memcheck:Overlap
fun:__memcpy_chk
fun:memmove
...
}
{
Requiring a file will add it to the loaded features, which may be reported as a leak.
Memcheck:Leak
...
fun:require_internal
...
}
{
recursive_list_access creates a hash called `list` that is stored on the threadptr_recursive_hash. This is reported as a memory leak.
Memcheck:Leak
...
fun:rb_ident_hash_new
fun:recursive_list_access
fun:exec_recursive
...
}
{
"Invalid read of size 8" when marking the stack of fibers
Memcheck:Addr8
fun:each_location*
...
}
{
Rust probes for statx(buf), will be fixed in Valgrind >= 3.1.6.0
Memcheck:Param
statx(buf)
...
fun:*try_statx*
...
}
{
Rust probes for statx(file_name), will be fixed in Valgrind >= 3.1.6.0
Memcheck:Param
statx(file_name)
...
fun:*try_statx*
...
}
{
strscan_do_scan in strscan.c will sometimes replace the ptr of the regex, which can be reported as a memory leak if the regex is stored in an iseq. https://github.com/ruby/ruby/pull/8136
Memcheck:Leak
...
fun:rb_reg_prepare_re
fun:strscan_do_scan
...
}
{
The callcache table (RCLASS_CC_TBL) is lazily created, so it is allocated when the first method that gets cached. If this happens in a native extension, it may be reported as a memory leak.
Memcheck:Leak
...
fun:rb_id_table_create
...
fun:rb_callable_method_entry
...
}
{
The date library lazily initializes Regexps using static local variables through the function `regcomp`. The Regexp will end up being reported as a memory leak.
Memcheck:Leak
...
fun:rb_enc_reg_new
...
fun:date__parse
...
}
================================================
FILE: test/ruby_memcheck/ext/extconf_one.rb
================================================
# frozen_string_literal: true
require "mkmf"
$warnflags&.gsub!("-Wdeclaration-after-statement", "") # rubocop:disable Style/GlobalVars
create_makefile("ruby_memcheck_c_test_one")
================================================
FILE: test/ruby_memcheck/ext/extconf_two.rb
================================================
# frozen_string_literal: true
require "mkmf"
$warnflags&.gsub!("-Wdeclaration-after-statement", "") # rubocop:disable Style/GlobalVars
create_makefile("ruby_memcheck_c_test_two")
================================================
FILE: test/ruby_memcheck/ext/ruby_memcheck_c_test_one.c
================================================
#include <ruby.h>
static VALUE cRubyMemcheckCTestOne;
static VALUE c_test_one_no_memory_leak(VALUE _)
{
return Qnil;
}
/* This function must not be inlined to ensure that it has a stack frame. */
static void __attribute__((noinline)) c_test_one_allocate_memory_leak(void)
{
volatile char *ptr = malloc(100);
ptr[0] = 'a';
}
static VALUE c_test_one_memory_leak(VALUE _)
{
c_test_one_allocate_memory_leak();
return Qnil;
}
static VALUE c_test_one_use_after_free(VALUE _)
{
volatile char *ptr = malloc(100);
free((void *)ptr);
ptr[0] = 'a';
return Qnil;
}
static VALUE c_test_one_uninitialized_value(VALUE _)
{
volatile int foo;
#pragma GCC diagnostic ignored "-Wuninitialized"
return foo == 0 ? rb_str_new_cstr("zero") : rb_str_new_cstr("not zero");
#pragma GCC diagnostic pop
}
static VALUE c_test_one_call_into_ruby_mem_leak(VALUE obj)
{
VALUE string = rb_eval_string("String.new(capacity: 10_000)");
RSTRING(string)->as.heap.ptr = NULL;
return Qnil;
}
void Init_ruby_memcheck_c_test_one(void)
{
/* Memory leaks in the Init functions should be ignored. */
c_test_one_allocate_memory_leak();
VALUE mRubyMemcheck = rb_define_module("RubyMemcheck");
cRubyMemcheckCTestOne = rb_define_class_under(mRubyMemcheck, "CTestOne", rb_cObject);
rb_global_variable(&cRubyMemcheckCTestOne);
rb_define_method(cRubyMemcheckCTestOne, "no_memory_leak", c_test_one_no_memory_leak, 0);
rb_define_method(cRubyMemcheckCTestOne, "memory_leak", c_test_one_memory_leak, 0);
rb_define_method(cRubyMemcheckCTestOne, "use_after_free", c_test_one_use_after_free, 0);
rb_define_method(cRubyMemcheckCTestOne, "uninitialized_value", c_test_one_uninitialized_value, 0);
rb_define_method(cRubyMemcheckCTestOne, "call_into_ruby_mem_leak", c_test_one_call_into_ruby_mem_leak, 0);
}
================================================
FILE: test/ruby_memcheck/ext/ruby_memcheck_c_test_two.c
================================================
#include <ruby.h>
static VALUE cRubyMemcheckCTestTwo;
static VALUE c_test_two_no_memory_leak(VALUE _)
{
return Qnil;
}
/* This function must not be inlined to ensure that it has a stack frame. */
static void __attribute__((noinline)) c_test_two_allocate_memory_leak(void)
{
volatile char *ptr = malloc(100);
ptr[0] = 'a';
}
static VALUE c_test_two_memory_leak(VALUE _)
{
c_test_two_allocate_memory_leak();
return Qnil;
}
static VALUE c_test_two_use_after_free(VALUE _)
{
volatile char *ptr = malloc(100);
free((void *)ptr);
ptr[0] = 'a';
return Qnil;
}
static VALUE c_test_two_uninitialized_value(VALUE _)
{
volatile int foo;
#pragma GCC diagnostic ignored "-Wuninitialized"
return foo == 0 ? rb_str_new_cstr("zero") : rb_str_new_cstr("not zero");
#pragma GCC diagnostic pop
}
static VALUE c_test_two_call_into_ruby_mem_leak(VALUE obj)
{
VALUE string = rb_eval_string("String.new(capacity: 10_000)");
RSTRING(string)->as.heap.ptr = NULL;
return Qnil;
}
void Init_ruby_memcheck_c_test_two(void)
{
/* Memory leaks in the Init functions should be ignored. */
c_test_two_allocate_memory_leak();
VALUE mRubyMemcheck = rb_define_module("RubyMemcheck");
cRubyMemcheckCTestTwo = rb_define_class_under(mRubyMemcheck, "CTestTwo", rb_cObject);
rb_global_variable(&cRubyMemcheckCTestTwo);
rb_define_method(cRubyMemcheckCTestTwo, "no_memory_leak", c_test_two_no_memory_leak, 0);
rb_define_method(cRubyMemcheckCTestTwo, "memory_leak", c_test_two_memory_leak, 0);
rb_define_method(cRubyMemcheckCTestTwo, "use_after_free", c_test_two_use_after_free, 0);
rb_define_method(cRubyMemcheckCTestTwo, "uninitialized_value", c_test_two_uninitialized_value, 0);
rb_define_method(cRubyMemcheckCTestTwo, "call_into_ruby_mem_leak", c_test_two_call_into_ruby_mem_leak, 0);
}
================================================
FILE: test/ruby_memcheck/rspec/rake_task_test.rb
================================================
# frozen_string_literal: true
require "ruby_memcheck"
require "ruby_memcheck/rspec/rake_task"
require "ruby_memcheck/shared_test_task_reporter_tests"
module RubyMemcheck
module RSpec
class RakeTaskTest < Minitest::Test
include SharedTestTaskReporterTests
def setup
@output_io = StringIO.new
build_configuration
end
private
def run_with_memcheck(code, raise_on_failure: true, spawn_opts: {})
ok = true
stdout = nil
Dir.chdir(Dir.mktmpdir) do |dir|
spec_dir = File.join(dir, "spec")
Dir.mkdir(spec_dir)
stdout_log = Tempfile.new("", spec_dir)
script = Tempfile.new(["", "_spec.rb"], spec_dir)
script.write(<<~RUBY)
# Redirect stdout to log file for RSpec output
$stdout.reopen(File.open("#{stdout_log.path}", "w"))
$LOAD_PATH.unshift("#{File.join(__dir__, "../ext")}")
require "ruby_memcheck_c_test_one"
require "ruby_memcheck_c_test_two"
RSpec.describe RubyMemcheck do
it "test" do
#{code}
end
end
RUBY
script.flush
begin
@test_task.run_task(false)
rescue SystemExit
# RSpec::Core::RakeTask#run_task calls Kernel.exit on failure
ok = false
end
# Get the stdout of RSpec
stdout = File.read(stdout_log.path)
# Check RSpec test passed
unless /^1 example, 0 failures$/.match?(stdout)
ok = false
end
end
if raise_on_failure && !ok
raise "Command failed. stdout:\n#{stdout}"
end
ok
end
def build_test_task
@test_task = RubyMemcheck::RSpec::RakeTask.new(@configuration)
end
end
end
end
================================================
FILE: test/ruby_memcheck/ruby_memcheck_suppression_test.rb
================================================
# frozen_string_literal: true
require "test_helper"
require "nokogiri"
module RubyMemcheck
class RubyMemcheckSuppressionTest < Minitest::Test
def setup
@configuration = Configuration.new
end
def test_given_a_suppression_node
suppression = ::Nokogiri::XML(<<~EOF).at_xpath("//suppression")
<foo>
<suppression>
<sname>insert_a_suppression_name_here</sname>
<skind>Memcheck:Leak</skind>
<skaux>match-leak-kinds: definite</skaux>
<sframe> <fun>malloc</fun> </sframe>
<sframe> <fun>objspace_xmalloc0</fun> </sframe>
<sframe> <fun>ruby_xmalloc0</fun> </sframe>
<sframe> <obj>/usr/lib/libX11.so.6.3.0</fun> </sframe>
<sframe> <fun>ruby_xmalloc_body</fun> </sframe>
<sframe> <fun>ruby_xmalloc</fun> </sframe>
</suppression>
</foo>
EOF
expected = <<~EOF
{
insert_a_suppression_name_here
Memcheck:Leak
fun:malloc
fun:objspace_xmalloc0
fun:ruby_xmalloc0
obj:/usr/lib/libX11.so.6.3.0
fun:ruby_xmalloc_body
fun:ruby_xmalloc
}
EOF
assert_equal(
expected,
RubyMemcheck::Suppression.new(@configuration, suppression).to_s,
)
end
end
end
================================================
FILE: test/ruby_memcheck/ruby_runner_test.rb
================================================
# frozen_string_literal: true
require "ruby_memcheck/shared_test_task_reporter_tests"
module RubyMemcheck
class RubyRunnerTest < Minitest::Test
include SharedTestTaskReporterTests
def setup
@output_io = StringIO.new
build_configuration
end
private
def run_with_memcheck(code, raise_on_failure: true, spawn_opts: {})
script = Tempfile.new
script.write(<<~RUBY)
require "ruby_memcheck_c_test_one"
require "ruby_memcheck_c_test_two"
#{code}
RUBY
script.flush
exit_code = @test_task.run(
"-I#{File.join(__dir__, "ext")}",
script.path,
**spawn_opts,
)
if raise_on_failure && exit_code != 0
raise "Command failed with status (#{exit_code})"
end
exit_code == 0
end
def build_test_task
@test_task = RubyMemcheck::RubyRunner.new(@configuration)
end
end
end
================================================
FILE: test/ruby_memcheck/shared_test_task_reporter_tests.rb
================================================
# frozen_string_literal: true
require "test_helper"
module RubyMemcheck
module SharedTestTaskReporterTests
def test_succeeds_when_there_is_no_memory_leak
ok = run_with_memcheck(<<~RUBY)
RubyMemcheck::CTestOne.new.no_memory_leak
RUBY
assert(ok)
assert_empty(@test_task.reporter.errors)
assert_empty(@output_io.string)
end
def test_reports_memory_leak
error = assert_raises do
run_with_memcheck(<<~RUBY)
RubyMemcheck::CTestOne.new.memory_leak
RUBY
end
assert_equal(RubyMemcheck::TestTaskReporter::VALGRIND_REPORT_MSG, error.message)
output = @output_io.string
assert_equal(1, @test_task.reporter.errors.length, output)
refute_empty(output)
assert_match(/^100 bytes in 1 blocks are definitely lost in loss record/, output)
assert_match(/^ \*c_test_one_memory_leak \(ruby_memcheck_c_test_one\.c:\d+\)$/, output)
end
def test_reports_use_after_free
error = assert_raises do
run_with_memcheck(<<~RUBY)
RubyMemcheck::CTestOne.new.use_after_free
RUBY
end
assert_equal(RubyMemcheck::TestTaskReporter::VALGRIND_REPORT_MSG, error.message)
output = @output_io.string
assert_equal(1, @test_task.reporter.errors.length, output)
refute_empty(output)
assert_match(/^Invalid write of size 1$/, output)
assert_match(/^ \*c_test_one_use_after_free \(ruby_memcheck_c_test_one\.c:\d+\)$/, output)
end
# Potential improvement: support uninitialized values
def test_does_not_report_uninitialized_value
run_with_memcheck(<<~RUBY)
RubyMemcheck::CTestOne.new.uninitialized_value
RUBY
assert_equal(0, @test_task.reporter.errors.length, @output_io.string)
assert_empty(@test_task.reporter.errors)
assert_empty(@output_io.string)
end
def test_call_into_ruby_mem_leak_does_not_report_when_RUBY_FREE_AT_EXIT_is_not_supported
skip if Configuration::RUBY_FREE_AT_EXIT_SUPPORTED
ok = run_with_memcheck(<<~RUBY)
RubyMemcheck::CTestOne.new.call_into_ruby_mem_leak
RUBY
assert(ok)
assert_empty(@test_task.reporter.errors)
assert_empty(@output_io.string)
end
def test_call_into_ruby_mem_leak_not_report_when_RUBY_FREE_AT_EXIT_is_supported
skip unless Configuration::RUBY_FREE_AT_EXIT_SUPPORTED
error = assert_raises do
run_with_memcheck(<<~RUBY)
RubyMemcheck::CTestOne.new.call_into_ruby_mem_leak
RUBY
end
assert_equal(RubyMemcheck::TestTaskReporter::VALGRIND_REPORT_MSG, error.message)
output = @output_io.string
assert_equal(1, @test_task.reporter.errors.length, output)
refute_empty(output)
assert_match(/^ \*c_test_one_call_into_ruby_mem_leak \(ruby_memcheck_c_test_one\.c:\d+\)$/, output)
end
def test_call_into_ruby_mem_leak_not_report_when_RUBY_FREE_AT_EXIT_is_supported_but_use_only_ruby_free_at_exit_disabled
skip unless Configuration::RUBY_FREE_AT_EXIT_SUPPORTED
build_configuration(use_only_ruby_free_at_exit: false)
ok = run_with_memcheck(<<~RUBY)
RubyMemcheck::CTestOne.new.call_into_ruby_mem_leak
RUBY
assert(ok)
assert_empty(@test_task.reporter.errors)
assert_empty(@output_io.string)
end
def test_call_into_ruby_mem_leak_reports_when_not_skipped
build_configuration(skipped_ruby_functions: [])
error = assert_raises do
run_with_memcheck(<<~RUBY)
RubyMemcheck::CTestOne.new.call_into_ruby_mem_leak
RUBY
end
assert_equal(RubyMemcheck::TestTaskReporter::VALGRIND_REPORT_MSG, error.message)
assert_operator(@test_task.reporter.errors.length, :>=, 1, @output_io.string)
end
def test_suppressions
build_configuration(valgrind_suppressions_dir: File.join(__dir__, "suppressions"))
ok = run_with_memcheck(<<~RUBY)
RubyMemcheck::CTestOne.new.memory_leak
RUBY
assert(ok)
assert_empty(@test_task.reporter.errors)
assert_empty(@output_io.string)
end
def test_generation_of_suppressions
build_configuration(valgrind_generate_suppressions: true)
error = assert_raises do
run_with_memcheck(<<~RUBY)
RubyMemcheck::CTestOne.new.memory_leak
RUBY
end
assert_equal(RubyMemcheck::TestTaskReporter::VALGRIND_REPORT_MSG, error.message)
output = @output_io.string
assert_equal(1, @test_task.reporter.errors.length, output)
refute_empty(output)
assert_match(/^100 bytes in 1 blocks are definitely lost in loss record/, output)
assert_match(/^ \*c_test_one_memory_leak \(ruby_memcheck_c_test_one\.c:\d+\)$/, output)
assert_match(/^ insert_a_suppression_name_here/, output)
assert_match(/^ Memcheck:Leak/, output)
assert_match(/^ fun:c_test_one_allocate_memory_leak/, output)
end
def test_follows_forked_children
error = assert_raises do
run_with_memcheck(<<~RUBY)
pid = Process.fork do
RubyMemcheck::CTestOne.new.memory_leak
end
Process.wait(pid)
RUBY
end
assert_equal(RubyMemcheck::TestTaskReporter::VALGRIND_REPORT_MSG, error.message)
output = @output_io.string
assert_equal(1, @test_task.reporter.errors.length, output)
refute_empty(output)
assert_match(/^100 bytes in 1 blocks are definitely lost in loss record/, output)
assert_match(/^ \*c_test_one_memory_leak \(ruby_memcheck_c_test_one\.c:\d+\)$/, output)
end
def test_reports_multiple_errors
error = assert_raises do
run_with_memcheck(<<~RUBY)
RubyMemcheck::CTestOne.new.memory_leak
RubyMemcheck::CTestOne.new.use_after_free
RUBY
end
assert_equal(RubyMemcheck::TestTaskReporter::VALGRIND_REPORT_MSG, error.message)
output = @output_io.string
assert_equal(2, @test_task.reporter.errors.length, output)
refute_empty(output)
assert_match(/^100 bytes in 1 blocks are definitely lost in loss record/, output)
assert_match(/^ \*c_test_one_memory_leak \(ruby_memcheck_c_test_one\.c:\d+\)$/, output)
assert_match(/^Invalid write of size 1$/, output)
assert_match(/^ \*c_test_one_use_after_free \(ruby_memcheck_c_test_one\.c:\d+\)$/, output)
end
def test_reports_errors_in_all_binaries
error = assert_raises do
run_with_memcheck(<<~RUBY)
RubyMemcheck::CTestOne.new.memory_leak
RubyMemcheck::CTestTwo.new.memory_leak
RUBY
end
assert_equal(RubyMemcheck::TestTaskReporter::VALGRIND_REPORT_MSG, error.message)
output = @output_io.string
assert_equal(2, @test_task.reporter.errors.length, output)
refute_empty(output)
assert_match(/^100 bytes in 1 blocks are definitely lost in loss record/, output)
assert_match(/^ \*c_test_one_memory_leak \(ruby_memcheck_c_test_one\.c:\d+\)$/, output)
assert_match(/^ \*c_test_two_memory_leak \(ruby_memcheck_c_test_two\.c:\d+\)$/, output)
end
def test_can_run_multiple_times
2.times do
ok = run_with_memcheck(<<~RUBY)
RubyMemcheck::CTestOne.new.no_memory_leak
RUBY
assert(ok)
end
end
def test_ruby_failure_without_errors
ok = run_with_memcheck(<<~RUBY, raise_on_failure: false, spawn_opts: { out: "/dev/null", err: "/dev/null" })
foobar
RUBY
refute(ok)
assert_empty(@test_task.reporter.errors)
assert_empty(@output_io.string)
rescue
$stderr.puts(@test_task.reporter.errors)
raise
end
def test_ruby_failure_with_errors
error = assert_raises do
run_with_memcheck(<<~RUBY, raise_on_failure: false, spawn_opts: { out: "/dev/null", err: "/dev/null" })
RubyMemcheck::CTestOne.new.memory_leak
raise
RUBY
end
assert_equal(RubyMemcheck::TestTaskReporter::VALGRIND_REPORT_MSG, error.message)
output = @output_io.string
assert_equal(1, @test_task.reporter.errors.length, output)
refute_empty(output)
assert_match(/^100 bytes in 1 blocks are definitely lost in loss record/, output)
assert_match(/^ \*c_test_one_memory_leak \(ruby_memcheck_c_test_one\.c:\d+\)$/, output)
end
def test_test_helper_is_loaded
Tempfile.create do |tempfile|
ok = run_with_memcheck(<<~RUBY)
File.write(#{tempfile.path.inspect}, $LOADED_FEATURES.join("\n"))
RUBY
assert(ok)
assert_empty(@test_task.reporter.errors)
assert_includes(tempfile.read, File.expand_path(File.join(__dir__, "../../lib/ruby_memcheck/test_helper.rb")))
end
end
def test_environment_variable_RUBY_MEMCHECK_RUNNING
Tempfile.create do |tempfile|
ok = run_with_memcheck(<<~RUBY, raise_on_failure: false)
File.write(#{tempfile.path.inspect}, ENV["RUBY_MEMCHECK_RUNNING"])
RUBY
assert(ok)
assert_empty(@test_task.reporter.errors)
assert_includes(tempfile.read, "1")
end
end
def test_environment_variable_RUBY_FREE_AT_EXIT
Tempfile.create do |tempfile|
ok = run_with_memcheck(<<~RUBY, raise_on_failure: false)
File.write(#{tempfile.path.inspect}, ENV["RUBY_FREE_AT_EXIT"])
RUBY
assert(ok)
assert_empty(@test_task.reporter.errors)
assert_includes(tempfile.read, "1")
end
end
def test_configration_binary_name
build_configuration(binary_name: "ruby_memcheck_c_test_one")
error = assert_raises do
run_with_memcheck(<<~RUBY)
RubyMemcheck::CTestOne.new.memory_leak
RubyMemcheck::CTestTwo.new.memory_leak
RUBY
end
assert_equal(RubyMemcheck::TestTaskReporter::VALGRIND_REPORT_MSG, error.message)
output = @output_io.string
assert_equal(1, @test_task.reporter.errors.length, output)
refute_empty(output)
assert_match(/^100 bytes in 1 blocks are definitely lost in loss record/, output)
assert_match(/^ \*c_test_one_memory_leak \(ruby_memcheck_c_test_one\.c:\d+\)$/, output)
end
def test_configration_invalid_binary_name
build_configuration(binary_name: "invalid_binary_name")
error = assert_raises do
run_with_memcheck(<<~RUBY)
RubyMemcheck::CTestOne.new.memory_leak
RUBY
end
assert_includes(error.message, "`invalid_binary_name`")
end
private
def run_with_memcheck(code, raise_on_failure: true, spawn_opts: {})
raise NotImplementedError
end
def build_configuration(
output_io: @output_io,
**options
)
@configuration = Configuration.new(
output_io: @output_io,
**options,
)
build_test_task
end
def build_test_task
raise NotImplementedError
end
end
end
================================================
FILE: test/ruby_memcheck/suppressions/ruby.supp
================================================
{
suppress memory_leak
Memcheck:Leak
...
fun:c_test_one_memory_leak
...
}
================================================
FILE: test/ruby_memcheck/test_task_test.rb
================================================
# frozen_string_literal: true
require "ruby_memcheck/shared_test_task_reporter_tests"
module RubyMemcheck
class TestTaskTest < Minitest::Test
include SharedTestTaskReporterTests
def setup
Rake::FileUtilsExt.verbose_flag = false
@output_io = StringIO.new
build_configuration
end
private
def run_with_memcheck(code, raise_on_failure: true, spawn_opts: {})
script = Tempfile.new
script.write(<<~RUBY)
require "ruby_memcheck_c_test_one"
require "ruby_memcheck_c_test_two"
#{code}
RUBY
script.flush
ok = nil
@test_task.ruby(
"-I#{File.join(__dir__, "ext")}",
script.path,
**spawn_opts,
) do |ok_val, status|
ok = ok_val
if raise_on_failure && !ok
raise "Command failed with status (#{status.exitstatus})"
end
end
ok
end
def build_test_task
@test_task = RubyMemcheck::TestTask.new(@configuration)
end
end
end
================================================
FILE: test/ruby_memcheck/valgrind_error_test.rb
================================================
# frozen_string_literal: true
require "test_helper"
require "nokogiri"
module RubyMemcheck
class ValgrindErrorTest < Minitest::Test
def setup
@configuration = Configuration.new
end
def test_raises_when_suppressions_generated_but_not_configured
output = ::Nokogiri::XML(<<~XML).at_xpath("//error")
<error>
<unique>0x1ab8</unique>
<tid>1</tid>
<kind>Leak_DefinitelyLost</kind>
<xwhat>
<text>48 bytes in 1 blocks are definitely lost in loss record 6,841 of 11,850</text>
<leakedbytes>48</leakedbytes>
<leakedblocks>1</leakedblocks>
</xwhat>
<stack>
</stack>
<suppression>
</suppression>
</foo>
XML
error = assert_raises do
RubyMemcheck::ValgrindError.new(@configuration, [], output)
end
assert_equal(ValgrindError::SUPPRESSION_NOT_CONFIGURED_ERROR_MSG, error.message)
end
end
end
================================================
FILE: test/ruby_memcheck_test.rb
================================================
# frozen_string_literal: true
require "test_helper"
class RubyMemcheckTest < Minitest::Test
def setup
RubyMemcheck.instance_variable_set(:@default_configuration, nil)
end
def test_config_sets_default_configuration
config = RubyMemcheck.config
assert_equal(config, RubyMemcheck.default_configuration)
end
def test_default_configuration_creates_new_configuration
config = RubyMemcheck.default_configuration
refute_nil(config)
assert_equal(config, RubyMemcheck.default_configuration)
end
end
================================================
FILE: test/test_helper.rb
================================================
# frozen_string_literal: true
$LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
require "ruby_memcheck"
require "minitest/autorun"
if ENV["CI"]
require "etc"
ENV["NCPU"] ||= Etc.nprocessors.to_s
require "minitest/parallel_fork"
end
gitextract_9z716og8/
├── .devcontainer/
│ ├── Dockerfile
│ └── devcontainer.json
├── .github/
│ ├── dependabot.yml
│ └── workflows/
│ ├── cla.yml
│ ├── lint.yml
│ ├── release.yml
│ └── test.yml
├── .gitignore
├── .rubocop.yml
├── .ruby-version
├── Gemfile
├── LICENSE.txt
├── README.md
├── Rakefile
├── exe/
│ └── ruby_memcheck
├── lib/
│ ├── ruby_memcheck/
│ │ ├── configuration.rb
│ │ ├── frame.rb
│ │ ├── rspec/
│ │ │ └── rake_task.rb
│ │ ├── ruby_runner.rb
│ │ ├── stack.rb
│ │ ├── suppression.rb
│ │ ├── test_helper.rb
│ │ ├── test_task.rb
│ │ ├── test_task_reporter.rb
│ │ ├── valgrind_error.rb
│ │ └── version.rb
│ └── ruby_memcheck.rb
├── ruby_memcheck.gemspec
├── suppressions/
│ └── ruby.supp
└── test/
├── ruby_memcheck/
│ ├── ext/
│ │ ├── extconf_one.rb
│ │ ├── extconf_two.rb
│ │ ├── ruby_memcheck_c_test_one.c
│ │ └── ruby_memcheck_c_test_two.c
│ ├── rspec/
│ │ └── rake_task_test.rb
│ ├── ruby_memcheck_suppression_test.rb
│ ├── ruby_runner_test.rb
│ ├── shared_test_task_reporter_tests.rb
│ ├── suppressions/
│ │ └── ruby.supp
│ ├── test_task_test.rb
│ └── valgrind_error_test.rb
├── ruby_memcheck_test.rb
└── test_helper.rb
SYMBOL INDEX (126 symbols across 20 files)
FILE: lib/ruby_memcheck.rb
type RubyMemcheck (line 18) | module RubyMemcheck
function config (line 20) | def config(**opts)
function default_configuration (line 24) | def default_configuration
FILE: lib/ruby_memcheck/configuration.rb
type RubyMemcheck (line 3) | module RubyMemcheck
class Configuration (line 4) | class Configuration
method initialize (line 54) | def initialize(
method command (line 95) | def command(*args)
method get_valgrind_suppression_files (line 125) | def get_valgrind_suppression_files(dir)
FILE: lib/ruby_memcheck/frame.rb
type RubyMemcheck (line 3) | module RubyMemcheck
class Frame (line 4) | class Frame
method initialize (line 7) | def initialize(configuration, loaded_binaries, frame_xml)
method in_ruby? (line 18) | def in_ruby?
method in_binary? (line 26) | def in_binary?
method binary_init_func? (line 32) | def binary_init_func?
method to_s (line 39) | def to_s
FILE: lib/ruby_memcheck/rspec/rake_task.rb
type RubyMemcheck (line 5) | module RubyMemcheck
type RSpec (line 6) | module RSpec
class RakeTask (line 7) | class RakeTask < ::RSpec::Core::RakeTask
method initialize (line 11) | def initialize(*args)
method run_task (line 22) | def run_task(verbose)
method spec_command (line 38) | def spec_command
FILE: lib/ruby_memcheck/ruby_runner.rb
type RubyMemcheck (line 3) | module RubyMemcheck
class RubyRunner (line 4) | class RubyRunner
method initialize (line 8) | def initialize(*args)
method run (line 17) | def run(*args, **options)
FILE: lib/ruby_memcheck/stack.rb
type RubyMemcheck (line 3) | module RubyMemcheck
class Stack (line 4) | class Stack
method initialize (line 7) | def initialize(configuration, loaded_binaries, stack_xml)
method skip? (line 12) | def skip?
method skip_using_ruby_free_at_exit? (line 22) | def skip_using_ruby_free_at_exit?
method skip_using_original_heuristics? (line 38) | def skip_using_original_heuristics?
FILE: lib/ruby_memcheck/suppression.rb
type RubyMemcheck (line 3) | module RubyMemcheck
class Suppression (line 4) | class Suppression
method initialize (line 7) | def initialize(configuration, suppression_node)
method to_s (line 11) | def to_s
FILE: lib/ruby_memcheck/test_task.rb
type RubyMemcheck (line 3) | module RubyMemcheck
class TestTask (line 4) | class TestTask < Rake::TestTask
method initialize (line 8) | def initialize(*args)
method ruby (line 19) | def ruby(*args, **options, &block)
FILE: lib/ruby_memcheck/test_task_reporter.rb
type RubyMemcheck (line 3) | module RubyMemcheck
class TestTaskReporter (line 4) | class TestTaskReporter
method initialize (line 10) | def initialize(configuration)
method run_ruby_with_valgrind (line 15) | def run_ruby_with_valgrind(&block)
method setup (line 21) | def setup
method report_valgrind_errors (line 27) | def report_valgrind_errors
method loaded_binaries (line 39) | def loaded_binaries
method valgrind_xml_files (line 61) | def valgrind_xml_files
method parse_valgrind_output (line 65) | def parse_valgrind_output
method remove_valgrind_xml_files (line 86) | def remove_valgrind_xml_files
method output_valgrind_errors (line 92) | def output_valgrind_errors
FILE: lib/ruby_memcheck/valgrind_error.rb
type RubyMemcheck (line 3) | module RubyMemcheck
class ValgrindError (line 4) | class ValgrindError
method initialize (line 10) | def initialize(configuration, loaded_binaries, error)
method skip? (line 29) | def skip?
method to_s (line 33) | def to_s
method should_filter? (line 49) | def should_filter?
method kind_leak? (line 53) | def kind_leak?
FILE: lib/ruby_memcheck/version.rb
type RubyMemcheck (line 3) | module RubyMemcheck
FILE: test/ruby_memcheck/ext/ruby_memcheck_c_test_one.c
function VALUE (line 5) | static VALUE c_test_one_no_memory_leak(VALUE _)
function c_test_one_allocate_memory_leak (line 11) | static void __attribute__((noinline)) c_test_one_allocate_memory_leak(void)
function VALUE (line 17) | static VALUE c_test_one_memory_leak(VALUE _)
function VALUE (line 23) | static VALUE c_test_one_use_after_free(VALUE _)
function VALUE (line 31) | static VALUE c_test_one_uninitialized_value(VALUE _)
function VALUE (line 39) | static VALUE c_test_one_call_into_ruby_mem_leak(VALUE obj)
function Init_ruby_memcheck_c_test_one (line 47) | void Init_ruby_memcheck_c_test_one(void)
FILE: test/ruby_memcheck/ext/ruby_memcheck_c_test_two.c
function VALUE (line 5) | static VALUE c_test_two_no_memory_leak(VALUE _)
function c_test_two_allocate_memory_leak (line 11) | static void __attribute__((noinline)) c_test_two_allocate_memory_leak(void)
function VALUE (line 17) | static VALUE c_test_two_memory_leak(VALUE _)
function VALUE (line 23) | static VALUE c_test_two_use_after_free(VALUE _)
function VALUE (line 31) | static VALUE c_test_two_uninitialized_value(VALUE _)
function VALUE (line 39) | static VALUE c_test_two_call_into_ruby_mem_leak(VALUE obj)
function Init_ruby_memcheck_c_test_two (line 47) | void Init_ruby_memcheck_c_test_two(void)
FILE: test/ruby_memcheck/rspec/rake_task_test.rb
type RubyMemcheck (line 7) | module RubyMemcheck
type RSpec (line 8) | module RSpec
class RakeTaskTest (line 9) | class RakeTaskTest < Minitest::Test
method setup (line 12) | def setup
method run_with_memcheck (line 19) | def run_with_memcheck(code, raise_on_failure: true, spawn_opts: {})
method build_test_task (line 69) | def build_test_task
FILE: test/ruby_memcheck/ruby_memcheck_suppression_test.rb
type RubyMemcheck (line 6) | module RubyMemcheck
class RubyMemcheckSuppressionTest (line 7) | class RubyMemcheckSuppressionTest < Minitest::Test
method setup (line 8) | def setup
method test_given_a_suppression_node (line 12) | def test_given_a_suppression_node
FILE: test/ruby_memcheck/ruby_runner_test.rb
type RubyMemcheck (line 5) | module RubyMemcheck
class RubyRunnerTest (line 6) | class RubyRunnerTest < Minitest::Test
method setup (line 9) | def setup
method run_with_memcheck (line 16) | def run_with_memcheck(code, raise_on_failure: true, spawn_opts: {})
method build_test_task (line 38) | def build_test_task
FILE: test/ruby_memcheck/shared_test_task_reporter_tests.rb
type RubyMemcheck (line 5) | module RubyMemcheck
type SharedTestTaskReporterTests (line 6) | module SharedTestTaskReporterTests
function test_succeeds_when_there_is_no_memory_leak (line 7) | def test_succeeds_when_there_is_no_memory_leak
function test_reports_memory_leak (line 17) | def test_reports_memory_leak
function test_reports_use_after_free (line 34) | def test_reports_use_after_free
function test_does_not_report_uninitialized_value (line 52) | def test_does_not_report_uninitialized_value
function test_call_into_ruby_mem_leak_does_not_report_when_RUBY_FREE_AT_EXIT_is_not_supported (line 62) | def test_call_into_ruby_mem_leak_does_not_report_when_RUBY_FREE_AT_E...
function test_call_into_ruby_mem_leak_not_report_when_RUBY_FREE_AT_EXIT_is_supported (line 74) | def test_call_into_ruby_mem_leak_not_report_when_RUBY_FREE_AT_EXIT_i...
function test_call_into_ruby_mem_leak_not_report_when_RUBY_FREE_AT_EXIT_is_supported_but_use_only_ruby_free_at_exit_disabled (line 91) | def test_call_into_ruby_mem_leak_not_report_when_RUBY_FREE_AT_EXIT_i...
function test_call_into_ruby_mem_leak_reports_when_not_skipped (line 105) | def test_call_into_ruby_mem_leak_reports_when_not_skipped
function test_suppressions (line 118) | def test_suppressions
function test_generation_of_suppressions (line 130) | def test_generation_of_suppressions
function test_follows_forked_children (line 152) | def test_follows_forked_children
function test_reports_multiple_errors (line 173) | def test_reports_multiple_errors
function test_reports_errors_in_all_binaries (line 193) | def test_reports_errors_in_all_binaries
function test_can_run_multiple_times (line 212) | def test_can_run_multiple_times
function test_ruby_failure_without_errors (line 221) | def test_ruby_failure_without_errors
function test_ruby_failure_with_errors (line 234) | def test_ruby_failure_with_errors
function test_test_helper_is_loaded (line 252) | def test_test_helper_is_loaded
function test_environment_variable_RUBY_MEMCHECK_RUNNING (line 264) | def test_environment_variable_RUBY_MEMCHECK_RUNNING
function test_environment_variable_RUBY_FREE_AT_EXIT (line 276) | def test_environment_variable_RUBY_FREE_AT_EXIT
function test_configration_binary_name (line 288) | def test_configration_binary_name
function test_configration_invalid_binary_name (line 307) | def test_configration_invalid_binary_name
function run_with_memcheck (line 319) | def run_with_memcheck(code, raise_on_failure: true, spawn_opts: {})
function build_configuration (line 323) | def build_configuration(
function build_test_task (line 334) | def build_test_task
FILE: test/ruby_memcheck/test_task_test.rb
type RubyMemcheck (line 5) | module RubyMemcheck
class TestTaskTest (line 6) | class TestTaskTest < Minitest::Test
method setup (line 9) | def setup
method run_with_memcheck (line 18) | def run_with_memcheck(code, raise_on_failure: true, spawn_opts: {})
method build_test_task (line 44) | def build_test_task
FILE: test/ruby_memcheck/valgrind_error_test.rb
type RubyMemcheck (line 6) | module RubyMemcheck
class ValgrindErrorTest (line 7) | class ValgrindErrorTest < Minitest::Test
method setup (line 8) | def setup
method test_raises_when_suppressions_generated_but_not_configured (line 12) | def test_raises_when_suppressions_generated_but_not_configured
FILE: test/ruby_memcheck_test.rb
class RubyMemcheckTest (line 5) | class RubyMemcheckTest < Minitest::Test
method setup (line 6) | def setup
method test_config_sets_default_configuration (line 10) | def test_config_sets_default_configuration
method test_default_configuration_creates_new_configuration (line 16) | def test_default_configuration_creates_new_configuration
Condensed preview — 42 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (63K chars).
[
{
"path": ".devcontainer/Dockerfile",
"chars": 111,
"preview": "FROM ghcr.io/rails/devcontainer/images/ruby:3.4.7\n\nRUN sudo apt-get update && sudo apt-get install -y valgrind\n"
},
{
"path": ".devcontainer/devcontainer.json",
"chars": 145,
"preview": "{\n\t\"name\": \"ruby_memcheck\",\n\t\"build\": {\n\t\t\"dockerfile\": \"Dockerfile\"\n\t},\n\t\"features\": {\n\t\t\"ghcr.io/devcontainers/feature"
},
{
"path": ".github/dependabot.yml",
"chars": 111,
"preview": "version: 2\nupdates:\n - package-ecosystem: \"bundler\"\n directory: \"/\"\n schedule:\n interval: \"weekly\"\n"
},
{
"path": ".github/workflows/cla.yml",
"chars": 619,
"preview": "# .github/workflows/cla.yml\nname: Contributor License Agreement (CLA)\n\non:\n pull_request_target:\n types: [opened, sy"
},
{
"path": ".github/workflows/lint.yml",
"chars": 259,
"preview": "name: Lint\non: [push, pull_request]\njobs:\n rubocop:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkou"
},
{
"path": ".github/workflows/release.yml",
"chars": 389,
"preview": "name: Release\n\non:\n release:\n types: [published]\n\njobs:\n release:\n permissions:\n contents: write\n id-t"
},
{
"path": ".github/workflows/test.yml",
"chars": 1242,
"preview": "name: Test\non: [push, pull_request]\njobs:\n test:\n runs-on: ubuntu-latest\n strategy:\n fail-fast: false\n "
},
{
"path": ".gitignore",
"chars": 89,
"preview": "/.bundle/\n/.yardoc\n/_yardoc/\n/coverage/\n/doc/\n/pkg/\n/spec/reports/\n/tmp/\n\n*.so\n.DS_Store\n"
},
{
"path": ".rubocop.yml",
"chars": 205,
"preview": "inherit_gem:\n rubocop-shopify: rubocop.yml\n\nAllCops:\n SuggestExtensions: false\n\nStyle/GlobalVars:\n Exclude:\n - tes"
},
{
"path": ".ruby-version",
"chars": 6,
"preview": "3.4.7\n"
},
{
"path": "Gemfile",
"chars": 70,
"preview": "# frozen_string_literal: true\n\nsource \"https://rubygems.org\"\n\ngemspec\n"
},
{
"path": "LICENSE.txt",
"chars": 1084,
"preview": "The MIT License (MIT)\n\nCopyright 2021-present, Shopify Inc.\n\nPermission is hereby granted, free of charge, to any person"
},
{
"path": "README.md",
"chars": 11891,
"preview": "# ruby_memcheck\n\nThis gem provides a sane way to use Valgrind's memcheck on your native extension gem.\n\n## Table of cont"
},
{
"path": "Rakefile",
"chars": 687,
"preview": "# frozen_string_literal: true\n\nrequire \"bundler/gem_tasks\"\nrequire \"rake/testtask\"\nrequire \"rake/extensiontask\"\n\nRake::T"
},
{
"path": "exe/ruby_memcheck",
"chars": 179,
"preview": "#!/usr/bin/env ruby\n# frozen_string_literal: true\n\n$LOAD_PATH.unshift(\"#{__dir__}/../lib\")\n\nrequire \"ruby_memcheck\"\n\nrun"
},
{
"path": "lib/ruby_memcheck/configuration.rb",
"chars": 5119,
"preview": "# frozen_string_literal: true\n\nmodule RubyMemcheck\n class Configuration\n DEFAULT_VALGRIND = \"valgrind\"\n DEFAULT_V"
},
{
"path": "lib/ruby_memcheck/frame.rb",
"chars": 1155,
"preview": "# frozen_string_literal: true\n\nmodule RubyMemcheck\n class Frame\n attr_reader :configuration, :loaded_binaries, :fn, "
},
{
"path": "lib/ruby_memcheck/rspec/rake_task.rb",
"chars": 962,
"preview": "# frozen_string_literal: true\n\nrequire \"rspec/core/rake_task\"\n\nmodule RubyMemcheck\n module RSpec\n class RakeTask < :"
},
{
"path": "lib/ruby_memcheck/ruby_runner.rb",
"chars": 668,
"preview": "# frozen_string_literal: true\n\nmodule RubyMemcheck\n class RubyRunner\n attr_reader :configuration\n attr_reader :re"
},
{
"path": "lib/ruby_memcheck/stack.rb",
"chars": 1640,
"preview": "# frozen_string_literal: true\n\nmodule RubyMemcheck\n class Stack\n attr_reader :configuration, :frames\n\n def initia"
},
{
"path": "lib/ruby_memcheck/suppression.rb",
"chars": 529,
"preview": "# frozen_string_literal: true\n\nmodule RubyMemcheck\n class Suppression\n attr_reader :root\n\n def initialize(configu"
},
{
"path": "lib/ruby_memcheck/test_helper.rb",
"chars": 635,
"preview": "# frozen_string_literal: true\n\nat_exit do\n File.open(ENV[\"RUBY_MEMCHECK_LOADED_FEATURES_FILE\"], \"w\") do |f|\n f.write"
},
{
"path": "lib/ruby_memcheck/test_task.rb",
"chars": 673,
"preview": "# frozen_string_literal: true\n\nmodule RubyMemcheck\n class TestTask < Rake::TestTask\n attr_reader :configuration\n "
},
{
"path": "lib/ruby_memcheck/test_task_reporter.rb",
"chars": 2563,
"preview": "# frozen_string_literal: true\n\nmodule RubyMemcheck\n class TestTaskReporter\n VALGRIND_REPORT_MSG = \"Valgrind reported"
},
{
"path": "lib/ruby_memcheck/valgrind_error.rb",
"chars": 1433,
"preview": "# frozen_string_literal: true\n\nmodule RubyMemcheck\n class ValgrindError\n SUPPRESSION_NOT_CONFIGURED_ERROR_MSG =\n "
},
{
"path": "lib/ruby_memcheck/version.rb",
"chars": 75,
"preview": "# frozen_string_literal: true\n\nmodule RubyMemcheck\n VERSION = \"3.0.1\"\nend\n"
},
{
"path": "lib/ruby_memcheck.rb",
"chars": 646,
"preview": "# frozen_string_literal: true\n\nrequire \"English\"\nrequire \"shellwords\"\nrequire \"tempfile\"\nrequire \"rake/testtask\"\n\nrequir"
},
{
"path": "ruby_memcheck.gemspec",
"chars": 1429,
"preview": "# frozen_string_literal: true\n\nrequire_relative \"lib/ruby_memcheck/version\"\n\nGem::Specification.new do |spec|\n spec.nam"
},
{
"path": "suppressions/ruby.supp",
"chars": 1775,
"preview": "{\n On platforms where memcpy is safe for overlapped memory, the compiler will sometimes replace memmove with memcpy. Va"
},
{
"path": "test/ruby_memcheck/ext/extconf_one.rb",
"chars": 182,
"preview": "# frozen_string_literal: true\n\nrequire \"mkmf\"\n\n$warnflags&.gsub!(\"-Wdeclaration-after-statement\", \"\") # rubocop:disable "
},
{
"path": "test/ruby_memcheck/ext/extconf_two.rb",
"chars": 182,
"preview": "# frozen_string_literal: true\n\nrequire \"mkmf\"\n\n$warnflags&.gsub!(\"-Wdeclaration-after-statement\", \"\") # rubocop:disable "
},
{
"path": "test/ruby_memcheck/ext/ruby_memcheck_c_test_one.c",
"chars": 1853,
"preview": "#include <ruby.h>\n\nstatic VALUE cRubyMemcheckCTestOne;\n\nstatic VALUE c_test_one_no_memory_leak(VALUE _)\n{\n return Qni"
},
{
"path": "test/ruby_memcheck/ext/ruby_memcheck_c_test_two.c",
"chars": 1853,
"preview": "#include <ruby.h>\n\nstatic VALUE cRubyMemcheckCTestTwo;\n\nstatic VALUE c_test_two_no_memory_leak(VALUE _)\n{\n return Qni"
},
{
"path": "test/ruby_memcheck/rspec/rake_task_test.rb",
"chars": 1870,
"preview": "# frozen_string_literal: true\n\nrequire \"ruby_memcheck\"\nrequire \"ruby_memcheck/rspec/rake_task\"\nrequire \"ruby_memcheck/sh"
},
{
"path": "test/ruby_memcheck/ruby_memcheck_suppression_test.rb",
"chars": 1341,
"preview": "# frozen_string_literal: true\n\nrequire \"test_helper\"\nrequire \"nokogiri\"\n\nmodule RubyMemcheck\n class RubyMemcheckSuppres"
},
{
"path": "test/ruby_memcheck/ruby_runner_test.rb",
"chars": 921,
"preview": "# frozen_string_literal: true\n\nrequire \"ruby_memcheck/shared_test_task_reporter_tests\"\n\nmodule RubyMemcheck\n class Ruby"
},
{
"path": "test/ruby_memcheck/shared_test_task_reporter_tests.rb",
"chars": 10939,
"preview": "# frozen_string_literal: true\n\nrequire \"test_helper\"\n\nmodule RubyMemcheck\n module SharedTestTaskReporterTests\n def t"
},
{
"path": "test/ruby_memcheck/suppressions/ruby.supp",
"chars": 89,
"preview": "{\n suppress memory_leak\n Memcheck:Leak\n ...\n fun:c_test_one_memory_leak\n ...\n}\n"
},
{
"path": "test/ruby_memcheck/test_task_test.rb",
"chars": 1010,
"preview": "# frozen_string_literal: true\n\nrequire \"ruby_memcheck/shared_test_task_reporter_tests\"\n\nmodule RubyMemcheck\n class Test"
},
{
"path": "test/ruby_memcheck/valgrind_error_test.rb",
"chars": 989,
"preview": "# frozen_string_literal: true\n\nrequire \"test_helper\"\nrequire \"nokogiri\"\n\nmodule RubyMemcheck\n class ValgrindErrorTest <"
},
{
"path": "test/ruby_memcheck_test.rb",
"chars": 530,
"preview": "# frozen_string_literal: true\n\nrequire \"test_helper\"\n\nclass RubyMemcheckTest < Minitest::Test\n def setup\n RubyMemche"
},
{
"path": "test/test_helper.rb",
"chars": 247,
"preview": "# frozen_string_literal: true\n\n$LOAD_PATH.unshift(File.expand_path(\"../lib\", __dir__))\nrequire \"ruby_memcheck\"\n\nrequire "
}
]
About this extraction
This page contains the full source code of the Shopify/ruby_memcheck GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 42 files (57.0 KB), approximately 16.4k tokens, and a symbol index with 126 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.