diff --git a/README.md b/README.md index f3a82aa..151c3d5 100644 --- a/README.md +++ b/README.md @@ -64,12 +64,12 @@ STORY-NUMBER1 | TEST-NAME1 | PASSED dumyTestName__DEOPSCSW_storyId1_DEOPSCSW_storyId2 ``` -# TMT Requirement Test Mapper +# TMT Requirement Test Mapper (aka RTM) ## Prerequisites - Story-Requirement mapping from JIRA. - The Story-Requirement mappings file need to be in the CSV format like following- + The Story-Requirement mappings file need to be in the CSV format like following- ``` STORY-NUMBER1,REQUIREMENT-NUMBER1 STORY-NUMBER2,REQUIREMENT-NUMBER2,REQUIREMENT-NUMBER3 @@ -81,7 +81,7 @@ STORY-NUMBER1 | TEST-NAME1 | PASSED STORY-NUMBER2 | TEST-NAME2 | FAILED ``` -## To generate reports +## To generate RTM-reports Call the TestRequirementMapper from the bash shell by executing command with following arguments - test-story mapping file path (generated using test reporter) @@ -90,3 +90,22 @@ Call the TestRequirementMapper from the bash shell by executing command with fol ``` > coursier launch --channel https://raw.githubusercontent.com/tmtsoftware/osw-apps/master/apps.json rtm:0.3.0 -- ``` + +## To aggregate several RTM-reports + +For some projects (for example ESW), the final RTM will consist of several RTMs from subprojects. +There is a special command to aggregate many RTM-reports into one, i.e. for ESW it will look like: +```shell +sbt "runMain tmt.test.reporter.RtmAggregator esw:path/to/rtm.txt esw-ts:path/to/rtm.txt esw-ocs-eng-ui:path/to/rtm.txt esw-observing-simulation:path/to/rtm.txt" +``` +Here, we are aggregating RTM-reports from four subproject into one. Each RTM-report is specified as a parameter (for example `esw:path/to/rtm.txt`) with the format: +```shell +: +``` +Where: +- `` some unique tag that you want to use in the aggregated RTM-report for the given subproject +- `` path to RTM-report for the given subproject + +As an output of the command, there will be created two files in the current folder: +- `aggregated.txt` - aggregated RTM-report in CSV format +- `aggregated.html` - aggregated RTM-report in HTML format diff --git a/src/main/scala/tmt/test/reporter/RtmAggregator.scala b/src/main/scala/tmt/test/reporter/RtmAggregator.scala new file mode 100644 index 0000000..7175495 --- /dev/null +++ b/src/main/scala/tmt/test/reporter/RtmAggregator.scala @@ -0,0 +1,40 @@ +package tmt.test.reporter + +import tmt.test.reporter.Separators.PIPE + +import scala.io.Source + +object RtmAggregator extends App { + val aggregatedRtm = args.flatMap(projAndPath => { + val (project, filePath) = projAndPath.split(":").toList match { + case p :: f :: Nil => (p, f) + case _ => throw new RuntimeException( + s"**** Provided parameter is not in valid format : '$projAndPath' ****\n" + + "RTM aggregator parameter should be in 'project:file' format" + ) + } + println("Project: " + project + "; Reading file: " + filePath) + val fileSource = Source.fromFile(filePath) + fileSource.getLines().map( line => { + val (story, req, test, status) = line.split(PIPE).toList match { + case s :: r :: t :: st :: Nil => (s.trim, r.trim, t.trim, st.trim) + case _ => + throw new RuntimeException( + s"**** Provided data ($filePath) is not in valid format : '$line' ****\n" + + "RTM mapping should be in 'story number | requirement | test name | test status' format (Pipe '|' separated format)" + ) + } + + // remove any extra text from story ID (and then append it to the test description) + val (storyId, someText) = story.split(" ").toList match { + case s :: Nil => (s, "") + case s :: theRest => (s, theRest.mkString(" ")) + } + + TestRequirementMapped(storyId, req, project + ": " + test + " " + someText, status.toUpperCase) + }) + }).toList + TestRequirementMapper.createCsvFile("./aggregated.txt", aggregatedRtm) + TestRequirementMapper.createHtmlReport("./aggregated.html", aggregatedRtm) + println("**** Successfully aggregated and mapped Test results to Requirements ****") +} \ No newline at end of file diff --git a/src/main/scala/tmt/test/reporter/TestRequirementMapper.scala b/src/main/scala/tmt/test/reporter/TestRequirementMapper.scala index 7f8f9d8..bc42b75 100644 --- a/src/main/scala/tmt/test/reporter/TestRequirementMapper.scala +++ b/src/main/scala/tmt/test/reporter/TestRequirementMapper.scala @@ -43,8 +43,8 @@ object TestRequirementMapper { val requirements = requirementsContent.asScala.toList.map { line => val (story, requirement) = line.splitAt(line.indexOf(COMMA)) match { - case (s, req) if !s.isEmpty => (s, req.drop(1)) // drop to remove the first comma & requirement can be empty. - case _ => + case (s, req) if s.nonEmpty => (s, req.drop(1)) // drop to remove the first comma & requirement can be empty. + case _ => throw new RuntimeException( s"**** Provided data is not in valid format : '$line' ****\n" + s"Story-Requirement mapping should be in 'story number $COMMA requirement' format (Comma ',' separated format)" @@ -55,86 +55,17 @@ object TestRequirementMapper { } // map tests to requirements and sort by story ID - val testAndReqMapped = storyResults - .map { storyResult => - val correspondingReq = requirements - .find(_.story == storyResult.story) // find the Requirements of given story - .map(_.number) // take out the Requirement number - .filter(!_.isEmpty) // remove if Requirement number is empty - .getOrElse(Requirement.EMPTY) + val testAndReqMapped = storyResults.map { storyResult => + val correspondingReq = requirements + .find(_.story == storyResult.story) // find the Requirements of given story + .map(_.number) // take out the Requirement number + .filter(_.nonEmpty) // remove if Requirement number is empty + .getOrElse(Requirement.EMPTY) TestRequirementMapped(storyResult.story, correspondingReq, storyResult.test, storyResult.status) } .sortWith((a, b) => a.story.compareTo(b.story) < 0) - def createHtmlReport(): Unit = { - val writer = new FileWriter(outputPath + ".html") - val testAndReqGrouped = testAndReqMapped - .filter(s => !s.story.isEmpty && s.story != Requirement.EMPTY) - .groupBy(_.story) - - html( - head( - raw("") - ), - body( - a(name := "toc"), - h1("RTM report"), - div("Generation time: ", Calendar.getInstance().getTime().toString), - h2("Summary"), - table(width := "50%")( - tr( - th("Story ID"), - th("Requirements"), - th(width := "10%")("Status") - ), - for ((storyId, testResults) <- testAndReqGrouped.toSeq) - yield tr( - td( - a(href := "#" + storyId)(storyId) - ), - td(testResults(0).reqNum.replace(",", ", ")), - if (testResults.count(t => t.status.toUpperCase == TestStatus.FAILED) > 0) td(color := "red")(TestStatus.FAILED) - else if (testResults.count(t => t.status.toUpperCase != TestStatus.PASSED) > 0) - td(color := "orange")(TestStatus.FAILED) - else td(color := "green")(TestStatus.PASSED) - ) - ), - for ((storyId, testResults) <- testAndReqGrouped.toSeq) - yield div( - h3( - a(name := storyId)(storyId) - ), - p("Requirements: ", testResults(0).reqNum.replace(",", ", ")), - p( - "JIRA link: ", - a(href := "https://tmt-project.atlassian.net/browse/" + storyId, target := "_blank")(storyId) - ), - p("Tests:"), - table(width := "50%")( - tr( - th("Test Name"), - th(width := "10%")("Status") - ), - for (testRes <- testResults) - yield tr( - td(testRes.test), - if (testRes.status.toUpperCase == TestStatus.FAILED) td(color := "red")(TestStatus.FAILED) - else if (testRes.status.toUpperCase == TestStatus.PASSED) td(color := "green")(TestStatus.PASSED) - else td(color := "orange")(testRes.status.toUpperCase) - ) - ), - p( - a(href := "#toc")("back to top") - ), - hr() - ) - ) - ).writeTo(writer) - - writer.close() - } - val outputFile = new File(outputPath) val indexPath = "/index.html" @@ -156,19 +87,87 @@ object TestRequirementMapper { } // create RTM in HTML format - createHtmlReport() + createHtmlReport(outputPath+ ".html", testAndReqMapped) // create index.html file createIndexFile() - // write to file + // write to csv-file println("[INFO] Writing results to - " + outputPath) Files.createDirectories(outputFile.getParentFile.toPath) - val writer = new FileWriter(outputPath) - testAndReqMapped.map(result => result.format(PIPE) + NEWLINE).foreach(writer.write) - writer.close() + createCsvFile(outputPath, testAndReqMapped) println( s"**** Successfully mapped Test results to Requirements **** : Check ${new File(outputPath).getCanonicalPath} for results" ) } + + def createCsvFile(outputPath: String, testAndReqMapped: List[TestRequirementMapped]): Unit = { + val writer = new FileWriter(outputPath) + testAndReqMapped.map(result => result.format(PIPE) + NEWLINE).foreach(writer.write) + writer.close() + } + + def createHtmlReport(outputPath: String, testAndReqMapped: List[TestRequirementMapped]): Unit = { + val writer = new FileWriter(outputPath) + val testAndReqGrouped = testAndReqMapped + .filter(s => s.story.nonEmpty && s.story != Requirement.EMPTY) + .groupBy(_.story) + + html( + head( + raw("") + ), + body( + a(name := "toc"), + h1("RTM report"), + div("Generation time: ", Calendar.getInstance().getTime().toString), + h2("Summary"), + table(width := "50%")( + tr( + th("Story ID"), + th("Requirements"), + th(width := "10%")("Status") + ), + for ((storyId, testResults) <- testAndReqGrouped.toSeq) yield tr( + td( + a(href := "#" + storyId)(storyId) + ), + td(testResults(0).reqNum.replace(",", ", ")), + if (testResults.count(t => t.status.toUpperCase == TestStatus.FAILED) > 0) td(color:="red")(TestStatus.FAILED) + else if (testResults.count(t => t.status.toUpperCase != TestStatus.PASSED) > 0) td(color:="orange")(TestStatus.FAILED) + else td(color:="green")(TestStatus.PASSED) + ) + ), + for ((storyId, testResults) <- testAndReqGrouped.toSeq) yield div( + h3( + a(name := storyId)(storyId) + ), + p("Requirements: ", testResults(0).reqNum.replace(",", ", ")), + p( + "JIRA link: ", a(href := "https://tmt-project.atlassian.net/browse/" + storyId, target := "_blank")(storyId) + ), + p("Tests:"), + table(width := "50%")( + tr( + th("Test Name"), + th(width := "10%")("Status") + ), + for (testRes <- testResults) yield tr( + td(testRes.test), + if (testRes.status.toUpperCase == TestStatus.FAILED) td(color:="red")(TestStatus.FAILED) + else if (testRes.status.toUpperCase == TestStatus.PASSED) td(color:="green")(TestStatus.PASSED) + else td(color:="orange")(testRes.status.toUpperCase) + ) + ), + p( + a(href := "#toc")("back to top") + ), + hr(), + ), + ) + ).writeTo(writer) + + writer.close() + } + }