417 lines
10 KiB
Odin
417 lines
10 KiB
Odin
package fjord
|
|
|
|
import "core:fmt"
|
|
import "core:log"
|
|
import "core:net"
|
|
import "core:os/os2"
|
|
import "core:path/slashpath"
|
|
import "core:strings"
|
|
import "core:thread"
|
|
import "core:time"
|
|
|
|
import "core:testing"
|
|
|
|
import http "http"
|
|
|
|
StandardError :: enum {
|
|
TestError,
|
|
}
|
|
|
|
Error :: union {
|
|
StandardError,
|
|
}
|
|
|
|
LOCAL_ADDRESS :: net.IP4_Address{127, 0, 0, 1}
|
|
|
|
@(private = "file")
|
|
port_counter := 50000
|
|
|
|
@(private = "file")
|
|
get_test_port :: proc() -> int {
|
|
port := port_counter
|
|
port_counter += 10
|
|
return port
|
|
}
|
|
|
|
first_handler :: proc(request: ^http.Request) -> (http.Response, Error) {
|
|
entity := request.path_variables["entity"]
|
|
|
|
body := strings.concatenate(
|
|
{"<div>Hello first ", entity, "</div>"},
|
|
context.temp_allocator,
|
|
)
|
|
response := http.make_response(
|
|
.Ok,
|
|
transmute([]byte)body,
|
|
.Html,
|
|
context.temp_allocator,
|
|
)
|
|
|
|
return response, nil
|
|
}
|
|
|
|
second_handler :: proc(request: ^http.Request) -> (http.Response, Error) {
|
|
entity := request.path_variables["entity"]
|
|
|
|
body := strings.concatenate(
|
|
{"<div>Hello second ", entity, "</div>"},
|
|
context.temp_allocator,
|
|
)
|
|
response := http.make_response(
|
|
.Ok,
|
|
transmute([]byte)body,
|
|
.Html,
|
|
context.temp_allocator,
|
|
)
|
|
|
|
return response, nil
|
|
}
|
|
|
|
third_handler :: proc(request: ^http.Request) -> (http.Response, Error) {
|
|
entity := request.path_variables["entity"]
|
|
other_entity := request.path_variables["other_entity"]
|
|
|
|
body := strings.concatenate(
|
|
{"<div>Hello third ", entity, " and ", other_entity, "</div>"},
|
|
context.temp_allocator,
|
|
)
|
|
response := http.make_response(
|
|
.Ok,
|
|
transmute([]byte)body,
|
|
.Html,
|
|
context.temp_allocator,
|
|
)
|
|
|
|
return response, nil
|
|
}
|
|
|
|
@(test)
|
|
test_router_ok :: proc(t: ^testing.T) {
|
|
context.logger = log.create_console_logger(.Info)
|
|
defer log.destroy_console_logger(context.logger)
|
|
|
|
endpoint := net.Endpoint {
|
|
address = LOCAL_ADDRESS,
|
|
port = get_test_port(),
|
|
}
|
|
THREAD_COUNT :: 8
|
|
|
|
server: Server(Error)
|
|
server_init(&server, endpoint, THREAD_COUNT, context.allocator)
|
|
defer server_destroy(&server)
|
|
|
|
server_add_route(&server, .GET, {"hello", ":entity"}, first_handler)
|
|
server_add_route(
|
|
&server,
|
|
.GET,
|
|
{"hello", ":entity", "only"},
|
|
second_handler,
|
|
)
|
|
server_add_route(
|
|
&server,
|
|
.GET,
|
|
{"hello", ":entity", "and", ":other_entity"},
|
|
third_handler,
|
|
)
|
|
|
|
handler, path_vars, ok := router_lookup(
|
|
&server.router,
|
|
.GET,
|
|
{"hello", "world"},
|
|
context.temp_allocator,
|
|
); assert(ok)
|
|
assert(handler == first_handler)
|
|
assert(path_vars["entity"] == "world")
|
|
|
|
handler, path_vars, ok = router_lookup(
|
|
&server.router,
|
|
.GET,
|
|
{"hello", "lonely world", "only"},
|
|
context.temp_allocator,
|
|
); assert(ok)
|
|
assert(handler == second_handler)
|
|
assert(path_vars["entity"] == "lonely world")
|
|
|
|
handler, path_vars, ok = router_lookup(
|
|
&server.router,
|
|
.GET,
|
|
{"hello", "world", "and", "worlds friend"},
|
|
context.temp_allocator,
|
|
); assert(ok)
|
|
assert(handler == third_handler)
|
|
assert(path_vars["entity"] == "world")
|
|
assert(path_vars["other_entity"] == "worlds friend")
|
|
|
|
ok = server_remove_route(&server, .GET, {"hello", ":entity"}); assert(ok)
|
|
ok = server_remove_route(&server, .GET, {"hello", ":entity"}); assert(!ok)
|
|
|
|
free_all(context.temp_allocator)
|
|
}
|
|
|
|
start_concurrent_server :: proc(
|
|
server: ^Server($Error_Type),
|
|
) -> ^thread.Thread {
|
|
ServerThreadData :: struct {
|
|
server: ^Server(Error_Type),
|
|
thread: ^thread.Thread,
|
|
}
|
|
|
|
server_thread_proc :: proc(t: ^thread.Thread) {
|
|
d := (^ServerThreadData)(t.data)
|
|
server_listen_and_serve(d.server)
|
|
}
|
|
|
|
d := new(ServerThreadData, context.temp_allocator)
|
|
d.server = server
|
|
|
|
if d.thread = thread.create(server_thread_proc); d.thread != nil {
|
|
d.thread.init_context = context
|
|
d.thread.data = rawptr(d)
|
|
thread.start(d.thread)
|
|
}
|
|
|
|
// Give server some time to start
|
|
time.sleep(50 * time.Millisecond)
|
|
return d.thread
|
|
}
|
|
|
|
shutdown_concurrent_server :: proc(
|
|
server: ^Server($Error_Type),
|
|
t: ^thread.Thread,
|
|
endpoint: net.Endpoint,
|
|
) {
|
|
server_shutdown(server)
|
|
|
|
// Send a dummy request to unblock accept_tcp, todo: fix this immidiate server shutdown
|
|
url := fmt.tprintf(
|
|
"http://%d.%d.%d.%d:%d/dummy",
|
|
endpoint.address.(net.IP4_Address)[0],
|
|
endpoint.address.(net.IP4_Address)[1],
|
|
endpoint.address.(net.IP4_Address)[2],
|
|
endpoint.address.(net.IP4_Address)[3],
|
|
endpoint.port,
|
|
)
|
|
_, _, _, err := os2.process_exec(
|
|
{command = []string{"curl", "-s", url}},
|
|
context.temp_allocator,
|
|
)
|
|
assert(err == nil)
|
|
|
|
thread.join(t)
|
|
thread.destroy(t)
|
|
}
|
|
|
|
assert_endpoint_response :: proc(
|
|
endpoint: net.Endpoint,
|
|
path: []string,
|
|
expected_response: string,
|
|
) {
|
|
// Build URL from endpoint and path
|
|
path_str := slashpath.join(path, context.temp_allocator)
|
|
url := fmt.tprintf(
|
|
"http://%d.%d.%d.%d:%d/%s",
|
|
endpoint.address.(net.IP4_Address)[0],
|
|
endpoint.address.(net.IP4_Address)[1],
|
|
endpoint.address.(net.IP4_Address)[2],
|
|
endpoint.address.(net.IP4_Address)[3],
|
|
endpoint.port,
|
|
path_str,
|
|
)
|
|
|
|
state, stdout, stderr, err := os2.process_exec(
|
|
{command = []string{"curl", "-s", url}},
|
|
context.temp_allocator,
|
|
)
|
|
|
|
if err != nil {
|
|
log.errorf("Failed to execute curl: %d %s %s", err, stdout, stderr)
|
|
assert(false, "curl execution failed")
|
|
return
|
|
}
|
|
|
|
if state.exit_code != 0 {
|
|
log.errorf("curl failed with exit code: %d", state.exit_code)
|
|
assert(false, "curl returned non-zero exit code")
|
|
return
|
|
}
|
|
|
|
response := string(stdout)
|
|
log.assertf(
|
|
response == expected_response,
|
|
"Expected: %s, but got: %s",
|
|
response,
|
|
expected_response,
|
|
)
|
|
}
|
|
|
|
@(test)
|
|
test_server_general_ok :: proc(t: ^testing.T) {
|
|
context.logger = log.create_console_logger(.Info)
|
|
defer log.destroy_console_logger(context.logger)
|
|
|
|
port := get_test_port()
|
|
endpoint := net.Endpoint {
|
|
address = LOCAL_ADDRESS,
|
|
port = port,
|
|
}
|
|
THREAD_COUNT :: 8
|
|
|
|
server: Server(Error)
|
|
server_init(&server, endpoint, THREAD_COUNT, context.allocator)
|
|
defer server_destroy(&server)
|
|
|
|
server_add_route(&server, .GET, {"hello", ":entity"}, first_handler)
|
|
server_add_route(
|
|
&server,
|
|
.GET,
|
|
{"hello", ":entity", "only"},
|
|
second_handler,
|
|
)
|
|
server_add_route(
|
|
&server,
|
|
.GET,
|
|
{"hello", ":entity", "and", ":other_entity"},
|
|
third_handler,
|
|
)
|
|
|
|
t := start_concurrent_server(&server)
|
|
|
|
// Try with some load
|
|
ITERATIONS :: 100
|
|
for i in 0 ..< ITERATIONS {
|
|
assert_endpoint_response(
|
|
endpoint,
|
|
{"hello", "world"},
|
|
"<div>Hello first world</div>",
|
|
)
|
|
assert_endpoint_response(
|
|
endpoint,
|
|
{"hello", "lonely%20world", "only"},
|
|
"<div>Hello second lonely%20world</div>",
|
|
)
|
|
assert_endpoint_response(
|
|
endpoint,
|
|
{"hello", "world", "and", "worlds%20friend"},
|
|
"<div>Hello third world and worlds%20friend</div>",
|
|
)
|
|
}
|
|
|
|
shutdown_concurrent_server(&server, t, endpoint)
|
|
|
|
free_all(context.temp_allocator)
|
|
}
|
|
|
|
@(test)
|
|
test_custom_not_found_handler :: proc(t: ^testing.T) {
|
|
context.logger = log.create_console_logger(.Info)
|
|
defer log.destroy_console_logger(context.logger)
|
|
|
|
port := get_test_port()
|
|
endpoint := net.Endpoint {
|
|
address = LOCAL_ADDRESS,
|
|
port = port,
|
|
}
|
|
THREAD_COUNT :: 8
|
|
|
|
server: Server(Error)
|
|
server_init(&server, endpoint, THREAD_COUNT, context.allocator)
|
|
defer server_destroy(&server)
|
|
|
|
custom_not_found_handler :: proc(
|
|
request: ^http.Request,
|
|
) -> (
|
|
http.Response,
|
|
Error,
|
|
) {
|
|
body := "<div>Custom 404</div>"
|
|
http_response := http.make_response(
|
|
.NotFound,
|
|
transmute([]byte)body,
|
|
.Html,
|
|
)
|
|
return http_response, nil
|
|
}
|
|
|
|
server_set_not_found_handler(&server, custom_not_found_handler)
|
|
|
|
server_add_route(
|
|
&server,
|
|
.GET,
|
|
{"exists"},
|
|
proc(request: ^http.Request) -> (http.Response, Error) {
|
|
body := "<div>This route exists</div>"
|
|
response := http.make_response(
|
|
.Ok,
|
|
transmute([]byte)body,
|
|
.Html,
|
|
context.temp_allocator,
|
|
)
|
|
return response, nil
|
|
},
|
|
)
|
|
|
|
t := start_concurrent_server(&server)
|
|
|
|
assert_endpoint_response(
|
|
endpoint,
|
|
{"does-not-exist"},
|
|
"<div>Custom 404</div>",
|
|
)
|
|
|
|
assert_endpoint_response(
|
|
endpoint,
|
|
{"exists"},
|
|
"<div>This route exists</div>",
|
|
)
|
|
|
|
shutdown_concurrent_server(&server, t, endpoint)
|
|
|
|
free_all(context.temp_allocator)
|
|
}
|
|
|
|
@(test)
|
|
test_custom_error_handler :: proc(t: ^testing.T) {
|
|
context.logger = log.create_console_logger(.Info)
|
|
defer log.destroy_console_logger(context.logger)
|
|
|
|
port := get_test_port()
|
|
endpoint := net.Endpoint {
|
|
address = LOCAL_ADDRESS,
|
|
port = port,
|
|
}
|
|
THREAD_COUNT :: 8
|
|
|
|
server: Server(Error)
|
|
server_init(&server, endpoint, THREAD_COUNT, context.allocator)
|
|
defer server_destroy(&server)
|
|
|
|
custom_error_handler :: proc(error: Error) -> http.Response {
|
|
body := "<div>Custom Error Handler</div>"
|
|
http_response := http.make_response(
|
|
.BadRequest,
|
|
transmute([]byte)body,
|
|
.Html,
|
|
)
|
|
return http_response
|
|
}
|
|
|
|
server_set_error_handler(&server, custom_error_handler)
|
|
|
|
error_handler :: proc(request: ^http.Request) -> (http.Response, Error) {
|
|
return http.Response{}, .TestError
|
|
}
|
|
|
|
server_add_route(&server, .GET, {"error"}, error_handler)
|
|
|
|
t := start_concurrent_server(&server)
|
|
|
|
assert_endpoint_response(
|
|
endpoint,
|
|
{"error"},
|
|
"<div>Custom Error Handler</div>",
|
|
)
|
|
|
|
shutdown_concurrent_server(&server, t, endpoint)
|
|
|
|
free_all(context.temp_allocator)
|
|
}
|