diff --git a/.gitignore b/.gitignore
index 11edcfd1b..b9d400acc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -66,3 +66,4 @@ perf.data.old
.idea/
.vscode/
centrallix-os/tmp/*
+centrallix-os/datasets/
diff --git a/centrallix-doc/Widgets/onepage.html b/centrallix-doc/Widgets/onepage.html
index 9a449b6a5..d2e733884 100644
--- a/centrallix-doc/Widgets/onepage.html
+++ b/centrallix-doc/Widgets/onepage.html
@@ -5976,7 +5976,7 @@
Child Properties:
| type |
string |
-The type of the column: "text", "check", or "image". "text" is a normal column, and displays the textual value of the data element. "check" displays a checkmark if the data is non-zero (integers) or for strings if the value is non-empty and not "N" or "No". "image" displays the image referred to by the pathname contained in the data value. |
+The type of the column: "text", "check", "image", or "progress". "text" is a normal column, and displays the textual value of the data element. "check" displays a checkmark if the data is non-zero (integers) or for strings if the value is non-empty and not "N" or "No". "image" displays the image referred to by the pathname contained in the data value. "progress" displays a progress bar, with additional fields such as bar_color, bar_textcollor, and bar_padding. |
| width |
diff --git a/centrallix-doc/Widgets/table.html b/centrallix-doc/Widgets/table.html
index d0538544b..a3361828d 100644
--- a/centrallix-doc/Widgets/table.html
+++ b/centrallix-doc/Widgets/table.html
@@ -329,7 +329,7 @@ Child Properties:
| type |
string |
-The type of the column: "text", "check", or "image". "text" is a normal column, and displays the textual value of the data element. "check" displays a checkmark if the data is non-zero (integers) or for strings if the value is non-empty and not "N" or "No". "image" displays the image referred to by the pathname contained in the data value. |
+The type of the column: "text", "check", "image", or "progress". "text" is a normal column, and displays the textual value of the data element. "check" displays a checkmark if the data is non-zero (integers) or for strings if the value is non-empty and not "N" or "No". "image" displays the image referred to by the pathname contained in the data value. "progress" displays a progress bar, with additional fields such as bar_color, bar_textcollor, and bar_padding. |
| width |
diff --git a/centrallix-doc/Widgets/widgets.xml b/centrallix-doc/Widgets/widgets.xml
index 6d283f5f2..c2c2e5eb5 100644
--- a/centrallix-doc/Widgets/widgets.xml
+++ b/centrallix-doc/Widgets/widgets.xml
@@ -3735,7 +3735,7 @@ myTabControl "widget/tab"
The title of the column to be displayed in the header row.
- The type of the column: "text", "check", or "image". "text" is a normal column, and displays the textual value of the data element. "check" displays a checkmark if the data is non-zero (integers) or for strings if the value is non-empty and not "N" or "No". "image" displays the image referred to by the pathname contained in the data value.
+ The type of the column: "text", "check", "image", or "progress". "text" is a normal column, and displays the textual value of the data element. "check" displays a checkmark if the data is non-zero (integers) or for strings if the value is non-empty and not "N" or "No". "image" displays the image referred to by the pathname contained in the data value. "progress" displays a progress bar, with additional fields such as bar_color, bar_textcollor, and bar_padding.
width of the column.
diff --git a/centrallix-lib/Makefile.in b/centrallix-lib/Makefile.in
index bffc2f030..9e58c4a09 100644
--- a/centrallix-lib/Makefile.in
+++ b/centrallix-lib/Makefile.in
@@ -6,7 +6,7 @@
## This library is made available under the GNU Lesser General Public
## License, version 2.1.
##
-## Copyright (C) 1998-2001 LightSys Technology Services, Inc.
+## Copyright (C) 1998-2026 LightSys Technology Services, Inc.
##
@SET_MAKE@
@@ -59,14 +59,14 @@ LIBS = @LIBS@ -lm
##
PROFILE=@PROFILE@
COVERAGE=@COVERAGE@
-CFLAGS=@CFLAGS@ @DEFS@ -Iinclude -DCXLIB_INTERNAL -DNM_USE_SYSMALLOC -Wall $(PROFILE) $(COVERAGE) -g
+CFLAGS=@CFLAGS@ @DEFS@ -Iinclude -DCXLIB_INTERNAL -DNM_USE_SYSMALLOC -Wall $(PROFILE) $(COVERAGE) -g -lm
MTCFLAGS=@CFLAGS@ @DEFS@ -Iinclude -DCXLIB_INTERNAL -DNM_USE_SYSMALLOC -Wall $(PROFILE) $(COVERAGE) -g -O0
TCFLAGS=$(patsubst -DNDEBUG,,$(CFLAGS))
-XSTATICFILES=mtask.o mtlexer.o memstr.o xarray.o xhash.o xstring.o mtsession.o newmalloc.o xhashqueue.o bdqs_transport.o xhandle.o xringqueue.o cxsec.o smmalloc.o qprintf.o strtcpy.o util.o
+XSTATICFILES=mtask.o mtlexer.o memstr.o xarray.o xhash.o xstring.o mtsession.o newmalloc.o xhashqueue.o bdqs_transport.o xhandle.o xringqueue.o cxsec.o smmalloc.o clusters.o qprintf.o strtcpy.o util.o timer.o check.o range.o
STATICFILES=$(patsubst %,src/%,$(XSTATICFILES))
-XDYNAMICFILES=mtask.lo mtlexer.lo memstr.lo xarray.lo xhash.lo xstring.lo mtsession.lo newmalloc.lo xhashqueue.lo bdqs_transport.lo xhandle.lo xringqueue.lo cxsec.lo smmalloc.lo qprintf.lo strtcpy.lo util.lo
+XDYNAMICFILES=mtask.lo mtlexer.lo memstr.lo xarray.lo xhash.lo xstring.lo mtsession.lo newmalloc.lo xhashqueue.lo bdqs_transport.lo xhandle.lo xringqueue.lo cxsec.lo smmalloc.lo clusters.lo qprintf.lo strtcpy.lo util.lo timer.lo check.lo range.lo
DYNAMICFILES=$(patsubst %,src/%,$(XDYNAMICFILES))
INCLUDEFILES:=$(wildcard include/*.h)
@@ -76,6 +76,19 @@ INCLUDEFILES:=$(patsubst include/cxlibconfig-all.h,,$(INCLUDEFILES))
TESTFILES:=$(wildcard tests/test_$(TONLY)*.c)
TESTPROGS:=$(patsubst %.c,%.bin,$(TESTFILES))
+## Valgrind setup for "make valtest". Override VALGRINDFLAGS to change the
+## checks performed, and set VALGRINDLOG to a directory to get one valgrind
+## log per test instead of the errors being printed with the test results.
+VALGRIND?=valgrind
+VALGRINDFLAGS?=-q
+VALGRINDLOG?=
+
+## How many tests "make test" and "make valtest" run at once (1 for one at a
+## time). Each test is a single-threaded process, so this scales with the core
+## count; the driver's lockup timeout is wall clock, so going well past the
+## core count risks reporting LOCKUP for a test that was only waiting for one.
+TESTJOBS?=$(shell nproc 2>/dev/null || echo 1)
+
.SUFFIXES: .c .o .lo
.PHONY: all
@@ -160,26 +173,86 @@ install: all
%.bin: %.c tests/t_driver.c libCentrallix.a
$(CC) $(TCFLAGS) tests/t_driver.c $< libCentrallix.a -o $@ ${LIBS}
+## Runs the selected tests $(TESTJOBS) at a time. Output is captured per test
+## rather than written straight to the terminal, then printed in listing order
+## as each test finishes, so parallel tests cannot interleave.
+##
+## TESTCMD runs once per test with the binary as "$$1" and its capture file as
+## "$$2". It writes to "$$2.part" and renames that to "$$2" when done so that
+## the rename is what marks the test as finished, then it exits with the test's
+## status. The driver exits 0 even when reporting FAIL or CRASH (see
+## tests/t_driver.c), so a nonzero status means the test did not complete
+## normally and must fail the target; only valgrind's --error-exitcode is
+## dropped. A test whose output never appears is reported and also fails.
+RUN_TESTS = \
+ RESULTS=$$(mktemp -d) || exit 1; \
+ trap 'rm -rf "$$RESULTS"' EXIT INT TERM; \
+ N=0; \
+ for PROG in $$( ls $(TESTPROGS) ); do \
+ case "$$PROG" in tests/test_$$TONLY*) \
+ N=$$((N+1)); \
+ printf '%s\n%s/%04d.out\n' "$$PROG" "$$RESULTS" "$$N";; \
+ esac; \
+ done > "$$RESULTS/list"; \
+ if [ "$$N" -eq 0 ]; then exit 0; fi; \
+ xargs -r -n 2 -P $(TESTJOBS) sh -c "$$TESTCMD" sh \
+ < "$$RESULTS/list" 2> "$$RESULTS/stderr" & \
+ XARGS=$$!; \
+ RC=0; \
+ I=0; \
+ while [ "$$I" -lt "$$N" ]; do \
+ I=$$((I+1)); \
+ OUT=$$(printf '%s/%04d.out' "$$RESULTS" "$$I"); \
+ while [ ! -e "$$OUT" ]; do \
+ kill -0 "$$XARGS" 2>/dev/null || break; \
+ sleep 0.05; \
+ done; \
+ if [ -e "$$OUT" ]; then \
+ cat "$$OUT"; \
+ else \
+ printf '%-62.62s FAIL (produced no result)\n' \
+ "$$(sed -n "$$((2 * I - 1))p" "$$RESULTS/list")"; \
+ RC=1; \
+ fi; \
+ done; \
+ wait "$$XARGS" || RC=$$?; \
+ cat "$$RESULTS/stderr" >&2; \
+ exit "$$RC"
+
.PHONY: test
test: $(TESTPROGS)
@printf "%-62.62s %s\n" "Test Name" "Stat Ops/sec"
@printf "%-62.62s %s\n" "----------------------------------------------------------------" "--------------"
- @for PROG in $$( ls $(TESTPROGS) ); do \
- if [ "$$TONLY" = "" -o "$${PROG#tests/test_$$TONLY}" != "$$PROG" ]; then \
- $$PROG; \
- fi \
- done
+ @TESTCMD='"$$1" > "$$2.part" 2>&1; \
+ S=$$?; \
+ mv "$$2.part" "$$2"; \
+ exit $$S'; \
+ $(RUN_TESTS)
.PHONY: valtest
valtest: $(TESTPROGS)
@printf "Running Regression Tests Under Valgrind...\n"
@printf "%-62.62s %s\n" "Test Name" "Stat Ops/sec"
@printf "%-62.62s %s\n" "----------------------------------------------------------------" "--------------"
- @for PROG in $$( ls $(TESTPROGS) ); do \
- if [ "$$TONLY" = "" -o "$${PROG#tests/test_$$TONLY}" != "$$PROG" ]; then \
- valgrind -q $$PROG; \
- fi \
- done
+ @if [ "$(VALGRINDLOG)" != "" ]; then mkdir -p "$(VALGRINDLOG)"; fi
+ @if [ "$(VALGRINDLOG)" = "" ]; then \
+ TESTCMD='$(VALGRIND) $(VALGRINDFLAGS) "$$1" > "$$2.part" 2>&1; \
+ S=$$?; \
+ mv "$$2.part" "$$2"; \
+ exit $$S'; \
+ else \
+ TESTCMD='NAME=$${1##*/}; \
+ LOG="$(VALGRINDLOG)/$${NAME%.bin}.log"; \
+ $(VALGRIND) $(VALGRINDFLAGS) --error-exitcode=125 --log-file="$$LOG" "$$1" > "$$2.part" 2>&1; \
+ S=$$?; \
+ if [ "$$S" = "125" ]; then \
+ printf " valgrind: errors reported -- %s\n" "$$LOG" >> "$$2.part"; \
+ S=0; \
+ fi; \
+ mv "$$2.part" "$$2"; \
+ exit $$S'; \
+ fi; \
+ $(RUN_TESTS)
.PHONY: cov-clean
cov-clean:
diff --git a/centrallix-lib/configure b/centrallix-lib/configure
index 5abf8e3cd..d8dc16fe4 100755
--- a/centrallix-lib/configure
+++ b/centrallix-lib/configure
@@ -3355,6 +3355,45 @@ _ACEOF
fi
+{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for main in -lm" >&5
+$as_echo_n "checking for main in -lm... " >&6; }
+if ${ac_cv_lib_m_main+:} false; then :
+ $as_echo_n "(cached) " >&6
+else
+ ac_check_lib_save_LIBS=$LIBS
+LIBS="-lm $LIBS"
+cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h. */
+
+
+int
+main ()
+{
+return main ();
+ ;
+ return 0;
+}
+_ACEOF
+if ac_fn_c_try_link "$LINENO"; then :
+ ac_cv_lib_m_main=yes
+else
+ ac_cv_lib_m_main=no
+fi
+rm -f core conftest.err conftest.$ac_objext \
+ conftest$ac_exeext conftest.$ac_ext
+LIBS=$ac_check_lib_save_LIBS
+fi
+{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_m_main" >&5
+$as_echo "$ac_cv_lib_m_main" >&6; }
+if test "x$ac_cv_lib_m_main" = xyes; then :
+ cat >>confdefs.h <<_ACEOF
+#define HAVE_LIBM 1
+_ACEOF
+
+ LIBS="-lm $LIBS"
+
+fi
+
ac_ext=c
ac_cpp='$CPP $CPPFLAGS'
diff --git a/centrallix-lib/configure.ac b/centrallix-lib/configure.ac
index 4b6308bd4..e5e16490a 100644
--- a/centrallix-lib/configure.ac
+++ b/centrallix-lib/configure.ac
@@ -18,6 +18,7 @@ dnl Checks for libraries.
dnl Replace `main' with a function in -lz:
AC_CHECK_LIB(z, main)
AC_CHECK_LIB(crypt, main)
+AC_CHECK_LIB(m, main)
dnl Checks for header files.
AC_HEADER_STDC
diff --git a/centrallix-lib/include/check.h b/centrallix-lib/include/check.h
new file mode 100644
index 000000000..838b46364
--- /dev/null
+++ b/centrallix-lib/include/check.h
@@ -0,0 +1,100 @@
+#ifndef CHECK_H
+#define CHECK_H
+
+/************************************************************************/
+/* Centrallix Application Server System */
+/* Centrallix Base Library */
+/* */
+/* Copyright (C) 1998-2026 LightSys Technology Services, Inc. */
+/* */
+/* You may use these files and this library under the terms of the */
+/* GNU Lesser General Public License, Version 2.1, contained in the */
+/* included file "COPYING". */
+/* */
+/* Module: check.c, check.h */
+/* Author: Israel Fuller */
+/* Date: October 13, 2025 */
+/* Description: A utility to help with error checking on function */
+/* return values, especially for library functions. */
+/************************************************************************/
+
+#include
+#include
+#include
+
+#include "expect.h"
+
+/** File name macro, expanding functionality like __FILE__ and __LINE__. **/
+#define __FILENAME__ \
+ ({ \
+ const char* last_directory = strrchr(__FILE__, '/'); \
+ ((last_directory != NULL) ? last_directory + 1 : __FILE__); \
+ })
+
+/** Internal error printer (forward declaration). **/
+void printErrInternal(const int error_code, const char* c_str, const char* file_name, const int line_number);
+
+#define printErr(error_code, c_str) printErrInternal(error_code, (c_str), __FILE__, __LINE__)
+#define printFail(c_str) do { errno = 0; printErr(-1, (c_str)); } while (0)
+
+/*** Ensures that developer diagnostics are printed if the result of the
+ *** passed function call is not zero. Not intended for user errors.
+ ***
+ *** @param result The expression to check. The text of this expression is
+ *** included in the error message if an error occurs.
+ *** @returns The result of the checked expression.
+ ***/
+#define check(result) \
+ ({ \
+ errno = 0; /* Reset errno to prevent confusion. */ \
+ int _r = (result); \
+ if (UNLIKELY(_r != 0)) printErr(_r, #result" failed"); \
+ _r; \
+ })
+
+/*** Ensures that developer diagnostics are printed if the result of the
+ *** passed function call is negative. Not intended for user errors.
+ ***
+ *** @param result The expression to check. The text of this expression is
+ *** included in the error message if an error occurs.
+ *** @returns The result of the checked expression.
+ ***/
+#define checkPos(result) \
+ ({ \
+ errno = 0; /* Reset errno to prevent confusion. */ \
+ int _r = (result); \
+ if (UNLIKELY(_r < 0)) printErr(_r, #result" failed"); \
+ _r; \
+ })
+
+/*** Ensures that developer diagnostics are printed if the result of the
+ *** passed function call is a NAN double. Not intended for user errors.
+ ***
+ *** @param result The expression to check. The text of this expression is
+ *** included in the error message if an error occurs.
+ *** @returns The result of the checked expression.
+ ***/
+#define checkDouble(result) \
+ ({ \
+ errno = 0; /* Reset errno to prevent confusion. */ \
+ double _r = (result); \
+ if (UNLIKELY(isnan(_r))) printErr(-1, #result" failed"); \
+ _r; \
+ })
+
+/*** Ensures that developer diagnostics are printed if the result of the
+ *** passed function call is a NULL pointer. Not intended for user errors.
+ ***
+ *** @param result The expression to check. The text of this expression is
+ *** included in the error message if an error occurs.
+ *** @returns The result of the checked expression.
+ ***/
+#define checkPtr(result) \
+ ({ \
+ errno = 0; /* Reset errno to prevent confusion. */ \
+ void* _r = (result); \
+ if (UNLIKELY(_r == NULL)) printErr(-1, #result" failed"); \
+ _r; \
+ })
+
+#endif /* CHECK_H */
diff --git a/centrallix-lib/include/clusters.h b/centrallix-lib/include/clusters.h
new file mode 100644
index 000000000..6d366d9f6
--- /dev/null
+++ b/centrallix-lib/include/clusters.h
@@ -0,0 +1,161 @@
+#ifndef CLUSTERS_H
+#define CLUSTERS_H
+
+/************************************************************************/
+/* Centrallix Application Server System */
+/* Centrallix Core */
+/* */
+/* Copyright (C) 1998-2026 LightSys Technology Services, Inc. */
+/* */
+/* This program is free software; you can redistribute it and/or modify */
+/* it under the terms of the GNU General Public License as published by */
+/* the Free Software Foundation; either version 2 of the License, or */
+/* (at your option) any later version. */
+/* */
+/* This program is distributed in the hope that it will be useful, */
+/* but WITHOUT ANY WARRANTY; without even the implied warranty of */
+/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */
+/* GNU General Public License for more details. */
+/* */
+/* You should have received a copy of the GNU General Public License */
+/* along with this program; if not, write to the Free Software */
+/* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA */
+/* 02111-1307 USA */
+/* */
+/* A copy of the GNU General Public License has been included in this */
+/* distribution in the file "COPYING". */
+/* */
+/* Module: clusters.c, clusters.h */
+/* Author: Israel Fuller */
+/* Creation: September 29, 2025 */
+/* Description Clustering library used to cluster and search data with */
+/* cosine or Levenshtein (aka. edit distance) similarity */
+/* measures. Used by the "clustering driver". */
+/* For more information on how to use this library, see */
+/* string_similarity.md in the centrallix-sysdoc folder. */
+/************************************************************************/
+
+#include
+#include
+
+#ifdef CXLIB_INTERNAL
+#include "xarray.h"
+#else
+#include "cxlib/xarray.h"
+#endif
+
+/** This file has additional documentation in string_similarity.md. **/
+
+
+/*** This value defines the number of dimensions used for a sparse
+ *** vector. The higher the number, the fewer collisions will be
+ *** encountered when using these vectors for cosine comparisons.
+ *** This is also called the vector table size, if viewing the
+ *** vector as a hash table of character pairs.
+ ***
+ *** 2147483647 is the signed int max, and is also a prime number.
+ *** Using this value ensures that the longest run of 0s will not
+ *** cause an int underflow with the current encoding scheme.
+ ***
+ *** Unfortunately, we can't use a number this large yet because
+ *** kmeans algorithm creates densely allocated centroids with
+ *** `CA_NUM_DIMS` dimensions, so a large number causes it to fail.
+ *** Thus, we use 251 as the largest prime number less than 256,
+ *** giving us a decent balance between collision reduction and
+ *** kmeans centroid performance/memory overhead.
+ ***/
+#define CA_NUM_DIMS 251
+
+/*** The character used to create a pair with the first and last characters
+ *** of a string. Currently set to 96, the character just before 'a' (97)
+ *** in the ASCII table.
+ ***/
+#define CA_BOUNDARY_CHAR ((unsigned char)('a' - 1))
+
+/** Types. **/
+typedef int* pVector; /* Sparse vector. */
+typedef double* pCentroid; /* Dense centroid. */
+#define CENTROID_SIZE (CA_NUM_DIMS * sizeof(double))
+
+/*** Information about detected matching pairs.
+ ***
+ *** @param i The index into the provided data for the first element of the pair.
+ *** @param j The index into the provided data for the second element of the pair.
+ *** @param similarity A number from 0 to 1, from a similarity function, showing
+ *** how similar the pairs are.
+ ***/
+typedef struct
+ {
+ unsigned int i, j;
+ double similarity;
+ }
+ Pair, *pPair;
+
+
+/** Edit distance function. **/
+int ca_edit_dist(const char* str1, const char* str2, const size_t str1_length, const size_t str2_length);
+
+/** Vector functions. **/
+pVector ca_build_vector(const char* str);
+unsigned int ca_sparse_len(const pVector vector);
+void ca_print_vector(const pVector vector);
+void ca_free_vector(pVector sparse_vector);
+
+/** k-means function. **/
+int ca_kmeans(
+ pVector* vectors,
+ const unsigned int num_vectors,
+ const unsigned int num_clusters,
+ const unsigned int max_iter,
+ const double min_improvement,
+ unsigned int* labels,
+ double* vector_sims,
+ bool auto_seed);
+
+/** Vector helper macros. **/
+#define ca_is_empty(vector) (vector[0] == -CA_NUM_DIMS)
+/*** Note: Given that CA_NUM_DIMS == 251, ca_build_vector("") will give the
+ *** vector we check for in the ca_has_no_pairs() macro, [-172, 11, -78],
+ *** which has a single pair of boundary characters.
+ *** If CA_NUM_DIMS is modified, this macro will need to be updated, hence the
+ *** compiler directive causing it to be undefined in this case, likely leading
+ *** to a lot of compiler or linker issues to remind the developer about this.
+ ***/
+#if CA_NUM_DIMS == 251
+#define ca_has_no_pairs(vector) \
+ ({ \
+ __typeof__ (vector) _v = (vector); \
+ _v[0] == -172 && _v[1] == 11 && _v[2] == -78; \
+ })
+#endif
+
+/** Comparison functions (see ca_search()). **/
+double ca_cos_compare(void* v1, void* v2);
+double ca_lev_compare(void* str1, void* str2);
+bool ca_eql(pVector v1, pVector v2);
+
+/** Similarity search functions. **/
+void* ca_most_similar(
+ void* target,
+ void** data,
+ const unsigned int num_data,
+ double (*similarity)(void*, void*),
+ const double threshold);
+pXArray ca_sliding_search(
+ void** data,
+ const unsigned int num_data,
+ const unsigned int window_size,
+ double (*similarity)(void*, void*),
+ const double threshold,
+ pXArray maybe_pairs);
+pXArray ca_complete_search(
+ void** data,
+ const unsigned int num_data,
+ double (*similarity)(void*, void*),
+ const double threshold,
+ pXArray maybe_pairs);
+
+/** Module management functions. **/
+void ca_init(void);
+
+#endif /* End of .h file. */
diff --git a/centrallix-lib/include/cxlibconfig-all.h.in b/centrallix-lib/include/cxlibconfig-all.h.in
index 3cc026a96..833adf5a5 100644
--- a/centrallix-lib/include/cxlibconfig-all.h.in
+++ b/centrallix-lib/include/cxlibconfig-all.h.in
@@ -21,6 +21,9 @@
/* Define to 1 if you have the `crypt' library (-lcrypt). */
#undef HAVE_LIBCRYPT
+/* Define to 1 if you have the `m' library (-lm). */
+#undef HAVE_LIBM
+
/* Define to 1 if you have the `z' library (-lz). */
#undef HAVE_LIBZ
diff --git a/centrallix-lib/include/magic.h b/centrallix-lib/include/magic.h
index 812483090..5c80d3cb1 100644
--- a/centrallix-lib/include/magic.h
+++ b/centrallix-lib/include/magic.h
@@ -5,7 +5,7 @@
/* Centrallix Application Server System */
/* Centrallix Base Library */
/* */
-/* Copyright (C) 1999-2001 LightSys Technology Services, Inc. */
+/* Copyright (C) 1999-2026 LightSys Technology Services, Inc. */
/* */
/* You may use these files and this library under the terms of the */
/* GNU Lesser General Public License, Version 2.1, contained in the */
@@ -25,11 +25,46 @@
/************************************************************************/
-
#ifdef DBMAGIC
-
-#define ASSERTMAGIC(x,y) ((!(x) || (((pMagicHdr)(x))->Magic == (y)))?0:(printf("LS-PANIC: Magic number assertion failed, unexpected %X != %X for %8.8lX\n",(x)?(((pMagicHdr)(x))->Magic):(0xEE1EE100),(y),(long)(x)),(*((int*)(8)) = *((int*)(0)))))
-#define ASSERTNOTMAGIC(x,y) ((!(x) || (((pMagicHdr)(x))->Magic != (y)))?0:(printf("LS-PANIC: Magic number assertion failed, unexpected %X\n",(y)),(*((int*)(8)) = *((int*)(0)))))
+#include
+#include
+
+#include "expect.h"
+
+/** Define the value used as a placeholder to mark null data. **/
+#define MGK_NULL_MARK 0xEE1EE100
+
+#define ASSERTMAGIC(data, expect) \
+ ({ \
+ const pMagicHdr _data = (pMagicHdr)(data); \
+ const Magic_t _expect = (expect); \
+ const Magic_t _actual = (_data == NULL) ? MGK_NULL_MARK : _data->Magic; \
+ if (UNLIKELY(_data != NULL && _actual != _expect)) \
+ { \
+ fprintf(stderr, \
+ "%s:%d: Magic assertion failed, unexpected %X != %X for %8.8lX.\n", \
+ __FILE__, __LINE__, _actual, _expect, (long)_data \
+ ); \
+ abort(); \
+ } \
+ 0; \
+ })
+
+#define ASSERTNOTMAGIC(data, expect) \
+ ({ \
+ const pMagicHdr _data = (pMagicHdr)(data); \
+ const Magic_t _expect = (expect); \
+ const Magic_t _actual = (_data == NULL) ? MGK_NULL_MARK : _data->Magic; \
+ if (UNLIKELY(_data != NULL && _actual == _expect)) \
+ { \
+ fprintf(stderr, \
+ "%s:%d: Magic assertion failed, unexpected %X.\n", \
+ __FILE__, __LINE__, _expect \
+ ); \
+ abort(); \
+ } \
+ 0; \
+ })
#else /* defined DBMAGIC */
@@ -38,9 +73,9 @@
#endif /* defined DBMAGIC */
-#define ISMAGIC(x,y) (((pMagicHdr)(x))->Magic == (y))
-#define ISNTMAGIC(x,y) (((pMagicHdr)(x))->Magic != (y))
-#define SETMAGIC(x,y) (((pMagicHdr)(x))->Magic = (y))
+#define ISMAGIC(data, expect) (((pMagicHdr)(data))->Magic == (expect))
+#define ISNTMAGIC(data, expect) (((pMagicHdr)(data))->Magic != (expect))
+#define SETMAGIC(data, expect) (((pMagicHdr)(data))->Magic = (expect))
typedef int Magic_t;
@@ -49,7 +84,7 @@ typedef struct
Magic_t Magic;
}
MagicHdr, *pMagicHdr;
-
+
#define MGK_FILE 0x12340001 /* mtask.h::File */
#define MGK_OBJECT 0x12340102 /* obj.h::Object */
#define MGK_OBJQUERY 0x1234015a /* obj.h::ObjQuery */
@@ -81,4 +116,12 @@ typedef struct
#define MGK_SMREGION 0x1200345c /* smmalloc.h::SmRegion */
#define MGK_SMBLOCK 0x1200349a /* smmalloc_private.h::SmBlock */
+#define MGK_CL_SOURCE_DATA 0x12340c19 /* objdrv_cluster.c::SourceData */
+#define MGK_CL_CLUSTER 0x12340c28 /* objdrv_cluster.c::Cluster */
+#define MGK_CL_CLUSTER_DATA 0x12340c37 /* objdrv_cluster.c::ClusterData */
+#define MGK_CL_SEARCH_DATA 0x12340c46 /* objdrv_cluster.c::SearchData */
+#define MGK_CL_NODE_DATA 0x12340c55 /* objdrv_cluster.c::NodeData */
+#define MGK_CL_DRIVER_DATA 0x12340c64 /* objdrv_cluster.c::DriverData */
+#define MGK_CL_QUERY_DATA 0x12340c73 /* objdrv_cluster.c::QueryData */
+
#endif /* not defined _MAGIC_H */
diff --git a/centrallix-lib/include/mtsession.h b/centrallix-lib/include/mtsession.h
index 29adaff59..a446ed517 100644
--- a/centrallix-lib/include/mtsession.h
+++ b/centrallix-lib/include/mtsession.h
@@ -5,7 +5,7 @@
/* Centrallix Application Server System */
/* Centrallix Base Library */
/* */
-/* Copyright (C) 1998-2001 LightSys Technology Services, Inc. */
+/* Copyright (C) 1998-2026 LightSys Technology Services, Inc. */
/* */
/* You may use these files and this library under the terms of the */
/* GNU Lesser General Public License, Version 2.1, contained in the */
@@ -32,6 +32,9 @@
#include "cxlib/xhash.h"
#endif
+#include
+#include
+
/** optimum salt size for mssGenCred() **/
#define MSS_SALT_SIZE 4
@@ -81,8 +84,11 @@ void* mssGetParam(char* paramname);
/** Error handling functions **/
int mssLog(int level, char* msg);
-int mssError(int clr, char* module, char* message, ...);
-int mssErrorErrno(int clr, char* module, char* message, ...);
+void mssError_internal(int clr, char* module, char* file, int line, char* message, ...);
+#define mssError(clear, module, message, ...) \
+ mssError_internal(clear, module, __FILE__, __LINE__, message, ##__VA_ARGS__)
+#define mssErrorErrno(clear, module, message, ...) \
+ mssError_internal(clear, module, __FILE__, __LINE__, message " (%s)", ##__VA_ARGS__, strerror(errno))
int mssClearError();
int mssPrintError(pFile fd);
int mssStringError(pXString str);
diff --git a/centrallix-lib/include/newmalloc.h b/centrallix-lib/include/newmalloc.h
index 6022e72e3..210cd9195 100644
--- a/centrallix-lib/include/newmalloc.h
+++ b/centrallix-lib/include/newmalloc.h
@@ -65,22 +65,22 @@ extern int blksiz[MAX_BLOCKS];
#endif
extern pOverlay lists[MAX_SIZE+1];
-void nmInitialize();
+void nmInitialize(void);
void nmSetErrFunction(int (*error_fn)());
-void nmClear();
-void nmCheckAll(); // checks for buffer overflows
+void nmClear(void);
+void nmCheckAll(void); // checks for buffer overflows
void* nmMalloc(int size);
void nmFree(void* ptr,int size);
-void nmStats();
+void nmStats(void);
void nmRegister(int size,char* name);
-void nmDebug();
-void nmDeltas();
+void nmDebug(void);
+void nmDeltas(void);
void* nmSysMalloc(int size);
void nmSysFree(void* ptr);
void* nmSysRealloc(void* ptr, int newsize);
char* nmSysStrdup(const char* ptr);
-void nmEnableTagging();
+void nmEnableTagging(void);
void nmRegisterTagID(int tag_id, char* name);
void nmSetTag(void* ptr, int tag_id, void* tag);
void* nmGetTag(void* ptr, int tag_id);
diff --git a/centrallix-lib/include/range.h b/centrallix-lib/include/range.h
new file mode 100644
index 000000000..30a38a287
--- /dev/null
+++ b/centrallix-lib/include/range.h
@@ -0,0 +1,68 @@
+#ifndef RANGE_H
+#define RANGE_H
+
+/************************************************************************/
+/* Centrallix Application Server System */
+/* Centrallix Base Library */
+/* */
+/* Copyright (C) 1998-2026 LightSys Technology Services, Inc. */
+/* */
+/* You may use these files and this library under the terms of the */
+/* GNU Lesser General Public License, Version 2.1, contained in the */
+/* included file "COPYING". */
+/* */
+/* Module: range.c, range.h */
+/* Author: Israel Fuller */
+/* Date: October 13, 2025 */
+/* Description: Adds some useful numerical range functions/macros that */
+/* C does not provide by default. */
+/************************************************************************/
+
+/** Rounds a double value to the given number of decimal places. **/
+double roundTo(double value, int decimals);
+
+#ifndef min
+/*** Returns the smaller of two values.
+ ***
+ *** @param a The first value.
+ *** @param b The second value.
+ *** @return The smaller of the two values.
+ ***/
+#define min(a, b) \
+ ({ \
+ __typeof__ (a) _a = (a); \
+ __typeof__ (b) _b = (b); \
+ (_a < _b) ? _a : _b; \
+ })
+#endif
+
+#ifndef max
+/*** Returns the larger of two values.
+ ***
+ *** @param a The first value.
+ *** @param b The second value.
+ *** @return The larger of the two values.
+ ***/
+#define max(a, b) \
+ ({ \
+ __typeof__ (a) _a = (a); \
+ __typeof__ (b) _b = (b); \
+ (_a > _b) ? _a : _b; \
+ })
+#endif
+
+#ifndef clamp
+/*** Clamps the value between a minimum and maximum bound.
+ ***
+ *** If minimum > maximum, always returns maximum.
+ ***
+ *** @param minimum The smallest allowed value.
+ *** @param value The value to clamp.
+ *** @param maximum The largest allowed value.
+ *** @return The value clamped within the range [minimum, maximum].
+ ***/
+#define clamp(minimum, value, maximum) \
+ max(minimum, min(value, maximum))
+#endif
+
+#endif /* RANGE_H */
diff --git a/centrallix-lib/include/strtcpy.h b/centrallix-lib/include/strtcpy.h
index 1db6b9c8a..9a1c289d3 100644
--- a/centrallix-lib/include/strtcpy.h
+++ b/centrallix-lib/include/strtcpy.h
@@ -2,13 +2,14 @@
#define _STRTCPY_H
#include
+#include
#include
/************************************************************************/
/* Centrallix Application Server System */
/* Centrallix Base Library */
/* */
-/* Copyright (C) 1998-2006 LightSys Technology Services, Inc. */
+/* Copyright (C) 1998-2026 LightSys Technology Services, Inc. */
/* */
/* You may use these files and this library under the terms of the */
/* GNU Lesser General Public License, Version 2.1, contained in the */
@@ -18,13 +19,14 @@
/* Author: Greg Beeley (GRB) */
/* Date: April 14th, 2006 */
/* */
-/* Description: Provides strtcpy(), a Truncating strcpy(), which */
-/* both respects the bounds of the destination and makes */
-/* sure the result is null-terminated. */
+/* Description: Provides truncating string functions, which respect the */
+/* bounds of the destination and ensure null-termination. */
/************************************************************************/
int strtcpy(char* dst, const char* src, size_t dstlen);
int strtcat(char* dst, const char* src, size_t dstlen);
+int strtcatf(char* dst, size_t dstlen, size_t* pos, const char* fmt, ...);
+int strtcatf_va(char* dst, size_t dstlen, size_t* pos, const char* fmt, va_list ap);
#endif /* not defined _STRTCPY_H */
diff --git a/centrallix-lib/include/timer.h b/centrallix-lib/include/timer.h
new file mode 100644
index 000000000..00c992039
--- /dev/null
+++ b/centrallix-lib/include/timer.h
@@ -0,0 +1,48 @@
+#ifndef TIMER_H
+#define TIMER_H
+
+/************************************************************************/
+/* Centrallix Application Server System */
+/* Centrallix Base Library */
+/* */
+/* Copyright (C) 1998-2026 LightSys Technology Services, Inc. */
+/* */
+/* You may use these files and this library under the terms of the */
+/* GNU Lesser General Public License, Version 2.1, contained in the */
+/* included file "COPYING". */
+/* */
+/* Module: timer.c, timer.h */
+/* Author: Israel Fuller */
+/* Date: October 13, 2025 */
+/* Description: A simple timer utility, intended for benchmarking code */
+/* performance in wall time. */
+/************************************************************************/
+
+typedef struct
+ {
+ double start, total;
+ }
+ Timer, *pTimer;
+
+pTimer timerInit(pTimer timer);
+pTimer timerNew(void);
+pTimer timerStart(pTimer timer);
+pTimer timerStop(pTimer timer);
+double timerGet(pTimer timer);
+pTimer timerReset(pTimer timer);
+void timerDeInit(pTimer timer);
+void timerFree(pTimer timer);
+
+/*** Debug function for quickly benchmarking the speed of C code. Do not use
+ *** this function in production code: It breaks compiler and mssError() line
+ *** numbers and is generally bad style.
+ ***/
+#define timerBenchmark(timer, c_code) \
+ { \
+ pTimer _timer = (timer); \
+ timerStart(_timer); \
+ { c_code }; \
+ timerStop(_timer); \
+ }
+
+#endif /* TIMER_H */
diff --git a/centrallix-lib/include/util.h b/centrallix-lib/include/util.h
index df4ba0d58..8813427b0 100644
--- a/centrallix-lib/include/util.h
+++ b/centrallix-lib/include/util.h
@@ -2,33 +2,40 @@
#define UTILITY_H
/************************************************************************/
-/* Centrallix Application Server System */
+/* Centrallix Application Server System */
/* Centrallix Base Library */
/* */
-/* Copyright (C) 1998-2011 LightSys Technology Services, Inc. */
+/* Copyright (C) 1998-2026 LightSys Technology Services, Inc. */
/* */
/* You may use these files and this library under the terms of the */
/* GNU Lesser General Public License, Version 2.1, contained in the */
/* included file "COPYING". */
/* */
-/* Module: (util.c,.h) */
-/* Author: Micah Shennum */
-/* Date: May 26, 2011 */
-/* Description: Collection of utilities */
+/* Module: util.c, util.h */
+/* Author: Micah Shennum and Israel Fuller */
+/* Date: May 26, 2011 and October 13, 2025 (respectively) */
+/* Description: Collection of utilities including: */
+/* - Utilities for parsing numbers. */
+/* - snprintBytes() for formatting a byte count. */
+/* - snprintCommasLlu() for formatting large numbers. */
+/* - fprintMem() for printing memory stats. */
/************************************************************************/
+#include
+
#ifdef __cplusplus
extern "C" {
#endif
-
int strtoi(const char *nptr, char **endptr, int base);
unsigned int strtoui(const char *nptr, char **endptr, int base);
+ char* snprintBytes(char* buf, const size_t buf_size, unsigned long bytes);
+ char* snprintCommasLlu(char* buf, size_t buf_size, unsigned long long value);
+ void fprintMem(FILE* out);
#ifdef __cplusplus
}
#endif
#endif /* UTILITY_H */
-
diff --git a/centrallix-lib/include/xarray.h b/centrallix-lib/include/xarray.h
index bcd107e79..7cad0d523 100644
--- a/centrallix-lib/include/xarray.h
+++ b/centrallix-lib/include/xarray.h
@@ -6,7 +6,7 @@
/* Centrallix Application Server System */
/* Centrallix Base Library */
/* */
-/* Copyright (C) 1998-2001 LightSys Technology Services, Inc. */
+/* Copyright (C) 1998-2026 LightSys Technology Services, Inc. */
/* */
/* You may use these files and this library under the terms of the */
/* GNU Lesser General Public License, Version 2.1, contained in the */
@@ -50,6 +50,8 @@ int xaCount(pXArray this);
int xaSetItem(pXArray this, int index, void* item);
int xaInsertBefore(pXArray this, int index, void* item);
int xaInsertAfter(pXArray this, int index, void* item);
+int xaTrim(pXArray this);
+void** xaToArray(pXArray this);
#define CLD(x,y,z) ((x)((y)->Children.Items[(z)]))
diff --git a/centrallix-lib/include/xhash.h b/centrallix-lib/include/xhash.h
index 1b5d8459a..53cc8d892 100644
--- a/centrallix-lib/include/xhash.h
+++ b/centrallix-lib/include/xhash.h
@@ -1,12 +1,11 @@
#ifndef _XHASH_H
#define _XHASH_H
-
/************************************************************************/
/* Centrallix Application Server System */
/* Centrallix Base Library */
/* */
-/* Copyright (C) 1998-2001 LightSys Technology Services, Inc. */
+/* Copyright (C) 1998-2026 LightSys Technology Services, Inc. */
/* */
/* You may use these files and this library under the terms of the */
/* GNU Lesser General Public License, Version 2.1, contained in the */
@@ -55,6 +54,7 @@ int xhAdd(pXHashTable this, char* key, char* data);
int xhRemove(pXHashTable this, char* key);
char* xhLookup(pXHashTable this, char* key);
int xhClear(pXHashTable this, int (*free_fn)(), void* free_arg);
+int xhForEach(pXHashTable this, int (*callback_fn)(pXHashEntry, void*), void* each_arg);
+int xhClearKeySafe(pXHashTable this, void (*free_fn)(pXHashEntry, void*), void* free_arg);
#endif /* _XHASH_H */
-
diff --git a/centrallix-lib/src/check.c b/centrallix-lib/src/check.c
new file mode 100644
index 000000000..eba3cbd9b
--- /dev/null
+++ b/centrallix-lib/src/check.c
@@ -0,0 +1,64 @@
+/************************************************************************/
+/* Centrallix Application Server System */
+/* Centrallix Base Library */
+/* */
+/* Copyright (C) 1998-2026 LightSys Technology Services, Inc. */
+/* */
+/* You may use these files and this library under the terms of the */
+/* GNU Lesser General Public License, Version 2.1, contained in the */
+/* included file "COPYING". */
+/* */
+/* Module: check.c, check.h */
+/* Author: Israel Fuller */
+/* Date: October 13, 2025 */
+/* Description: A utility to help with error checking on function */
+/* return values, especially for library functions. */
+/************************************************************************/
+
+#include
+#include
+
+#include "check.h"
+#include "strtcpy.h"
+
+#define ERR_BUF_SIZE 1024
+
+/*** Function for printing an error when code fails.
+ ***
+ *** @param error_code The error code number returned by a failing C function (or -1 if not applicable).
+ *** @param c_str The C statement/value that failed, usually a function call.
+ *** @param file_name The name of the file in which error occurred.
+ *** @param line_number The line number in the file at which the error occurred.
+ ***/
+void
+printErrInternal(const int error_code, const char* c_str, const char* file_name, const int line_number)
+ {
+ /** Grab errno before any library call of ours can overwrite it. **/
+ const int saved_errno = errno;
+ size_t i = 0;
+ char error_buf[ERR_BUF_SIZE];
+
+ /** Initialize buffer. **/
+ error_buf[0] = '\0';
+
+ /** Create a clear, concise, and descriptive error message. **/
+ strtcatf(error_buf, sizeof(error_buf), &i, "%s:%d: %s", file_name, line_number, c_str);
+
+ /** Fill it out with as much info as we can reasonably find. **/
+ if (error_code != -1)
+ strtcatf(error_buf, sizeof(error_buf), &i, " (error code %d)", error_code);
+ if (saved_errno != 0)
+ strtcatf(error_buf, sizeof(error_buf), &i, ": %s", strerror(saved_errno));
+
+ /** Print the error message. **/
+ if (i == 0)
+ /** Failed to make error message. Fallback to a more basic error. **/
+ fprintf(stderr,
+ "%s:%d: %s. (Failed to build full error message.)\n",
+ file_name, line_number, c_str
+ );
+ else
+ fprintf(stderr, "%s.\n", error_buf);
+
+ return;
+ }
diff --git a/centrallix-lib/src/clusters.c b/centrallix-lib/src/clusters.c
new file mode 100644
index 000000000..a71b8baf9
--- /dev/null
+++ b/centrallix-lib/src/clusters.c
@@ -0,0 +1,1138 @@
+/************************************************************************/
+/* Centrallix Application Server System */
+/* Centrallix Core */
+/* */
+/* Copyright (C) 1998-2026 LightSys Technology Services, Inc. */
+/* */
+/* This program is free software; you can redistribute it and/or modify */
+/* it under the terms of the GNU General Public License as published by */
+/* the Free Software Foundation; either version 2 of the License, or */
+/* (at your option) any later version. */
+/* */
+/* This program is distributed in the hope that it will be useful, */
+/* but WITHOUT ANY WARRANTY; without even the implied warranty of */
+/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */
+/* GNU General Public License for more details. */
+/* */
+/* You should have received a copy of the GNU General Public License */
+/* along with this program; if not, write to the Free Software */
+/* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA */
+/* 02111-1307 USA */
+/* */
+/* A copy of the GNU General Public License has been included in this */
+/* distribution in the file "COPYING". */
+/* */
+/* Module: clusters.c, clusters.h */
+/* Author: Israel Fuller */
+/* Creation: September 29, 2025 */
+/* Description Clustering library used to cluster and search data with */
+/* cosine or Levenshtein (aka. edit distance) similarity */
+/* measures. Used by the "clustering driver". */
+/* For more information on how to use this library, see */
+/* string_similarity.md in the centrallix-sysdoc folder. */
+/************************************************************************/
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "check.h"
+#include "clusters.h"
+#include "expect.h"
+#include "newmalloc.h"
+#include "range.h"
+#include "xarray.h"
+
+/** This file has additional documentation in string_similarity.md. **/
+
+
+/*** Gets the hash for a pair of ASCII characters, represented by unsigned ints.
+ *** Thank you professor John Delano for this hashing algorithm.
+ ***
+ *** @param c1 The first character in the pair.
+ *** @param c2 The second character in the pair.
+ *** @returns The resulting hash.
+ ***/
+static unsigned int
+ca_i_hash_char_pair(const unsigned char c1, const unsigned char c2)
+ {
+ const double sum = (c1 * c1 * c1) + (c2 * c2 * c2);
+ const double scale = ((double)c1 + 1.0) / ((double)c2 + 1.0);
+ const unsigned int hash = (unsigned int)round(sum * scale) - 1u;
+ return hash % CA_NUM_DIMS;
+ }
+
+/*** An internal struct for temporarily storing character pairs while building
+ *** sparse vectors.
+ ***
+ *** @param c1 The first character in the character pair.
+ *** @param c2 The second character in the character pair.
+ *** @param hash The hash for the two characters, calculated by calling the
+ *** ca_i_hash_char_pair() function (above).
+ **/
+typedef struct
+ {
+ unsigned char c1, c2;
+ unsigned int hash;
+ }
+ CharPair, *pCharPair;
+
+/*** Internal function to compare two character pairs, allowing us to sort them
+ *** by hash (ascending).
+ ***
+ *** @param p1 The first pCharPair.
+ *** @param p2 The second pCharPair.
+ *** @returns An int > 0 if p1's hash is larger.
+ *** An int < 0 if p2's hash is larger.
+ *** 0 if p1 and p2 have identical hashes.
+ ***/
+static int
+ca_i_char_pair_cmp(const void *p1, const void *p2)
+ {
+ const CharPair *a = p1, *b = p2;
+ return a->hash - b->hash;
+ }
+
+/*** Builds a sparse vector for cosine compare using the provided string.
+ ***
+ *** Vectors are based on the frequencies of character pairs in the string.
+ *** Insignificant characters like spaces or punctuation are ignored (see
+ *** code for more detail) and all characters are converted to lowercase.
+ *** The boundary character (see `CA_BOUNDARY_CHAR`) is used to make pairs
+ *** on the start and end of strings.
+ ***
+ *** The only supported characters for the passed `char*` are whitespace,
+ *** punctuation, uppercase and lowercase letters, and numbers. Passing
+ *** other characters results in undefined behavior.
+ ***
+ *** The function uses the following modified ASCII table:
+ *** ```csv
+ *** #, char, #, char, #, char
+ *** 97, 'a', 109, 'm', 121, 'y'
+ *** 98, 'b', 110, 'n', 122, 'z'
+ *** 99, 'c', 111, 'o', 123, '0'
+ *** 100, 'd', 112, 'p', 124, '1'
+ *** 101, 'e', 113, 'q', 125, '2'
+ *** 102, 'f', 114, 'r', 126, '3'
+ *** 103, 'g', 115, 's', 127, '4'
+ *** 104, 'h', 116, 't', 128, '5'
+ *** 105, 'i', 117, 'u', 129, '6'
+ *** 106, 'j', 118, 'v', 130, '7'
+ *** 107, 'k', 119, 'w', 131, '8'
+ *** 108, 'l', 120, 'x', 132, '9'
+ *** ```
+ ***
+ *** After hashing each character pair, we add some number from 1 to 13 to the
+ *** corresponding dimension based on the characters to improve performance in
+ *** cases where collisions occur. However, for most names, this process gives
+ *** a lot of zeros and a FEW positive numbers. Thus, we represent this vector
+ *** using a sparse abstraction where an entry of `-n` represents a run of `n`
+ *** consecutive zeros. Consider the following example:
+ ***
+ *** Dense Vector: `[1,0,0,0,3,0]`
+ *** Sparse Representation: `[1,-3,3,-1]`
+ ***
+ *** Using these sparse vectors greatly reduces the required memory and gives
+ *** approximately an x5 boost to performance on a test dataset with short
+ *** strings that rarely approached 32 characters (using 251 dimensions). The
+ *** only cost is more algorithmically complex code.
+ ***
+ *** @param str The string to be divided into pairs and hashed to make the vector.
+ *** @returns The sparse vector built using the hashed character pairs.
+ ***/
+pVector
+ca_build_vector(const char* str)
+ {
+ unsigned char* chars = NULL;
+ CharPair* char_pairs = NULL;
+ pVector sparse_vector = NULL;
+ pVector trimmed_sparse_vector = NULL;
+
+ /** Guard null. **/
+ if (str == NULL) return NULL;
+
+ /** Allocate memory to store the characters. **/
+ unsigned int num_chars = 0u;
+ chars = checkPtr(nmSysMalloc((strlen(str) + 2u) * sizeof(unsigned char)));
+ if (UNLIKELY(chars == NULL)) goto err_free;
+
+ /** Store characters. **/
+ chars[num_chars++] = CA_BOUNDARY_CHAR; /* Starting boundary character. */
+ for (const char* char_ptr = str; *char_ptr != '\0'; char_ptr++)
+ {
+ char maybe_char = *char_ptr;
+ if (maybe_char < 0) fprintf(stderr, "Warning: Unexpected negative char '%c' in string: \"%s\"\n", maybe_char, str);
+ unsigned char c = (unsigned char)maybe_char;
+
+ /** Ignore insignificant characters. **/
+ /** isspace(): space, \n, \v, \f, \r **/
+ /** ispunct(): !"#$%&'()*+,-./:;<=>?@[\]^_{|}~ **/
+ if (c != CA_BOUNDARY_CHAR && (isspace(c) || ispunct(c))) continue;
+
+ /** Shift numbers to the end of the lowercase letters. **/
+ if ('0' <= c && c <= '9') c += 75u;
+
+ /** Store the character. **/
+ chars[num_chars++] = tolower(c);
+ }
+ chars[num_chars++] = CA_BOUNDARY_CHAR; /* Ending boundary character. */
+
+
+ /** Compute character pair hashes. **/
+ char_pairs = checkPtr(nmSysMalloc(num_chars * sizeof(CharPair)));
+ if (UNLIKELY(char_pairs == NULL)) goto err_free;
+ const unsigned int num_pairs = num_chars - 1u;
+ for (unsigned int i = 0u; i < num_pairs; i++)
+ {
+ /** Store characters. **/
+ char_pairs[i].c1 = chars[i];
+ char_pairs[i].c2 = chars[i + 1];
+
+ /** Hash the character pair into an index (dimension). **/
+ /** Note that the passed value should always be between 97 ('a') and 132 ('9'). **/
+ char_pairs[i].hash = ca_i_hash_char_pair(chars[i], chars[i + 1]);
+ }
+
+ /** Free unused memory. **/
+ nmSysFree(chars);
+ chars = NULL;
+
+
+ /** Sort char_pairs by hash value. **/
+ qsort(char_pairs, num_pairs, sizeof(CharPair), ca_i_char_pair_cmp);
+
+
+ /** Allocate space for the sparse vector. **/
+ sparse_vector = checkPtr(nmSysMalloc((num_pairs * 2u + 1u) * sizeof(int)));
+ if (sparse_vector == NULL) goto err_free;
+
+ /** Build the sparse vector from the character pairs. **/
+ unsigned int cur = 0u, dim = 0u;
+ for (unsigned int i = 0u; i < num_pairs;)
+ {
+ unsigned int hash = char_pairs[i].hash;
+
+ /** Proceed through the pairs until we find a unique hash. **/
+ /** Dividing value by 2 each time reduces the impact of repeated pairs. **/
+ int value = 0;
+ for (; i < num_pairs && char_pairs[i].hash == hash; i++)
+ {
+ value /= 2; /* Reduce impact of repeated pairs. */
+ value += ((unsigned int)char_pairs[i].c1 + (unsigned int)char_pairs[i].c2) % 13u + 1u;
+ }
+
+ /** Skip zeros to reach the dimension index specified by the hash. **/
+ unsigned int num_zeros = hash - dim;
+ if (num_zeros > 0u)
+ {
+ sparse_vector[cur++] = (int)-num_zeros;
+ dim = hash;
+ }
+
+ /** Add the value to the sparse vector. **/
+ sparse_vector[cur++] = value;
+ dim++;
+ }
+ if (dim != CA_NUM_DIMS) sparse_vector[cur++] = -(CA_NUM_DIMS - dim);
+
+ /** Free unused memory. **/
+ nmSysFree(char_pairs);
+ char_pairs = NULL;
+
+
+ /** Trim extra space wasted by identical hashes. **/
+ trimmed_sparse_vector = checkPtr(nmSysRealloc(sparse_vector, cur * sizeof(int)));
+ if (trimmed_sparse_vector == NULL) goto err_free;
+ sparse_vector = NULL; /* Mark memory freed by nmSysRealloc() no longer valid. */
+
+ /** Return the result. **/
+ return trimmed_sparse_vector;
+
+ err_free:
+ /** Cleanup. **/
+ if (trimmed_sparse_vector != NULL) nmSysFree(trimmed_sparse_vector);
+ if (sparse_vector != NULL) nmSysFree(sparse_vector);
+ if (char_pairs != NULL) nmSysFree(char_pairs);
+ if (chars != NULL) nmSysFree(chars);
+ return NULL;
+ }
+
+/*** Free memory allocated to store a sparse vector.
+ ***
+ *** @param sparse_vector The sparse vector being freed.
+ ***/
+void
+ca_free_vector(pVector sparse_vector)
+ {
+ nmSysFree(sparse_vector);
+
+ return;
+ }
+
+/*** Parse a token from a sparsely allocated vector and write its value to
+ *** `token_value`. The number of dimensions consumed in the process is
+ *** written `dims_consumed`.
+ ***
+ *** @param token The sparse vector token to be parsed.
+ *** @param dims_consumed The location to store the number of dimensions
+ *** consumed, aka. how many indexes we would need to advance if this
+ *** were a densely allocated vector.
+ *** @param token_value The location to save the `token_value`.
+ ***/
+static void
+ca_parse_vector_token(const int token, unsigned int* dims_consumed, unsigned int* token_value)
+ {
+ if (token < 0)
+ {
+ /** This run contains -token zeros. **/
+ *dims_consumed = (unsigned)(-token);
+ *token_value = 0u;
+ }
+ else
+ {
+ /** This run contains one token_value. **/
+ *dims_consumed = 1u;
+ *token_value = (unsigned)(token);
+ }
+
+ return;
+ }
+
+/*** Compute the actual number of ints stored in memory to hold the given
+ *** sparsely allocated vector.
+ ***
+ *** @param vector The vector.
+ *** @returns The computed length.
+ ***/
+unsigned int
+ca_sparse_len(const pVector vector)
+ {
+ unsigned int i = 0u;
+
+ /*** TODO: Israel - Add code here to use nmSysGetSize(), if it is
+ *** available, once the newmalloc branch is merged. This would
+ *** allow us to know the size immediately without needing to
+ *** traverse the array.
+ ***/
+
+ for (unsigned int dim = 0u; dim < CA_NUM_DIMS;)
+ {
+ /** Parse the vector token. **/
+ unsigned int dims_consumed = 0u, val = 0u;
+ ca_parse_vector_token(vector[i++], &dims_consumed, &val);
+
+ /** Move ahead the requested number of dimensions. **/
+ dim += dims_consumed;
+ }
+
+ return i;
+ }
+
+/*** Print the underlying implementation-level values of a sparsely allocated
+ *** vector (for debugging).
+ ***
+ *** @param vector The vector to print.
+ ***/
+void
+ca_print_vector(const pVector vector)
+ {
+ if (vector == NULL)
+ {
+ printf("Vector: NULL");
+ return;
+ }
+
+ const unsigned int len = ca_sparse_len(vector);
+ printf("Vector: [%d", vector[0]);
+ for (unsigned int i = 1u; i < len; i++)
+ printf(", %d", vector[i]);
+ printf("]");
+
+ return;
+ }
+
+/*** Compute the magnitude of a sparsely allocated vector.
+ ***
+ *** @param vector The vector.
+ *** @returns The computed magnitude.
+ ***/
+static double
+ca_magnitude_sparse(const pVector vector)
+ {
+ unsigned int magnitude = 0u;
+
+ for (unsigned int i = 0u, dim = 0u; dim < CA_NUM_DIMS;)
+ {
+ /** Parse the vector token. **/
+ unsigned int dims_consumed = 0u, val = 0u;
+ ca_parse_vector_token(vector[i++], &dims_consumed, &val);
+
+ /** Increase magnitude. **/
+ magnitude += val * val;
+
+ /** Move ahead the requested number of dimensions. **/
+ dim += dims_consumed;
+ }
+
+ return sqrt((double)magnitude);
+ }
+
+/*** Compute the magnitude of a densely allocated centroid.
+ ***
+ *** @param centroid The centroid.
+ *** @returns The computed magnitude.
+ ***/
+static double
+ca_magnitude_dense(const pCentroid centroid)
+ {
+ double magnitude = 0.0;
+
+ for (unsigned int i = 0u; i < CA_NUM_DIMS; i++)
+ magnitude += centroid[i] * centroid[i];
+
+ return sqrt(magnitude);
+ }
+
+/*** Calculate the similarity between two sparsely allocated vectors by taking
+ *** their dot product.
+ ***
+ *** @param v1 Sparse vector #1.
+ *** @param v2 Sparse vector #2.
+ *** @returns Similarity between 0 and 1 where
+ *** 1 indicates identical and
+ *** 0 indicates completely different.
+ ***/
+static double
+ca_sparse_similarity(const pVector v1, const pVector v2)
+ {
+ /** Calculate dot product. **/
+ unsigned int vec1_remaining = 0u, vec2_remaining = 0u;
+ unsigned int dim = 0u, i1 = 0u, i2 = 0u, dot_product = 0u;
+ while (dim < CA_NUM_DIMS)
+ {
+ unsigned int val1 = 0u, val2 = 0u;
+ if (vec1_remaining == 0u) ca_parse_vector_token(v1[i1++], &vec1_remaining, &val1);
+ if (vec2_remaining == 0u) ca_parse_vector_token(v2[i2++], &vec2_remaining, &val2);
+
+ /*** Accumulate the dot_product. If either vector is 0 here,
+ *** the total is 0 and this statement does nothing.
+ ***/
+ dot_product += val1 * val2;
+
+ /** Consume overlap from both runs. **/
+ unsigned int overlap = min(vec1_remaining, vec2_remaining);
+ vec1_remaining -= overlap;
+ vec2_remaining -= overlap;
+ dim += overlap;
+ }
+
+ /** Optimization: Skip computing magnitudes for completely different vectors. **/
+ if (dot_product == 0u) return 0.0;
+
+ /** Return the difference score. **/
+ return (double)dot_product / (ca_magnitude_sparse(v1) * ca_magnitude_sparse(v2));
+ }
+
+/*** Calculate the difference between two sparsely allocated vectors by
+ *** subtracting their dot product from 1.0.
+ ***
+ *** @param v1 Sparse vector #1.
+ *** @param v2 Sparse vector #2.
+ *** @returns Similarity between 0 and 1 where
+ *** 1 indicates completely different and
+ *** 0 indicates identical.
+ ***/
+#define ca_sparse_dif(v1, v2) (1.0 - ca_sparse_similarity(v1, v2))
+
+/*** Calculate the similarity between a sparsely allocated vector and a densely
+ *** allocated centroid by taking their dot product.
+ ***
+ *** @param v1 Sparse vector #1.
+ *** @param c1 Dense centroid #1.
+ *** @returns Similarity between 0 and 1 where
+ *** 1 indicates identical and
+ *** 0 indicates completely different.
+ ***/
+static double
+ca_sparse_similarity_to_centroid(const pVector v1, const pCentroid c1)
+ {
+ double dot_product = 0.0;
+
+ for (unsigned int i = 0u, dim = 0u; dim < CA_NUM_DIMS;)
+ {
+ /** Parse the vector token. **/
+ unsigned int dims_consumed = 0u, val = 0u;
+ ca_parse_vector_token(v1[i++], &dims_consumed, &val);
+
+ /** Increase dot product (skipped for zero-values). **/
+ if (val > 0u) dot_product += (double)val * c1[dim];
+
+ /** Move ahead the requested number of dimensions. **/
+ dim += dims_consumed;
+ }
+
+ /** Return the difference score. **/
+ return dot_product / (ca_magnitude_sparse(v1) * ca_magnitude_dense(c1));
+ }
+
+/*** Calculate the difference between a sparsely allocated vector and a densely
+ *** allocated centroid by subtracting their dot product from 1.0.
+ ***
+ *** @param v1 Sparse vector #1.
+ *** @param c1 Dense centroid #2.
+ *** @returns Difference between 0 and 1 where
+ *** 1 indicates completely different and
+ *** 0 indicates identical.
+ ***/
+#define ca_sparse_dif_to_centroid(v1, c1) (1.0 - ca_sparse_similarity_to_centroid(v1, c1))
+
+/*** Computes Levenshtein distance between two strings.
+ ***
+ *** @param str1 The first string.
+ *** @param str2 The second string.
+ *** @param str1_length The length of the first string, or 0 to detect from
+ *** the length from the null-terminator.
+ *** @param str2_length The length of the second string, or 0 to detect from
+ *** the length from the null-terminator.
+ *** @returns The edit distance between the two strings, or a negative value on error.
+ ***/
+int
+ca_edit_dist(const char* str1, const char* str2, const size_t str1_length, const size_t str2_length)
+ {
+ int result = -1;
+ unsigned int** lev_matrix = NULL;
+
+ /*** lev_matrix:
+ *** For all i and j, d[i][j] will hold the Levenshtein distance between
+ *** the first i characters of s and the first j characters of t.
+ ***
+ *** As they say, no dynamic programming algorithm is complete without a
+ *** matrix that you fill out and it has the answer in the final location.
+ ***/
+ const size_t str1_len = (str1_length == 0u) ? strlen(str1) : str1_length;
+ const size_t str2_len = (str2_length == 0u) ? strlen(str2) : str2_length;
+ lev_matrix = checkPtr(nmSysMalloc((str1_len + 1) * sizeof(unsigned int*)));
+ if (lev_matrix == NULL) goto end;
+ for (unsigned int i = 0u; i < str1_len + 1u; i++)
+ {
+ lev_matrix[i] = checkPtr(nmSysMalloc((str2_len + 1) * sizeof(unsigned int)));
+ if (lev_matrix[i] == NULL) goto end;
+ }
+
+ /*** Base case #0:
+ *** Transforming an empty string into an empty string has 0 cost.
+ ***/
+ lev_matrix[0][0] = 0u;
+
+ /*** Base case #1:
+ *** Any source prefix can be transformed into an empty string by
+ *** dropping each character.
+ ***/
+ for (unsigned int i = 1u; i <= str1_len; i++)
+ lev_matrix[i][0] = i;
+
+ /*** Base case #2:
+ *** Any target prefixes can be transformed into an empty string by
+ *** inserting each character.
+ ***/
+ for (unsigned int j = 1u; j <= str2_len; j++)
+ lev_matrix[0][j] = j;
+
+ /** General Case. **/
+ for (unsigned int i = 1u; i <= str1_len; i++)
+ {
+ for (unsigned int j = 1u; j <= str2_len; j++)
+ {
+ /** If the characters are equal, no change is needed. **/
+ if (str1[i - 1] == str2[j - 1])
+ lev_matrix[i][j] = lev_matrix[i - 1][j - 1];
+
+ /*** We need to make a change, so use the operation with the
+ *** lowest cost out of delete, insert, replace, or swap.
+ ***/
+ else
+ {
+ unsigned int cost_delete = lev_matrix[i - 1][j] + 1u;
+ unsigned int cost_insert = lev_matrix[i][j - 1] + 1u;
+ unsigned int cost_replace = lev_matrix[i-1][j-1] + 1u;
+
+ /** If a swap is possible, calculate the cost. **/
+ bool can_swap = (
+ i > 1 && j > 1 &&
+ str1[i - 1] == str2[j - 2] &&
+ str1[i - 2] == str2[j - 1]
+ );
+ unsigned int cost_swap = (can_swap) ? lev_matrix[i - 2][j - 2] + 1 : UINT_MAX;
+
+ /** Assign the best operation. **/
+ lev_matrix[i][j] = min(min(min(cost_delete, cost_insert), cost_replace), cost_swap);
+ }
+ }
+ }
+
+ /** Store result. **/
+ unsigned int unsigned_result = lev_matrix[str1_len][str2_len];
+ if (unsigned_result > INT_MAX)
+ {
+ fprintf(stderr,
+ "Warning: Integer overflow detected in ca_edit_dist(\"%s\", \"%s\", %lu, %lu) = %u > %d\n",
+ str1, str2, str1_length, str2_length, unsigned_result, INT_MAX
+ );
+ }
+ result = (int)unsigned_result;
+
+ /** Cleanup. **/
+ end:
+ if (lev_matrix != NULL)
+ {
+ for (unsigned int i = 0u; i < str1_len + 1u; i++)
+ {
+ if (lev_matrix[i] == NULL) break;
+ else nmSysFree(lev_matrix[i]);
+ }
+ nmSysFree(lev_matrix);
+ }
+
+ /** Done. **/
+ return result;
+ }
+
+/*** Compares two strings using their cosine similarity, returning a value
+ *** between `0.0` (completely different) and `1.0` (identical). If either
+ *** OR BOTH strings are NULL, this function returns `0.0`.
+ ***
+ *** @attention - Note: Punctuation, whitespace, etc. are ignored due to how
+ *** cosine vector hashing is implemented.
+ ***
+ *** @attention - This function takes `void*` instead of `pVector` so that it
+ *** can be used as the similarity function in the ca_search() function
+ *** family without needing a messy typecast to avoid the compiler warning.
+ *** However, behavior is undefined if `v1` and `v2` are not `pVector`s.
+ ***
+ *** @param v1 A `pVector` to the first string to compare.
+ *** @param v2 A `pVector` to the second string to compare.
+ *** @returns The cosine similarity between the two strings.
+ ***/
+double
+ca_cos_compare(void* v1, void* v2)
+ {
+ /** Input validation checks. **/
+ if (v1 == NULL || v2 == NULL) return 0.0;
+ if (v1 == v2) return 1.0;
+
+ /** Input validation checks. **/
+ const pVector vec1 = v1, vec2 = v2;
+ const bool v1_empty = (vec1 == NULL || ca_is_empty(vec1) || ca_has_no_pairs(vec1));
+ const bool v2_empty = (vec2 == NULL || ca_is_empty(vec2) || ca_has_no_pairs(vec2));
+ if (v1_empty && v2_empty) return 1.0;
+ if (v1_empty && !v2_empty) return 0.0;
+ if (!v1_empty && v2_empty) return 0.0;
+
+ /** Apply rounding to avoid annoying floating point issues before returning. **/
+ return round(ca_sparse_similarity(vec1, vec2) * 1000000.0) / 1000000.0;
+ }
+
+/*** Compares two strings using their Levenshtein edit distance to compute a
+ *** similarity between `0.0` (completely different) and `1.0` (identical).
+ *** If both strings are empty, this function returns `1.0` (identical). If
+ *** either OR BOTH strings are NULL, this function returns `0.0`.
+ ***
+ *** @attention - Note: Unlike `ca_cos_compare()`, punctuation, whitespace,
+ *** etc. are NOT ignored. In fact, this functions supports strings that
+ *** contain ANY valid ASCII characters other than the NULL-terminator,
+ *** which is used to terminate the string.
+ ***
+ *** @attention - This function takes `void*` instead of `char*` so that it
+ *** can be used as the similarity function in the ca_search() function
+ *** family without needing a messy typecast to avoid the compiler warning.
+ *** However, behavior is undefined if `v1` and `v2` are not `char*`s.
+ ***
+ *** @param str1 A `char*` to the first string to compare.
+ *** @param str2 A `char*` to the second string to compare.
+ *** @returns The Levenshtein similarity between the two strings,
+ *** or NAN on failure.
+ ***/
+double
+ca_lev_compare(void* str1, void* str2)
+ {
+ /** Input validation checks. **/
+ if (str1 == NULL || str2 == NULL) return 0.0;
+ if (str1 == str2) return 1.0;
+
+ /** Handle string length. **/
+ const size_t len1 = strlen(str1);
+ const size_t len2 = strlen(str2);
+ if (len1 == 0lu && len2 == 0lu) return 1.0;
+ if (len1 != 0lu && len2 == 0lu) return 0.0;
+ if (len1 == 0lu && len2 != 0lu) return 0.0;
+
+ /** Compute levenshtein edit distance. **/
+ const int edit_dist = checkPos(ca_edit_dist((const char*)str1, (const char*)str2, len1, len2));
+ if (edit_dist < 0) return NAN;
+
+ /** Normalize edit distance into a similarity measure. **/
+ const double normalized_similarity = 1.0 - (double)edit_dist / (double)max(len1, len2);
+
+ /** Apply rounding to avoid annoying floating point issues before returning. **/
+ return round(normalized_similarity * 1000000.0) / 1000000.0;
+ }
+
+/*** Check if two sparse vectors are identical, typically used for debugging
+ *** or testing. True if both vectors are `NULL`, but false if one is and the
+ *** other is not.
+ ***
+ *** @param v1 The first vector.
+ *** @param v2 The second vector.
+ *** @returns true if they are equal,
+ *** false if any element is different.
+ ***/
+bool
+ca_eql(pVector v1, pVector v2)
+ {
+ /** Edge cases. **/
+ if (v1 == v2) return true;
+ if (v1 == NULL || v2 == NULL) return false;
+
+ const unsigned int len = ca_sparse_len(v1);
+ if (len != ca_sparse_len(v2)) return false;
+ for (unsigned int i = 0u; i < len; i++)
+ if (v1[i] != v2[i]) return false;
+
+ return true;
+ }
+
+/*** Calculate the average size of all clusters in a set of vectors.
+ ***
+ *** @param vectors The vectors of the dataset (allocated sparsely).
+ *** @param num_vectors The number of vectors in the dataset.
+ *** @param labels The clusters to which vectors are assigned.
+ *** @param centroids The locations of the centroids (allocated densely).
+ *** @param num_clusters The number of centroids (k).
+ *** @returns The average cluster size.
+ ***/
+static double
+ca_i_get_cluster_size(
+ pVector* vectors,
+ const unsigned int num_vectors,
+ unsigned int* labels,
+ pCentroid* centroids,
+ const unsigned int num_clusters)
+ {
+ double result = NAN;
+ double* cluster_sums = NULL;
+ unsigned int* cluster_counts = NULL;
+
+ /** Allocate space to store clusters as averages are computed. **/
+ /*** We use nmMalloc() here because this function is usually called
+ *** repeatedly with the same number of clusters at the end of the
+ *** loop in ca_kmeans(). Also, ca_kmeans() may be called multiple
+ *** times with the same k value, increasing this benefit.
+ ***/
+ cluster_sums = checkPtr(nmMalloc(num_clusters * sizeof(double)));
+ cluster_counts = checkPtr(nmMalloc(num_clusters * sizeof(unsigned int)));
+ if (cluster_sums == NULL || cluster_counts == NULL) goto end;
+ for (unsigned int i = 0u; i < num_clusters; i++)
+ {
+ cluster_sums[i] = 0.0;
+ cluster_counts[i] = 0u;
+ }
+
+ /** Sum the difference from each vector to its cluster centroid. **/
+ for (unsigned int i = 0u; i < num_vectors; i++)
+ {
+ const unsigned int label = labels[i];
+ cluster_sums[label] += ca_sparse_dif_to_centroid(vectors[i], centroids[label]);
+ cluster_counts[label]++;
+ }
+
+ /** Add up the average cluster size. **/
+ double cluster_total = 0.0;
+ unsigned int num_valid_clusters = 0u;
+ for (unsigned int label = 0u; label < num_clusters; label++)
+ {
+ const unsigned int cluster_count = cluster_counts[label];
+ if (cluster_count == 0u) continue;
+
+ cluster_total += cluster_sums[label] / cluster_count;
+ num_valid_clusters++;
+ }
+
+ /** Calculate average sizes. **/
+ result = cluster_total / num_valid_clusters;
+
+ end:
+ /** Clean up. **/
+ if (cluster_sums != NULL) nmFree(cluster_sums, num_clusters * sizeof(double));
+ if (cluster_counts != NULL) nmFree(cluster_counts, num_clusters * sizeof(unsigned int));
+
+ return result;
+ }
+
+/*** Executes the k-means clustering algorithm. Selects `num_clusters` random
+ *** vectors as initial centroids, using `rand()` (to set a seed, call srand()
+ *** and pass false for `auto_seed`). Each iteration, points are assigned to the
+ *** nearest centroid using cosine similarity on the provided sparse vectors.
+ *** After this, centroids are moved to the center of their points. This
+ *** process repeats until the `min_improvement` threshold is not met, or
+ *** `max_iter` is reached (whichever happens first).
+ ***
+ *** @attention - `num_vectors` must be the length of both `vectors` and `labels`.
+ ***
+ *** @param vectors The sparse cosine similarity vectors representing the data
+ *** to cluster.
+ *** @param num_vectors The number of vectors (in `vectors`) to cluster.
+ *** @param num_clusters The number of clusters to generate (also called k).
+ *** @param max_iter A hard cutoff for the max number of iterations, applied
+ *** even if the `min_improvement` threshold is still met on each iteration.
+ *** @param min_improvement The minimum improvement threshold that must be met
+ *** each clustering iteration. If this is not met, the iterations stop.
+ *** Pass -1.0 to disable this and iterate for as long as the centroids keep
+ *** changing (or until `max_iter` is reached).
+ *** @param labels Initialized by this function to stores the final cluster
+ *** identities of the vectors after clustering is completed. Each value
+ *** will be `0 <= n < num_clusters`. This buffer can be uninitialized.
+ *** @param vector_sims An array of num_vectors elements, allocated by the
+ *** caller, where index i stores the similarity of vector i to its assigned
+ *** cluster. Passing NULL skips evaluation of these values.
+ *** @param auto_seed If true, the function will set its own seed. Otherwise,
+ *** it will use rand() without setting a seed, so the caller should use
+ *** srand() to set a seed before calling.
+ *** @returns 0 if successful, or -1 if an error occurs.
+ ***/
+int
+ca_kmeans(
+ pVector* vectors,
+ const unsigned int num_vectors,
+ const unsigned int num_clusters,
+ const unsigned int max_iter,
+ const double min_improvement,
+ unsigned int* labels,
+ double* vector_sims,
+ bool auto_seed)
+ {
+ pCentroid* centroids = NULL;
+ pCentroid* new_centroids = NULL;
+
+ /** Setup variables. **/
+ bool successful = false;
+ unsigned int cluster_counts[num_clusters];
+ memset(labels, 0u, num_vectors * sizeof(unsigned int));
+
+ /** Allocate space to store centroids and new_centroids. **/
+ /** Dynamic allocation is required because these densely allocated arrays might be up to 500KB! **/
+ const size_t centroids_size = num_clusters * sizeof(pCentroid);
+ centroids = checkPtr(nmMalloc(centroids_size));
+ new_centroids = checkPtr(nmMalloc(centroids_size));
+ if (centroids == NULL || new_centroids == NULL) goto end;
+ memset(centroids, 0, centroids_size);
+ memset(new_centroids, 0, centroids_size);
+ for (unsigned int i = 0u; i < num_clusters; i++)
+ {
+ centroids[i] = checkPtr(nmMalloc(CENTROID_SIZE));
+ new_centroids[i] = checkPtr(nmMalloc(CENTROID_SIZE));
+ if (centroids[i] == NULL || new_centroids[i] == NULL) goto end;
+ memset(centroids[i], 0, CENTROID_SIZE);
+ memset(new_centroids[i], 0, CENTROID_SIZE);
+ }
+
+ /** Select random vectors to use as the initial centroids. **/
+ if (auto_seed) srand(time(NULL));
+ for (unsigned int i = 0u; i < num_clusters; i++)
+ {
+ /** Pick a random vector. **/
+ const pVector vector = vectors[rand() % num_vectors];
+
+ /** Sparse copy the vector to expand it into a densely allocated centroid. **/
+ pCentroid centroid = centroids[i];
+ for (unsigned int i = 0u, dim = 0u; dim < CA_NUM_DIMS;)
+ {
+ const int token = vector[i++];
+ if (token > 0) centroid[dim++] = (double)token;
+ else for (unsigned int j = 0u; j < (unsigned)-token; j++) centroid[dim++] = 0.0;
+ }
+ }
+
+ /** Main kmeans loop. **/
+ double old_average_cluster_size = 1.0;
+ for (unsigned int iter = 0u; iter < max_iter; iter++)
+ {
+ bool changed = false;
+
+ /** Reset new centroids. **/
+ for (unsigned int i = 0u; i < num_clusters; i++)
+ {
+ cluster_counts[i] = 0u;
+ for (unsigned int dim = 0; dim < CA_NUM_DIMS; dim++)
+ new_centroids[i][dim] = 0.0;
+ }
+
+ /** Assign each point to the nearest centroid. **/
+ for (unsigned int i = 0u; i < num_vectors; i++)
+ {
+ const pVector vector = vectors[i];
+ double min_dist = DBL_MAX;
+ unsigned int best_centroid_label = 0u;
+
+ /** Find nearest centroid. **/
+ for (unsigned int j = 0u; j < num_clusters; j++)
+ {
+ const double dist = ca_sparse_dif_to_centroid(vector, centroids[j]);
+ if (dist < min_dist)
+ {
+ min_dist = dist;
+ best_centroid_label = j;
+ }
+ }
+
+ /** Update label to new centroid, if necessary. **/
+ if (labels[i] != best_centroid_label)
+ {
+ labels[i] = best_centroid_label;
+ changed = true;
+ }
+
+ /** Accumulate values for new centroid calculation. **/
+ pCentroid best_centroid = new_centroids[best_centroid_label];
+ for (unsigned int i = 0u, dim = 0u; dim < CA_NUM_DIMS;)
+ {
+ const int val = vector[i++];
+ if (val < 0) dim += (unsigned)(-val);
+ else best_centroid[dim++] += (double)val;
+ }
+ cluster_counts[best_centroid_label]++;
+ }
+
+ /** Stop if centroids didn't change. **/
+ if (!changed) break;
+
+ /** Update centroids. **/
+ for (unsigned int i = 0u; i < num_clusters; i++)
+ {
+ if (cluster_counts[i] == 0u) continue;
+ pCentroid centroid = centroids[i];
+ const pCentroid new_centroid = new_centroids[i];
+ const unsigned int cluster_count = cluster_counts[i];
+ for (unsigned int dim = 0u; dim < CA_NUM_DIMS; dim++)
+ centroid[dim] = new_centroid[dim] / cluster_count;
+ }
+
+ /** Is there enough improvement? **/
+ if (min_improvement <= -1.0) continue; /** Skip check if it will never end the loop. **/
+ const double average_cluster_size = checkDouble(ca_i_get_cluster_size(vectors, num_vectors, labels, centroids, num_clusters));
+ if (isnan(average_cluster_size)) goto end;
+ const double improvement = old_average_cluster_size - average_cluster_size;
+ if (improvement < min_improvement) break;
+ old_average_cluster_size = average_cluster_size;
+ }
+
+ /** Compute vector similarities, if requested. **/
+ if (vector_sims != NULL)
+ {
+ for (unsigned int i = 0u; i < num_vectors; i++)
+ vector_sims[i] = ca_sparse_similarity_to_centroid(vectors[i], centroids[labels[i]]);
+ }
+
+ /** Success. **/
+ successful = true;
+
+ end:
+ /** Clean up. **/
+ if (centroids != NULL)
+ {
+ for (unsigned int i = 0u; i < num_clusters; i++)
+ {
+ if (centroids[i] != NULL) nmFree(centroids[i], CENTROID_SIZE);
+ else break;
+ }
+ nmFree(centroids, num_clusters * sizeof(pCentroid));
+ }
+ if (new_centroids != NULL)
+ {
+ for (unsigned int i = 0u; i < num_clusters; i++)
+ {
+ if (new_centroids[i] != NULL) nmFree(new_centroids[i], CENTROID_SIZE);
+ else break;
+ }
+ nmFree(new_centroids, num_clusters * sizeof(pCentroid));
+ }
+
+ /** Return the function result code. **/
+ return (successful) ? 0 : -1;
+ }
+
+/*** Finds the data that is the most similar to the target and returns
+ *** it if the similarity meets the threshold.
+ ***
+ *** @param target The target data to compare to the rest of the data.
+ *** @param data The rest of the data, compared against the target.
+ *** @param num_data The number of elements in data. Specify 0 to detect
+ *** length on a null terminated array of data.
+ *** @param similarity A function that takes two data items (aka. pointers
+ *** in the array pointed to by `data`) and returns their similarity.
+ *** @param threshold The minimum similarity threshold. If the most similar
+ *** data does not meet this threshold, the function returns NULL.
+ *** @returns A pointer to the most similar piece of data found in the data
+ *** array, or NULL if the most similar data did not meet the threshold.
+ ***/
+void*
+ca_most_similar(
+ void* target,
+ void** data,
+ const unsigned int num_data,
+ double (*similarity)(void*, void*),
+ const double threshold)
+ {
+ void* most_similar = NULL;
+ double best_sim = -INFINITY;
+
+ /** Error cases. **/
+ if (target == NULL
+ || data == NULL
+ || similarity == NULL
+ || (threshold < 0.0 || 1.0 < threshold)
+ || isnan(threshold)
+ ) return NULL;
+
+ /** Iterate over all data options to find the one with the highest similarity. **/
+ for (unsigned int i = 0u; (num_data == 0u && data[i] != NULL) || (i < num_data); i++)
+ {
+ const double sim = similarity(target, data[i]);
+ if (isnan(sim)) continue; /* Skip failed comparison. */
+ if (sim > best_sim && sim >= threshold)
+ {
+ most_similar = data[i];
+ best_sim = sim;
+ }
+ }
+
+ return most_similar;
+ }
+
+
+/*** Runs a sliding search over the provided data, comparing each element to
+ *** the next `window_size` elements. If any comparison yields a similarity
+ *** greater than the threshold, it is stored in the returned xArray.
+ ***
+ *** @param data The data to be searched.
+ *** @param num_data The number of data items in `data`.
+ *** @param window_size The size of the sliding window used for the search.
+ *** @param similarity A function which takes two data items of the type of
+ *** the `data` param and returns their similarity.
+ *** @param threshold The minimum threshold required for each pair.
+ *** @param maybe_pairs A pointer to an xArray to which pairs should be added.
+ *** Pass NULL to allocate a new one.
+ *** @returns An xArray of the pairs found. If `maybe_pairs` is not NULL, this
+ *** will be that xArray, to allow for chaining.
+ ***/
+pXArray
+ca_sliding_search(
+ void** data,
+ const unsigned int num_data,
+ const unsigned int window_size,
+ double (*similarity)(void*, void*),
+ const double threshold,
+ pXArray maybe_pairs)
+ {
+ pXArray pairs = maybe_pairs;
+
+ /** Error cases. **/
+ if (data == NULL
+ || num_data == 0
+ || window_size == 0
+ || similarity == NULL
+ || (threshold < 0.0 || 1.0 < threshold)
+ || isnan(threshold)
+ ) goto err;
+
+ /** Allocate space for pairs (if it is not provided). **/
+ if (pairs == NULL)
+ {
+ /** Guess that we will need space for two pairs per data point. **/
+ const int guess_size = num_data * 2;
+ pairs = checkPtr(xaNew(guess_size));
+ if (pairs == NULL) goto err;
+ }
+ const int num_starting_pairs = pairs->nItems;
+
+ /** Search for pairs. **/
+ for (unsigned int i = 0u; i < num_data; i++)
+ {
+ const unsigned int window_start = i + 1u;
+ const unsigned int window_end = min(i + window_size, num_data);
+ for (unsigned int j = window_start; j < window_end; j++)
+ {
+ const double sim = checkDouble(similarity(data[i], data[j]));
+ if (isnan(sim) || sim < 0.0 || 1.0 < sim)
+ {
+ fprintf(stderr, "Invalid similarity %g.\n", sim);
+ goto err_free;
+ }
+ if (sim > threshold) /* Pair found! */
+ {
+ pPair pair = (pPair)checkPtr(nmMalloc(sizeof(Pair)));
+ if (pair == NULL) goto err_free;
+ pair->i = i;
+ pair->j = j;
+ pair->similarity = sim;
+ if (checkPos(xaAddItem(pairs, (void*)pair)) < 0) goto err_free;
+ }
+ }
+ }
+
+ /** Success. **/
+ return pairs;
+
+ err_free:
+ /** Error cleanup: Free the pairs that we added to the XArray. **/
+ while (pairs->nItems > num_starting_pairs)
+ nmFree(pairs->Items[--pairs->nItems], sizeof(Pair));
+ if (maybe_pairs == NULL) check(xaFree(pairs)); /* Failure ignored. */
+
+ err:
+ return NULL;
+ }
+
+/*** Runs a complete search over the provided data, comparing each element to
+ *** each other element. If any comparison yields a similarity greater than the
+ *** threshold, it is stored in the returned xArray.
+ ***
+ *** @param data The data to be searched.
+ *** @param num_data The number of data items in `data`.
+ *** @param similarity A function which takes two data items of the type of
+ *** the `data` param and returns their similarity.
+ *** @param threshold The minimum threshold required for each pair.
+ *** @param maybe_pairs A pointer to an xArray to which pairs should be added.
+ *** Pass NULL to allocate a new one.
+ *** @returns An xArray of the pairs found. If `maybe_pairs` is not NULL, this
+ *** will be that xArray, to allow for chaining.
+ ***/
+pXArray
+ca_complete_search(
+ void** data,
+ const unsigned int num_data,
+ double (*similarity)(void*, void*),
+ const double threshold,
+ pXArray maybe_pairs)
+ {
+ return ca_sliding_search(data, num_data, num_data, similarity, threshold, maybe_pairs);
+ }
+
+/** Initialize the module. **/
+void
+ca_init(void)
+ {
+ nmRegister(CENTROID_SIZE, "Centroid");
+ nmRegister(sizeof(Pair), "Pair");
+
+ return;
+ }
+
+/** Scope cleanup. **/
+#undef ca_sparse_dif
+#undef ca_sparse_dif_to_centroid
diff --git a/centrallix-lib/src/mtask.c b/centrallix-lib/src/mtask.c
index e49e54cbb..1e2302040 100644
--- a/centrallix-lib/src/mtask.c
+++ b/centrallix-lib/src/mtask.c
@@ -41,7 +41,7 @@
/* Centrallix Application Server System */
/* Centrallix Base Library */
/* */
-/* Copyright (C) 1998-2001 LightSys Technology Services, Inc. */
+/* Copyright (C) 1998-2026 LightSys Technology Services, Inc. */
/* */
/* You may use these files and this library under the terms of the */
/* GNU Lesser General Public License, Version 2.1, contained in the */
@@ -54,13 +54,14 @@
/* Description: */
/* */
/* The MTASK Multithreading Tasking Module provides non-preemptive */
-/* threading services for Centrallix. It has been shown to be useable */
+/* threading services for Centrallix. It has been shown to be usable */
/* on a variety of platforms, although the values for MT_TASKSEP may */
/* sometimes need to be adjusted. This module does NOT provide for */
/* kernel threads or for preemptive threading. */
/************************************************************************/
+#include "expect.h"
#include "newmalloc.h"
#include "mtask.h"
#include "xstring.h"
@@ -2412,8 +2413,8 @@ thClearFlags(pThread thr, int flags)
int
thExcessiveRecursion()
{
- unsigned char buf[1];
- return (MTASK.CurrentThread->Stack - buf > MT_STACK_HIGHWATER);
+ unsigned char stack_ptr[1];
+ return UNLIKELY(MTASK.CurrentThread->Stack - stack_ptr > MT_STACK_HIGHWATER);
}
@@ -3611,7 +3612,7 @@ netGetRemotePort(pFile net_filedesc)
}
-/*** NETCONNECTTCP creats a client socket and connects it to a
+/*** NETCONNECTTCP creates a client socket and connects it to a
*** server on a given TCP service/port and host name. The flag
*** NET_U_NOBLOCK causes the request to return immediately even
*** if the connection is still trying to establish. Further
diff --git a/centrallix-lib/src/mtlexer.c b/centrallix-lib/src/mtlexer.c
index 88fa9fb73..0d10bb288 100644
--- a/centrallix-lib/src/mtlexer.c
+++ b/centrallix-lib/src/mtlexer.c
@@ -7,6 +7,7 @@
#include
#include
#include
+
#include "newmalloc.h"
#include "mtask.h"
#include "mtlexer.h"
@@ -904,7 +905,7 @@ mlxNextToken(pLxSession this)
}
else
{
- mssError(1,"MLX","Unexpected character encountered");
+ mssError(1, "MLX", "Unexpected character encountered: '%c'", ch);
this->TokType = MLX_TOK_ERROR;
break;
}
diff --git a/centrallix-lib/src/mtsession.c b/centrallix-lib/src/mtsession.c
index f3b378811..4bec27a5f 100644
--- a/centrallix-lib/src/mtsession.c
+++ b/centrallix-lib/src/mtsession.c
@@ -3,6 +3,7 @@
#include "cxlibconfig-internal.h"
#endif
#include
+#include
#include
#include
#include
@@ -23,13 +24,14 @@
#include "xstring.h"
#include "xhash.h"
#include "strtcpy.h"
+#include "check.h"
#include "cxsec.h"
/************************************************************************/
/* Centrallix Application Server System */
/* Centrallix Base Library */
/* */
-/* Copyright (C) 1998-2001 LightSys Technology Services, Inc. */
+/* Copyright (C) 1998-2026 LightSys Technology Services, Inc. */
/* */
/* You may use these files and this library under the terms of the */
/* GNU Lesser General Public License, Version 2.1, contained in the */
@@ -453,195 +455,91 @@ mssEndSession(pMtSession s)
}
-/*** mssError - Add an error message to the error stack, optionally
- *** clearing the existing contents thereof.
+/*** mssError_internal - Displays error text to the user (but no stack trace).
+ *** Does not exit the program, allowing the calling function to fail, creating
+ *** a cascade of error messages which provides useful info.
+ ***
+ *** @param clr Whether to clear the current error stack. As a rule of thumb,
+ *** if you are the first one to detect the error, clear the stack so that
+ *** other unrelated messages are not shown. If you are detecting an error
+ *** from another function that may also call an mssError() function, do
+ *** not clear the stack.
+ *** @param module The name or abbreviation of the module in which this
+ *** function is being called, to help developers narrow down the location
+ *** of the error.
+ *** @param file The name of the file where the error was detected.
+ *** @param line The line number where the error was detected.
+ *** @param format The format text for the error, which accepts any format
+ *** specifier that would be accepted by printf().
+ *** @param ... Variables matching format specifiers in the format.
***/
-int
-mssError(int clr, char* module, char* message, ...)
+void
+mssError_internal(int clr, char* module, char* file, int line, char* message, ...)
{
- va_list vl;
- char* msg;
- pMtSession s;
- XString xs;
- char* ptr;
- char* cur_pos;
- char* str;
- int i;
- char nbuf[16];
- char ch;
-
- /** Build the real error msg. **/
- xsInit(&xs);
- cur_pos = message;
- va_start(vl, message);
- while((ptr = strchr(cur_pos, '%')))
- {
- xsConcatenate(&xs, cur_pos, ptr - cur_pos);
- switch(ptr[1])
- {
- case '\0':
- xsConcatenate(&xs, "%", 1);
- cur_pos = ptr+1;
- break;
- case '%':
- xsConcatenate(&xs, "%", 1);
- cur_pos = ptr+2;
- break;
- case 's':
- str = va_arg(vl, char*);
- xsConcatenate(&xs, str?str:"(NULL)", -1);
- cur_pos = ptr + 2;
- break;
- case 'c':
- ch = va_arg(vl, int);
- xsConcatenate(&xs, &ch, 1);
- cur_pos = ptr + 2;
- break;
- case 'd':
- i = va_arg(vl, int);
- sprintf(nbuf,"%d",i);
- xsConcatenate(&xs, nbuf, -1);
- cur_pos = ptr + 2;
- break;
- default:
- cur_pos = ptr + 2;
- break;
- }
- }
- va_end(vl);
- if (*cur_pos) xsConcatenate(&xs, cur_pos, -1);
+ char err_msg[BUFSIZ];
+ size_t i = 0;
+
+ /** Prevent issues from interlacing this function with prints to stdout. **/
+ check(fflush(stdout)); /* Failure ignored. */
+
+ /** Add line number to error message. **/
+ err_msg[0] = '\0';
+ strtcatf(err_msg, sizeof(err_msg), &i, "%s:%d: ", file, line);
+
+ /** Write the module to the start of the error message. */
+ strtcatf(err_msg, sizeof(err_msg), &i, "%s: ", module);
+
+ /** Process the message format with all the same rules as printf(). **/
+ va_list args;
+ va_start(args, message);
+ strtcatf_va(err_msg, sizeof(err_msg), &i, message, args);
+ va_end(args);
/** Get current session **/
- s = (pMtSession)thGetParam(NULL,"mss");
- if (!s || MSS.LogAllErrors)
+ pMtSession s = thGetParam(NULL, "mss");
+ const bool log_error = (s == NULL || MSS.LogAllErrors);
+
+ /** Use standard logging without a session context, if needed. **/
+ if (log_error)
{
- /*printf("mssError: Error occurred outside of session context.\n");*/
- if (!strcmp(MSS.LogMethod,"syslog"))
+ /** Use the requested logging method. **/
+ if (strcmp(MSS.LogMethod, "syslog") == 0)
{
- if (!s)
- syslog(LOG_ERR, "System: %s: %.256s\n", module, xs.String);
+ if (s == NULL)
+ syslog(LOG_ERR, "System: %.256s\n", err_msg);
else
- syslog(LOG_WARNING, "User '%s': %s: %.256s\n", s->UserName, module, xs.String);
+ syslog(LOG_WARNING, "User '%s': %.256s\n", s->UserName, err_msg);
}
- else if (!strcmp(MSS.LogMethod, "stdout"))
+ else if (strcmp(MSS.LogMethod, "stdout") == 0)
{
- printf("%s: %s: %s\n",MSS.AppName[0]?MSS.AppName:"error",module,xs.String);
+ printf("%s: %s\n", (MSS.AppName[0]) ? MSS.AppName : "error", err_msg);
}
- if (!s) return -1;
}
- /** Need to clear? **/
- if (clr) mssClearError();
-
- /** Allocate space and construct the error text. **/
- msg = (char*)nmSysMalloc(strlen(module)+strlen(xs.String)+3);
- if (!msg)
+ /** If a session is available, try to add the error to the error list. **/
+ if (s != NULL)
{
- perror("mssError: Could not allocate error");
- printf("mssError: %s: %s\n",module,xs.String);
- return -1;
- }
- sprintf(msg,"%s: %s",module,xs.String);
- xaAddItem(&(s->ErrList),(void*)msg);
- xsDeInit(&xs);
+ /** Clear the error context, if requested. **/
+ if (clr) check(mssClearError()); /* Failure ignored. */
- return 0;
- }
-
-
-/*** mssErrorErrno - Adds an error to the error stack, but in this
- *** case it takes the error information from the current errno.
- ***/
-int
-mssErrorErrno(int clr, char* module, char* message, ...)
- {
- va_list vl;
- char* msg;
- char* err;
- pMtSession s;
- int en;
- char* str;
- int i;
- XString xs;
- char nbuf[16];
- char* cur_pos;
- char* ptr;
-
- /** Build the real error msg. **/
- xsInit(&xs);
- cur_pos = message;
- va_start(vl, message);
- while((ptr = strchr(cur_pos, '%')))
- {
- xsConcatenate(&xs, cur_pos, ptr - cur_pos);
- switch(ptr[1])
- {
- case '\0':
- xsConcatenate(&xs, "%", 1);
- cur_pos = ptr+1;
- break;
- case '%':
- xsConcatenate(&xs, "%", 1);
- cur_pos = ptr+2;
- break;
- case 's':
- str = va_arg(vl, char*);
- xsConcatenate(&xs, str?str:"(NULL)", -1);
- cur_pos = ptr + 2;
- break;
- case 'd':
- i = va_arg(vl, int);
- sprintf(nbuf,"%d",i);
- xsConcatenate(&xs, nbuf, -1);
- cur_pos = ptr + 2;
- break;
- default:
- cur_pos = ptr + 2;
- break;
- }
- }
- va_end(vl);
- if (*cur_pos) xsConcatenate(&xs, cur_pos, -1);
-
- /** Get current errno. **/
- en = errno;
- err = strerror(en);
-
- /** Get session. **/
- s = (pMtSession)thGetParam(NULL,"mss");
- if (!s || MSS.LogAllErrors)
- {
- /*printf("mssErrorErrno: Error occurred outside of session context.\n");*/
- if (!strcmp(MSS.LogMethod,"syslog"))
+ /** Allocate space and construct the error text. **/
+ char* allocated_err_msg = checkPtr(nmSysStrdup(err_msg));
+ if (allocated_err_msg == NULL)
{
- if (!s)
- syslog(LOG_ERR, "System: %s: %.256s (%s)\n", module, xs.String, err);
- else
- syslog(LOG_WARNING, "User '%s': %s: %.256s (%s)\n", s->UserName, module, xs.String, err);
+ fprintf(stderr, "Failed to store error message: %s\n", err_msg);
+ return; /* Give up. */
}
- else
+
+ /** Store the error. **/
+ if (checkPos(xaAddItem(&(s->ErrList), (void*)allocated_err_msg)) < 0)
{
- printf("%s: %s: %s (%s)\n",MSS.AppName[0]?MSS.AppName:"error",module,xs.String,err);
+ fprintf(stderr, "Failed to add error message to session error list: %s\n", err_msg);
+ nmSysFree(allocated_err_msg);
+ return; /* Give up. */
}
- if (!s) return -1;
}
- /** Need to clear? **/
- if (clr) mssClearError();
-
- /** Allocate space and construct the error text. **/
- msg = (char*)nmSysMalloc(strlen(module)+strlen(xs.String)+6 + strlen(err));
- if (!msg)
- {
- perror("mssErrorErrno: Could not allocate error");
- printf("mssErrorErrno: %s: %s (%s)\n",module,xs.String,err);
- return -1;
- }
- sprintf(msg,"%s: %s (%s)",module,xs.String,err);
- xaAddItem(&(s->ErrList),(void*)msg);
- xsDeInit(&xs);
-
- return 0;
+ return;
}
@@ -728,21 +626,26 @@ mssUserError(pXString str)
int i;
pMtSession s;
char* item;
- char* colon;
+ char* sep;
/** Get session. **/
s = (pMtSession)thGetParam(NULL,"mss");
if (!s) return -1;
- /** Create a space-separated string of the messages, without module codes **/
+ /*** Create a space-separated string of the messages, without the source
+ *** location and module code that mssError_internal() writes in front
+ *** of each one. Both end in ": ", which the message itself may also
+ *** contain, so only the first two are skipped.
+ ***/
for(i=s->ErrList.nItems-1;i>=0;i--)
{
item = (char*)(s->ErrList.Items[i]);
if (item)
{
- colon = strchr(item, ':');
- if (colon)
- item = colon + 2;
+ sep = strstr(item, ": ");
+ if (sep) sep = strstr(sep + 2, ": ");
+ if (sep)
+ item = sep + 2;
xsConcatenate(str, item, -1);
if (i > 0)
xsConcatenate(str, " ", 1);
diff --git a/centrallix-lib/src/newmalloc.c b/centrallix-lib/src/newmalloc.c
index 32c02b85d..7b231fb45 100644
--- a/centrallix-lib/src/newmalloc.c
+++ b/centrallix-lib/src/newmalloc.c
@@ -102,7 +102,7 @@ typedef struct _RB
pRegisteredBlockType blknames[MAX_SIZE+1];
void
-nmInitialize()
+nmInitialize(void)
{
int i;
@@ -150,7 +150,7 @@ nmCheckItem(pMemStruct mem)
#endif
void
-nmCheckAll()
+nmCheckAll(void)
{
#ifdef BUFFER_OVERFLOW_CHECKING
pMemStruct mem;
@@ -240,7 +240,7 @@ nmSetErrFunction(int (*error_fn)())
}
void
-nmClear()
+nmClear(void)
{
int i;
pOverlay ov,del;
@@ -425,7 +425,7 @@ nmFree(void* ptr, int size)
void
-nmStats()
+nmStats(void)
{
if (!isinit) nmInitialize();
@@ -463,7 +463,7 @@ nmRegister(int size,char* name)
void
-nmDebug()
+nmDebug(void)
{
int i;
pRegisteredBlockType blk;
@@ -497,7 +497,7 @@ nmDebug()
void
-nmDeltas()
+nmDeltas(void)
{
int i, total;
pRegisteredBlockType blk;
diff --git a/centrallix-lib/src/range.c b/centrallix-lib/src/range.c
new file mode 100644
index 000000000..59701c2da
--- /dev/null
+++ b/centrallix-lib/src/range.c
@@ -0,0 +1,27 @@
+/************************************************************************/
+/* Centrallix Application Server System */
+/* Centrallix Base Library */
+/* */
+/* Copyright (C) 1998-2026 LightSys Technology Services, Inc. */
+/* */
+/* You may use these files and this library under the terms of the */
+/* GNU Lesser General Public License, Version 2.1, contained in the */
+/* included file "COPYING". */
+/* */
+/* Module: range.c, range.h */
+/* Author: Israel Fuller */
+/* Date: October 13, 2025 */
+/* Description: Adds some useful numerical range functions/macros that */
+/* C does not provide by default, such as min(), max(), */
+/* clamp(), etc. */
+/************************************************************************/
+
+#include
+
+#include "range.h"
+
+double roundTo(double value, int decimals)
+ {
+ const double mul = pow(10, decimals);
+ return round(value * mul) / mul;
+ }
diff --git a/centrallix-lib/src/strtcpy.c b/centrallix-lib/src/strtcpy.c
index 8b4366716..fbab07901 100644
--- a/centrallix-lib/src/strtcpy.c
+++ b/centrallix-lib/src/strtcpy.c
@@ -1,4 +1,7 @@
+#include
+#include
#include
+
#include "strtcpy.h"
#include "expect.h"
@@ -16,17 +19,23 @@
/* Author: Greg Beeley (GRB) */
/* Date: April 14th, 2006 */
/* */
-/* Description: Provides strtcpy(), a Truncating strcpy(), which */
-/* both respects the bounds of the destination and makes */
-/* sure the result is null-terminated. */
+/* Description: Provides truncating string functions, which respect the */
+/* bounds of the destination and ensure null-termination. */
/************************************************************************/
-/*** strtcat() - truncating string concatenation
- ***
- *** Appends to dst, being sure to not overflow the given dstlen size.
- *** Returns number of bytes actually copied, including null terminator.
- *** If truncated, returns -(bytes copied).
+/*** Truncating string concatenation.
+ ***
+ *** Appends the source string to the destination string, truncating the data
+ *** if needed to avoid overflowing the destination buffer.
+ ***
+ *** @param dst A pointer to the destination buffer.
+ *** @param src A pointer to the source buffer to read from.
+ *** @param dstlen The total allocated memory of the destination buffer (not
+ *** the length of available space), used to avoid writing off the end of
+ *** the buffer.
+ *** @returns number of bytes actually copied, including null terminator. If
+ *** truncated, returns `-(bytes copied)`.
***/
int
strtcat(char* dst, const char* src, size_t dstlen)
@@ -46,10 +55,18 @@ strtcat(char* dst, const char* src, size_t dstlen)
}
-/*** strtcpy() - truncating string copy
- ***
- *** returns number of bytes actually copied (including null terminator)
- *** if truncated, returns -(bytes copied), which is the same as -(dstlen).
+/*** Truncating string copy.
+ ***
+ *** Truncates written data if the source data is too long to fit into the
+ *** allocated destination buffer.
+ ***
+ *** @param dst A pointer to the destination buffer.
+ *** @param src A pointer to the source buffer to read from.
+ *** @param dstlen The length of the destination buffer, used to avoid writing
+ *** off the end of the allocated memory.
+ *** @returns The number of bytes actually copied (including null terminator).
+ *** If the data is truncated, returns `-(bytes copied)`, which is the same
+ *** as `-(dstlen)`.
***/
int
strtcpy(char* dst, const char* src, size_t dstlen)
@@ -77,3 +94,60 @@ strtcpy(char* dst, const char* src, size_t dstlen)
return origlen - dstlen;
#endif
}
+
+
+/*** strtcatf_va() - same as strtcatf(), but takes a va_list instead of
+ *** a variable argument list.
+ ***/
+int
+strtcatf_va(char* dst, size_t dstlen, size_t* pos, const char* fmt, va_list ap)
+ {
+ size_t start = *pos;
+ int ret;
+
+ /** No room for even one character. **/
+ if (UNLIKELY((dstlen == 0 || start >= dstlen - 1)))
+ return 0;
+
+ ret = vsnprintf(dst + start, dstlen - start, fmt, ap);
+
+ /** vsnprintf() failed, so discard whatever it left behind. **/
+ if (UNLIKELY((ret < 0)))
+ {
+ dst[start] = '\0';
+ return 0;
+ }
+
+ /** Output overran dst, so it was truncated and dst is now full. **/
+ if (UNLIKELY((start + (size_t)ret >= dstlen)))
+ {
+ *pos = dstlen - 1;
+ return -(int)(dstlen - start);
+ }
+
+ *pos = start + (size_t)ret;
+ return ret + 1;
+ }
+
+
+/*** strtcatf() - truncating formatted string concatenation
+ ***
+ *** Appends a printf-style message to dst, being sure to not overflow the
+ *** given dstlen size. *pos is the offset of dst's terminating null, and
+ *** advances past the appended text, so chained calls need no checks in
+ *** between. A full dst, or a *pos outside it, appends nothing.
+ *** Returns number of bytes actually appended, including null terminator.
+ *** If truncated, returns -(bytes appended).
+ ***/
+int
+strtcatf(char* dst, size_t dstlen, size_t* pos, const char* fmt, ...)
+ {
+ va_list ap;
+ int ret;
+
+ va_start(ap, fmt);
+ ret = strtcatf_va(dst, dstlen, pos, fmt, ap);
+ va_end(ap);
+
+ return ret;
+ }
diff --git a/centrallix-lib/src/timer.c b/centrallix-lib/src/timer.c
new file mode 100644
index 000000000..8cbb76a8b
--- /dev/null
+++ b/centrallix-lib/src/timer.c
@@ -0,0 +1,152 @@
+/************************************************************************/
+/* Centrallix Application Server System */
+/* Centrallix Base Library */
+/* */
+/* Copyright (C) 1998-2026 LightSys Technology Services, Inc. */
+/* */
+/* You may use these files and this library under the terms of the */
+/* GNU Lesser General Public License, Version 2.1, contained in the */
+/* included file "COPYING". */
+/* */
+/* Module: timer.c, timer.h */
+/* Author: Israel Fuller */
+/* Date: October 13, 2025 */
+/* Description: A simple timer utility, intended for benchmarking code */
+/* performance in wall time. */
+/************************************************************************/
+
+#include
+#include
+
+#include "check.h"
+#include "expect.h"
+#include "newmalloc.h"
+
+#include "timer.h"
+
+/*** Get the current monotonic time in seconds.
+ ***
+ *** @returns The current monotonic time as a fractional number of seconds.
+ ***/
+static double
+getTime(void)
+ {
+ struct timespec ts;
+
+ if (check(clock_gettime(CLOCK_MONOTONIC, &ts)) != 0)
+ return NAN;
+
+ return (double)ts.tv_sec + (double)ts.tv_nsec / 1.0e9f;
+ }
+
+/*** Initialize a timer struct. The initial timer is not yet started and has
+ *** no total time saved.
+ ***
+ *** @param timer The timer to initialize.
+ *** @returns `timer`, for chaining.
+ ***/
+pTimer
+timerInit(pTimer timer)
+ {
+ if (UNLIKELY(timer == NULL)) return NULL;
+
+ timer->start = NAN;
+ timer->total = 0.0;
+
+ return timer;
+ }
+
+/*** Allocate and initialize a new timer.
+ ***
+ *** @returns A newly allocated timer, or NULL if allocation fails.
+ ***/
+pTimer
+timerNew(void)
+ {
+ return timerInit(checkPtr(nmMalloc(sizeof(Timer))));
+ }
+
+/*** Start timing. If the timer was already timing, does nothing.
+ ***
+ *** @param timer The timer to start.
+ *** @returns `timer`, for chaining.
+ ***/
+pTimer
+timerStart(pTimer timer)
+ {
+ if (UNLIKELY(timer == NULL)) return NULL;
+ if (isnan(timer->start)) timer->start = getTime();
+
+ return timer;
+ }
+
+/*** Stop timing and add the elapsed time to the timer total. If the timer
+ *** isn't running, or if the clock can't be read, does nothing.
+ ***
+ *** @param timer The timer to stop.
+ *** @returns `timer`, for chaining.
+ ***/
+pTimer
+timerStop(pTimer timer)
+ {
+ if (UNLIKELY(timer == NULL)) return NULL;
+ if (isnan(timer->start)) return timer;
+
+ /** Keep the timer running rather than poisoning the total with NAN. **/
+ const double stop_time = getTime();
+ if (isnan(stop_time)) return timer;
+
+ timer->total += stop_time - timer->start;
+ timer->start = NAN;
+
+ return timer;
+ }
+
+/*** Get the total time that elapsed while the timer was running.
+ ***
+ *** @param timer The timer to read.
+ *** @returns The total time in seconds, or NAN if an error occurs.
+ ***/
+double
+timerGet(pTimer timer)
+ {
+ if (UNLIKELY(timer == NULL)) return NAN;
+
+ const double current_time = (isnan(timer->start)) ? 0.0 : (getTime() - timer->start);
+
+ return current_time + timer->total;
+ }
+
+/*** Reset a timer to its initial state, where it isn't started and no time
+ *** has elapsed yet.
+ ***
+ *** @param timer The timer to reset.
+ *** @returns `timer`, for chaining.
+ ***/
+pTimer
+timerReset(pTimer timer)
+ {
+ return timerInit(timer);
+ }
+
+/*** De-initialize a timer allocated by timerInit().
+ ***
+ *** @param timer The timer to de-initialize.
+ ***/
+void
+timerDeInit(pTimer timer) {}
+
+/*** De-initialize and free a timer allocated by timerNew().
+ ***
+ *** @param timer The timer to free.
+ ***/
+void
+timerFree(pTimer timer)
+ {
+ if (UNLIKELY(timer == NULL)) return;
+
+ timerDeInit(timer);
+ nmFree(timer, sizeof(Timer));
+
+ return;
+ }
diff --git a/centrallix-lib/src/util.c b/centrallix-lib/src/util.c
index 629b59c79..c4f4234c5 100644
--- a/centrallix-lib/src/util.c
+++ b/centrallix-lib/src/util.c
@@ -1,24 +1,35 @@
/************************************************************************/
-/* Centrallix Application Server System */
+/* Centrallix Application Server System */
/* Centrallix Base Library */
/* */
-/* Copyright (C) 1998-2011 LightSys Technology Services, Inc. */
+/* Copyright (C) 1998-2026 LightSys Technology Services, Inc. */
/* */
/* You may use these files and this library under the terms of the */
/* GNU Lesser General Public License, Version 2.1, contained in the */
/* included file "COPYING". */
/* */
-/* Module: (util.c,.h) */
-/* Author: Micah Shennum */
-/* Date: May 26, 2011 */
-/* Description: Collection of utilities */
+/* Module: util.c, util.h */
+/* Author: Micah Shennum and Israel Fuller */
+/* Date: May 26, 2011 and October 13, 2025 (respectively) */
+/* Description: Collection of utilities including: */
+/* - Utilities for parsing numbers. */
+/* - snprintBytes() for formatting a byte count. */
+/* - snprintCommasLlu() for formatting large numbers. */
+/* - fprintMem() for printing memory stats. */
/************************************************************************/
+#include
+#include
+#include
+#include
#include
-
#include
-#include
-#include
+#include
+
+#include "check.h"
+#include "newmalloc.h"
+#include "range.h"
+
#include "util.h"
/**
@@ -77,3 +88,148 @@ unsigned int strtoui(const char *nptr, char **endptr, int base){
//return as tmp;
return (unsigned int)tmp;
}
+
+/*** snprintBytes() allows one to pick between CS units, where the kibibyte
+ *** (KiB) is 1024 bytes, and metric units where the kilobyte (KB) is 1000 bytes.
+ *** Fun Fact: Windows uses kibibytes, but displays them as KB.
+ ***/
+#define USE_METRIC false
+static char* units_cs[] = {"bytes", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"};
+static char* units_metric[] = {"bytes", "KB", "MB", "GB", "TB", "PB", "EB"};
+#define N_UNITS ((unsigned int)(sizeof(units_cs) / sizeof(units_cs[0])))
+
+/*** Displays a size in bytes using the largest unit where the result would be
+ *** at least 1.0. Units up to the exbibyte (EiB) and exabyte (EB) are
+ *** supported, which is enough for any unsigned long: the largest possible
+ *** value is 18,446,744,073,709,551,615, which is just under 16 EiB (or
+ *** approximately 18.45 EB).
+ ***
+ *** @param buf The buffer to which new text will be written, using snprintf().
+ *** @param buf_size The amount of space in the buffer, passed to snprintf().
+ *** It is recommended to provide a buffer that is at least 12 characters
+ *** long to avoid truncation.
+ *** @param bytes The number of bytes, which will be formatted and written
+ *** to the buffer.
+ *** @returns buf, for chaining.
+ ***/
+char*
+snprintBytes(char* buf, const size_t buf_size, unsigned long bytes)
+ {
+ char** units = (USE_METRIC) ? units_metric : units_cs;
+ const double unit_size = (USE_METRIC) ? 1000.0 : 1024.0;
+
+ /** Search for the largest unit where the value would be at least 1. **/
+ const double size = (double)bytes;
+ for (unsigned char i = N_UNITS - 1; i >= 1u; i--)
+ {
+ const double denominator = pow(unit_size, i);
+ if (size >= denominator)
+ {
+ const double converted_size = size / denominator;
+ if (converted_size >= 100.0)
+ snprintf(buf, buf_size, "%.5g %s", converted_size, units[i]);
+ else if (converted_size >= 10.0)
+ snprintf(buf, buf_size, "%.4g %s", converted_size, units[i]);
+ else /* if (converted_size >= 1.0) - Always true. */
+ snprintf(buf, buf_size, "%.3g %s", converted_size, units[i]);
+ return buf;
+ }
+ }
+
+ /** None of the larger units work, so we just use bytes. **/
+ snprintf(buf, buf_size, "%lu %s", bytes, units[0]);
+
+ return buf;
+ }
+#undef N_UNITS
+
+/*** Print a large number formatted with comas to a buffer.
+ ***
+ *** @param buf The buffer to print the number into.
+ *** @param buf_size The maximum number of characters to add to the buffer.
+ *** @param value The value to write into the buffer.
+ *** @returns `buf`, or NULL if `buf_size` is 0.
+ */
+char*
+snprintCommasLlu(char* buf, size_t buf_size, unsigned long long value)
+ {
+ if (buf_size == 0) return NULL;
+ if (value == 0)
+ {
+ if (buf_size > 1) { buf[0] = '0'; buf[1] = '\0'; }
+ else buf[0] = '\0';
+ return buf;
+ }
+
+ /*** Write the number to the string in reverse order, adding commas as
+ *** they are needed.
+ ***/
+ char tmp[32];
+ unsigned int ti = 0;
+ while (value > 0 && ti < sizeof(tmp) - 1)
+ {
+ if (ti % 4 == 3) tmp[ti++] = ',';
+ tmp[ti++] = '0' + (value % 10);
+ value /= 10;
+ }
+ tmp[ti] = '\0';
+
+ unsigned int outlen = min(ti, buf_size - 1u);
+ for (unsigned int i = 0u; i < outlen; i++) buf[i] = tmp[ti - i - 1];
+ buf[outlen] = '\0';
+
+ return buf;
+ }
+
+/** Print summary the current memory in use to the file pointer. **/
+void
+fprintMem(FILE* out)
+ {
+ FILE* fp = fopen("/proc/self/statm", "r");
+ if (fp == NULL) { perror("fopen()"); return; }
+
+ /** Get page counts. **/
+ long size, resident, share, text, lib, data, dt;
+ if (fscanf(fp, "%ld %ld %ld %ld %ld %ld %ld",
+ &size, &resident, &share, &text, &lib, &data, &dt) != 7)
+ {
+ fprintf(stderr, "Failed to read memory info\n");
+ check(fclose(fp)); /* Failure ignored. */
+ return;
+ }
+ check(fclose(fp)); /* Failure ignored. */
+
+ if (resident < 0)
+ {
+ if (resident != -1)
+ fprintf(stderr, "Unexpected value for resident page count: %ld.\n", resident);
+
+ printFail("Failed to get resident page count");
+ return;
+ }
+
+ /** Get page size. **/
+ const long page_size = sysconf(_SC_PAGESIZE); /* in bytes */
+ if (page_size < 0)
+ {
+ if (page_size != -1)
+ fprintf(stderr, "Unexpected value for page size: %ld.\n", page_size);
+
+ printFail("Failed to get page size");
+ return;
+ }
+
+ /** Get the number of resident bytes used. **/
+ const unsigned long resident_bytes = (unsigned long)resident * (unsigned long)page_size;
+ char buf[16];
+ snprintBytes(buf, sizeof(buf), resident_bytes);
+
+ /** fprintf() out data. **/
+ fprintf(out, "Memory used: %lu bytes (%s)\n", resident_bytes, buf);
+ fprintf(out,
+ "Share %ldb, Text %ldb, Lib %ldb, Data %ldb\n",
+ share * page_size, text * page_size, lib * page_size, data * page_size
+ );
+
+ return;
+ }
diff --git a/centrallix-lib/src/xarray.c b/centrallix-lib/src/xarray.c
index 477e2fe24..9fb94dba0 100644
--- a/centrallix-lib/src/xarray.c
+++ b/centrallix-lib/src/xarray.c
@@ -6,13 +6,14 @@
#include
#include
#include "xarray.h"
+#include "check.h"
#include "newmalloc.h"
/************************************************************************/
/* Centrallix Application Server System */
/* Centrallix Base Library */
/* */
-/* Copyright (C) 1998-2001 LightSys Technology Services, Inc. */
+/* Copyright (C) 1998-2026 LightSys Technology Services, Inc. */
/* */
/* You may use these files and this library under the terms of the */
/* GNU Lesser General Public License, Version 2.1, contained in the */
@@ -408,4 +409,45 @@ int xaInsertAfter(pXArray this, int index, void* item)
return index+1;
}
+/*** Trims an xArray so that the allocated space matches the number of items
+ *** in the array.
+ ***
+ *** @param this The array to be trimmed.
+ *** @returns 0 if successful, or -1 if an error occurs.
+ ***/
+int
+xaTrim(pXArray this)
+ {
+ /** Reallocating to size 0 is undefined behavior, so skip. **/
+ if (this->nItems == 0)
+ return 0;
+
+ /** Allocate the new internal items array. **/
+ const size_t new_size = this->nItems * sizeof(void*);
+ void* new_items = checkPtr(nmSysRealloc(this->Items, new_size));
+ if (new_items == NULL) return -1;
+
+ /** Update the struct. **/
+ this->Items = new_items;
+ this->nAlloc = this->nItems;
+
+ return 0;
+ }
+/*** Returns a new array with a shallow copy of the data of the xArray. This
+ *** new array is allocated with `nmSysMalloc()` and is the exact length that
+ *** is needed to store the items in the xArray.
+ ***
+ *** @param this The array to be read.
+ *** @returns The new array, or NULL if an error occurs.
+ ***/
+void**
+xaToArray(pXArray this)
+ {
+ const size_t size = this->nItems * sizeof(void*);
+ void** result = checkPtr(nmSysMalloc(size));
+ if (result == NULL) return NULL;
+ memcpy(result, this->Items, size);
+
+ return result;
+ }
diff --git a/centrallix-lib/src/xhash.c b/centrallix-lib/src/xhash.c
index afeb432b5..aad795cd4 100644
--- a/centrallix-lib/src/xhash.c
+++ b/centrallix-lib/src/xhash.c
@@ -13,7 +13,7 @@
/* Centrallix Application Server System */
/* Centrallix Base Library */
/* */
-/* Copyright (C) 1998-2001 LightSys Technology Services, Inc. */
+/* Copyright (C) 1998-2026 LightSys Technology Services, Inc. */
/* */
/* You may use these files and this library under the terms of the */
/* GNU Lesser General Public License, Version 2.1, contained in the */
@@ -290,4 +290,103 @@ xhClear(pXHashTable this, int (*free_fn)(), void* free_arg)
return 0;
}
+/*** Executes an operation on each entry of the hash table.
+ ***
+ *** @param this The affected hash table (passing NULL causes undefined
+ *** behavior).
+ *** @param callback_fn A callback function to be called on each hash table
+ *** entry. It takes 2 parameters: the current hash table entry and a void*
+ *** argument specified using each_arg. If any invocation of the callback
+ *** function returns a value other than 0, xhForEach() will immediately
+ *** fail, returning that value as the error code.
+ *** @param each_arg An additional argument which will be passed to each
+ *** invocation of the callback function.
+ *** @returns 0 if the function executes successfully.
+ *** 1 if the callback function is NULL.
+ *** n (where n != 0) if the callback function returns n.
+ ***/
+int
+xhForEach(pXHashTable this, int (*callback_fn)(pXHashEntry, void*), void* each_arg)
+ {
+ if (callback_fn == NULL) return 1;
+
+ for (int row = 0; row < this->nRows; row++)
+ {
+ pXHashEntry entry = (pXHashEntry)(this->Rows.Items[row]);
+ while (entry != NULL)
+ {
+ pXHashEntry next = entry->Next;
+ const int ret = callback_fn(entry, each_arg);
+ if (ret != 0) return ret;
+ entry = next;
+ }
+ }
+
+ return 0;
+ }
+
+/*** A helper function for `xhClearKeySafe()`. Deallocates a hash table entry
+ *** after calling the appropriate free function with the provided free arg.
+ ***
+ *** @param entry A pointer to the hash table entry to be freed (passing NULL
+ *** causes undefined behavior).
+ *** @param arg A pointer to a void* array with 2 elements: The first element
+ *** is a function pointer to the free function, which we invoke using the
+ *** provided entry and the free_arg, specified as the second element of
+ *** this array.
+ *** @returns 0, success.
+ ****/
+static int
+xh_i_FreeEntry(pXHashEntry entry, void* arg)
+ {
+ /*** The passed void* actually points to a void* array with 2 elements.
+ ***
+ *** The first element is a function pointer to the free function, which
+ *** we invoke using the provided entry and the free_arg, specified as the
+ *** second element of the array.
+ ***
+ *** Interestingly, you can write this code in one line like this:
+ *** ((void (*)(pXHashEntry, void*))((void**)arg)[0])(entry, ((void**)arg)[1]);
+ *** But I value code readability, so fortunately, I can't be THAT clever...
+ ***/
+ void** args = (void**)arg;
+ void (*free_fn)(pXHashEntry, void*) = args[0];
+ free_fn(entry, args[1]);
+
+ /** Free the entry. **/
+ nmFree(entry, sizeof(XHashEntry));
+
+ return 0;
+ }
+/*** Clears all contents from a hash table. The free function is passed each
+ *** hash entry struct, allowing it to free both the value and key, if needed.
+ ***
+ *** @param this The affected hash table (passing NULL causes undefined
+ *** behavior).
+ *** @param free_fn A pointer to a free function which will be called with a
+ *** pointer to each `XHashEntry` before they are deallocated. It is also
+ *** passed a `void*`, which will be `free_arg` (the third argument).
+ *** @param free_arg The void pointer value passed to the free function.
+ *** @returns 0 if successful, or
+ *** -1 if `free_fn()` is `NULL`.
+ ***/
+int
+xhClearKeySafe(pXHashTable this, void (*free_fn)(pXHashEntry, void*), void* free_arg)
+ {
+ if (free_fn == NULL) return -1;
+
+ /** Free each row. **/
+ void* args[2] = {free_fn, free_arg};
+ const int ret = xhForEach(this, xh_i_FreeEntry, args);
+
+ /** Mark all rows as empty. **/
+ for (int i = 0; i < this->nRows; i++)
+ this->Rows.Items[i] = NULL;
+ this->nItems = 0;
+
+ /*** We are successful only if the free function didn't fail (and it should
+ *** not be able to fail).
+ ***/
+ return ret;
+ }
diff --git a/centrallix-lib/tests/t_driver.c b/centrallix-lib/tests/t_driver.c
index 20c0091c3..7949b0231 100644
--- a/centrallix-lib/tests/t_driver.c
+++ b/centrallix-lib/tests/t_driver.c
@@ -1,30 +1,30 @@
-#include "cxlibconfig-internal.h"
-#include "mtask.h"
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-
/************************************************************************/
/* Centrallix Application Server System */
/* Centrallix Base Library */
/* */
-/* Copyright (C) 2005 LightSys Technology Services, Inc. */
+/* Copyright (C) 2005-2026 LightSys Technology Services, Inc. */
/* */
/* You may use these files and this library under the terms of the */
/* GNU Lesser General Public License, Version 2.1, contained in the */
/* included file "COPYING". */
/* */
-/* Module: test_00baseline.c */
+/* Module: test_00baseline.c */
/* Author: Greg Beeley (GRB) */
-/* Creation: March 11th, 2005 */
-/* Description: Test suite entry to generate a baseline comparison value*/
+/* Creation: March 11th, 2005 */
+/* Description: Test suite driver for centrallix-lib tests. */
/************************************************************************/
+#include
+#include
+#include
+#include
+#include
+
+#include "cxlibconfig-internal.h"
+#include "mtask.h"
+#include "test_utils.h"
+#include "util.h"
+
long long test(char**);
@@ -36,12 +36,14 @@ segv_handler(int v)
printf("%-62.62s CRASH\n", tname);
exit(0);
}
+
void
abort_handler(int v)
{
printf("%-62.62s ABORT\n", tname);
exit(0);
}
+
void
alarm_handler(int v)
{
@@ -56,19 +58,61 @@ start(void* v)
clock_t start,end;
long long rval;
+ /** Setup handlers for signals that may occur during a test. **/
signal(SIGSEGV, segv_handler);
signal(SIGABRT, abort_handler);
signal(SIGALRM, alarm_handler);
- alarm(10);
+ alarm(LOCKUP_SECONDS);
+
+ /** Run the test while tracking CPU time. **/
times(&t);
start = t.tms_utime + t.tms_stime + t.tms_cutime + t.tms_cstime;
rval = test(&tname);
times(&t);
end = t.tms_utime + t.tms_stime + t.tms_cutime + t.tms_cstime;
+
+ /** Print test results. **/
if (rval < 0)
printf("%-62.62s FAIL\n", tname);
else
- printf("%-62.62s PASS %lld\n", tname, rval*100/(long long)(end - start));
+ {
+ long long duration = end - start;
+ if (duration == 0)
+ {
+ printf("%-62.62s PASS ???\n", tname);
+ printf("Warning: Test ran too fast! Ops/sec could not be measured. Please run tests in a loop or use loopTest() from test_utils.h.\n");
+ return;
+ }
+ double ops_per_second = rval * (100.0 / duration);
+
+ /** Round to four significant figures. **/
+ int precision = 3;
+ unsigned long long factor = 1;
+ double scaled = ops_per_second;
+ while (scaled >= 10.0)
+ {
+ scaled /= 10.0;
+ if (precision > 0)
+ precision--;
+ else
+ factor *= 10;
+ }
+ while (scaled > 0.0 && scaled < 1.0)
+ {
+ scaled *= 10.0;
+ precision++;
+ }
+
+ /** Print whole numbers with commas. **/
+ if (precision == 0)
+ {
+ char buf[32];
+ unsigned long long rounded = (unsigned long long)(ops_per_second / factor + 0.5) * factor;
+ printf("%-62.62s PASS %s\n", tname, snprintCommasLlu(buf, sizeof(buf), rounded));
+ }
+ else
+ printf("%-62.62s PASS %.*f\n", tname, precision, ops_per_second);
+ }
return;
}
@@ -79,4 +123,3 @@ main(int argc, char* argv[])
mtInitialize(0, start);
return 0;
}
-
diff --git a/centrallix-lib/tests/test_00baseline.c b/centrallix-lib/tests/test_00baseline.c
index b2012c6b1..72773400d 100644
--- a/centrallix-lib/tests/test_00baseline.c
+++ b/centrallix-lib/tests/test_00baseline.c
@@ -3,17 +3,26 @@
#include
#include
#include
+#include
+#include "test_utils.h"
-long long
-test(char** tname)
+/** No-op operations per pass, enough that the loop dominates the timing. **/
+#define OPS_PER_PASS (1000*1000)
+
+static bool
+doTest(void)
{
int i;
- int iter;
int array[2] = {0};
- *tname = "BASELINE - should pass";
- iter = 1000*1000*300;
- for(i=0;i
+#include
+#include
+#include
+#include
+
+/** Test dependencies. **/
+#include "test_utils.h"
+
+/** Tested module. **/
+#include "clusters.h"
+
+static bool doTest(void)
+ {
+ bool success = true;
+
+ /** Kitten tests. **/
+ success &= EXPECT_EQL(ca_edit_dist("kitten", "kitten", 0, 0), 0, "%d"); /* 0 edits. */
+ success &= EXPECT_EQL(ca_edit_dist("kitten", "skitten", 0, 0), 1, "%d"); /* 1 insert. */
+ success &= EXPECT_EQL(ca_edit_dist("kitten", "itten", 0, 0), 1, "%d"); /* 1 delete. */
+ success &= EXPECT_EQL(ca_edit_dist("kitten", "mitten", 0, 0), 1, "%d"); /* 1 replace. */
+ success &= EXPECT_EQL(ca_edit_dist("kitten", "smitten", 0, 0), 2, "%d"); /* 1 insert and 1 replace. */
+ success &= EXPECT_EQL(ca_edit_dist("kitten", "iktten", 0, 0), 1, "%d"); /* 1 transpose. */
+ success &= EXPECT_EQL(ca_edit_dist("kitten", "kittens", 0, 0), 1, "%d"); /* 1 insert (end). */
+ success &= EXPECT_EQL(ca_edit_dist("kitten", "kitte", 0, 0), 1, "%d"); /* 1 delete (end). */
+ success &= EXPECT_EQL(ca_edit_dist("kitten", "kittem", 0, 0), 1, "%d"); /* 1 replace (end). */
+ success &= EXPECT_EQL(ca_edit_dist("kitten", "kittne", 0, 0), 1, "%d"); /* 1 transpose (end). */
+
+ /** Alternate words. **/
+ success &= EXPECT_EQL(ca_edit_dist("lawn", "flown", 0, 0), 2, "%d"); /* 1 insert and one replace. */
+ success &= EXPECT_EQL(ca_edit_dist("hello", "hello!", 0, 0), 1, "%d"); /* 1 insert (end). */
+ success &= EXPECT_EQL(ca_edit_dist("zert", "zerf", 0, 0), 1, "%d"); /* 1 replace (end). */
+ success &= EXPECT_EQL(ca_edit_dist("llearr", "lear", 0, 0), 2, "%d"); /* 2 deletes (start & end). */
+
+ /** Long strings for testing edge cases. **/
+ char* str1 = "This is a very long string!! I do not expect this function to need to process a string longer than this, because this string is a full 254 characters. That is pretty long. The object system limits strings to this size so we cannot make a longer string...";
+ const size_t str_size = (strlen(str1) + 1) * sizeof(char);
+ char* str2 = memcpy(malloc(str_size), str1, str_size);
+ char* str3 = "This is quite a lengthy string. I do not expect the function to compute any longer string since this one is a full 254 characters. That is plenty, even if someone adds many contact details to their record!! Thus, this test should cover most cases we see.";
+
+ /** Test edge cases. **/
+ success &= EXPECT_EQL(ca_edit_dist("", "", 0, 0), 0, "%d"); /* Identical, empty string: 0 edits. */
+ success &= EXPECT_EQL(ca_edit_dist(str1, str1, 0, 0), 0, "%d"); /* Identical, very long strings. */
+ success &= EXPECT_EQL(ca_edit_dist(str1, str2, 0, 0), 0, "%d"); /* Identical, very long strings (different pointers). */
+ success &= EXPECT_EQL(ca_edit_dist(str2, str3, 0, 0), 133, "%d"); /* 133 edits. */
+
+ /** Empty string comparsions. **/
+ success &= EXPECT_EQL(ca_edit_dist(str1, "", 0, 0), (int)strlen(str1), "%d");
+ success &= EXPECT_EQL(ca_edit_dist(str2, "", 0, 0), (int)strlen(str2), "%d");
+ success &= EXPECT_EQL(ca_edit_dist(str3, "", 0, 0), (int)strlen(str3), "%d");
+
+ /** Specifying lengths with overflows. **/
+ success &= EXPECT_EQL(ca_edit_dist("A string with edits", "A string without edits", 13, 13), 0, "%d");
+ success &= EXPECT_EQL(ca_edit_dist("A string with edits", "A string without edits", 13, 0), 9, "%d");
+ success &= EXPECT_EQL(ca_edit_dist("A string with edits", "A string without edits", 0, 13), 6, "%d");
+ success &= EXPECT_EQL(ca_edit_dist("A string with edits", "A string without edits", 0, 0), 3, "%d");
+ success &= EXPECT_EQL(ca_edit_dist("A string with edits", "A string without edits", 19, 0), 3, "%d");
+ success &= EXPECT_EQL(ca_edit_dist("A string with edits", "A string without edits", 0, 19), 6, "%d");
+
+ /** Test for errors in identical pointer optimizations with specified lengths. **/
+ char identical_string[] = "Identical String!";
+ success &= EXPECT_EQL(ca_edit_dist(identical_string, identical_string, 0, 0), 0, "%d");
+ success &= EXPECT_EQL(ca_edit_dist(identical_string, identical_string, 0, 17), 0, "%d");
+ success &= EXPECT_EQL(ca_edit_dist(identical_string, identical_string, 17, 0), 0, "%d");
+ success &= EXPECT_EQL(ca_edit_dist(identical_string, identical_string, 0, 16), 1, "%d");
+ success &= EXPECT_EQL(ca_edit_dist(identical_string, identical_string, 0, 8), 9, "%d");
+ success &= EXPECT_EQL(ca_edit_dist(identical_string, identical_string, 5, 6), 1, "%d");
+ success &= EXPECT_EQL(ca_edit_dist(identical_string, identical_string, 16, 13), 3, "%d");
+ success &= EXPECT_EQL(ca_edit_dist(identical_string, identical_string, 0, 1), 16, "%d");
+ success &= EXPECT_EQL(ca_edit_dist(identical_string, identical_string, 1, 0), 16, "%d");
+
+ return success;
+ }
+
+long long test(char** tname)
+ {
+ *tname = "cluster-00 ca_edit_dist()";
+ return loopTest(doTest) * 36;
+ }
diff --git a/centrallix-lib/tests/test_clusters_01.c b/centrallix-lib/tests/test_clusters_01.c
new file mode 100644
index 000000000..dbf5ef585
--- /dev/null
+++ b/centrallix-lib/tests/test_clusters_01.c
@@ -0,0 +1,51 @@
+/************************************************************************/
+/* Centrallix Application Server System */
+/* Centrallix Base Library */
+/* */
+/* Copyright (C) 2025-2026 LightSys Technology Services, Inc. */
+/* */
+/* You may use these files and this library under the terms of the */
+/* GNU Lesser General Public License, Version 2.1, contained in the */
+/* included file "COPYING". */
+/* */
+/* Module: test_clusters_01.c */
+/* Author: Israel Fuller */
+/* Creation: November 25th, 2025 */
+/* Description: Test the ca_edit_dist() function from clusters.h. */
+/************************************************************************/
+
+#include
+#include
+#include
+#include
+#include
+
+/** Test dependencies. **/
+#include "test_utils.h"
+
+/** Tested module. **/
+#include "clusters.h"
+
+
+static bool doTest(void)
+ {
+ bool success = true;
+
+ /** Many, many replace edits. **/
+ const static unsigned short num_edits = 2039;
+ char AAA[num_edits + 1], BBB[num_edits + 1];
+ memset(AAA, 'A', num_edits);
+ memset(BBB, 'B', num_edits);
+ AAA[num_edits] = BBB[num_edits] = '\0';
+ success &= EXPECT_EQL(ca_edit_dist(AAA, "", 0, 0), num_edits, "%d");
+ success &= EXPECT_EQL(ca_edit_dist("", BBB, 0, 0), num_edits, "%d");
+ success &= EXPECT_EQL(ca_edit_dist(AAA, BBB, 0, 0), num_edits, "%d");
+
+ return success;
+ }
+
+long long test(char** tname)
+ {
+ *tname = "cluster-01 ca_edit_dist(): Stress test";
+ return loopTest(doTest) * 3;
+ }
diff --git a/centrallix-lib/tests/test_clusters_02.c b/centrallix-lib/tests/test_clusters_02.c
new file mode 100644
index 000000000..1ec6bc2b8
--- /dev/null
+++ b/centrallix-lib/tests/test_clusters_02.c
@@ -0,0 +1,85 @@
+/************************************************************************/
+/* Centrallix Application Server System */
+/* Centrallix Base Library */
+/* */
+/* Copyright (C) 2025-2026 LightSys Technology Services, Inc. */
+/* */
+/* You may use these files and this library under the terms of the */
+/* GNU Lesser General Public License, Version 2.1, contained in the */
+/* included file "COPYING". */
+/* */
+/* Module: test_clusters_02.c */
+/* Author: Israel Fuller */
+/* Creation: November 25th, 2025 */
+/* Description: Test the ca_build_vector() function from clusters.h. */
+/************************************************************************/
+
+#include
+#include
+#include
+#include
+#include
+
+/** Test dependencies. **/
+#include "test_utils.h"
+
+/** Tested module. **/
+#include "clusters.h"
+
+
+static bool doTest(void)
+ {
+ bool success = true;
+
+ /** Make an array to STORE() pointers to vectors so we can free them. **/
+ const unsigned int max_index = 16u;
+ unsigned int index = 0u;
+ pVector free_list[max_index];
+ #define STORE(v) (free_list[index++] = (v))
+ #define vec(s) STORE(ca_build_vector(s))
+
+ /** Edge case: Null string. **/
+ success &= EXPECT_EQL(ca_build_vector(NULL), NULL, "%p");
+
+ /** Edge case: Empty string. **/
+ success &= EXPECT_VEC_EQL(vec(""), ((int[]){-172, 11, -78}));
+
+ /** Single letter cases. **/
+ success &= EXPECT_VEC_EQL(vec("a"), ((int[]){-204, 12, -25, 12, -20}));
+ success &= EXPECT_VEC_EQL(vec("b"), ((int[]){-151, 13, -11, 13, -87}));
+ success &= EXPECT_VEC_EQL(vec("v"), ((int[]){-221, 7, -19, 7, -9}));
+
+ /** Multi-letter cases. **/
+ success &= EXPECT_VEC_EQL(vec("def"), ((int[]){-79, 4, -51, 2, -4, 7, -64, 9, -49}));
+ success &= EXPECT_VEC_EQL(vec("vec"), ((int[]){-37, 1, -175, 12, -18, 6, -8, 7, -9}));
+
+ /** White space and punctuation should be ignored. **/
+ success &= EXPECT_VEC_EQL(vec("Yippee!!!"), vec(">>->y i!&P^^_pe$/\n?e"));
+
+ /** Clean up using the free list. **/
+ if (index >= max_index)
+ {
+ printf(" > MEMORY ERROR!!\n");
+ printf(" > Allocated %u vectors, overflowing the free list of size %u.\n", index + 1, max_index);
+ printf(" > Increase the size of the free list (aka. max_index) to %u or more.\n", index + 1);
+ return false;
+ }
+ while (index > 0u)
+ {
+ pVector cur_vector = free_list[--index];
+ if (cur_vector == NULL) continue;
+ else ca_free_vector(cur_vector);
+ }
+
+ return success;
+ }
+
+long long test(char** tname)
+ {
+ *tname = "cluster-02 ca_build_vector()";
+ return loopTest(doTest) * 9;
+ }
+
+/** Clean up scope. **/
+#undef STORE
+#undef vec
diff --git a/centrallix-lib/tests/test_clusters_03.c b/centrallix-lib/tests/test_clusters_03.c
new file mode 100644
index 000000000..2eef4d240
--- /dev/null
+++ b/centrallix-lib/tests/test_clusters_03.c
@@ -0,0 +1,91 @@
+/************************************************************************/
+/* Centrallix Application Server System */
+/* Centrallix Base Library */
+/* */
+/* Copyright (C) 2025-2026 LightSys Technology Services, Inc. */
+/* */
+/* You may use these files and this library under the terms of the */
+/* GNU Lesser General Public License, Version 2.1, contained in the */
+/* included file "COPYING". */
+/* */
+/* Module: test_clusters_03.c */
+/* Author: Israel Fuller */
+/* Creation: November 26th, 2025 */
+/* Description: Test the ca_cos_compare() function from clusters.h. */
+/************************************************************************/
+
+#include
+#include
+#include
+#include
+#include
+
+/** Test dependencies. **/
+#include "test_utils.h"
+
+/** Tested module. **/
+#include "clusters.h"
+
+
+static bool doTest(void)
+ {
+ bool success = true;
+
+ /** Make an array to STORE() pointers to vectors so we can free them. **/
+ const unsigned int max_index = 32u;
+ unsigned int index = 0u;
+ pVector free_list[max_index];
+ #define STORE(v) (free_list[index++] = (v))
+
+ /** ca_cos_compare() shortcut macro. **/
+ #define cos_cmp(str1, str2) ({ \
+ pVector _v1 = STORE(ca_build_vector(str1)); \
+ pVector _v2 = STORE(ca_build_vector(str2)); \
+ ca_cos_compare(_v1, _v2); \
+ })
+
+ /** Basic tests of cosine similarity. **/
+ success &= EXPECT_RANGE(cos_cmp("hello", "hello"), 0.999, 1.0, "%g");
+ success &= EXPECT_RANGE(cos_cmp("hello", "zephora"), 0.0, 0.001, "%g");
+ success &= EXPECT_RANGE(cos_cmp("hello", "hello world"), 0.6, 0.7, "%g");
+ success &= EXPECT_RANGE(cos_cmp("hello there", "hellow there"), 0.9, 1.0, "%g");
+
+ /** Tests on fabricated contact information. */
+ /*** All email addresses and phone numbers are imaginary and were
+ *** fabricated for the purposes of this test.
+ ***/
+ success &= EXPECT_RANGE(cos_cmp("Cynthia Adams; cynthiaadams@gmail.com; 720-769-1293", "Timothy Adams; thetbear@gmail.com; 720-891-1470"), 0.49, 0.54, "%g");
+ success &= EXPECT_RANGE(cos_cmp("Timothy Adams; thetbear@gmail.com; 720-891-1470", "Lance Freson; lancetheturtle@gmail.com; 720-111-8189"), 0.45, 0.50, "%g");
+ success &= EXPECT_RANGE(cos_cmp("Lance Freson; lancetheturtle@gmail.com; 720-111-8189", "Gregory Freson; greatgregory@gmail.com; 720-198-5791"), 0.425, 0.475, "%g");
+ success &= EXPECT_RANGE(cos_cmp("Gregory Freson; greatgregory@gmail.com; 720-198-5791", "Gregory Freson; greatgregory@gmail.co; 720-198-5791"), 0.94, 0.99, "%g");
+ success &= EXPECT_RANGE(cos_cmp("Nathan Mayor; nmmayor@yahoo.com; +1-800-192-9128", "Mindy Mayor; nmmayor@yahoo.com; 720-981-9149"), 0.575, 0.625, "%g");
+ success &= EXPECT_RANGE(cos_cmp("This is an identical case", "This is an identical case"), 0.975, 1.00, "%g");
+ success &= EXPECT_RANGE(cos_cmp("Samuel", "Alex"), 0.00, 0.025, "%g");
+
+ /** Clean up scope. **/
+ #undef STORE
+ #undef cos_cmp
+
+ /** Clean up using the free list. **/
+ if (index >= max_index)
+ {
+ printf(" > MEMORY ERROR!!\n");
+ printf(" > Allocated %u vectors, overflowing the free list of size %u.\n", index + 1, max_index);
+ printf(" > Increase the size of the free list (aka. max_index) to %u or more.\n", index + 1);
+ return false;
+ }
+ while (index > 0u)
+ {
+ pVector cur_vector = free_list[--index];
+ if (cur_vector == NULL) continue;
+ else ca_free_vector(cur_vector);
+ }
+
+ return success;
+ }
+
+long long test(char** tname)
+ {
+ *tname = "cluster-03 ca_cos_compare()";
+ return loopTest(doTest) * 11;
+ }
diff --git a/centrallix-lib/tests/test_clusters_04.c b/centrallix-lib/tests/test_clusters_04.c
new file mode 100644
index 000000000..110d512e5
--- /dev/null
+++ b/centrallix-lib/tests/test_clusters_04.c
@@ -0,0 +1,70 @@
+/************************************************************************/
+/* Centrallix Application Server System */
+/* Centrallix Base Library */
+/* */
+/* Copyright (C) 2025-2026 LightSys Technology Services, Inc. */
+/* */
+/* You may use these files and this library under the terms of the */
+/* GNU Lesser General Public License, Version 2.1, contained in the */
+/* included file "COPYING". */
+/* */
+/* Module: test_clusters_04.c */
+/* Author: Israel Fuller */
+/* Creation: November 26th, 2025 */
+/* Description: Test the ca_lev_compare() function from clusters.h. */
+/************************************************************************/
+
+#include
+#include
+#include
+#include
+#include
+
+/** Test dependencies. **/
+#include "test_utils.h"
+
+/** Tested module. **/
+#include "clusters.h"
+
+
+static bool doTest(void)
+ {
+ bool success = true;
+
+ /** Basic tests of Levenshtein edit distance similarity. **/
+ success &= EXPECT_RANGE(ca_lev_compare("hello", "hello"), 0.99, 1.0, "%g");
+ success &= EXPECT_RANGE(ca_lev_compare("hello", "hello!"), 0.8, 1.0, "%g");
+ success &= EXPECT_RANGE(ca_lev_compare("hello", "asdfkh"), 0.0, 0.1, "%g");
+ success &= EXPECT_RANGE(ca_lev_compare("hello", "aaaaaaaaaaaaaaaaa"), 0.0, 0.1, "%g");
+ success &= EXPECT_RANGE(ca_lev_compare("hello", "nope"), 0.0, 0.2, "%g");
+ success &= EXPECT_RANGE(ca_lev_compare("hello", "noepo"), 0.15, 0.25, "%g");
+ success &= EXPECT_RANGE(ca_lev_compare("below", "hello!"), 0.4, 0.6, "%g");
+ success &= EXPECT_RANGE(ca_lev_compare("kitten", "smitten"), 0.65, 0.85, "%g");
+ success &= EXPECT_RANGE(ca_lev_compare("hello", "bobbobbobbob"), 0.0, 0.1, "%g");
+ success &= EXPECT_RANGE(ca_lev_compare("hello", ""), 0.0, 0.05, "%g");
+ success &= EXPECT_RANGE(ca_lev_compare("", ""), 0.99, 1.0, "%g");
+ success &= EXPECT_RANGE(ca_lev_compare("blooooop", "blob"), 0.3, 0.5, "%g");
+ success &= EXPECT_RANGE(ca_lev_compare("", "!"), 0.0, 0.01, "%g");
+ success &= EXPECT_RANGE(ca_lev_compare("h", "h"), 0.99, 1.0, "%g");
+ success &= EXPECT_RANGE(ca_lev_compare("hi", "hi"), 0.99, 1.0, "%g");
+
+ /** Kitten tests with specific edit operations. **/
+ success &= EXPECT_RANGE(ca_lev_compare("kitten", "kitten"), 0.99, 1.0, "%g");
+ success &= EXPECT_RANGE(ca_lev_compare("kitten", "skitten"), 0.8, 0.9, "%g");
+ success &= EXPECT_RANGE(ca_lev_compare("kitten", "itten"), 0.8, 0.9, "%g");
+ success &= EXPECT_RANGE(ca_lev_compare("kitten", "mitten"), 0.8, 0.9, "%g");
+ success &= EXPECT_RANGE(ca_lev_compare("kitten", "smitten"), 0.7, 0.8, "%g");
+ success &= EXPECT_RANGE(ca_lev_compare("kitten", "iktten"), 0.8, 0.9, "%g");
+ success &= EXPECT_RANGE(ca_lev_compare("kitten", "kittens"), 0.8, 0.9, "%g");
+ success &= EXPECT_RANGE(ca_lev_compare("kitten", "kitte"), 0.8, 0.9, "%g");
+ success &= EXPECT_RANGE(ca_lev_compare("kitten", "kittem"), 0.8, 0.9, "%g");
+ success &= EXPECT_RANGE(ca_lev_compare("kitten", "kittne"), 0.8, 0.9, "%g");
+
+ return success;
+ }
+
+long long test(char** tname)
+ {
+ *tname = "cluster-04 ca_lev_compare()";
+ return loopTest(doTest) * 25;
+ }
diff --git a/centrallix-lib/tests/test_clusters_05.c b/centrallix-lib/tests/test_clusters_05.c
new file mode 100644
index 000000000..84af473e3
--- /dev/null
+++ b/centrallix-lib/tests/test_clusters_05.c
@@ -0,0 +1,185 @@
+/************************************************************************/
+/* Centrallix Application Server System */
+/* Centrallix Base Library */
+/* */
+/* Copyright (C) 2025-2026 LightSys Technology Services, Inc. */
+/* */
+/* You may use these files and this library under the terms of the */
+/* GNU Lesser General Public License, Version 2.1, contained in the */
+/* included file "COPYING". */
+/* */
+/* Module: test_clusters_05.c */
+/* Author: Israel Fuller */
+/* Creation: November 26th, 2025 */
+/* Description: Test the ca_most_similar() function from clusters.h. */
+/************************************************************************/
+
+#include
+#include
+#include
+#include
+#include
+
+/** Test dependencies. **/
+#include "test_utils.h"
+#include "check.h"
+#include "xhash.h"
+
+/** Tested module. **/
+#include "clusters.h"
+
+
+/** Mock similarity function. **/
+static const unsigned int key_length = 64u;
+static pXHashTable mock_sims = NULL;
+static bool* success_ptr = NULL;
+static double getSimMock(void* v1, void* v2)
+ {
+ char key[key_length];
+ char* str1 = v1;
+ char* str2 = v2;
+
+ /** Try key1, key2. **/
+ snprintf(key, sizeof(key), "%s|%s", str1, str2);
+ double* sim = (double*)xhLookup(mock_sims, key);
+ if (sim != NULL) goto found;
+
+ /** Try key2, key1. **/
+ snprintf(key, sizeof(key), "%s|%s", str2, str1);
+ sim = (double*)xhLookup(mock_sims, key);
+ if (sim != NULL) goto found;
+
+ /** Key not found. **/
+ fprintf(stderr, " > getSimMock(\"%s\", \"%s\"): No sim provided!\n", str1, str2);
+ *success_ptr = false;
+ return NAN;
+
+ found:
+ /** Key found. **/
+ return *sim;
+ }
+
+static int do_nothing() { return 0; }
+
+static bool doTest(void)
+ {
+ bool success = true;
+
+ /** Check error cases. **/
+ success &= EXPECT_STR_EQL(ca_most_similar(NULL, (void*[]){"str_abc", "str1"}, 2, ca_lev_compare, 0.0), NULL);
+ success &= EXPECT_STR_EQL(ca_most_similar("str", NULL, 2, ca_lev_compare, 0.0), NULL);
+ success &= EXPECT_STR_EQL(ca_most_similar("str", (void*[]){"str_abc", "str1"}, 0, ca_lev_compare, 0.0), NULL);
+ success &= EXPECT_STR_EQL(ca_most_similar("str", (void*[]){"str_abc", "str1"}, 2, NULL, 0.0), NULL);
+ success &= EXPECT_STR_EQL(ca_most_similar("str", (void*[]){"str_abc", "str1"}, 2, ca_lev_compare, 1.1), NULL);
+ success &= EXPECT_STR_EQL(ca_most_similar("str", (void*[]){"str_abc", "str1"}, 2, ca_lev_compare, -0.1), NULL);
+ success &= EXPECT_STR_EQL(ca_most_similar("str", (void*[]){"str_abc", "str1"}, 2, ca_lev_compare, INFINITY), NULL);
+ success &= EXPECT_STR_EQL(ca_most_similar("str", (void*[]){"str_abc", "str1"}, 2, ca_lev_compare, -INFINITY), NULL);
+ success &= EXPECT_STR_EQL(ca_most_similar("str", (void*[]){"str_abc", "str1"}, 2, ca_lev_compare, NAN), NULL);
+
+ /** Simple test cases. **/
+ success &= EXPECT_STR_EQL(ca_most_similar("str1", (void*[]){"str_abc", "str1"}, 2, ca_lev_compare, 0.0), "str1");
+ success &= EXPECT_STR_EQL(ca_most_similar("str", (void*[]){"str_abc", "str1"}, 2, ca_lev_compare, 0.0), "str1");
+ success &= EXPECT_STR_EQL(ca_most_similar("kitten", (void*[]){"str_abc", "str1"}, 2, ca_lev_compare, 0.0), "str1");
+ success &= EXPECT_STR_EQL(ca_most_similar("str1", (void*[]){"str2", "str", "eight"}, 3, ca_lev_compare, 0.0), "str2");
+
+ /** Many, identically similar options. */
+ success &= EXPECT_STR_EQL(ca_most_similar("kitten",
+ (void*[]){"skitten", "itten", "mitten", "iktten", "kittens", "kitte", "kittem", "kittne"}, 8,
+ ca_lev_compare, 0.0), "skitten");
+
+ /** Pointer-perfect handling. **/
+ char* target = "string";
+ success &= EXPECT_EQL((char*)ca_most_similar(target, (void*[]){"str", target}, 2, ca_lev_compare, 0.0), target, "%s");
+
+ /** List overflow. **/
+ success &= EXPECT_STR_EQL(ca_most_similar("target", (void*[]){"str1", "targets", "target", "walmart"}, 2, ca_lev_compare, 0.0), "targets");
+
+ /** Threshold exceeded. **/
+ success &= EXPECT_STR_EQL(ca_most_similar("blob", (void*[]){"blooooop", "targets", "string"}, 3, ca_lev_compare, 0.0), "blooooop");
+ success &= EXPECT_STR_EQL(ca_most_similar("blob", (void*[]){"blooooop", "targets", "string"}, 3, ca_lev_compare, 0.5), NULL);
+ success &= EXPECT_STR_EQL(ca_most_similar("hello", (void*[]){"bane", "noepo", "stars"}, 3, ca_lev_compare, 0.0), "noepo");
+ success &= EXPECT_STR_EQL(ca_most_similar("hello", (void*[]){"bane", "noepo", "stars"}, 3, ca_lev_compare, 0.25), NULL);
+ success &= EXPECT_STR_EQL(ca_most_similar("kitten", (void*[]){"skitten", "fit"}, 2, ca_lev_compare, 0.0), "skitten");
+ success &= EXPECT_STR_EQL(ca_most_similar("kitten", (void*[]){"skitten", "fit"}, 2, ca_lev_compare, 0.9), NULL);
+
+ /** Make an array to STORE() pointers to vectors so we can free them. **/
+ const unsigned int max_index = 32u;
+ unsigned int index = 0u;
+ pVector free_list[max_index];
+ #define STORE(v) (free_list[index++] = (v))
+ #define vec(s) STORE(ca_build_vector(s))
+
+ /** Alternative similarity function. **/
+ pVector hello = vec("hello"), fellow = vec("fellow"), felon = vec("felon");
+ pVector held = vec("held"), zephora = vec("zephora"), hexza = vec("hexza");
+ pVector hello_there = vec("hello there"), hello_world = vec("hello world");
+ pVector hellow_there = vec("hellow there");
+ success &= EXPECT_VEC_EQL(ca_most_similar(hello, (void*[]){fellow, felon, hello, held}, 4, ca_cos_compare, 0.0), hello);
+ success &= EXPECT_VEC_EQL(ca_most_similar(hello, (void*[]){zephora, hello_world, hexza}, 3, ca_cos_compare, 0.0), hello_world);
+ success &= EXPECT_VEC_EQL(ca_most_similar(hello, (void*[]){zephora, hello_world}, 1, ca_cos_compare, 0.0), zephora);
+ success &= EXPECT_VEC_EQL(ca_most_similar(hello, (void*[]){zephora}, 1, ca_cos_compare, 0.0), zephora);
+ success &= EXPECT_VEC_EQL(ca_most_similar(hello_there, (void*[]){hello_world, zephora, hellow_there, hexza}, 4, ca_cos_compare, 0.0), hellow_there);
+ success &= EXPECT_VEC_EQL(ca_most_similar(hello_there, (void*[]){hello_world, zephora, hellow_there}, 2, ca_cos_compare, 0.0), hello_world);
+ success &= EXPECT_VEC_EQL(ca_most_similar(hello_there, (void*[]){hello_world, zephora, hellow_there}, 2, ca_cos_compare, 0.8), NULL);
+
+ /** Special characters (ignored by the similarity function). **/
+ pVector yip = vec("Yippee!!!");
+ pVector str1 = vec("@*#((%^!&@*-+!"), str2 = vec(">>->y i!&P^^_pe$/\n?e"), str3 = vec("yip");
+ success &= EXPECT_VEC_EQL(yip, str2);
+ success &= EXPECT_VEC_EQL(ca_most_similar(yip, (void*[]){str1, str2, str3}, 3, ca_cos_compare, 0.0), str2);
+ success &= EXPECT_VEC_EQL(ca_most_similar(yip, (void*[]){str1, str2, str3}, 3, ca_cos_compare, 1.0), str2);
+
+ /** Clean up scope. **/
+ #undef STORE
+ #undef vec
+
+ /** Clean up using the free list. **/
+ if (index >= max_index)
+ {
+ printf(" > MEMORY ERROR!!\n");
+ printf(" > Allocated %u vectors, overflowing the free list of size %u.\n", index + 1, max_index);
+ printf(" > Increase the size of the free list (aka. max_index) to %u or more.\n", index + 1);
+ return false;
+ }
+ while (index > 0u)
+ {
+ pVector cur_vector = free_list[--index];
+ if (cur_vector == NULL) continue;
+ else ca_free_vector(cur_vector);
+ }
+
+ /** Set up the mock similarity function. **/
+ XHashTable sim_table;
+ if (check(xhInit(&sim_table, 64, 0)) != 0) return false;
+ mock_sims = &sim_table;
+ success_ptr = &success;
+
+ /** Completely different strings are similar. **/
+ double str1_str = 0.2, str1_str2 = 0.1, str1_eight = 0.8;
+ if (check(xhAdd(&sim_table, "str1|str", (void*)&str1_str)) != 0) return false;
+ if (check(xhAdd(&sim_table, "str1|str2", (void*)&str1_str2)) != 0) return false;
+ if (check(xhAdd(&sim_table, "str1|eight", (void*)&str1_eight)) != 0) return false;
+ success &= EXPECT_STR_EQL(ca_most_similar("str1", (void*[]){"str2", "str", "eight"}, 3, getSimMock, 0.0), "eight");
+ success &= EXPECT_STR_EQL(ca_most_similar("str1", (void*[]){"str2", "str", "eight"}, 3, getSimMock, 0.9), NULL);
+ if (check(xhClear(&sim_table, do_nothing, NULL)) != 0) return false;
+
+ /** Nans are skipped. **/
+ double val_nan = 0.8, val_vals = NAN, val_val = 0.2;
+ if (check(xhAdd(&sim_table, "val|nan", (void*)&val_nan)) != 0) return false;
+ if (check(xhAdd(&sim_table, "val|vals", (void*)&val_vals)) != 0) return false;
+ if (check(xhAdd(&sim_table, "val|val", (void*)&val_val)) != 0) return false;
+ success &= EXPECT_STR_EQL(ca_most_similar("val", (void*[]){"val", "vals", "nan"}, 3, getSimMock, 0.0), "nan");
+ success &= EXPECT_STR_EQL(ca_most_similar("val", (void*[]){"val", "vals", "nan"}, 3, getSimMock, 0.9), NULL);
+ if (check(xhClear(&sim_table, do_nothing, NULL)) != 0) return false;
+
+ /** Clean up. **/
+ if (check(xhDeInit(&sim_table)) != 0) return false;
+
+ return success;
+ }
+
+long long test(char** tname)
+ {
+ *tname = "cluster-05 ca_most_similar()";
+ return loopTest(doTest) * 35;
+ }
diff --git a/centrallix-lib/tests/test_clusters_06.c b/centrallix-lib/tests/test_clusters_06.c
new file mode 100644
index 000000000..c4bcb20df
--- /dev/null
+++ b/centrallix-lib/tests/test_clusters_06.c
@@ -0,0 +1,161 @@
+/************************************************************************/
+/* Centrallix Application Server System */
+/* Centrallix Base Library */
+/* */
+/* Copyright (C) 2025-2026 LightSys Technology Services, Inc. */
+/* */
+/* You may use these files and this library under the terms of the */
+/* GNU Lesser General Public License, Version 2.1, contained in the */
+/* included file "COPYING". */
+/* */
+/* Module: test_clusters_06.c */
+/* Author: Israel Fuller */
+/* Creation: November 26th, 2025 */
+/* Description: Test the searching functions from clusters.h. */
+/************************************************************************/
+
+#include
+#include
+#include
+#include
+#include
+
+/** Test dependencies. **/
+#include "check.h"
+#include "newmalloc.h"
+#include "test_utils.h"
+
+/** Tested module. **/
+#include "clusters.h"
+
+
+static int cmp_Pairs(const void* v1, const void* v2)
+ {
+ const Pair* Pair1 = v1;
+ const Pair* Pair2 = v2;
+ return Pair1->i - Pair2->i;
+ }
+
+static void freePairs(pXArray xPairs)
+ {
+ /** The search functions hand ownership of each pair to the caller. **/
+ while (xPairs->nItems > 0)
+ nmFree(xPairs->Items[--xPairs->nItems], sizeof(Pair));
+ check(xaDeInit(xPairs));
+ }
+
+#define EXPECT_PAIR(Pair, k1, k2, sim_min, sim_max) \
+ ({ \
+ bool success = true; \
+ pPair d = (Pair); \
+ success &= EXPECT_EQL(d->i, k1, "%u"); \
+ success &= EXPECT_EQL(d->j, k2, "%u"); \
+ success &= EXPECT_RANGE(d->similarity, sim_min, sim_max, "%g"); \
+ success; \
+ })
+
+
+static bool doTest(void)
+ {
+ bool success = true;
+
+ /** Allocate some test data. **/
+ void* data[] = {
+ "string",
+ "string2",
+ "str",
+ "hello world",
+ "data",
+ "string3",
+ };
+
+ /** Check error cases. **/
+ success &= EXPECT_EQL(ca_complete_search(NULL, 6, ca_lev_compare, 0.8, NULL), NULL, "%p");
+ success &= EXPECT_EQL(ca_complete_search(data, 0, ca_lev_compare, 0.8, NULL), NULL, "%p");
+ success &= EXPECT_EQL(ca_complete_search(data, 6, NULL, 0.8, NULL), NULL, "%p");
+ success &= EXPECT_EQL(ca_complete_search(data, 6, ca_lev_compare, 1.1, NULL), NULL, "%p");
+ success &= EXPECT_EQL(ca_complete_search(data, 6, ca_lev_compare, -0.1, NULL), NULL, "%p");
+ success &= EXPECT_EQL(ca_complete_search(data, 6, ca_lev_compare, INFINITY, NULL), NULL, "%p");
+ success &= EXPECT_EQL(ca_complete_search(data, 6, ca_lev_compare, -INFINITY, NULL), NULL, "%p");
+ success &= EXPECT_EQL(ca_complete_search(data, 6, ca_lev_compare, NAN, NULL), NULL, "%p");
+
+ /** Test complete search. **/
+ {
+ XArray xPairs;
+ if (check(xaInit(&xPairs, 4)) != 0) return false;
+ success &= EXPECT_EQL(ca_complete_search(data, 6, ca_lev_compare, 0.8, &xPairs), &xPairs, "%p");
+ pPair* Pairs = (pPair*)xPairs.Items;
+ for (unsigned int i = 0u; i < xPairs.nItems; i++)
+ {
+ pPair cur = Pairs[i];
+ if (cur->i > cur->j)
+ {
+ unsigned int temp = cur->i;
+ cur->i = cur->j;
+ cur->j = temp;
+ }
+ }
+ qsort(Pairs, xPairs.nItems, sizeof(pPair), cmp_Pairs);
+ success &= EXPECT_EQL(xPairs.nItems, 3, "%d");
+ success &= EXPECT_PAIR(Pairs[0], 0, 1, 0.8, 1.0);
+ success &= EXPECT_PAIR(Pairs[1], 0, 5, 0.8, 1.0);
+ success &= EXPECT_PAIR(Pairs[2], 1, 5, 0.8, 1.0);
+ freePairs(&xPairs);
+ }
+
+ /** Test sliding search: Large window. **/
+ {
+ XArray xPairs;
+ if (check(xaInit(&xPairs, 4)) != 0) return false;
+ success &= EXPECT_EQL(ca_sliding_search(data, 6, 5, ca_lev_compare, 0.8, &xPairs), &xPairs, "%p");
+ pPair* Pairs = (pPair*)xPairs.Items;
+ for (unsigned int i = 0u; i < xPairs.nItems; i++)
+ {
+ pPair cur = Pairs[i];
+ if (cur->i > cur->j)
+ {
+ unsigned int temp = cur->i;
+ cur->i = cur->j;
+ cur->j = temp;
+ }
+ }
+ qsort(Pairs, xPairs.nItems, sizeof(pPair), cmp_Pairs);
+ success &= EXPECT_EQL(xPairs.nItems, 2, "%d");
+ success &= EXPECT_PAIR(Pairs[0], 0, 1, 0.8, 1.0);
+ // success &= EXPECT_PAIR(Pairs[1], 0, 5, 0.8, 1.0); /* Sliding search misses this pair. */
+ success &= EXPECT_PAIR(Pairs[1], 1, 5, 0.8, 1.0);
+ freePairs(&xPairs);
+ }
+
+ /** Test sliding search: Small window. **/
+ {
+ XArray xPairs;
+ if (check(xaInit(&xPairs, 4)) != 0) return false;
+ success &= EXPECT_EQL(ca_sliding_search(data, 6, 2, ca_lev_compare, 0.8, &xPairs), &xPairs, "%p");
+ pPair* Pairs = (pPair*)xPairs.Items;
+ for (unsigned int i = 0u; i < xPairs.nItems; i++)
+ {
+ pPair cur = Pairs[i];
+ if (cur->i > cur->j)
+ {
+ unsigned int temp = cur->i;
+ cur->i = cur->j;
+ cur->j = temp;
+ }
+ }
+ qsort(Pairs, xPairs.nItems, sizeof(pPair), cmp_Pairs);
+ success &= EXPECT_EQL(xPairs.nItems, 1, "%d");
+ success &= EXPECT_PAIR(Pairs[0], 0, 1, 0.8, 1.0);
+ // success &= EXPECT_PAIR(Pairs[1], 0, 5, 0.8, 1.0); /* Sliding search misses this pair. */
+ // success &= EXPECT_PAIR(Pairs[2], 1, 5, 0.8, 1.0); /* Sliding search misses this pair. */
+ freePairs(&xPairs);
+ }
+
+ return success;
+ }
+
+long long test(char** tname)
+ {
+ *tname = "cluster-06 Searching";
+ return loopTest(doTest) * 3;
+ }
diff --git a/centrallix-lib/tests/test_memstr_00.c b/centrallix-lib/tests/test_memstr_00.c
index 38d13f938..f161832c8 100755
--- a/centrallix-lib/tests/test_memstr_00.c
+++ b/centrallix-lib/tests/test_memstr_00.c
@@ -1,3 +1,19 @@
+/************************************************************************/
+/* Centrallix Application Server System */
+/* Centrallix Base Library */
+/* */
+/* Copyright (C) 2014-2026 LightSys Technology Services, Inc. */
+/* */
+/* You may use these files and this library under the terms of the */
+/* GNU Lesser General Public License, Version 2.1, contained in the */
+/* included file "COPYING". */
+/* */
+/* Module: test_memstr_00.c */
+/* Author: Brady Steed */
+/* Creation: July 10, 2014 */
+/* Description: Test the memstr function. */
+/************************************************************************/
+
#include
#include
#include
@@ -5,39 +21,41 @@
#include
#include "memstr.h"
#include
+#include
+#include "test_utils.h"
-long long
-test(char** tname)
+static bool
+doTest(void)
{
- int i;
- int iter;
char buffer1[40];
char buffer2[20];
char * ptr;
- *tname = "memstr-00 correct null ptr";
- iter = 4000;
- for(i=0;i
#include
#include
+#include
#include "mtsession.h"
#include "mtlexer.h"
#include
+#include "test_utils.h"
-long long
-test(char** tname)
- {
- int i;
- int iter;
- int flags;
- pLxSession lxs;
- int t;
- int j;
- char str[65536] = "";
- int n_flagtype = 4;
- int n_tok = 3;
- int flagtype[4] = {MLX_F_EOF, MLX_F_EOF | MLX_F_EOL, MLX_F_EOL, 0};
- int toktype[4][3] = { {MLX_TOK_EOF, MLX_TOK_ERROR, MLX_TOK_ERROR},
+#define N_FLAGTYPE 4
+#define N_TOK 3
+
+static char str[65536] = "";
+static int flagtype[N_FLAGTYPE] = {MLX_F_EOF, MLX_F_EOF | MLX_F_EOL, MLX_F_EOL, 0};
+static int toktype[N_FLAGTYPE][N_TOK] = {
+ {MLX_TOK_EOF, MLX_TOK_ERROR, MLX_TOK_ERROR},
{MLX_TOK_EOL, MLX_TOK_EOF, MLX_TOK_ERROR},
{MLX_TOK_EOL, MLX_TOK_ERROR, MLX_TOK_ERROR},
{MLX_TOK_ERROR, MLX_TOK_ERROR, MLX_TOK_ERROR} };
- *tname = "mtlexer-00 empty string and eol/eof/error test";
-
- mssInitialize("system", "", "", 0, "test");
+static bool
+doTest(void)
+ {
+ int f;
+ int j;
+ int t;
+ pLxSession lxs;
- iter = 400000;
- for(i=0;i
#include
#include
+#include
#include "mtsession.h"
#include "mtlexer.h"
#include
+#include "test_utils.h"
-long long
-test(char** tname)
- {
- int i;
- int iter;
- int flags;
- pLxSession lxs;
- int t;
- char* strval;
- int j;
- char str[65536] = "'hello world'";
- int n_flagtype = 4;
- int n_tok = 4;
- int flagtype[4] = {MLX_F_EOF, MLX_F_EOF | MLX_F_EOL, MLX_F_EOL, 0};
- int toktype[4][4] = { {MLX_TOK_STRING, MLX_TOK_EOF, MLX_TOK_ERROR, MLX_TOK_ERROR},
+#define N_FLAGTYPE 4
+#define N_TOK 4
+
+static char str[65536] = "'hello world'";
+static int flagtype[N_FLAGTYPE] = {MLX_F_EOF, MLX_F_EOF | MLX_F_EOL, MLX_F_EOL, 0};
+static int toktype[N_FLAGTYPE][N_TOK] = {
+ {MLX_TOK_STRING, MLX_TOK_EOF, MLX_TOK_ERROR, MLX_TOK_ERROR},
{MLX_TOK_STRING, MLX_TOK_EOL, MLX_TOK_EOF, MLX_TOK_ERROR},
{MLX_TOK_STRING, MLX_TOK_EOL, MLX_TOK_ERROR, MLX_TOK_ERROR},
{MLX_TOK_STRING, MLX_TOK_ERROR, MLX_TOK_ERROR, MLX_TOK_ERROR} };
- char* tokstr[4] = { "hello world", NULL, NULL, NULL };
-
- *tname = "mtlexer-01 string token and eol/eof/error test";
+static char* tokstr[N_TOK] = { "hello world", NULL, NULL, NULL };
- mssInitialize("system", "", "", 0, "test");
+static bool
+doTest(void)
+ {
+ int f;
+ int j;
+ int t;
+ char* strval;
+ pLxSession lxs;
- iter = 400000;
- for(i=0;i
#include
#include
+#include
#include "mtsession.h"
#include "mtlexer.h"
#include
+#include "test_utils.h"
-long long
-test(char** tname)
- {
- int i;
- int iter;
- int flags;
- pLxSession lxs;
- int t;
- char* strval;
- int j;
- int strcnt;
- char str[65536] = "'string one' 'string two'\n'string three' 'string four'\r\n'string five'";
- int n_flagtype = 4;
- int n_tok = 9;
- int flagtype[4] = {MLX_F_EOF, MLX_F_EOF | MLX_F_EOL, MLX_F_EOL, 0};
- int toktype[4][9] = {
+#define N_FLAGTYPE 4
+#define N_TOK 9
+
+static char str[65536] = "'string one' 'string two'\n'string three' 'string four'\r\n'string five'";
+static int flagtype[N_FLAGTYPE] = {MLX_F_EOF, MLX_F_EOF | MLX_F_EOL, MLX_F_EOL, 0};
+static int toktype[N_FLAGTYPE][N_TOK] = {
{MLX_TOK_STRING, MLX_TOK_STRING, MLX_TOK_STRING, MLX_TOK_STRING, MLX_TOK_STRING, MLX_TOK_EOF, MLX_TOK_ERROR, MLX_TOK_ERROR, MLX_TOK_ERROR},
{MLX_TOK_STRING, MLX_TOK_STRING, MLX_TOK_EOL, MLX_TOK_STRING, MLX_TOK_STRING, MLX_TOK_EOL, MLX_TOK_STRING, MLX_TOK_EOL, MLX_TOK_EOF },
{MLX_TOK_STRING, MLX_TOK_STRING, MLX_TOK_EOL, MLX_TOK_STRING, MLX_TOK_STRING, MLX_TOK_EOL, MLX_TOK_STRING, MLX_TOK_EOL, MLX_TOK_ERROR },
{MLX_TOK_STRING, MLX_TOK_STRING, MLX_TOK_STRING, MLX_TOK_STRING, MLX_TOK_STRING, MLX_TOK_ERROR, MLX_TOK_ERROR, MLX_TOK_ERROR, MLX_TOK_ERROR},
};
- char* tokstr[6] = { "string one", "string two", "string three", "string four", "string five", NULL };
-
- *tname = "mtlexer-02 three lines of strings and eol/eof/error test";
+static char* tokstr[6] = { "string one", "string two", "string three", "string four", "string five", NULL };
- mssInitialize("system", "", "", 0, "test");
+static bool
+doTest(void)
+ {
+ int f;
+ int j;
+ int t;
+ int strcnt;
+ char* strval;
+ pLxSession lxs;
- iter = 200000;
- for(i=0;i
+#include
+#include "test_utils.h"
-long long
-test(char** tname)
+/** Longest line the sweep builds. **/
+#define MAX_LEN 6000
+
+#define N_TOK 7
+
+static char str[65536] = "";
+static int toktype[9] = {MLX_TOK_STRING, MLX_TOK_EOL, MLX_TOK_STRING, MLX_TOK_EOL, MLX_TOK_STRING, MLX_TOK_EOL, MLX_TOK_EOF };
+static char* tokstr[6];
+
+/*** Number of passes needed to cover every line length. Each pass tests the
+ *** lengths congruent to it, spread across the whole sweep, so even a single
+ *** pass exercises the longest lines.
+ ***/
+#define N_PASS 250
+
+/** Pass under test, advanced each time and wrapped once all passes are done. **/
+static int sweep = 0;
+
+static bool
+doTest(void)
{
int i;
- int iter;
- pLxSession lxs;
- int t;
- char* strval;
- int alloc;
int j;
+ int t;
int strcnt;
- char str[65536] = "";
- int n_tok = 7;
- int toktype[9] = {MLX_TOK_STRING, MLX_TOK_EOL, MLX_TOK_STRING, MLX_TOK_EOL, MLX_TOK_STRING, MLX_TOK_EOL, MLX_TOK_EOF };
- char* tokstr[6];
-
- *tname = "mtlexer-03 BID#156 - line length based failure";
-
- mssInitialize("system", "", "", 0, "test");
-
- iter = 6000;
+ int alloc;
+ char* strval;
+ pLxSession lxs;
- memset(str, 'a', iter+1);
- tokstr[0] = malloc(iter+2);
- memset(tokstr[0], 'a', iter+1);
- tokstr[1] = "nextline";
- tokstr[2] = "thirdline";
- /*tokstr[0] = malloc(2046);
- memset(tokstr[0], 'a', 2045);
- tokstr[0][2045] = '\0';
- tokstr[1] = malloc(2045);
- memset(tokstr[1], 'b', 2044);
- tokstr[1][2044] = '\0';
- tokstr[2] = malloc(2047);
- memset(tokstr[2], 'c', 2046);
- tokstr[2][2046] = '\0';
- sprintf(str, "%s\r\n%s\r\n%s\r\n", tokstr[0], tokstr[1], tokstr[2]);*/
-
- for(i=0;i= N_PASS) sweep = 0;
+ for(i=sweep++;i
+#include
+#include "test_utils.h"
-long long
-test(char** tname)
+static char str[65536] = "";
+
+static bool
+doTest(void)
{
- int i;
- int iter;
pLxSession lxs;
- char str[65536] = "";
-
- *tname = "mtlexer-04 open/close session";
-
- mssInitialize("system", "", "", 0, "test");
- iter = 700000;
+ lxs = mlxStringSession(str, MLX_F_EOL | MLX_F_EOF | MLX_F_IFSONLY);
+ assert(lxs != NULL);
+ mlxCloseSession(lxs);
- for(i=0;i
+#include
+#include "test_utils.h"
-long long
-test(char** tname)
+/** Integers in the data file, and so the ops performed by one pass. **/
+#define N_INTS 60000
+
+static bool
+doTest(void)
{
- int i;
int j;
int t;
int n;
- int iter;
pLxSession lxs;
pFile fd;
- *tname = "mtlexer-05 integer data, one per line with eol/eof";
-
- mssInitialize("system", "", "", 0, "test");
-
- iter = 10;
-
- for(i=0;i
+#include
+#include "test_utils.h"
-long long
-test(char** tname)
+/** Integers in the data file, and so the ops performed by one pass. **/
+#define N_INTS 60000
+
+static bool
+doTest(void)
{
- int i;
int j;
int t;
int n;
- int iter;
pLxSession lxs;
pFile fd;
- *tname = "mtlexer-06 integer data, all on one line with eol/eof";
-
- mssInitialize("system", "", "", 0, "test");
-
- iter = 10;
-
- for(i=0;i
+#include
+#include "test_utils.h"
-long long
-test(char** tname)
+static char* reswds[] = { "reserved", NULL };
+
+/** Token count and repeat count read from the data file by the last pass. **/
+static int n_tokens = 0;
+static int n_iter = 0;
+
+static bool
+doTest(void)
{
- int i;
int j;
int k;
int t;
- int iter;
int flags;
pLxSession lxs;
pFile fd;
int tokens[256];
- int n_tokens;
- int n_iter;
- char* reswds[] = { "reserved", NULL };
+
+ fd = fdOpen("tests/test_mtlexer_07.txt", O_RDONLY, 0600);
+ assert(fd != NULL);
+ flags = MLX_F_EOL | MLX_F_FILENAMES | MLX_F_DBLBRACE | MLX_F_SSTRING;
+ lxs = mlxOpenSession(fd, flags);
+ assert(lxs != NULL);
+ mlxSetReservedWords(lxs, reswds);
+ t = mlxNextToken(lxs);
+ assert(t == MLX_TOK_INTEGER);
+ n_iter = mlxIntVal(lxs);
+ assert(mlxNextToken(lxs) == MLX_TOK_EOL);
+ n_tokens = 0;
+ while((t = mlxNextToken(lxs)) == MLX_TOK_INTEGER)
+ {
+ assert(n_tokens < 256);
+ tokens[n_tokens++] = mlxIntVal(lxs);
+ assert(tokens[n_tokens-1] > 0 && tokens[n_tokens-1] <= MLX_TOK_MAX);
+ }
+ assert(t == MLX_TOK_EOL);
+ for(j=0;j 0 && tokens[n_tokens-1] <= MLX_TOK_MAX);
- }
- assert(t == MLX_TOK_EOL);
- for(j=0;j 0) rval *= (long long)n_tokens * n_iter;
- return iter * n_tokens * n_iter;
+ return rval;
}
diff --git a/centrallix-lib/tests/test_mtlexer_08.c b/centrallix-lib/tests/test_mtlexer_08.c
index 315912840..d00f34a83 100644
--- a/centrallix-lib/tests/test_mtlexer_08.c
+++ b/centrallix-lib/tests/test_mtlexer_08.c
@@ -7,42 +7,41 @@
#include "mtsession.h"
#include "mtlexer.h"
#include
+#include
+#include "test_utils.h"
-long long
-test(char** tname)
+static char* teststr = "'string' 'test string' \"string\" \"test string\" 'string\\'s' \"\\\"string\\\"\" 'string\\\\string' 'string\"string' \"string'string\"";
+static char* strs[] = {"string", "test string", "string", "test string", "string's", "\"string\"", "string\\string", "string\"string", "string'string", NULL};
+
+static bool
+doTest(void)
{
- int i;
int cnt;
int t;
- char* teststr = "'string' 'test string' \"string\" \"test string\" 'string\\'s' \"\\\"string\\\"\" 'string\\\\string' 'string\"string' \"string'string\"";
- char* strs[] = {"string", "test string", "string", "test string", "string's", "\"string\"", "string\\string", "string\"string", "string'string", NULL};
- char *str;
- int iter;
+ char* str;
pLxSession lxs;
- *tname = "mtlexer-08 string quoting";
-
- mssInitialize("system", "", "", 0, "test");
-
- iter = 150000;
-
- for(i=0;i
+#include
+#include "test_utils.h"
-long long
-test(char** tname)
- {
- int i;
- int iter;
- pLxSession lxs;
- int t;
- char* strval;
- int alloc;
- int j;
- int strcnt;
- char str[65536] = "";
- int n_tok = 3;
- int toktype[3] = {MLX_TOK_STRING, MLX_TOK_EOL, MLX_TOK_EOF };
- char* tokstr[6];
+/** Longest string the sweep builds. **/
+#define MAX_LEN 6000
- *tname = "mtlexer-09 strings spanning multiple lines";
+#define N_TOK 3
- mssInitialize("system", "", "", 0, "test");
+static char str[65536] = "";
+static int toktype[N_TOK] = {MLX_TOK_STRING, MLX_TOK_EOL, MLX_TOK_EOF };
+static char* tokstr[6];
- iter = 6000;
+/*** Number of passes needed to cover every string length. Each pass tests the
+ *** lengths congruent to it, spread across the whole sweep, so even a single
+ *** pass exercises the longest strings.
+ ***/
+#define N_PASS 250
- memset(str, 'a', iter+3);
- tokstr[0] = malloc(iter+2);
- memset(tokstr[0], 'a', iter+1);
- str[0] = '"';
- str[iter+2] = '"';
+/** Pass under test, advanced each time and wrapped once all passes are done. **/
+static int sweep = 0;
- for(i=0;i= N_PASS) sweep = 0;
+ for(i=sweep++;i
+#include
+#include "test_utils.h"
-long long
-test(char** tname)
- {
- int i,j;
- char* teststr = "0 -1 1 -255 -256 255 256 -127 -128 127 128 32767 32768 -32767 -32768 65535 65536 -65535 -65536 16777216 -16777216 2147483647 -2147483648\n"
+static char* teststr = "0 -1 1 -255 -256 255 256 -127 -128 127 128 32767 32768 -32767 -32768 65535 65536 -65535 -65536 16777216 -16777216 2147483647 -2147483648\n"
".0 .1 .10 .01 0.0 0.1 0.10 0.01 1.0 1.1 1.10 1.01 10.0 10.1 10.10 10.01 -.0 -.1 -.10 -.01 -0.0 -0.1 -0.10 -0.01 -1.0 -1.1 -1.10 -1.01 -10.0 -10.1 -10.10 -10.01";
- int integers[] = {0, -1, 1, -255, -256, 255, 256, -127, -128, 127, 128, 32767, 32768, -32767, -32768, 65535, 65536, -65535, -65536, 16777216, -16777216, 2147483647, -2147483647-1 };
- double doubles[] = {.0, .1, .10, .01, 0.0, 0.1, 0.10, 0.01, 1.0, 1.1, 1.10, 1.01, 10.0, 10.1, 10.10, 10.01, -.0, -.1, -.10, -.01, -0.0, -0.1, -0.10, -0.01, -1.0, -1.1, -1.10, -1.01, -10.0, -10.1, -10.10, -10.01};
+static int integers[] = {0, -1, 1, -255, -256, 255, 256, -127, -128, 127, 128, 32767, 32768, -32767, -32768, 65535, 65536, -65535, -65536, 16777216, -16777216, 2147483647, -2147483647-1 };
+static double doubles[] = {.0, .1, .10, .01, 0.0, 0.1, 0.10, 0.01, 1.0, 1.1, 1.10, 1.01, 10.0, 10.1, 10.10, 10.01, -.0, -.1, -.10, -.01, -0.0, -0.1, -0.10, -0.01, -1.0, -1.1, -1.10, -1.01, -10.0, -10.1, -10.10, -10.01};
+
+#define N_INTEGERS (sizeof(integers)/sizeof(integers[0]))
+#define N_DOUBLES (sizeof(doubles)/sizeof(doubles[0]))
+
+static bool
+doTest(void)
+ {
+ unsigned int j;
int n;
double d;
- int iter;
pLxSession lxs;
- *tname = "mtlexer-10 integer and double parsing";
-
- mssInitialize("system", "", "", 0, "test");
-
- iter = 15000;
-
- for(i=0;i
#include
#include
+#include
#include "mtsession.h"
#include "mtlexer.h"
#include
+#include "test_utils.h"
-long long
-test(char** tname)
- {
- int i;
- int iter;
- int flags;
- pLxSession lxs;
- int t;
- char* strval;
- int j;
- int strcnt;
- char str[65536] = "select Select SELECT insert Insert INSERT what What WHAT";
- char* reswds[] = {"select", "insert", NULL};
- int n_flagtype = 4;
- int n_tok = 9;
- int flagtype[4] = {MLX_F_ICASE, MLX_F_ICASER, MLX_F_ICASEK, 0};
- int toktype[4][9] = {
+#define N_FLAGTYPE 4
+#define N_TOK 9
+
+static char str[65536] = "select Select SELECT insert Insert INSERT what What WHAT";
+static char* reswds[] = {"select", "insert", NULL};
+static int flagtype[N_FLAGTYPE] = {MLX_F_ICASE, MLX_F_ICASER, MLX_F_ICASEK, 0};
+static int toktype[N_FLAGTYPE][N_TOK] = {
{MLX_TOK_RESERVEDWD, MLX_TOK_RESERVEDWD, MLX_TOK_RESERVEDWD, MLX_TOK_RESERVEDWD, MLX_TOK_RESERVEDWD, MLX_TOK_RESERVEDWD, MLX_TOK_KEYWORD, MLX_TOK_KEYWORD, MLX_TOK_KEYWORD},
{MLX_TOK_RESERVEDWD, MLX_TOK_RESERVEDWD, MLX_TOK_RESERVEDWD, MLX_TOK_RESERVEDWD, MLX_TOK_RESERVEDWD, MLX_TOK_RESERVEDWD, MLX_TOK_KEYWORD, MLX_TOK_KEYWORD, MLX_TOK_KEYWORD},
{MLX_TOK_RESERVEDWD, MLX_TOK_KEYWORD, MLX_TOK_KEYWORD, MLX_TOK_RESERVEDWD, MLX_TOK_KEYWORD, MLX_TOK_KEYWORD, MLX_TOK_KEYWORD, MLX_TOK_KEYWORD, MLX_TOK_KEYWORD},
{MLX_TOK_RESERVEDWD, MLX_TOK_KEYWORD, MLX_TOK_KEYWORD, MLX_TOK_RESERVEDWD, MLX_TOK_KEYWORD, MLX_TOK_KEYWORD, MLX_TOK_KEYWORD, MLX_TOK_KEYWORD, MLX_TOK_KEYWORD},
};
- char* tokstr[4][9] ={
+static char* tokstr[N_FLAGTYPE][N_TOK] = {
{"select","select","select", "insert","insert","insert", "what","what","what"},
{"select","select","select", "insert","insert","insert", "what","What","WHAT"},
{"select","select","select", "insert","insert","insert", "what","what","what"},
{"select","Select","SELECT", "insert","Insert","INSERT", "what","What","WHAT"},
};
- *tname = "mtlexer-11 case (in)sensitive keywords and reserved words";
-
- mssInitialize("system", "", "", 0, "test");
+static bool
+doTest(void)
+ {
+ int f;
+ int j;
+ int t;
+ int strcnt;
+ char* strval;
+ pLxSession lxs;
- iter = 200000;
- for(i=0;i
+#include
+#include "test_utils.h"
-long long
-test(char** tname)
+/** Longest keyword the sweep builds. **/
+#define MAX_LEN 20000
+
+static char str[65536] = "";
+
+/** Length under test, advanced each pass and wrapped at the longest case. **/
+static int sweep = 0;
+
+static bool
+doTest(void)
{
int i;
- int iter;
pLxSession lxs;
char* strval;
int alloc;
- char str[65536] = "";
-
- *tname = "mtlexer-12 normal/oversized keywords";
- mssInitialize("system", "", "", 0, "test");
+ if (sweep >= MAX_LEN) sweep = 0;
+ i = sweep++;
- iter = 20000;
- for(i=0;i
+#include
+#include "test_utils.h"
-long long
-test(char** tname)
+/** Longest integer the sweep builds. **/
+#define MAX_LEN 20000
+
+static char str[65536] = "";
+
+/** Length under test, advanced each pass and wrapped at the longest case. **/
+static int sweep = 0;
+
+/** The repunit str currently holds, grown alongside the sweep. **/
+static int iv = 0;
+
+static bool
+doTest(void)
{
int i;
- int iv;
- int iter;
- pLxSession lxs;
int n;
- char str[65536] = "";
-
- *tname = "mtlexer-13 normal/oversized integers";
+ pLxSession lxs;
- mssInitialize("system", "", "", 0, "test");
+ if (sweep >= MAX_LEN)
+ {
+ sweep = 0;
+ iv = 0;
+ }
+ i = sweep++;
- iter = 20000;
- iv=0;
- for(i=0;i
+#include
+#include "test_utils.h"
-long long
-test(char** tname)
+/** Longest number the sweep builds. **/
+#define MAX_LEN 20000
+
+static char str[65536] = "";
+
+/** Length under test, advanced each pass and wrapped at the longest case. **/
+static int sweep = 0;
+
+static bool
+doTest(void)
{
int i;
- int iter;
- pLxSession lxs;
double d;
- char str[65536] = "";
-
- *tname = "mtlexer-14 normal/oversized double floating point";
+ pLxSession lxs;
- mssInitialize("system", "", "", 0, "test");
+ if (sweep >= MAX_LEN) sweep = 0;
+ i = sweep++;
- iter = 20000;
- for(i=0;i
+#include
+#include "test_utils.h"
-long long
-test(char** tname)
+/** Integers in the data file, and so the ops performed by one pass. **/
+#define N_INTS 12
+
+static int flagtypes[5] = { MLX_F_CPPCOMM, MLX_F_POUNDCOMM, MLX_F_SEMICOMM, MLX_F_DASHCOMM, MLX_F_CCOMM };
+
+#define N_FLAGTYPES ((int)(sizeof(flagtypes)/sizeof(flagtypes[0])))
+
+static bool
+doTest(void)
{
int i;
int j;
int t;
int n;
- int iter;
- int flagtypes[5] = { MLX_F_CPPCOMM, MLX_F_POUNDCOMM, MLX_F_SEMICOMM, MLX_F_DASHCOMM, MLX_F_CCOMM };
- int n_flagtypes = 5;
int flags;
pLxSession lxs;
pFile fd;
- *tname = "mtlexer-15 comments // # ; -- /**/ long and short";
-
- mssInitialize("system", "", "", 0, "test");
-
- iter = 6000;
-
flags = 0;
- for(i=0;i
+#include
+#include "test_utils.h"
-long long
-test(char** tname)
+/** Integers in the data file, and so the ops performed by one pass. **/
+#define N_INTS 12
+
+static int flagtypes[5] = { MLX_F_CPPCOMM, MLX_F_POUNDCOMM, MLX_F_SEMICOMM, MLX_F_DASHCOMM, MLX_F_CCOMM };
+
+#define N_FLAGTYPES ((int)(sizeof(flagtypes)/sizeof(flagtypes[0])))
+
+static bool
+doTest(void)
{
int i;
int j;
int t;
int n;
- int iter;
- int flagtypes[5] = { MLX_F_CPPCOMM, MLX_F_POUNDCOMM, MLX_F_SEMICOMM, MLX_F_DASHCOMM, MLX_F_CCOMM };
- int n_flagtypes = 5;
int flags;
pLxSession lxs;
pFile fd;
- *tname = "mtlexer-16 comments // # ; -- /**/ short only";
-
- mssInitialize("system", "", "none", 0, "test");
-
- iter = 6000;
-
flags = 0;
- for(i=0;i
#include
#include
+#include
#include "mtsession.h"
#include "mtlexer.h"
#include
+#include "test_utils.h"
-long long
-test(char** tname)
- {
- int i;
- int iter;
- int flags;
- pLxSession lxs;
- int t;
- char* strval;
- int j;
- int strcnt;
- char str[65536] = "\n\n'string one' 'string two'\n'string three' 'string four'\r\n'string five'\r\n\r\nString Six";
- int n_flagtype = 4;
- int n_tok = 15;
- int flagtype[4] = {MLX_F_EOF, MLX_F_EOF | MLX_F_EOL, MLX_F_EOL, 0};
- int toktype[4][15] = {
+#define N_FLAGTYPE 4
+#define N_TOK 15
+
+static char str[65536] = "\n\n'string one' 'string two'\n'string three' 'string four'\r\n'string five'\r\n\r\nString Six";
+static int flagtype[N_FLAGTYPE] = {MLX_F_EOF, MLX_F_EOF | MLX_F_EOL, MLX_F_EOL, 0};
+static int toktype[N_FLAGTYPE][N_TOK] = {
{MLX_TOK_STRING, MLX_TOK_STRING, MLX_TOK_STRING, MLX_TOK_STRING, MLX_TOK_STRING, MLX_TOK_STRING, MLX_TOK_STRING, MLX_TOK_EOF, MLX_TOK_ERROR, MLX_TOK_ERROR, MLX_TOK_ERROR, MLX_TOK_ERROR, MLX_TOK_ERROR, MLX_TOK_ERROR, MLX_TOK_ERROR},
{MLX_TOK_STRING, MLX_TOK_EOL, MLX_TOK_STRING, MLX_TOK_EOL, MLX_TOK_STRING, MLX_TOK_EOL, MLX_TOK_STRING, MLX_TOK_EOL, MLX_TOK_STRING, MLX_TOK_EOL, MLX_TOK_STRING, MLX_TOK_EOL, MLX_TOK_STRING, MLX_TOK_EOL, MLX_TOK_EOF },
{MLX_TOK_STRING, MLX_TOK_EOL, MLX_TOK_STRING, MLX_TOK_EOL, MLX_TOK_STRING, MLX_TOK_EOL, MLX_TOK_STRING, MLX_TOK_EOL, MLX_TOK_STRING, MLX_TOK_EOL, MLX_TOK_STRING, MLX_TOK_EOL, MLX_TOK_STRING, MLX_TOK_EOL, MLX_TOK_ERROR },
{MLX_TOK_STRING, MLX_TOK_STRING, MLX_TOK_STRING, MLX_TOK_STRING, MLX_TOK_STRING, MLX_TOK_STRING, MLX_TOK_STRING, MLX_TOK_ERROR, MLX_TOK_ERROR, MLX_TOK_ERROR, MLX_TOK_ERROR, MLX_TOK_ERROR, MLX_TOK_ERROR, MLX_TOK_ERROR, MLX_TOK_ERROR},
};
- char* tokstr[8] = { "\n", "\n", "'string one' 'string two'\n", "'string three' 'string four'\r\n", "'string five'\r\n", "\r\n", "String Six", NULL };
-
- *tname = "mtlexer-17 LINEONLY mode test";
+static char* tokstr[8] = { "\n", "\n", "'string one' 'string two'\n", "'string three' 'string four'\r\n", "'string five'\r\n", "\r\n", "String Six", NULL };
- mssInitialize("system", "", "", 0, "test");
+static bool
+doTest(void)
+ {
+ int f;
+ int j;
+ int t;
+ int strcnt;
+ char* strval;
+ pLxSession lxs;
- iter = 100000;
- for(i=0;i
#include
#include
+#include
#include "mtsession.h"
#include "mtlexer.h"
#include
+#include "test_utils.h"
-long long
-test(char** tname)
- {
- int i;
- int iter;
- int flags;
- pLxSession lxs;
- int t;
- char* strval;
- int j;
- int strcnt;
- char str[65536] = "\n\n'string one' 'string two'\n'string three' 'string four'\r\n'string five'\r\n\r\nString Six\n";
- int n_flagtype = 4;
- int n_tok = 15;
- int flagtype[4] = {MLX_F_EOF, MLX_F_EOF | MLX_F_EOL, MLX_F_EOL, 0};
- int toktype[4][15] = {
+#define N_FLAGTYPE 4
+#define N_TOK 15
+
+static char str[65536] = "\n\n'string one' 'string two'\n'string three' 'string four'\r\n'string five'\r\n\r\nString Six\n";
+static int flagtype[N_FLAGTYPE] = {MLX_F_EOF, MLX_F_EOF | MLX_F_EOL, MLX_F_EOL, 0};
+static int toktype[N_FLAGTYPE][N_TOK] = {
{MLX_TOK_STRING, MLX_TOK_STRING, MLX_TOK_STRING, MLX_TOK_STRING, MLX_TOK_STRING, MLX_TOK_STRING, MLX_TOK_STRING, MLX_TOK_EOF, MLX_TOK_ERROR, MLX_TOK_ERROR, MLX_TOK_ERROR, MLX_TOK_ERROR, MLX_TOK_ERROR, MLX_TOK_ERROR, MLX_TOK_ERROR},
{MLX_TOK_STRING, MLX_TOK_EOL, MLX_TOK_STRING, MLX_TOK_EOL, MLX_TOK_STRING, MLX_TOK_EOL, MLX_TOK_STRING, MLX_TOK_EOL, MLX_TOK_STRING, MLX_TOK_EOL, MLX_TOK_STRING, MLX_TOK_EOL, MLX_TOK_STRING, MLX_TOK_EOL, MLX_TOK_EOF },
{MLX_TOK_STRING, MLX_TOK_EOL, MLX_TOK_STRING, MLX_TOK_EOL, MLX_TOK_STRING, MLX_TOK_EOL, MLX_TOK_STRING, MLX_TOK_EOL, MLX_TOK_STRING, MLX_TOK_EOL, MLX_TOK_STRING, MLX_TOK_EOL, MLX_TOK_STRING, MLX_TOK_EOL, MLX_TOK_ERROR },
{MLX_TOK_STRING, MLX_TOK_STRING, MLX_TOK_STRING, MLX_TOK_STRING, MLX_TOK_STRING, MLX_TOK_STRING, MLX_TOK_STRING, MLX_TOK_ERROR, MLX_TOK_ERROR, MLX_TOK_ERROR, MLX_TOK_ERROR, MLX_TOK_ERROR, MLX_TOK_ERROR, MLX_TOK_ERROR, MLX_TOK_ERROR},
};
- char* tokstr[8] = { "\n", "\n", "'string one' 'string two'\n", "'string three' 'string four'\r\n", "'string five'\r\n", "\r\n", "String Six\n", NULL };
-
- *tname = "mtlexer-18 LINEONLY mode test - file ends in newline";
+static char* tokstr[8] = { "\n", "\n", "'string one' 'string two'\n", "'string three' 'string four'\r\n", "'string five'\r\n", "\r\n", "String Six\n", NULL };
- mssInitialize("system", "", "", 0, "test");
+static bool
+doTest(void)
+ {
+ int f;
+ int j;
+ int t;
+ int strcnt;
+ char* strval;
+ pLxSession lxs;
- iter = 100000;
- for(i=0;i
+#include
+#include "test_utils.h"
-long long
-test(char** tname)
+#define N_TOK 16
+
+static char str[65536] = "Header: 'Val1 Val2 Val3'\r\nHeader-2: Val1 'Val2 Val3'\r\nHeader-3: 'Val1 Val2' Val3\r\n";
+static int toktype[N_TOK] = {MLX_TOK_KEYWORD, MLX_TOK_COLON, MLX_TOK_STRING, MLX_TOK_STRING, MLX_TOK_STRING, MLX_TOK_EOL, MLX_TOK_KEYWORD, MLX_TOK_COLON, MLX_TOK_STRING, MLX_TOK_EOL, MLX_TOK_KEYWORD, MLX_TOK_COLON, MLX_TOK_STRING, MLX_TOK_KEYWORD, MLX_TOK_EOL, MLX_TOK_EOF};
+static char* tokstr[10] = { "Header", "'Val1", "Val2", "Val3'", "Header-2", " Val1 'Val2 Val3'\r\n", "Header-3", "Val1 Val2", "Val3", NULL };
+static int setflags[N_TOK] = {0, 0, MLX_F_IFSONLY, 0, 0, 0, 0, 0, MLX_F_LINEONLY, 0, 0, 0, 0, 0, 0, 0 };
+static int unsetflags[N_TOK] = {0, 0, 0, 0, 0, MLX_F_IFSONLY, 0, 0, 0, MLX_F_LINEONLY, 0, 0, 0, 0, 0, 0 };
+
+static bool
+doTest(void)
{
- int i;
- int iter;
- pLxSession lxs;
- int t;
- char* strval;
int j;
+ int t;
int strcnt;
- char str[65536] = "Header: 'Val1 Val2 Val3'\r\nHeader-2: Val1 'Val2 Val3'\r\nHeader-3: 'Val1 Val2' Val3\r\n";
- int n_tok = 16;
- int toktype[16] = {MLX_TOK_KEYWORD, MLX_TOK_COLON, MLX_TOK_STRING, MLX_TOK_STRING, MLX_TOK_STRING, MLX_TOK_EOL, MLX_TOK_KEYWORD, MLX_TOK_COLON, MLX_TOK_STRING, MLX_TOK_EOL, MLX_TOK_KEYWORD, MLX_TOK_COLON, MLX_TOK_STRING, MLX_TOK_KEYWORD, MLX_TOK_EOL, MLX_TOK_EOF};
- char* tokstr[10] = { "Header", "'Val1", "Val2", "Val3'", "Header-2", " Val1 'Val2 Val3'\r\n", "Header-3", "Val1 Val2", "Val3", NULL };
- int setflags[16] = {0, 0, MLX_F_IFSONLY, 0, 0, 0, 0, 0, MLX_F_LINEONLY, 0, 0, 0, 0, 0, 0, 0 };
- int unsetflags[16] = {0, 0, 0, 0, 0, MLX_F_IFSONLY, 0, 0, 0, MLX_F_LINEONLY, 0, 0, 0, 0, 0, 0 };
-
- *tname = "mtlexer-19 enabling/disabling LINEONLY/IFSONLY during parsing";
-
- mssInitialize("system", "", "", 0, "test");
+ char* strval;
+ pLxSession lxs;
- iter = 100000;
- for(i=0;i
+#include
+#include "test_utils.h"
-long long
-test(char** tname)
+static bool
+doTest(void)
{
- int i;
int j;
int t;
int n;
- int iter;
pLxSession lxs;
pFile fd;
char buf[256];
- *tname = "mtlexer-20 NODISCARD flag test";
-
- mssInitialize("system", "", "", 0, "test");
-
- iter = 60000;
-
- for(i=0;i= 0);
- buf[n] = '\0';
- assert(strcmp(buf, "This is some text.\n") == 0);
- fdClose(fd, 0);
}
+ t = mlxNextToken(lxs);
+ assert(t == MLX_TOK_EOL);
+ mlxCloseSession(lxs);
+ n = fdRead(fd, buf, sizeof(buf) - 1, 0, 0);
+ assert(n >= 0);
+ buf[n] = '\0';
+ assert(strcmp(buf, "This is some text.\n") == 0);
+ fdClose(fd, 0);
- return iter;
+ return true;
}
+long long
+test(char** tname)
+ {
+ *tname = "mtlexer-20 NODISCARD flag test";
+ mssInitialize("system", "", "", 0, "test");
+ return loopTest(doTest);
+ }
diff --git a/centrallix-lib/tests/test_mtlexer_21.c b/centrallix-lib/tests/test_mtlexer_21.c
index 59e4281fa..ea34cad95 100644
--- a/centrallix-lib/tests/test_mtlexer_21.c
+++ b/centrallix-lib/tests/test_mtlexer_21.c
@@ -7,46 +7,44 @@
#include "mtsession.h"
#include "mtlexer.h"
#include
+#include
+#include "test_utils.h"
-long long
-test(char** tname)
+static bool
+doTest(void)
{
- int i;
int j;
int t;
int n;
- int iter;
pLxSession lxs;
pFile fd;
char buf[256];
- *tname = "mtlexer-21 NODISCARD test (LINEONLY + ~EOL)";
-
- mssInitialize("system", "", "", 0, "test");
-
- iter = 60000;
-
- for(i=0;i= 0);
- buf[n] = '\0';
- assert(strcmp(buf, "This is some text.\n") == 0);
- fdClose(fd, 0);
}
+ t = mlxNextToken(lxs);
+ assert(t == MLX_TOK_STRING);
+ mlxCloseSession(lxs);
+ n = fdRead(fd, buf, sizeof(buf) - 1, 0, 0);
+ assert(n >= 0);
+ buf[n] = '\0';
+ assert(strcmp(buf, "This is some text.\n") == 0);
+ fdClose(fd, 0);
- return iter;
+ return true;
}
+long long
+test(char** tname)
+ {
+ *tname = "mtlexer-21 NODISCARD test (LINEONLY + ~EOL)";
+ mssInitialize("system", "", "", 0, "test");
+ return loopTest(doTest);
+ }
diff --git a/centrallix-lib/tests/test_newmalloc_00.c b/centrallix-lib/tests/test_newmalloc_00.c
new file mode 100644
index 000000000..33039bac5
--- /dev/null
+++ b/centrallix-lib/tests/test_newmalloc_00.c
@@ -0,0 +1,207 @@
+/************************************************************************/
+/* Centrallix Application Server System */
+/* Centrallix Base Library */
+/* */
+/* Copyright (C) 2025-2026 LightSys Technology Services, Inc. */
+/* */
+/* You may use these files and this library under the terms of the */
+/* GNU Lesser General Public License, Version 2.1, contained in the */
+/* included file "COPYING". */
+/* */
+/* Module: test_newmalloc_00.c */
+/* Author: Israel Fuller */
+/* Creation: November 25th, 2025 */
+/* Description: Test the nmSysMalloc(), nmSysFree(), nmSysRealloc(), */
+/* and nmSysStrDup functions from the NewMalloc library. */
+/************************************************************************/
+
+#include
+#include
+#include
+#include
+
+/** Test dependencies. **/
+#include "test_utils.h"
+#include "check.h"
+#include "range.h"
+
+/** Tested module. **/
+#include "newmalloc.h"
+
+/*** Valgrind instruments every memory access, so the bulk data sizes are
+ *** divided by this factor when running under it, keeping the test inside
+ *** the driver's lockup timeout.
+ ***/
+#ifdef USING_VALGRIND
+#include "valgrind/valgrind.h"
+#define BULK_DIVISOR (RUNNING_ON_VALGRIND ? 16lu : 1lu)
+#else
+#define BULK_DIVISOR 1lu
+#endif
+
+#define TEST_LIMIT (16384lu / BULK_DIVISOR)
+#define LARGE_BUF_SIZE (256000000lu / BULK_DIVISOR)
+
+static unsigned int seed_counter = 0;
+static char* err_buf;
+static unsigned int err_buf_i;
+static unsigned int err_buf_size;
+
+static int mockErrorFn(char* error_msg)
+ {
+ const size_t len = strlen(error_msg) + 1lu;
+
+ /** Ensure enough space to store the error. **/
+ while (len > err_buf_size - err_buf_i)
+ {
+ err_buf_size *= 2;
+ err_buf = checkPtr(realloc(err_buf, err_buf_size));
+ }
+
+ err_buf_i += snprintf(
+ err_buf + err_buf_i,
+ err_buf_size - err_buf_i,
+ "> %s\n", error_msg
+ );
+
+ return 0;
+ }
+
+/** Initialize memory of a given size with random data. **/
+static void* randomInit(void* ptr, size_t size)
+ {
+ if (ptr == NULL) return NULL;
+ unsigned char* p = (unsigned char*)ptr;
+ for (size_t i = 0; i < size; i++) {
+ p[i] = (unsigned char)(rand() % 256);
+ }
+ return ptr;
+ }
+
+static bool doTest(void)
+ {
+ bool success = true;
+
+ /** Set a consistent, distinct seed for each test iteration. **/
+ srand(seed_counter++);
+
+ /** Initialize the mock error function. **/
+ err_buf = checkPtr(malloc(err_buf_size = 256));
+ err_buf_i = snprintf(err_buf, err_buf_size, "%s", "");
+ nmSetErrFunction(mockErrorFn);
+
+ /** Basic string data. **/
+ char* str1;
+ success &= EXPECT_NOT_NULL(str1 = nmSysMalloc(16));
+ snprintf(str1, 16, "ThisIsSomeData!");
+ char* str2;
+ success &= EXPECT_NOT_NULL(str2 = nmSysMalloc(32));
+ snprintf(str2, 32, "ThisDataIsDifferentStringData.\n");
+ success &= EXPECT_STR_EQL(str1, "ThisIsSomeData!");
+ success &= EXPECT_STR_EQL(str2, "ThisDataIsDifferentStringData.\n");
+
+ /** Random data, varying sizes. **/
+ void** data = checkPtr(malloc(TEST_LIMIT * sizeof(void*)));
+ void** test = checkPtr(malloc(TEST_LIMIT * sizeof(void*)));
+ for (size_t i = 1lu; i < TEST_LIMIT; i++)
+ {
+ success &= EXPECT_NOT_NULL(test[i] = nmSysMalloc(i));
+ data[i] = randomInit(checkPtr(malloc(i)), i);
+ memcpy(test[i], data[i], i); /* Write test data into test memory. */
+ }
+ for (size_t i = TEST_LIMIT - 1lu; i > 0lu; i--)
+ success &= EXPECT_EQL(memcmp(data[i], test[i], i), 0, "%d");
+
+ /** Basic string data is unharmed. **/
+ success &= EXPECT_STR_EQL(str1, "ThisIsSomeData!");
+ success &= EXPECT_STR_EQL(str2, "ThisDataIsDifferentStringData.\n");
+
+ /** Reallocate all variably sized memory to a different size. **/
+ for (size_t i = TEST_LIMIT - 1lu; i > 0lu; i--)
+ success &= EXPECT_NOT_NULL(test[i] = nmSysRealloc(test[i], TEST_LIMIT - i));
+ for (size_t i = 1lu; i < TEST_LIMIT; i++)
+ success &= EXPECT_EQL(memcmp(data[i], test[i], min(i, TEST_LIMIT - i)), 0, "%d");
+
+ /** Basic string data is unharmed. **/
+ success &= EXPECT_STR_EQL(str1, "ThisIsSomeData!");
+ success &= EXPECT_STR_EQL(str2, "ThisDataIsDifferentStringData.\n");
+
+ /** Testing strdup. **/
+ char* str_dup1;
+ char* str_dup2;
+ success &= EXPECT_NOT_NULL(str_dup1 = nmSysStrdup(str1));
+ success &= EXPECT_NOT_NULL(str_dup2 = nmSysStrdup(str2));
+ success &= EXPECT_STR_EQL(str_dup1, "ThisIsSomeData!");
+ success &= EXPECT_STR_EQL(str_dup2, "ThisDataIsDifferentStringData.\n");
+ str_dup1[12] = '\0';
+ str_dup2[2] = 'a';
+ str_dup2[3] = 't';
+ success &= EXPECT_STR_EQL(str_dup1, "ThisIsSomeDa");
+ success &= EXPECT_STR_EQL(str_dup2, "ThatDataIsDifferentStringData.\n");
+
+ /** Basic string data is unharmed. **/
+ success &= EXPECT_STR_EQL(str1, "ThisIsSomeData!");
+ success &= EXPECT_STR_EQL(str2, "ThisDataIsDifferentStringData.\n");
+
+ /** Free random data, varying sizes. **/
+ for (size_t i = 1lu; i < TEST_LIMIT; i++)
+ {
+ free(data[i]);
+ nmSysFree(test[i]);
+ }
+ free(data);
+ free(test);
+
+ /** Basic string data is unharmed. **/
+ success &= EXPECT_STR_EQL(str1, "ThisIsSomeData!");
+ success &= EXPECT_STR_EQL(str2, "ThisDataIsDifferentStringData.\n");
+
+ /** Free data. **/
+ nmSysFree(str1);
+ nmSysFree(str2);
+
+ /** Dup string data is unharmed. **/
+ success &= EXPECT_STR_EQL(str_dup1, "ThisIsSomeDa");
+ success &= EXPECT_STR_EQL(str_dup2, "ThatDataIsDifferentStringData.\n");
+
+ /** Large singular allocation. **/
+ void* large_buf;
+ success &= EXPECT_NOT_NULL(large_buf = nmSysMalloc(LARGE_BUF_SIZE));
+ for (size_t i = LARGE_BUF_SIZE - 1lu; i > 0lu; i--)
+ *((unsigned char*)large_buf + i) = (unsigned char)(i % 255lu);
+ *(unsigned char*)large_buf = 0u;
+ size_t mismatches = 0lu;
+ for (size_t i = 0lu; i < LARGE_BUF_SIZE; i++)
+ if (*((unsigned char*)large_buf + i) != (unsigned char)(i % 255lu)) mismatches++;
+ success &= EXPECT_EQL(mismatches, 0lu, "%zu");
+
+ /** Dup string data is unharmed. **/
+ success &= EXPECT_STR_EQL(str_dup1, "ThisIsSomeDa");
+ success &= EXPECT_STR_EQL(str_dup2, "ThatDataIsDifferentStringData.\n");
+
+ /** Free dups. **/
+ nmSysFree(str_dup1);
+ nmSysFree(str_dup2);
+
+ /** Free large allocation. **/
+ nmSysFree(large_buf);
+
+ /** Expect no captured errors. **/
+ success &= EXPECT_STR_EQL(err_buf, "");
+
+ /** Clean up. **/
+ free(err_buf);
+
+ return success;
+ }
+
+long long test(char** tname)
+ {
+ *tname = "newmalloc-00 nmSysMalloc(), nmSysFree(), nmSysRealloc(), & nmSysStrdup()";
+ return loopTest(doTest) * ((long long)TEST_LIMIT + 3ll);
+ }
+
+/** Scope cleanup. **/
+#undef BULK_DIVISOR
+#undef TEST_LIMIT
+#undef LARGE_BUF_SIZE
diff --git a/centrallix-lib/tests/test_newmalloc_01.c b/centrallix-lib/tests/test_newmalloc_01.c
new file mode 100644
index 000000000..2dd707178
--- /dev/null
+++ b/centrallix-lib/tests/test_newmalloc_01.c
@@ -0,0 +1,174 @@
+/************************************************************************/
+/* Centrallix Application Server System */
+/* Centrallix Base Library */
+/* */
+/* Copyright (C) 2025-2026 LightSys Technology Services, Inc. */
+/* */
+/* You may use these files and this library under the terms of the */
+/* GNU Lesser General Public License, Version 2.1, contained in the */
+/* included file "COPYING". */
+/* */
+/* Module: test_newmalloc_01.c */
+/* Author: Israel Fuller */
+/* Creation: December 15th, 2025 */
+/* Description: Test the nmMalloc(), nmFree(), and nmClear() functions */
+/* from the NewMalloc library. */
+/************************************************************************/
+
+#include
+#include
+#include
+#include
+
+/** Test dependencies. **/
+#include "test_utils.h"
+#include "check.h"
+
+/** Tested module. **/
+#include "newmalloc.h"
+
+/*** Valgrind instruments every memory access, so the bulk data sizes are
+ *** divided by this factor when running under it, keeping the test inside
+ *** the driver's lockup timeout.
+ ***/
+#ifdef USING_VALGRIND
+#include "valgrind/valgrind.h"
+#define BULK_DIVISOR (RUNNING_ON_VALGRIND ? 16lu : 1lu)
+#else
+#define BULK_DIVISOR 1lu
+#endif
+
+#define TEST_LIMIT (16384lu / BULK_DIVISOR)
+#define LARGE_BUF_SIZE (256000000lu / BULK_DIVISOR)
+
+static unsigned int seed_counter = 0;
+static char* err_buf;
+static unsigned int err_buf_i;
+static unsigned int err_buf_size;
+
+static int mockErrorFn(char* error_msg)
+ {
+ const size_t len = strlen(error_msg) + 1lu;
+
+ /** Ensure enough space to store the error. **/
+ while (len > err_buf_size - err_buf_i)
+ {
+ err_buf_size *= 2;
+ err_buf = checkPtr(realloc(err_buf, err_buf_size));
+ }
+
+ err_buf_i += snprintf(
+ err_buf + err_buf_i,
+ err_buf_size - err_buf_i,
+ "> %s\n", error_msg
+ );
+
+ return 0;
+ }
+
+/** Initialize memory of a given size with random data. **/
+static void* randomInit(void* ptr, size_t size)
+ {
+ if (ptr == NULL) return NULL;
+ unsigned char* p = (unsigned char*)ptr;
+ for (size_t i = 0; i < size; i++) {
+ p[i] = (unsigned char)(rand() % 256);
+ }
+ return ptr;
+ }
+
+static bool doTest(void)
+ {
+ bool success = true;
+
+ /** Set a consistent, distinct seed for each test iteration. **/
+ srand(seed_counter++);
+
+ /** Initialize the mock error function. **/
+ err_buf = checkPtr(malloc(err_buf_size = 256));
+ err_buf_i = snprintf(err_buf, err_buf_size, "%s", "");
+ nmSetErrFunction(mockErrorFn);
+
+ /** Basic string data. **/
+ char* str1;
+ success &= EXPECT_NOT_NULL(str1 = nmMalloc(16));
+ snprintf(str1, 16, "ThisIsSomeData!");
+ char* str2;
+ success &= EXPECT_NOT_NULL(str2 = nmMalloc(32));
+ snprintf(str2, 32, "ThisDataIsDifferentStringData.\n");
+ success &= EXPECT_STR_EQL(str1, "ThisIsSomeData!");
+ success &= EXPECT_STR_EQL(str2, "ThisDataIsDifferentStringData.\n");
+
+ /** Random data, varying sizes. **/
+ void** data = checkPtr(malloc(TEST_LIMIT * sizeof(void*)));
+ void** test = checkPtr(malloc(TEST_LIMIT * sizeof(void*)));
+ for (size_t i = 1lu; i < TEST_LIMIT; i++)
+ {
+ success &= EXPECT_NOT_NULL(test[i] = nmMalloc(i));
+ data[i] = randomInit(checkPtr(malloc(i)), i);
+ memcpy(test[i], data[i], i);
+ }
+ for (size_t i = TEST_LIMIT - 1lu; i > 0lu; i--)
+ success &= EXPECT_EQL(memcmp(data[i], test[i], i), 0, "%d");
+
+ /** Basic string data is unharmed. **/
+ success &= EXPECT_STR_EQL(str1, "ThisIsSomeData!");
+ success &= EXPECT_STR_EQL(str2, "ThisDataIsDifferentStringData.\n");
+
+ /** Large singular allocation. **/
+ void* large_buf;
+ success &= EXPECT_NOT_NULL(large_buf = nmMalloc(LARGE_BUF_SIZE));
+ for (size_t i = LARGE_BUF_SIZE - 1lu; i > 0lu; i--)
+ *((unsigned char*)large_buf + i) = (unsigned char)(i % 255lu);
+ *(unsigned char*)large_buf = 0u;
+ size_t mismatches = 0lu;
+ for (size_t i = 0lu; i < LARGE_BUF_SIZE; i++)
+ if (*((unsigned char*)large_buf + i) != (unsigned char)(i % 255lu)) mismatches++;
+ success &= EXPECT_EQL(mismatches, 0lu, "%zu");
+
+ /** Dup string data is unharmed. **/
+ success &= EXPECT_STR_EQL(str1, "ThisIsSomeData!");
+ success &= EXPECT_STR_EQL(str2, "ThisDataIsDifferentStringData.\n");
+
+ /** Free random data, varying sizes. **/
+ for (size_t i = 1lu; i < TEST_LIMIT; i++)
+ {
+ free(data[i]);
+ nmFree(test[i], i);
+ }
+ free(data);
+ free(test);
+
+ /** Basic string data is unharmed. **/
+ success &= EXPECT_STR_EQL(str1, "ThisIsSomeData!");
+ success &= EXPECT_STR_EQL(str2, "ThisDataIsDifferentStringData.\n");
+
+ /** Free data. **/
+ nmFree(str1, 16);
+ nmFree(str2, 32);
+
+ /** Free large allocation. **/
+ nmFree(large_buf, LARGE_BUF_SIZE);
+
+ /** Clear cache. **/
+ nmClear();
+
+ /** Expect no captured errors. **/
+ success &= EXPECT_STR_EQL(err_buf, "");
+
+ /** Clean up. **/
+ free(err_buf);
+
+ return success;
+ }
+
+long long test(char** tname)
+ {
+ *tname = "newmalloc-01 nmMalloc(), nmFree(), & nmClear()";
+ return loopTest(doTest) * ((long long)TEST_LIMIT + 3ll);
+ }
+
+/** Scope cleanup. **/
+#undef BULK_DIVISOR
+#undef TEST_LIMIT
+#undef LARGE_BUF_SIZE
diff --git a/centrallix-lib/tests/test_newmalloc_02.c b/centrallix-lib/tests/test_newmalloc_02.c
new file mode 100644
index 000000000..08fcd6345
--- /dev/null
+++ b/centrallix-lib/tests/test_newmalloc_02.c
@@ -0,0 +1,88 @@
+/************************************************************************/
+/* Centrallix Application Server System */
+/* Centrallix Base Library */
+/* */
+/* Copyright (C) 2026 LightSys Technology Services, Inc. */
+/* */
+/* You may use these files and this library under the terms of the */
+/* GNU Lesser General Public License, Version 2.1, contained in the */
+/* included file "COPYING". */
+/* */
+/* Module: test_newmalloc_02.c */
+/* Author: Israel Fuller */
+/* Creation: September 9th, 2026 */
+/* Description: Test the nmStats() function from the NewMalloc library. */
+/************************************************************************/
+
+#include
+#include
+#include
+#include
+
+/** Test dependencies. **/
+#include "test_utils.h"
+#include "check.h"
+
+/** Tested module. **/
+#include "newmalloc.h"
+
+#define BLOCK_COUNT 64lu
+#define BLOCK_SIZE 128
+
+static bool doTest(void)
+ {
+ bool success = true;
+
+ /** Give the allocator some activity to report on. **/
+ void* blocks[BLOCK_COUNT];
+ for (size_t i = 0lu; i < BLOCK_COUNT; i++)
+ success &= EXPECT_NOT_NULL(blocks[i] = nmMalloc(BLOCK_SIZE));
+ for (size_t i = 0lu; i < BLOCK_COUNT; i++)
+ nmFree(blocks[i], BLOCK_SIZE);
+
+ /*** Debug info, captured to verify that nmStats() prints the stats.
+ *** nmStats() prints via the library's own stdout, so capturing it
+ *** requires us to redirect that file descriptor into a pipe that
+ *** we flush into a buffer. This deadlocks if stats prints over
+ *** 64kb of data and fills the pipe, but that shouldn't happen.
+ ***/
+ char stats_buf[2048];
+ int stats_pipe[2];
+ success &= EXPECT_EQL(pipe(stats_pipe), 0, "%d");
+ fflush(stdout);
+ int saved_stdout = dup(STDOUT_FILENO);
+ dup2(stats_pipe[1], STDOUT_FILENO);
+ close(stats_pipe[1]);
+ nmStats(); /** Run target code. **/
+ fflush(stdout);
+ dup2(saved_stdout, STDOUT_FILENO);
+ close(saved_stdout);
+ ssize_t stats_len = read(stats_pipe[0], stats_buf, sizeof(stats_buf) - 1lu);
+ close(stats_pipe[0]);
+ stats_buf[(stats_len > 0) ? stats_len : 0] = '\0';
+
+ /*** Every counter line is reported. The counts themselves are only
+ *** tracked when the library is built with NMMALLOC_PROFILING, so
+ *** their values aren't checked here.
+ ***/
+ success &= EXPECT_RANGE(strlen(stats_buf), (size_t)32, sizeof(stats_buf) - 1lu, "%zu");
+ success &= EXPECT_NOT_NULL(strstr(stats_buf, "NewMalloc subsystem statistics:"));
+ success &= EXPECT_NOT_NULL(strstr(stats_buf, "nmMalloc:"));
+ success &= EXPECT_NOT_NULL(strstr(stats_buf, "nmFree:"));
+ success &= EXPECT_NOT_NULL(strstr(stats_buf, "bigblks:"));
+
+ /** Clear cache. **/
+ nmClear();
+
+ return success;
+ }
+
+long long test(char** tname)
+ {
+ *tname = "newmalloc-02 nmStats()";
+ return loopTest(doTest);
+ }
+
+/** Scope cleanup. **/
+#undef BLOCK_COUNT
+#undef BLOCK_SIZE
diff --git a/centrallix-lib/tests/test_qprintf_00.c b/centrallix-lib/tests/test_qprintf_00.c
index f8ad85dbc..ceac9ef72 100644
--- a/centrallix-lib/tests/test_qprintf_00.c
+++ b/centrallix-lib/tests/test_qprintf_00.c
@@ -5,42 +5,44 @@
#include
#include "qprintf.h"
#include
+#include
+#include "test_utils.h"
-long long
-test(char** tname)
+static bool
+doTest(void)
{
- int i;
- int iter;
unsigned char buf[44];
- *tname = "qprintf-00 constant string using snprintf()";
- iter = 200000;
- for(i=0;i
#include "qprintf.h"
#include
+#include
+#include "test_utils.h"
-long long
-test(char** tname)
+static bool
+doTest(void)
{
- int i;
- int iter;
unsigned char buf[44];
- *tname = "qprintf-01 constant string using qpfPrintf()";
- iter = 200000;
- for(i=0;i
#include "qprintf.h"
#include
+#include
+#include "test_utils.h"
-long long
-test(char** tname)
+static bool
+doTest(void)
{
- int i;
- int iter;
unsigned char buf[44];
- *tname = "qprintf-02 constant string with truncation, qpfPrintf()";
- iter = 200000;
- for(i=0;i
#include "qprintf.h"
#include
+#include
+#include "test_utils.h"
-long long
-test(char** tname)
+static bool
+doTest(void)
{
- int i;
- int iter;
unsigned char buf[44];
- *tname = "qprintf-03 constant string, 1char overflow, using qpfPrintf()";
- iter = 200000;
- for(i=0;i
#include "qprintf.h"
#include
+#include
+#include "test_utils.h"
-long long
-test(char** tname)
+static bool
+doTest(void)
{
- int i;
- int iter;
unsigned char buf[44];
- *tname = "qprintf-04 empty string using qpfPrintf()";
- iter = 200000;
- for(i=0;i
#include "qprintf.h"
#include
+#include
+#include "test_utils.h"
-long long
-test(char** tname)
+static bool
+doTest(void)
{
- int i;
- int iter;
unsigned char buf[44];
- *tname = "qprintf-05 constant string into 0-sized buf using qpfPrintf()";
- iter = 200000;
- for(i=0;i
#include "qprintf.h"
#include
+#include
+#include "test_utils.h"
-long long
-test(char** tname)
+static bool
+doTest(void)
{
- int i;
- int iter;
unsigned char buf[44];
- *tname = "qprintf-06 constant string into 1-sized buf using qpfPrintf()";
- iter = 200000;
- for(i=0;i
#include "qprintf.h"
#include
+#include
+#include "test_utils.h"
-long long
-test(char** tname)
+static bool
+doTest(void)
{
- int i, rval;
- int iter;
+ int rval;
unsigned char buf[44];
- *tname = "qprintf-07 %STR insertion in middle without overflow";
- iter = 200000;
- for(i=0;i
#include "qprintf.h"
#include
+#include
+#include "test_utils.h"
-long long
-test(char** tname)
+static bool
+doTest(void)
{
- int i, rval;
- int iter;
+ int rval;
unsigned char buf[44];
- *tname = "qprintf-08 %STR insertion at end without overflow";
- iter = 200000;
- for(i=0;i
#include "qprintf.h"
#include
+#include
+#include "test_utils.h"
-long long
-test(char** tname)
+static bool
+doTest(void)
{
- int i, rval;
- int iter;
+ int rval;
unsigned char buf[44];
- *tname = "qprintf-09 %STR insertion at beginning without overflow";
- iter = 200000;
- for(i=0;i
#include "qprintf.h"
#include
+#include
+#include "test_utils.h"
-long long
-test(char** tname)
+static bool
+doTest(void)
{
- int i, rval;
- int iter;
+ int rval;
unsigned char buf[44];
- *tname = "qprintf-10 compare with snprintf(), %s inserted";
- iter = 200000;
- for(i=0;i
#include "qprintf.h"
#include
+#include
+#include "test_utils.h"
-long long
-test(char** tname)
+static bool
+doTest(void)
{
- int i, rval;
- int iter;
+ int rval;
unsigned char buf[44];
- *tname = "qprintf-11 %STR insertion in middle with overflow after STR";
- iter = 200000;
- for(i=0;i
#include "qprintf.h"
#include
+#include
+#include "test_utils.h"
-long long
-test(char** tname)
+static bool
+doTest(void)
{
- int i, rval;
- int iter;
+ int rval;
unsigned char buf[44];
- *tname = "qprintf-12 %STR insertion in middle with overflow in STR";
- iter = 200000;
- for(i=0;i
#include "qprintf.h"
#include
+#include
+#include "test_utils.h"
-long long
-test(char** tname)
+static bool
+doTest(void)
{
- int i, rval;
- int iter;
+ int rval;
unsigned char buf[44];
- *tname = "qprintf-13 %STR insertion in middle with overflow before STR";
- iter = 200000;
- for(i=0;i
#include "qprintf.h"
#include
+#include
+#include "test_utils.h"
-long long
-test(char** tname)
+static bool
+doTest(void)
{
- int i, rval;
- int iter;
+ int rval;
unsigned char buf[44];
- *tname = "qprintf-14 %INT insertion in middle without overflow";
- iter = 200000;
- for(i=0;i
#include "qprintf.h"
#include
+#include
+#include "test_utils.h"
-long long
-test(char** tname)
+static bool
+doTest(void)
{
- int i, rval;
- int iter;
+ int rval;
unsigned char buf[44];
- *tname = "qprintf-15 %POS insertion in middle without overflow";
- iter = 200000;
- for(i=0;i
#include
+#include
+#include "test_utils.h"
-long long
-test(char** tname)
+static bool
+doTest(void)
{
- int i, rval;
- int iter;
+ int rval;
unsigned char buf[44];
- *tname = "qprintf-16 %POS insertion without overflow, negative";
- iter = 200000;
- for(i=0;i
#include "qprintf.h"
#include
+#include
+#include "test_utils.h"
-long long
-test(char** tname)
+static bool
+doTest(void)
{
- int i, rval;
- int iter;
+ int rval;
unsigned char buf[44];
- *tname = "qprintf-17 %NSTR insertion in middle without overflow";
- iter = 200000;
- for(i=0;i
#include "qprintf.h"
#include
+#include
+#include "test_utils.h"
-long long
-test(char** tname)
+static bool
+doTest(void)
{
- int i, rval;
- int iter;
+ int rval;
unsigned char buf[44];
- *tname = "qprintf-18 %NSTR insertion in middle with insert overflow";
- iter = 200000;
- for(i=0;i
#include "qprintf.h"
#include
+#include
+#include "test_utils.h"
-long long
-test(char** tname)
+static bool
+doTest(void)
{
- int i, rval;
- int iter;
+ int rval;
unsigned char buf[44];
- *tname = "qprintf-19 %*STR insertion in middle with insert overflow";
- iter = 200000;
- for(i=0;i
#include "qprintf.h"
#include
+#include
+#include "test_utils.h"
-long long
-test(char** tname)
+static bool
+doTest(void)
{
- int i, rval;
- int iter;
+ int rval;
unsigned char buf[44];
- *tname = "qprintf-20 %CHR insertion in middle without overflow";
- iter = 200000;
- for(i=0;i
#include "qprintf.h"
#include
+#include