From 99c04099984315412ec3a6f169465c59b8f126cf Mon Sep 17 00:00:00 2001 From: JohannesFriedrich Date: Sat, 30 May 2026 08:40:26 +0200 Subject: [PATCH] add new reporter for sonarqube --- NAMESPACE | 1 + R/reporter-sonarqube.R | 155 +++++++++++++++++++++++++++++++++++++++ man/CheckReporter.Rd | 1 + man/DebugReporter.Rd | 1 + man/FailReporter.Rd | 1 + man/JunitReporter.Rd | 1 + man/ListReporter.Rd | 1 + man/LlmReporter.Rd | 1 + man/LocationReporter.Rd | 1 + man/MinimalReporter.Rd | 1 + man/MultiReporter.Rd | 1 + man/ProgressReporter.Rd | 1 + man/RStudioReporter.Rd | 1 + man/Reporter.Rd | 1 + man/SilentReporter.Rd | 1 + man/SlowReporter.Rd | 2 + man/SonarqubeReporter.Rd | 40 ++++++++++ man/StopReporter.Rd | 1 + man/SummaryReporter.Rd | 1 + man/TapReporter.Rd | 1 + man/TeamcityReporter.Rd | 1 + 21 files changed, 215 insertions(+) create mode 100644 R/reporter-sonarqube.R create mode 100644 man/SonarqubeReporter.Rd diff --git a/NAMESPACE b/NAMESPACE index 604f6fd44..b7547dfa8 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -48,6 +48,7 @@ export(RStudioReporter) export(Reporter) export(SilentReporter) export(SlowReporter) +export(SonarqubeReporter) export(StopReporter) export(SummaryReporter) export(TapReporter) diff --git a/R/reporter-sonarqube.R b/R/reporter-sonarqube.R new file mode 100644 index 000000000..baaa3d9ca --- /dev/null +++ b/R/reporter-sonarqube.R @@ -0,0 +1,155 @@ +#' Report results in SonarQube generic test execution XML format +#' +#' @description +#' This reporter produces XML output following the SonarQube +#' [Generic Test Execution Report Format](https://docs.sonarsource.com/sonarqube-server/analyzing-source-code/test-coverage/generic-test-data#generic-test-execution), +#' written to a file (or stdout). The resulting XML can be imported into +#' SonarQube by setting the `sonar.testExecutionReportPaths` analysis +#' parameter. Requires the _xml2_ package. +#' +#' Test files become `` elements (with the path attribute set to the +#' relative test file path), and individual `test_that()` blocks become +#' `` elements. On failure, error, or skip, a corresponding +#' child element (``, ``, or ``) is added to the +#' test case. +#' +#' @export +#' @family reporters +SonarqubeReporter <- R6::R6Class( + "SonarqubeReporter", + inherit = Reporter, + public = list( + timer = NULL, + doc = NULL, + root = NULL, + file_node = NULL, + file_name = NULL, + + elapsed_time = function() { + time <- (private$proctime() - self$timer)[["elapsed"]] + self$timer <- private$proctime() + time + }, + + start_reporter = function() { + check_installed("xml2", "to use SonarqubeReporter") + + self$timer <- private$proctime() + self$doc <- xml2::xml_new_document() + self$root <- xml2::xml_add_child( + self$doc, + "testExecutions", + version = "1" + ) + }, + + start_file = function(file) { + self$file_name <- file + + # Build path relative to the project root so SonarQube can locate + # the test file. TESTTHAT_WD holds the original working directory + # (i.e. the project root) while getwd() is the test directory. + path <- file + project_root <- Sys.getenv("TESTTHAT_WD", unset = "") + if (nzchar(project_root)) { + root <- normalizePath(project_root, winslash = "/", mustWork = FALSE) + current <- normalizePath(getwd(), winslash = "/", mustWork = FALSE) + if (startsWith(current, root)) { + rel_dir <- substring(current, nchar(root) + 2L) + if (nzchar(rel_dir)) { + path <- file.path(rel_dir, file) + } + } + } + + self$file_node <- xml2::xml_add_child( + self$root, + "file", + path = path + ) + }, + + start_test = function(context, test) { + # Reset timer at the start of each test + self$timer <- private$proctime() + }, + + add_result = function(context, test, result) { + withr::local_options(list(OutDec = ".")) + + time <- self$elapsed_time() + # SonarQube expects duration in milliseconds + duration_ms <- round(time * 1000) + + # If no file node was started, create a default one + if (is.null(self$file_node)) { + self$start_file(self$file_name %||% "(unknown)") + } + + name <- test %||% "(unnamed)" + testcase <- xml2::xml_add_child( + self$file_node, + "testCase", + name = name, + duration = as.character(duration_ms) + ) + + first_line <- function(x) { + loc <- expectation_location(x, " (", ")") + paste0(strsplit(cli::ansi_strip(x$message), split = "\n")[[1]][1], loc) + } + + if (expectation_error(result)) { + error_node <- xml2::xml_add_child( + testcase, + "error", + message = first_line(result) + ) + xml2::xml_text(error_node) <- cli::ansi_strip(format(result)) + } else if (expectation_failure(result)) { + failure_node <- xml2::xml_add_child( + testcase, + "failure", + message = first_line(result) + ) + xml2::xml_text(failure_node) <- cli::ansi_strip(format(result)) + } else if (expectation_skip(result)) { + xml2::xml_add_child(testcase, "skipped", message = first_line(result)) + } + }, + + end_file = function() { + self$file_node <- NULL + }, + + end_reporter = function() { + if (is.character(self$out)) { + xml2::write_xml(self$doc, self$out, format = TRUE) + } else if (inherits(self$out, "connection")) { + file <- withr::local_tempfile() + xml2::write_xml(self$doc, file, format = TRUE) + cat(brio::read_file(file), file = self$out) + } else { + cli::cli_abort("Unsupported output type: {toString(self$out)}.") + } + } + ), + + private = list( + proctime = function() { + proc.time() + } + ) +) + +# Mock for deterministic testing +SonarqubeReporterMock <- R6::R6Class( + "SonarqubeReporterMock", + inherit = SonarqubeReporter, + public = list(), + private = list( + proctime = function() { + c(user = 0, system = 0, elapsed = 0) + } + ) +) diff --git a/man/CheckReporter.Rd b/man/CheckReporter.Rd index e9adc9bfe..fe000cfc2 100644 --- a/man/CheckReporter.Rd +++ b/man/CheckReporter.Rd @@ -22,6 +22,7 @@ Other reporters: \code{\link{Reporter}}, \code{\link{SilentReporter}}, \code{\link{SlowReporter}}, +\code{\link{SonarqubeReporter}}, \code{\link{StopReporter}}, \code{\link{SummaryReporter}}, \code{\link{TapReporter}}, diff --git a/man/DebugReporter.Rd b/man/DebugReporter.Rd index 9939629a8..d94e6bf99 100644 --- a/man/DebugReporter.Rd +++ b/man/DebugReporter.Rd @@ -22,6 +22,7 @@ Other reporters: \code{\link{Reporter}}, \code{\link{SilentReporter}}, \code{\link{SlowReporter}}, +\code{\link{SonarqubeReporter}}, \code{\link{StopReporter}}, \code{\link{SummaryReporter}}, \code{\link{TapReporter}}, diff --git a/man/FailReporter.Rd b/man/FailReporter.Rd index e466789cf..7b60d38ec 100644 --- a/man/FailReporter.Rd +++ b/man/FailReporter.Rd @@ -23,6 +23,7 @@ Other reporters: \code{\link{Reporter}}, \code{\link{SilentReporter}}, \code{\link{SlowReporter}}, +\code{\link{SonarqubeReporter}}, \code{\link{StopReporter}}, \code{\link{SummaryReporter}}, \code{\link{TapReporter}}, diff --git a/man/JunitReporter.Rd b/man/JunitReporter.Rd index d65f0a0bb..be32aa3db 100644 --- a/man/JunitReporter.Rd +++ b/man/JunitReporter.Rd @@ -35,6 +35,7 @@ Other reporters: \code{\link{Reporter}}, \code{\link{SilentReporter}}, \code{\link{SlowReporter}}, +\code{\link{SonarqubeReporter}}, \code{\link{StopReporter}}, \code{\link{SummaryReporter}}, \code{\link{TapReporter}}, diff --git a/man/ListReporter.Rd b/man/ListReporter.Rd index e4172d2d5..72608a4c7 100644 --- a/man/ListReporter.Rd +++ b/man/ListReporter.Rd @@ -22,6 +22,7 @@ Other reporters: \code{\link{Reporter}}, \code{\link{SilentReporter}}, \code{\link{SlowReporter}}, +\code{\link{SonarqubeReporter}}, \code{\link{StopReporter}}, \code{\link{SummaryReporter}}, \code{\link{TapReporter}}, diff --git a/man/LlmReporter.Rd b/man/LlmReporter.Rd index a12364512..1e015e8a6 100644 --- a/man/LlmReporter.Rd +++ b/man/LlmReporter.Rd @@ -27,6 +27,7 @@ Other reporters: \code{\link{Reporter}}, \code{\link{SilentReporter}}, \code{\link{SlowReporter}}, +\code{\link{SonarqubeReporter}}, \code{\link{StopReporter}}, \code{\link{SummaryReporter}}, \code{\link{TapReporter}}, diff --git a/man/LocationReporter.Rd b/man/LocationReporter.Rd index f21fc072e..d5cfe5e69 100644 --- a/man/LocationReporter.Rd +++ b/man/LocationReporter.Rd @@ -23,6 +23,7 @@ Other reporters: \code{\link{Reporter}}, \code{\link{SilentReporter}}, \code{\link{SlowReporter}}, +\code{\link{SonarqubeReporter}}, \code{\link{StopReporter}}, \code{\link{SummaryReporter}}, \code{\link{TapReporter}}, diff --git a/man/MinimalReporter.Rd b/man/MinimalReporter.Rd index 762b256b4..4b757768b 100644 --- a/man/MinimalReporter.Rd +++ b/man/MinimalReporter.Rd @@ -24,6 +24,7 @@ Other reporters: \code{\link{Reporter}}, \code{\link{SilentReporter}}, \code{\link{SlowReporter}}, +\code{\link{SonarqubeReporter}}, \code{\link{StopReporter}}, \code{\link{SummaryReporter}}, \code{\link{TapReporter}}, diff --git a/man/MultiReporter.Rd b/man/MultiReporter.Rd index 3fe4444a3..f9ad08c4c 100644 --- a/man/MultiReporter.Rd +++ b/man/MultiReporter.Rd @@ -22,6 +22,7 @@ Other reporters: \code{\link{Reporter}}, \code{\link{SilentReporter}}, \code{\link{SlowReporter}}, +\code{\link{SonarqubeReporter}}, \code{\link{StopReporter}}, \code{\link{SummaryReporter}}, \code{\link{TapReporter}}, diff --git a/man/ProgressReporter.Rd b/man/ProgressReporter.Rd index a85f9e1a1..b857f29ff 100644 --- a/man/ProgressReporter.Rd +++ b/man/ProgressReporter.Rd @@ -33,6 +33,7 @@ Other reporters: \code{\link{Reporter}}, \code{\link{SilentReporter}}, \code{\link{SlowReporter}}, +\code{\link{SonarqubeReporter}}, \code{\link{StopReporter}}, \code{\link{SummaryReporter}}, \code{\link{TapReporter}}, diff --git a/man/RStudioReporter.Rd b/man/RStudioReporter.Rd index 102ce52b2..43c735711 100644 --- a/man/RStudioReporter.Rd +++ b/man/RStudioReporter.Rd @@ -22,6 +22,7 @@ Other reporters: \code{\link{Reporter}}, \code{\link{SilentReporter}}, \code{\link{SlowReporter}}, +\code{\link{SonarqubeReporter}}, \code{\link{StopReporter}}, \code{\link{SummaryReporter}}, \code{\link{TapReporter}}, diff --git a/man/Reporter.Rd b/man/Reporter.Rd index a020bd22d..724fe7efe 100644 --- a/man/Reporter.Rd +++ b/man/Reporter.Rd @@ -38,6 +38,7 @@ Other reporters: \code{\link{RStudioReporter}}, \code{\link{SilentReporter}}, \code{\link{SlowReporter}}, +\code{\link{SonarqubeReporter}}, \code{\link{StopReporter}}, \code{\link{SummaryReporter}}, \code{\link{TapReporter}}, diff --git a/man/SilentReporter.Rd b/man/SilentReporter.Rd index 65279d754..4ce623455 100644 --- a/man/SilentReporter.Rd +++ b/man/SilentReporter.Rd @@ -23,6 +23,7 @@ Other reporters: \code{\link{RStudioReporter}}, \code{\link{Reporter}}, \code{\link{SlowReporter}}, +\code{\link{SonarqubeReporter}}, \code{\link{StopReporter}}, \code{\link{SummaryReporter}}, \code{\link{TapReporter}}, diff --git a/man/SlowReporter.Rd b/man/SlowReporter.Rd index 2791a1602..03c9a2cef 100644 --- a/man/SlowReporter.Rd +++ b/man/SlowReporter.Rd @@ -32,6 +32,7 @@ Other reporters: \code{\link{RStudioReporter}}, \code{\link{Reporter}}, \code{\link{SilentReporter}}, +\code{\link{SonarqubeReporter}}, \code{\link{StopReporter}}, \code{\link{SummaryReporter}}, \code{\link{TapReporter}}, @@ -51,6 +52,7 @@ Other reporters: \code{\link{RStudioReporter}}, \code{\link{Reporter}}, \code{\link{SilentReporter}}, +\code{\link{SonarqubeReporter}}, \code{\link{StopReporter}}, \code{\link{SummaryReporter}}, \code{\link{TapReporter}}, diff --git a/man/SonarqubeReporter.Rd b/man/SonarqubeReporter.Rd new file mode 100644 index 000000000..b7d6899a9 --- /dev/null +++ b/man/SonarqubeReporter.Rd @@ -0,0 +1,40 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/reporter-sonarqube.R +\name{SonarqubeReporter} +\alias{SonarqubeReporter} +\title{Report results in SonarQube generic test execution XML format} +\description{ +This reporter produces XML output following the SonarQube +\href{https://docs.sonarsource.com/sonarqube-server/analyzing-source-code/test-coverage/generic-test-data#generic-test-execution}{Generic Test Execution Report Format}, +written to a file (or stdout). The resulting XML can be imported into +SonarQube by setting the \code{sonar.testExecutionReportPaths} analysis +parameter. Requires the \emph{xml2} package. + +Test files become \verb{} elements (with the path attribute set to the +relative test file path), and individual \code{test_that()} blocks become +\verb{} elements. On failure, error, or skip, a corresponding +child element (\verb{}, \verb{}, or \verb{}) is added to the +test case. +} +\seealso{ +Other reporters: +\code{\link{CheckReporter}}, +\code{\link{DebugReporter}}, +\code{\link{FailReporter}}, +\code{\link{JunitReporter}}, +\code{\link{ListReporter}}, +\code{\link{LlmReporter}}, +\code{\link{LocationReporter}}, +\code{\link{MinimalReporter}}, +\code{\link{MultiReporter}}, +\code{\link{ProgressReporter}}, +\code{\link{RStudioReporter}}, +\code{\link{Reporter}}, +\code{\link{SilentReporter}}, +\code{\link{SlowReporter}}, +\code{\link{StopReporter}}, +\code{\link{SummaryReporter}}, +\code{\link{TapReporter}}, +\code{\link{TeamcityReporter}} +} +\concept{reporters} diff --git a/man/StopReporter.Rd b/man/StopReporter.Rd index 76d6761f8..6ae1b0963 100644 --- a/man/StopReporter.Rd +++ b/man/StopReporter.Rd @@ -24,6 +24,7 @@ Other reporters: \code{\link{Reporter}}, \code{\link{SilentReporter}}, \code{\link{SlowReporter}}, +\code{\link{SonarqubeReporter}}, \code{\link{SummaryReporter}}, \code{\link{TapReporter}}, \code{\link{TeamcityReporter}} diff --git a/man/SummaryReporter.Rd b/man/SummaryReporter.Rd index 53ff6570f..1824bf686 100644 --- a/man/SummaryReporter.Rd +++ b/man/SummaryReporter.Rd @@ -30,6 +30,7 @@ Other reporters: \code{\link{Reporter}}, \code{\link{SilentReporter}}, \code{\link{SlowReporter}}, +\code{\link{SonarqubeReporter}}, \code{\link{StopReporter}}, \code{\link{TapReporter}}, \code{\link{TeamcityReporter}} diff --git a/man/TapReporter.Rd b/man/TapReporter.Rd index a92c154e5..948b93412 100644 --- a/man/TapReporter.Rd +++ b/man/TapReporter.Rd @@ -24,6 +24,7 @@ Other reporters: \code{\link{Reporter}}, \code{\link{SilentReporter}}, \code{\link{SlowReporter}}, +\code{\link{SonarqubeReporter}}, \code{\link{StopReporter}}, \code{\link{SummaryReporter}}, \code{\link{TeamcityReporter}} diff --git a/man/TeamcityReporter.Rd b/man/TeamcityReporter.Rd index 6eeadf049..38dbec604 100644 --- a/man/TeamcityReporter.Rd +++ b/man/TeamcityReporter.Rd @@ -24,6 +24,7 @@ Other reporters: \code{\link{Reporter}}, \code{\link{SilentReporter}}, \code{\link{SlowReporter}}, +\code{\link{SonarqubeReporter}}, \code{\link{StopReporter}}, \code{\link{SummaryReporter}}, \code{\link{TapReporter}}