Author: brane
Date: Wed May 28 09:36:46 2025
New Revision: 1925899
URL: http://svn.apache.org/viewvc?rev=1925899&view=rev
Log:
Improve the XML validation support in the test suite by making it optional
and adding some tooling for different ways to install and use the required
dependencies:
* run_tests.py --create-python-venv=/abspath /abs-srcdir
will only create a virtual environment, at /abspath/__venv__, install
dependencies and print the path to the python interpreter in that venv.
* run_test.py --python-venv=/abspath ... will run the tests assuming
there's a virtual environment in /abspath/__venv__ and will not
create one in the work test area.
* Without either of those arguments, the tests will run as before and
will create a virtual environment in the test work area, but the
dependency checks have been improved so that we don't have to run
pip every time.
* build/run_tests.py
(create_parser): Add options --create-python-venv and --pyhon-venv.
(TestHarness._init_py_tests): Forward --python-venv to svntest.main.
Run svntest.main.ensure_dependencies() after svntest.main.parse_options().
(TestHarness.run): Scan the logs for XML validation errors.
(main_create_venv): New. Implements --create-python-venv.
(main): Forward to main_create_venv when the option was provided.
* subversion/tests/cmdline/svntest/main.py; Import importlib.
(venv_dir): Remove.
(venv_base): New global variable.
(venv_path): New, replaces venv_dir, but is now a callable.
(venv_create): New global flag.
(found_dependencies): New; the set of dependency modules that were found
in the runtime environment, not necessarily in our virtual environment.
(dependencies_ensured): Remove.
(unless_ra_type_dav): Remove predicate.
(is_bad_xml_fatal): New predicate. Will return False unless all required
depdendencies to perform XML validation wer found.
(TestSpawningThread.run_one): Forward --python-venv to the subprocess.
(_create_parser): Add option --python-venv.
(parse_options): Override venv_base and venv_create for --python-venv.
(run_tests): Remove the call to ensure_dependencies() and ...
(execute_tests): ... move it here, with a consistency check.
(ensure_dependencies): Completely change the search for dependencies,
taking into account modules installed outside our virtual environment.
Only create the venv if some dependencies are missing, and when noticed
overriden by the --python-venv option.
(create_python_venv): New; creates the virtual environment.
* subversion/tests/cmdline/svntest/verify.py
(validate_xml_schema): Validation errors can be non-fatal. Properly use
the logger instead of just printing messages, so that run_test.py can
parse them from the log file.
* subversion/tests/cmdline/prop_tests.py
(xml_unsafe_author2): Update the XFail condition. Add a note about
the reason why the XML is invalid (see SVN-4919).
* CMakeLists.txt: Create the virtual environment for testing in the
root of the test area and use the python executable from the venv
to run the tests.
Modified:
subversion/trunk/CMakeLists.txt
subversion/trunk/build/run_tests.py
subversion/trunk/subversion/tests/cmdline/prop_tests.py
subversion/trunk/subversion/tests/cmdline/svntest/main.py
subversion/trunk/subversion/tests/cmdline/svntest/verify.py
Modified: subversion/trunk/CMakeLists.txt
URL:
http://svn.apache.org/viewvc/subversion/trunk/CMakeLists.txt?rev=1925899&r1=1925898&r2=1925899&view=diff
==============================================================================
--- subversion/trunk/CMakeLists.txt (original)
+++ subversion/trunk/CMakeLists.txt Wed May 28 09:36:46 2025
@@ -779,12 +779,28 @@ if(SVN_ENABLE_TESTS)
find_package(Python3 COMPONENTS Interpreter REQUIRED)
set(run_tests_script "${CMAKE_CURRENT_SOURCE_DIR}/build/run_tests.py")
set(list_tests_script "${CMAKE_CURRENT_SOURCE_DIR}/build/list_tests.py")
+ set(test_base_dir "${CMAKE_CURRENT_BINARY_DIR}/Testing")
+
+ # Create the virtual environment for Python tests.
+ execute_process(
+ COMMAND
+ "${Python3_EXECUTABLE}" "${run_tests_script}"
+ --create-python-venv "${test_base_dir}"
+ ${CMAKE_CURRENT_SOURCE_DIR}
+ OUTPUT_VARIABLE command_output
+ WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
+ RESULT_VARIABLE command_result
+ )
+ if (command_result)
+ message(FATAL_ERROR "run_tests.py --create-python-venv failed.")
+ endif()
+ string(STRIP "${command_output}" python3_test_executable)
function(add_py_test name prog)
if(SVN_TEST_CONFIGURE_FOR_PARALLEL)
- set(test_root "${CMAKE_CURRENT_BINARY_DIR}/Testing/${name}")
+ set(test_root "${test_base_dir}/${name}")
else()
- set(test_root "${CMAKE_CURRENT_BINARY_DIR}/Testing")
+ set(test_root "${test_base_dir}")
endif()
file(MAKE_DIRECTORY "${test_root}/subversion/tests/cmdline")
@@ -793,7 +809,7 @@ if(SVN_ENABLE_TESTS)
NAME
"${name}"
COMMAND
- "${Python3_EXECUTABLE}" "${run_tests_script}"
+ "${python3_test_executable}" "${run_tests_script}"
--bin ${binary_dir}
--tools-bin ${binary_dir}
--verbose
Modified: subversion/trunk/build/run_tests.py
URL:
http://svn.apache.org/viewvc/subversion/trunk/build/run_tests.py?rev=1925899&r1=1925898&r2=1925899&view=diff
==============================================================================
--- subversion/trunk/build/run_tests.py (original)
+++ subversion/trunk/build/run_tests.py Wed May 28 09:36:46 2025
@@ -259,6 +259,8 @@ class TestHarness:
cmdline.append('--tools-bin=%s' % self.opts.tools_bin)
if self.opts.svn_bin is not None:
cmdline.append('--bin=%s' % self.opts.svn_bin)
+ if self.opts.venv_base is not None:
+ cmdline.append('--python-venv=%s' % self.opts.venv_base)
if self.opts.url is not None:
cmdline.append('--url=%s' % self.opts.url)
if self.opts.fs_type is not None:
@@ -326,16 +328,17 @@ class TestHarness:
global svntest
svntest = importlib.import_module('svntest')
- extra_packages = svntest.main.ensure_dependencies()
svntest.main.parse_options(cmdline, optparse.SUPPRESS_USAGE)
svntest.testcase.TextColors.disable()
+ dependency_path = svntest.main.ensure_dependencies()
# We have to update PYTHONPATH, otherwise the whole setting up of a
# virtualenv and installing dependencies will happen for every test case.
- python_path = os.environ.get("PYTHONPATH")
- python_path = (extra_packages if not python_path
- else "%s:%s" % (extra_packages, python_path))
- os.environ["PYTHONPATH"] = python_path
+ if dependency_path:
+ python_path = os.environ.get("PYTHONPATH")
+ python_path = (dependency_path if not python_path
+ else "%s:%s" % (dependency_path, python_path))
+ os.environ["PYTHONPATH"] = python_path
finally:
os.chdir(old_cwd)
@@ -674,6 +677,12 @@ class TestHarness:
for x in failed_list:
sys.stdout.write(x)
+ xml_error_list = [x for x in log_lines if x[:8] == 'E: XML: ']
+ if xml_error_list:
+ print('There were some XML validation errors, checking' + self.logfile)
+ for x in sorted(set(xml_error_list)):
+ sys.stdout.write(x[3:])
+
# Print summaries, from least interesting to most interesting.
if self.opts.list_tests:
print('Summary of test listing:')
@@ -1032,6 +1041,13 @@ def create_parser():
help='Use the svn binaries installed in this path')
parser.add_option('--tools-bin', action='store', dest='tools_bin',
help='Use the svn tools installed in this path')
+ parser.add_option('--create-python-venv', action='store', dest='create_venv',
+ help=('Create the Python virtual environment inside this'
+ ' path and install the dependencies used by the'
+ ' test suite, then exit. Do not run any tests.'))
+ parser.add_option('--python-venv', action='store', dest='venv_base',
+ help=('Use the virtual environment inside this path to'
+ ' find the dependencies used by the test suite.'))
parser.add_option('--fsfs-sharding', action='store', type='int',
help='Default shard size (for fsfs)')
parser.add_option('--fsfs-packing', action='store_true',
@@ -1097,12 +1113,22 @@ def create_parser():
def main():
(opts, args) = create_parser().parse_args(sys.argv[1:])
+ if opts.create_venv:
+ main_create_venv(opts, args)
+ sys.exit(0)
+
+ # Normal mode: don't create a virtual environment, run tests or whatever
+ # else was requested instead. Create the virtual environment on demand.
+ assert not opts.create_venv
if len(args) < 3:
print("{}: at least three positional arguments required; got {!r}".format(
os.path.basename(sys.argv[0]), args
))
sys.exit(2)
+ abs_srcdir = args[0]
+ abs_builddir = args[1]
+ programs = args[2:]
if opts.log_to_stdout:
logfile = None
@@ -1111,11 +1137,30 @@ def main():
logfile = os.path.abspath('tests.log')
faillogfile = os.path.abspath('fails.log')
- th = TestHarness(args[0], args[1], logfile, faillogfile, opts)
- failed = th.run(args[2:])
+ th = TestHarness(abs_srcdir, abs_builddir, logfile, faillogfile, opts)
+ failed = th.run(programs)
if failed:
sys.exit(1)
+def main_create_venv(opts, args):
+ # Environment creation mode: create the requested virtual environment,
+ # install required dependencies and exit.
+ assert opts.create_venv
+
+ if len(args) < 1:
+ print("{}: at least one positional argument required; got {!r}".format(
+ os.path.basename(sys.argv[0]), args
+ ))
+ sys.exit(2)
+ abs_srcdir = args[0]
+
+ sys.path.insert(0, os.path.join(abs_srcdir, "subversion", "tests",
"cmdline"))
+ svntest = importlib.import_module("svntest")
+ svntest.main.venv_base = opts.create_venv
+ venv_dir = svntest.main.venv_path()
+ python_prog, _ = svntest.main.create_python_venv(venv_dir, quiet=True)
+ print(python_prog)
+
# Run main if not imported as a module
if __name__ == '__main__':
Modified: subversion/trunk/subversion/tests/cmdline/prop_tests.py
URL:
http://svn.apache.org/viewvc/subversion/trunk/subversion/tests/cmdline/prop_tests.py?rev=1925899&r1=1925898&r2=1925899&view=diff
==============================================================================
--- subversion/trunk/subversion/tests/cmdline/prop_tests.py (original)
+++ subversion/trunk/subversion/tests/cmdline/prop_tests.py Wed May 28 09:36:46
2025
@@ -2641,7 +2641,8 @@ def xml_unsafe_author(sbox):
@Issue(4415)
@Issue(4919)
-@XFail(svntest.main.unless_ra_type_dav)
+@XFail(lambda: (svntest.main.is_bad_xml_fatal()
+ and not svntest.main.is_ra_type_dav()))
def xml_unsafe_author2(sbox):
"svn:author with XML unsafe chars 2"
@@ -2668,6 +2669,13 @@ def xml_unsafe_author2(sbox):
expected_author = 'foo\bbar'
# Use svn ls in --xml mode to test locale independent output.
+ # FIXME: Theat literal \b in the author field is invalid XML.
+ # Should be encoded as a character entity and enclosed
+ # in a CDATA section, like this:
+ #
+ # <[CDATA[foo@#08;bar]]>
+ # or
+ # foo<[CDATA[@#08;]]>bar
expected_output = [
'<?xml version="1.0" encoding="UTF-8"?>\n',
'<lists>\n',
Modified: subversion/trunk/subversion/tests/cmdline/svntest/main.py
URL:
http://svn.apache.org/viewvc/subversion/trunk/subversion/tests/cmdline/svntest/main.py?rev=1925899&r1=1925898&r2=1925899&view=diff
==============================================================================
--- subversion/trunk/subversion/tests/cmdline/svntest/main.py (original)
+++ subversion/trunk/subversion/tests/cmdline/svntest/main.py Wed May 28
09:36:46 2025
@@ -36,6 +36,7 @@ import xml
import urllib
import logging
import hashlib
+import importlib
import zipfile
import codecs
import queue
@@ -222,11 +223,13 @@ work_dir = "svn-test-work"
# Directory for the Python virtual environment where we install
# external dependencies of the test environment
-venv_dir = os.path.join(work_dir, "__venv__")
+venv_base = work_dir
+venv_path = lambda: os.path.join(venv_base, "__venv__")
+venv_create = True
# List of dependencies
+found_dependencies = set()
SVN_TESTS_REQUIRE = ["lxml", "rnc2rng"]
-dependencies_ensured = False
# Constant for the merge info property.
SVN_PROP_MERGEINFO = "svn:mergeinfo"
@@ -1614,9 +1617,6 @@ def tests_verify_dump_load_cross_check()
def is_ra_type_dav():
return options.test_area_url.startswith('http')
-def unless_ra_type_dav():
- return not is_ra_type_dav()
-
def is_ra_type_dav_neon():
"""Return True iff running tests over RA-Neon.
CAUTION: Result is only valid if svn was built to support both."""
@@ -1756,6 +1756,13 @@ def is_httpd_authz_provider_enabled():
def is_remote_http_connection_allowed():
return options.allow_remote_http_connection
+# XML schema validation
+def is_bad_xml_fatal():
+ """Are we treating invalid XML output as a fatal error?"""
+ # Only if we have all the necessary dependencies.
+ return {'lxml', 'rnc2rnd'} & found_dependencies
+
+
def wc_format(ver=None):
"""Return the WC format number used by Subversion version VER.
@@ -1859,6 +1866,8 @@ class TestSpawningThread(threading.Threa
args.append('--allow-remote-http-connection')
if options.svn_bin:
args.append('--bin=' + options.svn_bin)
+ if options.venv_base:
+ args.append('--python-venv=' + options.venv_base)
if options.store_pristine:
args.append('--store-pristine=' + options.store_pristine)
if options.valgrind:
@@ -2233,6 +2242,9 @@ def _create_parser(usage=None):
help='Whether to clean up')
parser.add_option('--enable-sasl', action='store_true',
help='Whether to enable SASL authentication')
+ parser.add_option('--python-venv', action='store', dest='venv_base',
+ help=('Use the virtual environment inside this path to'
+ ' find the dependencies used by the test suite.'))
parser.add_option('--bin', action='store', dest='svn_bin',
help='Use the svn binaries installed in this path')
parser.add_option('--use-jsvn', action='store_true',
@@ -2339,6 +2351,8 @@ def parse_options(arglist=sys.argv[1:],
"""Parse the arguments in arg_list, and set the global options object with
the results"""
+ global venv_base
+ global venv_create
global options
parser = _create_parser(usage)
@@ -2388,7 +2402,9 @@ def parse_options(arglist=sys.argv[1:],
svn_wc__max_supported_format_version(),
options.wc_format_version))
- pass
+ if options.venv_base:
+ venv_base = options.venv_base
+ venv_create = False
return (parser, args)
@@ -2420,7 +2436,6 @@ def run_tests(test_list, serial_only = F
appropriate exit code.
"""
- ensure_dependencies()
sys.exit(execute_tests(test_list, serial_only))
def ensure_dependencies():
@@ -2431,18 +2446,42 @@ def ensure_dependencies():
upgrade the venv in that case. In practice, we won't.
"""
- global dependencies_ensured
- if dependencies_ensured:
- return
-
+ venv_dir = os.path.abspath(venv_path())
package_path = os.path.join(venv_dir, "lib",
"python%d.%d" % sys.version_info[:2],
"site-packages")
- package_path = os.path.abspath(package_path)
- if package_path in sys.path:
- dependencies_ensured = True
- return
+ # Check if all our dependencies are installed. It doesn't matter if
+ # they're installed in our venv, as long as they're available.
+ found_dependencies.clear()
+ saved_sys_path = sys.path[:]
+ try:
+ sys.path.insert(0, package_path)
+ for package in SVN_TESTS_REQUIRE:
+ importlib.import_module(package)
+ found_dependencies.add(package)
+ have_required = True
+ except ImportError:
+ have_required = False
+ finally:
+ sys.path[:] = saved_sys_path
+
+ if have_required:
+ if package_path not in sys.path:
+ sys.path.append(package_path)
+ return package_path
+
+ if venv_create:
+ python_prog, python_path = create_python_venv(venv_dir)
+ if python_prog is not None:
+ assert python_path == package_path
+ if package_path not in sys.path:
+ sys.path.append(package_path)
+ found_dependencies.update(set(SVN_TESTS_REQUIRE))
+ return package_path
+ return None
+
+def create_python_venv(venv_dir, quiet=False):
try:
# Create the virtual environment
if not os.path.isdir(venv_dir):
@@ -2450,19 +2489,22 @@ def ensure_dependencies():
safe_rmtree(venv_dir)
venv.create(venv_dir, with_pip=True)
- # Install any (new) dependencies
- pip = os.path.join(venv_dir, venv_bin, "pip"+_exe)
+ # Install the dependencies
+ pip = os.path.join(venv_dir, venv_bin, "pip" + _exe)
pip_options = ("--disable-pip-version-check", "--require-virtualenv")
subprocess.run([pip, *pip_options, "install", *SVN_TESTS_REQUIRE],
- check=True)
+ check=True, stdout=subprocess.PIPE if quiet else None)
+ importlib.invalidate_caches()
- sys.path.append(package_path)
- dependencies_ensured = True
- return package_path
- except Exception as ex:
- print("WARNING: Could not install test dependencies,"
- " some tests will be skipped", file=sys.stderr)
- print(ex, file=sys.stderr)
+ python_prog = os.path.join(venv_dir, venv_bin, "python" + _exe)
+ python_path = os.path.join(venv_dir, "lib",
+ "python%d.%d" % sys.version_info[:2],
+ "site-packages")
+ return python_prog, python_path
+ except Exception:
+ if logger:
+ logger.warning('Could not install test dependencies', exc_info=True)
+ return None, None
def get_issue_details(issue_numbers):
"""For each issue number in ISSUE_NUMBERS query the issue
@@ -2662,6 +2704,10 @@ def execute_tests(test_list, serial_only
wc_incomplete_tester_binary = os.path.join(options.tools_bin,
'wc-incomplete-tester' + _exe)
+ assert options.venv_base is None or venv_base == options.venv_base, \
+ 'venv_base=%s options.venv_base=%s' % (venv_base, options.venv_base)
+ ensure_dependencies()
+
######################################################################
# Cleanup: if a previous run crashed or interrupted the python
Modified: subversion/trunk/subversion/tests/cmdline/svntest/verify.py
URL:
http://svn.apache.org/viewvc/subversion/trunk/subversion/tests/cmdline/svntest/verify.py?rev=1925899&r1=1925898&r2=1925899&view=diff
==============================================================================
--- subversion/trunk/subversion/tests/cmdline/svntest/verify.py (original)
+++ subversion/trunk/subversion/tests/cmdline/svntest/verify.py Wed May 28
09:36:46 2025
@@ -1062,12 +1062,14 @@ def validate_xml_schema(name: str, lines
source = ''.join(lines)
document = etree.parse(BytesIO(source.encode("utf-8")))
if not schema.validate(document):
- print(schema.error_log)
- raise SVNXMLSchemaValidationError("Schema: %s" % schema_name)
+ raise SVNXMLSchemaValidationError(schema.error_log)
except ImportError:
- print("ERROR: Pyhton module lxml.etree is required for XML validation")
- raise svntest.Failure()
- except Exception:
- print("ERROR: invalid XML")
- print("\n".join(repr(line) for line in lines))
- raise
+ logger.error("XML: Module lxml.etree not found")
+ return
+ except Exception as ex:
+ logger.error("XML: " + str(ex))
+ logger.warning("XML:\n" + "\n".join(repr(line) for line in lines))
+ if svntest.main.is_bad_xml_fatal():
+ raise
+ else:
+ logger.warning("XML:", exc_info=True)