You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@subversion.apache.org by gb...@apache.org on 2013/08/21 18:09:52 UTC

svn commit: r1516196 [26/26] - in /subversion/branches/invoke-diff-cmd-feature: ./ build/ build/ac-macros/ build/generator/ build/generator/swig/ build/generator/templates/ contrib/client-side/emacs/ contrib/hook-scripts/ doc/programmer/ notes/ notes/h...

Modified: subversion/branches/invoke-diff-cmd-feature/tools/server-side/svnauthz.c
URL: http://svn.apache.org/viewvc/subversion/branches/invoke-diff-cmd-feature/tools/server-side/svnauthz.c?rev=1516196&r1=1516195&r2=1516196&view=diff
==============================================================================
--- subversion/branches/invoke-diff-cmd-feature/tools/server-side/svnauthz.c (original)
+++ subversion/branches/invoke-diff-cmd-feature/tools/server-side/svnauthz.c Wed Aug 21 16:09:43 2013
@@ -59,7 +59,9 @@ static const apr_getopt_option_t options
   {"repository", svnauthz__repos, 1, ("repository authz name")},
   {"transaction", 't', 1, ("transaction id")},
   {"is", svnauthz__is, 1,
-    ("instead of outputing, tests if the access is ARG\n"
+    ("instead of outputting, test if the access is\n"
+     "                             "
+     "exactly ARG\n"
      "                             "
      "ARG can be one of the following values:\n"
      "                             "
@@ -67,10 +69,12 @@ static const apr_getopt_option_t options
      "                             "
      "    r    read-only access\n"
      "                             "
-     "   no    no access\n")
+     "   no    no access")
   },
-  {"groups-file", svnauthz__groups_file, 1, ("path to the global groups file")},
-  {"recursive", 'R', 0, ("recursive access to path")},
+  {"groups-file", svnauthz__groups_file, 1,
+   ("use the groups from file ARG")},
+  {"recursive", 'R', 0,
+   ("determine recursive access to PATH")},
   {0, 0, 0, 0}
 };
 
@@ -129,27 +133,32 @@ static const svn_opt_subcommand_desc2_t 
    {'t'} },
   {"accessof", subcommand_accessof, {0} /* no aliases */,
    ("Print or test the permissions set by an authz file.\n"
-    "usage: 1. svnauthz accessof [--username USER] [--groups-file GROUPS_FILE] TARGET\n"
-    "       2. svnauthz accessof [--username USER] [--groups-file GROUPS_FILE] \\\n"
-    "                            -t TXN REPOS_PATH FILE_PATH\n\n"
-    "  1. Prints the access of USER based on TARGET.\n"
+    "usage: 1. svnauthz accessof TARGET\n"
+    "       2. svnauthz accessof -t TXN REPOS_PATH FILE_PATH\n"
+    "\n"
+    "  1. Prints the access of USER to PATH based on authorization file at TARGET.\n"
     "     TARGET can be a path to a file or an absolute file:// URL to an authz\n"
-    "     file in a repository, but cannot be a repository relative URL (^/).\n\n"
-    "  2. Prints the access of USER based on authz file at FILE_PATH in the\n"
-    "     transaction TXN in the repository at REPOS_PATH.\n\n"
-    "  If the --username argument is omitted then access of an anonymous user\n"
-    "  will be printed.  If --path argument is omitted prints if any access\n"
-    "  to the repo is allowed.  If --groups-file is specified, the groups from\n"
-    "  GROUPS_FILE will be used.\n\n"
+    "     file in a repository, but cannot be a repository relative URL (^/).\n"
+    "\n"
+    "  2. Prints the access of USER to PATH based on authz file at FILE_PATH in the\n"
+    "     transaction TXN in the repository at REPOS_PATH.\n"
+    "\n"
+    "  USER is the argument to the --username option; if that option is not\n"
+    "  provided, then access of an anonymous user will be printed or tested.\n"
+    "\n"
+    "  PATH is the argument to the --path option; if that option is not provided,\n"
+    "  the maximal access to any path in the repository will be considered.\n"
+    "\n"
     "Outputs one of the following:\n"
     "     rw    write access (which also implies read)\n"
     "      r    read access\n"
-    "     no    no access\n\n"
+    "     no    no access\n"
+    "\n"
     "Returns:\n"
-    "    0   when syntax is OK and --is argument (if any) matches.\n"
+    "    0   when syntax is OK and '--is' argument (if any) matches.\n"
     "    1   when syntax is invalid.\n"
     "    2   operational error\n"
-    "    3   when --is argument doesn't match\n"
+    "    3   when '--is' argument doesn't match\n"
     ),
    {'t', svnauthz__username, svnauthz__path, svnauthz__repos, svnauthz__is,
     svnauthz__groups_file, 'R'} },

Modified: subversion/branches/invoke-diff-cmd-feature/tools/server-side/svnpubsub/commit-hook.py
URL: http://svn.apache.org/viewvc/subversion/branches/invoke-diff-cmd-feature/tools/server-side/svnpubsub/commit-hook.py?rev=1516196&r1=1516195&r2=1516196&view=diff
==============================================================================
--- subversion/branches/invoke-diff-cmd-feature/tools/server-side/svnpubsub/commit-hook.py (original)
+++ subversion/branches/invoke-diff-cmd-feature/tools/server-side/svnpubsub/commit-hook.py Wed Aug 21 16:09:43 2013
@@ -19,8 +19,10 @@
 SVNLOOK="/usr/local/svn-install/current/bin/svnlook"
 #SVNLOOK="/usr/local/bin/svnlook"
 
+HOST="127.0.0.1"
+PORT=2069
+
 import sys
-import subprocess
 try:
     import simplejson as json
 except ImportError:
@@ -28,35 +30,32 @@ except ImportError:
 
 import urllib2
 
-HOST="127.0.0.1"
-PORT=2069
-
-def svncmd(cmd):
-    return subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE)
+import svnpubsub.util
 
-def svncmd_uuid(repo):
-    cmd = "%s uuid %s" % (SVNLOOK, repo)
-    p = svncmd(cmd)
-    return p.stdout.read().strip()
-
-def svncmd_info(repo, revision):
-    cmd = "%s info -r %s %s" % (SVNLOOK, revision, repo)
-    p = svncmd(cmd)
-    data = p.stdout.read().split("\n")
+def svnlook(cmd, **kwargs):
+    args = [SVNLOOK] + cmd
+    return svnpubsub.util.check_output(args, **kwargs)
+
+def svnlook_uuid(repo):
+    cmd = ["uuid", "--", repo]
+    return svnlook(cmd).strip()
+
+def svnlook_info(repo, revision):
+    cmd = ["info", "-r", revision, "--", repo]
+    data = svnlook(cmd, universal_newlines=True).split("\n")
     #print data
     return {'author': data[0].strip(),
             'date': data[1].strip(),
             'log': "\n".join(data[3:]).strip()}
 
-def svncmd_changed(repo, revision):
-    cmd = "%s changed -r %s %s" % (SVNLOOK, revision, repo)
-    p = svncmd(cmd)
+def svnlook_changed(repo, revision):
+    cmd = ["changed", "-r", revision, "--", repo]
+    lines = svnlook(cmd, universal_newlines=True).split("\n")
     changed = {}
-    while True:
-        line = p.stdout.readline()
-        if not line:
-            break
+    for line in lines:
         line = line.strip()
+        if not line:
+            continue
         (flags, filename) = (line[0:3], line[4:])
         changed[filename] = {'flags': flags}
     return changed
@@ -71,23 +70,23 @@ def do_put(body):
 
 def main(repo, revision):
     revision = revision.lstrip('r')
-    i = svncmd_info(repo, revision)
+    i = svnlook_info(repo, revision)
     data = {'type': 'svn',
             'format': 1,
             'id': int(revision),
             'changed': {},
-            'repository': svncmd_uuid(repo),
+            'repository': svnlook_uuid(repo),
             'committer': i['author'],
             'log': i['log'],
             'date': i['date'],
             }
-    data['changed'].update(svncmd_changed(repo, revision))
+    data['changed'].update(svnlook_changed(repo, revision))
     body = json.dumps(data)
     do_put(body)
 
 if __name__ == "__main__":
-    if len(sys.argv) != 3:
-        print "invalid args"
-        sys.exit(0)
+    if len(sys.argv) not in (3, 4):
+        sys.stderr.write("invalid args\n")
+        sys.exit(1)
 
-    main(sys.argv[1], sys.argv[2])
+    main(*sys.argv[1:3])

Modified: subversion/branches/invoke-diff-cmd-feature/tools/server-side/svnpubsub/daemonize.py
URL: http://svn.apache.org/viewvc/subversion/branches/invoke-diff-cmd-feature/tools/server-side/svnpubsub/daemonize.py?rev=1516196&r1=1516195&r2=1516196&view=diff
==============================================================================
--- subversion/branches/invoke-diff-cmd-feature/tools/server-side/svnpubsub/daemonize.py (original)
+++ subversion/branches/invoke-diff-cmd-feature/tools/server-side/svnpubsub/daemonize.py Wed Aug 21 16:09:43 2013
@@ -24,6 +24,7 @@ import os
 import signal
 import sys
 import time
+import multiprocessing  # requires Python 2.6
 
 
 # possible return values from Daemon.daemonize()
@@ -50,11 +51,11 @@ class Daemon(object):
   def daemonize_exit(self):
     try:
       result = self.daemonize()
-    except (ChildFailed, DaemonFailed) as e:
+    except (ChildFailed, DaemonFailed), e:
       # duplicate the exit code
       sys.exit(e.code)
     except (ChildTerminatedAbnormally, ChildForkFailed,
-            DaemonTerminatedAbnormally, DaemonForkFailed) as e:
+            DaemonTerminatedAbnormally, DaemonForkFailed), e:
       sys.stderr.write('ERROR: %s\n' % e)
       sys.exit(1)
     except ChildResumedIncorrectly:
@@ -71,29 +72,41 @@ class Daemon(object):
     # in original process. daemon is up and running. we're done.
 
   def daemonize(self):
-    # fork off a child that can detach itself from this process.
-    try:
-      pid = os.fork()
-    except OSError as e:
-      raise ChildForkFailed(e.errno, e.strerror)
-
-    if pid > 0:
-      # we're in the parent. let's wait for the child to finish setting
-      # things up -- on our exit, we want to ensure the child is accepting
-      # connections.
-      cpid, status = os.waitpid(pid, 0)
-      assert pid == cpid
-      if os.WIFEXITED(status):
-        code = os.WEXITSTATUS(status)
-        if code:
-          raise ChildFailed(code)
-        return DAEMON_RUNNING
-
-      # the child did not exit cleanly.
-      raise ChildTerminatedAbnormally(status)
+    ### review error situations. map to backwards compat. ??
+    ### be mindful of daemonize_exit().
+    ### we should try and raise ChildFailed / ChildTerminatedAbnormally.
+    ### ref: older revisions. OR: remove exceptions.
+
+    child_is_ready = multiprocessing.Event()
+    child_completed = multiprocessing.Event()
+
+    p = multiprocessing.Process(target=self._first_child,
+                                args=(child_is_ready, child_completed))
+    p.start()
+    
+    # Wait for the child to finish setting things up (in case we need
+    # to communicate with it). It will only exit when ready.
+    ### use a timeout here! (parameterized, of course)
+    p.join()
+
+    ### need to propagate errors, to adjust the return codes
+    if child_completed.is_set():
+      ### what was the exit status?
+      return DAEMON_COMPLETE
+    if child_is_ready.is_set():
+      return DAEMON_RUNNING
+
+    ### how did we get here?! the immediate child should not exit without
+    ### signalling ready/complete. some kind of error.
+    return DAEMON_STARTED
 
+  def _first_child(self, child_is_ready, child_completed):
     # we're in the child.
 
+    ### NOTE: the original design was a bit bunk. Exceptions raised from
+    ### this point are within the child processes. We need to signal the
+    ### errors to the parent in other ways.
+
     # decouple from the parent process
     os.chdir('/')
     os.umask(0)
@@ -102,56 +115,86 @@ class Daemon(object):
     # remember this pid so the second child can signal it.
     thispid = os.getpid()
 
-    # register a signal handler so the SIGUSR1 doesn't stop the process.
-    # this object will also record whether if got signalled.
-    daemon_accepting = SignalCatcher(signal.SIGUSR1)
-
-    # if the daemon process exits before sending SIGUSR1, then we need to see
-    # the problem. trap SIGCHLD with a SignalCatcher.
+    # if the daemon process exits before signalling readiness, then we
+    # need to see the problem. trap SIGCHLD with a SignalCatcher.
     daemon_exit = SignalCatcher(signal.SIGCHLD)
 
     # perform the second fork
     try:
       pid = os.fork()
-    except OSError as e:
+    except OSError, e:
+      ### this won't make it to the parent process
       raise DaemonForkFailed(e.errno, e.strerror)
 
     if pid > 0:
       # in the parent.
 
-      # we want to wait for the daemon to signal that it has created and
-      # bound the socket, and is (thus) ready for connections. if the
-      # daemon improperly exits before serving, we'll see SIGCHLD and the
-      # .pause will return.
-      ### we should add a timeout to this. allow an optional parameter to
-      ### specify the timeout, in case it takes a long time to start up.
-      signal.pause()
+
+      # Wait for the child to be ready for operation.
+      while True:
+        # The readiness event will invariably be signalled early/first.
+        # If it *doesn't* get signalled because the child has prematurely
+        # exited, then we will pause 10ms before noticing the exit. The
+        # pause is acceptable since that is aberrant/unexpected behavior.
+        ### is there a way to break this wait() on a signal such as SIGCHLD?
+        ### parameterize this wait, in case the app knows children may
+        ### fail quickly?
+        if child_is_ready.wait(timeout=0.010):
+          # The child signalled readiness. Yay!
+          break
+        if daemon_exit.signalled:
+          # Whoops. The child exited without signalling :-(
+          break
+        # Python 2.6 compat: .wait() may exit when set, but return None
+        if child_is_ready.is_set():
+          break
+        # A simple timeout. The child is taking a while to prepare. Go
+        # back and wait for readiness.
 
       if daemon_exit.signalled:
+        # Tell the parent that the child has exited.
+        ### we need to communicate the exit status, if possible.
+        child_completed.set()
+
         # reap the daemon process, getting its exit code. bubble it up.
         cpid, status = os.waitpid(pid, 0)
         assert pid == cpid
         if os.WIFEXITED(status):
           code = os.WEXITSTATUS(status)
           if code:
+            ### this won't make it to the parent process
             raise DaemonFailed(code)
+          ### this return value is ignored
           return DAEMON_NOT_RUNNING
 
         # the daemon did not exit cleanly.
+        ### this won't make it to the parent process
         raise DaemonTerminatedAbnormally(status)
 
-      if daemon_accepting.signalled:
-        # the daemon is up and running, so save the pid and return success.
-        if self.pidfile:
-          open(self.pidfile, 'w').write('%d\n' % pid)
-        return DAEMON_STARTED
+      # child_is_ready got asserted. the daemon is up and running, so
+      # save the pid and return success.
+      if self.pidfile:
+        # Be wary of symlink attacks
+        try:
+          os.remove(self.pidfile)
+        except OSError:
+          pass
+        fd = os.open(self.pidfile, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0444)
+        os.write(fd, '%d\n' % pid)
+        os.close(fd)
 
+      ### this return value is ignored
+      return DAEMON_STARTED
+
+      ### old code. what to do with this? throw ChildResumedIncorrectly
+      ### or just toss this and the exception.
       # some other signal popped us out of the pause. the daemon might not
       # be running.
+      ### this won't make it to the parent process
       raise ChildResumedIncorrectly()
 
-    # we're a deamon now. get rid of the final remnants of the parent.
-    # start by restoring default signal handlers
+    # we're a daemon now. get rid of the final remnants of the parent:
+    # restore the signal handlers and switch std* to the proper files.
     signal.signal(signal.SIGUSR1, signal.SIG_DFL)
     signal.signal(signal.SIGCHLD, signal.SIG_DFL)
     sys.stdout.flush()
@@ -169,30 +212,31 @@ class Daemon(object):
     so.close()
     se.close()
 
-    # TEST: don't release the parent immediately. the whole parent stack
-    #       should pause along with this sleep.
+    ### TEST: don't release the parent immediately. the whole parent stack
+    ###       should pause along with this sleep.
     #time.sleep(10)
 
     # everything is set up. call the initialization function.
     self.setup()
 
-    # sleep for one second before signalling. we want to make sure the
-    # parent has called signal.pause()
-    ### we should think of a better wait around the race condition.
-    time.sleep(1)
+    ### TEST: exit before signalling.
+    #sys.exit(0)
+    #sys.exit(1)
 
-    # okay. the daemon is ready. signal the parent to tell it we're set.
-    os.kill(thispid, signal.SIGUSR1)
+    # the child is now ready for parent/anyone to communicate with it.
+    child_is_ready.set()
 
     # start the daemon now.
     self.run()
 
     # The daemon is shutting down, so toss the pidfile.
-    try:
-      os.remove(self.pidfile)
-    except OSError:
-      pass
+    if self.pidfile:
+      try:
+        os.remove(self.pidfile)
+      except OSError:
+        pass
 
+    ### this return value is ignored
     return DAEMON_COMPLETE
 
   def setup(self):
@@ -202,6 +246,34 @@ class Daemon(object):
     raise NotImplementedError
 
 
+class _Detacher(Daemon):
+  def __init__(self, target, logfile='/dev/null', pidfile=None,
+               args=(), kwargs={}):
+    Daemon.__init__(self, logfile, pidfile)
+    self.target = target
+    self.args = args
+    self.kwargs = kwargs
+
+  def setup(self):
+    pass
+
+  def run(self):
+    self.target(*self.args, **self.kwargs)
+
+    
+def run_detached(target, *args, **kwargs):
+  """Simple function to run TARGET as a detached daemon.
+  
+  The additional arguments/keywords will be passed along. This function
+  does not return -- sys.exit() will be called as appropriate.
+  
+  (capture SystemExit if logging/reporting is necessary)
+  ### if needed, a variant of this func could be written to not exit
+  """
+  d = _Detacher(target, args=args, kwargs=kwargs)
+  d.daemonize_exit()
+
+
 class SignalCatcher(object):
   def __init__(self, signum):
     self.signalled = False

Modified: subversion/branches/invoke-diff-cmd-feature/tools/server-side/svnpubsub/svnpubsub/client.py
URL: http://svn.apache.org/viewvc/subversion/branches/invoke-diff-cmd-feature/tools/server-side/svnpubsub/svnpubsub/client.py?rev=1516196&r1=1516195&r2=1516196&view=diff
==============================================================================
--- subversion/branches/invoke-diff-cmd-feature/tools/server-side/svnpubsub/svnpubsub/client.py (original)
+++ subversion/branches/invoke-diff-cmd-feature/tools/server-side/svnpubsub/svnpubsub/client.py Wed Aug 21 16:09:43 2013
@@ -62,7 +62,8 @@ class SvnpubsubClientException(Exception
 
 class Client(asynchat.async_chat):
 
-  def __init__(self, url, commit_callback, event_callback):
+  def __init__(self, url, commit_callback, event_callback,
+               metadata_callback = None):
     asynchat.async_chat.__init__(self)
 
     self.last_activity = time.time()
@@ -82,7 +83,8 @@ class Client(asynchat.async_chat):
 
     self.event_callback = event_callback
 
-    self.parser = JSONRecordHandler(commit_callback, event_callback)
+    self.parser = JSONRecordHandler(commit_callback, event_callback,
+                                    metadata_callback)
 
     # Wait for the end of headers. Then we start parsing JSON.
     self.set_terminator(b'\r\n\r\n')
@@ -126,36 +128,50 @@ class Client(asynchat.async_chat):
       self.ibuffer.append(data)
 
 
+class Notification(object):
+  def __init__(self, data):
+    self.__dict__.update(data)
+
+class Commit(Notification):
+  KIND = 'COMMIT'
+
+class Metadata(Notification):
+  KIND = 'METADATA'
+
+
 class JSONRecordHandler:
-  def __init__(self, commit_callback, event_callback):
+  def __init__(self, commit_callback, event_callback, metadata_callback):
     self.commit_callback = commit_callback
     self.event_callback = event_callback
+    self.metadata_callback = metadata_callback
+
+  EXPECTED_VERSION = 1
 
   def feed(self, record):
     obj = json.loads(record)
     if 'svnpubsub' in obj:
       actual_version = obj['svnpubsub'].get('version')
-      EXPECTED_VERSION = 1
-      if actual_version != EXPECTED_VERSION:
-        raise SvnpubsubClientException("Unknown svnpubsub format: %r != %d"
-                                       % (actual_format, expected_format))
+      if actual_version != self.EXPECTED_VERSION:
+        raise SvnpubsubClientException(
+          "Unknown svnpubsub format: %r != %d"
+          % (actual_version, self.EXPECTED_VERSION))
       self.event_callback('version', obj['svnpubsub']['version'])
     elif 'commit' in obj:
       commit = Commit(obj['commit'])
       self.commit_callback(commit)
     elif 'stillalive' in obj:
       self.event_callback('ping', obj['stillalive'])
-
-
-class Commit(object):
-  def __init__(self, commit):
-    self.__dict__.update(commit)
+    elif 'metadata' in obj and self.metadata_callback:
+      metadata = Metadata(obj['metadata'])
+      self.metadata_callback(metadata)
 
 
 class MultiClient(object):
-  def __init__(self, urls, commit_callback, event_callback):
+  def __init__(self, urls, commit_callback, event_callback,
+               metadata_callback = None):
     self.commit_callback = commit_callback
     self.event_callback = event_callback
+    self.metadata_callback = metadata_callback
 
     # No target time, as no work to do
     self.target_time = 0
@@ -185,9 +201,15 @@ class MultiClient(object):
   def _add_channel(self, url):
     # Simply instantiating the client will install it into the global map
     # for processing in the main event loop.
-    Client(url,
-           functools.partial(self.commit_callback, url),
-           functools.partial(self._reconnect, url))
+    if self.metadata_callback:
+      Client(url,
+             functools.partial(self.commit_callback, url),
+             functools.partial(self._reconnect, url),
+             functools.partial(self.metadata_callback, url))
+    else:
+      Client(url,
+             functools.partial(self.commit_callback, url),
+             functools.partial(self._reconnect, url))
 
   def _check_stale(self):
     now = time.time()

Modified: subversion/branches/invoke-diff-cmd-feature/tools/server-side/svnpubsub/svnpubsub/server.py
URL: http://svn.apache.org/viewvc/subversion/branches/invoke-diff-cmd-feature/tools/server-side/svnpubsub/svnpubsub/server.py?rev=1516196&r1=1516195&r2=1516196&view=diff
==============================================================================
--- subversion/branches/invoke-diff-cmd-feature/tools/server-side/svnpubsub/svnpubsub/server.py (original)
+++ subversion/branches/invoke-diff-cmd-feature/tools/server-side/svnpubsub/svnpubsub/server.py Wed Aug 21 16:09:43 2013
@@ -25,20 +25,27 @@
 # Instead of using a complicated XMPP/AMPQ/JMS/super messaging service,
 # we have simple HTTP GETs and PUTs to get data in and out.
 #
-# Currently supports both XML and JSON serialization.
+# Currently supports JSON serialization.
 #
 # Example Sub clients:
-#   curl -sN http://127.0.0.1:2069/commits
-#   curl -sN http://127.0.0.1:2069/commits/svn/*
-#   curl -sN http://127.0.0.1:2069/commits/svn
-#   curl -sN http://127.0.0.1:2069/commits/*/13f79535-47bb-0310-9956-ffa450edef68
-#   curl -sN http://127.0.0.1:2069/commits/svn/13f79535-47bb-0310-9956-ffa450edef68
+#   curl -sN  http://127.0.0.1:2069/commits
+#   curl -sN 'http://127.0.0.1:2069/commits/svn/*'
+#   curl -sN  http://127.0.0.1:2069/commits/svn
+#   curl -sN 'http://127.0.0.1:2069/commits/*/13f79535-47bb-0310-9956-ffa450edef68'
+#   curl -sN  http://127.0.0.1:2069/commits/svn/13f79535-47bb-0310-9956-ffa450edef68
 #
-#   URL is built into 2 parts:
-#       /commits/${optional_type}/${optional_repository}
+#   curl -sN  http://127.0.0.1:2069/metadata
+#   curl -sN 'http://127.0.0.1:2069/metadata/svn/*'
+#   curl -sN  http://127.0.0.1:2069/metadata/svn
+#   curl -sN 'http://127.0.0.1:2069/metadata/*/13f79535-47bb-0310-9956-ffa450edef68'
+#   curl -sN  http://127.0.0.1:2069/metadata/svn/13f79535-47bb-0310-9956-ffa450edef68
 #
-#   If the type is included in the URL, you will only get commits of that type.
-#   The type can be * and then you will receive commits of any type.
+#   URLs are constructed from 3 parts:
+#       /${notification}/${optional_type}/${optional_repository}
+#
+#   Notifications can be sent for commits or metadata (e.g., revprop) changes.
+#   If the type is included in the URL, you will only get notifications of that type.
+#   The type can be * and then you will receive notifications of any type.
 #
 #   If the repository is included in the URL, you will only receive
 #   messages about that repository.  The repository can be * and then you
@@ -71,7 +78,7 @@ from twisted.python import log
 
 import time
 
-class Commit:
+class Notification(object):
     def __init__(self, r):
         self.__dict__.update(r)
         if not self.check_value('repository'):
@@ -86,7 +93,16 @@ class Commit:
     def check_value(self, k):
         return hasattr(self, k) and self.__dict__[k]
 
-    def render_commit(self):
+    def render(self):
+        raise NotImplementedError
+
+    def render_log(self):
+        raise NotImplementedError
+
+class Commit(Notification):
+    KIND = 'COMMIT'
+
+    def render(self):
         obj = {'commit': {}}
         obj['commit'].update(self.__dict__)
         return json.dumps(obj)
@@ -96,20 +112,32 @@ class Commit:
             paths_changed = " %d paths changed" % len(self.changed)
         except:
             paths_changed = ""
-        return "%s:%s repo '%s' id '%s'%s" % (self.type,
-                                  self.format,
-                                  self.repository,
-                                  self.id,
-                                  paths_changed)
+        return "commit %s:%s repo '%s' id '%s'%s" % (
+            self.type, self.format, self.repository, self.id,
+            paths_changed)
+
+class Metadata(Notification):
+    KIND = 'METADATA'
+
+    def render(self):
+        obj = {'metadata': {}}
+        obj['metadata'].update(self.__dict__)
+        return json.dumps(obj)
+
+    def render_log(self):
+        return "metadata %s:%s repo '%s' id '%s' revprop '%s'" % (
+            self.type, self.format, self.repository, self.id,
+            self.revprop['name'])
 
 
 HEARTBEAT_TIME = 15
 
 class Client(object):
-    def __init__(self, pubsub, r, type, repository):
+    def __init__(self, pubsub, r, kind, type, repository):
         self.pubsub = pubsub
         r.notifyFinish().addErrback(self.finished)
         self.r = r
+        self.kind = kind
         self.type = type
         self.repository = repository
         self.alive = True
@@ -123,11 +151,14 @@ class Client(object):
         except ValueError:
             pass
 
-    def interested_in(self, commit):
-        if self.type and self.type != commit.type:
+    def interested_in(self, notification):
+        if self.kind != notification.KIND:
             return False
 
-        if self.repository and self.repository != commit.repository:
+        if self.type and self.type != notification.type:
+            return False
+
+        if self.repository and self.repository != notification.repository:
             return False
 
         return True
@@ -152,7 +183,8 @@ class Client(object):
         self.r.write(str(input))
 
     def write_start(self):
-        self.r.setHeader('content-type', 'application/json')
+        self.r.setHeader('X-SVNPubSub-Version', '1')
+        self.r.setHeader('content-type', 'application/vnd.apache.vc-notify+json')
         self.write('{"svnpubsub": {"version": 1}}\n\0')
 
     def write_heartbeat(self):
@@ -163,6 +195,13 @@ class SvnPubSub(resource.Resource):
     isLeaf = True
     clients = []
 
+    __notification_uri_map = {'commits': Commit.KIND,
+                              'metadata': Metadata.KIND}
+
+    def __init__(self, notification_class):
+        resource.Resource.__init__(self)
+        self.__notification_class = notification_class
+
     def cc(self):
         return len(self.clients)
 
@@ -182,6 +221,11 @@ class SvnPubSub(resource.Resource):
             request.setResponseCode(400)
             return "Invalid path\n"
 
+        kind = self.__notification_uri_map.get(uri[1], None)
+        if kind is None:
+            request.setResponseCode(400)
+            return "Invalid path\n"
+
         if uri_len >= 3:
           type = uri[2]
 
@@ -194,17 +238,18 @@ class SvnPubSub(resource.Resource):
         if repository == '*':
           repository = None
 
-        c = Client(self, request, type, repository)
+        c = Client(self, request, kind, type, repository)
         self.clients.append(c)
         c.start()
         return twisted.web.server.NOT_DONE_YET
 
-    def notifyAll(self, commit):
-        data = commit.render_commit()
+    def notifyAll(self, notification):
+        data = notification.render()
 
-        log.msg("COMMIT: %s (%d clients)" % (commit.render_log(), self.cc()))
+        log.msg("%s: %s (%d clients)"
+                % (notification.KIND, notification.render_log(), self.cc()))
         for client in self.clients:
-            if client.interested_in(commit):
+            if client.interested_in(notification):
                 client.write_data(data)
 
     def render_PUT(self, request):
@@ -217,19 +262,23 @@ class SvnPubSub(resource.Resource):
         #import pdb;pdb.set_trace()
         #print "input: %s" % (input)
         try:
-            c = json.loads(input)
-            commit = Commit(c)
+            data = json.loads(input)
+            notification = self.__notification_class(data)
         except ValueError as e:
             request.setResponseCode(400)
-            log.msg("COMMIT: failed due to: %s" % str(e))
-            return str(e)
-        self.notifyAll(commit)
+            errstr = str(e)
+            log.msg("%s: failed due to: %s" % (notification.KIND, errstr))
+            return errstr
+        self.notifyAll(notification)
         return "Ok"
 
+
 def svnpubsub_server():
     root = resource.Resource()
-    s = SvnPubSub()
-    root.putChild("commits", s)
+    c = SvnPubSub(Commit)
+    m = SvnPubSub(Metadata)
+    root.putChild('commits', c)
+    root.putChild('metadata', m)
     return server.Site(root)
 
 if __name__ == "__main__":

Modified: subversion/branches/invoke-diff-cmd-feature/tools/server-side/svnpubsub/svnwcsub.py
URL: http://svn.apache.org/viewvc/subversion/branches/invoke-diff-cmd-feature/tools/server-side/svnpubsub/svnwcsub.py?rev=1516196&r1=1516195&r2=1516196&view=diff
==============================================================================
--- subversion/branches/invoke-diff-cmd-feature/tools/server-side/svnpubsub/svnwcsub.py (original)
+++ subversion/branches/invoke-diff-cmd-feature/tools/server-side/svnpubsub/svnwcsub.py Wed Aug 21 16:09:43 2013
@@ -69,27 +69,22 @@ except ImportError:
 
 import daemonize
 import svnpubsub.client
-
-# check_output() is only available in Python 2.7. Allow us to run with
-# earlier versions
-try:
-    check_output = subprocess.check_output
-except AttributeError:
-    def check_output(args, env):  # note: we only use these two args
-        pipe = subprocess.Popen(args, stdout=subprocess.PIPE, env=env)
-        output, _ = pipe.communicate()
-        if pipe.returncode:
-            raise subprocess.CalledProcessError(pipe.returncode, args)
-        return output
+import svnpubsub.util
 
 assert hasattr(subprocess, 'check_call')
 def check_call(*args, **kwds):
-    """Wrapper around subprocess.check_call() that logs stderr upon failure."""
+    """Wrapper around subprocess.check_call() that logs stderr upon failure,
+    with an optional list of exit codes to consider non-failure."""
     assert 'stderr' not in kwds
+    if '__okayexits' in kwds:
+        __okayexits = kwds['__okayexits']
+        del kwds['__okayexits']
+    else:
+        __okayexits = set([0]) # EXIT_SUCCESS
     kwds.update(stderr=subprocess.PIPE)
     pipe = subprocess.Popen(*args, **kwds)
     output, errput = pipe.communicate()
-    if pipe.returncode:
+    if pipe.returncode not in __okayexits:
         cmd = args[0] if len(args) else kwds.get('args', '(no command)')
         # TODO: log stdout too?
         logging.error('Command failed: returncode=%d command=%r stderr=%r',
@@ -103,7 +98,7 @@ def check_call(*args, **kwds):
 def svn_info(svnbin, env, path):
     "Run 'svn info' on the target path, returning a dict of info data."
     args = [svnbin, "info", "--non-interactive", "--", path]
-    output = check_output(args, env=env).strip()
+    output = svnpubsub.util.check_output(args, env=env).strip()
     info = { }
     for line in output.split('\n'):
         idx = line.index(':')
@@ -303,6 +298,21 @@ class BackgroundWorker(threading.Thread)
 
         logging.info("updating: %s", wc.path)
 
+        ## Run the hook
+        HEAD = svn_info(self.svnbin, self.env, wc.url)['Revision']
+        if self.hook:
+            hook_mode = ['pre-update', 'pre-boot'][boot]
+            logging.info('running hook: %s at %s',
+                         wc.path, hook_mode)
+            args = [self.hook, hook_mode, wc.path, HEAD, wc.url]
+            rc = check_call(args, env=self.env, __okayexits=[0, 1])
+            if rc == 1:
+                # TODO: log stderr
+                logging.warn('hook denied update of %s at %s',
+                             wc.path, hook_mode)
+                return
+            del rc
+
         ### we need to move some of these args into the config. these are
         ### still specific to the ASF setup.
         args = [self.svnbin, 'switch',
@@ -313,12 +323,13 @@ class BackgroundWorker(threading.Thread)
                 '--config-option',
                 'config:miscellany:use-commit-times=on',
                 '--',
-                wc.url,
+                wc.url + '@' + HEAD,
                 wc.path]
         check_call(args, env=self.env)
 
         ### check the loglevel before running 'svn info'?
         info = svn_info(self.svnbin, self.env, wc.path)
+        assert info['Revision'] == HEAD
         logging.info("updated: %s now at r%s", wc.path, info['Revision'])
 
         ## Run the hook

Modified: subversion/branches/invoke-diff-cmd-feature/tools/server-side/svnpubsub/watcher.py
URL: http://svn.apache.org/viewvc/subversion/branches/invoke-diff-cmd-feature/tools/server-side/svnpubsub/watcher.py?rev=1516196&r1=1516195&r2=1516196&view=diff
==============================================================================
--- subversion/branches/invoke-diff-cmd-feature/tools/server-side/svnpubsub/watcher.py (original)
+++ subversion/branches/invoke-diff-cmd-feature/tools/server-side/svnpubsub/watcher.py Wed Aug 21 16:09:43 2013
@@ -35,6 +35,9 @@ def _commit(url, commit):
   print('COMMIT: from %s' % url)
   pprint.pprint(vars(commit), indent=2)
 
+def _metadata(url, metadata):
+  print('METADATA: from %s' % url)
+  pprint.pprint(vars(metadata), indent=2)
 
 def _event(url, event_name, event_arg):
   if event_arg:
@@ -44,7 +47,7 @@ def _event(url, event_name, event_arg):
 
 
 def main(urls):
-  mc = svnpubsub.client.MultiClient(urls, _commit, _event)
+  mc = svnpubsub.client.MultiClient(urls, _commit, _event, _metadata)
   mc.run_forever()
 
 

Modified: subversion/branches/invoke-diff-cmd-feature/win-tests.py
URL: http://svn.apache.org/viewvc/subversion/branches/invoke-diff-cmd-feature/win-tests.py?rev=1516196&r1=1516195&r2=1516196&view=diff
==============================================================================
--- subversion/branches/invoke-diff-cmd-feature/win-tests.py (original)
+++ subversion/branches/invoke-diff-cmd-feature/win-tests.py Wed Aug 21 16:09:43 2013
@@ -84,6 +84,8 @@ def _usage_exit():
   print("  --disable-bulk-updates : Disable bulk updates on HTTP server")
   print("  --ssl-cert             : Path to SSL server certificate to trust.")
   print("  --javahl               : Run the javahl tests instead of the normal tests")
+  print("  --swig=language        : Run the swig perl/python/ruby tests instead of")
+  print("                           the normal tests")
   print("  --list                 : print test doc strings only")
   print("  --milestone-filter=RE  : RE is a regular expression pattern that (when")
   print("                           used with --list) limits the tests listed to")
@@ -108,29 +110,24 @@ CMDLINE_TEST_SCRIPT_NATIVE_PATH = CMDLIN
 sys.path.insert(0, os.path.join('build', 'generator'))
 sys.path.insert(1, 'build')
 
-import gen_win
+import gen_win_dependencies
+import gen_base
 version_header = os.path.join('subversion', 'include', 'svn_version.h')
 cp = configparser.ConfigParser()
 cp.read('gen-make.opts')
-gen_obj = gen_win.GeneratorBase('build.conf', version_header,
-                                cp.items('options'))
+gen_obj = gen_win_dependencies.GenDependenciesBase('build.conf', version_header,
+                                                   cp.items('options'))
 all_tests = gen_obj.test_progs + gen_obj.bdb_test_progs \
           + gen_obj.scripts + gen_obj.bdb_scripts
 client_tests = [x for x in all_tests if x.startswith(CMDLINE_TEST_SCRIPT_PATH)]
 
-svn_dlls = []
-for section in gen_obj.sections.values():
-  if section.options.get("msvc-export"):
-    dll_basename = section.name + "-" + str(gen_obj.version) + ".dll"
-    svn_dlls.append(os.path.join("subversion", section.name, dll_basename))
-
 opts, args = my_getopt(sys.argv[1:], 'hrdvqct:pu:f:',
                        ['release', 'debug', 'verbose', 'quiet', 'cleanup',
                         'test=', 'url=', 'svnserve-args=', 'fs-type=', 'asp.net-hack',
                         'httpd-dir=', 'httpd-port=', 'httpd-daemon',
                         'httpd-server', 'http-short-circuit', 'httpd-no-log',
                         'disable-http-v2', 'disable-bulk-updates', 'help',
-                        'fsfs-packing', 'fsfs-sharding=', 'javahl',
+                        'fsfs-packing', 'fsfs-sharding=', 'javahl', 'swig=',
                         'list', 'enable-sasl', 'bin=', 'parallel',
                         'config-file=', 'server-minor-version=', 'log-level=',
                         'log-to-stdout', 'mode-filter=', 'milestone-filter=',
@@ -156,6 +153,7 @@ http_bulk_updates = True
 list_tests = None
 milestone_filter = None
 test_javahl = None
+test_swig = None
 enable_sasl = None
 svn_bin = None
 parallel = None
@@ -216,6 +214,11 @@ for opt, val in opts:
     fsfs_packing = 1
   elif opt == '--javahl':
     test_javahl = 1
+  elif opt == '--swig':
+    if val not in ['perl', 'python', 'ruby']:
+      sys.stderr.write('Running \'%s\' swig tests not supported (yet).\n' 
+                        % (val,))
+    test_swig = val
   elif opt == '--list':
     list_tests = 1
   elif opt == '--milestone-filter':
@@ -289,12 +292,18 @@ def create_target_dir(dirname):
       print("mkdir: %s" % tgt_dir)
     os.makedirs(tgt_dir)
 
-def copy_changed_file(src, tgt):
+def copy_changed_file(src, tgt=None, to_dir=None, cleanup=True):
   if not os.path.isfile(src):
     print('Could not find ' + src)
     sys.exit(1)
-  if os.path.isdir(tgt):
-    tgt = os.path.join(tgt, os.path.basename(src))
+
+  if to_dir and not tgt:
+    tgt = os.path.join(to_dir, os.path.basename(src))
+  elif not tgt or (tgt and to_dir):
+    raise RuntimeError("Using 'tgt' *or* 'to_dir' is required" % (tgt,))
+  elif tgt and os.path.isdir(tgt):
+    raise RuntimeError("'%s' is a directory. Use to_dir=" % (tgt,))
+
   if os.path.exists(tgt):
     assert os.path.isfile(tgt)
     if filecmp.cmp(src, tgt):
@@ -306,57 +315,35 @@ def copy_changed_file(src, tgt):
     print("copy: %s" % src)
     print("  to: %s" % tgt)
   shutil.copy(src, tgt)
-  return 1
 
-def copy_execs(baton, dirname, names):
-  copied_execs = baton
-  for name in names:
-    if not name.endswith('.exe'):
-      continue
-    src = os.path.join(dirname, name)
-    tgt = os.path.join(abs_builddir, dirname, name)
-    create_target_dir(dirname)
-    if copy_changed_file(src, tgt):
-      copied_execs.append(tgt)
+  if cleanup:
+    copied_execs.append(tgt)
 
 def locate_libs():
   "Move DLLs to a known location and set env vars"
 
-  dlls = []
-
-  # look for APR 1.x dll's and use those if found
-  apr_test_path = os.path.join(gen_obj.apr_path, objdir, 'libapr-1.dll')
-  if os.path.exists(apr_test_path):
-    suffix = "-1"
-  else:
-    suffix = ""
-
-  if cp.has_option('options', '--with-static-apr'):
-    dlls.append(os.path.join(gen_obj.apr_path, objdir,
-                             'libapr%s.dll' % (suffix)))
-    dlls.append(os.path.join(gen_obj.apr_util_path, objdir,
-                             'libaprutil%s.dll' % (suffix)))
-
-  if gen_obj.libintl_path is not None:
-    dlls.append(os.path.join(gen_obj.libintl_path, 'bin', 'intl3_svn.dll'))
+  debug = (objdir == 'Debug')
+  
+  for lib in gen_obj._libraries.values():
+
+    if debug:
+      name, dir = lib.debug_dll_name, lib.debug_dll_dir
+    else:
+      name, dir = lib.dll_name, lib.dll_dir
+      
+    if name and dir:
+      src = os.path.join(dir, name)
+      if os.path.exists(src):
+        copy_changed_file(src, to_dir=abs_builddir, cleanup=False)
 
-  if gen_obj.bdb_lib is not None:
-    partial_path = os.path.join(gen_obj.bdb_path, 'bin', gen_obj.bdb_lib)
-    if objdir == 'Debug':
-      dlls.append(partial_path + 'd.dll')
-    else:
-      dlls.append(partial_path + '.dll')
-
-  if gen_obj.sasl_path is not None:
-    dlls.append(os.path.join(gen_obj.sasl_path, 'lib', 'libsasl.dll'))
-
-  for dll in dlls:
-    copy_changed_file(dll, abs_objdir)
 
   # Copy the Subversion library DLLs
-  if not cp.has_option('options', '--disable-shared'):
-    for svn_dll in svn_dlls:
-      copy_changed_file(os.path.join(abs_objdir, svn_dll), abs_objdir)
+  for i in gen_obj.graph.get_all_sources(gen_base.DT_INSTALL):
+    if isinstance(i, gen_base.TargetLib) and i.msvc_export:
+      src = os.path.join(abs_objdir, i.filename)
+      if os.path.isfile(src):
+        copy_changed_file(src, to_dir=abs_builddir,
+                          cleanup=False)
 
   # Copy the Apache modules
   if run_httpd and cp.has_option('options', '--with-httpd'):
@@ -367,11 +354,11 @@ def locate_libs():
     mod_dontdothat_path = os.path.join(abs_objdir, 'tools', 'server-side',
                                         'mod_dontdothat', 'mod_dontdothat.so')
 
-    copy_changed_file(mod_dav_svn_path, abs_objdir)
-    copy_changed_file(mod_authz_svn_path, abs_objdir)
-    copy_changed_file(mod_dontdothat_path, abs_objdir)
+    copy_changed_file(mod_dav_svn_path, to_dir=abs_builddir, cleanup=False)
+    copy_changed_file(mod_authz_svn_path, to_dir=abs_builddir, cleanup=False)
+    copy_changed_file(mod_dontdothat_path, to_dir=abs_builddir, cleanup=False)
 
-  os.environ['PATH'] = abs_objdir + os.pathsep + os.environ['PATH']
+  os.environ['PATH'] = abs_builddir + os.pathsep + os.environ['PATH']
 
 def fix_case(path):
     path = os.path.normpath(path)
@@ -592,6 +579,12 @@ class Httpd:
 
   def _create_dontdothat_file(self):
     "Create empty mime.types file"
+    # If the tests have not previously been run or were cleaned
+    # up, then 'svn-test-work' does not exist yet.
+    parent_dir = os.path.dirname(self.dontdothat_file)
+    if not os.path.exists(parent_dir):
+      os.makedirs(parent_dir)
+
     fp = open(self.dontdothat_file, 'w')
     fp.write('[recursive-actions]\n')
     fp.write('/ = deny\n')
@@ -602,7 +595,7 @@ class Httpd:
     return 'LoadModule ' + name + " " + self._quote(full_path) + '\n'
 
   def _svn_module(self, name, path):
-    full_path = os.path.join(self.abs_objdir, path)
+    full_path = os.path.join(self.abs_builddir, path)
     return 'LoadModule ' + name + ' ' + self._quote(full_path) + '\n'
 
   def _svn_repo(self, name):
@@ -682,21 +675,17 @@ class Httpd:
     print('Httpd.stop_daemon not implemented')
 
 # Move the binaries to the test directory
+create_target_dir(abs_builddir)
 locate_libs()
 if create_dirs:
-  old_cwd = os.getcwd()
-  try:
-    os.chdir(abs_objdir)
-    baton = copied_execs
-    for dirpath, dirs, files in os.walk('subversion'):
-      copy_execs(baton, dirpath, files)
-    for dirpath, dirs, files in os.walk('tools/server-side'):
-      copy_execs(baton, dirpath, files)
-  except:
-    os.chdir(old_cwd)
-    raise
-  else:
-    os.chdir(old_cwd)
+  for i in gen_obj.graph.get_all_sources(gen_base.DT_INSTALL):
+    if isinstance(i, gen_base.TargetExe):
+      src = os.path.join(abs_objdir, i.filename)
+
+      if os.path.isfile(src):
+        dst = os.path.join(abs_builddir, i.filename)
+        create_target_dir(os.path.dirname(dst))
+        copy_changed_file(src, dst)
 
 # Create the base directory for Python tests
 create_target_dir(CMDLINE_TEST_SCRIPT_NATIVE_PATH)
@@ -754,7 +743,7 @@ else:
   print('Testing %s configuration on %s' % (objdir, repo_loc))
 sys.path.insert(0, os.path.join(abs_srcdir, 'build'))
 
-if not test_javahl:
+if not test_javahl and not test_swig:
   import run_tests
   if log_to_stdout:
     log_file = None
@@ -782,7 +771,7 @@ if not test_javahl:
     raise
   else:
     os.chdir(old_cwd)
-else:
+elif test_javahl:
   failed = False
   args = (
           'java.exe',
@@ -820,6 +809,148 @@ else:
   if (r != 0):
     print('[Test runner reported failure]')
     failed = True
+elif test_swig == 'perl':
+  failed = False
+  swig_dir = os.path.join(abs_builddir, 'swig')
+  swig_pl_dir = os.path.join(swig_dir, 'p5lib')
+  swig_pl_svn = os.path.join(swig_pl_dir, 'SVN')
+  swig_pl_auto_svn = os.path.join(swig_pl_dir, 'auto', 'SVN')
+
+  create_target_dir(swig_pl_svn)
+
+  for i in gen_obj.graph.get_all_sources(gen_base.DT_INSTALL):
+    if isinstance(i, gen_base.TargetSWIG) and i.lang == 'perl':
+      mod_dir = os.path.join(swig_pl_auto_svn, '_' + i.name[5:].capitalize())
+      create_target_dir(mod_dir)
+      copy_changed_file(os.path.join(abs_objdir, i.filename), to_dir=mod_dir)
+
+    elif isinstance(i, gen_base.TargetSWIGLib) and i.lang == 'perl':
+      copy_changed_file(os.path.join(abs_objdir, i.filename),
+                        to_dir=abs_builddir)
+
+  pm_src = os.path.join(abs_srcdir, 'subversion', 'bindings', 'swig', 'perl',
+                        'native')
+
+  tests = []
+
+  for root, dirs, files in os.walk(pm_src):
+    for name in files:
+      if name.endswith('.pm'):
+        fn = os.path.join(root, name)
+        copy_changed_file(fn, to_dir=swig_pl_svn)
+      elif name.endswith('.t'):
+        tests.append(os.path.relpath(os.path.join(root, name), pm_src))
+
+  perl5lib = swig_pl_dir
+  if 'PERL5LIB' in os.environ:
+    perl5lib += os.pathsep + os.environ['PERL5LIB']
+
+  perl_exe = 'perl.exe'
+
+  print('-- Running Swig Perl tests --')
+  old_cwd = os.getcwd()
+  try:
+    os.chdir(pm_src)
+
+    os.environ['PERL5LIB'] = perl5lib
+    os.environ["SVN_DBG_NO_ABORT_ON_ERROR_LEAK"] = 'YES'
+
+    r = subprocess.call([
+              perl_exe,
+              '-MExtUtils::Command::MM',
+              '-e', 'test_harness()'
+              ] + tests)
+  finally:
+    os.chdir(old_cwd)
+
+  if (r != 0):
+    print('[Test runner reported failure]')
+    failed = True
+  sys.exit(1)
+elif test_swig == 'python':
+  failed = False
+  swig_dir = os.path.join(abs_builddir, 'swig')
+  swig_py_dir = os.path.join(swig_dir, 'pylib')
+  swig_py_libsvn = os.path.join(swig_py_dir, 'libsvn')
+  swig_py_svn = os.path.join(swig_py_dir, 'svn')
+
+  create_target_dir(swig_py_libsvn)
+  create_target_dir(swig_py_svn)
+
+  for i in gen_obj.graph.get_all_sources(gen_base.DT_INSTALL):
+    if (isinstance(i, gen_base.TargetSWIG)
+        or isinstance(i, gen_base.TargetSWIGLib)) and i.lang == 'python':
+
+      src = os.path.join(abs_objdir, i.filename)
+      copy_changed_file(src, to_dir=swig_py_libsvn)
+
+  py_src = os.path.join(abs_srcdir, 'subversion', 'bindings', 'swig', 'python')
+
+  for py_file in os.listdir(py_src):
+    if py_file.endswith('.py'):
+      copy_changed_file(os.path.join(py_src, py_file),
+                        to_dir=swig_py_libsvn)
+
+  py_src_svn = os.path.join(py_src, 'svn')
+  for py_file in os.listdir(py_src_svn):
+    if py_file.endswith('.py'):
+      copy_changed_file(os.path.join(py_src_svn, py_file),
+                        to_dir=swig_py_svn)
+
+  print('-- Running Swig Python tests --')
+
+  pythonpath = swig_py_dir
+  if 'PYTHONPATH' in os.environ:
+    pythonpath += os.pathsep + os.environ['PYTHONPATH']
+
+  python_exe = 'python.exe'
+  old_cwd = os.getcwd()
+  try:
+    os.environ['PYTHONPATH'] = pythonpath
+
+    r = subprocess.call([
+              python_exe,
+              os.path.join(py_src, 'tests', 'run_all.py')
+              ])
+  finally:
+    os.chdir(old_cwd)
+
+    if (r != 0):
+      print('[Test runner reported failure]')
+      failed = True
+
+elif test_swig == 'ruby':
+  failed = False
+
+  if 'ruby' not in gen_obj._libraries:
+    print('Ruby not found. Skipping Ruby tests')
+  else:
+    ruby_lib = gen_obj._libraries['ruby']
+
+    ruby_exe = 'ruby.exe'
+    ruby_subdir = os.path.join('subversion', 'bindings', 'swig', 'ruby')
+    ruby_args = [
+        '-I', os.path.join(abs_srcdir, ruby_subdir),
+        os.path.join(abs_srcdir, ruby_subdir, 'test', 'run-test.rb'),
+        '--verbose'
+      ]
+
+    print('-- Running Swig Ruby tests --')
+    old_cwd = os.getcwd()
+    try:
+      os.chdir(ruby_subdir)
+
+      os.environ["BUILD_TYPE"] = objdir
+      os.environ["SVN_DBG_NO_ABORT_ON_ERROR_LEAK"] = 'YES'
+      r = subprocess.call([ruby_exe] + ruby_args)
+    finally:
+      os.chdir(old_cwd)
+
+    sys.stdout.flush()
+    sys.stderr.flush()
+    if (r != 0):
+      print('[Test runner reported failure]')
+      failed = True
 
 # Stop service daemon, if any
 if daemon: