Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/ruby_memcheck.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
require "ruby_memcheck/valgrind_error"
require "ruby_memcheck/suppression"
require "ruby_memcheck/version"
require "ruby_memcheck/xml_backend"

module RubyMemcheck
class << self
Expand Down
11 changes: 2 additions & 9 deletions lib/ruby_memcheck/test_task_reporter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -63,18 +63,11 @@ def valgrind_xml_files
end

def parse_valgrind_output
require "nokogiri"

@errors = []
backend = XmlBackend.create

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
backend.each_error(file) do |error_xml|
error = ValgrindError.new(configuration, loaded_binaries, error_xml)
next if error.skip?

Expand Down
55 changes: 55 additions & 0 deletions lib/ruby_memcheck/xml_backend.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# frozen_string_literal: true

module RubyMemcheck
module XmlBackend
LOAD_ERROR_MSG =
"ruby_memcheck requires either nokogiri or libxml-ruby to parse Valgrind XML output"

def self.create
backend_class.new
end

def self.backend_class
@backend_class ||= detect_backend_class
end

class NodeAdapter
def initialize(node)
@node = node
end

def at_xpath(path)
wrap(find_one(path))
end

def xpath(path)
find_all(path).map { |node| wrap(node) }
end

def content
@node.content
end

def name
@node.name
end

private

def wrap(node)
node && self.class.new(node)
end
end

def self.detect_backend_class
require "ruby_memcheck/xml_backend/libxml_backend"
LibxmlBackend
rescue LoadError
require "ruby_memcheck/xml_backend/nokogiri_backend"
NokogiriBackend
rescue LoadError
raise LoadError, LOAD_ERROR_MSG
end
private_class_method :detect_backend_class
end
end
36 changes: 36 additions & 0 deletions lib/ruby_memcheck/xml_backend/libxml_backend.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# frozen_string_literal: true

require "libxml-ruby"

module RubyMemcheck
module XmlBackend
class LibxmlNodeAdapter < NodeAdapter
private

def find_one(path)
@node.find(path).first
end

def find_all(path)
@node.find(path)
end
end

class LibxmlBackend
def each_error(file)
reader = LibXML::XML::Reader.file(file)

while reader.read
next unless reader.node_type == LibXML::XML::Reader::TYPE_ELEMENT
next unless reader.name == "error"

yield parse(reader.read_outer_xml)
end
end

def parse(xml)
LibxmlNodeAdapter.new(LibXML::XML::Document.string(xml).root)
end
end
end
end
39 changes: 39 additions & 0 deletions lib/ruby_memcheck/xml_backend/nokogiri_backend.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# frozen_string_literal: true

require "nokogiri"

module RubyMemcheck
module XmlBackend
class NokogiriNodeAdapter < NodeAdapter
private

def find_one(path)
@node.at_xpath(path)
end

def find_all(path)
@node.xpath(path)
end
end

class NokogiriBackend
def each_error(file)
File.open(file) do |io|
reader = Nokogiri::XML::Reader(io) 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

yield parse(node.outer_xml)
end
end
end

def parse(xml)
NokogiriNodeAdapter.new(Nokogiri::XML::Document.parse(xml).root)
end
end
end
end
43 changes: 23 additions & 20 deletions test/ruby_memcheck/ruby_memcheck_suppression_test.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# frozen_string_literal: true

require "test_helper"
require "nokogiri"

module RubyMemcheck
class RubyMemcheckSuppressionTest < Minitest::Test
Expand All @@ -10,21 +9,6 @@ def setup
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
Expand All @@ -37,10 +21,29 @@ def test_given_a_suppression_node
fun:ruby_xmalloc
}
EOF
assert_equal(
expected,
RubyMemcheck::Suppression.new(@configuration, suppression).to_s,
)

RubyMemcheck.test_xml_backends.each do |backend|
suppression = backend.parse(<<~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</obj> </sframe>
<sframe> <fun>ruby_xmalloc_body</fun> </sframe>
<sframe> <fun>ruby_xmalloc</fun> </sframe>
</suppression>
</foo>
EOF

assert_equal(
expected,
RubyMemcheck::Suppression.new(@configuration, suppression).to_s,
)
end
end
end
end
41 changes: 21 additions & 20 deletions test/ruby_memcheck/valgrind_error_test.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# frozen_string_literal: true

require "test_helper"
require "nokogiri"

module RubyMemcheck
class ValgrindErrorTest < Minitest::Test
Expand All @@ -10,29 +9,31 @@ def setup
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>
RubyMemcheck.test_xml_backends.each do |backend|
output = backend.parse(<<~XML)
<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>
<stack>
</stack>

<suppression>
</suppression>
</foo>
XML
<suppression>
</suppression>
</error>
XML

error = assert_raises do
RubyMemcheck::ValgrindError.new(@configuration, [], output)
error = assert_raises do
RubyMemcheck::ValgrindError.new(@configuration, [], output)
end
assert_equal(ValgrindError::SUPPRESSION_NOT_CONFIGURED_ERROR_MSG, error.message)
end
assert_equal(ValgrindError::SUPPRESSION_NOT_CONFIGURED_ERROR_MSG, error.message)
end
end
end
50 changes: 50 additions & 0 deletions test/ruby_memcheck/xml_backend_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# frozen_string_literal: true

require "test_helper"

module RubyMemcheck
class XmlBackendTest < Minitest::Test
XML = <<~XML
<valgrindoutput>
<error>
<kind>Leak_DefinitelyLost</kind>
<xwhat>
<text>100 bytes in 1 blocks are definitely lost</text>
</xwhat>
<stack>
<frame>
<fn>c_test_one_memory_leak</fn>
<obj>/tmp/ruby_memcheck_c_test_one.so</obj>
</frame>
</stack>
</error>
</valgrindoutput>
XML

def test_prefers_libxml_when_available
assert_equal("RubyMemcheck::XmlBackend::LibxmlBackend", XmlBackend.backend_class.name)
end

def test_each_error_with_each_available_backend
Tempfile.create(["ruby_memcheck", ".xml"]) do |file|
file.write(XML)
file.flush

RubyMemcheck.test_xml_backends.each do |backend|
errors = []

backend.each_error(file.path) do |error|
errors << error
end

assert_equal(1, errors.length)
assert_equal("Leak_DefinitelyLost", errors.first.at_xpath("kind").content)
assert_equal(
"100 bytes in 1 blocks are definitely lost",
errors.first.at_xpath("xwhat/text").content,
)
end
end
end
end
end
22 changes: 22 additions & 0 deletions test/test_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,28 @@

require "minitest/autorun"

module RubyMemcheck
def self.test_xml_backends
backends = []

begin
require "ruby_memcheck/xml_backend/nokogiri_backend"
backends << XmlBackend::NokogiriBackend.new
rescue LoadError
nil
end

begin
require "ruby_memcheck/xml_backend/libxml_backend"
backends << XmlBackend::LibxmlBackend.new
rescue LoadError
nil
end

backends
end
end

if ENV["CI"]
require "etc"
ENV["NCPU"] ||= Etc.nprocessors.to_s
Expand Down
Loading