From 90fe91f4ea8baea94ec7f2e26589a560342983a5 Mon Sep 17 00:00:00 2001 From: Evgeny Shirykalov Date: Wed, 27 Aug 2025 09:44:06 -0700 Subject: [PATCH] test: Link unit tests with nginx library. Some unit tests may use C functions from nginx. To support this scenario, special static library is built. Necessary linker flags are also added. Since these changes are needed for specific unit tests only, new feature "unittest" is defined. --- Cargo.toml | 6 +- nginx-src/Cargo.toml | 4 + nginx-src/libnginx/config | 5 + nginx-src/libnginx/config.make | 73 +++++++++++++ nginx-src/libnginx/libnginx.c | 187 +++++++++++++++++++++++++++++++++ nginx-src/libnginx/libnginx.h | 14 +++ nginx-src/src/lib.rs | 6 ++ nginx-sys/Cargo.toml | 1 + nginx-sys/build/main.rs | 96 +++++++++++++++-- src/core/pool.rs | 102 ++++++++++++++++++ 10 files changed, 485 insertions(+), 9 deletions(-) create mode 100644 nginx-src/libnginx/config create mode 100644 nginx-src/libnginx/config.make create mode 100644 nginx-src/libnginx/libnginx.c create mode 100644 nginx-src/libnginx/libnginx.h diff --git a/Cargo.toml b/Cargo.toml index fd1021f8..d1d07c70 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,11 +36,14 @@ all-features = true default-target = "x86_64-unknown-linux-gnu" targets = [] +[workspace.dependencies] +nginx-sys = { path = "nginx-sys", version = "0.5.0-beta" } + [dependencies] allocator-api2 = { version = "0.2.21", default-features = false } async-task = { version = "4.7.1", optional = true } lock_api = "0.4.13" -nginx-sys = { path = "nginx-sys", version = "0.5.0-beta"} +nginx-sys = { workspace = true } pin-project-lite = { version = "0.2.16", optional = true } [features] @@ -69,4 +72,5 @@ vendored = ["nginx-sys/vendored"] maintenance = { status = "experimental" } [dev-dependencies] +nginx-sys = { workspace = true, features = ["unittest"] } tempfile = { version = "3.20.0", default-features = false } diff --git a/nginx-src/Cargo.toml b/nginx-src/Cargo.toml index 4ac31046..57c1efcd 100644 --- a/nginx-src/Cargo.toml +++ b/nginx-src/Cargo.toml @@ -12,6 +12,10 @@ homepage.workspace = true repository.workspace = true rust-version.workspace = true +[features] +# Builds nginx library from vendored sources for unit tests. +unittest = [] + [dependencies] duct = "1" flate2 = "1" diff --git a/nginx-src/libnginx/config b/nginx-src/libnginx/config new file mode 100644 index 00000000..ffde6d7f --- /dev/null +++ b/nginx-src/libnginx/config @@ -0,0 +1,5 @@ + +# Copyright (C) Nginx, Inc. + + +ngx_addon_name="libnginx" diff --git a/nginx-src/libnginx/config.make b/nginx-src/libnginx/config.make new file mode 100644 index 00000000..dfdfac96 --- /dev/null +++ b/nginx-src/libnginx/config.make @@ -0,0 +1,73 @@ + +# Copyright (C) Nginx, Inc. + + +ngx_addon_name=libnginx +ngx_module=$ngx_addon_name +ngx_module_c=$ngx_addon_dir/libnginx.c + +ngx_ar="\$(AR)" +ngx_libext=.a +ngx_libout="r " + +case "$NGX_CC_NAME" in + + msvc) + ngx_ar=lib + ngx_libext=.lib + ngx_libout="/OUT:" + ;; + +esac + +if test -n "$NGX_PCH"; then + ngx_cc="\$(CC) $ngx_compile_opt \$(CFLAGS) $ngx_use_pch \$(ALL_INCS)" +else + ngx_cc="\$(CC) $ngx_compile_opt \$(CFLAGS) \$(CORE_INCS)" +fi + +ngx_module_objs= +for ngx_src in $ngx_module_c +do + ngx_obj="addon/`basename \`dirname $ngx_src\``" + + test -d $NGX_OBJS/$ngx_obj || mkdir -p $NGX_OBJS/$ngx_obj + + ngx_obj=`echo $ngx_obj/\`basename $ngx_src\` \ + | sed -e "s/\//$ngx_regex_dirsep/g" \ + -e "s#^\(.*\.\)c\\$#$ngx_objs_dir\1$ngx_objext#g"` + + ngx_module_objs="$ngx_module_objs $ngx_obj" + + cat << END >> $NGX_MAKEFILE + +$ngx_obj: \$(CORE_DEPS)$ngx_cont$ngx_src + $ngx_cc$ngx_tab$ngx_objout$ngx_obj$ngx_tab$ngx_src$NGX_AUX + +END + +done + +ngx_objs=`echo $ngx_module_objs $ngx_modules_obj $ngx_all_objs \ + | sed -e "s/[^ ]*\\/nginx\\.$ngx_objext//g" \ + -e "s/ *\([^ ][^ ]*\)/$ngx_long_regex_cont\1/g" \ + -e "s/\//$ngx_regex_dirsep/g"` + +ngx_deps=`echo $ngx_module_objs $ngx_modules_obj $ngx_all_objs \ + | sed -e "s/[^ ]*\\/nginx\\.$ngx_objext//g" \ + -e "s/ *\([^ ][^ ]*\)/$ngx_regex_cont\1/g" \ + -e "s/\//$ngx_regex_dirsep/g"` + +ngx_obj=$NGX_OBJS$ngx_dirsep$ngx_module$ngx_libext + +cat << END >> $NGX_MAKEFILE + +modules: $ngx_obj + +$ngx_obj: $ngx_deps$ngx_spacer + $ngx_ar $ngx_long_start$ngx_libout$ngx_obj$ngx_long_cont$ngx_objs +$ngx_long_end + +LIBNGINX_LDFLAGS = $NGX_LD_OPT $CORE_LIBS + +END diff --git a/nginx-src/libnginx/libnginx.c b/nginx-src/libnginx/libnginx.c new file mode 100644 index 00000000..da122305 --- /dev/null +++ b/nginx-src/libnginx/libnginx.c @@ -0,0 +1,187 @@ + +/* + * Copyright (C) Nginx, Inc. + */ + + +#include +#include +#include + +#include "libnginx.h" + + +/* + * We need to build nginx.c to correctly initialize ngx_core_module, + * but exclude an existing definition of main. + */ +#define main main_unused +#include "nginx.c" +#undef main + + +static ngx_int_t libngx_write_temp_conf_file(ngx_cycle_t *cycle, + ngx_str_t *data, ngx_str_t *name); + + +ngx_cycle_t * +libngx_init(u_char *prefix) +{ + static ngx_cycle_t *cycle = NGX_CONF_UNSET_PTR, init_cycle; + + ngx_log_t *log; + char *const argv[] = { "nginx" }; + + if (cycle != NGX_CONF_UNSET_PTR) { + return cycle; + } + + cycle = NULL; + + ngx_conf_params = (u_char *) "daemon off; master_process off;"; + ngx_error_log = (u_char *) ""; + ngx_prefix = prefix; + + ngx_debug_init(); + + if (ngx_strerror_init() != NGX_OK) { + return cycle; + } + + ngx_max_sockets = -1; + + ngx_time_init(); + +#if (NGX_PCRE) + ngx_regex_init(); +#endif + + ngx_pid = ngx_getpid(); + ngx_parent = ngx_getppid(); + + log = ngx_log_init(ngx_prefix, ngx_error_log); + if (log == NULL) { + return NULL; + } + + log->log_level = NGX_LOG_INFO; + +#if (NGX_OPENSSL) + ngx_ssl_init(log); +#endif + + ngx_memzero(&init_cycle, sizeof(ngx_cycle_t)); + init_cycle.log = log; + init_cycle.log_use_stderr = 1; + ngx_cycle = &init_cycle; + + init_cycle.pool = ngx_create_pool(1024, log); + if (init_cycle.pool == NULL) { + return NULL; + } + + if (ngx_save_argv(&init_cycle, sizeof(argv)/sizeof(argv[0]), argv) != NGX_OK) { + return NULL; + } + + if (ngx_process_options(&init_cycle) != NGX_OK) { + return NULL; + } + + if (ngx_os_init(log) != NGX_OK) { + return NULL; + } + + if (ngx_crc32_table_init() != NGX_OK) { + return NULL; + } + + ngx_slab_sizes_init(); + + if (ngx_preinit_modules() != NGX_OK) { + return NULL; + } + + cycle = &init_cycle; + return cycle; +} + + +ngx_int_t +libngx_create_cycle(ngx_cycle_t *cycle, ngx_str_t *conf) +{ + ngx_str_t conf_file; + + ngx_cycle = cycle; + + if (libngx_write_temp_conf_file(cycle, conf, &conf_file) != NGX_OK) { + return NGX_ERROR; + } + + ngx_conf_file = conf_file.data; + + if (ngx_process_options(cycle) != NGX_OK) { + return NGX_ERROR; + } + + cycle = ngx_init_cycle(cycle); + if (cycle == NULL) { + return NGX_ERROR; + } + + ngx_cycle = cycle; + + return NGX_OK; +} + + +static ngx_int_t +libngx_write_temp_conf_file(ngx_cycle_t *cycle, ngx_str_t *data, + ngx_str_t *name) +{ + ngx_int_t rc; + ngx_path_t *path; + ngx_temp_file_t tf; + + path = ngx_pcalloc(cycle->pool, sizeof(ngx_path_t)); + if (path == NULL) { + return NGX_ERROR; + } + + ngx_memzero(&tf, sizeof(ngx_temp_file_t)); + + tf.file.fd = NGX_INVALID_FILE; + tf.file.log = cycle->log; + tf.access = NGX_FILE_OWNER_ACCESS; + tf.clean = 1; + tf.path = path; + tf.pool = cycle->pool; + tf.persistent = 1; + + ngx_str_set(&path->name, "conf"); + + rc = ngx_conf_full_name(cycle, &path->name, 0); + if (rc != NGX_OK) { + return rc; + } + + if (ngx_create_dir(path->name.data, ngx_dir_access(tf.access)) + == NGX_FILE_ERROR) + { + return ngx_errno; + } + + rc = ngx_create_temp_file(&tf.file, tf.path, tf.pool, tf.persistent, + tf.clean, tf.access); + if (rc != NGX_OK) { + return rc; + } + + if (ngx_write_file(&tf.file, data->data, data->len, 0) == NGX_ERROR) { + return NGX_ERROR; + } + + *name = tf.file.name; + + return NGX_OK; +} diff --git a/nginx-src/libnginx/libnginx.h b/nginx-src/libnginx/libnginx.h new file mode 100644 index 00000000..c2b52fb5 --- /dev/null +++ b/nginx-src/libnginx/libnginx.h @@ -0,0 +1,14 @@ + +/* + * Copyright (C) Nginx, Inc + */ + + +#ifndef _LIBNGINX_H_INCLUDED_ +#define _LIBNGINX_H_INCLUDED_ + +ngx_cycle_t *libngx_init(u_char *prefix); +ngx_int_t libngx_create_cycle(ngx_cycle_t *cycle, ngx_str_t *conf); + + +#endif /* _LIBNGINX_H_INCLUDED_ */ diff --git a/nginx-src/src/lib.rs b/nginx-src/src/lib.rs index a606196a..fa52f39c 100644 --- a/nginx-src/src/lib.rs +++ b/nginx-src/src/lib.rs @@ -113,6 +113,12 @@ fn nginx_configure_flags(vendored: &[String]) -> Vec { nginx_opts.push(format!("--with-ld-opt={ldflags}")); } + #[cfg(feature = "unittest")] + nginx_opts.push(format!( + "--add-dynamic-module={}/libnginx", + env!("CARGO_MANIFEST_DIR") + )); + nginx_opts } diff --git a/nginx-sys/Cargo.toml b/nginx-sys/Cargo.toml index 4c2ce3ed..94f1a558 100644 --- a/nginx-sys/Cargo.toml +++ b/nginx-sys/Cargo.toml @@ -34,3 +34,4 @@ shlex = "1.3" [features] vendored = ["dep:nginx-src"] +unittest = ["vendored", "nginx-src/unittest"] diff --git a/nginx-sys/build/main.rs b/nginx-sys/build/main.rs index 8f85bd0c..f806cceb 100644 --- a/nginx-sys/build/main.rs +++ b/nginx-sys/build/main.rs @@ -192,7 +192,11 @@ impl NginxSource { /// Generates Rust bindings for NGINX fn generate_binding(nginx: &NginxSource) { let autoconf_makefile_path = nginx.build_dir.join("Makefile"); - let (includes, defines) = parse_makefile(&autoconf_makefile_path); + let ParsedMakefile { + includes, + defines, + libs, + } = parse_makefile(&autoconf_makefile_path); let includes: Vec<_> = includes .into_iter() .map(|path| { @@ -216,7 +220,22 @@ fn generate_binding(nginx: &NginxSource) { } })); - print_cargo_metadata(nginx, &includes, &defines).expect("cargo dependency metadata"); + let libs: Vec<_> = libs + .into_iter() + .map(|lib_opt| { + if let LibOption::LibPath(path) = lib_opt { + if Path::new(&path).is_absolute() { + LibOption::LibPath(path) + } else { + LibOption::LibPath(nginx.source_dir.join(path).to_string_lossy().to_string()) + } + } else { + lib_opt + } + }) + .collect(); + + print_cargo_metadata(nginx, &includes, &defines, &libs).expect("cargo dependency metadata"); // bindgen targets the latest known stable by default let rust_target: bindgen::RustTarget = env::var("CARGO_PKG_RUST_VERSION") @@ -249,12 +268,28 @@ fn generate_binding(nginx: &NginxSource) { .expect("Couldn't write bindings!"); } +/// Represents a library flags for NGINX: +/// * `LibPath`: A library directory path +/// * `Lib`: A library name +pub enum LibOption { + LibPath(String), + Lib(String), +} + +/// Parsed representation of the NGINX Makefile: +/// * `includes`: List of include paths +/// * `defines`: List of preprocessor definitions +/// * `libs`: List of libraries and/or library directories +pub struct ParsedMakefile { + pub includes: Vec, + pub defines: Vec<(String, Option)>, + pub libs: Vec, +} + /// Reads through the makefile generated by autoconf and finds all of the includes /// and definitions used to compile nginx. This is used to generate the correct bindings /// for the nginx source code. -pub fn parse_makefile( - nginx_autoconf_makefile_path: &PathBuf, -) -> (Vec, Vec<(String, Option)>) { +pub fn parse_makefile(nginx_autoconf_makefile_path: &PathBuf) -> ParsedMakefile { fn parse_line( includes: &mut Vec, defines: &mut Vec<(String, Option)>, @@ -286,11 +321,35 @@ pub fn parse_makefile( } } + fn parse_lib_line(libs: &mut Vec, line: &str) { + let mut words = shlex::Shlex::new(line); + + while let Some(word) = words.next() { + if let Some(lib_dir) = word.strip_prefix("-L") { + let value = if lib_dir.is_empty() { + words.next().expect("-L argument") + } else { + lib_dir.to_string() + }; + libs.push(LibOption::LibPath(value)); + } else if let Some(lib) = word.strip_prefix("-l") { + let value = if lib.is_empty() { + words.next().expect("-l argument") + } else { + lib.to_string() + }; + libs.push(LibOption::Lib(value)); + } + } + } + let mut all_incs = vec![]; let mut cflags_includes = vec![]; let mut defines = vec![]; + let mut libs = vec![]; + let makefile_contents = match read_to_string(nginx_autoconf_makefile_path) { Ok(path) => path, Err(e) => { @@ -316,6 +375,8 @@ pub fn parse_makefile( parse_line(&mut all_incs, &mut defines, tail); } else if let Some(tail) = line.strip_prefix("CFLAGS") { parse_line(&mut cflags_includes, &mut defines, tail); + } else if let Some(tail) = line.strip_prefix("LIBNGINX_LDFLAGS") { + parse_lib_line(&mut libs, tail); } line.clear(); @@ -323,10 +384,11 @@ pub fn parse_makefile( cflags_includes.extend(all_incs); - ( - cflags_includes.into_iter().map(PathBuf::from).collect(), + ParsedMakefile { + includes: cflags_includes.into_iter().map(PathBuf::from).collect(), defines, - ) + libs, + } } /// Collect info about the nginx configuration and expose it to the dependents via @@ -335,6 +397,7 @@ pub fn print_cargo_metadata>( nginx: &NginxSource, includes: &[T], defines: &[(String, Option)], + _libs: &[LibOption], ) -> Result<(), Box> { // Unquote and merge C string constants let unquote_re = regex::Regex::new(r#""(.*?[^\\])"\s*"#).unwrap(); @@ -420,6 +483,23 @@ pub fn print_cargo_metadata>( println!("cargo::metadata=os={ngx_os}"); println!("cargo::rustc-cfg=ngx_os=\"{ngx_os}\""); + #[cfg(feature = "unittest")] + { + // Linker flags to use libnginx in tests + println!( + "cargo::rustc-link-search={}", + nginx.build_dir.to_str().expect("Unicode build path") + ); + + for lib in _libs { + if let LibOption::LibPath(ref path) = lib { + println!("cargo::rustc-link-search={path}"); + } else if let LibOption::Lib(ref name) = lib { + println!("cargo::rustc-link-lib={name}"); + } + } + } + Ok(()) } diff --git a/src/core/pool.rs b/src/core/pool.rs index 59a41bd0..f7a6bc50 100644 --- a/src/core/pool.rs +++ b/src/core/pool.rs @@ -326,3 +326,105 @@ impl Pool { unsafe extern "C" fn cleanup_type(data: *mut c_void) { ptr::drop_in_place(data as *mut T); } + +#[cfg(all(test, feature = "vendored"))] +mod tests { + + use nginx_sys::{ngx_create_pool, ngx_destroy_pool}; + + use super::*; + + #[link(name = "nginx", kind = "static")] + extern "C" { + pub fn libngx_init(prefix: *mut nginx_sys::u_char) -> *mut nginx_sys::ngx_cycle_t; + } + + #[test] + fn test_pool_resize() { + unsafe { + libngx_init("prefix".as_ptr() as *mut nginx_sys::u_char); + }; + + let mut log: nginx_sys::ngx_log_t = unsafe { core::mem::zeroed() }; + let p: *mut nginx_sys::ngx_pool_t = unsafe { ngx_create_pool(1024, &mut log) }; + let pool = unsafe { Pool::from_ngx_pool(p) }; + + let layout = Layout::from_size_align(16, 8).unwrap(); + let slice_ptr = Allocator::allocate(&pool, layout).unwrap(); + let ptr = slice_ptr.as_ptr().cast::(); + + let new_layout = Layout::from_size_align(32, 8).unwrap(); + let nonnull_ptr = unsafe { NonNull::new_unchecked(ptr) }; + let newptr = unsafe { pool.resize(nonnull_ptr, layout, new_layout) }; + + assert!(newptr.is_ok()); + assert!(core::ptr::addr_eq(newptr.unwrap().as_ptr(), ptr)); + + unsafe { ngx_destroy_pool(p) }; + } + + #[test] + fn test_vec() { + unsafe { + libngx_init("prefix".as_ptr() as *mut nginx_sys::u_char); + }; + + let mut log: nginx_sys::ngx_log_t = unsafe { core::mem::zeroed() }; + let p: *mut nginx_sys::ngx_pool_t = unsafe { ngx_create_pool(1024, &mut log) }; + let pool = unsafe { Pool::from_ngx_pool(p) }; + + let mut v1: allocator_api2::vec::Vec = allocator_api2::vec::Vec::new_in(pool); + + v1.reserve(4); + assert!(v1.capacity() >= 4); + let v1_ptr1 = v1.as_ptr(); + + v1.reserve(4); + assert!(v1.capacity() >= 8); + let v1_ptr2 = v1.as_ptr(); + + assert!(v1_ptr1 == v1_ptr2); + + v1.resize(4, 1); + + v1.shrink_to_fit(); + let v1_ptr3 = v1.as_ptr(); + + assert!(v1_ptr1 == v1_ptr3); + + unsafe { ngx_destroy_pool(p) }; + } + + #[test] + fn test_two_vecs() { + unsafe { + libngx_init("prefix".as_ptr() as *mut nginx_sys::u_char); + }; + + let mut log: nginx_sys::ngx_log_t = unsafe { core::mem::zeroed() }; + let p: *mut nginx_sys::ngx_pool_t = unsafe { ngx_create_pool(2048, &mut log) }; + let pool = unsafe { Pool::from_ngx_pool(p) }; + + let mut v1: allocator_api2::vec::Vec = + allocator_api2::vec::Vec::new_in(pool.clone()); + + v1.reserve(128); + assert!(v1.capacity() >= 128); + let v1_ptr1 = v1.as_ptr(); + + v1.resize(128, 1); + + let mut v2: allocator_api2::vec::Vec = allocator_api2::vec::Vec::new_in(pool); + + v2.reserve(128); + assert!(v2.capacity() >= 128); + + v1.reserve(128); + assert!(v1.capacity() >= 256, "actual capacity: {}", v1.capacity()); + let v1_ptr2 = v1.as_ptr(); + + assert!(v1_ptr1 != v1_ptr2); + + unsafe { ngx_destroy_pool(p) }; + } +}