diff --git a/spec/manual/string_normalize_spec.cr b/spec/manual/string_normalize_spec.cr index 96d58c946894..9a67a891b702 100644 --- a/spec/manual/string_normalize_spec.cr +++ b/spec/manual/string_normalize_spec.cr @@ -1,6 +1,6 @@ require "spec" require "http/client" -require "../support/string" +require "spec/helpers/string" UCD_ROOT = "http://www.unicode.org/Public/#{Unicode::VERSION}/ucd/" @@ -27,31 +27,9 @@ private struct CodepointsEqualExpectation end end -# same as `assert_prints`, but uses `CodepointsEqualExpectation` instead of `eq` private macro assert_prints_codepoints(call, str, desc, *, file = __FILE__, line = __LINE__) %expectation = CodepointsEqualExpectation.new(({{ str }}).as(String), {{ desc }}) - - %result = {{ call }} - %result.should be_a(String), file: {{ file }}, line: {{ line }} - %result.should %expectation, file: {{ file }}, line: {{ line }} - - String.build do |io| - {% if call.receiver %}{{ call.receiver }}.{% end %}{{ call.name }}( - io, - {% for arg in call.args %} {{ arg }}, {% end %} - {% if call.named_args %} {% for narg in call.named_args %} {{ narg.name }}: {{ narg.value }}, {% end %} {% end %} - ) {{ call.block }} - end.should %expectation, file: {{ file }}, line: {{ line }} - - {% unless flag?(:without_iconv) %} - string_build_via_utf16 do |io| - {% if call.receiver %}{{ call.receiver }}.{% end %}{{ call.name }}( - io, - {% for arg in call.args %} {{ arg }}, {% end %} - {% if call.named_args %} {% for narg in call.named_args %} {{ narg.name }}: {{ narg.value }}, {% end %} {% end %} - ) {{ call.block }} - end.should %expectation, file: {{ file }}, line: {{ line }} - {% end %} + assert_prints({{ call }}, should: %expectation, file: {{ file }}, line: {{ line }}) end private def assert_normalized(source, target, form : Unicode::NormalizationForm, *, file = __FILE__, line = __LINE__) diff --git a/spec/std/base64_spec.cr b/spec/std/base64_spec.cr index 89e5d863825a..567340d6fa38 100644 --- a/spec/std/base64_spec.cr +++ b/spec/std/base64_spec.cr @@ -1,7 +1,7 @@ require "spec" require "base64" require "crystal/digest/md5" -require "../support/string" +require "spec/helpers/string" # rearrange parameters for `assert_prints` {% for method in %w(encode strict_encode urlsafe_encode) %} diff --git a/spec/std/big/big_decimal_spec.cr b/spec/std/big/big_decimal_spec.cr index 085736de5294..81fd2fb82b36 100644 --- a/spec/std/big/big_decimal_spec.cr +++ b/spec/std/big/big_decimal_spec.cr @@ -1,6 +1,6 @@ require "spec" require "big" -require "../../support/string" +require "spec/helpers/string" describe BigDecimal do it "initializes from valid input" do diff --git a/spec/std/big/big_float_spec.cr b/spec/std/big/big_float_spec.cr index 69322ef86c1a..beea465df8c3 100644 --- a/spec/std/big/big_float_spec.cr +++ b/spec/std/big/big_float_spec.cr @@ -1,6 +1,6 @@ require "spec" require "big" -require "../../support/string" +require "spec/helpers/string" private def it_converts_to_s(value : BigFloat, str, *, file = __FILE__, line = __LINE__) it "converts to #{str}", file: file, line: line do diff --git a/spec/std/char_spec.cr b/spec/std/char_spec.cr index 2c5ea0345252..e947fc17d296 100644 --- a/spec/std/char_spec.cr +++ b/spec/std/char_spec.cr @@ -1,7 +1,7 @@ require "spec" require "unicode" require "spec/helpers/iterate" -require "../support/string" +require "spec/helpers/string" describe "Char" do describe "#upcase" do diff --git a/spec/std/csv/csv_build_spec.cr b/spec/std/csv/csv_build_spec.cr index 7cbb91f7f746..b222df22ab93 100644 --- a/spec/std/csv/csv_build_spec.cr +++ b/spec/std/csv/csv_build_spec.cr @@ -1,6 +1,6 @@ require "spec" require "csv" -require "../../support/string" +require "spec/helpers/string" describe CSV do describe "build" do diff --git a/spec/std/enum_spec.cr b/spec/std/enum_spec.cr index 0b7c846138c2..437ba42e1c1f 100644 --- a/spec/std/enum_spec.cr +++ b/spec/std/enum_spec.cr @@ -1,5 +1,5 @@ require "spec" -require "../support/string" +require "spec/helpers/string" enum SpecEnum : Int8 One diff --git a/spec/std/float_printer_spec.cr b/spec/std/float_printer_spec.cr index 94c2409c71a9..3db7b344b170 100644 --- a/spec/std/float_printer_spec.cr +++ b/spec/std/float_printer_spec.cr @@ -35,7 +35,7 @@ require "spec" require "./spec_helper" -require "../support/string" +require "spec/helpers/string" require "../support/number" # Tests that `v.to_s` is the same as the *v* literal is written in the source diff --git a/spec/std/float_spec.cr b/spec/std/float_spec.cr index b93597c389c6..4ddfd194313d 100644 --- a/spec/std/float_spec.cr +++ b/spec/std/float_spec.cr @@ -1,5 +1,5 @@ require "spec" -require "../support/string" +require "spec/helpers/string" describe "Float" do describe "**" do diff --git a/spec/std/http/http_spec.cr b/spec/std/http/http_spec.cr index 6d23b9356cd6..6159cdc9dc5e 100644 --- a/spec/std/http/http_spec.cr +++ b/spec/std/http/http_spec.cr @@ -1,6 +1,6 @@ require "spec" require "http" -require "../../support/string" +require "spec/helpers/string" private def http_quote_string(io : IO, string) HTTP.quote_string(string, io) diff --git a/spec/std/humanize_spec.cr b/spec/std/humanize_spec.cr index a7342b495947..482be02b77eb 100644 --- a/spec/std/humanize_spec.cr +++ b/spec/std/humanize_spec.cr @@ -1,5 +1,5 @@ require "spec" -require "../support/string" +require "spec/helpers/string" private LENGTH_UNITS = ->(magnitude : Int32, number : Float64) do case magnitude diff --git a/spec/std/json/builder_spec.cr b/spec/std/json/builder_spec.cr index 66f9be33b741..2bbd9c49a5ef 100644 --- a/spec/std/json/builder_spec.cr +++ b/spec/std/json/builder_spec.cr @@ -1,6 +1,6 @@ require "spec" require "json" -require "../../support/string" +require "spec/helpers/string" private def assert_built(expected, *, file = __FILE__, line = __LINE__, &) assert_prints JSON.build { |json| with json yield json }, expected, file: file, line: line diff --git a/spec/std/mime/media_type_spec.cr b/spec/std/mime/media_type_spec.cr index 5ed73256ecb4..383e62c5ea42 100644 --- a/spec/std/mime/media_type_spec.cr +++ b/spec/std/mime/media_type_spec.cr @@ -1,6 +1,6 @@ require "../spec_helper" require "mime/media_type" -require "../../support/string" +require "spec/helpers/string" private def parse(string) type = MIME::MediaType.parse(string) diff --git a/spec/std/process/status_spec.cr b/spec/std/process/status_spec.cr index 871e2bd837df..45f60aba4c06 100644 --- a/spec/std/process/status_spec.cr +++ b/spec/std/process/status_spec.cr @@ -1,5 +1,5 @@ require "spec" -require "../../support/string" +require "spec/helpers/string" private def exit_status(status) {% if flag?(:unix) %} diff --git a/spec/std/slice_spec.cr b/spec/std/slice_spec.cr index 72803e0b661b..4a2469f69aa7 100644 --- a/spec/std/slice_spec.cr +++ b/spec/std/slice_spec.cr @@ -1,6 +1,6 @@ require "spec" require "spec/helpers/iterate" -require "../support/string" +require "spec/helpers/string" private class BadSortingClass include Comparable(self) diff --git a/spec/std/socket/address_spec.cr b/spec/std/socket/address_spec.cr index 6835b8e518d1..d2e4768db987 100644 --- a/spec/std/socket/address_spec.cr +++ b/spec/std/socket/address_spec.cr @@ -1,7 +1,7 @@ require "spec" require "socket" require "../../support/win32" -require "../../support/string" +require "spec/helpers/string" describe Socket::Address do describe ".parse" do diff --git a/spec/std/sprintf_spec.cr b/spec/std/sprintf_spec.cr index bd72c962bf35..ad6ceda4809d 100644 --- a/spec/std/sprintf_spec.cr +++ b/spec/std/sprintf_spec.cr @@ -1,5 +1,5 @@ require "./spec_helper" -require "../support/string" +require "spec/helpers/string" require "big" # use same name for `sprintf` and `IO#printf` so that `assert_prints` can be leveraged diff --git a/spec/std/string_spec.cr b/spec/std/string_spec.cr index 7a9f587126f3..905930463cfc 100644 --- a/spec/std/string_spec.cr +++ b/spec/std/string_spec.cr @@ -1,6 +1,6 @@ require "./spec_helper" require "spec/helpers/iterate" -require "../support/string" +require "spec/helpers/string" describe "String" do describe "[]" do @@ -692,7 +692,8 @@ describe "String" do end it "does not touch invalid code units in an otherwise ascii string" do - assert_prints "\xB5!\xE0\xC1\xB5?".capitalize, "\xB5!\xE0\xC1\xB5?" + "\xB5!\xE0\xC1\xB5?".capitalize.should eq("\xB5!\xE0\xC1\xB5?") + String.build { |io| "\xB5!\xE0\xC1\xB5?".capitalize(io) }.should eq("\xB5!\xE0\xC1\xB5?".scrub) end end @@ -710,8 +711,10 @@ describe "String" do end it "does not touch invalid code units in an otherwise ascii string" do - assert_prints "\xB5!\xE0\xC1\xB5?".titleize, "\xB5!\xE0\xC1\xB5?" - assert_prints "a\xA0b".titleize, "A\xA0b" + "\xB5!\xE0\xC1\xB5?".titleize.should eq("\xB5!\xE0\xC1\xB5?") + "a\xA0b".titleize.should eq("A\xA0b") + String.build { |io| "\xB5!\xE0\xC1\xB5?".titleize(io) }.should eq("\xB5!\xE0\xC1\xB5?".scrub) + String.build { |io| "a\xA0b".titleize(io) }.should eq("A\xA0b".scrub) end end diff --git a/spec/std/time/format_spec.cr b/spec/std/time/format_spec.cr index 08baa2e910fd..8e7cb27b6bd7 100644 --- a/spec/std/time/format_spec.cr +++ b/spec/std/time/format_spec.cr @@ -1,5 +1,5 @@ require "./spec_helper" -require "../../support/string" +require "spec/helpers/string" def parse_time(format, string) Time.parse_utc(format, string) diff --git a/spec/std/uri_spec.cr b/spec/std/uri_spec.cr index 402263742a96..9f0e69458629 100644 --- a/spec/std/uri_spec.cr +++ b/spec/std/uri_spec.cr @@ -2,7 +2,7 @@ require "spec" require "uri" require "uri/json" require "uri/yaml" -require "../support/string" +require "spec/helpers/string" private def assert_uri(string, file = __FILE__, line = __LINE__, **args) it "`#{string}`", file, line do diff --git a/spec/std/uuid_spec.cr b/spec/std/uuid_spec.cr index 5086b7964f95..0992c92095d1 100644 --- a/spec/std/uuid_spec.cr +++ b/spec/std/uuid_spec.cr @@ -1,6 +1,6 @@ require "spec" require "uuid" -require "../support/string" +require "spec/helpers/string" describe "UUID" do describe "#==" do diff --git a/spec/std/xml/builder_spec.cr b/spec/std/xml/builder_spec.cr index 0fd7450b0104..3aaaa9f503d0 100644 --- a/spec/std/xml/builder_spec.cr +++ b/spec/std/xml/builder_spec.cr @@ -1,6 +1,6 @@ require "spec" require "xml" -require "../../support/string" +require "spec/helpers/string" private def assert_built(expected, quote_char = nil, *, file = __FILE__, line = __LINE__, &) assert_prints XML.build(quote_char: quote_char) { |xml| with xml yield xml }, expected, file: file, line: line diff --git a/spec/std/xml/xml_spec.cr b/spec/std/xml/xml_spec.cr index 3c695f517497..efbb79e6e226 100644 --- a/spec/std/xml/xml_spec.cr +++ b/spec/std/xml/xml_spec.cr @@ -1,6 +1,6 @@ require "spec" require "xml" -require "../../support/string" +require "spec/helpers/string" describe XML do it "parses" do diff --git a/spec/std/yaml/builder_spec.cr b/spec/std/yaml/builder_spec.cr index e225a745587d..342777445ca6 100644 --- a/spec/std/yaml/builder_spec.cr +++ b/spec/std/yaml/builder_spec.cr @@ -1,6 +1,6 @@ require "spec" require "yaml" -require "../../support/string" +require "spec/helpers/string" private def assert_built(expected, expect_document_end = false, *, file = __FILE__, line = __LINE__, &) # libyaml 0.2.1 removed the erroneously written document end marker (`...`) after some scalars in root context (see https://github.com/yaml/libyaml/pull/18). diff --git a/spec/support/string.cr b/spec/support/string.cr deleted file mode 100644 index be2306bdd509..000000000000 --- a/spec/support/string.cr +++ /dev/null @@ -1,61 +0,0 @@ -# Builds a `String` through a UTF-16 `IO`. -# -# Similar to `String.build`, but the yielded `IO` is configured to use the -# UTF-16 encoding, and the written contents are decoded back into a UTF-8 -# `String`. This method is mainly used by `assert_prints` to test the behaviour -# of string-generating methods under different encodings. -# -# Raises if the `without_iconv` flag is set. -def string_build_via_utf16(& : IO -> _) - {% if flag?(:without_iconv) %} - raise NotImplementedError.new("string_build_via_utf16") - {% else %} - io = IO::Memory.new - io.set_encoding(IO::ByteFormat::SystemEndian == IO::ByteFormat::LittleEndian ? "UTF-16LE" : "UTF-16BE") - yield io - byte_slice = io.to_slice - utf16_slice = byte_slice.unsafe_slice_of(UInt16) - String.from_utf16(utf16_slice) - {% end %} -end - -# Asserts that the given *call* and its `IO`-accepting variants produce the -# given string *str*. -# -# Given a call of the form `foo.bar(*args, **opts)`, tests the following cases: -# -# * This call itself should return a `String` equal to *str*. -# * `String.build { |io| foo.bar(io, *args, **opts) }` should be equal to -# `str.scrub`; writing to a `String::Builder` must not produce any invalid -# UTF-8 byte sequences. -# * `string_build_via_utf16 { |io| foo.bar(io, *args, **opts) }` should also be -# equal to `str.scrub`; that is, the `IO` overload should not fail when the -# `IO` argument uses a non-default encoding. This case is skipped if the -# `without_iconv` flag is set. -macro assert_prints(call, str, *, file = __FILE__, line = __LINE__) - %str = ({{ str }}).as(String) - %file = {{ file }} - %line = {{ line }} - - %result = {{ call }} - %result.should be_a(String), file: %file, line: %line - %result.should eq(%str), file: %file, line: %line - - String.build do |io| - {% if call.receiver %}{{ call.receiver }}.{% end %}{{ call.name }}( - io, - {% for arg in call.args %} {{ arg }}, {% end %} - {% if call.named_args %} {% for narg in call.named_args %} {{ narg.name }}: {{ narg.value }}, {% end %} {% end %} - ) {{ call.block }} - end.should eq(%str.scrub), file: %file, line: %line - - {% unless flag?(:without_iconv) %} - string_build_via_utf16 do |io| - {% if call.receiver %}{{ call.receiver }}.{% end %}{{ call.name }}( - io, - {% for arg in call.args %} {{ arg }}, {% end %} - {% if call.named_args %} {% for narg in call.named_args %} {{ narg.name }}: {{ narg.value }}, {% end %} {% end %} - ) {{ call.block }} - end.should eq(%str.scrub), file: %file, line: %line - {% end %} -end diff --git a/src/spec/helpers/string.cr b/src/spec/helpers/string.cr new file mode 100644 index 000000000000..892d2012524e --- /dev/null +++ b/src/spec/helpers/string.cr @@ -0,0 +1,92 @@ +module Spec::Methods + # Asserts that the given *call* and its `IO`-accepting variant both match the + # given *expectation*, used to test string printing. + # + # Given a call of the form `foo.bar(*args, **opts)`, this tests the following + # cases: + # + # * The call itself. Additionally this call must return a `String`. + # * `String.build { |io| foo.bar(io, *args, **opts) }`, which constructs a + # `String` via an `IO` overload. + # * `io = ...; foo.bar(io, *args, **opts); io.to_s`, where `io` is an `IO` + # configured to use the UTF-16 encoding, and contents written to it are + # decoded back into a UTF-8 `String`. This case ensures that the `IO` + # overload does not produce malformed UTF-8 byte sequences via a non-default + # encoding. This case is skipped if the `without_iconv` flag is set. + # + # The overload that accepts a *str* argument is usually easier to work with. + macro assert_prints(call, *, should expectation, file = __FILE__, line = __LINE__) + %expectation = {{ expectation }} + %file = {{ file }} + %line = {{ line }} + + %result = {{ call }} + %result.should be_a(::String), file: %file, line: %line + %result.should(%expectation, file: %file, line: %line) + + ::String.build do |io| + {% if call.receiver %}{{ call.receiver }}.{% end %}{{ call.name }}( + io, + {% for arg in call.args %} {{ arg }}, {% end %} + {% if call.named_args %} {% for narg in call.named_args %} {{ narg.name }}: {{ narg.value }}, {% end %} {% end %} + ) {{ call.block }} + end.should(%expectation, file: %file, line: %line) + + {% unless flag?(:without_iconv) %} + %utf16_io = ::IO::Memory.new + %utf16_io.set_encoding(::IO::ByteFormat::SystemEndian == ::IO::ByteFormat::LittleEndian ? "UTF-16LE" : "UTF-16BE") + {% if call.receiver %}{{ call.receiver }}.{% end %}{{ call.name }}( + %utf16_io, + {% for arg in call.args %} {{ arg }}, {% end %} + {% if call.named_args %} {% for narg in call.named_args %} {{ narg.name }}: {{ narg.value }}, {% end %} {% end %} + ) {{ call.block }} + %result = ::String.from_utf16(%utf16_io.to_slice.unsafe_slice_of(::UInt16)) + %result.should(%expectation, file: %file, line: %line) + {% end %} + end + + # Asserts that the given *call* and its `IO`-accepting variant both produce + # the given string *str*. + # + # Equivalent to `assert_prints call, should: eq(str)`. *str* must be validly + # encoded in UTF-8. + # + # ``` + # require "spec" + # require "spec/helpers/string" + # + # it "prints integers with `Int#to_s`" do + # assert_prints 123.to_s, "123" + # assert_prints 123.to_s(16), "7b" + # end + # ``` + # + # Methods that do not follow the convention of `IO`-accepting and + # `String`-returning overloads can also be tested as long as suitable wrapper + # methods are defined: + # + # ``` + # require "spec" + # require "spec/helpers/string" + # + # private def fprintf(format, *args) + # sprintf(format, *args) + # end + # + # private def fprintf(io : IO, format, *args) + # io.printf(format, *args) + # end + # + # it "prints with `sprintf` and `IO#printf`" do + # assert_prints fprintf("%d", 123), "123" + # assert_prints fprintf("%x %b", 123, 6), "7b 110" + # end + # ``` + macro assert_prints(call, str, *, file = __FILE__, line = __LINE__) + %str = ({{ str }}).as(::String) + unless %str.valid_encoding? + ::fail "`str` contains invalid UTF-8 byte sequences: #{%str.inspect}", file: {{ file }}, line: {{ line }} + end + assert_prints({{ call }}, should: eq(%str), file: {{ file }}, line: {{ line }}) + end +end