diff --git a/.Rbuildignore b/.Rbuildignore index 48d26b5..19dc5a7 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -6,12 +6,11 @@ ^\.DS_Store$ ^vignettes/\.DS_Store$ ^man/figures/.*\.png$ -build/ ^Makefile$ -^\.gdal_doc_cache +^\.gdal_doc_cache.*$ ^LICENSE\.md$ ^docs$ -^\.\.Rcheck$ +^.*\.Rcheck$ ^\.test_cache$ ^\.git$ ^\.claude$ @@ -27,5 +26,4 @@ build/ ^examples$ ^scratch$ ^venv$ -^venv/ ^build/ diff --git a/.github/dockerfiles/Dockerfile.template b/.github/dockerfiles/Dockerfile.template index e3169d7..b8dd88b 100644 --- a/.github/dockerfiles/Dockerfile.template +++ b/.github/dockerfiles/Dockerfile.template @@ -186,9 +186,12 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ libxml2-dev \ locales \ pandoc \ + python3-pip \ qpdf && \ rm -rf /var/lib/apt/lists/* +RUN pip3 install --no-cache-dir osgeo-gdal + # Generate en_US.UTF-8 locale to prevent Sys.setlocale() warnings RUN echo "en_US.UTF-8 UTF-8" > /etc/locale.gen && \ locale-gen en_US.UTF-8 && \ diff --git a/.github/workflows/R-CMD-check-docker.yml b/.github/workflows/R-CMD-check-docker.yml index e2938eb..1c11fac 100644 --- a/.github/workflows/R-CMD-check-docker.yml +++ b/.github/workflows/R-CMD-check-docker.yml @@ -103,10 +103,21 @@ jobs: run: | Rscript -e "roxygen2::roxygenise()" - - name: Run R CMD check + - name: Clean build and cache artifacts + run: | + rm -rf build/gdal_repo build/*.o build/*.so .gdal_doc_cache_* + + - name: Build source package + run: | + cd .. + R CMD build --no-manual gdalcli + + - name: Run R CMD check on tarball run: | export LANG=en_US.UTF-8 - R CMD check --no-manual . + cd .. + R CMD check --no-manual *.tar.gz + mv gdalcli.Rcheck gdalcli/gdalcli.Rcheck - name: Install package run: | @@ -138,22 +149,6 @@ jobs: } " - - name: Show test and examples output - if: always() - run: | - if [ -f "gdalcli.Rcheck/tests/runtests.Rout.fail" ]; then - echo "=== Test Output (Failures) ===" - tail -100 "gdalcli.Rcheck/tests/runtests.Rout.fail" - elif [ -f "gdalcli.Rcheck/tests/runtests.Rout" ]; then - echo "=== Test Output ===" - tail -50 "gdalcli.Rcheck/tests/runtests.Rout" - fi - - if [ -f "gdalcli.Rcheck/gdalcli-Ex.Rout" ]; then - echo "=== Examples Output ===" - tail -30 "gdalcli.Rcheck/gdalcli-Ex.Rout" - fi - - name: Upload check results uses: actions/upload-artifact@v7 if: always() @@ -161,4 +156,4 @@ jobs: name: check-results-${{ matrix.r-version }}-gdal-${{ matrix.gdal-version }} path: gdalcli.Rcheck/ retention-days: 7 - if-no-files-found: ignore + if-no-files-found: warn diff --git a/.gitignore b/.gitignore index 108e57d..f24b9ae 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,5 @@ venv/ scratch/ .Rproj.user *.Rproj -*.Rhistory \ No newline at end of file +*.Rhistory +gdalcli.Rcheck/ \ No newline at end of file diff --git a/Makefile b/Makefile index 8918311..1886002 100644 --- a/Makefile +++ b/Makefile @@ -142,4 +142,4 @@ dev: regen docs check-man @echo "============================================" @echo "[OK] Quick dev build complete!" @echo "============================================" - @echo "" \ No newline at end of file + @echo "" diff --git a/NEWS.md b/NEWS.md index 7c2b7ff..746b267 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,4 +1,4 @@ -# gdalcli 0.5.2 (2026-03-21) +# gdalcli 0.5.2 (2026-03-25) - Added Arrow support for in-memory vector processing - Added arrow R package to Docker dependencies diff --git a/R/core-gdalg-transpiler.R b/R/core-gdalg-transpiler.R index b5f2e52..61dd172 100644 --- a/R/core-gdalg-transpiler.R +++ b/R/core-gdalg-transpiler.R @@ -26,6 +26,11 @@ #' @keywords internal #' @noRd .quote_argument <- function(str) { + # Empty strings must be quoted + if (str == "") { + return("''") + } + # Characters that are safe without quoting (alphanumeric + common safe chars) # This covers most common filenames and simple arguments if (grepl("^[a-zA-Z0-9/_.:=-]*$", str)) { @@ -66,13 +71,13 @@ # Generate the CLI flag from argument name # Special mappings for common deviations - flag_mapping <- c( + flag_mapping <- list( "resolution" = "--resolution", "size" = "--ts", "extent" = "--te" ) cli_flag <- if (arg_name %in% names(flag_mapping)) { - flag_mapping[arg_name] + flag_mapping[[arg_name]] } else { paste0("--", gsub("_", "-", arg_name)) } @@ -80,7 +85,7 @@ # Handle logical/boolean arguments if (is.logical(value)) { if (value) { - return(cli_flag) # Just the flag for TRUE + return(unname(cli_flag)) # Just the flag for TRUE } else { return(character(0)) # Nothing for FALSE } @@ -89,7 +94,7 @@ # Check if this is a composite (fixed-count, comma-separated) argument is_composite <- FALSE arg_meta <- arg_mapping[[arg_name]] - if (!is.null(arg_meta) && !is.null(arg_meta$min_count) && !is.null(arg_meta$max_count)) { + if (!is.null(arg_meta) && is.list(arg_meta) && !is.null(arg_meta$min_count) && !is.null(arg_meta$max_count)) { is_composite <- arg_meta$min_count == arg_meta$max_count && arg_meta$min_count > 1 } @@ -98,7 +103,7 @@ if (is_composite) { # Composite: comma-separated formatted_values <- sapply(value, function(v) .quote_argument(as.character(v)), USE.NAMES = FALSE) - return(c(cli_flag, paste(formatted_values, collapse = ","))) + return(unname(c(cli_flag, paste(formatted_values, collapse = ",")))) } else { # Repeatable: repeated flags formatted_values <- sapply(value, function(v) .quote_argument(as.character(v)), USE.NAMES = FALSE) @@ -107,13 +112,13 @@ for (val in formatted_values) { result <- c(result, cli_flag, val) } - return(result) + return(unname(result)) } } # Single value: quote and return formatted_value <- .quote_argument(as.character(value)) - c(cli_flag, formatted_value) + unname(c(cli_flag, formatted_value)) } @@ -329,7 +334,7 @@ # For now, use a simpler grepl-based approach that's more reliable # This regex matches either single-quoted strings or sequences of non-whitespace - pattern <- "(['\"][^'\"]*['\"]|[^ \\t]+)" + pattern <- "(['\"][^'\"]*['\"]|[^ \t]+)" # Use gregexpr with the ENTIRE result to pass to regmatches matches <- gregexpr(pattern, step_str) diff --git a/R/core-optional-features.R b/R/core-optional-features.R index 7c37947..31c2f21 100644 --- a/R/core-optional-features.R +++ b/R/core-optional-features.R @@ -62,11 +62,16 @@ #' Check Arrow Vectors Capability #' -#' Internal check: Arrow support available in GDAL +#' Internal check: Arrow support available in GDAL (requires GDAL 3.12+) #' #' @keywords internal #' @noRd .check_arrow_vectors_available <- function() { + # Requires GDAL 3.12+ + if (!gdal_check_version("3.12", op = ">=")) { + return(FALSE) + } + # Check if GDAL was compiled with Arrow support .check_gdal_has_arrow_driver() } diff --git a/R/core-options.R b/R/core-options.R index 5cdf836..2783e23 100644 --- a/R/core-options.R +++ b/R/core-options.R @@ -38,6 +38,7 @@ #' @param audit_logging Logical. Log all job executions. Default: FALSE. #' @param stream_out_format Character. Default output streaming format: #' NULL (no streaming), "text", "raw", "json", or "stdout". Default: NULL. +#' @param ... Additional arguments (not allowed; raises error if provided). #' #' @return Invisibly returns a list of current gdalcli options (before modification). #' @@ -111,7 +112,17 @@ gdalcli_options <- function(checkpoint = NULL, backend = NULL, verbose = NULL, audit_logging = NULL, - stream_out_format = NULL) { + stream_out_format = NULL, + ...) { + # Check for unknown arguments + dots <- list(...) + if (length(dots) > 0) { + unknown_arg <- names(dots)[1] + cli::cli_abort( + "Unknown option: {unknown_arg}. Valid options are: checkpoint, checkpoint_dir, backend, verbose, audit_logging, stream_out_format" + ) + } + # Store current options for return value current_opts <- .get_all_gdalcli_options() diff --git a/man/gdalcli_options.Rd b/man/gdalcli_options.Rd index eeb6287..db3ab90 100644 --- a/man/gdalcli_options.Rd +++ b/man/gdalcli_options.Rd @@ -10,7 +10,8 @@ gdalcli_options( backend = NULL, verbose = NULL, audit_logging = NULL, - stream_out_format = NULL + stream_out_format = NULL, + ... ) } \arguments{ @@ -32,6 +33,8 @@ Default: "auto".} \item{stream_out_format}{Character. Default output streaming format: NULL (no streaming), "text", "raw", "json", or "stdout". Default: NULL.} + +\item{...}{Additional arguments (not allowed; raises error if provided).} } \value{ Invisibly returns a list of current gdalcli options (before modification). diff --git a/tests/testthat/setup.R b/tests/testthat/setup.R index ed6cf10..f68c78b 100644 --- a/tests/testthat/setup.R +++ b/tests/testthat/setup.R @@ -37,6 +37,11 @@ if (requireNamespace("reticulate", quietly = TRUE)) { } # Ensure step mappings are loaded for tests -if (is.null(.gdalcli_env$step_mappings)) { - .load_step_mappings() +if (requireNamespace("gdalcli", quietly = TRUE)) { + if (exists(".gdalcli_env", where = asNamespace("gdalcli"))) { + env <- get(".gdalcli_env", envir = asNamespace("gdalcli")) + if (is.null(env$step_mappings)) { + gdalcli:::.load_step_mappings() + } + } } \ No newline at end of file diff --git a/tests/testthat/test_arrow_integration.R b/tests/testthat/test_arrow_integration.R index 8dac378..38cbb83 100644 --- a/tests/testthat/test_arrow_integration.R +++ b/tests/testthat/test_arrow_integration.R @@ -126,7 +126,7 @@ test_that("arrow table has schema", { tbl <- arrow::arrow_table(df) schema <- arrow::schema(tbl) - expect_type(schema, "list") + expect_true(inherits(schema, c("Schema", "ArrowObject", "R6"))) expect_true(length(schema) > 0) }) @@ -345,8 +345,8 @@ test_that("Round-trip conversion preserves basic structure", { tbl <- arrow::arrow_table(df) arrow::write_feather(tbl, temp_file) - # Read back - tbl_read <- arrow::read_feather(temp_file) + # Read back as Arrow table (as_data_frame = FALSE to get Arrow table) + tbl_read <- arrow::read_feather(temp_file, as_data_frame = FALSE) expect_s3_class(tbl_read, "ArrowTabular") } }) diff --git a/tests/testthat/test_backends.R b/tests/testthat/test_backends.R index 10b772c..0989ae5 100644 --- a/tests/testthat/test_backends.R +++ b/tests/testthat/test_backends.R @@ -348,13 +348,13 @@ find_python_gdal_env <- function() { if (!requireNamespace("reticulate", quietly = TRUE)) { return(FALSE) } - - tryCatch({ + + suppressWarnings(tryCatch({ # Check current environment first if (reticulate::py_module_available("osgeo.gdal")) { return(TRUE) } - + # Try venv in project root and common relative locations venv_paths <- c( file.path(getwd(), "venv"), # venv in current dir @@ -362,7 +362,7 @@ find_python_gdal_env <- function() { "venv", # relative venv "~/.venv" # user home venv ) - + for (path in venv_paths) { expanded_path <- path.expand(path) if (dir.exists(expanded_path)) { @@ -372,7 +372,7 @@ find_python_gdal_env <- function() { } } } - + # Try standard virtualenv locations venvs <- reticulate::virtualenv_list() for (venv in venvs) { @@ -385,7 +385,7 @@ find_python_gdal_env <- function() { } } FALSE - }, error = function(e) FALSE) + }, error = function(e) FALSE)) } # Helper function to check if Python GDAL is available @@ -534,7 +534,7 @@ test_that("gdalraster backend returns text output from raster_info", { # If we got output, verify it's character if (!is.null(result)) { - expect_is(result, "character") + expect_type(result, "character") expect_gt(nchar(result), 0) } }) @@ -748,9 +748,9 @@ test_that("gdalraster backend produces text output from raster_info", { # Execute with gdalraster backend requesting text output job <- gdal_raster_info(input = sample_file) result <- gdal_job_run(job, backend = "gdalraster", stream_out_format = "text") - + # Verify output - expect_is(result, "character") + expect_type(result, "character") expect_gt(nchar(result), 0) # Should contain GDAL output characteristics @@ -779,7 +779,7 @@ test_that("reticulate backend produces text output from raster_info", { expect_true(result) } else { # Should be character text - expect_is(result, "character") + expect_type(result, "character") expect_gt(nchar(result), 0) } }) @@ -878,12 +878,12 @@ test_that("execution tests handle both backends with stream_out_format parameter # Test text format job1 <- gdal_raster_info(input = sample_file) result_text <- gdal_job_run(job1, backend = "gdalraster", stream_out_format = "text") - expect_is(result_text, "character") + expect_type(result_text, "character") # Test raw format job2 <- gdal_raster_info(input = sample_file) result_raw <- gdal_job_run(job2, backend = "gdalraster", stream_out_format = "raw") - expect_is(result_raw, "raw") + expect_type(result_raw, "raw") # Raw and text should both represent the same output expect_equal(length(result_raw), nchar(result_text)) @@ -984,7 +984,7 @@ test_that("backends handle environment variables properly", { # Should produce valid output or be skipped if (result != "skipped") { - expect_is(result, "character") + expect_type(result, "character") expect_gt(nchar(result), 0) } }) diff --git a/tests/testthat/test_gdalg_serialization.R b/tests/testthat/test_gdalg_serialization.R index d1d903a..94232ff 100644 --- a/tests/testthat/test_gdalg_serialization.R +++ b/tests/testthat/test_gdalg_serialization.R @@ -325,9 +325,9 @@ test_that("gdal_save_pipeline saves GDALG JSON format to disk", { # Check file contains valid JSON json_content <- yyjsonr::read_json_file(tmpfile) - expect_equal(json_content$type, "gdal_streamed_alg") - expect_true(is.character(json_content$command_line)) - expect_true(json_content$relative_paths_relative_to_this_file) + expect_equal(json_content$gdalg$type, "gdal_streamed_alg") + expect_true(is.character(json_content$gdalg$command_line)) + expect_true(json_content$gdalg$relative_paths_relative_to_this_file) }) test_that("gdal_load_pipeline loads GDALG JSON and reconstructs pipeline", { @@ -401,17 +401,17 @@ test_that("gdal_load_pipeline validates GDALG spec structure", { # Invalid: missing type invalid_spec <- list(command_line = "...") writeLines(yyjsonr::write_json_str(invalid_spec, pretty = TRUE), tmpfile) - expect_error(gdal_load_pipeline(tmpfile), "type") + expect_error(gdal_load_pipeline(tmpfile), "Cannot determine format") # Invalid: wrong type value invalid_spec <- list(type = "wrong", command_line = "...") writeLines(yyjsonr::write_json_str(invalid_spec, pretty = TRUE), tmpfile) - expect_error(gdal_load_pipeline(tmpfile), 'gdal_streamed_alg') + expect_error(gdal_load_pipeline(tmpfile), "Cannot determine format") # Invalid: missing command_line invalid_spec <- list(type = "gdal_streamed_alg") writeLines(yyjsonr::write_json_str(invalid_spec, pretty = TRUE), tmpfile) - expect_error(gdal_load_pipeline(tmpfile), "command_line") + expect_error(gdal_load_pipeline(tmpfile), "Specification must contain") }) test_that("Complex vector pipeline with multiple operations round-trips", { diff --git a/tests/testthat/test_options.R b/tests/testthat/test_options.R index f4d6c5c..7b2580d 100644 --- a/tests/testthat/test_options.R +++ b/tests/testthat/test_options.R @@ -9,12 +9,14 @@ test_that("gdalcli_options() returns default values when called without argument result <- gdalcli_options() - expect_is(result, "list") - expect_named(result, c("backend", "stream_out_format", "verbose", "audit_logging")) + expect_type(result, "list") + expect_named(result, c("checkpoint", "checkpoint_dir", "backend", "verbose", "audit_logging", "stream_out_format")) expect_equal(result$backend, "auto") expect_null(result$stream_out_format) expect_false(result$verbose) expect_false(result$audit_logging) + expect_false(result$checkpoint) + expect_equal(result$checkpoint_dir, getwd()) }) test_that("gdalcli_options() can set single option", { @@ -94,11 +96,11 @@ test_that("gdalcli_options() accepts all valid backend options", { test_that("gdalcli_options() rejects invalid backend values", { expect_error( gdalcli_options(backend = "invalid_backend"), - "Invalid backend.*invalid_backend.*Valid backends are" + "backend must be one of" ) expect_error( gdalcli_options(backend = "gdal"), - "Invalid backend.*gdal.*Valid backends are" + "backend must be one of" ) }) diff --git a/tests/testthat/test_pipeline.R b/tests/testthat/test_pipeline.R index b42ab98..c316387 100644 --- a/tests/testthat/test_pipeline.R +++ b/tests/testthat/test_pipeline.R @@ -502,7 +502,10 @@ test_that("gdal_compose convenience function detects pipeline type", { job2 <- gdal_raster_convert(output = "output.tif") # Create pipeline using convenience function - pipeline_job <- gdal_compose(jobs = list(job1, job2)) + expect_warning( + {pipeline_job <- gdal_compose(jobs = list(job1, job2))}, + "gdal_compose\\(\\) is deprecated" + ) expect_s3_class(pipeline_job, "gdal_job") expect_equal(pipeline_job$command_path[1], "raster") @@ -518,7 +521,10 @@ test_that("gdal_compose convenience function works with vector jobs", { job2 <- gdal_vector_convert(output = "output.shp") # Create pipeline using convenience function - pipeline_job <- gdal_compose(jobs = list(job1, job2)) + expect_warning( + {pipeline_job <- gdal_compose(jobs = list(job1, job2))}, + "gdal_compose\\(\\) is deprecated" + ) expect_s3_class(pipeline_job, "gdal_job") expect_equal(pipeline_job$command_path[1], "vector")