diff options
author | Anas Nashif <anas.nashif@intel.com> | 2012-10-30 15:39:57 -0700 |
---|---|---|
committer | Anas Nashif <anas.nashif@intel.com> | 2012-10-30 15:39:57 -0700 |
commit | 035c7fabc3b82cbc9a346c11abe2e9462b4c0379 (patch) | |
tree | 7e40f5a790eae329a8c5d3e59f046451767956ff /Source/CTest | |
download | cmake-035c7fabc3b82cbc9a346c11abe2e9462b4c0379.tar.gz cmake-035c7fabc3b82cbc9a346c11abe2e9462b4c0379.tar.bz2 cmake-035c7fabc3b82cbc9a346c11abe2e9462b4c0379.zip |
Imported Upstream version 2.8.9upstream/2.8.9
Diffstat (limited to 'Source/CTest')
83 files changed, 21954 insertions, 0 deletions
diff --git a/Source/CTest/cmCTestBZR.cxx b/Source/CTest/cmCTestBZR.cxx new file mode 100644 index 000000000..381c70ce5 --- /dev/null +++ b/Source/CTest/cmCTestBZR.cxx @@ -0,0 +1,523 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2009 Kitware, Inc. + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ +#include "cmCTestBZR.h" + +#include "cmCTest.h" +#include "cmSystemTools.h" +#include "cmXMLParser.h" +#include "cmXMLSafe.h" + +#include <cmsys/RegularExpression.hxx> + +#include <cm_expat.h> + +//---------------------------------------------------------------------------- +extern "C" +int cmBZRXMLParserUnknownEncodingHandler(void*, + const XML_Char *name, + XML_Encoding *info) +{ + static const int latin1[]= + { + 0x0000, 0x0001, 0x0002, 0x0003, 0x0004, 0x0005, 0x0006, 0x0007, + 0x0008, 0x0009, 0x000A, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F, + 0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x0015, 0x0016, 0x0017, + 0x0018, 0x0019, 0x001A, 0x001B, 0x001C, 0x001D, 0x001E, 0x001F, + 0x0020, 0x0021, 0x0022, 0x0023, 0x0024, 0x0025, 0x0026, 0x0027, + 0x0028, 0x0029, 0x002A, 0x002B, 0x002C, 0x002D, 0x002E, 0x002F, + 0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037, + 0x0038, 0x0039, 0x003A, 0x003B, 0x003C, 0x003D, 0x003E, 0x003F, + 0x0040, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047, + 0x0048, 0x0049, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F, + 0x0050, 0x0051, 0x0052, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, + 0x0058, 0x0059, 0x005A, 0x005B, 0x005C, 0x005D, 0x005E, 0x005F, + 0x0060, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067, + 0x0068, 0x0069, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F, + 0x0070, 0x0071, 0x0072, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, + 0x0078, 0x0079, 0x007A, 0x007B, 0x007C, 0x007D, 0x007E, 0x007F, + 0x20AC, 0x0081, 0x201A, 0x0192, 0x201E, 0x2026, 0x2020, 0x2021, + 0x02C6, 0x2030, 0x0160, 0x2039, 0x0152, 0x008D, 0x017D, 0x008F, + 0x0090, 0x2018, 0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014, + 0x02DC, 0x2122, 0x0161, 0x203A, 0x0153, 0x009D, 0x017E, 0x0178, + 0x00A0, 0x00A1, 0x00A2, 0x00A3, 0x00A4, 0x00A5, 0x00A6, 0x00A7, + 0x00A8, 0x00A9, 0x00AA, 0x00AB, 0x00AC, 0x00AD, 0x00AE, 0x00AF, + 0x00B0, 0x00B1, 0x00B2, 0x00B3, 0x00B4, 0x00B5, 0x00B6, 0x00B7, + 0x00B8, 0x00B9, 0x00BA, 0x00BB, 0x00BC, 0x00BD, 0x00BE, 0x00BF, + 0x00C0, 0x00C1, 0x00C2, 0x00C3, 0x00C4, 0x00C5, 0x00C6, 0x00C7, + 0x00C8, 0x00C9, 0x00CA, 0x00CB, 0x00CC, 0x00CD, 0x00CE, 0x00CF, + 0x00D0, 0x00D1, 0x00D2, 0x00D3, 0x00D4, 0x00D5, 0x00D6, 0x00D7, + 0x00D8, 0x00D9, 0x00DA, 0x00DB, 0x00DC, 0x00DD, 0x00DE, 0x00DF, + 0x00E0, 0x00E1, 0x00E2, 0x00E3, 0x00E4, 0x00E5, 0x00E6, 0x00E7, + 0x00E8, 0x00E9, 0x00EA, 0x00EB, 0x00EC, 0x00ED, 0x00EE, 0x00EF, + 0x00F0, 0x00F1, 0x00F2, 0x00F3, 0x00F4, 0x00F5, 0x00F6, 0x00F7, + 0x00F8, 0x00F9, 0x00FA, 0x00FB, 0x00FC, 0x00FD, 0x00FE, 0x00FF + }; + + // The BZR xml output plugin can use some encodings that are not + // recognized by expat. This will lead to an error, e.g. "Error + // parsing bzr log xml: unknown encoding", the following is a + // workaround for these unknown encodings. + if(name == std::string("ascii") || name == std::string("cp1252") || + name == std::string("ANSI_X3.4-1968")) + { + for(unsigned int i=0;i<256;++i) info->map[i] = latin1[i]; + return 1; + } + + return 0; +} + +//---------------------------------------------------------------------------- +cmCTestBZR::cmCTestBZR(cmCTest* ct, std::ostream& log): + cmCTestGlobalVC(ct, log) +{ + this->PriorRev = this->Unknown; + // Even though it is specified in the documention, with bzr 1.13 + // BZR_PROGRESS_BAR has no effect. In the future this bug might be fixed. + // Since it doesn't hurt, we specify this environment variable. + cmSystemTools::PutEnv("BZR_PROGRESS_BAR=none"); +} + +//---------------------------------------------------------------------------- +cmCTestBZR::~cmCTestBZR() +{ +} + +//---------------------------------------------------------------------------- +class cmCTestBZR::InfoParser: public cmCTestVC::LineParser +{ +public: + InfoParser(cmCTestBZR* bzr, const char* prefix): + BZR(bzr), CheckOutFound(false) + { + this->SetLog(&bzr->Log, prefix); + this->RegexCheckOut.compile("checkout of branch: *([^\t\r\n]+)$"); + this->RegexParent.compile("parent branch: *([^\t\r\n]+)$"); + } +private: + cmCTestBZR* BZR; + bool CheckOutFound; + cmsys::RegularExpression RegexCheckOut; + cmsys::RegularExpression RegexParent; + virtual bool ProcessLine() + { + if(this->RegexCheckOut.find(this->Line)) + { + this->BZR->URL = this->RegexCheckOut.match(1); + CheckOutFound = true; + } + else if(!CheckOutFound && this->RegexParent.find(this->Line)) + { + this->BZR->URL = this->RegexParent.match(1); + } + return true; + } +}; + +//---------------------------------------------------------------------------- +class cmCTestBZR::RevnoParser: public cmCTestVC::LineParser +{ +public: + RevnoParser(cmCTestBZR* bzr, const char* prefix, std::string& rev): + Rev(rev) + { + this->SetLog(&bzr->Log, prefix); + this->RegexRevno.compile("^([0-9]+)$"); + } +private: + std::string& Rev; + cmsys::RegularExpression RegexRevno; + virtual bool ProcessLine() + { + if(this->RegexRevno.find(this->Line)) + { + this->Rev = this->RegexRevno.match(1); + } + return true; + } +}; + +//---------------------------------------------------------------------------- +std::string cmCTestBZR::LoadInfo() +{ + // Run "bzr info" to get the repository info from the work tree. + const char* bzr = this->CommandLineTool.c_str(); + const char* bzr_info[] = {bzr, "info", 0}; + InfoParser iout(this, "info-out> "); + OutputLogger ierr(this->Log, "info-err> "); + this->RunChild(bzr_info, &iout, &ierr); + + // Run "bzr revno" to get the repository revision number from the work tree. + const char* bzr_revno[] = {bzr, "revno", 0}; + std::string rev; + RevnoParser rout(this, "revno-out> ", rev); + OutputLogger rerr(this->Log, "revno-err> "); + this->RunChild(bzr_revno, &rout, &rerr); + + return rev; +} + +void cmCTestBZR::NoteOldRevision() +{ + this->OldRevision = this->LoadInfo(); + this->Log << "Revision before update: " << this->OldRevision << "\n"; + cmCTestLog(this->CTest, HANDLER_OUTPUT, " Old revision of repository is: " + << this->OldRevision << "\n"); + this->PriorRev.Rev = this->OldRevision; +} + +//---------------------------------------------------------------------------- +void cmCTestBZR::NoteNewRevision() +{ + this->NewRevision = this->LoadInfo(); + this->Log << "Revision after update: " << this->NewRevision << "\n"; + cmCTestLog(this->CTest, HANDLER_OUTPUT, " New revision of repository is: " + << this->NewRevision << "\n"); + this->Log << "URL = " << this->URL << "\n"; +} + +//---------------------------------------------------------------------------- +class cmCTestBZR::LogParser: public cmCTestVC::OutputLogger, + private cmXMLParser +{ +public: + LogParser(cmCTestBZR* bzr, const char* prefix): + OutputLogger(bzr->Log, prefix), BZR(bzr), + EmailRegex("(.*) <([A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+)>") + { this->InitializeParser(); } + ~LogParser() { this->CleanupParser(); } + + virtual int InitializeParser() + { + int res = cmXMLParser::InitializeParser(); + if (res) + { + XML_SetUnknownEncodingHandler(static_cast<XML_Parser>(this->Parser), + cmBZRXMLParserUnknownEncodingHandler, 0); + } + return res; + } +private: + cmCTestBZR* BZR; + + typedef cmCTestBZR::Revision Revision; + typedef cmCTestBZR::Change Change; + Revision Rev; + std::vector<Change> Changes; + Change CurChange; + std::vector<char> CData; + + cmsys::RegularExpression EmailRegex; + + 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**) + { + this->CData.clear(); + if(strcmp(name, "log") == 0) + { + this->Rev = Revision(); + this->Changes.clear(); + } + // affected-files can contain blocks of + // modified, unknown, renamed, kind-changed, removed, conflicts, added + else if(strcmp(name, "modified") == 0 + || strcmp(name, "renamed") == 0 + || strcmp(name, "kind-changed") == 0) + { + this->CurChange = Change(); + this->CurChange.Action = 'M'; + } + else if(strcmp(name, "added") == 0) + { + this->CurChange = Change(); + this->CurChange = 'A'; + } + else if(strcmp(name, "removed") == 0) + { + this->CurChange = Change(); + this->CurChange = 'D'; + } + else if(strcmp(name, "unknown") == 0 + || strcmp(name, "conflicts") == 0) + { + // Should not happen here + this->CurChange = Change(); + } + } + + 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, "log") == 0) + { + this->BZR->DoRevision(this->Rev, this->Changes); + } + else if((strcmp(name, "file") == 0 || strcmp(name, "directory") == 0) + && !this->CData.empty()) + { + this->CurChange.Path.assign(&this->CData[0], this->CData.size()); + cmSystemTools::ConvertToUnixSlashes(this->CurChange.Path); + this->Changes.push_back(this->CurChange); + } + else if(strcmp(name, "symlink") == 0 && !this->CData.empty()) + { + // symlinks have an arobase at the end in the log + this->CurChange.Path.assign(&this->CData[0], this->CData.size()-1); + cmSystemTools::ConvertToUnixSlashes(this->CurChange.Path); + this->Changes.push_back(this->CurChange); + } + else if(strcmp(name, "committer") == 0 && !this->CData.empty()) + { + this->Rev.Author.assign(&this->CData[0], this->CData.size()); + if(this->EmailRegex.find(this->Rev.Author)) + { + this->Rev.Author = this->EmailRegex.match(1); + this->Rev.EMail = this->EmailRegex.match(2); + } + } + else if(strcmp(name, "timestamp") == 0 && !this->CData.empty()) + { + this->Rev.Date.assign(&this->CData[0], this->CData.size()); + } + else if(strcmp(name, "message") == 0 && !this->CData.empty()) + { + this->Rev.Log.assign(&this->CData[0], this->CData.size()); + } + else if(strcmp(name, "revno") == 0 && !this->CData.empty()) + { + this->Rev.Rev.assign(&this->CData[0], this->CData.size()); + } + this->CData.clear(); + } + + virtual void ReportError(int, int, const char* msg) + { + this->BZR->Log << "Error parsing bzr log xml: " << msg << "\n"; + } +}; + +//---------------------------------------------------------------------------- +class cmCTestBZR::UpdateParser: public cmCTestVC::LineParser +{ +public: + UpdateParser(cmCTestBZR* bzr, const char* prefix): BZR(bzr) + { + this->SetLog(&bzr->Log, prefix); + this->RegexUpdate.compile("^([-+R?XCP ])([NDKM ])([* ]) +(.+)$"); + } +private: + cmCTestBZR* BZR; + cmsys::RegularExpression RegexUpdate; + + virtual bool ProcessChunk(const char* first, int length) + { + bool last_is_new_line = (*first == '\r' || *first == '\n'); + + const char* const last = first + length; + for(const char* c = first; c != last; ++c) + { + if(*c == '\r' || *c == '\n') + { + if(!last_is_new_line) + { + // Log this line. + if(this->Log && this->Prefix) + { + *this->Log << this->Prefix << this->Line << "\n"; + } + + // Hand this line to the subclass implementation. + if(!this->ProcessLine()) + { + this->Line = ""; + return false; + } + + this->Line = ""; + last_is_new_line = true; + } + } + else + { + // Append this character to the line under construction. + this->Line.append(1, *c); + last_is_new_line = false; + } + } + return true; + } + + bool ProcessLine() + { + if(this->RegexUpdate.find(this->Line)) + { + this->DoPath(this->RegexUpdate.match(1)[0], + this->RegexUpdate.match(2)[0], + this->RegexUpdate.match(3)[0], + this->RegexUpdate.match(4)); + } + return true; + } + + void DoPath(char c0, char c1, char c2, std::string path) + { + if(path.empty()) return; + cmSystemTools::ConvertToUnixSlashes(path); + + const std::string dir = cmSystemTools::GetFilenamePath(path); + const std::string name = cmSystemTools::GetFilenameName(path); + + if ( c0=='C' ) + { + this->BZR->Dirs[dir][name].Status = PathConflicting; + return; + } + + if ( c1=='M' || c1=='K' || c1=='N' || c1=='D' || c2 =='*' ) + { + this->BZR->Dirs[dir][name].Status = PathUpdated; + return; + } + } +}; + +//---------------------------------------------------------------------------- +bool cmCTestBZR::UpdateImpl() +{ + // Get user-specified update options. + std::string opts = this->CTest->GetCTestConfiguration("UpdateOptions"); + if(opts.empty()) + { + opts = this->CTest->GetCTestConfiguration("BZRUpdateOptions"); + } + std::vector<cmStdString> args = cmSystemTools::ParseArguments(opts.c_str()); + + // TODO: if(this->CTest->GetTestModel() == cmCTest::NIGHTLY) + + // Use "bzr pull" to update the working tree. + std::vector<char const*> bzr_update; + bzr_update.push_back(this->CommandLineTool.c_str()); + bzr_update.push_back("pull"); + + for(std::vector<cmStdString>::const_iterator ai = args.begin(); + ai != args.end(); ++ai) + { + bzr_update.push_back(ai->c_str()); + } + + bzr_update.push_back(this->URL.c_str()); + + bzr_update.push_back(0); + + // For some reason bzr uses stderr to display the update status. + OutputLogger out(this->Log, "pull-out> "); + UpdateParser err(this, "pull-err> "); + return this->RunUpdateCommand(&bzr_update[0], &out, &err); +} + +//---------------------------------------------------------------------------- +void cmCTestBZR::LoadRevisions() +{ + cmCTestLog(this->CTest, HANDLER_OUTPUT, + " Gathering version information (one . per revision):\n" + " " << std::flush); + + // We are interested in every revision included in the update. + this->Revisions.clear(); + std::string revs; + if(atoi(this->OldRevision.c_str()) <= atoi(this->NewRevision.c_str())) + { + // DoRevision takes care of discarding the information about OldRevision + revs = this->OldRevision + ".." + this->NewRevision; + } + else + { + return; + } + + // Run "bzr log" to get all global revisions of interest. + const char* bzr = this->CommandLineTool.c_str(); + const char* bzr_log[] = {bzr, "log", "-v", "-r", revs.c_str(), "--xml", + this->URL.c_str(), 0}; + { + LogParser out(this, "log-out> "); + OutputLogger err(this->Log, "log-err> "); + this->RunChild(bzr_log, &out, &err); + } + cmCTestLog(this->CTest, HANDLER_OUTPUT, std::endl); +} + +//---------------------------------------------------------------------------- +class cmCTestBZR::StatusParser: public cmCTestVC::LineParser +{ +public: + StatusParser(cmCTestBZR* bzr, const char* prefix): BZR(bzr) + { + this->SetLog(&bzr->Log, prefix); + this->RegexStatus.compile("^([-+R?XCP ])([NDKM ])([* ]) +(.+)$"); + } +private: + cmCTestBZR* BZR; + cmsys::RegularExpression RegexStatus; + bool ProcessLine() + { + if(this->RegexStatus.find(this->Line)) + { + this->DoPath(this->RegexStatus.match(1)[0], + this->RegexStatus.match(2)[0], + this->RegexStatus.match(3)[0], + this->RegexStatus.match(4)); + } + return true; + } + + void DoPath(char c0, char c1, char c2, std::string path) + { + if(path.empty()) return; + cmSystemTools::ConvertToUnixSlashes(path); + + if ( c0=='C' ) + { + this->BZR->DoModification(PathConflicting, path); + return; + } + + if ( c0 == '+' || c0 == 'R' || c0 == 'P' + || c1=='M' || c1=='K' || c1=='N' || c1=='D' + || c2 =='*' ) + { + this->BZR->DoModification(PathModified, path); + return; + } + } +}; + +//---------------------------------------------------------------------------- +void cmCTestBZR::LoadModifications() +{ + // Run "bzr status" which reports local modifications. + const char* bzr = this->CommandLineTool.c_str(); + const char* bzr_status[] = {bzr, "status", "-SV", 0}; + StatusParser out(this, "status-out> "); + OutputLogger err(this->Log, "status-err> "); + this->RunChild(bzr_status, &out, &err); +} diff --git a/Source/CTest/cmCTestBZR.h b/Source/CTest/cmCTestBZR.h new file mode 100644 index 000000000..df688e1ee --- /dev/null +++ b/Source/CTest/cmCTestBZR.h @@ -0,0 +1,55 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2009 Kitware, Inc. + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ +#ifndef cmCTestBZR_h +#define cmCTestBZR_h + +#include "cmCTestGlobalVC.h" + +/** \class cmCTestBZR + * \brief Interaction with bzr command-line tool + * + */ +class cmCTestBZR: public cmCTestGlobalVC +{ +public: + /** Construct with a CTest instance and update log stream. */ + cmCTestBZR(cmCTest* ctest, std::ostream& log); + + virtual ~cmCTestBZR(); + +private: + // Implement cmCTestVC internal API. + virtual void NoteOldRevision(); + virtual void NoteNewRevision(); + virtual bool UpdateImpl(); + + // URL of repository directory checked out in the working tree. + std::string URL; + + std::string LoadInfo(); + void LoadModifications(); + void LoadRevisions(); + + // Parsing helper classes. + class InfoParser; + class RevnoParser; + class LogParser; + class UpdateParser; + class StatusParser; + friend class InfoParser; + friend class RevnoParser; + friend class LogParser; + friend class UpdateParser; + friend class StatusParser; +}; + +#endif diff --git a/Source/CTest/cmCTestBatchTestHandler.cxx b/Source/CTest/cmCTestBatchTestHandler.cxx new file mode 100644 index 000000000..00bb6fa70 --- /dev/null +++ b/Source/CTest/cmCTestBatchTestHandler.cxx @@ -0,0 +1,143 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2009 Kitware, Inc., Insight Software Consortium + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ + +#include "cmCTestBatchTestHandler.h" +#include "cmProcess.h" +#include "cmStandardIncludes.h" +#include "cmCTest.h" +#include "cmSystemTools.h" +#include <stdlib.h> + +cmCTestBatchTestHandler::~cmCTestBatchTestHandler() +{ +} + +//--------------------------------------------------------- +void cmCTestBatchTestHandler::RunTests() +{ + this->WriteBatchScript(); + this->SubmitBatchScript(); +} + +//--------------------------------------------------------- +void cmCTestBatchTestHandler::WriteBatchScript() +{ + this->Script = this->CTest->GetBinaryDir() + + "/Testing/CTestBatch.txt"; + std::fstream fout; + fout.open(this->Script.c_str(), std::ios::out); + fout << "#!/bin/sh\n"; + + for(TestMap::iterator i = this->Tests.begin(); i != this->Tests.end(); ++i) + { + this->WriteSrunArgs(i->first, fout); + this->WriteTestCommand(i->first, fout); + fout << "\n"; + } + fout.flush(); + fout.close(); +} + +//--------------------------------------------------------- +void cmCTestBatchTestHandler::WriteSrunArgs(int test, std::fstream& fout) +{ + cmCTestTestHandler::cmCTestTestProperties* properties = + this->Properties[test]; + + fout << "srun "; + //fout << "--jobid=" << test << " "; + fout << "-J=" << properties->Name << " "; + + //Write dependency information + /*if(this->Tests[test].size() > 0) + { + fout << "-P=afterany"; + for(TestSet::iterator i = this->Tests[test].begin(); + i != this->Tests[test].end(); ++i) + { + fout << ":" << *i; + } + fout << " "; + }*/ + if(properties->RunSerial) + { + fout << "--exclusive "; + } + if(properties->Processors > 1) + { + fout << "-n" << properties->Processors << " "; + } +} + +//--------------------------------------------------------- +void cmCTestBatchTestHandler::WriteTestCommand(int test, std::fstream& fout) +{ + std::vector<std::string> args = this->Properties[test]->Args; + std::vector<std::string> processArgs; + std::string command; + + command = this->TestHandler->FindTheExecutable(args[1].c_str()); + command = cmSystemTools::ConvertToOutputPath(command.c_str()); + + //Prepends memcheck args to our command string if this is a memcheck + this->TestHandler->GenerateTestCommand(processArgs); + processArgs.push_back(command); + + for(std::vector<std::string>::iterator arg = processArgs.begin(); + arg != processArgs.end(); ++arg) + { + fout << *arg << " "; + } + + std::vector<std::string>::iterator i = args.begin(); + ++i; //the test name + ++i; //the executable (command) + if(args.size() > 2) + { + fout << "'"; + } + while(i != args.end()) + { + fout << "\"" << *i << "\""; //args to the test executable + ++i; + + if(i == args.end() && args.size() > 2) + { + fout << "'"; + } + fout << " "; + } + //TODO ZACH build TestResult.FullCommandLine + //this->TestResult.FullCommandLine = this->TestCommand; +} + +//--------------------------------------------------------- +void cmCTestBatchTestHandler::SubmitBatchScript() +{ + cmProcess sbatch; + std::vector<std::string> args; + args.push_back(this->Script); + args.push_back("-o"); + args.push_back(this->CTest->GetBinaryDir() + + "/Testing/CTestBatch.txt"); + + sbatch.SetCommand("sbatch"); + sbatch.SetCommandArguments(args); + /*if(sbatch.StartProcess()) + { + //success condition + } + else + { + //fail condition + }*/ +} diff --git a/Source/CTest/cmCTestBatchTestHandler.h b/Source/CTest/cmCTestBatchTestHandler.h new file mode 100644 index 000000000..fdfd1be29 --- /dev/null +++ b/Source/CTest/cmCTestBatchTestHandler.h @@ -0,0 +1,41 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2009 Kitware, Inc., Insight Software Consortium + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ + +#ifndef cmCTestBatchTestHandler_h +#define cmCTestBatchTestHandler_h + +#include <cmStandardIncludes.h> +#include <cmCTestTestHandler.h> +#include <cmCTestMultiProcessHandler.h> +#include <cmCTestRunTest.h> + +/** \class cmCTestBatchTestHandler + * \brief run parallel ctest + * + * cmCTestBatchTestHandler + */ +class cmCTestBatchTestHandler : public cmCTestMultiProcessHandler +{ +public: + ~cmCTestBatchTestHandler(); + virtual void RunTests(); +protected: + void WriteBatchScript(); + void WriteSrunArgs(int test, std::fstream& fout); + void WriteTestCommand(int test, std::fstream& fout); + + void SubmitBatchScript(); + + std::string Script; +}; + +#endif diff --git a/Source/CTest/cmCTestBuildAndTestHandler.cxx b/Source/CTest/cmCTestBuildAndTestHandler.cxx new file mode 100644 index 000000000..8a2d65a7b --- /dev/null +++ b/Source/CTest/cmCTestBuildAndTestHandler.cxx @@ -0,0 +1,526 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2009 Kitware, Inc., Insight Software Consortium + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ + +#include "cmCTestBuildAndTestHandler.h" + +#include "cmSystemTools.h" +#include "cmCTest.h" +#include "cmake.h" +#include "cmGlobalGenerator.h" +#include <cmsys/Process.h> +#include "cmCTestTestHandler.h" + +//---------------------------------------------------------------------- +cmCTestBuildAndTestHandler::cmCTestBuildAndTestHandler() +{ + this->BuildTwoConfig = false; + this->BuildNoClean = false; + this->BuildNoCMake = false; + this->Timeout = 0; +} + +//---------------------------------------------------------------------- +void cmCTestBuildAndTestHandler::Initialize() +{ + this->BuildTargets.erase( + this->BuildTargets.begin(), this->BuildTargets.end()); + this->Superclass::Initialize(); +} + +//---------------------------------------------------------------------- +const char* cmCTestBuildAndTestHandler::GetOutput() +{ + return this->Output.c_str(); +} +//---------------------------------------------------------------------- +int cmCTestBuildAndTestHandler::ProcessHandler() +{ + this->Output = ""; + std::string output; + cmSystemTools::ResetErrorOccuredFlag(); + int retv = this->RunCMakeAndTest(&this->Output); + cmSystemTools::ResetErrorOccuredFlag(); + return retv; +} + +//---------------------------------------------------------------------- +int cmCTestBuildAndTestHandler::RunCMake(std::string* outstring, + cmOStringStream &out, std::string &cmakeOutString, std::string &cwd, + cmake *cm) +{ + unsigned int k; + std::vector<std::string> args; + args.push_back(this->CTest->GetCMakeExecutable()); + args.push_back(this->SourceDir); + if(this->BuildGenerator.size()) + { + std::string generator = "-G"; + generator += this->BuildGenerator; + args.push_back(generator); + } + + const char* config = 0; + if ( this->CTest->GetConfigType().size() > 0 ) + { + config = this->CTest->GetConfigType().c_str(); + } +#ifdef CMAKE_INTDIR + if(!config) + { + config = CMAKE_INTDIR; + } +#endif + + if ( config ) + { + std::string btype + = "-DCMAKE_BUILD_TYPE:STRING=" + std::string(config); + args.push_back(btype); + } + + for(k=0; k < this->BuildOptions.size(); ++k) + { + args.push_back(this->BuildOptions[k]); + } + if (cm->Run(args) != 0) + { + out << "Error: cmake execution failed\n"; + out << cmakeOutString << "\n"; + // return to the original directory + cmSystemTools::ChangeDirectory(cwd.c_str()); + if(outstring) + { + *outstring = out.str(); + } + else + { + cmCTestLog(this->CTest, ERROR_MESSAGE, out.str() << std::endl); + } + return 1; + } + // do another config? + if(this->BuildTwoConfig) + { + if (cm->Run(args) != 0) + { + out << "Error: cmake execution failed\n"; + out << cmakeOutString << "\n"; + // return to the original directory + cmSystemTools::ChangeDirectory(cwd.c_str()); + if(outstring) + { + *outstring = out.str(); + } + else + { + cmCTestLog(this->CTest, ERROR_MESSAGE, out.str() << std::endl); + } + return 1; + } + } + out << "======== CMake output ======\n"; + out << cmakeOutString; + out << "======== End CMake output ======\n"; + return 0; +} + +//---------------------------------------------------------------------- +void CMakeMessageCallback(const char* m, const char*, bool&, void* s) +{ + std::string* out = (std::string*)s; + *out += m; + *out += "\n"; +} + +void CMakeProgressCallback(const char*msg, float , void * s) +{ + std::string* out = (std::string*)s; + *out += msg; + *out += "\n"; +} + +//---------------------------------------------------------------------- +void CMakeStdoutCallback(const char* m, int len, void* s) +{ + std::string* out = (std::string*)s; + out->append(m, len); +} +struct cmSetupOutputCaptureCleanup +{ + ~cmSetupOutputCaptureCleanup() + { + cmSystemTools::SetErrorCallback(0, 0); + cmSystemTools::SetStdoutCallback(0, 0); + } +}; + +//---------------------------------------------------------------------- +int cmCTestBuildAndTestHandler::RunCMakeAndTest(std::string* outstring) +{ + unsigned int k; + std::string cmakeOutString; + cmSystemTools::SetErrorCallback(CMakeMessageCallback, &cmakeOutString); + cmSystemTools::SetStdoutCallback(CMakeStdoutCallback, &cmakeOutString); + // make sure SetStdoutCallback and SetErrorCallback are set to null + // after this function exits so that they do not point at a destroyed + // string cmakeOutString + cmSetupOutputCaptureCleanup cleanup; + static_cast<void>(cleanup); + cmOStringStream out; + + // if the generator and make program are not specified then it is an error + if (!this->BuildGenerator.size() || !this->BuildMakeProgram.size()) + { + if(outstring) + { + *outstring = + "--build-and-test requires that both the generator and makeprogram " + "be provided using the --build-generator and --build-makeprogram " + "command line options. "; + } + return 1; + } + + if ( this->CTest->GetConfigType().size() == 0 && + this->ConfigSample.size()) + { + // use the config sample to set the ConfigType + std::string fullPath; + std::string resultingConfig; + std::vector<std::string> extraPaths; + std::vector<std::string> failed; + fullPath = + cmCTestTestHandler::FindExecutable(this->CTest, + this->ConfigSample.c_str(), + resultingConfig, + extraPaths, + failed); + if (fullPath.size() && resultingConfig.size()) + { + this->CTest->SetConfigType(resultingConfig.c_str()); + } + out << "Using config sample with results: " + << fullPath << " and " << resultingConfig << std::endl; + } + + // we need to honor the timeout specified, the timeout include cmake, build + // and test time + double clock_start = cmSystemTools::GetTime(); + + // make sure the binary dir is there + std::string cwd = cmSystemTools::GetCurrentWorkingDirectory(); + out << "Internal cmake changing into directory: " + << this->BinaryDir << std::endl; + if (!cmSystemTools::FileIsDirectory(this->BinaryDir.c_str())) + { + cmSystemTools::MakeDirectory(this->BinaryDir.c_str()); + } + cmSystemTools::ChangeDirectory(this->BinaryDir.c_str()); + + // should we cmake? + cmake cm; + cm.SetProgressCallback(CMakeProgressCallback, &cmakeOutString); + cm.SetGlobalGenerator(cm.CreateGlobalGenerator( + this->BuildGenerator.c_str())); + + if(!this->BuildNoCMake) + { + // do the cmake step, no timeout here since it is not a sub process + if (this->RunCMake(outstring,out,cmakeOutString,cwd,&cm)) + { + return 1; + } + } + + // do the build + std::vector<std::string>::iterator tarIt; + if ( this->BuildTargets.size() == 0 ) + { + this->BuildTargets.push_back(""); + } + for ( tarIt = this->BuildTargets.begin(); + tarIt != this->BuildTargets.end(); ++ tarIt ) + { + double remainingTime = 0; + if (this->Timeout > 0) + { + remainingTime = this->Timeout - cmSystemTools::GetTime() + clock_start; + if (remainingTime <= 0) + { + if(outstring) + { + *outstring = "--build-and-test timeout exceeded. "; + } + return 1; + } + } + std::string output; + const char* config = 0; + if ( this->CTest->GetConfigType().size() > 0 ) + { + config = this->CTest->GetConfigType().c_str(); + } +#ifdef CMAKE_INTDIR + if(!config) + { + config = CMAKE_INTDIR; + } +#endif + if(!config) + { + config = "Debug"; + } + int retVal = cm.GetGlobalGenerator()->Build( + this->SourceDir.c_str(), this->BinaryDir.c_str(), + this->BuildProject.c_str(), tarIt->c_str(), + &output, this->BuildMakeProgram.c_str(), + config, + !this->BuildNoClean, + false, remainingTime); + out << output; + // if the build failed then return + if (retVal) + { + if(outstring) + { + *outstring = out.str(); + } + return 1; + } + } + if(outstring) + { + *outstring = out.str(); + } + + // if no test was specified then we are done + if (!this->TestCommand.size()) + { + return 0; + } + + // now run the compiled test if we can find it + // store the final location in fullPath + std::string fullPath; + std::string resultingConfig; + std::vector<std::string> extraPaths; + // if this->ExecutableDirectory is set try that as well + if (this->ExecutableDirectory.size()) + { + std::string tempPath = this->ExecutableDirectory; + tempPath += "/"; + tempPath += this->TestCommand; + extraPaths.push_back(tempPath); + } + std::vector<std::string> failed; + fullPath = + cmCTestTestHandler::FindExecutable(this->CTest, + this->TestCommand.c_str(), + resultingConfig, + extraPaths, + failed); + + if(!cmSystemTools::FileExists(fullPath.c_str())) + { + out << "Could not find path to executable, perhaps it was not built: " + << this->TestCommand << "\n"; + out << "tried to find it in these places:\n"; + out << fullPath.c_str() << "\n"; + for(unsigned int i=0; i < failed.size(); ++i) + { + out << failed[i] << "\n"; + } + if(outstring) + { + *outstring = out.str(); + } + else + { + cmCTestLog(this->CTest, ERROR_MESSAGE, out.str()); + } + // return to the original directory + cmSystemTools::ChangeDirectory(cwd.c_str()); + return 1; + } + + std::vector<const char*> testCommand; + testCommand.push_back(fullPath.c_str()); + for(k=0; k < this->TestCommandArgs.size(); ++k) + { + testCommand.push_back(this->TestCommandArgs[k].c_str()); + } + testCommand.push_back(0); + std::string outs; + int retval = 0; + // run the test from the this->BuildRunDir if set + if(this->BuildRunDir.size()) + { + out << "Run test in directory: " << this->BuildRunDir << "\n"; + cmSystemTools::ChangeDirectory(this->BuildRunDir.c_str()); + } + out << "Running test command: \"" << fullPath << "\""; + for(k=0; k < this->TestCommandArgs.size(); ++k) + { + out << " \"" << this->TestCommandArgs[k] << "\""; + } + out << "\n"; + + // how much time is remaining + double remainingTime = 0; + if (this->Timeout > 0) + { + remainingTime = this->Timeout - cmSystemTools::GetTime() + clock_start; + if (remainingTime <= 0) + { + if(outstring) + { + *outstring = "--build-and-test timeout exceeded. "; + } + return 1; + } + } + + int runTestRes = this->CTest->RunTest(testCommand, &outs, &retval, 0, + remainingTime, 0); + + if(runTestRes != cmsysProcess_State_Exited || retval != 0) + { + out << "Test command failed: " << testCommand[0] << "\n"; + retval = 1; + } + + out << outs << "\n"; + if(outstring) + { + *outstring = out.str(); + } + else + { + cmCTestLog(this->CTest, OUTPUT, out.str() << std::endl); + } + return retval; +} + +//---------------------------------------------------------------------- +int cmCTestBuildAndTestHandler::ProcessCommandLineArguments( + const std::string& currentArg, size_t& idx, + const std::vector<std::string>& allArgs) +{ + // --build-and-test options + if(currentArg.find("--build-and-test",0) == 0 && idx < allArgs.size() - 1) + { + if(idx+2 < allArgs.size()) + { + idx++; + this->SourceDir = allArgs[idx]; + idx++; + this->BinaryDir = allArgs[idx]; + // dir must exist before CollapseFullPath is called + cmSystemTools::MakeDirectory(this->BinaryDir.c_str()); + this->BinaryDir + = cmSystemTools::CollapseFullPath(this->BinaryDir.c_str()); + this->SourceDir + = cmSystemTools::CollapseFullPath(this->SourceDir.c_str()); + } + else + { + cmCTestLog(this->CTest, ERROR_MESSAGE, + "--build-and-test must have source and binary dir" << std::endl); + return 0; + } + } + if(currentArg.find("--build-target",0) == 0 && idx < allArgs.size() - 1) + { + idx++; + this->BuildTargets.push_back(allArgs[idx]); + } + if(currentArg.find("--build-nocmake",0) == 0) + { + this->BuildNoCMake = true; + } + if(currentArg.find("--build-run-dir",0) == 0 && idx < allArgs.size() - 1) + { + idx++; + this->BuildRunDir = allArgs[idx]; + } + if(currentArg.find("--build-two-config",0) == 0) + { + this->BuildTwoConfig = true; + } + if(currentArg.find("--build-exe-dir",0) == 0 && idx < allArgs.size() - 1) + { + idx++; + this->ExecutableDirectory = allArgs[idx]; + } + if(currentArg.find("--test-timeout",0) == 0 && idx < allArgs.size() - 1) + { + idx++; + this->Timeout = atof(allArgs[idx].c_str()); + } + if(currentArg.find("--build-generator",0) == 0 && idx < allArgs.size() - 1) + { + idx++; + this->BuildGenerator = allArgs[idx]; + } + if(currentArg.find("--build-project",0) == 0 && idx < allArgs.size() - 1) + { + idx++; + this->BuildProject = allArgs[idx]; + } + if(currentArg.find("--build-makeprogram",0) == 0 && + idx < allArgs.size() - 1) + { + idx++; + this->BuildMakeProgram = allArgs[idx]; + } + if(currentArg.find("--build-config-sample",0) == 0 && + idx < allArgs.size() - 1) + { + idx++; + this->ConfigSample = allArgs[idx]; + } + if(currentArg.find("--build-noclean",0) == 0) + { + this->BuildNoClean = true; + } + if(currentArg.find("--build-options",0) == 0 && idx < allArgs.size() - 1) + { + ++idx; + bool done = false; + while(idx < allArgs.size() && !done) + { + this->BuildOptions.push_back(allArgs[idx]); + if(idx+1 < allArgs.size() + && (allArgs[idx+1] == "--build-target" || + allArgs[idx+1] == "--test-command")) + { + done = true; + } + else + { + ++idx; + } + } + } + if(currentArg.find("--test-command",0) == 0 && idx < allArgs.size() - 1) + { + ++idx; + this->TestCommand = allArgs[idx]; + while(idx+1 < allArgs.size()) + { + ++idx; + this->TestCommandArgs.push_back(allArgs[idx]); + } + } + return 1; +} + diff --git a/Source/CTest/cmCTestBuildAndTestHandler.h b/Source/CTest/cmCTestBuildAndTestHandler.h new file mode 100644 index 000000000..6e5f7a6ab --- /dev/null +++ b/Source/CTest/cmCTestBuildAndTestHandler.h @@ -0,0 +1,78 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2009 Kitware, Inc. + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ + +#ifndef cmCTestBuildAndTestHandler_h +#define cmCTestBuildAndTestHandler_h + + +#include "cmCTestGenericHandler.h" +#include "cmListFileCache.h" + +class cmake; + +/** \class cmCTestBuildAndTestHandler + * \brief A class that handles ctest -S invocations + * + */ +class cmCTestBuildAndTestHandler : public cmCTestGenericHandler +{ +public: + cmTypeMacro(cmCTestBuildAndTestHandler, cmCTestGenericHandler); + + /* + * The main entry point for this class + */ + int ProcessHandler(); + + //! Set all the build and test arguments + virtual int ProcessCommandLineArguments( + const std::string& currentArg, size_t& idx, + const std::vector<std::string>& allArgs); + + /* + * Get the output variable + */ + const char* GetOutput(); + + cmCTestBuildAndTestHandler(); + + virtual void Initialize(); + +protected: + ///! Run CMake and build a test and then run it as a single test. + int RunCMakeAndTest(std::string* output); + int RunCMake(std::string* outstring, cmOStringStream &out, + std::string &cmakeOutString, + std::string &cwd, cmake *cm); + + cmStdString Output; + + std::string BuildGenerator; + std::vector<std::string> BuildOptions; + bool BuildTwoConfig; + std::string BuildMakeProgram; + std::string ConfigSample; + std::string SourceDir; + std::string BinaryDir; + std::string BuildProject; + std::string TestCommand; + bool BuildNoClean; + std::string BuildRunDir; + std::string ExecutableDirectory; + std::vector<std::string> TestCommandArgs; + std::vector<std::string> BuildTargets; + bool BuildNoCMake; + double Timeout; +}; + +#endif + diff --git a/Source/CTest/cmCTestBuildCommand.cxx b/Source/CTest/cmCTestBuildCommand.cxx new file mode 100644 index 000000000..b984e850f --- /dev/null +++ b/Source/CTest/cmCTestBuildCommand.cxx @@ -0,0 +1,193 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2009 Kitware, Inc., Insight Software Consortium + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ +#include "cmCTestBuildCommand.h" + +#include "cmCTest.h" +#include "cmCTestGenericHandler.h" +#include "cmCTestBuildHandler.h" +#include "cmake.h" +#include "cmGlobalGenerator.h" + + +//---------------------------------------------------------------------------- +cmCTestBuildCommand::cmCTestBuildCommand() +{ + this->GlobalGenerator = 0; + this->Arguments[ctb_NUMBER_ERRORS] = "NUMBER_ERRORS"; + this->Arguments[ctb_NUMBER_WARNINGS] = "NUMBER_WARNINGS"; + this->Arguments[ctb_TARGET] = "TARGET"; + this->Arguments[ctb_CONFIGURATION] = "CONFIGURATION"; + this->Arguments[ctb_FLAGS] = "FLAGS"; + this->Arguments[ctb_PROJECT_NAME] = "PROJECT_NAME"; + this->Arguments[ctb_LAST] = 0; + this->Last = ctb_LAST; +} + +//---------------------------------------------------------------------------- +cmCTestBuildCommand::~cmCTestBuildCommand() +{ + if ( this->GlobalGenerator ) + { + delete this->GlobalGenerator; + this->GlobalGenerator = 0; + } +} + +//---------------------------------------------------------------------------- +cmCTestGenericHandler* cmCTestBuildCommand::InitializeHandler() +{ + cmCTestGenericHandler* handler + = this->CTest->GetInitializedHandler("build"); + if ( !handler ) + { + this->SetError("internal CTest error. Cannot instantiate build handler"); + return 0; + } + this->Handler = (cmCTestBuildHandler*)handler; + + const char* ctestBuildCommand + = this->Makefile->GetDefinition("CTEST_BUILD_COMMAND"); + if ( ctestBuildCommand && *ctestBuildCommand ) + { + this->CTest->SetCTestConfiguration("MakeCommand", ctestBuildCommand); + } + else + { + const char* cmakeGeneratorName + = this->Makefile->GetDefinition("CTEST_CMAKE_GENERATOR"); + const char* cmakeProjectName + = (this->Values[ctb_PROJECT_NAME] && *this->Values[ctb_PROJECT_NAME]) + ? this->Values[ctb_PROJECT_NAME] + : this->Makefile->GetDefinition("CTEST_PROJECT_NAME"); + + // Build configuration is determined by: CONFIGURATION argument, + // or CTEST_BUILD_CONFIGURATION script variable, or + // CTEST_CONFIGURATION_TYPE script variable, or ctest -C command + // line argument... in that order. + // + const char* ctestBuildConfiguration + = this->Makefile->GetDefinition("CTEST_BUILD_CONFIGURATION"); + const char* cmakeBuildConfiguration + = (this->Values[ctb_CONFIGURATION] && *this->Values[ctb_CONFIGURATION]) + ? this->Values[ctb_CONFIGURATION] + : ((ctestBuildConfiguration && *ctestBuildConfiguration) + ? ctestBuildConfiguration + : this->CTest->GetConfigType().c_str()); + + const char* cmakeBuildAdditionalFlags + = (this->Values[ctb_FLAGS] && *this->Values[ctb_FLAGS]) + ? this->Values[ctb_FLAGS] + : this->Makefile->GetDefinition("CTEST_BUILD_FLAGS"); + const char* cmakeBuildTarget + = (this->Values[ctb_TARGET] && *this->Values[ctb_TARGET]) + ? this->Values[ctb_TARGET] + : this->Makefile->GetDefinition("CTEST_BUILD_TARGET"); + + if ( cmakeGeneratorName && *cmakeGeneratorName && + cmakeProjectName && *cmakeProjectName ) + { + if ( !cmakeBuildConfiguration ) + { + cmakeBuildConfiguration = "Release"; + } + if ( this->GlobalGenerator ) + { + if ( strcmp(this->GlobalGenerator->GetName(), + cmakeGeneratorName) != 0 ) + { + delete this->GlobalGenerator; + this->GlobalGenerator = 0; + } + } + if ( !this->GlobalGenerator ) + { + this->GlobalGenerator = + this->Makefile->GetCMakeInstance()->CreateGlobalGenerator( + cmakeGeneratorName); + } + this->GlobalGenerator->FindMakeProgram(this->Makefile); + const char* cmakeMakeProgram + = this->Makefile->GetDefinition("CMAKE_MAKE_PROGRAM"); + if(strlen(cmakeBuildConfiguration) == 0) + { + const char* config = 0; +#ifdef CMAKE_INTDIR + config = CMAKE_INTDIR; +#endif + if(!config) + { + config = "Debug"; + } + cmakeBuildConfiguration = config; + } + + std::string buildCommand + = this->GlobalGenerator-> + GenerateBuildCommand(cmakeMakeProgram, + cmakeProjectName, + cmakeBuildAdditionalFlags, cmakeBuildTarget, + cmakeBuildConfiguration, true, false); + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, + "SetMakeCommand:" + << buildCommand.c_str() << "\n"); + this->CTest->SetCTestConfiguration("MakeCommand", buildCommand.c_str()); + } + else + { + cmOStringStream ostr; + ostr << "has no project to build. If this is a " + "\"built with CMake\" project, verify that CTEST_CMAKE_GENERATOR " + "and CTEST_PROJECT_NAME are set." + "\n" + "CTEST_PROJECT_NAME is usually set in CTestConfig.cmake. Verify " + "that CTestConfig.cmake exists, or CTEST_PROJECT_NAME " + "is set in the script, or PROJECT_NAME is passed as an argument " + "to ctest_build." + "\n" + "Alternatively, set CTEST_BUILD_COMMAND to build the project " + "with a custom command line."; + this->SetError(ostr.str().c_str()); + return 0; + } + } + + if(const char* useLaunchers = + this->Makefile->GetDefinition("CTEST_USE_LAUNCHERS")) + { + this->CTest->SetCTestConfiguration("UseLaunchers", useLaunchers); + } + + return handler; +} + + +bool cmCTestBuildCommand::InitialPass(std::vector<std::string> const& args, + cmExecutionStatus &status) +{ + bool ret = cmCTestHandlerCommand::InitialPass(args, status); + if ( this->Values[ctb_NUMBER_ERRORS] && *this->Values[ctb_NUMBER_ERRORS]) + { + cmOStringStream str; + str << this->Handler->GetTotalErrors(); + this->Makefile->AddDefinition( + this->Values[ctb_NUMBER_ERRORS], str.str().c_str()); + } + if ( this->Values[ctb_NUMBER_WARNINGS] + && *this->Values[ctb_NUMBER_WARNINGS]) + { + cmOStringStream str; + str << this->Handler->GetTotalWarnings(); + this->Makefile->AddDefinition( + this->Values[ctb_NUMBER_WARNINGS], str.str().c_str()); + } + return ret; +} diff --git a/Source/CTest/cmCTestBuildCommand.h b/Source/CTest/cmCTestBuildCommand.h new file mode 100644 index 000000000..cabc39b32 --- /dev/null +++ b/Source/CTest/cmCTestBuildCommand.h @@ -0,0 +1,98 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2009 Kitware, Inc., Insight Software Consortium + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ +#ifndef cmCTestBuildCommand_h +#define cmCTestBuildCommand_h + +#include "cmCTestHandlerCommand.h" + +class cmGlobalGenerator; +class cmCTestBuildHandler; + +/** \class cmCTestBuild + * \brief Run a ctest script + * + * cmCTestBuildCommand defineds the command to build the project. + */ +class cmCTestBuildCommand : public cmCTestHandlerCommand +{ +public: + + cmCTestBuildCommand(); + ~cmCTestBuildCommand(); + + /** + * This is a virtual constructor for the command. + */ + virtual cmCommand* Clone() + { + cmCTestBuildCommand* ni = new cmCTestBuildCommand; + ni->CTest = this->CTest; + ni->CTestScriptHandler = this->CTestScriptHandler; + return ni; + } + + /** + * The name of the command as specified in CMakeList.txt. + */ + virtual const char* GetName() const { return "ctest_build";} + + /** + * Succinct documentation. + */ + virtual const char* GetTerseDocumentation() const + { + return "Build the project."; + } + virtual bool InitialPass(std::vector<std::string> const& args, + cmExecutionStatus &status); + /** + * More documentation. + */ + virtual const char* GetFullDocumentation() const + { + return + " ctest_build([BUILD build_dir] [TARGET target] [RETURN_VALUE res]\n" + " [APPEND][NUMBER_ERRORS val] [NUMBER_WARNINGS val])\n" + "Builds the given build directory and stores results in Build.xml. " + "If no BUILD is given, the CTEST_BINARY_DIRECTORY variable is used.\n" + "The TARGET variable can be used to specify a build target. If none " + "is specified, the \"all\" target will be built.\n" + "The RETURN_VALUE option specifies a variable in which to store the " + "return value of the native build tool. " + "The NUMBER_ERRORS and NUMBER_WARNINGS options specify variables in " + "which to store the number of build errors and warnings detected." + "\n" + CTEST_COMMAND_APPEND_OPTION_DOCS; + } + + cmTypeMacro(cmCTestBuildCommand, cmCTestHandlerCommand); + + cmGlobalGenerator* GlobalGenerator; + +protected: + cmCTestBuildHandler* Handler; + enum { + ctb_BUILD = ct_LAST, + ctb_NUMBER_ERRORS, + ctb_NUMBER_WARNINGS, + ctb_TARGET, + ctb_CONFIGURATION, + ctb_FLAGS, + ctb_PROJECT_NAME, + ctb_LAST + }; + + cmCTestGenericHandler* InitializeHandler(); +}; + + +#endif diff --git a/Source/CTest/cmCTestBuildHandler.cxx b/Source/CTest/cmCTestBuildHandler.cxx new file mode 100644 index 000000000..27bb06c13 --- /dev/null +++ b/Source/CTest/cmCTestBuildHandler.cxx @@ -0,0 +1,1310 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2009 Kitware, Inc., Insight Software Consortium + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ + +#include "cmCTestBuildHandler.h" + +#include "cmCTest.h" +#include "cmake.h" +#include "cmMakefile.h" +#include "cmLocalGenerator.h" +#include "cmGlobalGenerator.h" +#include "cmGeneratedFileStream.h" +#include "cmXMLSafe.h" +#include "cmFileTimeComparison.h" + +//#include <cmsys/RegularExpression.hxx> +#include <cmsys/Process.h> +#include <cmsys/Directory.hxx> + +// used for sleep +#ifdef _WIN32 +#include "windows.h" +#endif + +#include <stdlib.h> +#include <time.h> +#include <math.h> +#include <float.h> + +#if defined(__BORLANDC__) +# pragma warn -8060 /* possibly incorrect assignment */ +#endif + +static const char* cmCTestErrorMatches[] = { + "^[Bb]us [Ee]rror", + "^[Ss]egmentation [Vv]iolation", + "^[Ss]egmentation [Ff]ault", + ":.*[Pp]ermission [Dd]enied", + "([^ :]+):([0-9]+): ([^ \\t])", + "([^:]+): error[ \\t]*[0-9]+[ \\t]*:", + "^Error ([0-9]+):", + "^Fatal", + "^Error: ", + "^Error ", + "[0-9] ERROR: ", + "^\"[^\"]+\", line [0-9]+: [^Ww]", + "^cc[^C]*CC: ERROR File = ([^,]+), Line = ([0-9]+)", + "^ld([^:])*:([ \\t])*ERROR([^:])*:", + "^ild:([ \\t])*\\(undefined symbol\\)", + "([^ :]+) : (error|fatal error|catastrophic error)", + "([^:]+): (Error:|error|undefined reference|multiply defined)", + "([^:]+)\\(([^\\)]+)\\) ?: (error|fatal error|catastrophic error)", + "^fatal error C[0-9]+:", + ": syntax error ", + "^collect2: ld returned 1 exit status", + "ld terminated with signal", + "Unsatisfied symbol", + "^Unresolved:", + "Undefined symbol", + "^Undefined[ \\t]+first referenced", + "^CMake Error.*:", + ":[ \\t]cannot find", + ":[ \\t]can't find", + ": \\*\\*\\* No rule to make target \\`.*\\'. Stop", + ": \\*\\*\\* No targets specified and no makefile found", + ": Invalid loader fixup for symbol", + ": Invalid fixups exist", + ": Can't find library for", + ": internal link edit command failed", + ": Unrecognized option \\`.*\\'", + "\", line [0-9]+\\.[0-9]+: [0-9]+-[0-9]+ \\([^WI]\\)", + "ld: 0706-006 Cannot find or open library file: -l ", + "ild: \\(argument error\\) can't find library argument ::", + "^could not be found and will not be loaded.", + "s:616 string too big", + "make: Fatal error: ", + "ld: 0711-993 Error occurred while writing to the output file:", + "ld: fatal: ", + "final link failed:", + "make: \\*\\*\\*.*Error", + "make\\[.*\\]: \\*\\*\\*.*Error", + "\\*\\*\\* Error code", + "nternal error:", + "Makefile:[0-9]+: \\*\\*\\* .* Stop\\.", + ": No such file or directory", + ": Invalid argument", + "^The project cannot be built\\.", + "^\\[ERROR\\]", + "^Command .* failed with exit code", + 0 +}; + +static const char* cmCTestErrorExceptions[] = { + "instantiated from ", + "candidates are:", + ": warning", + ": \\(Warning\\)", + ": note", + "makefile:", + "Makefile:", + ":[ \\t]+Where:", + "([^ :]+):([0-9]+): Warning", + "------ Build started: .* ------", + 0 +}; + +static const char* cmCTestWarningMatches[] = { + "([^ :]+):([0-9]+): warning:", + "([^ :]+):([0-9]+): note:", + "^cc[^C]*CC: WARNING File = ([^,]+), Line = ([0-9]+)", + "^ld([^:])*:([ \\t])*WARNING([^:])*:", + "([^:]+): warning ([0-9]+):", + "^\"[^\"]+\", line [0-9]+: [Ww](arning|arnung)", + "([^:]+): warning[ \\t]*[0-9]+[ \\t]*:", + "^(Warning|Warnung) ([0-9]+):", + "^(Warning|Warnung)[ :]", + "WARNING: ", + "([^ :]+) : warning", + "([^:]+): warning", + "\", line [0-9]+\\.[0-9]+: [0-9]+-[0-9]+ \\([WI]\\)", + "^cxx: Warning:", + ".*file: .* has no symbols", + "([^ :]+):([0-9]+): (Warning|Warnung)", + "\\([0-9]*\\): remark #[0-9]*", + "\".*\", line [0-9]+: remark\\([0-9]*\\):", + "cc-[0-9]* CC: REMARK File = .*, Line = [0-9]*", + "^CMake Warning.*:", + "^\\[WARNING\\]", + 0 +}; + +static const char* cmCTestWarningExceptions[] = { + "/usr/.*/X11/Xlib\\.h:[0-9]+: war.*: ANSI C\\+\\+ forbids declaration", + "/usr/.*/X11/Xutil\\.h:[0-9]+: war.*: ANSI C\\+\\+ forbids declaration", + "/usr/.*/X11/XResource\\.h:[0-9]+: war.*: ANSI C\\+\\+ forbids declaration", + "WARNING 84 :", + "WARNING 47 :", + "makefile:", + "Makefile:", + "warning: Clock skew detected. Your build may be incomplete.", + "/usr/openwin/include/GL/[^:]+:", + "bind_at_load", + "XrmQGetResource", + "IceFlush", + "warning LNK4089: all references to [^ \\t]+ discarded by .OPT:REF", + "ld32: WARNING 85: definition of dataKey in", + "cc: warning 422: Unknown option \"\\+b", + "_with_warning_C", + 0 +}; + +struct cmCTestBuildCompileErrorWarningRex +{ + const char* RegularExpressionString; + int FileIndex; + int LineIndex; +}; + +static cmCTestBuildCompileErrorWarningRex +cmCTestWarningErrorFileLine[] = { + { "^Warning W[0-9]+ ([a-zA-Z.\\:/0-9_+ ~-]+) ([0-9]+):", 1, 2 }, + { "^([a-zA-Z./0-9_+ ~-]+):([0-9]+):", 1, 2 }, + { "^([a-zA-Z.\\:/0-9_+ ~-]+)\\(([0-9]+)\\)", 1, 2 }, + { "^[0-9]+>([a-zA-Z.\\:/0-9_+ ~-]+)\\(([0-9]+)\\)", 1, 2 }, + { "^([a-zA-Z./0-9_+ ~-]+)\\(([0-9]+)\\)", 1, 2 }, + { "\"([a-zA-Z./0-9_+ ~-]+)\", line ([0-9]+)", 1, 2 }, + { "File = ([a-zA-Z./0-9_+ ~-]+), Line = ([0-9]+)", 1, 2 }, + { 0, 0, 0 } +}; + +//---------------------------------------------------------------------- +cmCTestBuildHandler::cmCTestBuildHandler() +{ + this->MaxPreContext = 10; + this->MaxPostContext = 10; + + this->MaxErrors = 50; + this->MaxWarnings = 50; + + this->LastErrorOrWarning = this->ErrorsAndWarnings.end(); + + this->UseCTestLaunch = false; +} + +//---------------------------------------------------------------------- +void cmCTestBuildHandler::Initialize() +{ + this->Superclass::Initialize(); + this->StartBuild = ""; + this->EndBuild = ""; + this->CustomErrorMatches.clear(); + this->CustomErrorExceptions.clear(); + this->CustomWarningMatches.clear(); + this->CustomWarningExceptions.clear(); + this->ReallyCustomWarningMatches.clear(); + this->ReallyCustomWarningExceptions.clear(); + this->ErrorWarningFileLineRegex.clear(); + + this->ErrorMatchRegex.clear(); + this->ErrorExceptionRegex.clear(); + this->WarningMatchRegex.clear(); + this->WarningExceptionRegex.clear(); + this->BuildProcessingQueue.clear(); + this->BuildProcessingErrorQueue.clear(); + this->BuildOutputLogSize = 0; + this->CurrentProcessingLine.clear(); + + this->SimplifySourceDir = ""; + this->SimplifyBuildDir = ""; + this->OutputLineCounter = 0; + this->ErrorsAndWarnings.clear(); + this->LastErrorOrWarning = this->ErrorsAndWarnings.end(); + this->PostContextCount = 0; + this->MaxPreContext = 10; + this->MaxPostContext = 10; + this->PreContext.clear(); + + this->TotalErrors = 0; + this->TotalWarnings = 0; + this->LastTickChar = 0; + + this->ErrorQuotaReached = false; + this->WarningQuotaReached = false; + + this->MaxErrors = 50; + this->MaxWarnings = 50; + + this->UseCTestLaunch = false; +} + +//---------------------------------------------------------------------- +void cmCTestBuildHandler::PopulateCustomVectors(cmMakefile *mf) +{ + this->CTest->PopulateCustomVector(mf, "CTEST_CUSTOM_ERROR_MATCH", + this->CustomErrorMatches); + this->CTest->PopulateCustomVector(mf, "CTEST_CUSTOM_ERROR_EXCEPTION", + this->CustomErrorExceptions); + this->CTest->PopulateCustomVector(mf, "CTEST_CUSTOM_WARNING_MATCH", + this->CustomWarningMatches); + this->CTest->PopulateCustomVector(mf, "CTEST_CUSTOM_WARNING_EXCEPTION", + this->CustomWarningExceptions); + this->CTest->PopulateCustomInteger(mf, + "CTEST_CUSTOM_MAXIMUM_NUMBER_OF_ERRORS", + this->MaxErrors); + this->CTest->PopulateCustomInteger(mf, + "CTEST_CUSTOM_MAXIMUM_NUMBER_OF_WARNINGS", + this->MaxWarnings); + + int n = -1; + this->CTest->PopulateCustomInteger(mf, "CTEST_CUSTOM_ERROR_PRE_CONTEXT", n); + if (n != -1) + { + this->MaxPreContext = static_cast<size_t>(n); + } + + n = -1; + this->CTest->PopulateCustomInteger(mf, "CTEST_CUSTOM_ERROR_POST_CONTEXT", n); + if (n != -1) + { + this->MaxPostContext = static_cast<size_t>(n); + } + + // Record the user-specified custom warning rules. + if(const char* customWarningMatchers = + mf->GetDefinition("CTEST_CUSTOM_WARNING_MATCH")) + { + cmSystemTools::ExpandListArgument(customWarningMatchers, + this->ReallyCustomWarningMatches); + } + if(const char* customWarningExceptions = + mf->GetDefinition("CTEST_CUSTOM_WARNING_EXCEPTION")) + { + cmSystemTools::ExpandListArgument(customWarningExceptions, + this->ReallyCustomWarningExceptions); + } +} + +//---------------------------------------------------------------------- +std::string cmCTestBuildHandler::GetMakeCommand() +{ + std::string makeCommand + = this->CTest->GetCTestConfiguration("MakeCommand"); + cmCTestLog(this->CTest, + HANDLER_VERBOSE_OUTPUT, "MakeCommand:" << makeCommand << + "\n"); + + std::string configType = this->CTest->GetConfigType(); + if (configType == "") + { + configType + = this->CTest->GetCTestConfiguration("DefaultCTestConfigurationType"); + } + if (configType == "") + { + configType = "Release"; + } + + cmSystemTools::ReplaceString(makeCommand, + "${CTEST_CONFIGURATION_TYPE}", configType.c_str()); + + return makeCommand; +} + +//---------------------------------------------------------------------- +//clearly it would be nice if this were broken up into a few smaller +//functions and commented... +int cmCTestBuildHandler::ProcessHandler() +{ + cmCTestLog(this->CTest, HANDLER_OUTPUT, "Build project" << std::endl); + + // do we have time for this + if (this->CTest->GetRemainingTimeAllowed() < 120) + { + return 0; + } + + int entry; + for ( entry = 0; + cmCTestWarningErrorFileLine[entry].RegularExpressionString; + ++ entry ) + { + cmCTestBuildHandler::cmCTestCompileErrorWarningRex r; + if ( r.RegularExpression.compile( + cmCTestWarningErrorFileLine[entry].RegularExpressionString) ) + { + r.FileIndex = cmCTestWarningErrorFileLine[entry].FileIndex; + r.LineIndex = cmCTestWarningErrorFileLine[entry].LineIndex; + this->ErrorWarningFileLineRegex.push_back(r); + } + else + { + cmCTestLog(this->CTest, ERROR_MESSAGE, + "Problem Compiling regular expression: " + << cmCTestWarningErrorFileLine[entry].RegularExpressionString + << std::endl); + } + } + + // Determine build command and build directory + std::string makeCommand = this->GetMakeCommand(); + if ( makeCommand.size() == 0 ) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, + "Cannot find MakeCommand key in the DartConfiguration.tcl" + << std::endl); + return -1; + } + + const std::string &buildDirectory + = this->CTest->GetCTestConfiguration("BuildDirectory"); + if ( buildDirectory.size() == 0 ) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, + "Cannot find BuildDirectory key in the DartConfiguration.tcl" + << std::endl); + return -1; + } + + std::string const& useLaunchers = + this->CTest->GetCTestConfiguration("UseLaunchers"); + this->UseCTestLaunch = cmSystemTools::IsOn(useLaunchers.c_str()); + + // Create a last build log + cmGeneratedFileStream ofs; + double elapsed_time_start = cmSystemTools::GetTime(); + if ( !this->StartLogFile("Build", ofs) ) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, "Cannot create build log file" + << std::endl); + } + + // Create lists of regular expression strings for errors, error exceptions, + // warnings and warning exceptions. + std::vector<cmStdString>::size_type cc; + for ( cc = 0; cmCTestErrorMatches[cc]; cc ++ ) + { + this->CustomErrorMatches.push_back(cmCTestErrorMatches[cc]); + } + for ( cc = 0; cmCTestErrorExceptions[cc]; cc ++ ) + { + this->CustomErrorExceptions.push_back(cmCTestErrorExceptions[cc]); + } + for ( cc = 0; cmCTestWarningMatches[cc]; cc ++ ) + { + this->CustomWarningMatches.push_back(cmCTestWarningMatches[cc]); + } + + for ( cc = 0; cmCTestWarningExceptions[cc]; cc ++ ) + { + this->CustomWarningExceptions.push_back(cmCTestWarningExceptions[cc]); + } + + // Pre-compile regular expressions objects for all regular expressions + std::vector<cmStdString>::iterator it; + +#define cmCTestBuildHandlerPopulateRegexVector(strings, regexes) \ + regexes.clear(); \ + cmCTestLog(this->CTest, DEBUG, this << "Add " #regexes \ + << std::endl); \ + for ( it = strings.begin(); it != strings.end(); ++it ) \ + { \ + cmCTestLog(this->CTest, DEBUG, "Add " #strings ": " \ + << it->c_str() << std::endl); \ + regexes.push_back(it->c_str()); \ + } + cmCTestBuildHandlerPopulateRegexVector( + this->CustomErrorMatches, this->ErrorMatchRegex); + cmCTestBuildHandlerPopulateRegexVector( + this->CustomErrorExceptions, this->ErrorExceptionRegex); + cmCTestBuildHandlerPopulateRegexVector( + this->CustomWarningMatches, this->WarningMatchRegex); + cmCTestBuildHandlerPopulateRegexVector( + this->CustomWarningExceptions, this->WarningExceptionRegex); + + + // Determine source and binary tree substitutions to simplify the output. + this->SimplifySourceDir = ""; + this->SimplifyBuildDir = ""; + if ( this->CTest->GetCTestConfiguration("SourceDirectory").size() > 20 ) + { + std::string srcdir + = this->CTest->GetCTestConfiguration("SourceDirectory") + "/"; + std::string srcdirrep; + for ( cc = srcdir.size()-2; cc > 0; cc -- ) + { + if ( srcdir[cc] == '/' ) + { + srcdirrep = srcdir.c_str() + cc; + srcdirrep = "/..." + srcdirrep; + srcdir = srcdir.substr(0, cc+1); + break; + } + } + this->SimplifySourceDir = srcdir; + } + if ( this->CTest->GetCTestConfiguration("BuildDirectory").size() > 20 ) + { + std::string bindir + = this->CTest->GetCTestConfiguration("BuildDirectory") + "/"; + std::string bindirrep; + for ( cc = bindir.size()-2; cc > 0; cc -- ) + { + if ( bindir[cc] == '/' ) + { + bindirrep = bindir.c_str() + cc; + bindirrep = "/..." + bindirrep; + bindir = bindir.substr(0, cc+1); + break; + } + } + this->SimplifyBuildDir = bindir; + } + + + // Ok, let's do the build + + // Remember start build time + this->StartBuild = this->CTest->CurrentTime(); + this->StartBuildTime = cmSystemTools::GetTime(); + int retVal = 0; + int res = cmsysProcess_State_Exited; + if ( !this->CTest->GetShowOnly() ) + { + res = this->RunMakeCommand(makeCommand.c_str(), &retVal, + buildDirectory.c_str(), 0, ofs); + } + else + { + cmCTestLog(this->CTest, DEBUG, "Build with command: " << makeCommand + << std::endl); + } + + // Remember end build time and calculate elapsed time + this->EndBuild = this->CTest->CurrentTime(); + this->EndBuildTime = cmSystemTools::GetTime(); + double elapsed_build_time = cmSystemTools::GetTime() - elapsed_time_start; + + // Cleanups strings in the errors and warnings list. + t_ErrorsAndWarningsVector::iterator evit; + if ( !this->SimplifySourceDir.empty() ) + { + for ( evit = this->ErrorsAndWarnings.begin(); + evit != this->ErrorsAndWarnings.end(); + ++ evit ) + { + cmSystemTools::ReplaceString( + evit->Text, this->SimplifySourceDir.c_str(), "/.../"); + cmSystemTools::ReplaceString( + evit->PreContext, this->SimplifySourceDir.c_str(), "/.../"); + cmSystemTools::ReplaceString( + evit->PostContext, this->SimplifySourceDir.c_str(), "/.../"); + } + } + + if ( !this->SimplifyBuildDir.empty() ) + { + for ( evit = this->ErrorsAndWarnings.begin(); + evit != this->ErrorsAndWarnings.end(); + ++ evit ) + { + cmSystemTools::ReplaceString( + evit->Text, this->SimplifyBuildDir.c_str(), "/.../"); + cmSystemTools::ReplaceString( + evit->PreContext, this->SimplifyBuildDir.c_str(), "/.../"); + cmSystemTools::ReplaceString( + evit->PostContext, this->SimplifyBuildDir.c_str(), "/.../"); + } + } + + // Generate XML output + cmGeneratedFileStream xofs; + if(!this->StartResultingXML(cmCTest::PartBuild, "Build", xofs)) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, "Cannot create build XML file" + << std::endl); + return -1; + } + this->GenerateXMLHeader(xofs); + if(this->UseCTestLaunch) + { + this->GenerateXMLLaunched(xofs); + } + else + { + this->GenerateXMLLogScraped(xofs); + } + this->GenerateXMLFooter(xofs, elapsed_build_time); + + if (res != cmsysProcess_State_Exited || retVal || this->TotalErrors > 0) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, "Error(s) when building project" + << std::endl); + } + + // Display message about number of errors and warnings + cmCTestLog(this->CTest, HANDLER_OUTPUT, " " << this->TotalErrors + << (this->TotalErrors >= this->MaxErrors ? " or more" : "") + << " Compiler errors" << std::endl); + cmCTestLog(this->CTest, HANDLER_OUTPUT, " " << this->TotalWarnings + << (this->TotalWarnings >= this->MaxWarnings ? " or more" : "") + << " Compiler warnings" << std::endl); + + return retVal; +} + +//---------------------------------------------------------------------------- +void cmCTestBuildHandler::GenerateXMLHeader(std::ostream& os) +{ + this->CTest->StartXML(os, this->AppendXML); + os << "<Build>\n" + << "\t<StartDateTime>" << this->StartBuild << "</StartDateTime>\n" + << "\t<StartBuildTime>" << + static_cast<unsigned int>(this->StartBuildTime) + << "</StartBuildTime>\n" + << "<BuildCommand>" + << cmXMLSafe(this->GetMakeCommand()) + << "</BuildCommand>" << std::endl; +} + +//---------------------------------------------------------------------------- +class cmCTestBuildHandler::FragmentCompare +{ +public: + FragmentCompare(cmFileTimeComparison* ftc): FTC(ftc) {} + FragmentCompare(): FTC(0) {} + bool operator()(std::string const& l, std::string const& r) + { + // Order files by modification time. Use lexicographic order + // among files with the same time. + int result; + if(this->FTC->FileTimeCompare(l.c_str(), r.c_str(), &result) && + result != 0) + { + return result < 0; + } + else + { + return l < r; + } + } +private: + cmFileTimeComparison* FTC; +}; + +//---------------------------------------------------------------------------- +void cmCTestBuildHandler::GenerateXMLLaunched(std::ostream& os) +{ + if(this->CTestLaunchDir.empty()) + { + return; + } + + // Sort XML fragments in chronological order. + cmFileTimeComparison ftc; + FragmentCompare fragmentCompare(&ftc); + typedef std::set<cmStdString, FragmentCompare> Fragments; + Fragments fragments(fragmentCompare); + + // Identify fragments on disk. + cmsys::Directory launchDir; + launchDir.Load(this->CTestLaunchDir.c_str()); + unsigned long n = launchDir.GetNumberOfFiles(); + for(unsigned long i=0; i < n; ++i) + { + const char* fname = launchDir.GetFile(i); + if(this->IsLaunchedErrorFile(fname)) + { + fragments.insert(this->CTestLaunchDir + "/" + fname); + ++this->TotalErrors; + } + else if(this->IsLaunchedWarningFile(fname)) + { + fragments.insert(this->CTestLaunchDir + "/" + fname); + ++this->TotalWarnings; + } + } + + // Copy the fragments into the final XML file. + for(Fragments::const_iterator fi = fragments.begin(); + fi != fragments.end(); ++fi) + { + this->GenerateXMLLaunchedFragment(os, fi->c_str()); + } +} + +//---------------------------------------------------------------------------- +void cmCTestBuildHandler::GenerateXMLLogScraped(std::ostream& os) +{ + std::vector<cmCTestBuildErrorWarning>& ew = this->ErrorsAndWarnings; + std::vector<cmCTestBuildErrorWarning>::iterator it; + + // only report the first 50 warnings and first 50 errors + int numErrorsAllowed = this->MaxErrors; + int numWarningsAllowed = this->MaxWarnings; + std::string srcdir = this->CTest->GetCTestConfiguration("SourceDirectory"); + // make sure the source dir is in the correct case on windows + // via a call to collapse full path. + srcdir = cmSystemTools::CollapseFullPath(srcdir.c_str()); + srcdir += "/"; + for ( it = ew.begin(); + it != ew.end() && (numErrorsAllowed || numWarningsAllowed); it++ ) + { + cmCTestBuildErrorWarning *cm = &(*it); + if ((cm->Error && numErrorsAllowed) || + (!cm->Error && numWarningsAllowed)) + { + if (cm->Error) + { + numErrorsAllowed--; + } + else + { + numWarningsAllowed--; + } + os << "\t<" << (cm->Error ? "Error" : "Warning") << ">\n" + << "\t\t<BuildLogLine>" << cm->LogLine << "</BuildLogLine>\n" + << "\t\t<Text>" << cmXMLSafe(cm->Text).Quotes(false) + << "\n</Text>" << std::endl; + std::vector<cmCTestCompileErrorWarningRex>::iterator rit; + for ( rit = this->ErrorWarningFileLineRegex.begin(); + rit != this->ErrorWarningFileLineRegex.end(); ++ rit ) + { + cmsys::RegularExpression* re = &rit->RegularExpression; + if ( re->find(cm->Text.c_str() ) ) + { + cm->SourceFile = re->match(rit->FileIndex); + // At this point we need to make this->SourceFile relative to + // the source root of the project, so cvs links will work + cmSystemTools::ConvertToUnixSlashes(cm->SourceFile); + if(cm->SourceFile.find("/.../") != cm->SourceFile.npos) + { + cmSystemTools::ReplaceString(cm->SourceFile, "/.../", ""); + std::string::size_type p = cm->SourceFile.find("/"); + if(p != cm->SourceFile.npos) + { + cm->SourceFile = cm->SourceFile.substr( + p+1, cm->SourceFile.size()-p); + } + } + else + { + // make sure it is a full path with the correct case + cm->SourceFile = cmSystemTools::CollapseFullPath( + cm->SourceFile.c_str()); + cmSystemTools::ReplaceString( + cm->SourceFile, srcdir.c_str(), ""); + } + cm->LineNumber = atoi(re->match(rit->LineIndex).c_str()); + break; + } + } + if ( !cm->SourceFile.empty() && cm->LineNumber >= 0 ) + { + if ( cm->SourceFile.size() > 0 ) + { + os << "\t\t<SourceFile>" << cm->SourceFile << "</SourceFile>" + << std::endl; + } + if ( cm->SourceFileTail.size() > 0 ) + { + os << "\t\t<SourceFileTail>" << cm->SourceFileTail + << "</SourceFileTail>" << std::endl; + } + if ( cm->LineNumber >= 0 ) + { + os << "\t\t<SourceLineNumber>" << cm->LineNumber + << "</SourceLineNumber>" << std::endl; + } + } + os << "\t\t<PreContext>" << cmXMLSafe(cm->PreContext).Quotes(false) + << "</PreContext>\n" + << "\t\t<PostContext>" << cmXMLSafe(cm->PostContext).Quotes(false); + // is this the last warning or error, if so notify + if ((cm->Error && !numErrorsAllowed) || + (!cm->Error && !numWarningsAllowed)) + { + os << "\nThe maximum number of reported warnings or errors has been " + "reached!!!\n"; + } + os << "</PostContext>\n" + << "\t\t<RepeatCount>0</RepeatCount>\n" + << "</" << (cm->Error ? "Error" : "Warning") << ">\n\n" + << std::endl; + } + } +} + +//---------------------------------------------------------------------------- +void cmCTestBuildHandler::GenerateXMLFooter(std::ostream& os, + double elapsed_build_time) +{ + os << "\t<Log Encoding=\"base64\" Compression=\"/bin/gzip\">\n\t</Log>\n" + << "\t<EndDateTime>" << this->EndBuild << "</EndDateTime>\n" + << "\t<EndBuildTime>" << static_cast<unsigned int>(this->EndBuildTime) + << "</EndBuildTime>\n" + << "<ElapsedMinutes>" << static_cast<int>(elapsed_build_time/6)/10.0 + << "</ElapsedMinutes>" + << "</Build>" << std::endl; + this->CTest->EndXML(os); +} + +//---------------------------------------------------------------------------- +void cmCTestBuildHandler::GenerateXMLLaunchedFragment(std::ostream& os, + const char* fname) +{ + std::ifstream fin(fname, std::ios::in | std::ios::binary); + std::string line; + while(cmSystemTools::GetLineFromStream(fin, line)) + { + os << line << "\n"; + } +} + +//---------------------------------------------------------------------------- +bool cmCTestBuildHandler::IsLaunchedErrorFile(const char* fname) +{ + // error-{hash}.xml + return (strncmp(fname, "error-", 6) == 0 && + strcmp(fname+strlen(fname)-4, ".xml") == 0); +} + +//---------------------------------------------------------------------------- +bool cmCTestBuildHandler::IsLaunchedWarningFile(const char* fname) +{ + // warning-{hash}.xml + return (strncmp(fname, "warning-", 8) == 0 && + strcmp(fname+strlen(fname)-4, ".xml") == 0); +} + +//###################################################################### +//###################################################################### +//###################################################################### +//###################################################################### + +//---------------------------------------------------------------------------- +class cmCTestBuildHandler::LaunchHelper +{ +public: + LaunchHelper(cmCTestBuildHandler* handler); + ~LaunchHelper(); +private: + cmCTestBuildHandler* Handler; + cmCTest* CTest; + + void WriteLauncherConfig(); + void WriteScrapeMatchers(const char* purpose, + std::vector<std::string> const& matchers); +}; + +//---------------------------------------------------------------------------- +cmCTestBuildHandler::LaunchHelper::LaunchHelper(cmCTestBuildHandler* handler): + Handler(handler), CTest(handler->CTest) +{ + std::string tag = this->CTest->GetCurrentTag(); + if(tag.empty()) + { + // This is not for a dashboard submission, so there is no XML. + // Skip enabling the launchers. + this->Handler->UseCTestLaunch = false; + } + else + { + // Compute a directory in which to store launcher fragments. + std::string& launchDir = this->Handler->CTestLaunchDir; + launchDir = this->CTest->GetBinaryDir(); + launchDir += "/Testing/"; + launchDir += tag; + launchDir += "/Build"; + + // Clean out any existing launcher fragments. + cmSystemTools::RemoveADirectory(launchDir.c_str()); + + if(this->Handler->UseCTestLaunch) + { + // Enable launcher fragments. + cmSystemTools::MakeDirectory(launchDir.c_str()); + this->WriteLauncherConfig(); + std::string launchEnv = "CTEST_LAUNCH_LOGS="; + launchEnv += launchDir; + cmSystemTools::PutEnv(launchEnv.c_str()); + } + } + + // If not using launchers, make sure they passthru. + if(!this->Handler->UseCTestLaunch) + { + cmSystemTools::UnsetEnv("CTEST_LAUNCH_LOGS"); + } +} + +//---------------------------------------------------------------------------- +cmCTestBuildHandler::LaunchHelper::~LaunchHelper() +{ + if(this->Handler->UseCTestLaunch) + { + cmSystemTools::UnsetEnv("CTEST_LAUNCH_LOGS"); + } +} + +//---------------------------------------------------------------------------- +void cmCTestBuildHandler::LaunchHelper::WriteLauncherConfig() +{ + this->WriteScrapeMatchers("Warning", + this->Handler->ReallyCustomWarningMatches); + this->WriteScrapeMatchers("WarningSuppress", + this->Handler->ReallyCustomWarningExceptions); + + // Give some testing configuration information to the launcher. + std::string fname = this->Handler->CTestLaunchDir; + fname += "/CTestLaunchConfig.cmake"; + cmGeneratedFileStream fout(fname.c_str()); + std::string srcdir = this->CTest->GetCTestConfiguration("SourceDirectory"); + fout << "set(CTEST_SOURCE_DIRECTORY \"" << srcdir << "\")\n"; +} + +//---------------------------------------------------------------------------- +void +cmCTestBuildHandler::LaunchHelper +::WriteScrapeMatchers(const char* purpose, + std::vector<std::string> const& matchers) +{ + if(matchers.empty()) + { + return; + } + std::string fname = this->Handler->CTestLaunchDir; + fname += "/Custom"; + fname += purpose; + fname += ".txt"; + cmGeneratedFileStream fout(fname.c_str()); + for(std::vector<std::string>::const_iterator mi = matchers.begin(); + mi != matchers.end(); ++mi) + { + fout << *mi << "\n"; + } +} + +//---------------------------------------------------------------------- +int cmCTestBuildHandler::RunMakeCommand(const char* command, + int* retVal, const char* dir, int timeout, std::ofstream& ofs) +{ + // First generate the command and arguments + std::vector<cmStdString> args = cmSystemTools::ParseArguments(command); + + if(args.size() < 1) + { + return false; + } + + std::vector<const char*> argv; + for(std::vector<cmStdString>::const_iterator a = args.begin(); + a != args.end(); ++a) + { + argv.push_back(a->c_str()); + } + argv.push_back(0); + + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, "Run command:"); + std::vector<const char*>::iterator ait; + for ( ait = argv.begin(); ait != argv.end() && *ait; ++ ait ) + { + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, " \"" << *ait << "\""); + } + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, std::endl); + + // Optionally use make rule launchers to record errors and warnings. + LaunchHelper launchHelper(this); + static_cast<void>(launchHelper); + + // Now create process object + cmsysProcess* cp = cmsysProcess_New(); + cmsysProcess_SetCommand(cp, &*argv.begin()); + cmsysProcess_SetWorkingDirectory(cp, dir); + cmsysProcess_SetOption(cp, cmsysProcess_Option_HideWindow, 1); + cmsysProcess_SetTimeout(cp, timeout); + cmsysProcess_Execute(cp); + + // Initialize tick's + std::string::size_type tick = 0; + const std::string::size_type tick_len = 1024; + + char* data; + int length; + cmCTestLog(this->CTest, HANDLER_OUTPUT, + " Each symbol represents " << tick_len << " bytes of output." + << std::endl + << (this->UseCTestLaunch? "" : + " '!' represents an error and '*' a warning.\n") + << " " << std::flush); + + // Initialize building structures + this->BuildProcessingQueue.clear(); + this->OutputLineCounter = 0; + this->ErrorsAndWarnings.clear(); + this->TotalErrors = 0; + this->TotalWarnings = 0; + this->BuildOutputLogSize = 0; + this->LastTickChar = '.'; + this->WarningQuotaReached = false; + this->ErrorQuotaReached = false; + + // For every chunk of data + int res; + while((res = cmsysProcess_WaitForData(cp, &data, &length, 0))) + { + // Replace '\0' with '\n', since '\0' does not really make sense. This is + // for Visual Studio output + for(int cc =0; cc < length; ++cc) + { + if(data[cc] == 0) + { + data[cc] = '\n'; + } + } + + // Process the chunk of data + if ( res == cmsysProcess_Pipe_STDERR ) + { + this->ProcessBuffer(data, length, tick, tick_len, ofs, + &this->BuildProcessingErrorQueue); + } + else + { + this->ProcessBuffer(data, length, tick, tick_len, ofs, + &this->BuildProcessingQueue); + } + } + + this->ProcessBuffer(0, 0, tick, tick_len, ofs, &this->BuildProcessingQueue); + this->ProcessBuffer(0, 0, tick, tick_len, ofs, + &this->BuildProcessingErrorQueue); + cmCTestLog(this->CTest, OUTPUT, " Size of output: " + << ((this->BuildOutputLogSize + 512) / 1024) << "K" << std::endl); + + // Properly handle output of the build command + cmsysProcess_WaitForExit(cp, 0); + int result = cmsysProcess_GetState(cp); + + if(result == cmsysProcess_State_Exited) + { + if (retVal) + { + *retVal = cmsysProcess_GetExitValue(cp); + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, + "Command exited with the value: " << *retVal << std::endl); + // if a non zero return value + if (*retVal) + { + // If there was an error running command, report that on the + // dashboard. + cmCTestBuildErrorWarning errorwarning; + errorwarning.LogLine = 1; + errorwarning.Text + = "*** WARNING non-zero return value in ctest from: "; + errorwarning.Text += argv[0]; + errorwarning.PreContext = ""; + errorwarning.PostContext = ""; + errorwarning.Error = false; + this->ErrorsAndWarnings.push_back(errorwarning); + this->TotalWarnings ++; + } + } + } + else if(result == cmsysProcess_State_Exception) + { + if (retVal) + { + *retVal = cmsysProcess_GetExitException(cp); + cmCTestLog(this->CTest, WARNING, "There was an exception: " << *retVal + << std::endl); + } + } + else if(result == cmsysProcess_State_Expired) + { + cmCTestLog(this->CTest, WARNING, "There was a timeout" << std::endl); + } + else if(result == cmsysProcess_State_Error) + { + // If there was an error running command, report that on the dashboard. + cmCTestBuildErrorWarning errorwarning; + errorwarning.LogLine = 1; + errorwarning.Text = "*** ERROR executing: "; + errorwarning.Text += cmsysProcess_GetErrorString(cp); + errorwarning.PreContext = ""; + errorwarning.PostContext = ""; + errorwarning.Error = true; + this->ErrorsAndWarnings.push_back(errorwarning); + this->TotalErrors ++; + cmCTestLog(this->CTest, ERROR_MESSAGE, "There was an error: " + << cmsysProcess_GetErrorString(cp) << std::endl); + } + + cmsysProcess_Delete(cp); + return result; +} + +//###################################################################### +//###################################################################### +//###################################################################### +//###################################################################### + +//---------------------------------------------------------------------- +void cmCTestBuildHandler::ProcessBuffer(const char* data, int length, + size_t& tick, size_t tick_len, std::ofstream& ofs, + t_BuildProcessingQueueType* queue) +{ + const std::string::size_type tick_line_len = 50; + const char* ptr; + for ( ptr = data; ptr < data+length; ptr ++ ) + { + queue->push_back(*ptr); + } + this->BuildOutputLogSize += length; + + // until there are any lines left in the buffer + while ( 1 ) + { + // Find the end of line + t_BuildProcessingQueueType::iterator it; + for ( it = queue->begin(); + it != queue->end(); + ++ it ) + { + if ( *it == '\n' ) + { + break; + } + } + + // Once certain number of errors or warnings reached, ignore future errors + // or warnings. + if ( this->TotalWarnings >= this->MaxWarnings ) + { + this->WarningQuotaReached = true; + } + if ( this->TotalErrors >= this->MaxErrors ) + { + this->ErrorQuotaReached = true; + } + + // If the end of line was found + if ( it != queue->end() ) + { + // Create a contiguous array for the line + this->CurrentProcessingLine.clear(); + t_BuildProcessingQueueType::iterator cit; + for ( cit = queue->begin(); cit != it; ++cit ) + { + this->CurrentProcessingLine.push_back(*cit); + } + this->CurrentProcessingLine.push_back(0); + const char* line = &*this->CurrentProcessingLine.begin(); + + // Process the line + int lineType = this->ProcessSingleLine(line); + + // Erase the line from the queue + queue->erase(queue->begin(), it+1); + + // Depending on the line type, produce error or warning, or nothing + cmCTestBuildErrorWarning errorwarning; + bool found = false; + switch ( lineType ) + { + case b_WARNING_LINE: + this->LastTickChar = '*'; + errorwarning.Error = false; + found = true; + this->TotalWarnings ++; + break; + case b_ERROR_LINE: + this->LastTickChar = '!'; + errorwarning.Error = true; + found = true; + this->TotalErrors ++; + break; + } + if ( found ) + { + // This is an error or warning, so generate report + errorwarning.LogLine = static_cast<int>(this->OutputLineCounter+1); + errorwarning.Text = line; + errorwarning.PreContext = ""; + errorwarning.PostContext = ""; + + // Copy pre-context to report + std::deque<cmStdString>::iterator pcit; + for ( pcit = this->PreContext.begin(); + pcit != this->PreContext.end(); + ++pcit ) + { + errorwarning.PreContext += *pcit + "\n"; + } + this->PreContext.clear(); + + // Store report + this->ErrorsAndWarnings.push_back(errorwarning); + this->LastErrorOrWarning = this->ErrorsAndWarnings.end()-1; + this->PostContextCount = 0; + } + else + { + // This is not an error or warning. + // So, figure out if this is a post-context line + if ( this->ErrorsAndWarnings.size() && + this->LastErrorOrWarning != this->ErrorsAndWarnings.end() && + this->PostContextCount < this->MaxPostContext ) + { + this->PostContextCount ++; + this->LastErrorOrWarning->PostContext += line; + if ( this->PostContextCount < this->MaxPostContext ) + { + this->LastErrorOrWarning->PostContext += "\n"; + } + } + else + { + // Otherwise store pre-context for the next error + this->PreContext.push_back(line); + if ( this->PreContext.size() > this->MaxPreContext ) + { + this->PreContext.erase(this->PreContext.begin(), + this->PreContext.end()-this->MaxPreContext); + } + } + } + this->OutputLineCounter ++; + } + else + { + break; + } + } + + // Now that the buffer is processed, display missing ticks + int tickDisplayed = false; + while ( this->BuildOutputLogSize > (tick * tick_len) ) + { + tick ++; + cmCTestLog(this->CTest, HANDLER_OUTPUT, this->LastTickChar); + tickDisplayed = true; + if ( tick % tick_line_len == 0 && tick > 0 ) + { + cmCTestLog(this->CTest, HANDLER_OUTPUT, " Size: " + << ((this->BuildOutputLogSize + 512) / 1024) << "K" << std::endl + << " "); + } + } + if ( tickDisplayed ) + { + this->LastTickChar = '.'; + } + + // And if this is verbose output, display the content of the chunk + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, + cmCTestLogWrite(data, length)); + + // Always store the chunk to the file + ofs << cmCTestLogWrite(data, length); +} + +//---------------------------------------------------------------------- +int cmCTestBuildHandler::ProcessSingleLine(const char* data) +{ + if(this->UseCTestLaunch) + { + // No log scraping when using launchers. + return b_REGULAR_LINE; + } + + cmCTestLog(this->CTest, DEBUG, "Line: [" << data << "]" << std::endl); + + std::vector<cmsys::RegularExpression>::iterator it; + + int warningLine = 0; + int errorLine = 0; + + // Check for regular expressions + + if ( !this->ErrorQuotaReached ) + { + // Errors + int wrxCnt = 0; + for ( it = this->ErrorMatchRegex.begin(); + it != this->ErrorMatchRegex.end(); + ++ it ) + { + if ( it->find(data) ) + { + errorLine = 1; + cmCTestLog(this->CTest, DEBUG, " Error Line: " << data + << " (matches: " << this->CustomErrorMatches[wrxCnt] << ")" + << std::endl); + break; + } + wrxCnt ++; + } + // Error exceptions + wrxCnt = 0; + for ( it = this->ErrorExceptionRegex.begin(); + it != this->ErrorExceptionRegex.end(); + ++ it ) + { + if ( it->find(data) ) + { + errorLine = 0; + cmCTestLog(this->CTest, DEBUG, " Not an error Line: " << data + << " (matches: " << this->CustomErrorExceptions[wrxCnt] << ")" + << std::endl); + break; + } + wrxCnt ++; + } + } + if ( !this->WarningQuotaReached ) + { + // Warnings + int wrxCnt = 0; + for ( it = this->WarningMatchRegex.begin(); + it != this->WarningMatchRegex.end(); + ++ it ) + { + if ( it->find(data) ) + { + warningLine = 1; + cmCTestLog(this->CTest, DEBUG, + " Warning Line: " << data + << " (matches: " << this->CustomWarningMatches[wrxCnt] << ")" + << std::endl); + break; + } + wrxCnt ++; + } + + wrxCnt = 0; + // Warning exceptions + for ( it = this->WarningExceptionRegex.begin(); + it != this->WarningExceptionRegex.end(); + ++ it ) + { + if ( it->find(data) ) + { + warningLine = 0; + cmCTestLog(this->CTest, DEBUG, " Not a warning Line: " << data + << " (matches: " << this->CustomWarningExceptions[wrxCnt] << ")" + << std::endl); + break; + } + wrxCnt ++; + } + } + if ( errorLine ) + { + return b_ERROR_LINE; + } + if ( warningLine ) + { + return b_WARNING_LINE; + } + return b_REGULAR_LINE; +} + diff --git a/Source/CTest/cmCTestBuildHandler.h b/Source/CTest/cmCTestBuildHandler.h new file mode 100644 index 000000000..439efd604 --- /dev/null +++ b/Source/CTest/cmCTestBuildHandler.h @@ -0,0 +1,152 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2009 Kitware, Inc. + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ + +#ifndef cmCTestBuildHandler_h +#define cmCTestBuildHandler_h + + +#include "cmCTestGenericHandler.h" +#include "cmListFileCache.h" + +#include <cmsys/RegularExpression.hxx> + +class cmMakefile; + +/** \class cmCTestBuildHandler + * \brief A class that handles ctest -S invocations + * + */ +class cmCTestBuildHandler : public cmCTestGenericHandler +{ +public: + cmTypeMacro(cmCTestBuildHandler, cmCTestGenericHandler); + + /* + * The main entry point for this class + */ + int ProcessHandler(); + + cmCTestBuildHandler(); + + void PopulateCustomVectors(cmMakefile *mf); + + /** + * Initialize handler + */ + virtual void Initialize(); + + int GetTotalErrors() { return this->TotalErrors;} + int GetTotalWarnings() { return this->TotalWarnings;} + +private: + std::string GetMakeCommand(); + + //! Run command specialized for make and configure. Returns process status + // and retVal is return value or exception. + int RunMakeCommand(const char* command, + int* retVal, const char* dir, int timeout, + std::ofstream& ofs); + + enum { + b_REGULAR_LINE, + b_WARNING_LINE, + b_ERROR_LINE + }; + + class cmCTestCompileErrorWarningRex + { + public: + cmCTestCompileErrorWarningRex() {} + int FileIndex; + int LineIndex; + cmsys::RegularExpression RegularExpression; + }; + + struct cmCTestBuildErrorWarning + { + bool Error; + int LogLine; + std::string Text; + std::string SourceFile; + std::string SourceFileTail; + int LineNumber; + std::string PreContext; + std::string PostContext; + }; + + // generate the XML output + void GenerateXMLHeader(std::ostream& os); + void GenerateXMLLaunched(std::ostream& os); + void GenerateXMLLogScraped(std::ostream& os); + void GenerateXMLFooter(std::ostream& os, double elapsed_build_time); + void GenerateXMLLaunchedFragment(std::ostream& os, const char* fname); + bool IsLaunchedErrorFile(const char* fname); + bool IsLaunchedWarningFile(const char* fname); + + std::string StartBuild; + std::string EndBuild; + double StartBuildTime; + double EndBuildTime; + + std::vector<cmStdString> CustomErrorMatches; + std::vector<cmStdString> CustomErrorExceptions; + std::vector<cmStdString> CustomWarningMatches; + std::vector<cmStdString> CustomWarningExceptions; + std::vector<std::string> ReallyCustomWarningMatches; + std::vector<std::string> ReallyCustomWarningExceptions; + std::vector<cmCTestCompileErrorWarningRex> ErrorWarningFileLineRegex; + + std::vector<cmsys::RegularExpression> ErrorMatchRegex; + std::vector<cmsys::RegularExpression> ErrorExceptionRegex; + std::vector<cmsys::RegularExpression> WarningMatchRegex; + std::vector<cmsys::RegularExpression> WarningExceptionRegex; + + typedef std::deque<char> t_BuildProcessingQueueType; + + void ProcessBuffer(const char* data, int length, size_t& tick, + size_t tick_len, std::ofstream& ofs, t_BuildProcessingQueueType* queue); + int ProcessSingleLine(const char* data); + + t_BuildProcessingQueueType BuildProcessingQueue; + t_BuildProcessingQueueType BuildProcessingErrorQueue; + size_t BuildOutputLogSize; + std::vector<char> CurrentProcessingLine; + + cmStdString SimplifySourceDir; + cmStdString SimplifyBuildDir; + size_t OutputLineCounter; + typedef std::vector<cmCTestBuildErrorWarning> t_ErrorsAndWarningsVector; + t_ErrorsAndWarningsVector ErrorsAndWarnings; + t_ErrorsAndWarningsVector::iterator LastErrorOrWarning; + size_t PostContextCount; + size_t MaxPreContext; + size_t MaxPostContext; + std::deque<cmStdString> PreContext; + + int TotalErrors; + int TotalWarnings; + char LastTickChar; + + bool ErrorQuotaReached; + bool WarningQuotaReached; + + int MaxErrors; + int MaxWarnings; + + bool UseCTestLaunch; + std::string CTestLaunchDir; + class LaunchHelper; + friend class LaunchHelper; + class FragmentCompare; +}; + +#endif diff --git a/Source/CTest/cmCTestCVS.cxx b/Source/CTest/cmCTestCVS.cxx new file mode 100644 index 000000000..726950704 --- /dev/null +++ b/Source/CTest/cmCTestCVS.cxx @@ -0,0 +1,319 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2009 Kitware, Inc. + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ +#include "cmCTestCVS.h" + +#include "cmCTest.h" +#include "cmSystemTools.h" +#include "cmXMLSafe.h" + +#include <cmsys/RegularExpression.hxx> + +//---------------------------------------------------------------------------- +cmCTestCVS::cmCTestCVS(cmCTest* ct, std::ostream& log): cmCTestVC(ct, log) +{ +} + +//---------------------------------------------------------------------------- +cmCTestCVS::~cmCTestCVS() +{ +} + +//---------------------------------------------------------------------------- +class cmCTestCVS::UpdateParser: public cmCTestVC::LineParser +{ +public: + UpdateParser(cmCTestCVS* cvs, const char* prefix): CVS(cvs) + { + this->SetLog(&cvs->Log, prefix); + // See "man cvs", section "update output". + this->RegexFileUpdated.compile("^([UP]) *(.*)"); + this->RegexFileModified.compile("^([MRA]) *(.*)"); + this->RegexFileConflicting.compile("^([C]) *(.*)"); + this->RegexFileRemoved1.compile( + "cvs[^ ]* update: `?([^']*)'? is no longer in the repository"); + this->RegexFileRemoved2.compile( + "cvs[^ ]* update: " + "warning: `?([^']*)'? is not \\(any longer\\) pertinent"); + } +private: + cmCTestCVS* CVS; + cmsys::RegularExpression RegexFileUpdated; + cmsys::RegularExpression RegexFileModified; + cmsys::RegularExpression RegexFileConflicting; + cmsys::RegularExpression RegexFileRemoved1; + cmsys::RegularExpression RegexFileRemoved2; + + virtual bool ProcessLine() + { + if(this->RegexFileUpdated.find(this->Line)) + { + this->DoFile(PathUpdated, this->RegexFileUpdated.match(2)); + } + else if(this->RegexFileModified.find(this->Line)) + { + this->DoFile(PathModified, this->RegexFileModified.match(2)); + } + else if(this->RegexFileConflicting.find(this->Line)) + { + this->DoFile(PathConflicting, this->RegexFileConflicting.match(2)); + } + else if(this->RegexFileRemoved1.find(this->Line)) + { + this->DoFile(PathUpdated, this->RegexFileRemoved1.match(1)); + } + else if(this->RegexFileRemoved2.find(this->Line)) + { + this->DoFile(PathUpdated, this->RegexFileRemoved2.match(1)); + } + return true; + } + + void DoFile(PathStatus status, std::string const& file) + { + std::string dir = cmSystemTools::GetFilenamePath(file); + std::string name = cmSystemTools::GetFilenameName(file); + this->CVS->Dirs[dir][name] = status; + } +}; + +//---------------------------------------------------------------------------- +bool cmCTestCVS::UpdateImpl() +{ + // Get user-specified update options. + std::string opts = this->CTest->GetCTestConfiguration("UpdateOptions"); + if(opts.empty()) + { + opts = this->CTest->GetCTestConfiguration("CVSUpdateOptions"); + if(opts.empty()) + { + opts = "-dP"; + } + } + std::vector<cmStdString> args = cmSystemTools::ParseArguments(opts.c_str()); + + // Specify the start time for nightly testing. + if(this->CTest->GetTestModel() == cmCTest::NIGHTLY) + { + args.push_back("-D" + this->GetNightlyTime() + " UTC"); + } + + // Run "cvs update" to update the work tree. + std::vector<char const*> cvs_update; + cvs_update.push_back(this->CommandLineTool.c_str()); + cvs_update.push_back("-z3"); + cvs_update.push_back("update"); + for(std::vector<cmStdString>::const_iterator ai = args.begin(); + ai != args.end(); ++ai) + { + cvs_update.push_back(ai->c_str()); + } + cvs_update.push_back(0); + + UpdateParser out(this, "up-out> "); + UpdateParser err(this, "up-err> "); + return this->RunUpdateCommand(&cvs_update[0], &out, &err); +} + +//---------------------------------------------------------------------------- +class cmCTestCVS::LogParser: public cmCTestVC::LineParser +{ +public: + typedef cmCTestCVS::Revision Revision; + LogParser(cmCTestCVS* cvs, const char* prefix, std::vector<Revision>& revs): + CVS(cvs), Revisions(revs), Section(SectionHeader) + { + this->SetLog(&cvs->Log, prefix), + this->RegexRevision.compile("^revision +([^ ]*) *$"); + this->RegexBranches.compile("^branches: .*$"); + this->RegexPerson.compile("^date: +([^;]+); +author: +([^;]+);"); + } +private: + cmCTestCVS* CVS; + std::vector<Revision>& Revisions; + cmsys::RegularExpression RegexRevision; + cmsys::RegularExpression RegexBranches; + cmsys::RegularExpression RegexPerson; + enum SectionType { SectionHeader, SectionRevisions, SectionEnd }; + SectionType Section; + Revision Rev; + + virtual bool ProcessLine() + { + if(this->Line == ("=======================================" + "======================================")) + { + // This line ends the revision list. + if(this->Section == SectionRevisions) + { + this->FinishRevision(); + } + this->Section = SectionEnd; + } + else if(this->Line == "----------------------------") + { + // This line divides revisions from the header and each other. + if(this->Section == SectionHeader) + { + this->Section = SectionRevisions; + } + else if(this->Section == SectionRevisions) + { + this->FinishRevision(); + } + } + else if(this->Section == SectionRevisions) + { + if(!this->Rev.Log.empty()) + { + // Continue the existing log. + this->Rev.Log += this->Line; + this->Rev.Log += "\n"; + } + else if(this->Rev.Rev.empty() && this->RegexRevision.find(this->Line)) + { + this->Rev.Rev = this->RegexRevision.match(1); + } + else if(this->Rev.Date.empty() && this->RegexPerson.find(this->Line)) + { + this->Rev.Date = this->RegexPerson.match(1); + this->Rev.Author = this->RegexPerson.match(2); + } + else if(!this->RegexBranches.find(this->Line)) + { + // Start the log. + this->Rev.Log += this->Line; + this->Rev.Log += "\n"; + } + } + return this->Section != SectionEnd; + } + + void FinishRevision() + { + if(!this->Rev.Rev.empty()) + { + // Record this revision. + this->CVS->Log << "Found revision " << this->Rev.Rev << "\n" + << " author = " << this->Rev.Author << "\n" + << " date = " << this->Rev.Date << "\n"; + this->Revisions.push_back(this->Rev); + + // We only need two revisions. + if(this->Revisions.size() >= 2) + { + this->Section = SectionEnd; + } + } + this->Rev = Revision(); + } +}; + +//---------------------------------------------------------------------------- +std::string cmCTestCVS::ComputeBranchFlag(std::string const& dir) +{ + // Compute the tag file location for this directory. + std::string tagFile = this->SourceDirectory; + if(!dir.empty()) + { + tagFile += "/"; + tagFile += dir; + } + tagFile += "/CVS/Tag"; + + // Lookup the branch in the tag file, if any. + std::string tagLine; + std::ifstream tagStream(tagFile.c_str()); + if(tagStream && cmSystemTools::GetLineFromStream(tagStream, tagLine) && + tagLine.size() > 1 && tagLine[0] == 'T') + { + // Use the branch specified in the tag file. + std::string flag = "-r"; + flag += tagLine.substr(1); + return flag; + } + else + { + // Use the default branch. + return "-b"; + } +} + +//---------------------------------------------------------------------------- +void cmCTestCVS::LoadRevisions(std::string const& file, + const char* branchFlag, + std::vector<Revision>& revisions) +{ + cmCTestLog(this->CTest, HANDLER_OUTPUT, "." << std::flush); + + // Run "cvs log" to get revisions of this file on this branch. + const char* cvs = this->CommandLineTool.c_str(); + const char* cvs_log[] = + {cvs, "log", "-N", branchFlag, file.c_str(), 0}; + + LogParser out(this, "log-out> ", revisions); + OutputLogger err(this->Log, "log-err> "); + this->RunChild(cvs_log, &out, &err); +} + +//---------------------------------------------------------------------------- +void cmCTestCVS::WriteXMLDirectory(std::ostream& xml, + std::string const& path, + Directory const& dir) +{ + const char* slash = path.empty()? "":"/"; + xml << "\t<Directory>\n" + << "\t\t<Name>" << cmXMLSafe(path) << "</Name>\n"; + + // Lookup the branch checked out in the working tree. + std::string branchFlag = this->ComputeBranchFlag(path); + + // Load revisions and write an entry for each file in this directory. + std::vector<Revision> revisions; + for(Directory::const_iterator fi = dir.begin(); fi != dir.end(); ++fi) + { + std::string full = path + slash + fi->first; + + // Load two real or unknown revisions. + revisions.clear(); + if(fi->second != PathUpdated) + { + // For local modifications the current rev is unknown and the + // prior rev is the latest from cvs. + revisions.push_back(this->Unknown); + } + this->LoadRevisions(full, branchFlag.c_str(), revisions); + revisions.resize(2, this->Unknown); + + // Write the entry for this file with these revisions. + File f(fi->second, &revisions[0], &revisions[1]); + this->WriteXMLEntry(xml, path, fi->first, full, f); + } + xml << "\t</Directory>\n"; +} + +//---------------------------------------------------------------------------- +bool cmCTestCVS::WriteXMLUpdates(std::ostream& xml) +{ + cmCTestLog(this->CTest, HANDLER_OUTPUT, + " Gathering version information (one . per updated file):\n" + " " << std::flush); + + for(std::map<cmStdString, Directory>::const_iterator + di = this->Dirs.begin(); di != this->Dirs.end(); ++di) + { + this->WriteXMLDirectory(xml, di->first, di->second); + } + + cmCTestLog(this->CTest, HANDLER_OUTPUT, std::endl); + + return true; +} diff --git a/Source/CTest/cmCTestCVS.h b/Source/CTest/cmCTestCVS.h new file mode 100644 index 000000000..b7fe567d8 --- /dev/null +++ b/Source/CTest/cmCTestCVS.h @@ -0,0 +1,51 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2009 Kitware, Inc. + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ +#ifndef cmCTestCVS_h +#define cmCTestCVS_h + +#include "cmCTestVC.h" + +/** \class cmCTestCVS + * \brief Interaction with cvs command-line tool + * + */ +class cmCTestCVS: public cmCTestVC +{ +public: + /** Construct with a CTest instance and update log stream. */ + cmCTestCVS(cmCTest* ctest, std::ostream& log); + + virtual ~cmCTestCVS(); + +private: + // Implement cmCTestVC internal API. + virtual bool UpdateImpl(); + virtual bool WriteXMLUpdates(std::ostream& xml); + + // Update status for files in each directory. + class Directory: public std::map<cmStdString, PathStatus> {}; + std::map<cmStdString, Directory> Dirs; + + std::string ComputeBranchFlag(std::string const& dir); + void LoadRevisions(std::string const& file, const char* branchFlag, + std::vector<Revision>& revisions); + void WriteXMLDirectory(std::ostream& xml, std::string const& path, + Directory const& dir); + + // Parsing helper classes. + class UpdateParser; + class LogParser; + friend class UpdateParser; + friend class LogParser; +}; + +#endif diff --git a/Source/CTest/cmCTestCommand.h b/Source/CTest/cmCTestCommand.h new file mode 100644 index 000000000..e2ebba8fd --- /dev/null +++ b/Source/CTest/cmCTestCommand.h @@ -0,0 +1,40 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2009 Kitware, Inc., Insight Software Consortium + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ +#ifndef cmCTestCommand_h +#define cmCTestCommand_h + +#include "cmCommand.h" + +class cmCTest; +class cmCTestScriptHandler; + +/** \class cmCTestCommand + * \brief A superclass for all commands added to the CTestScriptHandler + * + * cmCTestCommand is the superclass for all commands that will be added to + * the ctest script handlers parser. + * + */ +class cmCTestCommand : public cmCommand +{ +public: + + cmCTestCommand() {this->CTest = 0; this->CTestScriptHandler = 0;} + + cmCTest *CTest; + cmCTestScriptHandler *CTestScriptHandler; + + cmTypeMacro(cmCTestCommand, cmCommand); +}; + + +#endif diff --git a/Source/CTest/cmCTestConfigureCommand.cxx b/Source/CTest/cmCTestConfigureCommand.cxx new file mode 100644 index 000000000..7a99ddfa0 --- /dev/null +++ b/Source/CTest/cmCTestConfigureCommand.cxx @@ -0,0 +1,172 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2009 Kitware, Inc., Insight Software Consortium + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ +#include "cmCTestConfigureCommand.h" + +#include "cmGlobalGenerator.h" +#include "cmCTest.h" +#include "cmCTestGenericHandler.h" + +cmCTestConfigureCommand::cmCTestConfigureCommand() +{ + this->Arguments[ctc_OPTIONS] = "OPTIONS"; + this->Arguments[ctc_LAST] = 0; + this->Last = ctc_LAST; +} + +cmCTestGenericHandler* cmCTestConfigureCommand::InitializeHandler() +{ + std::vector<std::string> options; + + if (this->Values[ctc_OPTIONS]) + { + cmSystemTools::ExpandListArgument(this->Values[ctc_OPTIONS], options); + } + + if ( this->Values[ct_BUILD] ) + { + this->CTest->SetCTestConfiguration("BuildDirectory", + cmSystemTools::CollapseFullPath( + this->Values[ct_BUILD]).c_str()); + } + else + { + this->CTest->SetCTestConfiguration("BuildDirectory", + cmSystemTools::CollapseFullPath( + this->Makefile->GetSafeDefinition("CTEST_BINARY_DIRECTORY")).c_str()); + } + + if ( this->Values[ct_SOURCE] ) + { + this->CTest->SetCTestConfiguration("SourceDirectory", + cmSystemTools::CollapseFullPath( + this->Values[ct_SOURCE]).c_str()); + } + else + { + this->CTest->SetCTestConfiguration("SourceDirectory", + cmSystemTools::CollapseFullPath( + this->Makefile->GetSafeDefinition("CTEST_SOURCE_DIRECTORY")).c_str()); + } + + if ( this->CTest->GetCTestConfiguration("BuildDirectory").empty() ) + { + this->SetError("Build directory not specified. Either use BUILD " + "argument to CTEST_CONFIGURE command or set CTEST_BINARY_DIRECTORY " + "variable"); + return 0; + } + + const char* ctestConfigureCommand + = this->Makefile->GetDefinition("CTEST_CONFIGURE_COMMAND"); + + if ( ctestConfigureCommand && *ctestConfigureCommand ) + { + this->CTest->SetCTestConfiguration("ConfigureCommand", + ctestConfigureCommand); + } + else + { + const char* cmakeGeneratorName + = this->Makefile->GetDefinition("CTEST_CMAKE_GENERATOR"); + if ( cmakeGeneratorName && *cmakeGeneratorName ) + { + const std::string& source_dir + = this->CTest->GetCTestConfiguration("SourceDirectory"); + if ( source_dir.empty() ) + { + this->SetError("Source directory not specified. Either use SOURCE " + "argument to CTEST_CONFIGURE command or set CTEST_SOURCE_DIRECTORY " + "variable"); + return 0; + } + + const std::string cmakelists_file = source_dir + "/CMakeLists.txt"; + if ( !cmSystemTools::FileExists(cmakelists_file.c_str()) ) + { + cmOStringStream e; + e << "CMakeLists.txt file does not exist [" + << cmakelists_file << "]"; + this->SetError(e.str().c_str()); + return 0; + } + + bool multiConfig = false; + bool cmakeBuildTypeInOptions = false; + + cmGlobalGenerator *gg = + this->Makefile->GetCMakeInstance()->CreateGlobalGenerator( + cmakeGeneratorName); + if(gg) + { + multiConfig = gg->IsMultiConfig(); + delete gg; + } + + std::string cmakeConfigureCommand = "\""; + cmakeConfigureCommand += this->CTest->GetCMakeExecutable(); + cmakeConfigureCommand += "\""; + + std::vector<std::string>::const_iterator it; + std::string option; + for (it= options.begin(); it!=options.end(); ++it) + { + option = *it; + + cmakeConfigureCommand += " \""; + cmakeConfigureCommand += option; + cmakeConfigureCommand += "\""; + + if ((0 != strstr(option.c_str(), "CMAKE_BUILD_TYPE=")) || + (0 != strstr(option.c_str(), "CMAKE_BUILD_TYPE:STRING="))) + { + cmakeBuildTypeInOptions = true; + } + } + + if (!multiConfig && !cmakeBuildTypeInOptions && + !this->CTest->GetConfigType().empty()) + { + cmakeConfigureCommand += " \"-DCMAKE_BUILD_TYPE:STRING="; + cmakeConfigureCommand += this->CTest->GetConfigType(); + cmakeConfigureCommand += "\""; + } + + cmakeConfigureCommand += " \"-G"; + cmakeConfigureCommand += cmakeGeneratorName; + cmakeConfigureCommand += "\""; + + cmakeConfigureCommand += " \""; + cmakeConfigureCommand += source_dir; + cmakeConfigureCommand += "\""; + + this->CTest->SetCTestConfiguration("ConfigureCommand", + cmakeConfigureCommand.c_str()); + } + else + { + this->SetError("Configure command is not specified. If this is a " + "\"built with CMake\" project, set CTEST_CMAKE_GENERATOR. If not, " + "set CTEST_CONFIGURE_COMMAND."); + return 0; + } + } + + cmCTestGenericHandler* handler + = this->CTest->GetInitializedHandler("configure"); + if ( !handler ) + { + this->SetError( + "internal CTest error. Cannot instantiate configure handler"); + return 0; + } + return handler; +} diff --git a/Source/CTest/cmCTestConfigureCommand.h b/Source/CTest/cmCTestConfigureCommand.h new file mode 100644 index 000000000..b343fc1e9 --- /dev/null +++ b/Source/CTest/cmCTestConfigureCommand.h @@ -0,0 +1,84 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2009 Kitware, Inc., Insight Software Consortium + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ +#ifndef cmCTestConfigureCommand_h +#define cmCTestConfigureCommand_h + +#include "cmCTestHandlerCommand.h" + +/** \class cmCTestConfigure + * \brief Run a ctest script + * + * cmCTestConfigureCommand defineds the command to configures the project. + */ +class cmCTestConfigureCommand : public cmCTestHandlerCommand +{ +public: + cmCTestConfigureCommand(); + + /** + * This is a virtual constructor for the command. + */ + virtual cmCommand* Clone() + { + cmCTestConfigureCommand* ni = new cmCTestConfigureCommand; + ni->CTest = this->CTest; + ni->CTestScriptHandler = this->CTestScriptHandler; + return ni; + } + + /** + * The name of the command as specified in CMakeList.txt. + */ + virtual const char* GetName() const { return "ctest_configure";} + + /** + * Succinct documentation. + */ + virtual const char* GetTerseDocumentation() const + { + return "Configure the project build tree."; + } + + /** + * More documentation. + */ + virtual const char* GetFullDocumentation() const + { + return + " ctest_configure([BUILD build_dir] [SOURCE source_dir] [APPEND]\n" + " [OPTIONS options] [RETURN_VALUE res])\n" + "Configures the given build directory and stores results in " + "Configure.xml. " + "If no BUILD is given, the CTEST_BINARY_DIRECTORY variable is used. " + "If no SOURCE is given, the CTEST_SOURCE_DIRECTORY variable is used. " + "The OPTIONS argument specifies command line arguments to pass to " + "the configuration tool. " + "The RETURN_VALUE option specifies a variable in which to store the " + "return value of the native build tool." + "\n" + CTEST_COMMAND_APPEND_OPTION_DOCS; + } + + cmTypeMacro(cmCTestConfigureCommand, cmCTestHandlerCommand); + +protected: + cmCTestGenericHandler* InitializeHandler(); + + enum { + ctc_FIRST = ct_LAST, + ctc_OPTIONS, + ctc_LAST + }; +}; + + +#endif diff --git a/Source/CTest/cmCTestConfigureHandler.cxx b/Source/CTest/cmCTestConfigureHandler.cxx new file mode 100644 index 000000000..33297564e --- /dev/null +++ b/Source/CTest/cmCTestConfigureHandler.cxx @@ -0,0 +1,132 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2009 Kitware, Inc., Insight Software Consortium + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ + +#include "cmCTestConfigureHandler.h" + +#include "cmCTest.h" +#include "cmGeneratedFileStream.h" +#include "cmake.h" +#include "cmXMLSafe.h" +#include <cmsys/Process.h> + + +//---------------------------------------------------------------------- +cmCTestConfigureHandler::cmCTestConfigureHandler() +{ +} + +//---------------------------------------------------------------------- +void cmCTestConfigureHandler::Initialize() +{ + this->Superclass::Initialize(); +} + +//---------------------------------------------------------------------- +//clearly it would be nice if this were broken up into a few smaller +//functions and commented... +int cmCTestConfigureHandler::ProcessHandler() +{ + cmCTestLog(this->CTest, HANDLER_OUTPUT, "Configure project" << std::endl); + std::string cCommand + = this->CTest->GetCTestConfiguration("ConfigureCommand"); + if ( cCommand.size() == 0 ) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, + "Cannot find ConfigureCommand key in the DartConfiguration.tcl" + << std::endl); + return -1; + } + + std::string buildDirectory + = this->CTest->GetCTestConfiguration("BuildDirectory"); + if ( buildDirectory.size() == 0 ) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, + "Cannot find BuildDirectory key in the DartConfiguration.tcl" + << std::endl); + return -1; + } + + double elapsed_time_start = cmSystemTools::GetTime(); + std::string output; + int retVal = 0; + int res = 0; + if ( !this->CTest->GetShowOnly() ) + { + cmGeneratedFileStream os; + if(!this->StartResultingXML(cmCTest::PartConfigure, "Configure", os)) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, "Cannot open configure file" + << std::endl); + return 1; + } + std::string start_time = this->CTest->CurrentTime(); + unsigned int start_time_time = static_cast<unsigned int>( + cmSystemTools::GetTime()); + + cmGeneratedFileStream ofs; + this->StartLogFile("Configure", ofs); + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, "Configure with command: " + << cCommand.c_str() << std::endl); + res = this->CTest->RunMakeCommand(cCommand.c_str(), &output, + &retVal, buildDirectory.c_str(), + 0, ofs); + + if ( ofs ) + { + ofs.close(); + } + + if ( os ) + { + this->CTest->StartXML(os, this->AppendXML); + os << "<Configure>\n" + << "\t<StartDateTime>" << start_time << "</StartDateTime>" + << std::endl + << "\t<StartConfigureTime>" << start_time_time + << "</StartConfigureTime>\n"; + + if ( res == cmsysProcess_State_Exited && retVal ) + { + os << retVal; + } + os << "<ConfigureCommand>" << cCommand.c_str() << "</ConfigureCommand>" + << std::endl; + cmCTestLog(this->CTest, DEBUG, "End" << std::endl); + os << "<Log>" << cmXMLSafe(output) << "</Log>" << std::endl; + std::string end_time = this->CTest->CurrentTime(); + os << "\t<ConfigureStatus>" << retVal << "</ConfigureStatus>\n" + << "\t<EndDateTime>" << end_time << "</EndDateTime>\n" + << "\t<EndConfigureTime>" << + static_cast<unsigned int>(cmSystemTools::GetTime()) + << "</EndConfigureTime>\n" + << "<ElapsedMinutes>" + << static_cast<int>( + (cmSystemTools::GetTime() - elapsed_time_start)/6)/10.0 + << "</ElapsedMinutes>" + << "</Configure>" << std::endl; + this->CTest->EndXML(os); + } + } + else + { + cmCTestLog(this->CTest, DEBUG, "Configure with command: " << cCommand + << std::endl); + } + if (! res || retVal ) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, + "Error(s) when configuring the project" << std::endl); + return -1; + } + return 0; +} diff --git a/Source/CTest/cmCTestConfigureHandler.h b/Source/CTest/cmCTestConfigureHandler.h new file mode 100644 index 000000000..d880bd7df --- /dev/null +++ b/Source/CTest/cmCTestConfigureHandler.h @@ -0,0 +1,39 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2009 Kitware, Inc. + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ + +#ifndef cmCTestConfigureHandler_h +#define cmCTestConfigureHandler_h + + +#include "cmCTestGenericHandler.h" +#include "cmListFileCache.h" + +/** \class cmCTestConfigureHandler + * \brief A class that handles ctest -S invocations + * + */ +class cmCTestConfigureHandler : public cmCTestGenericHandler +{ +public: + cmTypeMacro(cmCTestConfigureHandler, cmCTestGenericHandler); + + /* + * The main entry point for this class + */ + int ProcessHandler(); + + cmCTestConfigureHandler(); + + void Initialize(); +}; + +#endif diff --git a/Source/CTest/cmCTestCoverageCommand.cxx b/Source/CTest/cmCTestCoverageCommand.cxx new file mode 100644 index 000000000..72ff720d3 --- /dev/null +++ b/Source/CTest/cmCTestCoverageCommand.cxx @@ -0,0 +1,73 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2009 Kitware, Inc., Insight Software Consortium + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ +#include "cmCTestCoverageCommand.h" + +#include "cmCTest.h" +#include "cmCTestCoverageHandler.h" + +//---------------------------------------------------------------------------- +cmCTestCoverageCommand::cmCTestCoverageCommand() +{ + this->LabelsMentioned = false; +} + +//---------------------------------------------------------------------------- +cmCTestGenericHandler* cmCTestCoverageCommand::InitializeHandler() +{ + this->CTest->SetCTestConfigurationFromCMakeVariable(this->Makefile, + "CoverageCommand", "CTEST_COVERAGE_COMMAND"); + + cmCTestCoverageHandler* handler = static_cast<cmCTestCoverageHandler*>( + this->CTest->GetInitializedHandler("coverage")); + if ( !handler ) + { + this->SetError("internal CTest error. Cannot instantiate test handler"); + return 0; + } + + // If a LABELS option was given, select only files with the labels. + if(this->LabelsMentioned) + { + handler->SetLabelFilter(this->Labels); + } + + return handler; +} + +//---------------------------------------------------------------------------- +bool cmCTestCoverageCommand::CheckArgumentKeyword(std::string const& arg) +{ + // Look for arguments specific to this command. + if(arg == "LABELS") + { + this->ArgumentDoing = ArgumentDoingLabels; + this->LabelsMentioned = true; + return true; + } + + // Look for other arguments. + return this->Superclass::CheckArgumentKeyword(arg); +} + +//---------------------------------------------------------------------------- +bool cmCTestCoverageCommand::CheckArgumentValue(std::string const& arg) +{ + // Handle states specific to this command. + if(this->ArgumentDoing == ArgumentDoingLabels) + { + this->Labels.insert(arg); + return true; + } + + // Look for other arguments. + return this->Superclass::CheckArgumentValue(arg); +} diff --git a/Source/CTest/cmCTestCoverageCommand.h b/Source/CTest/cmCTestCoverageCommand.h new file mode 100644 index 000000000..2fe762c45 --- /dev/null +++ b/Source/CTest/cmCTestCoverageCommand.h @@ -0,0 +1,90 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2009 Kitware, Inc., Insight Software Consortium + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ +#ifndef cmCTestCoverageCommand_h +#define cmCTestCoverageCommand_h + +#include "cmCTestHandlerCommand.h" + +/** \class cmCTestCoverage + * \brief Run a ctest script + * + * cmCTestCoverageCommand defineds the command to test the project. + */ +class cmCTestCoverageCommand : public cmCTestHandlerCommand +{ +public: + + cmCTestCoverageCommand(); + + /** + * This is a virtual constructor for the command. + */ + virtual cmCommand* Clone() + { + cmCTestCoverageCommand* ni = new cmCTestCoverageCommand; + ni->CTest = this->CTest; + ni->CTestScriptHandler = this->CTestScriptHandler; + return ni; + } + + /** + * The name of the command as specified in CMakeList.txt. + */ + virtual const char* GetName() const { return "ctest_coverage";} + + /** + * Succinct documentation. + */ + virtual const char* GetTerseDocumentation() const + { + return "Collect coverage tool results."; + } + + /** + * More documentation. + */ + virtual const char* GetFullDocumentation() const + { + return + " ctest_coverage([BUILD build_dir] [RETURN_VALUE res] [APPEND]\n" + " [LABELS label1 [label2 [...]]])\n" + "Perform the coverage of the given build directory and stores results " + "in Coverage.xml. The second argument is a variable that will hold " + "value." + "\n" + "The LABELS option filters the coverage report to include only " + "source files labeled with at least one of the labels specified." + "\n" + CTEST_COMMAND_APPEND_OPTION_DOCS; + } + + cmTypeMacro(cmCTestCoverageCommand, cmCTestHandlerCommand); + +protected: + cmCTestGenericHandler* InitializeHandler(); + + virtual bool CheckArgumentKeyword(std::string const& arg); + virtual bool CheckArgumentValue(std::string const& arg); + + enum + { + ArgumentDoingLabels = Superclass::ArgumentDoingLast1, + ArgumentDoingLast2 + }; + + bool LabelsMentioned; + std::set<cmStdString> Labels; +}; + + +#endif + diff --git a/Source/CTest/cmCTestCoverageHandler.cxx b/Source/CTest/cmCTestCoverageHandler.cxx new file mode 100644 index 000000000..81d366937 --- /dev/null +++ b/Source/CTest/cmCTestCoverageHandler.cxx @@ -0,0 +1,2144 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2009 Kitware, Inc., Insight Software Consortium + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ +#include "cmCTestCoverageHandler.h" +#include "cmParsePHPCoverage.h" +#include "cmParseGTMCoverage.h" +#include "cmParseCacheCoverage.h" +#include "cmCTest.h" +#include "cmake.h" +#include "cmMakefile.h" +#include "cmSystemTools.h" +#include "cmGeneratedFileStream.h" +#include "cmXMLSafe.h" + +#include <cmsys/Process.h> +#include <cmsys/RegularExpression.hxx> +#include <cmsys/Glob.hxx> +#include <cmsys/stl/iterator> +#include <cmsys/stl/algorithm> + +#include <stdlib.h> +#include <math.h> +#include <float.h> + +#define SAFEDIV(x,y) (((y)!=0)?((x)/(y)):(0)) + +class cmCTestRunProcess +{ +public: + cmCTestRunProcess() + { + this->Process = cmsysProcess_New(); + this->PipeState = -1; + this->TimeOut = -1; + } + ~cmCTestRunProcess() + { + if(!(this->PipeState == -1) + && !(this->PipeState == cmsysProcess_Pipe_None ) + && !(this->PipeState == cmsysProcess_Pipe_Timeout)) + { + this->WaitForExit(); + } + cmsysProcess_Delete(this->Process); + } + void SetCommand(const char* command) + { + this->CommandLineStrings.clear(); + this->CommandLineStrings.push_back(command);; + } + void AddArgument(const char* arg) + { + if(arg) + { + this->CommandLineStrings.push_back(arg); + } + } + void SetWorkingDirectory(const char* dir) + { + this->WorkingDirectory = dir; + } + void SetTimeout(double t) + { + this->TimeOut = t; + } + bool StartProcess() + { + std::vector<const char*> args; + for(std::vector<std::string>::iterator i = + this->CommandLineStrings.begin(); + i != this->CommandLineStrings.end(); ++i) + { + args.push_back(i->c_str()); + } + args.push_back(0); // null terminate + cmsysProcess_SetCommand(this->Process, &*args.begin()); + if(this->WorkingDirectory.size()) + { + cmsysProcess_SetWorkingDirectory(this->Process, + this->WorkingDirectory.c_str()); + } + + cmsysProcess_SetOption(this->Process, + cmsysProcess_Option_HideWindow, 1); + if(this->TimeOut != -1) + { + cmsysProcess_SetTimeout(this->Process, this->TimeOut); + } + cmsysProcess_Execute(this->Process); + this->PipeState = cmsysProcess_GetState(this->Process); + // if the process is running or exited return true + if(this->PipeState == cmsysProcess_State_Executing + || this->PipeState == cmsysProcess_State_Exited) + { + return true; + } + return false; + } + void SetStdoutFile(const char* fname) + { + cmsysProcess_SetPipeFile(this->Process, cmsysProcess_Pipe_STDOUT, fname); + } + void SetStderrFile(const char* fname) + { + cmsysProcess_SetPipeFile(this->Process, cmsysProcess_Pipe_STDERR, fname); + } + int WaitForExit(double* timeout =0) + { + this->PipeState = cmsysProcess_WaitForExit(this->Process, + timeout); + return this->PipeState; + } + int GetProcessState() { return this->PipeState;} +private: + int PipeState; + cmsysProcess* Process; + std::vector<std::string> CommandLineStrings; + std::string WorkingDirectory; + double TimeOut; +}; + + +//---------------------------------------------------------------------- + +//---------------------------------------------------------------------- +cmCTestCoverageHandler::cmCTestCoverageHandler() +{ +} + +//---------------------------------------------------------------------- +void cmCTestCoverageHandler::Initialize() +{ + this->Superclass::Initialize(); + this->CustomCoverageExclude.clear(); + this->SourceLabels.clear(); + this->LabelIdMap.clear(); + this->Labels.clear(); + this->LabelFilter.clear(); +} + +//---------------------------------------------------------------------------- +void cmCTestCoverageHandler::CleanCoverageLogFiles(std::ostream& log) +{ + std::string logGlob = this->CTest->GetCTestConfiguration("BuildDirectory"); + logGlob += "/Testing/"; + logGlob += this->CTest->GetCurrentTag(); + logGlob += "/CoverageLog*"; + cmsys::Glob gl; + gl.FindFiles(logGlob.c_str()); + std::vector<std::string> const& files = gl.GetFiles(); + for(std::vector<std::string>::const_iterator fi = files.begin(); + fi != files.end(); ++fi) + { + log << "Removing old coverage log: " << *fi << "\n"; + cmSystemTools::RemoveFile(fi->c_str()); + } +} + +//---------------------------------------------------------------------- +bool cmCTestCoverageHandler::StartCoverageLogFile( + cmGeneratedFileStream& covLogFile, int logFileCount) +{ + char covLogFilename[1024]; + sprintf(covLogFilename, "CoverageLog-%d", logFileCount); + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, "Open file: " + << covLogFilename << std::endl); + if(!this->StartResultingXML(cmCTest::PartCoverage, + covLogFilename, covLogFile)) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, "Cannot open log file: " + << covLogFilename << std::endl); + return false; + } + std::string local_start_time = this->CTest->CurrentTime(); + this->CTest->StartXML(covLogFile, this->AppendXML); + covLogFile << "<CoverageLog>" << std::endl + << "\t<StartDateTime>" << local_start_time << "</StartDateTime>" + << "\t<StartTime>" + << static_cast<unsigned int>(cmSystemTools::GetTime()) + << "</StartTime>" + << std::endl; + return true; +} + +//---------------------------------------------------------------------- +void cmCTestCoverageHandler::EndCoverageLogFile(cmGeneratedFileStream& ostr, + int logFileCount) +{ + std::string local_end_time = this->CTest->CurrentTime(); + ostr << "\t<EndDateTime>" << local_end_time << "</EndDateTime>" << std::endl + << "\t<EndTime>" << + static_cast<unsigned int>(cmSystemTools::GetTime()) + << "</EndTime>" << std::endl + << "</CoverageLog>" << std::endl; + this->CTest->EndXML(ostr); + char covLogFilename[1024]; + sprintf(covLogFilename, "CoverageLog-%d.xml", logFileCount); + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, "Close file: " + << covLogFilename << std::endl); + ostr.Close(); +} + +//---------------------------------------------------------------------- +bool cmCTestCoverageHandler::ShouldIDoCoverage(const char* file, + const char* srcDir, + const char* binDir) +{ + if(this->IsFilteredOut(file)) + { + return false; + } + + std::vector<cmsys::RegularExpression>::iterator sit; + for ( sit = this->CustomCoverageExcludeRegex.begin(); + sit != this->CustomCoverageExcludeRegex.end(); ++ sit ) + { + if ( sit->find(file) ) + { + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, " File " << file + << " is excluded in CTestCustom.ctest" << std::endl;); + return false; + } + } + + std::string fSrcDir = cmSystemTools::CollapseFullPath(srcDir); + std::string fBinDir = cmSystemTools::CollapseFullPath(binDir); + std::string fFile = cmSystemTools::CollapseFullPath(file); + bool sourceSubDir = cmSystemTools::IsSubDirectory(fFile.c_str(), + fSrcDir.c_str()); + bool buildSubDir = cmSystemTools::IsSubDirectory(fFile.c_str(), + fBinDir.c_str()); + // Always check parent directory of the file. + std::string fileDir = cmSystemTools::GetFilenamePath(fFile.c_str()); + std::string checkDir; + + // We also need to check the binary/source directory pair. + if ( sourceSubDir && buildSubDir ) + { + if ( fSrcDir.size() > fBinDir.size() ) + { + checkDir = fSrcDir; + } + else + { + checkDir = fBinDir; + } + } + else if ( sourceSubDir ) + { + checkDir = fSrcDir; + } + else if ( buildSubDir ) + { + checkDir = fBinDir; + } + std::string ndc + = cmSystemTools::FileExistsInParentDirectories(".NoDartCoverage", + fFile.c_str(), checkDir.c_str()); + if ( ndc.size() ) + { + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, "Found: " << ndc.c_str() + << " so skip coverage of " << file << std::endl); + return false; + } + + // By now checkDir should be set to parent directory of the file. + // Get the relative path to the file an apply it to the opposite directory. + // If it is the same as fileDir, then ignore, otherwise check. + std::string relPath; + if(checkDir.size() ) + { + relPath = cmSystemTools::RelativePath(checkDir.c_str(), + fFile.c_str()); + } + else + { + relPath = fFile; + } + if ( checkDir == fSrcDir ) + { + checkDir = fBinDir; + } + else + { + checkDir = fSrcDir; + } + fFile = checkDir + "/" + relPath; + fFile = cmSystemTools::GetFilenamePath(fFile.c_str()); + + if ( fileDir == fFile ) + { + // This is in-source build, so we trust the previous check. + return true; + } + + ndc = cmSystemTools::FileExistsInParentDirectories(".NoDartCoverage", + fFile.c_str(), checkDir.c_str()); + if ( ndc.size() ) + { + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, "Found: " << ndc.c_str() + << " so skip coverage of: " << file << std::endl); + return false; + } + // Ok, nothing in source tree, nothing in binary tree + return true; +} + +//---------------------------------------------------------------------- +//clearly it would be nice if this were broken up into a few smaller +//functions and commented... +int cmCTestCoverageHandler::ProcessHandler() +{ + this->CTest->ClearSubmitFiles(cmCTest::PartCoverage); + int error = 0; + // do we have time for this + if (this->CTest->GetRemainingTimeAllowed() < 120) + { + return error; + } + + std::string coverage_start_time = this->CTest->CurrentTime(); + unsigned int coverage_start_time_time = static_cast<unsigned int>( + cmSystemTools::GetTime()); + std::string sourceDir + = this->CTest->GetCTestConfiguration("SourceDirectory"); + std::string binaryDir + = this->CTest->GetCTestConfiguration("BuildDirectory"); + + this->LoadLabels(); + + cmGeneratedFileStream ofs; + double elapsed_time_start = cmSystemTools::GetTime(); + if ( !this->StartLogFile("Coverage", ofs) ) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, + "Cannot create LastCoverage.log file" << std::endl); + } + + ofs << "Performing coverage: " << elapsed_time_start << std::endl; + this->CleanCoverageLogFiles(ofs); + + cmSystemTools::ConvertToUnixSlashes(sourceDir); + cmSystemTools::ConvertToUnixSlashes(binaryDir); + + cmCTestLog(this->CTest, HANDLER_OUTPUT, "Performing coverage" << std::endl); + + cmCTestCoverageHandlerContainer cont; + cont.Error = error; + cont.SourceDir = sourceDir; + cont.BinaryDir = binaryDir; + cont.OFS = &ofs; + + // setup the regex exclude stuff + this->CustomCoverageExcludeRegex.clear(); + std::vector<cmStdString>::iterator rexIt; + for ( rexIt = this->CustomCoverageExclude.begin(); + rexIt != this->CustomCoverageExclude.end(); + ++ rexIt ) + { + this->CustomCoverageExcludeRegex.push_back( + cmsys::RegularExpression(rexIt->c_str())); + } + + if(this->HandleBullseyeCoverage(&cont)) + { + return cont.Error; + } + int file_count = 0; + file_count += this->HandleGCovCoverage(&cont); + error = cont.Error; + if ( file_count < 0 ) + { + return error; + } + file_count += this->HandleTracePyCoverage(&cont); + error = cont.Error; + if ( file_count < 0 ) + { + return error; + } + file_count += this->HandlePHPCoverage(&cont); + error = cont.Error; + if ( file_count < 0 ) + { + return error; + } + file_count += this->HandleMumpsCoverage(&cont); + error = cont.Error; + if ( file_count < 0 ) + { + return error; + } + + std::set<std::string> uncovered = this->FindUncoveredFiles(&cont); + + if ( file_count == 0 ) + { + cmCTestLog(this->CTest, WARNING, + " Cannot find any coverage files. Ignoring Coverage request." + << std::endl); + return error; + } + cmGeneratedFileStream covSumFile; + cmGeneratedFileStream covLogFile; + + if(!this->StartResultingXML(cmCTest::PartCoverage, "Coverage", covSumFile)) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, + "Cannot open coverage summary file." << std::endl); + return -1; + } + + this->CTest->StartXML(covSumFile, this->AppendXML); + // Produce output xml files + + covSumFile << "<Coverage>" << std::endl + << "\t<StartDateTime>" << coverage_start_time << "</StartDateTime>" + << std::endl + << "\t<StartTime>" << coverage_start_time_time << "</StartTime>" + << std::endl; + int logFileCount = 0; + if ( !this->StartCoverageLogFile(covLogFile, logFileCount) ) + { + return -1; + } + cmCTestCoverageHandlerContainer::TotalCoverageMap::iterator fileIterator; + int cnt = 0; + long total_tested = 0; + long total_untested = 0; + //std::string fullSourceDir = sourceDir + "/"; + //std::string fullBinaryDir = binaryDir + "/"; + cmCTestLog(this->CTest, HANDLER_OUTPUT, std::endl); + cmCTestLog(this->CTest, HANDLER_OUTPUT, + " Accumulating results (each . represents one file):" << std::endl); + cmCTestLog(this->CTest, HANDLER_OUTPUT, " "); + + std::vector<std::string> errorsWhileAccumulating; + + file_count = 0; + for ( fileIterator = cont.TotalCoverage.begin(); + fileIterator != cont.TotalCoverage.end(); + ++fileIterator ) + { + cmCTestLog(this->CTest, HANDLER_OUTPUT, "." << std::flush); + file_count ++; + if ( file_count % 50 == 0 ) + { + cmCTestLog(this->CTest, HANDLER_OUTPUT, " processed: " << file_count + << " out of " + << cont.TotalCoverage.size() << std::endl); + cmCTestLog(this->CTest, HANDLER_OUTPUT, " "); + } + + const std::string fullFileName = fileIterator->first; + bool shouldIDoCoverage + = this->ShouldIDoCoverage(fullFileName.c_str(), + sourceDir.c_str(), binaryDir.c_str()); + if ( !shouldIDoCoverage ) + { + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, + ".NoDartCoverage found, so skip coverage check for: " + << fullFileName.c_str() + << std::endl); + continue; + } + + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, + "Process file: " << fullFileName << std::endl); + + if ( !cmSystemTools::FileExists(fullFileName.c_str()) ) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, "Cannot find file: " + << fullFileName.c_str() << std::endl); + continue; + } + + if ( ++cnt % 100 == 0 ) + { + this->EndCoverageLogFile(covLogFile, logFileCount); + logFileCount ++; + if ( !this->StartCoverageLogFile(covLogFile, logFileCount) ) + { + return -1; + } + } + + const std::string fileName + = cmSystemTools::GetFilenameName(fullFileName.c_str()); + std::string shortFileName = + this->CTest->GetShortPathToFile(fullFileName.c_str()); + const cmCTestCoverageHandlerContainer::SingleFileCoverageVector& fcov + = fileIterator->second; + covLogFile << "\t<File Name=\"" << cmXMLSafe(fileName) + << "\" FullPath=\"" << cmXMLSafe(shortFileName) << "\">\n" + << "\t\t<Report>" << std::endl; + + std::ifstream ifs(fullFileName.c_str()); + if ( !ifs) + { + cmOStringStream ostr; + ostr << "Cannot open source file: " << fullFileName.c_str(); + errorsWhileAccumulating.push_back(ostr.str()); + error ++; + continue; + } + + int tested = 0; + int untested = 0; + + cmCTestCoverageHandlerContainer::SingleFileCoverageVector::size_type cc; + std::string line; + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, + "Actually performing coverage for: " << fullFileName << std::endl); + for ( cc= 0; cc < fcov.size(); cc ++ ) + { + if ( !cmSystemTools::GetLineFromStream(ifs, line) && + cc != fcov.size() -1 ) + { + cmOStringStream ostr; + ostr << "Problem reading source file: " << fullFileName.c_str() + << " line:" << cc << " out total: " << fcov.size()-1; + errorsWhileAccumulating.push_back(ostr.str()); + error ++; + break; + } + covLogFile << "\t\t<Line Number=\"" << cc << "\" Count=\"" << fcov[cc] + << "\">" + << cmXMLSafe(line) << "</Line>" << std::endl; + if ( fcov[cc] == 0 ) + { + untested ++; + } + else if ( fcov[cc] > 0 ) + { + tested ++; + } + } + if ( cmSystemTools::GetLineFromStream(ifs, line) ) + { + cmOStringStream ostr; + ostr << "Looks like there are more lines in the file: " << line; + errorsWhileAccumulating.push_back(ostr.str()); + } + float cper = 0; + float cmet = 0; + if ( tested + untested > 0 ) + { + cper = (100 * SAFEDIV(static_cast<float>(tested), + static_cast<float>(tested + untested))); + cmet = ( SAFEDIV(static_cast<float>(tested + 10), + static_cast<float>(tested + untested + 10))); + } + total_tested += tested; + total_untested += untested; + covLogFile << "\t\t</Report>" << std::endl + << "\t</File>" << std::endl; + covSumFile << "\t<File Name=\"" << cmXMLSafe(fileName) + << "\" FullPath=\"" << cmXMLSafe( + this->CTest->GetShortPathToFile(fullFileName.c_str())) + << "\" Covered=\"" << (tested+untested > 0 ? "true":"false") << "\">\n" + << "\t\t<LOCTested>" << tested << "</LOCTested>\n" + << "\t\t<LOCUnTested>" << untested << "</LOCUnTested>\n" + << "\t\t<PercentCoverage>"; + covSumFile.setf(std::ios::fixed, std::ios::floatfield); + covSumFile.precision(2); + covSumFile << (cper) << "</PercentCoverage>\n" + << "\t\t<CoverageMetric>"; + covSumFile.setf(std::ios::fixed, std::ios::floatfield); + covSumFile.precision(2); + covSumFile << (cmet) << "</CoverageMetric>\n"; + this->WriteXMLLabels(covSumFile, shortFileName); + covSumFile << "\t</File>" << std::endl; + } + + //Handle all the files in the extra coverage globs that have no cov data + for(std::set<std::string>::iterator i = uncovered.begin(); + i != uncovered.end(); ++i) + { + std::string fileName = cmSystemTools::GetFilenameName(*i); + std::string fullPath = cont.SourceDir + "/" + *i; + + covLogFile << "\t<File Name=\"" << cmXMLSafe(fileName) + << "\" FullPath=\"" << cmXMLSafe(*i) << "\">\n" + << "\t\t<Report>" << std::endl; + + std::ifstream ifs(fullPath.c_str()); + if (!ifs) + { + cmOStringStream ostr; + ostr << "Cannot open source file: " << fullPath.c_str(); + errorsWhileAccumulating.push_back(ostr.str()); + error ++; + continue; + } + int untested = 0; + std::string line; + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, + "Actually performing coverage for: " << i->c_str() << std::endl); + while (cmSystemTools::GetLineFromStream(ifs, line)) + { + covLogFile << "\t\t<Line Number=\"" << untested << "\" Count=\"0\">" + << cmXMLSafe(line) << "</Line>" << std::endl; + untested ++; + } + covLogFile << "\t\t</Report>\n\t</File>" << std::endl; + + total_untested += untested; + covSumFile << "\t<File Name=\"" << cmXMLSafe(fileName) + << "\" FullPath=\"" << cmXMLSafe(i->c_str()) + << "\" Covered=\"true\">\n" + << "\t\t<LOCTested>0</LOCTested>\n" + << "\t\t<LOCUnTested>" << untested << "</LOCUnTested>\n" + << "\t\t<PercentCoverage>0</PercentCoverage>\n" + << "\t\t<CoverageMetric>0</CoverageMetric>\n"; + this->WriteXMLLabels(covSumFile, *i); + covSumFile << "\t</File>" << std::endl; + } + + this->EndCoverageLogFile(covLogFile, logFileCount); + + if ( errorsWhileAccumulating.size() > 0 ) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, std::endl); + cmCTestLog(this->CTest, ERROR_MESSAGE, + "Error(s) while accumulating results:" << std::endl); + std::vector<std::string>::iterator erIt; + for ( erIt = errorsWhileAccumulating.begin(); + erIt != errorsWhileAccumulating.end(); + ++ erIt ) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, + " " << erIt->c_str() << std::endl); + } + } + + long total_lines = total_tested + total_untested; + float percent_coverage = 100 * SAFEDIV(static_cast<float>(total_tested), + static_cast<float>(total_lines)); + if ( total_lines == 0 ) + { + percent_coverage = 0; + } + + std::string end_time = this->CTest->CurrentTime(); + + covSumFile << "\t<LOCTested>" << total_tested << "</LOCTested>\n" + << "\t<LOCUntested>" << total_untested << "</LOCUntested>\n" + << "\t<LOC>" << total_lines << "</LOC>\n" + << "\t<PercentCoverage>"; + covSumFile.setf(std::ios::fixed, std::ios::floatfield); + covSumFile.precision(2); + covSumFile << (percent_coverage)<< "</PercentCoverage>\n" + << "\t<EndDateTime>" << end_time << "</EndDateTime>\n" + << "\t<EndTime>" << + static_cast<unsigned int>(cmSystemTools::GetTime()) + << "</EndTime>\n"; + covSumFile << "<ElapsedMinutes>" << + static_cast<int>((cmSystemTools::GetTime() - elapsed_time_start)/6)/10.0 + << "</ElapsedMinutes>" + << "</Coverage>" << std::endl; + this->CTest->EndXML(covSumFile); + + cmCTestLog(this->CTest, HANDLER_OUTPUT, "" << std::endl + << "\tCovered LOC: " + << total_tested << std::endl + << "\tNot covered LOC: " << total_untested << std::endl + << "\tTotal LOC: " << total_lines << std::endl + << "\tPercentage Coverage: " + << std::setiosflags(std::ios::fixed) + << std::setprecision(2) + << (percent_coverage) << "%" << std::endl); + + ofs << "\tCovered LOC: " << total_tested << std::endl + << "\tNot covered LOC: " << total_untested << std::endl + << "\tTotal LOC: " << total_lines << std::endl + << "\tPercentage Coverage: " + << std::setiosflags(std::ios::fixed) + << std::setprecision(2) + << (percent_coverage) << "%" << std::endl; + + + if ( error ) + { + return -1; + } + return 0; +} + +//---------------------------------------------------------------------- +void cmCTestCoverageHandler::PopulateCustomVectors(cmMakefile *mf) +{ + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, + " Add coverage exclude regular expressions." << std::endl); + this->CTest->PopulateCustomVector(mf, "CTEST_CUSTOM_COVERAGE_EXCLUDE", + this->CustomCoverageExclude); + this->CTest->PopulateCustomVector(mf, "CTEST_EXTRA_COVERAGE_GLOB", + this->ExtraCoverageGlobs); + std::vector<cmStdString>::iterator it; + for ( it = this->CustomCoverageExclude.begin(); + it != this->CustomCoverageExclude.end(); + ++ it ) + { + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, " Add coverage exclude: " + << it->c_str() << std::endl); + } + for ( it = this->ExtraCoverageGlobs.begin(); + it != this->ExtraCoverageGlobs.end(); ++it) + { + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, " Add coverage glob: " + << it->c_str() << std::endl); + } +} + +//---------------------------------------------------------------------- +// Fix for issue #4971 where the case of the drive letter component of +// the filenames might be different when analyzing gcov output. +// +// Compare file names: fnc(fn1) == fnc(fn2) // fnc == file name compare +// +#ifdef _WIN32 +#define fnc(s) cmSystemTools::LowerCase(s) +#else +#define fnc(s) s +#endif + +//---------------------------------------------------------------------- +bool IsFileInDir(const std::string &infile, const std::string &indir) +{ + std::string file = cmSystemTools::CollapseFullPath(infile.c_str()); + std::string dir = cmSystemTools::CollapseFullPath(indir.c_str()); + + if ( + file.size() > dir.size() && + (fnc(file.substr(0, dir.size())) == fnc(dir)) && + file[dir.size()] == '/' + ) + { + return true; + } + + return false; +} + +//---------------------------------------------------------------------- +int cmCTestCoverageHandler::HandlePHPCoverage( + cmCTestCoverageHandlerContainer* cont) +{ + cmParsePHPCoverage cov(*cont, this->CTest); + std::string coverageDir = this->CTest->GetBinaryDir() + "/xdebugCoverage"; + if(cmSystemTools::FileIsDirectory(coverageDir.c_str())) + { + cov.ReadPHPCoverageDirectory(coverageDir.c_str()); + } + return static_cast<int>(cont->TotalCoverage.size()); +} +//---------------------------------------------------------------------- +int cmCTestCoverageHandler::HandleMumpsCoverage( + cmCTestCoverageHandlerContainer* cont) +{ + // try gtm coverage + cmParseGTMCoverage cov(*cont, this->CTest); + std::string coverageFile = this->CTest->GetBinaryDir() + + "/gtm_coverage.mcov"; + if(cmSystemTools::FileExists(coverageFile.c_str())) + { + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, + "Parsing Cache Coverage: " << coverageFile + << std::endl); + cov.ReadCoverageFile(coverageFile.c_str()); + return static_cast<int>(cont->TotalCoverage.size()); + } + else + { + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, + " Cannot find foobar GTM coverage file: " << coverageFile + << std::endl); + } + cmParseCacheCoverage ccov(*cont, this->CTest); + coverageFile = this->CTest->GetBinaryDir() + + "/cache_coverage.cmcov"; + if(cmSystemTools::FileExists(coverageFile.c_str())) + { + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, + "Parsing Cache Coverage: " << coverageFile + << std::endl); + ccov.ReadCoverageFile(coverageFile.c_str()); + } + else + { + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, + " Cannot find Cache coverage file: " << coverageFile + << std::endl); + } + return static_cast<int>(cont->TotalCoverage.size()); +} + +struct cmCTestCoverageHandlerLocale +{ + cmCTestCoverageHandlerLocale() + { + if(const char* l = cmSystemTools::GetEnv("LC_ALL")) + { + lc_all = l; + } + if(lc_all != "C") + { + cmSystemTools::PutEnv("LC_ALL=C"); + } + } + ~cmCTestCoverageHandlerLocale() + { + if(!lc_all.empty()) + { + cmSystemTools::PutEnv(("LC_ALL=" + lc_all).c_str()); + } + else + { + cmSystemTools::UnsetEnv("LC_ALL"); + } + } + std::string lc_all; +}; + +//---------------------------------------------------------------------- +int cmCTestCoverageHandler::HandleGCovCoverage( + cmCTestCoverageHandlerContainer* cont) +{ + std::string gcovCommand + = this->CTest->GetCTestConfiguration("CoverageCommand"); + std::string gcovExtraFlags + = this->CTest->GetCTestConfiguration("CoverageExtraFlags"); + + // Style 1 + std::string st1gcovOutputRex1 + = "[0-9]+\\.[0-9]+% of [0-9]+ (source |)lines executed in file (.*)$"; + std::string st1gcovOutputRex2 = "^Creating (.*\\.gcov)\\."; + cmsys::RegularExpression st1re1(st1gcovOutputRex1.c_str()); + cmsys::RegularExpression st1re2(st1gcovOutputRex2.c_str()); + + + // Style 2 + std::string st2gcovOutputRex1 = "^File *[`'](.*)'$"; + std::string st2gcovOutputRex2 + = "Lines executed: *[0-9]+\\.[0-9]+% of [0-9]+$"; + std::string st2gcovOutputRex3 = "^(.*)reating [`'](.*\\.gcov)'"; + std::string st2gcovOutputRex4 = "^(.*):unexpected EOF *$"; + std::string st2gcovOutputRex5 = "^(.*):cannot open source file*$"; + std::string st2gcovOutputRex6 + = "^(.*):source file is newer than graph file `(.*)'$"; + cmsys::RegularExpression st2re1(st2gcovOutputRex1.c_str()); + cmsys::RegularExpression st2re2(st2gcovOutputRex2.c_str()); + cmsys::RegularExpression st2re3(st2gcovOutputRex3.c_str()); + cmsys::RegularExpression st2re4(st2gcovOutputRex4.c_str()); + cmsys::RegularExpression st2re5(st2gcovOutputRex5.c_str()); + cmsys::RegularExpression st2re6(st2gcovOutputRex6.c_str()); + + std::vector<std::string> files; + this->FindGCovFiles(files); + std::vector<std::string>::iterator it; + + if ( files.size() == 0 ) + { + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, + " Cannot find any GCov coverage files." + << std::endl); + // No coverage files is a valid thing, so the exit code is 0 + return 0; + } + + std::string testingDir = this->CTest->GetBinaryDir() + "/Testing"; + std::string tempDir = testingDir + "/CoverageInfo"; + std::string currentDirectory = cmSystemTools::GetCurrentWorkingDirectory(); + cmSystemTools::MakeDirectory(tempDir.c_str()); + cmSystemTools::ChangeDirectory(tempDir.c_str()); + + int gcovStyle = 0; + + std::set<std::string> missingFiles; + + std::string actualSourceFile = ""; + cmCTestLog(this->CTest, HANDLER_OUTPUT, + " Processing coverage (each . represents one file):" << std::endl); + cmCTestLog(this->CTest, HANDLER_OUTPUT, " "); + int file_count = 0; + + // make sure output from gcov is in English! + cmCTestCoverageHandlerLocale locale_C; + static_cast<void>(locale_C); + + // files is a list of *.da and *.gcda files with coverage data in them. + // These are binary files that you give as input to gcov so that it will + // give us text output we can analyze to summarize coverage. + // + for ( it = files.begin(); it != files.end(); ++ it ) + { + cmCTestLog(this->CTest, HANDLER_OUTPUT, "." << std::flush); + + // Call gcov to get coverage data for this *.gcda file: + // + std::string fileDir = cmSystemTools::GetFilenamePath(it->c_str()); + std::string command = "\"" + gcovCommand + "\" " + + gcovExtraFlags + " " + + "-o \"" + fileDir + "\" " + + "\"" + *it + "\""; + + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, command.c_str() + << std::endl); + + std::string output = ""; + std::string errors = ""; + int retVal = 0; + *cont->OFS << "* Run coverage for: " << fileDir.c_str() << std::endl; + *cont->OFS << " Command: " << command.c_str() << std::endl; + int res = this->CTest->RunCommand(command.c_str(), &output, &errors, + &retVal, tempDir.c_str(), 0 /*this->TimeOut*/); + + *cont->OFS << " Output: " << output.c_str() << std::endl; + *cont->OFS << " Errors: " << errors.c_str() << std::endl; + if ( ! res ) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, + "Problem running coverage on file: " << it->c_str() << std::endl); + cmCTestLog(this->CTest, ERROR_MESSAGE, + "Command produced error: " << errors << std::endl); + cont->Error ++; + continue; + } + if ( retVal != 0 ) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, "Coverage command returned: " + << retVal << " while processing: " << it->c_str() << std::endl); + cmCTestLog(this->CTest, ERROR_MESSAGE, + "Command produced error: " << cont->Error << std::endl); + } + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, + "--------------------------------------------------------------" + << std::endl + << output << std::endl + << "--------------------------------------------------------------" + << std::endl); + + std::vector<cmStdString> lines; + std::vector<cmStdString>::iterator line; + + cmSystemTools::Split(output.c_str(), lines); + + for ( line = lines.begin(); line != lines.end(); ++line) + { + std::string sourceFile; + std::string gcovFile; + + cmCTestLog(this->CTest, DEBUG, "Line: [" << line->c_str() << "]" + << std::endl); + + if ( line->size() == 0 ) + { + // Ignore empty line; probably style 2 + } + else if ( st1re1.find(line->c_str()) ) + { + if ( gcovStyle == 0 ) + { + gcovStyle = 1; + } + if ( gcovStyle != 1 ) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, "Unknown gcov output style e1" + << std::endl); + cont->Error ++; + break; + } + + actualSourceFile = ""; + sourceFile = st1re1.match(2); + } + else if ( st1re2.find(line->c_str() ) ) + { + if ( gcovStyle == 0 ) + { + gcovStyle = 1; + } + if ( gcovStyle != 1 ) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, "Unknown gcov output style e2" + << std::endl); + cont->Error ++; + break; + } + + gcovFile = st1re2.match(1); + } + else if ( st2re1.find(line->c_str() ) ) + { + if ( gcovStyle == 0 ) + { + gcovStyle = 2; + } + if ( gcovStyle != 2 ) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, "Unknown gcov output style e3" + << std::endl); + cont->Error ++; + break; + } + + actualSourceFile = ""; + sourceFile = st2re1.match(1); + } + else if ( st2re2.find(line->c_str() ) ) + { + if ( gcovStyle == 0 ) + { + gcovStyle = 2; + } + if ( gcovStyle != 2 ) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, "Unknown gcov output style e4" + << std::endl); + cont->Error ++; + break; + } + } + else if ( st2re3.find(line->c_str() ) ) + { + if ( gcovStyle == 0 ) + { + gcovStyle = 2; + } + if ( gcovStyle != 2 ) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, "Unknown gcov output style e5" + << std::endl); + cont->Error ++; + break; + } + + gcovFile = st2re3.match(2); + } + else if ( st2re4.find(line->c_str() ) ) + { + if ( gcovStyle == 0 ) + { + gcovStyle = 2; + } + if ( gcovStyle != 2 ) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, "Unknown gcov output style e6" + << std::endl); + cont->Error ++; + break; + } + + cmCTestLog(this->CTest, WARNING, "Warning: " << st2re4.match(1) + << " had unexpected EOF" << std::endl); + } + else if ( st2re5.find(line->c_str() ) ) + { + if ( gcovStyle == 0 ) + { + gcovStyle = 2; + } + if ( gcovStyle != 2 ) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, "Unknown gcov output style e7" + << std::endl); + cont->Error ++; + break; + } + + cmCTestLog(this->CTest, WARNING, "Warning: Cannot open file: " + << st2re5.match(1) << std::endl); + } + else if ( st2re6.find(line->c_str() ) ) + { + if ( gcovStyle == 0 ) + { + gcovStyle = 2; + } + if ( gcovStyle != 2 ) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, "Unknown gcov output style e8" + << std::endl); + cont->Error ++; + break; + } + + cmCTestLog(this->CTest, WARNING, "Warning: File: " << st2re6.match(1) + << " is newer than " << st2re6.match(2) << std::endl); + } + else + { + cmCTestLog(this->CTest, ERROR_MESSAGE, + "Unknown gcov output line: [" << line->c_str() << "]" << std::endl); + cont->Error ++; + //abort(); + } + + + // If the last line of gcov output gave us a valid value for gcovFile, + // and we have an actualSourceFile, then insert a (or add to existing) + // SingleFileCoverageVector for actualSourceFile: + // + if ( !gcovFile.empty() && !actualSourceFile.empty() ) + { + cmCTestCoverageHandlerContainer::SingleFileCoverageVector& vec + = cont->TotalCoverage[actualSourceFile]; + + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, " in gcovFile: " + << gcovFile << std::endl); + + std::ifstream ifile(gcovFile.c_str()); + if ( ! ifile ) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, "Cannot open file: " + << gcovFile << std::endl); + } + else + { + long cnt = -1; + std::string nl; + while ( cmSystemTools::GetLineFromStream(ifile, nl) ) + { + cnt ++; + + //TODO: Handle gcov 3.0 non-coverage lines + + // Skip empty lines + if ( !nl.size() ) + { + continue; + } + + // Skip unused lines + if ( nl.size() < 12 ) + { + continue; + } + + // Read the coverage count from the beginning of the gcov output + // line + std::string prefix = nl.substr(0, 12); + int cov = atoi(prefix.c_str()); + + // Read the line number starting at the 10th character of the gcov + // output line + std::string lineNumber = nl.substr(10, 5); + + int lineIdx = atoi(lineNumber.c_str())-1; + if ( lineIdx >= 0 ) + { + while ( vec.size() <= static_cast<size_t>(lineIdx) ) + { + vec.push_back(-1); + } + + // Initially all entries are -1 (not used). If we get coverage + // information, increment it to 0 first. + if ( vec[lineIdx] < 0 ) + { + if ( cov > 0 || prefix.find("#") != prefix.npos ) + { + vec[lineIdx] = 0; + } + } + + vec[lineIdx] += cov; + } + } + } + + actualSourceFile = ""; + } + + + if ( !sourceFile.empty() && actualSourceFile.empty() ) + { + gcovFile = ""; + + // Is it in the source dir or the binary dir? + // + if ( IsFileInDir(sourceFile, cont->SourceDir) ) + { + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, " produced s: " + << sourceFile.c_str() << std::endl); + *cont->OFS << " produced in source dir: " << sourceFile.c_str() + << std::endl; + actualSourceFile + = cmSystemTools::CollapseFullPath(sourceFile.c_str()); + } + else if ( IsFileInDir(sourceFile, cont->BinaryDir) ) + { + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, " produced b: " + << sourceFile.c_str() << std::endl); + *cont->OFS << " produced in binary dir: " << sourceFile.c_str() + << std::endl; + actualSourceFile + = cmSystemTools::CollapseFullPath(sourceFile.c_str()); + } + + if ( actualSourceFile.empty() ) + { + if ( missingFiles.find(sourceFile) == missingFiles.end() ) + { + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, + "Something went wrong" << std::endl); + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, + "Cannot find file: [" + << sourceFile.c_str() << "]" << std::endl); + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, + " in source dir: [" + << cont->SourceDir.c_str() << "]" + << std::endl); + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, + " or binary dir: [" + << cont->BinaryDir.size() << "]" + << std::endl); + *cont->OFS << " Something went wrong. Cannot find file: " + << sourceFile.c_str() + << " in source dir: " << cont->SourceDir.c_str() + << " or binary dir: " << cont->BinaryDir.c_str() << std::endl; + + missingFiles.insert(sourceFile); + } + } + } + } + + file_count++; + + if ( file_count % 50 == 0 ) + { + cmCTestLog(this->CTest, HANDLER_OUTPUT, " processed: " << file_count + << " out of " << files.size() << std::endl); + cmCTestLog(this->CTest, HANDLER_OUTPUT, " "); + } + } + + cmSystemTools::ChangeDirectory(currentDirectory.c_str()); + return file_count; +} + +//---------------------------------------------------------------------------- +void cmCTestCoverageHandler::FindGCovFiles(std::vector<std::string>& files) +{ + cmsys::Glob gl; + gl.RecurseOn(); + gl.RecurseThroughSymlinksOff(); + + for(LabelMapType::const_iterator lmi = this->TargetDirs.begin(); + lmi != this->TargetDirs.end(); ++lmi) + { + // Skip targets containing no interesting labels. + if(!this->IntersectsFilter(lmi->second)) + { + continue; + } + + // Coverage files appear next to their object files in the target + // support directory. + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, + " globbing for coverage in: " << lmi->first << std::endl); + std::string daGlob = lmi->first; + daGlob += "/*.da"; + gl.FindFiles(daGlob); + files.insert(files.end(), gl.GetFiles().begin(), gl.GetFiles().end()); + daGlob = lmi->first; + daGlob += "/*.gcda"; + gl.FindFiles(daGlob); + files.insert(files.end(), gl.GetFiles().begin(), gl.GetFiles().end()); + } +} + +//---------------------------------------------------------------------- +int cmCTestCoverageHandler::HandleTracePyCoverage( + cmCTestCoverageHandlerContainer* cont) +{ + cmsys::Glob gl; + gl.RecurseOn(); + gl.RecurseThroughSymlinksOff(); + std::string daGlob = cont->BinaryDir + "/*.cover"; + gl.FindFiles(daGlob); + std::vector<std::string> files = gl.GetFiles(); + + if ( files.size() == 0 ) + { + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, + " Cannot find any Python Trace.py coverage files." + << std::endl); + // No coverage files is a valid thing, so the exit code is 0 + return 0; + } + + std::string testingDir = this->CTest->GetBinaryDir() + "/Testing"; + std::string tempDir = testingDir + "/CoverageInfo"; + std::string currentDirectory = cmSystemTools::GetCurrentWorkingDirectory(); + cmSystemTools::MakeDirectory(tempDir.c_str()); + cmSystemTools::ChangeDirectory(tempDir.c_str()); + + cmSystemTools::ChangeDirectory(currentDirectory.c_str()); + + std::vector<std::string>::iterator fileIt; + int file_count = 0; + for ( fileIt = files.begin(); fileIt != files.end(); ++ fileIt ) + { + std::string fileName = this->FindFile(cont, *fileIt); + if ( fileName.empty() ) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, + "Cannot find source Python file corresponding to: " + << fileIt->c_str() << std::endl); + continue; + } + + std::string actualSourceFile + = cmSystemTools::CollapseFullPath(fileName.c_str()); + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, + " Check coverage for file: " << actualSourceFile.c_str() + << std::endl); + cmCTestCoverageHandlerContainer::SingleFileCoverageVector* vec + = &cont->TotalCoverage[actualSourceFile]; + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, + " in file: " << fileIt->c_str() << std::endl); + std::ifstream ifile(fileIt->c_str()); + if ( ! ifile ) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, "Cannot open file: " + << fileIt->c_str() << std::endl); + } + else + { + long cnt = -1; + std::string nl; + while ( cmSystemTools::GetLineFromStream(ifile, nl) ) + { + cnt ++; + + // Skip empty lines + if ( !nl.size() ) + { + continue; + } + + // Skip unused lines + if ( nl.size() < 12 ) + { + continue; + } + + // Read the coverage count from the beginning of the Trace.py output + // line + std::string prefix = nl.substr(0, 6); + if ( prefix[5] != ' ' && prefix[5] != ':' ) + { + // This is a hack. We should really do something more elaborate + prefix = nl.substr(0, 7); + if ( prefix[6] != ' ' && prefix[6] != ':' ) + { + prefix = nl.substr(0, 8); + if ( prefix[7] != ' ' && prefix[7] != ':' ) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, + "Currently the limit is maximum coverage of 999999" + << std::endl); + } + } + } + int cov = atoi(prefix.c_str()); + if ( prefix[prefix.size()-1] != ':' ) + { + // This line does not have ':' so no coverage here. That said, + // Trace.py does not handle not covered lines versus comments etc. + // So, this will be set to 0. + cov = 0; + } + cmCTestLog(this->CTest, DEBUG, "Prefix: " << prefix.c_str() + << " cov: " << cov + << std::endl); + // Read the line number starting at the 10th character of the gcov + // output line + long lineIdx = cnt; + if ( lineIdx >= 0 ) + { + while ( vec->size() <= + static_cast<size_t>(lineIdx) ) + { + vec->push_back(-1); + } + // Initially all entries are -1 (not used). If we get coverage + // information, increment it to 0 first. + if ( (*vec)[lineIdx] < 0 ) + { + if ( cov >= 0 ) + { + (*vec)[lineIdx] = 0; + } + } + (*vec)[lineIdx] += cov; + } + } + } + ++ file_count; + } + cmSystemTools::ChangeDirectory(currentDirectory.c_str()); + return file_count; +} + +//---------------------------------------------------------------------- +std::string cmCTestCoverageHandler::FindFile( + cmCTestCoverageHandlerContainer* cont, + std::string fileName) +{ + std::string fileNameNoE + = cmSystemTools::GetFilenameWithoutLastExtension(fileName); + // First check in source and binary directory + std::string fullName = cont->SourceDir + "/" + fileNameNoE + ".py"; + if ( cmSystemTools::FileExists(fullName.c_str()) ) + { + return fullName; + } + fullName = cont->BinaryDir + "/" + fileNameNoE + ".py"; + if ( cmSystemTools::FileExists(fullName.c_str()) ) + { + return fullName; + } + return ""; +} + +// This is a header put on each marked up source file +namespace +{ + const char* bullseyeHelp[] = + {" Coverage produced by bullseye covbr tool: ", + " www.bullseye.com/help/ref_covbr.html", + " * An arrow --> indicates incomplete coverage.", + " * An X indicates a function that was invoked, a switch label that ", + " was exercised, a try-block that finished, or an exception handler ", + " that was invoked.", + " * A T or F indicates a boolean decision that evaluated true or false,", + " respectively.", + " * A t or f indicates a boolean condition within a decision if the ", + " condition evaluated true or false, respectively.", + " * A k indicates a constant decision or condition.", + " * The slash / means this probe is excluded from summary results. ", + 0}; +} + +//---------------------------------------------------------------------- +int cmCTestCoverageHandler::RunBullseyeCoverageBranch( + cmCTestCoverageHandlerContainer* cont, + std::set<cmStdString>& coveredFileNames, + std::vector<std::string>& files, + std::vector<std::string>& filesFullPath) +{ + if(files.size() != filesFullPath.size()) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, + "Files and full path files not the same size?:\n"); + return 0; + } + // create the output stream for the CoverageLog-N.xml file + cmGeneratedFileStream covLogFile; + int logFileCount = 0; + if ( !this->StartCoverageLogFile(covLogFile, logFileCount) ) + { + return -1; + } + // for each file run covbr on that file to get the coverage + // information for that file + std::string outputFile; + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, + "run covbr: " + << std::endl); + + if(!this->RunBullseyeCommand(cont, "covbr", 0, outputFile)) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, "error running covbr for." << "\n"); + return -1; + } + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, + "covbr output in " << outputFile + << std::endl); + // open the output file + std::ifstream fin(outputFile.c_str()); + if(!fin) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, + "Cannot open coverage file: " << + outputFile.c_str() << std::endl); + return 0; + } + std::map<cmStdString, cmStdString> fileMap; + std::vector<std::string>::iterator fp = filesFullPath.begin(); + for(std::vector<std::string>::iterator f = files.begin(); + f != files.end(); ++f, ++fp) + { + fileMap[*f] = *fp; + } + + int count =0; // keep count of the number of files + // Now parse each line from the bullseye cov log file + std::string lineIn; + bool valid = false; // are we in a valid output file + int line = 0; // line of the current file + cmStdString file; + while(cmSystemTools::GetLineFromStream(fin, lineIn)) + { + bool startFile = false; + if(lineIn.size() > 1 && lineIn[lineIn.size()-1] == ':') + { + file = lineIn.substr(0, lineIn.size()-1); + if(coveredFileNames.find(file) != coveredFileNames.end()) + { + startFile = true; + } + } + if(startFile) + { + // if we are in a valid file close it because a new one started + if(valid) + { + covLogFile << "\t\t</Report>" << std::endl + << "\t</File>" << std::endl; + } + // only allow 100 files in each log file + if ( count != 0 && count % 100 == 0 ) + { + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, + "start a new log file: " + << count + << std::endl); + this->EndCoverageLogFile(covLogFile, logFileCount); + logFileCount ++; + if ( !this->StartCoverageLogFile(covLogFile, logFileCount) ) + { + return -1; + } + count++; // move on one + } + std::map<cmStdString, cmStdString>::iterator + i = fileMap.find(file); + // if the file should be covered write out the header for that file + if(i != fileMap.end()) + { + // we have a new file so count it in the output + count++; + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, + "Produce coverage for file: " + << file.c_str() << " " << count + << std::endl); + // start the file output + covLogFile << "\t<File Name=\"" + << cmXMLSafe(i->first) + << "\" FullPath=\"" << cmXMLSafe( + this->CTest->GetShortPathToFile( + i->second.c_str())) << "\">" << std::endl + << "\t\t<Report>" << std::endl; + // write the bullseye header + line =0; + for(int k =0; bullseyeHelp[k] != 0; ++k) + { + covLogFile << "\t\t<Line Number=\"" << line << "\" Count=\"-1\">" + << cmXMLSafe(bullseyeHelp[k]) + << "</Line>" << std::endl; + line++; + } + valid = true; // we are in a valid file section + } + else + { + // this is not a file that we want coverage for + valid = false; + } + } + // we are not at a start file, and we are in a valid file output the line + else if(valid) + { + covLogFile << "\t\t<Line Number=\"" << line << "\" Count=\"-1\">" + << cmXMLSafe(lineIn) + << "</Line>" << std::endl; + line++; + } + } + // if we ran out of lines a valid file then close that file + if(valid) + { + covLogFile << "\t\t</Report>" << std::endl + << "\t</File>" << std::endl; + } + this->EndCoverageLogFile(covLogFile, logFileCount); + return 1; +} + +//---------------------------------------------------------------------- +int cmCTestCoverageHandler::RunBullseyeCommand( + cmCTestCoverageHandlerContainer* cont, + const char* cmd, + const char* arg, + std::string& outputFile) +{ + std::string program = cmSystemTools::FindProgram(cmd); + if(program.size() == 0) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, "Cannot find :" << cmd << "\n"); + return 0; + } + if(arg) + { + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, + "Run : " << program.c_str() << " " << arg << "\n"); + } + else + { + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, + "Run : " << program.c_str() << "\n"); + } + // create a process object and start it + cmCTestRunProcess runCoverageSrc; + runCoverageSrc.SetCommand(program.c_str()); + runCoverageSrc.AddArgument(arg); + std::string stdoutFile = cont->BinaryDir + "/Testing/Temporary/"; + stdoutFile += this->GetCTestInstance()->GetCurrentTag(); + stdoutFile += "-"; + stdoutFile += cmd; + std::string stderrFile = stdoutFile; + stdoutFile += ".stdout"; + stderrFile += ".stderr"; + runCoverageSrc.SetStdoutFile(stdoutFile.c_str()); + runCoverageSrc.SetStderrFile(stderrFile.c_str()); + if(!runCoverageSrc.StartProcess()) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, "Could not run : " + << program.c_str() << " " << arg << "\n" + << "kwsys process state : " + << runCoverageSrc.GetProcessState()); + return 0; + } + // since we set the output file names wait for it to end + runCoverageSrc.WaitForExit(); + outputFile = stdoutFile; + return 1; +} + +//---------------------------------------------------------------------- +int cmCTestCoverageHandler::RunBullseyeSourceSummary( + cmCTestCoverageHandlerContainer* cont) +{ + // Run the covsrc command and create a temp outputfile + std::string outputFile; + if(!this->RunBullseyeCommand(cont, "covsrc", "-c", outputFile)) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, "error running covsrc:\n"); + return 0; + } + + std::ostream& tmpLog = *cont->OFS; + // copen the Coverage.xml file in the Testing directory + cmGeneratedFileStream covSumFile; + if(!this->StartResultingXML(cmCTest::PartCoverage, "Coverage", covSumFile)) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, + "Cannot open coverage summary file." << std::endl); + return 0; + } + this->CTest->StartXML(covSumFile, this->AppendXML); + double elapsed_time_start = cmSystemTools::GetTime(); + std::string coverage_start_time = this->CTest->CurrentTime(); + covSumFile << "<Coverage>" << std::endl + << "\t<StartDateTime>" + << coverage_start_time << "</StartDateTime>" + << std::endl + << "\t<StartTime>" + << static_cast<unsigned int>(cmSystemTools::GetTime()) + << "</StartTime>" + << std::endl; + std::string stdline; + std::string errline; + // expected output: + // first line is: + // "Source","Function Coverage","out of","%","C/D Coverage","out of","%" + // after that data follows in that format + std::string sourceFile; + int functionsCalled = 0; + int totalFunctions = 0; + int percentFunction = 0; + int branchCovered = 0; + int totalBranches = 0; + int percentBranch = 0; + double total_tested = 0; + double total_untested = 0; + double total_functions = 0; + double percent_coverage =0; + double number_files = 0; + std::vector<std::string> coveredFiles; + std::vector<std::string> coveredFilesFullPath; + // Read and parse the summary output file + std::ifstream fin(outputFile.c_str()); + if(!fin) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, + "Cannot open coverage summary file: " << + outputFile.c_str() << std::endl); + return 0; + } + std::set<cmStdString> coveredFileNames; + while(cmSystemTools::GetLineFromStream(fin, stdline)) + { + // if we have a line of output from stdout + if(stdline.size()) + { + // parse the comma separated output + this->ParseBullsEyeCovsrcLine(stdline, + sourceFile, + functionsCalled, + totalFunctions, + percentFunction, + branchCovered, + totalBranches, + percentBranch); + // The first line is the header + if(sourceFile == "Source" || sourceFile == "Total") + { + continue; + } + std::string file = sourceFile; + coveredFileNames.insert(file); + if(!cmSystemTools::FileIsFullPath(sourceFile.c_str())) + { + // file will be relative to the binary dir + file = cont->BinaryDir; + file += "/"; + file += sourceFile; + } + file = cmSystemTools::CollapseFullPath(file.c_str()); + bool shouldIDoCoverage + = this->ShouldIDoCoverage(file.c_str(), + cont->SourceDir.c_str(), + cont->BinaryDir.c_str()); + if ( !shouldIDoCoverage ) + { + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, + ".NoDartCoverage found, so skip coverage check for: " + << file.c_str() + << std::endl); + continue; + } + + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, + "Doing coverage for: " + << file.c_str() + << std::endl); + + coveredFiles.push_back(sourceFile); + coveredFilesFullPath.push_back(file); + + number_files++; + total_functions += totalFunctions; + total_tested += functionsCalled; + total_untested += (totalFunctions - functionsCalled); + + std::string fileName = cmSystemTools::GetFilenameName(file.c_str()); + std::string shortFileName = + this->CTest->GetShortPathToFile(file.c_str()); + + float cper = static_cast<float>(percentBranch + percentFunction); + if(totalBranches > 0) + { + cper /= 2.0f; + } + percent_coverage += cper; + float cmet = static_cast<float>(percentFunction + percentBranch); + if(totalBranches > 0) + { + cmet /= 2.0f; + } + cmet /= 100.0f; + tmpLog << stdline.c_str() << "\n"; + tmpLog << fileName << "\n"; + tmpLog << "functionsCalled: " << functionsCalled/100 << "\n"; + tmpLog << "totalFunctions: " << totalFunctions/100 << "\n"; + tmpLog << "percentFunction: " << percentFunction << "\n"; + tmpLog << "branchCovered: " << branchCovered << "\n"; + tmpLog << "totalBranches: " << totalBranches << "\n"; + tmpLog << "percentBranch: " << percentBranch << "\n"; + tmpLog << "percentCoverage: " << percent_coverage << "\n"; + tmpLog << "coverage metric: " << cmet << "\n"; + covSumFile << "\t<File Name=\"" << cmXMLSafe(sourceFile) + << "\" FullPath=\"" << cmXMLSafe(shortFileName) + << "\" Covered=\"" << (cmet>0?"true":"false") << "\">\n" + << "\t\t<BranchesTested>" + << branchCovered + << "</BranchesTested>\n" + << "\t\t<BranchesUnTested>" + << totalBranches - branchCovered + << "</BranchesUnTested>\n" + << "\t\t<FunctionsTested>" + << functionsCalled + << "</FunctionsTested>\n" + << "\t\t<FunctionsUnTested>" + << totalFunctions - functionsCalled + << "</FunctionsUnTested>\n" + // Hack for conversion of function to loc assume a function + // has 100 lines of code + << "\t\t<LOCTested>" << functionsCalled *100 + << "</LOCTested>\n" + << "\t\t<LOCUnTested>" + << (totalFunctions - functionsCalled)*100 + << "</LOCUnTested>\n" + << "\t\t<PercentCoverage>"; + covSumFile.setf(std::ios::fixed, std::ios::floatfield); + covSumFile.precision(2); + covSumFile << (cper) << "</PercentCoverage>\n" + << "\t\t<CoverageMetric>"; + covSumFile.setf(std::ios::fixed, std::ios::floatfield); + covSumFile.precision(2); + covSumFile << (cmet) << "</CoverageMetric>\n"; + this->WriteXMLLabels(covSumFile, shortFileName); + covSumFile << "\t</File>" << std::endl; + } + } + std::string end_time = this->CTest->CurrentTime(); + covSumFile << "\t<LOCTested>" << total_tested << "</LOCTested>\n" + << "\t<LOCUntested>" << total_untested << "</LOCUntested>\n" + << "\t<LOC>" << total_functions << "</LOC>\n" + << "\t<PercentCoverage>"; + covSumFile.setf(std::ios::fixed, std::ios::floatfield); + covSumFile.precision(2); + covSumFile + << SAFEDIV(percent_coverage,number_files)<< "</PercentCoverage>\n" + << "\t<EndDateTime>" << end_time << "</EndDateTime>\n" + << "\t<EndTime>" << static_cast<unsigned int>(cmSystemTools::GetTime()) + << "</EndTime>\n"; + covSumFile + << "<ElapsedMinutes>" << + static_cast<int>((cmSystemTools::GetTime() - elapsed_time_start)/6)/10.0 + << "</ElapsedMinutes>" + << "</Coverage>" << std::endl; + this->CTest->EndXML(covSumFile); + + // Now create the coverage information for each file + return this->RunBullseyeCoverageBranch(cont, + coveredFileNames, + coveredFiles, + coveredFilesFullPath); +} + +//---------------------------------------------------------------------- +int cmCTestCoverageHandler::HandleBullseyeCoverage( + cmCTestCoverageHandlerContainer* cont) +{ + const char* covfile = cmSystemTools::GetEnv("COVFILE"); + if(!covfile || strlen(covfile) == 0) + { + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, + " COVFILE environment variable not found, not running " + " bullseye\n"); + return 0; + } + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, + " run covsrc with COVFILE=[" + << covfile + << "]" << std::endl); + if(!this->RunBullseyeSourceSummary(cont)) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, + "Error running bullseye summary.\n"); + return 0; + } + cmCTestLog(this->CTest, DEBUG, "HandleBullseyeCoverage return 1 " + << std::endl); + return 1; +} + +bool cmCTestCoverageHandler::GetNextInt(std::string const& inputLine, + std::string::size_type& pos, + int& value) +{ + std::string::size_type start = pos; + pos = inputLine.find(',', start); + value = atoi(inputLine.substr(start, pos).c_str()); + if(pos == inputLine.npos) + { + return true; + } + pos++; + return true; +} + +bool cmCTestCoverageHandler::ParseBullsEyeCovsrcLine( + std::string const& inputLine, + std::string& sourceFile, + int& functionsCalled, + int& totalFunctions, + int& percentFunction, + int& branchCovered, + int& totalBranches, + int& percentBranch) +{ + // find the first comma + std::string::size_type pos = inputLine.find(','); + if(pos == inputLine.npos) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, "Error parsing string : " + << inputLine.c_str() << "\n"); + return false; + } + // the source file has "" around it so extract out the file name + sourceFile = inputLine.substr(1,pos-2); + pos++; + if(!this->GetNextInt(inputLine, pos, functionsCalled)) + { + return false; + } + if(!this->GetNextInt(inputLine, pos, totalFunctions)) + { + return false; + } + if(!this->GetNextInt(inputLine, pos, percentFunction)) + { + return false; + } + if(!this->GetNextInt(inputLine, pos, branchCovered)) + { + return false; + } + if(!this->GetNextInt(inputLine, pos, totalBranches)) + { + return false; + } + if(!this->GetNextInt(inputLine, pos, percentBranch)) + { + return false; + } + // should be at the end now + if(pos != inputLine.npos) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, "Error parsing input : " + << inputLine.c_str() << " last pos not npos = " << pos << + "\n"); + } + return true; +} + +//---------------------------------------------------------------------- +int cmCTestCoverageHandler::GetLabelId(std::string const& label) +{ + LabelIdMapType::iterator i = this->LabelIdMap.find(label); + if(i == this->LabelIdMap.end()) + { + int n = int(this->Labels.size()); + this->Labels.push_back(label); + LabelIdMapType::value_type entry(label, n); + i = this->LabelIdMap.insert(entry).first; + } + return i->second; +} + +//---------------------------------------------------------------------- +void cmCTestCoverageHandler::LoadLabels() +{ + std::string fileList = this->CTest->GetBinaryDir(); + fileList += cmake::GetCMakeFilesDirectory(); + fileList += "/TargetDirectories.txt"; + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, + " target directory list [" << fileList << "]\n"); + std::ifstream finList(fileList.c_str()); + std::string line; + while(cmSystemTools::GetLineFromStream(finList, line)) + { + this->LoadLabels(line.c_str()); + } +} + +//---------------------------------------------------------------------- +void cmCTestCoverageHandler::LoadLabels(const char* dir) +{ + LabelSet& dirLabels = this->TargetDirs[dir]; + std::string fname = dir; + fname += "/Labels.txt"; + std::ifstream fin(fname.c_str()); + if(!fin) + { + return; + } + + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, + " loading labels from [" << fname << "]\n"); + bool inTarget = true; + std::string source; + std::string line; + std::vector<int> targetLabels; + while(cmSystemTools::GetLineFromStream(fin, line)) + { + if(line.empty() || line[0] == '#') + { + // Ignore blank and comment lines. + continue; + } + else if(line[0] == ' ') + { + // Label lines appear indented by one space. + std::string label = line.substr(1); + int id = this->GetLabelId(label); + dirLabels.insert(id); + if(inTarget) + { + targetLabels.push_back(id); + } + else + { + this->SourceLabels[source].insert(id); + } + } + else + { + // Non-indented lines specify a source file name. The first one + // is the end of the target-wide labels. + inTarget = false; + + source = this->CTest->GetShortPathToFile(line.c_str()); + + // Label the source with the target labels. + LabelSet& labelSet = this->SourceLabels[source]; + for(std::vector<int>::const_iterator li = targetLabels.begin(); + li != targetLabels.end(); ++li) + { + labelSet.insert(*li); + } + } + } +} + +//---------------------------------------------------------------------- +void cmCTestCoverageHandler::WriteXMLLabels(std::ofstream& os, + std::string const& source) +{ + LabelMapType::const_iterator li = this->SourceLabels.find(source); + if(li != this->SourceLabels.end() && !li->second.empty()) + { + os << "\t\t<Labels>\n"; + for(LabelSet::const_iterator lsi = li->second.begin(); + lsi != li->second.end(); ++lsi) + { + os << "\t\t\t<Label>" << cmXMLSafe(this->Labels[*lsi]) << "</Label>\n"; + } + os << "\t\t</Labels>\n"; + } +} + +//---------------------------------------------------------------------------- +void +cmCTestCoverageHandler::SetLabelFilter(std::set<cmStdString> const& labels) +{ + this->LabelFilter.clear(); + for(std::set<cmStdString>::const_iterator li = labels.begin(); + li != labels.end(); ++li) + { + this->LabelFilter.insert(this->GetLabelId(*li)); + } +} + +//---------------------------------------------------------------------- +bool cmCTestCoverageHandler::IntersectsFilter(LabelSet const& labels) +{ + // If there is no label filter then nothing is filtered out. + if(this->LabelFilter.empty()) + { + return true; + } + + std::vector<int> ids; + cmsys_stl::set_intersection + (labels.begin(), labels.end(), + this->LabelFilter.begin(), this->LabelFilter.end(), + cmsys_stl::back_inserter(ids)); + return !ids.empty(); +} + +//---------------------------------------------------------------------- +bool cmCTestCoverageHandler::IsFilteredOut(std::string const& source) +{ + // If there is no label filter then nothing is filtered out. + if(this->LabelFilter.empty()) + { + return false; + } + + // The source is filtered out if it does not have any labels in + // common with the filter set. + std::string shortSrc = this->CTest->GetShortPathToFile(source.c_str()); + LabelMapType::const_iterator li = this->SourceLabels.find(shortSrc); + if(li != this->SourceLabels.end()) + { + return !this->IntersectsFilter(li->second); + } + return true; +} + +//---------------------------------------------------------------------- +std::set<std::string> cmCTestCoverageHandler::FindUncoveredFiles( + cmCTestCoverageHandlerContainer* cont) +{ + std::set<std::string> extraMatches; + + for(std::vector<cmStdString>::iterator i = this->ExtraCoverageGlobs.begin(); + i != this->ExtraCoverageGlobs.end(); ++i) + { + cmsys::Glob gl; + gl.RecurseOn(); + gl.RecurseThroughSymlinksOff(); + std::string glob = cont->SourceDir + "/" + *i; + gl.FindFiles(glob); + std::vector<std::string> files = gl.GetFiles(); + for(std::vector<std::string>::iterator f = files.begin(); + f != files.end(); ++f) + { + if(this->ShouldIDoCoverage(f->c_str(), + cont->SourceDir.c_str(), cont->BinaryDir.c_str())) + { + extraMatches.insert(this->CTest->GetShortPathToFile( + f->c_str())); + } + } + } + + if(extraMatches.size()) + { + for(cmCTestCoverageHandlerContainer::TotalCoverageMap::iterator i = + cont->TotalCoverage.begin(); i != cont->TotalCoverage.end(); ++i) + { + std::string shortPath = this->CTest->GetShortPathToFile( + i->first.c_str()); + extraMatches.erase(shortPath); + } + } + return extraMatches; +} diff --git a/Source/CTest/cmCTestCoverageHandler.h b/Source/CTest/cmCTestCoverageHandler.h new file mode 100644 index 000000000..92b0b2285 --- /dev/null +++ b/Source/CTest/cmCTestCoverageHandler.h @@ -0,0 +1,139 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2009 Kitware, Inc. + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ + +#ifndef cmCTestCoverageHandler_h +#define cmCTestCoverageHandler_h + + +#include "cmCTestGenericHandler.h" +#include "cmListFileCache.h" + +#include <cmsys/RegularExpression.hxx> + +class cmGeneratedFileStream; +class cmCTestCoverageHandlerContainer +{ +public: + int Error; + std::string SourceDir; + std::string BinaryDir; + typedef std::vector<int> SingleFileCoverageVector; + typedef std::map<std::string, SingleFileCoverageVector> TotalCoverageMap; + TotalCoverageMap TotalCoverage; + std::ostream* OFS; +}; +/** \class cmCTestCoverageHandler + * \brief A class that handles coverage computaiton for ctest + * + */ +class cmCTestCoverageHandler : public cmCTestGenericHandler +{ +public: + cmTypeMacro(cmCTestCoverageHandler, cmCTestGenericHandler); + + /* + * The main entry point for this class + */ + int ProcessHandler(); + + cmCTestCoverageHandler(); + + virtual void Initialize(); + + /** + * This method is called when reading CTest custom file + */ + void PopulateCustomVectors(cmMakefile *mf); + + /** Report coverage only for sources with these labels. */ + void SetLabelFilter(std::set<cmStdString> const& labels); + +private: + bool ShouldIDoCoverage(const char* file, const char* srcDir, + const char* binDir); + void CleanCoverageLogFiles(std::ostream& log); + bool StartCoverageLogFile(cmGeneratedFileStream& ostr, int logFileCount); + void EndCoverageLogFile(cmGeneratedFileStream& ostr, int logFileCount); + + //! Handle coverage using GCC's GCov + int HandleGCovCoverage(cmCTestCoverageHandlerContainer* cont); + void FindGCovFiles(std::vector<std::string>& files); + + //! Handle coverage using xdebug php coverage + int HandlePHPCoverage(cmCTestCoverageHandlerContainer* cont); + //! Handle coverage for mumps + int HandleMumpsCoverage(cmCTestCoverageHandlerContainer* cont); + + //! Handle coverage using Bullseye + int HandleBullseyeCoverage(cmCTestCoverageHandlerContainer* cont); + int RunBullseyeSourceSummary(cmCTestCoverageHandlerContainer* cont); + int RunBullseyeCoverageBranch(cmCTestCoverageHandlerContainer* cont, + std::set<cmStdString>& coveredFileNames, + std::vector<std::string>& files, + std::vector<std::string>& filesFullPath); + + int RunBullseyeCommand( + cmCTestCoverageHandlerContainer* cont, + const char* cmd, + const char* arg, + std::string& outputFile); + bool ParseBullsEyeCovsrcLine( + std::string const& inputLine, + std::string& sourceFile, + int& functionsCalled, + int& totalFunctions, + int& percentFunction, + int& branchCovered, + int& totalBranches, + int& percentBranch); + bool GetNextInt(std::string const& inputLine, + std::string::size_type& pos, + int& value); + //! Handle Python coverage using Python's Trace.py + int HandleTracePyCoverage(cmCTestCoverageHandlerContainer* cont); + + // Find the source file based on the source and build tree. This is used for + // Trace.py mode, since that one does not tell us where the source file is. + std::string FindFile(cmCTestCoverageHandlerContainer* cont, + std::string fileName); + + std::set<std::string> FindUncoveredFiles( + cmCTestCoverageHandlerContainer* cont); + std::vector<cmStdString> CustomCoverageExclude; + std::vector<cmsys::RegularExpression> CustomCoverageExcludeRegex; + std::vector<cmStdString> ExtraCoverageGlobs; + + + // Map from source file to label ids. + class LabelSet: public std::set<int> {}; + typedef std::map<cmStdString, LabelSet> LabelMapType; + LabelMapType SourceLabels; + LabelMapType TargetDirs; + + // Map from label name to label id. + typedef std::map<cmStdString, int> LabelIdMapType; + LabelIdMapType LabelIdMap; + std::vector<std::string> Labels; + int GetLabelId(std::string const& label); + + // Label reading and writing methods. + void LoadLabels(); + void LoadLabels(const char* dir); + void WriteXMLLabels(std::ofstream& os, std::string const& source); + + // Label-based filtering. + std::set<int> LabelFilter; + bool IntersectsFilter(LabelSet const& labels); + bool IsFilteredOut(std::string const& source); +}; + +#endif diff --git a/Source/CTest/cmCTestEmptyBinaryDirectoryCommand.cxx b/Source/CTest/cmCTestEmptyBinaryDirectoryCommand.cxx new file mode 100644 index 000000000..56312c0fb --- /dev/null +++ b/Source/CTest/cmCTestEmptyBinaryDirectoryCommand.cxx @@ -0,0 +1,36 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2009 Kitware, Inc., Insight Software Consortium + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ +#include "cmCTestEmptyBinaryDirectoryCommand.h" + +#include "cmCTestScriptHandler.h" + +bool cmCTestEmptyBinaryDirectoryCommand +::InitialPass(std::vector<std::string> const& args, cmExecutionStatus &) +{ + if(args.size() != 1 ) + { + this->SetError("called with incorrect number of arguments"); + return false; + } + + if ( !cmCTestScriptHandler::EmptyBinaryDirectory(args[0].c_str()) ) + { + cmOStringStream ostr; + ostr << "problem removing the binary directory: " << args[0].c_str(); + this->SetError(ostr.str().c_str()); + return false; + } + + return true; +} + + diff --git a/Source/CTest/cmCTestEmptyBinaryDirectoryCommand.h b/Source/CTest/cmCTestEmptyBinaryDirectoryCommand.h new file mode 100644 index 000000000..a763fe9e9 --- /dev/null +++ b/Source/CTest/cmCTestEmptyBinaryDirectoryCommand.h @@ -0,0 +1,78 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2009 Kitware, Inc., Insight Software Consortium + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ +#ifndef cmCTestEmptyBinaryDirectoryCommand_h +#define cmCTestEmptyBinaryDirectoryCommand_h + +#include "cmCTestCommand.h" + +/** \class cmCTestEmptyBinaryDirectory + * \brief Run a ctest script + * + * cmLibrarysCommand defines a list of executable (i.e., test) + * programs to create. + */ +class cmCTestEmptyBinaryDirectoryCommand : public cmCTestCommand +{ +public: + + cmCTestEmptyBinaryDirectoryCommand() {} + + /** + * This is a virtual constructor for the command. + */ + virtual cmCommand* Clone() + { + cmCTestEmptyBinaryDirectoryCommand* ni + = new cmCTestEmptyBinaryDirectoryCommand; + ni->CTest = this->CTest; + ni->CTestScriptHandler = this->CTestScriptHandler; + return ni; + } + + /** + * This is called when the command is first encountered in + * the CMakeLists.txt file. + */ + virtual bool InitialPass(std::vector<std::string> const& args, + cmExecutionStatus &status); + + /** + * The name of the command as specified in CMakeList.txt. + */ + virtual const char* GetName() const { return "ctest_empty_binary_directory";} + + /** + * Succinct documentation. + */ + virtual const char* GetTerseDocumentation() const + { + return "empties the binary directory"; + } + + /** + * More documentation. + */ + virtual const char* GetFullDocumentation() const + { + return + " ctest_empty_binary_directory( directory )\n" + "Removes a binary directory. This command will perform some checks " + "prior to deleting the directory in an attempt to avoid malicious " + "or accidental directory deletion."; + } + + cmTypeMacro(cmCTestEmptyBinaryDirectoryCommand, cmCTestCommand); + +}; + + +#endif diff --git a/Source/CTest/cmCTestGIT.cxx b/Source/CTest/cmCTestGIT.cxx new file mode 100644 index 000000000..2c1a0afd9 --- /dev/null +++ b/Source/CTest/cmCTestGIT.cxx @@ -0,0 +1,652 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2009 Kitware, Inc. + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ +#include "cmCTestGIT.h" + +#include "cmCTest.h" +#include "cmSystemTools.h" +#include "cmXMLSafe.h" + +#include <cmsys/RegularExpression.hxx> +#include <cmsys/ios/sstream> +#include <cmsys/Process.h> + +#include <sys/types.h> +#include <time.h> +#include <ctype.h> + +//---------------------------------------------------------------------------- +static unsigned int cmCTestGITVersion(unsigned int epic, unsigned int major, + unsigned int minor, unsigned int fix) +{ + // 1.6.5.0 maps to 10605000 + return fix + minor*1000 + major*100000 + epic*10000000; +} + +//---------------------------------------------------------------------------- +cmCTestGIT::cmCTestGIT(cmCTest* ct, std::ostream& log): + cmCTestGlobalVC(ct, log) +{ + this->PriorRev = this->Unknown; + this->CurrentGitVersion = 0; +} + +//---------------------------------------------------------------------------- +cmCTestGIT::~cmCTestGIT() +{ +} + +//---------------------------------------------------------------------------- +class cmCTestGIT::OneLineParser: public cmCTestVC::LineParser +{ +public: + OneLineParser(cmCTestGIT* git, const char* prefix, + std::string& l): Line1(l) + { + this->SetLog(&git->Log, prefix); + } +private: + std::string& Line1; + virtual bool ProcessLine() + { + // Only the first line is of interest. + this->Line1 = this->Line; + return false; + } +}; + +//---------------------------------------------------------------------------- +std::string cmCTestGIT::GetWorkingRevision() +{ + // Run plumbing "git rev-list" to get work tree revision. + const char* git = this->CommandLineTool.c_str(); + const char* git_rev_list[] = {git, "rev-list", "-n", "1", "HEAD", 0}; + std::string rev; + OneLineParser out(this, "rl-out> ", rev); + OutputLogger err(this->Log, "rl-err> "); + this->RunChild(git_rev_list, &out, &err); + return rev; +} + +//---------------------------------------------------------------------------- +void cmCTestGIT::NoteOldRevision() +{ + this->OldRevision = this->GetWorkingRevision(); + cmCTestLog(this->CTest, HANDLER_OUTPUT, " Old revision of repository is: " + << this->OldRevision << "\n"); + this->PriorRev.Rev = this->OldRevision; +} + +//---------------------------------------------------------------------------- +void cmCTestGIT::NoteNewRevision() +{ + this->NewRevision = this->GetWorkingRevision(); + cmCTestLog(this->CTest, HANDLER_OUTPUT, " New revision of repository is: " + << this->NewRevision << "\n"); +} + +//---------------------------------------------------------------------------- +std::string cmCTestGIT::FindGitDir() +{ + std::string git_dir; + + // Run "git rev-parse --git-dir" to locate the real .git directory. + const char* git = this->CommandLineTool.c_str(); + char const* git_rev_parse[] = {git, "rev-parse", "--git-dir", 0}; + std::string git_dir_line; + OneLineParser rev_parse_out(this, "rev-parse-out> ", git_dir_line); + OutputLogger rev_parse_err(this->Log, "rev-parse-err> "); + if(this->RunChild(git_rev_parse, &rev_parse_out, &rev_parse_err)) + { + git_dir = git_dir_line; + } + if(git_dir.empty()) + { + git_dir = ".git"; + } + + // Git reports a relative path only when the .git directory is in + // the current directory. + if(git_dir[0] == '.') + { + git_dir = this->SourceDirectory + "/" + git_dir; + } +#if defined(_WIN32) && !defined(__CYGWIN__) + else if(git_dir[0] == '/') + { + // Cygwin Git reports a full path that Cygwin understands, but we + // are a Windows application. Run "cygpath" to get Windows path. + std::string cygpath_exe = cmSystemTools::GetFilenamePath(git); + cygpath_exe += "/cygpath.exe"; + if(cmSystemTools::FileExists(cygpath_exe.c_str())) + { + char const* cygpath[] = {cygpath_exe.c_str(), "-w", git_dir.c_str(), 0}; + OneLineParser cygpath_out(this, "cygpath-out> ", git_dir_line); + OutputLogger cygpath_err(this->Log, "cygpath-err> "); + if(this->RunChild(cygpath, &cygpath_out, &cygpath_err)) + { + git_dir = git_dir_line; + } + } + } +#endif + return git_dir; +} + +//---------------------------------------------------------------------------- +std::string cmCTestGIT::FindTopDir() +{ + std::string top_dir = this->SourceDirectory; + + // Run "git rev-parse --show-cdup" to locate the top of the tree. + const char* git = this->CommandLineTool.c_str(); + char const* git_rev_parse[] = {git, "rev-parse", "--show-cdup", 0}; + std::string cdup; + OneLineParser rev_parse_out(this, "rev-parse-out> ", cdup); + OutputLogger rev_parse_err(this->Log, "rev-parse-err> "); + if(this->RunChild(git_rev_parse, &rev_parse_out, &rev_parse_err) && + !cdup.empty()) + { + top_dir += "/"; + top_dir += cdup; + top_dir = cmSystemTools::CollapseFullPath(top_dir.c_str()); + } + return top_dir; +} + +//---------------------------------------------------------------------------- +bool cmCTestGIT::UpdateByFetchAndReset() +{ + const char* git = this->CommandLineTool.c_str(); + + // Use "git fetch" to get remote commits. + std::vector<char const*> git_fetch; + git_fetch.push_back(git); + git_fetch.push_back("fetch"); + + // Add user-specified update options. + std::string opts = this->CTest->GetCTestConfiguration("UpdateOptions"); + if(opts.empty()) + { + opts = this->CTest->GetCTestConfiguration("GITUpdateOptions"); + } + std::vector<cmStdString> args = cmSystemTools::ParseArguments(opts.c_str()); + for(std::vector<cmStdString>::const_iterator ai = args.begin(); + ai != args.end(); ++ai) + { + git_fetch.push_back(ai->c_str()); + } + + // Sentinel argument. + git_fetch.push_back(0); + + // Fetch upstream refs. + OutputLogger fetch_out(this->Log, "fetch-out> "); + OutputLogger fetch_err(this->Log, "fetch-err> "); + if(!this->RunUpdateCommand(&git_fetch[0], &fetch_out, &fetch_err)) + { + return false; + } + + // Identify the merge head that would be used by "git pull". + std::string sha1; + { + std::string fetch_head = this->FindGitDir() + "/FETCH_HEAD"; + std::ifstream fin(fetch_head.c_str(), std::ios::in | std::ios::binary); + if(!fin) + { + this->Log << "Unable to open " << fetch_head << "\n"; + return false; + } + std::string line; + while(sha1.empty() && cmSystemTools::GetLineFromStream(fin, line)) + { + this->Log << "FETCH_HEAD> " << line << "\n"; + if(line.find("\tnot-for-merge\t") == line.npos) + { + std::string::size_type pos = line.find('\t'); + if(pos != line.npos) + { + sha1 = line.substr(0, pos); + } + } + } + if(sha1.empty()) + { + this->Log << "FETCH_HEAD has no upstream branch candidate!\n"; + return false; + } + } + + // Reset the local branch to point at that tracked from upstream. + char const* git_reset[] = {git, "reset", "--hard", sha1.c_str(), 0}; + OutputLogger reset_out(this->Log, "reset-out> "); + OutputLogger reset_err(this->Log, "reset-err> "); + return this->RunChild(&git_reset[0], &reset_out, &reset_err); +} + +//---------------------------------------------------------------------------- +bool cmCTestGIT::UpdateByCustom(std::string const& custom) +{ + std::vector<std::string> git_custom_command; + cmSystemTools::ExpandListArgument(custom, git_custom_command, true); + std::vector<char const*> git_custom; + for(std::vector<std::string>::const_iterator + i = git_custom_command.begin(); i != git_custom_command.end(); ++i) + { + git_custom.push_back(i->c_str()); + } + git_custom.push_back(0); + + OutputLogger custom_out(this->Log, "custom-out> "); + OutputLogger custom_err(this->Log, "custom-err> "); + return this->RunUpdateCommand(&git_custom[0], &custom_out, &custom_err); +} + +//---------------------------------------------------------------------------- +bool cmCTestGIT::UpdateInternal() +{ + std::string custom = this->CTest->GetCTestConfiguration("GITUpdateCustom"); + if(!custom.empty()) + { + return this->UpdateByCustom(custom); + } + return this->UpdateByFetchAndReset(); +} + +//---------------------------------------------------------------------------- +bool cmCTestGIT::UpdateImpl() +{ + if(!this->UpdateInternal()) + { + return false; + } + + std::string top_dir = this->FindTopDir(); + const char* git = this->CommandLineTool.c_str(); + const char* recursive = "--recursive"; + + // Git < 1.6.5.0 did not support --recursive + if(this->GetGitVersion() < cmCTestGITVersion(1,6,5,0)) + { + recursive = 0; + // No need to require >= 1.6.5.0 if there are no submodules. + if(cmSystemTools::FileExists((top_dir + "/.gitmodules").c_str())) + { + this->Log << "Git < 1.6.5.0 cannot update submodules recursively\n"; + } + } + + char const* git_submodule[] = {git, "submodule", "update", recursive, 0}; + OutputLogger submodule_out(this->Log, "submodule-out> "); + OutputLogger submodule_err(this->Log, "submodule-err> "); + return this->RunChild(git_submodule, &submodule_out, &submodule_err, + top_dir.c_str()); +} + +//---------------------------------------------------------------------------- +unsigned int cmCTestGIT::GetGitVersion() +{ + if(!this->CurrentGitVersion) + { + const char* git = this->CommandLineTool.c_str(); + char const* git_version[] = {git, "--version", 0}; + std::string version; + OneLineParser version_out(this, "version-out> ", version); + OutputLogger version_err(this->Log, "version-err> "); + unsigned int v[4] = {0,0,0,0}; + if(this->RunChild(git_version, &version_out, &version_err) && + sscanf(version.c_str(), "git version %u.%u.%u.%u", + &v[0], &v[1], &v[2], &v[3]) >= 3) + { + this->CurrentGitVersion = cmCTestGITVersion(v[0], v[1], v[2], v[3]); + } + } + return this->CurrentGitVersion; +} + +//---------------------------------------------------------------------------- +/* Diff format: + + :src-mode dst-mode src-sha1 dst-sha1 status\0 + src-path\0 + [dst-path\0] + + The format is repeated for every file changed. The [dst-path\0] + line appears only for lines with status 'C' or 'R'. See 'git help + diff-tree' for details. +*/ +class cmCTestGIT::DiffParser: public cmCTestVC::LineParser +{ +public: + DiffParser(cmCTestGIT* git, const char* prefix): + LineParser('\0', false), GIT(git), DiffField(DiffFieldNone) + { + this->SetLog(&git->Log, prefix); + } + + typedef cmCTestGIT::Change Change; + std::vector<Change> Changes; +protected: + cmCTestGIT* GIT; + enum DiffFieldType { DiffFieldNone, DiffFieldChange, + DiffFieldSrc, DiffFieldDst }; + DiffFieldType DiffField; + Change CurChange; + + void DiffReset() + { + this->DiffField = DiffFieldNone; + this->Changes.clear(); + } + + virtual bool ProcessLine() + { + if(this->Line[0] == ':') + { + this->DiffField = DiffFieldChange; + this->CurChange = Change(); + } + if(this->DiffField == DiffFieldChange) + { + // :src-mode dst-mode src-sha1 dst-sha1 status + if(this->Line[0] != ':') + { + this->DiffField = DiffFieldNone; + return true; + } + const char* src_mode_first = this->Line.c_str()+1; + const char* src_mode_last = this->ConsumeField(src_mode_first); + const char* dst_mode_first = this->ConsumeSpace(src_mode_last); + const char* dst_mode_last = this->ConsumeField(dst_mode_first); + const char* src_sha1_first = this->ConsumeSpace(dst_mode_last); + const char* src_sha1_last = this->ConsumeField(src_sha1_first); + const char* dst_sha1_first = this->ConsumeSpace(src_sha1_last); + const char* dst_sha1_last = this->ConsumeField(dst_sha1_first); + const char* status_first = this->ConsumeSpace(dst_sha1_last); + const char* status_last = this->ConsumeField(status_first); + if(status_first != status_last) + { + this->CurChange.Action = *status_first; + this->DiffField = DiffFieldSrc; + } + else + { + this->DiffField = DiffFieldNone; + } + } + else if(this->DiffField == DiffFieldSrc) + { + // src-path + if(this->CurChange.Action == 'C') + { + // Convert copy to addition of destination. + this->CurChange.Action = 'A'; + this->DiffField = DiffFieldDst; + } + else if(this->CurChange.Action == 'R') + { + // Convert rename to deletion of source and addition of destination. + this->CurChange.Action = 'D'; + this->CurChange.Path = this->Line; + this->Changes.push_back(this->CurChange); + + this->CurChange = Change('A'); + this->DiffField = DiffFieldDst; + } + else + { + this->CurChange.Path = this->Line; + this->Changes.push_back(this->CurChange); + this->DiffField = this->DiffFieldNone; + } + } + else if(this->DiffField == DiffFieldDst) + { + // dst-path + this->CurChange.Path = this->Line; + this->Changes.push_back(this->CurChange); + this->DiffField = this->DiffFieldNone; + } + return true; + } + + const char* ConsumeSpace(const char* c) + { + while(*c && isspace(*c)) { ++c; } + return c; + } + const char* ConsumeField(const char* c) + { + while(*c && !isspace(*c)) { ++c; } + return c; + } +}; + +//---------------------------------------------------------------------------- +/* Commit format: + + commit ...\n + tree ...\n + parent ...\n + author ...\n + committer ...\n + \n + Log message indented by (4) spaces\n + (even blank lines have the spaces)\n + [[ + \n + [Diff format] + OR + \0 + ]] + + The header may have more fields. See 'git help diff-tree'. +*/ +class cmCTestGIT::CommitParser: public cmCTestGIT::DiffParser +{ +public: + CommitParser(cmCTestGIT* git, const char* prefix): + DiffParser(git, prefix), Section(SectionHeader) + { + this->Separator = SectionSep[this->Section]; + } + +private: + typedef cmCTestGIT::Revision Revision; + enum SectionType { SectionHeader, SectionBody, SectionDiff, SectionCount }; + static char const SectionSep[SectionCount]; + SectionType Section; + Revision Rev; + + struct Person + { + std::string Name; + std::string EMail; + unsigned long Time; + long TimeZone; + Person(): Name(), EMail(), Time(0), TimeZone(0) {} + }; + + void ParsePerson(const char* str, Person& person) + { + // Person Name <person@domain.com> 1234567890 +0000 + const char* c = str; + while(*c && isspace(*c)) { ++c; } + + const char* name_first = c; + while(*c && *c != '<') { ++c; } + const char* name_last = c; + while(name_last != name_first && isspace(*(name_last-1))) { --name_last; } + person.Name.assign(name_first, name_last-name_first); + + const char* email_first = *c? ++c : c; + while(*c && *c != '>') { ++c; } + const char* email_last = *c? c++ : c; + person.EMail.assign(email_first, email_last-email_first); + + person.Time = strtoul(c, (char**)&c, 10); + person.TimeZone = strtol(c, (char**)&c, 10); + } + + virtual bool ProcessLine() + { + if(this->Line.empty()) + { + if(this->Section == SectionBody && this->LineEnd == '\0') + { + // Skip SectionDiff + this->NextSection(); + } + this->NextSection(); + } + else + { + switch(this->Section) + { + case SectionHeader: this->DoHeaderLine(); break; + case SectionBody: this->DoBodyLine(); break; + case SectionDiff: this->DiffParser::ProcessLine(); break; + case SectionCount: break; // never happens + } + } + return true; + } + + void NextSection() + { + this->Section = SectionType((this->Section+1) % SectionCount); + this->Separator = SectionSep[this->Section]; + if(this->Section == SectionHeader) + { + this->GIT->DoRevision(this->Rev, this->Changes); + this->Rev = Revision(); + this->DiffReset(); + } + } + + void DoHeaderLine() + { + // Look for header fields that we need. + if(strncmp(this->Line.c_str(), "commit ", 7) == 0) + { + this->Rev.Rev = this->Line.c_str()+7; + } + else if(strncmp(this->Line.c_str(), "author ", 7) == 0) + { + Person author; + this->ParsePerson(this->Line.c_str()+7, author); + this->Rev.Author = author.Name; + this->Rev.EMail = author.EMail; + this->Rev.Date = this->FormatDateTime(author); + } + else if(strncmp(this->Line.c_str(), "committer ", 10) == 0) + { + Person committer; + this->ParsePerson(this->Line.c_str()+10, committer); + this->Rev.Committer = committer.Name; + this->Rev.CommitterEMail = committer.EMail; + this->Rev.CommitDate = this->FormatDateTime(committer); + } + } + + void DoBodyLine() + { + // Commit log lines are indented by 4 spaces. + if(this->Line.size() >= 4) + { + this->Rev.Log += this->Line.substr(4); + } + this->Rev.Log += "\n"; + } + + std::string FormatDateTime(Person const& person) + { + // Convert the time to a human-readable format that is also easy + // to machine-parse: "CCYY-MM-DD hh:mm:ss". + time_t seconds = static_cast<time_t>(person.Time); + struct tm* t = gmtime(&seconds); + char dt[1024]; + sprintf(dt, "%04d-%02d-%02d %02d:%02d:%02d", + t->tm_year+1900, t->tm_mon+1, t->tm_mday, + t->tm_hour, t->tm_min, t->tm_sec); + std::string out = dt; + + // Add the time-zone field "+zone" or "-zone". + char tz[32]; + if(person.TimeZone >= 0) + { + sprintf(tz, " +%04ld", person.TimeZone); + } + else + { + sprintf(tz, " -%04ld", -person.TimeZone); + } + out += tz; + return out; + } +}; + +char const cmCTestGIT::CommitParser::SectionSep[SectionCount] = +{'\n', '\n', '\0'}; + +//---------------------------------------------------------------------------- +void cmCTestGIT::LoadRevisions() +{ + // Use 'git rev-list ... | git diff-tree ...' to get revisions. + std::string range = this->OldRevision + ".." + this->NewRevision; + const char* git = this->CommandLineTool.c_str(); + const char* git_rev_list[] = + {git, "rev-list", "--reverse", range.c_str(), "--", 0}; + const char* git_diff_tree[] = + {git, "diff-tree", "--stdin", "--always", "-z", "-r", "--pretty=raw", + "--encoding=utf-8", 0}; + this->Log << this->ComputeCommandLine(git_rev_list) << " | " + << this->ComputeCommandLine(git_diff_tree) << "\n"; + + cmsysProcess* cp = cmsysProcess_New(); + cmsysProcess_AddCommand(cp, git_rev_list); + cmsysProcess_AddCommand(cp, git_diff_tree); + cmsysProcess_SetWorkingDirectory(cp, this->SourceDirectory.c_str()); + + CommitParser out(this, "dt-out> "); + OutputLogger err(this->Log, "dt-err> "); + this->RunProcess(cp, &out, &err); + + // Send one extra zero-byte to terminate the last record. + out.Process("", 1); + + cmsysProcess_Delete(cp); +} + +//---------------------------------------------------------------------------- +void cmCTestGIT::LoadModifications() +{ + const char* git = this->CommandLineTool.c_str(); + + // Use 'git update-index' to refresh the index w.r.t. the work tree. + const char* git_update_index[] = {git, "update-index", "--refresh", 0}; + OutputLogger ui_out(this->Log, "ui-out> "); + OutputLogger ui_err(this->Log, "ui-err> "); + this->RunChild(git_update_index, &ui_out, &ui_err); + + // Use 'git diff-index' to get modified files. + const char* git_diff_index[] = {git, "diff-index", "-z", "HEAD", 0}; + DiffParser out(this, "di-out> "); + OutputLogger err(this->Log, "di-err> "); + this->RunChild(git_diff_index, &out, &err); + + for(std::vector<Change>::const_iterator ci = out.Changes.begin(); + ci != out.Changes.end(); ++ci) + { + this->DoModification(PathModified, ci->Path); + } +} diff --git a/Source/CTest/cmCTestGIT.h b/Source/CTest/cmCTestGIT.h new file mode 100644 index 000000000..f4fae8f9d --- /dev/null +++ b/Source/CTest/cmCTestGIT.h @@ -0,0 +1,57 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2009 Kitware, Inc. + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ +#ifndef cmCTestGIT_h +#define cmCTestGIT_h + +#include "cmCTestGlobalVC.h" + +/** \class cmCTestGIT + * \brief Interaction with git command-line tool + * + */ +class cmCTestGIT: public cmCTestGlobalVC +{ +public: + /** Construct with a CTest instance and update log stream. */ + cmCTestGIT(cmCTest* ctest, std::ostream& log); + + virtual ~cmCTestGIT(); + +private: + unsigned int CurrentGitVersion; + unsigned int GetGitVersion(); + std::string GetWorkingRevision(); + virtual void NoteOldRevision(); + virtual void NoteNewRevision(); + virtual bool UpdateImpl(); + + std::string FindGitDir(); + std::string FindTopDir(); + + bool UpdateByFetchAndReset(); + bool UpdateByCustom(std::string const& custom); + bool UpdateInternal(); + + void LoadRevisions(); + void LoadModifications(); + +public: // needed by older Sun compilers + // Parsing helper classes. + class OneLineParser; + class DiffParser; + class CommitParser; + friend class OneLineParser; + friend class DiffParser; + friend class CommitParser; +}; + +#endif diff --git a/Source/CTest/cmCTestGenericHandler.cxx b/Source/CTest/cmCTestGenericHandler.cxx new file mode 100644 index 000000000..fd75e45e8 --- /dev/null +++ b/Source/CTest/cmCTestGenericHandler.cxx @@ -0,0 +1,171 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2009 Kitware, Inc., Insight Software Consortium + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ + +#include "cmCTestGenericHandler.h" +#include "cmSystemTools.h" + +#include "cmCTest.h" + +//---------------------------------------------------------------------- +cmCTestGenericHandler::cmCTestGenericHandler() +{ + this->HandlerVerbose = cmSystemTools::OUTPUT_NONE; + this->CTest = 0; + this->SubmitIndex = 0; + this->AppendXML = false; +} + +//---------------------------------------------------------------------- +cmCTestGenericHandler::~cmCTestGenericHandler() +{ +} + +//---------------------------------------------------------------------- +void cmCTestGenericHandler::SetOption(const char* op, const char* value) +{ + if ( !op ) + { + return; + } + if ( !value ) + { + cmCTestGenericHandler::t_StringToString::iterator remit + = this->Options.find(op); + if ( remit != this->Options.end() ) + { + this->Options.erase(remit); + } + return; + } + + this->Options[op] = value; +} + +//---------------------------------------------------------------------- +void cmCTestGenericHandler::SetPersistentOption(const char* op, + const char* value) +{ + this->SetOption(op, value); + if ( !op ) + { + return; + } + if ( !value ) + { + cmCTestGenericHandler::t_StringToString::iterator remit + = this->PersistentOptions.find(op); + if ( remit != this->PersistentOptions.end() ) + { + this->PersistentOptions.erase(remit); + } + return; + } + + this->PersistentOptions[op] = value; +} + +//---------------------------------------------------------------------- +void cmCTestGenericHandler::Initialize() +{ + this->AppendXML = false; + this->Options.clear(); + t_StringToString::iterator it; + for ( it = this->PersistentOptions.begin(); + it != this->PersistentOptions.end(); + ++ it ) + { + this->Options[it->first.c_str()] = it->second.c_str(); + } +} + +//---------------------------------------------------------------------- +const char* cmCTestGenericHandler::GetOption(const char* op) +{ + cmCTestGenericHandler::t_StringToString::iterator remit + = this->Options.find(op); + if ( remit == this->Options.end() ) + { + return 0; + } + return remit->second.c_str(); +} + +//---------------------------------------------------------------------- +bool cmCTestGenericHandler::StartResultingXML(cmCTest::Part part, + const char* name, + cmGeneratedFileStream& xofs) +{ + if ( !name ) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, + "Cannot create resulting XML file without providing the name" + << std::endl;); + return false; + } + cmOStringStream ostr; + ostr << name; + if ( this->SubmitIndex > 0 ) + { + ostr << "_" << this->SubmitIndex; + } + ostr << ".xml"; + if(this->CTest->GetCurrentTag().empty()) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, + "Current Tag empty, this may mean NightlyStartTime / " + "CTEST_NIGHTLY_START_TIME was not set correctly. Or " + "maybe you forgot to call ctest_start() before calling " + "ctest_configure()." << std::endl); + cmSystemTools::SetFatalErrorOccured(); + return false; + } + if( !this->CTest->OpenOutputFile(this->CTest->GetCurrentTag(), + ostr.str().c_str(), xofs, true) ) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, + "Cannot create resulting XML file: " << ostr.str().c_str() + << std::endl); + return false; + } + this->CTest->AddSubmitFile(part, ostr.str().c_str()); + return true; +} + +//---------------------------------------------------------------------- +bool cmCTestGenericHandler::StartLogFile(const char* name, + cmGeneratedFileStream& xofs) +{ + if ( !name ) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, + "Cannot create log file without providing the name" << std::endl;); + return false; + } + cmOStringStream ostr; + ostr << "Last" << name; + if ( this->SubmitIndex > 0 ) + { + ostr << "_" << this->SubmitIndex; + } + if ( !this->CTest->GetCurrentTag().empty() ) + { + ostr << "_" << this->CTest->GetCurrentTag(); + } + ostr << ".log"; + if( !this->CTest->OpenOutputFile("Temporary", ostr.str().c_str(), xofs) ) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, "Cannot create log file: " + << ostr.str().c_str() << std::endl); + return false; + } + return true; +} diff --git a/Source/CTest/cmCTestGenericHandler.h b/Source/CTest/cmCTestGenericHandler.h new file mode 100644 index 000000000..18189ec2b --- /dev/null +++ b/Source/CTest/cmCTestGenericHandler.h @@ -0,0 +1,107 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2009 Kitware, Inc. + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ + +#ifndef cmCTestGenericHandler_h +#define cmCTestGenericHandler_h + + +#include "cmObject.h" +#include "cmCTest.h" +#include "cmSystemTools.h" //OutputOption + +class cmMakefile; +class cmCTestCommand; +class cmGeneratedFileStream; + +/** \class cmCTestGenericHandler + * \brief A superclass of all CTest Handlers + * + */ +class cmCTestGenericHandler : public cmObject +{ +public: + /** + * If verbose then more informaiton is printed out + */ + void SetVerbose(bool val) + { this->HandlerVerbose = val ? + cmSystemTools::OUTPUT_MERGE : cmSystemTools::OUTPUT_NONE; } + + /** + * Populate internals from CTest custom scripts + */ + virtual void PopulateCustomVectors(cmMakefile *) {} + + /** + * Do the actual processing. Subclass has to override it. + * Return < 0 if error. + */ + virtual int ProcessHandler() = 0; + + /** + * Process command line arguments that are applicable for the handler + */ + virtual int ProcessCommandLineArguments( + const std::string& /*currentArg*/, size_t& /*idx*/, + const std::vector<std::string>& /*allArgs*/) { return 1; } + + /** + * Initialize handler + */ + virtual void Initialize(); + + /** + * Set the CTest instance + */ + void SetCTestInstance(cmCTest* ctest) { this->CTest = ctest; } + cmCTest* GetCTestInstance() { return this->CTest; } + + /** + * Construct handler + */ + cmCTestGenericHandler(); + virtual ~cmCTestGenericHandler(); + + typedef std::map<cmStdString,cmStdString> t_StringToString; + + + void SetPersistentOption(const char* op, const char* value); + void SetOption(const char* op, const char* value); + const char* GetOption(const char* op); + + void SetCommand(cmCTestCommand* command) + { + this->Command = command; + } + + void SetSubmitIndex(int idx) { this->SubmitIndex = idx; } + int GetSubmitIndex() { return this->SubmitIndex; } + + void SetAppendXML(bool b) { this->AppendXML = b; } + +protected: + bool StartResultingXML(cmCTest::Part part, + const char* name, cmGeneratedFileStream& xofs); + bool StartLogFile(const char* name, cmGeneratedFileStream& xofs); + + bool AppendXML; + cmSystemTools::OutputOption HandlerVerbose; + cmCTest *CTest; + t_StringToString Options; + t_StringToString PersistentOptions; + + cmCTestCommand* Command; + int SubmitIndex; +}; + +#endif + diff --git a/Source/CTest/cmCTestGlobalVC.cxx b/Source/CTest/cmCTestGlobalVC.cxx new file mode 100644 index 000000000..8c51102e5 --- /dev/null +++ b/Source/CTest/cmCTestGlobalVC.cxx @@ -0,0 +1,142 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2009 Kitware, Inc. + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ +#include "cmCTestGlobalVC.h" + +#include "cmCTest.h" +#include "cmSystemTools.h" +#include "cmXMLSafe.h" + +#include <cmsys/RegularExpression.hxx> + +//---------------------------------------------------------------------------- +cmCTestGlobalVC::cmCTestGlobalVC(cmCTest* ct, std::ostream& log): + cmCTestVC(ct, log) +{ + this->PriorRev = this->Unknown; +} + +//---------------------------------------------------------------------------- +cmCTestGlobalVC::~cmCTestGlobalVC() +{ +} + +//---------------------------------------------------------------------------- +const char* cmCTestGlobalVC::LocalPath(std::string const& path) +{ + return path.c_str(); +} + +//---------------------------------------------------------------------------- +void cmCTestGlobalVC::DoRevision(Revision const& revision, + std::vector<Change> const& changes) +{ + // Ignore changes in the old revision. + if(revision.Rev == this->OldRevision) + { + this->PriorRev = revision; + return; + } + + // Indicate we found a revision. + cmCTestLog(this->CTest, HANDLER_OUTPUT, "." << std::flush); + + // Store the revision. + this->Revisions.push_back(revision); + + // Report this revision. + Revision const& rev = this->Revisions.back(); + this->Log << "Found revision " << rev.Rev << "\n" + << " author = " << rev.Author << "\n" + << " date = " << rev.Date << "\n"; + + // Update information about revisions of the changed files. + for(std::vector<Change>::const_iterator ci = changes.begin(); + ci != changes.end(); ++ci) + { + if(const char* local = this->LocalPath(ci->Path)) + { + std::string dir = cmSystemTools::GetFilenamePath(local); + std::string name = cmSystemTools::GetFilenameName(local); + File& file = this->Dirs[dir][name]; + file.PriorRev = file.Rev? file.Rev : &this->PriorRev; + file.Rev = &rev; + this->Log << " " << ci->Action << " " << local << " " << "\n"; + } + } +} + +//---------------------------------------------------------------------------- +void cmCTestGlobalVC::DoModification(PathStatus status, + std::string const& path) +{ + std::string dir = cmSystemTools::GetFilenamePath(path); + std::string name = cmSystemTools::GetFilenameName(path); + File& file = this->Dirs[dir][name]; + file.Status = status; + // For local modifications the current rev is unknown and the + // prior rev is the latest from svn. + if(!file.Rev && !file.PriorRev) + { + file.PriorRev = &this->PriorRev; + } +} + +//---------------------------------------------------------------------------- +void cmCTestGlobalVC::WriteXMLDirectory(std::ostream& xml, + std::string const& path, + Directory const& dir) +{ + const char* slash = path.empty()? "":"/"; + xml << "\t<Directory>\n" + << "\t\t<Name>" << cmXMLSafe(path) << "</Name>\n"; + for(Directory::const_iterator fi = dir.begin(); fi != dir.end(); ++fi) + { + std::string full = path + slash + fi->first; + this->WriteXMLEntry(xml, path, fi->first, full, fi->second); + } + xml << "\t</Directory>\n"; +} + +//---------------------------------------------------------------------------- +void cmCTestGlobalVC::WriteXMLGlobal(std::ostream& xml) +{ + if(!this->NewRevision.empty()) + { + xml << "\t<Revision>" << this->NewRevision << "</Revision>\n"; + } + if(!this->OldRevision.empty() && this->OldRevision != this->NewRevision) + { + xml << "\t<PriorRevision>" << this->OldRevision << "</PriorRevision>\n"; + } +} + +//---------------------------------------------------------------------------- +bool cmCTestGlobalVC::WriteXMLUpdates(std::ostream& xml) +{ + cmCTestLog(this->CTest, HANDLER_OUTPUT, + " Gathering version information (one . per revision):\n" + " " << std::flush); + this->LoadRevisions(); + cmCTestLog(this->CTest, HANDLER_OUTPUT, std::endl); + + this->LoadModifications(); + + this->WriteXMLGlobal(xml); + + for(std::map<cmStdString, Directory>::const_iterator + di = this->Dirs.begin(); di != this->Dirs.end(); ++di) + { + this->WriteXMLDirectory(xml, di->first, di->second); + } + + return true; +} diff --git a/Source/CTest/cmCTestGlobalVC.h b/Source/CTest/cmCTestGlobalVC.h new file mode 100644 index 000000000..a648a5982 --- /dev/null +++ b/Source/CTest/cmCTestGlobalVC.h @@ -0,0 +1,68 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2009 Kitware, Inc. + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ +#ifndef cmCTestGlobalVC_h +#define cmCTestGlobalVC_h + +#include "cmCTestVC.h" + +/** \class cmCTestGlobalVC + * \brief Base class for handling globally-versioned trees + * + */ +class cmCTestGlobalVC: public cmCTestVC +{ +public: + /** Construct with a CTest instance and update log stream. */ + cmCTestGlobalVC(cmCTest* ctest, std::ostream& log); + + virtual ~cmCTestGlobalVC(); + +protected: + // Implement cmCTestVC internal API. + virtual bool WriteXMLUpdates(std::ostream& xml); + + /** Represent a vcs-reported action for one path in a revision. */ + struct Change + { + char Action; + std::string Path; + Change(char a = '?'): Action(a) {} + }; + + // Update status for files in each directory. + class Directory: public std::map<cmStdString, File> {}; + std::map<cmStdString, Directory> Dirs; + + // Old and new repository revisions. + std::string OldRevision; + std::string NewRevision; + + // Information known about old revision. + Revision PriorRev; + + // Information about revisions from a svn log. + std::list<Revision> Revisions; + + virtual const char* LocalPath(std::string const& path); + + virtual void DoRevision(Revision const& revision, + std::vector<Change> const& changes); + virtual void DoModification(PathStatus status, std::string const& path); + virtual void LoadModifications() = 0; + virtual void LoadRevisions() = 0; + + virtual void WriteXMLGlobal(std::ostream& xml); + void WriteXMLDirectory(std::ostream& xml, std::string const& path, + Directory const& dir); +}; + +#endif diff --git a/Source/CTest/cmCTestHG.cxx b/Source/CTest/cmCTestHG.cxx new file mode 100644 index 000000000..86a7617f1 --- /dev/null +++ b/Source/CTest/cmCTestHG.cxx @@ -0,0 +1,338 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2009 Kitware, Inc. + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License 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 += this->CData[i]; + } + else + { + output.push_back(currPath); + currPath = ""; + } + } + 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); +} diff --git a/Source/CTest/cmCTestHG.h b/Source/CTest/cmCTestHG.h new file mode 100644 index 000000000..1eaf9337e --- /dev/null +++ b/Source/CTest/cmCTestHG.h @@ -0,0 +1,47 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2009 Kitware, Inc. + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License 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 diff --git a/Source/CTest/cmCTestHandlerCommand.cxx b/Source/CTest/cmCTestHandlerCommand.cxx new file mode 100644 index 000000000..1957e04e3 --- /dev/null +++ b/Source/CTest/cmCTestHandlerCommand.cxx @@ -0,0 +1,197 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2009 Kitware, Inc., Insight Software Consortium + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ +#include "cmCTestHandlerCommand.h" + +#include "cmCTest.h" +#include "cmCTestGenericHandler.h" + +cmCTestHandlerCommand::cmCTestHandlerCommand() +{ + const size_t INIT_SIZE = 100; + size_t cc; + this->Arguments.reserve(INIT_SIZE); + for ( cc = 0; cc < INIT_SIZE; ++ cc ) + { + this->Arguments.push_back(0); + } + this->Arguments[ct_RETURN_VALUE] = "RETURN_VALUE"; + this->Arguments[ct_SOURCE] = "SOURCE"; + this->Arguments[ct_BUILD] = "BUILD"; + this->Arguments[ct_SUBMIT_INDEX] = "SUBMIT_INDEX"; + this->Last = ct_LAST; + this->AppendXML = false; +} + +bool cmCTestHandlerCommand +::InitialPass(std::vector<std::string> const& args, cmExecutionStatus &) +{ + // Allocate space for argument values. + this->Values.clear(); + this->Values.resize(this->Last, 0); + + // Process input arguments. + this->ArgumentDoing = ArgumentDoingNone; + for(unsigned int i=0; i < args.size(); ++i) + { + // Check this argument. + if(!this->CheckArgumentKeyword(args[i]) && + !this->CheckArgumentValue(args[i])) + { + cmOStringStream e; + e << "called with unknown argument \"" << args[i] << "\"."; + this->SetError(e.str().c_str()); + return false; + } + + // Quit if an argument is invalid. + if(this->ArgumentDoing == ArgumentDoingError) + { + return false; + } + } + + // Set the config type of this ctest to the current value of the + // CTEST_CONFIGURATION_TYPE script variable if it is defined. + // The current script value trumps the -C argument on the command + // line. + const char* ctestConfigType = + this->Makefile->GetDefinition("CTEST_CONFIGURATION_TYPE"); + if (ctestConfigType) + { + this->CTest->SetConfigType(ctestConfigType); + } + + cmCTestLog(this->CTest, DEBUG, "Initialize handler" << std::endl;); + cmCTestGenericHandler* handler = this->InitializeHandler(); + if ( !handler ) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, + "Cannot instantiate test handler " << this->GetName() + << std::endl); + return false; + } + + handler->SetAppendXML(this->AppendXML); + + handler->PopulateCustomVectors(this->Makefile); + if ( this->Values[ct_BUILD] ) + { + this->CTest->SetCTestConfiguration("BuildDirectory", + cmSystemTools::CollapseFullPath( + this->Values[ct_BUILD]).c_str()); + } + else + { + const char* bdir = + this->Makefile->GetSafeDefinition("CTEST_BINARY_DIRECTORY"); + if(bdir) + { + this-> + CTest->SetCTestConfiguration("BuildDirectory", + cmSystemTools::CollapseFullPath(bdir).c_str()); + } + else + { + cmCTestLog(this->CTest, ERROR_MESSAGE, + "CTEST_BINARY_DIRECTORY not set" << std::endl;); + } + } + if ( this->Values[ct_SOURCE] ) + { + cmCTestLog(this->CTest, DEBUG, + "Set source directory to: " << this->Values[ct_SOURCE] << std::endl); + this->CTest->SetCTestConfiguration("SourceDirectory", + cmSystemTools::CollapseFullPath( + this->Values[ct_SOURCE]).c_str()); + } + else + { + this->CTest->SetCTestConfiguration("SourceDirectory", + cmSystemTools::CollapseFullPath( + this->Makefile->GetSafeDefinition("CTEST_SOURCE_DIRECTORY")).c_str()); + } + if ( this->Values[ct_SUBMIT_INDEX] ) + { + if ( this->CTest->GetDartVersion() <= 1 ) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, + "Dart before version 2.0 does not support collecting submissions." + << std::endl + << "Please upgrade the server to Dart 2 or higher, or do not use " + "SUBMIT_INDEX." << std::endl); + } + else + { + handler->SetSubmitIndex(atoi(this->Values[ct_SUBMIT_INDEX])); + } + } + std::string current_dir = cmSystemTools::GetCurrentWorkingDirectory(); + cmSystemTools::ChangeDirectory( + this->CTest->GetCTestConfiguration("BuildDirectory").c_str()); + int res = handler->ProcessHandler(); + if ( this->Values[ct_RETURN_VALUE] && *this->Values[ct_RETURN_VALUE]) + { + cmOStringStream str; + str << res; + this->Makefile->AddDefinition( + this->Values[ct_RETURN_VALUE], str.str().c_str()); + } + cmSystemTools::ChangeDirectory(current_dir.c_str()); + return true; +} + +//---------------------------------------------------------------------------- +bool cmCTestHandlerCommand::CheckArgumentKeyword(std::string const& arg) +{ + // Look for non-value arguments common to all commands. + if(arg == "APPEND") + { + this->ArgumentDoing = ArgumentDoingNone; + this->AppendXML = true; + return true; + } + + // Check for a keyword in our argument/value table. + for(unsigned int k=0; k < this->Arguments.size(); ++k) + { + if(this->Arguments[k] && arg == this->Arguments[k]) + { + this->ArgumentDoing = ArgumentDoingKeyword; + this->ArgumentIndex = k; + return true; + } + } + return false; +} + +//---------------------------------------------------------------------------- +bool cmCTestHandlerCommand::CheckArgumentValue(std::string const& arg) +{ + if(this->ArgumentDoing == ArgumentDoingKeyword) + { + this->ArgumentDoing = ArgumentDoingNone; + unsigned int k = this->ArgumentIndex; + if(this->Values[k]) + { + cmOStringStream e; + e << "Called with more than one value for " << this->Arguments[k]; + this->Makefile->IssueMessage(cmake::FATAL_ERROR, e.str()); + this->ArgumentDoing = ArgumentDoingError; + return true; + } + this->Values[k] = arg.c_str(); + cmCTestLog(this->CTest, DEBUG, "Set " << this->Arguments[k] + << " to " << arg << "\n"); + return true; + } + return false; +} diff --git a/Source/CTest/cmCTestHandlerCommand.h b/Source/CTest/cmCTestHandlerCommand.h new file mode 100644 index 000000000..8ef2b8028 --- /dev/null +++ b/Source/CTest/cmCTestHandlerCommand.h @@ -0,0 +1,77 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2009 Kitware, Inc., Insight Software Consortium + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ +#ifndef cmCTestHandlerCommand_h +#define cmCTestHandlerCommand_h + +#include "cmCTestCommand.h" + +class cmCTestGenericHandler; + +/** \class cmCTestHandler + * \brief Run a ctest script + * + * cmCTestHandlerCommand defineds the command to test the project. + */ +class cmCTestHandlerCommand : public cmCTestCommand +{ +public: + cmCTestHandlerCommand(); + + /** + * This is called when the command is first encountered in + * the CMakeLists.txt file. + */ + virtual bool InitialPass(std::vector<std::string> const& args, + cmExecutionStatus &status); + + cmTypeMacro(cmCTestHandlerCommand, cmCTestCommand); + + enum + { + ct_NONE, + ct_RETURN_VALUE, + ct_BUILD, + ct_SOURCE, + ct_SUBMIT_INDEX, + ct_LAST + }; + +protected: + virtual cmCTestGenericHandler* InitializeHandler() = 0; + + // Command argument handling. + virtual bool CheckArgumentKeyword(std::string const& arg); + virtual bool CheckArgumentValue(std::string const& arg); + enum + { + ArgumentDoingNone, + ArgumentDoingError, + ArgumentDoingKeyword, + ArgumentDoingLast1 + }; + int ArgumentDoing; + unsigned int ArgumentIndex; + + bool AppendXML; + + std::string ReturnVariable; + std::vector<const char*> Arguments; + std::vector<const char*> Values; + size_t Last; +}; + +#define CTEST_COMMAND_APPEND_OPTION_DOCS \ + "The APPEND option marks results for append to those previously " \ + "submitted to a dashboard server since the last ctest_start. " \ + "Append semantics are defined by the dashboard server in use." + +#endif diff --git a/Source/CTest/cmCTestLaunch.cxx b/Source/CTest/cmCTestLaunch.cxx new file mode 100644 index 000000000..9831d0200 --- /dev/null +++ b/Source/CTest/cmCTestLaunch.cxx @@ -0,0 +1,719 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2009 Kitware, Inc., Insight Software Consortium + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ +#include "cmCTestLaunch.h" + +#include "cmGeneratedFileStream.h" +#include "cmSystemTools.h" +#include "cmXMLSafe.h" +#include "cmake.h" + +#include <cmsys/MD5.h> +#include <cmsys/Process.h> +#include <cmsys/RegularExpression.hxx> + +//---------------------------------------------------------------------------- +cmCTestLaunch::cmCTestLaunch(int argc, const char* const* argv) +{ + this->Passthru = true; + this->Process = 0; + this->ExitCode = 1; + this->CWD = cmSystemTools::GetCurrentWorkingDirectory(); + + if(!this->ParseArguments(argc, argv)) + { + return; + } + + this->ComputeFileNames(); + + this->ScrapeRulesLoaded = false; + this->HaveOut = false; + this->HaveErr = false; + this->Process = cmsysProcess_New(); +} + +//---------------------------------------------------------------------------- +cmCTestLaunch::~cmCTestLaunch() +{ + cmsysProcess_Delete(this->Process); + if(!this->Passthru) + { + cmSystemTools::RemoveFile(this->LogOut.c_str()); + cmSystemTools::RemoveFile(this->LogErr.c_str()); + } +} + +//---------------------------------------------------------------------------- +bool cmCTestLaunch::ParseArguments(int argc, const char* const* argv) +{ + // Launcher options occur first and are separated from the real + // command line by a '--' option. + enum Doing { DoingNone, + DoingOutput, + DoingSource, + DoingLanguage, + DoingTargetName, + DoingTargetType, + DoingBuildDir, + DoingCount }; + Doing doing = DoingNone; + int arg0 = 0; + for(int i=1; !arg0 && i < argc; ++i) + { + const char* arg = argv[i]; + if(strcmp(arg, "--") == 0) + { + arg0 = i+1; + } + else if(strcmp(arg, "--output") == 0) + { + doing = DoingOutput; + } + else if(strcmp(arg, "--source") == 0) + { + doing = DoingSource; + } + else if(strcmp(arg, "--language") == 0) + { + doing = DoingLanguage; + } + else if(strcmp(arg, "--target-name") == 0) + { + doing = DoingTargetName; + } + else if(strcmp(arg, "--target-type") == 0) + { + doing = DoingTargetType; + } + else if(strcmp(arg, "--build-dir") == 0) + { + doing = DoingBuildDir; + } + else if(doing == DoingOutput) + { + this->OptionOutput = arg; + doing = DoingNone; + } + else if(doing == DoingSource) + { + this->OptionSource = arg; + doing = DoingNone; + } + else if(doing == DoingLanguage) + { + this->OptionLanguage = arg; + if(this->OptionLanguage == "CXX") + { + this->OptionLanguage = "C++"; + } + doing = DoingNone; + } + else if(doing == DoingTargetName) + { + this->OptionTargetName = arg; + doing = DoingNone; + } + else if(doing == DoingTargetType) + { + this->OptionTargetType = arg; + doing = DoingNone; + } + else if(doing == DoingBuildDir) + { + this->OptionBuildDir = arg; + doing = DoingNone; + } + } + + // Extract the real command line. + if(arg0) + { + this->RealArgC = argc - arg0; + this->RealArgV = argv + arg0; + for(int i=0; i < this->RealArgC; ++i) + { + this->HandleRealArg(this->RealArgV[i]); + } + return true; + } + else + { + this->RealArgC = 0; + this->RealArgV = 0; + std::cerr << "No launch/command separator ('--') found!\n"; + return false; + } +} + +//---------------------------------------------------------------------------- +void cmCTestLaunch::HandleRealArg(const char* arg) +{ +#ifdef _WIN32 + // Expand response file arguments. + if(arg[0] == '@' && cmSystemTools::FileExists(arg+1)) + { + std::ifstream fin(arg+1); + std::string line; + while(cmSystemTools::GetLineFromStream(fin, line)) + { + cmSystemTools::ParseWindowsCommandLine(line.c_str(), this->RealArgs); + } + return; + } +#endif + this->RealArgs.push_back(arg); +} + +//---------------------------------------------------------------------------- +void cmCTestLaunch::ComputeFileNames() +{ + // We just passthru the behavior of the real command unless the + // CTEST_LAUNCH_LOGS environment variable is set. + const char* d = getenv("CTEST_LAUNCH_LOGS"); + if(!(d && *d)) + { + return; + } + this->Passthru = false; + + // The environment variable specifies the directory into which we + // generate build logs. + this->LogDir = d; + cmSystemTools::ConvertToUnixSlashes(this->LogDir); + this->LogDir += "/"; + + // We hash the input command working dir and command line to obtain + // a repeatable and (probably) unique name for log files. + char hash[32]; + cmsysMD5* md5 = cmsysMD5_New(); + cmsysMD5_Initialize(md5); + cmsysMD5_Append(md5, (unsigned char const*)(this->CWD.c_str()), -1); + for(std::vector<std::string>::const_iterator ai = this->RealArgs.begin(); + ai != this->RealArgs.end(); ++ai) + { + cmsysMD5_Append(md5, (unsigned char const*)ai->c_str(), -1); + } + cmsysMD5_FinalizeHex(md5, hash); + cmsysMD5_Delete(md5); + this->LogHash.assign(hash, 32); + + // We store stdout and stderr in temporary log files. + this->LogOut = this->LogDir; + this->LogOut += "launch-"; + this->LogOut += this->LogHash; + this->LogOut += "-out.txt"; + this->LogErr = this->LogDir; + this->LogErr += "launch-"; + this->LogErr += this->LogHash; + this->LogErr += "-err.txt"; +} + +//---------------------------------------------------------------------------- +void cmCTestLaunch::RunChild() +{ + // Ignore noopt make rules + if(this->RealArgs.empty() || this->RealArgs[0] == ":") + { + this->ExitCode = 0; + return; + } + + // Prepare to run the real command. + cmsysProcess* cp = this->Process; + cmsysProcess_SetCommand(cp, this->RealArgV); + + std::ofstream fout; + std::ofstream ferr; + if(this->Passthru) + { + // In passthru mode we just share the output pipes. + cmsysProcess_SetPipeShared(cp, cmsysProcess_Pipe_STDOUT, 1); + cmsysProcess_SetPipeShared(cp, cmsysProcess_Pipe_STDERR, 1); + } + else + { + // In full mode we record the child output pipes to log files. + fout.open(this->LogOut.c_str(), + std::ios::out | std::ios::binary); + ferr.open(this->LogErr.c_str(), + std::ios::out | std::ios::binary); + } + + // Run the real command. + cmsysProcess_Execute(cp); + + // Record child stdout and stderr if necessary. + if(!this->Passthru) + { + char* data = 0; + int length = 0; + while(int p = cmsysProcess_WaitForData(cp, &data, &length, 0)) + { + if(p == cmsysProcess_Pipe_STDOUT) + { + fout.write(data, length); + std::cout.write(data, length); + this->HaveOut = true; + } + else if(p == cmsysProcess_Pipe_STDERR) + { + ferr.write(data, length); + std::cerr.write(data, length); + this->HaveErr = true; + } + } + } + + // Wait for the real command to finish. + cmsysProcess_WaitForExit(cp, 0); + this->ExitCode = cmsysProcess_GetExitValue(cp); +} + +//---------------------------------------------------------------------------- +int cmCTestLaunch::Run() +{ + if(!this->Process) + { + std::cerr << "Could not allocate cmsysProcess instance!\n"; + return -1; + } + + this->RunChild(); + + if(this->CheckResults()) + { + return this->ExitCode; + } + + this->LoadConfig(); + this->WriteXML(); + + return this->ExitCode; +} + +//---------------------------------------------------------------------------- +void cmCTestLaunch::LoadLabels() +{ + if(this->OptionBuildDir.empty() || this->OptionTargetName.empty()) + { + return; + } + + // Labels are listed in per-target files. + std::string fname = this->OptionBuildDir; + fname += cmake::GetCMakeFilesDirectory(); + fname += "/"; + fname += this->OptionTargetName; + fname += ".dir/Labels.txt"; + + // We are interested in per-target labels for this source file. + std::string source = this->OptionSource; + cmSystemTools::ConvertToUnixSlashes(source); + + // Load the labels file. + std::ifstream fin(fname.c_str(), std::ios::in | std::ios::binary); + if(!fin) { return; } + bool inTarget = true; + bool inSource = false; + std::string line; + while(cmSystemTools::GetLineFromStream(fin, line)) + { + if(line.empty() || line[0] == '#') + { + // Ignore blank and comment lines. + continue; + } + else if(line[0] == ' ') + { + // Label lines appear indented by one space. + if(inTarget || inSource) + { + this->Labels.insert(line.c_str()+1); + } + } + else if(!this->OptionSource.empty() && !inSource) + { + // Non-indented lines specify a source file name. The first one + // is the end of the target-wide labels. Use labels following a + // matching source. + inTarget = false; + inSource = this->SourceMatches(line, source); + } + else + { + return; + } + } +} + +//---------------------------------------------------------------------------- +bool cmCTestLaunch::SourceMatches(std::string const& lhs, + std::string const& rhs) +{ + // TODO: Case sensitivity, UseRelativePaths, etc. Note that both + // paths in the comparison get generated by CMake. This is done for + // every source in the target, so it should be efficient (cannot use + // cmSystemTools::IsSameFile). + return lhs == rhs; +} + +//---------------------------------------------------------------------------- +bool cmCTestLaunch::IsError() const +{ + return this->ExitCode != 0; +} + +//---------------------------------------------------------------------------- +void cmCTestLaunch::WriteXML() +{ + // Name the xml file. + std::string logXML = this->LogDir; + logXML += this->IsError()? "error-" : "warning-"; + logXML += this->LogHash; + logXML += ".xml"; + + // Use cmGeneratedFileStream to atomically create the report file. + cmGeneratedFileStream fxml(logXML.c_str()); + fxml << "\t<Failure type=\"" + << (this->IsError()? "Error" : "Warning") << "\">\n"; + this->WriteXMLAction(fxml); + this->WriteXMLCommand(fxml); + this->WriteXMLResult(fxml); + this->WriteXMLLabels(fxml); + fxml << "\t</Failure>\n"; +} + +//---------------------------------------------------------------------------- +void cmCTestLaunch::WriteXMLAction(std::ostream& fxml) +{ + fxml << "\t\t<!-- Meta-information about the build action -->\n"; + fxml << "\t\t<Action>\n"; + + // TargetName + if(!this->OptionTargetName.empty()) + { + fxml << "\t\t\t<TargetName>" + << cmXMLSafe(this->OptionTargetName) + << "</TargetName>\n"; + } + + // Language + if(!this->OptionLanguage.empty()) + { + fxml << "\t\t\t<Language>" + << cmXMLSafe(this->OptionLanguage) + << "</Language>\n"; + } + + // SourceFile + if(!this->OptionSource.empty()) + { + std::string source = this->OptionSource; + cmSystemTools::ConvertToUnixSlashes(source); + + // If file is in source tree use its relative location. + if(cmSystemTools::FileIsFullPath(this->SourceDir.c_str()) && + cmSystemTools::FileIsFullPath(source.c_str()) && + cmSystemTools::IsSubDirectory(source.c_str(), + this->SourceDir.c_str())) + { + source = cmSystemTools::RelativePath(this->SourceDir.c_str(), + source.c_str()); + } + + fxml << "\t\t\t<SourceFile>" + << cmXMLSafe(source) + << "</SourceFile>\n"; + } + + // OutputFile + if(!this->OptionOutput.empty()) + { + fxml << "\t\t\t<OutputFile>" + << cmXMLSafe(this->OptionOutput) + << "</OutputFile>\n"; + } + + // OutputType + const char* outputType = 0; + if(!this->OptionTargetType.empty()) + { + if(this->OptionTargetType == "EXECUTABLE") + { + outputType = "executable"; + } + else if(this->OptionTargetType == "SHARED_LIBRARY") + { + outputType = "shared library"; + } + else if(this->OptionTargetType == "MODULE_LIBRARY") + { + outputType = "module library"; + } + else if(this->OptionTargetType == "STATIC_LIBRARY") + { + outputType = "static library"; + } + } + else if(!this->OptionSource.empty()) + { + outputType = "object file"; + } + if(outputType) + { + fxml << "\t\t\t<OutputType>" + << cmXMLSafe(outputType) + << "</OutputType>\n"; + } + + fxml << "\t\t</Action>\n"; +} + +//---------------------------------------------------------------------------- +void cmCTestLaunch::WriteXMLCommand(std::ostream& fxml) +{ + fxml << "\n"; + fxml << "\t\t<!-- Details of command -->\n"; + fxml << "\t\t<Command>\n"; + if(!this->CWD.empty()) + { + fxml << "\t\t\t<WorkingDirectory>" + << cmXMLSafe(this->CWD) + << "</WorkingDirectory>\n"; + } + for(std::vector<std::string>::const_iterator ai = this->RealArgs.begin(); + ai != this->RealArgs.end(); ++ai) + { + fxml << "\t\t\t<Argument>" + << cmXMLSafe(ai->c_str()) + << "</Argument>\n"; + } + fxml << "\t\t</Command>\n"; +} + +//---------------------------------------------------------------------------- +void cmCTestLaunch::WriteXMLResult(std::ostream& fxml) +{ + fxml << "\n"; + fxml << "\t\t<!-- Result of command -->\n"; + fxml << "\t\t<Result>\n"; + + // StdOut + fxml << "\t\t\t<StdOut>"; + this->DumpFileToXML(fxml, this->LogOut); + fxml << "</StdOut>\n"; + + // StdErr + fxml << "\t\t\t<StdErr>"; + this->DumpFileToXML(fxml, this->LogErr); + fxml << "</StdErr>\n"; + + // ExitCondition + fxml << "\t\t\t<ExitCondition>"; + cmsysProcess* cp = this->Process; + switch (cmsysProcess_GetState(cp)) + { + case cmsysProcess_State_Starting: + fxml << "No process has been executed"; break; + case cmsysProcess_State_Executing: + fxml << "The process is still executing"; break; + case cmsysProcess_State_Disowned: + fxml << "Disowned"; break; + case cmsysProcess_State_Killed: + fxml << "Killed by parent"; break; + + case cmsysProcess_State_Expired: + fxml << "Killed when timeout expired"; break; + case cmsysProcess_State_Exited: + fxml << this->ExitCode; break; + case cmsysProcess_State_Exception: + fxml << "Terminated abnormally: " + << cmXMLSafe(cmsysProcess_GetExceptionString(cp)); break; + case cmsysProcess_State_Error: + fxml << "Error administrating child process: " + << cmXMLSafe(cmsysProcess_GetErrorString(cp)); break; + }; + fxml << "</ExitCondition>\n"; + + fxml << "\t\t</Result>\n"; +} + +//---------------------------------------------------------------------------- +void cmCTestLaunch::WriteXMLLabels(std::ostream& fxml) +{ + this->LoadLabels(); + if(!this->Labels.empty()) + { + fxml << "\n"; + fxml << "\t\t<!-- Interested parties -->\n"; + fxml << "\t\t<Labels>\n"; + for(std::set<cmStdString>::const_iterator li = this->Labels.begin(); + li != this->Labels.end(); ++li) + { + fxml << "\t\t\t<Label>" << cmXMLSafe(*li) << "</Label>\n"; + } + fxml << "\t\t</Labels>\n"; + } +} + +//---------------------------------------------------------------------------- +void cmCTestLaunch::DumpFileToXML(std::ostream& fxml, + std::string const& fname) +{ + std::ifstream fin(fname.c_str(), std::ios::in | std::ios::binary); + + std::string line; + const char* sep = ""; + while(cmSystemTools::GetLineFromStream(fin, line)) + { + fxml << sep << cmXMLSafe(line).Quotes(false); + sep = "\n"; + } +} + +//---------------------------------------------------------------------------- +bool cmCTestLaunch::CheckResults() +{ + // Skip XML in passthru mode. + if(this->Passthru) + { + return true; + } + + // We always report failure for error conditions. + if(this->IsError()) + { + return false; + } + + // Scrape the output logs to look for warnings. + if((this->HaveErr && this->ScrapeLog(this->LogErr)) || + (this->HaveOut && this->ScrapeLog(this->LogOut))) + { + return false; + } + return true; +} + +//---------------------------------------------------------------------------- +void cmCTestLaunch::LoadScrapeRules() +{ + if(this->ScrapeRulesLoaded) + { + return; + } + this->ScrapeRulesLoaded = true; + + // Common compiler warning formats. These are much simpler than the + // full log-scraping expressions because we do not need to extract + // file and line information. + this->RegexWarning.push_back("(^|[ :])[Ww][Aa][Rr][Nn][Ii][Nn][Gg]"); + this->RegexWarning.push_back("(^|[ :])[Rr][Ee][Mm][Aa][Rr][Kk]"); + this->RegexWarning.push_back("(^|[ :])[Nn][Oo][Tt][Ee]"); + + // Load custom match rules given to us by CTest. + this->LoadScrapeRules("Warning", this->RegexWarning); + this->LoadScrapeRules("WarningSuppress", this->RegexWarningSuppress); +} + +//---------------------------------------------------------------------------- +void +cmCTestLaunch +::LoadScrapeRules(const char* purpose, + std::vector<cmsys::RegularExpression>& regexps) +{ + std::string fname = this->LogDir; + fname += "Custom"; + fname += purpose; + fname += ".txt"; + std::ifstream fin(fname.c_str(), std::ios::in | std::ios::binary); + std::string line; + cmsys::RegularExpression rex; + while(cmSystemTools::GetLineFromStream(fin, line)) + { + if(rex.compile(line.c_str())) + { + regexps.push_back(rex); + } + } +} + +//---------------------------------------------------------------------------- +bool cmCTestLaunch::ScrapeLog(std::string const& fname) +{ + this->LoadScrapeRules(); + + // Look for log file lines matching warning expressions but not + // suppression expressions. + std::ifstream fin(fname.c_str(), std::ios::in | std::ios::binary); + std::string line; + while(cmSystemTools::GetLineFromStream(fin, line)) + { + if(this->Match(line.c_str(), this->RegexWarning) && + !this->Match(line.c_str(), this->RegexWarningSuppress)) + { + return true; + } + } + return false; +} + +//---------------------------------------------------------------------------- +bool cmCTestLaunch::Match(std::string const& line, + std::vector<cmsys::RegularExpression>& regexps) +{ + for(std::vector<cmsys::RegularExpression>::iterator ri = regexps.begin(); + ri != regexps.end(); ++ri) + { + if(ri->find(line.c_str())) + { + return true; + } + } + return false; +} + +//---------------------------------------------------------------------------- +int cmCTestLaunch::Main(int argc, const char* const argv[]) +{ + if(argc == 2) + { + std::cerr << "ctest --launch: this mode is for internal CTest use only" + << std::endl; + return 1; + } + cmCTestLaunch self(argc, argv); + return self.Run(); +} + +//---------------------------------------------------------------------------- +#include "cmGlobalGenerator.h" +#include "cmLocalGenerator.h" +#include "cmMakefile.h" +#include "cmake.h" +#include <cmsys/auto_ptr.hxx> +void cmCTestLaunch::LoadConfig() +{ + cmake cm; + cmGlobalGenerator gg; + gg.SetCMakeInstance(&cm); + cmsys::auto_ptr<cmLocalGenerator> lg(gg.CreateLocalGenerator()); + cmMakefile* mf = lg->GetMakefile(); + std::string fname = this->LogDir; + fname += "CTestLaunchConfig.cmake"; + if(cmSystemTools::FileExists(fname.c_str()) && + mf->ReadListFile(0, fname.c_str())) + { + this->SourceDir = mf->GetSafeDefinition("CTEST_SOURCE_DIRECTORY"); + cmSystemTools::ConvertToUnixSlashes(this->SourceDir); + } +} diff --git a/Source/CTest/cmCTestLaunch.h b/Source/CTest/cmCTestLaunch.h new file mode 100644 index 000000000..7457e8357 --- /dev/null +++ b/Source/CTest/cmCTestLaunch.h @@ -0,0 +1,104 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2009 Kitware, Inc., Insight Software Consortium + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ +#ifndef cmCTestLaunch_h +#define cmCTestLaunch_h + +#include "cmStandardIncludes.h" +#include <cmsys/RegularExpression.hxx> + +/** \class cmCTestLaunch + * \brief Launcher for make rules to report results for ctest + * + * This implements the 'ctest --launch' tool. + */ +class cmCTestLaunch +{ +public: + /** Entry point from ctest executable main(). */ + static int Main(int argc, const char* const argv[]); +private: + // Initialize the launcher from its command line. + cmCTestLaunch(int argc, const char* const* argv); + ~cmCTestLaunch(); + + // Run the real command. + int Run(); + void RunChild(); + + // Methods to check the result of the real command. + bool IsError() const; + bool CheckResults(); + + // Launcher options specified before the real command. + std::string OptionOutput; + std::string OptionSource; + std::string OptionLanguage; + std::string OptionTargetName; + std::string OptionTargetType; + std::string OptionBuildDir; + bool ParseArguments(int argc, const char* const* argv); + + // The real command line appearing after launcher arguments. + int RealArgC; + const char* const* RealArgV; + std::string CWD; + + // The real command line after response file expansion. + std::vector<std::string> RealArgs; + void HandleRealArg(const char* arg); + + // A hash of the real command line is unique and unlikely to collide. + std::string LogHash; + void ComputeFileNames(); + + bool Passthru; + struct cmsysProcess_s* Process; + int ExitCode; + + // Temporary log files for stdout and stderr of real command. + std::string LogDir; + std::string LogOut; + std::string LogErr; + bool HaveOut; + bool HaveErr; + + // Labels associated with the build rule. + std::set<cmStdString> Labels; + void LoadLabels(); + bool SourceMatches(std::string const& lhs, + std::string const& rhs); + + // Regular expressions to match warnings and their exceptions. + bool ScrapeRulesLoaded; + std::vector<cmsys::RegularExpression> RegexWarning; + std::vector<cmsys::RegularExpression> RegexWarningSuppress; + void LoadScrapeRules(); + void LoadScrapeRules(const char* purpose, + std::vector<cmsys::RegularExpression>& regexps); + bool ScrapeLog(std::string const& fname); + bool Match(std::string const& line, + std::vector<cmsys::RegularExpression>& regexps); + + // Methods to generate the xml fragment. + void WriteXML(); + void WriteXMLAction(std::ostream& fxml); + void WriteXMLCommand(std::ostream& fxml); + void WriteXMLResult(std::ostream& fxml); + void WriteXMLLabels(std::ostream& fxml); + void DumpFileToXML(std::ostream& fxml, std::string const& fname); + + // Configuration + void LoadConfig(); + std::string SourceDir; +}; + +#endif diff --git a/Source/CTest/cmCTestMemCheckCommand.cxx b/Source/CTest/cmCTestMemCheckCommand.cxx new file mode 100644 index 000000000..535c9934f --- /dev/null +++ b/Source/CTest/cmCTestMemCheckCommand.cxx @@ -0,0 +1,32 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2009 Kitware, Inc., Insight Software Consortium + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ +#include "cmCTestMemCheckCommand.h" + +#include "cmCTest.h" +#include "cmCTestGenericHandler.h" + + +cmCTestGenericHandler* cmCTestMemCheckCommand::InitializeActualHandler() +{ + cmCTestGenericHandler* handler + = this->CTest->GetInitializedHandler("memcheck"); + + this->CTest->SetCTestConfigurationFromCMakeVariable(this->Makefile, + "MemoryCheckCommand", "CTEST_MEMORYCHECK_COMMAND"); + this->CTest->SetCTestConfigurationFromCMakeVariable(this->Makefile, + "MemoryCheckCommandOptions", "CTEST_MEMORYCHECK_COMMAND_OPTIONS"); + this->CTest->SetCTestConfigurationFromCMakeVariable(this->Makefile, + "MemoryCheckSuppressionFile", "CTEST_MEMORYCHECK_SUPPRESSIONS_FILE"); + + return handler; +} + diff --git a/Source/CTest/cmCTestMemCheckCommand.h b/Source/CTest/cmCTestMemCheckCommand.h new file mode 100644 index 000000000..399fe8b22 --- /dev/null +++ b/Source/CTest/cmCTestMemCheckCommand.h @@ -0,0 +1,88 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2009 Kitware, Inc., Insight Software Consortium + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ +#ifndef cmCTestMemCheckCommand_h +#define cmCTestMemCheckCommand_h + +#include "cmCTestTestCommand.h" + +class cmCTestGenericHandler; + +/** \class cmCTestMemCheck + * \brief Run a ctest script + * + * cmCTestMemCheckCommand defineds the command to test the project. + */ +class cmCTestMemCheckCommand : public cmCTestTestCommand +{ +public: + + cmCTestMemCheckCommand() {} + + /** + * This is a virtual constructor for the command. + */ + virtual cmCommand* Clone() + { + cmCTestMemCheckCommand* ni = new cmCTestMemCheckCommand; + ni->CTest = this->CTest; + ni->CTestScriptHandler = this->CTestScriptHandler; + return ni; + } + + /** + * The name of the command as specified in CMakeList.txt. + */ + virtual const char* GetName() const { return "ctest_memcheck";} + + /** + * Succinct documentation. + */ + virtual const char* GetTerseDocumentation() const + { + return "Run tests with a dynamic analysis tool."; + } + + /** + * More documentation. + */ + virtual const char* GetFullDocumentation() const + { + return + " ctest_memcheck([BUILD build_dir] [RETURN_VALUE res] [APPEND]\n" + " [START start number] [END end number]\n" + " [STRIDE stride number] [EXCLUDE exclude regex ]\n" + " [INCLUDE include regex] \n" + " [EXCLUDE_LABEL exclude regex] \n" + " [INCLUDE_LABEL label regex] \n" + " [PARALLEL_LEVEL level] )\n" + "Tests the given build directory and stores results in MemCheck.xml. " + "The second argument is a variable that will hold value. Optionally, " + "you can specify the starting test number START, the ending test number " + "END, the number of tests to skip between each test STRIDE, a regular " + "expression for tests to run INCLUDE, or a regular expression for tests " + "not to run EXCLUDE. EXCLUDE_LABEL and INCLUDE_LABEL are regular " + "expressions for tests to be included or excluded by the test " + "property LABEL. PARALLEL_LEVEL should be set to a positive number " + "representing the number of tests to be run in parallel." + "\n" + CTEST_COMMAND_APPEND_OPTION_DOCS; + } + + cmTypeMacro(cmCTestMemCheckCommand, cmCTestTestCommand); + +protected: + cmCTestGenericHandler* InitializeActualHandler(); +}; + + +#endif + diff --git a/Source/CTest/cmCTestMemCheckHandler.cxx b/Source/CTest/cmCTestMemCheckHandler.cxx new file mode 100644 index 000000000..3e4ecddaa --- /dev/null +++ b/Source/CTest/cmCTestMemCheckHandler.cxx @@ -0,0 +1,955 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2009 Kitware, Inc., Insight Software Consortium + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ + +#include "cmCTestMemCheckHandler.h" +#include "cmXMLParser.h" +#include "cmCTest.h" +#include "cmake.h" +#include "cmGeneratedFileStream.h" +#include <cmsys/Process.h> +#include <cmsys/RegularExpression.hxx> +#include <cmsys/Base64.h> +#include "cmMakefile.h" +#include "cmXMLSafe.h" + +#include <stdlib.h> +#include <math.h> +#include <float.h> + +struct CatToErrorType +{ + const char* ErrorCategory; + int ErrorCode; +}; + + +static CatToErrorType cmCTestMemCheckBoundsChecker[] = { + // Error tags + {"Write Overrun", cmCTestMemCheckHandler::ABW}, + {"Read Overrun", cmCTestMemCheckHandler::ABR}, + {"Memory Overrun", cmCTestMemCheckHandler::ABW}, + {"Allocation Conflict", cmCTestMemCheckHandler::FMM}, + {"Bad Pointer Use", cmCTestMemCheckHandler::FMW}, + {"Dangling Pointer", cmCTestMemCheckHandler::FMR}, + {0,0} +}; + +// parse the xml file storing the installed version of Xcode on +// the machine +class cmBoundsCheckerParser : public cmXMLParser +{ +public: + cmBoundsCheckerParser(cmCTest* c) { this->CTest = c;} + void StartElement(const char* name, const char** atts) + { + if(strcmp(name, "MemoryLeak") == 0) + { + this->Errors.push_back(cmCTestMemCheckHandler::MLK); + } + if(strcmp(name, "ResourceLeak") == 0) + { + this->Errors.push_back(cmCTestMemCheckHandler::MLK); + } + if(strcmp(name, "Error") == 0) + { + this->ParseError(atts); + } + if(strcmp(name, "Dangling Pointer") == 0) + { + this->ParseError(atts); + } + // Create the log + cmOStringStream ostr; + ostr << name << ":\n"; + int i = 0; + for(; atts[i] != 0; i+=2) + { + ostr << " " << cmXMLSafe(atts[i]) + << " - " << cmXMLSafe(atts[i+1]) << "\n"; + } + ostr << "\n"; + this->Log += ostr.str(); + } + void EndElement(const char* ) + { + } + + const char* GetAttribute(const char* name, const char** atts) + { + int i = 0; + for(; atts[i] != 0; ++i) + { + if(strcmp(name, atts[i]) == 0) + { + return atts[i+1]; + } + } + return 0; + } + void ParseError(const char** atts) + { + CatToErrorType* ptr = cmCTestMemCheckBoundsChecker; + const char* cat = this->GetAttribute("ErrorCategory", atts); + if(!cat) + { + this->Errors.push_back(cmCTestMemCheckHandler::ABW); // do not know + cmCTestLog(this->CTest, ERROR_MESSAGE, + "No Category found in Bounds checker XML\n" ); + return; + } + while(ptr->ErrorCategory && cat) + { + if(strcmp(ptr->ErrorCategory, cat) == 0) + { + this->Errors.push_back(ptr->ErrorCode); + return; // found it we are done + } + ptr++; + } + if(ptr->ErrorCategory) + { + this->Errors.push_back(cmCTestMemCheckHandler::ABW); // do not know + cmCTestLog(this->CTest, ERROR_MESSAGE, + "Found unknown Bounds Checker error " + << ptr->ErrorCategory << std::endl); + } + } + cmCTest* CTest; + std::vector<int> Errors; + std::string Log; +}; + +#define BOUNDS_CHECKER_MARKER \ +"******######*****Begin BOUNDS CHECKER XML******######******" +//---------------------------------------------------------------------- +static const char* cmCTestMemCheckResultStrings[] = { + "ABR", + "ABW", + "ABWL", + "COR", + "EXU", + "FFM", + "FIM", + "FMM", + "FMR", + "FMW", + "FUM", + "IPR", + "IPW", + "MAF", + "MLK", + "MPK", + "NPR", + "ODS", + "PAR", + "PLK", + "UMC", + "UMR", + 0 +}; + + +//---------------------------------------------------------------------- +static const char* cmCTestMemCheckResultLongStrings[] = { + "Threading Problem", + "ABW", + "ABWL", + "COR", + "EXU", + "FFM", + "FIM", + "Mismatched deallocation", + "FMR", + "FMW", + "FUM", + "IPR", + "IPW", + "MAF", + "Memory Leak", + "Potential Memory Leak", + "NPR", + "ODS", + "Invalid syscall param", + "PLK", + "Uninitialized Memory Conditional", + "Uninitialized Memory Read", + 0 +}; + + +//---------------------------------------------------------------------- +cmCTestMemCheckHandler::cmCTestMemCheckHandler() +{ + this->MemCheck = true; + this->CustomMaximumPassedTestOutputSize = 0; + this->CustomMaximumFailedTestOutputSize = 0; +} + +//---------------------------------------------------------------------- +void cmCTestMemCheckHandler::Initialize() +{ + this->Superclass::Initialize(); + this->CustomMaximumPassedTestOutputSize = 0; + this->CustomMaximumFailedTestOutputSize = 0; + this->MemoryTester = ""; + this->MemoryTesterOptions.clear(); + this->MemoryTesterStyle = UNKNOWN; + this->MemoryTesterOutputFile = ""; + int cc; + for ( cc = 0; cc < NO_MEMORY_FAULT; cc ++ ) + { + this->MemoryTesterGlobalResults[cc] = 0; + } + +} + +//---------------------------------------------------------------------- +int cmCTestMemCheckHandler::PreProcessHandler() +{ + if ( !this->InitializeMemoryChecking() ) + { + return 0; + } + + if ( !this->ExecuteCommands(this->CustomPreMemCheck) ) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, + "Problem executing pre-memcheck command(s)." << std::endl); + return 0; + } + return 1; +} + +//---------------------------------------------------------------------- +int cmCTestMemCheckHandler::PostProcessHandler() +{ + if ( !this->ExecuteCommands(this->CustomPostMemCheck) ) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, + "Problem executing post-memcheck command(s)." << std::endl); + return 0; + } + return 1; +} + +//---------------------------------------------------------------------- +void cmCTestMemCheckHandler::GenerateTestCommand( + std::vector<std::string>& args) +{ + std::vector<cmStdString>::size_type pp; + std::string memcheckcommand = ""; + memcheckcommand = this->MemoryTester; + for ( pp = 0; pp < this->MemoryTesterOptions.size(); pp ++ ) + { + args.push_back(this->MemoryTesterOptions[pp]); + memcheckcommand += " \""; + memcheckcommand += this->MemoryTesterOptions[pp]; + memcheckcommand += "\""; + } + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, "Memory check command: " + << memcheckcommand << std::endl); +} + +//---------------------------------------------------------------------- +void cmCTestMemCheckHandler::PopulateCustomVectors(cmMakefile *mf) +{ + this->cmCTestTestHandler::PopulateCustomVectors(mf); + this->CTest->PopulateCustomVector(mf, "CTEST_CUSTOM_PRE_MEMCHECK", + this->CustomPreMemCheck); + this->CTest->PopulateCustomVector(mf, "CTEST_CUSTOM_POST_MEMCHECK", + this->CustomPostMemCheck); + + this->CTest->PopulateCustomVector(mf, + "CTEST_CUSTOM_MEMCHECK_IGNORE", + this->CustomTestsIgnore); +} + +//---------------------------------------------------------------------- +void cmCTestMemCheckHandler::GenerateDartOutput(std::ostream& os) +{ + if ( !this->CTest->GetProduceXML() ) + { + return; + } + + this->CTest->StartXML(os, this->AppendXML); + os << "<DynamicAnalysis Checker=\""; + switch ( this->MemoryTesterStyle ) + { + case cmCTestMemCheckHandler::VALGRIND: + os << "Valgrind"; + break; + case cmCTestMemCheckHandler::PURIFY: + os << "Purify"; + break; + case cmCTestMemCheckHandler::BOUNDS_CHECKER: + os << "BoundsChecker"; + break; + default: + os << "Unknown"; + } + os << "\">" << std::endl; + + os << "\t<StartDateTime>" << this->StartTest << "</StartDateTime>\n" + << "\t<StartTestTime>" << this->StartTestTime << "</StartTestTime>\n" + << "\t<TestList>\n"; + cmCTestMemCheckHandler::TestResultsVector::size_type cc; + for ( cc = 0; cc < this->TestResults.size(); cc ++ ) + { + cmCTestTestResult *result = &this->TestResults[cc]; + std::string testPath = result->Path + "/" + result->Name; + os << "\t\t<Test>" << cmXMLSafe( + this->CTest->GetShortPathToFile(testPath.c_str())) + << "</Test>" << std::endl; + } + os << "\t</TestList>\n"; + cmCTestLog(this->CTest, HANDLER_OUTPUT, + "-- Processing memory checking output: "); + size_t total = this->TestResults.size(); + size_t step = total / 10; + size_t current = 0; + for ( cc = 0; cc < this->TestResults.size(); cc ++ ) + { + cmCTestTestResult *result = &this->TestResults[cc]; + std::string memcheckstr; + int memcheckresults[cmCTestMemCheckHandler::NO_MEMORY_FAULT]; + int kk; + bool res = this->ProcessMemCheckOutput(result->Output, memcheckstr, + memcheckresults); + if ( res && result->Status == cmCTestMemCheckHandler::COMPLETED ) + { + continue; + } + this->CleanTestOutput(memcheckstr, + static_cast<size_t>(this->CustomMaximumFailedTestOutputSize)); + this->WriteTestResultHeader(os, result); + os << "\t\t<Results>" << std::endl; + for ( kk = 0; cmCTestMemCheckResultLongStrings[kk]; kk ++ ) + { + if ( memcheckresults[kk] ) + { + os << "\t\t\t<Defect type=\"" << cmCTestMemCheckResultLongStrings[kk] + << "\">" + << memcheckresults[kk] + << "</Defect>" << std::endl; + } + this->MemoryTesterGlobalResults[kk] += memcheckresults[kk]; + } + + std::string logTag; + if(this->CTest->ShouldCompressMemCheckOutput()) + { + this->CTest->CompressString(memcheckstr); + logTag = "\t<Log compression=\"gzip\" encoding=\"base64\">\n"; + } + else + { + logTag = "\t<Log>\n"; + } + + os + << "\t\t</Results>\n" + << logTag << cmXMLSafe(memcheckstr) << std::endl + << "\t</Log>\n"; + this->WriteTestResultFooter(os, result); + if ( current < cc ) + { + cmCTestLog(this->CTest, HANDLER_OUTPUT, "#" << std::flush); + current += step; + } + } + cmCTestLog(this->CTest, HANDLER_OUTPUT, std::endl); + cmCTestLog(this->CTest, HANDLER_OUTPUT, "Memory checking results:" + << std::endl); + os << "\t<DefectList>" << std::endl; + for ( cc = 0; cmCTestMemCheckResultStrings[cc]; cc ++ ) + { + if ( this->MemoryTesterGlobalResults[cc] ) + { +#ifdef cerr +# undef cerr +#endif + std::cerr.width(35); +#define cerr no_cerr + cmCTestLog(this->CTest, HANDLER_OUTPUT, + cmCTestMemCheckResultLongStrings[cc] << " - " + << this->MemoryTesterGlobalResults[cc] << std::endl); + os << "\t\t<Defect Type=\"" << cmCTestMemCheckResultLongStrings[cc] + << "\"/>" << std::endl; + } + } + os << "\t</DefectList>" << std::endl; + + os << "\t<EndDateTime>" << this->EndTest << "</EndDateTime>" << std::endl; + os << "\t<EndTestTime>" << this->EndTestTime + << "</EndTestTime>" << std::endl; + os << "<ElapsedMinutes>" + << static_cast<int>(this->ElapsedTestingTime/6)/10.0 + << "</ElapsedMinutes>\n"; + + os << "</DynamicAnalysis>" << std::endl; + this->CTest->EndXML(os); + + +} + +//---------------------------------------------------------------------- +bool cmCTestMemCheckHandler::InitializeMemoryChecking() +{ + // Setup the command + if ( cmSystemTools::FileExists(this->CTest->GetCTestConfiguration( + "MemoryCheckCommand").c_str()) ) + { + this->MemoryTester + = cmSystemTools::ConvertToOutputPath(this->CTest->GetCTestConfiguration( + "MemoryCheckCommand").c_str()); + } + else if ( cmSystemTools::FileExists(this->CTest->GetCTestConfiguration( + "PurifyCommand").c_str()) ) + { + this->MemoryTester + = cmSystemTools::ConvertToOutputPath(this->CTest->GetCTestConfiguration( + "PurifyCommand").c_str()); + } + else if ( cmSystemTools::FileExists(this->CTest->GetCTestConfiguration( + "ValgrindCommand").c_str()) ) + { + this->MemoryTester + = cmSystemTools::ConvertToOutputPath(this->CTest->GetCTestConfiguration( + "ValgrindCommand").c_str()); + } + else if ( cmSystemTools::FileExists(this->CTest->GetCTestConfiguration( + "BoundsCheckerCommand").c_str()) ) + { + this->MemoryTester + = cmSystemTools::ConvertToOutputPath(this->CTest->GetCTestConfiguration( + "BoundsCheckerCommand").c_str()); + } + else + { + cmCTestLog(this->CTest, WARNING, + "Memory checker (MemoryCheckCommand) " + "not set, or cannot find the specified program." + << std::endl); + return false; + } + + if ( this->MemoryTester[0] == '\"' && + this->MemoryTester[this->MemoryTester.size()-1] == '\"' ) + { + this->MemoryTester + = this->MemoryTester.substr(1, this->MemoryTester.size()-2); + } + + // Setup the options + std::string memoryTesterOptions; + if ( this->CTest->GetCTestConfiguration( + "MemoryCheckCommandOptions").size() ) + { + memoryTesterOptions = this->CTest->GetCTestConfiguration( + "MemoryCheckCommandOptions"); + } + else if ( this->CTest->GetCTestConfiguration( + "ValgrindCommandOptions").size() ) + { + memoryTesterOptions = this->CTest->GetCTestConfiguration( + "ValgrindCommandOptions"); + } + this->MemoryTesterOptions + = cmSystemTools::ParseArguments(memoryTesterOptions.c_str()); + + this->MemoryTesterOutputFile + = this->CTest->GetBinaryDir() + "/Testing/Temporary/MemoryChecker.log"; + + if ( this->MemoryTester.find("valgrind") != std::string::npos ) + { + this->MemoryTesterStyle = cmCTestMemCheckHandler::VALGRIND; + if ( this->MemoryTesterOptions.empty() ) + { + this->MemoryTesterOptions.push_back("-q"); + this->MemoryTesterOptions.push_back("--tool=memcheck"); + this->MemoryTesterOptions.push_back("--leak-check=yes"); + this->MemoryTesterOptions.push_back("--show-reachable=yes"); + this->MemoryTesterOptions.push_back("--workaround-gcc296-bugs=yes"); + this->MemoryTesterOptions.push_back("--num-callers=50"); + } + if ( this->CTest->GetCTestConfiguration( + "MemoryCheckSuppressionFile").size() ) + { + if ( !cmSystemTools::FileExists(this->CTest->GetCTestConfiguration( + "MemoryCheckSuppressionFile").c_str()) ) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, + "Cannot find memory checker suppression file: " + << this->CTest->GetCTestConfiguration( + "MemoryCheckSuppressionFile").c_str() << std::endl); + return false; + } + std::string suppressions = "--suppressions=" + + this->CTest->GetCTestConfiguration("MemoryCheckSuppressionFile"); + this->MemoryTesterOptions.push_back(suppressions); + } + } + else if ( this->MemoryTester.find("purify") != std::string::npos ) + { + this->MemoryTesterStyle = cmCTestMemCheckHandler::PURIFY; + std::string outputFile; +#ifdef _WIN32 + if( this->CTest->GetCTestConfiguration( + "MemoryCheckSuppressionFile").size() ) + { + if( !cmSystemTools::FileExists(this->CTest->GetCTestConfiguration( + "MemoryCheckSuppressionFile").c_str()) ) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, + "Cannot find memory checker suppression file: " + << this->CTest->GetCTestConfiguration( + "MemoryCheckSuppressionFile").c_str() << std::endl); + return false; + } + std::string filterFiles = "/FilterFiles=" + + this->CTest->GetCTestConfiguration("MemoryCheckSuppressionFile"); + this->MemoryTesterOptions.push_back(filterFiles); + } + outputFile = "/SAVETEXTDATA="; +#else + outputFile = "-log-file="; +#endif + outputFile += this->MemoryTesterOutputFile; + this->MemoryTesterOptions.push_back(outputFile); + } + else if ( this->MemoryTester.find("BC") != std::string::npos ) + { + this->BoundsCheckerXMLFile = this->MemoryTesterOutputFile; + std::string dpbdFile = this->CTest->GetBinaryDir() + + "/Testing/Temporary/MemoryChecker.DPbd"; + this->BoundsCheckerDPBDFile = dpbdFile; + this->MemoryTesterStyle = cmCTestMemCheckHandler::BOUNDS_CHECKER; + this->MemoryTesterOptions.push_back("/B"); + this->MemoryTesterOptions.push_back(dpbdFile); + this->MemoryTesterOptions.push_back("/X"); + this->MemoryTesterOptions.push_back(this->MemoryTesterOutputFile); + this->MemoryTesterOptions.push_back("/M"); + } + else + { + cmCTestLog(this->CTest, ERROR_MESSAGE, + "Do not understand memory checker: " << this->MemoryTester.c_str() + << std::endl); + return false; + } + + std::vector<cmStdString>::size_type cc; + for ( cc = 0; cmCTestMemCheckResultStrings[cc]; cc ++ ) + { + this->MemoryTesterGlobalResults[cc] = 0; + } + return true; +} + +//---------------------------------------------------------------------- +bool cmCTestMemCheckHandler::ProcessMemCheckOutput(const std::string& str, + std::string& log, int* results) +{ + std::string::size_type cc; + for ( cc = 0; cc < cmCTestMemCheckHandler::NO_MEMORY_FAULT; cc ++ ) + { + results[cc] = 0; + } + + if ( this->MemoryTesterStyle == cmCTestMemCheckHandler::VALGRIND ) + { + return this->ProcessMemCheckValgrindOutput(str, log, results); + } + else if ( this->MemoryTesterStyle == cmCTestMemCheckHandler::PURIFY ) + { + return this->ProcessMemCheckPurifyOutput(str, log, results); + } + else if ( this->MemoryTesterStyle == + cmCTestMemCheckHandler::BOUNDS_CHECKER ) + { + return this->ProcessMemCheckBoundsCheckerOutput(str, log, results); + } + else + { + log.append("\nMemory checking style used was: "); + log.append("None that I know"); + log = str; + } + + + return true; +} + +//---------------------------------------------------------------------- +bool cmCTestMemCheckHandler::ProcessMemCheckPurifyOutput( + const std::string& str, std::string& log, + int* results) +{ + std::vector<cmStdString> lines; + cmSystemTools::Split(str.c_str(), lines); + cmOStringStream ostr; + log = ""; + + cmsys::RegularExpression pfW("^\\[[WEI]\\] ([A-Z][A-Z][A-Z][A-Z]*): "); + + int defects = 0; + + for( std::vector<cmStdString>::iterator i = lines.begin(); + i != lines.end(); ++i) + { + int failure = cmCTestMemCheckHandler::NO_MEMORY_FAULT; + if ( pfW.find(*i) ) + { + int cc; + for ( cc = 0; cc < cmCTestMemCheckHandler::NO_MEMORY_FAULT; cc ++ ) + { + if ( pfW.match(1) == cmCTestMemCheckResultStrings[cc] ) + { + failure = cc; + break; + } + } + if ( cc == cmCTestMemCheckHandler::NO_MEMORY_FAULT ) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, "Unknown Purify memory fault: " + << pfW.match(1) << std::endl); + ostr << "*** Unknown Purify memory fault: " << pfW.match(1) + << std::endl; + } + } + if ( failure != NO_MEMORY_FAULT ) + { + ostr << "<b>" << cmCTestMemCheckResultStrings[failure] << "</b> "; + results[failure] ++; + defects ++; + } + ostr << cmXMLSafe(*i) << std::endl; + } + + log = ostr.str(); + if ( defects ) + { + return false; + } + return true; +} + +//---------------------------------------------------------------------- +bool cmCTestMemCheckHandler::ProcessMemCheckValgrindOutput( + const std::string& str, std::string& log, + int* results) +{ + std::vector<cmStdString> lines; + cmSystemTools::Split(str.c_str(), lines); + bool unlimitedOutput = false; + if(str.find("CTEST_FULL_OUTPUT") != str.npos || + this->CustomMaximumFailedTestOutputSize == 0) + { + unlimitedOutput = true; + } + + std::string::size_type cc; + + cmOStringStream ostr; + log = ""; + + int defects = 0; + + cmsys::RegularExpression valgrindLine("^==[0-9][0-9]*=="); + + cmsys::RegularExpression vgFIM( + "== .*Invalid free\\(\\) / delete / delete\\[\\]"); + cmsys::RegularExpression vgFMM( + "== .*Mismatched free\\(\\) / delete / delete \\[\\]"); + cmsys::RegularExpression vgMLK1( + "== .*[0-9,]+ bytes in [0-9,]+ blocks are definitely lost" + " in loss record [0-9,]+ of [0-9,]+"); + cmsys::RegularExpression vgMLK2( + "== .*[0-9,]+ \\([0-9,]+ direct, [0-9,]+ indirect\\)" + " bytes in [0-9,]+ blocks are definitely lost" + " in loss record [0-9,]+ of [0-9,]+"); + cmsys::RegularExpression vgPAR( + "== .*Syscall param .* (contains|points to) unaddressable byte\\(s\\)"); + cmsys::RegularExpression vgMPK1( + "== .*[0-9,]+ bytes in [0-9,]+ blocks are possibly lost in" + " loss record [0-9,]+ of [0-9,]+"); + cmsys::RegularExpression vgMPK2( + "== .*[0-9,]+ bytes in [0-9,]+ blocks are still reachable" + " in loss record [0-9,]+ of [0-9,]+"); + cmsys::RegularExpression vgUMC( + "== .*Conditional jump or move depends on uninitialised value\\(s\\)"); + cmsys::RegularExpression vgUMR1( + "== .*Use of uninitialised value of size [0-9,]+"); + cmsys::RegularExpression vgUMR2("== .*Invalid read of size [0-9,]+"); + cmsys::RegularExpression vgUMR3("== .*Jump to the invalid address "); + cmsys::RegularExpression vgUMR4("== .*Syscall param .* contains " + "uninitialised or unaddressable byte\\(s\\)"); + cmsys::RegularExpression vgUMR5("== .*Syscall param .* uninitialised"); + cmsys::RegularExpression vgIPW("== .*Invalid write of size [0-9,]+"); + cmsys::RegularExpression vgABR("== .*pthread_mutex_unlock: mutex is " + "locked by a different thread"); + std::vector<std::string::size_type> nonValGrindOutput; + double sttime = cmSystemTools::GetTime(); + cmCTestLog(this->CTest, DEBUG, "Start test: " << lines.size() << std::endl); + std::string::size_type totalOutputSize = 0; + bool outputFull = false; + for ( cc = 0; cc < lines.size(); cc ++ ) + { + cmCTestLog(this->CTest, DEBUG, "test line " + << lines[cc] << std::endl); + + if ( valgrindLine.find(lines[cc]) ) + { + cmCTestLog(this->CTest, DEBUG, "valgrind line " + << lines[cc] << std::endl); + int failure = cmCTestMemCheckHandler::NO_MEMORY_FAULT; + if ( vgFIM.find(lines[cc]) ) + { + failure = cmCTestMemCheckHandler::FIM; + } + else if ( vgFMM.find(lines[cc]) ) + { + failure = cmCTestMemCheckHandler::FMM; + } + else if ( vgMLK1.find(lines[cc]) ) + { + failure = cmCTestMemCheckHandler::MLK; + } + else if ( vgMLK2.find(lines[cc]) ) + { + failure = cmCTestMemCheckHandler::MLK; + } + else if ( vgPAR.find(lines[cc]) ) + { + failure = cmCTestMemCheckHandler::PAR; + } + else if ( vgMPK1.find(lines[cc]) ) + { + failure = cmCTestMemCheckHandler::MPK; + } + else if ( vgMPK2.find(lines[cc]) ) + { + failure = cmCTestMemCheckHandler::MPK; + } + else if ( vgUMC.find(lines[cc]) ) + { + failure = cmCTestMemCheckHandler::UMC; + } + else if ( vgUMR1.find(lines[cc]) ) + { + failure = cmCTestMemCheckHandler::UMR; + } + else if ( vgUMR2.find(lines[cc]) ) + { + failure = cmCTestMemCheckHandler::UMR; + } + else if ( vgUMR3.find(lines[cc]) ) + { + failure = cmCTestMemCheckHandler::UMR; + } + else if ( vgUMR4.find(lines[cc]) ) + { + failure = cmCTestMemCheckHandler::UMR; + } + else if ( vgUMR5.find(lines[cc]) ) + { + failure = cmCTestMemCheckHandler::UMR; + } + else if ( vgIPW.find(lines[cc]) ) + { + failure = cmCTestMemCheckHandler::IPW; + } + else if ( vgABR.find(lines[cc]) ) + { + failure = cmCTestMemCheckHandler::ABR; + } + + if ( failure != cmCTestMemCheckHandler::NO_MEMORY_FAULT ) + { + ostr << "<b>" << cmCTestMemCheckResultStrings[failure] << "</b> "; + results[failure] ++; + defects ++; + } + totalOutputSize += lines[cc].size(); + ostr << cmXMLSafe(lines[cc]) << std::endl; + } + else + { + nonValGrindOutput.push_back(cc); + } + } + // Now put all all the non valgrind output into the test output + if(!outputFull) + { + for(std::vector<std::string::size_type>::iterator i = + nonValGrindOutput.begin(); i != nonValGrindOutput.end(); ++i) + { + totalOutputSize += lines[*i].size(); + cmCTestLog(this->CTest, DEBUG, "before xml safe " + << lines[*i] << std::endl); + cmCTestLog(this->CTest, DEBUG, "after xml safe " + << cmXMLSafe(lines[*i]) << std::endl); + + ostr << cmXMLSafe(lines[*i]) << std::endl; + if(!unlimitedOutput && totalOutputSize > + static_cast<size_t>(this->CustomMaximumFailedTestOutputSize)) + { + outputFull = true; + ostr << "....\n"; + ostr << "Test Output for this test has been truncated see testing" + " machine logs for full output,\n"; + ostr << "or put CTEST_FULL_OUTPUT in the output of " + "this test program.\n"; + } + } + } + cmCTestLog(this->CTest, DEBUG, "End test (elapsed: " + << (cmSystemTools::GetTime() - sttime) << std::endl); + log = ostr.str(); + if ( defects ) + { + return false; + } + return true; +} + + + +//---------------------------------------------------------------------- +bool cmCTestMemCheckHandler::ProcessMemCheckBoundsCheckerOutput( + const std::string& str, std::string& log, + int* results) +{ + log = ""; + double sttime = cmSystemTools::GetTime(); + std::vector<cmStdString> lines; + cmSystemTools::Split(str.c_str(), lines); + cmCTestLog(this->CTest, DEBUG, "Start test: " << lines.size() << std::endl); + std::vector<cmStdString>::size_type cc; + for ( cc = 0; cc < lines.size(); cc ++ ) + { + if(lines[cc] == BOUNDS_CHECKER_MARKER) + { + break; + } + } + cmBoundsCheckerParser parser(this->CTest); + parser.InitializeParser(); + if(cc < lines.size()) + { + for(cc++; cc < lines.size(); ++cc) + { + std::string& theLine = lines[cc]; + // check for command line arguments that are not escaped + // correctly by BC + if(theLine.find("TargetArgs=") != theLine.npos) + { + // skip this because BC gets it wrong and we can't parse it + } + else if(!parser.ParseChunk(theLine.c_str(), theLine.size())) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, + "Error in ParseChunk: " << theLine.c_str() + << std::endl); + } + } + } + int defects = 0; + for(cc =0; cc < parser.Errors.size(); ++cc) + { + results[parser.Errors[cc]]++; + defects++; + } + cmCTestLog(this->CTest, DEBUG, "End test (elapsed: " + << (cmSystemTools::GetTime() - sttime) << std::endl); + if(defects) + { + // only put the output of Bounds Checker if there were + // errors or leaks detected + log = parser.Log; + return false; + } + return true; +} + +// This method puts the bounds checker output file into the output +// for the test +void +cmCTestMemCheckHandler::PostProcessBoundsCheckerTest(cmCTestTestResult& res) +{ + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, + "PostProcessBoundsCheckerTest for : " + << res.Name.c_str() << std::endl); + if ( !cmSystemTools::FileExists(this->MemoryTesterOutputFile.c_str()) ) + { + std::string log = "Cannot find memory tester output file: " + + this->MemoryTesterOutputFile; + cmCTestLog(this->CTest, ERROR_MESSAGE, log.c_str() << std::endl); + return; + } + // put a scope around this to close ifs so the file can be removed + { + std::ifstream ifs(this->MemoryTesterOutputFile.c_str()); + if ( !ifs ) + { + std::string log = "Cannot read memory tester output file: " + + this->MemoryTesterOutputFile; + cmCTestLog(this->CTest, ERROR_MESSAGE, log.c_str() << std::endl); + return; + } + res.Output += BOUNDS_CHECKER_MARKER; + res.Output += "\n"; + std::string line; + while ( cmSystemTools::GetLineFromStream(ifs, line) ) + { + res.Output += line; + res.Output += "\n"; + } + } + cmSystemTools::Delay(1000); + cmSystemTools::RemoveFile(this->BoundsCheckerDPBDFile.c_str()); + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, "Remove: " + << this->BoundsCheckerDPBDFile.c_str() << std::endl); + cmSystemTools::RemoveFile(this->BoundsCheckerXMLFile.c_str()); + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, "Remove: " + << this->BoundsCheckerXMLFile.c_str() << std::endl); +} + +void +cmCTestMemCheckHandler::PostProcessPurifyTest(cmCTestTestResult& res) +{ + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, + "PostProcessPurifyTest for : " + << res.Name.c_str() << std::endl); + if ( !cmSystemTools::FileExists(this->MemoryTesterOutputFile.c_str()) ) + { + std::string log = "Cannot find memory tester output file: " + + this->MemoryTesterOutputFile; + cmCTestLog(this->CTest, ERROR_MESSAGE, log.c_str() << std::endl); + return; + } + std::ifstream ifs(this->MemoryTesterOutputFile.c_str()); + if ( !ifs ) + { + std::string log = "Cannot read memory tester output file: " + + this->MemoryTesterOutputFile; + cmCTestLog(this->CTest, ERROR_MESSAGE, log.c_str() << std::endl); + return; + } + std::string line; + while ( cmSystemTools::GetLineFromStream(ifs, line) ) + { + res.Output += line; + res.Output += "\n"; + } +} diff --git a/Source/CTest/cmCTestMemCheckHandler.h b/Source/CTest/cmCTestMemCheckHandler.h new file mode 100644 index 000000000..427d4717d --- /dev/null +++ b/Source/CTest/cmCTestMemCheckHandler.h @@ -0,0 +1,125 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2009 Kitware, Inc. + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ + +#ifndef cmCTestMemCheckHandler_h +#define cmCTestMemCheckHandler_h + + +#include "cmCTestTestHandler.h" +#include "cmListFileCache.h" + +class cmMakefile; + +/** \class cmCTestMemCheckHandler + * \brief A class that handles ctest -S invocations + * + */ +class cmCTestMemCheckHandler : public cmCTestTestHandler +{ + friend class cmCTestRunTest; +public: + cmTypeMacro(cmCTestMemCheckHandler, cmCTestTestHandler); + + void PopulateCustomVectors(cmMakefile *mf); + + cmCTestMemCheckHandler(); + + void Initialize(); +protected: + virtual int PreProcessHandler(); + virtual int PostProcessHandler(); + virtual void GenerateTestCommand(std::vector<std::string>& args); + +private: + + enum { // Memory checkers + UNKNOWN = 0, + VALGRIND, + PURIFY, + BOUNDS_CHECKER + }; +public: + enum { // Memory faults + ABR = 0, + ABW, + ABWL, + COR, + EXU, + FFM, + FIM, + FMM, + FMR, + FMW, + FUM, + IPR, + IPW, + MAF, + MLK, + MPK, + NPR, + ODS, + PAR, + PLK, + UMC, + UMR, + NO_MEMORY_FAULT + }; +private: + enum { // Program statuses + NOT_RUN = 0, + TIMEOUT, + SEGFAULT, + ILLEGAL, + INTERRUPT, + NUMERICAL, + OTHER_FAULT, + FAILED, + BAD_COMMAND, + COMPLETED + }; + std::string BoundsCheckerDPBDFile; + std::string BoundsCheckerXMLFile; + std::string MemoryTester; + std::vector<cmStdString> MemoryTesterOptions; + int MemoryTesterStyle; + std::string MemoryTesterOutputFile; + int MemoryTesterGlobalResults[NO_MEMORY_FAULT]; + + ///! Initialize memory checking subsystem. + bool InitializeMemoryChecking(); + + /** + * Generate the Dart compatible output + */ + void GenerateDartOutput(std::ostream& os); + + std::vector<cmStdString> CustomPreMemCheck; + std::vector<cmStdString> CustomPostMemCheck; + + //! Parse Valgrind/Purify/Bounds Checker result out of the output + //string. After running, log holds the output and results hold the + //different memmory errors. + bool ProcessMemCheckOutput(const std::string& str, + std::string& log, int* results); + bool ProcessMemCheckValgrindOutput(const std::string& str, + std::string& log, int* results); + bool ProcessMemCheckPurifyOutput(const std::string& str, + std::string& log, int* results); + bool ProcessMemCheckBoundsCheckerOutput(const std::string& str, + std::string& log, int* results); + + void PostProcessPurifyTest(cmCTestTestResult& res); + void PostProcessBoundsCheckerTest(cmCTestTestResult& res); +}; + +#endif + diff --git a/Source/CTest/cmCTestMultiProcessHandler.cxx b/Source/CTest/cmCTestMultiProcessHandler.cxx new file mode 100644 index 000000000..2cae80215 --- /dev/null +++ b/Source/CTest/cmCTestMultiProcessHandler.cxx @@ -0,0 +1,691 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2009 Kitware, Inc., Insight Software Consortium + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ +#include "cmCTestMultiProcessHandler.h" +#include "cmProcess.h" +#include "cmStandardIncludes.h" +#include "cmCTest.h" +#include "cmSystemTools.h" +#include <stdlib.h> +#include <stack> +#include <float.h> + +class TestComparator +{ +public: + TestComparator(cmCTestMultiProcessHandler* handler) : Handler(handler) {} + ~TestComparator() {} + + // Sorts tests in descending order of cost + bool operator() (int index1, int index2) const + { + return Handler->Properties[index1]->Cost > + Handler->Properties[index2]->Cost; + } + +private: + cmCTestMultiProcessHandler* Handler; +}; + +cmCTestMultiProcessHandler::cmCTestMultiProcessHandler() +{ + this->ParallelLevel = 1; + this->Completed = 0; + this->RunningCount = 0; + this->StopTimePassed = false; +} + +cmCTestMultiProcessHandler::~cmCTestMultiProcessHandler() +{ +} + + // Set the tests +void +cmCTestMultiProcessHandler::SetTests(TestMap& tests, + PropertiesMap& properties) +{ + this->Tests = tests; + this->Properties = properties; + this->Total = this->Tests.size(); + // set test run map to false for all + for(TestMap::iterator i = this->Tests.begin(); + i != this->Tests.end(); ++i) + { + this->TestRunningMap[i->first] = false; + this->TestFinishMap[i->first] = false; + } + if(!this->CTest->GetShowOnly()) + { + this->ReadCostData(); + this->CreateTestCostList(); + } +} + + // Set the max number of tests that can be run at the same time. +void cmCTestMultiProcessHandler::SetParallelLevel(size_t level) +{ + this->ParallelLevel = level < 1 ? 1 : level; +} + +//--------------------------------------------------------- +void cmCTestMultiProcessHandler::RunTests() +{ + this->CheckResume(); + if(!this->CheckCycles()) + { + return; + } + this->TestHandler->SetMaxIndex(this->FindMaxIndex()); + this->StartNextTests(); + while(this->Tests.size() != 0) + { + if(this->StopTimePassed) + { + return; + } + this->CheckOutput(); + this->StartNextTests(); + } + // let all running tests finish + while(this->CheckOutput()) + { + } + this->MarkFinished(); + this->UpdateCostData(); +} + +//--------------------------------------------------------- +void cmCTestMultiProcessHandler::StartTestProcess(int test) +{ + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, "test " << test << "\n"); + this->TestRunningMap[test] = true; // mark the test as running + // now remove the test itself + this->EraseTest(test); + this->RunningCount += GetProcessorsUsed(test); + + cmCTestRunTest* testRun = new cmCTestRunTest(this->TestHandler); + testRun->SetIndex(test); + testRun->SetTestProperties(this->Properties[test]); + + std::string current_dir = cmSystemTools::GetCurrentWorkingDirectory(); + cmSystemTools::ChangeDirectory(this->Properties[test]->Directory.c_str()); + + // Lock the resources we'll be using + this->LockResources(test); + + if(testRun->StartTest(this->Total)) + { + this->RunningTests.insert(testRun); + } + else if(testRun->IsStopTimePassed()) + { + this->StopTimePassed = true; + delete testRun; + return; + } + else + { + this->UnlockResources(test); + this->Completed++; + this->TestFinishMap[test] = true; + this->TestRunningMap[test] = false; + this->RunningCount -= GetProcessorsUsed(test); + testRun->EndTest(this->Completed, this->Total, false); + this->Failed->push_back(this->Properties[test]->Name); + delete testRun; + } + cmSystemTools::ChangeDirectory(current_dir.c_str()); +} + +//--------------------------------------------------------- +void cmCTestMultiProcessHandler::LockResources(int index) +{ + for(std::set<std::string>::iterator i = + this->Properties[index]->LockedResources.begin(); + i != this->Properties[index]->LockedResources.end(); ++i) + { + this->LockedResources.insert(*i); + } +} + +//--------------------------------------------------------- +void cmCTestMultiProcessHandler::UnlockResources(int index) +{ + for(std::set<std::string>::iterator i = + this->Properties[index]->LockedResources.begin(); + i != this->Properties[index]->LockedResources.end(); ++i) + { + this->LockedResources.erase(*i); + } +} + +//--------------------------------------------------------- +void cmCTestMultiProcessHandler::EraseTest(int test) +{ + this->Tests.erase(test); + this->SortedTests.erase( + std::find(this->SortedTests.begin(), this->SortedTests.end(), test)); +} + +//--------------------------------------------------------- +inline size_t cmCTestMultiProcessHandler::GetProcessorsUsed(int test) +{ + size_t processors = + static_cast<int>(this->Properties[test]->Processors); + //If this is set to run serially, it must run alone. + //Also, if processors setting is set higher than the -j + //setting, we default to using all of the process slots. + if(this->Properties[test]->RunSerial + || processors > this->ParallelLevel) + { + processors = this->ParallelLevel; + } + return processors; +} + +//--------------------------------------------------------- +bool cmCTestMultiProcessHandler::StartTest(int test) +{ + //Check for locked resources + for(std::set<std::string>::iterator i = + this->Properties[test]->LockedResources.begin(); + i != this->Properties[test]->LockedResources.end(); ++i) + { + if(this->LockedResources.find(*i) != this->LockedResources.end()) + { + return false; + } + } + + // copy the depend tests locally because when + // a test is finished it will be removed from the depend list + // and we don't want to be iterating a list while removing from it + TestSet depends = this->Tests[test]; + size_t totalDepends = depends.size(); + if(totalDepends) + { + for(TestSet::const_iterator i = depends.begin(); + i != depends.end(); ++i) + { + // if the test is not already running then start it + if(!this->TestRunningMap[*i]) + { + // this test might be finished, but since + // this is a copy of the depend map we might + // still have it + if(!this->TestFinishMap[*i]) + { + // only start one test in this function + return this->StartTest(*i); + } + else + { + // the depend has been and finished + totalDepends--; + } + } + } + } + // if there are no depends left then run this test + if(totalDepends == 0) + { + this->StartTestProcess(test); + return true; + } + // This test was not able to start because it is waiting + // on depends to run + return false; +} + +//--------------------------------------------------------- +void cmCTestMultiProcessHandler::StartNextTests() +{ + size_t numToStart = this->ParallelLevel - this->RunningCount; + if(numToStart == 0) + { + return; + } + + TestList copy = this->SortedTests; + for(TestList::iterator test = copy.begin(); test != copy.end(); ++test) + { + //in case this test has already been started due to dependency + if(this->TestRunningMap[*test] || this->TestFinishMap[*test]) + { + continue; + } + size_t processors = GetProcessorsUsed(*test); + if(processors > numToStart) + { + return; + } + if(this->StartTest(*test)) + { + if(this->StopTimePassed) + { + return; + } + numToStart -= processors; + } + if(numToStart == 0) + { + return; + } + } +} + +//--------------------------------------------------------- +bool cmCTestMultiProcessHandler::CheckOutput() +{ + // no more output we are done + if(this->RunningTests.size() == 0) + { + return false; + } + std::vector<cmCTestRunTest*> finished; + std::string out, err; + for(std::set<cmCTestRunTest*>::const_iterator i = this->RunningTests.begin(); + i != this->RunningTests.end(); ++i) + { + cmCTestRunTest* p = *i; + if(!p->CheckOutput()) + { + finished.push_back(p); + } + } + for( std::vector<cmCTestRunTest*>::iterator i = finished.begin(); + i != finished.end(); ++i) + { + this->Completed++; + cmCTestRunTest* p = *i; + int test = p->GetIndex(); + + if(p->EndTest(this->Completed, this->Total, true)) + { + this->Passed->push_back(p->GetTestProperties()->Name); + } + else + { + this->Failed->push_back(p->GetTestProperties()->Name); + } + for(TestMap::iterator j = this->Tests.begin(); + j != this->Tests.end(); ++j) + { + j->second.erase(test); + } + this->TestFinishMap[test] = true; + this->TestRunningMap[test] = false; + this->RunningTests.erase(p); + this->WriteCheckpoint(test); + this->UnlockResources(test); + this->RunningCount -= GetProcessorsUsed(test); + delete p; + } + return true; +} + +//--------------------------------------------------------- +void cmCTestMultiProcessHandler::UpdateCostData() +{ + std::string fname = this->CTest->GetCostDataFile(); + std::string tmpout = fname + ".tmp"; + std::fstream fout; + fout.open(tmpout.c_str(), std::ios::out); + + PropertiesMap temp = this->Properties; + + if(cmSystemTools::FileExists(fname.c_str())) + { + std::ifstream fin; + fin.open(fname.c_str()); + + std::string line; + while(std::getline(fin, line)) + { + if(line == "---") break; + std::vector<cmsys::String> parts = + cmSystemTools::SplitString(line.c_str(), ' '); + //Format: <name> <previous_runs> <avg_cost> + if(parts.size() < 3) break; + + std::string name = parts[0]; + int prev = atoi(parts[1].c_str()); + float cost = static_cast<float>(atof(parts[2].c_str())); + + int index = this->SearchByName(name); + if(index == -1) + { + // This test is not in memory. We just rewrite the entry + fout << name << " " << prev << " " << cost << "\n"; + } + else + { + // Update with our new average cost + fout << name << " " << this->Properties[index]->PreviousRuns << " " + << this->Properties[index]->Cost << "\n"; + temp.erase(index); + } + } + fin.close(); + cmSystemTools::RemoveFile(fname.c_str()); + } + + // Add all tests not previously listed in the file + for(PropertiesMap::iterator i = temp.begin(); i != temp.end(); ++i) + { + fout << i->second->Name << " " << i->second->PreviousRuns << " " + << i->second->Cost << "\n"; + } + + // Write list of failed tests + fout << "---\n"; + for(std::vector<cmStdString>::iterator i = this->Failed->begin(); + i != this->Failed->end(); ++i) + { + fout << i->c_str() << "\n"; + } + fout.close(); + cmSystemTools::RenameFile(tmpout.c_str(), fname.c_str()); +} + +//--------------------------------------------------------- +void cmCTestMultiProcessHandler::ReadCostData() +{ + std::string fname = this->CTest->GetCostDataFile(); + + if(cmSystemTools::FileExists(fname.c_str(), true)) + { + std::ifstream fin; + fin.open(fname.c_str()); + std::string line; + while(std::getline(fin, line)) + { + if(line == "---") break; + + std::vector<cmsys::String> parts = + cmSystemTools::SplitString(line.c_str(), ' '); + + // Probably an older version of the file, will be fixed next run + if(parts.size() < 3) + { + fin.close(); + return; + } + + std::string name = parts[0]; + int prev = atoi(parts[1].c_str()); + float cost = static_cast<float>(atof(parts[2].c_str())); + + int index = this->SearchByName(name); + if(index == -1) continue; + + this->Properties[index]->PreviousRuns = prev; + // When not running in parallel mode, don't use cost data + if(this->ParallelLevel > 1 && + this->Properties[index] && + this->Properties[index]->Cost == 0) + { + this->Properties[index]->Cost = cost; + } + } + // Next part of the file is the failed tests + while(std::getline(fin, line)) + { + if(line != "") + { + this->LastTestsFailed.push_back(line); + } + } + fin.close(); + } +} + +//--------------------------------------------------------- +int cmCTestMultiProcessHandler::SearchByName(std::string name) +{ + int index = -1; + + for(PropertiesMap::iterator i = this->Properties.begin(); + i != this->Properties.end(); ++i) + { + if(i->second->Name == name) + { + index = i->first; + } + } + return index; +} + +//--------------------------------------------------------- +void cmCTestMultiProcessHandler::CreateTestCostList() +{ + for(TestMap::iterator i = this->Tests.begin(); + i != this->Tests.end(); ++i) + { + SortedTests.push_back(i->first); + + //If the test failed last time, it should be run first, so max the cost. + //Only do this for parallel runs; in non-parallel runs, avoid clobbering + //the test's explicitly set cost. + if(this->ParallelLevel > 1 && + std::find(this->LastTestsFailed.begin(), this->LastTestsFailed.end(), + this->Properties[i->first]->Name) != this->LastTestsFailed.end()) + { + this->Properties[i->first]->Cost = FLT_MAX; + } + } + + TestComparator comp(this); + std::stable_sort(SortedTests.begin(), SortedTests.end(), comp); +} + +//--------------------------------------------------------- +void cmCTestMultiProcessHandler::WriteCheckpoint(int index) +{ + std::string fname = this->CTest->GetBinaryDir() + + "/Testing/Temporary/CTestCheckpoint.txt"; + std::fstream fout; + fout.open(fname.c_str(), std::ios::app | std::ios::out); + fout << index << "\n"; + fout.close(); +} + +//--------------------------------------------------------- +void cmCTestMultiProcessHandler::MarkFinished() +{ + std::string fname = this->CTest->GetBinaryDir() + + "/Testing/Temporary/CTestCheckpoint.txt"; + cmSystemTools::RemoveFile(fname.c_str()); +} + +//--------------------------------------------------------- +//For ShowOnly mode +void cmCTestMultiProcessHandler::PrintTestList() +{ + this->TestHandler->SetMaxIndex(this->FindMaxIndex()); + int count = 0; + + for (PropertiesMap::iterator it = this->Properties.begin(); + it != this->Properties.end(); ++it) + { + count++; + cmCTestTestHandler::cmCTestTestProperties& p = *it->second; + + //push working dir + std::string current_dir = cmSystemTools::GetCurrentWorkingDirectory(); + cmSystemTools::ChangeDirectory(p.Directory.c_str()); + + cmCTestRunTest testRun(this->TestHandler); + testRun.SetIndex(p.Index); + testRun.SetTestProperties(&p); + testRun.ComputeArguments(); //logs the command in verbose mode + + if(p.Labels.size()) //print the labels + { + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, "Labels:"); + } + for(std::vector<std::string>::iterator label = p.Labels.begin(); + label != p.Labels.end(); ++label) + { + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, " " << *label); + } + if(p.Labels.size()) //print the labels + { + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, std::endl); + } + + if (this->TestHandler->MemCheck) + { + cmCTestLog(this->CTest, HANDLER_OUTPUT, " Memory Check"); + } + else + { + cmCTestLog(this->CTest, HANDLER_OUTPUT, " Test"); + } + cmOStringStream indexStr; + indexStr << " #" << p.Index << ":"; + cmCTestLog(this->CTest, HANDLER_OUTPUT, + std::setw(3 + getNumWidth(this->TestHandler->GetMaxIndex())) + << indexStr.str().c_str()); + cmCTestLog(this->CTest, HANDLER_OUTPUT, " "); + cmCTestLog(this->CTest, HANDLER_OUTPUT, p.Name.c_str() << std::endl); + //pop working dir + cmSystemTools::ChangeDirectory(current_dir.c_str()); + } + + cmCTestLog(this->CTest, HANDLER_OUTPUT, std::endl << "Total Tests: " + << this->Total << std::endl); +} + +void cmCTestMultiProcessHandler::PrintLabels() +{ + std::set<std::string> allLabels; + for (PropertiesMap::iterator it = this->Properties.begin(); + it != this->Properties.end(); ++it) + { + cmCTestTestHandler::cmCTestTestProperties& p = *it->second; + allLabels.insert(p.Labels.begin(), p.Labels.end()); + } + + if(allLabels.size()) + { + cmCTestLog(this->CTest, HANDLER_OUTPUT, "All Labels:" << std::endl); + } + else + { + cmCTestLog(this->CTest, HANDLER_OUTPUT, "No Labels Exist" << std::endl); + } + for(std::set<std::string>::iterator label = allLabels.begin(); + label != allLabels.end(); ++label) + { + cmCTestLog(this->CTest, HANDLER_OUTPUT, " " << *label << std::endl); + } +} + +//--------------------------------------------------------- +void cmCTestMultiProcessHandler::CheckResume() +{ + std::string fname = this->CTest->GetBinaryDir() + + "/Testing/Temporary/CTestCheckpoint.txt"; + if(this->CTest->GetFailover()) + { + if(cmSystemTools::FileExists(fname.c_str(), true)) + { + *this->TestHandler->LogFile << "Resuming previously interrupted test set" + << std::endl + << "----------------------------------------------------------" + << std::endl; + + std::ifstream fin; + fin.open(fname.c_str()); + std::string line; + while(std::getline(fin, line)) + { + int index = atoi(line.c_str()); + this->RemoveTest(index); + } + fin.close(); + } + } + else if(cmSystemTools::FileExists(fname.c_str(), true)) + { + cmSystemTools::RemoveFile(fname.c_str()); + } +} + +//--------------------------------------------------------- +void cmCTestMultiProcessHandler::RemoveTest(int index) +{ + this->EraseTest(index); + this->Properties.erase(index); + this->TestRunningMap[index] = false; + this->TestFinishMap[index] = true; + this->Completed++; +} + +//--------------------------------------------------------- +int cmCTestMultiProcessHandler::FindMaxIndex() +{ + int max = 0; + cmCTestMultiProcessHandler::TestMap::iterator i = this->Tests.begin(); + for(; i != this->Tests.end(); ++i) + { + if(i->first > max) + { + max = i->first; + } + } + return max; +} + +//Returns true if no cycles exist in the dependency graph +bool cmCTestMultiProcessHandler::CheckCycles() +{ + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, + "Checking test dependency graph..." << std::endl); + for(TestMap::iterator it = this->Tests.begin(); + it != this->Tests.end(); ++it) + { + //DFS from each element to itself + int root = it->first; + std::set<int> visited; + std::stack<int> s; + s.push(root); + while(!s.empty()) + { + int test = s.top(); + s.pop(); + if(visited.insert(test).second) + { + for(TestSet::iterator d = this->Tests[test].begin(); + d != this->Tests[test].end(); ++d) + { + if(*d == root) + { + //cycle exists + cmCTestLog(this->CTest, ERROR_MESSAGE, + "Error: a cycle exists in the test dependency graph " + "for the test \"" << this->Properties[root]->Name << + "\".\nPlease fix the cycle and run ctest again.\n"); + return false; + } + else + { + s.push(*d); + } + } + } + } + } + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, + "Checking test dependency graph end" << std::endl); + return true; +} diff --git a/Source/CTest/cmCTestMultiProcessHandler.h b/Source/CTest/cmCTestMultiProcessHandler.h new file mode 100644 index 000000000..1483440c6 --- /dev/null +++ b/Source/CTest/cmCTestMultiProcessHandler.h @@ -0,0 +1,116 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2009 Kitware, Inc., Insight Software Consortium + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ +#ifndef cmCTestMultiProcessHandler_h +#define cmCTestMultiProcessHandler_h + +#include <cmStandardIncludes.h> +#include <cmCTestTestHandler.h> +#include <cmCTestRunTest.h> + +/** \class cmCTestMultiProcessHandler + * \brief run parallel ctest + * + * cmCTestMultiProcessHandler + */ +class cmCTestMultiProcessHandler +{ + friend class TestComparator; +public: + struct TestSet : public std::set<int> {}; + struct TestMap : public std::map<int, TestSet> {}; + struct TestList : public std::vector<int> {}; + struct PropertiesMap : public + std::map<int, cmCTestTestHandler::cmCTestTestProperties*> {}; + + cmCTestMultiProcessHandler(); + virtual ~cmCTestMultiProcessHandler(); + // Set the tests + void SetTests(TestMap& tests, PropertiesMap& properties); + // Set the max number of tests that can be run at the same time. + void SetParallelLevel(size_t); + virtual void RunTests(); + void PrintTestList(); + void PrintLabels(); + + void SetPassFailVectors(std::vector<cmStdString>* passed, + std::vector<cmStdString>* failed) + { + this->Passed = passed; + this->Failed = failed; + } + void SetTestResults(std::vector<cmCTestTestHandler::cmCTestTestResult>* r) + { this->TestResults = r; } + + void SetCTest(cmCTest* ctest) { this->CTest = ctest;} + + void SetTestHandler(cmCTestTestHandler * handler) + { this->TestHandler = handler; } + + cmCTestTestHandler * GetTestHandler() + { return this->TestHandler; } +protected: + // Start the next test or tests as many as are allowed by + // ParallelLevel + void StartNextTests(); + void StartTestProcess(int test); + bool StartTest(int test); + // Mark the checkpoint for the given test + void WriteCheckpoint(int index); + + void UpdateCostData(); + void ReadCostData(); + // Return index of a test based on its name + int SearchByName(std::string name); + + void CreateTestCostList(); + // Removes the checkpoint file + void MarkFinished(); + void EraseTest(int index); + // Return true if there are still tests running + // check all running processes for output and exit case + bool CheckOutput(); + void RemoveTest(int index); + //Check if we need to resume an interrupted test set + void CheckResume(); + //Check if there are any circular dependencies + bool CheckCycles(); + int FindMaxIndex(); + inline size_t GetProcessorsUsed(int index); + + void LockResources(int index); + void UnlockResources(int index); + // map from test number to set of depend tests + TestMap Tests; + TestList SortedTests; + //Total number of tests we'll be running + size_t Total; + //Number of tests that are complete + size_t Completed; + size_t RunningCount; + bool StopTimePassed; + //list of test properties (indices concurrent to the test map) + PropertiesMap Properties; + std::map<int, bool> TestRunningMap; + std::map<int, bool> TestFinishMap; + std::map<int, cmStdString> TestOutput; + std::vector<cmStdString>* Passed; + std::vector<cmStdString>* Failed; + std::vector<std::string> LastTestsFailed; + std::set<std::string> LockedResources; + std::vector<cmCTestTestHandler::cmCTestTestResult>* TestResults; + size_t ParallelLevel; // max number of process that can be run at once + std::set<cmCTestRunTest*> RunningTests; // current running tests + cmCTestTestHandler * TestHandler; + cmCTest* CTest; +}; + +#endif diff --git a/Source/CTest/cmCTestReadCustomFilesCommand.cxx b/Source/CTest/cmCTestReadCustomFilesCommand.cxx new file mode 100644 index 000000000..5db01f98d --- /dev/null +++ b/Source/CTest/cmCTestReadCustomFilesCommand.cxx @@ -0,0 +1,35 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2009 Kitware, Inc. + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ +#include "cmCTestReadCustomFilesCommand.h" + +#include "cmCTest.h" + +bool cmCTestReadCustomFilesCommand +::InitialPass(std::vector<std::string> const& args, cmExecutionStatus &) +{ + if (args.size() < 1) + { + this->SetError("called with incorrect number of arguments"); + return false; + } + + std::vector<std::string>::const_iterator dit; + for ( dit = args.begin(); dit != args.end(); ++ dit ) + { + this->CTest->ReadCustomConfigurationFileTree(dit->c_str(), + this->Makefile); + } + + return true; +} + + diff --git a/Source/CTest/cmCTestReadCustomFilesCommand.h b/Source/CTest/cmCTestReadCustomFilesCommand.h new file mode 100644 index 000000000..f382b0f2d --- /dev/null +++ b/Source/CTest/cmCTestReadCustomFilesCommand.h @@ -0,0 +1,75 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2009 Kitware, Inc. + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ +#ifndef cmCTestReadCustomFilesCommand_h +#define cmCTestReadCustomFilesCommand_h + +#include "cmCTestCommand.h" + +/** \class cmCTestReadCustomFiles + * \brief Run a ctest script + * + * cmLibrarysCommand defines a list of executable (i.e., test) + * programs to create. + */ +class cmCTestReadCustomFilesCommand : public cmCTestCommand +{ +public: + + cmCTestReadCustomFilesCommand() {} + + /** + * This is a virtual constructor for the command. + */ + virtual cmCommand* Clone() + { + cmCTestReadCustomFilesCommand* ni = new cmCTestReadCustomFilesCommand; + ni->CTest = this->CTest; + return ni; + } + + /** + * This is called when the command is first encountered in + * the CMakeLists.txt file. + */ + virtual bool InitialPass(std::vector<std::string> const& args, + cmExecutionStatus &status); + + /** + * The name of the command as specified in CMakeList.txt. + */ + virtual const char* GetName() const { return "ctest_read_custom_files";} + + /** + * Succinct documentation. + */ + virtual const char* GetTerseDocumentation() const + { + return "read CTestCustom files."; + } + + /** + * More documentation. + */ + virtual const char* GetFullDocumentation() const + { + return + " ctest_read_custom_files( directory ... )\n" + "Read all the CTestCustom.ctest or CTestCustom.cmake files from " + "the given directory."; + } + + cmTypeMacro(cmCTestReadCustomFilesCommand, cmCTestCommand); + +}; + + +#endif diff --git a/Source/CTest/cmCTestRunScriptCommand.cxx b/Source/CTest/cmCTestRunScriptCommand.cxx new file mode 100644 index 000000000..fe429bd9d --- /dev/null +++ b/Source/CTest/cmCTestRunScriptCommand.cxx @@ -0,0 +1,65 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2009 Kitware, Inc., Insight Software Consortium + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ +#include "cmCTestRunScriptCommand.h" + +#include "cmCTestScriptHandler.h" + +bool cmCTestRunScriptCommand +::InitialPass(std::vector<std::string> const& args, cmExecutionStatus &) +{ + if(args.size() < 1 ) + { + this->CTestScriptHandler->RunCurrentScript(); + return true; + } + + bool np = false; + unsigned int i = 0; + if (args[i] == "NEW_PROCESS") + { + np = true; + i++; + } + int start = i; + // run each script + std::string returnVariable; + for (i = start; i < args.size(); ++i) + { + if(args[i] == "RETURN_VALUE") + { + ++i; + if(i < args.size()) + { + returnVariable = args[i]; + } + } + } + for (i = start; i < args.size(); ++i) + { + if(args[i] == "RETURN_VALUE") + { + ++i; + } + else + { + int ret; + cmCTestScriptHandler::RunScript(this->CTest, args[i].c_str(), !np, + &ret); + cmOStringStream str; + str << ret; + this->Makefile->AddDefinition(returnVariable.c_str(), str.str().c_str()); + } + } + return true; +} + + diff --git a/Source/CTest/cmCTestRunScriptCommand.h b/Source/CTest/cmCTestRunScriptCommand.h new file mode 100644 index 000000000..6df69afa6 --- /dev/null +++ b/Source/CTest/cmCTestRunScriptCommand.h @@ -0,0 +1,80 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2009 Kitware, Inc., Insight Software Consortium + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ +#ifndef cmCTestRunScriptCommand_h +#define cmCTestRunScriptCommand_h + +#include "cmCTestCommand.h" + +/** \class cmCTestRunScript + * \brief Run a ctest script + * + * cmLibrarysCommand defines a list of executable (i.e., test) + * programs to create. + */ +class cmCTestRunScriptCommand : public cmCTestCommand +{ +public: + + cmCTestRunScriptCommand() {} + + /** + * This is a virtual constructor for the command. + */ + virtual cmCommand* Clone() + { + cmCTestRunScriptCommand* ni = new cmCTestRunScriptCommand; + ni->CTest = this->CTest; + ni->CTestScriptHandler = this->CTestScriptHandler; + return ni; + } + + /** + * This is called when the command is first encountered in + * the CMakeLists.txt file. + */ + virtual bool InitialPass(std::vector<std::string> const& args, + cmExecutionStatus &status); + + /** + * The name of the command as specified in CMakeList.txt. + */ + virtual const char* GetName() const { return "ctest_run_script";} + + /** + * Succinct documentation. + */ + virtual const char* GetTerseDocumentation() const + { + return "runs a ctest -S script"; + } + + /** + * More documentation. + */ + virtual const char* GetFullDocumentation() const + { + return + " ctest_run_script([NEW_PROCESS] script_file_name script_file_name1 \n" + " script_file_name2 ... [RETURN_VALUE var])\n" + "Runs a script or scripts much like if it was run from ctest -S. " + "If no argument is provided then the current script is run using " + "the current settings of the variables. If NEW_PROCESS is specified " + "then each script will be run in a separate process." + "If RETURN_VALUE is specified the return value of the last script " + "run will be put into var."; + } + + cmTypeMacro(cmCTestRunScriptCommand, cmCTestCommand); +}; + + +#endif diff --git a/Source/CTest/cmCTestRunTest.cxx b/Source/CTest/cmCTestRunTest.cxx new file mode 100644 index 000000000..c3de5dcf6 --- /dev/null +++ b/Source/CTest/cmCTestRunTest.cxx @@ -0,0 +1,735 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2009 Kitware, Inc., Insight Software Consortium + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ + +#include "cmCTestRunTest.h" +#include "cmCTestMemCheckHandler.h" +#include "cmCTest.h" +#include "cmSystemTools.h" +#include "cm_curl.h" + +#include <cm_zlib.h> +#include <cmsys/Base64.h> + +cmCTestRunTest::cmCTestRunTest(cmCTestTestHandler* handler) +{ + this->CTest = handler->CTest; + this->TestHandler = handler; + this->TestProcess = 0; + this->TestResult.ExecutionTime =0; + this->TestResult.ReturnValue = 0; + this->TestResult.Status = cmCTestTestHandler::NOT_RUN; + this->TestResult.TestCount = 0; + this->TestResult.Properties = 0; + this->ProcessOutput = ""; + this->CompressedOutput = ""; + this->CompressionRatio = 2; + this->StopTimePassed = false; +} + +cmCTestRunTest::~cmCTestRunTest() +{ +} + +//---------------------------------------------------------------------------- +bool cmCTestRunTest::CheckOutput() +{ + // Read lines for up to 0.1 seconds of total time. + double timeout = 0.1; + double timeEnd = cmSystemTools::GetTime() + timeout; + std::string line; + while((timeout = timeEnd - cmSystemTools::GetTime(), timeout > 0)) + { + int p = this->TestProcess->GetNextOutputLine(line, timeout); + if(p == cmsysProcess_Pipe_None) + { + // Process has terminated and all output read. + return false; + } + else if(p == cmsysProcess_Pipe_STDOUT || + p == cmsysProcess_Pipe_STDERR) + { + // Store this line of output. + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, + this->GetIndex() << ": " << line << std::endl); + this->ProcessOutput += line; + this->ProcessOutput += "\n"; + } + else // if(p == cmsysProcess_Pipe_Timeout) + { + break; + } + } + return true; +} + +//--------------------------------------------------------- +// Streamed compression of test output. The compressed data +// is appended to this->CompressedOutput +void cmCTestRunTest::CompressOutput() +{ + int ret; + z_stream strm; + + unsigned char* in = + reinterpret_cast<unsigned char*>( + const_cast<char*>(this->ProcessOutput.c_str())); + //zlib makes the guarantee that this is the maximum output size + int outSize = static_cast<int>( + static_cast<double>(this->ProcessOutput.size()) * 1.001 + 13.0); + unsigned char* out = new unsigned char[outSize]; + + strm.zalloc = Z_NULL; + strm.zfree = Z_NULL; + strm.opaque = Z_NULL; + ret = deflateInit(&strm, -1); //default compression level + if (ret != Z_OK) + { + delete[] out; + return; + } + + strm.avail_in = static_cast<uInt>(this->ProcessOutput.size()); + strm.next_in = in; + strm.avail_out = outSize; + strm.next_out = out; + ret = deflate(&strm, Z_FINISH); + + if(ret == Z_STREAM_ERROR || ret != Z_STREAM_END) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, "Error during output " + "compression. Sending uncompressed output." << std::endl); + delete[] out; + return; + } + + (void)deflateEnd(&strm); + + unsigned char *encoded_buffer + = new unsigned char[static_cast<int>(outSize * 1.5)]; + + unsigned long rlen + = cmsysBase64_Encode(out, strm.total_out, encoded_buffer, 1); + + for(unsigned long i = 0; i < rlen; i++) + { + this->CompressedOutput += encoded_buffer[i]; + } + + if(strm.total_in) + { + this->CompressionRatio = static_cast<double>(strm.total_out) / + static_cast<double>(strm.total_in); + } + + delete [] encoded_buffer; + delete [] out; +} + +//--------------------------------------------------------- +bool cmCTestRunTest::EndTest(size_t completed, size_t total, bool started) +{ + if ((!this->TestHandler->MemCheck && + this->CTest->ShouldCompressTestOutput()) || + (this->TestHandler->MemCheck && + this->CTest->ShouldCompressMemCheckOutput())) + { + this->CompressOutput(); + } + + this->WriteLogOutputTop(completed, total); + std::string reason; + bool passed = true; + int res = started ? this->TestProcess->GetProcessStatus() + : cmsysProcess_State_Error; + int retVal = this->TestProcess->GetExitValue(); + std::vector<std::pair<cmsys::RegularExpression, + std::string> >::iterator passIt; + bool forceFail = false; + bool outputTestErrorsToConsole = false; + if ( this->TestProperties->RequiredRegularExpressions.size() > 0 ) + { + bool found = false; + for ( passIt = this->TestProperties->RequiredRegularExpressions.begin(); + passIt != this->TestProperties->RequiredRegularExpressions.end(); + ++ passIt ) + { + if ( passIt->first.find(this->ProcessOutput.c_str()) ) + { + found = true; + reason = "Required regular expression found."; + } + } + if ( !found ) + { + reason = "Required regular expression not found."; + forceFail = true; + } + reason += "Regex=["; + for ( passIt = this->TestProperties->RequiredRegularExpressions.begin(); + passIt != this->TestProperties->RequiredRegularExpressions.end(); + ++ passIt ) + { + reason += passIt->second; + reason += "\n"; + } + reason += "]"; + } + if ( this->TestProperties->ErrorRegularExpressions.size() > 0 ) + { + for ( passIt = this->TestProperties->ErrorRegularExpressions.begin(); + passIt != this->TestProperties->ErrorRegularExpressions.end(); + ++ passIt ) + { + if ( passIt->first.find(this->ProcessOutput.c_str()) ) + { + reason = "Error regular expression found in output."; + reason += " Regex=["; + reason += passIt->second; + reason += "]"; + forceFail = true; + } + } + } + if (res == cmsysProcess_State_Exited) + { + bool success = + !forceFail && (retVal == 0 || + this->TestProperties->RequiredRegularExpressions.size()); + if((success && !this->TestProperties->WillFail) + || (!success && this->TestProperties->WillFail)) + { + this->TestResult.Status = cmCTestTestHandler::COMPLETED; + cmCTestLog(this->CTest, HANDLER_OUTPUT, " Passed " ); + } + else + { + this->TestResult.Status = cmCTestTestHandler::FAILED; + cmCTestLog(this->CTest, HANDLER_OUTPUT, "***Failed " << reason ); + outputTestErrorsToConsole = this->CTest->OutputTestOutputOnTestFailure; + } + } + else if ( res == cmsysProcess_State_Expired ) + { + cmCTestLog(this->CTest, HANDLER_OUTPUT, "***Timeout "); + this->TestResult.Status = cmCTestTestHandler::TIMEOUT; + outputTestErrorsToConsole = this->CTest->OutputTestOutputOnTestFailure; + } + else if ( res == cmsysProcess_State_Exception ) + { + outputTestErrorsToConsole = this->CTest->OutputTestOutputOnTestFailure; + cmCTestLog(this->CTest, HANDLER_OUTPUT, "***Exception: "); + switch(this->TestProcess->GetExitException()) + { + case cmsysProcess_Exception_Fault: + cmCTestLog(this->CTest, HANDLER_OUTPUT, "SegFault"); + this->TestResult.Status = cmCTestTestHandler::SEGFAULT; + break; + case cmsysProcess_Exception_Illegal: + cmCTestLog(this->CTest, HANDLER_OUTPUT, "Illegal"); + this->TestResult.Status = cmCTestTestHandler::ILLEGAL; + break; + case cmsysProcess_Exception_Interrupt: + cmCTestLog(this->CTest, HANDLER_OUTPUT, "Interrupt"); + this->TestResult.Status = cmCTestTestHandler::INTERRUPT; + break; + case cmsysProcess_Exception_Numerical: + cmCTestLog(this->CTest, HANDLER_OUTPUT, "Numerical"); + this->TestResult.Status = cmCTestTestHandler::NUMERICAL; + break; + default: + cmCTestLog(this->CTest, HANDLER_OUTPUT, "Other"); + this->TestResult.Status = cmCTestTestHandler::OTHER_FAULT; + } + } + else //cmsysProcess_State_Error + { + cmCTestLog(this->CTest, HANDLER_OUTPUT, "***Not Run "); + } + + passed = this->TestResult.Status == cmCTestTestHandler::COMPLETED; + char buf[1024]; + sprintf(buf, "%6.2f sec", this->TestProcess->GetTotalTime()); + cmCTestLog(this->CTest, HANDLER_OUTPUT, buf << "\n" ); + + if ( outputTestErrorsToConsole ) + { + cmCTestLog(this->CTest, HANDLER_OUTPUT, this->ProcessOutput << std::endl ); + } + + if ( this->TestHandler->LogFile ) + { + *this->TestHandler->LogFile << "Test time = " << buf << std::endl; + } + + // Set the working directory to the tests directory + std::string oldpath = cmSystemTools::GetCurrentWorkingDirectory(); + cmSystemTools::ChangeDirectory(this->TestProperties->Directory.c_str()); + + this->DartProcessing(); + + // restore working directory + cmSystemTools::ChangeDirectory(oldpath.c_str()); + + + // if this is doing MemCheck then all the output needs to be put into + // Output since that is what is parsed by cmCTestMemCheckHandler + if(!this->TestHandler->MemCheck && started) + { + this->TestHandler->CleanTestOutput(this->ProcessOutput, + static_cast<size_t> + (this->TestResult.Status == cmCTestTestHandler::COMPLETED ? + this->TestHandler->CustomMaximumPassedTestOutputSize : + this->TestHandler->CustomMaximumFailedTestOutputSize)); + } + this->TestResult.Reason = reason; + if (this->TestHandler->LogFile) + { + bool pass = true; + const char* reasonType = "Test Pass Reason"; + if(this->TestResult.Status != cmCTestTestHandler::COMPLETED && + this->TestResult.Status != cmCTestTestHandler::NOT_RUN) + { + reasonType = "Test Fail Reason"; + pass = false; + } + double ttime = this->TestProcess->GetTotalTime(); + int hours = static_cast<int>(ttime / (60 * 60)); + int minutes = static_cast<int>(ttime / 60) % 60; + int seconds = static_cast<int>(ttime) % 60; + char buffer[100]; + sprintf(buffer, "%02d:%02d:%02d", hours, minutes, seconds); + *this->TestHandler->LogFile + << "----------------------------------------------------------" + << std::endl; + if(this->TestResult.Reason.size()) + { + *this->TestHandler->LogFile << reasonType << ":\n" + << this->TestResult.Reason << "\n"; + } + else + { + if(pass) + { + *this->TestHandler->LogFile << "Test Passed.\n"; + } + else + { + *this->TestHandler->LogFile << "Test Failed.\n"; + } + } + *this->TestHandler->LogFile << "\"" << this->TestProperties->Name.c_str() + << "\" end time: " << this->CTest->CurrentTime() << std::endl + << "\"" << this->TestProperties->Name.c_str() << "\" time elapsed: " + << buffer << std::endl + << "----------------------------------------------------------" + << std::endl << std::endl; + } + // if the test actually started and ran + // record the results in TestResult + if(started) + { + bool compress = !this->TestHandler->MemCheck && + this->CompressionRatio < 1 && + this->CTest->ShouldCompressTestOutput(); + this->TestResult.Output = compress ? this->CompressedOutput + : this->ProcessOutput; + this->TestResult.CompressOutput = compress; + this->TestResult.ReturnValue = this->TestProcess->GetExitValue(); + this->TestResult.CompletionStatus = "Completed"; + this->TestResult.ExecutionTime = this->TestProcess->GetTotalTime(); + this->MemCheckPostProcess(); + this->ComputeWeightedCost(); + } + // Always push the current TestResult onto the + // TestHandler vector + this->TestHandler->TestResults.push_back(this->TestResult); + delete this->TestProcess; + return passed; +} + +//---------------------------------------------------------------------- +void cmCTestRunTest::ComputeWeightedCost() +{ + double prev = static_cast<double>(this->TestProperties->PreviousRuns); + double avgcost = static_cast<double>(this->TestProperties->Cost); + double current = this->TestResult.ExecutionTime; + + if(this->TestResult.Status == cmCTestTestHandler::COMPLETED) + { + this->TestProperties->Cost = + static_cast<float>(((prev * avgcost) + current) / (prev + 1.0)); + this->TestProperties->PreviousRuns++; + } +} + +//---------------------------------------------------------------------- +void cmCTestRunTest::MemCheckPostProcess() +{ + if(!this->TestHandler->MemCheck) + { + return; + } + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, this->Index + << ": process test output now: " + << this->TestProperties->Name.c_str() << " " + << this->TestResult.Name.c_str() << std::endl); + cmCTestMemCheckHandler * handler = static_cast<cmCTestMemCheckHandler*> + (this->TestHandler); + if(handler->MemoryTesterStyle == cmCTestMemCheckHandler::BOUNDS_CHECKER) + { + handler->PostProcessBoundsCheckerTest(this->TestResult); + } + else if(handler->MemoryTesterStyle == cmCTestMemCheckHandler::PURIFY) + { + handler->PostProcessPurifyTest(this->TestResult); + } +} + +//---------------------------------------------------------------------- +// Starts the execution of a test. Returns once it has started +bool cmCTestRunTest::StartTest(size_t total) +{ + cmCTestLog(this->CTest, HANDLER_OUTPUT, std::setw(2*getNumWidth(total) + 8) + << "Start " + << std::setw(getNumWidth(this->TestHandler->GetMaxIndex())) + << this->TestProperties->Index << ": " + << this->TestProperties->Name << std::endl); + this->ComputeArguments(); + std::vector<std::string>& args = this->TestProperties->Args; + this->TestResult.Properties = this->TestProperties; + this->TestResult.ExecutionTime = 0; + this->TestResult.CompressOutput = false; + this->TestResult.ReturnValue = -1; + this->TestResult.CompletionStatus = "Failed to start"; + this->TestResult.Status = cmCTestTestHandler::BAD_COMMAND; + this->TestResult.TestCount = this->TestProperties->Index; + this->TestResult.Name = this->TestProperties->Name; + this->TestResult.Path = this->TestProperties->Directory.c_str(); + + if(args.size() >= 2 && args[1] == "NOT_AVAILABLE") + { + this->TestProcess = new cmProcess; + std::string msg; + if(this->CTest->GetConfigType().empty()) + { + msg = "Test not available without configuration."; + msg += " (Missing \"-C <config>\"?)"; + } + else + { + msg = "Test not available in configuration \""; + msg += this->CTest->GetConfigType(); + msg += "\"."; + } + *this->TestHandler->LogFile << msg << std::endl; + cmCTestLog(this->CTest, ERROR_MESSAGE, msg << std::endl); + this->TestResult.Output = msg; + this->TestResult.FullCommandLine = ""; + this->TestResult.CompletionStatus = "Not Run"; + this->TestResult.Status = cmCTestTestHandler::NOT_RUN; + return false; + } + + // Check if all required files exist + for(std::vector<std::string>::iterator i = + this->TestProperties->RequiredFiles.begin(); + i != this->TestProperties->RequiredFiles.end(); ++i) + { + std::string file = *i; + + if(!cmSystemTools::FileExists(file.c_str())) + { + //Required file was not found + this->TestProcess = new cmProcess; + *this->TestHandler->LogFile << "Unable to find required file: " + << file.c_str() << std::endl; + cmCTestLog(this->CTest, ERROR_MESSAGE, "Unable to find required file: " + << file.c_str() << std::endl); + this->TestResult.Output = "Unable to find required file: " + file; + this->TestResult.FullCommandLine = ""; + this->TestResult.CompletionStatus = "Not Run"; + this->TestResult.Status = cmCTestTestHandler::NOT_RUN; + return false; + } + } + // log and return if we did not find the executable + if (this->ActualCommand == "") + { + // if the command was not found create a TestResult object + // that has that information + this->TestProcess = new cmProcess; + *this->TestHandler->LogFile << "Unable to find executable: " + << args[1].c_str() << std::endl; + cmCTestLog(this->CTest, ERROR_MESSAGE, "Unable to find executable: " + << args[1].c_str() << std::endl); + this->TestResult.Output = "Unable to find executable: " + args[1]; + this->TestResult.FullCommandLine = ""; + this->TestResult.CompletionStatus = "Not Run"; + this->TestResult.Status = cmCTestTestHandler::NOT_RUN; + return false; + } + this->StartTime = this->CTest->CurrentTime(); + + double timeout = this->ResolveTimeout(); + + if(this->StopTimePassed) + { + return false; + } + return this->ForkProcess(timeout, this->TestProperties->ExplicitTimeout, + &this->TestProperties->Environment); +} + +//---------------------------------------------------------------------- +void cmCTestRunTest::ComputeArguments() +{ + std::vector<std::string>::const_iterator j = + this->TestProperties->Args.begin(); + ++j; // skip test name + + // find the test executable + if(this->TestHandler->MemCheck) + { + cmCTestMemCheckHandler * handler = static_cast<cmCTestMemCheckHandler*> + (this->TestHandler); + this->ActualCommand = handler->MemoryTester.c_str(); + this->TestProperties->Args[1] = this->TestHandler->FindTheExecutable( + this->TestProperties->Args[1].c_str()); + } + else + { + this->ActualCommand = + this->TestHandler->FindTheExecutable( + this->TestProperties->Args[1].c_str()); + ++j; //skip the executable (it will be actualCommand) + } + std::string testCommand + = cmSystemTools::ConvertToOutputPath(this->ActualCommand.c_str()); + + //Prepends memcheck args to our command string + this->TestHandler->GenerateTestCommand(this->Arguments); + for(std::vector<std::string>::iterator i = this->Arguments.begin(); + i != this->Arguments.end(); ++i) + { + testCommand += " \""; + testCommand += *i; + testCommand += "\""; + } + + for(;j != this->TestProperties->Args.end(); ++j) + { + testCommand += " \""; + testCommand += *j; + testCommand += "\""; + this->Arguments.push_back(*j); + } + this->TestResult.FullCommandLine = testCommand; + + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, std::endl + << this->Index << ": " + << (this->TestHandler->MemCheck?"MemCheck":"Test") + << " command: " << testCommand + << std::endl); +} + +//---------------------------------------------------------------------- +void cmCTestRunTest::DartProcessing() +{ + if (!this->ProcessOutput.empty() && + this->ProcessOutput.find("<DartMeasurement") != this->ProcessOutput.npos) + { + if (this->TestHandler->DartStuff.find(this->ProcessOutput.c_str())) + { + std::string dartString = this->TestHandler->DartStuff.match(1); + // keep searching and replacing until none are left + while (this->TestHandler->DartStuff1.find(this->ProcessOutput.c_str())) + { + // replace the exact match for the string + cmSystemTools::ReplaceString(this->ProcessOutput, + this->TestHandler->DartStuff1.match(1).c_str(), ""); + } + this->TestResult.RegressionImages + = this->TestHandler->GenerateRegressionImages(dartString); + } + } +} + +//---------------------------------------------------------------------- +double cmCTestRunTest::ResolveTimeout() +{ + double timeout = this->TestProperties->Timeout; + + if(this->CTest->GetStopTime() == "") + { + return timeout; + } + struct tm* lctime; + time_t current_time = time(0); + lctime = gmtime(¤t_time); + int gm_hour = lctime->tm_hour; + time_t gm_time = mktime(lctime); + lctime = localtime(¤t_time); + int local_hour = lctime->tm_hour; + + int tzone_offset = local_hour - gm_hour; + if(gm_time > current_time && gm_hour < local_hour) + { + // this means gm_time is on the next day + tzone_offset -= 24; + } + else if(gm_time < current_time && gm_hour > local_hour) + { + // this means gm_time is on the previous day + tzone_offset += 24; + } + + tzone_offset *= 100; + char buf[1024]; + // add todays year day and month to the time in str because + // curl_getdate no longer assumes the day is today + sprintf(buf, "%d%02d%02d %s %+05i", + lctime->tm_year + 1900, + lctime->tm_mon + 1, + lctime->tm_mday, + this->CTest->GetStopTime().c_str(), + tzone_offset); + + time_t stop_time = curl_getdate(buf, ¤t_time); + if(stop_time == -1) + { + return timeout; + } + + //the stop time refers to the next day + if(this->CTest->NextDayStopTime) + { + stop_time += 24*60*60; + } + int stop_timeout = static_cast<int>(stop_time - current_time) % (24*60*60); + this->CTest->LastStopTimeout = stop_timeout; + + if(stop_timeout <= 0 || stop_timeout > this->CTest->LastStopTimeout) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, "The stop time has been passed. " + "Stopping all tests." << std::endl); + this->StopTimePassed = true; + return 0; + } + return timeout == 0 ? stop_timeout : + (timeout < stop_timeout ? timeout : stop_timeout); +} + +//---------------------------------------------------------------------- +bool cmCTestRunTest::ForkProcess(double testTimeOut, bool explicitTimeout, + std::vector<std::string>* environment) +{ + this->TestProcess = new cmProcess; + this->TestProcess->SetId(this->Index); + this->TestProcess->SetWorkingDirectory( + this->TestProperties->Directory.c_str()); + this->TestProcess->SetCommand(this->ActualCommand.c_str()); + this->TestProcess->SetCommandArguments(this->Arguments); + + // determine how much time we have + double timeout = this->CTest->GetRemainingTimeAllowed() - 120; + if (this->CTest->GetTimeOut() > 0 && this->CTest->GetTimeOut() < timeout) + { + timeout = this->CTest->GetTimeOut(); + } + if (testTimeOut > 0 + && testTimeOut < this->CTest->GetRemainingTimeAllowed()) + { + timeout = testTimeOut; + } + // always have at least 1 second if we got to here + if (timeout <= 0) + { + timeout = 1; + } + // handle timeout explicitly set to 0 + if (testTimeOut == 0 && explicitTimeout) + { + timeout = 0; + } + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, this->Index << ": " + << "Test timeout computed to be: " << timeout << "\n"); + + this->TestProcess->SetTimeout(timeout); + +#ifdef CMAKE_BUILD_WITH_CMAKE + cmSystemTools::SaveRestoreEnvironment sre; +#endif + + if (environment && environment->size()>0) + { + cmSystemTools::AppendEnv(*environment); + } + + return this->TestProcess->StartProcess(); +} + +void cmCTestRunTest::WriteLogOutputTop(size_t completed, size_t total) +{ + cmCTestLog(this->CTest, HANDLER_OUTPUT, std::setw(getNumWidth(total)) + << completed << "/"); + cmCTestLog(this->CTest, HANDLER_OUTPUT, std::setw(getNumWidth(total)) + << total << " "); + + if ( this->TestHandler->MemCheck ) + { + cmCTestLog(this->CTest, HANDLER_OUTPUT, "MemCheck"); + } + else + { + cmCTestLog(this->CTest, HANDLER_OUTPUT, "Test"); + } + + cmOStringStream indexStr; + indexStr << " #" << this->Index << ":"; + cmCTestLog(this->CTest, HANDLER_OUTPUT, + std::setw(3 + getNumWidth(this->TestHandler->GetMaxIndex())) + << indexStr.str().c_str()); + cmCTestLog(this->CTest, HANDLER_OUTPUT, " "); + const int maxTestNameWidth = this->CTest->GetMaxTestNameWidth(); + std::string outname = this->TestProperties->Name + " "; + outname.resize(maxTestNameWidth + 4, '.'); + + *this->TestHandler->LogFile << this->TestProperties->Index << "/" + << this->TestHandler->TotalNumberOfTests << " Testing: " + << this->TestProperties->Name << std::endl; + *this->TestHandler->LogFile << this->TestProperties->Index << "/" + << this->TestHandler->TotalNumberOfTests + << " Test: " << this->TestProperties->Name.c_str() << std::endl; + *this->TestHandler->LogFile << "Command: \"" << this->ActualCommand << "\""; + + for (std::vector<std::string>::iterator i = this->Arguments.begin(); + i != this->Arguments.end(); ++i) + { + *this->TestHandler->LogFile + << " \"" << i->c_str() << "\""; + } + *this->TestHandler->LogFile << std::endl + << "Directory: " << this->TestProperties->Directory << std::endl + << "\"" << this->TestProperties->Name.c_str() << "\" start time: " + << this->StartTime << std::endl; + + *this->TestHandler->LogFile + << "Output:" << std::endl + << "----------------------------------------------------------" + << std::endl; + *this->TestHandler->LogFile + << this->ProcessOutput.c_str() << "<end of output>" << std::endl; + + cmCTestLog(this->CTest, HANDLER_OUTPUT, outname.c_str()); + cmCTestLog(this->CTest, DEBUG, "Testing " + << this->TestProperties->Name.c_str() << " ... "); +} diff --git a/Source/CTest/cmCTestRunTest.h b/Source/CTest/cmCTestRunTest.h new file mode 100644 index 000000000..89456d505 --- /dev/null +++ b/Source/CTest/cmCTestRunTest.h @@ -0,0 +1,112 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2009 Kitware, Inc., Insight Software Consortium + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ +#ifndef cmCTestRunTest_h +#define cmCTestRunTest_h + +#include <cmStandardIncludes.h> +#include <cmCTestTestHandler.h> +#include <cmProcess.h> + +/** \class cmRunTest + * \brief represents a single test to be run + * + * cmRunTest contains the information related to running a single test + */ +class cmCTestRunTest +{ +public: + cmCTestRunTest(cmCTestTestHandler* handler); + ~cmCTestRunTest(); + + void SetTestProperties(cmCTestTestHandler::cmCTestTestProperties * prop) + { this->TestProperties = prop; } + + cmCTestTestHandler::cmCTestTestProperties * GetTestProperties() + { return this->TestProperties; } + + void SetIndex(int i) { this->Index = i; } + + int GetIndex() { return this->Index; } + + std::string GetProcessOutput() { return this->ProcessOutput; } + + bool IsStopTimePassed() { return this->StopTimePassed; } + + cmCTestTestHandler::cmCTestTestResult GetTestResults() + { return this->TestResult; } + + // Read and store output. Returns true if it must be called again. + bool CheckOutput(); + + // Compresses the output, writing to CompressedOutput + void CompressOutput(); + + //launch the test process, return whether it started correctly + bool StartTest(size_t total); + //capture and report the test results + bool EndTest(size_t completed, size_t total, bool started); + //Called by ctest -N to log the command string + void ComputeArguments(); + + void ComputeWeightedCost(); +private: + void DartProcessing(); + void ExeNotFound(std::string exe); + // Figures out a final timeout which is min(STOP_TIME, NOW+TIMEOUT) + double ResolveTimeout(); + bool ForkProcess(double testTimeOut, bool explicitTimeout, + std::vector<std::string>* environment); + void WriteLogOutputTop(size_t completed, size_t total); + //Run post processing of the process output for MemCheck + void MemCheckPostProcess(); + + cmCTestTestHandler::cmCTestTestProperties * TestProperties; + //Pointer back to the "parent"; the handler that invoked this test run + cmCTestTestHandler * TestHandler; + cmCTest * CTest; + cmProcess * TestProcess; + //If the executable to run is ctest, don't create a new process; + //just instantiate a new cmTest. (Can be disabled for a single test + //if this option is set to false.) + //bool OptimizeForCTest; + + bool UsePrefixCommand; + std::string PrefixCommand; + + std::string ProcessOutput; + std::string CompressedOutput; + double CompressionRatio; + //The test results + cmCTestTestHandler::cmCTestTestResult TestResult; + int Index; + std::string StartTime; + std::string ActualCommand; + std::vector<std::string> Arguments; + bool StopTimePassed; +}; + +inline int getNumWidth(size_t n) +{ + int numWidth = 1; + if(n >= 10) + { + numWidth = 2; + } + if(n >= 100) + { + numWidth = 3; + } + return numWidth; +} + +#endif + diff --git a/Source/CTest/cmCTestSVN.cxx b/Source/CTest/cmCTestSVN.cxx new file mode 100644 index 000000000..fab9a8cba --- /dev/null +++ b/Source/CTest/cmCTestSVN.cxx @@ -0,0 +1,450 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2009 Kitware, Inc. + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ +#include "cmCTestSVN.h" + +#include "cmCTest.h" +#include "cmSystemTools.h" +#include "cmXMLParser.h" +#include "cmXMLSafe.h" + +#include <cmsys/RegularExpression.hxx> + +//---------------------------------------------------------------------------- +cmCTestSVN::cmCTestSVN(cmCTest* ct, std::ostream& log): + cmCTestGlobalVC(ct, log) +{ + this->PriorRev = this->Unknown; +} + +//---------------------------------------------------------------------------- +cmCTestSVN::~cmCTestSVN() +{ +} + +//---------------------------------------------------------------------------- +void cmCTestSVN::CleanupImpl() +{ + const char* svn = this->CommandLineTool.c_str(); + const char* svn_cleanup[] = {svn, "cleanup", 0}; + OutputLogger out(this->Log, "cleanup-out> "); + OutputLogger err(this->Log, "cleanup-err> "); + this->RunChild(svn_cleanup, &out, &err); +} + +//---------------------------------------------------------------------------- +class cmCTestSVN::InfoParser: public cmCTestVC::LineParser +{ +public: + InfoParser(cmCTestSVN* svn, const char* prefix, std::string& rev): + SVN(svn), Rev(rev) + { + this->SetLog(&svn->Log, prefix); + this->RegexRev.compile("^Revision: ([0-9]+)"); + this->RegexURL.compile("^URL: +([^ ]+) *$"); + this->RegexRoot.compile("^Repository Root: +([^ ]+) *$"); + } +private: + cmCTestSVN* SVN; + std::string& Rev; + cmsys::RegularExpression RegexRev; + cmsys::RegularExpression RegexURL; + cmsys::RegularExpression RegexRoot; + virtual bool ProcessLine() + { + if(this->RegexRev.find(this->Line)) + { + this->Rev = this->RegexRev.match(1); + } + else if(this->RegexURL.find(this->Line)) + { + this->SVN->URL = this->RegexURL.match(1); + } + else if(this->RegexRoot.find(this->Line)) + { + this->SVN->Root = this->RegexRoot.match(1); + } + return true; + } +}; + +//---------------------------------------------------------------------------- +static bool cmCTestSVNPathStarts(std::string const& p1, std::string const& p2) +{ + // Does path p1 start with path p2? + if(p1.size() == p2.size()) + { + return p1 == p2; + } + else if(p1.size() > p2.size() && p1[p2.size()] == '/') + { + return strncmp(p1.c_str(), p2.c_str(), p2.size()) == 0; + } + else + { + return false; + } +} + +//---------------------------------------------------------------------------- +std::string cmCTestSVN::LoadInfo() +{ + // Run "svn info" to get the repository info from the work tree. + const char* svn = this->CommandLineTool.c_str(); + const char* svn_info[] = {svn, "info", 0}; + std::string rev; + InfoParser out(this, "info-out> ", rev); + OutputLogger err(this->Log, "info-err> "); + this->RunChild(svn_info, &out, &err); + return rev; +} + +//---------------------------------------------------------------------------- +void cmCTestSVN::NoteOldRevision() +{ + this->OldRevision = this->LoadInfo(); + this->Log << "Revision before update: " << this->OldRevision << "\n"; + cmCTestLog(this->CTest, HANDLER_OUTPUT, " Old revision of repository is: " + << this->OldRevision << "\n"); + this->PriorRev.Rev = this->OldRevision; +} + +//---------------------------------------------------------------------------- +void cmCTestSVN::NoteNewRevision() +{ + this->NewRevision = this->LoadInfo(); + this->Log << "Revision after update: " << this->NewRevision << "\n"; + cmCTestLog(this->CTest, HANDLER_OUTPUT, " New revision of repository is: " + << this->NewRevision << "\n"); + + // this->Root = ""; // uncomment to test GuessBase + this->Log << "URL = " << this->URL << "\n"; + this->Log << "Root = " << this->Root << "\n"; + + // Compute the base path the working tree has checked out under + // the repository root. + if(!this->Root.empty() && cmCTestSVNPathStarts(this->URL, this->Root)) + { + this->Base = cmCTest::DecodeURL(this->URL.substr(this->Root.size())); + this->Base += "/"; + } + this->Log << "Base = " << this->Base << "\n"; +} + +//---------------------------------------------------------------------------- +void cmCTestSVN::GuessBase(std::vector<Change> const& changes) +{ + // Subversion did not give us a good repository root so we need to + // guess the base path from the URL and the paths in a revision with + // changes under it. + + // Consider each possible URL suffix from longest to shortest. + for(std::string::size_type slash = this->URL.find('/'); + this->Base.empty() && slash != std::string::npos; + slash = this->URL.find('/', slash+1)) + { + // If the URL suffix is a prefix of at least one path then it is the base. + std::string base = cmCTest::DecodeURL(this->URL.substr(slash)); + for(std::vector<Change>::const_iterator ci = changes.begin(); + this->Base.empty() && ci != changes.end(); ++ci) + { + if(cmCTestSVNPathStarts(ci->Path, base)) + { + this->Base = base; + } + } + } + + // We always append a slash so that we know paths beginning in the + // base lie under its path. If no base was found then the working + // tree must be a checkout of the entire repo and this will match + // the leading slash in all paths. + this->Base += "/"; + + this->Log << "Guessed Base = " << this->Base << "\n"; +} + +//---------------------------------------------------------------------------- +const char* cmCTestSVN::LocalPath(std::string const& path) +{ + if(path.size() > this->Base.size() && + strncmp(path.c_str(), this->Base.c_str(), this->Base.size()) == 0) + { + // This path lies under the base, so return a relative path. + return path.c_str() + this->Base.size(); + } + else + { + // This path does not lie under the base, so ignore it. + return 0; + } +} + +//---------------------------------------------------------------------------- +class cmCTestSVN::UpdateParser: public cmCTestVC::LineParser +{ +public: + UpdateParser(cmCTestSVN* svn, const char* prefix): SVN(svn) + { + this->SetLog(&svn->Log, prefix); + this->RegexUpdate.compile("^([ADUCGE ])([ADUCGE ])[B ] +(.+)$"); + } +private: + cmCTestSVN* SVN; + cmsys::RegularExpression RegexUpdate; + + bool ProcessLine() + { + if(this->RegexUpdate.find(this->Line)) + { + this->DoPath(this->RegexUpdate.match(1)[0], + this->RegexUpdate.match(2)[0], + this->RegexUpdate.match(3)); + } + return true; + } + + void DoPath(char path_status, char prop_status, std::string const& path) + { + char status = (path_status != ' ')? path_status : prop_status; + std::string dir = cmSystemTools::GetFilenamePath(path); + std::string name = cmSystemTools::GetFilenameName(path); + // See "svn help update". + switch(status) + { + case 'G': + this->SVN->Dirs[dir][name].Status = PathModified; + break; + case 'C': + this->SVN->Dirs[dir][name].Status = PathConflicting; + break; + case 'A': case 'D': case 'U': + this->SVN->Dirs[dir][name].Status = PathUpdated; + break; + case 'E': // TODO? + case '?': case ' ': default: + break; + } + } +}; + +//---------------------------------------------------------------------------- +bool cmCTestSVN::UpdateImpl() +{ + // Get user-specified update options. + std::string opts = this->CTest->GetCTestConfiguration("UpdateOptions"); + if(opts.empty()) + { + opts = this->CTest->GetCTestConfiguration("SVNUpdateOptions"); + } + std::vector<cmStdString> args = cmSystemTools::ParseArguments(opts.c_str()); + + // Specify the start time for nightly testing. + if(this->CTest->GetTestModel() == cmCTest::NIGHTLY) + { + args.push_back("-r{" + this->GetNightlyTime() + " +0000}"); + } + + std::vector<char const*> svn_update; + svn_update.push_back(this->CommandLineTool.c_str()); + svn_update.push_back("update"); + svn_update.push_back("--non-interactive"); + for(std::vector<cmStdString>::const_iterator ai = args.begin(); + ai != args.end(); ++ai) + { + svn_update.push_back(ai->c_str()); + } + svn_update.push_back(0); + + UpdateParser out(this, "up-out> "); + OutputLogger err(this->Log, "up-err> "); + return this->RunUpdateCommand(&svn_update[0], &out, &err); +} + +//---------------------------------------------------------------------------- +class cmCTestSVN::LogParser: public cmCTestVC::OutputLogger, + private cmXMLParser +{ +public: + LogParser(cmCTestSVN* svn, const char* prefix): + OutputLogger(svn->Log, prefix), SVN(svn) { this->InitializeParser(); } + ~LogParser() { this->CleanupParser(); } +private: + cmCTestSVN* SVN; + + typedef cmCTestSVN::Revision Revision; + typedef cmCTestSVN::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(); + } + else if(strcmp(name, "path") == 0) + { + this->CurChange = Change(); + if(const char* action = this->FindAttribute(atts, "action")) + { + this->CurChange.Action = action[0]; + } + } + } + + 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->SVN->DoRevision(this->Rev, this->Changes); + } + else if(strcmp(name, "path") == 0 && !this->CData.empty()) + { + this->CurChange.Path.assign(&this->CData[0], this->CData.size()); + this->Changes.push_back(this->CurChange); + } + else if(strcmp(name, "author") == 0 && !this->CData.empty()) + { + this->Rev.Author.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()); + } + this->CData.clear(); + } + + virtual void ReportError(int, int, const char* msg) + { + this->SVN->Log << "Error parsing svn log xml: " << msg << "\n"; + } +}; + +//---------------------------------------------------------------------------- +void cmCTestSVN::LoadRevisions() +{ + // We are interested in every revision included in the update. + std::string revs; + if(atoi(this->OldRevision.c_str()) < atoi(this->NewRevision.c_str())) + { + revs = "-r" + this->OldRevision + ":" + this->NewRevision; + } + else + { + revs = "-r" + this->NewRevision; + } + + // Run "svn log" to get all global revisions of interest. + const char* svn = this->CommandLineTool.c_str(); + const char* svn_log[] = {svn, "log", "--xml", "-v", revs.c_str(), 0}; + { + LogParser out(this, "log-out> "); + OutputLogger err(this->Log, "log-err> "); + this->RunChild(svn_log, &out, &err); + } +} + +//---------------------------------------------------------------------------- +void cmCTestSVN::DoRevision(Revision const& revision, + std::vector<Change> const& changes) +{ + // Guess the base checkout path from the changes if necessary. + if(this->Base.empty() && !changes.empty()) + { + this->GuessBase(changes); + } + this->cmCTestGlobalVC::DoRevision(revision, changes); +} + +//---------------------------------------------------------------------------- +class cmCTestSVN::StatusParser: public cmCTestVC::LineParser +{ +public: + StatusParser(cmCTestSVN* svn, const char* prefix): SVN(svn) + { + this->SetLog(&svn->Log, prefix); + this->RegexStatus.compile("^([ACDIMRX?!~ ])([CM ])[ L]... +(.+)$"); + } +private: + cmCTestSVN* SVN; + cmsys::RegularExpression RegexStatus; + bool ProcessLine() + { + if(this->RegexStatus.find(this->Line)) + { + this->DoPath(this->RegexStatus.match(1)[0], + this->RegexStatus.match(2)[0], + this->RegexStatus.match(3)); + } + return true; + } + + void DoPath(char path_status, char prop_status, std::string const& path) + { + char status = (path_status != ' ')? path_status : prop_status; + // See "svn help status". + switch(status) + { + case 'M': case '!': case 'A': case 'D': case 'R': + this->SVN->DoModification(PathModified, path); + break; + case 'C': case '~': + this->SVN->DoModification(PathConflicting, path); + break; + case 'X': case 'I': case '?': case ' ': default: + break; + } + } +}; + +//---------------------------------------------------------------------------- +void cmCTestSVN::LoadModifications() +{ + // Run "svn status" which reports local modifications. + const char* svn = this->CommandLineTool.c_str(); + const char* svn_status[] = {svn, "status", "--non-interactive", 0}; + StatusParser out(this, "status-out> "); + OutputLogger err(this->Log, "status-err> "); + this->RunChild(svn_status, &out, &err); +} + +//---------------------------------------------------------------------------- +void cmCTestSVN::WriteXMLGlobal(std::ostream& xml) +{ + this->cmCTestGlobalVC::WriteXMLGlobal(xml); + + xml << "\t<SVNPath>" << this->Base << "</SVNPath>\n"; +} diff --git a/Source/CTest/cmCTestSVN.h b/Source/CTest/cmCTestSVN.h new file mode 100644 index 000000000..f72c58fb0 --- /dev/null +++ b/Source/CTest/cmCTestSVN.h @@ -0,0 +1,68 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2009 Kitware, Inc. + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ +#ifndef cmCTestSVN_h +#define cmCTestSVN_h + +#include "cmCTestGlobalVC.h" + +/** \class cmCTestSVN + * \brief Interaction with subversion command-line tool + * + */ +class cmCTestSVN: public cmCTestGlobalVC +{ +public: + /** Construct with a CTest instance and update log stream. */ + cmCTestSVN(cmCTest* ctest, std::ostream& log); + + virtual ~cmCTestSVN(); + +private: + // Implement cmCTestVC internal API. + virtual void CleanupImpl(); + virtual void NoteOldRevision(); + virtual void NoteNewRevision(); + virtual bool UpdateImpl(); + + // URL of repository directory checked out in the working tree. + std::string URL; + + // URL of repository root directory. + std::string Root; + + // Directory under repository root checked out in working tree. + std::string Base; + + std::string LoadInfo(); + void LoadModifications(); + void LoadRevisions(); + + void GuessBase(std::vector<Change> const& changes); + const char* LocalPath(std::string const& path); + + void DoRevision(Revision const& revision, + std::vector<Change> const& changes); + + void WriteXMLGlobal(std::ostream& xml); + + // Parsing helper classes. + class InfoParser; + class LogParser; + class StatusParser; + class UpdateParser; + friend class InfoParser; + friend class LogParser; + friend class StatusParser; + friend class UpdateParser; +}; + +#endif diff --git a/Source/CTest/cmCTestScriptHandler.cxx b/Source/CTest/cmCTestScriptHandler.cxx new file mode 100644 index 000000000..8643cb3f7 --- /dev/null +++ b/Source/CTest/cmCTestScriptHandler.cxx @@ -0,0 +1,1095 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2009 Kitware, Inc., Insight Software Consortium + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ + +#include "cmCTestScriptHandler.h" + +#include "cmCTest.h" +#include "cmake.h" +#include "cmFunctionBlocker.h" +#include "cmMakefile.h" +#include "cmLocalGenerator.h" +#include "cmGlobalGenerator.h" +#include "cmGeneratedFileStream.h" + +//#include <cmsys/RegularExpression.hxx> +#include <cmsys/Process.h> + +// used for sleep +#ifdef _WIN32 +#include "windows.h" +#endif + +#include <stdlib.h> +#include <time.h> +#include <math.h> +#include <float.h> + +// needed for sleep +#if !defined(_WIN32) +# include <unistd.h> +#endif + +#include "cmCTestBuildCommand.h" +#include "cmCTestConfigureCommand.h" +#include "cmCTestCoverageCommand.h" +#include "cmCTestEmptyBinaryDirectoryCommand.h" +#include "cmCTestMemCheckCommand.h" +#include "cmCTestReadCustomFilesCommand.h" +#include "cmCTestRunScriptCommand.h" +#include "cmCTestSleepCommand.h" +#include "cmCTestStartCommand.h" +#include "cmCTestSubmitCommand.h" +#include "cmCTestTestCommand.h" +#include "cmCTestUpdateCommand.h" +#include "cmCTestUploadCommand.h" + +#define CTEST_INITIAL_CMAKE_OUTPUT_FILE_NAME "CTestInitialCMakeOutput.log" + +// used to keep elapsed time up to date +class cmCTestScriptFunctionBlocker : public cmFunctionBlocker +{ +public: + cmCTestScriptFunctionBlocker() {} + virtual ~cmCTestScriptFunctionBlocker() {} + virtual bool IsFunctionBlocked(const cmListFileFunction& lff, + cmMakefile &mf, + cmExecutionStatus &); + //virtual bool ShouldRemove(const cmListFileFunction& lff, cmMakefile &mf); + //virtual void ScopeEnded(cmMakefile &mf); + + cmCTestScriptHandler* CTestScriptHandler; +}; + +// simply update the time and don't block anything +bool cmCTestScriptFunctionBlocker:: +IsFunctionBlocked(const cmListFileFunction& , cmMakefile &, + cmExecutionStatus &) +{ + this->CTestScriptHandler->UpdateElapsedTime(); + return false; +} + +//---------------------------------------------------------------------- +cmCTestScriptHandler::cmCTestScriptHandler() +{ + this->Backup = false; + this->EmptyBinDir = false; + this->EmptyBinDirOnce = false; + this->Makefile = 0; + this->LocalGenerator = 0; + this->CMake = 0; + this->GlobalGenerator = 0; + + this->ScriptStartTime = 0; + + // the *60 is becuase the settings are in minutes but GetTime is seconds + this->MinimumInterval = 30*60; + this->ContinuousDuration = -1; +} + +//---------------------------------------------------------------------- +void cmCTestScriptHandler::Initialize() +{ + this->Superclass::Initialize(); + this->Backup = false; + this->EmptyBinDir = false; + this->EmptyBinDirOnce = false; + + this->SourceDir = ""; + this->BinaryDir = ""; + this->BackupSourceDir = ""; + this->BackupBinaryDir = ""; + this->CTestRoot = ""; + this->CVSCheckOut = ""; + this->CTestCmd = ""; + this->UpdateCmd = ""; + this->CTestEnv = ""; + this->InitialCache = ""; + this->CMakeCmd = ""; + this->CMOutFile = ""; + this->ExtraUpdates.clear(); + + this->MinimumInterval = 20*60; + this->ContinuousDuration = -1; + + // what time in seconds did this script start running + this->ScriptStartTime = 0; + + this->Makefile = 0; + if (this->LocalGenerator) + { + delete this->LocalGenerator; + } + this->LocalGenerator = 0; + if (this->GlobalGenerator) + { + delete this->GlobalGenerator; + } + this->GlobalGenerator = 0; + if (this->CMake) + { + delete this->CMake; + } +} + +//---------------------------------------------------------------------- +cmCTestScriptHandler::~cmCTestScriptHandler() +{ + // local generator owns the makefile + this->Makefile = 0; + if (this->LocalGenerator) + { + delete this->LocalGenerator; + } + this->LocalGenerator = 0; + if (this->GlobalGenerator) + { + delete this->GlobalGenerator; + } + this->GlobalGenerator = 0; + if (this->CMake) + { + delete this->CMake; + } +} + + +//---------------------------------------------------------------------- +// just adds an argument to the vector +void cmCTestScriptHandler::AddConfigurationScript(const char *script, + bool pscope) +{ + this->ConfigurationScripts.push_back(script); + this->ScriptProcessScope.push_back(pscope); +} + + +//---------------------------------------------------------------------- +// the generic entry point for handling scripts, this routine will run all +// the scripts provides a -S arguments +int cmCTestScriptHandler::ProcessHandler() +{ + int res = 0; + for (size_t i=0; i < this->ConfigurationScripts.size(); ++i) + { + // for each script run it + res += this->RunConfigurationScript + (cmSystemTools::CollapseFullPath(this->ConfigurationScripts[i].c_str()), + this->ScriptProcessScope[i]); + } + if ( res ) + { + return -1; + } + return 0; +} + +void cmCTestScriptHandler::UpdateElapsedTime() +{ + if (this->LocalGenerator) + { + // set the current elapsed time + char timeString[20]; + int itime = static_cast<unsigned int>(cmSystemTools::GetTime() + - this->ScriptStartTime); + sprintf(timeString,"%i",itime); + this->LocalGenerator->GetMakefile()->AddDefinition("CTEST_ELAPSED_TIME", + timeString); + } +} + +//---------------------------------------------------------------------- +void cmCTestScriptHandler::AddCTestCommand(cmCTestCommand* command) +{ + cmCTestCommand* newCom = command; + newCom->CTest = this->CTest; + newCom->CTestScriptHandler = this; + this->CMake->AddCommand(newCom); +} + +int cmCTestScriptHandler::ExecuteScript(const std::string& total_script_arg) +{ + // execute the script passing in the arguments to the script as well as the + // arguments from this invocation of cmake + std::vector<const char*> argv; + argv.push_back(this->CTest->GetCTestExecutable()); + argv.push_back("-SR"); + argv.push_back(total_script_arg.c_str()); + + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, + "Executable for CTest is: " << + this->CTest->GetCTestExecutable() << "\n"); + + // now pass through all the other arguments + std::vector<cmStdString> &initArgs = + this->CTest->GetInitialCommandLineArguments(); + //*** need to make sure this does not have the current script *** + for(size_t i=1; i < initArgs.size(); ++i) + { + argv.push_back(initArgs[i].c_str()); + } + argv.push_back(0); + + // Now create process object + cmsysProcess* cp = cmsysProcess_New(); + cmsysProcess_SetCommand(cp, &*argv.begin()); + //cmsysProcess_SetWorkingDirectory(cp, dir); + cmsysProcess_SetOption(cp, cmsysProcess_Option_HideWindow, 1); + //cmsysProcess_SetTimeout(cp, timeout); + cmsysProcess_Execute(cp); + + std::vector<char> out; + std::vector<char> err; + std::string line; + int pipe = cmSystemTools::WaitForLine(cp, line, 100.0, out, err); + while(pipe != cmsysProcess_Pipe_None) + { + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, "Output: " + << line << "\n"); + if(pipe == cmsysProcess_Pipe_STDERR) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, line << "\n"); + } + else if(pipe == cmsysProcess_Pipe_STDOUT) + { + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, line << "\n"); + } + pipe = cmSystemTools::WaitForLine(cp, line, 100, out, err); + } + + // Properly handle output of the build command + cmsysProcess_WaitForExit(cp, 0); + int result = cmsysProcess_GetState(cp); + int retVal = 0; + bool failed = false; + if(result == cmsysProcess_State_Exited) + { + retVal = cmsysProcess_GetExitValue(cp); + } + else if(result == cmsysProcess_State_Exception) + { + retVal = cmsysProcess_GetExitException(cp); + cmCTestLog(this->CTest, ERROR_MESSAGE, "\tThere was an exception: " + << cmsysProcess_GetExceptionString(cp) << " " << + retVal << std::endl); + failed = true; + } + else if(result == cmsysProcess_State_Expired) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, "\tThere was a timeout" + << std::endl); + failed = true; + } + else if(result == cmsysProcess_State_Error) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, "\tError executing ctest: " + << cmsysProcess_GetErrorString(cp) << std::endl); + failed = true; + } + cmsysProcess_Delete(cp); + if(failed) + { + cmOStringStream message; + message << "Error running command: ["; + message << result << "] "; + for(std::vector<const char*>::iterator i = argv.begin(); + i != argv.end(); ++i) + { + if(*i) + { + message << *i << " "; + } + } + cmCTestLog(this->CTest, ERROR_MESSAGE, + message.str() << argv[0] << std::endl); + return -1; + } + return retVal; +} + +void cmCTestScriptHandler::CreateCMake() +{ + // create a cmake instance to read the configuration script + if (this->CMake) + { + delete this->CMake; + delete this->GlobalGenerator; + delete this->LocalGenerator; + } + this->CMake = new cmake; + this->CMake->AddCMakePaths(); + this->GlobalGenerator = new cmGlobalGenerator; + this->GlobalGenerator->SetCMakeInstance(this->CMake); + + this->LocalGenerator = this->GlobalGenerator->CreateLocalGenerator(); + this->Makefile = this->LocalGenerator->GetMakefile(); + + // Set CMAKE_CURRENT_SOURCE_DIR and CMAKE_CURRENT_BINARY_DIR. + // Also, some commands need Makefile->GetCurrentDirectory(). + std::string cwd = cmSystemTools::GetCurrentWorkingDirectory(); + this->Makefile->SetStartDirectory(cwd.c_str()); + this->Makefile->SetStartOutputDirectory(cwd.c_str()); + + // remove all cmake commands which are not scriptable, since they can't be + // used in ctest scripts + this->CMake->RemoveUnscriptableCommands(); + + // add any ctest specific commands, probably should have common superclass + // for ctest commands to clean this up. If a couple more commands are + // created with the same format lets do that - ken + this->AddCTestCommand(new cmCTestBuildCommand); + this->AddCTestCommand(new cmCTestConfigureCommand); + this->AddCTestCommand(new cmCTestCoverageCommand); + this->AddCTestCommand(new cmCTestEmptyBinaryDirectoryCommand); + this->AddCTestCommand(new cmCTestMemCheckCommand); + this->AddCTestCommand(new cmCTestReadCustomFilesCommand); + this->AddCTestCommand(new cmCTestRunScriptCommand); + this->AddCTestCommand(new cmCTestSleepCommand); + this->AddCTestCommand(new cmCTestStartCommand); + this->AddCTestCommand(new cmCTestSubmitCommand); + this->AddCTestCommand(new cmCTestTestCommand); + this->AddCTestCommand(new cmCTestUpdateCommand); + this->AddCTestCommand(new cmCTestUploadCommand); +} + +void cmCTestScriptHandler::GetCommandDocumentation( + std::vector<cmDocumentationEntry>& v) const +{ + this->CMake->GetCommandDocumentation(v); +} + +//---------------------------------------------------------------------- +// this sets up some variables for the script to use, creates the required +// cmake instance and generators, and then reads in the script +int cmCTestScriptHandler::ReadInScript(const std::string& total_script_arg) +{ + // Reset the error flag so that the script is read in no matter what + cmSystemTools::ResetErrorOccuredFlag(); + + // if the argument has a , in it then it needs to be broken into the fist + // argument (which is the script) and the second argument which will be + // passed into the scripts as S_ARG + std::string script = total_script_arg; + std::string script_arg; + if (total_script_arg.find(",") != std::string::npos) + { + script = total_script_arg.substr(0,total_script_arg.find(",")); + script_arg = total_script_arg.substr(total_script_arg.find(",")+1); + } + // make sure the file exists + if (!cmSystemTools::FileExists(script.c_str())) + { + cmSystemTools::Error("Cannot find file: ", script.c_str()); + return 1; + } + + // read in the list file to fill the cache + // create a cmake instance to read the configuration script + this->CreateCMake(); + + // set a variable with the path to the current script + this->Makefile->AddDefinition("CTEST_SCRIPT_DIRECTORY", + cmSystemTools::GetFilenamePath(script).c_str()); + this->Makefile->AddDefinition("CTEST_SCRIPT_NAME", + cmSystemTools::GetFilenameName(script).c_str()); + this->Makefile->AddDefinition("CTEST_EXECUTABLE_NAME", + this->CTest->GetCTestExecutable()); + this->Makefile->AddDefinition("CMAKE_EXECUTABLE_NAME", + this->CTest->GetCMakeExecutable()); + this->Makefile->AddDefinition("CTEST_RUN_CURRENT_SCRIPT", true); + this->UpdateElapsedTime(); + + // add the script arg if defined + if (script_arg.size()) + { + this->Makefile->AddDefinition("CTEST_SCRIPT_ARG", script_arg.c_str()); + } + + // always add a function blocker to update the elapsed time + cmCTestScriptFunctionBlocker *f = new cmCTestScriptFunctionBlocker(); + f->CTestScriptHandler = this; + this->Makefile->AddFunctionBlocker(f); + + + /* Execute CTestScriptMode.cmake, which loads CMakeDetermineSystem and + CMakeSystemSpecificInformation, so + that variables like CMAKE_SYSTEM and also the search paths for libraries, + header and executables are set correctly and can be used. Makes new-style + ctest scripting easier. */ + std::string systemFile = + this->Makefile->GetModulesFile("CTestScriptMode.cmake"); + if (!this->Makefile->ReadListFile(0, systemFile.c_str()) || + cmSystemTools::GetErrorOccuredFlag()) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, "Error in read:" + << systemFile.c_str() << "\n"); + return 2; + } + + // Add definitions of variables passed in on the command line: + const std::map<std::string, std::string> &defs = + this->CTest->GetDefinitions(); + for (std::map<std::string, std::string>::const_iterator it = defs.begin(); + it != defs.end(); ++it) + { + this->Makefile->AddDefinition(it->first.c_str(), it->second.c_str()); + } + + // finally read in the script + if (!this->Makefile->ReadListFile(0, script.c_str()) || + cmSystemTools::GetErrorOccuredFlag()) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, "Error in read script: " + << script.c_str() + << std::endl); + // Reset the error flag so that it can run more than + // one script with an error when you + // use ctest_run_script + cmSystemTools::ResetErrorOccuredFlag(); + return 2; + } + + return 0; +} + + +//---------------------------------------------------------------------- +// extract variabels from the script to set ivars +int cmCTestScriptHandler::ExtractVariables() +{ + // Temporary variables + const char* minInterval; + const char* contDuration; + + this->SourceDir + = this->Makefile->GetSafeDefinition("CTEST_SOURCE_DIRECTORY"); + this->BinaryDir + = this->Makefile->GetSafeDefinition("CTEST_BINARY_DIRECTORY"); + + // add in translations for src and bin + cmSystemTools::AddKeepPath(this->SourceDir.c_str()); + cmSystemTools::AddKeepPath(this->BinaryDir.c_str()); + + this->CTestCmd + = this->Makefile->GetSafeDefinition("CTEST_COMMAND"); + this->CVSCheckOut + = this->Makefile->GetSafeDefinition("CTEST_CVS_CHECKOUT"); + this->CTestRoot + = this->Makefile->GetSafeDefinition("CTEST_DASHBOARD_ROOT"); + this->UpdateCmd + = this->Makefile->GetSafeDefinition("CTEST_UPDATE_COMMAND"); + if ( this->UpdateCmd.empty() ) + { + this->UpdateCmd + = this->Makefile->GetSafeDefinition("CTEST_CVS_COMMAND"); + } + this->CTestEnv + = this->Makefile->GetSafeDefinition("CTEST_ENVIRONMENT"); + this->InitialCache + = this->Makefile->GetSafeDefinition("CTEST_INITIAL_CACHE"); + this->CMakeCmd + = this->Makefile->GetSafeDefinition("CTEST_CMAKE_COMMAND"); + this->CMOutFile + = this->Makefile->GetSafeDefinition("CTEST_CMAKE_OUTPUT_FILE_NAME"); + + this->Backup + = this->Makefile->IsOn("CTEST_BACKUP_AND_RESTORE"); + this->EmptyBinDir + = this->Makefile->IsOn("CTEST_START_WITH_EMPTY_BINARY_DIRECTORY"); + this->EmptyBinDirOnce + = this->Makefile->IsOn("CTEST_START_WITH_EMPTY_BINARY_DIRECTORY_ONCE"); + + minInterval + = this->Makefile->GetDefinition("CTEST_CONTINUOUS_MINIMUM_INTERVAL"); + contDuration + = this->Makefile->GetDefinition("CTEST_CONTINUOUS_DURATION"); + + char updateVar[40]; + int i; + for (i = 1; i < 10; ++i) + { + sprintf(updateVar,"CTEST_EXTRA_UPDATES_%i",i); + const char *updateVal = this->Makefile->GetDefinition(updateVar); + if ( updateVal ) + { + if ( this->UpdateCmd.empty() ) + { + cmSystemTools::Error(updateVar, + " specified without specifying CTEST_CVS_COMMAND."); + return 12; + } + this->ExtraUpdates.push_back(updateVal); + } + } + + // in order to backup and restore we also must have the cvs root + if (this->Backup && this->CVSCheckOut.empty()) + { + cmSystemTools::Error( + "Backup was requested without specifying CTEST_CVS_CHECKOUT."); + return 3; + } + + // make sure the required info is here + if (this->SourceDir.empty() || + this->BinaryDir.empty() || + this->CTestCmd.empty()) + { + std::string msg = "CTEST_SOURCE_DIRECTORY = "; + msg += (!this->SourceDir.empty()) ? this->SourceDir.c_str() : "(Null)"; + msg += "\nCTEST_BINARY_DIRECTORY = "; + msg += (!this->BinaryDir.empty()) ? this->BinaryDir.c_str() : "(Null)"; + msg += "\nCTEST_COMMAND = "; + msg += (!this->CTestCmd.empty()) ? this->CTestCmd.c_str() : "(Null)"; + cmSystemTools::Error( + "Some required settings in the configuration file were missing:\n", + msg.c_str()); + return 4; + } + + // if the dashboard root isn't specified then we can compute it from the + // this->SourceDir + if (this->CTestRoot.empty() ) + { + this->CTestRoot = cmSystemTools::GetFilenamePath(this->SourceDir).c_str(); + } + + // the script may override the minimum continuous interval + if (minInterval) + { + this->MinimumInterval = 60 * atof(minInterval); + } + if (contDuration) + { + this->ContinuousDuration = 60.0 * atof(contDuration); + } + + + this->UpdateElapsedTime(); + + return 0; +} + +//---------------------------------------------------------------------- +void cmCTestScriptHandler::SleepInSeconds(unsigned int secondsToWait) +{ +#if defined(_WIN32) + Sleep(1000*secondsToWait); +#else + sleep(secondsToWait); +#endif +} + +//---------------------------------------------------------------------- +// run a specific script +int cmCTestScriptHandler::RunConfigurationScript +(const std::string& total_script_arg, bool pscope) +{ +#ifdef CMAKE_BUILD_WITH_CMAKE + cmSystemTools::SaveRestoreEnvironment sre; +#endif + + int result; + + this->ScriptStartTime = + cmSystemTools::GetTime(); + + // read in the script + if (pscope) + { + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, + "Reading Script: " << total_script_arg << std::endl); + result = this->ReadInScript(total_script_arg); + } + else + { + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, + "Executing Script: " << total_script_arg << std::endl); + result = this->ExecuteScript(total_script_arg); + } + if (result) + { + return result; + } + + // only run the curent script if we should + if (this->Makefile && this->Makefile->IsOn("CTEST_RUN_CURRENT_SCRIPT")) + { + return this->RunCurrentScript(); + } + return result; +} + +//---------------------------------------------------------------------- +int cmCTestScriptHandler::RunCurrentScript() +{ + int result; + + // do not run twice + this->Makefile->AddDefinition("CTEST_RUN_CURRENT_SCRIPT", false); + + // no popup widows + cmSystemTools::SetRunCommandHideConsole(true); + + // extract the vars from the cache and store in ivars + result = this->ExtractVariables(); + if (result) + { + return result; + } + + // set any environment variables + if (!this->CTestEnv.empty()) + { + std::vector<std::string> envArgs; + cmSystemTools::ExpandListArgument(this->CTestEnv.c_str(),envArgs); + cmSystemTools::AppendEnv(envArgs); + } + + // now that we have done most of the error checking finally run the + // dashboard, we may be asked to repeatedly run this dashboard, such as + // for a continuous, do we ned to run it more than once? + if ( this->ContinuousDuration >= 0 ) + { + this->UpdateElapsedTime(); + double ending_time = cmSystemTools::GetTime() + this->ContinuousDuration; + if (this->EmptyBinDirOnce) + { + this->EmptyBinDir = true; + } + do + { + double interval = cmSystemTools::GetTime(); + result = this->RunConfigurationDashboard(); + interval = cmSystemTools::GetTime() - interval; + if (interval < this->MinimumInterval) + { + this->SleepInSeconds( + static_cast<unsigned int>(this->MinimumInterval - interval)); + } + if (this->EmptyBinDirOnce) + { + this->EmptyBinDir = false; + } + } + while (cmSystemTools::GetTime() < ending_time); + } + // otherwise just run it once + else + { + result = this->RunConfigurationDashboard(); + } + + return result; +} + +//---------------------------------------------------------------------- +int cmCTestScriptHandler::CheckOutSourceDir() +{ + std::string command; + std::string output; + int retVal; + bool res; + + if (!cmSystemTools::FileExists(this->SourceDir.c_str()) && + !this->CVSCheckOut.empty()) + { + // we must now checkout the src dir + output = ""; + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, + "Run cvs: " << this->CVSCheckOut << std::endl); + res = cmSystemTools::RunSingleCommand(this->CVSCheckOut.c_str(), &output, + &retVal, this->CTestRoot.c_str(), this->HandlerVerbose, + 0 /*this->TimeOut*/); + if (!res || retVal != 0) + { + cmSystemTools::Error("Unable to perform cvs checkout:\n", + output.c_str()); + return 6; + } + } + return 0; +} + +//---------------------------------------------------------------------- +int cmCTestScriptHandler::BackupDirectories() +{ + int retVal; + + // compute the backup names + this->BackupSourceDir = this->SourceDir; + this->BackupSourceDir += "_CMakeBackup"; + this->BackupBinaryDir = this->BinaryDir; + this->BackupBinaryDir += "_CMakeBackup"; + + // backup the binary and src directories if requested + if (this->Backup) + { + // if for some reason those directories exist then first delete them + if (cmSystemTools::FileExists(this->BackupSourceDir.c_str())) + { + cmSystemTools::RemoveADirectory(this->BackupSourceDir.c_str()); + } + if (cmSystemTools::FileExists(this->BackupBinaryDir.c_str())) + { + cmSystemTools::RemoveADirectory(this->BackupBinaryDir.c_str()); + } + + // first rename the src and binary directories + rename(this->SourceDir.c_str(), this->BackupSourceDir.c_str()); + rename(this->BinaryDir.c_str(), this->BackupBinaryDir.c_str()); + + // we must now checkout the src dir + retVal = this->CheckOutSourceDir(); + if (retVal) + { + this->RestoreBackupDirectories(); + return retVal; + } + } + + return 0; +} + + +//---------------------------------------------------------------------- +int cmCTestScriptHandler::PerformExtraUpdates() +{ + std::string command; + std::string output; + int retVal; + bool res; + + // do an initial cvs update as required + command = this->UpdateCmd; + std::vector<cmStdString>::iterator it; + for (it = this->ExtraUpdates.begin(); + it != this->ExtraUpdates.end(); + ++ it ) + { + std::vector<std::string> cvsArgs; + cmSystemTools::ExpandListArgument(it->c_str(),cvsArgs); + if (cvsArgs.size() == 2) + { + std::string fullCommand = command; + fullCommand += " update "; + fullCommand += cvsArgs[1]; + output = ""; + retVal = 0; + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, "Run Update: " + << fullCommand.c_str() << std::endl); + res = cmSystemTools::RunSingleCommand(fullCommand.c_str(), &output, + &retVal, cvsArgs[0].c_str(), + this->HandlerVerbose, 0 /*this->TimeOut*/); + if (!res || retVal != 0) + { + cmSystemTools::Error("Unable to perform extra updates:\n", + it->c_str(), "\nWith output:\n", + output.c_str()); + return 0; + } + } + } + return 0; +} + + +//---------------------------------------------------------------------- +// run a single dashboard entry +int cmCTestScriptHandler::RunConfigurationDashboard() +{ + // local variables + std::string command; + std::string output; + int retVal; + bool res; + + // make sure the src directory is there, if it isn't then we might be able + // to check it out from cvs + retVal = this->CheckOutSourceDir(); + if (retVal) + { + return retVal; + } + + // backup the dirs if requested + retVal = this->BackupDirectories(); + if (retVal) + { + return retVal; + } + + // clear the binary directory? + if (this->EmptyBinDir) + { + if ( !cmCTestScriptHandler::EmptyBinaryDirectory( + this->BinaryDir.c_str()) ) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, + "Problem removing the binary directory" << std::endl); + } + } + + // make sure the binary directory exists if it isn't the srcdir + if (!cmSystemTools::FileExists(this->BinaryDir.c_str()) && + this->SourceDir != this->BinaryDir) + { + if (!cmSystemTools::MakeDirectory(this->BinaryDir.c_str())) + { + cmSystemTools::Error("Unable to create the binary directory:\n", + this->BinaryDir.c_str()); + this->RestoreBackupDirectories(); + return 7; + } + } + + // if the binary directory and the source directory are the same, + // and we are starting with an empty binary directory, then that means + // we must check out the source tree + if (this->EmptyBinDir && this->SourceDir == this->BinaryDir) + { + // make sure we have the required info + if (this->CVSCheckOut.empty()) + { + cmSystemTools::Error("You have specified the source and binary " + "directories to be the same (an in source build). You have also " + "specified that the binary directory is to be erased. This means " + "that the source will have to be checked out from CVS. But you have " + "not specified CTEST_CVS_CHECKOUT"); + return 8; + } + + // we must now checkout the src dir + retVal = this->CheckOutSourceDir(); + if (retVal) + { + this->RestoreBackupDirectories(); + return retVal; + } + } + + // backup the dirs if requested + retVal = this->PerformExtraUpdates(); + if (retVal) + { + return retVal; + } + + // put the initial cache into the bin dir + if (!this->InitialCache.empty()) + { + if (!this->WriteInitialCache(this->BinaryDir.c_str(), + this->InitialCache.c_str())) + { + this->RestoreBackupDirectories(); + return 9; + } + } + + // do an initial cmake to setup the DartConfig file + int cmakeFailed = 0; + std::string cmakeFailedOuput; + if (!this->CMakeCmd.empty()) + { + command = this->CMakeCmd; + command += " \""; + command += this->SourceDir; + output = ""; + command += "\""; + retVal = 0; + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, "Run cmake command: " + << command.c_str() << std::endl); + res = cmSystemTools::RunSingleCommand(command.c_str(), &output, + &retVal, this->BinaryDir.c_str(), + this->HandlerVerbose, 0 /*this->TimeOut*/); + + if ( !this->CMOutFile.empty() ) + { + std::string cmakeOutputFile = this->CMOutFile; + if ( !cmSystemTools::FileIsFullPath(cmakeOutputFile.c_str()) ) + { + cmakeOutputFile = this->BinaryDir + "/" + cmakeOutputFile; + } + + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, + "Write CMake output to file: " << cmakeOutputFile.c_str() + << std::endl); + cmGeneratedFileStream fout(cmakeOutputFile.c_str()); + if ( fout ) + { + fout << output.c_str(); + } + else + { + cmCTestLog(this->CTest, ERROR_MESSAGE, + "Cannot open CMake output file: " + << cmakeOutputFile.c_str() << " for writing" << std::endl); + } + } + if (!res || retVal != 0) + { + // even if this fails continue to the next step + cmakeFailed = 1; + cmakeFailedOuput = output; + } + } + + // run ctest, it may be more than one command in here + std::vector<std::string> ctestCommands; + cmSystemTools::ExpandListArgument(this->CTestCmd,ctestCommands); + // for each variable/argument do a putenv + for (unsigned i = 0; i < ctestCommands.size(); ++i) + { + command = ctestCommands[i]; + output = ""; + retVal = 0; + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, "Run ctest command: " + << command.c_str() << std::endl); + res = cmSystemTools::RunSingleCommand(command.c_str(), &output, + &retVal, this->BinaryDir.c_str(), this->HandlerVerbose, + 0 /*this->TimeOut*/); + + // did something critical fail in ctest + if (!res || cmakeFailed || + retVal & cmCTest::BUILD_ERRORS) + { + this->RestoreBackupDirectories(); + if (cmakeFailed) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, + "Unable to run cmake:" << std::endl + << cmakeFailedOuput.c_str() << std::endl); + return 10; + } + cmCTestLog(this->CTest, ERROR_MESSAGE, + "Unable to run ctest:" << std::endl + << "command: " << command.c_str() << std::endl + << "output: " << output.c_str() << std::endl); + if (!res) + { + return 11; + } + return retVal * 100; + } + } + + // if all was succesful, delete the backup dirs to free up disk space + if (this->Backup) + { + cmSystemTools::RemoveADirectory(this->BackupSourceDir.c_str()); + cmSystemTools::RemoveADirectory(this->BackupBinaryDir.c_str()); + } + + return 0; +} + +//------------------------------------------------------------------------- +bool cmCTestScriptHandler::WriteInitialCache(const char* directory, + const char* text) +{ + std::string cacheFile = directory; + cacheFile += "/CMakeCache.txt"; + cmGeneratedFileStream fout(cacheFile.c_str()); + if(!fout) + { + return false; + } + + if (text!=0) + { + fout.write(text, strlen(text)); + } + + // Make sure the operating system has finished writing the file + // before closing it. This will ensure the file is finished before + // the check below. + fout.flush(); + fout.close(); + return true; +} + +//------------------------------------------------------------------------- +void cmCTestScriptHandler::RestoreBackupDirectories() +{ + // if we backed up the dirs and the build failed, then restore + // the backed up dirs + if (this->Backup) + { + // if for some reason those directories exist then first delete them + if (cmSystemTools::FileExists(this->SourceDir.c_str())) + { + cmSystemTools::RemoveADirectory(this->SourceDir.c_str()); + } + if (cmSystemTools::FileExists(this->BinaryDir.c_str())) + { + cmSystemTools::RemoveADirectory(this->BinaryDir.c_str()); + } + // rename the src and binary directories + rename(this->BackupSourceDir.c_str(), this->SourceDir.c_str()); + rename(this->BackupBinaryDir.c_str(), this->BinaryDir.c_str()); + } +} + +bool cmCTestScriptHandler::RunScript(cmCTest* ctest, const char *sname, + bool InProcess, int* returnValue) +{ + cmCTestScriptHandler* sh = new cmCTestScriptHandler(); + sh->SetCTestInstance(ctest); + sh->AddConfigurationScript(sname,InProcess); + int res = sh->ProcessHandler(); + if(returnValue) + { + *returnValue = res; + } + delete sh; + return true; +} + +bool cmCTestScriptHandler::EmptyBinaryDirectory(const char *sname) +{ + // try to avoid deleting root + if (!sname || strlen(sname) < 2) + { + return false; + } + + // try to avoid deleting directories that we shouldn't + std::string check = sname; + check += "/CMakeCache.txt"; + if(cmSystemTools::FileExists(check.c_str()) && + !cmSystemTools::RemoveADirectory(sname)) + { + return false; + } + return true; +} + +//------------------------------------------------------------------------- +double cmCTestScriptHandler::GetRemainingTimeAllowed() +{ + if (!this->Makefile) + { + return 1.0e7; + } + + const char *timelimitS + = this->Makefile->GetDefinition("CTEST_TIME_LIMIT"); + + if (!timelimitS) + { + return 1.0e7; + } + + double timelimit = atof(timelimitS); + + return timelimit - cmSystemTools::GetTime() + this->ScriptStartTime; +} diff --git a/Source/CTest/cmCTestScriptHandler.h b/Source/CTest/cmCTestScriptHandler.h new file mode 100644 index 000000000..a7cf6ba65 --- /dev/null +++ b/Source/CTest/cmCTestScriptHandler.h @@ -0,0 +1,172 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2009 Kitware, Inc. + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ + +#ifndef cmCTestScriptHandler_h +#define cmCTestScriptHandler_h + + +#include "cmCTestGenericHandler.h" +#include "cmListFileCache.h" + +class cmMakefile; +class cmLocalGenerator; +class cmGlobalGenerator; +class cmake; +class cmCTestCommand; + +/** \class cmCTestScriptHandler + * \brief A class that handles ctest -S invocations + * + * CTest script is controlled using several variables that script has to + * specify and some optional ones. Required ones are: + * CTEST_SOURCE_DIRECTORY - Source directory of the project + * CTEST_BINARY_DIRECTORY - Binary directory of the project + * CTEST_COMMAND - Testing commands + * + * Optional variables are: + * CTEST_BACKUP_AND_RESTORE + * CTEST_CMAKE_COMMAND + * CTEST_CMAKE_OUTPUT_FILE_NAME + * CTEST_CONTINUOUS_DURATION + * CTEST_CONTINUOUS_MINIMUM_INTERVAL + * CTEST_CVS_CHECKOUT + * CTEST_CVS_COMMAND + * CTEST_UPDATE_COMMAND + * CTEST_DASHBOARD_ROOT + * CTEST_ENVIRONMENT + * CTEST_INITIAL_CACHE + * CTEST_START_WITH_EMPTY_BINARY_DIRECTORY + * CTEST_START_WITH_EMPTY_BINARY_DIRECTORY_ONCE + * + * In addition the following variables can be used. The number can be 1-10. + * CTEST_EXTRA_UPDATES_1 + * CTEST_EXTRA_UPDATES_2 + * ... + * CTEST_EXTRA_UPDATES_10 + * + * CTest script can use the following arguments CTest provides: + * CTEST_SCRIPT_ARG + * CTEST_SCRIPT_DIRECTORY + * CTEST_SCRIPT_NAME + * + */ +class cmCTestScriptHandler : public cmCTestGenericHandler +{ +public: + cmTypeMacro(cmCTestScriptHandler, cmCTestGenericHandler); + + /** + * Add a script to run, and if is should run in the current process + */ + void AddConfigurationScript(const char *, bool pscope); + + /** + * Run a dashboard using a specified confiuration script + */ + int ProcessHandler(); + + /* + * Run a script + */ + static bool RunScript(cmCTest* ctest, const char *script, bool InProcess, + int* returnValue); + int RunCurrentScript(); + + /* + * Empty Binary Directory + */ + static bool EmptyBinaryDirectory(const char *dir); + + /* + * Write an initial CMakeCache.txt from the given contents. + */ + static bool WriteInitialCache(const char* directory, const char* text); + + /* + * Some elapsed time handling functions + */ + static void SleepInSeconds(unsigned int secondsToWait); + void UpdateElapsedTime(); + + /** + * Return the time remaianing that the script is allowed to run in + * seconds if the user has set the variable CTEST_TIME_LIMIT. If that has + * not been set it returns 1e7 seconds + */ + double GetRemainingTimeAllowed(); + + cmCTestScriptHandler(); + ~cmCTestScriptHandler(); + + void Initialize(); + + void CreateCMake(); + void GetCommandDocumentation(std::vector<cmDocumentationEntry>& v) const; + cmake* GetCMake() { return this->CMake;} +private: + // reads in a script + int ReadInScript(const std::string& total_script_arg); + int ExecuteScript(const std::string& total_script_arg); + + // extract vars from the script to set ivars + int ExtractVariables(); + + // perform a CVS checkout of the source dir + int CheckOutSourceDir(); + + // perform any extra cvs updates that were requested + int PerformExtraUpdates(); + + // backup and restore dirs + int BackupDirectories(); + void RestoreBackupDirectories(); + + int RunConfigurationScript(const std::string& script, bool pscope); + int RunConfigurationDashboard(); + + // Add ctest command + void AddCTestCommand(cmCTestCommand* command); + + std::vector<cmStdString> ConfigurationScripts; + std::vector<bool> ScriptProcessScope; + + bool Backup; + bool EmptyBinDir; + bool EmptyBinDirOnce; + + cmStdString SourceDir; + cmStdString BinaryDir; + cmStdString BackupSourceDir; + cmStdString BackupBinaryDir; + cmStdString CTestRoot; + cmStdString CVSCheckOut; + cmStdString CTestCmd; + cmStdString UpdateCmd; + cmStdString CTestEnv; + cmStdString InitialCache; + cmStdString CMakeCmd; + cmStdString CMOutFile; + std::vector<cmStdString> ExtraUpdates; + + double MinimumInterval; + double ContinuousDuration; + + // what time in seconds did this script start running + double ScriptStartTime; + + cmMakefile *Makefile; + cmLocalGenerator *LocalGenerator; + cmGlobalGenerator *GlobalGenerator; + cmake *CMake; +}; + +#endif diff --git a/Source/CTest/cmCTestSleepCommand.cxx b/Source/CTest/cmCTestSleepCommand.cxx new file mode 100644 index 000000000..5b0697219 --- /dev/null +++ b/Source/CTest/cmCTestSleepCommand.cxx @@ -0,0 +1,55 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2009 Kitware, Inc., Insight Software Consortium + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ +#include "cmCTestSleepCommand.h" + +#include "cmCTestScriptHandler.h" +#include <stdlib.h> // required for atoi + +bool cmCTestSleepCommand +::InitialPass(std::vector<std::string> const& args, cmExecutionStatus &) +{ + if (args.size() < 1) + { + this->SetError("called with incorrect number of arguments"); + return false; + } + + // sleep for specified seconds + unsigned int time1 = atoi(args[0].c_str()); + if(args.size() == 1 ) + { + cmCTestScriptHandler::SleepInSeconds(time1); + // update the elapsed time since it could have slept for a while + this->CTestScriptHandler->UpdateElapsedTime(); + return true; + } + + // sleep up to a duration + if(args.size() == 3 ) + { + unsigned int duration = atoi(args[1].c_str()); + unsigned int time2 = atoi(args[2].c_str()); + if (time1 + duration > time2) + { + duration = (time1 + duration - time2); + cmCTestScriptHandler::SleepInSeconds(duration); + // update the elapsed time since it could have slept for a while + this->CTestScriptHandler->UpdateElapsedTime(); + } + return true; + } + + this->SetError("called with incorrect number of arguments"); + return false; +} + + diff --git a/Source/CTest/cmCTestSleepCommand.h b/Source/CTest/cmCTestSleepCommand.h new file mode 100644 index 000000000..80fd6afb7 --- /dev/null +++ b/Source/CTest/cmCTestSleepCommand.h @@ -0,0 +1,77 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2009 Kitware, Inc., Insight Software Consortium + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ +#ifndef cmCTestSleepCommand_h +#define cmCTestSleepCommand_h + +#include "cmCTestCommand.h" + +/** \class cmCTestSleep + * \brief Run a ctest script + * + * cmLibrarysCommand defines a list of executable (i.e., test) + * programs to create. + */ +class cmCTestSleepCommand : public cmCTestCommand +{ +public: + + cmCTestSleepCommand() {} + + /** + * This is a virtual constructor for the command. + */ + virtual cmCommand* Clone() + { + cmCTestSleepCommand* ni = new cmCTestSleepCommand; + ni->CTest = this->CTest; + ni->CTestScriptHandler = this->CTestScriptHandler; + return ni; + } + + /** + * This is called when the command is first encountered in + * the CMakeLists.txt file. + */ + virtual bool InitialPass(std::vector<std::string> const& args, + cmExecutionStatus &status); + + /** + * The name of the command as specified in CMakeList.txt. + */ + virtual const char* GetName() const { return "ctest_sleep";} + + /** + * Succinct documentation. + */ + virtual const char* GetTerseDocumentation() const + { + return "sleeps for some amount of time"; + } + + /** + * More documentation. + */ + virtual const char* GetFullDocumentation() const + { + return + " ctest_sleep(<seconds>)\n" + "Sleep for given number of seconds.\n" + " ctest_sleep(<time1> <duration> <time2>)\n" + "Sleep for t=(time1 + duration - time2) seconds if t > 0."; + } + + cmTypeMacro(cmCTestSleepCommand, cmCTestCommand); + +}; + + +#endif diff --git a/Source/CTest/cmCTestStartCommand.cxx b/Source/CTest/cmCTestStartCommand.cxx new file mode 100644 index 000000000..228a17300 --- /dev/null +++ b/Source/CTest/cmCTestStartCommand.cxx @@ -0,0 +1,170 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2009 Kitware, Inc., Insight Software Consortium + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ +#include "cmCTestStartCommand.h" + +#include "cmCTest.h" +#include "cmLocalGenerator.h" +#include "cmGlobalGenerator.h" +#include "cmCTestVC.h" +#include "cmGeneratedFileStream.h" + +cmCTestStartCommand::cmCTestStartCommand() +{ + this->CreateNewTag = true; +} + +bool cmCTestStartCommand +::InitialPass(std::vector<std::string> const& args, cmExecutionStatus &) +{ + if (args.size() < 1) + { + this->SetError("called with incorrect number of arguments"); + return false; + } + + size_t cnt = 0; + const char* smodel = args[cnt].c_str(); + const char* src_dir = 0; + const char* bld_dir = 0; + + cnt++; + + this->CTest->SetSpecificTrack(0); + if ( cnt < args.size() -1 ) + { + if ( args[cnt] == "TRACK" ) + { + cnt ++; + this->CTest->SetSpecificTrack(args[cnt].c_str()); + cnt ++; + } + } + + if (cnt < args.size()) + { + if (args[cnt] == "APPEND") + { + cnt ++; + this->CreateNewTag = false; + } + } + + if ( cnt < args.size() ) + { + src_dir = args[cnt].c_str(); + cnt ++; + if ( cnt < args.size() ) + { + bld_dir = args[cnt].c_str(); + } + } + if ( !src_dir ) + { + src_dir = this->Makefile->GetDefinition("CTEST_SOURCE_DIRECTORY"); + } + if ( !bld_dir) + { + bld_dir = this->Makefile->GetDefinition("CTEST_BINARY_DIRECTORY"); + } + if ( !src_dir ) + { + this->SetError("source directory not specified. Specify source directory " + "as an argument or set CTEST_SOURCE_DIRECTORY"); + return false; + } + if ( !bld_dir) + { + this->SetError("binary directory not specified. Specify binary directory " + "as an argument or set CTEST_BINARY_DIRECTORY"); + return false; + } + + cmSystemTools::AddKeepPath(src_dir); + cmSystemTools::AddKeepPath(bld_dir); + + this->CTest->EmptyCTestConfiguration(); + + std::string sourceDir = cmSystemTools::CollapseFullPath(src_dir); + std::string binaryDir = cmSystemTools::CollapseFullPath(bld_dir); + this->CTest->SetCTestConfiguration("SourceDirectory", sourceDir.c_str()); + this->CTest->SetCTestConfiguration("BuildDirectory", binaryDir.c_str()); + + cmCTestLog(this->CTest, HANDLER_OUTPUT, "Run dashboard with model " + << smodel << std::endl + << " Source directory: " << src_dir << std::endl + << " Build directory: " << bld_dir << std::endl); + const char* track = this->CTest->GetSpecificTrack(); + if ( track ) + { + cmCTestLog(this->CTest, HANDLER_OUTPUT, + " Track: " << track << std::endl); + } + + // Log startup actions. + std::string startLogFile = binaryDir + "/Testing/Temporary/LastStart.log"; + cmGeneratedFileStream ofs(startLogFile.c_str()); + if(!ofs) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, + "Cannot create log file: LastStart.log" << std::endl); + return false; + } + + // Make sure the source directory exists. + if(!this->InitialCheckout(ofs, sourceDir)) + { + return false; + } + if(!cmSystemTools::FileIsDirectory(sourceDir.c_str())) + { + cmOStringStream e; + e << "given source path\n" + << " " << sourceDir << "\n" + << "which is not an existing directory. " + << "Set CTEST_CHECKOUT_COMMAND to a command line to create it."; + this->SetError(e.str().c_str()); + return false; + } + + this->Makefile->AddDefinition("CTEST_RUN_CURRENT_SCRIPT", "OFF"); + this->CTest->SetSuppressUpdatingCTestConfiguration(true); + int model = this->CTest->GetTestModelFromString(smodel); + this->CTest->SetTestModel(model); + this->CTest->SetProduceXML(true); + + return this->CTest->InitializeFromCommand(this); +} + +//---------------------------------------------------------------------------- +bool cmCTestStartCommand::InitialCheckout( + std::ostream& ofs, std::string const& sourceDir) +{ + // Use the user-provided command to create the source tree. + const char* initialCheckoutCommand + = this->Makefile->GetDefinition("CTEST_CHECKOUT_COMMAND"); + if(!initialCheckoutCommand) + { + initialCheckoutCommand = + this->Makefile->GetDefinition("CTEST_CVS_CHECKOUT"); + } + if(initialCheckoutCommand) + { + // Use a generic VC object to run and log the command. + cmCTestVC vc(this->CTest, ofs); + vc.SetSourceDirectory(sourceDir.c_str()); + if(!vc.InitialCheckout(initialCheckoutCommand)) + { + return false; + } + } + return true; +} diff --git a/Source/CTest/cmCTestStartCommand.h b/Source/CTest/cmCTestStartCommand.h new file mode 100644 index 000000000..6be47703e --- /dev/null +++ b/Source/CTest/cmCTestStartCommand.h @@ -0,0 +1,92 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2009 Kitware, Inc., Insight Software Consortium + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ +#ifndef cmCTestStartCommand_h +#define cmCTestStartCommand_h + +#include "cmCTestCommand.h" + +/** \class cmCTestStart + * \brief Run a ctest script + * + * cmCTestStartCommand defineds the command to start the nightly testing. + */ +class cmCTestStartCommand : public cmCTestCommand +{ +public: + + cmCTestStartCommand(); + + /** + * This is a virtual constructor for the command. + */ + virtual cmCommand* Clone() + { + cmCTestStartCommand* ni = new cmCTestStartCommand; + ni->CTest = this->CTest; + ni->CTestScriptHandler = this->CTestScriptHandler; + ni->CreateNewTag = this->CreateNewTag; + return ni; + } + + /** + * This is called when the command is first encountered in + * the CMakeLists.txt file. + */ + virtual bool InitialPass(std::vector<std::string> const& args, + cmExecutionStatus &status); + + /** + * Will this invocation of ctest_start create a new TAG file? + */ + bool ShouldCreateNewTag() + { + return this->CreateNewTag; + } + + /** + * The name of the command as specified in CMakeList.txt. + */ + virtual const char* GetName() const { return "ctest_start";} + + /** + * Succinct documentation. + */ + virtual const char* GetTerseDocumentation() const + { + return "Starts the testing for a given model"; + } + + /** + * More documentation. + */ + virtual const char* GetFullDocumentation() const + { + return + " ctest_start(Model [TRACK <track>] [APPEND] [source [binary]])\n" + "Starts the testing for a given model. The command should be called " + "after the binary directory is initialized. If the 'source' and " + "'binary' directory are not specified, it reads the " + "CTEST_SOURCE_DIRECTORY and CTEST_BINARY_DIRECTORY. If the track is " + "specified, the submissions will go to the specified track. " + "If APPEND is used, the existing TAG is used rather than " + "creating a new one based on the current time stamp."; + } + + cmTypeMacro(cmCTestStartCommand, cmCTestCommand); + +private: + bool InitialCheckout(std::ostream& ofs, std::string const& sourceDir); + bool CreateNewTag; +}; + + +#endif diff --git a/Source/CTest/cmCTestSubmitCommand.cxx b/Source/CTest/cmCTestSubmitCommand.cxx new file mode 100644 index 000000000..24974e3bc --- /dev/null +++ b/Source/CTest/cmCTestSubmitCommand.cxx @@ -0,0 +1,255 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2009 Kitware, Inc., Insight Software Consortium + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ +#include "cmCTestSubmitCommand.h" + +#include "cmCTest.h" +#include "cmCTestGenericHandler.h" +#include "cmCTestSubmitHandler.h" + +cmCTestGenericHandler* cmCTestSubmitCommand::InitializeHandler() +{ + const char* ctestDropMethod + = this->Makefile->GetDefinition("CTEST_DROP_METHOD"); + const char* ctestDropSite + = this->Makefile->GetDefinition("CTEST_DROP_SITE"); + const char* ctestDropLocation + = this->Makefile->GetDefinition("CTEST_DROP_LOCATION"); + const char* ctestTriggerSite + = this->Makefile->GetDefinition("CTEST_TRIGGER_SITE"); + bool ctestDropSiteCDash + = this->Makefile->IsOn("CTEST_DROP_SITE_CDASH"); + + if ( !ctestDropMethod ) + { + ctestDropMethod = "http"; + } + + if ( !ctestDropSite ) + { + // error: CDash requires CTEST_DROP_SITE definition + // in CTestConfig.cmake + } + if ( !ctestDropLocation ) + { + // error: CDash requires CTEST_DROP_LOCATION definition + // in CTestConfig.cmake + } + + this->CTest->SetCTestConfiguration("DropMethod", ctestDropMethod); + this->CTest->SetCTestConfiguration("DropSite", ctestDropSite); + this->CTest->SetCTestConfiguration("DropLocation", ctestDropLocation); + + this->CTest->SetCTestConfiguration("IsCDash", + ctestDropSiteCDash ? "TRUE" : "FALSE"); + + // Only propagate TriggerSite for non-CDash projects: + // + if ( !ctestDropSiteCDash ) + { + this->CTest->SetCTestConfiguration("TriggerSite", ctestTriggerSite); + } + + this->CTest->SetCTestConfigurationFromCMakeVariable(this->Makefile, + "CurlOptions", "CTEST_CURL_OPTIONS"); + this->CTest->SetCTestConfigurationFromCMakeVariable(this->Makefile, + "DropSiteUser", "CTEST_DROP_SITE_USER"); + this->CTest->SetCTestConfigurationFromCMakeVariable(this->Makefile, + "DropSitePassword", "CTEST_DROP_SITE_PASSWORD"); + this->CTest->SetCTestConfigurationFromCMakeVariable(this->Makefile, + "ScpCommand", "CTEST_SCP_COMMAND"); + + const char* notesFilesVariable + = this->Makefile->GetDefinition("CTEST_NOTES_FILES"); + if (notesFilesVariable) + { + std::vector<std::string> notesFiles; + std::vector<cmStdString> newNotesFiles; + cmSystemTools::ExpandListArgument(notesFilesVariable,notesFiles); + std::vector<std::string>::iterator it; + for ( it = notesFiles.begin(); + it != notesFiles.end(); + ++ it ) + { + newNotesFiles.push_back(*it); + } + this->CTest->GenerateNotesFile(newNotesFiles); + } + + const char* extraFilesVariable + = this->Makefile->GetDefinition("CTEST_EXTRA_SUBMIT_FILES"); + if (extraFilesVariable) + { + std::vector<std::string> extraFiles; + std::vector<cmStdString> newExtraFiles; + cmSystemTools::ExpandListArgument(extraFilesVariable,extraFiles); + std::vector<std::string>::iterator it; + for ( it = extraFiles.begin(); + it != extraFiles.end(); + ++ it ) + { + newExtraFiles.push_back(*it); + } + if ( !this->CTest->SubmitExtraFiles(newExtraFiles)) + { + this->SetError("problem submitting extra files."); + return 0; + } + } + + cmCTestGenericHandler* handler + = this->CTest->GetInitializedHandler("submit"); + if ( !handler ) + { + this->SetError("internal CTest error. Cannot instantiate submit handler"); + return 0; + } + + // If no FILES or PARTS given, *all* PARTS are submitted by default. + // + // If FILES are given, but not PARTS, only the FILES are submitted + // and *no* PARTS are submitted. + // (This is why we select the empty "noParts" set in the + // FilesMentioned block below...) + // + // If PARTS are given, only the selected PARTS are submitted. + // + // If both PARTS and FILES are given, only the selected PARTS *and* + // all the given FILES are submitted. + + // If given explicit FILES to submit, pass them to the handler. + // + if(this->FilesMentioned) + { + // Intentionally select *no* PARTS. (Pass an empty set.) If PARTS + // were also explicitly mentioned, they will be selected below... + // But FILES with no PARTS mentioned should just submit the FILES + // without any of the default parts. + // + std::set<cmCTest::Part> noParts; + static_cast<cmCTestSubmitHandler*>(handler)->SelectParts(noParts); + + static_cast<cmCTestSubmitHandler*>(handler)->SelectFiles(this->Files); + } + + // If a PARTS option was given, select only the named parts for submission. + // + if(this->PartsMentioned) + { + static_cast<cmCTestSubmitHandler*>(handler)->SelectParts(this->Parts); + } + + static_cast<cmCTestSubmitHandler*>(handler)->SetOption("RetryDelay", + this->RetryDelay.c_str()); + static_cast<cmCTestSubmitHandler*>(handler)->SetOption("RetryCount", + this->RetryCount.c_str()); + static_cast<cmCTestSubmitHandler*>(handler)->SetOption("InternalTest", + this->InternalTest ? "ON" : "OFF"); + + return handler; +} + + +//---------------------------------------------------------------------------- +bool cmCTestSubmitCommand::CheckArgumentKeyword(std::string const& arg) +{ + // Look for arguments specific to this command. + if(arg == "PARTS") + { + this->ArgumentDoing = ArgumentDoingParts; + this->PartsMentioned = true; + return true; + } + + if(arg == "FILES") + { + this->ArgumentDoing = ArgumentDoingFiles; + this->FilesMentioned = true; + return true; + } + + if(arg == "RETRY_COUNT") + { + this->ArgumentDoing = ArgumentDoingRetryCount; + return true; + } + + if(arg == "RETRY_DELAY") + { + this->ArgumentDoing = ArgumentDoingRetryDelay; + return true; + } + + if(arg == "INTERNAL_TEST_CHECKSUM") + { + this->InternalTest = true; + return true; + } + + // Look for other arguments. + return this->Superclass::CheckArgumentKeyword(arg); +} + + +//---------------------------------------------------------------------------- +bool cmCTestSubmitCommand::CheckArgumentValue(std::string const& arg) +{ + // Handle states specific to this command. + if(this->ArgumentDoing == ArgumentDoingParts) + { + cmCTest::Part p = this->CTest->GetPartFromName(arg.c_str()); + if(p != cmCTest::PartCount) + { + this->Parts.insert(p); + } + else + { + cmOStringStream e; + e << "Part name \"" << arg << "\" is invalid."; + this->Makefile->IssueMessage(cmake::FATAL_ERROR, e.str()); + this->ArgumentDoing = ArgumentDoingError; + } + return true; + } + + if(this->ArgumentDoing == ArgumentDoingFiles) + { + cmStdString filename(arg); + if(cmSystemTools::FileExists(filename.c_str())) + { + this->Files.insert(filename); + } + else + { + cmOStringStream e; + e << "File \"" << filename << "\" does not exist. Cannot submit " + << "a non-existent file."; + this->Makefile->IssueMessage(cmake::FATAL_ERROR, e.str()); + this->ArgumentDoing = ArgumentDoingError; + } + return true; + } + + if(this->ArgumentDoing == ArgumentDoingRetryCount) + { + this->RetryCount = arg; + return true; + } + + if(this->ArgumentDoing == ArgumentDoingRetryDelay) + { + this->RetryDelay = arg; + return true; + } + + // Look for other arguments. + return this->Superclass::CheckArgumentValue(arg); +} diff --git a/Source/CTest/cmCTestSubmitCommand.h b/Source/CTest/cmCTestSubmitCommand.h new file mode 100644 index 000000000..53ee8754e --- /dev/null +++ b/Source/CTest/cmCTestSubmitCommand.h @@ -0,0 +1,118 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2009 Kitware, Inc., Insight Software Consortium + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ +#ifndef cmCTestSubmitCommand_h +#define cmCTestSubmitCommand_h + +#include "cmCTestHandlerCommand.h" +#include "cmCTest.h" + +/** \class cmCTestSubmit + * \brief Run a ctest script + * + * cmCTestSubmitCommand defineds the command to submit the test results for + * the project. + */ +class cmCTestSubmitCommand : public cmCTestHandlerCommand +{ +public: + + cmCTestSubmitCommand() + { + this->PartsMentioned = false; + this->FilesMentioned = false; + this->InternalTest = false; + this->RetryCount = ""; + this->RetryDelay = ""; + } + + /** + * This is a virtual constructor for the command. + */ + virtual cmCommand* Clone() + { + cmCTestSubmitCommand* ni = new cmCTestSubmitCommand; + ni->CTest = this->CTest; + ni->CTestScriptHandler = this->CTestScriptHandler; + return ni; + } + + /** + * The name of the command as specified in CMakeList.txt. + */ + virtual const char* GetName() const { return "ctest_submit";} + + /** + * Succinct documentation. + */ + virtual const char* GetTerseDocumentation() const + { + return "Submit results to a dashboard server."; + } + + /** + * More documentation. + */ + virtual const char* GetFullDocumentation() const + { + return + " ctest_submit([PARTS ...] [FILES ...] [RETRY_COUNT count] " + " [RETRY_DELAY delay][RETURN_VALUE res])\n" + "By default all available parts are submitted if no PARTS or FILES " + "are specified. " + "The PARTS option lists a subset of parts to be submitted. " + "Valid part names are:\n" + " Start = nothing\n" + " Update = ctest_update results, in Update.xml\n" + " Configure = ctest_configure results, in Configure.xml\n" + " Build = ctest_build results, in Build.xml\n" + " Test = ctest_test results, in Test.xml\n" + " Coverage = ctest_coverage results, in Coverage.xml\n" + " MemCheck = ctest_memcheck results, in DynamicAnalysis.xml\n" + " Notes = Files listed by CTEST_NOTES_FILES, in Notes.xml\n" + " ExtraFiles = Files listed by CTEST_EXTRA_SUBMIT_FILES\n" + " Submit = nothing\n" + "The FILES option explicitly lists specific files to be submitted. " + "Each individual file must exist at the time of the call.\n" + "The RETRY_DELAY option specifies how long in seconds to wait after " + "a timed-out submission before attempting to re-submit.\n" + "The RETRY_COUNT option specifies how many times to retry a timed-out " + "submission.\n"; + } + + cmTypeMacro(cmCTestSubmitCommand, cmCTestHandlerCommand); + +protected: + cmCTestGenericHandler* InitializeHandler(); + + virtual bool CheckArgumentKeyword(std::string const& arg); + virtual bool CheckArgumentValue(std::string const& arg); + + enum + { + ArgumentDoingParts = Superclass::ArgumentDoingLast1, + ArgumentDoingFiles, + ArgumentDoingRetryDelay, + ArgumentDoingRetryCount, + ArgumentDoingLast2 + }; + + bool PartsMentioned; + std::set<cmCTest::Part> Parts; + bool FilesMentioned; + bool InternalTest; + cmCTest::SetOfStrings Files; + std::string RetryCount; + std::string RetryDelay; +}; + + +#endif diff --git a/Source/CTest/cmCTestSubmitHandler.cxx b/Source/CTest/cmCTestSubmitHandler.cxx new file mode 100644 index 000000000..142bb4613 --- /dev/null +++ b/Source/CTest/cmCTestSubmitHandler.cxx @@ -0,0 +1,1508 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2009 Kitware, Inc. + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ +#include "cmCTestSubmitHandler.h" + +#include "cmSystemTools.h" +#include "cmVersion.h" +#include "cmGeneratedFileStream.h" +#include "cmCTest.h" +#include "cmXMLParser.h" + +#include <cmsys/Process.h> +#include <cmsys/Base64.h> + +// For XML-RPC submission +#include "cm_xmlrpc.h" + +// For curl submission +#include "cm_curl.h" + +#include <sys/stat.h> + +#define SUBMIT_TIMEOUT_IN_SECONDS_DEFAULT 120 + +typedef std::vector<char> cmCTestSubmitHandlerVectorOfChar; + +//---------------------------------------------------------------------------- +class cmCTestSubmitHandler::ResponseParser: public cmXMLParser +{ +public: + ResponseParser() { this->Status = STATUS_OK; } + ~ResponseParser() {} + +public: + + enum StatusType + { + STATUS_OK, + STATUS_WARNING, + STATUS_ERROR + }; + + StatusType Status; + std::string CDashVersion; + std::string Filename; + std::string MD5; + std::string Message; + +private: + + std::vector<char> CurrentValue; + + std::string GetCurrentValue() + { + std::string val; + if(this->CurrentValue.size()) + { + val.assign(&this->CurrentValue[0], this->CurrentValue.size()); + } + return val; + } + + virtual void StartElement(const char* name, const char** atts) + { + this->CurrentValue.clear(); + if(strcmp(name, "cdash") == 0) + { + this->CDashVersion = this->FindAttribute(atts, "version"); + } + } + + virtual void CharacterDataHandler(const char* data, int length) + { + this->CurrentValue.insert(this->CurrentValue.end(), data, data+length); + } + + virtual void EndElement(const char* name) + { + if(strcmp(name, "status") == 0) + { + std::string status = cmSystemTools::UpperCase(this->GetCurrentValue()); + if(status == "OK" || status == "SUCCESS") + { + this->Status = STATUS_OK; + } + else if(status == "WARNING") + { + this->Status = STATUS_WARNING; + } + else + { + this->Status = STATUS_ERROR; + } + } + else if(strcmp(name, "filename") == 0) + { + this->Filename = this->GetCurrentValue(); + } + else if(strcmp(name, "md5") == 0) + { + this->MD5 = this->GetCurrentValue(); + } + else if(strcmp(name, "message") == 0) + { + this->Message = this->GetCurrentValue(); + } + } +}; + + +static size_t +cmCTestSubmitHandlerWriteMemoryCallback(void *ptr, size_t size, size_t nmemb, + void *data) +{ + register int realsize = (int)(size * nmemb); + + cmCTestSubmitHandlerVectorOfChar *vec + = static_cast<cmCTestSubmitHandlerVectorOfChar*>(data); + const char* chPtr = static_cast<char*>(ptr); + vec->insert(vec->end(), chPtr, chPtr + realsize); + + return realsize; +} + +static size_t +cmCTestSubmitHandlerCurlDebugCallback(CURL *, curl_infotype, char *chPtr, + size_t size, void *data) +{ + cmCTestSubmitHandlerVectorOfChar *vec + = static_cast<cmCTestSubmitHandlerVectorOfChar*>(data); + vec->insert(vec->end(), chPtr, chPtr + size); + + return size; +} + +//---------------------------------------------------------------------------- +cmCTestSubmitHandler::cmCTestSubmitHandler() : HTTPProxy(), FTPProxy() +{ + this->Initialize(); +} + +//---------------------------------------------------------------------------- +void cmCTestSubmitHandler::Initialize() +{ + // We submit all available parts by default. + for(cmCTest::Part p = cmCTest::PartStart; + p != cmCTest::PartCount; p = cmCTest::Part(p+1)) + { + this->SubmitPart[p] = true; + } + this->CDash = false; + this->HasWarnings = false; + this->HasErrors = false; + this->Superclass::Initialize(); + this->HTTPProxy = ""; + this->HTTPProxyType = 0; + this->HTTPProxyAuth = ""; + this->FTPProxy = ""; + this->FTPProxyType = 0; + this->LogFile = 0; + this->Files.clear(); +} + +//---------------------------------------------------------------------------- +bool cmCTestSubmitHandler::SubmitUsingFTP(const cmStdString& localprefix, + const std::set<cmStdString>& files, + const cmStdString& remoteprefix, + const cmStdString& url) +{ + CURL *curl; + CURLcode res; + FILE* ftpfile; + char error_buffer[1024]; + + /* In windows, this will init the winsock stuff */ + ::curl_global_init(CURL_GLOBAL_ALL); + + cmCTest::SetOfStrings::const_iterator file; + for ( file = files.begin(); file != files.end(); ++file ) + { + /* get a curl handle */ + curl = curl_easy_init(); + if(curl) + { + // Using proxy + if ( this->FTPProxyType > 0 ) + { + curl_easy_setopt(curl, CURLOPT_PROXY, this->FTPProxy.c_str()); + switch (this->FTPProxyType) + { + case 2: + curl_easy_setopt(curl, CURLOPT_PROXYTYPE, CURLPROXY_SOCKS4); + break; + case 3: + curl_easy_setopt(curl, CURLOPT_PROXYTYPE, CURLPROXY_SOCKS5); + break; + default: + curl_easy_setopt(curl, CURLOPT_PROXYTYPE, CURLPROXY_HTTP); + } + } + + // enable uploading + ::curl_easy_setopt(curl, CURLOPT_UPLOAD, 1); + + // if there is little to no activity for too long stop submitting + ::curl_easy_setopt(curl, CURLOPT_LOW_SPEED_LIMIT, 1); + ::curl_easy_setopt(curl, CURLOPT_LOW_SPEED_TIME, + SUBMIT_TIMEOUT_IN_SECONDS_DEFAULT); + + ::curl_easy_setopt(curl, CURLOPT_UPLOAD, 1); + + cmStdString local_file = *file; + if ( !cmSystemTools::FileExists(local_file.c_str()) ) + { + local_file = localprefix + "/" + *file; + } + cmStdString upload_as + = url + "/" + remoteprefix + cmSystemTools::GetFilenameName(*file); + + struct stat st; + if ( ::stat(local_file.c_str(), &st) ) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, " Cannot find file: " + << local_file.c_str() << std::endl); + ::curl_easy_cleanup(curl); + ::curl_global_cleanup(); + return false; + } + + ftpfile = ::fopen(local_file.c_str(), "rb"); + *this->LogFile << "\tUpload file: " << local_file.c_str() << " to " + << upload_as.c_str() << std::endl; + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, " Upload file: " + << local_file.c_str() << " to " + << upload_as.c_str() << std::endl); + + ::curl_easy_setopt(curl, CURLOPT_VERBOSE, 1); + + // specify target + ::curl_easy_setopt(curl,CURLOPT_URL, upload_as.c_str()); + + // now specify which file to upload + ::curl_easy_setopt(curl, CURLOPT_INFILE, ftpfile); + + // and give the size of the upload (optional) + ::curl_easy_setopt(curl, CURLOPT_INFILESIZE, + static_cast<long>(st.st_size)); + + // and give curl the buffer for errors + ::curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, &error_buffer); + + // specify handler for output + ::curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, + cmCTestSubmitHandlerWriteMemoryCallback); + ::curl_easy_setopt(curl, CURLOPT_DEBUGFUNCTION, + cmCTestSubmitHandlerCurlDebugCallback); + + /* we pass our 'chunk' struct to the callback function */ + cmCTestSubmitHandlerVectorOfChar chunk; + cmCTestSubmitHandlerVectorOfChar chunkDebug; + ::curl_easy_setopt(curl, CURLOPT_FILE, (void *)&chunk); + ::curl_easy_setopt(curl, CURLOPT_DEBUGDATA, (void *)&chunkDebug); + + // Now run off and do what you've been told! + res = ::curl_easy_perform(curl); + + if ( chunk.size() > 0 ) + { + cmCTestLog(this->CTest, DEBUG, "CURL output: [" + << cmCTestLogWrite(&*chunk.begin(), chunk.size()) << "]" + << std::endl); + } + if ( chunkDebug.size() > 0 ) + { + cmCTestLog(this->CTest, DEBUG, "CURL debug output: [" + << cmCTestLogWrite(&*chunkDebug.begin(), chunkDebug.size()) << "]" + << std::endl); + } + + fclose(ftpfile); + if ( res ) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, + " Error when uploading file: " + << local_file.c_str() << std::endl); + cmCTestLog(this->CTest, ERROR_MESSAGE, " Error message was: " + << error_buffer << std::endl); + *this->LogFile << " Error when uploading file: " + << local_file.c_str() + << std::endl + << " Error message was: " + << error_buffer << std::endl + << " Curl output was: "; + // avoid dereference of empty vector + if(chunk.size()) + { + *this->LogFile << cmCTestLogWrite(&*chunk.begin(), chunk.size()); + cmCTestLog(this->CTest, ERROR_MESSAGE, "CURL output: [" + << cmCTestLogWrite(&*chunk.begin(), chunk.size()) << "]" + << std::endl); + } + *this->LogFile << std::endl; + ::curl_easy_cleanup(curl); + ::curl_global_cleanup(); + return false; + } + // always cleanup + ::curl_easy_cleanup(curl); + cmCTestLog(this->CTest, HANDLER_OUTPUT, " Uploaded: " + local_file + << std::endl); + } + } + ::curl_global_cleanup(); + return true; +} + +//---------------------------------------------------------------------------- +// Uploading files is simpler +bool cmCTestSubmitHandler::SubmitUsingHTTP(const cmStdString& localprefix, + const std::set<cmStdString>& files, + const cmStdString& remoteprefix, + const cmStdString& url) +{ + CURL *curl; + CURLcode res; + FILE* ftpfile; + char error_buffer[1024]; + + /* In windows, this will init the winsock stuff */ + ::curl_global_init(CURL_GLOBAL_ALL); + cmStdString dropMethod(this->CTest->GetCTestConfiguration("DropMethod")); + cmStdString curlopt(this->CTest->GetCTestConfiguration("CurlOptions")); + std::vector<std::string> args; + cmSystemTools::ExpandListArgument(curlopt.c_str(), args); + bool verifyPeerOff = false; + bool verifyHostOff = false; + for( std::vector<std::string>::iterator i = args.begin(); + i != args.end(); ++i) + { + if(*i == "CURLOPT_SSL_VERIFYPEER_OFF") + { + verifyPeerOff = true; + } + if(*i == "CURLOPT_SSL_VERIFYHOST_OFF") + { + verifyHostOff = true; + } + } + cmStdString::size_type kk; + cmCTest::SetOfStrings::const_iterator file; + for ( file = files.begin(); file != files.end(); ++file ) + { + /* get a curl handle */ + curl = curl_easy_init(); + if(curl) + { + if(verifyPeerOff) + { + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, + " Set CURLOPT_SSL_VERIFYPEER to off\n"); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0); + } + if(verifyHostOff) + { + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, + " Set CURLOPT_SSL_VERIFYHOST to off\n"); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0); + } + + // Using proxy + if ( this->HTTPProxyType > 0 ) + { + curl_easy_setopt(curl, CURLOPT_PROXY, this->HTTPProxy.c_str()); + switch (this->HTTPProxyType) + { + case 2: + curl_easy_setopt(curl, CURLOPT_PROXYTYPE, CURLPROXY_SOCKS4); + break; + case 3: + curl_easy_setopt(curl, CURLOPT_PROXYTYPE, CURLPROXY_SOCKS5); + break; + default: + curl_easy_setopt(curl, CURLOPT_PROXYTYPE, CURLPROXY_HTTP); + if (this->HTTPProxyAuth.size() > 0) + { + curl_easy_setopt(curl, CURLOPT_PROXYUSERPWD, + this->HTTPProxyAuth.c_str()); + } + } + } + if(this->CTest->ShouldUseHTTP10()) + { + curl_easy_setopt(curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0); + } + // enable HTTP ERROR parsing + curl_easy_setopt(curl, CURLOPT_FAILONERROR, 1); + /* enable uploading */ + curl_easy_setopt(curl, CURLOPT_UPLOAD, 1); + + // if there is little to no activity for too long stop submitting + ::curl_easy_setopt(curl, CURLOPT_LOW_SPEED_LIMIT, 1); + ::curl_easy_setopt(curl, CURLOPT_LOW_SPEED_TIME, + SUBMIT_TIMEOUT_IN_SECONDS_DEFAULT); + + /* HTTP PUT please */ + ::curl_easy_setopt(curl, CURLOPT_PUT, 1); + ::curl_easy_setopt(curl, CURLOPT_VERBOSE, 1); + + cmStdString local_file = *file; + if ( !cmSystemTools::FileExists(local_file.c_str()) ) + { + local_file = localprefix + "/" + *file; + } + cmStdString remote_file + = remoteprefix + cmSystemTools::GetFilenameName(*file); + + *this->LogFile << "\tUpload file: " << local_file.c_str() << " to " + << remote_file.c_str() << std::endl; + + cmStdString ofile = ""; + for ( kk = 0; kk < remote_file.size(); kk ++ ) + { + char c = remote_file[kk]; + char hexCh[4] = { 0, 0, 0, 0 }; + hexCh[0] = c; + switch ( c ) + { + case '+': + case '?': + case '/': + case '\\': + case '&': + case ' ': + case '=': + case '%': + sprintf(hexCh, "%%%02X", (int)c); + ofile.append(hexCh); + break; + default: + ofile.append(hexCh); + } + } + cmStdString upload_as + = url + ((url.find("?",0) == cmStdString::npos) ? "?" : "&") + + "FileName=" + ofile; + + upload_as += "&MD5="; + + if(cmSystemTools::IsOn(this->GetOption("InternalTest"))) + { + upload_as += "bad_md5sum"; + } + else + { + char md5[33]; + cmSystemTools::ComputeFileMD5(local_file.c_str(), md5); + md5[32] = 0; + upload_as += md5; + } + + struct stat st; + if ( ::stat(local_file.c_str(), &st) ) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, " Cannot find file: " + << local_file.c_str() << std::endl); + ::curl_easy_cleanup(curl); + ::curl_global_cleanup(); + return false; + } + + ftpfile = ::fopen(local_file.c_str(), "rb"); + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, " Upload file: " + << local_file.c_str() << " to " + << upload_as.c_str() << " Size: " << st.st_size << std::endl); + + // specify target + ::curl_easy_setopt(curl,CURLOPT_URL, upload_as.c_str()); + + // now specify which file to upload + ::curl_easy_setopt(curl, CURLOPT_INFILE, ftpfile); + + // and give the size of the upload (optional) + ::curl_easy_setopt(curl, CURLOPT_INFILESIZE, + static_cast<long>(st.st_size)); + + // and give curl the buffer for errors + ::curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, &error_buffer); + + // specify handler for output + ::curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, + cmCTestSubmitHandlerWriteMemoryCallback); + ::curl_easy_setopt(curl, CURLOPT_DEBUGFUNCTION, + cmCTestSubmitHandlerCurlDebugCallback); + + /* we pass our 'chunk' struct to the callback function */ + cmCTestSubmitHandlerVectorOfChar chunk; + cmCTestSubmitHandlerVectorOfChar chunkDebug; + ::curl_easy_setopt(curl, CURLOPT_FILE, (void *)&chunk); + ::curl_easy_setopt(curl, CURLOPT_DEBUGDATA, (void *)&chunkDebug); + + // Now run off and do what you've been told! + res = ::curl_easy_perform(curl); + + if(cmSystemTools::IsOn(this->GetOption("InternalTest")) && + cmSystemTools::VersionCompare(cmSystemTools::OP_LESS, + this->CTest->GetCDashVersion().c_str(), "1.7")) + { + // mock failure output for internal test case + std::string mock_output = "<cdash version=\"1.7.0\">\n" + " <status>ERROR</status>\n" + " <message>Checksum failed for file.</message>\n" + "</cdash>\n"; + chunk.clear(); + chunk.assign(mock_output.begin(), mock_output.end()); + } + + if ( chunk.size() > 0 ) + { + cmCTestLog(this->CTest, DEBUG, "CURL output: [" + << cmCTestLogWrite(&*chunk.begin(), chunk.size()) << "]" + << std::endl); + this->ParseResponse(chunk); + } + if ( chunkDebug.size() > 0 ) + { + cmCTestLog(this->CTest, DEBUG, "CURL debug output: [" + << cmCTestLogWrite(&*chunkDebug.begin(), chunkDebug.size()) << "]" + << std::endl); + } + + // If curl failed for any reason, or checksum fails, wait and retry + // + if(res != CURLE_OK || this->HasErrors) + { + std::string retryDelay = this->GetOption("RetryDelay") == NULL ? + "" : this->GetOption("RetryDelay"); + std::string retryCount = this->GetOption("RetryCount") == NULL ? + "" : this->GetOption("RetryCount"); + + int delay = retryDelay == "" ? atoi(this->CTest->GetCTestConfiguration( + "CTestSubmitRetryDelay").c_str()) : atoi(retryDelay.c_str()); + int count = retryCount == "" ? atoi(this->CTest->GetCTestConfiguration( + "CTestSubmitRetryCount").c_str()) : atoi(retryCount.c_str()); + + for(int i = 0; i < count; i++) + { + cmCTestLog(this->CTest, HANDLER_OUTPUT, + " Submit failed, waiting " << delay << " seconds...\n"); + + double stop = cmSystemTools::GetTime() + delay; + while(cmSystemTools::GetTime() < stop) + { + cmSystemTools::Delay(100); + } + + cmCTestLog(this->CTest, HANDLER_OUTPUT, + " Retry submission: Attempt " << (i + 1) << " of " + << count << std::endl); + + ::fclose(ftpfile); + ftpfile = ::fopen(local_file.c_str(), "rb"); + ::curl_easy_setopt(curl, CURLOPT_INFILE, ftpfile); + + chunk.clear(); + chunkDebug.clear(); + this->HasErrors = false; + + res = ::curl_easy_perform(curl); + + if ( chunk.size() > 0 ) + { + cmCTestLog(this->CTest, DEBUG, "CURL output: [" + << cmCTestLogWrite(&*chunk.begin(), chunk.size()) << "]" + << std::endl); + this->ParseResponse(chunk); + } + + if(res == CURLE_OK && !this->HasErrors) + { + break; + } + } + } + + fclose(ftpfile); + if ( res ) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, + " Error when uploading file: " + << local_file.c_str() << std::endl); + cmCTestLog(this->CTest, ERROR_MESSAGE, " Error message was: " + << error_buffer << std::endl); + *this->LogFile << " Error when uploading file: " + << local_file.c_str() + << std::endl + << " Error message was: " << error_buffer + << std::endl; + // avoid deref of begin for zero size array + if(chunk.size()) + { + *this->LogFile << " Curl output was: " + << cmCTestLogWrite(&*chunk.begin(), chunk.size()) + << std::endl; + cmCTestLog(this->CTest, ERROR_MESSAGE, "CURL output: [" + << cmCTestLogWrite(&*chunk.begin(), chunk.size()) << "]" + << std::endl); + } + ::curl_easy_cleanup(curl); + ::curl_global_cleanup(); + return false; + } + // always cleanup + ::curl_easy_cleanup(curl); + cmCTestLog(this->CTest, HANDLER_OUTPUT, " Uploaded: " + local_file + << std::endl); + } + } + ::curl_global_cleanup(); + return true; +} + +//---------------------------------------------------------------------------- +void cmCTestSubmitHandler +::ParseResponse(cmCTestSubmitHandlerVectorOfChar chunk) +{ + std::string output = ""; + output.append(chunk.begin(), chunk.end()); + + if(output.find("<cdash") != output.npos) + { + ResponseParser parser; + parser.Parse(output.c_str()); + + if(parser.Status != ResponseParser::STATUS_OK) + { + this->HasErrors = true; + cmCTestLog(this->CTest, HANDLER_OUTPUT, " Submission failed: " << + parser.Message << std::endl); + return; + } + } + output = cmSystemTools::UpperCase(output); + if(output.find("WARNING") != std::string::npos) + { + this->HasWarnings = true; + } + if(output.find("ERROR") != std::string::npos) + { + this->HasErrors = true; + } + + if(this->HasWarnings || this->HasErrors) + { + cmCTestLog(this->CTest, HANDLER_OUTPUT, " Server Response:\n" << + cmCTestLogWrite(&*chunk.begin(), chunk.size()) << "\n"); + } +} + +//---------------------------------------------------------------------------- +bool cmCTestSubmitHandler::TriggerUsingHTTP( + const std::set<cmStdString>& files, + const cmStdString& remoteprefix, + const cmStdString& url) +{ + CURL *curl; + char error_buffer[1024]; + /* In windows, this will init the winsock stuff */ + ::curl_global_init(CURL_GLOBAL_ALL); + + cmCTest::SetOfStrings::const_iterator file; + for ( file = files.begin(); file != files.end(); ++file ) + { + /* get a curl handle */ + curl = curl_easy_init(); + if(curl) + { + // Using proxy + if ( this->HTTPProxyType > 0 ) + { + curl_easy_setopt(curl, CURLOPT_PROXY, this->HTTPProxy.c_str()); + switch (this->HTTPProxyType) + { + case 2: + curl_easy_setopt(curl, CURLOPT_PROXYTYPE, CURLPROXY_SOCKS4); + break; + case 3: + curl_easy_setopt(curl, CURLOPT_PROXYTYPE, CURLPROXY_SOCKS5); + break; + default: + curl_easy_setopt(curl, CURLOPT_PROXYTYPE, CURLPROXY_HTTP); + if (this->HTTPProxyAuth.size() > 0) + { + curl_easy_setopt(curl, CURLOPT_PROXYUSERPWD, + this->HTTPProxyAuth.c_str()); + } + } + } + + ::curl_easy_setopt(curl, CURLOPT_VERBOSE, 1); + + // and give curl the buffer for errors + ::curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, &error_buffer); + + // specify handler for output + ::curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, + cmCTestSubmitHandlerWriteMemoryCallback); + ::curl_easy_setopt(curl, CURLOPT_DEBUGFUNCTION, + cmCTestSubmitHandlerCurlDebugCallback); + + /* we pass our 'chunk' struct to the callback function */ + cmCTestSubmitHandlerVectorOfChar chunk; + cmCTestSubmitHandlerVectorOfChar chunkDebug; + ::curl_easy_setopt(curl, CURLOPT_FILE, (void *)&chunk); + ::curl_easy_setopt(curl, CURLOPT_DEBUGDATA, (void *)&chunkDebug); + + cmStdString rfile + = remoteprefix + cmSystemTools::GetFilenameName(*file); + cmStdString ofile = ""; + cmStdString::iterator kk; + for ( kk = rfile.begin(); kk < rfile.end(); ++ kk) + { + char c = *kk; + char hexCh[4] = { 0, 0, 0, 0 }; + hexCh[0] = c; + switch ( c ) + { + case '+': + case '?': + case '/': + case '\\': + case '&': + case ' ': + case '=': + case '%': + sprintf(hexCh, "%%%02X", (int)c); + ofile.append(hexCh); + break; + default: + ofile.append(hexCh); + } + } + cmStdString turl + = url + ((url.find("?",0) == cmStdString::npos) ? "?" : "&") + + "xmlfile=" + ofile; + *this->LogFile << "Trigger url: " << turl.c_str() << std::endl; + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, " Trigger url: " + << turl.c_str() << std::endl); + curl_easy_setopt(curl, CURLOPT_HTTPAUTH, CURLAUTH_ANY); + curl_easy_setopt(curl, CURLOPT_URL, turl.c_str()); + if ( curl_easy_perform(curl) ) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, " Error when triggering: " + << turl.c_str() << std::endl); + cmCTestLog(this->CTest, ERROR_MESSAGE, " Error message was: " + << error_buffer << std::endl); + *this->LogFile << "\tTriggering failed with error: " << error_buffer + << std::endl + << " Error message was: " << error_buffer + << std::endl; + if(chunk.size()) + { + *this->LogFile + << " Curl output was: " + << cmCTestLogWrite(&*chunk.begin(), chunk.size()) << std::endl; + cmCTestLog(this->CTest, ERROR_MESSAGE, "CURL output: [" + << cmCTestLogWrite(&*chunk.begin(), chunk.size()) << "]" + << std::endl); + } + ::curl_easy_cleanup(curl); + ::curl_global_cleanup(); + return false; + } + + if ( chunk.size() > 0 ) + { + cmCTestLog(this->CTest, DEBUG, "CURL output: [" + << cmCTestLogWrite(&*chunk.begin(), chunk.size()) << "]" + << std::endl); + } + if ( chunkDebug.size() > 0 ) + { + cmCTestLog(this->CTest, DEBUG, "CURL debug output: [" + << cmCTestLogWrite(&*chunkDebug.begin(), chunkDebug.size()) + << "]" << std::endl); + } + + // always cleanup + ::curl_easy_cleanup(curl); + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, std::endl); + } + } + ::curl_global_cleanup(); + cmCTestLog(this->CTest, HANDLER_OUTPUT, " Dart server triggered..." + << std::endl); + return true; +} + +//---------------------------------------------------------------------------- +bool cmCTestSubmitHandler::SubmitUsingSCP( + const cmStdString& scp_command, + const cmStdString& localprefix, + const std::set<cmStdString>& files, + const cmStdString& remoteprefix, + const cmStdString& url) +{ + if ( !scp_command.size() || !localprefix.size() || + !files.size() || !remoteprefix.size() || !url.size() ) + { + return 0; + } + std::vector<const char*> argv; + argv.push_back(scp_command.c_str()); // Scp command + argv.push_back(scp_command.c_str()); // Dummy string for file + argv.push_back(scp_command.c_str()); // Dummy string for remote url + argv.push_back(0); + + cmsysProcess* cp = cmsysProcess_New(); + cmsysProcess_SetOption(cp, cmsysProcess_Option_HideWindow, 1); + //cmsysProcess_SetTimeout(cp, timeout); + + int problems = 0; + + cmCTest::SetOfStrings::const_iterator file; + for ( file = files.begin(); file != files.end(); ++file ) + { + int retVal; + + std::string lfname = localprefix; + cmSystemTools::ConvertToUnixSlashes(lfname); + lfname += "/" + *file; + lfname = cmSystemTools::ConvertToOutputPath(lfname.c_str()); + argv[1] = lfname.c_str(); + std::string rfname = url + "/" + remoteprefix + *file; + argv[2] = rfname.c_str(); + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, "Execute \"" << argv[0] + << "\" \"" << argv[1] << "\" \"" + << argv[2] << "\"" << std::endl); + *this->LogFile << "Execute \"" << argv[0] << "\" \"" << argv[1] << "\" \"" + << argv[2] << "\"" << std::endl; + + cmsysProcess_SetCommand(cp, &*argv.begin()); + cmsysProcess_Execute(cp); + char* data; + int length; + + while(cmsysProcess_WaitForData(cp, &data, &length, 0)) + { + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, + cmCTestLogWrite(data, length)); + } + + cmsysProcess_WaitForExit(cp, 0); + + int result = cmsysProcess_GetState(cp); + + if(result == cmsysProcess_State_Exited) + { + retVal = cmsysProcess_GetExitValue(cp); + if ( retVal != 0 ) + { + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, "\tSCP returned: " + << retVal << std::endl); + *this->LogFile << "\tSCP returned: " << retVal << std::endl; + problems ++; + } + } + else if(result == cmsysProcess_State_Exception) + { + retVal = cmsysProcess_GetExitException(cp); + cmCTestLog(this->CTest, ERROR_MESSAGE, "\tThere was an exception: " + << retVal << std::endl); + *this->LogFile << "\tThere was an exception: " << retVal << std::endl; + problems ++; + } + else if(result == cmsysProcess_State_Expired) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, "\tThere was a timeout" + << std::endl); + *this->LogFile << "\tThere was a timeout" << std::endl; + problems ++; + } + else if(result == cmsysProcess_State_Error) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, "\tError executing SCP: " + << cmsysProcess_GetErrorString(cp) << std::endl); + *this->LogFile << "\tError executing SCP: " + << cmsysProcess_GetErrorString(cp) << std::endl; + problems ++; + } + } + cmsysProcess_Delete(cp); + if ( problems ) + { + return false; + } + return true; +} + +//---------------------------------------------------------------------------- +bool cmCTestSubmitHandler::SubmitUsingCP( + const cmStdString& localprefix, + const std::set<cmStdString>& files, + const cmStdString& remoteprefix, + const cmStdString& destination) +{ + if ( !localprefix.size() || + !files.size() || !remoteprefix.size() || !destination.size() ) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, + "Missing arguments for submit via cp:\n" + << "\tlocalprefix: " << localprefix << "\n" + << "\tNumber of files: " << files.size() << "\n" + << "\tremoteprefix: " << remoteprefix << "\n" + << "\tdestination: " << destination << std::endl); + return 0; + } + cmCTest::SetOfStrings::const_iterator file; + bool problems = false; + for ( file = files.begin(); file != files.end(); ++file ) + { + std::string lfname = localprefix; + cmSystemTools::ConvertToUnixSlashes(lfname); + lfname += "/" + *file; + std::string rfname = destination + "/" + remoteprefix + *file; + cmSystemTools::CopyFileAlways(lfname.c_str(), rfname.c_str()); + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, " Copy file: " + << lfname.c_str() << " to " + << rfname.c_str() << std::endl); + } + std::string tagDoneFile = destination + "/" + remoteprefix + "DONE"; + cmSystemTools::Touch(tagDoneFile.c_str(), true); + if ( problems ) + { + return false; + } + return true; +} + + +//---------------------------------------------------------------------------- +#if defined(CTEST_USE_XMLRPC) +bool cmCTestSubmitHandler::SubmitUsingXMLRPC(const cmStdString& localprefix, + const std::set<cmStdString>& files, + const cmStdString& remoteprefix, + const cmStdString& url) +{ + xmlrpc_env env; + char ctestString[] = "CTest"; + std::string ctestVersionString = cmVersion::GetCMakeVersion(); + char* ctestVersion = const_cast<char*>(ctestVersionString.c_str()); + + cmStdString realURL = url + "/" + remoteprefix + "/Command/"; + + /* Start up our XML-RPC client library. */ + xmlrpc_client_init(XMLRPC_CLIENT_NO_FLAGS, ctestString, ctestVersion); + + /* Initialize our error-handling environment. */ + xmlrpc_env_init(&env); + + /* Call the famous server at UserLand. */ + cmCTestLog(this->CTest, HANDLER_OUTPUT, " Submitting to: " + << realURL.c_str() << " (" << remoteprefix.c_str() << ")" << std::endl); + cmCTest::SetOfStrings::const_iterator file; + for ( file = files.begin(); file != files.end(); ++file ) + { + xmlrpc_value *result; + + cmStdString local_file = *file; + if ( !cmSystemTools::FileExists(local_file.c_str()) ) + { + local_file = localprefix + "/" + *file; + } + cmCTestLog(this->CTest, HANDLER_OUTPUT, " Submit file: " + << local_file.c_str() << std::endl); + struct stat st; + if ( ::stat(local_file.c_str(), &st) ) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, " Cannot find file: " + << local_file.c_str() << std::endl); + return false; + } + + // off_t can be bigger than size_t. fread takes size_t. + // make sure the file is not too big. + if(static_cast<off_t>(static_cast<size_t>(st.st_size)) != + static_cast<off_t>(st.st_size)) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, " File too big: " + << local_file.c_str() << std::endl); + return false; + } + size_t fileSize = static_cast<size_t>(st.st_size); + FILE* fp = fopen(local_file.c_str(), "rb"); + if ( !fp ) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, " Cannot open file: " + << local_file.c_str() << std::endl); + return false; + } + + unsigned char *fileBuffer = new unsigned char[fileSize]; + if ( fread(fileBuffer, 1, fileSize, fp) != fileSize ) + { + delete [] fileBuffer; + fclose(fp); + cmCTestLog(this->CTest, ERROR_MESSAGE, " Cannot read file: " + << local_file.c_str() << std::endl); + return false; + } + fclose(fp); + + char remoteCommand[] = "Submit.put"; + char* pRealURL = const_cast<char*>(realURL.c_str()); + result = xmlrpc_client_call(&env, pRealURL, remoteCommand, + "(6)", fileBuffer, (xmlrpc_int32)fileSize ); + + delete [] fileBuffer; + + if ( env.fault_occurred ) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, " Submission problem: " + << env.fault_string << " (" << env.fault_code << ")" << std::endl); + xmlrpc_env_clean(&env); + xmlrpc_client_cleanup(); + return false; + } + + /* Dispose of our result value. */ + xmlrpc_DECREF(result); + } + + /* Clean up our error-handling environment. */ + xmlrpc_env_clean(&env); + + /* Shutdown our XML-RPC client library. */ + xmlrpc_client_cleanup(); + return true; +} +#else +bool cmCTestSubmitHandler::SubmitUsingXMLRPC(cmStdString const&, + std::set<cmStdString> const&, + cmStdString const&, + cmStdString const&) +{ + return false; +} +#endif + +//---------------------------------------------------------------------------- +int cmCTestSubmitHandler::ProcessHandler() +{ + std::string iscdash = this->CTest->GetCTestConfiguration("IsCDash"); + // cdash does not need to trigger so just return true + if(iscdash.size()) + { + this->CDash = true; + } + + const std::string &buildDirectory + = this->CTest->GetCTestConfiguration("BuildDirectory"); + if ( buildDirectory.size() == 0 ) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, + "Cannot find BuildDirectory key in the DartConfiguration.tcl" + << std::endl); + return -1; + } + + if ( getenv("HTTP_PROXY") ) + { + this->HTTPProxyType = 1; + this->HTTPProxy = getenv("HTTP_PROXY"); + if ( getenv("HTTP_PROXY_PORT") ) + { + this->HTTPProxy += ":"; + this->HTTPProxy += getenv("HTTP_PROXY_PORT"); + } + if ( getenv("HTTP_PROXY_TYPE") ) + { + cmStdString type = getenv("HTTP_PROXY_TYPE"); + // HTTP/SOCKS4/SOCKS5 + if ( type == "HTTP" ) + { + this->HTTPProxyType = 1; + } + else if ( type == "SOCKS4" ) + { + this->HTTPProxyType = 2; + } + else if ( type == "SOCKS5" ) + { + this->HTTPProxyType = 3; + } + } + if ( getenv("HTTP_PROXY_USER") ) + { + this->HTTPProxyAuth = getenv("HTTP_PROXY_USER"); + } + if ( getenv("HTTP_PROXY_PASSWD") ) + { + this->HTTPProxyAuth += ":"; + this->HTTPProxyAuth += getenv("HTTP_PROXY_PASSWD"); + } + } + + if ( getenv("FTP_PROXY") ) + { + this->FTPProxyType = 1; + this->FTPProxy = getenv("FTP_PROXY"); + if ( getenv("FTP_PROXY_PORT") ) + { + this->FTPProxy += ":"; + this->FTPProxy += getenv("FTP_PROXY_PORT"); + } + if ( getenv("FTP_PROXY_TYPE") ) + { + cmStdString type = getenv("FTP_PROXY_TYPE"); + // HTTP/SOCKS4/SOCKS5 + if ( type == "HTTP" ) + { + this->FTPProxyType = 1; + } + else if ( type == "SOCKS4" ) + { + this->FTPProxyType = 2; + } + else if ( type == "SOCKS5" ) + { + this->FTPProxyType = 3; + } + } + } + + if ( this->HTTPProxy.size() > 0 ) + { + cmCTestLog(this->CTest, HANDLER_OUTPUT, " Use HTTP Proxy: " + << this->HTTPProxy << std::endl); + } + if ( this->FTPProxy.size() > 0 ) + { + cmCTestLog(this->CTest, HANDLER_OUTPUT, " Use FTP Proxy: " + << this->FTPProxy << std::endl); + } + cmGeneratedFileStream ofs; + this->StartLogFile("Submit", ofs); + + cmCTest::SetOfStrings files; + std::string prefix = this->GetSubmitResultsPrefix(); + + if (!this->Files.empty()) + { + // Submit the explicitly selected files: + // + cmCTest::SetOfStrings::const_iterator it; + for (it = this->Files.begin(); it != this->Files.end(); ++it) + { + files.insert(*it); + } + } + + // Add to the list of files to submit from any selected, existing parts: + // + + // TODO: + // Check if test is enabled + + this->CTest->AddIfExists(cmCTest::PartUpdate, "Update.xml"); + this->CTest->AddIfExists(cmCTest::PartConfigure, "Configure.xml"); + this->CTest->AddIfExists(cmCTest::PartBuild, "Build.xml"); + this->CTest->AddIfExists(cmCTest::PartTest, "Test.xml"); + if(this->CTest->AddIfExists(cmCTest::PartCoverage, "Coverage.xml")) + { + cmCTest::VectorOfStrings gfiles; + std::string gpath + = buildDirectory + "/Testing/" + this->CTest->GetCurrentTag(); + std::string::size_type glen = gpath.size() + 1; + gpath = gpath + "/CoverageLog*"; + cmCTestLog(this->CTest, DEBUG, "Globbing for: " << gpath.c_str() + << std::endl); + if ( cmSystemTools::SimpleGlob(gpath, gfiles, 1) ) + { + size_t cc; + for ( cc = 0; cc < gfiles.size(); cc ++ ) + { + gfiles[cc] = gfiles[cc].substr(glen); + cmCTestLog(this->CTest, DEBUG, "Glob file: " << gfiles[cc].c_str() + << std::endl); + this->CTest->AddSubmitFile(cmCTest::PartCoverage, gfiles[cc].c_str()); + } + } + else + { + cmCTestLog(this->CTest, ERROR_MESSAGE, "Problem globbing" << std::endl); + } + } + this->CTest->AddIfExists(cmCTest::PartMemCheck, "DynamicAnalysis.xml"); + this->CTest->AddIfExists(cmCTest::PartMemCheck, "Purify.xml"); + this->CTest->AddIfExists(cmCTest::PartNotes, "Notes.xml"); + this->CTest->AddIfExists(cmCTest::PartUpload, "Upload.xml"); + + // Query parts for files to submit. + for(cmCTest::Part p = cmCTest::PartStart; + p != cmCTest::PartCount; p = cmCTest::Part(p+1)) + { + // Skip parts we are not submitting. + if(!this->SubmitPart[p]) + { + continue; + } + + // Submit files from this part. + std::vector<std::string> const& pfiles = this->CTest->GetSubmitFiles(p); + for(std::vector<std::string>::const_iterator pi = pfiles.begin(); + pi != pfiles.end(); ++pi) + { + files.insert(*pi); + } + } + + if ( ofs ) + { + ofs << "Upload files:" << std::endl; + int cnt = 0; + cmCTest::SetOfStrings::iterator it; + for ( it = files.begin(); it != files.end(); ++ it ) + { + ofs << cnt << "\t" << it->c_str() << std::endl; + cnt ++; + } + } + cmCTestLog(this->CTest, HANDLER_OUTPUT, "Submit files (using " + << this->CTest->GetCTestConfiguration("DropMethod") << ")" + << std::endl); + const char* specificTrack = this->CTest->GetSpecificTrack(); + if ( specificTrack ) + { + cmCTestLog(this->CTest, HANDLER_OUTPUT, " Send to track: " + << specificTrack << std::endl); + } + this->SetLogFile(&ofs); + + cmStdString dropMethod(this->CTest->GetCTestConfiguration("DropMethod")); + + if ( dropMethod == "" || dropMethod == "ftp" ) + { + ofs << "Using drop method: FTP" << std::endl; + cmCTestLog(this->CTest, HANDLER_OUTPUT, " Using FTP submit method" + << std::endl + << " Drop site: ftp://"); + std::string url = "ftp://"; + url += cmCTest::MakeURLSafe( + this->CTest->GetCTestConfiguration("DropSiteUser")) + ":" + + cmCTest::MakeURLSafe(this->CTest->GetCTestConfiguration( + "DropSitePassword")) + "@" + + this->CTest->GetCTestConfiguration("DropSite") + + cmCTest::MakeURLSafe( + this->CTest->GetCTestConfiguration("DropLocation")); + if ( this->CTest->GetCTestConfiguration("DropSiteUser").size() > 0 ) + { + cmCTestLog(this->CTest, HANDLER_OUTPUT, + this->CTest->GetCTestConfiguration( + "DropSiteUser").c_str()); + if ( this->CTest->GetCTestConfiguration("DropSitePassword").size() > 0 ) + { + cmCTestLog(this->CTest, HANDLER_OUTPUT, ":******"); + } + cmCTestLog(this->CTest, HANDLER_OUTPUT, "@"); + } + cmCTestLog(this->CTest, HANDLER_OUTPUT, + this->CTest->GetCTestConfiguration("DropSite") + << this->CTest->GetCTestConfiguration("DropLocation") << std::endl); + if ( !this->SubmitUsingFTP(buildDirectory + "/Testing/" + + this->CTest->GetCurrentTag(), + files, prefix, url) ) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, + " Problems when submitting via FTP" + << std::endl); + ofs << " Problems when submitting via FTP" << std::endl; + return -1; + } + if(!this->CDash) + { + cmCTestLog(this->CTest, HANDLER_OUTPUT, " Using HTTP trigger method" + << std::endl + << " Trigger site: " + << this->CTest->GetCTestConfiguration("TriggerSite") + << std::endl); + if ( !this-> + TriggerUsingHTTP(files, prefix, + this->CTest->GetCTestConfiguration("TriggerSite"))) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, + " Problems when triggering via HTTP" << std::endl); + ofs << " Problems when triggering via HTTP" << std::endl; + return -1; + } + cmCTestLog(this->CTest, HANDLER_OUTPUT, " Submission successful" + << std::endl); + ofs << " Submission successful" << std::endl; + return 0; + } + } + else if ( dropMethod == "http" || dropMethod == "https" ) + { + std::string url = dropMethod; + url += "://"; + ofs << "Using drop method: " << dropMethod << std::endl; + cmCTestLog(this->CTest, HANDLER_OUTPUT, " Using HTTP submit method" + << std::endl + << " Drop site:" << url); + if ( this->CTest->GetCTestConfiguration("DropSiteUser").size() > 0 ) + { + url += this->CTest->GetCTestConfiguration("DropSiteUser"); + cmCTestLog(this->CTest, HANDLER_OUTPUT, + this->CTest->GetCTestConfiguration("DropSiteUser").c_str()); + if ( this->CTest->GetCTestConfiguration("DropSitePassword").size() > 0 ) + { + url += ":" + this->CTest->GetCTestConfiguration("DropSitePassword"); + cmCTestLog(this->CTest, HANDLER_OUTPUT, ":******"); + } + url += "@"; + cmCTestLog(this->CTest, HANDLER_OUTPUT, "@"); + } + url += this->CTest->GetCTestConfiguration("DropSite") + + this->CTest->GetCTestConfiguration("DropLocation"); + cmCTestLog(this->CTest, HANDLER_OUTPUT, + this->CTest->GetCTestConfiguration("DropSite") + << this->CTest->GetCTestConfiguration("DropLocation") << std::endl); + if ( !this->SubmitUsingHTTP(buildDirectory + "/Testing/" + + this->CTest->GetCurrentTag(), files, prefix, url) ) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, + " Problems when submitting via HTTP" << std::endl); + ofs << " Problems when submitting via HTTP" << std::endl; + return -1; + } + if(!this->CDash) + { + cmCTestLog(this->CTest, HANDLER_OUTPUT, " Using HTTP trigger method" + << std::endl + << " Trigger site: " + << this->CTest->GetCTestConfiguration("TriggerSite") + << std::endl); + if ( !this-> + TriggerUsingHTTP(files, prefix, + this->CTest->GetCTestConfiguration("TriggerSite"))) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, + " Problems when triggering via HTTP" << std::endl); + ofs << " Problems when triggering via HTTP" << std::endl; + return -1; + } + } + if(this->HasErrors) + { + cmCTestLog(this->CTest, HANDLER_OUTPUT, " Errors occurred during " + "submission." << std::endl); + ofs << " Errors occurred during submission. " << std::endl; + } + else + { + cmCTestLog(this->CTest, HANDLER_OUTPUT, " Submission successful" << + (this->HasWarnings ? ", with warnings." : "") << std::endl); + ofs << " Submission successful" << + (this->HasWarnings ? ", with warnings." : "") << std::endl; + } + + return 0; + } + else if ( dropMethod == "xmlrpc" ) + { +#if defined(CTEST_USE_XMLRPC) + ofs << "Using drop method: XML-RPC" << std::endl; + cmCTestLog(this->CTest, HANDLER_OUTPUT, " Using XML-RPC submit method" + << std::endl); + std::string url = this->CTest->GetCTestConfiguration("DropSite"); + prefix = this->CTest->GetCTestConfiguration("DropLocation"); + if ( !this->SubmitUsingXMLRPC(buildDirectory + "/Testing/" + + this->CTest->GetCurrentTag(), files, prefix, url) ) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, + " Problems when submitting via XML-RPC" << std::endl); + ofs << " Problems when submitting via XML-RPC" << std::endl; + return -1; + } + cmCTestLog(this->CTest, HANDLER_OUTPUT, " Submission successful" + << std::endl); + ofs << " Submission successful" << std::endl; + return 0; +#else + cmCTestLog(this->CTest, ERROR_MESSAGE, + " Submission method \"xmlrpc\" not compiled into CTest!" + << std::endl); + return -1; +#endif + } + else if ( dropMethod == "scp" ) + { + std::string url; + std::string oldWorkingDirectory; + if ( this->CTest->GetCTestConfiguration("DropSiteUser").size() > 0 ) + { + url += this->CTest->GetCTestConfiguration("DropSiteUser") + "@"; + } + url += this->CTest->GetCTestConfiguration("DropSite") + ":" + + this->CTest->GetCTestConfiguration("DropLocation"); + + // change to the build directory so that we can uses a relative path + // on windows since scp dosn't support "c:" a drive in the path + oldWorkingDirectory = cmSystemTools::GetCurrentWorkingDirectory(); + cmSystemTools::ChangeDirectory(buildDirectory.c_str()); + + if ( !this->SubmitUsingSCP( + this->CTest->GetCTestConfiguration("ScpCommand"), + "Testing/"+this->CTest->GetCurrentTag(), files, prefix, url) ) + { + cmSystemTools::ChangeDirectory(oldWorkingDirectory.c_str()); + cmCTestLog(this->CTest, ERROR_MESSAGE, + " Problems when submitting via SCP" + << std::endl); + ofs << " Problems when submitting via SCP" << std::endl; + return -1; + } + cmSystemTools::ChangeDirectory(oldWorkingDirectory.c_str()); + cmCTestLog(this->CTest, HANDLER_OUTPUT, " Submission successful" + << std::endl); + ofs << " Submission successful" << std::endl; + return 0; + } + else if ( dropMethod == "cp" ) + { + std::string location + = this->CTest->GetCTestConfiguration("DropLocation"); + + + // change to the build directory so that we can uses a relative path + // on windows since scp dosn't support "c:" a drive in the path + std::string + oldWorkingDirectory = cmSystemTools::GetCurrentWorkingDirectory(); + cmSystemTools::ChangeDirectory(buildDirectory.c_str()); + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, " Change directory: " + << buildDirectory.c_str() << std::endl); + + if ( !this->SubmitUsingCP( + "Testing/"+this->CTest->GetCurrentTag(), + files, + prefix, + location) ) + { + cmSystemTools::ChangeDirectory(oldWorkingDirectory.c_str()); + cmCTestLog(this->CTest, ERROR_MESSAGE, + " Problems when submitting via CP" + << std::endl); + ofs << " Problems when submitting via cp" << std::endl; + return -1; + } + cmSystemTools::ChangeDirectory(oldWorkingDirectory.c_str()); + cmCTestLog(this->CTest, HANDLER_OUTPUT, " Submission successful" + << std::endl); + ofs << " Submission successful" << std::endl; + return 0; + } + + cmCTestLog(this->CTest, ERROR_MESSAGE, " Unknown submission method: \"" + << dropMethod << "\"" << std::endl); + return -1; +} + +//---------------------------------------------------------------------------- +std::string cmCTestSubmitHandler::GetSubmitResultsPrefix() +{ + std::string name = this->CTest->GetCTestConfiguration("Site") + + "___" + this->CTest->GetCTestConfiguration("BuildName") + + "___" + this->CTest->GetCurrentTag() + "-" + + this->CTest->GetTestModelString() + "___XML___"; + return name; +} + +//---------------------------------------------------------------------------- +void cmCTestSubmitHandler::SelectParts(std::set<cmCTest::Part> const& parts) +{ + // Check whether each part is selected. + for(cmCTest::Part p = cmCTest::PartStart; + p != cmCTest::PartCount; p = cmCTest::Part(p+1)) + { + this->SubmitPart[p] = + (std::set<cmCTest::Part>::const_iterator(parts.find(p)) != parts.end()); + } +} + +//---------------------------------------------------------------------------- +void cmCTestSubmitHandler::SelectFiles(cmCTest::SetOfStrings const& files) +{ + cmCTest::SetOfStrings::const_iterator it; + for (it = files.begin(); it != files.end(); ++it) + { + this->Files.insert(*it); + } +} diff --git a/Source/CTest/cmCTestSubmitHandler.h b/Source/CTest/cmCTestSubmitHandler.h new file mode 100644 index 000000000..14eac80b5 --- /dev/null +++ b/Source/CTest/cmCTestSubmitHandler.h @@ -0,0 +1,98 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2009 Kitware, Inc., Insight Software Consortium + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ +#ifndef cmCTestSubmitHandler_h +#define cmCTestSubmitHandler_h + +#include "cmCTestGenericHandler.h" + +/** \class cmCTestSubmitHandler + * \brief Helper class for CTest + * + * Submit testing results + * + */ +class cmCTestSubmitHandler : public cmCTestGenericHandler +{ +public: + cmTypeMacro(cmCTestSubmitHandler, cmCTestGenericHandler); + + cmCTestSubmitHandler(); + ~cmCTestSubmitHandler() { this->LogFile = 0; } + + /* + * The main entry point for this class + */ + int ProcessHandler(); + + void Initialize(); + + /** Specify a set of parts (by name) to submit. */ + void SelectParts(std::set<cmCTest::Part> const& parts); + + /** Specify a set of files to submit. */ + void SelectFiles(cmCTest::SetOfStrings const& files); + +private: + void SetLogFile(std::ostream* ost) { this->LogFile = ost; } + + /** + * Submit file using various ways + */ + bool SubmitUsingFTP(const cmStdString& localprefix, + const std::set<cmStdString>& files, + const cmStdString& remoteprefix, + const cmStdString& url); + bool SubmitUsingHTTP(const cmStdString& localprefix, + const std::set<cmStdString>& files, + const cmStdString& remoteprefix, + const cmStdString& url); + bool SubmitUsingSCP(const cmStdString& scp_command, + const cmStdString& localprefix, + const std::set<cmStdString>& files, + const cmStdString& remoteprefix, + const cmStdString& url); + + bool SubmitUsingCP( const cmStdString& localprefix, + const std::set<cmStdString>& files, + const cmStdString& remoteprefix, + const cmStdString& url); + + bool TriggerUsingHTTP(const std::set<cmStdString>& files, + const cmStdString& remoteprefix, + const cmStdString& url); + + bool SubmitUsingXMLRPC(const cmStdString& localprefix, + const std::set<cmStdString>& files, + const cmStdString& remoteprefix, + const cmStdString& url); + + typedef std::vector<char> cmCTestSubmitHandlerVectorOfChar; + + void ParseResponse(cmCTestSubmitHandlerVectorOfChar chunk); + + std::string GetSubmitResultsPrefix(); + + class ResponseParser; + cmStdString HTTPProxy; + int HTTPProxyType; + cmStdString HTTPProxyAuth; + cmStdString FTPProxy; + int FTPProxyType; + std::ostream* LogFile; + bool SubmitPart[cmCTest::PartCount]; + bool CDash; + bool HasWarnings; + bool HasErrors; + cmCTest::SetOfStrings Files; +}; + +#endif diff --git a/Source/CTest/cmCTestTestCommand.cxx b/Source/CTest/cmCTestTestCommand.cxx new file mode 100644 index 000000000..5aee035c7 --- /dev/null +++ b/Source/CTest/cmCTestTestCommand.cxx @@ -0,0 +1,112 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2009 Kitware, Inc., Insight Software Consortium + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ +#include "cmCTestTestCommand.h" + +#include "cmCTest.h" +#include "cmCTestGenericHandler.h" + +cmCTestTestCommand::cmCTestTestCommand() +{ + this->Arguments[ctt_START] = "START"; + this->Arguments[ctt_END] = "END"; + this->Arguments[ctt_STRIDE] = "STRIDE"; + this->Arguments[ctt_EXCLUDE] = "EXCLUDE"; + this->Arguments[ctt_INCLUDE] = "INCLUDE"; + this->Arguments[ctt_EXCLUDE_LABEL] = "EXCLUDE_LABEL"; + this->Arguments[ctt_INCLUDE_LABEL] = "INCLUDE_LABEL"; + this->Arguments[ctt_PARALLEL_LEVEL] = "PARALLEL_LEVEL"; + this->Arguments[ctt_SCHEDULE_RANDOM] = "SCHEDULE_RANDOM"; + this->Arguments[ctt_STOP_TIME] = "STOP_TIME"; + this->Arguments[ctt_LAST] = 0; + this->Last = ctt_LAST; +} + +cmCTestGenericHandler* cmCTestTestCommand::InitializeHandler() +{ + const char* ctestTimeout = + this->Makefile->GetDefinition("CTEST_TEST_TIMEOUT"); + + double timeout = this->CTest->GetTimeOut(); + if ( ctestTimeout ) + { + timeout = atof(ctestTimeout); + } + else + { + if ( timeout <= 0 ) + { + // By default use timeout of 10 minutes + timeout = 600; + } + } + this->CTest->SetTimeOut(timeout); + cmCTestGenericHandler* handler = this->InitializeActualHandler(); + if ( this->Values[ctt_START] || this->Values[ctt_END] || + this->Values[ctt_STRIDE] ) + { + cmOStringStream testsToRunString; + if ( this->Values[ctt_START] ) + { + testsToRunString << this->Values[ctt_START]; + } + testsToRunString << ","; + if ( this->Values[ctt_END] ) + { + testsToRunString << this->Values[ctt_END]; + } + testsToRunString << ","; + if ( this->Values[ctt_STRIDE] ) + { + testsToRunString << this->Values[ctt_STRIDE]; + } + handler->SetOption("TestsToRunInformation", + testsToRunString.str().c_str()); + } + if(this->Values[ctt_EXCLUDE]) + { + handler->SetOption("ExcludeRegularExpression", this->Values[ctt_EXCLUDE]); + } + if(this->Values[ctt_INCLUDE]) + { + handler->SetOption("IncludeRegularExpression", this->Values[ctt_INCLUDE]); + } + if(this->Values[ctt_EXCLUDE_LABEL]) + { + handler->SetOption("ExcludeLabelRegularExpression", + this->Values[ctt_EXCLUDE_LABEL]); + } + if(this->Values[ctt_INCLUDE_LABEL]) + { + handler->SetOption("LabelRegularExpression", + this->Values[ctt_INCLUDE_LABEL]); + } + if(this->Values[ctt_PARALLEL_LEVEL]) + { + handler->SetOption("ParallelLevel", + this->Values[ctt_PARALLEL_LEVEL]); + } + if(this->Values[ctt_SCHEDULE_RANDOM]) + { + handler->SetOption("ScheduleRandom", + this->Values[ctt_SCHEDULE_RANDOM]); + } + if(this->Values[ctt_STOP_TIME]) + { + this->CTest->SetStopTime(this->Values[ctt_STOP_TIME]); + } + return handler; +} + +cmCTestGenericHandler* cmCTestTestCommand::InitializeActualHandler() +{ + return this->CTest->GetInitializedHandler("test"); +} diff --git a/Source/CTest/cmCTestTestCommand.h b/Source/CTest/cmCTestTestCommand.h new file mode 100644 index 000000000..d184ff2a0 --- /dev/null +++ b/Source/CTest/cmCTestTestCommand.h @@ -0,0 +1,107 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2009 Kitware, Inc., Insight Software Consortium + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ +#ifndef cmCTestTestCommand_h +#define cmCTestTestCommand_h + +#include "cmCTestHandlerCommand.h" + +/** \class cmCTestTest + * \brief Run a ctest script + * + * cmCTestTestCommand defineds the command to test the project. + */ +class cmCTestTestCommand : public cmCTestHandlerCommand +{ +public: + + cmCTestTestCommand(); + + /** + * This is a virtual constructor for the command. + */ + virtual cmCommand* Clone() + { + cmCTestTestCommand* ni = new cmCTestTestCommand; + ni->CTest = this->CTest; + ni->CTestScriptHandler = this->CTestScriptHandler; + return ni; + } + + /** + * The name of the command as specified in CMakeList.txt. + */ + virtual const char* GetName() const { return "ctest_test";} + + /** + * Succinct documentation. + */ + virtual const char* GetTerseDocumentation() const + { + return "Run tests in the project build tree."; + } + + /** + * More documentation. + */ + virtual const char* GetFullDocumentation() const + { + return + " ctest_test([BUILD build_dir] [APPEND]\n" + " [START start number] [END end number]\n" + " [STRIDE stride number] [EXCLUDE exclude regex ]\n" + " [INCLUDE include regex] [RETURN_VALUE res] \n" + " [EXCLUDE_LABEL exclude regex] \n" + " [INCLUDE_LABEL label regex] \n" + " [PARALLEL_LEVEL level] \n" + " [SCHEDULE_RANDOM on] \n" + " [STOP_TIME time of day]) \n" + "Tests the given build directory and stores results in Test.xml. The " + "second argument is a variable that will hold value. Optionally, " + "you can specify the starting test number START, the ending test number " + "END, the number of tests to skip between each test STRIDE, a regular " + "expression for tests to run INCLUDE, or a regular expression for tests " + "to not run EXCLUDE. EXCLUDE_LABEL and INCLUDE_LABEL are regular " + "expression for test to be included or excluded by the test " + "property LABEL. PARALLEL_LEVEL should be set to a positive number " + "representing the number of tests to be run in parallel. " + "SCHEDULE_RANDOM will launch tests in a random order, and is " + "typically used to detect implicit test dependencies. STOP_TIME is the " + "time of day at which the tests should all stop running." + "\n" + CTEST_COMMAND_APPEND_OPTION_DOCS; + } + + cmTypeMacro(cmCTestTestCommand, cmCTestHandlerCommand); + +protected: + virtual cmCTestGenericHandler* InitializeActualHandler(); + cmCTestGenericHandler* InitializeHandler(); + + enum { + ctt_BUILD = ct_LAST, + ctt_RETURN_VALUE, + ctt_START, + ctt_END, + ctt_STRIDE, + ctt_EXCLUDE, + ctt_INCLUDE, + ctt_EXCLUDE_LABEL, + ctt_INCLUDE_LABEL, + ctt_PARALLEL_LEVEL, + ctt_SCHEDULE_RANDOM, + ctt_STOP_TIME, + ctt_LAST + }; +}; + + +#endif diff --git a/Source/CTest/cmCTestTestHandler.cxx b/Source/CTest/cmCTestTestHandler.cxx new file mode 100644 index 000000000..ead449edd --- /dev/null +++ b/Source/CTest/cmCTestTestHandler.cxx @@ -0,0 +1,2267 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2009 Kitware, Inc., Insight Software Consortium + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ + +#include "cmCTestTestHandler.h" +#include "cmCTestMultiProcessHandler.h" +#include "cmCTestBatchTestHandler.h" +#include "cmCTest.h" +#include "cmCTestRunTest.h" +#include "cmake.h" +#include "cmGeneratedFileStream.h" +#include <cmsys/Process.h> +#include <cmsys/RegularExpression.hxx> +#include <cmsys/Base64.h> +#include "cmMakefile.h" +#include "cmGlobalGenerator.h" +#include "cmLocalGenerator.h" +#include "cmCommand.h" +#include "cmSystemTools.h" +#include "cmXMLSafe.h" +#include "cm_utf8.h" + +#include <stdlib.h> +#include <math.h> +#include <float.h> + +#include <memory> // auto_ptr +#include <set> + +//---------------------------------------------------------------------- +class cmCTestSubdirCommand : public cmCommand +{ +public: + /** + * This is a virtual constructor for the command. + */ + virtual cmCommand* Clone() + { + cmCTestSubdirCommand* c = new cmCTestSubdirCommand; + c->TestHandler = this->TestHandler; + return c; + } + + /** + * This is called when the command is first encountered in + * the CMakeLists.txt file. + */ + virtual bool InitialPass(std::vector<std::string> const& args, + cmExecutionStatus &); + + /** + * The name of the command as specified in CMakeList.txt. + */ + virtual const char* GetName() const { return "subdirs";} + + // Unused methods + virtual const char* GetTerseDocumentation() const { return ""; } + virtual const char* GetFullDocumentation() const { return ""; } + + cmTypeMacro(cmCTestSubdirCommand, cmCommand); + + cmCTestTestHandler* TestHandler; +}; + +//---------------------------------------------------------------------- +bool cmCTestSubdirCommand +::InitialPass(std::vector<std::string> const& args, cmExecutionStatus &) +{ + if(args.size() < 1 ) + { + this->SetError("called with incorrect number of arguments"); + return false; + } + std::vector<std::string>::const_iterator it; + std::string cwd = cmSystemTools::GetCurrentWorkingDirectory(); + for ( it = args.begin(); it != args.end(); ++ it ) + { + cmSystemTools::ChangeDirectory(cwd.c_str()); + std::string fname; + + if(cmSystemTools::FileIsFullPath(it->c_str())) + { + fname = *it; + } + else + { + fname = cwd; + fname += "/"; + fname += *it; + } + + if ( !cmSystemTools::FileIsDirectory(fname.c_str()) ) + { + // No subdirectory? So what... + continue; + } + cmSystemTools::ChangeDirectory(fname.c_str()); + const char* testFilename; + if( cmSystemTools::FileExists("CTestTestfile.cmake") ) + { + // does the CTestTestfile.cmake exist ? + testFilename = "CTestTestfile.cmake"; + } + else if( cmSystemTools::FileExists("DartTestfile.txt") ) + { + // does the DartTestfile.txt exist ? + testFilename = "DartTestfile.txt"; + } + else + { + // No CTestTestfile? Who cares... + cmSystemTools::ChangeDirectory(cwd.c_str()); + continue; + } + fname += "/"; + fname += testFilename; + bool readit = + this->Makefile->ReadListFile(this->Makefile->GetCurrentListFile(), + fname.c_str()); + cmSystemTools::ChangeDirectory(cwd.c_str()); + if(!readit) + { + std::string m = "Could not find include file: "; + m += fname; + this->SetError(m.c_str()); + return false; + } + } + return true; +} + +//---------------------------------------------------------------------- +class cmCTestAddSubdirectoryCommand : public cmCommand +{ +public: + /** + * This is a virtual constructor for the command. + */ + virtual cmCommand* Clone() + { + cmCTestAddSubdirectoryCommand* c = new cmCTestAddSubdirectoryCommand; + c->TestHandler = this->TestHandler; + return c; + } + + /** + * This is called when the command is first encountered in + * the CMakeLists.txt file. + */ + virtual bool InitialPass(std::vector<std::string> const& args, + cmExecutionStatus &); + + /** + * The name of the command as specified in CMakeList.txt. + */ + virtual const char* GetName() const { return "add_subdirectory";} + + // Unused methods + virtual const char* GetTerseDocumentation() const { return ""; } + virtual const char* GetFullDocumentation() const { return ""; } + + cmTypeMacro(cmCTestAddSubdirectoryCommand, cmCommand); + + cmCTestTestHandler* TestHandler; +}; + +//---------------------------------------------------------------------- +bool cmCTestAddSubdirectoryCommand +::InitialPass(std::vector<std::string> const& args, cmExecutionStatus &) +{ + if(args.size() < 1 ) + { + this->SetError("called with incorrect number of arguments"); + return false; + } + + std::string cwd = cmSystemTools::GetCurrentWorkingDirectory(); + cmSystemTools::ChangeDirectory(cwd.c_str()); + std::string fname = cwd; + fname += "/"; + fname += args[1]; + + if ( !cmSystemTools::FileExists(fname.c_str()) ) + { + // No subdirectory? So what... + return true; + } + cmSystemTools::ChangeDirectory(fname.c_str()); + const char* testFilename; + if( cmSystemTools::FileExists("CTestTestfile.cmake") ) + { + // does the CTestTestfile.cmake exist ? + testFilename = "CTestTestfile.cmake"; + } + else if( cmSystemTools::FileExists("DartTestfile.txt") ) + { + // does the DartTestfile.txt exist ? + testFilename = "DartTestfile.txt"; + } + else + { + // No CTestTestfile? Who cares... + cmSystemTools::ChangeDirectory(cwd.c_str()); + return true; + } + fname += "/"; + fname += testFilename; + bool readit = + this->Makefile->ReadListFile(this->Makefile->GetCurrentListFile(), + fname.c_str()); + cmSystemTools::ChangeDirectory(cwd.c_str()); + if(!readit) + { + std::string m = "Could not find include file: "; + m += fname; + this->SetError(m.c_str()); + return false; + } + return true; +} + +//---------------------------------------------------------------------- +class cmCTestAddTestCommand : public cmCommand +{ +public: + /** + * This is a virtual constructor for the command. + */ + virtual cmCommand* Clone() + { + cmCTestAddTestCommand* c = new cmCTestAddTestCommand; + c->TestHandler = this->TestHandler; + return c; + } + + /** + * This is called when the command is first encountered in + * the CMakeLists.txt file. + */ + virtual bool InitialPass(std::vector<std::string> const&, + cmExecutionStatus &); + + /** + * The name of the command as specified in CMakeList.txt. + */ + virtual const char* GetName() const { return "ADD_TEST";} + + // Unused methods + virtual const char* GetTerseDocumentation() const { return ""; } + virtual const char* GetFullDocumentation() const { return ""; } + + cmTypeMacro(cmCTestAddTestCommand, cmCommand); + + cmCTestTestHandler* TestHandler; +}; + +//---------------------------------------------------------------------- +bool cmCTestAddTestCommand +::InitialPass(std::vector<std::string> const& args, cmExecutionStatus &) +{ + if ( args.size() < 2 ) + { + this->SetError("called with incorrect number of arguments"); + return false; + } + return this->TestHandler->AddTest(args); +} + +//---------------------------------------------------------------------- +class cmCTestSetTestsPropertiesCommand : public cmCommand +{ +public: + /** + * This is a virtual constructor for the command. + */ + virtual cmCommand* Clone() + { + cmCTestSetTestsPropertiesCommand* c + = new cmCTestSetTestsPropertiesCommand; + c->TestHandler = this->TestHandler; + return c; + } + + /** + * This is called when the command is first encountered in + * the CMakeLists.txt file. + */ + virtual bool InitialPass(std::vector<std::string> const&, + cmExecutionStatus &); + + /** + * The name of the command as specified in CMakeList.txt. + */ + virtual const char* GetName() const { return "SET_TESTS_PROPERTIES";} + + // Unused methods + virtual const char* GetTerseDocumentation() const { return ""; } + virtual const char* GetFullDocumentation() const { return ""; } + + cmTypeMacro(cmCTestSetTestsPropertiesCommand, cmCommand); + + cmCTestTestHandler* TestHandler; +}; + +//---------------------------------------------------------------------- +bool cmCTestSetTestsPropertiesCommand +::InitialPass(std::vector<std::string> const& args, cmExecutionStatus &) +{ + return this->TestHandler->SetTestsProperties(args); +} + +//---------------------------------------------------------------------- +// get the next number in a string with numbers separated by , +// pos is the start of the search and pos2 is the end of the search +// pos becomes pos2 after a call to GetNextNumber. +// -1 is returned at the end of the list. +inline int GetNextNumber(std::string const& in, + int& val, + std::string::size_type& pos, + std::string::size_type& pos2) +{ + pos2 = in.find(',', pos); + if(pos2 != in.npos) + { + if(pos2-pos == 0) + { + val = -1; + } + else + { + val = atoi(in.substr(pos, pos2-pos).c_str()); + } + pos = pos2+1; + return 1; + } + else + { + if(in.size()-pos == 0) + { + val = -1; + } + else + { + val = atoi(in.substr(pos, in.size()-pos).c_str()); + } + return 0; + } +} + +//---------------------------------------------------------------------- +// get the next number in a string with numbers separated by , +// pos is the start of the search and pos2 is the end of the search +// pos becomes pos2 after a call to GetNextNumber. +// -1 is returned at the end of the list. +inline int GetNextRealNumber(std::string const& in, + double& val, + std::string::size_type& pos, + std::string::size_type& pos2) +{ + pos2 = in.find(',', pos); + if(pos2 != in.npos) + { + if(pos2-pos == 0) + { + val = -1; + } + else + { + val = atof(in.substr(pos, pos2-pos).c_str()); + } + pos = pos2+1; + return 1; + } + else + { + if(in.size()-pos == 0) + { + val = -1; + } + else + { + val = atof(in.substr(pos, in.size()-pos).c_str()); + } + return 0; + } +} + + +//---------------------------------------------------------------------- +cmCTestTestHandler::cmCTestTestHandler() +{ + this->UseUnion = false; + + this->UseIncludeLabelRegExpFlag = false; + this->UseExcludeLabelRegExpFlag = false; + this->UseIncludeRegExpFlag = false; + this->UseExcludeRegExpFlag = false; + this->UseExcludeRegExpFirst = false; + + this->CustomMaximumPassedTestOutputSize = 1 * 1024; + this->CustomMaximumFailedTestOutputSize = 300 * 1024; + + this->MemCheck = false; + + this->LogFile = 0; + + // regex to detect <DartMeasurement>...</DartMeasurement> + this->DartStuff.compile( + "(<DartMeasurement.*/DartMeasurement[a-zA-Z]*>)"); + // regex to detect each individual <DartMeasurement>...</DartMeasurement> + this->DartStuff1.compile( + "(<DartMeasurement[^<]*</DartMeasurement[a-zA-Z]*>)"); +} + +//---------------------------------------------------------------------- +void cmCTestTestHandler::Initialize() +{ + this->Superclass::Initialize(); + + this->ElapsedTestingTime = -1; + + this->TestResults.clear(); + + this->CustomTestsIgnore.clear(); + this->StartTest = ""; + this->EndTest = ""; + + this->CustomPreTest.clear(); + this->CustomPostTest.clear(); + this->CustomMaximumPassedTestOutputSize = 1 * 1024; + this->CustomMaximumFailedTestOutputSize = 300 * 1024; + + this->TestsToRun.clear(); + + this->UseIncludeLabelRegExpFlag = false; + this->UseExcludeLabelRegExpFlag = false; + this->UseIncludeRegExpFlag = false; + this->UseExcludeRegExpFlag = false; + this->UseExcludeRegExpFirst = false; + this->IncludeLabelRegularExpression = ""; + this->ExcludeLabelRegularExpression = ""; + this->IncludeRegExp = ""; + this->ExcludeRegExp = ""; + + TestsToRunString = ""; + this->UseUnion = false; + this->TestList.clear(); +} + +//---------------------------------------------------------------------- +void cmCTestTestHandler::PopulateCustomVectors(cmMakefile *mf) +{ + this->CTest->PopulateCustomVector(mf, "CTEST_CUSTOM_PRE_TEST", + this->CustomPreTest); + this->CTest->PopulateCustomVector(mf, "CTEST_CUSTOM_POST_TEST", + this->CustomPostTest); + this->CTest->PopulateCustomVector(mf, + "CTEST_CUSTOM_TESTS_IGNORE", + this->CustomTestsIgnore); + this->CTest->PopulateCustomInteger(mf, + "CTEST_CUSTOM_MAXIMUM_PASSED_TEST_OUTPUT_SIZE", + this->CustomMaximumPassedTestOutputSize); + this->CTest->PopulateCustomInteger(mf, + "CTEST_CUSTOM_MAXIMUM_FAILED_TEST_OUTPUT_SIZE", + this->CustomMaximumFailedTestOutputSize); +} + +//---------------------------------------------------------------------- +int cmCTestTestHandler::PreProcessHandler() +{ + if ( !this->ExecuteCommands(this->CustomPreTest) ) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, + "Problem executing pre-test command(s)." << std::endl); + return 0; + } + return 1; +} + +//---------------------------------------------------------------------- +int cmCTestTestHandler::PostProcessHandler() +{ + if ( !this->ExecuteCommands(this->CustomPostTest) ) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, + "Problem executing post-test command(s)." << std::endl); + return 0; + } + return 1; +} + +//---------------------------------------------------------------------- +//clearly it would be nice if this were broken up into a few smaller +//functions and commented... +int cmCTestTestHandler::ProcessHandler() +{ + // Update internal data structure from generic one + this->SetTestsToRunInformation(this->GetOption("TestsToRunInformation")); + this->SetUseUnion(cmSystemTools::IsOn(this->GetOption("UseUnion"))); + if(cmSystemTools::IsOn(this->GetOption("ScheduleRandom"))) + { + this->CTest->SetScheduleType("Random"); + } + if(this->GetOption("ParallelLevel")) + { + this->CTest->SetParallelLevel(atoi(this->GetOption("ParallelLevel"))); + } + + const char* val; + val = this->GetOption("LabelRegularExpression"); + if ( val ) + { + this->UseIncludeLabelRegExpFlag = true; + this->IncludeLabelRegExp = val; + } + val = this->GetOption("ExcludeLabelRegularExpression"); + if ( val ) + { + this->UseExcludeLabelRegExpFlag = true; + this->ExcludeLabelRegularExpression = val; + } + val = this->GetOption("IncludeRegularExpression"); + if ( val ) + { + this->UseIncludeRegExp(); + this->SetIncludeRegExp(val); + } + val = this->GetOption("ExcludeRegularExpression"); + if ( val ) + { + this->UseExcludeRegExp(); + this->SetExcludeRegExp(val); + } + + this->TestResults.clear(); + + cmCTestLog(this->CTest, HANDLER_OUTPUT, + (this->MemCheck ? "Memory check" : "Test") + << " project " << cmSystemTools::GetCurrentWorkingDirectory() + << std::endl); + if ( ! this->PreProcessHandler() ) + { + return -1; + } + + cmGeneratedFileStream mLogFile; + this->StartLogFile((this->MemCheck ? "DynamicAnalysis" : "Test"), mLogFile); + this->LogFile = &mLogFile; + + std::vector<cmStdString> passed; + std::vector<cmStdString> failed; + int total; + + //start the real time clock + double clock_start, clock_finish; + clock_start = cmSystemTools::GetTime(); + + this->ProcessDirectory(passed, failed); + + clock_finish = cmSystemTools::GetTime(); + + total = int(passed.size()) + int(failed.size()); + + if (total == 0) + { + if ( !this->CTest->GetShowOnly() && !this->CTest->ShouldPrintLabels() ) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, "No tests were found!!!" + << std::endl); + } + } + else + { + if (this->HandlerVerbose && passed.size() && + (this->UseIncludeRegExpFlag || this->UseExcludeRegExpFlag)) + { + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, std::endl + << "The following tests passed:" << std::endl); + for(std::vector<cmStdString>::iterator j = passed.begin(); + j != passed.end(); ++j) + { + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, "\t" << *j + << std::endl); + } + } + + float percent = float(passed.size()) * 100.0f / float(total); + if ( failed.size() > 0 && percent > 99) + { + percent = 99; + } + + cmCTestLog(this->CTest, HANDLER_OUTPUT, std::endl + << static_cast<int>(percent + .5) << "% tests passed, " + << failed.size() << " tests failed out of " + << total << std::endl); + if(this->CTest->GetLabelSummary()) + { + this->PrintLabelSummary(); + } + char realBuf[1024]; + sprintf(realBuf, "%6.2f sec", (double)(clock_finish - clock_start)); + cmCTestLog(this->CTest, HANDLER_OUTPUT, "\nTotal Test time (real) = " + << realBuf << "\n" ); + + if (failed.size()) + { + cmGeneratedFileStream ofs; + cmCTestLog(this->CTest, HANDLER_OUTPUT, std::endl + << "The following tests FAILED:" << std::endl); + this->StartLogFile("TestsFailed", ofs); + + typedef std::set<cmCTestTestHandler::cmCTestTestResult, + cmCTestTestResultLess> SetOfTests; + SetOfTests resultsSet(this->TestResults.begin(), + this->TestResults.end()); + + for(SetOfTests::iterator ftit = resultsSet.begin(); + ftit != resultsSet.end(); ++ftit) + { + if ( ftit->Status != cmCTestTestHandler::COMPLETED ) + { + ofs << ftit->TestCount << ":" << ftit->Name << std::endl; + cmCTestLog(this->CTest, HANDLER_OUTPUT, "\t" << std::setw(3) + << ftit->TestCount << " - " + << ftit->Name.c_str() << " (" + << this->GetTestStatus(ftit->Status) << ")" + << std::endl); + } + } + } + } + + if ( this->CTest->GetProduceXML() ) + { + cmGeneratedFileStream xmlfile; + if( !this->StartResultingXML( + (this->MemCheck ? cmCTest::PartMemCheck : cmCTest::PartTest), + (this->MemCheck ? "DynamicAnalysis" : "Test"), xmlfile) ) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, "Cannot create " + << (this->MemCheck ? "memory check" : "testing") + << " XML file" << std::endl); + this->LogFile = 0; + return 1; + } + this->GenerateDartOutput(xmlfile); + } + + if ( ! this->PostProcessHandler() ) + { + this->LogFile = 0; + return -1; + } + + if ( !failed.empty() ) + { + this->LogFile = 0; + return -1; + } + this->LogFile = 0; + return 0; +} + +//---------------------------------------------------------------------- +void cmCTestTestHandler::PrintLabelSummary() +{ + cmCTestTestHandler::ListOfTests::iterator it = this->TestList.begin(); + cmCTestTestHandler::TestResultsVector::iterator ri = + this->TestResults.begin(); + std::map<cmStdString, double> labelTimes; + std::set<cmStdString> labels; + // initialize maps + std::string::size_type maxlen = 0; + for(; it != this->TestList.end(); ++it) + { + cmCTestTestProperties& p = *it; + if(p.Labels.size() != 0) + { + for(std::vector<std::string>::iterator l = p.Labels.begin(); + l != p.Labels.end(); ++l) + { + if((*l).size() > maxlen) + { + maxlen = (*l).size(); + } + labels.insert(*l); + labelTimes[*l] = 0; + } + } + } + ri = this->TestResults.begin(); + // fill maps + for(; ri != this->TestResults.end(); ++ri) + { + cmCTestTestResult &result = *ri; + cmCTestTestProperties& p = *result.Properties; + if(p.Labels.size() != 0) + { + for(std::vector<std::string>::iterator l = p.Labels.begin(); + l != p.Labels.end(); ++l) + { + labelTimes[*l] += result.ExecutionTime; + } + } + } + // now print times + if(labels.size()) + { + cmCTestLog(this->CTest, HANDLER_OUTPUT, "\nLabel Time Summary:"); + } + for(std::set<cmStdString>::const_iterator i = labels.begin(); + i != labels.end(); ++i) + { + std::string label = *i; + label.resize(maxlen +3, ' '); + char buf[1024]; + sprintf(buf, "%6.2f sec", labelTimes[*i]); + cmCTestLog(this->CTest, HANDLER_OUTPUT, "\n" + << label << " = " << buf ); + if ( this->LogFile ) + { + *this->LogFile << "\n" << *i << " = " + << buf << "\n"; + } + } + if(labels.size()) + { + if(this->LogFile) + { + *this->LogFile << "\n"; + } + cmCTestLog(this->CTest, HANDLER_OUTPUT, "\n"); + } + +} + +//---------------------------------------------------------------------- +void cmCTestTestHandler::CheckLabelFilterInclude(cmCTestTestProperties& it) +{ + // if not using Labels to filter then return + if (!this->UseIncludeLabelRegExpFlag ) + { + return; + } + // if there are no labels and we are filtering by labels + // then exclude the test as it does not have the label + if(it.Labels.size() == 0 ) + { + it.IsInBasedOnREOptions = false; + return; + } + // check to see if the label regular expression matches + bool found = false; // assume it does not match + // loop over all labels and look for match + for(std::vector<std::string>::iterator l = it.Labels.begin(); + l != it.Labels.end(); ++l) + { + if(this->IncludeLabelRegularExpression.find(*l)) + { + found = true; + } + } + // if no match was found, exclude the test + if(!found) + { + it.IsInBasedOnREOptions = false; + } +} + + +//---------------------------------------------------------------------- +void cmCTestTestHandler::CheckLabelFilterExclude(cmCTestTestProperties& it) +{ + // if not using Labels to filter then return + if (!this->UseExcludeLabelRegExpFlag ) + { + return; + } + // if there are no labels and we are excluding by labels + // then do nothing as a no label can not be a match + if(it.Labels.size() == 0 ) + { + return; + } + // check to see if the label regular expression matches + bool found = false; // assume it does not match + // loop over all labels and look for match + for(std::vector<std::string>::iterator l = it.Labels.begin(); + l != it.Labels.end(); ++l) + { + if(this->ExcludeLabelRegularExpression.find(*l)) + { + found = true; + } + } + // if match was found, exclude the test + if(found) + { + it.IsInBasedOnREOptions = false; + } +} + +//---------------------------------------------------------------------- +void cmCTestTestHandler::CheckLabelFilter(cmCTestTestProperties& it) +{ + this->CheckLabelFilterInclude(it); + this->CheckLabelFilterExclude(it); +} + +//---------------------------------------------------------------------- +void cmCTestTestHandler::ComputeTestList() +{ + this->TestList.clear(); // clear list of test + this->GetListOfTests(); + cmCTestTestHandler::ListOfTests::size_type tmsize = this->TestList.size(); + // how many tests are in based on RegExp? + int inREcnt = 0; + cmCTestTestHandler::ListOfTests::iterator it; + for ( it = this->TestList.begin(); it != this->TestList.end(); it ++ ) + { + this->CheckLabelFilter(*it); + if (it->IsInBasedOnREOptions) + { + inREcnt ++; + } + } + // expand the test list based on the union flag + if (this->UseUnion) + { + this->ExpandTestsToRunInformation((int)tmsize); + } + else + { + this->ExpandTestsToRunInformation(inREcnt); + } + // Now create a final list of tests to run + int cnt = 0; + inREcnt = 0; + std::string last_directory = ""; + ListOfTests finalList; + for ( it = this->TestList.begin(); it != this->TestList.end(); it ++ ) + { + cnt ++; + if (it->IsInBasedOnREOptions) + { + inREcnt++; + } + + if (this->UseUnion) + { + // if it is not in the list and not in the regexp then skip + if ((this->TestsToRun.size() && + std::find(this->TestsToRun.begin(), this->TestsToRun.end(), cnt) + == this->TestsToRun.end()) && !it->IsInBasedOnREOptions) + { + continue; + } + } + else + { + // is this test in the list of tests to run? If not then skip it + if ((this->TestsToRun.size() && + std::find(this->TestsToRun.begin(), + this->TestsToRun.end(), inREcnt) + == this->TestsToRun.end()) || !it->IsInBasedOnREOptions) + { + continue; + } + } + it->Index = cnt; // save the index into the test list for this test + finalList.push_back(*it); + } + // Save the total number of tests before exclusions + this->TotalNumberOfTests = this->TestList.size(); + // Set the TestList to the final list of all test + this->TestList = finalList; + std::string::size_type max = this->CTest->GetMaxTestNameWidth(); + for (it = this->TestList.begin(); + it != this->TestList.end(); it ++ ) + { + cmCTestTestProperties& p = *it; + if(max < p.Name.size()) + { + max = p.Name.size(); + } + } + if(static_cast<std::string::size_type>(this->CTest->GetMaxTestNameWidth()) + != max) + { + this->CTest->SetMaxTestNameWidth(static_cast<int>(max)); + } +} + +bool cmCTestTestHandler::GetValue(const char* tag, + int& value, + std::ifstream& fin) +{ + std::string line; + bool ret = true; + cmSystemTools::GetLineFromStream(fin, line); + if(line == tag) + { + fin >> value; + ret = cmSystemTools::GetLineFromStream(fin, line); // read blank line + } + else + { + cmCTestLog(this->CTest, ERROR_MESSAGE, + "parse error: missing tag: " + << tag << " found [" << line << "]" << std::endl); + ret = false; + } + return ret; +} + +bool cmCTestTestHandler::GetValue(const char* tag, + double& value, + std::ifstream& fin) +{ + std::string line; + cmSystemTools::GetLineFromStream(fin, line); + bool ret = true; + if(line == tag) + { + fin >> value; + ret = cmSystemTools::GetLineFromStream(fin, line); // read blank line + } + else + { + cmCTestLog(this->CTest, ERROR_MESSAGE, + "parse error: missing tag: " + << tag << " found [" << line << "]" << std::endl); + ret = false; + } + return ret; +} + +bool cmCTestTestHandler::GetValue(const char* tag, + bool& value, + std::ifstream& fin) +{ + std::string line; + cmSystemTools::GetLineFromStream(fin, line); + bool ret = true; + if(line == tag) + { +#ifdef __HAIKU__ + int tmp = 0; + fin >> tmp; + value = false; + if(tmp) + { + value = true; + } +#else + fin >> value; +#endif + ret = cmSystemTools::GetLineFromStream(fin, line); // read blank line + } + else + { + cmCTestLog(this->CTest, ERROR_MESSAGE, + "parse error: missing tag: " + << tag << " found [" << line << "]" << std::endl); + ret = false; + } + return ret; +} + +bool cmCTestTestHandler::GetValue(const char* tag, + size_t& value, + std::ifstream& fin) +{ + std::string line; + cmSystemTools::GetLineFromStream(fin, line); + bool ret = true; + if(line == tag) + { + fin >> value; + ret = cmSystemTools::GetLineFromStream(fin, line); // read blank line + } + else + { + cmCTestLog(this->CTest, ERROR_MESSAGE, + "parse error: missing tag: " + << tag << " found [" << line.c_str() << "]" << std::endl); + ret = false; + } + return ret; +} + +bool cmCTestTestHandler::GetValue(const char* tag, + std::string& value, + std::ifstream& fin) +{ + std::string line; + cmSystemTools::GetLineFromStream(fin, line); + bool ret = true; + if(line == tag) + { + ret = cmSystemTools::GetLineFromStream(fin, value); + } + else + { + cmCTestLog(this->CTest, ERROR_MESSAGE, + "parse error: missing tag: " + << tag << " found [" << line << "]" << std::endl); + ret = false; + } + return ret; +} + +//--------------------------------------------------------------------- +void cmCTestTestHandler::ProcessDirectory(std::vector<cmStdString> &passed, + std::vector<cmStdString> &failed) +{ + this->ComputeTestList(); + this->StartTest = this->CTest->CurrentTime(); + this->StartTestTime = static_cast<unsigned int>(cmSystemTools::GetTime()); + double elapsed_time_start = cmSystemTools::GetTime(); + + cmCTestMultiProcessHandler* parallel = this->CTest->GetBatchJobs() ? + new cmCTestBatchTestHandler : new cmCTestMultiProcessHandler; + parallel->SetCTest(this->CTest); + parallel->SetParallelLevel(this->CTest->GetParallelLevel()); + parallel->SetTestHandler(this); + + *this->LogFile << "Start testing: " + << this->CTest->CurrentTime() << std::endl + << "----------------------------------------------------------" + << std::endl; + + cmCTestMultiProcessHandler::TestMap tests; + cmCTestMultiProcessHandler::PropertiesMap properties; + + bool randomSchedule = this->CTest->GetScheduleType() == "Random"; + if(randomSchedule) + { + srand((unsigned)time(0)); + } + + for (ListOfTests::iterator it = this->TestList.begin(); + it != this->TestList.end(); ++it) + { + cmCTestTestProperties& p = *it; + cmCTestMultiProcessHandler::TestSet depends; + + if(randomSchedule) + { + p.Cost = static_cast<float>(rand()); + } + + if(p.Timeout == 0 && this->CTest->GetGlobalTimeout() != 0) + { + p.Timeout = this->CTest->GetGlobalTimeout(); + } + + if(p.Depends.size()) + { + for(std::vector<std::string>::iterator i = p.Depends.begin(); + i != p.Depends.end(); ++i) + { + for(ListOfTests::iterator it2 = this->TestList.begin(); + it2 != this->TestList.end(); ++it2) + { + if(it2->Name == *i) + { + depends.insert(it2->Index); + break; // break out of test loop as name can only match 1 + } + } + } + } + tests[it->Index] = depends; + properties[it->Index] = &*it; + } + parallel->SetTests(tests, properties); + parallel->SetPassFailVectors(&passed, &failed); + this->TestResults.clear(); + parallel->SetTestResults(&this->TestResults); + + if(this->CTest->ShouldPrintLabels()) + { + parallel->PrintLabels(); + } + else if(this->CTest->GetShowOnly()) + { + parallel->PrintTestList(); + } + else + { + parallel->RunTests(); + } + delete parallel; + this->EndTest = this->CTest->CurrentTime(); + this->EndTestTime = static_cast<unsigned int>(cmSystemTools::GetTime()); + this->ElapsedTestingTime = cmSystemTools::GetTime() - elapsed_time_start; + *this->LogFile << "End testing: " + << this->CTest->CurrentTime() << std::endl; +} + +//---------------------------------------------------------------------- +void cmCTestTestHandler::GenerateTestCommand(std::vector<std::string>&) +{ +} + +//---------------------------------------------------------------------- +void cmCTestTestHandler::GenerateDartOutput(std::ostream& os) +{ + if ( !this->CTest->GetProduceXML() ) + { + return; + } + + this->CTest->StartXML(os, this->AppendXML); + os << "<Testing>\n" + << "\t<StartDateTime>" << this->StartTest << "</StartDateTime>\n" + << "\t<StartTestTime>" << this->StartTestTime << "</StartTestTime>\n" + << "\t<TestList>\n"; + cmCTestTestHandler::TestResultsVector::size_type cc; + for ( cc = 0; cc < this->TestResults.size(); cc ++ ) + { + cmCTestTestResult *result = &this->TestResults[cc]; + std::string testPath = result->Path + "/" + result->Name; + os << "\t\t<Test>" << cmXMLSafe( + this->CTest->GetShortPathToFile(testPath.c_str())) + << "</Test>" << std::endl; + } + os << "\t</TestList>\n"; + for ( cc = 0; cc < this->TestResults.size(); cc ++ ) + { + cmCTestTestResult *result = &this->TestResults[cc]; + this->WriteTestResultHeader(os, result); + os << "\t\t<Results>" << std::endl; + if ( result->Status != cmCTestTestHandler::NOT_RUN ) + { + if ( result->Status != cmCTestTestHandler::COMPLETED || + result->ReturnValue ) + { + os << "\t\t\t<NamedMeasurement type=\"text/string\" " + "name=\"Exit Code\"><Value>" + << cmXMLSafe(this->GetTestStatus(result->Status)) + << "</Value>" + "</NamedMeasurement>\n" + << "\t\t\t<NamedMeasurement type=\"text/string\" " + "name=\"Exit Value\"><Value>" + << result->ReturnValue + << "</Value></NamedMeasurement>" + << std::endl; + } + os << result->RegressionImages; + os << "\t\t\t<NamedMeasurement type=\"numeric/double\" " + << "name=\"Execution Time\"><Value>" + << result->ExecutionTime + << "</Value></NamedMeasurement>\n"; + if(result->Reason.size()) + { + const char* reasonType = "Pass Reason"; + if(result->Status != cmCTestTestHandler::COMPLETED && + result->Status != cmCTestTestHandler::NOT_RUN) + { + reasonType = "Fail Reason"; + } + os << "\t\t\t<NamedMeasurement type=\"text/string\" " + << "name=\"" << reasonType << "\"><Value>" + << cmXMLSafe(result->Reason) + << "</Value></NamedMeasurement>\n"; + } + os + << "\t\t\t<NamedMeasurement type=\"text/string\" " + << "name=\"Completion Status\"><Value>" + << cmXMLSafe(result->CompletionStatus) + << "</Value></NamedMeasurement>\n"; + } + os + << "\t\t\t<NamedMeasurement type=\"text/string\" " + << "name=\"Command Line\"><Value>" + << cmXMLSafe(result->FullCommandLine) + << "</Value></NamedMeasurement>\n"; + std::map<cmStdString,cmStdString>::iterator measureIt; + for ( measureIt = result->Properties->Measurements.begin(); + measureIt != result->Properties->Measurements.end(); + ++ measureIt ) + { + os + << "\t\t\t<NamedMeasurement type=\"text/string\" " + << "name=\"" << measureIt->first.c_str() << "\"><Value>" + << cmXMLSafe(measureIt->second) + << "</Value></NamedMeasurement>\n"; + } + os + << "\t\t\t<Measurement>\n" + << "\t\t\t\t<Value" + << (result->CompressOutput ? + " encoding=\"base64\" compression=\"gzip\">" + : ">"); + os << cmXMLSafe(result->Output); + os + << "</Value>\n" + << "\t\t\t</Measurement>\n" + << "\t\t</Results>\n"; + + this->AttachFiles(os, result); + this->WriteTestResultFooter(os, result); + } + + os << "\t<EndDateTime>" << this->EndTest << "</EndDateTime>\n" + << "\t<EndTestTime>" << this->EndTestTime << "</EndTestTime>\n" + << "<ElapsedMinutes>" + << static_cast<int>(this->ElapsedTestingTime/6)/10.0 + << "</ElapsedMinutes>" + << "</Testing>" << std::endl; + this->CTest->EndXML(os); +} + +//---------------------------------------------------------------------------- +void cmCTestTestHandler::WriteTestResultHeader(std::ostream& os, + cmCTestTestResult* result) +{ + os << "\t<Test Status=\""; + if ( result->Status == cmCTestTestHandler::COMPLETED ) + { + os << "passed"; + } + else if ( result->Status == cmCTestTestHandler::NOT_RUN ) + { + os << "notrun"; + } + else + { + os << "failed"; + } + std::string testPath = result->Path + "/" + result->Name; + os << "\">\n" + << "\t\t<Name>" << cmXMLSafe(result->Name) << "</Name>\n" + << "\t\t<Path>" << cmXMLSafe( + this->CTest->GetShortPathToFile(result->Path.c_str())) << "</Path>\n" + << "\t\t<FullName>" << cmXMLSafe( + this->CTest->GetShortPathToFile(testPath.c_str())) << "</FullName>\n" + << "\t\t<FullCommandLine>" + << cmXMLSafe(result->FullCommandLine) + << "</FullCommandLine>\n"; +} + +//---------------------------------------------------------------------------- +void cmCTestTestHandler::WriteTestResultFooter(std::ostream& os, + cmCTestTestResult* result) +{ + if(!result->Properties->Labels.empty()) + { + os << "\t\t<Labels>\n"; + std::vector<std::string> const& labels = result->Properties->Labels; + for(std::vector<std::string>::const_iterator li = labels.begin(); + li != labels.end(); ++li) + { + os << "\t\t\t<Label>" << cmXMLSafe(*li) << "</Label>\n"; + } + os << "\t\t</Labels>\n"; + } + + os + << "\t</Test>" << std::endl; +} + +//---------------------------------------------------------------------- +void cmCTestTestHandler::AttachFiles(std::ostream& os, + cmCTestTestResult* result) +{ + if(result->Status != cmCTestTestHandler::COMPLETED + && result->Properties->AttachOnFail.size()) + { + result->Properties->AttachedFiles.insert( + result->Properties->AttachedFiles.end(), + result->Properties->AttachOnFail.begin(), + result->Properties->AttachOnFail.end()); + } + for(std::vector<std::string>::const_iterator file = + result->Properties->AttachedFiles.begin(); + file != result->Properties->AttachedFiles.end(); ++file) + { + std::string base64 = this->CTest->Base64GzipEncodeFile(*file); + std::string fname = cmSystemTools::GetFilenameName(*file); + os << "\t\t<NamedMeasurement name=\"Attached File\" encoding=\"base64\" " + "compression=\"tar/gzip\" filename=\"" << fname << "\" type=\"file\">" + "\n\t\t\t<Value>\n\t\t\t" + << base64 + << "\n\t\t\t</Value>\n\t\t</NamedMeasurement>\n"; + } +} + +//---------------------------------------------------------------------- +int cmCTestTestHandler::ExecuteCommands(std::vector<cmStdString>& vec) +{ + std::vector<cmStdString>::iterator it; + for ( it = vec.begin(); it != vec.end(); ++it ) + { + int retVal = 0; + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, "Run command: " << *it + << std::endl); + if ( !cmSystemTools::RunSingleCommand(it->c_str(), 0, &retVal, 0, + cmSystemTools::OUTPUT_MERGE + /*this->Verbose*/) || retVal != 0 ) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, "Problem running command: " + << *it << std::endl); + return 0; + } + } + return 1; +} + + +//---------------------------------------------------------------------- +// Find the appropriate executable to run for a test +std::string cmCTestTestHandler::FindTheExecutable(const char *exe) +{ + std::string resConfig; + std::vector<std::string> extraPaths; + std::vector<std::string> failedPaths; + if(strcmp(exe, "NOT_AVAILABLE") == 0) + { + return exe; + } + return cmCTestTestHandler::FindExecutable(this->CTest, + exe, resConfig, + extraPaths, + failedPaths); +} + +// add additional configurations to the search path +void cmCTestTestHandler +::AddConfigurations(cmCTest *ctest, + std::vector<std::string> &attempted, + std::vector<std::string> &attemptedConfigs, + std::string filepath, + std::string &filename) +{ + std::string tempPath; + + if (filepath.size() && + filepath[filepath.size()-1] != '/') + { + filepath += "/"; + } + tempPath = filepath + filename; + attempted.push_back(tempPath); + attemptedConfigs.push_back(""); + + if(ctest->GetConfigType().size()) + { + tempPath = filepath; + tempPath += ctest->GetConfigType(); + tempPath += "/"; + tempPath += filename; + attempted.push_back(tempPath); + attemptedConfigs.push_back(ctest->GetConfigType()); + // If the file is an OSX bundle then the configtyp + // will be at the start of the path + tempPath = ctest->GetConfigType(); + tempPath += "/"; + tempPath += filepath; + tempPath += filename; + attempted.push_back(tempPath); + attemptedConfigs.push_back(ctest->GetConfigType()); + } + else + { + // no config specified to try some options + tempPath = filepath; + tempPath += "Release/"; + tempPath += filename; + attempted.push_back(tempPath); + attemptedConfigs.push_back("Release"); + tempPath = filepath; + tempPath += "Debug/"; + tempPath += filename; + attempted.push_back(tempPath); + attemptedConfigs.push_back("Debug"); + tempPath = filepath; + tempPath += "MinSizeRel/"; + tempPath += filename; + attempted.push_back(tempPath); + attemptedConfigs.push_back("MinSizeRel"); + tempPath = filepath; + tempPath += "RelWithDebInfo/"; + tempPath += filename; + attempted.push_back(tempPath); + attemptedConfigs.push_back("RelWithDebInfo"); + tempPath = filepath; + tempPath += "Deployment/"; + tempPath += filename; + attempted.push_back(tempPath); + attemptedConfigs.push_back("Deployment"); + tempPath = filepath; + tempPath += "Development/"; + tempPath += filename; + attempted.push_back(tempPath); + attemptedConfigs.push_back("Deployment"); + } +} + + +//---------------------------------------------------------------------- +// Find the appropriate executable to run for a test +std::string cmCTestTestHandler +::FindExecutable(cmCTest *ctest, + const char *testCommand, + std::string &resultingConfig, + std::vector<std::string> &extraPaths, + std::vector<std::string> &failed) +{ + // now run the compiled test if we can find it + std::vector<std::string> attempted; + std::vector<std::string> attemptedConfigs; + std::string tempPath; + std::string filepath = + cmSystemTools::GetFilenamePath(testCommand); + std::string filename = + cmSystemTools::GetFilenameName(testCommand); + + cmCTestTestHandler::AddConfigurations(ctest, attempted, + attemptedConfigs, + filepath,filename); + + // even if a fullpath was specified also try it relative to the current + // directory + if (filepath.size() && filepath[0] == '/') + { + std::string localfilepath = filepath.substr(1,filepath.size()-1); + cmCTestTestHandler::AddConfigurations(ctest, attempted, + attemptedConfigs, + localfilepath,filename); + } + + + // if extraPaths are provided and we were not passed a full path, try them, + // try any extra paths + if (filepath.size() == 0) + { + for (unsigned int i = 0; i < extraPaths.size(); ++i) + { + std::string filepathExtra = + cmSystemTools::GetFilenamePath(extraPaths[i]); + std::string filenameExtra = + cmSystemTools::GetFilenameName(extraPaths[i]); + cmCTestTestHandler::AddConfigurations(ctest,attempted, + attemptedConfigs, + filepathExtra, + filenameExtra); + } + } + + // store the final location in fullPath + std::string fullPath; + + // now look in the paths we specified above + for(unsigned int ai=0; + ai < attempted.size() && fullPath.size() == 0; ++ai) + { + // first check without exe extension + if(cmSystemTools::FileExists(attempted[ai].c_str()) + && !cmSystemTools::FileIsDirectory(attempted[ai].c_str())) + { + fullPath = cmSystemTools::CollapseFullPath(attempted[ai].c_str()); + resultingConfig = attemptedConfigs[ai]; + } + // then try with the exe extension + else + { + failed.push_back(attempted[ai].c_str()); + tempPath = attempted[ai]; + tempPath += cmSystemTools::GetExecutableExtension(); + if(cmSystemTools::FileExists(tempPath.c_str()) + && !cmSystemTools::FileIsDirectory(tempPath.c_str())) + { + fullPath = cmSystemTools::CollapseFullPath(tempPath.c_str()); + resultingConfig = attemptedConfigs[ai]; + } + else + { + failed.push_back(tempPath.c_str()); + } + } + } + + // if everything else failed, check the users path, but only if a full path + // wasn't specified + if (fullPath.size() == 0 && filepath.size() == 0) + { + std::string path = cmSystemTools::FindProgram(filename.c_str()); + if (path != "") + { + resultingConfig = ""; + return path; + } + } + if(fullPath.size() == 0) + { + cmCTestLog(ctest, HANDLER_OUTPUT, + "Could not find executable " << testCommand << "\n" + << "Looked in the following places:\n"); + for(std::vector<std::string>::iterator i = failed.begin(); + i != failed.end(); ++i) + { + cmCTestLog(ctest, HANDLER_OUTPUT, + i->c_str() << "\n"); + } + } + + return fullPath; +} + + +//---------------------------------------------------------------------- +void cmCTestTestHandler::GetListOfTests() +{ + if ( !this->IncludeLabelRegExp.empty() ) + { + this->IncludeLabelRegularExpression. + compile(this->IncludeLabelRegExp.c_str()); + } + if ( !this->IncludeLabelRegExp.empty() ) + { + this->ExcludeLabelRegularExpression. + compile(this->ExcludeLabelRegExp.c_str()); + } + if ( !this->IncludeRegExp.empty() ) + { + this->IncludeTestsRegularExpression.compile(this->IncludeRegExp.c_str()); + } + if ( !this->ExcludeRegExp.empty() ) + { + this->ExcludeTestsRegularExpression.compile(this->ExcludeRegExp.c_str()); + } + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, + "Constructing a list of tests" << std::endl); + cmake cm; + cmGlobalGenerator gg; + gg.SetCMakeInstance(&cm); + std::auto_ptr<cmLocalGenerator> lg(gg.CreateLocalGenerator()); + cmMakefile *mf = lg->GetMakefile(); + mf->AddDefinition("CTEST_CONFIGURATION_TYPE", + this->CTest->GetConfigType().c_str()); + + // Add handler for ADD_TEST + cmCTestAddTestCommand* newCom1 = new cmCTestAddTestCommand; + newCom1->TestHandler = this; + cm.AddCommand(newCom1); + + // Add handler for SUBDIRS + cmCTestSubdirCommand* newCom2 = + new cmCTestSubdirCommand; + newCom2->TestHandler = this; + cm.AddCommand(newCom2); + + // Add handler for ADD_SUBDIRECTORY + cmCTestAddSubdirectoryCommand* newCom3 = + new cmCTestAddSubdirectoryCommand; + newCom3->TestHandler = this; + cm.AddCommand(newCom3); + + // Add handler for SET_SOURCE_FILES_PROPERTIES + cmCTestSetTestsPropertiesCommand* newCom4 + = new cmCTestSetTestsPropertiesCommand; + newCom4->TestHandler = this; + cm.AddCommand(newCom4); + + const char* testFilename; + if( cmSystemTools::FileExists("CTestTestfile.cmake") ) + { + // does the CTestTestfile.cmake exist ? + testFilename = "CTestTestfile.cmake"; + } + else if( cmSystemTools::FileExists("DartTestfile.txt") ) + { + // does the DartTestfile.txt exist ? + testFilename = "DartTestfile.txt"; + } + else + { + return; + } + + if ( !mf->ReadListFile(0, testFilename) ) + { + return; + } + if ( cmSystemTools::GetErrorOccuredFlag() ) + { + return; + } + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, + "Done constructing a list of tests" << std::endl); +} + +//---------------------------------------------------------------------- +void cmCTestTestHandler::UseIncludeRegExp() +{ + this->UseIncludeRegExpFlag = true; +} + +//---------------------------------------------------------------------- +void cmCTestTestHandler::UseExcludeRegExp() +{ + this->UseExcludeRegExpFlag = true; + this->UseExcludeRegExpFirst = this->UseIncludeRegExpFlag ? false : true; +} + +//---------------------------------------------------------------------- +const char* cmCTestTestHandler::GetTestStatus(int status) +{ + static const char statuses[][100] = { + "Not Run", + "Timeout", + "SEGFAULT", + "ILLEGAL", + "INTERRUPT", + "NUMERICAL", + "OTHER_FAULT", + "Failed", + "BAD_COMMAND", + "Completed" + }; + + if ( status < cmCTestTestHandler::NOT_RUN || + status > cmCTestTestHandler::COMPLETED ) + { + return "No Status"; + } + return statuses[status]; +} + +//---------------------------------------------------------------------- +void cmCTestTestHandler::ExpandTestsToRunInformation(size_t numTests) +{ + if (this->TestsToRunString.empty()) + { + return; + } + + int start; + int end = -1; + double stride = -1; + std::string::size_type pos = 0; + std::string::size_type pos2; + // read start + if(GetNextNumber(this->TestsToRunString, start, pos, pos2)) + { + // read end + if(GetNextNumber(this->TestsToRunString, end, pos, pos2)) + { + // read stride + if(GetNextRealNumber(this->TestsToRunString, stride, pos, pos2)) + { + int val =0; + // now read specific numbers + while(GetNextNumber(this->TestsToRunString, val, pos, pos2)) + { + this->TestsToRun.push_back(val); + } + this->TestsToRun.push_back(val); + } + } + } + + // if start is not specified then we assume we start at 1 + if(start == -1) + { + start = 1; + } + + // if end isnot specified then we assume we end with the last test + if(end == -1) + { + end = static_cast<int>(numTests); + } + + // if the stride wasn't specified then it defaults to 1 + if(stride == -1) + { + stride = 1; + } + + // if we have a range then add it + if(end != -1 && start != -1 && stride > 0) + { + int i = 0; + while (i*stride + start <= end) + { + this->TestsToRun.push_back(static_cast<int>(i*stride+start)); + ++i; + } + } + + // sort the array + std::sort(this->TestsToRun.begin(), this->TestsToRun.end(), + std::less<int>()); + // remove duplicates + std::vector<int>::iterator new_end = + std::unique(this->TestsToRun.begin(), this->TestsToRun.end()); + this->TestsToRun.erase(new_end, this->TestsToRun.end()); +} + +//---------------------------------------------------------------------- +// Just for convenience +#define SPACE_REGEX "[ \t\r\n]" +//---------------------------------------------------------------------- +std::string cmCTestTestHandler::GenerateRegressionImages( + const std::string& xml) +{ + cmsys::RegularExpression twoattributes( + "<DartMeasurement" + SPACE_REGEX "*(name|type|encoding|compression)=\"([^\"]*)\"" + SPACE_REGEX "*(name|type|encoding|compression)=\"([^\"]*)\"" + SPACE_REGEX "*>([^<]*)</DartMeasurement>"); + cmsys::RegularExpression threeattributes( + "<DartMeasurement" + SPACE_REGEX "*(name|type|encoding|compression)=\"([^\"]*)\"" + SPACE_REGEX "*(name|type|encoding|compression)=\"([^\"]*)\"" + SPACE_REGEX "*(name|type|encoding|compression)=\"([^\"]*)\"" + SPACE_REGEX "*>([^<]*)</DartMeasurement>"); + cmsys::RegularExpression fourattributes( + "<DartMeasurement" + SPACE_REGEX "*(name|type|encoding|compression)=\"([^\"]*)\"" + SPACE_REGEX "*(name|type|encoding|compression)=\"([^\"]*)\"" + SPACE_REGEX "*(name|type|encoding|compression)=\"([^\"]*)\"" + SPACE_REGEX "*(name|type|encoding|compression)=\"([^\"]*)\"" + SPACE_REGEX "*>([^<]*)</DartMeasurement>"); + cmsys::RegularExpression cdatastart( + "<DartMeasurement" + SPACE_REGEX "*(name|type|encoding|compression)=\"([^\"]*)\"" + SPACE_REGEX "*(name|type|encoding|compression)=\"([^\"]*)\"" + SPACE_REGEX "*>" + SPACE_REGEX "*<!\\[CDATA\\["); + cmsys::RegularExpression cdataend( + "]]>" + SPACE_REGEX "*</DartMeasurement>"); + cmsys::RegularExpression measurementfile( + "<DartMeasurementFile" + SPACE_REGEX "*(name|type|encoding|compression)=\"([^\"]*)\"" + SPACE_REGEX "*(name|type|encoding|compression)=\"([^\"]*)\"" + SPACE_REGEX "*>([^<]*)</DartMeasurementFile>"); + + cmOStringStream ostr; + bool done = false; + std::string cxml = xml; + while ( ! done ) + { + if ( twoattributes.find(cxml) ) + { + ostr + << "\t\t\t<NamedMeasurement" + << " " << twoattributes.match(1) << "=\"" + << twoattributes.match(2) << "\"" + << " " << twoattributes.match(3) << "=\"" + << twoattributes.match(4) << "\"" + << "><Value>" << twoattributes.match(5) + << "</Value></NamedMeasurement>" + << std::endl; + cxml.erase(twoattributes.start(), + twoattributes.end() - twoattributes.start()); + } + else if ( threeattributes.find(cxml) ) + { + ostr + << "\t\t\t<NamedMeasurement" + << " " << threeattributes.match(1) << "=\"" + << threeattributes.match(2) << "\"" + << " " << threeattributes.match(3) << "=\"" + << threeattributes.match(4) << "\"" + << " " << threeattributes.match(5) << "=\"" + << threeattributes.match(6) << "\"" + << "><Value>" << threeattributes.match(7) + << "</Value></NamedMeasurement>" + << std::endl; + cxml.erase(threeattributes.start(), + threeattributes.end() - threeattributes.start()); + } + else if ( fourattributes.find(cxml) ) + { + ostr + << "\t\t\t<NamedMeasurement" + << " " << fourattributes.match(1) << "=\"" + << fourattributes.match(2) << "\"" + << " " << fourattributes.match(3) << "=\"" + << fourattributes.match(4) << "\"" + << " " << fourattributes.match(5) << "=\"" + << fourattributes.match(6) << "\"" + << " " << fourattributes.match(7) << "=\"" + << fourattributes.match(8) << "\"" + << "><Value>" << fourattributes.match(9) + << "</Value></NamedMeasurement>" + << std::endl; + cxml.erase(fourattributes.start(), + fourattributes.end() - fourattributes.start()); + } + else if ( cdatastart.find(cxml) && cdataend.find(cxml) ) + { + ostr + << "\t\t\t<NamedMeasurement" + << " " << cdatastart.match(1) << "=\"" + << cdatastart.match(2) << "\"" + << " " << cdatastart.match(3) << "=\"" + << cdatastart.match(4) << "\"" + << "><Value><![CDATA[" + << cxml.substr(cdatastart.end(), cdataend.start() - cdatastart.end()) + << "]]></Value></NamedMeasurement>" + << std::endl; + cxml.erase(cdatastart.start(), + cdataend.end() - cdatastart.start()); + } + else if ( measurementfile.find(cxml) ) + { + const std::string& filename = + cmCTest::CleanString(measurementfile.match(5)); + if ( cmSystemTools::FileExists(filename.c_str()) ) + { + long len = cmSystemTools::FileLength(filename.c_str()); + if ( len == 0 ) + { + std::string k1 = measurementfile.match(1); + std::string v1 = measurementfile.match(2); + std::string k2 = measurementfile.match(3); + std::string v2 = measurementfile.match(4); + if ( cmSystemTools::LowerCase(k1) == "type" ) + { + v1 = "text/string"; + } + if ( cmSystemTools::LowerCase(k2) == "type" ) + { + v2 = "text/string"; + } + + ostr + << "\t\t\t<NamedMeasurement" + << " " << k1 << "=\"" << v1 << "\"" + << " " << k2 << "=\"" << v2 << "\"" + << " encoding=\"none\"" + << "><Value>Image " << filename.c_str() + << " is empty</Value></NamedMeasurement>"; + } + else + { + std::ifstream ifs(filename.c_str(), std::ios::in +#ifdef _WIN32 + | std::ios::binary +#endif + ); + unsigned char *file_buffer = new unsigned char [ len + 1 ]; + ifs.read(reinterpret_cast<char*>(file_buffer), len); + unsigned char *encoded_buffer + = new unsigned char [ static_cast<int>( + static_cast<double>(len) * 1.5 + 5.0) ]; + + unsigned long rlen + = cmsysBase64_Encode(file_buffer, len, encoded_buffer, 1); + unsigned long cc; + + ostr + << "\t\t\t<NamedMeasurement" + << " " << measurementfile.match(1) << "=\"" + << measurementfile.match(2) << "\"" + << " " << measurementfile.match(3) << "=\"" + << measurementfile.match(4) << "\"" + << " encoding=\"base64\"" + << ">" << std::endl << "\t\t\t\t<Value>"; + for ( cc = 0; cc < rlen; cc ++ ) + { + ostr << encoded_buffer[cc]; + if ( cc % 60 == 0 && cc ) + { + ostr << std::endl; + } + } + ostr + << "</Value>" << std::endl << "\t\t\t</NamedMeasurement>" + << std::endl; + delete [] file_buffer; + delete [] encoded_buffer; + } + } + else + { + int idx = 4; + if ( measurementfile.match(1) == "name" ) + { + idx = 2; + } + ostr + << "\t\t\t<NamedMeasurement" + << " name=\"" << measurementfile.match(idx) << "\"" + << " text=\"text/string\"" + << "><Value>File " << filename.c_str() + << " not found</Value></NamedMeasurement>" + << std::endl; + cmCTestLog(this->CTest, HANDLER_OUTPUT, "File \"" << filename.c_str() + << "\" not found." << std::endl); + } + cxml.erase(measurementfile.start(), + measurementfile.end() - measurementfile.start()); + } + else + { + done = true; + } + } + return ostr.str(); +} + +//---------------------------------------------------------------------- +void cmCTestTestHandler::SetIncludeRegExp(const char *arg) +{ + this->IncludeRegExp = arg; +} + +//---------------------------------------------------------------------- +void cmCTestTestHandler::SetExcludeRegExp(const char *arg) +{ + this->ExcludeRegExp = arg; +} + +//---------------------------------------------------------------------- +void cmCTestTestHandler::SetTestsToRunInformation(const char* in) +{ + if ( !in ) + { + return; + } + this->TestsToRunString = in; + // if the argument is a file, then read it and use the contents as the + // string + if(cmSystemTools::FileExists(in)) + { + std::ifstream fin(in); + unsigned long filelen = cmSystemTools::FileLength(in); + char* buff = new char[filelen+1]; + fin.getline(buff, filelen); + buff[fin.gcount()] = 0; + this->TestsToRunString = buff; + delete [] buff; + } +} + +//---------------------------------------------------------------------------- +bool cmCTestTestHandler::CleanTestOutput(std::string& output, size_t length) +{ + if(!length || length >= output.size() || + output.find("CTEST_FULL_OUTPUT") != output.npos) + { + return true; + } + + // Truncate at given length but do not break in the middle of a multi-byte + // UTF-8 encoding. + char const* const begin = output.c_str(); + char const* const end = begin + output.size(); + char const* const truncate = begin + length; + char const* current = begin; + while(current < truncate) + { + unsigned int ch; + if(const char* next = cm_utf8_decode_character(current, end, &ch)) + { + if(next > truncate) + { + break; + } + current = next; + } + else // Bad byte will be handled by cmXMLSafe. + { + ++current; + } + } + output = output.substr(0, current - begin); + + // Append truncation message. + cmOStringStream msg; + msg << "...\n" + "The rest of the test output was removed since it exceeds the threshold " + "of " << length << " bytes.\n"; + output += msg.str(); + return true; +} + +//---------------------------------------------------------------------- +bool cmCTestTestHandler::SetTestsProperties( + const std::vector<std::string>& args) +{ + std::vector<std::string>::const_iterator it; + std::vector<cmStdString> tests; + bool found = false; + for ( it = args.begin(); it != args.end(); ++ it ) + { + if ( *it == "PROPERTIES" ) + { + found = true; + break; + } + tests.push_back(*it); + } + if ( !found ) + { + return false; + } + ++ it; // skip PROPERTIES + for ( ; it != args.end(); ++ it ) + { + std::string key = *it; + ++ it; + if ( it == args.end() ) + { + break; + } + std::string val = *it; + std::vector<cmStdString>::const_iterator tit; + for ( tit = tests.begin(); tit != tests.end(); ++ tit ) + { + cmCTestTestHandler::ListOfTests::iterator rtit; + for ( rtit = this->TestList.begin(); + rtit != this->TestList.end(); + ++ rtit ) + { + if ( *tit == rtit->Name ) + { + if ( key == "WILL_FAIL" ) + { + rtit->WillFail = cmSystemTools::IsOn(val.c_str()); + } + if ( key == "ATTACHED_FILES" ) + { + std::vector<std::string> lval; + cmSystemTools::ExpandListArgument(val.c_str(), lval); + + for(std::vector<std::string>::iterator f = lval.begin(); + f != lval.end(); ++f) + { + rtit->AttachedFiles.push_back(*f); + } + } + if ( key == "ATTACHED_FILES_ON_FAIL" ) + { + std::vector<std::string> lval; + cmSystemTools::ExpandListArgument(val.c_str(), lval); + + for(std::vector<std::string>::iterator f = lval.begin(); + f != lval.end(); ++f) + { + rtit->AttachOnFail.push_back(*f); + } + } + if ( key == "RESOURCE_LOCK" ) + { + std::vector<std::string> lval; + cmSystemTools::ExpandListArgument(val.c_str(), lval); + + for(std::vector<std::string>::iterator f = lval.begin(); + f != lval.end(); ++f) + { + rtit->LockedResources.insert(*f); + } + } + if ( key == "TIMEOUT" ) + { + rtit->Timeout = atof(val.c_str()); + rtit->ExplicitTimeout = true; + } + if ( key == "COST" ) + { + rtit->Cost = static_cast<float>(atof(val.c_str())); + } + if ( key == "REQUIRED_FILES" ) + { + std::vector<std::string> lval; + cmSystemTools::ExpandListArgument(val.c_str(), lval); + + for(std::vector<std::string>::iterator f = lval.begin(); + f != lval.end(); ++f) + { + rtit->RequiredFiles.push_back(*f); + } + } + if ( key == "RUN_SERIAL" ) + { + rtit->RunSerial = cmSystemTools::IsOn(val.c_str()); + } + if ( key == "FAIL_REGULAR_EXPRESSION" ) + { + std::vector<std::string> lval; + cmSystemTools::ExpandListArgument(val.c_str(), lval); + std::vector<std::string>::iterator crit; + for ( crit = lval.begin(); crit != lval.end(); ++ crit ) + { + rtit->ErrorRegularExpressions.push_back( + std::pair<cmsys::RegularExpression, std::string>( + cmsys::RegularExpression(crit->c_str()), + std::string(crit->c_str()))); + } + } + if ( key == "PROCESSORS" ) + { + rtit->Processors = atoi(val.c_str()); + if(rtit->Processors < 1) + { + rtit->Processors = 1; + } + } + if ( key == "DEPENDS" ) + { + std::vector<std::string> lval; + cmSystemTools::ExpandListArgument(val.c_str(), lval); + std::vector<std::string>::iterator crit; + for ( crit = lval.begin(); crit != lval.end(); ++ crit ) + { + rtit->Depends.push_back(*crit); + } + } + if ( key == "ENVIRONMENT" ) + { + std::vector<std::string> lval; + cmSystemTools::ExpandListArgument(val.c_str(), lval); + std::vector<std::string>::iterator crit; + for ( crit = lval.begin(); crit != lval.end(); ++ crit ) + { + rtit->Environment.push_back(*crit); + } + } + if ( key == "LABELS" ) + { + std::vector<std::string> lval; + cmSystemTools::ExpandListArgument(val.c_str(), lval); + std::vector<std::string>::iterator crit; + for ( crit = lval.begin(); crit != lval.end(); ++ crit ) + { + rtit->Labels.push_back(*crit); + } + } + if ( key == "MEASUREMENT" ) + { + size_t pos = val.find_first_of("="); + if ( pos != val.npos ) + { + std::string mKey = val.substr(0, pos); + const char* mVal = val.c_str() + pos + 1; + rtit->Measurements[mKey] = mVal; + } + else + { + rtit->Measurements[val] = "1"; + } + } + if ( key == "PASS_REGULAR_EXPRESSION" ) + { + std::vector<std::string> lval; + cmSystemTools::ExpandListArgument(val.c_str(), lval); + std::vector<std::string>::iterator crit; + for ( crit = lval.begin(); crit != lval.end(); ++ crit ) + { + rtit->RequiredRegularExpressions.push_back( + std::pair<cmsys::RegularExpression, std::string>( + cmsys::RegularExpression(crit->c_str()), + std::string(crit->c_str()))); + } + } + if ( key == "WORKING_DIRECTORY" ) + { + rtit->Directory = val; + } + } + } + } + } + return true; +} + +//---------------------------------------------------------------------- +bool cmCTestTestHandler::AddTest(const std::vector<std::string>& args) +{ + const std::string& testname = args[0]; + cmCTestLog(this->CTest, DEBUG, "Add test: " << args[0] << std::endl); + + if (this->UseExcludeRegExpFlag && + this->UseExcludeRegExpFirst && + this->ExcludeTestsRegularExpression.find(testname.c_str())) + { + return true; + } + if ( this->MemCheck ) + { + std::vector<cmStdString>::iterator it; + bool found = false; + for ( it = this->CustomTestsIgnore.begin(); + it != this->CustomTestsIgnore.end(); ++ it ) + { + if ( *it == testname ) + { + found = true; + break; + } + } + if ( found ) + { + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, "Ignore memcheck: " + << *it << std::endl); + return true; + } + } + else + { + std::vector<cmStdString>::iterator it; + bool found = false; + for ( it = this->CustomTestsIgnore.begin(); + it != this->CustomTestsIgnore.end(); ++ it ) + { + if ( *it == testname ) + { + found = true; + break; + } + } + if ( found ) + { + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, "Ignore test: " + << *it << std::endl); + return true; + } + } + + cmCTestTestProperties test; + test.Name = testname; + test.Args = args; + test.Directory = cmSystemTools::GetCurrentWorkingDirectory(); + cmCTestLog(this->CTest, DEBUG, "Set test directory: " + << test.Directory << std::endl); + + test.IsInBasedOnREOptions = true; + test.WillFail = false; + test.RunSerial = false; + test.Timeout = 0; + test.ExplicitTimeout = false; + test.Cost = 0; + test.Processors = 1; + test.PreviousRuns = 0; + if (this->UseIncludeRegExpFlag && + !this->IncludeTestsRegularExpression.find(testname.c_str())) + { + test.IsInBasedOnREOptions = false; + } + else if (this->UseExcludeRegExpFlag && + !this->UseExcludeRegExpFirst && + this->ExcludeTestsRegularExpression.find(testname.c_str())) + { + test.IsInBasedOnREOptions = false; + } + this->TestList.push_back(test); + return true; +} + diff --git a/Source/CTest/cmCTestTestHandler.h b/Source/CTest/cmCTestTestHandler.h new file mode 100644 index 000000000..8e59e5915 --- /dev/null +++ b/Source/CTest/cmCTestTestHandler.h @@ -0,0 +1,273 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2009 Kitware, Inc. + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ + +#ifndef cmCTestTestHandler_h +#define cmCTestTestHandler_h + + +#include "cmCTestGenericHandler.h" +#include <cmsys/RegularExpression.hxx> + +class cmMakefile; + +/** \class cmCTestTestHandler + * \brief A class that handles ctest -S invocations + * + */ +class cmCTestTestHandler : public cmCTestGenericHandler +{ + friend class cmCTestRunTest; + friend class cmCTestMultiProcessHandler; + friend class cmCTestBatchTestHandler; +public: + cmTypeMacro(cmCTestTestHandler, cmCTestGenericHandler); + + /** + * The main entry point for this class + */ + int ProcessHandler(); + + /** + * When both -R and -I are used should te resulting test list be the + * intersection or the union of the lists. By default it is the + * intersection. + */ + void SetUseUnion(bool val) { this->UseUnion = val; } + + /** + * This method is called when reading CTest custom file + */ + void PopulateCustomVectors(cmMakefile *mf); + + ///! Control the use of the regular expresisons, call these methods to turn + ///them on + void UseIncludeRegExp(); + void UseExcludeRegExp(); + void SetIncludeRegExp(const char *); + void SetExcludeRegExp(const char *); + + void SetMaxIndex(int n) {this->MaxIndex = n;} + int GetMaxIndex() {return this->MaxIndex;} + + ///! pass the -I argument down + void SetTestsToRunInformation(const char*); + + cmCTestTestHandler(); + + /* + * Add the test to the list of tests to be executed + */ + bool AddTest(const std::vector<std::string>& args); + + /* + * Set tests properties + */ + bool SetTestsProperties(const std::vector<std::string>& args); + + void Initialize(); + + // NOTE: This struct is Saved/Restored + // in cmCTestTestHandler, if you add to this class + // then you must add the new members to that code or + // ctest -j N will break for that feature + struct cmCTestTestProperties + { + cmStdString Name; + cmStdString Directory; + std::vector<std::string> Args; + std::vector<std::string> RequiredFiles; + std::vector<std::string> Depends; + std::vector<std::string> AttachedFiles; + std::vector<std::string> AttachOnFail; + std::vector<std::pair<cmsys::RegularExpression, + std::string> > ErrorRegularExpressions; + std::vector<std::pair<cmsys::RegularExpression, + std::string> > RequiredRegularExpressions; + std::map<cmStdString, cmStdString> Measurements; + bool IsInBasedOnREOptions; + bool WillFail; + float Cost; + int PreviousRuns; + bool RunSerial; + double Timeout; + bool ExplicitTimeout; + int Index; + //Requested number of process slots + int Processors; + std::vector<std::string> Environment; + std::vector<std::string> Labels; + std::set<std::string> LockedResources; + }; + + struct cmCTestTestResult + { + std::string Name; + std::string Path; + std::string Reason; + std::string FullCommandLine; + double ExecutionTime; + int ReturnValue; + int Status; + bool CompressOutput; + std::string CompletionStatus; + std::string Output; + std::string RegressionImages; + int TestCount; + cmCTestTestProperties* Properties; + }; + + struct cmCTestTestResultLess + { + bool operator() (const cmCTestTestResult &lhs, + const cmCTestTestResult &rhs) const + { + return lhs.TestCount < rhs.TestCount; + } + }; + + // add configurations to a search path for an executable + static void AddConfigurations(cmCTest *ctest, + std::vector<std::string> &attempted, + std::vector<std::string> &attemptedConfigs, + std::string filepath, + std::string &filename); + + // full signature static method to find an executable + static std::string FindExecutable(cmCTest *ctest, + const char *testCommand, + std::string &resultingConfig, + std::vector<std::string> &extraPaths, + std::vector<std::string> &failed); + + typedef std::vector<cmCTestTestProperties> ListOfTests; +protected: + // compute a final test list + virtual int PreProcessHandler(); + virtual int PostProcessHandler(); + virtual void GenerateTestCommand(std::vector<std::string>& args); + int ExecuteCommands(std::vector<cmStdString>& vec); + + void WriteTestResultHeader(std::ostream& os, cmCTestTestResult* result); + void WriteTestResultFooter(std::ostream& os, cmCTestTestResult* result); + // Write attached test files into the xml + void AttachFiles(std::ostream& os, cmCTestTestResult* result); + + //! Clean test output to specified length + bool CleanTestOutput(std::string& output, size_t length); + + double ElapsedTestingTime; + + typedef std::vector<cmCTestTestResult> TestResultsVector; + TestResultsVector TestResults; + + std::vector<cmStdString> CustomTestsIgnore; + std::string StartTest; + std::string EndTest; + unsigned int StartTestTime; + unsigned int EndTestTime; + bool MemCheck; + int CustomMaximumPassedTestOutputSize; + int CustomMaximumFailedTestOutputSize; + int MaxIndex; +public: + enum { // Program statuses + NOT_RUN = 0, + TIMEOUT, + SEGFAULT, + ILLEGAL, + INTERRUPT, + NUMERICAL, + OTHER_FAULT, + FAILED, + BAD_COMMAND, + COMPLETED + }; + +private: + /** + * Generate the Dart compatible output + */ + virtual void GenerateDartOutput(std::ostream& os); + + void PrintLabelSummary(); + /** + * Run the tests for a directory and any subdirectories + */ + void ProcessDirectory(std::vector<cmStdString> &passed, + std::vector<cmStdString> &failed); + + /** + * Get the list of tests in directory and subdirectories. + */ + void GetListOfTests(); + // compute the lists of tests that will actually run + // based on union regex and -I stuff + void ComputeTestList(); + + bool GetValue(const char* tag, + std::string& value, + std::ifstream& fin); + bool GetValue(const char* tag, + int& value, + std::ifstream& fin); + bool GetValue(const char* tag, + size_t& value, + std::ifstream& fin); + bool GetValue(const char* tag, + bool& value, + std::ifstream& fin); + bool GetValue(const char* tag, + double& value, + std::ifstream& fin); + /** + * Find the executable for a test + */ + std::string FindTheExecutable(const char *exe); + + const char* GetTestStatus(int status); + void ExpandTestsToRunInformation(size_t numPossibleTests); + + std::vector<cmStdString> CustomPreTest; + std::vector<cmStdString> CustomPostTest; + + std::vector<int> TestsToRun; + + bool UseIncludeLabelRegExpFlag; + bool UseExcludeLabelRegExpFlag; + bool UseIncludeRegExpFlag; + bool UseExcludeRegExpFlag; + bool UseExcludeRegExpFirst; + std::string IncludeLabelRegExp; + std::string ExcludeLabelRegExp; + std::string IncludeRegExp; + std::string ExcludeRegExp; + cmsys::RegularExpression IncludeLabelRegularExpression; + cmsys::RegularExpression ExcludeLabelRegularExpression; + cmsys::RegularExpression IncludeTestsRegularExpression; + cmsys::RegularExpression ExcludeTestsRegularExpression; + + std::string GenerateRegressionImages(const std::string& xml); + cmsys::RegularExpression DartStuff1; + void CheckLabelFilter(cmCTestTestProperties& it); + void CheckLabelFilterExclude(cmCTestTestProperties& it); + void CheckLabelFilterInclude(cmCTestTestProperties& it); + + std::string TestsToRunString; + bool UseUnion; + ListOfTests TestList; + size_t TotalNumberOfTests; + cmsys::RegularExpression DartStuff; + + std::ostream* LogFile; +}; + +#endif diff --git a/Source/CTest/cmCTestUpdateCommand.cxx b/Source/CTest/cmCTestUpdateCommand.cxx new file mode 100644 index 000000000..841434911 --- /dev/null +++ b/Source/CTest/cmCTestUpdateCommand.cxx @@ -0,0 +1,78 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2009 Kitware, Inc., Insight Software Consortium + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ +#include "cmCTestUpdateCommand.h" + +#include "cmCTest.h" +#include "cmCTestGenericHandler.h" + +cmCTestGenericHandler* cmCTestUpdateCommand::InitializeHandler() +{ + if ( this->Values[ct_SOURCE] ) + { + this->CTest->SetCTestConfiguration("SourceDirectory", + cmSystemTools::CollapseFullPath( + this->Values[ct_SOURCE]).c_str()); + } + else + { + this->CTest->SetCTestConfiguration("SourceDirectory", + cmSystemTools::CollapseFullPath( + this->Makefile->GetDefinition("CTEST_SOURCE_DIRECTORY")).c_str()); + } + std::string source_dir + = this->CTest->GetCTestConfiguration("SourceDirectory"); + + this->CTest->SetCTestConfigurationFromCMakeVariable(this->Makefile, + "UpdateCommand", "CTEST_UPDATE_COMMAND"); + this->CTest->SetCTestConfigurationFromCMakeVariable(this->Makefile, + "UpdateOptions", "CTEST_UPDATE_OPTIONS"); + this->CTest->SetCTestConfigurationFromCMakeVariable(this->Makefile, + "CVSCommand", "CTEST_CVS_COMMAND"); + this->CTest->SetCTestConfigurationFromCMakeVariable(this->Makefile, + "CVSUpdateOptions", "CTEST_CVS_UPDATE_OPTIONS"); + this->CTest->SetCTestConfigurationFromCMakeVariable(this->Makefile, + "SVNCommand", "CTEST_SVN_COMMAND"); + this->CTest->SetCTestConfigurationFromCMakeVariable(this->Makefile, + "SVNUpdateOptions", "CTEST_SVN_UPDATE_OPTIONS"); + this->CTest->SetCTestConfigurationFromCMakeVariable(this->Makefile, + "BZRCommand", "CTEST_BZR_COMMAND"); + this->CTest->SetCTestConfigurationFromCMakeVariable(this->Makefile, + "BZRUpdateOptions", "CTEST_BZR_UPDATE_OPTIONS"); + this->CTest->SetCTestConfigurationFromCMakeVariable(this->Makefile, + "GITCommand", "CTEST_GIT_COMMAND"); + this->CTest->SetCTestConfigurationFromCMakeVariable(this->Makefile, + "GITUpdateOptions", "CTEST_GIT_UPDATE_OPTIONS"); + this->CTest->SetCTestConfigurationFromCMakeVariable(this->Makefile, + "GITUpdateCustom", "CTEST_GIT_UPDATE_CUSTOM"); + this->CTest->SetCTestConfigurationFromCMakeVariable(this->Makefile, + "HGCommand", "CTEST_HG_COMMAND"); + this->CTest->SetCTestConfigurationFromCMakeVariable(this->Makefile, + "HGUpdateOptions", "CTEST_HG_UPDATE_OPTIONS"); + + cmCTestGenericHandler* handler + = this->CTest->GetInitializedHandler("update"); + if ( !handler ) + { + this->SetError("internal CTest error. Cannot instantiate update handler"); + return 0; + } + handler->SetCommand(this); + if ( source_dir.empty() ) + { + this->SetError("source directory not specified. Please use SOURCE tag"); + return 0; + } + handler->SetOption("SourceDirectory", source_dir.c_str()); + return handler; +} + + diff --git a/Source/CTest/cmCTestUpdateCommand.h b/Source/CTest/cmCTestUpdateCommand.h new file mode 100644 index 000000000..c578fff6f --- /dev/null +++ b/Source/CTest/cmCTestUpdateCommand.h @@ -0,0 +1,73 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2009 Kitware, Inc., Insight Software Consortium + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ +#ifndef cmCTestUpdateCommand_h +#define cmCTestUpdateCommand_h + +#include "cmCTestHandlerCommand.h" + +/** \class cmCTestUpdate + * \brief Run a ctest script + * + * cmCTestUpdateCommand defineds the command to updates the repository. + */ +class cmCTestUpdateCommand : public cmCTestHandlerCommand +{ +public: + + cmCTestUpdateCommand() {} + + /** + * This is a virtual constructor for the command. + */ + virtual cmCommand* Clone() + { + cmCTestUpdateCommand* ni = new cmCTestUpdateCommand; + ni->CTest = this->CTest; + ni->CTestScriptHandler = this->CTestScriptHandler; + return ni; + } + + /** + * The name of the command as specified in CMakeList.txt. + */ + virtual const char* GetName() const { return "ctest_update";} + + /** + * Succinct documentation. + */ + virtual const char* GetTerseDocumentation() const + { + return "Update the work tree from version control."; + } + + /** + * More documentation. + */ + virtual const char* GetFullDocumentation() const + { + return + " ctest_update([SOURCE source] [RETURN_VALUE res])\n" + "Updates the given source directory and stores results in Update.xml. " + "If no SOURCE is given, the CTEST_SOURCE_DIRECTORY variable is used. " + "The RETURN_VALUE option specifies a variable in which to store the " + "result, which is the number of files updated or -1 on error." + ; + } + + cmTypeMacro(cmCTestUpdateCommand, cmCTestHandlerCommand); + +protected: + cmCTestGenericHandler* InitializeHandler(); +}; + + +#endif diff --git a/Source/CTest/cmCTestUpdateHandler.cxx b/Source/CTest/cmCTestUpdateHandler.cxx new file mode 100644 index 000000000..9eae3f3ac --- /dev/null +++ b/Source/CTest/cmCTestUpdateHandler.cxx @@ -0,0 +1,404 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2009 Kitware, Inc., Insight Software Consortium + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ + +#include "cmCTestUpdateHandler.h" + +#include "cmCTest.h" +#include "cmake.h" +#include "cmMakefile.h" +#include "cmLocalGenerator.h" +#include "cmGlobalGenerator.h" +#include "cmVersion.h" +#include "cmGeneratedFileStream.h" +#include "cmXMLParser.h" +#include "cmXMLSafe.h" + +#include "cmCTestVC.h" +#include "cmCTestCVS.h" +#include "cmCTestSVN.h" +#include "cmCTestBZR.h" +#include "cmCTestGIT.h" +#include "cmCTestHG.h" + +#include <cmsys/auto_ptr.hxx> + +//#include <cmsys/RegularExpression.hxx> +#include <cmsys/Process.h> + +// used for sleep +#ifdef _WIN32 +#include "windows.h" +#endif + +#include <stdlib.h> +#include <math.h> +#include <float.h> + +//---------------------------------------------------------------------- +static const char* cmCTestUpdateHandlerUpdateStrings[] = +{ + "Unknown", + "CVS", + "SVN", + "BZR", + "GIT", + "HG" +}; + +static const char* cmCTestUpdateHandlerUpdateToString(int type) +{ + if ( type < cmCTestUpdateHandler::e_UNKNOWN || + type >= cmCTestUpdateHandler::e_LAST ) + { + return cmCTestUpdateHandlerUpdateStrings[cmCTestUpdateHandler::e_UNKNOWN]; + } + return cmCTestUpdateHandlerUpdateStrings[type]; +} + +class cmCTestUpdateHandlerLocale +{ +public: + cmCTestUpdateHandlerLocale(); + ~cmCTestUpdateHandlerLocale(); +private: + std::string saveLCMessages; +}; + +cmCTestUpdateHandlerLocale::cmCTestUpdateHandlerLocale() +{ + const char* lcmess = cmSystemTools::GetEnv("LC_MESSAGES"); + if(lcmess) + { + saveLCMessages = lcmess; + } + // if LC_MESSAGES is not set to C, then + // set it, so that svn/cvs info will be in english ascii + if(! (lcmess && strcmp(lcmess, "C") == 0)) + { + cmSystemTools::PutEnv("LC_MESSAGES=C"); + } +} + +cmCTestUpdateHandlerLocale::~cmCTestUpdateHandlerLocale() +{ + // restore the value of LC_MESSAGES after running the version control + // commands + if(saveLCMessages.size()) + { + std::string put = "LC_MESSAGES="; + put += saveLCMessages; + cmSystemTools::PutEnv(put.c_str()); + } + else + { + cmSystemTools::UnsetEnv("LC_MESSAGES"); + } +} + +//---------------------------------------------------------------------- +cmCTestUpdateHandler::cmCTestUpdateHandler() +{ +} + +//---------------------------------------------------------------------- +void cmCTestUpdateHandler::Initialize() +{ + this->Superclass::Initialize(); + this->UpdateCommand = ""; + this->UpdateType = e_CVS; +} + +//---------------------------------------------------------------------- +int cmCTestUpdateHandler::DetermineType(const char* cmd, const char* type) +{ + cmCTestLog(this->CTest, DEBUG, "Determine update type from command: " << cmd + << " and type: " << type << std::endl); + if ( type && *type ) + { + cmCTestLog(this->CTest, DEBUG, "Type specified: " << type << std::endl); + std::string stype = cmSystemTools::LowerCase(type); + if ( stype.find("cvs") != std::string::npos ) + { + return cmCTestUpdateHandler::e_CVS; + } + if ( stype.find("svn") != std::string::npos ) + { + return cmCTestUpdateHandler::e_SVN; + } + if ( stype.find("bzr") != std::string::npos ) + { + return cmCTestUpdateHandler::e_BZR; + } + if ( stype.find("git") != std::string::npos ) + { + return cmCTestUpdateHandler::e_GIT; + } + if ( stype.find("hg") != std::string::npos ) + { + return cmCTestUpdateHandler::e_HG; + } + } + else + { + cmCTestLog(this->CTest, DEBUG, "Type not specified, check command: " + << cmd << std::endl); + std::string stype = cmSystemTools::LowerCase(cmd); + if ( stype.find("cvs") != std::string::npos ) + { + return cmCTestUpdateHandler::e_CVS; + } + if ( stype.find("svn") != std::string::npos ) + { + return cmCTestUpdateHandler::e_SVN; + } + if ( stype.find("bzr") != std::string::npos ) + { + return cmCTestUpdateHandler::e_BZR; + } + if ( stype.find("git") != std::string::npos ) + { + return cmCTestUpdateHandler::e_GIT; + } + if ( stype.find("hg") != std::string::npos ) + { + return cmCTestUpdateHandler::e_HG; + } + } + return cmCTestUpdateHandler::e_UNKNOWN; +} + +//---------------------------------------------------------------------- +//clearly it would be nice if this were broken up into a few smaller +//functions and commented... +int cmCTestUpdateHandler::ProcessHandler() +{ + // Make sure VCS tool messages are in English so we can parse them. + cmCTestUpdateHandlerLocale fixLocale; + static_cast<void>(fixLocale); + + // Get source dir + const char* sourceDirectory = this->GetOption("SourceDirectory"); + if ( !sourceDirectory ) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, + "Cannot find SourceDirectory key in the DartConfiguration.tcl" + << std::endl); + return -1; + } + + cmGeneratedFileStream ofs; + if ( !this->CTest->GetShowOnly() ) + { + this->StartLogFile("Update", ofs); + } + + cmCTestLog(this->CTest, HANDLER_OUTPUT, " Updating the repository: " + << sourceDirectory << std::endl); + + if(!this->SelectVCS()) + { + return -1; + } + + cmCTestLog(this->CTest, HANDLER_OUTPUT, " Use " + << cmCTestUpdateHandlerUpdateToString(this->UpdateType) + << " repository type" + << std::endl;); + + // Create an object to interact with the VCS tool. + cmsys::auto_ptr<cmCTestVC> vc; + switch (this->UpdateType) + { + case e_CVS: vc.reset(new cmCTestCVS(this->CTest, ofs)); break; + 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); + vc->SetSourceDirectory(sourceDirectory); + + // Cleanup the working tree. + vc->Cleanup(); + + // + // Now update repository and remember what files were updated + // + cmGeneratedFileStream os; + if(!this->StartResultingXML(cmCTest::PartUpdate, "Update", os)) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, "Cannot open log file" + << std::endl); + return -1; + } + std::string start_time = this->CTest->CurrentTime(); + unsigned int start_time_time = + static_cast<unsigned int>(cmSystemTools::GetTime()); + double elapsed_time_start = cmSystemTools::GetTime(); + + bool updated = vc->Update(); + + os << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + << "<Update mode=\"Client\" Generator=\"ctest-" + << cmVersion::GetCMakeVersion() << "\">\n" + << "\t<Site>" << this->CTest->GetCTestConfiguration("Site") << "</Site>\n" + << "\t<BuildName>" << this->CTest->GetCTestConfiguration("BuildName") + << "</BuildName>\n" + << "\t<BuildStamp>" << this->CTest->GetCurrentTag() << "-" + << this->CTest->GetTestModelString() << "</BuildStamp>" << std::endl; + os << "\t<StartDateTime>" << start_time << "</StartDateTime>\n" + << "\t<StartTime>" << start_time_time << "</StartTime>\n" + << "\t<UpdateCommand>" + << cmXMLSafe(vc->GetUpdateCommandLine()).Quotes(false) + << "</UpdateCommand>\n" + << "\t<UpdateType>" << cmXMLSafe( + cmCTestUpdateHandlerUpdateToString(this->UpdateType)) + << "</UpdateType>\n"; + + vc->WriteXML(os); + + int localModifications = 0; + int numUpdated = vc->GetPathCount(cmCTestVC::PathUpdated); + if(numUpdated) + { + cmCTestLog(this->CTest, HANDLER_OUTPUT, + " Found " << numUpdated << " updated files\n"); + } + if(int numModified = vc->GetPathCount(cmCTestVC::PathModified)) + { + cmCTestLog(this->CTest, HANDLER_OUTPUT, + " Found " << numModified << " locally modified files\n"); + localModifications += numModified; + } + if(int numConflicting = vc->GetPathCount(cmCTestVC::PathConflicting)) + { + cmCTestLog(this->CTest, HANDLER_OUTPUT, + " Found " << numConflicting << " conflicting files\n"); + localModifications += numConflicting; + } + + cmCTestLog(this->CTest, DEBUG, "End" << std::endl); + std::string end_time = this->CTest->CurrentTime(); + os << "\t<EndDateTime>" << end_time << "</EndDateTime>\n" + << "\t<EndTime>" << static_cast<unsigned int>(cmSystemTools::GetTime()) + << "</EndTime>\n" + << "<ElapsedMinutes>" << + static_cast<int>((cmSystemTools::GetTime() - elapsed_time_start)/6)/10.0 + << "</ElapsedMinutes>\n" + << "\t<UpdateReturnStatus>"; + if(localModifications) + { + os << "Update error: There are modified or conflicting files in the " + "repository"; + cmCTestLog(this->CTest, ERROR_MESSAGE, + " There are modified or conflicting files in the repository" + << std::endl); + } + if(!updated) + { + os << "Update command failed:\n" << vc->GetUpdateCommandLine(); + cmCTestLog(this->CTest, ERROR_MESSAGE, " Update command failed: " + << vc->GetUpdateCommandLine() << "\n"); + } + os << "</UpdateReturnStatus>" << std::endl; + os << "</Update>" << std::endl; + return numUpdated; +} + +//---------------------------------------------------------------------- +int cmCTestUpdateHandler::DetectVCS(const char* dir) +{ + std::string sourceDirectory = dir; + cmCTestLog(this->CTest, DEBUG, "Check directory: " + << sourceDirectory.c_str() << std::endl); + sourceDirectory += "/.svn"; + if ( cmSystemTools::FileExists(sourceDirectory.c_str()) ) + { + return cmCTestUpdateHandler::e_SVN; + } + sourceDirectory = dir; + sourceDirectory += "/CVS"; + if ( cmSystemTools::FileExists(sourceDirectory.c_str()) ) + { + return cmCTestUpdateHandler::e_CVS; + } + sourceDirectory = dir; + sourceDirectory += "/.bzr"; + if ( cmSystemTools::FileExists(sourceDirectory.c_str()) ) + { + return cmCTestUpdateHandler::e_BZR; + } + sourceDirectory = dir; + sourceDirectory += "/.git"; + if ( cmSystemTools::FileExists(sourceDirectory.c_str()) ) + { + return cmCTestUpdateHandler::e_GIT; + } + sourceDirectory = dir; + sourceDirectory += "/.hg"; + if ( cmSystemTools::FileExists(sourceDirectory.c_str()) ) + { + return cmCTestUpdateHandler::e_HG; + } + return cmCTestUpdateHandler::e_UNKNOWN; +} + +//---------------------------------------------------------------------- +bool cmCTestUpdateHandler::SelectVCS() +{ + // Get update command + this->UpdateCommand = this->CTest->GetCTestConfiguration("UpdateCommand"); + + // Detect the VCS managing the source tree. + this->UpdateType = this->DetectVCS(this->GetOption("SourceDirectory")); + if (this->UpdateType == e_UNKNOWN) + { + // The source tree does not have a recognized VCS. Check the + // configuration value or command name. + this->UpdateType = this->DetermineType(this->UpdateCommand.c_str(), + this->CTest->GetCTestConfiguration("UpdateType").c_str()); + } + + // If no update command was specified, lookup one for this VCS tool. + if (this->UpdateCommand.empty()) + { + const char* key = 0; + switch (this->UpdateType) + { + case e_CVS: key = "CVSCommand"; break; + 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) + { + this->UpdateCommand = this->CTest->GetCTestConfiguration(key); + } + if (this->UpdateCommand.empty()) + { + cmOStringStream e; + e << "Cannot find UpdateCommand "; + if (key) + { + e << "or " << key; + } + e << " configuration key."; + cmCTestLog(this->CTest, ERROR_MESSAGE, e.str() << std::endl); + return false; + } + } + + return true; +} diff --git a/Source/CTest/cmCTestUpdateHandler.h b/Source/CTest/cmCTestUpdateHandler.h new file mode 100644 index 000000000..55ec974dc --- /dev/null +++ b/Source/CTest/cmCTestUpdateHandler.h @@ -0,0 +1,76 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2009 Kitware, Inc. + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ + +#ifndef cmCTestUpdateHandler_h +#define cmCTestUpdateHandler_h + + +#include "cmCTestGenericHandler.h" +#include "cmListFileCache.h" + +#if defined(__sgi) && !defined(__GNUC__) +# pragma set woff 1375 /* base class destructor not virtual */ +#endif + +/** \class cmCTestUpdateHandler + * \brief A class that handles ctest -S invocations + * + */ +class cmCTestUpdateHandler : public cmCTestGenericHandler +{ +public: + cmTypeMacro(cmCTestUpdateHandler, cmCTestGenericHandler); + + /* + * The main entry point for this class + */ + int ProcessHandler(); + + cmCTestUpdateHandler(); + + enum { + e_UNKNOWN = 0, + e_CVS, + e_SVN, + e_BZR, + e_GIT, + e_HG, + e_LAST + }; + + /** + * Initialize handler + */ + virtual void Initialize(); + +private: + // Some structures needed for update + struct StringPair : + public std::pair<std::string, std::string>{}; + struct UpdateFiles : public std::vector<StringPair>{}; + + // Determine the type of version control + int DetermineType(const char* cmd, const char* type); + + // The VCS command to update the working tree. + std::string UpdateCommand; + int UpdateType; + + int DetectVCS(const char* dir); + bool SelectVCS(); +}; + +#if defined(__sgi) && !defined(__GNUC__) +# pragma reset woff 1375 /* base class destructor not virtual */ +#endif + +#endif diff --git a/Source/CTest/cmCTestUploadCommand.cxx b/Source/CTest/cmCTestUploadCommand.cxx new file mode 100644 index 000000000..731c1c7a3 --- /dev/null +++ b/Source/CTest/cmCTestUploadCommand.cxx @@ -0,0 +1,69 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2009 Kitware, Inc., Insight Software Consortium + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ +#include "cmCTestUploadCommand.h" + +#include "cmCTest.h" +#include "cmCTestGenericHandler.h" +#include "cmCTestUploadHandler.h" + +cmCTestGenericHandler* cmCTestUploadCommand::InitializeHandler() +{ + cmCTestGenericHandler* handler + = this->CTest->GetInitializedHandler("upload"); + if ( !handler ) + { + this->SetError("internal CTest error. Cannot instantiate upload handler"); + return 0; + } + static_cast<cmCTestUploadHandler*>(handler)->SetFiles(this->Files); + + return handler; +} + + +//---------------------------------------------------------------------------- +bool cmCTestUploadCommand::CheckArgumentKeyword(std::string const& arg) +{ + if(arg == "FILES") + { + this->ArgumentDoing = ArgumentDoingFiles; + return true; + } + return false; +} + + +//---------------------------------------------------------------------------- +bool cmCTestUploadCommand::CheckArgumentValue(std::string const& arg) +{ + if(this->ArgumentDoing == ArgumentDoingFiles) + { + cmStdString filename(arg); + if(cmSystemTools::FileExists(filename.c_str())) + { + this->Files.insert(filename); + return true; + } + else + { + cmOStringStream e; + e << "File \"" << filename << "\" does not exist. Cannot submit " + << "a non-existent file."; + this->Makefile->IssueMessage(cmake::FATAL_ERROR, e.str()); + this->ArgumentDoing = ArgumentDoingError; + return false; + } + } + + // Look for other arguments. + return this->Superclass::CheckArgumentValue(arg); +} diff --git a/Source/CTest/cmCTestUploadCommand.h b/Source/CTest/cmCTestUploadCommand.h new file mode 100644 index 000000000..62f379f6d --- /dev/null +++ b/Source/CTest/cmCTestUploadCommand.h @@ -0,0 +1,85 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2009 Kitware, Inc., Insight Software Consortium + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ +#ifndef cmCTestUploadCommand_h +#define cmCTestUploadCommand_h + +#include "cmCTestHandlerCommand.h" +#include "cmCTest.h" + +/** \class cmCTestUpload + * \brief Run a ctest script + * + * cmCTestUploadCommand defines the command to upload result files for + * the project. + */ +class cmCTestUploadCommand : public cmCTestHandlerCommand +{ +public: + + cmCTestUploadCommand() + { + } + + /** + * This is a virtual constructor for the command. + */ + virtual cmCommand* Clone() + { + cmCTestUploadCommand* ni = new cmCTestUploadCommand; + ni->CTest = this->CTest; + ni->CTestScriptHandler = this->CTestScriptHandler; + return ni; + } + + /** + * The name of the command as specified in CMakeList.txt. + */ + virtual const char* GetName() const { return "ctest_upload";} + + /** + * Succinct documentation. + */ + virtual const char* GetTerseDocumentation() const + { + return "Upload files to a dashboard server."; + } + + /** + * More documentation. + */ + virtual const char* GetFullDocumentation() const + { + return + " ctest_upload(FILES ...)\n" + "Pass a list of files to be sent along with the build results to " + "the dashboard server.\n"; + } + + cmTypeMacro(cmCTestUploadCommand, cmCTestHandlerCommand); + +protected: + cmCTestGenericHandler* InitializeHandler(); + + virtual bool CheckArgumentKeyword(std::string const& arg); + virtual bool CheckArgumentValue(std::string const& arg); + + enum + { + ArgumentDoingFiles = Superclass::ArgumentDoingLast1, + ArgumentDoingLast2 + }; + + cmCTest::SetOfStrings Files; +}; + + +#endif diff --git a/Source/CTest/cmCTestUploadHandler.cxx b/Source/CTest/cmCTestUploadHandler.cxx new file mode 100644 index 000000000..caf2e5370 --- /dev/null +++ b/Source/CTest/cmCTestUploadHandler.cxx @@ -0,0 +1,77 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2009 Kitware, Inc. + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ +#include "cmCTestUploadHandler.h" + +#include "cmGeneratedFileStream.h" +#include "cmVersion.h" +#include "cmXMLSafe.h" + +//---------------------------------------------------------------------------- +cmCTestUploadHandler::cmCTestUploadHandler() +{ + this->Initialize(); +} + +//---------------------------------------------------------------------------- +void cmCTestUploadHandler::Initialize() +{ + this->Superclass::Initialize(); + this->Files.clear(); +} + +void cmCTestUploadHandler::SetFiles(const cmCTest::SetOfStrings& files) +{ + this->Files = files; +} + +//---------------------------------------------------------------------------- +int cmCTestUploadHandler::ProcessHandler() +{ + cmGeneratedFileStream ofs; + if ( !this->CTest->OpenOutputFile(this->CTest->GetCurrentTag(), + "Upload.xml", ofs)) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, + "Cannot open Upload.xml file" << std::endl); + return -1; + } + + cmCTest::SetOfStrings::const_iterator it; + ofs << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + << "<?xml-stylesheet type=\"text/xsl\" " + "href=\"Dart/Source/Server/XSL/Build.xsl " + "<file:///Dart/Source/Server/XSL/Build.xsl> \"?>\n" + << "<Site BuildName=\"" + << this->CTest->GetCTestConfiguration("BuildName") + << "\" BuildStamp=\"" + << this->CTest->GetCurrentTag() << "-" + << this->CTest->GetTestModelString() << "\" Name=\"" + << this->CTest->GetCTestConfiguration("Site") << "\" Generator=\"ctest" + << cmVersion::GetCMakeVersion() + << "\">\n"; + this->CTest->AddSiteProperties(ofs); + ofs << "<Upload>\n"; + + for ( it = this->Files.begin(); it != this->Files.end(); it ++ ) + { + cmCTestLog(this->CTest, OUTPUT, + "\tUpload file: " << it->c_str() << std::endl); + ofs << "<File filename=\"" << cmXMLSafe(*it) << "\">\n" + << "<Content encoding=\"base64\">\n"; + ofs << this->CTest->Base64EncodeFile(*it); + ofs << "\n</Content>\n" + << "</File>\n"; + } + ofs << "</Upload>\n" + << "</Site>\n"; + return 0; +} diff --git a/Source/CTest/cmCTestUploadHandler.h b/Source/CTest/cmCTestUploadHandler.h new file mode 100644 index 000000000..23ed35a25 --- /dev/null +++ b/Source/CTest/cmCTestUploadHandler.h @@ -0,0 +1,45 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2009 Kitware, Inc., Insight Software Consortium + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ +#ifndef cmCTestUploadHandler_h +#define cmCTestUploadHandler_h + +#include "cmCTestGenericHandler.h" + +/** \class cmCTestUploadHandler + * \brief Helper class for CTest + * + * Submit arbitrary files + * + */ +class cmCTestUploadHandler : public cmCTestGenericHandler +{ +public: + cmTypeMacro(cmCTestUploadHandler, cmCTestGenericHandler); + + cmCTestUploadHandler(); + ~cmCTestUploadHandler() {} + + /* + * The main entry point for this class + */ + int ProcessHandler(); + + void Initialize(); + + /** Specify a set of files to submit. */ + void SetFiles(cmCTest::SetOfStrings const& files); + +private: + cmCTest::SetOfStrings Files; +}; + +#endif diff --git a/Source/CTest/cmCTestVC.cxx b/Source/CTest/cmCTestVC.cxx new file mode 100644 index 000000000..fbee2272e --- /dev/null +++ b/Source/CTest/cmCTestVC.cxx @@ -0,0 +1,241 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2009 Kitware, Inc. + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ +#include "cmCTestVC.h" + +#include "cmCTest.h" +#include "cmSystemTools.h" +#include "cmXMLSafe.h" + +#include <cmsys/Process.h> + +//---------------------------------------------------------------------------- +cmCTestVC::cmCTestVC(cmCTest* ct, std::ostream& log): CTest(ct), Log(log) +{ + this->PathCount[PathUpdated] = 0; + this->PathCount[PathModified] = 0; + this->PathCount[PathConflicting] = 0; + this->Unknown.Date = "Unknown"; + this->Unknown.Author = "Unknown"; + this->Unknown.Rev = "Unknown"; +} + +//---------------------------------------------------------------------------- +cmCTestVC::~cmCTestVC() +{ +} + +//---------------------------------------------------------------------------- +void cmCTestVC::SetCommandLineTool(std::string const& tool) +{ + this->CommandLineTool = tool; +} + +//---------------------------------------------------------------------------- +void cmCTestVC::SetSourceDirectory(std::string const& dir) +{ + this->SourceDirectory = dir; +} + +//---------------------------------------------------------------------------- +bool cmCTestVC::InitialCheckout(const char* command) +{ + cmCTestLog(this->CTest, HANDLER_OUTPUT, + " First perform the initial checkout: " << command << "\n"); + + // Make the parent directory in which to perform the checkout. + std::string parent = cmSystemTools::GetFilenamePath(this->SourceDirectory); + cmCTestLog(this->CTest, HANDLER_OUTPUT, + " Perform checkout in directory: " << parent << "\n"); + if(!cmSystemTools::MakeDirectory(parent.c_str())) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, + "Cannot create directory: " << parent << std::endl); + return false; + } + + // Construct the initial checkout command line. + std::vector<cmStdString> args = cmSystemTools::ParseArguments(command); + std::vector<char const*> vc_co; + for(std::vector<cmStdString>::const_iterator ai = args.begin(); + ai != args.end(); ++ai) + { + vc_co.push_back(ai->c_str()); + } + vc_co.push_back(0); + + // Run the initial checkout command and log its output. + this->Log << "--- Begin Initial Checkout ---\n"; + OutputLogger out(this->Log, "co-out> "); + OutputLogger err(this->Log, "co-err> "); + bool result = this->RunChild(&vc_co[0], &out, &err, parent.c_str()); + this->Log << "--- End Initial Checkout ---\n"; + if(!result) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, + "Initial checkout failed!" << std::endl); + } + return result; +} + +//---------------------------------------------------------------------------- +bool cmCTestVC::RunChild(char const* const* cmd, OutputParser* out, + OutputParser* err, const char* workDir) +{ + this->Log << this->ComputeCommandLine(cmd) << "\n"; + + cmsysProcess* cp = cmsysProcess_New(); + cmsysProcess_SetCommand(cp, cmd); + workDir = workDir? workDir : this->SourceDirectory.c_str(); + cmsysProcess_SetWorkingDirectory(cp, workDir); + this->RunProcess(cp, out, err); + int result = cmsysProcess_GetExitValue(cp); + cmsysProcess_Delete(cp); + return result == 0; +} + +//---------------------------------------------------------------------------- +std::string cmCTestVC::ComputeCommandLine(char const* const* cmd) +{ + cmOStringStream line; + const char* sep = ""; + for(const char* const* arg = cmd; *arg; ++arg) + { + line << sep << "\"" << *arg << "\""; + sep = " "; + } + return line.str(); +} + +//---------------------------------------------------------------------------- +bool cmCTestVC::RunUpdateCommand(char const* const* cmd, + OutputParser* out, OutputParser* err) +{ + // Report the command line. + this->UpdateCommandLine = this->ComputeCommandLine(cmd); + if(this->CTest->GetShowOnly()) + { + this->Log << this->UpdateCommandLine << "\n"; + return true; + } + + // Run the command. + return this->RunChild(cmd, out, err); +} + +//---------------------------------------------------------------------------- +std::string cmCTestVC::GetNightlyTime() +{ + // Get the nightly start time corresponding to the current dau. + struct tm* t = this->CTest->GetNightlyTime( + this->CTest->GetCTestConfiguration("NightlyStartTime"), + this->CTest->GetTomorrowTag()); + char current_time[1024]; + sprintf(current_time, "%04d-%02d-%02d %02d:%02d:%02d", + t->tm_year + 1900, + t->tm_mon + 1, + t->tm_mday, + t->tm_hour, + t->tm_min, + t->tm_sec); + return std::string(current_time); +} + +//---------------------------------------------------------------------------- +void cmCTestVC::Cleanup() +{ + this->Log << "--- Begin Cleanup ---\n"; + this->CleanupImpl(); + this->Log << "--- End Cleanup ---\n"; +} + +//---------------------------------------------------------------------------- +void cmCTestVC::CleanupImpl() +{ + // We do no cleanup by default. +} + +//---------------------------------------------------------------------------- +bool cmCTestVC::Update() +{ + this->NoteOldRevision(); + this->Log << "--- Begin Update ---\n"; + bool result = this->UpdateImpl(); + this->Log << "--- End Update ---\n"; + this->NoteNewRevision(); + return result; +} + +//---------------------------------------------------------------------------- +void cmCTestVC::NoteOldRevision() +{ + // We do nothing by default. +} + +//---------------------------------------------------------------------------- +void cmCTestVC::NoteNewRevision() +{ + // We do nothing by default. +} + +//---------------------------------------------------------------------------- +bool cmCTestVC::UpdateImpl() +{ + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, + "* Unknown VCS tool, not updating!" << std::endl); + return true; +} + +//---------------------------------------------------------------------------- +bool cmCTestVC::WriteXML(std::ostream& xml) +{ + this->Log << "--- Begin Revisions ---\n"; + bool result = this->WriteXMLUpdates(xml); + this->Log << "--- End Revisions ---\n"; + return result; +} + +//---------------------------------------------------------------------------- +bool cmCTestVC::WriteXMLUpdates(std::ostream&) +{ + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, + "* CTest cannot extract updates for this VCS tool.\n"); + return true; +} + +//---------------------------------------------------------------------------- +void cmCTestVC::WriteXMLEntry(std::ostream& xml, + std::string const& path, + std::string const& name, + std::string const& full, + File const& f) +{ + static const char* desc[3] = { "Updated", "Modified", "Conflicting"}; + Revision const& rev = f.Rev? *f.Rev : this->Unknown; + std::string prior = f.PriorRev? f.PriorRev->Rev : std::string("Unknown"); + xml << "\t\t<" << desc[f.Status] << ">\n" + << "\t\t\t<File>" << cmXMLSafe(name) << "</File>\n" + << "\t\t\t<Directory>" << cmXMLSafe(path) << "</Directory>\n" + << "\t\t\t<FullName>" << cmXMLSafe(full) << "</FullName>\n" + << "\t\t\t<CheckinDate>" << cmXMLSafe(rev.Date) << "</CheckinDate>\n" + << "\t\t\t<Author>" << cmXMLSafe(rev.Author) << "</Author>\n" + << "\t\t\t<Email>" << cmXMLSafe(rev.EMail) << "</Email>\n" + << "\t\t\t<Committer>" << cmXMLSafe(rev.Committer) << "</Committer>\n" + << "\t\t\t<CommitterEmail>" << cmXMLSafe(rev.CommitterEMail) + << "</CommitterEmail>\n" + << "\t\t\t<CommitDate>" << cmXMLSafe(rev.CommitDate) + << "</CommitDate>\n" + << "\t\t\t<Log>" << cmXMLSafe(rev.Log) << "</Log>\n" + << "\t\t\t<Revision>" << cmXMLSafe(rev.Rev) << "</Revision>\n" + << "\t\t\t<PriorRevision>" << cmXMLSafe(prior) << "</PriorRevision>\n" + << "\t\t</" << desc[f.Status] << ">\n"; + ++this->PathCount[f.Status]; +} diff --git a/Source/CTest/cmCTestVC.h b/Source/CTest/cmCTestVC.h new file mode 100644 index 000000000..44e1dacbb --- /dev/null +++ b/Source/CTest/cmCTestVC.h @@ -0,0 +1,133 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2009 Kitware, Inc. + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ +#ifndef cmCTestVC_h +#define cmCTestVC_h + +#include "cmProcessTools.h" + +class cmCTest; + +/** \class cmCTestVC + * \brief Base class for version control system handlers + * + */ +class cmCTestVC: public cmProcessTools +{ +public: + /** Construct with a CTest instance and update log stream. */ + cmCTestVC(cmCTest* ctest, std::ostream& log); + + virtual ~cmCTestVC(); + + /** Command line tool to invoke. */ + void SetCommandLineTool(std::string const& tool); + + /** Top-level source directory. */ + void SetSourceDirectory(std::string const& dir); + + /** Get the date/time specification for the current nightly start time. */ + std::string GetNightlyTime(); + + /** Prepare the work tree. */ + bool InitialCheckout(const char* command); + + /** Perform cleanup operations on the work tree. */ + void Cleanup(); + + /** Update the working tree to the new revision. */ + bool Update(); + + /** Get the command line used by the Update method. */ + std::string const& GetUpdateCommandLine() const + { return this->UpdateCommandLine; } + + /** Write Update.xml entries for the updates found. */ + bool WriteXML(std::ostream& xml); + + /** Enumerate non-trivial working tree states during update. */ + enum PathStatus { PathUpdated, PathModified, PathConflicting }; + + /** Get the number of working tree paths in each state after update. */ + int GetPathCount(PathStatus s) const { return this->PathCount[s]; } + +protected: + // Internal API to be implemented by subclasses. + virtual void CleanupImpl(); + virtual void NoteOldRevision(); + virtual bool UpdateImpl(); + virtual void NoteNewRevision(); + virtual bool WriteXMLUpdates(std::ostream& xml); + + /** Basic information about one revision of a tree or file. */ + struct Revision + { + std::string Rev; + std::string Date; + std::string Author; + std::string EMail; + std::string Committer; + std::string CommitterEMail; + std::string CommitDate; + std::string Log; + }; + + struct File; + friend struct File; + + /** Represent change to one file. */ + struct File + { + PathStatus Status; + Revision const* Rev; + Revision const* PriorRev; + File(): Status(PathUpdated), Rev(0), PriorRev(0) {} + File(PathStatus status, Revision const* rev, Revision const* priorRev): + Status(status), Rev(rev), PriorRev(priorRev) {} + }; + + /** Convert a list of arguments to a human-readable command line. */ + static std::string ComputeCommandLine(char const* const* cmd); + + /** Run a command line and send output to given parsers. */ + bool RunChild(char const* const* cmd, OutputParser* out, + OutputParser* err, const char* workDir = 0); + + /** Run VC update command line and send output to given parsers. */ + bool RunUpdateCommand(char const* const* cmd, + OutputParser* out, OutputParser* err = 0); + + /** Write xml element for one file. */ + void WriteXMLEntry(std::ostream& xml, std::string const& path, + std::string const& name, std::string const& full, + File const& f); + + // Instance of cmCTest running the script. + cmCTest* CTest; + + // A stream to which we write log information. + std::ostream& Log; + + // Basic information about the working tree. + std::string CommandLineTool; + std::string SourceDirectory; + + // Record update command info. + std::string UpdateCommandLine; + + // Placeholder for unknown revisions. + Revision Unknown; + + // Count paths reported with each PathStatus value. + int PathCount[3]; +}; + +#endif diff --git a/Source/CTest/cmParseCacheCoverage.cxx b/Source/CTest/cmParseCacheCoverage.cxx new file mode 100644 index 000000000..137f344df --- /dev/null +++ b/Source/CTest/cmParseCacheCoverage.cxx @@ -0,0 +1,220 @@ +#include "cmStandardIncludes.h" +#include <stdio.h> +#include <stdlib.h> +#include "cmSystemTools.h" +#include "cmParseCacheCoverage.h" +#include <cmsys/Directory.hxx> +#include <cmsys/Glob.hxx> + + +cmParseCacheCoverage::cmParseCacheCoverage( + cmCTestCoverageHandlerContainer& cont, + cmCTest* ctest) + :cmParseMumpsCoverage(cont, ctest) +{ +} + + +bool cmParseCacheCoverage::LoadCoverageData(const char* d) +{ + // load all the .mcov files in the specified directory + cmsys::Directory dir; + if(!dir.Load(d)) + { + return false; + } + size_t numf; + unsigned int i; + numf = dir.GetNumberOfFiles(); + for (i = 0; i < numf; i++) + { + std::string file = dir.GetFile(i); + if(file != "." && file != ".." + && !cmSystemTools::FileIsDirectory(file.c_str())) + { + std::string path = d; + path += "/"; + path += file; + if(cmSystemTools::GetFilenameLastExtension(path) == ".cmcov") + { + if(!this->ReadCMCovFile(path.c_str())) + { + return false; + } + } + } + } + return true; +} + +// not currently used, but leave it in case we want it in the future +void cmParseCacheCoverage::RemoveUnCoveredFiles() +{ + // loop over the coverage data computed and remove all files + // that only have -1 or 0 for the lines. + cmCTestCoverageHandlerContainer::TotalCoverageMap::iterator ci = + this->Coverage.TotalCoverage.begin(); + while(ci != this->Coverage.TotalCoverage.end()) + { + cmCTestCoverageHandlerContainer::SingleFileCoverageVector& v = + ci->second; + bool nothing = true; + for(cmCTestCoverageHandlerContainer::SingleFileCoverageVector::iterator i= + v.begin(); i != v.end(); ++i) + { + if(*i > 0) + { + nothing = false; + break; + } + } + if(nothing) + { + cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, + "No coverage found in: " << ci->first + << std::endl); + this->Coverage.TotalCoverage.erase(ci++); + } + else + { + ++ci; + } + } +} + +bool cmParseCacheCoverage::SplitString(std::vector<std::string>& args, + std::string const& line) +{ + std::string::size_type pos1 = 0; + std::string::size_type pos2 = line.find(',', 0); + if(pos2 == std::string::npos) + { + return false; + } + std::string arg; + while(pos2 != std::string::npos) + { + arg = line.substr(pos1, pos2-pos1); + args.push_back(arg); + pos1 = pos2+1; + pos2 = line.find(',',pos1); + } + arg = line.substr(pos1); + args.push_back(arg); + return true; +} + +bool cmParseCacheCoverage::ReadCMCovFile(const char* file) +{ + std::ifstream in(file); + if(!in) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, + "Can not open : " + << file << "\n"); + return false; + } + std::string line; + std::vector<std::string> separateLine; + if(!cmSystemTools::GetLineFromStream(in, line)) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, + "Empty file : " + << file << " referenced in this line of cmcov data:\n" + "[" << line << "]\n"); + return false; + } + separateLine.clear(); + this->SplitString(separateLine, line); + if(separateLine.size() !=4 || separateLine[0] != "Routine" + || separateLine[1] != "Line" || separateLine[2] != "RtnLine" + || separateLine[3] != "Code") + { + cmCTestLog(this->CTest, ERROR_MESSAGE, + "Bad first line of cmcov file : " + << file << " line:\n" + "[" << line << "]\n"); + } + std::string routine; + std::string filepath; + while(cmSystemTools::GetLineFromStream(in, line)) + { + // clear out line argument vector + separateLine.clear(); + // parse the comma separated line + this->SplitString(separateLine, line); + // might have more because code could have a quoted , in it + // but we only care about the first 3 args anyway + if(separateLine.size() < 4) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, + "Bad line of cmcov file expected at least 4 found: " + << separateLine.size() << " " + << file << " line:\n" + "[" << line << "]\n"); + for(std::string::size_type i = 0; i < separateLine.size(); ++i) + { + cmCTestLog(this->CTest, ERROR_MESSAGE,"" + << separateLine[1] << " "); + } + cmCTestLog(this->CTest, ERROR_MESSAGE, "\n"); + return false; + } + // if we do not have a routine yet, then it should be + // the first argument in the vector + if(routine.size() == 0) + { + routine = separateLine[0]; + // Find the full path to the file + if(!this->FindMumpsFile(routine, filepath)) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, + "Could not find mumps file for routine: " + << routine << "\n"); + filepath = ""; + continue; // move to next line + } + } + // if we have a routine name, check for end of routine + else + { + // Totals in arg 0 marks the end of a routine + if(separateLine[0].substr(0, 6) == "Totals") + { + routine = ""; // at the end of this routine + filepath = ""; + continue; // move to next line + } + } + // if the file path was not found for the routine + // move to next line. We should have already warned + // after the call to FindMumpsFile that we did not find + // it, so don't report again to cut down on output + if(filepath.size() == 0) + { + continue; + } + // now we are ready to set the coverage from the line of data + cmCTestCoverageHandlerContainer::SingleFileCoverageVector& + coverageVector = this->Coverage.TotalCoverage[filepath]; + std::string::size_type linenumber = atoi(separateLine[1].c_str()) -1; + int count = atoi(separateLine[2].c_str()); + if(linenumber > coverageVector.size()) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, + "Parse error line is greater than number of lines in file: " + << linenumber << " " << filepath << "\n"); + continue; // skip setting count to avoid crash + } + // now add to count for linenumber + // for some reason the cache coverage adds extra lines to the + // end of the file in some cases. Since they do not exist, we will + // mark them as non executable + while(linenumber >= coverageVector.size()) + { + coverageVector.push_back(-1); + } + coverageVector[linenumber] += count; + } + return true; +} diff --git a/Source/CTest/cmParseCacheCoverage.h b/Source/CTest/cmParseCacheCoverage.h new file mode 100644 index 000000000..114eb92d3 --- /dev/null +++ b/Source/CTest/cmParseCacheCoverage.h @@ -0,0 +1,42 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2009 Kitware, Inc. + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ + +#ifndef cmParseCacheCoverage_h +#define cmParseCacheCoverage_h + +#include "cmParseMumpsCoverage.h" + +/** \class cmParseCacheCoverage + * \brief Parse Cache coverage information + * + * This class is used to parse Cache coverage information for + * mumps. + */ +class cmParseCacheCoverage : public cmParseMumpsCoverage +{ +public: + cmParseCacheCoverage(cmCTestCoverageHandlerContainer& cont, + cmCTest* ctest); +protected: + // implement virtual from parent + bool LoadCoverageData(const char* dir); + // remove files with no coverage + void RemoveUnCoveredFiles(); + // Read a single mcov file + bool ReadCMCovFile(const char* f); + // split a string based on , + bool SplitString(std::vector<std::string>& args, + std::string const& line); +}; + + +#endif diff --git a/Source/CTest/cmParseGTMCoverage.cxx b/Source/CTest/cmParseGTMCoverage.cxx new file mode 100644 index 000000000..5dfcfe50a --- /dev/null +++ b/Source/CTest/cmParseGTMCoverage.cxx @@ -0,0 +1,272 @@ +#include "cmStandardIncludes.h" +#include <stdio.h> +#include <stdlib.h> +#include "cmSystemTools.h" +#include "cmParseGTMCoverage.h" +#include <cmsys/Directory.hxx> +#include <cmsys/Glob.hxx> + + +cmParseGTMCoverage::cmParseGTMCoverage(cmCTestCoverageHandlerContainer& cont, + cmCTest* ctest) + :cmParseMumpsCoverage(cont, ctest) +{ +} + + +bool cmParseGTMCoverage::LoadCoverageData(const char* d) +{ + // load all the .mcov files in the specified directory + cmsys::Directory dir; + if(!dir.Load(d)) + { + return false; + } + size_t numf; + unsigned int i; + numf = dir.GetNumberOfFiles(); + for (i = 0; i < numf; i++) + { + std::string file = dir.GetFile(i); + if(file != "." && file != ".." + && !cmSystemTools::FileIsDirectory(file.c_str())) + { + std::string path = d; + path += "/"; + path += file; + if(cmSystemTools::GetFilenameLastExtension(path) == ".mcov") + { + if(!this->ReadMCovFile(path.c_str())) + { + return false; + } + } + } + } + return true; +} + +bool cmParseGTMCoverage::ReadMCovFile(const char* file) +{ + std::ifstream in(file); + if(!in) + { + return false; + } + std::string line; + std::string lastfunction; + std::string lastroutine; + std::string lastpath; + int lastoffset = 0; + while( cmSystemTools::GetLineFromStream(in, line)) + { + // only look at lines that have coverage data + if(line.find("^ZZCOVERAGE") == line.npos) + { + continue; + } + std::string filepath; + std::string function; + std::string routine; + int linenumber = 0; + int count = 0; + this->ParseMCOVLine(line, routine, function, linenumber, count); + // skip this one + if(routine == "RSEL") + { + continue; + } + // no need to search the file if we just did it + if(function == lastfunction && lastroutine == routine) + { + if(lastpath.size()) + { + this->Coverage.TotalCoverage[lastpath][lastoffset + linenumber] + += count; + } + else + { + cmCTestLog(this->CTest, ERROR_MESSAGE, + "Can not find mumps file : " + << lastroutine << + " referenced in this line of mcov data:\n" + "[" << line << "]\n"); + } + continue; + } + // Find the full path to the file + bool found = this->FindMumpsFile(routine, filepath); + if(found) + { + int lineoffset; + if(this->FindFunctionInMumpsFile(filepath, + function, + lineoffset)) + { + cmCTestCoverageHandlerContainer::SingleFileCoverageVector& + coverageVector = this->Coverage.TotalCoverage[filepath]; + coverageVector[lineoffset + linenumber] += count; + } + lastoffset = lineoffset; + } + else + { + cmCTestLog(this->CTest, ERROR_MESSAGE, + "Can not find mumps file : " + << routine << " referenced in this line of mcov data:\n" + "[" << line << "]\n"); + } + lastfunction = function; + lastroutine = routine; + lastpath = filepath; + } + return true; +} + +bool cmParseGTMCoverage::FindFunctionInMumpsFile(std::string const& filepath, + std::string const& function, + int& lineoffset) +{ + std::ifstream in(filepath.c_str()); + if(!in) + { + return false; + } + std::string line; + int linenum = 0; + while( cmSystemTools::GetLineFromStream(in, line)) + { + std::string::size_type pos = line.find(function.c_str()); + if(pos == 0) + { + char nextchar = line[function.size()]; + if(nextchar == ' ' || nextchar == '(') + { + lineoffset = linenum; + return true; + } + } + if(pos == 1) + { + char prevchar = line[0]; + char nextchar = line[function.size()+1]; + if(prevchar == '%' && (nextchar == ' ' || nextchar == '(')) + { + lineoffset = linenum; + return true; + } + } + linenum++; // move to next line count + } + lineoffset = 0; + cmCTestLog(this->CTest, ERROR_MESSAGE, + "Could not find entry point : " + << function << " in " << filepath << "\n"); + return false; +} + +bool cmParseGTMCoverage::ParseMCOVLine(std::string const& line, + std::string& routine, + std::string& function, + int& linenumber, + int& count) +{ + // this method parses lines from the .mcov file + // each line has ^COVERAGE(...) in it, and there + // are several varients of coverage lines: + // + // ^COVERAGE("DIC11","PR1",0)="2:0:0:0" + // ( file , entry, line ) = "number_executed:timing_info" + // ^COVERAGE("%RSEL","SRC")="1:0:0:0" + // ( file , entry ) = "number_executed:timing_info" + // ^COVERAGE("%RSEL","init",8,"FOR_LOOP",1)=1 + // ( file , entry, line, IGNORE ) =number_executed + std::vector<cmStdString> args; + std::string::size_type pos = line.find('(', 0); + // if no ( is found, then return line has no coverage + if(pos == std::string::npos) + { + return false; + } + std::string arg; + bool done = false; + // separate out all of the comma separated arguments found + // in the COVERAGE(...) line + while(line[pos] && !done) + { + // save the char we are looking at + char cur = line[pos]; + // , or ) means end of argument + if(cur == ',' || cur == ')') + { + // save the argument into the argument vector + args.push_back(arg); + // start on a new argument + arg = ""; + // if we are at the end of the ), then finish while loop + if(cur == ')') + { + done = true; + } + } + else + { + // all chars except ", (, and % get stored in the arg string + if(cur != '\"' && cur != '(' && cur != '%') + { + arg.append(1, line[pos]); + } + } + // move to next char + pos++; + } + // now parse the right hand side of the = + pos = line.find('='); + // no = found, this is an error + if(pos == line.npos) + { + return false; + } + pos++; // move past = + + // if the next positing is not a ", then this is a + // COVERAGE(..)=count line and turn the rest of the string + // past the = into an integer and set it to count + if(line[pos] != '\"') + { + count = atoi(line.substr(pos).c_str()); + } + else + { + // this means line[pos] is a ", and we have a + // COVERAGE(...)="1:0:0:0" type of line + pos++; // move past " + // find the first : past the " + std::string::size_type pos2 = line.find(':', pos); + // turn the string between the " and the first : into an integer + // and set it to count + count = atoi(line.substr(pos, pos2-pos).c_str()); + } + // less then two arguments is an error + if(args.size() < 2) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, + "Error parsing mcov line: [" << line << "]\n"); + return false; + } + routine = args[0]; // the routine is the first argument + function = args[1]; // the function in the routine is the second + // in the two argument only format + // ^COVERAGE("%RSEL","SRC"), the line offset is 0 + if(args.size() == 2) + { + linenumber = 0; + } + else + { + // this is the format for this line + // ^COVERAGE("%RSEL","SRC",count) + linenumber = atoi(args[2].c_str()); + } + return true; +} diff --git a/Source/CTest/cmParseGTMCoverage.h b/Source/CTest/cmParseGTMCoverage.h new file mode 100644 index 000000000..c6d7ef919 --- /dev/null +++ b/Source/CTest/cmParseGTMCoverage.h @@ -0,0 +1,49 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2009 Kitware, Inc. + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ + +#ifndef cmParseGTMCoverage_h +#define cmParseGTMCoverage_h + +#include "cmParseMumpsCoverage.h" + +/** \class cmParseGTMCoverage + * \brief Parse GTM coverage information + * + * This class is used to parse GTM coverage information for + * mumps. + */ +class cmParseGTMCoverage : public cmParseMumpsCoverage +{ +public: + cmParseGTMCoverage(cmCTestCoverageHandlerContainer& cont, + cmCTest* ctest); +protected: + // implement virtual from parent + bool LoadCoverageData(const char* dir); + // Read a single mcov file + bool ReadMCovFile(const char* f); + // find out what line in a mumps file (filepath) the given entry point + // or function is. lineoffset is set by this method. + bool FindFunctionInMumpsFile(std::string const& filepath, + std::string const& function, + int& lineoffset); + // parse a line from a .mcov file, and fill in the + // routine, function, linenumber and coverage count + bool ParseMCOVLine(std::string const& line, + std::string& routine, + std::string& function, + int& linenumber, + int& count); +}; + + +#endif diff --git a/Source/CTest/cmParseMumpsCoverage.cxx b/Source/CTest/cmParseMumpsCoverage.cxx new file mode 100644 index 000000000..37e8bd082 --- /dev/null +++ b/Source/CTest/cmParseMumpsCoverage.cxx @@ -0,0 +1,165 @@ +#include "cmStandardIncludes.h" +#include <stdio.h> +#include <stdlib.h> +#include "cmSystemTools.h" +#include "cmParseGTMCoverage.h" +#include <cmsys/Directory.hxx> +#include <cmsys/Glob.hxx> + + +cmParseMumpsCoverage::cmParseMumpsCoverage( + cmCTestCoverageHandlerContainer& cont, + cmCTest* ctest) + :Coverage(cont), CTest(ctest) +{ +} + +cmParseMumpsCoverage::~cmParseMumpsCoverage() +{ +} + +bool cmParseMumpsCoverage::ReadCoverageFile(const char* file) +{ + // Read the gtm_coverage.mcov file, that has two lines of data: + // packages:/full/path/to/Vista/Packages + // coverage_dir:/full/path/to/dir/with/*.mcov + std::ifstream in(file); + if(!in) + { + return false; + } + std::string line; + while(cmSystemTools::GetLineFromStream(in, line)) + { + std::string::size_type pos = line.find(':', 0); + std::string packages; + if(pos != std::string::npos) + { + std::string type = line.substr(0, pos); + std::string path = line.substr(pos+1); + if(type == "packages") + { + this->LoadPackages(path.c_str()); + } + else if(type == "coverage_dir") + { + this->LoadCoverageData(path.c_str()); + } + else + { + cmCTestLog(this->CTest, ERROR_MESSAGE, + "Parse Error in Mumps coverage file :\n" + << file << + "\ntype: [" << type << "]\npath:[" << path << "]\n" + "input line: [" << line << "]\n"); + } + } + } + return true; +} + +void cmParseMumpsCoverage::InitializeMumpsFile(std::string& file) +{ + // initialize the coverage information for a given mumps file + std::ifstream in(file.c_str()); + if(!in) + { + return; + } + std::string line; + cmCTestCoverageHandlerContainer::SingleFileCoverageVector& + coverageVector = this->Coverage.TotalCoverage[file]; + if(!cmSystemTools::GetLineFromStream(in, line)) + { + return; + } + // first line of a .m file can never be run + coverageVector.push_back(-1); + while( cmSystemTools::GetLineFromStream(in, line) ) + { + // putting in a 0 for a line means it is executable code + // putting in a -1 for a line means it is not executable code + int val = -1; // assume line is not executable + bool found = false; + std::string::size_type i = 0; + // (1) Search for the first whitespace or semicolon character on a line. + //This will skip over labels if the line starts with one, or will simply + //be the first character on the line for non-label lines. + for(; i < line.size(); ++i) + { + if(line[i] == ' ' || line[i] == '\t' || line[i] == ';') + { + found = true; + break; + } + } + if(found) + { + // (2) If the first character found above is whitespace then continue the + // search for the first following non-whitespace character. + if(line[i] == ' ' || line[i] == '\t') + { + while(i < line.size() && (line[i] == ' ' || line[i] == '\t')) + { + i++; + } + } + // (3) If the character found is not a semicolon then the line counts for + // coverage. + if(i < line.size() && line[i] != ';') + { + val = 0; + } + } + coverageVector.push_back(val); + } +} + +bool cmParseMumpsCoverage::LoadPackages(const char* d) +{ + cmsys::Glob glob; + glob.RecurseOn(); + std::string pat = d; + pat += "/*.m"; + glob.FindFiles(pat.c_str()); + std::vector<std::string>& files = glob.GetFiles(); + std::vector<std::string>::iterator fileIt; + for ( fileIt = files.begin(); fileIt != files.end(); + ++ fileIt ) + { + std::string name = cmSystemTools::GetFilenameName(*fileIt); + this->RoutineToDirectory[name.substr(0, name.size()-2)] = *fileIt; + // initialze each file, this is left out until CDash is fixed + // to handle large numbers of files + this->InitializeMumpsFile(*fileIt); + } + return true; +} + +bool cmParseMumpsCoverage::FindMumpsFile(std::string const& routine, + std::string& filepath) +{ + std::map<cmStdString, cmStdString>::iterator i = + this->RoutineToDirectory.find(routine); + if(i != this->RoutineToDirectory.end()) + { + filepath = i->second; + return true; + } + else + { + // try some alternate names + const char* tryname[] = {"GUX", "GTM", "ONT", 0}; + for(int k=0; tryname[k] != 0; k++) + { + std::string routine2 = routine + tryname[k]; + i = this->RoutineToDirectory.find(routine2); + if(i != this->RoutineToDirectory.end()) + { + filepath = i->second; + return true; + } + } + } + return false; +} diff --git a/Source/CTest/cmParseMumpsCoverage.h b/Source/CTest/cmParseMumpsCoverage.h new file mode 100644 index 000000000..c1effa79b --- /dev/null +++ b/Source/CTest/cmParseMumpsCoverage.h @@ -0,0 +1,52 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2009 Kitware, Inc. + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ + +#ifndef cmParseMumpsCoverage_h +#define cmParseMumpsCoverage_h + +#include "cmStandardIncludes.h" +#include "cmCTestCoverageHandler.h" + +/** \class cmParseMumpsCoverage + * \brief Parse Mumps coverage information + * + * This class is used as the base class for Mumps coverage + * parsing. + */ +class cmParseMumpsCoverage +{ +public: + cmParseMumpsCoverage(cmCTestCoverageHandlerContainer& cont, + cmCTest* ctest); + virtual ~cmParseMumpsCoverage(); + // This is the toplevel coverage file locating the coverage files + // and the mumps source code package tree. + bool ReadCoverageFile(const char* file); +protected: + // sub classes will use this to + // load all coverage files found in the given directory + virtual bool LoadCoverageData(const char* d) = 0; + // search the package directory for mumps files and fill + // in the RoutineToDirectory map + bool LoadPackages(const char* dir); + // initialize the coverage information for a single mumps file + void InitializeMumpsFile(std::string& file); + // Find mumps file for routine + bool FindMumpsFile(std::string const& routine, + std::string& filepath); +protected: + std::map<cmStdString, cmStdString> RoutineToDirectory; + cmCTestCoverageHandlerContainer& Coverage; + cmCTest* CTest; +}; + +#endif diff --git a/Source/CTest/cmParsePHPCoverage.cxx b/Source/CTest/cmParsePHPCoverage.cxx new file mode 100644 index 000000000..593b2d1a8 --- /dev/null +++ b/Source/CTest/cmParsePHPCoverage.cxx @@ -0,0 +1,253 @@ +#include "cmStandardIncludes.h" +#include "cmSystemTools.h" +#include "cmParsePHPCoverage.h" +#include <cmsys/Directory.hxx> + +/* + To setup coverage for php. + + - edit php.ini to add auto prepend and append php files from phpunit + auto_prepend_file = + auto_append_file = + - run the tests + - run this program on all the files in c:/tmp + +*/ + +cmParsePHPCoverage::cmParsePHPCoverage(cmCTestCoverageHandlerContainer& cont, + cmCTest* ctest) + :Coverage(cont), CTest(ctest) +{ +} + +bool cmParsePHPCoverage::ReadUntil(std::ifstream& in, char until) +{ + char c = 0; + while(in.get(c) && c != until) + { + } + if(c != until) + { + return false; + } + return true; +} +bool cmParsePHPCoverage::ReadCoverageArray(std::ifstream& in, + cmStdString const& fileName) +{ + cmCTestCoverageHandlerContainer::SingleFileCoverageVector& coverageVector + = this->Coverage.TotalCoverage[fileName]; + + char c; + char buf[4]; + in.read(buf, 3); + buf[3] = 0; + if(strcmp(buf, ";a:") != 0) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, + "failed to read start of coverage array, found : " + << buf << "\n"); + return false; + } + int size = 0; + if(!this->ReadInt(in, size)) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, + "failed to read size "); + return false; + } + if(!in.get(c) && c == '{') + { + cmCTestLog(this->CTest, ERROR_MESSAGE, + "failed to read open {\n"); + return false; + } + for(int i =0; i < size; i++) + { + this->ReadUntil(in, ':'); + int line = 0; + this->ReadInt(in, line); + // ok xdebug may have a bug here + // it seems to be 1 based but often times + // seems to have a 0'th line. + line--; + if(line < 0) + { + line = 0; + } + this->ReadUntil(in, ':'); + int value = 0; + this->ReadInt(in, value); + // make sure the vector is the right size and is + // initialized with -1 for each line + while(coverageVector.size() <= static_cast<size_t>(line) ) + { + coverageVector.push_back(-1); + } + // if value is less than 0, set it to zero + // TODO figure out the difference between + // -1 and -2 in xdebug coverage?? For now + // assume less than 0 is just not covered + // CDash expects -1 for non executable code (like comments) + // and 0 for uncovered code, and a positive value + // for number of times a line was executed + if(value < 0) + { + value = 0; + } + // if unset then set it to value + if(coverageVector[line] == -1) + { + coverageVector[line] = value; + } + // otherwise increment by value + else + { + coverageVector[line] += value; + } + } + return true; +} + +bool cmParsePHPCoverage::ReadInt(std::ifstream& in, int& v) +{ + std::string s; + char c = 0; + while(in.get(c) && c != ':' && c != ';') + { + s += c; + } + v = atoi(s.c_str()); + return true; +} + +bool cmParsePHPCoverage::ReadArraySize(std::ifstream& in, int& size) +{ + char c = 0; + in.get(c); + if(c != 'a') + { + return false; + } + if(in.get(c) && c == ':') + { + if(this->ReadInt(in, size)) + { + return true; + } + } + return false; +} + +bool cmParsePHPCoverage::ReadFileInformation(std::ifstream& in) +{ + char buf[4]; + in.read(buf, 2); + buf[2] = 0; + if(strcmp(buf, "s:") != 0) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, + "failed to read start of file info found: [" << buf << "]\n"); + return false; + } + char c; + int size = 0; + if(this->ReadInt(in, size)) + { + size++; // add one for null termination + char* s = new char[size+1]; + // read open quote + if(in.get(c) && c != '"') + { + delete[] s; + return false; + } + // read the string data + in.read(s, size-1); + s[size-1] = 0; + cmStdString fileName = s; + delete [] s; + // read close quote + if(in.get(c) && c != '"') + { + cmCTestLog(this->CTest, ERROR_MESSAGE, + "failed to read close quote\n" + << "read [" << c << "]\n"); + return false; + } + if(!this->ReadCoverageArray(in, fileName) ) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, + "failed to read coverage array for file: " + << fileName << "\n"); + return false; + } + return true; + } + return false; +} + + +bool cmParsePHPCoverage::ReadPHPData(const char* file) +{ + std::ifstream in(file); + if(!in) + { + return false; + } + int size = 0; + this->ReadArraySize(in, size); + char c = 0; + in.get(c); + if(c != '{') + { + cmCTestLog(this->CTest, ERROR_MESSAGE, + "failed to read open array\n"); + return false; + } + for(int i =0; i < size; i++) + { + if(!this->ReadFileInformation(in)) + { + cmCTestLog(this->CTest, ERROR_MESSAGE, + "Failed to read file #" << i << "\n"); + return false; + } + in.get(c); + if(c != '}') + { + cmCTestLog(this->CTest, ERROR_MESSAGE, + "failed to read close array\n"); + return false; + } + } + return true; +} + +bool cmParsePHPCoverage::ReadPHPCoverageDirectory(const char* d) +{ + cmsys::Directory dir; + if(!dir.Load(d)) + { + return false; + } + size_t numf; + unsigned int i; + numf = dir.GetNumberOfFiles(); + for (i = 0; i < numf; i++) + { + std::string file = dir.GetFile(i); + if(file != "." && file != ".." + && !cmSystemTools::FileIsDirectory(file.c_str())) + { + std::string path = d; + path += "/"; + path += file; + if(!this->ReadPHPData(path.c_str())) + { + return false; + } + } + } + return true; +} diff --git a/Source/CTest/cmParsePHPCoverage.h b/Source/CTest/cmParsePHPCoverage.h new file mode 100644 index 000000000..d50a83c58 --- /dev/null +++ b/Source/CTest/cmParsePHPCoverage.h @@ -0,0 +1,45 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2009 Kitware, Inc. + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ + +#ifndef cmParsePHPCoverage_h +#define cmParsePHPCoverage_h + +#include "cmStandardIncludes.h" +#include "cmCTestCoverageHandler.h" + +/** \class cmParsePHPCoverage + * \brief Parse xdebug PHP coverage information + * + * This class is used to parse php coverage information produced + * by xdebug. The data is stored as a php dump of the array + * return by xdebug coverage. It is an array of arrays. + */ +class cmParsePHPCoverage +{ +public: + cmParsePHPCoverage(cmCTestCoverageHandlerContainer& cont, + cmCTest* ctest); + bool ReadPHPCoverageDirectory(const char* dir); + void PrintCoverage(); +private: + bool ReadPHPData(const char* file); + bool ReadArraySize(std::ifstream& in, int& size); + bool ReadFileInformation(std::ifstream& in); + bool ReadInt(std::ifstream& in, int& v); + bool ReadCoverageArray(std::ifstream& in, cmStdString const&); + bool ReadUntil(std::ifstream& in, char until); + cmCTestCoverageHandlerContainer& Coverage; + cmCTest* CTest; +}; + + +#endif diff --git a/Source/CTest/cmProcess.cxx b/Source/CTest/cmProcess.cxx new file mode 100644 index 000000000..0ee631fe9 --- /dev/null +++ b/Source/CTest/cmProcess.cxx @@ -0,0 +1,272 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2009 Kitware, Inc., Insight Software Consortium + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ + +#include <cmProcess.h> +#include <cmSystemTools.h> + +cmProcess::cmProcess() +{ + this->Process = 0; + this->Timeout = 0; + this->TotalTime = 0; + this->ExitValue = 0; + this->Id = 0; + this->StartTime = 0; +} + +cmProcess::~cmProcess() +{ + cmsysProcess_Delete(this->Process); +} +void cmProcess::SetCommand(const char* command) +{ + this->Command = command; +} + +void cmProcess::SetCommandArguments(std::vector<std::string> const& args) +{ + this->Arguments = args; +} + +bool cmProcess::StartProcess() +{ + if(this->Command.size() == 0) + { + return false; + } + this->StartTime = cmSystemTools::GetTime(); + this->ProcessArgs.clear(); + // put the command as arg0 + this->ProcessArgs.push_back(this->Command.c_str()); + // now put the command arguments in + for(std::vector<std::string>::iterator i = this->Arguments.begin(); + i != this->Arguments.end(); ++i) + { + this->ProcessArgs.push_back(i->c_str()); + } + this->ProcessArgs.push_back(0); // null terminate the list + this->Process = cmsysProcess_New(); + cmsysProcess_SetCommand(this->Process, &*this->ProcessArgs.begin()); + if(this->WorkingDirectory.size()) + { + cmsysProcess_SetWorkingDirectory(this->Process, + this->WorkingDirectory.c_str()); + } + cmsysProcess_SetTimeout(this->Process, this->Timeout); + cmsysProcess_Execute(this->Process); + return (cmsysProcess_GetState(this->Process) + == cmsysProcess_State_Executing); +} + +//---------------------------------------------------------------------------- +bool cmProcess::Buffer::GetLine(std::string& line) +{ + // Scan for the next newline. + for(size_type sz = this->size(); this->Last != sz; ++this->Last) + { + if((*this)[this->Last] == '\n' || (*this)[this->Last] == '\0') + { + // Extract the range first..last as a line. + const char* text = &*this->begin() + this->First; + size_type length = this->Last - this->First; + while(length && text[length-1] == '\r') + { + length --; + } + line.assign(text, length); + + // Start a new range for the next line. + ++this->Last; + this->First = Last; + + // Return the line extracted. + return true; + } + } + + // Available data have been exhausted without a newline. + if(this->First != 0) + { + // Move the partial line to the beginning of the buffer. + this->erase(this->begin(), this->begin() + this->First); + this->First = 0; + this->Last = this->size(); + } + return false; +} + +//---------------------------------------------------------------------------- +bool cmProcess::Buffer::GetLast(std::string& line) +{ + // Return the partial last line, if any. + if(!this->empty()) + { + line.assign(&*this->begin(), this->size()); + this->First = this->Last = 0; + this->clear(); + return true; + } + return false; +} + +//---------------------------------------------------------------------------- +int cmProcess::GetNextOutputLine(std::string& line, double timeout) +{ + for(;;) + { + // Look for lines already buffered. + if(this->StdOut.GetLine(line)) + { + return cmsysProcess_Pipe_STDOUT; + } + else if(this->StdErr.GetLine(line)) + { + return cmsysProcess_Pipe_STDERR; + } + + // Check for more data from the process. + char* data; + int length; + int p = cmsysProcess_WaitForData(this->Process, &data, &length, &timeout); + if(p == cmsysProcess_Pipe_Timeout) + { + return cmsysProcess_Pipe_Timeout; + } + else if(p == cmsysProcess_Pipe_STDOUT) + { + this->StdOut.insert(this->StdOut.end(), data, data+length); + } + else if(p == cmsysProcess_Pipe_STDERR) + { + this->StdErr.insert(this->StdErr.end(), data, data+length); + } + else // p == cmsysProcess_Pipe_None + { + // The process will provide no more data. + break; + } + } + + // Look for partial last lines. + if(this->StdOut.GetLast(line)) + { + return cmsysProcess_Pipe_STDOUT; + } + else if(this->StdErr.GetLast(line)) + { + return cmsysProcess_Pipe_STDERR; + } + + // No more data. Wait for process exit. + if(!cmsysProcess_WaitForExit(this->Process, &timeout)) + { + return cmsysProcess_Pipe_Timeout; + } + + // Record exit information. + this->ExitValue = cmsysProcess_GetExitValue(this->Process); + this->TotalTime = cmSystemTools::GetTime() - this->StartTime; + // std::cerr << "Time to run: " << this->TotalTime << "\n"; + return cmsysProcess_Pipe_None; +} + +// return the process status +int cmProcess::GetProcessStatus() +{ + if(!this->Process) + { + return cmsysProcess_State_Exited; + } + return cmsysProcess_GetState(this->Process); +} + +int cmProcess::ReportStatus() +{ + int result = 1; + switch(cmsysProcess_GetState(this->Process)) + { + case cmsysProcess_State_Starting: + { + std::cerr << "cmProcess: Never started " + << this->Command << " process.\n"; + } break; + case cmsysProcess_State_Error: + { + std::cerr << "cmProcess: Error executing " << this->Command + << " process: " + << cmsysProcess_GetErrorString(this->Process) + << "\n"; + } break; + case cmsysProcess_State_Exception: + { + std::cerr << "cmProcess: " << this->Command + << " process exited with an exception: "; + switch(cmsysProcess_GetExitException(this->Process)) + { + case cmsysProcess_Exception_None: + { + std::cerr << "None"; + } break; + case cmsysProcess_Exception_Fault: + { + std::cerr << "Segmentation fault"; + } break; + case cmsysProcess_Exception_Illegal: + { + std::cerr << "Illegal instruction"; + } break; + case cmsysProcess_Exception_Interrupt: + { + std::cerr << "Interrupted by user"; + } break; + case cmsysProcess_Exception_Numerical: + { + std::cerr << "Numerical exception"; + } break; + case cmsysProcess_Exception_Other: + { + std::cerr << "Unknown"; + } break; + } + std::cerr << "\n"; + } break; + case cmsysProcess_State_Executing: + { + std::cerr << "cmProcess: Never terminated " << + this->Command << " process.\n"; + } break; + case cmsysProcess_State_Exited: + { + result = cmsysProcess_GetExitValue(this->Process); + std::cerr << "cmProcess: " << this->Command + << " process exited with code " + << result << "\n"; + } break; + case cmsysProcess_State_Expired: + { + std::cerr << "cmProcess: killed " << this->Command + << " process due to timeout.\n"; + } break; + case cmsysProcess_State_Killed: + { + std::cerr << "cmProcess: killed " << this->Command << " process.\n"; + } break; + } + return result; + +} + + +int cmProcess::GetExitException() +{ + return cmsysProcess_GetExitException(this->Process); +} diff --git a/Source/CTest/cmProcess.h b/Source/CTest/cmProcess.h new file mode 100644 index 000000000..ff99ca23d --- /dev/null +++ b/Source/CTest/cmProcess.h @@ -0,0 +1,82 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2000-2009 Kitware, Inc., Insight Software Consortium + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ +#ifndef cmProcess_h +#define cmProcess_h + + +#include "cmStandardIncludes.h" +#include <cmsys/Process.h> + + +/** \class cmProcess + * \brief run a process with c++ + * + * cmProcess wraps the kwsys process stuff in a c++ class. + */ +class cmProcess +{ +public: + cmProcess(); + ~cmProcess(); + const char* GetCommand() { return this->Command.c_str();} + void SetCommand(const char* command); + void SetCommandArguments(std::vector<std::string> const& arg); + void SetWorkingDirectory(const char* dir) { this->WorkingDirectory = dir;} + void SetTimeout(double t) { this->Timeout = t;} + // Return true if the process starts + bool StartProcess(); + + // return the process status + int GetProcessStatus(); + // Report the status of the program + int ReportStatus(); + int GetId() { return this->Id; } + void SetId(int id) { this->Id = id;} + int GetExitValue() { return this->ExitValue;} + double GetTotalTime() { return this->TotalTime;} + int GetExitException(); + /** + * Read one line of output but block for no more than timeout. + * Returns: + * cmsysProcess_Pipe_None = Process terminated and all output read + * cmsysProcess_Pipe_STDOUT = Line came from stdout + * cmsysProcess_Pipe_STDOUT = Line came from stderr + * cmsysProcess_Pipe_Timeout = Timeout expired while waiting + */ + int GetNextOutputLine(std::string& line, double timeout); +private: + double Timeout; + double StartTime; + double TotalTime; + cmsysProcess* Process; + class Buffer: public std::vector<char> + { + // Half-open index range of partial line already scanned. + size_type First; + size_type Last; + public: + Buffer(): First(0), Last(0) {} + bool GetLine(std::string& line); + bool GetLast(std::string& line); + }; + Buffer StdErr; + Buffer StdOut; + std::string Command; + std::string WorkingDirectory; + std::vector<std::string> Arguments; + std::vector<const char*> ProcessArgs; + std::string Output; + int Id; + int ExitValue; +}; + +#endif |