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)


Reply via email to