From: Koji Arai Date: 2002-03-31T23:07:50+09:00 Subject: [ruby-list:34679] Re: 安心できる Marshal と、 Ruby バージョンの検査法 新井です。 In message "[ruby-list:34678] Re: 安心できる Marshalと、 Rubyバージョンの検査法" on 31 Mar 2002 17:55:19 +0900, Koji Arai wrote: > 新井です。 > > In message "[ruby-list:34676] 安心できる Marshalと、 Ruby バージョンの検査法" > on 31 Mar 2002 01:15:53 +0900, > MALTA Koji wrote: > > 丸田と申します。 > > > > (1) > > 「安心できないMarshalのバグ」が修正されたRubyのバージョンはいくつでしょ > > うか? > 1.6.7 Marshal.load にバグあり(Marshal のフォーマットとは関係ない) > 1.6.3 Float をダンプしたときの精度が変わった。 > Bignum の dump -> load が元に戻らない > > となっています。他にも特異クラス関係で変化(バグ修正)があった > ような覚えがあります。ダンプフォーマットの履歴は独立してマニュ > アルに用意した方が良さそうですね。しばし時間をください頑張っ > てみようと思います。 ひとまず、RubyUnit によるテストを書いて動作確認だけで違いを みてみました。これでは、古いバージョンのフォーマットを新しい バージョンで確実に読めるかどうか等はわからないのですが、まあ それはおいおいソースでも見て調べることにします。 対象は version 1.6.0 〜 1.6.7 と 1.7 latest (2002-03-29) で す。テストスクリプトの $version_dependent_behavior で条件分 岐してあるところは振るまいがバージョンにより異なる箇所です。 この違いをまとめると、(()は 1.7 の振るまい) <= 1.6.7 o クラスを clone したもののインスタンスはダンプできるが、ロー ドできない(無名クラスのオブジェクトになるのでダンプできない) <= 1.6.5 o クラスを clone したもののインスタンスはダンプできるが、ロー ドすると変なオブジェクトができる(?) o 特異クラスは、特異でないクラスにダンプされる(特異クラスはダンプできない) o 無名クラスは、ダンプできるがロードできない(無名クラスはダンプできない) <= 1.6.4 o モジュールはダンプできるがロードできない(ロードできる) o 無名モジュールは、ダンプできるがロードできない(無名モジュー ルはダンプできない) <= 1.6.3 o Float をダンプしたときに保存する精度が低い <= 1.6.2 o 正規表現の /m, /x オプションの有無がダンプ時に保存されない 1.6.2, 1.6.3 o 1.6.2, 1.6.3のみBignum をダンプしたものをロードできない この関連バグは他にもあったと思うが再現スクリプトが不明。 <= 1.6.1 o Range が終端を含むかどうかのフラグがダンプ時に保存されない 古いバージョンでダンプできるがロードできないものが、新しい バージョンでどうロードされるかを調べる必要がありますね。 この他、気づいた点ありましたら教えて頂ければ幸いです。 そのうち、マニュアルにも反映させようと思います。 -- 新井康司 (Koji Arai) # test for Marshal for ruby version 1.6 require 'runit/testcase' require 'runit/cui/testrunner' $version_dependent_behavior = true # for test_userClass, test_userModule module UserModule def foo end end class UserClass def foo end end class TestMarshal < RUNIT::TestCase def assert_no_dumpable(obj) assert_exception(TypeError) { begin Marshal.dump obj rescue ArgumentError if /can't dump anonymous/ =~ $!.message raise TypeError else raise "unknown error" end end } end def assert_dumpable_but_not_equal(obj) obj2 = Marshal.load(Marshal.dump(obj)) assert(obj != obj2) assert_equals(obj2.type, obj2.type) end def assert_dumpable_and_equal(obj) obj2 = Marshal.load(Marshal.dump(obj)) assert_equals(obj, obj2) assert_equals(obj.type, obj2.type) end def test_Object assert_dumpable_but_not_equal Object.new end =begin # object with singleton method def test_singletonObject obj = Object.new # On ruby version 1.6.0 - 1.6.2, cause parse error (nested method) class <"1",2=>"2") # 1.7 feature. if $version_dependent_behavior and RUBY_VERSION >= '1.7.0' # On ruby version 1.7, hash with default Proc cannot be able to dumped # see [ruby-dev:15417] assert_no_dumpable(Hash.new { }) end end def test_IO assert_no_dumpable IO.new(0) end def test_File assert_no_dumpable File.open("/") end def test_MatchData assert_no_dumpable(/foo/ =~ "foo" && $~) end def test_Method assert_no_dumpable Object.method(:method) end def test_UnboundMethod assert_no_dumpable Object.instance_method(:id) end def test_Module # On ruby version 1.6.0 - 1.6.4, loaded module is not a module. if $version_dependent_behavior and RUBY_VERSION <= '1.6.4' dump = Marshal.dump Enumerable assert_exception(TypeError) { begin Marshal.load dump rescue TypeError if /is not a module/ =~ $!.message raise else raise 'unknown error' end end } else assert_dumpable_and_equal Enumerable end end def test_userModule # On ruby version 1.6.0 - 1.6.4, loaded module is not a module. if $version_dependent_behavior and RUBY_VERSION <= '1.6.4' # same as test_Module else # Note: this module must be defineed for Marshal.load. assert_dumpable_and_equal(UserModule) end end def test_anonymousModule # On ruby version 1.6.0 - 1.6.4, anonymous class is able to be dumped, # but loaded object is not identical. if $version_dependent_behavior and RUBY_VERSION <= '1.6.4' dump = Marshal.dump(Module.new) assert_exception(ArgumentError) { begin Marshal.load(dump) rescue ArgumentError if /can't retrieve anonymous class/ =~ $!.message raise else raise "unknown error" end end } else assert_no_dumpable Module.new end end def test_Class assert_dumpable_and_equal Class end def test_userClass # Note: this class must be defineed for Marshal.load. assert_dumpable_and_equal(UserClass) end def test_anonymousClass # On ruby version 1.6.0 - 1.6.5, anonymous class able to be dumped, # but can't load it. if $version_dependent_behavior and RUBY_VERSION <= '1.6.5' dump = Marshal.dump(Class.new) assert_exception(ArgumentError) { begin Marshal.load(dump) rescue ArgumentError if /can't retrieve anonymous class/ =~ $!.message raise else raise "unknown error" end end } else assert_no_dumpable Class.new end end def test_clonedClass # On ruby version 1.6.0 - 1.6.7, instance of cloned class is able to # dumped, but loaded object is not identical. # see [ruby-dev:14961] if $version_dependent_behavior if RUBY_VERSION <= '1.6.5' obj = String.clone.new("foo") dump = Marshal.dump(obj) obj2 = Marshal.load dump assert(obj == obj2) assert(obj.type != obj2.type) assert(obj.type.inspect == obj2.type.inspect) elsif RUBY_VERSION <= '1.6.7' dump = Marshal.dump(String.clone.new("foo")) assert_exception(ArgumentError) { Marshal.load dump } else assert_no_dumpable String.clone.new("foo") end else # anonymous class can't be dumped assert_no_dumpable String.clone.new("foo") end end def test_Numeric # assert_fail("") end def test_Integer # assert_fail("") end def test_Fixnum assert_dumpable_and_equal 100 end def test_Bignum # derived from Rubicon assert_dumpable_and_equal 123456789012345678901234567890 assert_dumpable_and_equal -123**99 if $version_dependent_behavior and %w(1.6.2 1.6.3).member?(RUBY_VERSION) dump = Marshal.dump 2**32 assert_exception(ArgumentError) { begin Marshal.load(dump) rescue ArgumentError if /marshal data too short/ =~ $!.message raise else raise "unknown error" end end } else assert_dumpable_and_equal 2**32 end end def test_Float assert_dumpable_and_equal 1.0 # On ruby version 1.6.4, dumped format changed from "%.12g" to "%.16g" if $version_dependent_behavior and RUBY_VERSION <= '1.6.3' assert_dumpable_but_not_equal Math::PI else assert_dumpable_and_equal Math::PI end end def test_Proc assert_no_dumpable proc { } end def test_Process__Status assert_dumpable_and_equal system("true") && $? end def test_Range # Range#== is changed from 1.6.2 # On ruby version 1.6.0 - 1.6.1, Range.new(1,2) != Range.new(1,2) # assert_dumpable_and_equal 1..2 # assert_dumpable_and_equal 1...2 obj = Marshal.load(Marshal.dump 1..2) assert_equals(1, obj.begin) assert_equals(2, obj.end) assert_equals(false, obj.exclude_end?) obj = Marshal.load(Marshal.dump 1...2) assert_equals(1, obj.begin) assert_equals(2, obj.end) # On ruby version 1.6.0 - 1.6.1, the attribute exclude_end? is not saved. if $version_dependent_behavior and RUBY_VERSION <= '1.6.1' assert_equals(false, obj.exclude_end?) else assert_equals(true, obj.exclude_end?) end end def test_Regexp # this test is no consider the /foo/p assert_dumpable_and_equal /foo/ assert_dumpable_and_equal /foo/i assert_dumpable_and_equal /foo/m assert_dumpable_and_equal /foo/x assert_dumpable_and_equal /foo/e assert_dumpable_and_equal /foo/s assert_dumpable_and_equal /foo/u # On ruby version 1.6.0 - 1.6.2, Regexp#== is ignore the option. for obj in [/foo/, /foo/i, /foo/m, /foo/x, /foo/e, /foo/s, /foo/u] obj2 = Marshal.load(Marshal.dump obj) if $version_dependent_behavior and RUBY_VERSION <= '1.6.2' and %w(/foo/m /foo/x).member?(obj.inspect) # On ruby version 1.6.0 - 1.6.2, # //m options is not saved. assert_equals('/foo/', obj2.inspect) else assert_equals(obj.inspect, obj2.inspect) end end end def test_String assert_dumpable_and_equal "foo" end def test_Struct assert_dumpable_and_equal Struct.new("Foo", :foo, :bar) Object.const_set('Foo', Struct.new(:foo, :bar)) assert_dumpable_and_equal Foo end def test_aStruct assert_dumpable_and_equal Struct.new("Bar", :foo, :bar).new("foo", "bar") # see [ruby-dev:14961] end def test_Symbol assert_dumpable_and_equal :foo end def test_Thread assert_no_dumpable Thread.new { sleep } end def test_ThreadGroup assert_no_dumpable ThreadGroup::Default end def test_Time assert_dumpable_and_equal Time.now assert_dumpable_and_equal Time.now.gmtime # time zone is not saved. assert_equals(false, Marshal.load(Marshal.dump(Time.now)).utc?) assert_equals(false, Marshal.load(Marshal.dump(Time.now.gmtime)).utc?) end def test_TrueClass assert_dumpable_and_equal true end def test_NilClass assert_dumpable_and_equal nil end end if $0 == __FILE__ if ARGV.size == 0 suite = RUNIT::TestSuite.new suite.add_test(TestMarshal.suite) else suite = RUNIT::TestSuite.new ARGV.each do |testmethod| suite.add_test(TestMarshal.new(testmethod)) end end RUNIT::CUI::TestRunner.run(suite) end