From 0a78b3ccd26cef04c2b0de347a16a329c801f6c1 Mon Sep 17 00:00:00 2001 From: Andra Antariksa Date: Thu, 2 Jul 2020 00:11:09 +0700 Subject: [PATCH] Implement Socket for Windows (WIP) --- src/errno.cr | 13 +- src/lib_c/x86_64-windows-msvc/c/errno.cr | 21 +-- src/lib_c/x86_64-windows-msvc/c/netdb.cr | 98 ++++++------- src/lib_c/x86_64-windows-msvc/c/sys/socket.cr | 45 +++++- src/lib_c/x86_64-windows-msvc/c/sys/un.cr | 6 + src/lib_c/x86_64-windows-msvc/c/winbase.cr | 2 +- src/socket.cr | 133 +++++++++++++----- src/socket/addrinfo.cr | 8 +- src/socket/tcp_socket.cr | 2 +- src/spec/expectations.cr | 9 +- 10 files changed, 214 insertions(+), 123 deletions(-) diff --git a/src/errno.cr b/src/errno.cr index dcb2b34f2b6d..23a0e74a1493 100644 --- a/src/errno.cr +++ b/src/errno.cr @@ -19,18 +19,7 @@ end enum Errno NONE = 0 - {% for value in %w(E2BIG EPERM ENOENT ESRCH EINTR EIO ENXIO ENOEXEC EBADF ECHILD EDEADLK ENOMEM - EACCES EFAULT ENOTBLK EBUSY EEXIST EXDEV ENODEV ENOTDIR EISDIR EINVAL ENFILE - EMFILE ENOTTY ETXTBSY EFBIG ENOSPC ESPIPE EROFS EMLINK EPIPE EDOM ERANGE EAGAIN - EWOULDBLOCK EINPROGRESS EALREADY ENOTSOCK EDESTADDRREQ EMSGSIZE EPROTOTYPE ENOPROTOOPT - EPROTONOSUPPORT ESOCKTNOSUPPORT EPFNOSUPPORT EAFNOSUPPORT EADDRINUSE EADDRNOTAVAIL - ENETDOWN ENETUNREACH ENETRESET ECONNABORTED ECONNRESET ENOBUFS EISCONN ENOTCONN - ESHUTDOWN ETOOMANYREFS ETIMEDOUT ECONNREFUSED ELOOP ENAMETOOLONG EHOSTDOWN - EHOSTUNREACH ENOTEMPTY EUSERS EDQUOT ESTALE EREMOTE ENOLCK ENOSYS EOVERFLOW - ECANCELED EIDRM ENOMSG EILSEQ EBADMSG EMULTIHOP ENODATA ENOLINK ENOSR ENOSTR - EPROTO ETIME EOPNOTSUPP ENOTRECOVERABLE EOWNERDEAD - - WSABASEERR WSAEINPROGRESS WSAEINTR) %} + {% for value in %w(E2BIG EPERM ENOENT ESRCH EINTR EIO ENXIO ENOEXEC EBADF ECHILD EDEADLK ENOMEM EACCES EFAULT ENOTBLK EBUSY EEXIST EXDEV ENODEV ENOTDIR EISDIR EINVAL ENFILE EMFILE ENOTTY ETXTBSY EFBIG ENOSPC ESPIPE EROFS EMLINK EPIPE EDOM ERANGE EAGAIN EWOULDBLOCK EINPROGRESS EALREADY ENOTSOCK EDESTADDRREQ EMSGSIZE EPROTOTYPE ENOPROTOOPT EPROTONOSUPPORT ESOCKTNOSUPPORT EPFNOSUPPORT EAFNOSUPPORT EADDRINUSE EADDRNOTAVAIL ENETDOWN ENETUNREACH ENETRESET ECONNABORTED ECONNRESET ENOBUFS EISCONN ENOTCONN ESHUTDOWN ETOOMANYREFS ETIMEDOUT ECONNREFUSED ELOOP ENAMETOOLONG EHOSTDOWN EHOSTUNREACH ENOTEMPTY EUSERS EDQUOT ESTALE EREMOTE ENOLCK ENOSYS EOVERFLOW ECANCELED EIDRM ENOMSG EILSEQ EBADMSG EMULTIHOP ENODATA ENOLINK ENOSR ENOSTR EPROTO ETIME EOPNOTSUPP ENOTRECOVERABLE EOWNERDEAD WSABASEERR WSAEINPROGRESS WSAEINTR WSAENOPROTOOPT) %} {% if LibC.has_constant?(value) %} {{value.id}} = LibC::{{value.id}} {% end %} diff --git a/src/lib_c/x86_64-windows-msvc/c/errno.cr b/src/lib_c/x86_64-windows-msvc/c/errno.cr index fea8e80dabfe..d225acc552c8 100644 --- a/src/lib_c/x86_64-windows-msvc/c/errno.cr +++ b/src/lib_c/x86_64-windows-msvc/c/errno.cr @@ -42,20 +42,21 @@ lib LibC # source https://docs.microsoft.com/en-us/windows/win32/winsock/windows-sockets-error-codes-2 WSAECONNABORTED = 10053 - ECONNABORTED = 10053 - WSAECONNRESET = 10054 - ECONNRESET = 10054 + ECONNABORTED = 10053 + WSAECONNRESET = 10054 + ECONNRESET = 10054 WSAECONNREFUSED = 10061 - ECONNREFUSED = 10061 - WSAEADDRINUSE = 10048 - EADDRINUSE = 10048 + ECONNREFUSED = 10061 + WSAEADDRINUSE = 10048 + EADDRINUSE = 10048 - WSABASEERR = 10000 + WSABASEERR = 10000 WSAEINPROGRESS = WSABASEERR + 36 - WSAEINTR = WSABASEERR + 4 + WSAEINTR = WSABASEERR + 4 + ENOPROTOOPT = 10042 - EISCONN = 106 - EALREADY = 114 + EISCONN = 106 + EALREADY = 114 EINPROGRESS = 115 alias ErrnoT = Int diff --git a/src/lib_c/x86_64-windows-msvc/c/netdb.cr b/src/lib_c/x86_64-windows-msvc/c/netdb.cr index 5f957b7a94d3..075afc8f7b3d 100644 --- a/src/lib_c/x86_64-windows-msvc/c/netdb.cr +++ b/src/lib_c/x86_64-windows-msvc/c/netdb.cr @@ -4,69 +4,69 @@ require "./sys/socket" lib LibC - AI_PASSIVE = 0x0001 - AI_CANONNAME = 0x0002 - AI_NUMERICHOST = 0x0004 - AI_ALL = 0x0100 - AI_ADDRCONFIG = 0x0400 - AI_V4MAPPED = 0x0800 - AI_NON_AUTHORITATIVE = 0x04000 - AI_SECURE = 0x08000 - AI_RETURN_PREFERRED_NAMES = 0x010000 - AI_FQDN = 0x00020000 - AI_FILESERVER = 0x00040000 + AI_PASSIVE = 0x0001 + AI_CANONNAME = 0x0002 + AI_NUMERICHOST = 0x0004 + AI_ALL = 0x0100 + AI_ADDRCONFIG = 0x0400 + AI_V4MAPPED = 0x0800 + AI_NON_AUTHORITATIVE = 0x04000 + AI_SECURE = 0x08000 + AI_RETURN_PREFERRED_NAMES = 0x010000 + AI_FQDN = 0x00020000 + AI_FILESERVER = 0x00040000 # lin AI_NUMERICSERV = 0x0400 # move to sys/socket - AF_UNSPEC = 0 - AF_INET = 2 - AF_IPX = 6 + AF_UNSPEC = 0 + AF_INET = 2 + AF_IPX = 6 AF_APPLETALK = 16 - AF_NETBIOS = 17 - AF_INET6 = 23 - AF_IRDA = 26 - AF_BTH = 32 - + AF_NETBIOS = 17 + AF_INET6 = 23 + AF_IRDA = 26 + AF_BTH = 32 + # lin - PF_INET = 2 - PF_INET6 = 10 - PF_UNIX = LibC::PF_LOCAL - PF_UNSPEC = 0 - PF_LOCAL = 1 - AF_UNIX = LibC::PF_UNIX + PF_INET = 2 + PF_INET6 = 10 + PF_UNIX = LibC::PF_LOCAL + PF_UNSPEC = 0 + PF_LOCAL = 1 + AF_UNIX = LibC::PF_UNIX - ## lin - EAI_AGAIN = -3 - EAI_BADFLAGS = -1 - EAI_FAIL = -4 - EAI_FAMILY = -6 - EAI_MEMORY = -10 - EAI_NONAME = -2 - EAI_SERVICE = -8 - EAI_SOCKTYPE = -7 - EAI_SYSTEM = -11 - EAI_OVERFLOW = -12 + # # lin + EAI_AGAIN = -3 + EAI_BADFLAGS = -1 + EAI_FAIL = -4 + EAI_FAMILY = -6 + EAI_MEMORY = -10 + EAI_NONAME = -2 + EAI_SERVICE = -8 + EAI_SOCKTYPE = -7 + EAI_SYSTEM = -11 + EAI_OVERFLOW = -12 # move to sys/socket - SOCK_STREAM = 1 - SOCK_DGRAM = 2 - SOCK_RAW = 3 - SOCK_RDM = 4 + SOCK_STREAM = 1 + SOCK_DGRAM = 2 + SOCK_RAW = 3 + SOCK_RDM = 4 SOCK_SEQPACKET = 5 # move to netinet/in - IPPROTO_TCP = 6 - IPPROTO_UDP = 17 - IPPROTO_RM = 113 - IPPROTO_IGMP = 2 + IPPROTO_TCP = 6 + IPPROTO_UDP = 17 + IPPROTO_RM = 113 + IPPROTO_IGMP = 2 # ipcp - BTHPROTO_RFCOMM = 3 - IPPROTO_ICMPV6 = 58 + BTHPROTO_RFCOMM = 3 + IPPROTO_ICMPV6 = 58 - ## lin - IPPROTO_IP = 0 + # # lin + IPPROTO_IP = 0 IPPROTO_RAW = 255 IPPROTO_ICMP = 1 @@ -87,7 +87,7 @@ lib LibC fun freeaddrinfo(pAddrInfo : PADDRINFOA) : VOID fun getaddrinfo(pNodeName : PCSTR, pServiceName : PCSTR, pHints : ADDRINFOA*, ppResult : PADDRINFOA*) : INT fun getnameinfo(pSockaddr : SOCKADDR*, sockaddrLength : SocklenT, pNodeBuffer : PCHAR, nodeBufferSize : DWORD, pServiceBuffer : PCHAR, serviceBufferSize : DWORD, flags : INT) : INT - + # fun gai_strerror = gai_strerrorA(ecode : Int) : UInt8* # See src/socket/addrinfo.cr for `gai_strerrorA` function definition end diff --git a/src/lib_c/x86_64-windows-msvc/c/sys/socket.cr b/src/lib_c/x86_64-windows-msvc/c/sys/socket.cr index 895bb024a165..d6a42c48fa9e 100644 --- a/src/lib_c/x86_64-windows-msvc/c/sys/socket.cr +++ b/src/lib_c/x86_64-windows-msvc/c/sys/socket.cr @@ -1,4 +1,5 @@ require "./types" +require "./un" @[Link("WS2_32")] lib LibC @@ -8,7 +9,7 @@ lib LibC SO_REUSEADDR = 0x0004 SO_BROADCAST = 0x0020 - SOL_SOCKET = 0xFFFF + SOL_SOCKET = 0xFFFF # -2147195266 is the value after convertion to long, actual value 2147772030 with type unsigned FIONBIO = -2147195266 @@ -20,6 +21,26 @@ lib LibC alias SOCKADDR = Sockaddr + struct WSAData + vVersion : WORD + wHighVersion : WORD + szDescription : StaticArray(UInt8, 257) + szSystemStatus : StaticArray(UInt8, 129) + iMaxSockets : UInt16 + iMaxUdpDg : UInt16 + lpVendorInfo : UInt8* + end + + struct SockaddrStorage + ss_family : Short + __ss_pad1 : StaticArray(Char, 6) + __ss_align : Int64 + __ss_pad2 : StaticArray(Char, 112) + end + + alias LPWSADATA = WSAData* + + fun wsastartup = WSAStartup(wVersionRequired : WORD, lpWSAData : LPWSADATA) : Int fun socket(af : Int, type : Int, protocol : Int) : SOCKET fun bind(s : SOCKET, addr : Sockaddr*, namelen : Int) : Int fun closesocket(s : SOCKET) : Int @@ -34,4 +55,26 @@ lib LibC fun connect(s : SOCKET, name : Sockaddr*, namelen : Int) : Int fun getsockname(s : SOCKET, name : Sockaddr*, namelen : Int*) : Int fun htons(hostshort : UShort) : UShort + fun getsockopt(s : SOCKET, level : Int, optname : Int, optval : UInt8*, optlen : Int*) : Int + fun sendto(s : SOCKET, buf : UInt8*, len : Int, flags : Int, to : Sockaddr*, tolen : Int) : Int + fun recvfrom(s : SOCKET, buf : Char*, len : Int, flags : Int, from : Sockaddr*, fromlen : Int*) : Int + + SO_RCVBUF = 0x1002 + TCP_NODELAY = 0x0001 + TCP_KEEPIDLE = 3 + TCP_KEEPCNT = 16 + TCP_KEEPINTVL = 17 + IP_MULTICAST_LOOP = 11 + IPV6_MULTICAST_LOOP = 11 + IPPROTO_IPV6 = 41 + IP_MULTICAST_TTL = 10 + IP_MULTICAST_IF = 9 + IPV6_MULTICAST_IF = 9 + IPV6_MULTICAST_HOPS = 10 + IP_ADD_MEMBERSHIP = 12 end + +# TODO +wsadata = uninitialized LibC::WSAData +wsaVersion = 514 +LibC.wsastartup(wsaVersion, pointerof(wsadata)) diff --git a/src/lib_c/x86_64-windows-msvc/c/sys/un.cr b/src/lib_c/x86_64-windows-msvc/c/sys/un.cr index e69de29bb2d1..24703158d583 100644 --- a/src/lib_c/x86_64-windows-msvc/c/sys/un.cr +++ b/src/lib_c/x86_64-windows-msvc/c/sys/un.cr @@ -0,0 +1,6 @@ +lib LibC + # struct IpMreq + # imr_multiaddr : IN_ADDR + # imr_interface : IN_ADDR + # end +end diff --git a/src/lib_c/x86_64-windows-msvc/c/winbase.cr b/src/lib_c/x86_64-windows-msvc/c/winbase.cr index 3b6a215497a8..da09c7082453 100644 --- a/src/lib_c/x86_64-windows-msvc/c/winbase.cr +++ b/src/lib_c/x86_64-windows-msvc/c/winbase.cr @@ -13,7 +13,7 @@ lib LibC FORMAT_MESSAGE_FROM_HMODULE = 0x00000800_u32 FORMAT_MESSAGE_FROM_SYSTEM = 0x00001000_u32 FORMAT_MESSAGE_ARGUMENT_ARRAY = 0x00002000_u32 - FORMAT_MESSAGE_MAX_WIDTH_MASK = 0x000000FF_u32 + FORMAT_MESSAGE_MAX_WIDTH_MASK = 0x000000FF_u32 fun FormatMessageW(dwFlags : DWORD, lpSource : Void*, dwMessageId : DWORD, dwLanguageId : DWORD, lpBuffer : LPWSTR, nSize : DWORD, arguments : Void*) : DWORD diff --git a/src/socket.cr b/src/socket.cr index 934be5abd727..c0e2bd0e028b 100644 --- a/src/socket.cr +++ b/src/socket.cr @@ -108,6 +108,7 @@ class Socket < IO end end {% else %}\ + def initialize(@family, @type, @protocol = Protocol::IP, blocking = false) @closed = false fd = LibC.socket(family, type, protocol) @@ -121,7 +122,6 @@ class Socket < IO end end {% end %} - # Creates a Socket from an existing socket file descriptor. {% if flag?(:win32) %} @@ -395,11 +395,19 @@ class Socket < IO # sock.connect Socket::UNIXAddress.new("/tmp/service.sock") # sock.send(Bytes[0]) # ``` - def send(message) : Int32 - evented_send(message.to_slice, "Error sending datagram") do |slice| - LibC.send(fd, slice.to_unsafe.as(Void*), slice.size, 0) + {% if flag?(:win32) %} + def send(message) : Int32 + evented_send(message.to_slice, "Error sending datagram") do |slice| + LibC.send(socket, slice.to_unsafe.as(UInt8*), slice.size, 0) + end end - end + {% else %} + def send(message) : Int32 + evented_send(message.to_slice, "Error sending datagram") do |slice| + LibC.send(fd, slice.to_unsafe.as(UInt8*), slice.size, 0) + end + end + {% end %} # Sends a message to the specified remote address. # @@ -411,13 +419,23 @@ class Socket < IO # sock.connect("example.com", 2000) # sock.send("text query", to: server) # ``` - def send(message, to addr : Address) : Int32 - slice = message.to_slice - bytes_sent = LibC.sendto(fd, slice.to_unsafe.as(Void*), slice.size, 0, addr, addr.size) - raise Socket::Error.from_errno("Error sending datagram to #{addr}") if bytes_sent == -1 - # to_i32 is fine because string/slice sizes are an Int32 - bytes_sent.to_i32 - end + {% if flag?(:win32) %} + def send(message, to addr : Address) : Int32 + slice = message.to_slice + bytes_sent = LibC.sendto(socket, slice.to_unsafe.as(UInt8*), slice.size, 0, addr, addr.size) + raise Socket::Error.from_errno("Error sending datagram to #{addr}") if bytes_sent == -1 + # to_i32 is fine because string/slice sizes are an Int32 + bytes_sent.to_i32 + end + {% else %} + def send(message, to addr : Address) : Int32 + slice = message.to_slice + bytes_sent = LibC.sendto(fd, slice.to_unsafe.as(Void*), slice.size, 0, addr, addr.size) + raise Socket::Error.from_errno("Error sending datagram to #{addr}") if bytes_sent == -1 + # to_i32 is fine because string/slice sizes are an Int32 + bytes_sent.to_i32 + end + {% end %} # Receives a text message from the previously bound address. # @@ -455,16 +473,29 @@ class Socket < IO {bytes_read, Address.from(sockaddr, addrlen)} end - protected def recvfrom(bytes) - sockaddr = Pointer(LibC::SockaddrStorage).malloc.as(LibC::Sockaddr*) - addrlen = LibC::SocklenT.new(sizeof(LibC::SockaddrStorage)) + {% if flag?(:win32) %} + protected def recvfrom(bytes) + sockaddr = Pointer(LibC::SockaddrStorage).malloc.as(LibC::Sockaddr*) + addrlen = LibC::SocklenT.new(sizeof(LibC::SockaddrStorage)) + + bytes_read = evented_read(bytes, "Error receiving datagram") do |slice| + LibC.recvfrom(socket, slice.to_unsafe.as(LibC::Char*), slice.size, 0, sockaddr, pointerof(addrlen)) + end - bytes_read = evented_read(bytes, "Error receiving datagram") do |slice| - LibC.recvfrom(fd, slice.to_unsafe.as(Void*), slice.size, 0, sockaddr, pointerof(addrlen)) + {bytes_read, sockaddr, addrlen} end + {% else %} + protected def recvfrom(bytes) + sockaddr = Pointer(LibC::SockaddrStorage).malloc.as(LibC::Sockaddr*) + addrlen = LibC::SocklenT.new(sizeof(LibC::SockaddrStorage)) - {bytes_read, sockaddr, addrlen} - end + bytes_read = evented_read(bytes, "Error receiving datagram") do |slice| + LibC.recvfrom(fd, slice.to_unsafe.as(Void*), slice.size, 0, sockaddr, pointerof(addrlen)) + end + + {bytes_read, sockaddr, addrlen} + end + {% end %} # Calls `shutdown(2)` with `SHUT_RD` def close_read @@ -518,23 +549,38 @@ class Socket < IO setsockopt_bool LibC::SO_REUSEADDR, val end - def reuse_port? - getsockopt(LibC::SO_REUSEPORT, 0) do |value| - return value != 0 + {% if flag?(:win32) %} + def reuse_port? + # TODO + # Check function return value + getsockopt(LibC::SO_REUSEADDR, 0) do |value| + return value != 0 + end + + if Errno.value == Errno::ENOPROTOOPT + return false + else + raise Socket::Error.from_errno("getsockopt") + end end + {% else %} + def reuse_port? + getsockopt(LibC::SO_REUSEPORT, 0) do |value| + return value != 0 + end - if Errno.value == Errno::ENOPROTOOPT - return false - else - raise Socket::Error.from_errno("getsockopt") + if Errno.value == Errno::ENOPROTOOPT + return false + else + raise Socket::Error.from_errno("getsockopt") + end end - end + {% end %} # TODO # Care {% if flag?(:win32) %} def reuse_port=(val : Bool) - end {% else %} def reuse_port=(val : Bool) @@ -593,12 +639,21 @@ class Socket < IO raise Socket::Error.from_errno("getsockopt") end - protected def getsockopt(optname, optval, level = LibC::SOL_SOCKET) - optsize = LibC::SocklenT.new(sizeof(typeof(optval))) - ret = LibC.getsockopt(fd, level, optname, (pointerof(optval).as(Void*)), pointerof(optsize)) - yield optval if ret == 0 - ret - end + {% if flag?(:win32) %} + protected def getsockopt(optname, optval, level = LibC::SOL_SOCKET) + optsize = LibC::SocklenT.new(sizeof(typeof(optval))) + ret = LibC.getsockopt(socket, level, optname, (pointerof(optval).as(UInt8*)), pointerof(optsize)) + yield optval if ret == 0 + ret + end + {% else %} + protected def getsockopt(optname, optval, level = LibC::SOL_SOCKET) + optsize = LibC::SocklenT.new(sizeof(typeof(optval))) + ret = LibC.getsockopt(fd, level, optname, (pointerof(optval).as(Void*)), pointerof(optsize)) + yield optval if ret == 0 + ret + end + {% end %} # NOTE: *optval* is restricted to `Int32` until sizeof works on variables. {% if flag?(:win32) %} @@ -648,11 +703,11 @@ class Socket < IO {% if flag?(:win32) %} def blocking=(value) - mode : UInt32 = if value - 1.to_u32 - else - 0.to_u32 - end + mode : UInt32 = if value + 1.to_u32 + else + 0.to_u32 + end LibC.ioctlsocket(self.socket, LibC::FIONBIO, pointerof(mode)) end {% else %} diff --git a/src/socket/addrinfo.cr b/src/socket/addrinfo.cr index 70f284bdb99f..337e68396f66 100644 --- a/src/socket/addrinfo.cr +++ b/src/socket/addrinfo.cr @@ -86,11 +86,11 @@ class Socket def self.new(error_code, domain) new error_code, String.new(self.gai_strerror(error_code)), domain end - + {% if flag?(:win32) %} # This is the recreation of gai_strerrorA in Windows, as they # define their gai_strerrorA in a C-header - # + # # See https://gist.github.com/andraantariksa/0dacbe0999427d8554286e568ee5f220#file-ws2tcpip-h-L682 def self.gai_strerror(ecode : Int) # The buffer size defined in WS2tcpip.h is 1024 char @@ -99,8 +99,8 @@ class Socket lang = (0x01_u16 << 10) | 0x00_u16 LibC.FormatMessageA(LibC::FORMAT_MESSAGE_FROM_SYSTEM | - LibC::FORMAT_MESSAGE_IGNORE_INSERTS | - LibC::FORMAT_MESSAGE_MAX_WIDTH_MASK, + LibC::FORMAT_MESSAGE_IGNORE_INSERTS | + LibC::FORMAT_MESSAGE_MAX_WIDTH_MASK, Pointer(LibC::DWORD).null, ecode, lang, buf, 1024, Pointer(UInt32).null) buf.to_slice diff --git a/src/socket/tcp_socket.cr b/src/socket/tcp_socket.cr index 430866df3128..067724b73531 100644 --- a/src/socket/tcp_socket.cr +++ b/src/socket/tcp_socket.cr @@ -61,7 +61,7 @@ class TCPSocket < IPSocket super fd, family, Type::STREAM, Protocol::TCP end {% end %} - + # Opens a TCP socket to a remote TCP server, yields it to the block, then # eventually closes the socket when the block returns. # diff --git a/src/spec/expectations.cr b/src/spec/expectations.cr index 168facbfb42f..bf29e058e13c 100644 --- a/src/spec/expectations.cr +++ b/src/spec/expectations.cr @@ -395,14 +395,12 @@ module Spec when Regex unless (ex_to_s =~ message) backtrace = ex.backtrace.join('\n') { |f| " # #{f}" } - fail "Expected #{klass} with message matching #{message.inspect}, " \ - "got #<#{ex.class}: #{ex_to_s}> with backtrace:\n#{backtrace}", file, line + fail "Expected #{klass} with message matching #{message.inspect}, got #<#{ex.class}: #{ex_to_s}> with backtrace:\n#{backtrace}", file, line end when String unless ex_to_s.includes?(message) backtrace = ex.backtrace.join('\n') { |f| " # #{f}" } - fail "Expected #{klass} with #{message.inspect}, got #<#{ex.class}: " \ - "#{ex_to_s}> with backtrace:\n#{backtrace}", file, line + fail "Expected #{klass} with #{message.inspect}, got #<#{ex.class}: #{ex_to_s}> with backtrace:\n#{backtrace}", file, line end when Nil # No need to check the message @@ -411,8 +409,7 @@ module Spec ex rescue ex backtrace = ex.backtrace.join('\n') { |f| " # #{f}" } - fail "Expected #{klass}, got #<#{ex.class}: #{ex}> with backtrace:\n" \ - "#{backtrace}", file, line + fail "Expected #{klass}, got #<#{ex.class}: #{ex}> with backtrace:\n#{backtrace}", file, line else fail "Expected #{klass} but nothing was raised", file, line end