# -*- coding: us-ascii -*- # frozen_string_literal: false require 'test/unit' require 'timeout' class TestData < Test::Unit::TestCase def test_define klass = Data.define(:foo, :bar) assert_kind_of(Class, klass) assert_equal(%i[foo bar], klass.members) assert_raise(NoMethodError) { Data.new(:foo) } assert_raise(TypeError) { Data.define(0) } # Because some code is shared with Struct, check we don't share unnecessary functionality assert_raise(TypeError) { Data.define(:foo, keyword_init: true) } assert_not_respond_to(Data.define, :define, "Cannot define from defined Data class") end def test_define_edge_cases # non-ascii klass = Data.define(:"r\u{e9}sum\u{e9}") o = klass.new(1) assert_equal(1, o.send(:"r\u{e9}sum\u{e9}")) # junk string klass = Data.define(:"a\000") o = klass.new(1) assert_equal(1, o.send(:"a\000")) # special characters in attribute names klass = Data.define(:a, :b?) x = Object.new o = klass.new("test", x) assert_same(x, o.b?) klass = Data.define(:a, :b!) x = Object.new o = klass.new("test", x) assert_same(x, o.b!) assert_raise(ArgumentError) { Data.define(:x=) } assert_raise(ArgumentError, /duplicate member/) { Data.define(:x, :x) } end def test_define_with_block klass = Data.define(:a, :b) do def c a + b end end assert_equal(3, klass.new(1, 2).c) end def test_initialize klass = Data.define(:foo, :bar) # Regular test = klass.new(1, 2) assert_equal(1, test.foo) assert_equal(2, test.bar) assert_equal(test, klass.new(1, 2)) assert_predicate(test, :frozen?) # Keywords test_kw = klass.new(foo: 1, bar: 2) assert_equal(1, test_kw.foo) assert_equal(2, test_kw.bar) assert_equal(test_kw, klass.new(foo: 1, bar: 2)) assert_equal(test_kw, test) # Wrong protocol assert_raise(ArgumentError) { klass.new(1) } assert_raise(ArgumentError) { klass.new(1, 2, 3) } assert_raise(ArgumentError) { klass.new(foo: 1) } assert_raise(ArgumentError) { klass.new(foo: 1, bar: 2, baz: 3) } # Could be converted to foo: 1, bar: 2, but too smart is confusing assert_raise(ArgumentError) { klass.new(1, bar: 2) } end def test_initialize_redefine klass = Data.define(:foo, :bar) do attr_reader :passed def initialize(*args, **kwargs) @passed = [args, kwargs] super(foo: 1, bar: 2) # so we can experiment with passing wrong numbers of args end end assert_equal([[], {foo: 1, bar: 2}], klass.new(foo: 1, bar: 2).passed) # Positional arguments are converted to keyword ones assert_equal([[], {foo: 1, bar: 2}], klass.new(1, 2).passed) # Missing arguments can be fixed in initialize assert_equal([[], {foo: 1}], klass.new(foo: 1).passed) assert_equal([[], {foo: 42}], klass.new(42).passed) # Extra keyword arguments can be dropped in initialize assert_equal([[], {foo: 1, bar: 2, baz: 3}], klass.new(foo: 1, bar: 2, baz: 3).passed) end def test_instance_behavior klass = Data.define(:foo, :bar) test = klass.new(1, 2) assert_equal(1, test.foo) assert_equal(2, test.bar) assert_equal(%i[foo bar], test.members) assert_equal(1, test.public_send(:foo)) assert_equal(0, test.method(:foo).arity) assert_equal([], test.method(:foo).parameters) assert_equal({foo: 1, bar: 2}, test.to_h) assert_equal({"foo"=>"1", "bar"=>"2"}, test.to_h { [_1.to_s, _2.to_s] }) assert_equal([1, 2], test.deconstruct) assert_equal({foo: 1, bar: 2}, test.deconstruct_keys(nil)) assert_equal({foo: 1}, test.deconstruct_keys(%i[foo])) assert_equal({foo: 1}, test.deconstruct_keys(%i[foo baz])) assert_equal({}, test.deconstruct_keys(%i[foo bar baz])) assert_raise(TypeError) { test.deconstruct_keys(0) } assert_kind_of(Integer, test.hash) end def test_hash measure = Data.define(:amount, :unit) assert_equal(measure[1, 'km'].hash, measure[1, 'km'].hash) assert_not_equal(measure[1, 'km'].hash, measure[10, 'km'].hash) assert_not_equal(measure[1, 'km'].hash, measure[1, 'm'].hash) assert_not_equal(measure[1, 'km'].hash, measure[1.0, 'km'].hash) # Structurally similar data class, but shouldn't be considered # the same hash key measurement = Data.define(:amount, :unit) assert_not_equal(measure[1, 'km'].hash, measurement[1, 'km'].hash) end def test_inspect klass = Data.define(:a) o = klass.new(1) assert_equal("#", o.inspect) Object.const_set(:Foo, klass) assert_equal("#", o.inspect) Object.instance_eval { remove_const(:Foo) } klass = Data.define(:@a) o = klass.new(1) assert_equal("#", o.inspect) end def test_equal klass1 = Data.define(:a) klass2 = Data.define(:a) o1 = klass1.new(1) o2 = klass1.new(1) o3 = klass2.new(1) assert_equal(o1, o2) assert_not_equal(o1, o3) end def test_eql klass1 = Data.define(:a) klass2 = Data.define(:a) o1 = klass1.new(1) o2 = klass1.new(1) o3 = klass2.new(1) assert_operator(o1, :eql?, o2) assert_not_operator(o1, :eql?, o3) end def test_with klass = Data.define(:foo, :bar) source = klass.new(foo: 1, bar: 2) # Simple test = source.with assert_equal(source.object_id, test.object_id) # Changes test = source.with(foo: 10) assert_equal(1, source.foo) assert_equal(2, source.bar) assert_equal(source, klass.new(foo: 1, bar: 2)) assert_equal(10, test.foo) assert_equal(2, test.bar) assert_equal(test, klass.new(foo: 10, bar: 2)) test = source.with(foo: 10, bar: 20) assert_equal(1, source.foo) assert_equal(2, source.bar) assert_equal(source, klass.new(foo: 1, bar: 2)) assert_equal(10, test.foo) assert_equal(20, test.bar) assert_equal(test, klass.new(foo: 10, bar: 20)) # Keyword splat changes = { foo: 10, bar: 20 } test = source.with(**changes) assert_equal(1, source.foo) assert_equal(2, source.bar) assert_equal(source, klass.new(foo: 1, bar: 2)) assert_equal(10, test.foo) assert_equal(20, test.bar) assert_equal(test, klass.new(foo: 10, bar: 20)) # Wrong protocol assert_raise_with_message(ArgumentError, "wrong number of arguments (given 1, expected 0)") do source.with(10) end assert_raise_with_message(ArgumentError, "unknown keywords: :baz, :quux") do source.with(foo: 1, bar: 2, baz: 3, quux: 4) end assert_raise_with_message(ArgumentError, "wrong number of arguments (given 1, expected 0)") do source.with(1, bar: 2) end assert_raise_with_message(ArgumentError, "wrong number of arguments (given 2, expected 0)") do source.with(1, 2) end assert_raise_with_message(ArgumentError, "wrong number of arguments (given 1, expected 0)") do source.with({ bar: 2 }) end end def test_with_initialize oddclass = Data.define(:odd) do def initialize(odd:) raise ArgumentError, "Not odd" unless odd.odd? super(odd: odd) end end assert_raise_with_message(ArgumentError, "Not odd") { oddclass.new(odd: 0) } odd = oddclass.new(odd: 1) assert_raise_with_message(ArgumentError, "Not odd") { odd.with(odd: 2) } end def test_memberless klass = Data.define test = klass.new assert_equal(klass.new, test) assert_not_equal(Data.define.new, test) assert_equal('#', test.inspect) assert_equal([], test.members) assert_equal({}, test.to_h) end def test_dup klass = Data.define(:foo, :bar) test = klass.new(foo: 1, bar: 2) assert_equal(klass.new(foo: 1, bar: 2), test.dup) assert_predicate(test.dup, :frozen?) end Klass = Data.define(:foo, :bar) def test_marshal test = Klass.new(foo: 1, bar: 2) loaded = Marshal.load(Marshal.dump(test)) assert_equal(test, loaded) assert_not_same(test, loaded) assert_predicate(loaded, :frozen?) end end