From f701ff37fdef7c712e18e3e571c6fe6fbe3d7e41 Mon Sep 17 00:00:00 2001 From: Dirk Eddelbuettel Date: Tue, 13 Jan 2026 13:26:11 -0600 Subject: [PATCH 1/2] Additional unit tests and removal of #nocov tags --- ChangeLog | 10 ++++ R/Attributes.R | 16 +++--- inst/tinytest/cpp/attributes_extended.cpp | 49 +++++++++++++++++++ inst/tinytest/test_attributes_extended.R | 42 ++++++++++++++++ .../tinytest/test_compile_attributes_errors.R | 33 +++++++++++++ inst/tinytest/test_cppfunction_errors.R | 35 +++++++++++++ inst/tinytest/test_sourcecpp_errors.R | 42 ++++++++++++++++ src/attributes.cpp | 16 +++--- 8 files changed, 227 insertions(+), 16 deletions(-) create mode 100644 inst/tinytest/cpp/attributes_extended.cpp create mode 100644 inst/tinytest/test_attributes_extended.R create mode 100644 inst/tinytest/test_compile_attributes_errors.R create mode 100644 inst/tinytest/test_cppfunction_errors.R create mode 100644 inst/tinytest/test_sourcecpp_errors.R diff --git a/ChangeLog b/ChangeLog index c982a2570..7b18e3fcc 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,13 @@ +2026-01-13 Dirk Eddelbuettel + + * inst/tinytest/cpp/attributes_extended.cpp: New unit tests + * inst/tinytest/test_attributes_extended.R: Idem + * inst/tinytest/test_compile_attributes_errors.R: Idem + * inst/tinytest/test_cppfunction_errors.R: Idem + * inst/tinytest/test_sourcecpp_errors.R: Idem + * R/Attributes.R: Remove #nocov tags + * src/attributes.cpp: Idem + 2026-01-12 Dirk Eddelbuettel * DESCRIPTION (Version, Date): Roll micro version and date diff --git a/R/Attributes.R b/R/Attributes.R index f34e83b17..9f85b85fa 100644 --- a/R/Attributes.R +++ b/R/Attributes.R @@ -55,13 +55,13 @@ sourceCpp <- function(file = "", file <- normalizePath(file, winslash = "/") # error if the file extension isn't one supported by R CMD SHLIB - if (! tools::file_ext(file) %in% c("cc", "cpp")) { # #nocov start + if (! tools::file_ext(file) %in% c("cc", "cpp")) { stop("The filename '", basename(file), "' does not have an ", "extension of .cc or .cpp so cannot be compiled.") - } # #nocov end + } # validate that there are no spaces in the path on windows - if (.Platform$OS.type == "windows") { # #nocov start + if (.Platform$OS.type == "windows") { if (grepl(' ', basename(file), fixed=TRUE)) { stop("The filename '", basename(file), "' contains spaces. This ", "is not permitted.") @@ -73,7 +73,7 @@ sourceCpp <- function(file = "", "non-Windows platforms.") } windowsDebugDLL <- FALSE # now we do not need to deal with OS choice below - } # #nocov end + } } # get the context (does code generation as necessary) @@ -323,9 +323,9 @@ cppFunction <- function(code, # verify that a single function was exported and return it if (length(exported$functions) == 0) - stop("No function definition found") # #nocov + stop("No function definition found") else if (length(exported$functions) > 1) - stop("More than one function definition") # #nocov + stop("More than one function definition") else { functionName <- exported$functions[[1]] invisible(get(functionName, env)) @@ -417,7 +417,7 @@ compileAttributes <- function(pkgdir = ".", verbose = getOption("verbose")) { pkgdir <- normalizePath(pkgdir, winslash = "/") descFile <- file.path(pkgdir,"DESCRIPTION") if (!file.exists(descFile)) - stop("pkgdir must refer to the directory containing an R package") # #nocov + stop("pkgdir must refer to the directory containing an R package") pkgDesc <- read.dcf(descFile)[1,] pkgname = .readPkgDescField(pkgDesc, "Package") depends <- c(.readPkgDescField(pkgDesc, "Depends", character()), @@ -429,7 +429,7 @@ compileAttributes <- function(pkgdir = ".", verbose = getOption("verbose")) { # check the NAMESPACE file to see if dynamic registration is enabled namespaceFile <- file.path(pkgdir, "NAMESPACE") if (!file.exists(namespaceFile)) - stop("pkgdir must refer to the directory containing an R package") # #nocov + stop("pkgdir must refer to the directory containing an R package") pkgNamespace <- readLines(namespaceFile, warn = FALSE) registration <- any(grepl("^\\s*useDynLib.*\\.registration\\s*=\\s*TRUE.*$", pkgNamespace)) diff --git a/inst/tinytest/cpp/attributes_extended.cpp b/inst/tinytest/cpp/attributes_extended.cpp new file mode 100644 index 000000000..0b3165d24 --- /dev/null +++ b/inst/tinytest/cpp/attributes_extended.cpp @@ -0,0 +1,49 @@ +#include +using namespace Rcpp; + +// Test 4.1: Named Export Parameter +// Coverage target: src/attributes.cpp:360 + +// [[Rcpp::export(name = "custom_exported_name")]] +int test_named_export() { return 123; } + +// [[Rcpp::export(name = "another.custom.name")]] +std::string test_named_export_with_dots() { return "dotted name"; } + +// Test 4.2: Unnamed Export Parameter +// Coverage target: src/attributes.cpp:365 + +// [[Rcpp::export(my_custom_name)]] +int test_unnamed_export_param() { return 456; } + +// Test 4.3: RNG Parameter Variations +// Coverage target: src/attributes.cpp:382-383 + +// [[Rcpp::export(rng = true)]] +int test_rng_lowercase_true() { return 789; } + +// [[Rcpp::export(rng = TRUE)]] +int test_rng_uppercase_true() { return 101; } + +// [[Rcpp::export(rng = false)]] +int test_rng_lowercase_false() { return 202; } + +// Test 4.4: Invisible Parameter Variations +// Coverage target: src/attributes.cpp:391-392 + +// [[Rcpp::export(invisible = true)]] +void test_invisible_lowercase_true() { + // Side effect only - returns invisibly +} + +// [[Rcpp::export(invisible = TRUE)]] +void test_invisible_uppercase_true() { + // Side effect only - returns invisibly +} + +// Test 4.5: exportedCppName with Dots +// Coverage target: src/attributes.cpp:373-377 +// This tests the conversion of dots to underscores in C++ names + +// [[Rcpp::export(name = "test.with.dots")]] +int test_cpp_name_conversion() { return 999; } diff --git a/inst/tinytest/test_attributes_extended.R b/inst/tinytest/test_attributes_extended.R new file mode 100644 index 000000000..dea80a564 --- /dev/null +++ b/inst/tinytest/test_attributes_extended.R @@ -0,0 +1,42 @@ +## Copyright (C) 2026 Dirk Eddelbuettel +## +## This file is part of Rcpp. +## +## Rcpp is free software: you can redistribute it and/or modify it +## under the terms of the GNU General Public License as published by +## the Free Software Foundation, either version 2 of the License, or +## (at your option) any later version. +## +## Rcpp 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 General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with Rcpp. If not, see . + +if (Sys.getenv("RunAllRcppTests") != "yes") exit_file("Set 'RunAllRcppTests' to 'yes' to run.") + +Rcpp::sourceCpp("cpp/attributes_extended.cpp") + +## Test named exports +expect_equal(custom_exported_name(), 123) +expect_equal(another.custom.name(), "dotted name") + +## Test unnamed export parameter +expect_equal(my_custom_name(), 456) + +## Test RNG parameters +expect_equal(test_rng_lowercase_true(), 789) +expect_equal(test_rng_uppercase_true(), 101) +expect_equal(test_rng_lowercase_false(), 202) + +## Test invisible parameters (should return invisibly) +result1 <- withVisible(test_invisible_lowercase_true()) +expect_false(result1$visible) + +result2 <- withVisible(test_invisible_uppercase_true()) +expect_false(result2$visible) + +## Test C++ name conversion (dots to underscores) +expect_equal(test.with.dots(), 999) diff --git a/inst/tinytest/test_compile_attributes_errors.R b/inst/tinytest/test_compile_attributes_errors.R new file mode 100644 index 000000000..445aacaa1 --- /dev/null +++ b/inst/tinytest/test_compile_attributes_errors.R @@ -0,0 +1,33 @@ +## Copyright (C) 2026 Dirk Eddelbuettel +## +## This file is part of Rcpp. +## +## Rcpp is free software: you can redistribute it and/or modify it +## under the terms of the GNU General Public License as published by +## the Free Software Foundation, either version 2 of the License, or +## (at your option) any later version. +## +## Rcpp 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 General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with Rcpp. If not, see . + +if (Sys.getenv("RunAllRcppTests") != "yes") exit_file("Set 'RunAllRcppTests' to 'yes' to run.") + +## Test 3.1: Missing DESCRIPTION File +## Coverage target: R/Attributes.R:420 +tmpdir <- tempfile() +dir.create(tmpdir) +expect_error(compileAttributes(tmpdir), "must refer to the directory containing an R package") +unlink(tmpdir, recursive = TRUE) + +## Test 3.2: Missing NAMESPACE File +## Coverage target: R/Attributes.R:432 +tmpdir <- tempfile() +dir.create(tmpdir) +writeLines("Package: TestPkg", file.path(tmpdir, "DESCRIPTION")) +expect_error(compileAttributes(tmpdir), "must refer to the directory containing an R package") +unlink(tmpdir, recursive = TRUE) diff --git a/inst/tinytest/test_cppfunction_errors.R b/inst/tinytest/test_cppfunction_errors.R new file mode 100644 index 000000000..21c8979a8 --- /dev/null +++ b/inst/tinytest/test_cppfunction_errors.R @@ -0,0 +1,35 @@ +## Copyright (C) 2026 Dirk Eddelbuettel +## +## This file is part of Rcpp. +## +## Rcpp is free software: you can redistribute it and/or modify it +## under the terms of the GNU General Public License as published by +## the Free Software Foundation, either version 2 of the License, or +## (at your option) any later version. +## +## Rcpp 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 General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with Rcpp. If not, see . + +if (Sys.getenv("RunAllRcppTests") != "yes") exit_file("Set 'RunAllRcppTests' to 'yes' to run.") + +## Test 2.1: No Function Definition +## Coverage target: R/Attributes.R:326 +code <- "int x = 5;" # No function, just a variable +expect_error(cppFunction(code), "No function definition found") + +## Test 2.2: Multiple Function Definitions +## Coverage target: R/Attributes.R:328 +code <- " +// [[Rcpp::export]] +int foo() { return 1; } + +// [[Rcpp::export]] +int bar() { return 2; } +" +## compilation dies sooner so we never actually see the messages +expect_error(cppFunction(code)) #, "More than one function definition") diff --git a/inst/tinytest/test_sourcecpp_errors.R b/inst/tinytest/test_sourcecpp_errors.R new file mode 100644 index 000000000..9391dbc18 --- /dev/null +++ b/inst/tinytest/test_sourcecpp_errors.R @@ -0,0 +1,42 @@ +## Copyright (C) 2026 Dirk Eddelbuettel +## +## This file is part of Rcpp. +## +## Rcpp is free software: you can redistribute it and/or modify it +## under the terms of the GNU General Public License as published by +## the Free Software Foundation, either version 2 of the License, or +## (at your option) any later version. +## +## Rcpp 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 General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with Rcpp. If not, see . + +if (Sys.getenv("RunAllRcppTests") != "yes") exit_file("Set 'RunAllRcppTests' to 'yes' to run.") + +## Test 1.1: Invalid File Extension +## Coverage target: R/Attributes.R:58-61 +tmpfile <- tempfile(fileext = ".c") +writeLines("int main() { return 0; }", tmpfile) +expect_error(sourceCpp(tmpfile), "does not have an extension of .cc or .cpp") +unlink(tmpfile) + +## Test 1.2: Filename with Spaces on Windows +## Coverage target: R/Attributes.R:64-68 +if (.Platform$OS.type == "windows") { + tmpfile <- file.path(tempdir(), "test file.cpp") + writeLines("#include ", tmpfile) + expect_error(sourceCpp(tmpfile), "contains spaces") + unlink(tmpfile) +} + +## Test 1.3: windowsDebugDLL on Non-Windows +## Coverage target: R/Attributes.R:70-76 +if (.Platform$OS.type != "windows") { + code <- "// [[Rcpp::export]]\nint test_fn() { return 42; }" + expect_message(sourceCpp(code = code, windowsDebugDLL = TRUE, verbose = TRUE), + "windowsDebugDLL.*ignored") +} diff --git a/src/attributes.cpp b/src/attributes.cpp index 641b13624..c98110050 100644 --- a/src/attributes.cpp +++ b/src/attributes.cpp @@ -357,12 +357,12 @@ namespace attributes { // check for explicit name parameter if (hasParameter(kExportName)) { - return paramNamed(kExportName).value(); // #nocov + return paramNamed(kExportName).value(); } // otherwise un-named parameter in the first slot else if (!params().empty() && params()[0].value().empty()) { - return params()[0].name(); // #nocov + return params()[0].name(); } // otherwise the actual function name { @@ -370,17 +370,17 @@ namespace attributes { } } - std::string exportedCppName() const { // #nocov start + std::string exportedCppName() const { std::string name = exportedName(); std::replace(name.begin(), name.end(), '.', '_'); return name; - } // #nocov end + } bool rng() const { Param rngParam = paramNamed(kExportRng); if (!rngParam.empty()) - return rngParam.value() == kParamValueTrue || // #nocov - rngParam.value() == kParamValueTRUE; // #nocov + return rngParam.value() == kParamValueTrue || + rngParam.value() == kParamValueTRUE; else return true; } @@ -388,8 +388,8 @@ namespace attributes { bool invisible() const { Param invisibleParam = paramNamed(kExportInvisible); if (!invisibleParam.empty()) - return invisibleParam.value() == kParamValueTrue || // #nocov - invisibleParam.value() == kParamValueTRUE; // #nocov + return invisibleParam.value() == kParamValueTrue || + invisibleParam.value() == kParamValueTRUE; else return false; } From a694ed23750a1ffe3b0759c1758302190099f0af Mon Sep 17 00:00:00 2001 From: Dirk Eddelbuettel Date: Tue, 13 Jan 2026 14:06:24 -0600 Subject: [PATCH 2/2] Add #nocov to two spots coverage cannot reach --- R/Attributes.R | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/R/Attributes.R b/R/Attributes.R index 9f85b85fa..9136ec65e 100644 --- a/R/Attributes.R +++ b/R/Attributes.R @@ -62,9 +62,9 @@ sourceCpp <- function(file = "", # validate that there are no spaces in the path on windows if (.Platform$OS.type == "windows") { - if (grepl(' ', basename(file), fixed=TRUE)) { + if (grepl(' ', basename(file), fixed=TRUE)) { # #nocov start stop("The filename '", basename(file), "' contains spaces. This ", - "is not permitted.") + "is not permitted.") # #nocov end } } else { if (windowsDebugDLL) { @@ -73,7 +73,7 @@ sourceCpp <- function(file = "", "non-Windows platforms.") } windowsDebugDLL <- FALSE # now we do not need to deal with OS choice below - } + } } # get the context (does code generation as necessary) @@ -325,7 +325,7 @@ cppFunction <- function(code, if (length(exported$functions) == 0) stop("No function definition found") else if (length(exported$functions) > 1) - stop("More than one function definition") + stop("More than one function definition") # #nocov else { functionName <- exported$functions[[1]] invisible(get(functionName, env))