PageRenderTime 68ms CodeModel.GetById 23ms RepoModel.GetById 0ms app.codeStats 0ms

/CQC/Reports.pm

https://bitbucket.org/cgj/mercury-qc
Perl | 1365 lines | 1198 code | 83 blank | 84 comment | 34 complexity | d52d86770655292165b783df07ab2b35 MD5 | raw file
  1. # -*-Perl-*-
  2. package CQC::Reports;
  3. # use perl to generate Quality Center reports for OSD projects
  4. use 5.010_000;
  5. use feature qw(say);
  6. use strict;
  7. use warnings;
  8. use vars qw($VERSION);
  9. our $VERSION = '0.01.01';
  10. # Task 'Export'
  11. use Exporter qw(import);
  12. our @EXPORT_OK
  13. = qw( rootCauseReport rtmByReqReport rtmByTestReport rtmByTestLabReport rtmReport );
  14. our %EXPORT_TAGS = (
  15. ALL => [qw( rootCauseReport rtmByReqReport rtmByTestReport rtmByTestLabReport rtmReport )]);
  16. use Data::Dumper;
  17. use Log::Log4perl qw(get_logger :nowarn);
  18. use Date::Format;
  19. use Storable;
  20. #use Devel::Size;
  21. use Win32::OLE qw(in valof);
  22. use Win32::OLE::Variant;
  23. use Win32::OLE::Const 'Microsoft Excel';
  24. use File::Basename;
  25. use File::Spec::Functions qw(:ALL);
  26. use Spreadsheet::WriteExcel::Utility;
  27. use Net::LDAP;
  28. use HTML::TreeBuilder;
  29. use HTML::FormatText;
  30. use Mercury::OTA qw(connectToQC connectQC disconnectQC releaseConnections
  31. bugView filterText filterGrid);
  32. use Mercury::Labels qw(dumpQCLabels assignBugLabels %bugLabel);
  33. use CQC::Reporter qw(:ALL);
  34. ## Initialized Global Variables
  35. # -*- subroutines -*-
  36. sub rootCauseReport {
  37. my $tdc = shift;
  38. my $fav = shift;
  39. my $template = shift;
  40. my $rcReport = shift;
  41. my $color = colorScheme();
  42. my %tab = (
  43. defects => 'defects',
  44. rcSummary => 'Root Cause summary',
  45. invalidRC_chart => 'Invalid Root Cause chart',
  46. fullSummary => 'Full RC summary',
  47. rcSTD => 'RC by STD',
  48. subTeam => 'Defects by Sub Team',
  49. scopeID => 'Defects by Scope ID-CR#',
  50. );
  51. my @pivots
  52. = ($tab{fullSummary}, $tab{rcSTD}, $tab{subTeam}, $tab{scopeID},);
  53. $template = rel2abs($template);
  54. my $logger = get_logger();
  55. $logger->logdie("$template does not exist.") if (!-e $template);
  56. my $view = bugView($tdc, $fav)
  57. or $logger->logdie("Favorite $fav not found");
  58. my @columns = split ';', filterGrid($view);
  59. my %bugLabel = assignBugLabels($tdc);
  60. my (@fieldLabels, @fieldWidths, %columnIndex, $rcCategory,
  61. $rcPhase, $rcReason, $rcUUID, $rcMgr,
  62. $rcSeverity, $rcStatus
  63. );
  64. my $columnOffset = 0;
  65. foreach my $column (@columns) {
  66. my ($width, $name) = split ',', $column;
  67. push @fieldLabels, $bugLabel{$name};
  68. push @fieldWidths, $width;
  69. $columnIndex{$bugLabel{$name}} = $columnOffset;
  70. given ($bugLabel{$name}) {
  71. when ('Root Cause Category') {
  72. $rcCategory = $columnOffset;
  73. }
  74. when ('Root Cause Phase') {
  75. $rcPhase = $columnOffset;
  76. }
  77. when ('Root Cause Reason') {
  78. $rcReason = $columnOffset;
  79. }
  80. when ('User 4') {
  81. $rcMgr = $columnOffset;
  82. }
  83. when ('Assigned To UUID') {
  84. $rcUUID = $columnOffset;
  85. }
  86. when ('Severity') {
  87. $rcSeverity = $columnOffset;
  88. }
  89. when ('Status') {
  90. $rcStatus = $columnOffset;
  91. }
  92. }
  93. $columnOffset++;
  94. }
  95. my $rcValid = scalar @columns + 1;
  96. my $defects_ref = getDefects($tdc, $fav);
  97. # store $defects_ref, 'rcStore';
  98. # my $defects_ref = retrieve('rcStore');
  99. # set up the report output
  100. $logger->debug("creating spreadsheet...");
  101. my $excel = Win32::OLE->new('Excel.Application', 'Quit')
  102. or $logger->logdie("cannot start Excel: ", Win32::OLE->LastError());
  103. # avoid excessive Excel dialogs
  104. $excel->{DisplayAlerts} = 0;
  105. $excel->{Visible} = 0;
  106. my $workbook = $excel->Workbooks->Open($template)
  107. or $logger->logdie("Can't open Excel workbook $template: ",
  108. Win32::OLE->LastError());
  109. # Task 'Process Defects'
  110. $logger->debug("processing each defect...");
  111. my %rootCause = getRootCauseMap($workbook);
  112. my (@invalidPhases, @invalidCategories, @invalidReasons, %totalDF, %team,
  113. $manager, $severity, $grandTotal);
  114. foreach my $defect (@{$defects_ref}) {
  115. # assign manager names for each defect
  116. if (not($manager = $defect->[$rcMgr])) {
  117. $manager = $defect->[$rcMgr] = getManager($defect->[$rcUUID]);
  118. }
  119. # cancelled defects are generally not included in the root cause
  120. # analysis, since there's no root cause for something that isn't
  121. # really a defect ;-), so just move on to the continue block
  122. next if ($defect->[$rcStatus] eq 'Cancelled');
  123. # validate the phase
  124. if (not(grep($_ eq $defect->[$rcPhase], @{$rootCause{Phase}}))) {
  125. $defect->[$rcValid] = 'Phase';
  126. push @invalidPhases, $defect;
  127. $team{$manager}{invalid}{total}++;
  128. $team{$manager}{invalid}{rcPhase}++;
  129. }
  130. else {
  131. # validate the category
  132. if (not(grep($_ eq $defect->[$rcCategory],
  133. @{$rootCause{Categories}})
  134. )
  135. )
  136. {
  137. $defect->[$rcValid] = 'Category';
  138. push @invalidCategories, $defect;
  139. $team{$manager}{invalid}{total}++;
  140. $team{$manager}{invalid}{rcCategory}++;
  141. $totalDF{invalid}{$defect->[$rcCategory]}
  142. {$defect->[$rcReason]}++;
  143. }
  144. else {
  145. # validate that reasons match categories
  146. if (not(grep ($_ eq $defect->[$rcReason],
  147. @{ $rootCause{Category}{$defect->[$rcCategory]}
  148. })
  149. )
  150. )
  151. {
  152. $defect->[$rcValid] = 'Reason';
  153. push @invalidReasons, $defect;
  154. $team{$manager}{invalid}{total}++;
  155. $team{$manager}{invalid}{rcReason}++;
  156. $totalDF{invalid}{$defect->[$rcCategory]}
  157. {$defect->[$rcReason]}++;
  158. }
  159. else {
  160. # if we got all the way to here, the defect must have
  161. # structurally valid root cause info
  162. $team{$manager}{valid}++;
  163. $totalDF{valid}{$defect->[$rcCategory]}
  164. {$defect->[$rcReason]}++;
  165. }
  166. }
  167. }
  168. }
  169. continue {
  170. $team{$manager}{$defect->[$rcStatus]}++;
  171. $team{$manager}{totalDF}++;
  172. $totalDF{$defect->[$rcStatus]}++;
  173. $grandTotal++;
  174. if ($defect->[$rcStatus] ne 'Cancelled') {
  175. $team{$manager}{$defect->[$rcSeverity]}++;
  176. $totalDF{$defect->[$rcSeverity]}++;
  177. }
  178. }
  179. # Task 'defects'
  180. $logger->debug("starting task defects: populating spreadsheet...");
  181. my $sheet = $workbook->Worksheets($tab{defects})
  182. or $logger->logdie("Can't open Excel worksheet: ",
  183. Win32::OLE->LastError());
  184. # store $defects_ref, 'rcStore';
  185. # write the defects if we have any
  186. if (scalar @{$defects_ref}) {
  187. # determine the lower-right corner of the range needed
  188. my $corner = xl_rowcol_to_cell(scalar(@{$defects_ref}),
  189. scalar(@{$defects_ref->[0]}) - 1);
  190. $sheet->Range("A2:$corner")->{Value} = $defects_ref;
  191. }
  192. # Task 'Root Cause summary'
  193. $logger->debug("starting task root cause summary...");
  194. $sheet = $workbook->Worksheets($tab{rcSummary})
  195. or $logger->logdie("Can't open Excel worksheet: ",
  196. Win32::OLE->LastError());
  197. # update the sheet date
  198. $sheet->Range('$C$1')->{Value} = xl_parse_date("Today");
  199. my %mgrCharts = (
  200. size => 105,
  201. gutter => 3,
  202. valid => [],
  203. invalid => [],
  204. );
  205. foreach $manager (sort keys %team) {
  206. $team{$manager}{valid} += 0;
  207. foreach my $item qw(total rcPhase rcCategory rcReason) {
  208. $team{$manager}{invalid}{$item} += 0;
  209. }
  210. push @{$mgrCharts{valid}}, $manager if ($team{$manager}{valid});
  211. push @{$mgrCharts{invalid}}, $manager
  212. if ($team{$manager}{invalid}{total});
  213. }
  214. $logger->debug("valid: ", scalar @{$mgrCharts{valid}},
  215. " : ", join('/', @{$mgrCharts{valid}}));
  216. # the invalid list includes managers with invalid phases but we don't currently
  217. # chart those
  218. $logger->debug("invalid: ", scalar @{$mgrCharts{invalid}},
  219. " : ", join('/', @{$mgrCharts{invalid}}));
  220. # add some charts
  221. # find the rows of interest for the pie charts
  222. my @categoryRows;
  223. my $usedRows = $sheet->UsedRange->Find(
  224. { What => "*",
  225. LookIn => xlValues,
  226. LookAt => xlPart,
  227. SearchDirection => xlPrevious,
  228. SearchOrder => xlByRows
  229. }
  230. )->{Row};
  231. foreach my $row (3 .. $usedRows - 1) {
  232. push @categoryRows, $row if $sheet->Cells($row, 2)->{Value};
  233. }
  234. # find the corner for our charts to start
  235. $logger->debug("invalid: ", scalar @{$mgrCharts{valid}});
  236. my $left = $sheet->Range("A1:AI12")->Width;
  237. my $top = $sheet->Range("A1:AI12")->Height;
  238. my $leftmargin = $left;
  239. my $row = 2;
  240. my $col = 15;
  241. foreach my $mgr (sort @{$mgrCharts{valid}}) {
  242. $sheet->Cells($row, $col++)->{Value} = $mgr;
  243. }
  244. $sheet->Cells($row, $col--)->{Value} = ' ';
  245. my $mgrChart = 0;
  246. foreach my $mgrCol (15 .. $col) {
  247. $logger->debug("total: ", $sheet->Cells($usedRows, $mgrCol)->{Value});
  248. next if ($sheet->Cells($usedRows, $mgrCol)->{Value} == 0);
  249. my $chart = $sheet->ChartObjects->Add(
  250. { Left => $left,
  251. Top => $top,
  252. Width => $mgrCharts{size},
  253. Height => $mgrCharts{size}
  254. }
  255. );
  256. my $source = $sheet->Cells($categoryRows[0], $mgrCol);
  257. foreach my $row (@categoryRows[1 .. $#categoryRows]) {
  258. $source = $excel->Union($source, $sheet->Cells($row, $mgrCol));
  259. }
  260. $chart->Chart->ChartWizard(
  261. { Source => $source,
  262. Gallery => xlPie,
  263. Title => $sheet->Cells(2, $mgrCol)->{Value},
  264. HasLegend => 0
  265. }
  266. );
  267. $chart->Chart->ChartTitle->Font->{Size} = 12;
  268. $chart->Chart->ApplyDataLabels(
  269. { Type => xlDataLabelsShowValue,
  270. HasLeaderLines => 0
  271. }
  272. );
  273. $chart->Chart->SeriesCollection(1)->DataLabels->{NumberFormat}
  274. = "0;-0;;@";
  275. $left += $mgrCharts{size} + $mgrCharts{gutter};
  276. if (not(++$mgrChart % 5)) {
  277. $top += $mgrCharts{size} + $mgrCharts{gutter};
  278. $left = $leftmargin;
  279. }
  280. }
  281. # Task 'Invalid Root Cause chart'
  282. $logger->debug("starting task invalid root cause chart...");
  283. $sheet = $workbook->Worksheets($tab{invalidRC_chart})
  284. or $logger->logdie("Can't open Excel worksheet: ",
  285. Win32::OLE->LastError());
  286. # populate only the categories and reasons that have data
  287. $row = 3;
  288. @categoryRows = ();
  289. foreach my $category (sort keys %{$totalDF{invalid}}) {
  290. my $categoryRow = $row++;
  291. push @categoryRows, $categoryRow;
  292. my $col = 2;
  293. my $reasons = scalar keys %{$totalDF{invalid}{$category}};
  294. $sheet->Cells($categoryRow, $col)->{Value} = $category;
  295. my $formula = '=SUM(R[1]C:R[' . $reasons . ']C)';
  296. $sheet->Cells($categoryRow, $col + 2)->{FormulaR1C1} = $formula;
  297. $sheet->Cells($categoryRow, $col + 4)->{FormulaR1C1} = $formula;
  298. $sheet->Cells($categoryRow, $col + 5)->{FormulaR1C1}
  299. = '=SUM(RC[-3], RC[-1])';
  300. $col += 6;
  301. foreach my $manager (keys %team) {
  302. $sheet->Cells($categoryRow, $col++)->{FormulaR1C1} = $formula;
  303. }
  304. $sheet->Range(xl_range($categoryRow, 2, $categoryRow, $col - 1))
  305. ->Interior->{Color} = $color->Gray;
  306. foreach my $reason (sort keys %{$totalDF{invalid}{$category}}) {
  307. my $reasonRow = $row++;
  308. my $col = 3;
  309. $sheet->Cells($reasonRow, $col)->{Value} = $reason;
  310. $sheet->Cells($reasonRow, $col + 1)->{FormulaR1C1}
  311. = '=SUMPRODUCT((defectStatus<>"Cancelled")'
  312. . '*(Root_Cause_Phase=Phase_3)'
  313. . '*(Root_Cause_Category='
  314. . "R${categoryRow}C2)"
  315. . '*(Root_Cause_Reason=RC3))';
  316. $sheet->Cells($reasonRow, $col + 3)->{FormulaR1C1}
  317. = '=SUMPRODUCT((defectStatus<>"Cancelled")'
  318. . '*(Root_Cause_Phase=Phase_4)'
  319. . '*(Root_Cause_Category='
  320. . "R${categoryRow}C2)"
  321. . '*(Root_Cause_Reason=RC3))';
  322. $col += 5;
  323. my $formula
  324. = '=SUMPRODUCT((defectStatus<>"Cancelled")'
  325. . '*((Root_Cause_Phase=Phase_3)+(Root_Cause_Phase=Phase_4))'
  326. . '*(Root_Cause_Category='
  327. . "R${categoryRow}C2)"
  328. . '*(Root_Cause_Reason=RC3)*(STD=R2C))';
  329. foreach my $manager (keys %team) {
  330. $sheet->Cells($reasonRow, $col++)->{FormulaR1C1} = $formula;
  331. }
  332. }
  333. }
  334. # grand totals
  335. $col = 2;
  336. $usedRows = $row;
  337. my $formula = '=';
  338. $sheet->Cells($row, $col)->{Value} = 'Grand Total';
  339. foreach my $row (@categoryRows[0 .. $#categoryRows]) {
  340. $formula .= "R${row}C+";
  341. }
  342. $formula =~ s/\+$//;
  343. $sheet->Cells($row, $col + 2)->{FormulaR1C1} = $formula;
  344. $sheet->Cells($row, $col + 4)->{FormulaR1C1} = $formula;
  345. $col += 6;
  346. foreach my $manager (keys %team) {
  347. $sheet->Cells($row, $col++)->{FormulaR1C1} = $formula;
  348. }
  349. my $range = $sheet->Range(xl_range($row, 2, $row, $col - 1));
  350. $range->Font->{Bold} = 'True';
  351. $range->Font->{Italic} = 'True';
  352. $range->Interior->{Color} = $color->LightBlue;
  353. # update chart series
  354. my $name = $sheet->{Name};
  355. my @x = map {"'$name'!R" . $_ . "C2"} @categoryRows;
  356. my @y = map {"'$name'!R" . $_ . "C7"} @categoryRows;
  357. # deal with some brain damage in what excel likes in series syntax
  358. # i.e., it gets really annoyed by superfluous parens and spaces
  359. my @series = (
  360. '=SERIES("Invalid Root Causes"',
  361. (scalar @x == 1) ? @x : '(' . join(',', @x) . ')',
  362. (scalar @y == 1) ? @y : '(' . join(',', @y) . ')',
  363. '1)'
  364. );
  365. $formula = join ',', @series;
  366. my $chart = $sheet->ChartObjects("Invalid Root Causes")->Chart;
  367. $chart->SeriesCollection(1)->{FormulaR1C1} = $formula;
  368. # find the corner for our charts to start
  369. $logger->debug("invalid: ", scalar @{$mgrCharts{invalid}});
  370. $left = $sheet->Range("A1:AC12")->Width;
  371. $top = $sheet->Range("A1:AC12")->Height;
  372. $leftmargin = $left;
  373. $row = 2;
  374. $col = 8;
  375. foreach my $mgr (sort @{$mgrCharts{invalid}}) {
  376. $sheet->Cells($row, $col++)->{Value} = $mgr;
  377. }
  378. $sheet->Cells($row, $col--)->{Value} = ' ';
  379. $mgrChart = 0;
  380. foreach my $mgrCol (8 .. $col) {
  381. my $col = xl_off_to_col($mgrCol);
  382. $logger->debug("total: ", $sheet->Cells($usedRows, $mgrCol)->{Value});
  383. next if ($sheet->Cells($usedRows, $mgrCol)->{Value} == 0);
  384. my $chart = $sheet->ChartObjects->Add(
  385. { Left => $left,
  386. Top => $top,
  387. Width => $mgrCharts{size},
  388. Height => $mgrCharts{size}
  389. }
  390. );
  391. my $source = $sheet->Cells($categoryRows[0], $mgrCol);
  392. foreach my $row (@categoryRows[1 .. $#categoryRows]) {
  393. $source = $excel->Union($source, $sheet->Cells($row, $mgrCol));
  394. }
  395. $chart->Chart->ChartWizard(
  396. { Source => $source,
  397. Gallery => xlPie,
  398. Title => $sheet->Cells(2, $mgrCol)->{Value},
  399. HasLegend => 0
  400. }
  401. );
  402. $chart->Chart->ChartTitle->Font->{Size} = 12;
  403. $chart->Chart->ApplyDataLabels(
  404. { Type => xlDataLabelsShowValue,
  405. HasLeaderLines => 0
  406. }
  407. );
  408. $chart->Chart->SeriesCollection(1)->DataLabels->{NumberFormat}
  409. = "0;-0;;@";
  410. $left += $mgrCharts{size} + $mgrCharts{gutter};
  411. if (not(++$mgrChart % 5)) {
  412. $top += $mgrCharts{size} + $mgrCharts{gutter};
  413. $left = $leftmargin;
  414. }
  415. }
  416. # refresh the pivot tables
  417. $workbook->RefreshAll;
  418. # TASK 'Invalid root causes'
  419. # add a worksheet for the invalid root causes
  420. $logger->debug("starting task invalid root causes...");
  421. $sheet = $workbook->Worksheets('Invalid root causes')
  422. or $logger->logdie("Can't open Excel worksheet: ",
  423. Win32::OLE->LastError());
  424. my %rcInvalid = (
  425. 'Invalid Phases' => \@invalidPhases,
  426. 'Invalid Categories' => \@invalidCategories,
  427. 'Invalid Reasons' => \@invalidReasons,
  428. );
  429. # which columns are we interested in?
  430. $logger->debug("which columns are we interested in...");
  431. my $usedCols = $sheet->UsedRange->Find(
  432. { What => "*",
  433. LookIn => xlValues,
  434. LookAt => xlPart,
  435. SearchDirection => xlPrevious,
  436. SearchOrder => xlByColumns
  437. }
  438. )->{Column};
  439. $row = 1;
  440. my @headers = ();
  441. my %headerIndex = ();
  442. foreach my $column (1 .. $usedCols) {
  443. my $hdr
  444. = $sheet->Cells($row, $column)->{Value} eq 'STD'
  445. ? 'User 4'
  446. : $sheet->Cells($row, $column)->{Value};
  447. $headerIndex{$hdr} = $column;
  448. push @headers, $hdr;
  449. }
  450. if (exists $headerIndex{'User 4'}) {
  451. $headerIndex{STD} = $headerIndex{'User 4'};
  452. }
  453. $logger->debug("used columns $usedCols");
  454. my @rcHdrs
  455. = ('Root Cause Phase', 'Root Cause Category', 'Root Cause Reason');
  456. foreach
  457. my $field ('Invalid Phases', 'Invalid Categories', 'Invalid Reasons')
  458. {
  459. $row++;
  460. # scribble a header row
  461. $logger->debug("processing $field...");
  462. $sheet->Cells($row, 1)->{Value} = $field;
  463. foreach my $header (@rcHdrs) {
  464. $sheet->Cells($row, $headerIndex{$header})->{Value} = $header;
  465. }
  466. my $range = $sheet->Range(xl_range($row, 1, $row, $usedCols));
  467. $range->Font->{Bold} = 'True';
  468. $range->Interior->{Color} = $color->MediumBlue;
  469. $logger->debug("write out the section values...");
  470. # save the section starting row and then pump out the section
  471. # !! it would be better if I wrote the whole block or at least
  472. # a whole row at a time instead of a cell at a time
  473. my $sectionStart = $row;
  474. foreach my $invalidRC (@{$rcInvalid{$field}}) {
  475. $row++;
  476. my @cells = ();
  477. foreach my $header (@headers) {
  478. push @cells, $invalidRC->[$columnIndex{$header}];
  479. }
  480. $sheet->Range(xl_range($row, 1, $row, $usedCols))->{Value}
  481. = [@cells];
  482. }
  483. $logger->debug("sort the section...");
  484. # sort the section by STD, RC Phase, RC Category, RC Reason
  485. $sheet->Sort->SortFields->Clear;
  486. for my $sortCol (
  487. 'STD',
  488. 'Root Cause Phase',
  489. 'Root Cause Category',
  490. 'Root Cause Reason'
  491. )
  492. {
  493. $sheet->Sort->SortFields->Add(
  494. { Key => $sheet->Range(
  495. $sheet->Cells($sectionStart, $headerIndex{$sortCol}),
  496. $sheet->Cells($row, $headerIndex{$sortCol})
  497. ),
  498. Order => xlAscending,
  499. SortOn => xlSortOnValues,
  500. DataOption => xlSortNormal
  501. }
  502. );
  503. }
  504. $sheet->Sort->SetRange(
  505. $sheet->Range(
  506. $sheet->Cells($sectionStart, $headerIndex{'STD'}),
  507. $sheet->Cells($row, $headerIndex{'Root Cause Reason'})
  508. )
  509. );
  510. $sheet->Sort->{Header} = xlYes;
  511. $sheet->Sort->Apply;
  512. $logger->debug("create and outline group...");
  513. # create an outline group for the section
  514. $sheet->Range("$sectionStart:$row")->Group;
  515. # to make outlining prettier, scribble trailing (or summary) row
  516. $row++;
  517. $sheet->Cells($row, 1)->{Value} = $field;
  518. foreach my $header (@rcHdrs) {
  519. $sheet->Cells($row, $headerIndex{$header})->{Value} = $header;
  520. }
  521. $range = $sheet->Range($sheet->Cells($row, 1),
  522. $sheet->Cells($row, $usedCols));
  523. $range->Font->{Bold} = 'True';
  524. $range->Borders(xlEdgeBottom)->{LineStyle} = xlContinuous;
  525. $range->Interior->{Color} = $color->LightBlue;
  526. }
  527. # collapse the outline to the first level
  528. $sheet->Outline->ShowLevels({RowLevels => 1});
  529. # # expand a group in the outline (leaving this code in as an example)
  530. # $sheet->Rows(3)->{ShowDetail} = 1;
  531. # Task 'Overview'
  532. # open worksheet for the Overview
  533. $logger->debug("starting task Overview...");
  534. $sheet = $workbook->Worksheets('Overview')
  535. or $logger->logdie("Can't open Excel worksheet: ",
  536. Win32::OLE->LastError());
  537. my @ovStatus = ('STD', 'Sev 1', 'Sev 2', 'Sev 3', 'Cancelled', 'Total');
  538. my @ovValidity = qw(Valid Invalid %Valid);
  539. my @ovRootcause = qw(Phase Category Reason);
  540. my @overviewHeaders = (@ovStatus, @ovValidity, @ovRootcause);
  541. my $lWidth = scalar @ovStatus;
  542. my $cWidth = scalar @ovValidity;
  543. my $rWidth = scalar @ovRootcause;
  544. my $tWidth = scalar @overviewHeaders;
  545. my $tRow = $row = 2;
  546. my $lCol = 2;
  547. my $rCol = $lCol + $tWidth - 1;
  548. $col = $lCol;
  549. $range = $sheet->Range(xl_range($row, $lCol, $row, $rCol));
  550. $range->{HorizontalAlignment} = xlHAlignLeft;
  551. $range->Font->{Bold} = 'True';
  552. $range->Font->{Color} = $color->White;
  553. $range->Interior->{Color} = $color->DarkBlue;
  554. $sheet->Range(
  555. xl_range($row, $lCol + $lWidth, $row, $lCol + $lWidth + $cWidth - 1))
  556. ->Interior->{Color} = $color->DarkGreen;
  557. $sheet->Range(xl_range($row, $lCol + $lWidth + $cWidth, $row, $rCol))
  558. ->Interior->{Color} = $color->DarkBrown;
  559. $sheet->Cells($row, $lCol)->{Value} = 'Defects by STD and Severity';
  560. $sheet->Cells($row, $lCol + $lWidth)->{Value} = 'Root Cause Validity';
  561. $sheet->Cells($row, $lCol + $lWidth + $cWidth)->{Value} = 'Invalid Area';
  562. $row++;
  563. $range = $sheet->Range(xl_range($row, $lCol, $row, $rCol));
  564. $range->Font->{Bold} = 'True';
  565. $range->Interior->{Color} = $color->MediumBlue;
  566. foreach my $header (@overviewHeaders) {
  567. $sheet->Cells($row, $col++)->{Value} = $header;
  568. }
  569. $row++;
  570. foreach my $teamName (sort keys %team) {
  571. if ($team{$teamName}{totalDF}) {
  572. my $col = $lCol;
  573. $sheet->Range(
  574. xl_range($row, $lCol, $row, $lCol + $lWidth + $cWidth - 2))
  575. ->Interior->{Color} = $color->LightBlue
  576. if ($row % 2);
  577. $sheet->Cells($row, $col)->{Value} = $teamName;
  578. $sheet->Cells($row, ++$col)->{Value}
  579. = $team{$teamName}{'Severity 1'};
  580. $sheet->Cells($row, ++$col)->{Value}
  581. = $team{$teamName}{'Severity 2'};
  582. $sheet->Cells($row, ++$col)->{Value}
  583. = $team{$teamName}{'Severity 3'};
  584. $sheet->Cells($row, ++$col)->{Value}
  585. = $team{$teamName}{'Cancelled'};
  586. $sheet->Cells($row, ++$col)->{Value} = $team{$teamName}{totalDF};
  587. $sheet->Cells($row, ++$col)->{Value} = $team{$teamName}{valid};
  588. $sheet->Cells($row, ++$col)->{Value}
  589. = $team{$teamName}{invalid}{total};
  590. $sheet->Cells($row, ++$col)->{FormulaR1C1}
  591. = '=IFERROR(RC[-2]/(RC[-2]+RC[-1]), "N/A")';
  592. $sheet->Range(
  593. xl_range($row, $lCol + $lWidth + $cWidth, $row, $rCol))
  594. ->Interior->{Color} = $color->LightBrown
  595. if ($row % 2);
  596. $sheet->Cells($row, ++$col)->{Value}
  597. = $team{$teamName}{invalid}{rcPhase};
  598. $sheet->Cells($row, ++$col)->{Value}
  599. = $team{$teamName}{invalid}{rcCategory};
  600. $sheet->Cells($row, ++$col)->{Value}
  601. = $team{$teamName}{invalid}{rcReason};
  602. $row++;
  603. }
  604. }
  605. $range = $sheet->Range(xl_range($row, $lCol, $row, $rCol));
  606. $range->Font->{Bold} = 'True';
  607. $range->Font->{Italic} = 'True';
  608. $range->Interior->{Color} = $color->MediumBlue;
  609. $range->Borders(xlEdgeTop)->{LineStyle} = xlContinuous;
  610. $range->Borders(xlEdgeBottom)->{LineStyle} = xlContinuous;
  611. $col = $lCol;
  612. $sheet->Cells($row, $col)->{Value} = 'Grand Total';
  613. $sheet->Cells($row, ++$col)->{Value} = $totalDF{'Severity 1'};
  614. $sheet->Cells($row, ++$col)->{Value} = $totalDF{'Severity 2'};
  615. $sheet->Cells($row, ++$col)->{Value} = $totalDF{'Severity 3'};
  616. $sheet->Cells($row, ++$col)->{Value} = $totalDF{'Cancelled'};
  617. $sheet->Cells($row, ++$col)->{Value} = $grandTotal;
  618. $sheet->Range(
  619. xl_range($tRow + 1, $lCol + $lWidth - 1, $row, $lCol + $lWidth - 1))
  620. ->Borders(xlEdgeRight)->{LineStyle} = xlContinuous;
  621. $sheet->Range(
  622. xl_range(
  623. $tRow + 1,
  624. $lCol + $lWidth + $cWidth - 1,
  625. $row,
  626. $lCol + $lWidth + $cWidth - 1
  627. )
  628. )->Borders(xlEdgeRight)->{LineStyle} = xlContinuous;
  629. for (1 .. $cWidth + $rWidth) {
  630. my $formula = '=SUM(R[' . ($tRow + 2 - $row) . ']C:R[-1]C)';
  631. $sheet->Cells($row, ++$col)->{FormulaR1C1} = $formula;
  632. }
  633. $sheet->Cells($row, $lCol + $lWidth + $cWidth - 1)->{FormulaR1C1}
  634. = '=RC[-2]/(RC[-2]+RC[-1])';
  635. $sheet->Range(xl_range($tRow, $lCol, $row, $rCol))->Font->{Name}
  636. = 'Calibri';
  637. $sheet->Range(xl_range($tRow, $lCol, $row, $rCol))->Font->{Size} = 11;
  638. # use conditional format for %valid
  639. $range = $sheet->Range(
  640. xl_range(
  641. $tRow + 2,
  642. $lCol + $lWidth + $cWidth - 1,
  643. $row - 1,
  644. $lCol + $lWidth + $cWidth - 1
  645. )
  646. );
  647. $range->FormatConditions->AddDatabar;
  648. $range->FormatConditions($range->FormatConditions->{Count})->{ShowValue}
  649. = 1;
  650. $range->FormatConditions($range->FormatConditions->{Count})
  651. ->SetFirstPriority;
  652. $range->FormatConditions(1)
  653. ->MinPoint->Modify({newtype => xlConditionValueLowestValue});
  654. $range->FormatConditions(1)
  655. ->MaxPoint->Modify({newtype => xlConditionValueHighestValue});
  656. $range->FormatConditions(1)->BarColor->{Color} = 13012579;
  657. $range->FormatConditions(1)->BarColor->{TintAndShade} = 0;
  658. # Task 'Features'
  659. # open worksheet for the Feature mapping
  660. $logger->debug("starting task Features...");
  661. $sheet = $workbook->Worksheets('Feature mapping')
  662. or $logger->logdie("Can't open Excel worksheet: ",
  663. Win32::OLE->LastError());
  664. $usedRows = $sheet->UsedRange->Find(
  665. { What => "*",
  666. LookIn => xlValues,
  667. LookAt => xlPart,
  668. SearchDirection => xlPrevious,
  669. SearchOrder => xlByRows
  670. }
  671. )->{Row};
  672. $usedCols = $sheet->UsedRange->Find(
  673. { What => "*",
  674. LookIn => xlValues,
  675. LookAt => xlPart,
  676. SearchDirection => xlPrevious,
  677. SearchOrder => xlByColumns
  678. }
  679. )->{Column};
  680. # build the list mapping features to scope items
  681. my $feature_ref
  682. = $sheet->Range(xl_range(2, 1, $usedRows, $usedCols))->{Value};
  683. my %feature;
  684. foreach my $row (@{$feature_ref}) {
  685. if (@$row[0]) {
  686. $name = @$row[0];
  687. push @{$feature{names}}, $name;
  688. next;
  689. }
  690. else {
  691. foreach my $item (@{$row}) {
  692. if ($item) {
  693. push @{$feature{name}{$name}{scopes}}, $item;
  694. }
  695. }
  696. }
  697. }
  698. # get the scope counts from the scope pivot
  699. my $scopeSheet = $workbook->Worksheets('Defects by Scope ID-CR#')
  700. or $logger->logdie("Can't open Excel worksheet: ",
  701. Win32::OLE->LastError());
  702. $usedRows = $scopeSheet->UsedRange->Find(
  703. { What => "*",
  704. LookIn => xlValues,
  705. LookAt => xlPart,
  706. SearchDirection => xlPrevious,
  707. SearchOrder => xlByRows
  708. }
  709. )->{Row};
  710. my $scope_ref
  711. = $scopeSheet->Range(xl_range(4, 1, $usedRows - 1, 2))->{Value};
  712. my %scopeID = map {$_->[0] => $_->[1]} @{$scope_ref};
  713. # process each scope item and assign to a feature
  714. foreach my $fn (@{$feature{names}}) {
  715. my $pattern = join '|', @{$feature{name}{$fn}{scopes}};
  716. $feature{name}{$fn}{pattern} = qr/$pattern/i;
  717. }
  718. my %featureSummary;
  719. foreach my $scope (keys %scopeID) {
  720. my $matched = 0;
  721. foreach my $fn (@{$feature{names}}) {
  722. if ($scope =~ /$feature{name}{$fn}{pattern}/) {
  723. $featureSummary{$fn}{$scope} = $scopeID{$scope};
  724. $matched = 1, last;
  725. }
  726. }
  727. if (not $matched) {
  728. $featureSummary{Others}{$scope} = $scopeID{$scope};
  729. }
  730. }
  731. # update the "Defect by Feature" sheet
  732. $logger->debug("starting task Defect by Feature...");
  733. $sheet = $workbook->Worksheets('Defects by Feature')
  734. or $logger->logdie("Can't open Excel worksheet: ",
  735. Win32::OLE->LastError());
  736. my @featureRows;
  737. $row = 1;
  738. foreach my $fn (@{$feature{names}}, 'Others') {
  739. next if (not exists $featureSummary{$fn});
  740. my $featureRow = ++$row;
  741. push @featureRows, $featureRow;
  742. if ($fn eq 'Others') {
  743. $sheet->Cells($featureRow, 1)->{Value}
  744. = 'Others (non-feature, CR, deferred, blank, etc.)';
  745. }
  746. else {
  747. $sheet->Cells($featureRow, 1)->{Value} = $fn;
  748. }
  749. $range = $sheet->Range(xl_range($featureRow, 1, $featureRow, 4));
  750. $range->Font->{Bold} = 'True';
  751. $range->Interior->{Color} = $color->Gray;
  752. foreach my $scope (sort keys %{$featureSummary{$fn}}) {
  753. $sheet->Cells(++$row, 2)->{Value} = $scope;
  754. $sheet->Cells($row, 3)->{Value} = $featureSummary{$fn}{$scope};
  755. }
  756. $sheet->Cells($featureRow, 4)->{FormulaR1C1}
  757. = '=SUM(R[+1]C[-1]:R[+' . ($row - $featureRow) . ']C[-1])';
  758. }
  759. $sheet->Cells(++$row, 1)->{Value} = 'Grand Total';
  760. $sheet->Cells($row, 4)->{FormulaR1C1} = '=SUM(R2C:R[-1]C';
  761. $range = $sheet->Range(xl_range($row, 1, $row, 4));
  762. $range->Font->{Bold} = 'True';
  763. $range->Font->{Italic} = 'True';
  764. $range->Interior->{Color} = $color->LightBlue;
  765. # update chart series
  766. $name = $sheet->{Name};
  767. @x = map {"'$name'!R" . $_ . "C1"} @categoryRows;
  768. @y = map {"'$name'!R" . $_ . "C4"} @categoryRows;
  769. # deal with some brain damage in what excel likes in series syntax
  770. # i.e., it gets really annoyed by superfluous parens and spaces
  771. @series = (
  772. '=SERIES("Defects by Feature"',
  773. (scalar @x == 1) ? @x : '(' . join(',', @x) . ')',
  774. (scalar @y == 1) ? @y : '(' . join(',', @y) . ')',
  775. '1)'
  776. );
  777. $formula = join ',', @series;
  778. $chart = $sheet->ChartObjects(1)->Chart;
  779. $chart->SeriesCollection(1)->{FormulaR1C1} = $formula;
  780. $logger->debug("finish up and save a snapshot...");
  781. # save a snapshot for today
  782. my $snapShot = saveSnapshot($workbook, $rcReport, 'Root Cause summary');
  783. # be nice and close the patient
  784. $workbook->Close();
  785. $excel->Quit();
  786. # $Data::Dumper::Terse = 1;
  787. # $logger->debug(Dumper(%team, %mgrCharts, %totalDF));
  788. return (\%team, \%totalDF, \%mgrCharts, $grandTotal);
  789. }
  790. sub getRootCauseMap {
  791. my $workbook = shift;
  792. my $wbName = 'Reason mapping';
  793. my $logger = get_logger();
  794. # open worksheet for the root cause mapping
  795. my $sheet = $workbook->Worksheets($wbName)
  796. or $logger->logdie("Can't open Excel worksheet: $wbName",
  797. Win32::OLE->LastError());
  798. my $usedRows = $sheet->UsedRange->Find(
  799. { What => "*",
  800. LookIn => xlValues,
  801. LookAt => xlPart,
  802. SearchDirection => xlPrevious,
  803. SearchOrder => xlByRows
  804. }
  805. )->{Row};
  806. # build the list mapping categories to reason items
  807. my $rc_ref = $sheet->Range(xl_range(2, 1, $usedRows, 2))->{Value};
  808. my %rootCause = (
  809. Phase => [
  810. 'Phase 3: Design, Development and Pre-Production Testing',
  811. 'Phase 4: Install and Production Readiness Test',
  812. ],
  813. );
  814. my @group;
  815. my $newGroup = 1;
  816. foreach my $row (@{$rc_ref}) {
  817. my ($category, $reason) = @{$row};
  818. if ($category) {
  819. if ($newGroup) {
  820. $newGroup = 0;
  821. @group = ();
  822. }
  823. push @{$rootCause{Categories}}, $category;
  824. push @group, $category;
  825. }
  826. else {
  827. push @{$rootCause{Reasons}}, $reason;
  828. foreach my $cat (@group) {
  829. push @{$rootCause{Category}{$cat}}, $reason;
  830. }
  831. $newGroup = 1;
  832. }
  833. }
  834. return %rootCause;
  835. }
  836. sub rtmByReqReport {
  837. my $tdc = shift;
  838. my $filters_ref = shift;
  839. my $report = shift;
  840. my $logger = get_logger();
  841. my %testCoverage;
  842. foreach my $filter (@$filters_ref) {
  843. my $reqFactory = $tdc->ReqFactory;
  844. my $reqFilter = $reqFactory->Filter;
  845. my @pathElements = split /\\/, $filter;
  846. my $parent = $reqFactory->Item(0);
  847. foreach my $element (@pathElements) {
  848. my @reqList
  849. = in $reqFactory->Find($parent->ID, 'RQ_REQ_NAME', $element,
  850. 16);
  851. (my $reqID = $reqList[0]) =~ s/,.*//;
  852. $parent = $reqFactory->Item($reqID);
  853. }
  854. next if (not defined $parent);
  855. my @requirements;
  856. @requirements = getReqChildren($reqFactory, $parent, \@requirements);
  857. foreach my $requirement (@requirements) {
  858. my @tests = in $requirement->GetCoverlist;
  859. foreach my $test (@tests) {
  860. # I don't understand why this is, but some tests don't have a
  861. # Subject field defined, so I have to ignore those.
  862. # TODO: Is there any way to get the QC folder path for those?
  863. if (not defined $test->Field('TS_SUBJECT')) {
  864. $logger->error('undefined subject with requirement ',
  865. $requirement->Name, ' and test ', $test->Name);
  866. }
  867. else {
  868. my $path = $test->Field('TS_SUBJECT')->Path . "\\"
  869. . $test->Name;
  870. $testCoverage{$path}->{Folder}
  871. = $test->Field('TS_SUBJECT')->Path;
  872. $testCoverage{$path}->{Name} = $test->Name;
  873. $testCoverage{$path}->{Description}
  874. = $test->Field('TS_DESCRIPTION');
  875. push @{$testCoverage{$path}->{Requirements}},
  876. $requirement->Field('RQ_USER_05');
  877. }
  878. }
  879. }
  880. $logger->debug("Filtered requirements found: ", scalar @requirements);
  881. }
  882. my @coverage;
  883. foreach my $test (keys %testCoverage) {
  884. push @coverage,
  885. [
  886. $testCoverage{$test}->{Folder},
  887. $testCoverage{$test}->{Name},
  888. $testCoverage{$test}->{Description},
  889. join("\n", @{$testCoverage{$test}->{Requirements}})
  890. ];
  891. }
  892. rtmReport(\@coverage, $report);
  893. return scalar(@coverage);
  894. }
  895. sub rtmByTestLabReport {
  896. my $tdc = shift;
  897. my $filters_ref = shift;
  898. my $report = shift;
  899. my $rpt_del_req = shift;
  900. my $cqc = shift;
  901. my $project = shift;
  902. my $logger = get_logger();
  903. my @testSets;
  904. foreach my $folder (@{$project->{folders}->{folder}}) {
  905. push @testSets, getFolderTestSets($tdc, $folder);
  906. }
  907. $logger->info('Total test sets found: ', scalar @testSets);
  908. my $progress = 0;
  909. my $totalTC = 0;
  910. my $totalTS = scalar(@testSets);
  911. my %tcField = mapTCFields($cqc);
  912. my (@covered, @notcovered);
  913. foreach my $ts (@testSets) {
  914. #
  915. # Get all the test instances in the project node
  916. #
  917. $progress++;
  918. my $tsFactory = $ts->TSTestFactory;
  919. my @testInstances = in $tsFactory->NewList('');
  920. my $tsCount = scalar(@testInstances);
  921. my $tsArea = $ts->TestSetFolder->Name;
  922. my $tsSubArea = $ts->Name;
  923. my $tcInstance = 0;
  924. $logger->debug("($progress/$totalTS) test instances in: ",
  925. $tsArea, "\\", $tsSubArea, " : ", $tsCount);
  926. $totalTC += $tsCount;
  927. if ($tsCount) {
  928. # Look at each test instance and collect its details
  929. foreach my $tc (@testInstances) {
  930. my $tn = $tc->Name;
  931. if ($tc->Test->HasCoverage) {
  932. push @covered, $tc;
  933. }
  934. else {
  935. push @notcovered, $tc;
  936. }
  937. }
  938. }
  939. }
  940. $logger->debug("covered: ", scalar(@covered),
  941. " not covered: ", scalar(@notcovered));
  942. my @coverage;
  943. my $tc = 0;
  944. foreach my $test (@covered) {
  945. my @coverList = in $test->Test->GetCoverList;
  946. my @requirements;
  947. foreach my $requirement (@coverList) {
  948. if (not $requirement->Field('RQ_USER_03')) {
  949. $logger->warn(
  950. 'Unknown status in requirement ',
  951. $requirement->Name,
  952. ' for test ',
  953. $test->Test->Field('TS_SUBJECT')->Path,
  954. '\\',
  955. $test->Test->Name
  956. );
  957. }
  958. # we don't want to include any requirements that have been
  959. # deleted so we need to check that status first
  960. if ( ($requirement->Field('RQ_USER_03'))
  961. && ($requirement->Field('RQ_USER_03') eq 'Deleted'))
  962. {
  963. $logger->warn(
  964. 'Deleted requirement ',
  965. $requirement->Name,
  966. ' for test ',
  967. $test->Test->Field('TS_SUBJECT')->Path,
  968. '\\',
  969. $test->Test->Name
  970. );
  971. }
  972. else {
  973. # we don't want the ReqPRO tag in the requirement name
  974. # so we use the Requirement Name field in RQ_USER_05 but
  975. # sometimes we see an anomaly in the requirements where
  976. # the field will be blank, so we fallback to the Name field
  977. # in those cases.
  978. if ($requirement->Field('RQ_USER_05')) {
  979. push @requirements, $requirement->Field('RQ_USER_05');
  980. }
  981. else {
  982. # QC requirements are now (7/11/11) being populated in
  983. # such a way that this is almost always true. So for now
  984. # I'm commenting out this warning message
  985. # $logger->warn(
  986. # 'Null requirement name field for ',
  987. # $test->Test->Field('TS_SUBJECT')->Path,
  988. # '\\', $test->Test->Name,
  989. # ', ReqPro tag name: ', $requirement->Field('RQ_USER_06'),
  990. # ', RQ_REQ_NAME: ', $requirement->Field('RQ_REQ_NAME'),
  991. # ', using: ', $requirement->Name
  992. # );
  993. push @requirements, $requirement->Name;
  994. }
  995. }
  996. }
  997. # we should get here with a requirements list but I suppose it's
  998. # possible that we could end up with an empty list if all the
  999. # requirements for this test case have been deleted, so we need
  1000. # to check for an empty list
  1001. if (@requirements) {
  1002. push @coverage,
  1003. [
  1004. $test->Test->Field('TS_SUBJECT')->Path,
  1005. $test->Test->Name,
  1006. getDescription($test, 'Lab'),
  1007. join("\n", @requirements),
  1008. ];
  1009. }
  1010. else {
  1011. $logger->warn(
  1012. 'Empty requirement set for test ',
  1013. $test->Test->Field('TS_SUBJECT')->Path,
  1014. '\\', $test->Test->Name
  1015. );
  1016. }
  1017. unless (++$tc % 100) {
  1018. $logger->debug("__ processing test case $tc");
  1019. }
  1020. }
  1021. $logger->debug("Filtered tests found: ", scalar @coverage);
  1022. rtmReport(\@coverage, $report);
  1023. return scalar(@coverage);
  1024. }
  1025. sub rtmByTestReport {
  1026. my $tdc = shift;
  1027. my $filters_ref = shift;
  1028. my $report = shift;
  1029. my $rpt_del_req = shift;
  1030. my $logger = get_logger();
  1031. my @coverage;
  1032. foreach my $filter (@$filters_ref) {
  1033. # filter from the current folder using a cross-filter to
  1034. # select only those tests which have an associated requirement
  1035. my $tsFactory = $tdc->TestFactory;
  1036. my $tsFilter = $tsFactory->Filter;
  1037. my $rqFactory = $tdc->ReqFactory;
  1038. my $rqFilter = $rqFactory->Filter;
  1039. $tsFilter->SetProperty('Filter', 'TS_SUBJECT', $filter);
  1040. $rqFilter->SetProperty('Filter', 'RQ_REQ_ID', '> 0');
  1041. $tsFilter->SetXFilter('TEST-REQ', "TRUE", $rqFilter->Text);
  1042. my @tsList = in $tsFilter->NewList();
  1043. $logger->debug("Total tests found: ", scalar(@tsList));
  1044. my $tc = 0;
  1045. foreach my $test (@tsList) {
  1046. my @coverList = in $test->GetCoverList;
  1047. my @requirements;
  1048. foreach my $requirement (@coverList) {
  1049. if (not $requirement->Field('RQ_USER_03')) {
  1050. $logger->warn(
  1051. 'Unknown status in requirement ',
  1052. $requirement->Name,
  1053. ' for test ',
  1054. $test->Field('TS_SUBJECT')->Path,
  1055. '\\',
  1056. $test->Name
  1057. );
  1058. }
  1059. # we don't want to include any requirements that have been
  1060. # deleted so we need to check that status first
  1061. if ( ($requirement->Field('RQ_USER_03'))
  1062. && ($requirement->Field('RQ_USER_03') eq 'Deleted'))
  1063. {
  1064. $logger->warn(
  1065. 'Deleted requirement ',
  1066. $requirement->Name,
  1067. ' for test ',
  1068. $test->Field('TS_SUBJECT')->Path,
  1069. '\\',
  1070. $test->Name
  1071. );
  1072. }
  1073. else {
  1074. # we don't want the ReqPRO tag in the requirement name
  1075. # so we use the Requirement Name field in RQ_USER_05 but
  1076. # sometimes we see an anomaly in the requirements where
  1077. # the field will be blank, so we fallback to the Name field
  1078. # in those cases.
  1079. if ($requirement->Field('RQ_USER_05')) {
  1080. push @requirements, $requirement->Field('RQ_USER_05');
  1081. }
  1082. else {
  1083. $logger->warn(
  1084. 'Null requirement name field for ',
  1085. $test->Field('TS_SUBJECT')->Path,
  1086. '\\', $test->Name
  1087. );
  1088. push @requirements, $requirement->Name;
  1089. }
  1090. }
  1091. }
  1092. # we should get here with a requirements list but I suppose it's
  1093. # possible that we could end up with an empty list if all the
  1094. # requirements for this test case have been deleted, so we need
  1095. # to check for an empty list
  1096. if (@requirements) {
  1097. push @coverage,
  1098. [
  1099. $test->Field('TS_SUBJECT')->Path,
  1100. $test->Name,
  1101. getDescription($test, 'Plan'),
  1102. join("\n", @requirements),
  1103. ];
  1104. }
  1105. else {
  1106. $logger->warn(
  1107. 'Empty requirement set for test ',
  1108. $test->Field('TS_SUBJECT')->Path,
  1109. '\\', $test->Name
  1110. );
  1111. }
  1112. unless (++$tc % 100) {
  1113. $logger->debug("__ processing test case $tc");
  1114. }
  1115. }
  1116. }
  1117. $logger->debug("Filtered tests found: ", scalar @coverage);
  1118. rtmReport(\@coverage, $report);
  1119. return scalar(@coverage);
  1120. }
  1121. sub rtmReport {
  1122. my $coverage = shift;
  1123. my $report = shift;
  1124. my $logger = get_logger();
  1125. # set up the report output
  1126. $logger->debug("creating spreadsheet...");
  1127. my $excel = Win32::OLE->new('Excel.Application', 'Quit')
  1128. or $logger->logdie("cannot start Excel: ", Win32::OLE->LastError());
  1129. # avoid excessive Excel dialogs
  1130. $excel->{DisplayAlerts} = 0;
  1131. $excel->{Visible} = 0;
  1132. my $workbook = $excel->Workbooks->Add
  1133. or $logger->logdie("Can't create Excel workbook",
  1134. Win32::OLE->LastError());
  1135. my $sheet = $workbook->Worksheets("Sheet1");
  1136. # write the covered requirements if we have any
  1137. if (scalar @$coverage) {
  1138. $logger->debug("Writing ", scalar(@$coverage), " tests to report");
  1139. my $columns = scalar @{$coverage->[0]};
  1140. my $rows = scalar @$coverage;
  1141. my $row = 1;
  1142. $sheet->Rows($row)->Font->{Bold} = 'True';
  1143. $sheet->Range('A:A')->{ColumnWidth} = 75;
  1144. $sheet->Range('B:B')->{ColumnWidth} = 30;
  1145. $sheet->Range('C:C')->{ColumnWidth} = 100;
  1146. $sheet->Range('D:D')->{ColumnWidth} = 40;
  1147. $sheet->Range($sheet->Cells($row, 1), $sheet->Cells($row, $columns))
  1148. ->{Value} = ['TC Path', 'Name', 'Description', 'Requirement'];
  1149. # FIXME - why do I have to write this row by row?
  1150. # I don't know the underlying cause but when I pass it the entire
  1151. # array it stops after 258 rows. If I write row by row it usually
  1152. # succeeds and doesn't take an inordinate amount of time. But even
  1153. # writing row by row doesn't always work... sometimes it fails to
  1154. # write one or more of the cells. What's strange is that it's not
  1155. # a fatal error and there isn't an apparent reason for the failure.
  1156. # The record that fails is larger than the others but not huge and
  1157. # certainly within excel's limits.
  1158. foreach my $tc (@$coverage) {
  1159. # say Devel::Size::total_size($tc);
  1160. $row++;
  1161. my $col = 1;
  1162. foreach my $cellvalue (@$tc) {
  1163. $sheet->Cells($row, $col++)->{Value} = $cellvalue;
  1164. if (Win32::OLE->LastError()) {
  1165. $logger->warn(Win32::OLE->LastError(),
  1166. " at row $row, col ", $col-1);
  1167. }
  1168. }
  1169. unless ($row % 100) {
  1170. $logger->debug("__ processing test case $row");
  1171. }
  1172. }
  1173. saveSnapshot($workbook, $report, 'Sheet1');
  1174. }
  1175. else {
  1176. $logger->warn(
  1177. "No covered requirements found; no spreadsheet created.");
  1178. }
  1179. $workbook->Close();
  1180. $excel->Quit();
  1181. }
  1182. # -*- stubs -*-
  1183. 1; # Magic true value required at end of module
  1184. __END__