help: the features of the build listed, and the tests skip what they need
Some checks failed
C/C++ CI Linux / ${{ matrix.target }} (ubuntu-24.04-arm) (push) Has been cancelled
C/C++ CI Linux / ${{ matrix.target }} (ubuntu-latest) (push) Has been cancelled
C/C++ CI MacOS / ${{ matrix.target }} (macos-15) (push) Has been cancelled
C/C++ CI Windows / ${{ matrix.target }} (windows-2022) (push) Has been cancelled
C/C++ CI cmake / ${{ matrix.target }} (macos-15) (push) Has been cancelled
C/C++ CI cmake / ${{ matrix.target }} (ubuntu-24.04-arm) (push) Has been cancelled
C/C++ CI cmake / ${{ matrix.target }} (ubuntu-latest) (push) Has been cancelled
C/C++ CI cmake / ${{ matrix.target }} (windows-2022) (push) Has been cancelled
C/C++ CI cmake / ubuntu-latest (wolfSSL) (push) Has been cancelled

3proxy --help (and the usage message of a bad invocation) now lists
what the build was made with: TLS and its backend, RADIUS, PCRE, IPv6,
UNIX sockets, the built-in HTTP server, transparency, ODBC and the
optional proxy services. The test runner reads the list before the
first case and the cases state the features they need with
t.skip_unless(), so a build without a feature skips its cases instead
of failing them; a binary too old to report anything skips the cases
with needs and runs the rest.
This commit is contained in:
Vladimir Dubrovin 2026-10-30 14:17:54 +03:00
parent a75b488aaf
commit 629a978c00
29 changed files with 195 additions and 14 deletions

View File

@ -382,6 +382,21 @@ void cyclestep(void){
#define RETURN(x) {res = x; goto CLEARRETURN;}
static void printusage(struct extparam *cconf, int argc, char * argv[]){
fprintf(stderr, "Usage: %s [conffile]\n", argv[0]);
#ifdef _WIN32
fprintf(stderr, "\n\t%s --install [conffile]\n\tto install as service\n"
"\n\t%s --remove\n\tto remove service\n", argv[0], argv[0]);
#else
fprintf(stderr, "\n if conffile is missing, configuration is expected from stdin\n");
#endif
fprintf(stderr, "\n\t%s --help\n\tthis message\n", argv[0]);
fprintf(stderr, "available socket options:\n\t%s\n", printopts("\n\t"));
fprintf(stderr, "built with:\n\t%s\n", printfeatures("\n\t"));
fprintf(stderr, "\n%s %s\n%s\n", cconf->stringtable[2], cconf->stringtable[3], copyright);
}
#ifndef _WINCE
int main(int argc, char * argv[]) {
@ -424,6 +439,10 @@ int WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLine, int
#endif
conf.stringtable = strings;
if(argc == 2 && (!strcmp((char *)argv[1], "--help") || !strcmp((char *)argv[1], "-h"))) {
printusage(&conf, argc, argv);
return 0;
}
#ifdef _WIN32
#ifndef _WINCE
if((argc == 2 || argc == 3)&& !strcmp((char *)argv[1], "--install")) {
@ -536,18 +555,9 @@ int WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLine, int
}
if(argc > 2 || !(fp)) {
fprintf(stderr, "Usage: %s [conffile]\n", argv[0]);
#ifdef _WIN32
fprintf(stderr, "\n\t%s --install [conffile]\n\tto install as service\n"
"\n\t%s --remove\n\tto remove service\n", argv[0], argv[0]);
#else
fprintf(stderr, "\n if conffile is missing, configuration is expected from stdin\n");
#endif
fprintf(stderr, "available socket options:\n\t%s\n", printopts("\n\t"));
fprintf(stderr, "\n%s %s\n%s\n", conf.stringtable[2], conf.stringtable[3], copyright);
printusage(&conf, argc, argv);
return 1;
}
}
#ifndef _WIN32
/* before readconfig() starts the services: a client gone while it is

View File

@ -512,6 +512,7 @@ extern struct socketoptions {
int getopts(const char *s, struct sockoptset *opts);
void setopts(SOCKET s, const struct sockoptset *opts);
char * printopts(char *sep);
char * printfeatures(char *sep);
#ifdef _WINCE
char * CEToUnicode (const char *str);

View File

@ -226,6 +226,69 @@ char * printopts(char *sep){
}
char featsbuf[1024];
/* The features of this build, by the names the manual and the test suite
know them, so a user can see what a binary was made with. */
char * printfeatures(char *sep){
int pos=0;
#define ADDFEATURE(x) pos += sprintf(featsbuf+pos,"%s%s",pos?sep:"",x)
#ifdef WITH_SSL
ADDFEATURE("ssl");
#ifdef WITH_WOLFSSL
ADDFEATURE("wolfssl");
#else
ADDFEATURE("openssl");
#endif
#endif
#ifndef NORADIUS
ADDFEATURE("radius");
#endif
#ifdef WITH_PCRE
ADDFEATURE("pcre");
#endif
#ifndef NOIPV6
ADDFEATURE("ipv6");
#endif
#ifdef WITH_UN
ADDFEATURE("unix");
#endif
#ifdef WITH_HTTPSRV
ADDFEATURE("httpsrv");
#endif
#ifdef WITH_TRANSPARENT
ADDFEATURE("transparent");
#endif
#ifdef WITH_NETFILTER
ADDFEATURE("netfilter");
#endif
#ifdef WITH_PF
ADDFEATURE("pf");
#endif
#ifdef WITH_ODBC
ADDFEATURE("odbc");
#endif
#ifdef WITH_FTP
ADDFEATURE("ftppr");
#endif
#ifdef WITH_POP3P
ADDFEATURE("pop3p");
#endif
#ifdef WITH_SMTPP
ADDFEATURE("smtpp");
#endif
#ifdef WITH_IMAPP
ADDFEATURE("imapp");
#endif
#ifndef NOPLUGINS
ADDFEATURE("plugins");
#endif
#undef ADDFEATURE
return featsbuf;
}
/* A comma separated list of options, each NAME or NAME=VALUE; a name alone
stands for 1, or for TCP_MAXSEG for the value of maxseg. Options are added
to what is already set, so a default is changed only by naming it, and

View File

@ -13,6 +13,14 @@ skips itself when that is missing or the build has no TLS support. With no
`--bin` it looks in `bin/`, then `build/bin/`, then the per-configuration
directories a multi-configuration CMake generator uses.
The binary reports what it was built with in `3proxy --help` (a `built
with:` list), and the runner reads that before the first case: a case names
the features it needs with `if not t.skip_unless("ssl"): return`, and
skips on a build without them instead of failing. A binary too old to
report its features skips every case that needs one, and the cases with no
feature needs still run; some cases also probe at runtime, as a build can
leave a feature compiled in but unusable.
The proxy under test is also the origin server the tests talk to: the `http`
command's `echo` operation reports back how a request arrived - method, path,
query, host, and the source port it came from - and `data` generates a body

View File

@ -4,6 +4,8 @@ import time
def run(t):
if not t.skip_unless("httpsrv"):
return
adm = t.free_port()
lim = t.free_port()
server = t.start("admin", f"""

View File

@ -7,7 +7,9 @@ handshake and the service supplies the port.
def run(t):
certs = t.certs()
if not t.skip_unless("httpsrv"):
return
certs = t.certs() if t.has("ssl") else None
plain = t.free_port()
port = t.free_port()
secure = t.free_port() if certs else None

View File

@ -60,6 +60,8 @@ def reply(f):
def run(t):
if not t.skip_unless("ftppr"):
return
servers = {}
for name, (user, password) in LOGINS.items():
listener = socket.socket()

View File

@ -2,6 +2,8 @@
def run(t):
if not t.skip_unless("httpsrv"):
return
srv = t.free_port()
openport = t.free_port()
t.start("httpsrv_auth", f"""

View File

@ -8,6 +8,8 @@ import os
def run(t):
if not t.skip_unless("httpsrv"):
return
root = os.path.join(t.tmpdir, "web")
os.makedirs(os.path.join(root, "picts", "set"), exist_ok=True)
with open(os.path.join(root, "a.html"), "w") as fp:

View File

@ -10,6 +10,8 @@ import os
def run(t):
if not t.skip_unless("httpsrv"):
return
root = os.path.join(t.tmpdir, "ka")
os.makedirs(root, exist_ok=True)
with open(os.path.join(root, "a.html"), "w") as fp:

View File

@ -4,6 +4,8 @@ import time
def run(t):
if not t.skip_unless("httpsrv"):
return
srv = t.free_port()
t.start("httpsrv_ops", f"""
log

View File

@ -6,6 +6,8 @@ most of them away before they ever reached the server.
def run(t):
if not t.skip_unless("httpsrv"):
return
srv = t.free_port()
t.start("httpsrv_parsing", f"""
log

View File

@ -9,6 +9,8 @@ import os
def run(t):
if not t.skip_unless("httpsrv"):
return
root = os.path.join(t.tmpdir, "pp")
os.makedirs(root, exist_ok=True)
with open(os.path.join(root, "a.html"), "w") as fp:

View File

@ -2,6 +2,8 @@
def run(t):
if not t.skip_unless("httpsrv"):
return
srv = t.free_port()
srv2 = t.free_port()
t.start("httpsrv_rules", f"""

View File

@ -7,6 +7,8 @@ nscache6: nscache holds the IPv4 side and nothing else.
def run(t):
if not t.skip_unless("httpsrv", "ipv6"):
return
if not t.has_ipv6():
t.skip("IPv6 (this machine has no IPv6 loopback)")
return

View File

@ -8,6 +8,8 @@ import time
def run(t):
if not t.skip_unless("httpsrv"):
return
# The origin runs in a process of its own, so the limit only applies to
# the mapper in front of it.
origin = t.free_port()

View File

@ -30,6 +30,8 @@ def wait_served(server, needle, count, since=0, timeout=5.0):
def run(t):
if not t.skip_unless("httpsrv"):
return
srv = t.free_port()
good = t.free_port()
spare = t.free_port()

View File

@ -46,6 +46,8 @@ UNHONOURED = (1, 99)
def run(t):
if not t.skip_unless("httpsrv"):
return
srv = t.free_port()
prx = t.free_port()
sks = t.free_port()

View File

@ -37,6 +37,8 @@ def wait_served(server, needle, count, since=0, timeout=5.0):
def run(t):
if not t.skip_unless("httpsrv"):
return
adm = t.free_port()
prx = t.free_port()
dummy = t.free_port()

View File

@ -20,6 +20,8 @@ def _has_pcre(t):
def run(t):
if not t.skip_unless("httpsrv", "pcre"):
return
if not _has_pcre(t):
t.skip("PCRE (this build has no PCRE support)")
return

View File

@ -26,6 +26,8 @@ def halfclose_origin(listener):
def run(t):
if not t.skip_unless("httpsrv"):
return
# --- tcppm ---------------------------------------------------------
origin = t.free_port()
mapped = t.free_port()

View File

@ -6,6 +6,8 @@ from a clean list.
def run(t):
if not t.skip_unless("httpsrv"):
return
srv = t.free_port()
other = t.free_port()
prx = t.free_port()

View File

@ -2,6 +2,8 @@
def run(t):
if not t.skip_unless("httpsrv"):
return
srv = t.free_port()
sks = t.free_port()
sauth = t.free_port()

View File

@ -11,6 +11,8 @@ def _no_tls(t, server):
def run(t):
if not t.skip_unless("httpsrv", "ssl"):
return
certs = t.certs()
if not certs:
t.skip("TLS (openssl is not available to generate certificates)")

View File

@ -2,6 +2,8 @@
def run(t):
if not t.skip_unless("httpsrv", "ssl"):
return
certs = t.certs()
if not certs:
t.skip("tlspr (openssl is not available to generate certificates)")

View File

@ -39,6 +39,8 @@ def _config(t, name, body):
def run(t):
if not t.skip_unless("httpsrv"):
return
# --- the command and its modes ------------------------------------
# These need no redirection, so they run wherever the feature is built.
out = _config(t, "transparent_probe", "log\ntransparent")

View File

@ -36,6 +36,8 @@ def unix_get(path, url, timeout=10):
def run(t):
if not t.skip_unless("httpsrv", "unix"):
return
if not hasattr(socket, "AF_UNIX") or sys.platform.startswith("win"):
t.skip("Unix domain sockets (not on this platform)")
return

View File

@ -122,10 +122,11 @@ class Failure(Exception):
class Tester:
"""The API a case runs against: start servers, make requests, assert."""
def __init__(self, binary, tmpdir, case):
def __init__(self, binary, tmpdir, case, features=None):
self.binary = binary
self.tmpdir = tmpdir
self.case = case
self.features = features if features is not None else frozenset()
self.servers = []
self.checks = []
self.timeout = 10
@ -135,6 +136,25 @@ class Tester:
self.logs = []
self.udp_servers = []
# ---- build features ----------------------------------------------
def has(self, *names):
"""Whether the binary reports every named feature."""
return all(name in self.features for name in names)
def skip_unless(self, *names):
"""Skip what needs a feature the binary was built without.
Returns False after recording the skip, so a case says
`if not t.skip_unless("ssl"): return` and keeps its checks out of
the failed count on a build that cannot pass them.
"""
missing = [name for name in names if name not in self.features]
if not missing:
return True
self.skip("built without: " + ", ".join(missing))
return False
# ---- servers -----------------------------------------------------
def has_ipv6(self):

View File

@ -47,6 +47,37 @@ def load_case(path):
return name, module
def built_features(binary):
"""The features the binary reports in `3proxy --help`, as a set.
A binary too old to have the list, or one that cannot be run at all,
reports nothing: every case that needs a feature then skips, and the
ones that need none still run.
"""
import subprocess
try:
done = subprocess.run([binary, "--help"], stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, timeout=15)
text = done.stdout.decode("utf-8", "replace")
except (OSError, subprocess.SubprocessError):
return frozenset()
features = set()
collecting = False
for line in text.splitlines():
stripped = line.strip()
if line.startswith("built with:"):
collecting = True
rest = stripped[len("built with:"):].strip()
if rest:
features.update(rest.split())
continue
if collecting:
if not line[:1].isspace() or not stripped:
break
features.update(stripped.split())
return frozenset(features)
def main():
parser = argparse.ArgumentParser()
parser.add_argument("pattern", nargs="?", default="",
@ -75,7 +106,9 @@ def main():
return 2
tmpdir = tempfile.mkdtemp(prefix="3proxy-tests.")
features = built_features(binary)
print(f"3proxy tests: {binary}")
print(f"built with: {' '.join(sorted(features)) or '(nothing reported)'}")
print(f"working in: {tmpdir}\n")
passed = failed = skipped = 0
@ -85,7 +118,7 @@ def main():
for path in paths:
name, module = load_case(path)
print(f" {name}")
tester = Tester(binary, tmpdir, name)
tester = Tester(binary, tmpdir, name, features)
error = None
try:
module.run(tester)