cmCTestSVN.cxx 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570
  1. /* Distributed under the OSI-approved BSD 3-Clause License. See accompanying
  2. file Copyright.txt or https://cmake.org/licensing for details. */
  3. #include "cmCTestSVN.h"
  4. #include "cmCTest.h"
  5. #include "cmCTestVC.h"
  6. #include "cmProcessTools.h"
  7. #include "cmSystemTools.h"
  8. #include "cmXMLParser.h"
  9. #include "cmXMLWriter.h"
  10. #include "cmsys/RegularExpression.hxx"
  11. #include <map>
  12. #include <ostream>
  13. #include <stdlib.h>
  14. #include <string.h>
  15. struct cmCTestSVN::Revision : public cmCTestVC::Revision
  16. {
  17. cmCTestSVN::SVNInfo* SVNInfo;
  18. };
  19. cmCTestSVN::cmCTestSVN(cmCTest* ct, std::ostream& log)
  20. : cmCTestGlobalVC(ct, log)
  21. {
  22. this->PriorRev = this->Unknown;
  23. }
  24. cmCTestSVN::~cmCTestSVN()
  25. {
  26. }
  27. void cmCTestSVN::CleanupImpl()
  28. {
  29. std::vector<const char*> svn_cleanup;
  30. svn_cleanup.push_back("cleanup");
  31. OutputLogger out(this->Log, "cleanup-out> ");
  32. OutputLogger err(this->Log, "cleanup-err> ");
  33. this->RunSVNCommand(svn_cleanup, &out, &err);
  34. }
  35. class cmCTestSVN::InfoParser : public cmCTestVC::LineParser
  36. {
  37. public:
  38. InfoParser(cmCTestSVN* svn, const char* prefix, std::string& rev,
  39. SVNInfo& svninfo)
  40. : Rev(rev)
  41. , SVNRepo(svninfo)
  42. {
  43. this->SetLog(&svn->Log, prefix);
  44. this->RegexRev.compile("^Revision: ([0-9]+)");
  45. this->RegexURL.compile("^URL: +([^ ]+) *$");
  46. this->RegexRoot.compile("^Repository Root: +([^ ]+) *$");
  47. }
  48. private:
  49. std::string& Rev;
  50. cmCTestSVN::SVNInfo& SVNRepo;
  51. cmsys::RegularExpression RegexRev;
  52. cmsys::RegularExpression RegexURL;
  53. cmsys::RegularExpression RegexRoot;
  54. bool ProcessLine() CM_OVERRIDE
  55. {
  56. if (this->RegexRev.find(this->Line)) {
  57. this->Rev = this->RegexRev.match(1);
  58. } else if (this->RegexURL.find(this->Line)) {
  59. this->SVNRepo.URL = this->RegexURL.match(1);
  60. } else if (this->RegexRoot.find(this->Line)) {
  61. this->SVNRepo.Root = this->RegexRoot.match(1);
  62. }
  63. return true;
  64. }
  65. };
  66. static bool cmCTestSVNPathStarts(std::string const& p1, std::string const& p2)
  67. {
  68. // Does path p1 start with path p2?
  69. if (p1.size() == p2.size()) {
  70. return p1 == p2;
  71. }
  72. if (p1.size() > p2.size() && p1[p2.size()] == '/') {
  73. return strncmp(p1.c_str(), p2.c_str(), p2.size()) == 0;
  74. }
  75. return false;
  76. }
  77. std::string cmCTestSVN::LoadInfo(SVNInfo& svninfo)
  78. {
  79. // Run "svn info" to get the repository info from the work tree.
  80. std::vector<const char*> svn_info;
  81. svn_info.push_back("info");
  82. svn_info.push_back(svninfo.LocalPath.c_str());
  83. std::string rev;
  84. InfoParser out(this, "info-out> ", rev, svninfo);
  85. OutputLogger err(this->Log, "info-err> ");
  86. this->RunSVNCommand(svn_info, &out, &err);
  87. return rev;
  88. }
  89. bool cmCTestSVN::NoteOldRevision()
  90. {
  91. if (!this->LoadRepositories()) {
  92. return false;
  93. }
  94. std::vector<SVNInfo>::iterator itbeg = this->Repositories.begin();
  95. std::vector<SVNInfo>::iterator itend = this->Repositories.end();
  96. for (; itbeg != itend; itbeg++) {
  97. SVNInfo& svninfo = *itbeg;
  98. svninfo.OldRevision = this->LoadInfo(svninfo);
  99. this->Log << "Revision for repository '" << svninfo.LocalPath
  100. << "' before update: " << svninfo.OldRevision << "\n";
  101. cmCTestLog(
  102. this->CTest, HANDLER_OUTPUT, " Old revision of external repository '"
  103. << svninfo.LocalPath << "' is: " << svninfo.OldRevision << "\n");
  104. }
  105. // Set the global old revision to the one of the root
  106. this->OldRevision = this->RootInfo->OldRevision;
  107. this->PriorRev.Rev = this->OldRevision;
  108. return true;
  109. }
  110. bool cmCTestSVN::NoteNewRevision()
  111. {
  112. if (!this->LoadRepositories()) {
  113. return false;
  114. }
  115. std::vector<SVNInfo>::iterator itbeg = this->Repositories.begin();
  116. std::vector<SVNInfo>::iterator itend = this->Repositories.end();
  117. for (; itbeg != itend; itbeg++) {
  118. SVNInfo& svninfo = *itbeg;
  119. svninfo.NewRevision = this->LoadInfo(svninfo);
  120. this->Log << "Revision for repository '" << svninfo.LocalPath
  121. << "' after update: " << svninfo.NewRevision << "\n";
  122. cmCTestLog(
  123. this->CTest, HANDLER_OUTPUT, " New revision of external repository '"
  124. << svninfo.LocalPath << "' is: " << svninfo.NewRevision << "\n");
  125. // svninfo.Root = ""; // uncomment to test GuessBase
  126. this->Log << "Repository '" << svninfo.LocalPath
  127. << "' URL = " << svninfo.URL << "\n";
  128. this->Log << "Repository '" << svninfo.LocalPath
  129. << "' Root = " << svninfo.Root << "\n";
  130. // Compute the base path the working tree has checked out under
  131. // the repository root.
  132. if (!svninfo.Root.empty() &&
  133. cmCTestSVNPathStarts(svninfo.URL, svninfo.Root)) {
  134. svninfo.Base =
  135. cmCTest::DecodeURL(svninfo.URL.substr(svninfo.Root.size()));
  136. svninfo.Base += "/";
  137. }
  138. this->Log << "Repository '" << svninfo.LocalPath
  139. << "' Base = " << svninfo.Base << "\n";
  140. }
  141. // Set the global new revision to the one of the root
  142. this->NewRevision = this->RootInfo->NewRevision;
  143. return true;
  144. }
  145. void cmCTestSVN::GuessBase(SVNInfo& svninfo,
  146. std::vector<Change> const& changes)
  147. {
  148. // Subversion did not give us a good repository root so we need to
  149. // guess the base path from the URL and the paths in a revision with
  150. // changes under it.
  151. // Consider each possible URL suffix from longest to shortest.
  152. for (std::string::size_type slash = svninfo.URL.find('/');
  153. svninfo.Base.empty() && slash != std::string::npos;
  154. slash = svninfo.URL.find('/', slash + 1)) {
  155. // If the URL suffix is a prefix of at least one path then it is the base.
  156. std::string base = cmCTest::DecodeURL(svninfo.URL.substr(slash));
  157. for (std::vector<Change>::const_iterator ci = changes.begin();
  158. svninfo.Base.empty() && ci != changes.end(); ++ci) {
  159. if (cmCTestSVNPathStarts(ci->Path, base)) {
  160. svninfo.Base = base;
  161. }
  162. }
  163. }
  164. // We always append a slash so that we know paths beginning in the
  165. // base lie under its path. If no base was found then the working
  166. // tree must be a checkout of the entire repo and this will match
  167. // the leading slash in all paths.
  168. svninfo.Base += "/";
  169. this->Log << "Guessed Base = " << svninfo.Base << "\n";
  170. }
  171. class cmCTestSVN::UpdateParser : public cmCTestVC::LineParser
  172. {
  173. public:
  174. UpdateParser(cmCTestSVN* svn, const char* prefix)
  175. : SVN(svn)
  176. {
  177. this->SetLog(&svn->Log, prefix);
  178. this->RegexUpdate.compile("^([ADUCGE ])([ADUCGE ])[B ] +(.+)$");
  179. }
  180. private:
  181. cmCTestSVN* SVN;
  182. cmsys::RegularExpression RegexUpdate;
  183. bool ProcessLine() CM_OVERRIDE
  184. {
  185. if (this->RegexUpdate.find(this->Line)) {
  186. this->DoPath(this->RegexUpdate.match(1)[0],
  187. this->RegexUpdate.match(2)[0], this->RegexUpdate.match(3));
  188. }
  189. return true;
  190. }
  191. void DoPath(char path_status, char prop_status, std::string const& path)
  192. {
  193. char status = (path_status != ' ') ? path_status : prop_status;
  194. std::string dir = cmSystemTools::GetFilenamePath(path);
  195. std::string name = cmSystemTools::GetFilenameName(path);
  196. // See "svn help update".
  197. switch (status) {
  198. case 'G':
  199. this->SVN->Dirs[dir][name].Status = PathModified;
  200. break;
  201. case 'C':
  202. this->SVN->Dirs[dir][name].Status = PathConflicting;
  203. break;
  204. case 'A':
  205. case 'D':
  206. case 'U':
  207. this->SVN->Dirs[dir][name].Status = PathUpdated;
  208. break;
  209. case 'E': // TODO?
  210. case '?':
  211. case ' ':
  212. default:
  213. break;
  214. }
  215. }
  216. };
  217. bool cmCTestSVN::UpdateImpl()
  218. {
  219. // Get user-specified update options.
  220. std::string opts = this->CTest->GetCTestConfiguration("UpdateOptions");
  221. if (opts.empty()) {
  222. opts = this->CTest->GetCTestConfiguration("SVNUpdateOptions");
  223. }
  224. std::vector<std::string> args = cmSystemTools::ParseArguments(opts.c_str());
  225. // Specify the start time for nightly testing.
  226. if (this->CTest->GetTestModel() == cmCTest::NIGHTLY) {
  227. args.push_back("-r{" + this->GetNightlyTime() + " +0000}");
  228. }
  229. std::vector<char const*> svn_update;
  230. svn_update.push_back("update");
  231. for (std::vector<std::string>::const_iterator ai = args.begin();
  232. ai != args.end(); ++ai) {
  233. svn_update.push_back(ai->c_str());
  234. }
  235. UpdateParser out(this, "up-out> ");
  236. OutputLogger err(this->Log, "up-err> ");
  237. return this->RunSVNCommand(svn_update, &out, &err);
  238. }
  239. bool cmCTestSVN::RunSVNCommand(std::vector<char const*> const& parameters,
  240. OutputParser* out, OutputParser* err)
  241. {
  242. if (parameters.empty()) {
  243. return false;
  244. }
  245. std::vector<char const*> args;
  246. args.push_back(this->CommandLineTool.c_str());
  247. args.insert(args.end(), parameters.begin(), parameters.end());
  248. args.push_back("--non-interactive");
  249. std::string userOptions = this->CTest->GetCTestConfiguration("SVNOptions");
  250. std::vector<std::string> parsedUserOptions =
  251. cmSystemTools::ParseArguments(userOptions.c_str());
  252. for (std::vector<std::string>::iterator i = parsedUserOptions.begin();
  253. i != parsedUserOptions.end(); ++i) {
  254. args.push_back(i->c_str());
  255. }
  256. args.push_back(nullptr);
  257. if (strcmp(parameters[0], "update") == 0) {
  258. return RunUpdateCommand(&args[0], out, err);
  259. }
  260. return RunChild(&args[0], out, err);
  261. }
  262. class cmCTestSVN::LogParser : public cmCTestVC::OutputLogger,
  263. private cmXMLParser
  264. {
  265. public:
  266. LogParser(cmCTestSVN* svn, const char* prefix, SVNInfo& svninfo)
  267. : OutputLogger(svn->Log, prefix)
  268. , SVN(svn)
  269. , SVNRepo(svninfo)
  270. {
  271. this->InitializeParser();
  272. }
  273. ~LogParser() CM_OVERRIDE { this->CleanupParser(); }
  274. private:
  275. cmCTestSVN* SVN;
  276. cmCTestSVN::SVNInfo& SVNRepo;
  277. typedef cmCTestSVN::Revision Revision;
  278. typedef cmCTestSVN::Change Change;
  279. Revision Rev;
  280. std::vector<Change> Changes;
  281. Change CurChange;
  282. std::vector<char> CData;
  283. bool ProcessChunk(const char* data, int length) CM_OVERRIDE
  284. {
  285. this->OutputLogger::ProcessChunk(data, length);
  286. this->ParseChunk(data, length);
  287. return true;
  288. }
  289. void StartElement(const std::string& name, const char** atts) CM_OVERRIDE
  290. {
  291. this->CData.clear();
  292. if (name == "logentry") {
  293. this->Rev = Revision();
  294. this->Rev.SVNInfo = &SVNRepo;
  295. if (const char* rev = this->FindAttribute(atts, "revision")) {
  296. this->Rev.Rev = rev;
  297. }
  298. this->Changes.clear();
  299. } else if (name == "path") {
  300. this->CurChange = Change();
  301. if (const char* action = this->FindAttribute(atts, "action")) {
  302. this->CurChange.Action = action[0];
  303. }
  304. }
  305. }
  306. void CharacterDataHandler(const char* data, int length) CM_OVERRIDE
  307. {
  308. this->CData.insert(this->CData.end(), data, data + length);
  309. }
  310. void EndElement(const std::string& name) CM_OVERRIDE
  311. {
  312. if (name == "logentry") {
  313. this->SVN->DoRevisionSVN(this->Rev, this->Changes);
  314. } else if (!this->CData.empty() && name == "path") {
  315. std::string orig_path(&this->CData[0], this->CData.size());
  316. std::string new_path = SVNRepo.BuildLocalPath(orig_path);
  317. this->CurChange.Path.assign(new_path);
  318. this->Changes.push_back(this->CurChange);
  319. } else if (!this->CData.empty() && name == "author") {
  320. this->Rev.Author.assign(&this->CData[0], this->CData.size());
  321. } else if (!this->CData.empty() && name == "date") {
  322. this->Rev.Date.assign(&this->CData[0], this->CData.size());
  323. } else if (!this->CData.empty() && name == "msg") {
  324. this->Rev.Log.assign(&this->CData[0], this->CData.size());
  325. }
  326. this->CData.clear();
  327. }
  328. void ReportError(int /*line*/, int /*column*/, const char* msg) CM_OVERRIDE
  329. {
  330. this->SVN->Log << "Error parsing svn log xml: " << msg << "\n";
  331. }
  332. };
  333. bool cmCTestSVN::LoadRevisions()
  334. {
  335. bool result = true;
  336. // Get revisions for all the external repositories
  337. std::vector<SVNInfo>::iterator itbeg = this->Repositories.begin();
  338. std::vector<SVNInfo>::iterator itend = this->Repositories.end();
  339. for (; itbeg != itend; itbeg++) {
  340. SVNInfo& svninfo = *itbeg;
  341. result = this->LoadRevisions(svninfo) && result;
  342. }
  343. return result;
  344. }
  345. bool cmCTestSVN::LoadRevisions(SVNInfo& svninfo)
  346. {
  347. // We are interested in every revision included in the update.
  348. std::string revs;
  349. if (atoi(svninfo.OldRevision.c_str()) < atoi(svninfo.NewRevision.c_str())) {
  350. revs = "-r" + svninfo.OldRevision + ":" + svninfo.NewRevision;
  351. } else {
  352. revs = "-r" + svninfo.NewRevision;
  353. }
  354. // Run "svn log" to get all global revisions of interest.
  355. std::vector<const char*> svn_log;
  356. svn_log.push_back("log");
  357. svn_log.push_back("--xml");
  358. svn_log.push_back("-v");
  359. svn_log.push_back(revs.c_str());
  360. svn_log.push_back(svninfo.LocalPath.c_str());
  361. LogParser out(this, "log-out> ", svninfo);
  362. OutputLogger err(this->Log, "log-err> ");
  363. return this->RunSVNCommand(svn_log, &out, &err);
  364. }
  365. void cmCTestSVN::DoRevisionSVN(Revision const& revision,
  366. std::vector<Change> const& changes)
  367. {
  368. // Guess the base checkout path from the changes if necessary.
  369. if (this->RootInfo->Base.empty() && !changes.empty()) {
  370. this->GuessBase(*this->RootInfo, changes);
  371. }
  372. // Ignore changes in the old revision for external repositories
  373. if (revision.Rev == revision.SVNInfo->OldRevision &&
  374. revision.SVNInfo->LocalPath != "") {
  375. return;
  376. }
  377. this->cmCTestGlobalVC::DoRevision(revision, changes);
  378. }
  379. class cmCTestSVN::StatusParser : public cmCTestVC::LineParser
  380. {
  381. public:
  382. StatusParser(cmCTestSVN* svn, const char* prefix)
  383. : SVN(svn)
  384. {
  385. this->SetLog(&svn->Log, prefix);
  386. this->RegexStatus.compile("^([ACDIMRX?!~ ])([CM ])[ L]... +(.+)$");
  387. }
  388. private:
  389. cmCTestSVN* SVN;
  390. cmsys::RegularExpression RegexStatus;
  391. bool ProcessLine() CM_OVERRIDE
  392. {
  393. if (this->RegexStatus.find(this->Line)) {
  394. this->DoPath(this->RegexStatus.match(1)[0],
  395. this->RegexStatus.match(2)[0], this->RegexStatus.match(3));
  396. }
  397. return true;
  398. }
  399. void DoPath(char path_status, char prop_status, std::string const& path)
  400. {
  401. char status = (path_status != ' ') ? path_status : prop_status;
  402. // See "svn help status".
  403. switch (status) {
  404. case 'M':
  405. case '!':
  406. case 'A':
  407. case 'D':
  408. case 'R':
  409. this->SVN->DoModification(PathModified, path);
  410. break;
  411. case 'C':
  412. case '~':
  413. this->SVN->DoModification(PathConflicting, path);
  414. break;
  415. case 'X':
  416. case 'I':
  417. case '?':
  418. case ' ':
  419. default:
  420. break;
  421. }
  422. }
  423. };
  424. bool cmCTestSVN::LoadModifications()
  425. {
  426. // Run "svn status" which reports local modifications.
  427. std::vector<const char*> svn_status;
  428. svn_status.push_back("status");
  429. StatusParser out(this, "status-out> ");
  430. OutputLogger err(this->Log, "status-err> ");
  431. this->RunSVNCommand(svn_status, &out, &err);
  432. return true;
  433. }
  434. void cmCTestSVN::WriteXMLGlobal(cmXMLWriter& xml)
  435. {
  436. this->cmCTestGlobalVC::WriteXMLGlobal(xml);
  437. xml.Element("SVNPath", this->RootInfo->Base);
  438. }
  439. class cmCTestSVN::ExternalParser : public cmCTestVC::LineParser
  440. {
  441. public:
  442. ExternalParser(cmCTestSVN* svn, const char* prefix)
  443. : SVN(svn)
  444. {
  445. this->SetLog(&svn->Log, prefix);
  446. this->RegexExternal.compile("^X..... +(.+)$");
  447. }
  448. private:
  449. cmCTestSVN* SVN;
  450. cmsys::RegularExpression RegexExternal;
  451. bool ProcessLine() CM_OVERRIDE
  452. {
  453. if (this->RegexExternal.find(this->Line)) {
  454. this->DoPath(this->RegexExternal.match(1));
  455. }
  456. return true;
  457. }
  458. void DoPath(std::string const& path)
  459. {
  460. // Get local path relative to the source directory
  461. std::string local_path;
  462. if (path.size() > this->SVN->SourceDirectory.size() &&
  463. strncmp(path.c_str(), this->SVN->SourceDirectory.c_str(),
  464. this->SVN->SourceDirectory.size()) == 0) {
  465. local_path = path.c_str() + this->SVN->SourceDirectory.size() + 1;
  466. } else {
  467. local_path = path;
  468. }
  469. this->SVN->Repositories.push_back(SVNInfo(local_path.c_str()));
  470. }
  471. };
  472. bool cmCTestSVN::LoadRepositories()
  473. {
  474. if (!this->Repositories.empty()) {
  475. return true;
  476. }
  477. // Info for root repository
  478. this->Repositories.push_back(SVNInfo(""));
  479. this->RootInfo = &(this->Repositories.back());
  480. // Run "svn status" to get the list of external repositories
  481. std::vector<const char*> svn_status;
  482. svn_status.push_back("status");
  483. ExternalParser out(this, "external-out> ");
  484. OutputLogger err(this->Log, "external-err> ");
  485. return this->RunSVNCommand(svn_status, &out, &err);
  486. }
  487. std::string cmCTestSVN::SVNInfo::BuildLocalPath(std::string const& path) const
  488. {
  489. std::string local_path;
  490. // Add local path prefix if not empty
  491. if (!this->LocalPath.empty()) {
  492. local_path += this->LocalPath;
  493. local_path += "/";
  494. }
  495. // Add path with base prefix removed
  496. if (path.size() > this->Base.size() &&
  497. strncmp(path.c_str(), this->Base.c_str(), this->Base.size()) == 0) {
  498. local_path += (path.c_str() + this->Base.size());
  499. } else {
  500. local_path += path;
  501. }
  502. return local_path;
  503. }