Skip to content

Commit 1d2cbc8

Browse files
committed
Tests working.
1 parent 3a2ab0f commit 1d2cbc8

File tree

9 files changed

+253
-134
lines changed

9 files changed

+253
-134
lines changed

.github/workflows/test-coverage.yaml

Lines changed: 0 additions & 58 deletions
This file was deleted.

.github/workflows/test-external.yaml

Lines changed: 0 additions & 33 deletions
This file was deleted.

.github/workflows/test.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ jobs:
1515
matrix:
1616
os:
1717
- ubuntu
18-
- macos
1918

2019
ruby:
2120
- "3.2"
@@ -37,10 +36,11 @@ jobs:
3736

3837
steps:
3938
- uses: actions/checkout@v4
40-
- uses: ruby/setup-ruby@v1
39+
- uses: ruby/setup-ruby-pkgs@v1
4140
with:
4241
ruby-version: ${{matrix.ruby}}
4342
bundler-cache: true
43+
apt-get: gdb
4444

4545
- name: Run tests
4646
timeout-minutes: 10

data/ruby/gdb/object.py

Lines changed: 204 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,16 @@ def get_constant(self, name):
2828
"""Lazy load and cache Ruby constants"""
2929
if name not in self._constants:
3030
try:
31-
self._constants[name] = int(gdb.parse_and_eval(name))
31+
# Handle special case for RUBY_Qnil which might not be a constant
32+
if name == "RUBY_Qnil":
33+
# RUBY_Qnil is typically 8 or 4 depending on Ruby version
34+
try:
35+
self._constants[name] = int(gdb.parse_and_eval(name))
36+
except:
37+
# Default to 8 for modern Ruby
38+
self._constants[name] = 8
39+
else:
40+
self._constants[name] = int(gdb.parse_and_eval(name))
3241
self.debug(f"Loaded constant {name} = {self._constants[name]}")
3342
except Exception as e:
3443
self.debug(f"Failed to load constant {name}: {e}")
@@ -138,14 +147,201 @@ def print_item_label(self, depth, index):
138147
"""Print a consistently formatted array item label"""
139148
self.print_with_indent(depth, f"[{index:>4}] I: ", end='')
140149

141-
def safe_rp(self, obj):
142-
"""Safely execute rp command with error handling"""
150+
def print_value(self, value):
151+
"""Print a primitive Ruby value (immediate values and simple types)"""
152+
val_int = int(value)
153+
154+
# Handle special constants
155+
if val_int == 0:
156+
print("false")
157+
return True
158+
elif val_int == 0x04 or val_int == 0x08:
159+
print("nil")
160+
return True
161+
elif val_int == 0x14:
162+
print("true")
163+
return True
164+
elif (val_int & 0x01) != 0:
165+
# Fixnum - shift right to get actual value
166+
print(val_int >> 1)
167+
return True
168+
elif (val_int & 0xFF) == 0x0C:
169+
# Symbol - extract the symbol name
170+
return self.print_symbol(value)
171+
172+
# For other types, try to inspect the object
173+
try:
174+
basic = value.cast(self.get_type("struct RBasic").pointer())
175+
flags = int(basic.dereference()['flags'])
176+
type_flag = flags & self.get_constant("RUBY_T_MASK")
177+
178+
if type_flag == self.get_constant("RUBY_T_STRING"):
179+
return self.print_string_value(value, flags)
180+
elif type_flag == self.get_constant("RUBY_T_SYMBOL"):
181+
return self.print_symbol_object(value)
182+
else:
183+
# Unknown type - print type and address
184+
print(f"<Object:0x{val_int:x} type=0x{type_flag:x}>")
185+
return True
186+
except Exception as e:
187+
print(f"<Value:0x{val_int:x}>")
188+
return True
189+
190+
def get_symbol_name(self, symbol_id):
191+
"""Get symbol name from ID using Ruby's global symbol table
192+
This is a direct translation of print_id from Ruby's .gdbinit"""
193+
try:
194+
# rb_id_to_serial
195+
tLAST_OP_ID = 163 # This is a constant from Ruby
196+
ID_ENTRY_UNIT = 512
197+
ID_ENTRY_SIZE = 2
198+
199+
id_val = symbol_id
200+
201+
if id_val > tLAST_OP_ID:
202+
try:
203+
RUBY_ID_SCOPE_SHIFT = int(self.get_constant("RUBY_ID_SCOPE_SHIFT"))
204+
except:
205+
RUBY_ID_SCOPE_SHIFT = 3 # Default value
206+
serial = id_val >> RUBY_ID_SCOPE_SHIFT
207+
else:
208+
serial = id_val
209+
210+
# Access ruby_global_symbols
211+
global_symbols = gdb.parse_and_eval("ruby_global_symbols")
212+
last_id = int(global_symbols['last_id'])
213+
214+
if not serial or serial > last_id:
215+
return None
216+
217+
idx = serial // ID_ENTRY_UNIT
218+
ids = global_symbols['ids'].cast(self.get_type("struct RArray").pointer())
219+
flags = int(ids.dereference()['basic']['flags'])
220+
221+
# Get idsptr and idslen based on whether array is embedded or heap
222+
if flags & self.get_constant("RUBY_FL_USER1"):
223+
idsptr = ids.dereference()['as']['ary']
224+
mask = self.get_constant("RUBY_FL_USER3") | self.get_constant("RUBY_FL_USER4")
225+
shift = self.get_constant("RUBY_FL_USHIFT") + 3
226+
idslen = (flags & mask) >> shift
227+
else:
228+
idsptr = ids.dereference()['as']['heap']['ptr']
229+
idslen = int(ids.dereference()['as']['heap']['len'])
230+
231+
if idx >= idslen:
232+
return None
233+
234+
t = 0
235+
ary_val = idsptr[idx]
236+
237+
# Check if RUBY_Qnil
238+
qnil = self.get_constant("RUBY_Qnil")
239+
if int(ary_val) == qnil:
240+
return None
241+
242+
ary = ary_val.cast(self.get_type("struct RArray").pointer())
243+
ary_flags = int(ary.dereference()['basic']['flags'])
244+
245+
# Get aryptr and arylen based on whether array is embedded or heap
246+
if ary_flags & self.get_constant("RUBY_FL_USER1"):
247+
aryptr = ary.dereference()['as']['ary']
248+
mask = self.get_constant("RUBY_FL_USER3") | self.get_constant("RUBY_FL_USER4")
249+
shift = self.get_constant("RUBY_FL_USHIFT") + 3
250+
arylen = (ary_flags & mask) >> shift
251+
else:
252+
aryptr = ary.dereference()['as']['heap']['ptr']
253+
arylen = int(ary.dereference()['as']['heap']['len'])
254+
255+
result = aryptr[(serial % ID_ENTRY_UNIT) * ID_ENTRY_SIZE + t]
256+
257+
# Check if RUBY_Qnil
258+
if int(result) == qnil:
259+
return None
260+
261+
# Extract string from result
262+
result_flags = int(result.cast(self.get_type("struct RBasic").pointer()).dereference()['flags'])
263+
return self.extract_string_data(result, result_flags)
264+
265+
except Exception as e:
266+
self.debug(f"Error getting symbol name: {e}")
267+
return None
268+
269+
def extract_string_data(self, value, flags):
270+
"""Extract raw string data from a Ruby String object"""
143271
try:
144-
gdb.execute(f"rp {obj}")
272+
rstring = value.cast(self.get_type("struct RString").pointer())
273+
length = int(rstring.dereference()['len'])
274+
275+
if length == 0:
276+
return ""
277+
278+
# Check if embedded or heap-allocated
279+
if (flags & self.get_constant("RUBY_FL_USER1")) != 0:
280+
# Heap string
281+
ptr = rstring.dereference()['as']['heap']['ptr']
282+
else:
283+
# Embedded string
284+
ptr = rstring.dereference()['as']['embed']['ary']
285+
286+
# Read the string data
287+
string_data = gdb.selected_inferior().read_memory(ptr, length).tobytes()
288+
try:
289+
return string_data.decode('utf-8')
290+
except:
291+
return string_data.hex()
292+
except Exception as e:
293+
self.debug(f"Error extracting string: {e}")
294+
return None
295+
296+
def print_symbol(self, value):
297+
"""Print a symbol (immediate symbol encoding)"""
298+
val_int = int(value)
299+
try:
300+
# Symbol ID is encoded in the VALUE
301+
# Different Ruby versions encode it differently
302+
# Try to extract and look up in global symbol table
303+
symbol_id = val_int >> 8
304+
305+
# Try to get symbol name from global symbol table
306+
symbol_name = self.get_symbol_name(symbol_id)
307+
308+
if symbol_name:
309+
print(f":{symbol_name}")
310+
else:
311+
print(f":id_0x{symbol_id:x}")
145312
return True
146313
except Exception as e:
147-
print(f"[Error printing object: {str(e)}]")
148-
return False
314+
self.debug(f"Error printing symbol: {e}")
315+
print(f":0x{val_int:x}")
316+
return True
317+
318+
def print_symbol_object(self, value):
319+
"""Print a T_SYMBOL object (heap-allocated symbol)"""
320+
try:
321+
rsymbol = value.cast(self.get_type("struct RSymbol").pointer())
322+
fstr = rsymbol.dereference()['fstr']
323+
324+
# fstr is a String containing the symbol name
325+
print(":", end='')
326+
self.print_string_value(fstr, int(fstr.cast(self.get_type("struct RBasic").pointer()).dereference()['flags']))
327+
return True
328+
except Exception as e:
329+
print(f":<Symbol:0x{int(value):x}>")
330+
return True
331+
332+
def print_string_value(self, value, flags):
333+
"""Print a Ruby string value"""
334+
string_data = self.extract_string_data(value, flags)
335+
if string_data is not None:
336+
print(string_data, end='')
337+
return True
338+
else:
339+
print(f"<String error>")
340+
return True
341+
342+
def safe_rp(self, obj):
343+
"""Legacy method - now redirects to print_value"""
344+
return self.print_value(obj)
149345

150346
def print_object(self, value, depth, max_depth):
151347
"""Print a value, recursively if it's a hash or array"""
@@ -309,7 +505,7 @@ def print_st_table(self, object_ptr, depth, max_depth):
309505
for i in range(num_entries):
310506
self.print_key_label(depth, i)
311507
key = st_table.dereference()['entries'][i]['key']
312-
self.safe_rp(key)
508+
self.print_object(key, depth + 1, max_depth)
313509

314510
self.print_value_label(depth)
315511
value = st_table.dereference()['entries'][i]['record']
@@ -338,7 +534,7 @@ def print_ar_table(self, object_ptr, flags, depth, max_depth):
338534
# Skip undefined/deleted entries:
339535
if int(key) != self.get_constant("RUBY_Qundef"):
340536
self.print_key_label(depth, i)
341-
self.safe_rp(key)
537+
self.print_object(key, depth + 1, max_depth)
342538

343539
self.print_value_label(depth)
344540
value = ar_table.dereference()['pairs'][i]['val']

fixtures/ruby/gdb/fixtures.rb

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,32 @@ def normalize_output(output)
164164
line.match?(/^Copyright/) ||
165165
line.match?(/^License/) ||
166166
line.match?(/^This is free software/) ||
167-
line.match?(/^There is NO WARRANTY/)
167+
line.match?(/^There is NO WARRANTY/) ||
168+
# Remove lines with process/thread IDs that change on each run
169+
line.match?(/\bLWP \d+\b/) || # Linux thread IDs
170+
line.match?(/\bprocess \d+\b/) || # Process IDs
171+
line.match?(/0x[0-9a-f]+\s+\(LWP/) || # Thread addresses with LWP
172+
line.match?(/^Thread \d+ "/) || # Thread hit messages
173+
line.match?(/^Thread 0x/) || # Thread exit messages
174+
line.match?(/^\[Thread /) || # Thread debugging messages
175+
line.match?(/^\[New Thread/) || # New thread messages
176+
line.match?(/^\[Inferior /) || # Inferior process messages
177+
line.match?(/^Using host libthread_db/) || # Thread debugging library
178+
line.match?(/^Reading symbols from/) || # Symbol loading
179+
line.match?(/Breakpoint \d+ at 0x[0-9a-f]+:/) # Breakpoint addresses (keep line number only)
180+
end
181+
182+
# Normalize lines to remove non-deterministic values
183+
lines.map! do |line|
184+
if line.match?(/^Breakpoint \d+ at /)
185+
# Keep only the meaningful part without the hex address
186+
line.gsub(/^(Breakpoint \d+) at 0x[0-9a-f]+:/, '\1:')
187+
elsif line.match?(/(AR Table|ST Table|Heap Array|Embedded Array|Heap Struct|Embedded Struct) at (0x[0-9a-f]+|\d+)/)
188+
# Normalize memory addresses (both hex and decimal) in table/array/struct headers
189+
line.gsub(/ at (?:0x[0-9a-f]+|\d+)/, ' at <address>')
190+
else
191+
line
192+
end
168193
end
169194

170195
lines.join("\n").strip + "\n"

0 commit comments

Comments
 (0)