输出部分(Output)——衍生类


[TextOutputter]

相关文件:TextOutputter.h,TextOutputter.cpp

Outputter的派生类,以纯文本格式输出TestResultCollector中的内容。通过ctor将所要输出的TestResultCollector对象以及输出设备对象传入其中:

TextOutputter::TextOutputter( TestResultCollector *result,
                              std::ostream &stream )
    : m_result( result )
    , m_stream( stream )
{
}

还覆盖了基类Outputter的write方法:

void TextOutputter::write() 
{
  printHeader();
  m_stream << std::endl;
  printFailures();
  m_stream << std::endl;
}

至于pringHeader,printFailures以及其他辅助函数的具体实现,此处不再叙述,这些内容多半是复杂的格式化输出,感兴趣的读者可以查看源码。

[CompilerOutputter]

相关文件:CompilerOutputter.h,CompilerOutputter.cpp

Outputter的又一个派生类,功能类似于TextOutputter,以编译器兼容方式(compiler compatible)输出TestResultCollector中的内容,以使你在IDE环境下可以跳转至相应的assertion failure。这就好像在IDE环境下编译程序产生了错误,而你可以在错误结果输出窗口中点击错误所在行跳转至对应的源码处。CompilerOutputter会在编译期间的末尾运行测试,在IDE的监视窗口中提供反馈结果,并且同样具有跳转至对应源码处的功能。

来看看CompilerOutputter到底有何神奇之处,能有如此非凡功能。

与TextOutputter类似,通过ctor将所要输出的TestResultCollector对象以及输出设备对象传入其中:

CompilerOutputter::CompilerOutputter( TestResultCollector *result,
                                      std::ostream &stream ) :
    m_result( result ),
    m_stream( stream )
{
}

同样也覆盖了基类Outputter的write方法:

void CompilerOutputter::write()
{
  if ( m_result->wasSuccessful() )
    printSucess();
  else
    printFailureReport();
}

然后是一些以print打头的格式化输出函数,此处不再鏊述。略有不同的是,多了一个splitMessageIntoLines函数和一个wrap函数。splitMessageIntoLines用于将一个字符串按行(即以“\n”为界)拆成一个字符串数组:

CompilerOutputter::Lines 
CompilerOutputter::splitMessageIntoLines( std::string message )
{
  Lines lines;

  std::string::iterator itStart = message.begin();
  while ( true )
  {
    std::string::iterator itEol = std::find( itStart, 
                                             message.end(), 
                                             '\n' );
    lines.push_back( message.substr( itStart - message.begin(),
                                     itEol - itStart ) );
    if ( itEol == message.end() )
      break;
    itStart = itEol +1;
  }
  return lines;
}

至于wrap,则是在调用splitMessageIntoLines后,对每行以80列为限,若超过就作折行处理。这些处理都是为了方便在IDE监视窗口中查看输出结果。即,不用因为单行内容太多,而来回移动横向滚动条:

std::string
CompilerOutputter::wrap( std::string message )
{
  Lines lines = splitMessageIntoLines( message );
  std::string wrapped;
  for ( Lines::iterator it = lines.begin(); it != lines.end(); ++it )
  {
    std::string line( *it );
    const int maxLineLength = 80;
    int index =0;
    while ( index < line.length() )
    {
      std::string line( line.substr( index, maxLineLength ) );
      wrapped += line;
      index += maxLineLength;
      if ( index < line.length() )
        wrapped += "\n";
    }
    wrapped += '\n';
  }
  return wrapped;
}

到此为止,还是没有看到CompilerOutputter有何特殊之处,我们来看一下,实际调用CompilerOutputter的例子,以下这段代码是从随CppUnit源码所附的范例中抽取出来的:

int main( int argc, char* argv[] ) {
  // if command line contains "-selftest" then this is the post build check
  // => the output must be in the compiler error format.
  bool selfTest = (argc > 1)  &&  
                  (std::string("-selftest") == argv[1]);

  CppUnit::TextUi::TestRunner runner;
  runner.addTest( CppUnitTest::suite() );   // Add the top suite to the test runner

  if ( selfTest )
  { // Change the default outputter to a compiler error format outputter
    // The test runner owns the new outputter.
    runner.setOutputter( CppUnit::CompilerOutputter::defaultOutputter( 
                                                       &runner.result(),
                                                        std::cerr ) );
  }

  // Run the test and don't wait a key if post build check.
  bool wasSucessful = runner.run( "", !selfTest );

  // Return error code 1 if the one of test failed.
  return wasSucessful ? 0 : 1;
}

从注释中可以看到,针对本例,关键环节,只是在Project Settings的Post-build step处加上一个自定义命令,当然这是针对VC IDE而言的:

$(TargetPath) -selftest

$(TargetPath)代表编译后生成的exe文件,-selftest则是命令行参数。有了这项设置,IDE的compiler就会在编译结束之后,即刻运行本测试程序。以下是一个运行结果的实例:

1  --------------------Configuration: CppUnitTestMain - Win32 Debug--------------------
2  Compiling...
3  ExceptionTest.cpp
4  Linking...
5  self test
6  .F...................................................................................
7  c:\program\cppunit-1.8.0\examples\cppunittest\exceptiontest.cpp(43) : Assertion
8  Test name: ExceptionTest.testConstructor
9  sourceLine != e.sourceLine()
10 Failures !!!
11 Run: 112   Failure total: 1   Failures: 1   Errors: 0
12 Error executing c:\winnt\system32\cmd.exe.
13
14 CppUnitTestMain.exe - 1 error(s), 0 warning(s)

其中,第1~4行以及第12~14是程序编译链接的正常输出,第5行是Post-build step中所指定的command的描述性文字,第6行是TextTestProgressListener的输出内容,第7~11行就是CompilerOutputter的输出内容。此处表明,在exceptiontest.cpp的第43行,有一个断言失败了,鼠标双击该行,即可转到相应的源文件处。

[XmlOutputter]

相关文件:XmlOutputter.h,XmlOutputter.cpp

Outputter的又一个派生类,其功能是以XML格式输出TestResultCollector中的内容。与前述的TextOutputter和CompilerOutputter类似,XmlOutputter也覆盖了基类Outputter的write方法:

void XmlOutputter::write()
{
  writeProlog();
  writeTestsResult();
}

不同的是,XmlOutputter多了一个成员变量m_encoding,它用来标记XML的编码方式,缺省值是ISO-8859-1。由此,ctor也略有不同了:

XmlOutputter::XmlOutputter( TestResultCollector *result,
                            std::ostream &stream,
                            std::string encoding ) :
    m_result( result ),
    m_stream( stream ),
    m_encoding( encoding )
{
}

writeProlog函数的作用是填写XML的头信息,很简单,不再多说。至于writeTestsResult,则是用来输出整个XML数据流的:

void XmlOutputter::writeTestsResult()
{
  Node *rootNode = makeRootNode();
  m_stream  <<  rootNode->toString();
  delete rootNode;
}

writeTestsResult首先调用makeRootNode方法生成XML数据流的根节点,然后调用节点的toString方法输出实际数据流,最后回收资源。这里出现了Node类,关于Node类的细节,稍后会讲到。先来看看与writeTestsResult相关的几个函数

XmlOutputter::Node *XmlOutputter::makeRootNode()
{
  Node *rootNode = new Node( "TestRun" );

  FailedTests failedTests;
  fillFailedTestsMap( failedTests );

  addFailedTests( failedTests, rootNode );
  addSucessfulTests( failedTests, rootNode );
  addStatistics( rootNode );

  return rootNode;
}

void XmlOutputter::fillFailedTestsMap( FailedTests &failedTests )
{
  const TestResultCollector::TestFailures &failures = m_result->failures();
  TestResultCollector::TestFailures::const_iterator itFailure = failures.begin();
  while ( itFailure != failures.end() )
  {
    TestFailure *failure = *itFailure++;
    failedTests.insert( std::make_pair(failure->failedTest(), failure ) );
  }
}

void XmlOutputter::addFailedTests( FailedTests &failedTests,
                                   Node *rootNode )
{
  Node *testsNode = new Node( "FailedTests" );
  rootNode->addNode( testsNode );

  const TestResultCollector::Tests &tests = m_result->tests();
  for ( int testNumber = 0; testNumber < tests.size(); ++testNumber )
  {
    Test *test = tests[testNumber];
    if ( failedTests.find( test ) != failedTests.end() )
      addFailedTest( test, failedTests[test], testNumber+1, testsNode );
  }
}

void XmlOutputter::addSucessfulTests( FailedTests &failedTests,
                                      Node *rootNode )
{
  Node *testsNode = new Node( "SucessfulTests" );
  rootNode->addNode( testsNode );

  const TestResultCollector::Tests &tests = m_result->tests();
  for ( int testNumber = 0; testNumber < tests.size(); ++testNumber )
  {
    Test *test = tests[testNumber];
    if ( failedTests.find( test ) == failedTests.end() )
      addSucessfulTest( test, testNumber+1, testsNode );
  }
}

void XmlOutputter::addStatistics( Node *rootNode )
{
  Node *statisticsNode = new Node( "Statistics" );
  rootNode->addNode( statisticsNode );
  statisticsNode->addNode( new Node( "Tests", m_result->runTests() ) );
  statisticsNode->addNode( new Node( "FailuresTotal", 
                                     m_result->testFailuresTotal() ) );
  statisticsNode->addNode( new Node( "Errors", m_result->testErrors() ) );
  statisticsNode->addNode( new Node( "Failures", m_result->testFailures() ) );
}

void XmlOutputter::addFailedTest( Test *test,
                                  TestFailure *failure,
                                  int testNumber,
                                  Node *testsNode )
{
  Exception *thrownException = failure->thrownException();
  
  Node *testNode = new Node( "FailedTest", thrownException->what() );
  testsNode->addNode( testNode );
  testNode->addAttribute( "id", testNumber );
  testNode->addNode( new Node( "Name", test->getName() ) );
  testNode->addNode( new Node( "FailureType", 
                               failure->isError() ? "Error" : "Assertion" ) );

  if ( failure->sourceLine().isValid() )
    addFailureLocation( failure, testNode );
}

void XmlOutputter::addFailureLocation( TestFailure *failure,
                                       Node *testNode )
{
  Node *locationNode = new Node( "Location" );
  testNode->addNode( locationNode );
  SourceLine sourceLine = failure->sourceLine();
  locationNode->addNode( new Node( "File", sourceLine.fileName() ) );
  locationNode->addNode( new Node( "Line", sourceLine.lineNumber() ) );
}

void XmlOutputter::addSucessfulTest( Test *test, 
                                     int testNumber,
                                     Node *testsNode )
{
  Node *testNode = new Node( "Test" );
  testsNode->addNode( testNode );
  testNode->addAttribute( "id", testNumber );
  testNode->addNode( new Node( "Name", test->getName() ) );
}

依据上述代码可以大致勾画出XML数据流的生成流程:

makeRootNode函数虽名曰创建根节点,实则创建整个XML数据流。在创建了一个名为“TestRun”的根节点之后,随即调用了fillFailedTestsMap方法,后者调用成员变量m_result的failures方法,得到所有失败之测试,填充一个FailedTests结构的变量以备后用(实为map<Test *,TestFailure*>)。

addFailedTests方法,生成了一个名为“FailedTests”的节点,作为“TestRun”的子节点,并调用了m_result的tests方法,针对每个测试,若能在前面填充的FailedTests结构的变量中找到(即为失败测试),则调用addFailedTest方法。后者生成一个名为“FailedTest”的节点,作为“FailedTests”的子节点,“FailedTest”节点的content描述了错误原因,这是通过调用Exception的what方法得到的,而Excetpion的实例则是通过调用失败测试的thrownException方法获取的。“FailedTest”节点有一个属性,表示测试的ID号,另外其下还有两个子节点,一个是测试名称,另一个是失败类型(Error/Assertion)。若有可能,还会记录错误所在的文件位置和行号。

与addFailedTests方法类似的另一个函数是addStatistics,它用来生成和成功测试相关的节点。此处不再鏊述。

addStatistics方法在最后被调用,用以生成统计信息,包括:测试个数(名为“Tests”的节点),失败测试总数(名为“FailuresTotal”的节点),错误个数(名为“Errors”的节点),失败个数(名为“Failures”的节点)。它们都从属于“Statistics”节点,而该节点则直接从属于根节点。

最后,再捎带提一下Node类,该类内嵌于XmlOutputter中,实现了一个功能简单的XML节点类,在后续版本中可能为一个Abstract Builder所取代。节点的内容(content)支持int型和string型数据,这一点可以从其两个不同版本的ctor中看到:

XmlOutputter::Node::Node( std::string elementName,
                          std::string content ) :
    m_name( elementName ),
    m_content( content )
{
}
    
XmlOutputter::Node::Node( std::string elementName,
                          int numericContent ) :
    m_name( elementName )
{
  m_content = asString( numericContent );
}

其中的asString是Node的一个辅助方法,用以将int型数据转换成string型数据。m_name和m_content分别代表节点对应的名称和内容。

Node中的m_attributes成员,代表了节点包含的属性,事实上,它对应的是一个deque类型的变量,而deque的每个元素则是std::pair<std::string,std::string>类型的,其中前一个string代表属性名,后一个string代表属性值。有了上述认识,我们就可以理解Node的addAttribute方法的含义了,同样是为了支持int型数据和string型数据,addAttribute具有两个版本:

void XmlOutputter::Node::addAttribute( std::string attributeName,
                                       std::string value  )
{
  m_attributes.push_back( Attribute( attributeName, value ) );
}

void XmlOutputter::Node::addAttribute( std::string attributeName,
                                       int numericValue )
{
  addAttribute( attributeName, asString( numericValue ) );
}

这里的Attribute即std::pair<std::string,std::string>:

typedef std::pair<std::string,std::string> Attribute;

由于XML的标记呈现树状结构的特点,因此作为标记对应物的Node类也应该支持这种节点嵌套的特性。因此,Node类中引入了m_nodes成员,用以代表当前节点下属的子节点,其实际类型是std::deque<Node *>。与之对应的addNode方法用以为当前节点添加一个新的子节点:

void XmlOutputter::Node::addNode( Node *node )
{
  m_nodes.push_back( node );
}

接下来是至为关键的toString方法,前面提到的XmlOutputter正是调用了根节点的toString方法才完成整个XML数据流的输出的:

std::string XmlOutputter::Node::toString() const
{
  // 添加begin tag
  std::string element = "<";
  element += m_name;
  element += " ";
  element += attributesAsString();
  element += " >\n";

  // 递归调用子节点的toString方法
  Nodes::const_iterator itNode = m_nodes.begin();
  while ( itNode != m_nodes.end() )
  {
    const Node *node = *itNode++;
    element += node->toString();
  }

  // 添加tag content
  element += m_content;

  // 添加end tag
  element += "</";
  element += m_name;
  element += ">\n";

  return element;
}

代码中的注释已清楚描述了toString的执行流程,需要再解释一下的是在生成启始标记时出现的attributesAsString方法:

std::string XmlOutputter::Node::attributesAsString() const
{
  std::string attributes;
  Attributes::const_iterator itAttribute = m_attributes.begin();
  while ( itAttribute != m_attributes.end() )
  {
    const Attribute &attribute = *itAttribute++;
    attributes += attribute.first;
    attributes += "=\"";
    attributes += escape( attribute.second );
    attributes += "\"";
  }
  return attributes;
}

可以看出,attributesAsString的作用是把当前节点的属性转换成字符串,并逐个拼接起来。至于escape方法,则是用来替换字符串中的某些特殊字符的,但其做法似乎简陋了些,怪不得作者要在旁边加上safe?这样的注释。

std::string XmlOutputter::Node::escape( std::string value ) const
{
  std::string escaped;
  for ( int index =0; index < value.length(); ++index )
  {
    char c = value[index ];
    switch ( c )    // escape all predefined XML entity (safe?)
    {
    case '<': 
      escaped += "<";
      break;
    case '>': 
      escaped += ">";
      break;
    case '&': 
      escaped += "&";
      break;
    case '\'': 
      escaped += "'";
      break;
    case '"': 
      escaped += """;
      break;
    default:
      escaped += c;
    }
  }
  
  return escaped;
}

至此,XmlOutputter部分的讲解就完成了。

返回目录