Full Code of Shopify/ruby_memcheck for AI

main f8d1db6ea771 cached
42 files
57.0 KB
16.4k tokens
126 symbols
1 requests
Download .txt
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
Download .txt
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
Download .txt
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.

Copied to clipboard!