ostruct icon indicating copy to clipboard operation
ostruct copied to clipboard

different behavior of stdlib ostruct vs. ostruct-gem

Open mrIllo opened this issue 5 months ago • 4 comments

I am on Ruby 3.0

With stdlib ostruct I get:

irb(main):002:0> x = OpenStruct.new
=> #<OpenStruct>
irb(main):002:0> x.varname1 = 'varval 1'
=> "varval 1"
irb(main):003:0> x.to_h
=> {:varname1=>"varval 1"}
irb(main):004:0> x.varname1 = 'varval 2'
=> "varval 2"
irb(main):005:0> x.to_h
=> {:varname1=>"varval 2"}
irb(main):006:0> x.varname2 = 'varval 3'
=> "varval 3"
irb(main):007:0> x.to_h
=> {:varname1=>"varval 2", :varname2=>"varval 3"}
irb(main):008:0> x.varname2 = 'varval 4'
=> "varval 4"
irb(main):009:0> x.to_h
=> {:varname1=>"varval 2", :varname2=>"varval 4"}

Starting with version 0.5.0 of the ostruct-gem: I get:

irb(main):001:0> x = OpenStruct.new
=> #<OpenStruct>
irb(main):002:0> x.varname1 = 'varval 1'
=> "varval 1"
irb(main):003:0> x.to_h
=> {:varname1=>"varval 1"}
irb(main):004:0> x.varname1 = 'varval 2'
=> "varval 2"
irb(main):005:0> x.to_h
=> {:varname1=>"varval 1", false=>"varval 2"}
irb(main):006:0> x.varname2 = 'varval 3'
=> "varval 3"
irb(main):007:0> x.to_h
=> {:varname1=>"varval 1", false=>"varval 2", :varname2=>"varval 3"}
irb(main):008:0> x.varname2 = 'varval 4'
=> "varval 4"
irb(main):009:0> x.to_h
=> {:varname1=>"varval 1", false=>"varval 4", :varname2=>"varval 3"}

one of many side effects is:

irb(main):009:0> y = x.dup
/var/lib/gems/3.0.0/gems/ostruct-0.5.0/lib/ostruct.rb:304:in `[]=': undefined method `to_sym' for false:FalseClass (NoMethodError)
Did you mean?  to_s

mrIllo avatar Jul 21 '25 20:07 mrIllo

I can't reproduce this.

Setup is as follows:

  • Using Docker (docker run -it --rm -v "$PWD":/var/app ruby:3.0 bash)
  • Using the test script below with varying ostruct version:
    • built-in (= no entry in Gemfile)
    • 0.4.0 (= latest/only 0.4.x release, latest release before 0.5.0)
    • 0.5.5 (= latest 0.5.x release)
    • 0.6.3 (= latest 0.6.x release and latest overall release)

Test script:

require "ostruct"

puts OpenStruct::VERSION

x = OpenStruct.new
x.varname1 = 'varval 1'
puts x.to_h

x.varname1 = 'varval 2'
puts x.to_h

x.varname2 = 'varval 3'
puts x.to_h

x.varname2 = 'varval 4'
puts x.to_h

Output with built-in:

0.3.1
{:varname1=>"varval 1"}
{:varname1=>"varval 2"}
{:varname1=>"varval 2", :varname2=>"varval 3"}
{:varname1=>"varval 2", :varname2=>"varval 4"}

Output with 0.4.0:

0.4.0
{:varname1=>"varval 1"}
{:varname1=>"varval 2"}
{:varname1=>"varval 2", :varname2=>"varval 3"}
{:varname1=>"varval 2", :varname2=>"varval 4"}

Output with 0.5.5:

0.5.5
{:varname1=>"varval 1"}
{:varname1=>"varval 2"}
{:varname1=>"varval 2", :varname2=>"varval 3"}
{:varname1=>"varval 2", :varname2=>"varval 4"}

Output with 0.6.3:

0.6.3
{:varname1=>"varval 1"}
{:varname1=>"varval 2"}
{:varname1=>"varval 2", :varname2=>"varval 3"}
{:varname1=>"varval 2", :varname2=>"varval 4"}

Have you run it against an otherwise empty project with just the ostruct gem in the Gemfile? If that does work, but it doesn't work in your project (which includes ostruct and other gems), then there's a chance that some other gem might modify the inner workings of OpenStruct.

The simplest way to find out (that I'm aware of) is to just grep in $GEM_HOME e.g. like so: grep -rl 'OpenStruct' "$GEM_HOME" or grep -rl 'ostruct' "$GEM_HOME" (you could obviously then exclude $GEM_HOME/gems/ostruct-0.5.0 from the matches, because obviously the gem itself includes its name). While this isn't 100% precise (e.g. it wouldn't include odd cases where someone would pass the class name in some extremely dynamic way), it's unlikely that you wouldn't spot an irregularity.

clemens avatar Jul 22 '25 22:07 clemens

Came from Ubuntu 22.04, using its standard ruby (3.0.2). Could reproduce on a totally fresh Ubuntu 24.04 with ruby 3.0.2 installed through rbenv. Couldn't reproduce from ruby 3.0.3 onwards, at ruby 3.0.1 it was reproducable. So it looks like this issue is bound to ruby 3 up to 3.0.2 (maybe with dependency on Ubuntu), while Ubuntu 22 only uses this version (https://launchpad.net/ubuntu/jammy/amd64/ruby3.0)

Thank you Clemens, for pointing this out

mrIllo avatar Jul 24 '25 16:07 mrIllo

I think I might have found the issue by looking at the changelog of Ruby 3.0.3 combined with the diff ostruct 0.5.0. As you can see, the only change in ostruct seems to have to do with Ractors. In the changelog, among all the other things, there seems to be a bug that looks eerily similar to yours.

I'm not 100% sure about this, since I've never used Ractors directly, but the example given in the bug report indicates that "every even closured variable" -- which I interpret to mean "every 2nd value" -- becomes false. In your case then, the only reason why there aren't multiple values with false is that because of the Hash-like nature of ostructs duplicate indexes are effectively impossible. But what points to it is the fact that assigning x.varname2 = 'varval 4' apparently sets false => "varval 4", even though assigning x.varname2 = 'varval 3' before worked as expected. So an interesting experiment would be to try and assign x.varname2 = 'varval 3' twice -- if that then gives you {:varname1=>"varval 1", false=>"varval 3", :varname2=>"varval 3"}, I think we have almost 100% guarantee that that's what's going on.

clemens avatar Jul 25 '25 21:07 clemens

I am unable to reproduce this, trying to replicate the same setups as here:

Came from Ubuntu 22.04, using its standard ruby (3.0.2). Could reproduce on a totally fresh Ubuntu 24.04 with ruby 3.0.2 installed through rbenv. Couldn't reproduce from ruby 3.0.3 onwards, at ruby 3.0.1 it was reproducable. So it looks like this issue is bound to ruby 3 up to 3.0.2 (maybe with dependency on Ubuntu), while Ubuntu 22 only uses this version (https://launchpad.net/ubuntu/jammy/amd64/ruby3.0)

All my tests were inside of docker, I've tried with both fresh ubuntu:22.04 and ubuntu:24.04 (latest) images. I've tried:

  • On 22.04, installing ruby from apt, then running the script's commands in irb both in stock state and after running gem install ostruct --version 0.5.0
  • On 22.04 installing ruby 3.0.2 from rbenv, then doing the same thing
  • On 24.04 installing ruby 3.0.2 from rbenv, then doing the same thing

Nothing ended up resulting in the wrong outputs.. My setup using rbenv was: Running Ubuntu 22.04 and 24.04 using docker run -it --rm ubuntu:24.04 bash (replace version accordingly) then doing the following:

apt-get update && apt-get install curl git build-essential zlib1g-dev
curl -fsSL https://github.com/rbenv/rbenv-installer/raw/HEAD/bin/rbenv-installer | bash
. ~/.bashrc
rbenv install 3.0.2
rbenv shell 3.0.2
irb
#<running script content here then quitting irb>
gem install ostruct --version 0.5.0
irb
#<running script content again then quitting irb>

@mrIllo What are you running Ubuntu on/in? Could this maybe be a case of Ubuntu not being up to date? I have just pulled these ubuntu images in docker. Maybe some architecture thing? I am running on amd64 (x86_64), but I could try on arm64, or running in a 32bit environment. Apart from that I have no idea what else might be different.

edit: as the ruby bug linked above is marked to be on 3.0.1, I tried on that version too, but that still didn't produce a wrong result

VDavid003 avatar Jul 25 '25 21:07 VDavid003