← Commit list

a5542c23e7

Stefan Eissing
Stefan Eissing
2026-04-10
SUCCESS Duration: 247s

Subject

pytest: adding support for dnsd test server

Binary

No binary (build failed)

Architecture & Compiler Flags

Architecture: x86_64-linux-gnu Compiler: gcc (Ubuntu 11.4.0-1ubuntu1~22.04.3) 11.4.0

—

Build Log

(log yok)

Commit Diff GitHub →

tests/http/Makefile.am+1 -0
@@ -28,6 +28,7 @@ TESTENV = \
testenv/certs.py \
testenv/client.py \
testenv/curl.py \
+ testenv/dnsd.py \
testenv/dante.py \
testenv/env.py \
testenv/httpd.py \
tests/http/test_21_resolve.py+48 -1
@@ -27,9 +27,10 @@
import logging
import os
from datetime import timedelta
+from typing import Generator
import pytest
-from testenv import CurlClient, Env, LocalClient
+from testenv import CurlClient, Env, LocalClient, Dnsd
log = logging.getLogger(__name__)
@@ -38,6 +39,13 @@ log = logging.getLogger(__name__)
@pytest.mark.skipif(condition=not Env.curl_has_feature('AsynchDNS'), reason="needs AsynchDNS")
class TestResolve:
+ @pytest.fixture(scope='class')
+ def dnsd(self, env: Env) -> Generator[Dnsd, None, None]:
+ dnsd = Dnsd(env=env)
+ assert dnsd.initial_start()
+ yield dnsd
+ dnsd.stop()
+
@pytest.fixture(autouse=True, scope='class')
def _class_scope(self, env, httpd):
indir = httpd.docs_dir
@@ -117,6 +125,45 @@ class TestResolve:
r.check_stats(count=count, http_status=0, exitcode=6)
assert r.duration > timedelta(milliseconds=count * delay_ms), f'{r}'
+ # dnsd with no answers
+ @pytest.mark.skipif(condition=not Env.curl_override_dns(), reason="no DNS override")
+ def test_21_06_dnsd_empty(self, env: Env, httpd, dnsd):
+ dnsd.set_answers()
+ run_env = os.environ.copy()
+ run_env['CURL_DNS_SERVER'] = f'127.0.0.1:{dnsd.port}'
+ curl = CurlClient(env=env, run_env=run_env, force_resolv=False)
+ url = f'https://test-dnsd.http.curl.invalid/'
+ r = curl.http_download(urls=[url], with_stats=True)
+ r.check_exit_code(6) # could not resolve host
+ r.check_stats(count=1, http_status=0, exitcode=6)
+
+ # dnsd with one answer for A
+ @pytest.mark.skipif(condition=not Env.curl_override_dns(), reason="no DNS override")
+ def test_21_07_dnsd_a(self, env: Env, httpd, dnsd):
+ dnsd.set_answers(addr_a=['127.0.0.1'])
+ run_env = os.environ.copy()
+ run_env['CURL_DNS_SERVER'] = f'127.0.0.1:{dnsd.port}'
+ curl = CurlClient(env=env, run_env=run_env, force_resolv=False)
+ url = f'https://{env.authority_for(env.domain1, "http/1.1")}/data.json'
+ r = curl.http_download(urls=[url], with_stats=True)
+ r.check_exit_code(0)
+ r.check_stats(count=1, http_status=200, exitcode=0)
+ assert r.stats[0]['remote_ip'] == '127.0.0.1'
+
+ # dnsd with one answer for AAAA
+ @pytest.mark.skipif(condition=not Env.curl_override_dns(), reason="no DNS override")
+ @pytest.mark.skipif(condition=not Env.curl_has_feature('IPv6'), reason="no IPv6")
+ def test_21_08_dnsd_aaaa(self, env: Env, httpd, dnsd):
+ dnsd.set_answers(addr_aaaa=['[::1]'])
+ run_env = os.environ.copy()
+ run_env['CURL_DNS_SERVER'] = f'127.0.0.1:{dnsd.port}'
+ curl = CurlClient(env=env, run_env=run_env, force_resolv=False)
+ url = f'https://{env.authority_for(env.domain1, "http/1.1")}/data.json'
+ r = curl.http_download(urls=[url], with_stats=True)
+ r.check_exit_code(0)
+ r.check_stats(count=1, http_status=200, exitcode=0)
+ assert r.stats[0]['remote_ip'] == '::1'
+
def _clean_files(self, files):
for file in files:
if os.path.exists(file):
tests/http/testenv/__init__.py+1 -0
@@ -36,6 +36,7 @@ from .caddy import Caddy
from .certs import Credentials, TestCA
from .client import LocalClient
from .dante import Dante
+from .dnsd import Dnsd
from .env import Env
from .httpd import Httpd
from .nghttpx import Nghttpx, NghttpxFwd, NghttpxQuic
tests/http/testenv/dnsd.py+157 -0
@@ -0,0 +1,157 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#***************************************************************************
+# _ _ ____ _
+# Project ___| | | | _ \| |
+# / __| | | | |_) | |
+# | (__| |_| | _ <| |___
+# \___|\___/|_| \_\_____|
+#
+# Copyright (C) Daniel Stenberg, <[email protected]>, et al.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at https://curl.se/docs/copyright.html.
+#
+# You may opt to use, copy, modify, merge, publish, distribute and/or sell
+# copies of the Software, and permit persons to whom the Software is
+# furnished to do so, under the terms of the COPYING file.
+#
+# This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
+# KIND, either express or implied.
+#
+# SPDX-License-Identifier: curl
+#
+###########################################################################
+#
+import logging
+import os
+import socket
+import subprocess
+import time
+from datetime import datetime, timedelta
+from typing import Dict, List, Optional
+
+from .env import Env
+from .ports import alloc_ports_and_do
+
+log = logging.getLogger(__name__)
+
+
+class Dnsd:
+
+ def __init__(self, env: Env):
+ self.env = env
+ self._cmd = os.path.join(env.build_dir, 'tests/server/servers')
+ self._port = 0
+ self.name = 'dnsd'
+ self._port_skey = 'dnsd'
+ self._port_specs = {
+ 'dnsd': socket.SOCK_DGRAM,
+ }
+ self._dnsd_dir = os.path.join(env.gen_dir, self.name)
+ self._log_dir = self._dnsd_dir
+ self._lock_dir = os.path.join(self._dnsd_dir, 'lock')
+ self._log_file = os.path.join(self._log_dir, 'dnsd.log')
+ self._conf_file = os.path.join(self._log_dir, 'dnsd.cmd')
+ self._pid_file = os.path.join(self._log_dir, 'dante.pid')
+ self._error_log = os.path.join(self._log_dir, 'dnsd.err.log')
+ self._process = None
+
+ self.clear_logs()
+
+ @property
+ def port(self) -> int:
+ return self._port
+
+ def clear_logs(self):
+ self._rmf(self._log_file)
+ self._rmf(self._error_log)
+
+ def exists(self):
+ return os.path.exists(self._cmd)
+
+ def is_running(self):
+ if self._process:
+ self._process.poll()
+ return self._process.returncode is None
+ return False
+
+ def start_if_needed(self):
+ if not self.is_running():
+ return self.start()
+ return True
+
+ def stop(self, wait_dead=True):
+ if self._process:
+ self._process.terminate()
+ self._process.wait(timeout=2)
+ self._process = None
+ return not wait_dead or True
+ return True
+
+ def restart(self):
+ self.stop()
+ return self.start()
+
+ def initial_start(self):
+ self._mkpath(self._lock_dir)
+
+ def startup(ports: Dict[str, int]) -> bool:
+ self._port = ports[self._port_skey]
+ if self.start():
+ self.env.update_ports(ports)
+ return True
+ self.stop()
+ self._port = 0
+ return False
+
+ return alloc_ports_and_do(self._port_specs, startup,
+ self.env.gen_root, max_tries=3)
+
+ def start(self, wait_live=True):
+ assert self._port > 0
+ if self._process:
+ self.stop()
+ self.set_answers() # empty answers by default
+ self._rmf(self._log_file)
+ args = [
+ self._cmd, 'dnsd',
+ '--port', str(self._port),
+ '--logdir', f'{self._log_dir}',
+ '--logfile', f'{self._log_file}',
+ '--pidfile', f'{self._pid_file}',
+ ]
+ procerr = open(self._error_log, 'a')
+ self._process = subprocess.Popen(args=args, stderr=procerr)
+ if self._process.returncode is not None:
+ return False
+ return self.wait_live(timeout=timedelta(seconds=Env.SERVER_TIMEOUT))
+
+ def wait_live(self, timeout: timedelta):
+ try_until = datetime.now() + timeout
+ while datetime.now() < try_until:
+ if os.path.exists(self._log_file):
+ return True
+ time.sleep(.1)
+ log.error(f"Server still not responding after {timeout}")
+ return False
+
+ def _rmf(self, path):
+ if os.path.exists(path):
+ os.remove(path)
+
+ def _mkpath(self, path):
+ if not os.path.exists(path):
+ os.makedirs(path)
+
+ def set_answers(self, addr_a: Optional[List[str]] = None,
+ addr_aaaa: Optional[List[str]] = None):
+ conf = []
+ if addr_a:
+ conf.extend([f'A: {addr}' for addr in addr_a])
+ if addr_aaaa:
+ conf.extend([f'AAAA: {addr}' for addr in addr_aaaa])
+ conf.append('\n')
+ with open(self._conf_file, 'w') as fd:
+ fd.write("\n".join(conf))
tests/http/testenv/env.py+5 -0
@@ -164,6 +164,7 @@ class EnvConfig:
raise RuntimeError(f'{self.curlinfo} failed with exit code: {p.returncode}')
self.curl_is_verbose = 'verbose-strings: ON' in p.stdout
self.curl_can_cert_status = 'cert-status: ON' in p.stdout
+ self.curl_override_dns = 'override-dns: ON' in p.stdout
self.ports = {}
@@ -510,6 +511,10 @@ class Env:
def curl_can_cert_status() -> bool:
return Env.CONFIG.curl_can_cert_status
+ @staticmethod
+ def curl_override_dns() -> bool:
+ return Env.CONFIG.curl_override_dns
+
@staticmethod
def curl_can_early_data() -> bool:
if Env.curl_uses_lib('gnutls'):
tests/server/dnsd.c+2 -1
@@ -365,7 +365,8 @@ static void read_instructions(void)
}
}
else {
- rc = 0;
+ /* accept empty line */
+ rc = buf[0] ? 0 : 1;
}
if(rc != 1) {
logmsg("Bad line in %s: '%s'\n", file, buf);