svn commit: r1925899 - in /subversion/trunk: CMakeLists.txt build/run_tests.py subversion/tests/cmdline/prop_tests.py subversion/tests/cmdline/svntest/main.py subversion/tests/cmdline/svntest/verify.py

[email protected]
Newsgroups gmane.comp.version-control.subversion.svn
Message-ID <[email protected]>
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)
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.