Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RTM-aggregator added, fixes, README modified #4

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 22 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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 -- <path of file containing Test-Story mapping > <path of file containing Story-Requirement mapping> <output path>
```

## 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
<sub-project>:<path-to-rtm>
```
Where:
- `<sub-project>` some unique tag that you want to use in the aggregated RTM-report for the given subproject
- `<path-to-rtm>` 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
40 changes: 40 additions & 0 deletions src/main/scala/tmt/test/reporter/RtmAggregator.scala
Original file line number Diff line number Diff line change
@@ -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 ****")
}
163 changes: 81 additions & 82 deletions src/main/scala/tmt/test/reporter/TestRequirementMapper.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)"
Expand All @@ -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("<style>table, th, td {border: 1px solid black; border-collapse: collapse; }</style>")
),
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"

Expand All @@ -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("<style>table, th, td {border: 1px solid black; border-collapse: collapse; }</style>")
),
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()
}

}