Skip to content

Commit 85d0e55

Browse files
committed
simple tcp server
1 parent 0f02e40 commit 85d0e55

File tree

4 files changed

+137
-42
lines changed

4 files changed

+137
-42
lines changed

.vscode/launch.json

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
// Use IntelliSense to learn about possible attributes.
3+
// Hover to view descriptions of existing attributes.
4+
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5+
"version": "0.2.0",
6+
"configurations": [
7+
{
8+
"type": "mix_task",
9+
"name": "mix (Default task)",
10+
"request": "launch",
11+
"projectDir": "${workspaceRoot}"
12+
},
13+
{
14+
"type": "mix_task",
15+
"name": "mix test",
16+
"request": "launch",
17+
"task": "test",
18+
"taskArgs": [
19+
"--trace"
20+
],
21+
"startApps": true,
22+
"projectDir": "${workspaceRoot}",
23+
"requireFiles": [
24+
"test/**/test_helper.exs",
25+
"test/**/*_test.exs"
26+
]
27+
}
28+
]
29+
}

lib/redex_server.ex

+41-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
defmodule Redex.Server do
2+
require Logger
23
@moduledoc """
34
A naive redis-server
45
"""
@@ -12,7 +13,45 @@ defmodule Redex.Server do
1213
{:ok, #PID<0.102.0>}
1314
1415
"""
15-
def start_link do
16-
{:ok, self()}
16+
def start_link args do
17+
Task.start_link(fn -> Redex.Server.accept(args) end)
18+
end
19+
20+
def accept(args) do
21+
# The options below mean:
22+
#
23+
# 1. `:binary` - receives data as binaries (instead of lists)
24+
# 2. `packet: :line` - receives data line by line
25+
# 3. `active: false` - blocks on `:gen_tcp.recv/2` until data is available
26+
# 4. `reuseaddr: true` - allows us to reuse the address if the listener crashes
27+
#
28+
port = Keyword.get(args, :port, 6379)
29+
{:ok, socket} =
30+
:gen_tcp.listen(port, [:binary, packet: :line, active: false, reuseaddr: true])
31+
Logger.info("Accepting connections on port #{port}")
32+
loop_acceptor(socket)
33+
end
34+
35+
defp loop_acceptor(socket) do
36+
{:ok, client} = :gen_tcp.accept(socket)
37+
serve(client)
38+
loop_acceptor(socket)
39+
end
40+
41+
defp serve(socket) do
42+
socket
43+
|> read_line()
44+
|> write_line(socket)
45+
46+
serve(socket)
47+
end
48+
49+
defp read_line(socket) do
50+
{:ok, data} = :gen_tcp.recv(socket, 0)
51+
data
52+
end
53+
54+
defp write_line(line, socket) do
55+
:gen_tcp.send(socket, line)
1756
end
1857
end

lib/test_utils.ex

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
defmodule Redex.TestUtils do
2+
def wait_for_server(retries \\ 5) do
3+
case :gen_tcp.connect('localhost', 6379, []) do
4+
{:ok, socket} ->
5+
:gen_tcp.close(socket)
6+
{:error, reason} ->
7+
cond do
8+
retries > 0 ->
9+
:timer.sleep(100)
10+
wait_for_server(retries - 1)
11+
:true ->
12+
Mix.raise "Cannot connect to Redis" <>
13+
" (http://localhost:6379):" <>
14+
" #{:inet.format_error(reason)}"
15+
end
16+
end
17+
end
18+
end

test/redex_test.exs

+49-40
Original file line numberDiff line numberDiff line change
@@ -4,63 +4,72 @@ defmodule RedexServerTest do
44

55
setup do
66
IO.puts "starting server"
7-
{:ok, server} = Redex.Server.start_link()
7+
{:ok, server} = Redex.Server.start_link([])
88
%{pid: server}
99
end
1010

11+
setup context do
12+
Redex.TestUtils.wait_for_server()
13+
context
14+
end
15+
1116
test "it starts", %{pid: pid} do
1217
assert Process.alive?(pid)
1318
end
19+
20+
test "it responds to ping", %{pid: pid} do
21+
22+
end
1423
end
1524

1625

17-
defmodule AssertionTest do
18-
use ExUnit.Case, async: true
26+
# defmodule AssertionTest do
27+
# use ExUnit.Case, async: true
1928

20-
# "setup_all" is called once per module before any test runs
21-
setup_all do
22-
IO.puts "Starting AssertionTest"
29+
# # "setup_all" is called once per module before any test runs
30+
# setup_all do
31+
# IO.puts "Starting AssertionTest"
2332

24-
# Context is not updated here
25-
:ok
26-
end
33+
# # Context is not updated here
34+
# :ok
35+
# end
2736

28-
# "setup" is called before each test
29-
setup do
30-
IO.puts "This is a setup callback for #{inspect self()}"
37+
# # "setup" is called before each test
38+
# setup do
39+
# IO.puts "This is a setup callback for #{inspect self()}"
3140

32-
on_exit fn ->
33-
IO.puts "This is invoked once the test is done. Process: #{inspect self()}"
34-
end
41+
# on_exit fn ->
42+
# IO.puts "This is invoked once the test is done. Process: #{inspect self()}"
43+
# end
3544

36-
# Returns extra metadata to be merged into context
37-
[hello: "world"]
45+
# # Returns extra metadata to be merged into context
46+
# [hello: "world"]
3847

39-
# Similarly, any of the following would work:
40-
# {:ok, [hello: "world"]}
41-
# %{hello: "world"}
42-
# {:ok, %{hello: "world"}}
43-
end
48+
# # Similarly, any of the following would work:
49+
# # {:ok, [hello: "world"]}
50+
# # %{hello: "world"}
51+
# # {:ok, %{hello: "world"}}
52+
# end
4453

45-
# Same as above, but receives the context as argument
46-
setup context do
47-
IO.puts "Setting up: #{context.test}"
48-
:ok
49-
end
54+
# # Same as above, but receives the context as argument
55+
# setup context do
56+
# IO.puts "Setting up: #{context.test}"
57+
# :ok
58+
# end
5059

51-
# Setups can also invoke a local or imported function that returns a context
52-
setup :invoke_local_or_imported_function
60+
# # Setups can also invoke a local or imported function that returns a context
61+
# setup :invoke_local_or_imported_function
5362

54-
test "always pass" do
55-
assert true
56-
end
63+
# test "always pass" do
64+
# assert true
65+
# end
5766

58-
test "uses metadata from setup", context do
59-
assert context[:hello] == "world"
60-
assert context[:from_named_setup] == true
61-
end
67+
# test "uses metadata from setup", context do
68+
# assert context[:hello] == "world"
69+
# assert context[:from_named_setup] == true
70+
# end
6271

63-
defp invoke_local_or_imported_function(context) do
64-
[from_named_setup: true]
65-
end
66-
end
72+
# defp invoke_local_or_imported_function(context) do
73+
# [from_named_setup: true]
74+
# end
75+
# end

0 commit comments

Comments
 (0)