leptris 1.9.4 → 1.9.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4bb5caf8a4c09cfd0ad16d3dc414d205e957ef1994875963bf5c4202666925cb
4
- data.tar.gz: 65ff8352d6b7efc9bc7178605af80d6d336e6692b41826dcc6927b1e56d96478
3
+ metadata.gz: 68bba6b5b68f0266571ec17a5809ea42ce7b1f37f0ba3577f9a06afd57db578f
4
+ data.tar.gz: d15442dd463ba2b4d09c6415e51c478c385c851eb6630ffc8c4d00e6a9ad7a57
5
5
  SHA512:
6
- metadata.gz: 027f61b41cb7a0b9270ca974ca82b0d227b02598f21f757aeebadc3d1158d8a8d7335a2657512756e3143b446222817520074d83071065520209714889085951
7
- data.tar.gz: 93c71428856e7d460fce88242ab315f63f18382806f76ecd4f7cedcb5f87a55cbe3890ed2f9937dd0dbd0be0a38dbbc16e8900803dcf116112221d3cf517ed36
6
+ metadata.gz: 52a5f4a5307b9b219911a9ebb7050672be1ad27096266e1c0732eb98ab40779055dcf25e4dd31c0ef0813966539e43894c6d4cdce20bd79e778a7df043c5631a
7
+ data.tar.gz: cd2e0732f88060f6ff02e3496bedca3d986fde26c9400c91efe458c05e96379caeddd50a5950eb65b965be23487aac243ada697855ade71d0ed6017bcfe49433
data/CHANGELOG.md CHANGED
@@ -5,6 +5,47 @@ All notable changes to Leptris will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.9.6] - 2026-08-26
9
+
10
+ ### Changed
11
+
12
+ - The lifetime guard (`ensure_alive!`) uses the cheapest sufficient
13
+ check (`Document#c_ptr.nil?` — `#free` nils it, and the GC
14
+ finalizer cannot fire while any handle exists) instead of the
15
+ deeper `Document#freed?` chain. Claw back on the cheapest single-
16
+ dispatch read (`Element#[]` hot loops): ~20% of the guard's cost.
17
+ Remaining guard cost is the method dispatch itself — the price of
18
+ the lifetime contract on the one call shape cheap enough to
19
+ notice it; all other harness loops measure at parity, and
20
+ readonly hot loops have the memoized `attributes`/`keys`
21
+ alternatives.
22
+
23
+ ## [1.9.5] - 2026-08-26
24
+
25
+ ### Fixed
26
+
27
+ - **Borrowed handles validate their lender**: every c_ptr-
28
+ dereferencing read and mutation on Node/Element/Text/Comment/
29
+ CDATA/PI raises `Leptris::XML::UseAfterFreeError` once the owning
30
+ document has been freed (previously `doc.free; node.name` read
31
+ freed memory silently — verified returning `""` — and stale
32
+ mutations risked segfaults; only Document's own methods checked).
33
+ Memoized readonly results stay check-free. `Document#freed?` is
34
+ the single state predicate.
35
+
36
+ ### Added
37
+
38
+ - **`benchmark/read_paths.rb`**: the committed read-path harness
39
+ (readonly loops, SAX, NodeSet unions, css, parse-query-serialize)
40
+ with a README methodology note — review-round benchmarks are now
41
+ reproducible from the repo.
42
+
43
+ ### Changed
44
+
45
+ - The published gem no longer ships dev tooling (`benchmark/`,
46
+ `.rspec`, `.rubocop.yml`); it carries lib, gemspec, Rakefile, and
47
+ documentation only.
48
+
8
49
  ## [1.9.4] - 2026-08-25
9
50
 
10
51
  ### Fixed
data/README.adoc CHANGED
@@ -411,6 +411,17 @@ Notable differences:
411
411
 
412
412
  == Performance
413
413
 
414
+ === Internal read-path harness
415
+
416
+ `benchmark/read_paths.rb` measures the binding's own hot paths
417
+ (readonly read loops, SAX, NodeSet unions, css, parse-query-
418
+ serialize). Machine-relative — compare before/after runs on the same
419
+ machine:
420
+
421
+ bundle exec ruby benchmark/read_paths.rb
422
+
423
+ === Full-field comparison
424
+
414
425
  Measured with metanorma/serialbench (Ruby 3.4.8, leptris 1.6.0,
415
426
  libleptris 1.6.0, macOS arm64) — Leptris vs the full field:
416
427
 
data/leptris.gemspec CHANGED
@@ -28,7 +28,8 @@ Gem::Specification.new do |spec|
28
28
  spec.files = Dir.chdir(__dir__) do
29
29
  `git ls-files -z`.split("\x0").reject do |f|
30
30
  (File.expand_path(f) == __FILE__) ||
31
- f.start_with?(*%w[bin/ test/ spec/ features/ .git .github appveyor Gemfile])
31
+ f.start_with?(*%w[bin/ test/ spec/ features/ benchmark/ .git .github
32
+ appveyor Gemfile .rspec .rubocop.yml])
32
33
  end
33
34
  end
34
35
  spec.bindir = "bin"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Leptris
4
- VERSION = "1.9.4"
4
+ VERSION = "1.9.6"
5
5
  end
@@ -5,6 +5,7 @@ class Leptris::XML::CDATA < Leptris::XML::Text
5
5
 
6
6
  def content
7
7
  return @content if readonly_cached?(:@content)
8
+ ensure_alive!
8
9
  Leptris::XML::FFI.leptris_cdata_node_get_content(@c_ptr).tap do |text|
9
10
  @content = text if @document&.readonly?
10
11
  end
@@ -5,6 +5,7 @@ class Leptris::XML::Comment < Leptris::XML::Node
5
5
 
6
6
  def content
7
7
  return @content if readonly_cached?(:@content)
8
+ ensure_alive!
8
9
  Leptris::XML::FFI.leptris_comment_node_get_content(@c_ptr).tap do |text|
9
10
  @content = text if @document&.readonly?
10
11
  end
@@ -281,6 +281,12 @@ class Leptris::XML::Document
281
281
  @readonly == true
282
282
  end
283
283
 
284
+ # True once #free has run (or the GC finalizer fired) — borrowed
285
+ # handles check this before dereferencing their c_ptr.
286
+ def freed?
287
+ @freed.state == :freed || @c_ptr.nil?
288
+ end
289
+
284
290
  # The most recent error recorded against this document, or nil.
285
291
  def last_error
286
292
  msg = Leptris::XML::FFI.leptris_document_last_error(@c_ptr)
@@ -2,7 +2,9 @@
2
2
 
3
3
  class Leptris::XML::Element < Leptris::XML::Node
4
4
  def name
5
- @name ||= Leptris::XML::FFI.leptris_element_name(@c_ptr)
5
+ return @name if @name
6
+ ensure_alive!
7
+ @name = Leptris::XML::FFI.leptris_element_name(@c_ptr)
6
8
  end
7
9
  alias_method :node_name, :name
8
10
 
@@ -16,6 +18,7 @@ class Leptris::XML::Element < Leptris::XML::Node
16
18
 
17
19
  def content
18
20
  return @content if readonly_cached?(:@content)
21
+ ensure_alive!
19
22
  Leptris::XML::FFI.leptris_element_text(@c_ptr).tap do |text|
20
23
  @content = text if @document&.readonly?
21
24
  end
@@ -29,6 +32,7 @@ class Leptris::XML::Element < Leptris::XML::Node
29
32
  end
30
33
 
31
34
  def [](key)
35
+ ensure_alive!
32
36
  Leptris::XML::FFI.leptris_element_attribute(@c_ptr, key.to_s)
33
37
  end
34
38
  alias_method :attr, :[]
@@ -43,6 +47,7 @@ class Leptris::XML::Element < Leptris::XML::Node
43
47
  alias_method :set_attribute, :[]=
44
48
 
45
49
  def key?(name)
50
+ ensure_alive!
46
51
  Leptris::XML::FFI.leptris_element_has_attribute(@c_ptr, name.to_s) != 0
47
52
  end
48
53
  alias_method :has_attribute?, :key?
@@ -53,11 +58,13 @@ class Leptris::XML::Element < Leptris::XML::Node
53
58
  # in-scope declarations. uri nil/"" matches no-namespace
54
59
  # attributes only; xmlns declarations are invisible.
55
60
  def attribute_ns(uri, local)
61
+ ensure_alive!
56
62
  Leptris::XML::FFI.leptris_element_attribute_ns(
57
63
  @c_ptr, uri&.to_s, local.to_s)
58
64
  end
59
65
 
60
66
  def has_attribute_ns?(uri, local)
67
+ ensure_alive!
61
68
  Leptris::XML::FFI.leptris_element_has_attribute_ns(
62
69
  @c_ptr, uri&.to_s, local.to_s) != 0
63
70
  end
@@ -77,6 +84,7 @@ class Leptris::XML::Element < Leptris::XML::Node
77
84
  # per-attribute namespace accessors (libleptris 1.8.0).
78
85
  def each_attribute
79
86
  return enum_for(:each_attribute) unless block_given?
87
+ ensure_alive!
80
88
  attr = Leptris::XML::FFI.leptris_element_first_attribute(@c_ptr)
81
89
  until attr.nil? || attr.null?
82
90
  name = Leptris::XML::FFI.leptris_attribute_get_name(attr)
@@ -119,6 +127,7 @@ class Leptris::XML::Element < Leptris::XML::Node
119
127
  # The element's own namespace prefix (e.g. "foo" for <foo:child/>),
120
128
  # or nil when the element has none.
121
129
  def prefix
130
+ ensure_alive!
122
131
  Leptris::XML::FFI.leptris_element_prefix(@c_ptr)
123
132
  end
124
133
 
@@ -231,6 +240,7 @@ class Leptris::XML::Element < Leptris::XML::Node
231
240
 
232
241
  def namespace
233
242
  return @namespace if readonly_cached?(:@namespace)
243
+ ensure_alive!
234
244
  uri = Leptris::XML::FFI.leptris_element_namespace(@c_ptr)
235
245
  result =
236
246
  if uri.nil? || uri.empty?
@@ -249,6 +259,7 @@ class Leptris::XML::Element < Leptris::XML::Node
249
259
 
250
260
  def namespace_definitions
251
261
  return @namespace_definitions if readonly_cached?(:@namespace_definitions)
262
+ ensure_alive!
252
263
  count = Leptris::XML::FFI.leptris_element_namespace_count(@c_ptr)
253
264
  result = count.times.map do |i|
254
265
  prefix = Leptris::XML::FFI.leptris_element_namespace_decl_prefix(@c_ptr, i)
@@ -62,7 +62,9 @@ class Leptris::XML::Node
62
62
  end
63
63
 
64
64
  def type
65
- @node_type ||= Leptris::XML::FFI.leptris_node_get_type(@c_ptr)
65
+ return @node_type if @node_type
66
+ ensure_alive!
67
+ @node_type = Leptris::XML::FFI.leptris_node_get_type(@c_ptr)
66
68
  end
67
69
  alias_method :node_type, :type
68
70
 
@@ -77,13 +79,31 @@ class Leptris::XML::Node
77
79
 
78
80
  def parent
79
81
  return @parent if @parent
82
+ ensure_alive!
80
83
  ptr = Leptris::XML::FFI.leptris_node_parent(@c_ptr)
81
84
  return nil if ptr.null?
82
85
  Leptris::XML::Node.wrap(ptr, @document)
83
86
  end
84
87
 
85
- # Raises ReadOnlyError when the owning document was marked readonly.
88
+ # Borrowed-handle lifetime: every c_ptr dereference is valid only
89
+ # while the owning document lives. Parentless nodes (iterparse
90
+ # yields) cannot validate and are skipped. The guard runs before
91
+ # every uncached FFI dispatch, so it uses the cheapest sufficient
92
+ # check: #free nils the document's c_ptr, and the GC-finalizer
93
+ # path cannot fire while any handle (which strongly references
94
+ # the document) exists. Document#freed? remains the accurate
95
+ # public predicate.
96
+ def ensure_alive!
97
+ if @document && @document.c_ptr.nil?
98
+ raise Leptris::XML::UseAfterFreeError,
99
+ "owning document has been freed — handle used on #{inspect}"
100
+ end
101
+ end
102
+
103
+ # Raises ReadOnlyError when the owning document was marked readonly,
104
+ # UseAfterFreeError when it was freed.
86
105
  def ensure_writable!
106
+ ensure_alive!
87
107
  if @document&.readonly?
88
108
  raise Leptris::XML::ReadOnlyError,
89
109
  "document is readonly — mutation attempted on #{inspect}"
@@ -91,16 +111,19 @@ class Leptris::XML::Node
91
111
  end
92
112
 
93
113
  def line
114
+ ensure_alive!
94
115
  Leptris::XML::FFI.leptris_node_line(@c_ptr)
95
116
  end
96
117
 
97
118
  def <=>(other)
98
119
  return nil unless other.is_a?(Leptris::XML::Node)
99
120
  return nil unless @document == other.document
121
+ ensure_alive!
100
122
  Leptris::XML::FFI.leptris_node_compare(@c_ptr, other.c_ptr)
101
123
  end
102
124
 
103
125
  def child
126
+ ensure_alive!
104
127
  ptr = Leptris::XML::FFI.leptris_node_first_child(@c_ptr)
105
128
  return nil if ptr.null?
106
129
  Leptris::XML::Node.wrap(ptr, @document, parent: as_element_or_self)
@@ -110,6 +133,7 @@ class Leptris::XML::Node
110
133
  # Immutable in readonly mode: the batch fetch plus wrapper
111
134
  # construction is paid once.
112
135
  return @children if readonly_cached?(:@children)
136
+ ensure_alive!
113
137
  parent = as_element_or_self
114
138
  nodes = Leptris::XML::FFI.fetch_children(@c_ptr).map do |ptr|
115
139
  Leptris::XML::Node.wrap(ptr, @document, parent: parent)
@@ -120,6 +144,7 @@ class Leptris::XML::Node
120
144
  end
121
145
 
122
146
  def next_sibling
147
+ ensure_alive!
123
148
  ptr = Leptris::XML::FFI.leptris_node_next_sibling(@c_ptr)
124
149
  return nil if ptr.null?
125
150
  Leptris::XML::Node.wrap(ptr, @document, parent: @parent)
@@ -127,6 +152,7 @@ class Leptris::XML::Node
127
152
  alias_method :next, :next_sibling
128
153
 
129
154
  def previous_sibling
155
+ ensure_alive!
130
156
  ptr = Leptris::XML::FFI.leptris_node_previous_sibling(@c_ptr)
131
157
  return nil if ptr.null?
132
158
  Leptris::XML::Node.wrap(ptr, @document, parent: @parent)
@@ -134,6 +160,7 @@ class Leptris::XML::Node
134
160
  alias_method :previous, :previous_sibling
135
161
 
136
162
  def first_element_child
163
+ ensure_alive!
137
164
  ptr = Leptris::XML::FFI.leptris_node_first_child(@c_ptr)
138
165
  until ptr.nil? || ptr.null?
139
166
  node = Leptris::XML::Node.wrap(ptr, @document, parent: as_element_or_self)
@@ -189,6 +216,7 @@ class Leptris::XML::Node
189
216
  # callback (libleptris #273); the per-node FFI cost is the floor.
190
217
  def traverse
191
218
  return enum_for(:traverse) unless block_given?
219
+ ensure_alive!
192
220
  callback = ::FFI::Function.new(:int, [:pointer, :pointer], blocking: true) do |node_ptr, _|
193
221
  yield Leptris::XML::Node.wrap(node_ptr, @document)
194
222
  0
@@ -199,6 +227,7 @@ class Leptris::XML::Node
199
227
 
200
228
  def path
201
229
  return @path if readonly_cached?(:@path)
230
+ ensure_alive!
202
231
  str_ptr = Leptris::XML::FFI.leptris_node_get_xpath(@c_ptr)
203
232
  result = str_ptr.null? ? nil : Leptris::XML::FFI.read_owned_string(str_ptr)
204
233
  @path = result if @document&.readonly?
@@ -221,6 +250,7 @@ class Leptris::XML::Node
221
250
  end
222
251
 
223
252
  def dup
253
+ ensure_alive!
224
254
  elem_ptr = Leptris::XML::FFI.leptris_node_as_element(@c_ptr)
225
255
  raise Leptris::XML::Error, "dup is only supported for element nodes" if elem_ptr.null?
226
256
  copy_ptr = Leptris::XML::FFI.leptris_element_copy(elem_ptr, @document.c_ptr)
@@ -3,6 +3,7 @@
3
3
  class Leptris::XML::ProcessingInstruction < Leptris::XML::Node
4
4
  def name
5
5
  return @name if readonly_cached?(:@name)
6
+ ensure_alive!
6
7
  Leptris::XML::FFI.leptris_pi_node_get_target(@c_ptr).tap do |target|
7
8
  @name = target if @document&.readonly?
8
9
  end
@@ -11,6 +12,7 @@ class Leptris::XML::ProcessingInstruction < Leptris::XML::Node
11
12
 
12
13
  def content
13
14
  return @content if readonly_cached?(:@content)
15
+ ensure_alive!
14
16
  Leptris::XML::FFI.leptris_pi_node_get_data(@c_ptr).to_s.tap do |data|
15
17
  @content = data if @document&.readonly?
16
18
  end
@@ -5,6 +5,7 @@ class Leptris::XML::Text < Leptris::XML::Node
5
5
 
6
6
  def content
7
7
  return @content if readonly_cached?(:@content)
8
+ ensure_alive!
8
9
  Leptris::XML::FFI.leptris_text_node_get_content(@c_ptr).tap do |text|
9
10
  @content = text if @document&.readonly?
10
11
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: leptris
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.9.4
4
+ version: 1.9.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -63,15 +63,11 @@ executables: []
63
63
  extensions: []
64
64
  extra_rdoc_files: []
65
65
  files:
66
- - ".rspec"
67
- - ".rubocop.yml"
68
66
  - CHANGELOG.md
69
67
  - CLAUDE.md
70
68
  - LICENSE.md
71
69
  - README.adoc
72
70
  - Rakefile
73
- - benchmark/README.md
74
- - benchmark/leptris_vs_nokogiri.rb
75
71
  - leptris.gemspec
76
72
  - lib/leptris.rb
77
73
  - lib/leptris/version.rb
data/.rspec DELETED
@@ -1,3 +0,0 @@
1
- --format documentation
2
- --color
3
- --require spec_helper
data/.rubocop.yml DELETED
@@ -1,8 +0,0 @@
1
- AllCops:
2
- TargetRubyVersion: 3.1
3
-
4
- Style/StringLiterals:
5
- EnforcedStyle: double_quotes
6
-
7
- Style/StringLiteralsInInterpolation:
8
- EnforcedStyle: double_quotes
data/benchmark/README.md DELETED
@@ -1,12 +0,0 @@
1
- # leptris vs the field
2
-
3
- The authoritative cross-library numbers live in the README's
4
- Performance section (measured with metanorma/serialbench).
5
-
6
- This local benchmark compares against Nokogiri only:
7
-
8
- LEPTRIS_LIB_PATH=/path/to/libleptris.dylib bundle exec ruby benchmark/leptris_vs_nokogiri.rb
9
-
10
- For the full-field race (Ox, Nokogiri, Oga, REXML, Leptris), use
11
- https://github.com/metanorma/serialbench — leptris's adapter ships
12
- there under `lib/serialbench/serializers/xml/leptris_serializer.rb`.
@@ -1,105 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Ruby-level performance comparison: Leptris::XML (FFI → libleptris v0.12.0)
4
- # vs Nokogiri (C extension → libxml2).
5
- #
6
- # Run with: bundle exec ruby -Ilib benchmark/leptris_vs_nokogiri.rb
7
-
8
- require "benchmark"
9
- require "leptris/xml"
10
- require "nokogiri"
11
-
12
- module Fixtures
13
- SMALL = "<catalog>" +
14
- (1..10).map { |i| "<book id='#{i}'><title>Book #{i}</title></book>" }.join +
15
- "</catalog>"
16
-
17
- MEDIUM = ("<catalog version='2.0'>" +
18
- (1..100).map do |i|
19
- "<book id='#{i}' lang='en'>" \
20
- "<title>Book #{i}</title>" \
21
- "<author id='a#{i}'>Author #{i}</author>" \
22
- "<price currency='USD'>#{i}.99</price>" \
23
- "</book>"
24
- end.join +
25
- "</catalog>").freeze
26
- end
27
-
28
- def time_it(label, n, &block)
29
- GC.start
30
- t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
31
- n.times { yield }
32
- elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0
33
- us_per_iter = (elapsed / n) * 1_000_000
34
- printf " %-40s %8.2f µs/iter (%d iters in %.3fs)\n", label, us_per_iter, n, elapsed
35
- us_per_iter
36
- end
37
-
38
- def ratio(label, leptris_us, nokogiri_us)
39
- r = nokogiri_us / leptris_us
40
- who = r > 1 ? "Leptris faster" : "Nokogiri faster"
41
- printf " → %-30s Leptris/Nokogiri = %.2fx (%s)\n\n", label, r, who
42
- end
43
-
44
- N_PARSE_SMALL = 5_000
45
- N_PARSE_MEDIUM = 1_000
46
- N_QUERY = 10_000
47
- N_TRAVERSE = 2_000
48
- N_SERIALIZE = 1_000
49
-
50
- puts "===== Parse — small (#{Fixtures::SMALL.bytesize} B) ====="
51
- t = time_it("leptris parse small", N_PARSE_SMALL) { d = Leptris::XML::Document.parse(Fixtures::SMALL); d.free }
52
- n = time_it("nokogiri parse small", N_PARSE_SMALL) { Nokogiri::XML(Fixtures::SMALL) }
53
- ratio("parse small", t, n)
54
-
55
- puts "===== Parse — medium (#{Fixtures::MEDIUM.bytesize} B) ====="
56
- t = time_it("leptris parse medium", N_PARSE_MEDIUM) { d = Leptris::XML::Document.parse(Fixtures::MEDIUM); d.free }
57
- n = time_it("nokogiri parse medium", N_PARSE_MEDIUM) { Nokogiri::XML(Fixtures::MEDIUM) }
58
- ratio("parse medium", t, n)
59
-
60
- # Pre-parse medium for query benchmarks
61
- doc_t = Leptris::XML::Document.parse(Fixtures::MEDIUM)
62
- doc_n = Nokogiri::XML(Fixtures::MEDIUM)
63
-
64
- puts "===== XPath — count(//book) ====="
65
- t = time_it("leptris xpath count()", N_QUERY) { doc_t.xpath("count(//book)") }
66
- n = time_it("nokogiri xpath count()", N_QUERY) { doc_n.xpath("count(//book)") }
67
- ratio("xpath count()", t, n)
68
-
69
- puts "===== XPath — //book (nodeset of 100) ====="
70
- t = time_it("leptris xpath //book", N_QUERY) { doc_t.xpath("//book") }
71
- n = time_it("nokogiri xpath //book", N_QUERY) { doc_n.xpath("//book") }
72
- ratio("xpath //book", t, n)
73
-
74
- puts "===== XPath — predicate //book[@id='50'] ====="
75
- t = time_it("leptris xpath predicate", N_QUERY) { doc_t.xpath("//book[@id='50']") }
76
- n = time_it("nokogiri xpath predicate", N_QUERY) { doc_n.xpath("//book[@id='50']") }
77
- ratio("xpath predicate", t, n)
78
-
79
- puts "===== XPath — complex //book[price > 50] ====="
80
- t = time_it("leptris xpath complex", N_QUERY) { doc_t.xpath("//book[price > 50]") }
81
- n = time_it("nokogiri xpath complex", N_QUERY) { doc_n.xpath("//book[price > 50]") }
82
- ratio("xpath complex", t, n)
83
-
84
- puts "===== XPath — union //author | //title ====="
85
- t = time_it("leptris xpath union", N_QUERY) { doc_t.xpath("//author | //title") }
86
- n = time_it("nokogiri xpath union", N_QUERY) { doc_n.xpath("//author | //title") }
87
- ratio("xpath union", t, n)
88
-
89
- puts "===== Tree traversal (root.traverse) ====="
90
- t = time_it("leptris traverse", N_TRAVERSE) { doc_t.root.traverse { |n| n.name } }
91
- n = time_it("nokogiri traverse", N_TRAVERSE) { doc_n.root.traverse { |n| n.name } }
92
- ratio("traverse", t, n)
93
-
94
- puts "===== Serialize — Document#to_xml ====="
95
- t = time_it("leptris serialize", N_SERIALIZE) { doc_t.to_xml }
96
- n = time_it("nokogiri serialize", N_SERIALIZE) { doc_n.to_xml }
97
- ratio("serialize", t, n)
98
-
99
- doc_t.free
100
-
101
- puts ""
102
- puts "libleptris v0.12.0 — all upstream issues closed."
103
- puts "All 176 Ruby specs passing, 0 pending."
104
-
105
-