diff --git a/.travis.yml b/.travis.yml index f71d9491..a57a33d2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,6 +18,7 @@ rvm: gemfile: - gemfiles/no_rails.gemfile - gemfiles/rails_5.gemfile + - gemfiles/rails_6.gemfile matrix: fast_finish: true @@ -36,3 +37,8 @@ matrix: - gemfile: gemfiles/rails_5.gemfile rvm: 2.5.5 os: osx + # Rails 6 only support Ruby 2.5 and above. + - gemfile: gemfiles/rails_6.gemfile + rvm: 2.3.8 + - gemfile: gemfiles/rails_6.gemfile + rvm: 2.4.6 diff --git a/Rakefile b/Rakefile index 824d28d1..bba5635d 100644 --- a/Rakefile +++ b/Rakefile @@ -72,6 +72,16 @@ begin Rake::Task[:test_all].enhance ["activesupport5"] end + if Rails.version =~ /6\.\d/ + Rake::TestTask.new "activesupport6" do |t| + t.libs << 'test' + t.pattern = 'test/activesupport6/*_test.rb' + t.warning = true + t.verbose = true + end + Rake::Task[:test_all].enhance ["activesupport6"] + end + Rake::TestTask.new "activerecord" do |t| t.libs << 'test' t.pattern = 'test/activerecord/*_test.rb' diff --git a/gemfiles/rails_6.gemfile b/gemfiles/rails_6.gemfile new file mode 100644 index 00000000..495113c5 --- /dev/null +++ b/gemfiles/rails_6.gemfile @@ -0,0 +1,6 @@ +source "https://rubygems.org" + +gem "rails", "6.0.0" +gem "sqlite3" + +gemspec :path => "../" diff --git a/test/activesupport6/Readme.md b/test/activesupport6/Readme.md new file mode 100644 index 00000000..5633ba02 --- /dev/null +++ b/test/activesupport6/Readme.md @@ -0,0 +1,6 @@ +Tests copied from [rails/activesupport/test/json/], +[rails/activesupport/lib/active_support], [rails/activesupport/test]. + +[rails/activesupport/test/json/]: https://github.com/rails/rails/tree/v6.0.0/activesupport/test/json +[rails/activesupport/lib/active_support]: https://github.com/rails/rails/tree/v6.0.0/activesupport/lib/active_support +[rails/activesupport/test]: https://github.com/rails/rails/tree/v6.0.0/activesupport/test diff --git a/test/activesupport6/abstract_unit.rb b/test/activesupport6/abstract_unit.rb new file mode 100644 index 00000000..2d8db992 --- /dev/null +++ b/test/activesupport6/abstract_unit.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +ORIG_ARGV = ARGV.dup + +require "active_support/core_ext/kernel/reporting" + +silence_warnings do + Encoding.default_internal = Encoding::UTF_8 + Encoding.default_external = Encoding::UTF_8 +end + +require "active_support/testing/autorun" +require "active_support/testing/method_call_assertions" + +ENV["NO_RELOAD"] = "1" +require "active_support" + +Thread.abort_on_exception = true + +# Show backtraces for deprecated behavior for quicker cleanup. +ActiveSupport::Deprecation.debug = true + +# Default to old to_time behavior but allow running tests with new behavior +ActiveSupport.to_time_preserves_timezone = ENV["PRESERVE_TIMEZONES"] == "1" + +# Disable available locale checks to avoid warnings running the test suite. +I18n.enforce_available_locales = false + +class ActiveSupport::TestCase + include ActiveSupport::Testing::MethodCallAssertions + + private + # Skips the current run on Rubinius using Minitest::Assertions#skip + def rubinius_skip(message = "") + skip message if RUBY_ENGINE == "rbx" + end + + # Skips the current run on JRuby using Minitest::Assertions#skip + def jruby_skip(message = "") + skip message if defined?(JRUBY_VERSION) + end +end + +require_relative "test_common" diff --git a/test/activesupport6/decoding_test.rb b/test/activesupport6/decoding_test.rb new file mode 100644 index 00000000..e6c30663 --- /dev/null +++ b/test/activesupport6/decoding_test.rb @@ -0,0 +1,133 @@ +# frozen_string_literal: true + +require_relative "abstract_unit" +require "active_support/json" +require "active_support/time" +require_relative "time_zone_test_helpers" + +require 'oj' + +Oj::Rails.set_decoder() + +class TestJSONDecoding < ActiveSupport::TestCase + include TimeZoneTestHelpers + + # Added for testing if Oj is used. + test "oj is used as an encoder" do + assert_equal ActiveSupport.json_encoder, Oj::Rails::Encoder + end + + class Foo + def self.json_create(object) + "Foo" + end + end + + TESTS = { + %q({"returnTo":{"\/categories":"\/"}}) => { "returnTo" => { "/categories" => "/" } }, + %q({"return\\"To\\":":{"\/categories":"\/"}}) => { "return\"To\":" => { "/categories" => "/" } }, + %q({"returnTo":{"\/categories":1}}) => { "returnTo" => { "/categories" => 1 } }, + %({"returnTo":[1,"a"]}) => { "returnTo" => [1, "a"] }, + %({"returnTo":[1,"\\"a\\",", "b"]}) => { "returnTo" => [1, "\"a\",", "b"] }, + %({"a": "'", "b": "5,000"}) => { "a" => "'", "b" => "5,000" }, + %({"a": "a's, b's and c's", "b": "5,000"}) => { "a" => "a's, b's and c's", "b" => "5,000" }, + # multibyte + %({"matzue": "松江", "asakusa": "浅草"}) => { "matzue" => "松江", "asakusa" => "浅草" }, + %({"a": "2007-01-01"}) => { "a" => Date.new(2007, 1, 1) }, + %({"a": "2007-01-01 01:12:34 Z"}) => { "a" => Time.utc(2007, 1, 1, 1, 12, 34) }, + %(["2007-01-01 01:12:34 Z"]) => [Time.utc(2007, 1, 1, 1, 12, 34)], + %(["2007-01-01 01:12:34 Z", "2007-01-01 01:12:35 Z"]) => [Time.utc(2007, 1, 1, 1, 12, 34), Time.utc(2007, 1, 1, 1, 12, 35)], + # no time zone + %({"a": "2007-01-01 01:12:34"}) => { "a" => Time.new(2007, 1, 1, 1, 12, 34, "-05:00") }, + # invalid date + %({"a": "1089-10-40"}) => { "a" => "1089-10-40" }, + # xmlschema date notation + %({"a": "2009-08-10T19:01:02"}) => { "a" => Time.new(2009, 8, 10, 19, 1, 2, "-04:00") }, + %({"a": "2009-08-10T19:01:02Z"}) => { "a" => Time.utc(2009, 8, 10, 19, 1, 2) }, + %({"a": "2009-08-10T19:01:02+02:00"}) => { "a" => Time.utc(2009, 8, 10, 17, 1, 2) }, + %({"a": "2009-08-10T19:01:02-05:00"}) => { "a" => Time.utc(2009, 8, 11, 00, 1, 2) }, + # needs to be *exact* + %({"a": " 2007-01-01 01:12:34 Z "}) => { "a" => " 2007-01-01 01:12:34 Z " }, + %({"a": "2007-01-01 : it's your birthday"}) => { "a" => "2007-01-01 : it's your birthday" }, + %([]) => [], + %({}) => {}, + %({"a":1}) => { "a" => 1 }, + %({"a": ""}) => { "a" => "" }, + %({"a":"\\""}) => { "a" => "\"" }, + %({"a": null}) => { "a" => nil }, + %({"a": true}) => { "a" => true }, + %({"a": false}) => { "a" => false }, + '{"bad":"\\\\","trailing":""}' => { "bad" => "\\", "trailing" => "" }, + %q({"a": "http:\/\/test.host\/posts\/1"}) => { "a" => "http://test.host/posts/1" }, + %q({"a": "\u003cunicode\u0020escape\u003e"}) => { "a" => "" }, + '{"a": "\\\\u0020skip double backslashes"}' => { "a" => "\\u0020skip double backslashes" }, + %q({"a": "\u003cbr /\u003e"}) => { "a" => "
" }, + %q({"b":["\u003ci\u003e","\u003cb\u003e","\u003cu\u003e"]}) => { "b" => ["", "", ""] }, + # test combination of dates and escaped or unicode encoded data in arrays + %q([{"d":"1970-01-01", "s":"\u0020escape"},{"d":"1970-01-01", "s":"\u0020escape"}]) => + [{ "d" => Date.new(1970, 1, 1), "s" => " escape" }, { "d" => Date.new(1970, 1, 1), "s" => " escape" }], + %q([{"d":"1970-01-01","s":"http:\/\/example.com"},{"d":"1970-01-01","s":"http:\/\/example.com"}]) => + [{ "d" => Date.new(1970, 1, 1), "s" => "http://example.com" }, + { "d" => Date.new(1970, 1, 1), "s" => "http://example.com" }], + # tests escaping of "\n" char with Yaml backend + %q({"a":"\n"}) => { "a" => "\n" }, + %q({"a":"\u000a"}) => { "a" => "\n" }, + %q({"a":"Line1\u000aLine2"}) => { "a" => "Line1\nLine2" }, + # prevent json unmarshalling + '{"json_class":"TestJSONDecoding::Foo"}' => { "json_class" => "TestJSONDecoding::Foo" }, + # json "fragments" - these are invalid JSON, but ActionPack relies on this + '"a string"' => "a string", + "1.1" => 1.1, + "1" => 1, + "-1" => -1, + "true" => true, + "false" => false, + "null" => nil + } + + TESTS.each_with_index do |(json, expected), index| + fail_message = "JSON decoding failed for #{json}" + + test "json decodes #{index}" do + with_tz_default "Eastern Time (US & Canada)" do + with_parse_json_times(true) do + silence_warnings do + if expected.nil? + assert_nil ActiveSupport::JSON.decode(json), fail_message + else + assert_equal expected, ActiveSupport::JSON.decode(json), fail_message + end + end + end + end + end + end + + test "json decodes time json with time parsing disabled" do + with_parse_json_times(false) do + expected = { "a" => "2007-01-01 01:12:34 Z" } + assert_equal expected, ActiveSupport::JSON.decode(%({"a": "2007-01-01 01:12:34 Z"})) + end + end + + def test_failed_json_decoding + assert_raise(ActiveSupport::JSON.parse_error) { ActiveSupport::JSON.decode(%(undefined)) } + assert_raise(ActiveSupport::JSON.parse_error) { ActiveSupport::JSON.decode(%({a: 1})) } + assert_raise(ActiveSupport::JSON.parse_error) { ActiveSupport::JSON.decode(%({: 1})) } + assert_raise(ActiveSupport::JSON.parse_error) { ActiveSupport::JSON.decode(%()) } + end + + def test_cannot_pass_unsupported_options + assert_raise(ArgumentError) { ActiveSupport::JSON.decode("", create_additions: true) } + end + + private + + def with_parse_json_times(value) + old_value = ActiveSupport.parse_json_times + ActiveSupport.parse_json_times = value + yield + ensure + ActiveSupport.parse_json_times = old_value + end +end diff --git a/test/activesupport6/encoding_test.rb b/test/activesupport6/encoding_test.rb new file mode 100644 index 00000000..73bdeba8 --- /dev/null +++ b/test/activesupport6/encoding_test.rb @@ -0,0 +1,507 @@ +# frozen_string_literal: true + +require "securerandom" +require_relative "abstract_unit" +require "active_support/core_ext/string/inflections" +require "active_support/json" +require "active_support/time" +require_relative "time_zone_test_helpers" +require_relative "encoding_test_cases" + +require 'oj' + +# Sets the ActiveSupport encoder to be Oj and also wraps the setting of +# globals. +Oj::Rails.set_encoder() +#Oj::Rails.optimize(Hash, Array, BigDecimal, Time, Range, Regexp, ActiveSupport::TimeWithZone) +Oj::Rails.optimize() + +class TestJSONEncoding < ActiveSupport::TestCase + include TimeZoneTestHelpers + + # Added for testing if Oj is used. + test "oj is used as an encoder" do + assert_equal ActiveSupport.json_encoder, Oj::Rails::Encoder + end + + def sorted_json(json) + if json.start_with?("{") && json.end_with?("}") + "{" + json[1..-2].split(",").sort.join(",") + "}" + else + json + end + end + + JSONTest::EncodingTestCases.constants.each do |class_tests| + define_method("test_#{class_tests[0..-6].underscore}") do + prev = ActiveSupport.use_standard_json_time_format + + standard_class_tests = /Standard/.match?(class_tests) + + ActiveSupport.escape_html_entities_in_json = !standard_class_tests + ActiveSupport.use_standard_json_time_format = standard_class_tests + JSONTest::EncodingTestCases.const_get(class_tests).each do |pair| + assert_equal pair.last, sorted_json(ActiveSupport::JSON.encode(pair.first)) + end + ensure + ActiveSupport.escape_html_entities_in_json = false + ActiveSupport.use_standard_json_time_format = prev + end + end + + def test_process_status + rubinius_skip "https://github.com/rubinius/rubinius/issues/3334" + + # There doesn't seem to be a good way to get a handle on a Process::Status object without actually + # creating a child process, hence this to populate $? + system("not_a_real_program_#{SecureRandom.hex}") + assert_equal %({"exitstatus":#{$?.exitstatus},"pid":#{$?.pid}}), ActiveSupport::JSON.encode($?) + end + + def test_hash_encoding + assert_equal %({\"a\":\"b\"}), ActiveSupport::JSON.encode(a: :b) + assert_equal %({\"a\":1}), ActiveSupport::JSON.encode("a" => 1) + assert_equal %({\"a\":[1,2]}), ActiveSupport::JSON.encode("a" => [1, 2]) + assert_equal %({"1":2}), ActiveSupport::JSON.encode(1 => 2) + + assert_equal %({\"a\":\"b\",\"c\":\"d\"}), sorted_json(ActiveSupport::JSON.encode(a: :b, c: :d)) + end + + def test_hash_keys_encoding + ActiveSupport.escape_html_entities_in_json = true + assert_equal "{\"\\u003c\\u003e\":\"\\u003c\\u003e\"}", ActiveSupport::JSON.encode("<>" => "<>") + ensure + ActiveSupport.escape_html_entities_in_json = false + end + + def test_utf8_string_encoded_properly + # The original test seems to expect that + # ActiveSupport.escape_html_entities_in_json reverts to true even after + # being set to false. I haven't been able to figure that out so the value is + # set to true, the default, before running the test. This might be wrong but + # for now it will have to do. + ActiveSupport.escape_html_entities_in_json = true + result = ActiveSupport::JSON.encode("€2.99") + assert_equal '"€2.99"', result + assert_equal(Encoding::UTF_8, result.encoding) + + result = ActiveSupport::JSON.encode("✎☺") + assert_equal '"✎☺"', result + assert_equal(Encoding::UTF_8, result.encoding) + end + + def test_non_utf8_string_transcodes + s = "二".encode("Shift_JIS") + result = ActiveSupport::JSON.encode(s) + assert_equal '"二"', result + assert_equal Encoding::UTF_8, result.encoding + end + + def test_wide_utf8_chars + w = "𠜎" + result = ActiveSupport::JSON.encode(w) + assert_equal '"𠜎"', result + end + + def test_wide_utf8_roundtrip + hash = { string: "𐒑" } + json = ActiveSupport::JSON.encode(hash) + decoded_hash = ActiveSupport::JSON.decode(json) + assert_equal "𐒑", decoded_hash["string"] + end + + def test_hash_key_identifiers_are_always_quoted + values = { 0 => 0, 1 => 1, :_ => :_, "$" => "$", "a" => "a", :A => :A, :A0 => :A0, "A0B" => "A0B" } + assert_equal %w( "$" "A" "A0" "A0B" "_" "a" "0" "1" ).sort, object_keys(ActiveSupport::JSON.encode(values)) + end + + def test_hash_should_allow_key_filtering_with_only + assert_equal %({"a":1}), ActiveSupport::JSON.encode({ "a" => 1, :b => 2, :c => 3 }, { only: "a" }) + end + + def test_hash_should_allow_key_filtering_with_except + assert_equal %({"b":2}), ActiveSupport::JSON.encode({ "foo" => "bar", :b => 2, :c => 3 }, { except: ["foo", :c] }) + end + + def test_time_to_json_includes_local_offset + with_standard_json_time_format(true) do + with_env_tz "US/Eastern" do + assert_equal %("2005-02-01T15:15:10.000-05:00"), ActiveSupport::JSON.encode(Time.local(2005, 2, 1, 15, 15, 10)) + end + end + end + + def test_hash_with_time_to_json + with_standard_json_time_format(false) do + assert_equal '{"time":"2009/01/01 00:00:00 +0000"}', { time: Time.utc(2009) }.to_json + end + end + + def test_nested_hash_with_float + assert_nothing_raised do + hash = { + "CHI" => { + display_name: "chicago", + latitude: 123.234 + } + } + ActiveSupport::JSON.encode(hash) + end + end + + def test_hash_like_with_options + h = JSONTest::Hashlike.new + json = h.to_json only: [:foo] + + assert_equal({ "foo" => "hello" }, JSON.parse(json)) + end + + def test_object_to_json_with_options + obj = Object.new + obj.instance_variable_set :@foo, "hello" + obj.instance_variable_set :@bar, "world" + json = obj.to_json only: ["foo"] + + assert_equal({ "foo" => "hello" }, JSON.parse(json)) + end + + def test_struct_to_json_with_options + struct = Struct.new(:foo, :bar).new + struct.foo = "hello" + struct.bar = "world" + json = struct.to_json only: [:foo] + + assert_equal({ "foo" => "hello" }, JSON.parse(json)) + end + + def test_struct_to_json_with_options_nested + klass = Struct.new(:foo, :bar) + struct = klass.new "hello", "world" + parent_struct = klass.new struct, "world" + json = parent_struct.to_json only: [:foo] + + assert_equal({ "foo" => { "foo" => "hello" } }, JSON.parse(json)) + end + + + def test_hash_should_pass_encoding_options_to_children_in_as_json + person = { + name: "John", + address: { + city: "London", + country: "UK" + } + } + json = person.as_json only: [:address, :city] + + assert_equal({ "address" => { "city" => "London" } }, json) + end + + def test_hash_should_pass_encoding_options_to_children_in_to_json + person = { + name: "John", + address: { + city: "London", + country: "UK" + } + } + json = person.to_json only: [:address, :city] + + assert_equal(%({"address":{"city":"London"}}), json) + end + + def test_array_should_pass_encoding_options_to_children_in_as_json + people = [ + { name: "John", address: { city: "London", country: "UK" } }, + { name: "Jean", address: { city: "Paris", country: "France" } } + ] + json = people.as_json only: [:address, :city] + expected = [ + { "address" => { "city" => "London" } }, + { "address" => { "city" => "Paris" } } + ] + + assert_equal(expected, json) + end + + def test_array_should_pass_encoding_options_to_children_in_to_json + people = [ + { name: "John", address: { city: "London", country: "UK" } }, + { name: "Jean", address: { city: "Paris", country: "France" } } + ] + json = people.to_json only: [:address, :city] + + assert_equal(%([{"address":{"city":"London"}},{"address":{"city":"Paris"}}]), json) + end + + People = Class.new(BasicObject) do + include Enumerable + def initialize + @people = [ + { name: "John", address: { city: "London", country: "UK" } }, + { name: "Jean", address: { city: "Paris", country: "France" } } + ] + end + def each(*, &blk) + @people.each do |p| + yield p if blk + p + end.each + end + end + + def test_enumerable_should_generate_json_with_as_json + json = People.new.as_json only: [:address, :city] + expected = [ + { "address" => { "city" => "London" } }, + { "address" => { "city" => "Paris" } } + ] + + assert_equal(expected, json) + end + + def test_enumerable_should_generate_json_with_to_json + json = People.new.to_json only: [:address, :city] + assert_equal(%([{"address":{"city":"London"}},{"address":{"city":"Paris"}}]), json) + end + + def test_enumerable_should_pass_encoding_options_to_children_in_as_json + json = People.new.each.as_json only: [:address, :city] + expected = [ + { "address" => { "city" => "London" } }, + { "address" => { "city" => "Paris" } } + ] + + assert_equal(expected, json) + end + + def test_enumerable_should_pass_encoding_options_to_children_in_to_json + json = People.new.each.to_json only: [:address, :city] + + assert_equal(%([{"address":{"city":"London"}},{"address":{"city":"Paris"}}]), json) + end + + class CustomWithOptions + attr_accessor :foo, :bar + + def as_json(options = {}) + options[:only] = %w(foo bar) + super(options) + end + end + + def test_hash_to_json_should_not_keep_options_around + f = CustomWithOptions.new + f.foo = "hello" + f.bar = "world" + + hash = { "foo" => f, "other_hash" => { "foo" => "other_foo", "test" => "other_test" } } + assert_equal({ "foo" => { "foo" => "hello", "bar" => "world" }, + "other_hash" => { "foo" => "other_foo", "test" => "other_test" } }, ActiveSupport::JSON.decode(hash.to_json)) + end + + def test_array_to_json_should_not_keep_options_around + f = CustomWithOptions.new + f.foo = "hello" + f.bar = "world" + + array = [f, { "foo" => "other_foo", "test" => "other_test" }] + assert_equal([{ "foo" => "hello", "bar" => "world" }, + { "foo" => "other_foo", "test" => "other_test" }], ActiveSupport::JSON.decode(array.to_json)) + end + + class OptionsTest + def as_json(options = :default) + options + end + end + + def test_hash_as_json_without_options + json = { foo: OptionsTest.new }.as_json + assert_equal({ "foo" => :default }, json) + end + + def test_array_as_json_without_options + json = [ OptionsTest.new ].as_json + assert_equal([:default], json) + end + + def test_struct_encoding + Struct.new("UserNameAndEmail", :name, :email) + Struct.new("UserNameAndDate", :name, :date) + Struct.new("Custom", :name, :sub) + user_email = Struct::UserNameAndEmail.new "David", "sample@example.com" + user_birthday = Struct::UserNameAndDate.new "David", Date.new(2010, 01, 01) + custom = Struct::Custom.new "David", user_birthday + + json_strings = "" + json_string_and_date = "" + json_custom = "" + + assert_nothing_raised do + json_strings = user_email.to_json + json_string_and_date = user_birthday.to_json + json_custom = custom.to_json + end + + assert_equal({ "name" => "David", + "sub" => { + "name" => "David", + "date" => "2010-01-01" } }, ActiveSupport::JSON.decode(json_custom)) + + assert_equal({ "name" => "David", "email" => "sample@example.com" }, + ActiveSupport::JSON.decode(json_strings)) + + assert_equal({ "name" => "David", "date" => "2010-01-01" }, + ActiveSupport::JSON.decode(json_string_and_date)) + end + + def test_nil_true_and_false_represented_as_themselves + assert_nil nil.as_json + assert_equal true, true.as_json + assert_equal false, false.as_json + end + + class HashWithAsJson < Hash + attr_accessor :as_json_called + + def initialize(*) + super + end + + def as_json(options = {}) + @as_json_called = true + super + end + end + + def test_json_gem_dump_by_passing_active_support_encoder + h = HashWithAsJson.new + h[:foo] = "hello" + h[:bar] = "world" + + assert_equal %({"foo":"hello","bar":"world"}), JSON.dump(h) + assert_nil h.as_json_called + end + + def test_json_gem_generate_by_passing_active_support_encoder + h = HashWithAsJson.new + h[:foo] = "hello" + h[:bar] = "world" + + assert_equal %({"foo":"hello","bar":"world"}), JSON.generate(h) + assert_nil h.as_json_called + end + + def test_json_gem_pretty_generate_by_passing_active_support_encoder + h = HashWithAsJson.new + h[:foo] = "hello" + h[:bar] = "world" + + assert_equal < Float::INFINITY } + end + end + + def test_to_json_works_when_as_json_returns_infinite_number + assert_equal '{"number":null}', InfiniteNumber.new.to_json + end + + class NaNNumber + def as_json(options = nil) + { "number" => Float::NAN } + end + end + + def test_to_json_works_when_as_json_returns_NaN_number + assert_equal '{"number":null}', NaNNumber.new.to_json + end + + def test_to_json_works_on_io_objects + assert_equal STDOUT.to_s.to_json, STDOUT.to_json + end + + private + + def object_keys(json_object) + json_object[1..-2].scan(/([^{}:,\s]+):/).flatten.sort + end + + def with_standard_json_time_format(boolean = true) + old, ActiveSupport.use_standard_json_time_format = ActiveSupport.use_standard_json_time_format, boolean + yield + ensure + ActiveSupport.use_standard_json_time_format = old + end + + def with_time_precision(value) + old_value = ActiveSupport::JSON::Encoding.time_precision + ActiveSupport::JSON::Encoding.time_precision = value + yield + ensure + ActiveSupport::JSON::Encoding.time_precision = old_value + end +end diff --git a/test/activesupport6/encoding_test_cases.rb b/test/activesupport6/encoding_test_cases.rb new file mode 100644 index 00000000..5a470045 --- /dev/null +++ b/test/activesupport6/encoding_test_cases.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true + +require "bigdecimal" +require "date" +require "time" +require "pathname" +require "uri" + +module JSONTest + class Foo + def initialize(a, b) + @a, @b = a, b + end + end + + class Hashlike + def to_hash + { foo: "hello", bar: "world" } + end + end + + class Custom + def initialize(serialized) + @serialized = serialized + end + + def as_json(options = nil) + @serialized + end + end + + MyStruct = Struct.new(:name, :value) do + def initialize(*) + @unused = "unused instance variable" + super + end + end + + module EncodingTestCases + TrueTests = [[ true, %(true) ]] + FalseTests = [[ false, %(false) ]] + NilTests = [[ nil, %(null) ]] + NumericTests = [[ 1, %(1) ], + [ 2.5, %(2.5) ], + [ 0.0 / 0.0, %(null) ], + [ 1.0 / 0.0, %(null) ], + [ -1.0 / 0.0, %(null) ], + [ BigDecimal("0.0") / BigDecimal("0.0"), %(null) ], + [ BigDecimal("2.5"), %("#{BigDecimal('2.5')}") ]] + + StringTests = [[ "this is the ", %("this is the \\u003cstring\\u003e")], + [ 'a "string" with quotes & an ampersand', %("a \\"string\\" with quotes \\u0026 an ampersand") ], + [ "http://test.host/posts/1", %("http://test.host/posts/1")], + [ "Control characters: \x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\u2028\u2029", + %("Control characters: \\u0000\\u0001\\u0002\\u0003\\u0004\\u0005\\u0006\\u0007\\b\\t\\n\\u000b\\f\\r\\u000e\\u000f\\u0010\\u0011\\u0012\\u0013\\u0014\\u0015\\u0016\\u0017\\u0018\\u0019\\u001a\\u001b\\u001c\\u001d\\u001e\\u001f\\u2028\\u2029") ]] + + ArrayTests = [[ ["a", "b", "c"], %([\"a\",\"b\",\"c\"]) ], + [ [1, "a", :b, nil, false], %([1,\"a\",\"b\",null,false]) ]] + + HashTests = [[ { foo: "bar" }, %({\"foo\":\"bar\"}) ], + [ { 1 => 1, 2 => "a", 3 => :b, 4 => nil, 5 => false }, %({\"1\":1,\"2\":\"a\",\"3\":\"b\",\"4\":null,\"5\":false}) ]] + + RangeTests = [[ 1..2, %("1..2")], + [ 1...2, %("1...2")], + [ 1.5..2.5, %("1.5..2.5")]] + + SymbolTests = [[ :a, %("a") ], + [ :this, %("this") ], + [ :"a b", %("a b") ]] + + ObjectTests = [[ Foo.new(1, 2), %({\"a\":1,\"b\":2}) ]] + HashlikeTests = [[ Hashlike.new, %({\"bar\":\"world\",\"foo\":\"hello\"}) ]] + StructTests = [[ MyStruct.new(:foo, "bar"), %({\"name\":\"foo\",\"value\":\"bar\"}) ], + [ MyStruct.new(nil, nil), %({\"name\":null,\"value\":null}) ]] + CustomTests = [[ Custom.new("custom"), '"custom"' ], + [ Custom.new(nil), "null" ], + [ Custom.new(:a), '"a"' ], + [ Custom.new([ :foo, "bar" ]), '["foo","bar"]' ], + [ Custom.new(foo: "hello", bar: "world"), '{"bar":"world","foo":"hello"}' ], + [ Custom.new(Hashlike.new), '{"bar":"world","foo":"hello"}' ], + [ Custom.new(Custom.new(Custom.new(:a))), '"a"' ]] + + RegexpTests = [[ /^a/, '"(?-mix:^a)"' ], [/^\w{1,2}[a-z]+/ix, '"(?ix-m:^\\\\w{1,2}[a-z]+)"']] + + URITests = [[ URI.parse("http://example.com"), %("http://example.com") ]] + + PathnameTests = [[ Pathname.new("lib/index.rb"), %("lib/index.rb") ]] + + DateTests = [[ Date.new(2005, 2, 1), %("2005/02/01") ]] + TimeTests = [[ Time.utc(2005, 2, 1, 15, 15, 10), %("2005/02/01 15:15:10 +0000") ]] + DateTimeTests = [[ DateTime.civil(2005, 2, 1, 15, 15, 10), %("2005/02/01 15:15:10 +0000") ]] + + StandardDateTests = [[ Date.new(2005, 2, 1), %("2005-02-01") ]] + StandardTimeTests = [[ Time.utc(2005, 2, 1, 15, 15, 10), %("2005-02-01T15:15:10.000Z") ]] + StandardDateTimeTests = [[ DateTime.civil(2005, 2, 1, 15, 15, 10), %("2005-02-01T15:15:10.000+00:00") ]] + StandardStringTests = [[ "this is the ", %("this is the ")]] + end +end diff --git a/test/activesupport6/test_common.rb b/test/activesupport6/test_common.rb new file mode 100644 index 00000000..9959f55d --- /dev/null +++ b/test/activesupport6/test_common.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +if ENV["BUILDKITE"] + require "minitest/reporters" + require "fileutils" + + module Minitest + def self.plugin_rails_ci_junit_format_test_report_for_buildkite_init(*) + dir = File.join(__dir__, "../test-reports/#{ENV['BUILDKITE_JOB_ID']}") + reporter << Minitest::Reporters::JUnitReporter.new(dir, false) + FileUtils.mkdir_p(dir) + end + end + + Minitest.load_plugins + Minitest.extensions.unshift "rails_ci_junit_format_test_report_for_buildkite" +end diff --git a/test/activesupport6/test_helper.rb b/test/activesupport6/test_helper.rb new file mode 100644 index 00000000..7be4108e --- /dev/null +++ b/test/activesupport6/test_helper.rb @@ -0,0 +1,163 @@ +# frozen_string_literal: true + +gem "minitest" # make sure we get the gem, not stdlib +require "minitest" +require "active_support/testing/tagged_logging" +require "active_support/testing/setup_and_teardown" +require "active_support/testing/assertions" +require "active_support/testing/deprecation" +require "active_support/testing/declarative" +require "active_support/testing/isolation" +require "active_support/testing/constant_lookup" +require "active_support/testing/time_helpers" +require "active_support/testing/file_fixtures" +require "active_support/testing/parallelization" +require "concurrent/utility/processor_counter" + +module ActiveSupport + class TestCase < ::Minitest::Test + Assertion = Minitest::Assertion + + class << self + # Sets the order in which test cases are run. + # + # ActiveSupport::TestCase.test_order = :random # => :random + # + # Valid values are: + # * +:random+ (to run tests in random order) + # * +:parallel+ (to run tests in parallel) + # * +:sorted+ (to run tests alphabetically by method name) + # * +:alpha+ (equivalent to +:sorted+) + def test_order=(new_order) + ActiveSupport.test_order = new_order + end + + # Returns the order in which test cases are run. + # + # ActiveSupport::TestCase.test_order # => :random + # + # Possible values are +:random+, +:parallel+, +:alpha+, +:sorted+. + # Defaults to +:random+. + def test_order + ActiveSupport.test_order ||= :random + end + + # Parallelizes the test suite. + # + # Takes a +workers+ argument that controls how many times the process + # is forked. For each process a new database will be created suffixed + # with the worker number. + # + # test-database-0 + # test-database-1 + # + # If ENV["PARALLEL_WORKERS"] is set the workers argument will be ignored + # and the environment variable will be used instead. This is useful for CI + # environments, or other environments where you may need more workers than + # you do for local testing. + # + # If the number of workers is set to +1+ or fewer, the tests will not be + # parallelized. + # + # If +workers+ is set to +:number_of_processors+, the number of workers will be + # set to the actual core count on the machine you are on. + # + # The default parallelization method is to fork processes. If you'd like to + # use threads instead you can pass with: :threads to the +parallelize+ + # method. Note the threaded parallelization does not create multiple + # database and will not work with system tests at this time. + # + # parallelize(workers: :number_of_processors, with: :threads) + # + # The threaded parallelization uses minitest's parallel executor directly. + # The processes parallelization uses a Ruby DRb server. + def parallelize(workers: :number_of_processors, with: :processes) + workers = Concurrent.physical_processor_count if workers == :number_of_processors + workers = ENV["PARALLEL_WORKERS"].to_i if ENV["PARALLEL_WORKERS"] + + return if workers <= 1 + + executor = case with + when :processes + Testing::Parallelization.new(workers) + when :threads + Minitest::Parallel::Executor.new(workers) + else + raise ArgumentError, "#{with} is not a supported parallelization executor." + end + + self.lock_threads = false if defined?(self.lock_threads) && with == :threads + + Minitest.parallel_executor = executor + + parallelize_me! + end + + # Set up hook for parallel testing. This can be used if you have multiple + # databases or any behavior that needs to be run after the process is forked + # but before the tests run. + # + # Note: this feature is not available with the threaded parallelization. + # + # In your +test_helper.rb+ add the following: + # + # class ActiveSupport::TestCase + # parallelize_setup do + # # create databases + # end + # end + def parallelize_setup(&block) + ActiveSupport::Testing::Parallelization.after_fork_hook do |worker| + yield worker + end + end + + # Clean up hook for parallel testing. This can be used to drop databases + # if your app uses multiple write/read databases or other clean up before + # the tests finish. This runs before the forked process is closed. + # + # Note: this feature is not available with the threaded parallelization. + # + # In your +test_helper.rb+ add the following: + # + # class ActiveSupport::TestCase + # parallelize_teardown do + # # drop databases + # end + # end + def parallelize_teardown(&block) + ActiveSupport::Testing::Parallelization.run_cleanup_hook do |worker| + yield worker + end + end + end + + alias_method :method_name, :name + + include ActiveSupport::Testing::TaggedLogging + prepend ActiveSupport::Testing::SetupAndTeardown + include ActiveSupport::Testing::Assertions + include ActiveSupport::Testing::Deprecation + include ActiveSupport::Testing::TimeHelpers + include ActiveSupport::Testing::FileFixtures + extend ActiveSupport::Testing::Declarative + + # test/unit backwards compatibility methods + alias :assert_raise :assert_raises + alias :assert_not_empty :refute_empty + alias :assert_not_equal :refute_equal + alias :assert_not_in_delta :refute_in_delta + alias :assert_not_in_epsilon :refute_in_epsilon + alias :assert_not_includes :refute_includes + alias :assert_not_instance_of :refute_instance_of + alias :assert_not_kind_of :refute_kind_of + alias :assert_no_match :refute_match + alias :assert_not_nil :refute_nil + alias :assert_not_operator :refute_operator + alias :assert_not_predicate :refute_predicate + alias :assert_not_respond_to :refute_respond_to + alias :assert_not_same :refute_same + + ActiveSupport.run_load_hooks(:active_support_test_case, self) + end +end diff --git a/test/activesupport6/time_zone_test_helpers.rb b/test/activesupport6/time_zone_test_helpers.rb new file mode 100644 index 00000000..85ed727c --- /dev/null +++ b/test/activesupport6/time_zone_test_helpers.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module TimeZoneTestHelpers + def with_tz_default(tz = nil) + old_tz = Time.zone + Time.zone = tz + yield + ensure + Time.zone = old_tz + end + + def with_env_tz(new_tz = "US/Eastern") + old_tz, ENV["TZ"] = ENV["TZ"], new_tz + yield + ensure + old_tz ? ENV["TZ"] = old_tz : ENV.delete("TZ") + end + + def with_preserve_timezone(value) + old_preserve_tz = ActiveSupport.to_time_preserves_timezone + ActiveSupport.to_time_preserves_timezone = value + yield + ensure + ActiveSupport.to_time_preserves_timezone = old_preserve_tz + end + + def with_tz_mappings(mappings) + old_mappings = ActiveSupport::TimeZone::MAPPING.dup + ActiveSupport::TimeZone.clear + ActiveSupport::TimeZone::MAPPING.clear + ActiveSupport::TimeZone::MAPPING.merge!(mappings) + + yield + ensure + ActiveSupport::TimeZone.clear + ActiveSupport::TimeZone::MAPPING.clear + ActiveSupport::TimeZone::MAPPING.merge!(old_mappings) + end +end