이 글은 월간 마이크로소프트웨어(일명 마소) 2008년 7월호에 기고한 글입니다. 물론 구성이나 내용 상의 차이가 있을 수 있습니다.
프로젝트를 안정감 있게 진행하려면 단위 테스트가 필수다. 품질이란 측면에서 테스트가 없는 코드와 테스트가 있는 코드는 상대가 안 된다. 직접 겪어보면 자신이 짠 코드가 얼마나 믿음직스럽지 못한지, 예기치 않은 문제가 얼마나 자주 일어나는지 경험하게 된다. 그러나 테스트 코드를 작성했더라도 빌드 서버와 연동하여 자동화해 놓지 않으면 안 된다. 테스트가 깨졌을 때 바로 그 사실을 알아야 문제가 커지기 전에 대처할 수 있다.
늘 그렇듯, 이번에도 시작하기에 앞서 복습부터 해보자. 우리는 좋은 단위 테스트 프레임워크(라이브러리)의 조건에 대해 알아봤다. 사용하기 쉽고, 예외(exception)와 충돌(crash)을 잘 다뤄야 하며, 다양한 출력(표준 출력, XML 파일)을 제공해야 한다. 그밖에 테스트 코드를 재활용할 때 쓰는 Setup 및 Teardown 기능과 같은 종류의 테스트를 묶어주는 Suite 기능이 있으면 더더욱 좋다.
이러한 원칙 아래 단위 테스트 프레임워크를 고르면 되는데, 이 칼럼에서는 특히 C++용에만 집중하기로 했다. 닷넷이나 자바를 비롯해 최근에 인기를 얻은 프로그래밍 환경에선 거의 표준이라 할만한 테스트 도구가 하나씩 있다. 이 칼럼에선 닷넷과 Visual C++만 고려하는데(게임 서버 개발 사례를 예로 들기 때문에), 닷넷에선 오픈 소스인 NUnit과 비주얼 스튜디오 2005 및 비주얼 스튜디오 2008과 함께 제공하는 MSTest가 대표적이다. 단 MSTest는 아직까지 64비트 환경을 지원하지 않으므로 NUnit을 쓰기로 했다.
NUnit은 참고 자료가 웹에 널려 있기 때문에 여기선 C++용 단위 테스트 프레임워크에만 집중하기로 했고, 앞서 언급한 조건에 맞는 라이브러리인 UnitTest++을 소개했다. 프로젝트마다 요구사항이 다르기 때문에 UnitTest++이 아닌 다른 도구가 더 좋은 경우도 있겠지만, 특별히 선호하는 도구가 없다면 UnitTest++이 무난한 선택이 될 것이다.
이번 시간엔 UnitTest++을 빌드 서버 CruiseControl .NET과 연동하는 방법을 알아보자.
UnitTest++ 애플리케이션 구성하기
UnitTest++를 빌드 서버와 연동하기 전에 먼저 이걸로 단위 테스트 코드를 짜보고 돌려봐야 감 잡기가 쉽다. 사람에 따라선 의견이 다를지 모르지만, 역시 예제 하나가 A4 한 장짜리 문서보다 낫다고 생각한다.
UnitTest++ 공식 사이트에 가면, “Documentation”과 “Money, a step by step example for Visual Studio .NET 2005”라는 문서가 있다. Documentation은 UnitTest++이 제공하는 매크로들을 소개하는 글인데, UnitTest++에 익숙해지고 나서 어쩌다 기억나지 않는 기능이 있을 때 훑어보기에 좋다. 처음엔 예제 위주로 구성된 “Money ~” 문서부터 보는 편이 좋다. 특히 이 문서는 Visual Studio 2005에서 UnitTest++을 활용하는 법을 보여주므로 우리가 다루는 상황에 적합하기도 하다.
이 문서에 따르면 UnitTest++ 프로젝트를 구성할 때는 다음과 같은 세 단계를 거친다.
-
UnitTest++.vsnet2005.sln을 열면 UnitTest++ 프로젝트와 UnitTest++을 테스트하는 TestUnitTest++ 프로젝트가 보인다.
-
솔루션을 빌드하면, 두 프로젝트를 빌드하고 알아서 단위 테스트를 돌린다. 이렇게 빌드를 하고 나면 UnitTest++.vsnet2005.lib 파일이 생기는데, 나중에 단위 테스트 애플리케이션에서 참조해 쓸 것이다.
워낙 문서가 알기 쉽게 작성되어서 더 이상 자세히 설명할 필요는 없으리라 생각한다. 영어에 능숙하지 않은 개발자라도 이해하기 쉽게 비주얼 스튜디오의 스크린샷을 보여준다. 그림 보고 따라 하면 그만이다.
- Visual Studio 2008에서 UnitTest++ 사용하기
-
구성된 “Money ~” 예제는 Visual Studio 2005가 출시됐을 무렵에 작성된 문서다. 그래서 Visual Studio 2008 사용자에겐 아무런 문제가 없는지 궁금할지도 모르겠다. 직접 해본 바로는 UnitTest++.vsnet2005.sln 솔루션 파일을 Visual Studio 2008에서 열면 아무런 문제 없이 솔루션 파일과 프로젝트 파일이 변환된다. Visual Studio 2008을 도입한지 서너 달이 지났지만, 여태까지 특별한 문젯거리는 없었다. 안심하고 써도 되겠다.
CruiseControl .NET 서버 구성하기
UnitTest++ 공식 사이트에서 제공하는 예제를 직접 돌려봤다면, 이제 기본적인 사용법은 알게 됐을 것이다. 이제부터는 UnitTest++과 CruiseControl .NET을 연동하여 단위 테스트 자동화를 꾀한다. 이제부터 다루는 사항은 공식 사이트의 문서엔 나와 있지 않다. 소스 코드를 분석하고, 삽질(?)을 여러 번 해보고 나서 알게 된 노하우들이다. 물론 세상엔 뛰어난 개발자들이 많기 때문에 구글이 답해줄 수 있는 내용이겠지만, 이 칼럼이 시간을 많이 절약하는 데 도움이 될 것이다.
“Money ~ “ 예제에서 봤겠지만, UnitTest++ 프로젝트는 콘솔 애플리케이션으로 설정하게 된다. 이때 단위 테스트 콘솔 애플리케이션의 main 코드는 목록 1처럼 작성한다.
- 목록 1. UnitTest++ 메인
-
#include "TestReporterStdout.h" #include "XmlTestReporter.h" int RunTests(UnitTest::TestReporter& reporter) { return UnitTest::RunAllTests(reporter, UnitTest::Test::GetTestList(), NULL, 0); } int _tmain(int argc, _TCHAR* argv[]) { UNREFERENCED_PARAMETER(argc); // 명령줄에서 파일 이름을 넘겼으면 테스트 결과를 XML 형식으로 파일에 기록한다. std::vector<std::wstring> args; if(argv) { while(*argv != NULL) { args.push_back(*argv); argv ++; } } // 결과를 표준입출력에 출력시킨다. if(args.size() == 1) { UnitTest::TestReporterStdout reporter; return RunTests(reporter); } // 결과를 텍스트 파일에 XML 양식으로 출력시킨다. std::ofstream f(args[1].c_str(), ios_base::ate); UnitTest::XmlTestReporter reporter(f); return RunTests(reporter); }
명령줄에서 아무런 인자 없이 단위 테스트 애플리케이션을 실행시키면 그림 1이나 그림 2처럼 결과가 나온다. 그림 1은 단위 테스트가 모두 성공했을 경우인데, 총 38개의 테스트가 성공했고 전체 실행 시간이 0.27초였다는 사실을 보여준다. 그림 2는 테스트 중 하나가 실패했다는 걸 알리며, 테스트가 실패한 코드의 위치와 그 코드를 보여준다. 그림 1을 보면 가장 윗줄엔 “good morning”이란 메시지가 보이는데, 이건 UnitTest++의 매크로가 아닌 printf를 사용해서 출력한 결과다. 이런 식으로 매크로 외의 기능을 써서 표준 출력에 뭔가를 출력시키면 곤란한 상황이 벌어질 수 있는데, 뒤에서 문제와 그 해결책에 대해 자세히 알아보겠다.
만약 명령줄에서 단위 테스트 애플리케이션을 실행시킬 때 출력 파일의 경로를 명시하면(예. “UnitTest.exe c:\results.xml”), 테스트 결과를 XML 파일로 출력해준다. 목록 2는 그림 2에 해당하는 XML 출력 파일이다.
- 목록 2. 그림 2에 해당하는 XML 포맷
-
<?xml version="1.0"?> <unittest-results tests="38" failedtests="1" failures="1" t ime="0.292"> <test suite="CoreManagedTest" name="MStringToString" time="0"/> <test suite="CoreManagedTest" name="TimeSpanTicksComparisonTest" time="0.002"/> <test suite="SmartDBManagedTest" name="DefaultConstructor Test" time="0.006"> <failure message=".\SharedObjectTest.cpp(23): error: Failure in ByteTransitionTest: nativeObj->ByteCol != static_cast<BYTE>(0)"/> </test> </unittest-results>
CruiseControl .NET 서버와 연동하기
CruiseControl .NET은 크게 빌드를 실행시키는 서버와 빌드 결과를 보여주는 웹 대시보드로 이뤄져 있다는 점은 여러 번 설명한 바 있다. 빌드를 실행해야 그 결과가 나오는 것이니 우선 CC.NET 서버부터 설정해보자. CC.NET 서버 기본 경로(예. “C:\Program Files (x86)\CruiseControl.NET\server”)에 있는 ccnet.config 파일을 열어서 목록 3과 같이 설정한다.
- 목록 3. ccnet.config 설정하기
-
<project name="MyProject" queue="MyQ" queuePriority="1"> <workingDirectory>C:\src\MyProject\trunk</workingDirectory> <modificationDelaySeconds>10</modificationDelaySeconds> <triggers> <intervalTrigger seconds="60" /> </triggers> <sourcecontrol type="svn"> <!-- 생략: 소스버전관리 시스템에서 최신 소스 코드 가져오기 --> </sourcecontrol> <tasks> <exec> <executable>cmd</executable> <buildArgs>/C if exist *.xml del *.xml</buildArgs> <baseDirectory>C:\src\MyProject\</baseDirectory> <buildTimeoutSeconds>60</buildTimeoutSeconds> </exec> <msbuild> <!-- 소스 코드 빌드하기 --> </msbuild> <exec> <executable>UnitTestExec.bat</executable> <buildArgs>DEBUG-MyUnitTest.exe C:\src\MyProject\MyUnitTest-Results.xml</buildArgs> <baseDirectory>C:\src\MyProject\trunk\server\binaries\Debug\</baseDirectory> <buildTimeoutSeconds>60</buildTimeoutSeconds> </exec> </tasks> <publishers> <merge> <files> <file>C:\src\MyProject\MyUnitTest-Results.xml</file> </files> </merge> <xmllogger/> <statistics/> </publishers> </project>
이러한 구성이 실제로 어떤 일을 하는지 간략히 살펴보자. <intervalTrigger>를 보면 60초마다 이 프로젝트 구성이 실행되게 되어 있다. <sourcecontrol>에선 서브버전 저장소에 접속하여 새 코드가 있는지 확인한다. 최신 코드가 있을 때만 뒤이은 설정이 적용되는데, 여기서부터가 중요하다.
우선 단위 테스트 결과를 저장할 장소로 “C:\src\MyProject\”를 지정했다. 물론 실제 경로는 각 프로젝트에 맞춰 적당히 결정하면 된다. 첫 <exec>에선 이전 테스트 때 만들어진 출력 파일을 지운다. 뒤이은 <msbuild>에서 실제 빌드가 이뤄지고 나면, 뒤이은 <exec>에서 단위 테스트 콘솔 애플리케이션을 실행한다. 그러고 나서 이렇게 만들어진 출력 결과를 CC.NET의 다른 결과와 병합해야 하는데, <publishers>에서 이런 일을 한다.
이때 주의해볼 부분은 UnitTestExec.bat란 배치 파일이다. 보통은 단위 테스트 콘솔 애플리케이션(예. UnitTest.exe)을 직접 실행해도 되지만, 이런 배치 파일을 쓰는 특별한 이유가 있다. 화면 1에선 printf 또는 std::cout을 써서 “good morning”이란 메시지를 표준 출력에 표시했다. 그런데 문제는 이렇게 표준 출력에 메시지를 출력하면 CC.NET의 빌드 결과가 망가진다. 이 문제는 지난 5월에도 알아봤는데, 그림 3을 보면 뭐가 어떻게 잘못되는지 알 수 있다. 대부분의 경우에 표준 출력에 메시지를 출력해도 웹 대시보드에 나오지만 정확한 결과가 그렇지 않을 경우가 간혹 나오기 때문에 방심할 일은 아니다. 그렇다고 단위 테스트를 할 때 콘솔 출력을 아예 안 하기도 힘들다. 불편할뿐더러 직접 개발한 라이브러리가 아니라면 콘솔 출력을 막는 것 자체가 불가능할 수도 있다.
이럴 때 목록 4의 UnitTestExec.bat 배치 파일을 활용하면 된다. UnitTest.exe를 직접 실행시키지 않고 UnitTestExec.bat 파일을 통해 실행시키면, XML 로그 파일을 망치지 않고도 단위 테스트를 수행할 수 있다. UnitTestExec.bat이 하는 일은 간단하다. 그저 새 창을 띄워서(start) 단위 테스트를 실행시킬 뿐이다. 그런데 여기서의 핵심은 start가 아니라 뒤이은 exit이다. 단위 테스트 실행 결과를 반드시 반환해야 한다. 그렇지 않으면 단위 테스트가 실패하더라도 CC.NET은 그 사실을 모른다.
- 목록 4. UnitTestExec.bat
-
@echo off start /wait %* > _tmp.txt exit /b %errorlevel%
여기까지 했으면 충분하다. 이제 단위 테스트 결과를 웹 대시보드에 출력해보자.
웹 대시보드 설정하기
안타깝게도 UnitTest++은 CruiseControl.NET이 직접 지원하진 않는다. 하지만 XML 파일로 결과를 출력시킬 수만 있다면, CruiseControl .NET과 연동할 수 있다. 단지 직접 지원해줄 때보단 훨씬 귀찮을 뿐이다. 하지만 그 정도 수고는 해볼 가치가 있다.
우선 웹 대시보드 기본 경로(예. “C:\Program Files (x86)\CruiseControl.NET\webdashboard”)에 가서 dashboard.config 파일을 연다. 여기선 목록 5처럼 unittests.xsl 파일에 대한 참조가 있는지 여부만 확인하면 된다. 기본적으로 참조하게 되어 있지만, 혹시 모르니 만전을 기하자.
- 목록 5. Dashboard.config
-
<buildReportBuildPlugin> <xslFileNames> <xslFile>xsl\header.xsl</xslFile> <xslFile>xsl\modifications.xsl</xslFile> <xslFile>xsl\compile-msbuild.xsl</xslFile> <xslFile>xsl\msbuild.xsl</xslFile> <xslFile>xsl\compile.xsl</xslFile> <xslFile>xsl\MsTest9Summary.xsl</xslFile> <xslFile>xsl\unittests.xsl</xslFile> <xslFile>xsl\fxcop-summary.xsl</xslFile> <xslFile>xsl\NCoverSummary.xsl</xslFile> <xslFile>xsl\SimianSummary.xsl</xslFile> <xslFile>xsl\fitnesse.xsl</xslFile> <xslFile>xsl\sourcemonitor-summary.xsl</xslFile> </xslFileNames> </buildReportBuildPlugin>
이제부터 정말 중요한 대목이다. “C:\Program Files (x86)\CruiseControl.NET\webdashboard\xsl\unittests.xsl” 파일을 열어보면, NUnit이나 JUnit과 같은 기본 단위 테스트 프레임워크의 출력 결과를 웹 대시보드에 보여주기 위한 XML 스타일시트가 있다. 여기에 UnitTest++를 적용하려면 unittests.xsl에 목록 6과 같은 내용을 추가하면 된다. 원래 250줄에 달할 정도로 내용이 길기 때문에 새로 추가해 넣어야 할 코드와 수정해야 할 코드만 적어 놓았다. 물론 보기에 불편하므로 이 글 아래에 전체 파일을 올려놓았으니 언제라도 다운로드 받으면 된다.
- 목록 6. Unittests.xsl
-
<!-- 추가해 넣는 코드 시작 --> <xsl:variable name="unittest__.result.list" select="/cruisecontrol/build/unittest-results"/> <xsl:variable name="unittest__.case.list" select="$unittest__.result.list/test"/> <xsl:variable name="unittest__.case.count" select="count($unittest__.case.list)"/> <xsl:variable name="unittest__.time" select="$unittest__.result.list/@time"/> <xsl:variable name="unittest__.failure.list" select="$unittest__.result.list/test/failure"/> <xsl:variable name="unittest__.failure.count" select="count($unittest__.result.list/test/failure)"/>. <!-- 추가해 넣는 코드 끝 --> <!-- 기존 코드를 수정한 코드 시작 --> <xsl:variable name="total.time" select="$unittest__.time + $nunit2.time + $junit.time"/> <xsl:variable name="total.notrun.count" select="$nunit2.notrun.count"/> <xsl:variable name="total.run.count" select="$unittest__.case.count + $nunit2.case.count + $junit.case.count - $total.notrun.count"/> <xsl:variable name="total.failure.count" select="$unittest__.failure.count + $nunit2.failure.count + $junit.failure.count + $junit.error.count"/> <!-- 기존 코드를 수정한 코드 끝 --> <!-- 기존 코드를 수정한 코드 시작 --> <xsl:apply-templates select="$unittest__.failure.list | $junit.failure.list | $nunit2.failure.list"/> <!-- 기존 코드를 수정한 코드 끝 --> <xsl:if test="$total.failure.count > 0"> <tr> <td class="sectionheader" colspan="2"> Unit Test Failure and Error Details (<xsl:value-of select="$total.failure.count"/>) </td> </tr> <!-- 기존 코드를 수정한 코드 시작 --> <xsl:call-template name="unittest__testdetail"> <xsl:with-param name="detailnodes" select="//test[.//failure]"/> </xsl:call-template> <!-- 기존 코드를 수정한 코드 끝 --> <xsl:call-template name="junittestdetail"> <xsl:with-param name="detailnodes" select="//testsuite/testcase[.//error]"/> </xsl:call-template> <xsl:call-template name="junittestdetail"> <xsl:with-param name="detailnodes" select="//testsuite/testcase[.//failure]"/> </xsl:call-template> <xsl:call-template name="nunit2testdetail"> <xsl:with-param name="detailnodes" select="//test-suite/results/test-case[.//failure]"/> </xsl:call-template> <tr><td colspan="2"> </td></tr> </xsl:if> <!-- 추가해 넣는 코드 시작 --> <xsl:template name="unittest__testdetail"> <xsl:param name="detailnodes"/> <xsl:for-each select="$detailnodes"> <xsl:if test="failure"> <tr><td class="section-data">Suite:</td><td class="section-data"><xsl:value-of select="@suite"/></td></tr> <tr><td class="section-data">Test:</td><td class="section-data"><xsl:value-of select="@name"/></td></tr> <tr><td class="section-data">Time:</td><td class="section-data"><xsl:value-of select="@time"/></td></tr> <tr><td class="section-data">Type:</td><td class="section-data"><span style="color: red;">Failure</span></td></tr> <xsl:for-each select="failure"> <tr><td class="section-data">Message:</td><td class="section-data"><xsl:value-of select="@message"/></td></tr> </xsl:for-each> </xsl:if> <tr><td colspan="2"><hr size="1" width="100%" color="#888888"/></td></tr> </xsl:for-each> </xsl:template> <!-- 추가해 넣는 코드 끝 -->
이렇게까지 해주면 웹 대시보드에 단위 테스트 결과가 잘 정리되어 표시된다. 목록 4는 그 예제인데, 어떤 테스트가 실패했는지 그 이름을 먼저 표시한 후, 뒤이어 좀더 상세한 정보를 출력한다. 실패한 테스트가 어느 Suite에 속하는지, 실패한 테스트의 이름은 무엇인지, 테스트에 걸린 시간은 어느 정도인지, 정확히 어느 코드가 실패했는지 웹을 통해 알 수 있다. 이 정도면 자동화된 빌드 서버를 통해 단위 테스트 결과를 운용하기에 딱 충분하다.
끝마치는 말
지금까지 UnitTest++과 CCNet을 연동하는 법에 대해 알아봤다. 맨땅에 헤딩하려면 어렵다. UnitTest++을 비롯한 각 도구의 사용법을 익히고, XSL 같이 평소엔 그리 쓸 일이 많지 않은 기술도 익혀야 한다. 하지만 누군가는 해결책을 알고 있기 마련이고, 그렇지 않더라도 해결책을 강구해내면 된다. 최근엔 UnitTest++을 쓰는 한국 개발자들이 꽤 많으니, 문제가 생기면 다른 이의 도움도 구해보길 바란다.
<?xml version="1.0"?> <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0"> <xsl:output method="html"/> <xsl:variable name="unittest__.result.list" select="/cruisecontrol/build/unittest-results"/> <!-- <xsl:variable name="unittest__.suite.list" select="$nunit2.result.list//test-suite"/> --> <xsl:variable name="unittest__.case.list" select="$unittest__.result.list/test"/> <xsl:variable name="unittest__.case.count" select="count($unittest__.case.list)"/> <xsl:variable name="unittest__.time" select="$unittest__.result.list/@time"/> <!-- <xsl:variable name="unittest__.failedtest.list" select=""/> --> <!-- <xsl:variable name="unittest__.failedtest.count" select="$unittest__.result.list/@failedtests"/> --> <xsl:variable name="unittest__.failure.list" select="$unittest__.result.list/test/failure"/> <xsl:variable name="unittest__.failure.count" select="count($unittest__.result.list/test/failure)"/> <xsl:variable name="nunit2.result.list" select="//test-results"/> <xsl:variable name="nunit2.suite.list" select="$nunit2.result.list//test-suite"/> <xsl:variable name="nunit2.case.list" select="$nunit2.suite.list/results/test-case"/> <xsl:variable name="nunit2.case.count" select="count($nunit2.case.list)"/> <xsl:variable name="nunit2.time" select="sum($nunit2.result.list/test-suite[position()=1]/@time)"/> <xsl:variable name="nunit2.failure.list" select="$nunit2.case.list/failure"/> <xsl:variable name="nunit2.failure.count" select="count($nunit2.failure.list)"/> <xsl:variable name="nunit2.notrun.list" select="$nunit2.case.list/reason"/> <xsl:variable name="nunit2.notrun.count" select="count($nunit2.notrun.list)"/> <xsl:variable name="junit.suite.list" select="//testsuite"/> <xsl:variable name="junit.case.list" select="$junit.suite.list/testcase"/> <xsl:variable name="junit.case.count" select="count($junit.case.list)"/> <xsl:variable name="junit.time" select="sum($junit.case.list/@time)"/> <xsl:variable name="junit.failure.list" select="$junit.case.list/failure"/> <xsl:variable name="junit.failure.count" select="count($junit.failure.list)"/> <xsl:variable name="junit.error.list" select="$junit.case.list/error"/> <xsl:variable name="junit.error.count" select="count($junit.error.list)"/> <xsl:variable name="total.time" select="$unittest__.time + $nunit2.time + $junit.time"/> <xsl:variable name="total.notrun.count" select="$nunit2.notrun.count"/> <xsl:variable name="total.run.count" select="$unittest__.case.count + $nunit2.case.count + $junit.case.count - $total.notrun.count"/> <xsl:variable name="total.failure.count" select="$unittest__.failure.count + $nunit2.failure.count + $junit.failure.count + $junit.error.count"/> <xsl:template match="/"> <table class="section-table" cellpadding="2" cellspacing="0" border="0" width="98%"> <!-- Unit Tests --> <tr> <td class="sectionheader" colspan="2"> Tests run: <xsl:value-of select="$total.run.count"/>, Failures: <xsl:value-of select="$total.failure.count"/>, Not run: <xsl:value-of select="$total.notrun.count"/>, Time: <xsl:value-of select="$total.time"/> seconds </td> </tr> <xsl:choose> <xsl:when test="$total.run.count = 0"> <tr><td colspan="2" class="section-data">No Tests Run</td></tr> <tr><td colspan="2" class="section-error">This project doesn't have any tests</td></tr> </xsl:when> <xsl:when test="$total.failure.count = 0"> <tr><td colspan="2" class="section-data">All Tests Passed</td></tr> </xsl:when> </xsl:choose> <xsl:apply-templates select="$junit.error.list"/> <xsl:apply-templates select="$unittest__.failure.list | $junit.failure.list | $nunit2.failure.list"/> <xsl:apply-templates select="$nunit2.notrun.list"/> <tr><td colspan="2"> </td></tr> <xsl:if test="$total.failure.count > 0"> <tr> <td class="sectionheader" colspan="2"> Unit Test Failure and Error Details (<xsl:value-of select="$total.failure.count"/>) </td> </tr> <xsl:call-template name="unittest__testdetail"> <xsl:with-param name="detailnodes" select="//test[.//failure]"/> </xsl:call-template> <xsl:call-template name="junittestdetail"> <xsl:with-param name="detailnodes" select="//testsuite/testcase[.//error]"/> </xsl:call-template> <xsl:call-template name="junittestdetail"> <xsl:with-param name="detailnodes" select="//testsuite/testcase[.//failure]"/> </xsl:call-template> <xsl:call-template name="nunit2testdetail"> <xsl:with-param name="detailnodes" select="//test-suite/results/test-case[.//failure]"/> </xsl:call-template> <tr><td colspan="2"> </td></tr> </xsl:if> <xsl:if test="$nunit2.notrun.count > 0"> <tr> <td class="sectionheader" colspan="2"> Warning Details (<xsl:value-of select="$nunit2.notrun.count"/>) </td> </tr> <xsl:call-template name="nunit2testdetail"> <xsl:with-param name="detailnodes" select="//test-suite/results/test-case[.//reason]"/> </xsl:call-template> <tr><td colspan="2"> </td></tr> </xsl:if> </table> </xsl:template> <!-- Unit Test Errors --> <xsl:template match="error"> <tr> <xsl:if test="position() mod 2 = 0"> <xsl:attribute name="class">section-oddrow</xsl:attribute> </xsl:if> <td class="section-data">Error</td> <td class="section-data"><xsl:value-of select="../@name"/></td> </tr> </xsl:template> <!-- Unit Test Failures --> <xsl:template match="failure"> <tr> <xsl:if test="($junit.error.count + position()) mod 2 = 0"> <xsl:attribute name="class">section-oddrow</xsl:attribute> </xsl:if> <td class="section-data">Failure</td> <td class="section-data"><xsl:value-of select="../@name"/></td> </tr> </xsl:template> <!-- Unit Test Warnings --> <xsl:template match="reason"> <tr> <xsl:if test="($total.failure.count + position()) mod 2 = 0"> <xsl:attribute name="class">section-oddrow</xsl:attribute> </xsl:if> <td class="section-data">Warning</td> <td class="section-data"><xsl:value-of select="../@name"/></td> </tr> </xsl:template> <!-- Unittest++ Test Failures And Warnings Detail Template --> <xsl:template name="unittest__testdetail"> <xsl:param name="detailnodes"/> <xsl:for-each select="$detailnodes"> <xsl:if test="failure"> <tr><td class="section-data">Suite:</td><td class="section-data"><xsl:value-of select="@suite"/></td></tr> <tr><td class="section-data">Test:</td><td class="section-data"><xsl:value-of select="@name"/></td></tr> <tr><td class="section-data">Time:</td><td class="section-data"><xsl:value-of select="@time"/></td></tr> <tr><td class="section-data">Type:</td><td class="section-data"><span style="color: red;">Failure</span></td></tr> <xsl:for-each select="failure"> <tr><td class="section-data">Message:</td><td class="section-data"><xsl:value-of select="@message"/></td></tr> </xsl:for-each> </xsl:if> <tr><td colspan="2"><hr size="1" width="100%" color="#888888"/></td></tr> </xsl:for-each> </xsl:template> <!-- JUnit Test Errors And Failures Detail Template --> <xsl:template name="junittestdetail"> <xsl:param name="detailnodes"/> <xsl:for-each select="$detailnodes"> <tr><td class="section-data">Test:</td><td class="section-data"><xsl:value-of select="@name"/></td></tr> <xsl:if test="error"> <tr><td class="section-data">Type:</td><td class="section-data">Error</td></tr> <tr><td class="section-data">Message:</td><td class="section-data"><xsl:value-of select="error/@message"/></td></tr> <tr> <td></td> <td class="section-error"> <pre><xsl:call-template name="br-replace"> <xsl:with-param name="word" select="error"/> </xsl:call-template></pre> </td> </tr> </xsl:if> <xsl:if test="failure"> <tr><td class="section-data">Type:</td><td class="section-data">Failure</td></tr> <tr><td class="section-data">Message:</td><td class="section-data"><xsl:value-of select="failure/@message"/></td></tr> <tr> <td></td> <td class="section-error"> <pre><xsl:call-template name="br-replace"> <xsl:with-param name="word" select="failure"/> </xsl:call-template></pre> </td> </tr> </xsl:if> <tr><td colspan="2"><hr size="1" width="100%" color="#888888"/></td></tr> </xsl:for-each> </xsl:template> <!-- NUnit Test Failures And Warnings Detail Template --> <xsl:template name="nunit2testdetail"> <xsl:param name="detailnodes"/> <xsl:for-each select="$detailnodes"> <xsl:if test="failure"> <tr><td class="section-data">Test:</td><td class="section-data"><xsl:value-of select="@name"/></td></tr> <tr><td class="section-data">Type:</td><td class="section-data">Failure</td></tr> <tr><td class="section-data">Message:</td><td class="section-data"><xsl:value-of select="failure//message"/></td></tr> <tr> <td></td> <td class="section-error"> <pre><xsl:value-of select="failure//stack-trace"/></pre> </td> </tr> </xsl:if> <xsl:if test="reason"> <tr><td class="section-data">Test:</td><td class="section-data"><xsl:value-of select="@name"/></td></tr> <tr><td class="section-data">Type:</td><td class="section-data">Warning</td></tr> <tr><td class="section-data">Message:</td><td class="section-data"><xsl:value-of select="reason//message"/></td></tr> </xsl:if> <tr><td colspan="2"><hr size="1" width="100%" color="#888888"/></td></tr> </xsl:for-each> </xsl:template> <xsl:template name="br-replace"> <xsl:param name="word"/> <xsl:variable name="cr"><xsl:text> <!-- </xsl:text> on next line on purpose to get newline --> </xsl:text></xsl:variable> <xsl:choose> <xsl:when test="contains($word,$cr)"> <xsl:value-of select="substring-before($word,$cr)"/> <br/> <xsl:call-template name="br-replace"> <xsl:with-param name="word" select="substring-after($word,$cr)"/> </xsl:call-template> </xsl:when> <xsl:otherwise> <xsl:value-of select="$word"/> </xsl:otherwise> </xsl:choose> </xsl:template> </xsl:stylesheet>