Просмотр исходного кода

ENH: Teach CTest to handle Mercurial repositories

This creates cmCTestHG to drive CTest Update handling on hg-based work
trees.  Currently we always update to the head of the remote tracking
branch (hg pull), so the nightly start time is ignored for Nightly
builds.  A later change will address this.

See issue #7879.  Patch from Emmanuel Christophe.  I modified the patch
slightly for code style, to finish up some parsing details, and to fix
the test.
Brad King 16 лет назад
Родитель
Сommit
d4d467dbd5

+ 11 - 0
Modules/CTest.cmake

@@ -80,6 +80,7 @@ IF(BUILD_TESTING)
     "Options passed to the cvs update command.")
   FIND_PROGRAM(SVNCOMMAND svn)
   FIND_PROGRAM(BZRCOMMAND bzr)
+  FIND_PROGRAM(HGCOMMAND hg)
 
   IF(NOT UPDATE_TYPE)
     IF(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/CVS")
@@ -90,6 +91,10 @@ IF(BUILD_TESTING)
       ELSE(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/.svn")
         IF(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/.bzr")
           SET(UPDATE_TYPE bzr)
+        ELSE(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/.bzr")
+          IF(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/.hg")
+            SET(UPDATE_TYPE hg)
+          ENDIF(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/.hg")
         ENDIF(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/.bzr")
       ENDIF(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/.svn")
     ENDIF(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/CVS")
@@ -114,6 +119,11 @@ IF(BUILD_TESTING)
       IF("${_update_type}" STREQUAL "bzr")
         SET(UPDATE_COMMAND "${BZRCOMMAND}")
         SET(UPDATE_OPTIONS "${BZR_UPDATE_OPTIONS}")
+      ELSE("${_update_type}" STREQUAL "bzr")
+        IF("${_update_type}" STREQUAL "hg")
+          SET(UPDATE_COMMAND "${HGCOMMAND}")
+          SET(UPDATE_OPTIONS "${HG_UPDATE_OPTIONS}")
+        ENDIF("${_update_type}" STREQUAL "hg")
       ENDIF("${_update_type}" STREQUAL "bzr")
     ENDIF("${_update_type}" STREQUAL "svn")
   ENDIF("${_update_type}" STREQUAL "cvs")
@@ -193,6 +203,7 @@ IF(BUILD_TESTING)
     CVSCOMMAND
     SVNCOMMAND
     BZRCOMMAND
+    HGCOMMAND
     CVS_UPDATE_OPTIONS
     SVN_UPDATE_OPTIONS
     BZR_UPDATE_OPTIONS

+ 2 - 0
Source/CMakeLists.txt

@@ -373,6 +373,8 @@ SET(CTEST_SRCS cmCTest.cxx
   CTest/cmCTestBZR.h
   CTest/cmCTestGIT.cxx
   CTest/cmCTestGIT.h
+  CTest/cmCTestHG.cxx
+  CTest/cmCTestHG.h
   )
 
 # Build CTestLib

+ 343 - 0
Source/CTest/cmCTestHG.cxx

@@ -0,0 +1,343 @@
+/*=========================================================================
+
+  Program:   CMake - Cross-Platform Makefile Generator
+  Module:    $RCSfile$
+  Language:  C++
+  Date:      $Date$
+  Version:   $Revision$
+
+  Copyright (c) 2002 Kitware, Inc. All rights reserved.
+  See Copyright.txt or http://www.cmake.org/HTML/Copyright.html for details.
+
+     This software is distributed WITHOUT ANY WARRANTY; without even
+     the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+     PURPOSE.  See the above copyright notices for more information.
+
+=========================================================================*/
+#include "cmCTestHG.h"
+
+#include "cmCTest.h"
+#include "cmSystemTools.h"
+#include "cmXMLParser.h"
+
+#include <cmsys/RegularExpression.hxx>
+
+//----------------------------------------------------------------------------
+cmCTestHG::cmCTestHG(cmCTest* ct, std::ostream& log):
+  cmCTestGlobalVC(ct, log)
+{
+  this->PriorRev = this->Unknown;
+}
+
+//----------------------------------------------------------------------------
+cmCTestHG::~cmCTestHG()
+{
+}
+
+//----------------------------------------------------------------------------
+class cmCTestHG::IdentifyParser: public cmCTestVC::LineParser
+{
+public:
+  IdentifyParser(cmCTestHG* hg, const char* prefix,
+                 std::string& rev): Rev(rev)
+    {
+    this->SetLog(&hg->Log, prefix);
+    this->RegexIdentify.compile("^([0-9a-f]+)");
+    }
+private:
+  std::string& Rev;
+  cmsys::RegularExpression RegexIdentify;
+
+  bool ProcessLine()
+    {
+    if(this->RegexIdentify.find(this->Line))
+      {
+      this->Rev = this->RegexIdentify.match(1);
+      return false;
+      }
+    return true;
+    }
+};
+
+//----------------------------------------------------------------------------
+class cmCTestHG::StatusParser: public cmCTestVC::LineParser
+{
+public:
+  StatusParser(cmCTestHG* hg, const char* prefix): HG(hg)
+    {
+    this->SetLog(&hg->Log, prefix);
+    this->RegexStatus.compile("([MARC!?I]) (.*)");
+    }
+
+private:
+  cmCTestHG* HG;
+  cmsys::RegularExpression RegexStatus;
+
+  bool ProcessLine()
+    {
+    if(this->RegexStatus.find(this->Line))
+      {
+      this->DoPath(this->RegexStatus.match(1)[0],
+                   this->RegexStatus.match(2));
+      }
+    return true;
+    }
+
+  void DoPath(char status, std::string const& path)
+    {
+    if(path.empty()) return;
+
+    // See "hg help status".  Note that there is no 'conflict' status.
+    switch(status)
+      {
+      case 'M': case 'A': case '!': case 'R':
+        this->HG->DoModification(PathModified, path);
+        break;
+      case 'I': case '?': case 'C': case ' ': default:
+        break;
+      }
+    }
+};
+
+//----------------------------------------------------------------------------
+std::string cmCTestHG::GetWorkingRevision()
+{
+  // Run plumbing "hg identify" to get work tree revision.
+  const char* hg = this->CommandLineTool.c_str();
+  const char* hg_identify[] = {hg, "identify","-i", 0};
+  std::string rev;
+  IdentifyParser out(this, "rev-out> ", rev);
+  OutputLogger err(this->Log, "rev-err> ");
+  this->RunChild(hg_identify, &out, &err);
+  return rev;
+}
+
+//----------------------------------------------------------------------------
+void cmCTestHG::NoteOldRevision()
+{
+  this->OldRevision = this->GetWorkingRevision();
+  cmCTestLog(this->CTest, HANDLER_OUTPUT, "   Old revision of repository is: "
+             << this->OldRevision << "\n");
+  this->PriorRev.Rev = this->OldRevision;
+}
+
+//----------------------------------------------------------------------------
+void cmCTestHG::NoteNewRevision()
+{
+  this->NewRevision = this->GetWorkingRevision();
+  cmCTestLog(this->CTest, HANDLER_OUTPUT, "   New revision of repository is: "
+             << this->NewRevision << "\n");
+}
+
+//----------------------------------------------------------------------------
+bool cmCTestHG::UpdateImpl()
+{
+  // Use "hg pull" followed by "hg update" to update the working tree.
+  {
+  const char* hg = this->CommandLineTool.c_str();
+  const char* hg_pull[] = {hg, "pull","-v", 0};
+  OutputLogger out(this->Log, "pull-out> ");
+  OutputLogger err(this->Log, "pull-err> ");
+  this->RunChild(&hg_pull[0], &out, &err);
+  }
+
+  // TODO: if(this->CTest->GetTestModel() == cmCTest::NIGHTLY)
+
+  std::vector<char const*> hg_update;
+  hg_update.push_back(this->CommandLineTool.c_str());
+  hg_update.push_back("update");
+  hg_update.push_back("-v");
+
+  // Add user-specified update options.
+  std::string opts = this->CTest->GetCTestConfiguration("UpdateOptions");
+  if(opts.empty())
+    {
+    opts = this->CTest->GetCTestConfiguration("HGUpdateOptions");
+    }
+  std::vector<cmStdString> args = cmSystemTools::ParseArguments(opts.c_str());
+  for(std::vector<cmStdString>::const_iterator ai = args.begin();
+      ai != args.end(); ++ai)
+    {
+    hg_update.push_back(ai->c_str());
+    }
+
+  // Sentinel argument.
+  hg_update.push_back(0);
+
+  OutputLogger out(this->Log, "update-out> ");
+  OutputLogger err(this->Log, "update-err> ");
+  return this->RunUpdateCommand(&hg_update[0], &out, &err);
+}
+
+//----------------------------------------------------------------------------
+class cmCTestHG::LogParser: public cmCTestVC::OutputLogger,
+                            private cmXMLParser
+{
+public:
+  LogParser(cmCTestHG* hg, const char* prefix):
+    OutputLogger(hg->Log, prefix), HG(hg) { this->InitializeParser(); }
+  ~LogParser() { this->CleanupParser(); }
+private:
+  cmCTestHG* HG;
+
+  typedef cmCTestHG::Revision Revision;
+  typedef cmCTestHG::Change Change;
+  Revision Rev;
+  std::vector<Change> Changes;
+  Change CurChange;
+  std::vector<char> CData;
+
+  virtual bool ProcessChunk(const char* data, int length)
+    {
+    this->OutputLogger::ProcessChunk(data, length);
+    this->ParseChunk(data, length);
+    return true;
+    }
+
+  virtual void StartElement(const char* name, const char** atts)
+    {
+    this->CData.clear();
+    if(strcmp(name, "logentry") == 0)
+      {
+      this->Rev = Revision();
+      if(const char* rev = this->FindAttribute(atts, "revision"))
+        {
+        this->Rev.Rev = rev;
+        }
+      this->Changes.clear();
+      }
+    }
+
+  virtual void CharacterDataHandler(const char* data, int length)
+    {
+    this->CData.insert(this->CData.end(), data, data+length);
+    }
+
+  virtual void EndElement(const char* name)
+    {
+    if(strcmp(name, "logentry") == 0)
+      {
+      this->HG->DoRevision(this->Rev, this->Changes);
+      }
+    else if(strcmp(name, "author") == 0 && !this->CData.empty())
+      {
+      this->Rev.Author.assign(&this->CData[0], this->CData.size());
+      }
+    else if ( strcmp(name, "email") == 0 && !this->CData.empty())
+      {
+      // this->Rev.Email.assign(&this->CData[0], this->CData.size());
+      }
+    else if(strcmp(name, "date") == 0 && !this->CData.empty())
+      {
+      this->Rev.Date.assign(&this->CData[0], this->CData.size());
+      }
+    else if(strcmp(name, "msg") == 0 && !this->CData.empty())
+      {
+      this->Rev.Log.assign(&this->CData[0], this->CData.size());
+      }
+    else if(strcmp(name, "files") == 0 && !this->CData.empty())
+      {
+      std::vector<std::string> paths = this->SplitCData();
+      for(unsigned int i = 0; i < paths.size(); ++i)
+        {
+        // Updated by default, will be modified using file_adds and
+        // file_dels.
+        this->CurChange = Change('U');
+        this->CurChange.Path = paths[i];
+        this->Changes.push_back(this->CurChange);
+        }
+      }
+    else if(strcmp(name, "file_adds") == 0 && !this->CData.empty())
+      {
+      std::string added_paths(this->CData.begin(), this->CData.end());
+      for(unsigned int i = 0; i < this->Changes.size(); ++i)
+        {
+        if(added_paths.find(this->Changes[i].Path) != std::string::npos)
+          {
+          this->Changes[i].Action = 'A';
+          }
+        }
+      }
+     else if(strcmp(name, "file_dels") == 0 && !this->CData.empty())
+      {
+      std::string added_paths(this->CData.begin(), this->CData.end());
+      for(unsigned int i = 0; i < this->Changes.size(); ++i)
+        {
+        if(added_paths.find(this->Changes[i].Path) != std::string::npos)
+          {
+          this->Changes[i].Action = 'D';
+          }
+        }
+      }
+    this->CData.clear();
+    }
+
+  std::vector<std::string> SplitCData()
+    {
+    std::vector<std::string> output;
+    std::string currPath;
+    for(unsigned int i=0; i < this->CData.size(); ++i)
+      {
+      if(this->CData[i] != ' ')
+        {
+        currPath.push_back(this->CData[i]);
+        }
+      else
+        {
+        output.push_back(currPath);
+        currPath.erase();
+        }
+      }
+    output.push_back(currPath);
+    return output;
+    }
+
+  virtual void ReportError(int, int, const char* msg)
+    {
+    this->HG->Log << "Error parsing hg log xml: " << msg << "\n";
+    }
+};
+
+//----------------------------------------------------------------------------
+void cmCTestHG::LoadRevisions()
+{
+  // Use 'hg log' to get revisions in a xml format.
+  //
+  // TODO: This should use plumbing or python code to be more precise.
+  // The "list of strings" templates like {files} will not work when
+  // the project has spaces in the path.  Also, they may not have
+  // proper XML escapes.
+  std::string range = this->OldRevision + ":" + this->NewRevision;
+  const char* hg = this->CommandLineTool.c_str();
+  const char* hgXMLTemplate =
+    "<logentry\n"
+    "   revision=\"{node|short}\">\n"
+    "  <author>{author|person}</author>\n"
+    "  <email>{author|email}</email>\n"
+    "  <date>{date|isodate}</date>\n"
+    "  <msg>{desc}</msg>\n"
+    "  <files>{files}</files>\n"
+    "  <file_adds>{file_adds}</file_adds>\n"
+    "  <file_dels>{file_dels}</file_dels>\n"
+    "</logentry>\n";
+  const char* hg_log[] = {hg, "log","--removed", "-r", range.c_str(),
+                          "--template", hgXMLTemplate, 0};
+
+  LogParser out(this, "log-out> ");
+  out.Process("<?xml version=\"1.0\"?>\n"
+              "<log>\n");
+  OutputLogger err(this->Log, "log-err> ");
+  this->RunChild(hg_log, &out, &err);
+  out.Process("</log>\n");
+}
+
+//----------------------------------------------------------------------------
+void cmCTestHG::LoadModifications()
+{
+  // Use 'hg status' to get modified files.
+  const char* hg = this->CommandLineTool.c_str();
+  const char* hg_status[] = {hg, "status", 0};
+  StatusParser out(this, "status-out> ");
+  OutputLogger err(this->Log, "status-err> ");
+  this->RunChild(hg_status, &out, &err);
+}

+ 52 - 0
Source/CTest/cmCTestHG.h

@@ -0,0 +1,52 @@
+/*=========================================================================
+
+  Program:   CMake - Cross-Platform Makefile Generator
+  Module:    $RCSfile$
+  Language:  C++
+  Date:      $Date$
+  Version:   $Revision$
+
+  Copyright (c) 2002 Kitware, Inc. All rights reserved.
+  See Copyright.txt or http://www.cmake.org/HTML/Copyright.html for details.
+
+     This software is distributed WITHOUT ANY WARRANTY; without even
+     the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+     PURPOSE.  See the above copyright notices for more information.
+
+=========================================================================*/
+#ifndef cmCTestHG_h
+#define cmCTestHG_h
+
+#include "cmCTestGlobalVC.h"
+
+/** \class cmCTestHG
+ * \brief Interaction with Mercurial command-line tool
+ *
+ */
+class cmCTestHG: public cmCTestGlobalVC
+{
+public:
+  /** Construct with a CTest instance and update log stream.  */
+  cmCTestHG(cmCTest* ctest, std::ostream& log);
+
+  virtual ~cmCTestHG();
+
+private:
+  std::string GetWorkingRevision();
+  virtual void NoteOldRevision();
+  virtual void NoteNewRevision();
+  virtual bool UpdateImpl();
+
+  void LoadRevisions();
+  void LoadModifications();
+
+  // Parsing helper classes.
+  class IdentifyParser;
+  class StatusParser;
+  class LogParser;
+  friend class IdentifyParser;
+  friend class StatusParser;
+  friend class LogParser;
+};
+
+#endif

+ 4 - 0
Source/CTest/cmCTestUpdateCommand.cxx

@@ -56,6 +56,10 @@ cmCTestGenericHandler* cmCTestUpdateCommand::InitializeHandler()
     "GITCommand", "CTEST_GIT_COMMAND");
   this->CTest->SetCTestConfigurationFromCMakeVariable(this->Makefile,
     "GITUpdateOptions", "CTEST_GIT_UPDATE_OPTIONS");
+  this->CTest->SetCTestConfigurationFromCMakeVariable(this->Makefile,
+    "HGCommand", "CTEST_HG_COMMAND");
+  this->CTest->SetCTestConfigurationFromCMakeVariable(this->Makefile,
+    "HGUpdateOptions", "CTEST_HG_UPDATE_OPTIONS");
 
   const char* initialCheckoutCommand
     = this->Makefile->GetDefinition("CTEST_CHECKOUT_COMMAND");

+ 19 - 1
Source/CTest/cmCTestUpdateHandler.cxx

@@ -32,6 +32,7 @@
 #include "cmCTestSVN.h"
 #include "cmCTestBZR.h"
 #include "cmCTestGIT.h"
+#include "cmCTestHG.h"
 
 #include <cmsys/auto_ptr.hxx>
 
@@ -54,7 +55,8 @@ static const char* cmCTestUpdateHandlerUpdateStrings[] =
   "CVS",
   "SVN",
   "BZR",
-  "GIT"
+  "GIT",
+  "HG"
 };
 
 static const char* cmCTestUpdateHandlerUpdateToString(int type)
@@ -145,6 +147,10 @@ int cmCTestUpdateHandler::DetermineType(const char* cmd, const char* type)
       {
       return cmCTestUpdateHandler::e_GIT;
       }
+    if ( stype.find("hg") != std::string::npos )
+      {
+      return cmCTestUpdateHandler::e_HG;
+      }
     }
   else
     {
@@ -167,6 +173,10 @@ int cmCTestUpdateHandler::DetermineType(const char* cmd, const char* type)
       {
       return cmCTestUpdateHandler::e_GIT;
       }
+    if ( stype.find("hg") != std::string::npos )
+      {
+      return cmCTestUpdateHandler::e_HG;
+      }
     }
   return cmCTestUpdateHandler::e_UNKNOWN;
 }
@@ -226,6 +236,7 @@ int cmCTestUpdateHandler::ProcessHandler()
     case e_SVN: vc.reset(new cmCTestSVN(this->CTest, ofs)); break;
     case e_BZR: vc.reset(new cmCTestBZR(this->CTest, ofs)); break;
     case e_GIT: vc.reset(new cmCTestGIT(this->CTest, ofs)); break;
+    case e_HG:  vc.reset(new cmCTestHG(this->CTest, ofs)); break;
     default:    vc.reset(new cmCTestVC(this->CTest, ofs));  break;
     }
   vc->SetCommandLineTool(this->UpdateCommand);
@@ -371,6 +382,12 @@ int cmCTestUpdateHandler::DetectVCS(const char* dir)
     {
     return cmCTestUpdateHandler::e_GIT;
     }
+  sourceDirectory = dir;
+  sourceDirectory += "/.hg";
+  if ( cmSystemTools::FileExists(sourceDirectory.c_str()) )
+    {
+    return cmCTestUpdateHandler::e_HG;
+    }
   return cmCTestUpdateHandler::e_UNKNOWN;
 }
 
@@ -400,6 +417,7 @@ bool cmCTestUpdateHandler::SelectVCS()
       case e_SVN: key = "SVNCommand"; break;
       case e_BZR: key = "BZRCommand"; break;
       case e_GIT: key = "GITCommand"; break;
+      case e_HG:  key = "HGCommand";  break;
       default: break;
       }
     if (key)

+ 1 - 0
Source/CTest/cmCTestUpdateHandler.h

@@ -48,6 +48,7 @@ public:
     e_SVN,
     e_BZR,
     e_GIT,
+    e_HG,
     e_LAST
   };
 

+ 18 - 0
Tests/CMakeLists.txt

@@ -976,6 +976,24 @@ ${CMake_BINARY_DIR}/bin/cmake -DVERSION=CVS -P ${CMake_SOURCE_DIR}/Utilities/Rel
       LIST(APPEND TEST_BUILD_DIRS "${CMake_BINARY_DIR}/Tests/${CTestUpdateGIT_DIR}")
     ENDIF(CTEST_TEST_UPDATE_GIT)
 
+    # Test CTest Update with HG
+    FIND_PROGRAM(HG_EXECUTABLE NAMES hg)
+    MARK_AS_ADVANCED(HG_EXECUTABLE)
+    SET(CTEST_TEST_UPDATE_HG 0)
+    IF(HG_EXECUTABLE)
+      IF(NOT "${CVS_EXECUTABLE}" MATCHES "cygwin" OR UNIX)
+        SET(CTEST_TEST_UPDATE_HG 1)
+      ENDIF(NOT "${CVS_EXECUTABLE}" MATCHES "cygwin" OR UNIX)
+    ENDIF(HG_EXECUTABLE)
+    IF(CTEST_TEST_UPDATE_HG)
+      SET(CTestUpdateHG_DIR "CTest UpdateHG")
+      CONFIGURE_FILE("${CMake_SOURCE_DIR}/Tests/CTestUpdateHG.cmake.in"
+        "${CMake_BINARY_DIR}/Tests/CTestUpdateHG.cmake" @ONLY)
+      ADD_TEST(CTest.UpdateHG ${CMAKE_CMAKE_COMMAND}
+        -P "${CMake_BINARY_DIR}/Tests/CTestUpdateHG.cmake"
+        )
+      LIST(APPEND TEST_BUILD_DIRS "${CMake_BINARY_DIR}/Tests/${CTestUpdateHG_DIR}")
+    ENDIF(CTEST_TEST_UPDATE_HG)
   ENDIF(CTEST_TEST_UPDATE)
 
   IF (CTEST_TEST_CTEST AND CMAKE_RUN_LONG_TESTS)

+ 163 - 0
Tests/CTestUpdateHG.cmake.in

@@ -0,0 +1,163 @@
+# This script drives creation of a Mercurial repository and checks
+# that CTest can update from it.
+
+#-----------------------------------------------------------------------------
+# Test in a directory next to this script.
+get_filename_component(TOP "${CMAKE_CURRENT_LIST_FILE}" PATH)
+set(TOP "${TOP}/@CTestUpdateHG_DIR@")
+
+# Include code common to all update tests.
+include("@CMAKE_CURRENT_SOURCE_DIR@/CTestUpdateCommon.cmake")
+
+#-----------------------------------------------------------------------------
+# Report hg tools in use.
+message("Using HG tools:")
+set(HG "@HG_EXECUTABLE@")
+message(" hg = ${HG}")
+
+#-----------------------------------------------------------------------------
+# Initialize the testing directory.
+message("Creating test directory...")
+init_testing()
+
+#-----------------------------------------------------------------------------
+# Create the repository.
+message("Creating repository...")
+file(MAKE_DIRECTORY ${TOP}/repo.hg)
+run_child(
+  WORKING_DIRECTORY ${TOP}/repo.hg
+  COMMAND ${HG} init
+  )
+set(REPO file://${TOP}/repo.hg)
+
+#-----------------------------------------------------------------------------
+# Import initial content into the repository.
+message("Importing content...")
+create_content(import)
+
+# Import the content into the repository.
+run_child(WORKING_DIRECTORY ${TOP}/import
+  COMMAND ${HG} init
+  )
+run_child(WORKING_DIRECTORY ${TOP}/import
+  COMMAND ${HG} add .
+  )
+run_child(WORKING_DIRECTORY ${TOP}/import
+  COMMAND ${HG} commit -m "Initial content"
+                       -u "Test Author <[email protected]>"
+  )
+run_child(WORKING_DIRECTORY ${TOP}/import
+  COMMAND ${HG} push "${REPO}"
+  )
+
+#-----------------------------------------------------------------------------
+# Create a working tree.
+message("Checking out first revision...")
+run_child(
+  WORKING_DIRECTORY ${TOP}
+  COMMAND ${HG} clone ${REPO} user-source
+  )
+
+#-----------------------------------------------------------------------------
+# Make changes in the working tree.
+message("Changing content...")
+update_content(user-source files_added files_removed dirs_added)
+if(dirs_added)
+  run_child(
+    WORKING_DIRECTORY ${TOP}/user-source
+    COMMAND ${HG} add ${dirs_added}
+    )
+endif(dirs_added)
+run_child(
+  WORKING_DIRECTORY ${TOP}/user-source
+  COMMAND ${HG} add ${files_added}
+  )
+run_child(
+  WORKING_DIRECTORY ${TOP}/user-source
+  COMMAND ${HG} rm ${files_removed}
+  )
+run_child(
+  WORKING_DIRECTORY ${TOP}/user-source
+  COMMAND ${HG} add
+  )
+
+#-----------------------------------------------------------------------------
+# Commit the changes to the repository.
+message("Committing revision 2...")
+run_child(
+  WORKING_DIRECTORY ${TOP}/user-source
+  COMMAND ${HG} commit -m "Changed content"
+                       -u "Test Author <[email protected]>"
+  )
+run_child(
+  WORKING_DIRECTORY ${TOP}/user-source
+  COMMAND ${HG} push
+  )
+
+#-----------------------------------------------------------------------------
+# Make changes in the working tree.
+message("Changing content again...")
+change_content(user-source)
+run_child(
+  WORKING_DIRECTORY ${TOP}/user-source
+  COMMAND ${HG} add
+  )
+
+#-----------------------------------------------------------------------------
+# Commit the changes to the repository.
+message("Committing revision 3...")
+run_child(
+  WORKING_DIRECTORY ${TOP}/user-source
+  COMMAND ${HG} commit -m "Changed content again"
+                       -u "Test Author <[email protected]>"
+  )
+run_child(
+  WORKING_DIRECTORY ${TOP}/user-source
+  COMMAND ${HG} push
+  )
+
+#-----------------------------------------------------------------------------
+# Go back to before the changes so we can test updating.
+message("Backing up to first revision...")
+run_child(
+  WORKING_DIRECTORY ${TOP}/user-source
+  COMMAND ${HG} update -C -r 0
+  )
+
+# Create a modified file.
+modify_content(user-source)
+
+#-----------------------------------------------------------------------------
+# Test updating the user work directory with the command-line interface.
+message("Running CTest Dashboard Command Line...")
+
+# Create the user build tree.
+create_build_tree(user-source user-binary)
+file(APPEND ${TOP}/user-binary/CTestConfiguration.ini
+  "# HG command configuration
+UpdateCommand: ${HG}
+")
+
+# Run the dashboard command line interface.
+run_dashboard_command_line(user-binary)
+
+#-----------------------------------------------------------------------------
+# Test initial checkout and update with a dashboard script.
+message("Running CTest Dashboard Script...")
+
+create_dashboard_script(dashboard.cmake
+  "# hg command configuration
+set(CTEST_HG_COMMAND \"${HG}\")
+set(CTEST_HG_UPDATE_OPTIONS)
+execute_process(
+  WORKING_DIRECTORY \"${TOP}\"
+  COMMAND \"${HG}\" clone \"${REPO}\" dash-source
+  )
+execute_process(
+  WORKING_DIRECTORY \"${TOP}/dash-source\"
+  COMMAND \"${HG}\" update -C -r 0
+  )
+")
+
+# Run the dashboard script with CTest.
+run_dashboard_script(dashboard.cmake)