Skip to content

Commit

Permalink
feat: Import coverage reports from utPLSQL (#156)
Browse files Browse the repository at this point in the history
  • Loading branch information
felipebz committed May 23, 2024
1 parent 2ba31ad commit b75eecf
Show file tree
Hide file tree
Showing 10 changed files with 263 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,15 @@ class PlSqlPlugin : Plugin {
.defaultValue(UtPlSqlSensor.DEFAULT_TEST_REPORT_PATH)
.multiValues(true)
.build(),
PropertyDefinition.builder(UtPlSqlSensor.COVERAGE_REPORT_PATH_KEY)
.name("Path to the utPLSQL coverage report(s)")
.description("Paths (absolute or relative) to report files with utPLSQL coverage data.")
.category(DEFAULT_CATEGORY)
.subCategory(TEST_AND_COVERAGE)
.onQualifiers(Qualifiers.PROJECT)
.defaultValue(UtPlSqlSensor.DEFAULT_COVERAGE_REPORT_PATH)
.multiValues(true)
.build(),

UtPlSqlSensor::class.java,
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/**
* Z PL/SQL Analyzer
* Copyright (C) 2015-2024 Felipe Zorzo
* mailto:felipe AT felipezorzo DOT com DOT br
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.plsqlopen.utplsql

import org.simpleframework.xml.Attribute
import org.simpleframework.xml.ElementList
import org.simpleframework.xml.Root

@Root(name = "coverage")
data class Coverage @JvmOverloads constructor(
@field:Attribute(name = "version")
var version: Int = 0,

@field:ElementList(name = "file", inline = true, required = false)
var files: List<CoveredFile>? = null
)

@Root(name = "file")
data class CoveredFile @JvmOverloads constructor(
@field:Attribute(name = "path")
var path: String = "",

@field:ElementList(name = "lineToCover", inline = true, required = false)
var linesToCover: List<LineToCover>? = null
)

@Root(name = "lineToCover")
data class LineToCover @JvmOverloads constructor(
@field:Attribute(name = "lineNumber")
var lineNumber: Int = 0,

@field:Attribute(name = "covered")
var covered: Boolean = false,

@field:Attribute(name = "branchesToCover", required = false)
var branchesToCover: Int? = null,

@field:Attribute(name = "coveredBranches", required = false)
var coveredBranches: Int? = null
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/**
* Z PL/SQL Analyzer
* Copyright (C) 2015-2024 Felipe Zorzo
* mailto:felipe AT felipezorzo DOT com DOT br
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.plsqlopen.utplsql

import org.simpleframework.xml.core.Persister
import org.sonar.api.batch.sensor.SensorContext
import org.sonar.api.notifications.AnalysisWarnings
import org.sonar.plsqlopen.symbols.ObjectLocator
import org.sonar.plugins.plsqlopen.api.PlSqlGrammar
import java.io.File


class CoverageResultImporter(private val objectLocator: ObjectLocator,
analysisWarnings: AnalysisWarnings) : AbstractReportImporter(analysisWarnings) {

override val reportType = "coverage"
override val reportKey = UtPlSqlSensor.COVERAGE_REPORT_PATH_KEY

override fun processReport(context: SensorContext, report: File) {
val serializer = Persister()
val coverage = serializer.read(Coverage::class.java, report)

coverage.files?.forEach { file ->
val filePath = file.path
var inputFile = context.fileSystem()
.inputFile(context.fileSystem().predicates().hasPath(filePath))

if (inputFile == null) {
val objectType = when (filePath.substringBeforeLast(' ')) {
"package body" -> PlSqlGrammar.CREATE_PACKAGE_BODY
"procedure" -> PlSqlGrammar.CREATE_PROCEDURE
"function" -> PlSqlGrammar.CREATE_FUNCTION
"trigger" -> PlSqlGrammar.CREATE_TRIGGER
"type body" -> PlSqlGrammar.CREATE_TYPE_BODY
else -> error("Unknown object type for file \"$filePath\"")
}
val objectName = filePath.substringAfterLast('.')

val mappedFile = objectLocator.findMainObject(objectName, objectType)

inputFile = mappedFile?.inputFile
}

if (inputFile != null) {
val newCoverage = context.newCoverage().onFile(inputFile)

file.linesToCover?.forEach { line ->
newCoverage.lineHits(line.lineNumber, if (line.covered) 1 else 0)

val branchesToCover = line.branchesToCover
val coveredBranches = line.coveredBranches ?: 0
if (branchesToCover != null) {
check(coveredBranches <= branchesToCover) {
"\"coveredBranches\" should not be greater than \"branchesToCover\" on line " +
"${line.lineNumber} for file \"$filePath\""
}

newCoverage.conditions(line.lineNumber, branchesToCover, coveredBranches)
}
}

newCoverage.save()
}
}
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,22 @@ import org.sonar.plsqlopen.symbols.ObjectLocator
class UtPlSqlSensor(objectLocator: ObjectLocator, analysisWarnings: AnalysisWarnings) : Sensor {

private val testResultImporter = TestResultImporter(objectLocator, analysisWarnings)
private val coverageResultImporter = CoverageResultImporter(objectLocator, analysisWarnings)

override fun describe(descriptor: SensorDescriptor) {
descriptor.name("Z PL/SQL Analyzer - utPLSQL Report Importer").onlyOnLanguage(PlSql.KEY)
}

override fun execute(context: SensorContext) {
testResultImporter.execute(context)
coverageResultImporter.execute(context)
}

companion object {
const val TEST_REPORT_PATH_KEY = "sonar.zpa.tests.reportPaths"
const val DEFAULT_TEST_REPORT_PATH = "utplsql-test.xml"
const val COVERAGE_REPORT_PATH_KEY = "sonar.zpa.coverage.reportPaths"
const val DEFAULT_COVERAGE_REPORT_PATH = "utplsql-coverage.xml"
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class PlSqlPluginTest {
val context = Plugin.Context(SonarRuntimeImpl.forSonarQube(Version.create(6, 0), SonarQubeSide.SERVER, SonarEdition.COMMUNITY))
val plugin = PlSqlPlugin()
plugin.define(context)
assertThat(context.extensions).hasSize(11)
assertThat(context.extensions).hasSize(12)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import org.sonar.plsqlopen.symbols.ObjectLocator
import org.sonar.plugins.plsqlopen.api.PlSqlFile
import org.sonar.plugins.plsqlopen.api.PlSqlGrammar
import java.io.File
import java.nio.charset.StandardCharsets

class UtPlSqlSensorTest {

Expand All @@ -60,7 +61,7 @@ class UtPlSqlSensorTest {
}

@Test
fun shouldImportReportWithPaths() {
fun shouldImportTestReportWithPaths() {
val testFile = TestInputFileBuilder("moduleKey", "path/to/file.sql")
.setType(InputFile.Type.TEST)
.build()
Expand All @@ -80,14 +81,15 @@ class UtPlSqlSensorTest {
}

@Test
fun shouldImportReportWithoutPaths() {
fun shouldImportTestReportWithoutPaths() {
val testFile = TestInputFileBuilder("moduleKey", "path/to/file.sql")
.setType(InputFile.Type.TEST)
.build()
context.fileSystem().add(testFile)

whenever(objectLocator.findTestObject(eq("test_package"), any())).thenReturn(
MappedObject("", PlSqlGrammar.CREATE_PACKAGE_BODY, PlSqlFile.Type.TEST, testFile.path(), testFile))
MappedObject("", PlSqlGrammar.CREATE_PACKAGE_BODY, PlSqlFile.Type.TEST, testFile.path(), testFile)
)

context.settings().setProperty(UtPlSqlSensor.TEST_REPORT_PATH_KEY, "test-report-with-paths.xml")
sensor.execute(context)
Expand All @@ -101,9 +103,65 @@ class UtPlSqlSensorTest {
}

@Test
fun invalidReport() {
fun invalidTestReport() {
context.settings().setProperty(UtPlSqlSensor.TEST_REPORT_PATH_KEY, "doesnotexists.xml")
sensor.execute(context)
verify(analysisWarnings).addUnique("No utPLSQL test report was found for sonar.zpa.tests.reportPaths using pattern doesnotexists.xml")
}

@Test
fun shouldImportCoverageReportWithPaths() {
val relativePath = "award_bonus.sql"
val mainFile = TestInputFileBuilder("moduleKey", relativePath)
.setType(InputFile.Type.MAIN)
.setCharset(StandardCharsets.UTF_8)
.initMetadata(File(context.fileSystem().baseDir(), relativePath).readText())
.build()
context.fileSystem().add(mainFile)

whenever(objectLocator.findMainObject(any(), any())).thenReturn(null)

context.settings().setProperty(UtPlSqlSensor.COVERAGE_REPORT_PATH_KEY, "coverage-report-with-paths.xml")
sensor.execute(context)

val key = mainFile.key()
assertThat(context.lineHits(key, 5)).isOne()
assertThat(context.lineHits(key, 10)).isOne()
assertThat(context.lineHits(key, 11)).isOne()
assertThat(context.lineHits(key, 13)).isOne()

assertThat(context.conditions(key, 10)).isEqualTo(2)
assertThat(context.coveredConditions(key, 10)).isEqualTo(2)
}

@Test
fun shouldImportCoverageReportWithoutPaths() {
val relativePath = "betwnstr.sql"
val mainFile = TestInputFileBuilder("moduleKey", relativePath)
.setType(InputFile.Type.MAIN)
.setCharset(StandardCharsets.UTF_8)
.initMetadata(File(context.fileSystem().baseDir(), relativePath).readText())
.build()
context.fileSystem().add(mainFile)

whenever(objectLocator.findMainObject(any(), any())).thenReturn(
MappedObject("", PlSqlGrammar.CREATE_FUNCTION, PlSqlFile.Type.MAIN, mainFile.path(), mainFile)
)

context.settings().setProperty(UtPlSqlSensor.COVERAGE_REPORT_PATH_KEY, "coverage-report-without-paths.xml")
sensor.execute(context)

val key = mainFile.key()
assertThat(context.lineHits(key, 2)).isOne()
assertThat(context.lineHits(key, 4)).isOne()
assertThat(context.lineHits(key, 5)).isOne()
assertThat(context.lineHits(key, 7)).isOne()
}

@Test
fun invalidCoverageReport() {
context.settings().setProperty(UtPlSqlSensor.COVERAGE_REPORT_PATH_KEY, "doesnotexists.xml")
sensor.execute(context)
verify(analysisWarnings).addUnique("No utPLSQL coverage report was found for sonar.zpa.coverage.reportPaths using pattern doesnotexists.xml")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
create or replace procedure award_bonus(emp_id number, sales_amt number) as
commission real;
comm_missing exception;
begin
select commission_pct
into commission
from employees_test
where employee_id = emp_id;

if commission is null then
raise comm_missing;
else
update employees_test
set salary = nvl(salary, 0) + sales_amt * commission
where employee_id = emp_id;
end if;
end;
/
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
create or replace function betwnstr(a_string varchar2, a_start_pos integer, a_end_pos integer) return varchar2 is
l_start_pos pls_integer := a_start_pos;
begin
if l_start_pos = 0 then
l_start_pos := 1;
end if;
return substr(a_string, l_start_pos, a_end_pos - l_start_pos + 1);
end;
/
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<coverage version="1">
<file path="award_bonus.sql">
<lineToCover lineNumber="5" covered="true"/>
<lineToCover lineNumber="10" covered="true" branchesToCover="2" coveredBranches="2"/>
<lineToCover lineNumber="11" covered="true"/>
<lineToCover lineNumber="13" covered="true"/>
</file>
</coverage>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<coverage version="1">
<file path="function ut3_demo.betwnstr">
<lineToCover lineNumber="2" covered="true"/>
<lineToCover lineNumber="4" covered="true"/>
<lineToCover lineNumber="5" covered="true"/>
<lineToCover lineNumber="7" covered="true"/>
</file>
</coverage>

0 comments on commit b75eecf

Please sign in to comment.