Author: brane
Date: Tue May 20 12:32:49 2025
New Revision: 1925717

URL: http://svn.apache.org/viewvc?rev=1925717&view=rev
Log:
Add support for verifying the svn command's XML output against the Relax NG
schemas that are defined in ./subversion/svn/schema. We use lxml.etree.

* subversion/tests/cmdline/svntest/main.py: Import venv.
  (venv_bin, venv_dir, SVN_TESTS_REQUIRE, dependencies_ensured: New globals.
  (ensure_dependencies): Create a Python virtual environment in the test
   working directory, install the dependencies and update sys.path.
  (run_tests): Call ensure_dependencies().
* subversion/tests/cmdline/svntest/verify.py:
   Import os but not sys, io.BytesIO and typing.Iterable.
  (SVNXMLSchemaValidationError): New exception type.
  (validate_xml_schema): Validate the XML output of 'svn' against a schema.
* subversion/tests/cmdline/svntest/actions.py
  (run_and_verify_svn_xml, run_and_verify_svn_xml2): New functions that
   run XML validation.
* subversion/tests/cmdline/blame_tests.py,
  subversion/tests/cmdline/log_tests.py: Add XML validation
* build/run_tests.py
  (TestHarness._init_py_tests): Update PYTHONPATH to point at the virtualenv.
* Makefile.in (check): No, Python 2.7 is no longer required.

Modified:
    subversion/trunk/Makefile.in
    subversion/trunk/build/run_tests.py
    subversion/trunk/subversion/tests/cmdline/blame_tests.py
    subversion/trunk/subversion/tests/cmdline/log_tests.py
    subversion/trunk/subversion/tests/cmdline/svntest/actions.py
    subversion/trunk/subversion/tests/cmdline/svntest/main.py
    subversion/trunk/subversion/tests/cmdline/svntest/verify.py

Modified: subversion/trunk/Makefile.in
URL: 
http://svn.apache.org/viewvc/subversion/trunk/Makefile.in?rev=1925717&r1=1925716&r2=1925717&view=diff
==============================================================================
--- subversion/trunk/Makefile.in (original)
+++ subversion/trunk/Makefile.in Tue May 20 12:32:49 2025
@@ -645,7 +645,7 @@ check: bin @TRANSFORM_LIBTOOL_SCRIPTS@ $
                    $$flags                                                  \
                    '$(abs_srcdir)' '$(abs_builddir)' $(TESTS);              \
        else                                                                 \
-         echo "make check: Python 2.7 or greater is required,";             \
+         echo "make check: Python 3.6 or greater is required,";             \
          echo "            but was not detected during configure";          \
          exit 1;                                                            \
        fi;

Modified: subversion/trunk/build/run_tests.py
URL: 
http://svn.apache.org/viewvc/subversion/trunk/build/run_tests.py?rev=1925717&r1=1925716&r2=1925717&view=diff
==============================================================================
--- subversion/trunk/build/run_tests.py (original)
+++ subversion/trunk/build/run_tests.py Tue May 20 12:32:49 2025
@@ -326,8 +326,16 @@ 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()
+
+      # 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
     finally:
       os.chdir(old_cwd)
 

Modified: subversion/trunk/subversion/tests/cmdline/blame_tests.py
URL: 
http://svn.apache.org/viewvc/subversion/trunk/subversion/tests/cmdline/blame_tests.py?rev=1925717&r1=1925716&r2=1925717&view=diff
==============================================================================
--- subversion/trunk/subversion/tests/cmdline/blame_tests.py (original)
+++ subversion/trunk/subversion/tests/cmdline/blame_tests.py Tue May 20 
12:32:49 2025
@@ -216,7 +216,7 @@ def blame_in_xml(sbox):
                                         None)
 
   # Retrieve last changed date from svn info
-  exit_code, output, error = svntest.actions.run_and_verify_svn(
+  exit_code, output, error = svntest.actions.run_and_verify_svn_xml(
     None, [],
     'log', file_path, '--xml', '-r1:2')
 
@@ -256,7 +256,7 @@ def blame_in_xml(sbox):
               '</target>\n',
               '</blame>\n']
 
-  exit_code, output, error = svntest.actions.run_and_verify_svn(
+  exit_code, output, error = svntest.actions.run_and_verify_svn_xml(
     None, [],
     'blame', file_path, '--xml')
 

Modified: subversion/trunk/subversion/tests/cmdline/log_tests.py
URL: 
http://svn.apache.org/viewvc/subversion/trunk/subversion/tests/cmdline/log_tests.py?rev=1925717&r1=1925716&r2=1925717&view=diff
==============================================================================
--- subversion/trunk/subversion/tests/cmdline/log_tests.py (original)
+++ subversion/trunk/subversion/tests/cmdline/log_tests.py Tue May 20 12:32:49 
2025
@@ -1008,7 +1008,7 @@ def log_xml_empty_date(sbox):
   date_re = re.compile('<date')
 
   # Ensure that we get a date before we delete the property.
-  exit_code, output, errput = svntest.actions.run_and_verify_svn(
+  exit_code, output, errput = svntest.actions.run_and_verify_svn_xml(
     None, [], 'log', '--xml', '-r1', sbox.wc_dir)
 
   matched = 0
@@ -1023,7 +1023,7 @@ def log_xml_empty_date(sbox):
                                      'pdel', '--revprop', '-r1', 'svn:date',
                                      sbox.wc_dir)
 
-  exit_code, output, errput = svntest.actions.run_and_verify_svn(
+  exit_code, output, errput = svntest.actions.run_and_verify_svn_xml(
     None, [], 'log', '--xml', '-r1', sbox.wc_dir)
 
   for line in output:
@@ -2798,22 +2798,11 @@ def log_with_merge_history_and_search(sb
   sbox.simple_commit(message='r4: merge')
   sbox.simple_update()
 
-  # Helper function
-  def count(haystack, needle):
-    """Return the number of times the string NEEDLE occurs in the string
-    HAYSTACK."""
-    return len(haystack.split(needle)) - 1
-
   # Check the output is valid
-  # ### Since the test is currently XFail, we only smoke test the output.
-  # ### When fixing this test to PASS, extend this validation.
   _, output, _ = svntest.main.run_svn(None, 'log', '--xml', '-g',
                                       '--search', "this will have no matches",
                                       sbox.ospath('A2'))
-
-  output = '\n'.join(output)
-  if count(output, "<logentry") != count(output, "</logentry"):
-    raise svntest.Failure("Apparently invalid XML in " + repr(output))
+  svntest.verify.validate_xml_schema('log', output)
 
 @XFail(svntest.main.is_ra_type_file)
 @Issue(4856)
@@ -2852,21 +2841,10 @@ def log_xml_with_merge_history(sbox):
   sbox.simple_commit(message='r9: merge A3=>A4')
   sbox.simple_update()
 
-  # Helper function
-  def count(haystack, needle):
-    """Return the number of times the string NEEDLE occurs in the string
-    HAYSTACK."""
-    return len(haystack.split(needle)) - 1
-
   # Check the output is valid
-  # ### Since the test is currently XFail, we only smoke test the output.
-  # ### When fixing this test to PASS, extend this validation.
   _, output, _ = svntest.main.run_svn(None, 'log', '--xml', '-g', '-r', '8:9',
                                       sbox.ospath('A4'))
-
-  output = '\n'.join(output)
-  if count(output, "<logentry") != count(output, "</logentry"):
-    raise svntest.Failure("Apparently invalid XML in " + repr(output))
+  svntest.verify.validate_xml_schema('log', output)
 
 ########################################################################
 # Run the tests

Modified: subversion/trunk/subversion/tests/cmdline/svntest/actions.py
URL: 
http://svn.apache.org/viewvc/subversion/trunk/subversion/tests/cmdline/svntest/actions.py?rev=1925717&r1=1925716&r2=1925717&view=diff
==============================================================================
--- subversion/trunk/subversion/tests/cmdline/svntest/actions.py (original)
+++ subversion/trunk/subversion/tests/cmdline/svntest/actions.py Tue May 20 
12:32:49 2025
@@ -325,6 +325,16 @@ def run_and_verify_svnversion2(wc_dir, t
   verify.verify_exit_code("Unexpected return code", exit_code, expected_exit)
   return exit_code, out, err
 
+def run_and_verify_svn_xml(expected_stdout, expected_stderr,
+                            command, *varargs):
+  """Like run_and_verify_svn but expects the output to be XML
+  and validates it against the schema for the given command"""
+  exit_code, out, err = run_and_verify_svn(expected_stdout, expected_stderr,
+                                           command, *varargs)
+  if exit_code == 0:
+    verify.validate_xml_schema(command, out)
+  return exit_code, out, err
+
 def run_and_verify_svn(expected_stdout, expected_stderr, *varargs):
   """like run_and_verify_svn2, but the expected exit code is assumed to
   be 0 if no output is expected on stderr, and 1 otherwise."""
@@ -339,6 +349,16 @@ def run_and_verify_svn(expected_stdout,
   return run_and_verify_svn2(expected_stdout, expected_stderr,
                              expected_exit, *varargs)
 
+def run_and_verify_svn_xml2(expected_stdout, expected_stderr,
+                            expected_exit, command, *varargs):
+  """Like run_and_verify_svn2 but expects the output to be XML
+  and validates it against the schema for the given command"""
+  exit_code, out, err = run_and_verify_svn2(expected_stdout, expected_stderr,
+                                            expected_exit, command, *varargs)
+  if exit_code == 0:
+    verify.validate_xml_schema(command, out)
+  return exit_code, out, err
+
 def run_and_verify_svn2(expected_stdout, expected_stderr,
                         expected_exit, *varargs):
   """Invoke main.run_svn() with *VARARGS. Return exit code as int; stdout,

Modified: subversion/trunk/subversion/tests/cmdline/svntest/main.py
URL: 
http://svn.apache.org/viewvc/subversion/trunk/subversion/tests/cmdline/svntest/main.py?rev=1925717&r1=1925716&r2=1925717&view=diff
==============================================================================
--- subversion/trunk/subversion/tests/cmdline/svntest/main.py (original)
+++ subversion/trunk/subversion/tests/cmdline/svntest/main.py Tue May 20 
12:32:49 2025
@@ -39,6 +39,7 @@ import hashlib
 import zipfile
 import codecs
 import queue
+import venv
 
 from urllib.parse import quote as urllib_parse_quote
 from urllib.parse import unquote as urllib_parse_unquote
@@ -119,11 +120,13 @@ class SVNRepositoryCreateFailure(Failure
 if sys.platform == 'win32':
   windows = True
   file_scheme_prefix = 'file:///'
+  venv_bin = 'Scripts'
   _exe = '.exe'
   _bat = '.bat'
   os.environ['SVN_DBG_STACKTRACES_TO_STDERR'] = 'y'
 else:
   windows = False
+  venv_bin = 'bin'
   file_scheme_prefix = 'file://'
   _exe = ''
   _bat = ''
@@ -170,7 +173,7 @@ S_ALL_RWX = S_ALL_READ | S_ALL_WRITE | S
 def P(relpath,
       head=os.path.dirname(os.path.dirname(os.path.abspath('.')))
       ):
-  if sys.platform=='win32':
+  if windows:
     return os.path.join(head, relpath + '.exe')
   else:
     return os.path.join(head, relpath)
@@ -217,6 +220,14 @@ options = None
 # this dir, so there's one point at which to mount, e.g., a ramdisk.
 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__")
+
+# List of dependencies
+SVN_TESTS_REQUIRE = ["lxml", "rnc2rng"]
+dependencies_ensured = False
+
 # Constant for the merge info property.
 SVN_PROP_MERGEINFO = "svn:mergeinfo"
 
@@ -859,7 +870,7 @@ def run_svnadmin(*varargs):
   exit_code, stdout_lines, stderr_lines = \
                        run_command(svnadmin_binary, 1, use_binary, *varargs)
 
-  if use_binary and sys.platform == 'win32':
+  if use_binary and windows:
     # Callers don't expect binary output on stderr
     stderr_lines = [x.replace('\r', '') for x in stderr_lines]
 
@@ -2406,8 +2417,50 @@ def run_tests(test_list, serial_only = F
         appropriate exit code.
   """
 
+  ensure_dependencies()
   sys.exit(execute_tests(test_list, serial_only))
 
+def ensure_dependencies():
+  """Install the dependencies we need for running the tests.
+
+  NOTE: this function des not handle the case where the Python
+        version has changed. In theory, we could automagically
+        upgrade the venv in that case. In practice, we won't.
+  """
+
+  global dependencies_ensured
+  if dependencies_ensured:
+    return
+
+  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
+
+  try:
+    # Create the virtual environment
+    if not os.path.isdir(venv_dir):
+      if os.path.exists(venv_dir):
+        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)
+    pip_options = ("--disable-pip-version-check", "--require-virtualenv")
+    subprocess.run([pip, *pip_options, "install", *SVN_TESTS_REQUIRE],
+                   check=True)
+
+    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)
+
 def get_issue_details(issue_numbers):
   """For each issue number in ISSUE_NUMBERS query the issue
      tracker and determine what the target milestone is and

Modified: subversion/trunk/subversion/tests/cmdline/svntest/verify.py
URL: 
http://svn.apache.org/viewvc/subversion/trunk/subversion/tests/cmdline/svntest/verify.py?rev=1925717&r1=1925716&r2=1925717&view=diff
==============================================================================
--- subversion/trunk/subversion/tests/cmdline/svntest/verify.py (original)
+++ subversion/trunk/subversion/tests/cmdline/svntest/verify.py Tue May 20 
12:32:49 2025
@@ -24,11 +24,13 @@
 #    under the License.
 ######################################################################
 
-import re, sys
+import os, re
 from difflib import unified_diff, ndiff
 import pprint
 import logging
 import itertools
+from io import BytesIO
+from typing import Iterable
 
 import svntest
 
@@ -77,6 +79,9 @@ class SVNDumpParseError(svntest.Failure)
   """Exception raised if parsing a dump file fails"""
   pass
 
+class SVNXMLSchemaValidationError(SVNUnexpectedOutput):
+  """Exception raised if XML output failed validation against its schema"""
+  pass
 
 ######################################################################
 # Comparison of expected vs. actual output
@@ -1039,3 +1044,26 @@ def make_diff_prop_modified(pname, pval1
     "## -1 +1 ##\n",
   ] + make_diff_prop_val("-", pval1) + make_diff_prop_val("+", pval2)
 
+
+__schema_dir = os.path.join(
+  os.path.dirname(
+    os.path.dirname(
+      os.path.dirname(
+        os.path.dirname(
+          os.path.abspath(__file__))))),
+  "svn", "schema")
+def validate_xml_schema(name: str, lines: Iterable[str]) -> None:
+  schema_name = name + ".rnc"
+  try:
+    # Imported in this scope because sys.path may not have been updated yet.
+    from lxml import etree #type:ignore
+    schema_file = os.path.join(__schema_dir, schema_name)
+    schema = etree.RelaxNG(file=schema_file)
+    source = ''.join(lines)
+    document = etree.parse(BytesIO(source.encode("utf-8")))
+    if not schema.validate(document):
+      raise SVNXMLSchemaValidationError("schema %s" % schema_name)
+  except Exception:
+    print("ERROR: XML output does not conform to schema", schema_name)
+    print(source)
+    raise


Reply via email to