vweb: add an optional Context.before_accept_loop/0 method, to make testing easier and more robust (#20538)

This commit is contained in:
Delyan Angelov 2024-01-15 11:42:41 +02:00 committed by GitHub
parent 9268241f96
commit 6cfca66e73
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 136 additions and 2 deletions

View File

@ -0,0 +1,120 @@
import os
import log
import time
import vweb
import net.http
const vexe = os.getenv('VEXE')
const vroot = os.dir(vexe)
const port = 28871
const welcome_text = 'Welcome to our simple vweb server'
// Use a known good http client like `curl` (if it exists):
const curl_executable = os.find_abs_path_of_executable('curl') or { '' }
const curl_ok = curl_supports_ipv6()
fn curl_supports_ipv6() bool {
if curl_executable == '' {
return false
}
curl_res := os.execute('${curl_executable} --version')
if curl_res.exit_code != 0 {
return false
}
if !curl_res.output.match_glob('curl*Features: * IPv6 *') {
return false
}
return true
}
fn testsuite_begin() {
log.set_level(.debug)
log.debug(@FN)
os.chdir(vroot) or {}
if curl_ok {
log.info('working curl_executable found at: ${curl_executable}')
} else {
log.warn('no working working curl_executable found')
}
start_services()
}
fn testsuite_end() {
log.debug(@FN)
}
//
fn ensure_curl_works(tname string) ? {
if !curl_ok {
log.warn('skipping test ${tname}, since it needs a working curl')
return none
}
}
fn test_curl_connecting_through_ipv4_works() {
ensure_curl_works(@FN) or { return }
res := os.execute('${curl_executable} --connect-timeout 0.5 --silent http://127.0.0.1:${port}/')
assert res.exit_code == 0, res.output
assert res.output == welcome_text
log.info('> ${@FN}')
}
fn test_net_http_connecting_through_ipv4_works() {
res := http.get('http://127.0.0.1:${port}/')!
assert res.status_code == 200, res.str()
assert res.status_msg == 'OK', res.str()
assert res.body == welcome_text, res.str()
log.info('> ${@FN}')
}
fn test_curl_connecting_through_ipv6_works() {
ensure_curl_works(@FN) or { return }
res := os.execute('${curl_executable} --silent --connect-timeout 0.5 http://[::1]:${port}/')
assert res.exit_code == 0, res.output
assert res.output == welcome_text
log.info('> ${@FN}')
}
fn test_net_http_connecting_through_ipv6_works() {
$if windows {
log.warn('skipping test ${@FN} on windows for now')
return
}
res := http.get('http://[::1]:${port}/')!
assert res.status_code == 200, res.str()
assert res.status_msg == 'OK', res.str()
assert res.body == welcome_text, res.str()
log.info('> ${@FN}')
}
//
struct App {
vweb.Context
mut:
started chan bool
}
pub fn (mut app App) before_accept_loop() {
app.started <- true
}
pub fn (mut app App) index() vweb.Result {
return app.text(welcome_text)
}
fn start_services() {
log.debug('starting watchdog thread to ensure the test will always exit one way or another...')
spawn fn (timeout_in_ms int) {
time.sleep(timeout_in_ms * time.millisecond)
log.error('Timeout of ${timeout_in_ms} ms reached, for webserver: pid: ${os.getpid()}. Exiting ...')
exit(1)
}(10_000)
log.debug('starting webserver...')
mut app := &App{}
spawn vweb.run(app, port)
_ := <-app.started
log.debug('webserver started')
}

View File

@ -204,9 +204,19 @@ pub fn (ctx Context) init_server() {
eprintln('init_server() has been deprecated, please init your web app in `fn main()`')
}
// before_accept_loop is called once the vweb app is started, and listening, but before the loop that accepts
// incomming request connections.
// It will be called in the main thread, that runs vweb.run/2 or vweb.run_at/2.
// It allows you to be notified about the successfull start of your app, and to synchronise your other threads
// with the webserver start, without error prone and slow pooling or time.sleep waiting.
// Defining this method is optional.
pub fn (ctx &Context) before_accept_loop() {
}
// before_request is called once before each request is routed.
// It will be called in one of multiple threads in a pool, serving requests,
// the same one, in which the matching route method will be executed right after it.
// Defining this method is optional.
// This method is called before every request (aka middleware).
// You can use it for checking user session cookies or to add headers.
pub fn (ctx Context) before_request() {}
// TODO - test
@ -567,6 +577,10 @@ pub fn run_at[T](global_app &T, params RunParams) ! {
}
flush_stdout()
unsafe {
global_app.before_accept_loop()
}
// Forever accept every connection that comes, and
// pass it through the channel, to the thread pool:
for {