diff --git a/DESCRIPTION b/DESCRIPTION index e4b823b..c382168 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -20,12 +20,11 @@ Depends: R (>= 3.6) Imports: abind, - curl, geosphere, ggplot2, grid, gridtext, - jsonlite, + httr2, lubridate, ncdf4, patchwork, @@ -41,7 +40,8 @@ Suggests: knitr, mockery, rmarkdown, - testthat + testthat, + vcr LinkingTo: Rcpp, RcppArmadillo diff --git a/NEWS.md b/NEWS.md index deb9b30..1e6909d 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,6 +1,7 @@ # resourcecode (development version) -- Custom labels are now handeled properly in `cut_seasons()` +- Custom labels are now handled properly in `cut_seasons()` ; +- Switch to {httr2} to download files and API request. # resourcecode 0.5.2 diff --git a/R/download_data.R b/R/download_data.R index 01615fc..193f134 100644 --- a/R/download_data.R +++ b/R/download_data.R @@ -11,10 +11,11 @@ #' @return a tibble with 2 columns and as many rows as needed #' @noRd get_parameters_raw <- function( - parameter = "hs", - node = 42, - start = as.POSIXct("1994-01-01Z00:00:00", tz = "UTC"), - end = as.POSIXct("1994-12-31Z23:00:00", tz = "UTC")) { + parameter = "hs", + node = 42, + start = as.POSIXct("1994-01-01Z00:00:00", tz = "UTC"), + end = as.POSIXct("1994-12-31Z23:00:00", tz = "UTC") +) { if (parameter == "tp") { single_parameter <- "fp" } else { @@ -27,7 +28,7 @@ get_parameters_raw <- function( # Cassandra database start indexing at 1, so decrements node number node <- node - 1 - request <- paste0( + request_url <- paste0( rcd_cassandra_url, "api/timeseries", "?parameter=", @@ -40,28 +41,69 @@ get_parameters_raw <- function( end_str ) - # Try retrieving and parsing JSON + # Try retrieving and parsing JSON using httr2 + resp <- tryCatch( + httr2::request(request_url) |> + httr2::req_error(is_error = \(resp) FALSE) |> # Don't auto-error on HTTP errors + httr2::req_retry(max_tries = 3) |> # Retry transient failures + httr2::req_timeout(30) |> # 30 second timeout + httr2::req_user_agent("Resourcecode R package") |> + httr2::req_perform(), + httr2_failure = function(cnd) { + message( + "Network error: Could not connect to the remote resource. ", + "The server may be unavailable." + ) + NULL + }, + error = function(e) { + message("Unexpected error retrieving data: ", conditionMessage(e)) + NULL + } + ) + + # If request failed, exit + if (is.null(resp)) { + return(NULL) + } + + # Check HTTP status + if (httr2::resp_status(resp) != 200) { + message( + "HTTP error ", + httr2::resp_status(resp), + ": ", + httr2::resp_status_desc(resp) + ) + return(NULL) + } + + # Parse JSON response res <- tryCatch( - jsonlite::fromJSON(request), + httr2::resp_body_json(resp, simplifyVector = TRUE), error = function(e) { - message("Could not retrieve data from the remote resource. ", - "The server may be unavailable or the URL may have changed.") - NULL # graceful fallback + message("Error parsing response: Invalid JSON format") + NULL } ) - # If retrieval failed, exit - if (is.null(res)) return(NULL) + # If parsing failed, exit + if (is.null(res)) { + return(NULL) + } # Check API-level error if (!is.null(res$errorcode) && res$errorcode != 0) { - message("The data source returned an error: ", res$errormessage, - "\nReturning NULL.") - return(NULL) # graceful fallback + message( + "The data source returned an error: ", + res$errormessage, + "\nReturning NULL." + ) + return(NULL) # graceful fallback } - - data <- res$result$data + # Convert list to data frame + data <- as.data.frame(res$result$data) colnames(data) <- c("time", parameter) data <- tibble::as_tibble(data) @@ -70,7 +112,7 @@ get_parameters_raw <- function( } data$time <- as.POSIXct( - data$time / 1000, + as.numeric(data$time) / 1000, origin = as.POSIXct("1970-01-01", tz = "UTC"), tz = "UTC" ) # Convert UNIX time (ms) to POSIXct format @@ -78,6 +120,7 @@ get_parameters_raw <- function( data } + #' Download time series of sea-state parameters from RESOURCECODE database #' #' If the remote resource is unavailable or returns an error, the function returns NULL @@ -91,21 +134,25 @@ get_parameters_raw <- function( #' @return a tibble with N-rows and `length(parameters)` columns. #' @export #' -#' @examplesIf curl::has_internet() -#' ts <- get_parameters(parameters = c("hs", "tp"), node = 42) -#' plot(ts$time, ts$hs, type = "l") +#' @examples +#' rscd_data <- get_parameters(parameters = c("hs", "tp"), node = 42) +#' if(!is.null(rscd_data)) plot(rscd_data$time, rscd_data$hs, type = "l") get_parameters <- function( - parameters = "hs", - node = 42, - start = as.POSIXct("1994-01-01 00:00:00", tz = "UTC"), - end = as.POSIXct("1994-12-31 23:00:00", tz = "UTC")) { + parameters = "hs", + node = 42, + start = as.POSIXct("1994-01-01 00:00:00", tz = "UTC"), + end = as.POSIXct("1994-12-31 23:00:00", tz = "UTC") +) { parameters <- tolower(parameters) if (any(parameters %nin% c("tp", resourcecodedata::rscd_variables$name))) { - errors <- parameters[parameters %nin% c("tp", resourcecodedata::rscd_variables$name)] + errors <- parameters[ + parameters %nin% c("tp", resourcecodedata::rscd_variables$name) + ] stop( "Requested parameters do not exists in the database: ", - paste0(errors, collapse = ", "), "." + paste0(errors, collapse = ", "), + "." ) } @@ -144,7 +191,8 @@ get_parameters <- function( stop( "'start' is outside the covered period: ", paste( - format(c(rscd_casandra_start_date, rscd_casandra_end_date), + format( + c(rscd_casandra_start_date, rscd_casandra_end_date), format = "%Y-%m-%d %H:%M %Z" ), collapse = " \u2014 " @@ -155,7 +203,8 @@ get_parameters <- function( stop( "'end' is outside the covered period: ", paste( - format(c(rscd_casandra_start_date, rscd_casandra_end_date), + format( + c(rscd_casandra_start_date, rscd_casandra_end_date), format = "%Y-%m-%d %H:%M %Z" ), collapse = " \u2014 " @@ -173,6 +222,12 @@ get_parameters <- function( end = end ) + # If first parameter retrieval failed, return NULL + if (is.null(out)) { + message("Failed to retrieve parameter: ", parameters[1]) + return(NULL) + } + for (i in seq_len(length(parameters) - 1)) { temp <- get_parameters_raw( parameters[i + 1], @@ -180,7 +235,14 @@ get_parameters <- function( start = start, end = end ) - out <- cbind(out, temp[, 2]) + + # If any subsequent parameter retrieval fails, return NULL + if (is.null(temp)) { + message("Failed to retrieve parameter: ", parameters[i + 1]) + return(NULL) + } + + out <- cbind.data.frame(out, temp[, 2]) } out } diff --git a/R/spectral_data_download.R b/R/spectral_data_download.R index e9a0565..b29ae96 100644 --- a/R/spectral_data_download.R +++ b/R/spectral_data_download.R @@ -7,36 +7,32 @@ #' #' @noRd #' @keywords internal -download_nc_data <- function(url, destfile) { - # Ensure destfile exists only if successful - success <- tryCatch( +download_nc <- function(url, destfile) { + tryCatch( { - curl::curl_download(url, destfile = destfile, mode = "wb") - TRUE + req <- httr2::request(url) |> + httr2::req_error(is_error = \(resp) FALSE) |> # Don't auto-error on HTTP errors + httr2::req_retry(max_tries = 3) |> # Retry transient failures + httr2::req_timeout(60) |> + httr2::req_user_agent("Resourcecode R package") |> + httr2::req_error(is_error = ~ .x$status_code >= 400) + + httr2::req_perform(req, path = destfile) + + if (!file.exists(destfile)) { + return(NULL) + } + + destfile }, error = function(e) { - message( - "Could not download spectral data. - The remote server may be unavailable or the URL may have changed." - ) - FALSE - }, - warning = function(w) { - message( - "A warning occurred while downloading the spectral data. - The resource may have changed.\n", - w - ) - FALSE + message("Download failed: ", conditionMessage(e)) + if (file.exists(destfile)) { + file.remove(destfile) + } + NULL } ) - - # If download failed, return NULL (do not leave partial file) - if (!success || !file.exists(destfile)) { - NULL - } else { - destfile - } } @@ -68,7 +64,7 @@ get_2d_spectrum_raw <- function(point, year, month) { temp <- tempfile(fileext = ".nc") - file <- download_nc_data(url, temp) + file <- download_nc(url, temp) if (is.null(file)) { message( @@ -145,7 +141,7 @@ get_1d_spectrum_raw <- function(point, year, month) { ) temp <- tempfile(fileext = ".nc") - file <- download_nc_data(url, temp) + file <- download_nc(url, temp) if (is.null(file)) { message( @@ -247,12 +243,14 @@ get_1d_spectrum_raw <- function(point, year, month) { #' } #' @export #' -#' @examplesIf curl::has_internet() +#' @examples #' spec2D <- get_2d_spectrum("SEMREVO", start = "1994-01-01", end = "1994-02-28") -#' image(spec2D$dir, spec2D$freq, spec2D$efth[, , 1], -#' xlab = "Direction (°)", -#' ylab = "Frequency (Hz" -#' ) +#' if(!is.null(spec2D)){ +#' image(spec2D$dir, spec2D$freq, spec2D$efth[, , 1], +#' xlab = "Direction (°)", +#' ylab = "Frequency (Hz" +#' ) +#' } get_2d_spectrum <- function(point, start = "1994-01-01", end = "1994-02-28") { stopifnot(length(point) == 1) @@ -294,8 +292,35 @@ get_2d_spectrum <- function(point, start = "1994-01-01", end = "1994-02-28") { out <- get_2d_spectrum_raw(point, years[1], months[1]) + if (is.null(out)) { + message( + "Failed to download data for ", + point, + " (", + years[1], + "-", + months[1], + ")" + ) + return(NULL) + } + for (m in seq_along(years[-1])) { temp <- get_2d_spectrum_raw(point, years[m + 1], months[m + 1]) + + if (is.null(temp)) { + message( + "Failed to download data for ", + point, + " (", + years[m + 1], + "-", + months[m + 1], + ")" + ) + return(NULL) + } + out$efth <- abind::abind(out$efth, temp$efth, along = 3) out$forcings <- rbind(out$forcings, temp$forcings) } @@ -344,17 +369,19 @@ get_2d_spectrum <- function(point, start = "1994-01-01", end = "1994-02-28") { #' } #' @export #' -#' @examplesIf curl::has_internet() +#' @examples #' spec1D <- get_1d_spectrum("SEMREVO", start = "1994-01-01", end = "1994-02-28") -#' r <- as.POSIXct(round(range(spec1D$forcings$time), "month")) -#' image(spec1D$forcings$time, spec1D$freq, t(spec1D$ef), -#' xaxt = "n", xlab = "Time", -#' ylab = "Frequency (Hz)" -#' ) -#' axis.POSIXct(1, spec1D$forcings$time, -#' at = seq(r[1], r[2], by = "week"), -#' format = "%Y-%m-%d", las = 2 -#' ) +#' if(!is.null(spec1D)){ +#' r <- as.POSIXct(round(range(spec1D$forcings$time), "month")) +#' image(spec1D$forcings$time, spec1D$freq, t(spec1D$ef), +#' xaxt = "n", xlab = "Time", +#' ylab = "Frequency (Hz)" +#' ) +#' axis.POSIXct(1, spec1D$forcings$time, +#' at = seq(r[1], r[2], by = "week"), +#' format = "%Y-%m-%d", las = 2 +#' ) +#' } get_1d_spectrum <- function(point, start = "1994-01-01", end = "1994-02-28") { stopifnot(length(point) == 1) @@ -396,8 +423,35 @@ get_1d_spectrum <- function(point, start = "1994-01-01", end = "1994-02-28") { out <- get_1d_spectrum_raw(point, years[1], months[1]) + if (is.null(out)) { + message( + "Failed to download data for ", + point, + " (", + years[1], + "-", + months[1], + ")" + ) + return(NULL) + } + for (m in seq_along(years[-1])) { temp <- get_1d_spectrum_raw(point, years[m + 1], months[m + 1]) + + if (is.null(temp)) { + message( + "Failed to download data for ", + point, + " (", + years[m + 1], + "-", + months[m + 1], + ")" + ) + return(NULL) + } + out$ef <- abind::abind(out$ef, temp$ef, along = 2) out$th1m <- abind::abind(out$th1m, temp$th1m, along = 2) out$th2m <- abind::abind(out$th2m, temp$th2m, along = 2) diff --git a/man/get_1d_spectrum.Rd b/man/get_1d_spectrum.Rd index 33acefa..84b3020 100644 --- a/man/get_1d_spectrum.Rd +++ b/man/get_1d_spectrum.Rd @@ -51,16 +51,16 @@ A list with 12 elements: Download the 1D spectrum data from IFREMER ftp } \examples{ -\dontshow{if (curl::has_internet()) withAutoprint(\{ # examplesIf} spec1D <- get_1d_spectrum("SEMREVO", start = "1994-01-01", end = "1994-02-28") -r <- as.POSIXct(round(range(spec1D$forcings$time), "month")) -image(spec1D$forcings$time, spec1D$freq, t(spec1D$ef), - xaxt = "n", xlab = "Time", - ylab = "Frequency (Hz)" -) -axis.POSIXct(1, spec1D$forcings$time, - at = seq(r[1], r[2], by = "week"), - format = "\%Y-\%m-\%d", las = 2 -) -\dontshow{\}) # examplesIf} +if(!is.null(spec1D)){ + r <- as.POSIXct(round(range(spec1D$forcings$time), "month")) + image(spec1D$forcings$time, spec1D$freq, t(spec1D$ef), + xaxt = "n", xlab = "Time", + ylab = "Frequency (Hz)" + ) + axis.POSIXct(1, spec1D$forcings$time, + at = seq(r[1], r[2], by = "week"), + format = "\%Y-\%m-\%d", las = 2 + ) +} } diff --git a/man/get_2d_spectrum.Rd b/man/get_2d_spectrum.Rd index b0428d6..332538a 100644 --- a/man/get_2d_spectrum.Rd +++ b/man/get_2d_spectrum.Rd @@ -44,11 +44,11 @@ A list with 9 elements: Download the 2D spectrum data from IFREMER ftp } \examples{ -\dontshow{if (curl::has_internet()) withAutoprint(\{ # examplesIf} spec2D <- get_2d_spectrum("SEMREVO", start = "1994-01-01", end = "1994-02-28") -image(spec2D$dir, spec2D$freq, spec2D$efth[, , 1], - xlab = "Direction (°)", - ylab = "Frequency (Hz" -) -\dontshow{\}) # examplesIf} +if(!is.null(spec2D)){ + image(spec2D$dir, spec2D$freq, spec2D$efth[, , 1], + xlab = "Direction (°)", + ylab = "Frequency (Hz" + ) +} } diff --git a/man/get_parameters.Rd b/man/get_parameters.Rd index abdada5..0c6eb98 100644 --- a/man/get_parameters.Rd +++ b/man/get_parameters.Rd @@ -28,8 +28,6 @@ If the remote resource is unavailable or returns an error, the function returns and emits an informative message. } \examples{ -\dontshow{if (curl::has_internet()) withAutoprint(\{ # examplesIf} -ts <- get_parameters(parameters = c("hs", "tp"), node = 42) -plot(ts$time, ts$hs, type = "l") -\dontshow{\}) # examplesIf} +rscd_data <- get_parameters(parameters = c("hs", "tp"), node = 42) +if(!is.null(rscd_data)) plot(rscd_data$time, rscd_data$hs, type = "l") } diff --git a/tests/testthat/Rplots.pdf b/tests/testthat/Rplots.pdf index ee6ecec..3d73607 100644 Binary files a/tests/testthat/Rplots.pdf and b/tests/testthat/Rplots.pdf differ diff --git a/tests/testthat/_vcr/boundary_dates.yml b/tests/testthat/_vcr/boundary_dates.yml new file mode 100644 index 0000000..fa120d3 --- /dev/null +++ b/tests/testthat/_vcr/boundary_dates.yml @@ -0,0 +1,20 @@ +http_interactions: +- request: + method: GET + uri: https://resourcecode-datacharts.ifremer.fr/api/timeseries?parameter=hs&node=41&start=1994-01-01T00%3A00%3A00Z&end=1994-01-01T01%3A00%3A00Z + response: + status: 200 + headers: + Date: Mon, 12 Jan 2026 14:22:22 GMT + Server: Microsoft-IIS/8.0 + Content-Type: application/json;charset=UTF-8 + Vary: Accept-Encoding + Content-Encoding: gzip + X-Content-Type-Options: nosniff + Referrer-Policy: strict-origin-when-cross-origin + Access-Control-Allow-Origin: '*' + Transfer-Encoding: chunked + body: + string: '{"errorcode":0,"errormessage":"","query":{"downsampling":"none","samplingparameter":"none","platform":"41","parameterCode":"hs","startTimeMillis":"757382400000","endTimeMillis":"757386000000","startTimeFormat":"1994-01-01T00:00:00Z","endTimeFormat":"1994-01-01T01:00:00Z"},"result":{"dataSetSizeBeforeRegression":2,"dataSetSizeAfterRegression":2,"dataSetSize":2,"data":[[757382400000,0.124],[757386000000,0.112]]}}' + recorded_at: 2026-01-12 14:22:22 +recorded_with: VCR-vcr/2.1.0 diff --git a/tests/testthat/_vcr/character_dates.yml b/tests/testthat/_vcr/character_dates.yml new file mode 100644 index 0000000..69fd7c9 --- /dev/null +++ b/tests/testthat/_vcr/character_dates.yml @@ -0,0 +1,20 @@ +http_interactions: +- request: + method: GET + uri: https://resourcecode-datacharts.ifremer.fr/api/timeseries?parameter=hs&node=41&start=1994-01-01T00%3A00%3A00Z&end=1994-01-02T00%3A00%3A00Z + response: + status: 200 + headers: + Date: Mon, 12 Jan 2026 14:22:20 GMT + Server: Microsoft-IIS/8.0 + Content-Type: application/json;charset=UTF-8 + Vary: Accept-Encoding + Content-Encoding: gzip + X-Content-Type-Options: nosniff + Referrer-Policy: strict-origin-when-cross-origin + Access-Control-Allow-Origin: '*' + Transfer-Encoding: chunked + body: + string: '{"errorcode":0,"errormessage":"","query":{"downsampling":"none","samplingparameter":"none","platform":"41","parameterCode":"hs","startTimeMillis":"757382400000","endTimeMillis":"757468800000","startTimeFormat":"1994-01-01T00:00:00Z","endTimeFormat":"1994-01-02T00:00:00Z"},"result":{"dataSetSizeBeforeRegression":25,"dataSetSizeAfterRegression":25,"dataSetSize":25,"data":[[757382400000,0.124],[757386000000,0.112],[757389600000,0.098],[757393200000,0.082],[757396800000,0.07],[757400400000,0.058],[757404000000,0.052],[757407600000,0.05],[757411200000,0.048],[757414800000,0.046],[757418400000,0.046],[757422000000,0.044],[757425600000,0.044],[757429200000,0.044],[757432800000,0.042],[757436400000,0.038],[757440000000,0.034],[757443600000,0.028],[757447200000,0.024],[757450800000,0.022],[757454400000,0.022],[757458000000,0.022],[757461600000,0.026],[757465200000,0.03],[757468800000,0.034]]}}' + recorded_at: 2026-01-12 14:22:20 +recorded_with: VCR-vcr/2.1.0 diff --git a/tests/testthat/_vcr/get_multiple_parameters.yml b/tests/testthat/_vcr/get_multiple_parameters.yml new file mode 100644 index 0000000..ade0d15 --- /dev/null +++ b/tests/testthat/_vcr/get_multiple_parameters.yml @@ -0,0 +1,38 @@ +http_interactions: +- request: + method: GET + uri: https://resourcecode-datacharts.ifremer.fr/api/timeseries?parameter=hs&node=41&start=1994-01-01T00%3A00%3A00Z&end=1994-01-02T00%3A00%3A00Z + response: + status: 200 + headers: + Date: Mon, 12 Jan 2026 14:22:18 GMT + Server: Microsoft-IIS/8.0 + Content-Type: application/json;charset=UTF-8 + Vary: Accept-Encoding + Content-Encoding: gzip + X-Content-Type-Options: nosniff + Referrer-Policy: strict-origin-when-cross-origin + Access-Control-Allow-Origin: '*' + Transfer-Encoding: chunked + body: + string: '{"errorcode":0,"errormessage":"","query":{"downsampling":"none","samplingparameter":"none","platform":"41","parameterCode":"hs","startTimeMillis":"757382400000","endTimeMillis":"757468800000","startTimeFormat":"1994-01-01T00:00:00Z","endTimeFormat":"1994-01-02T00:00:00Z"},"result":{"dataSetSizeBeforeRegression":25,"dataSetSizeAfterRegression":25,"dataSetSize":25,"data":[[757382400000,0.124],[757386000000,0.112],[757389600000,0.098],[757393200000,0.082],[757396800000,0.07],[757400400000,0.058],[757404000000,0.052],[757407600000,0.05],[757411200000,0.048],[757414800000,0.046],[757418400000,0.046],[757422000000,0.044],[757425600000,0.044],[757429200000,0.044],[757432800000,0.042],[757436400000,0.038],[757440000000,0.034],[757443600000,0.028],[757447200000,0.024],[757450800000,0.022],[757454400000,0.022],[757458000000,0.022],[757461600000,0.026],[757465200000,0.03],[757468800000,0.034]]}}' + recorded_at: 2026-01-12 14:22:18 +- request: + method: GET + uri: https://resourcecode-datacharts.ifremer.fr/api/timeseries?parameter=fp&node=41&start=1994-01-01T00%3A00%3A00Z&end=1994-01-02T00%3A00%3A00Z + response: + status: 200 + headers: + Date: Mon, 12 Jan 2026 14:22:18 GMT + Server: Microsoft-IIS/8.0 + Content-Type: application/json;charset=UTF-8 + Vary: Accept-Encoding + Content-Encoding: gzip + X-Content-Type-Options: nosniff + Referrer-Policy: strict-origin-when-cross-origin + Access-Control-Allow-Origin: '*' + Transfer-Encoding: chunked + body: + string: '{"errorcode":0,"errormessage":"","query":{"downsampling":"none","samplingparameter":"none","platform":"41","parameterCode":"fp","startTimeMillis":"757382400000","endTimeMillis":"757468800000","startTimeFormat":"1994-01-01T00:00:00Z","endTimeFormat":"1994-01-02T00:00:00Z"},"result":{"dataSetSizeBeforeRegression":25,"dataSetSizeAfterRegression":25,"dataSetSize":25,"data":[[757382400000,0.067],[757386000000,0.067],[757389600000,0.068],[757393200000,0.069],[757396800000,0.069],[757400400000,0.069],[757404000000,0.068],[757407600000,0.068],[757411200000,0.068],[757414800000,0.068],[757418400000,0.068],[757422000000,0.069],[757425600000,0.07],[757429200000,0.071],[757432800000,0.072],[757436400000,0.073],[757440000000,0.074],[757443600000,0.074],[757447200000,0.074],[757450800000,0.073],[757454400000,0.072],[757458000000,0.072],[757461600000,0.072],[757465200000,0.072],[757468800000,0.072]]}}' + recorded_at: 2026-01-12 14:22:19 +recorded_with: VCR-vcr/2.1.0 diff --git a/tests/testthat/_vcr/get_single_parameter.yml b/tests/testthat/_vcr/get_single_parameter.yml new file mode 100644 index 0000000..5f5edcf --- /dev/null +++ b/tests/testthat/_vcr/get_single_parameter.yml @@ -0,0 +1,20 @@ +http_interactions: +- request: + method: GET + uri: https://resourcecode-datacharts.ifremer.fr/api/timeseries?parameter=hs&node=41&start=1994-01-01T00%3A00%3A00Z&end=1994-01-02T00%3A00%3A00Z + response: + status: 200 + headers: + Date: Mon, 12 Jan 2026 14:22:18 GMT + Server: Microsoft-IIS/8.0 + Content-Type: application/json;charset=UTF-8 + Vary: Accept-Encoding + Content-Encoding: gzip + X-Content-Type-Options: nosniff + Referrer-Policy: strict-origin-when-cross-origin + Access-Control-Allow-Origin: '*' + Transfer-Encoding: chunked + body: + string: '{"errorcode":0,"errormessage":"","query":{"downsampling":"none","samplingparameter":"none","platform":"41","parameterCode":"hs","startTimeMillis":"757382400000","endTimeMillis":"757468800000","startTimeFormat":"1994-01-01T00:00:00Z","endTimeFormat":"1994-01-02T00:00:00Z"},"result":{"dataSetSizeBeforeRegression":25,"dataSetSizeAfterRegression":25,"dataSetSize":25,"data":[[757382400000,0.124],[757386000000,0.112],[757389600000,0.098],[757393200000,0.082],[757396800000,0.07],[757400400000,0.058],[757404000000,0.052],[757407600000,0.05],[757411200000,0.048],[757414800000,0.046],[757418400000,0.046],[757422000000,0.044],[757425600000,0.044],[757429200000,0.044],[757432800000,0.042],[757436400000,0.038],[757440000000,0.034],[757443600000,0.028],[757447200000,0.024],[757450800000,0.022],[757454400000,0.022],[757458000000,0.022],[757461600000,0.026],[757465200000,0.03],[757468800000,0.034]]}}' + recorded_at: 2026-01-12 14:22:18 +recorded_with: VCR-vcr/2.1.0 diff --git a/tests/testthat/_vcr/numeric_dates.yml b/tests/testthat/_vcr/numeric_dates.yml new file mode 100644 index 0000000..69fd7c9 --- /dev/null +++ b/tests/testthat/_vcr/numeric_dates.yml @@ -0,0 +1,20 @@ +http_interactions: +- request: + method: GET + uri: https://resourcecode-datacharts.ifremer.fr/api/timeseries?parameter=hs&node=41&start=1994-01-01T00%3A00%3A00Z&end=1994-01-02T00%3A00%3A00Z + response: + status: 200 + headers: + Date: Mon, 12 Jan 2026 14:22:20 GMT + Server: Microsoft-IIS/8.0 + Content-Type: application/json;charset=UTF-8 + Vary: Accept-Encoding + Content-Encoding: gzip + X-Content-Type-Options: nosniff + Referrer-Policy: strict-origin-when-cross-origin + Access-Control-Allow-Origin: '*' + Transfer-Encoding: chunked + body: + string: '{"errorcode":0,"errormessage":"","query":{"downsampling":"none","samplingparameter":"none","platform":"41","parameterCode":"hs","startTimeMillis":"757382400000","endTimeMillis":"757468800000","startTimeFormat":"1994-01-01T00:00:00Z","endTimeFormat":"1994-01-02T00:00:00Z"},"result":{"dataSetSizeBeforeRegression":25,"dataSetSizeAfterRegression":25,"dataSetSize":25,"data":[[757382400000,0.124],[757386000000,0.112],[757389600000,0.098],[757393200000,0.082],[757396800000,0.07],[757400400000,0.058],[757404000000,0.052],[757407600000,0.05],[757411200000,0.048],[757414800000,0.046],[757418400000,0.046],[757422000000,0.044],[757425600000,0.044],[757429200000,0.044],[757432800000,0.042],[757436400000,0.038],[757440000000,0.034],[757443600000,0.028],[757447200000,0.024],[757450800000,0.022],[757454400000,0.022],[757458000000,0.022],[757461600000,0.026],[757465200000,0.03],[757468800000,0.034]]}}' + recorded_at: 2026-01-12 14:22:20 +recorded_with: VCR-vcr/2.1.0 diff --git a/tests/testthat/_vcr/tp_conversion.yml b/tests/testthat/_vcr/tp_conversion.yml new file mode 100644 index 0000000..d423ff1 --- /dev/null +++ b/tests/testthat/_vcr/tp_conversion.yml @@ -0,0 +1,20 @@ +http_interactions: +- request: + method: GET + uri: https://resourcecode-datacharts.ifremer.fr/api/timeseries?parameter=fp&node=41&start=1994-01-01T00%3A00%3A00Z&end=1994-01-02T00%3A00%3A00Z + response: + status: 200 + headers: + Date: Mon, 12 Jan 2026 14:22:22 GMT + Server: Microsoft-IIS/8.0 + Content-Type: application/json;charset=UTF-8 + Vary: Accept-Encoding + Content-Encoding: gzip + X-Content-Type-Options: nosniff + Referrer-Policy: strict-origin-when-cross-origin + Access-Control-Allow-Origin: '*' + Transfer-Encoding: chunked + body: + string: '{"errorcode":0,"errormessage":"","query":{"downsampling":"none","samplingparameter":"none","platform":"41","parameterCode":"fp","startTimeMillis":"757382400000","endTimeMillis":"757468800000","startTimeFormat":"1994-01-01T00:00:00Z","endTimeFormat":"1994-01-02T00:00:00Z"},"result":{"dataSetSizeBeforeRegression":25,"dataSetSizeAfterRegression":25,"dataSetSize":25,"data":[[757382400000,0.067],[757386000000,0.067],[757389600000,0.068],[757393200000,0.069],[757396800000,0.069],[757400400000,0.069],[757404000000,0.068],[757407600000,0.068],[757411200000,0.068],[757414800000,0.068],[757418400000,0.068],[757422000000,0.069],[757425600000,0.07],[757429200000,0.071],[757432800000,0.072],[757436400000,0.073],[757440000000,0.074],[757443600000,0.074],[757447200000,0.074],[757450800000,0.073],[757454400000,0.072],[757458000000,0.072],[757461600000,0.072],[757465200000,0.072],[757468800000,0.072]]}}' + recorded_at: 2026-01-12 14:22:22 +recorded_with: VCR-vcr/2.1.0 diff --git a/tests/testthat/_vcr/week_of_data.yml b/tests/testthat/_vcr/week_of_data.yml new file mode 100644 index 0000000..ac0afca --- /dev/null +++ b/tests/testthat/_vcr/week_of_data.yml @@ -0,0 +1,39 @@ +http_interactions: +- request: + method: GET + uri: https://resourcecode-datacharts.ifremer.fr/api/timeseries?parameter=hs&node=41&start=1994-01-01T00%3A00%3A00Z&end=1994-01-07T23%3A00%3A00Z + response: + status: 200 + headers: + Date: Mon, 12 Jan 2026 14:22:20 GMT + Server: Microsoft-IIS/8.0 + Content-Type: application/json;charset=UTF-8 + Vary: Accept-Encoding + Content-Encoding: gzip + X-Content-Type-Options: nosniff + Referrer-Policy: strict-origin-when-cross-origin + Access-Control-Allow-Origin: '*' + Transfer-Encoding: chunked + body: + string: '{"errorcode":0,"errormessage":"","query":{"downsampling":"none","samplingparameter":"none","platform":"41","parameterCode":"hs","startTimeMillis":"757382400000","endTimeMillis":"757983600000","startTimeFormat":"1994-01-01T00:00:00Z","endTimeFormat":"1994-01-07T23:00:00Z"},"result":{"dataSetSizeBeforeRegression":168,"dataSetSizeAfterRegression":168,"dataSetSize":168,"data":[[757382400000,0.124],[757386000000,0.112],[757389600000,0.098],[757393200000,0.082],[757396800000,0.07],[757400400000,0.058],[757404000000,0.052],[757407600000,0.05],[757411200000,0.048],[757414800000,0.046],[757418400000,0.046],[757422000000,0.044],[757425600000,0.044],[757429200000,0.044],[757432800000,0.042],[757436400000,0.038],[757440000000,0.034],[757443600000,0.028],[757447200000,0.024],[757450800000,0.022],[757454400000,0.022],[757458000000,0.022],[757461600000,0.026],[757465200000,0.03],[757468800000,0.034],[757472400000,0.036],[757476000000,0.036],[757479600000,0.034],[757483200000,0.03],[757486800000,0.026],[757490400000,0.022],[757494000000,0.018],[757497600000,0.018],[757501200000,0.02],[757504800000,0.024],[757508400000,0.028],[757512000000,0.032],[757515600000,0.034],[757519200000,0.034],[757522800000,0.032],[757526400000,0.032],[757530000000,0.036],[757533600000,0.056],[757537200000,0.066],[757540800000,0.062],[757544400000,0.058],[757548000000,0.058],[757551600000,0.058],[757555200000,0.058],[757558800000,0.062],[757562400000,0.062],[757566000000,0.058],[757569600000,0.052],[757573200000,0.042],[757576800000,0.032],[757580400000,0.026],[757584000000,0.022],[757587600000,0.02],[757591200000,0.022],[757594800000,0.024],[757598400000,0.028],[757602000000,0.032],[757605600000,0.034],[757609200000,0.034],[757612800000,0.032],[757616400000,0.03],[757620000000,0.028],[757623600000,0.024],[757627200000,0.024],[757630800000,0.024],[757634400000,0.026],[757638000000,0.03],[757641600000,0.034],[757645200000,0.036],[757648800000,0.038],[757652400000,0.04],[757656000000,0.038],[757659600000,0.036],[757663200000,0.032],[757666800000,0.028],[757670400000,0.026],[757674000000,0.03],[757677600000,0.068],[757681200000,0.08],[757684800000,0.082],[757688400000,0.088],[757692000000,0.096],[757695600000,0.104],[757699200000,0.11],[757702800000,0.112],[757706400000,0.112],[757710000000,0.108],[757713600000,0.1],[757717200000,0.096],[757720800000,0.1],[757724400000,0.108],[757728000000,0.116],[757731600000,0.124],[757735200000,0.126],[757738800000,0.128],[757742400000,0.124],[757746000000,0.114],[757749600000,0.106],[757753200000,0.102],[757756800000,0.1],[757760400000,0.094],[757764000000,0.092],[757767600000,0.094],[757771200000,0.094],[757774800000,0.092],[757778400000,0.09],[757782000000,0.088],[757785600000,0.084],[757789200000,0.08],[757792800000,0.074],[757796400000,0.074],[757800000000,0.074],[757803600000,0.078],[757807200000,0.076],[757810800000,0.074],[757814400000,0.074],[757818000000,0.072],[757821600000,0.066],[757825200000,0.064],[757828800000,0.068],[757832400000,0.074],[757836000000,0.076],[757839600000,0.076],[757843200000,0.074],[757846800000,0.074],[757850400000,0.068],[757854000000,0.06],[757857600000,0.056],[757861200000,0.05],[757864800000,0.046],[757868400000,0.04],[757872000000,0.038],[757875600000,0.034],[757879200000,0.032],[757882800000,0.03],[757886400000,0.028],[757890000000,0.024],[757893600000,0.022],[757897200000,0.018],[757900800000,0.016],[757904400000,0.016],[757908000000,0.016],[757911600000,0.018],[757915200000,0.02],[757918800000,0.022],[757922400000,0.024],[757926000000,0.024],[757929600000,0.024],[757933200000,0.022],[757936800000,0.02],[757940400000,0.018],[757944000000,0.016],[757947600000,0.016],[757951200000,0.018],[757954800000,0.02],[757958400000,0.024],[757962000000,0.028],[757965600000,0.03],[757969200000,0.032],[757972800000,0.03],[757976400000,0.026],[757980000000,0.024],[757983600000,0.022]]}}' + recorded_at: 2026-01-12 14:22:20 +- request: + method: GET + uri: https://resourcecode-datacharts.ifremer.fr/api/timeseries?parameter=fp&node=41&start=1994-01-01T00%3A00%3A00Z&end=1994-01-07T23%3A00%3A00Z + response: + status: 200 + headers: + Date: Mon, 12 Jan 2026 14:22:20 GMT + Server: Microsoft-IIS/8.0 + Content-Type: application/json;charset=UTF-8 + Vary: Accept-Encoding + Content-Encoding: gzip + X-Content-Type-Options: nosniff + Referrer-Policy: strict-origin-when-cross-origin + Access-Control-Allow-Origin: '*' + Connection: close + Transfer-Encoding: chunked + body: + string: '{"errorcode":0,"errormessage":"","query":{"downsampling":"none","samplingparameter":"none","platform":"41","parameterCode":"fp","startTimeMillis":"757382400000","endTimeMillis":"757983600000","startTimeFormat":"1994-01-01T00:00:00Z","endTimeFormat":"1994-01-07T23:00:00Z"},"result":{"dataSetSizeBeforeRegression":168,"dataSetSizeAfterRegression":168,"dataSetSize":168,"data":[[757382400000,0.067],[757386000000,0.067],[757389600000,0.068],[757393200000,0.069],[757396800000,0.069],[757400400000,0.069],[757404000000,0.068],[757407600000,0.068],[757411200000,0.068],[757414800000,0.068],[757418400000,0.068],[757422000000,0.069],[757425600000,0.07],[757429200000,0.071],[757432800000,0.072],[757436400000,0.073],[757440000000,0.074],[757443600000,0.074],[757447200000,0.074],[757450800000,0.073],[757454400000,0.072],[757458000000,0.072],[757461600000,0.072],[757465200000,0.072],[757468800000,0.072],[757472400000,0.072],[757476000000,0.072],[757479600000,0.072],[757483200000,0.071],[757486800000,0.069],[757490400000,0.069],[757494000000,0.069],[757497600000,0.069],[757501200000,0.07],[757504800000,0.071],[757508400000,0.072],[757512000000,0.073],[757515600000,0.074],[757519200000,0.074],[757522800000,0.075],[757526400000,0.077],[757530000000,0.079],[757533600000,0.08],[757537200000,0.862],[757540800000,0.821],[757544400000,0.796],[757548000000,0.799],[757551600000,0.081],[757555200000,0.081],[757558800000,0.081],[757562400000,0.082],[757566000000,0.082],[757569600000,0.083],[757573200000,0.084],[757576800000,0.085],[757580400000,0.085],[757584000000,0.085],[757587600000,0.046],[757591200000,0.047],[757594800000,0.049],[757598400000,0.049],[757602000000,0.05],[757605600000,0.05],[757609200000,0.051],[757612800000,0.052],[757616400000,0.053],[757620000000,0.054],[757623600000,0.055],[757627200000,0.056],[757630800000,0.056],[757634400000,0.057],[757638000000,0.059],[757641600000,0.059],[757645200000,0.06],[757648800000,0.06],[757652400000,0.061],[757656000000,0.061],[757659600000,0.062],[757663200000,0.062],[757666800000,0.062],[757670400000,0.062],[757674000000,0.062],[757677600000,0.063],[757681200000,0.064],[757684800000,0.065],[757688400000,0.066],[757692000000,0.066],[757695600000,0.067],[757699200000,0.652],[757702800000,0.643],[757706400000,0.639],[757710000000,0.647],[757713600000,0.655],[757717200000,0.662],[757720800000,0.66],[757724400000,0.646],[757728000000,0.616],[757731600000,0.605],[757735200000,0.6],[757738800000,0.597],[757742400000,0.599],[757746000000,0.607],[757749600000,0.62],[757753200000,0.651],[757756800000,0.661],[757760400000,0.665],[757764000000,0.669],[757767600000,0.668],[757771200000,0.664],[757774800000,0.664],[757778400000,0.667],[757782000000,0.671],[757785600000,0.676],[757789200000,0.688],[757792800000,0.709],[757796400000,0.725],[757800000000,0.729],[757803600000,0.729],[757807200000,0.722],[757810800000,0.723],[757814400000,0.722],[757818000000,0.715],[757821600000,0.713],[757825200000,0.721],[757828800000,0.734],[757832400000,0.733],[757836000000,0.728],[757839600000,0.724],[757843200000,0.724],[757846800000,0.726],[757850400000,0.728],[757854000000,0.733],[757857600000,0.74],[757861200000,0.755],[757864800000,0.775],[757868400000,0.783],[757872000000,0.09],[757875600000,0.09],[757879200000,0.072],[757882800000,0.072],[757886400000,0.073],[757890000000,0.073],[757893600000,0.074],[757897200000,0.074],[757900800000,0.075],[757904400000,0.075],[757908000000,0.075],[757911600000,0.074],[757915200000,0.07],[757918800000,0.069],[757922400000,0.069],[757926000000,0.069],[757929600000,0.071],[757933200000,0.072],[757936800000,0.073],[757940400000,0.073],[757944000000,0.073],[757947600000,0.073],[757951200000,0.073],[757954800000,0.073],[757958400000,0.073],[757962000000,0.073],[757965600000,0.074],[757969200000,0.075],[757972800000,0.076],[757976400000,0.077],[757980000000,0.077],[757983600000,0.078]]}}' + recorded_at: 2026-01-12 14:22:21 +recorded_with: VCR-vcr/2.1.0 diff --git a/tests/testthat/test-data_download.R b/tests/testthat/test-data_download.R deleted file mode 100644 index d3c588f..0000000 --- a/tests/testthat/test-data_download.R +++ /dev/null @@ -1,224 +0,0 @@ -test_that("Errors in 'get_parameters()' are handled correcly", { - expect_error( - get_parameters("tépé"), - "Requested parameters do not exists in the database: tépé" - ) - expect_error( - get_parameters(node = 0), - "The requested location do no exist in the database." - ) - expect_error( - get_parameters(node = c(10, 100)), - "The function can retreive only one location a time." - ) - expect_error( - get_parameters(start = 1), - paste0( - "'start' is outside the covered period: ", - paste( - format( - c( - resourcecode:::rscd_casandra_start_date, - resourcecode:::rscd_casandra_end_date - ), - format = "%Y-%m-%d %H:%M %Z" - ), - collapse = " \u2014 " - ) - ) - ) - expect_error( - get_parameters(end = 1e10), - paste0( - "'end' is outside the covered period: ", - paste( - format( - c( - resourcecode:::rscd_casandra_start_date, - resourcecode:::rscd_casandra_end_date - ), - format = "%Y-%m-%d %H:%M %Z" - ), - collapse = " \u2014 " - ) - ) - ) - expect_error( - get_parameters( - start = "1994-01-31 01:00:00", - end = "1994-01-11 01:00:00" - ), - "'end' must be after 'start'" - ) -}) - -test_that("downloading parameters data works", { - skip_if_offline() - dat <- get_parameters( - parameters = c("hs", "tp"), - node = 42, - start = "1994-01-01 00:00:00 UTC", - end = 760057200 - ) - expect_s3_class(dat, "data.frame") - expect_equal(names(dat), c("time", "hs", "tp")) - expect_equal(NROW(dat), 24 * 31) - expect_equal( - get_parameters( - parameters = c("hs", "tp"), - node = 42, - start = 757382400, - end = "1994-01-31 23:00:00 UTC" - ), - dat - ) -}) - -test_that("get_parameters_raw() returns NULL and message on failure", { - get_parameters_raw <- getFromNamespace("get_parameters_raw", "resourcecode") - # mock a function that throws error (as if network/API failed) - mock_fromJSON <- function(...) stop("network failure") # nolint - - # temporarily replace fromJSON inside your function - mockery::stub(get_parameters_raw, "jsonlite::fromJSON", mock_fromJSON) - - expect_message( - result <- get_parameters_raw("hs"), - "Could not retrieve data" - ) - - expect_null(result) -}) - -test_that("get_parameters_raw() handles API-side error codes", { - get_parameters_raw <- getFromNamespace("get_parameters_raw", "resourcecode") - fake_api_response <- list(errorcode = 123, errormessage = "Invalid request") - - mock_fromJSON <- function(...) fake_api_response # nolint - - mockery::stub(get_parameters_raw, "jsonlite::fromJSON", mock_fromJSON) - - expect_message( - result <- get_parameters_raw("anything"), - "The data source returned an error" - ) - - expect_null(result) -}) - -test_that("downloading 1D spectral data works", { - skip_if_offline() - spec <- get_1d_spectrum( - 1L, - start = "1994-12-01 00:00:00 UTC", - end = "1995-01-31 00:00:00 UTC" - ) - expect_equal(spec, get_1d_spectrum(1L, start = 786243600, end = 791506800)) - expect_type(spec, "list") - expect_equal( - names(spec), - c( - "longitude", - "latitude", - "frequency1", - "frequency2", - "ef", - "th1m", - "th2m", - "sth1m", - "sth2m", - "freq", - "forcings", - "station" - ) - ) - expect_equal(NROW(spec$forcings), 1488) - expect_equal(spec$station, "E001500N52000") -}) - - -test_that("downloading 2D spectral data works", { - skip_if_offline() - spec <- get_2d_spectrum( - 1L, - start = "1994-12-01 00:00:00 UTC", - end = "1995-01-31 00:00:00 UTC" - ) - expect_equal(spec, get_2d_spectrum(1L, start = 786243600, end = 791506800)) - expect_type(spec, "list") - expect_equal( - names(spec), - c( - "longitude", - "latitude", - "frequency1", - "frequency2", - "efth", - "freq", - "dir", - "forcings", - "station" - ) - ) - expect_equal(NROW(spec$forcings), 1488) - expect_equal(spec$station, "E001500N52000") -}) - -test_that("download_nc_data() fails gracefully when FTP not available", { - download_nc_data <- getFromNamespace("download_nc_data", "resourcecode") - # Mock curl_download to throw an error (simulating network failure) - mock_download <- function(...) stop("FTP connection failed") - - # Replace curl_download inside the function - mockery::stub(download_nc_data, "curl::curl_download", mock_download) - - expect_message( - result <- download_nc_data( - "ftp://example.org/file.dat", - tempfile(fileext = ".nc") - ), - "Could not download spectral data" - ) - - expect_null(result) -}) - -test_that("get_1d_spectrum() fails gracefully when FTP not available", { - download_nc_data <- getFromNamespace("download_nc_data", "resourcecode") - # Mock curl_download to throw an error (simulating network failure) - mock_download <- function(...) stop("FTP connection failed") - - # Replace curl_download inside the function - testthat::local_mocked_bindings(download_nc_data = function(...) NULL) - - expect_message( - result <- get_1d_spectrum( - "SEMREVO", - start = "1994-01-01", - end = "1994-02-28" - ), - "Could not download spectral data" - ) - - expect_null(result) -}) - -test_that("get_2d_spectrum() fails gracefully when FTP not available", { - download_nc_data <- getFromNamespace("download_nc_data", "resourcecode") - # Mock curl_download to throw an error (simulating network failure) - mock_download <- function(...) stop("FTP connection failed") - - # Replace curl_download inside the function - testthat::local_mocked_bindings(download_nc_data = function(...) NULL) - - expect_message( - result <- get_2d_spectrum( - "SEMREVO", - start = "1994-01-01", - end = "1994-02-28" - ), - "Could not download spectral data" - ) - - expect_null(result) -}) diff --git a/tests/testthat/test-spectral_data_download.R b/tests/testthat/test-spectral_data_download.R new file mode 100644 index 0000000..2113c27 --- /dev/null +++ b/tests/testthat/test-spectral_data_download.R @@ -0,0 +1,422 @@ +# Tests for get_1d_spectrum() +test_that("get_1d_spectrum retrieves data successfully", { + skip_if_offline() + + spec <- get_1d_spectrum( + "SEMREVO", + start = "1994-01-01", + end = "1994-02-28" + ) + + expect_type(spec, "list") + expect_named( + spec, + c( + "longitude", + "latitude", + "frequency1", + "frequency2", + "ef", + "th1m", + "th2m", + "sth1m", + "sth2m", + "freq", + "forcings", + "station" + ) + ) + expect_s3_class(spec$forcings, "data.frame") + expect_shape(spec$forcings, dim = c(1416, 14)) + expect_equal(spec$station, "SEMREVO") +}) + +test_that("get_1d_spectrum handles numeric node input", { + #vcr::local_cassette("get_1d_spectrum_numeric_node") + skip_if_offline() + + spec_by_name <- get_1d_spectrum( + "SEMREVO", + start = "1994-01-01", + end = "1994-01-31" + ) + + # Find the index of SEMREVO in rscd_spectral + idx <- which(resourcecodedata::rscd_spectral$name == "SEMREVO") + + spec_by_index <- get_1d_spectrum( + idx, + start = "1994-01-01", + end = "1994-01-31" + ) + + expect_equal(spec_by_name$station, spec_by_index$station) +}) + +test_that("get_1d_spectrum handles character date inputs", { + #vcr::local_cassette("get_1d_spectrum_character_dates") + skip_if_offline() + + spec <- get_1d_spectrum( + "SEMREVO", + start = "1994-01-01", + end = "1994-01-31" + ) + + expect_type(spec, "list") + expect_shape(spec$forcings, dim = c(744, 14)) +}) + +test_that("get_1d_spectrum handles numeric (UNIX timestamp) date inputs", { + # vcr::local_cassette("get_1d_spectrum_numeric_dates") + skip_if_offline() + + start_unix <- as.numeric(as.POSIXct("1994-01-01", tz = "UTC")) + end_unix <- as.numeric(as.POSIXct("1994-01-31", tz = "UTC")) + + spec <- get_1d_spectrum( + "SEMREVO", + start = start_unix, + end = end_unix + ) + + expect_type(spec, "list") + expect_gt(nrow(spec$forcings), 0) +}) + +test_that("get_1d_spectrum handles multi-month requests", { + # vcr::local_cassette("get_1d_spectrum_multi_month") + skip_if_offline() + + spec <- get_1d_spectrum( + "SEMREVO", + start = "1994-01-01", + end = "1994-03-31" + ) + + expect_type(spec, "list") + # Should have data for 3 months + expect_gt(nrow(spec$forcings), 2000) # Rough estimate +}) + +test_that("get_1d_spectrum validates forcings data structure", { + # vcr::local_cassette("get_1d_spectrum_forcings_structure") + skip_if_offline() + + spec <- get_1d_spectrum( + "SEMREVO", + start = "1994-01-01", + end = "1994-01-31" + ) + + # Check forcings has expected columns + expected_cols <- c( + "time", + "dpt", + "wnd", + "wnddir", + "cur", + "curdir", + "hs", + "fp", + "f02", + "f0m1", + "th1p", + "sth1p", + "dir", + "spr" + ) + expect_true(all(expected_cols %in% names(spec$forcings))) + + # Check time is POSIXct + expect_s3_class(spec$forcings$time, "POSIXct") +}) + +test_that("get_1d_spectrum validates spectral arrays dimensions", { + # vcr::local_cassette("get_1d_spectrum_array_dimensions") + skip_if_offline() + + spec <- get_1d_spectrum( + "SEMREVO", + start = "1994-01-01", + end = "1994-01-31" + ) + + # Check that spectral arrays have correct dimensions + expect_true(is.matrix(spec$ef)) + expect_equal(nrow(spec$ef), length(spec$freq)) + expect_equal(ncol(spec$ef), nrow(spec$forcings)) +}) + +# Error handling tests - input validation (no network needed) +test_that("get_1d_spectrum validates point input length", { + expect_error( + get_1d_spectrum( + c("SEMREVO", "AUTRE"), + start = "1994-01-01", + end = "1994-01-31" + ), + "length\\(point\\) == 1" + ) +}) + +test_that("get_1d_spectrum validates point exists", { + expect_error( + get_1d_spectrum( + "INVALID_POINT", + start = "1994-01-01", + end = "1994-01-31" + ), + "point %in% resourcecodedata::rscd_spectral\\$name" + ) +}) + +test_that("get_1d_spectrum validates date range", { + expect_error( + get_1d_spectrum( + "SEMREVO", + start = "1994-01-31", + end = "1994-01-01" + ), + "end >= start" + ) +}) + +test_that("get_1d_spectrum validates start date is within coverage", { + expect_error( + get_1d_spectrum( + "SEMREVO", + start = "1980-01-01", # Before hindcast period + end = "1994-01-31" + ), + "format\\(start, \"%Y\"\\) >= format\\(rscd_hindcast_start_date" + ) +}) + +test_that("get_1d_spectrum validates end date is within coverage", { + expect_error( + get_1d_spectrum( + "SEMREVO", + start = "1994-01-01", + end = "2030-01-01" # After hindcast period + ), + "format\\(end, \"%Y\"\\) <= format\\(rscd_hindcast_end_date" + ) +}) + +# Network failure tests - using mocks +test_that("get_1d_spectrum fails gracefully when first download fails", { + # Mock the internal raw function to return NULL (simulating download failure) + mockery::stub( + get_1d_spectrum, + "get_1d_spectrum_raw", + NULL + ) + + # Should fail or return NULL depending on your implementation + # Update this based on how you handle NULL in get_1d_spectrum + expect_null( + get_1d_spectrum( + "SEMREVO", + start = "1994-01-01", + end = "1994-01-31" + ) + ) +}) + +# Tests for get_2d_spectrum() +test_that("get_2d_spectrum retrieves data successfully", { + # vcr::local_cassette("get_2d_spectrum_basic") + skip_if_offline() + + spec <- get_2d_spectrum( + "SEMREVO", + start = "1994-01-01", + end = "1994-01-31" + ) + + expect_type(spec, "list") + expect_named( + spec, + c( + "longitude", + "latitude", + "frequency1", + "frequency2", + "efth", + "freq", + "dir", + "forcings", + "station" + ) + ) + expect_s3_class(spec$forcings, "data.frame") + expect_shape(spec$forcings, dim = c(744, 6)) + expect_equal(spec$station, "SEMREVO") +}) + +test_that("get_2d_spectrum handles numeric node input", { + # vcr::local_cassette("get_2d_spectrum_numeric_node") + skip_if_offline() + + spec_by_name <- get_2d_spectrum( + "SEMREVO", + start = "1994-01-01", + end = "1994-01-31" + ) + + idx <- which(resourcecodedata::rscd_spectral$name == "SEMREVO") + + spec_by_index <- get_2d_spectrum( + idx, + start = "1994-01-01", + end = "1994-01-31" + ) + + expect_equal(spec_by_name$station, spec_by_index$station) +}) + +test_that("get_2d_spectrum handles numeric date inputs", { + # vcr::local_cassette("get_2d_spectrum_numeric_dates") + skip_if_offline() + + start_unix <- as.numeric(as.POSIXct("1994-01-01", tz = "UTC")) + end_unix <- as.numeric(as.POSIXct("1994-01-31", tz = "UTC")) + + spec <- get_2d_spectrum( + "SEMREVO", + start = start_unix, + end = end_unix + ) + + expect_type(spec, "list") + expect_gt(nrow(spec$forcings), 0) +}) + +test_that("get_2d_spectrum handles multi-month requests", { + # vcr::local_cassette("get_2d_spectrum_multi_month") + skip_if_offline() + + spec <- get_2d_spectrum( + "SEMREVO", + start = "1994-01-01", + end = "1994-02-28" + ) + + expect_type(spec, "list") + expect_shape(spec$forcings, dim = c(1416, 6)) + + # Check forcings has expected columns + expected_cols <- c("time", "dpt", "wnd", "wnddir", "cur", "curdir") + expect_true(all(expected_cols %in% names(spec$forcings))) + + # Check time is POSIXct + expect_s3_class(spec$forcings$time, "POSIXct") + + #Spectral data + expect_true(is.array(spec$efth)) + expect_equal(length(dim(spec$efth)), 3) + expect_equal(dim(spec$efth)[1], length(spec$dir)) + expect_equal(dim(spec$efth)[2], length(spec$freq)) + expect_equal(dim(spec$efth)[3], nrow(spec$forcings)) +}) + +# Error handling tests - input validation (no network needed) +test_that("get_2d_spectrum validates point input length", { + expect_error( + get_2d_spectrum( + c("SEMREVO", "AUTRE"), + start = "1994-01-01", + end = "1994-01-31" + ), + "length\\(point\\) == 1" + ) +}) + +test_that("get_2d_spectrum validates point exists", { + expect_error( + get_2d_spectrum( + "INVALID_POINT", + start = "1994-01-01", + end = "1994-01-31" + ), + "point %in% resourcecodedata::rscd_spectral\\$name" + ) +}) + +test_that("get_2d_spectrum validates date range", { + expect_error( + get_2d_spectrum( + "SEMREVO", + start = "1994-01-31", + end = "1994-01-01" + ), + "end >= start" + ) +}) + +test_that("get_2d_spectrum validates start date within coverage", { + expect_error( + get_2d_spectrum( + "SEMREVO", + start = "1980-01-01", + end = "1994-01-31" + ), + "format\\(start, \"%Y\"\\) >= format\\(rscd_hindcast_start_date" + ) +}) + +test_that("get_2d_spectrum validates end date within coverage", { + expect_error( + get_2d_spectrum( + "SEMREVO", + start = "1994-01-01", + end = "2030-01-01" + ), + "format\\(end, \"%Y\"\\) <= format\\(rscd_hindcast_end_date" + ) +}) + +# Network failure tests - using mocks (no vcr needed) +test_that("get_2d_spectrum fails gracefully when first download fails", { + mockery::stub( + get_2d_spectrum, + "get_2d_spectrum_raw", + NULL + ) + + expect_null( + get_2d_spectrum( + "SEMREVO", + start = "1994-01-01", + end = "1994-01-31" + ) + ) +}) + +# Edge case: Boundary dates +test_that("get_1d_spectrum accepts dates at exact boundaries", { + # vcr::local_cassette("get_1d_spectrum_boundary_dates") + + # Test with start date at exact boundary + expect_no_error( + get_1d_spectrum( + "SEMREVO", + start = format(resourcecode:::rscd_hindcast_start_date, "%Y-01-01"), + end = format(resourcecode:::rscd_hindcast_start_date, "%Y-01-31") + ) + ) +}) + +test_that("get_2d_spectrum accepts dates at exact boundaries", { + # vcr::local_cassette("get_2d_spectrum_boundary_dates") + + # Test with start date at exact boundary + expect_no_error( + get_2d_spectrum( + "SEMREVO", + start = format(resourcecode:::rscd_hindcast_start_date, "%Y-01-01"), + end = format(resourcecode:::rscd_hindcast_start_date, "%Y-01-31") + ) + ) +}) diff --git a/tests/testthat/tests_download_parameters.R b/tests/testthat/tests_download_parameters.R new file mode 100644 index 0000000..664e08d --- /dev/null +++ b/tests/testthat/tests_download_parameters.R @@ -0,0 +1,350 @@ +# Configure vcr for your tests +# vcr_configure( +# dir = "tests/fixtures/vcr_cassettes", +# filter_sensitive_data = list( +# # If you have API keys or sensitive data in URLs, filter them here +# # "<<>>" = Sys.getenv("SECRET_KEY") +# ) +# ) + +#Tests for get_parameters function (which also tests get_parameters_raw internally) +test_that("get_parameters retrieves single parameter and tests basic functionality", { + vcr::local_cassette("get_single_parameter") + result <- get_parameters( + parameters = "hs", + node = 42, + start = as.POSIXct("1994-01-01 00:00:00", tz = "UTC"), + end = as.POSIXct("1994-01-02 00:00:00", tz = "UTC") + ) + + expect_s3_class(result, "data.frame") + expect_named(result, c("time", "hs")) + expect_s3_class(result$time, "POSIXct") + expect_type(result$hs, "double") + expect_true(nrow(result) > 0) +}) + +test_that("get_parameters retrieves multiple parameters including tp conversion", { + vcr::local_cassette("get_multiple_parameters") + result <- get_parameters( + parameters = c("hs", "tp"), + node = 42, + start = as.POSIXct("1994-01-01 00:00:00", tz = "UTC"), + end = as.POSIXct("1994-01-02 00:00:00", tz = "UTC") + ) + + expect_s3_class(result, "data.frame") + expect_named(result, c("time", "hs", "tp")) + expect_true(nrow(result) > 0) + # Check that tp values are positive (tests fp to tp conversion in get_parameters_raw) + expect_true(all(result$tp > 0, na.rm = TRUE)) +}) + +test_that("get_parameters handles character date inputs", { + vcr::local_cassette("character_dates") + result <- get_parameters( + parameters = "hs", + node = 42, + start = "1994-01-01 00:00:00", + end = "1994-01-02 00:00:00" + ) + + expect_s3_class(result, "data.frame") + expect_true(nrow(result) > 0) +}) + +test_that("get_parameters handles numeric date inputs", { + vcr::local_cassette("numeric_dates") + start_num <- as.numeric(as.POSIXct("1994-01-01 00:00:00", tz = "UTC")) + end_num <- as.numeric(as.POSIXct("1994-01-02 00:00:00", tz = "UTC")) + + result <- get_parameters( + parameters = "hs", + node = 42, + start = start_num, + end = end_num + ) + + expect_s3_class(result, "data.frame") + expect_true(nrow(result) > 0) +}) + +# Error handling tests (these don't need vcr as they fail before API call) +test_that("get_parameters validates parameter names", { + expect_error( + get_parameters( + parameters = c("hs", "invalid_param"), + node = 42 + ), + "Requested parameters do not exists" + ) +}) + +test_that("get_parameters validates node input", { + expect_error( + get_parameters( + parameters = "hs", + node = c(42, 43) + ), + "only one location a time" + ) +}) + +test_that("get_parameters validates date range", { + expect_error( + get_parameters( + parameters = "hs", + node = 42, + start = as.POSIXct("1994-01-02 00:00:00", tz = "UTC"), + end = as.POSIXct("1994-01-01 00:00:00", tz = "UTC") + ), + "'end' must be after 'start'" + ) +}) + +# Test with recorded fixtures to ensure consistent behavior +test_that("get_parameters produces expected data structure over time range", { + vcr::local_cassette("week_of_data") + result <- get_parameters( + parameters = c("hs", "tp"), + node = 42, + start = as.POSIXct("1994-01-01 00:00:00", tz = "UTC"), + end = as.POSIXct("1994-01-07 23:00:00", tz = "UTC") + ) + + expect_s3_class(result, "data.frame") + expect_equal(ncol(result), 3) # time, hs, tp + # Expect roughly hourly data for a week (168 hours) + expect_gt(nrow(result), 100) + expect_lt(nrow(result), 200) + + # Check data types + expect_s3_class(result$time, "POSIXct") + expect_type(result$hs, "double") + expect_type(result$tp, "double") +}) + +test_that("get_parameters_raw handles HTTP 404 error", { + mock_resp <- structure( + list(status_code = 404), + class = "httr2_response" + ) + + mockery::stub(get_parameters_raw, "httr2::req_perform", mock_resp) + mockery::stub(get_parameters_raw, "httr2::resp_status", 404) + mockery::stub(get_parameters_raw, "httr2::resp_status_desc", "Not Found") + + expect_message( + result <- get_parameters_raw( + parameter = "hs", + node = 42, + start = as.POSIXct("1994-01-01 00:00:00", tz = "UTC"), + end = as.POSIXct("1994-01-02 00:00:00", tz = "UTC") + ), + "HTTP error 404" + ) + + expect_null(result) +}) + +test_that("get_parameters_raw handles HTTP 500 server error", { + mock_resp <- structure( + list(status_code = 500), + class = "httr2_response" + ) + + mockery::stub(get_parameters_raw, "httr2::req_perform", mock_resp) + mockery::stub(get_parameters_raw, "httr2::resp_status", 500) + mockery::stub( + get_parameters_raw, + "httr2::resp_status_desc", + "Internal Server Error" + ) + + expect_message( + result <- get_parameters_raw( + parameter = "hs", + node = 42, + start = as.POSIXct("1994-01-01 00:00:00", tz = "UTC"), + end = as.POSIXct("1994-01-02 00:00:00", tz = "UTC") + ), + "HTTP error 500" + ) + + expect_null(result) +}) + +test_that("get_parameters handles network connection failure gracefully", { + # Mock the internal function to simulate network failure + mockery::stub( + get_parameters, + "get_parameters_raw", + NULL # Simulates what get_parameters_raw returns on network failure + ) + + expect_message( + result <- get_parameters( + parameters = "hs", + node = 42, + start = as.POSIXct("1994-01-01 00:00:00", tz = "UTC"), + end = as.POSIXct("1994-01-02 00:00:00", tz = "UTC") + ), + "Failed to retrieve parameter: hs" + ) + + expect_null(result) +}) + +test_that("get_parameters_raw handles API-level error in response", { + mock_resp <- structure( + list(status_code = 200), + class = "httr2_response" + ) + + mock_json <- list( + errorcode = 1, + errormessage = "Invalid node parameter" + ) + + mockery::stub(get_parameters_raw, "httr2::req_perform", mock_resp) + mockery::stub(get_parameters_raw, "httr2::resp_status", 200) + mockery::stub(get_parameters_raw, "httr2::resp_body_json", mock_json) + + expect_message( + result <- get_parameters_raw( + parameter = "hs", + node = 42, + start = as.POSIXct("1994-01-01 00:00:00", tz = "UTC"), + end = as.POSIXct("1994-01-02 00:00:00", tz = "UTC") + ), + "Invalid node parameter" + ) + + expect_null(result) +}) + +test_that("get_parameters handles failure in get_parameters_raw for single parameter", { + # Mock get_parameters_raw to return NULL (simulating any failure) + mockery::stub(get_parameters, "get_parameters_raw", NULL) + + expect_message( + result <- get_parameters( + parameters = "hs", + node = 42, + start = as.POSIXct("1994-01-01 00:00:00", tz = "UTC"), + end = as.POSIXct("1994-01-02 00:00:00", tz = "UTC") + ), + "Failed to retrieve parameter: hs" + ) + + expect_null(result) +}) + +test_that("get_parameters handles partial failure with multiple parameters", { + # First call succeeds, second call fails + mock_success <- tibble::tibble( + time = as.POSIXct("1994-01-01 00:00:00", tz = "UTC"), + hs = 1.5 + ) + + mockery::stub( + get_parameters, + "get_parameters_raw", + mockery::mock(mock_success, NULL, cycle = TRUE) + ) + + expect_message( + result <- get_parameters( + parameters = c("hs", "tp"), + node = 42, + start = as.POSIXct("1994-01-01 00:00:00", tz = "UTC"), + end = as.POSIXct("1994-01-02 00:00:00", tz = "UTC") + ), + "Failed to retrieve parameter: tp" + ) + + expect_null(result) +}) + + +test_that("Errors in 'get_parameters()' are handled correcly", { + expect_error( + get_parameters("tépé"), + "Requested parameters do not exists in the database: tépé" + ) + expect_error( + get_parameters(node = 0), + "The requested location do no exist in the database." + ) + expect_error( + get_parameters(node = c(10, 100)), + "The function can retreive only one location a time." + ) + expect_error( + get_parameters(start = 1), + paste0( + "'start' is outside the covered period: ", + paste( + format( + c( + resourcecode:::rscd_casandra_start_date, + resourcecode:::rscd_casandra_end_date + ), + format = "%Y-%m-%d %H:%M %Z" + ), + collapse = " \u2014 " + ) + ) + ) + expect_error( + get_parameters(end = 1e10), + paste0( + "'end' is outside the covered period: ", + paste( + format( + c( + resourcecode:::rscd_casandra_start_date, + resourcecode:::rscd_casandra_end_date + ), + format = "%Y-%m-%d %H:%M %Z" + ), + collapse = " \u2014 " + ) + ) + ) + expect_error( + get_parameters( + start = "1994-01-31 01:00:00", + end = "1994-01-11 01:00:00" + ), + "'end' must be after 'start'" + ) +}) + +test_that("get_parameters accepts dates at exact boundaries", { + vcr::local_cassette("boundary_dates") + + expect_no_error( + get_parameters( + parameters = "hs", + node = 42, + start = resourcecode:::rscd_casandra_start_date, + end = resourcecode:::rscd_casandra_start_date + 3600 # 1 hour later + ) + ) +}) + +test_that("tp parameter conversion handles edge cases", { + vcr::local_cassette("tp_conversion") + + result <- get_parameters( + parameters = "tp", + node = 42, + start = as.POSIXct("1994-01-01 00:00:00", tz = "UTC"), + end = as.POSIXct("1994-01-02 00:00:00", tz = "UTC") + ) + + # All tp values should be positive and finite + expect_true(all(is.finite(result$tp))) + expect_true(all(result$tp > 0)) +}) diff --git a/vignettes/resourcecode.Rmd b/vignettes/resourcecode.Rmd index 69e9f4b..87bc1cb 100644 --- a/vignettes/resourcecode.Rmd +++ b/vignettes/resourcecode.Rmd @@ -117,17 +117,19 @@ point_of_interest <- c(longitude = -4.6861533, latitude = 48.3026514) node <- closest_point_field(point_of_interest) node ts <- get_parameters(node = node$point, parameters = c("hs", "tp", "dp", "cge")) -ggplot(tidyr::pivot_longer(ts, -1), aes(x = time, y = value, col = name)) + - geom_line() + - coord_cartesian(expand = FALSE) + - facet_wrap(~name, ncol = 2, scales = "free_y") + - scale_x_datetime(name = NULL, date_breaks = "month") + - scale_y_continuous(name = NULL) + - theme_minimal() + - theme( - legend.position = "none", - axis.text.x = element_text(angle = 60, hjust = 1) - ) +if (!is.null(ts)) { + ggplot(tidyr::pivot_longer(ts, -1), aes(x = time, y = value, col = name)) + + geom_line() + + coord_cartesian(expand = FALSE) + + facet_wrap(~name, ncol = 2, scales = "free_y") + + scale_x_datetime(name = NULL, date_breaks = "month") + + scale_y_continuous(name = NULL) + + theme_minimal() + + theme( + legend.position = "none", + axis.text.x = element_text(angle = 60, hjust = 1) + ) +} ``` - 1D and 2D spectra of the SPEC grid can be downloaded directly from IFREMER FTP using functions `get_1Dspectrum()` and `get_2Dspectrum()`. We also provide a plotting function for the 2D spectrum. @@ -153,7 +155,7 @@ str(spec_2d) In addition, it is possible to plot the wave elevation directional spectra for any given time, which can be specified by the time index or directly the date: ```{r,warning=FALSE, fig.height=8,fig.width=8} -plot_2d_specta(spec_2d, "1994-01-15 18:00") +if (!is.null(spec_2d)) plot_2d_specta(spec_2d, "1994-01-15 18:00") ```