different behavior of stdlib ostruct vs. ostruct-gem
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
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)
- built-in (= no entry in
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.
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
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.
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
rubyfromapt, then running the script's commands inirbboth in stock state and after runninggem 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