File: tcp_socket_spec.cr

package info (click to toggle)
crystal 1.14.0%2Bdfsg-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 24,384 kB
  • sloc: javascript: 6,400; sh: 695; makefile: 269; ansic: 121; python: 105; cpp: 77; xml: 32
file content (216 lines) | stat: -rw-r--r-- 6,388 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
{% skip_file if flag?(:wasm32) %}

require "./spec_helper"
require "../../support/win32"

# TODO: Windows networking in the interpreter requires #12495
{% if flag?(:interpreted) && flag?(:win32) %}
  pending TCPSocket
  {% skip_file %}
{% end %}

describe TCPSocket, tags: "network" do
  describe "#connect" do
    each_ip_family do |family, address|
      it "connects to server" do
        port = unused_local_port

        TCPServer.open(address, port) do |server|
          TCPSocket.open(address, port) do |client|
            client.local_address.address.should eq address

            sock = server.accept

            sock.closed?.should be_false
            client.closed?.should be_false

            sock.local_address.port.should eq(port)
            sock.local_address.address.should eq(address)

            client.remote_address.port.should eq(port)
            sock.remote_address.address.should eq address
          end
        end
      end

      it "raises when connection is refused" do
        port = unused_local_port

        expect_raises(Socket::ConnectError, "Error connecting to '#{address}:#{port}'") do
          TCPSocket.new(address, port)
        end
      end

      it "raises when port is negative" do
        error = expect_raises(Socket::Addrinfo::Error) do
          TCPSocket.new(address, -12)
        end
        error.os_error.should eq({% if flag?(:win32) %}
          WinError::WSATYPE_NOT_FOUND
        {% elsif flag?(:linux) && !flag?(:android) %}
          Errno.new(LibC::EAI_SERVICE)
        {% else %}
          Errno.new(LibC::EAI_NONAME)
        {% end %})
      end

      it "raises when port is zero" do
        expect_raises(Socket::ConnectError) do
          TCPSocket.new(address, 0)
        end
      end
    end

    describe "address resolution" do
      it "connects to localhost" do
        port = unused_local_port

        TCPServer.open("localhost", port) do |server|
          TCPSocket.open("localhost", port) do |client|
            server.accept
          end
        end
      end

      it "raises when host doesn't exist" do
        err = expect_raises(Socket::Error, "Hostname lookup for doesnotexist.example.org. failed") do
          TCPSocket.new("doesnotexist.example.org.", 12345)
        end
        # FIXME: Resolve special handling for win32. The error code handling should be identical.
        {% if flag?(:win32) %}
          [WinError::WSAHOST_NOT_FOUND, WinError::WSATRY_AGAIN].should contain err.os_error
        {% elsif flag?(:android) %}
          err.os_error.should eq(Errno.new(LibC::EAI_NODATA))
        {% else %}
          [Errno.new(LibC::EAI_NONAME), Errno.new(LibC::EAI_AGAIN)].should contain err.os_error
        {% end %}
      end

      it "raises (rather than segfault on darwin) when host doesn't exist and port is 0" do
        err = expect_raises(Socket::Error, "Hostname lookup for doesnotexist.example.org. failed") do
          TCPSocket.new("doesnotexist.example.org.", 0)
        end
        # FIXME: Resolve special handling for win32. The error code handling should be identical.
        {% if flag?(:win32) %}
          [WinError::WSAHOST_NOT_FOUND, WinError::WSATRY_AGAIN].should contain err.os_error
        {% elsif flag?(:android) %}
          err.os_error.should eq(Errno.new(LibC::EAI_NODATA))
        {% else %}
          [Errno.new(LibC::EAI_NONAME), Errno.new(LibC::EAI_AGAIN)].should contain err.os_error
        {% end %}
      end
    end

    it "fails to connect IPv6 to IPv4 server" do
      port = unused_local_port

      TCPServer.open("0.0.0.0", port) do |server|
        expect_raises(Socket::ConnectError, "Error connecting to '::1:#{port}'") do
          TCPSocket.new("::1", port)
        end
      end
    end
  end

  it "sync from server" do
    port = unused_local_port

    TCPServer.open("::", port) do |server|
      TCPSocket.open("localhost", port) do |client|
        sock = server.accept
        sock.sync?.should eq(server.sync?)
      end

      # test sync flag propagation after accept
      server.sync = !server.sync?

      TCPSocket.open("localhost", port) do |client|
        sock = server.accept
        sock.sync?.should eq(server.sync?)
      end
    end
  end

  it "settings" do
    port = unused_local_port

    TCPServer.open("::", port) do |server|
      TCPSocket.open("localhost", port) do |client|
        # test protocol specific socket options
        (client.tcp_nodelay = true).should be_true
        client.tcp_nodelay?.should be_true
        (client.tcp_nodelay = false).should be_false
        client.tcp_nodelay?.should be_false

        {% unless flag?(:openbsd) %}
          (client.tcp_keepalive_idle = 42).should eq 42
          client.tcp_keepalive_idle.should eq 42
          (client.tcp_keepalive_interval = 42).should eq 42
          client.tcp_keepalive_interval.should eq 42
          (client.tcp_keepalive_count = 42).should eq 42
          client.tcp_keepalive_count.should eq 42
        {% end %}
      end
    end
  end

  it "fails when connection is refused" do
    port = TCPServer.open("localhost", 0) do |server|
      server.local_address.port
    end

    expect_raises(Socket::ConnectError, "Error connecting to 'localhost:#{port}'") do
      TCPSocket.new("localhost", port)
    end
  end

  it "sends and receives messages" do
    port = unused_local_port

    TCPServer.open("::", port) do |server|
      TCPSocket.open("localhost", port) do |client|
        sock = server.accept

        client << "ping"
        sock.gets(4).should eq("ping")
        sock << "pong"
        client.gets(4).should eq("pong")
      end
    end
  end

  it "sends and receives messages" do
    port = unused_local_port

    channel = Channel(Exception?).new
    spawn do
      TCPServer.open("::", port) do |server|
        channel.send nil
        sock = server.accept
        sock.read_timeout = 3.second
        sock.write_timeout = 3.second

        sock.gets(4).should eq("ping")
        sock << "pong"
        channel.send nil
      end
    rescue exc
      channel.send exc
    end

    if exc = channel.receive
      raise exc
    end

    TCPSocket.open("localhost", port) do |client|
      client.read_timeout = 3.second
      client.write_timeout = 3.second
      client << "ping"
      client.gets(4).should eq("pong")
    end

    if exc = channel.receive
      raise exc
    end
  end
end