From 6b0cd0be99ef1c47d9df65da4c0f162bf7538c64 Mon Sep 17 00:00:00 2001 From: meetagrawal09 Date: Wed, 7 Jun 2023 18:18:58 +0530 Subject: [PATCH 01/32] tested take.samples, append.covariate, filter_sunleaf_traits --- .../tests/testthat/test.covariate.functions.R | 32 +++++++++++++++++++ base/db/tests/testthat/test.take.samples.R | 10 ++++++ 2 files changed, 42 insertions(+) create mode 100644 base/db/tests/testthat/test.covariate.functions.R create mode 100644 base/db/tests/testthat/test.take.samples.R diff --git a/base/db/tests/testthat/test.covariate.functions.R b/base/db/tests/testthat/test.covariate.functions.R new file mode 100644 index 00000000000..27fccd4eabe --- /dev/null +++ b/base/db/tests/testthat/test.covariate.functions.R @@ -0,0 +1,32 @@ +test_that("`append.covariate` able to append new column for covariates in given data based on id", { + data <- data.frame( + id = c(1, 2, 3, 4), + name = c("a", "b", "c", "d") + ) + covariates.data <- data.frame( + trait_id = c( 1, 2, 3, 4, 4), + level = c("A", "B", "C", "D", "E"), + name = c("a", "b", "c", "d", "e") + ) + updated_data <- append.covariate(data, "new_covariates_col", covariates.data) + expect_equal(updated_data$new_covariates_col, c("A", "B", "C", "D")) + expect_equal(colnames(updated_data), c("id", "new_covariates_col", "name")) +}) + +test_that("`filter_sunleaf_traits`able to filter out upper canopy leaves", { + data <- data.frame( + id = c(1, 2, 3, 4), + name = c("a", "b", "c", "d") + ) + covariates <- data.frame( + trait_id = c(1, 2, 3, 4), + name = c("leaf", "canopy_layer", "canopy_layer", "sunlight"), + level = c(1.2, 0.5, 0.7, 0.67) + ) + + updated_data <- filter_sunleaf_traits(data, covariates) + expect_equal(updated_data$name, c("a", "c", "d")) + + # temporary column gets removed + expect_equal(colnames(updated_data), c("id", "name")) +}) \ No newline at end of file diff --git a/base/db/tests/testthat/test.take.samples.R b/base/db/tests/testthat/test.take.samples.R new file mode 100644 index 00000000000..738bd6fab61 --- /dev/null +++ b/base/db/tests/testthat/test.take.samples.R @@ -0,0 +1,10 @@ +test_that("`take.samples` returns mean when stat is NA", { + summary = list(mean = 10, stat = NA) + expect_equal(take.samples(summary = summary), summary$mean) +}) + +test_that("`take.samples` returns a vector of length sample.size for given summary stats", { + summary = list(mean = 10, stat = 10) + sample.size = 10 + expect_equal(length(take.samples(summary = summary, sample.size = sample.size)), sample.size) +}) \ No newline at end of file From 2f99ed472304bd3a34d0fcb3991e45a2bb744d0a Mon Sep 17 00:00:00 2001 From: meetagrawal09 Date: Wed, 7 Jun 2023 21:04:58 +0530 Subject: [PATCH 02/32] moved around tests --- base/db/tests/testthat/test.derive.traits.R | 7 ------- base/db/tests/testthat/test.take.samples.R | 5 +++++ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/base/db/tests/testthat/test.derive.traits.R b/base/db/tests/testthat/test.derive.traits.R index ae4b4ba93c9..4ba88caf3ed 100644 --- a/base/db/tests/testthat/test.derive.traits.R +++ b/base/db/tests/testthat/test.derive.traits.R @@ -6,13 +6,6 @@ # which accompanies this distribution, and is available at # http://opensource.ncsa.illinois.edu/license.html #------------------------------------------------------------------------------- -test_that("take.samples works",{ - expect_equal(take.samples(summary = data.frame(mean = 1, stat = NA)), 1) - set.seed(0) - test.sample <- take.samples(summary = data.frame(mean = 1, stat = 1), - sample.size = 2) - expect_equal(test.sample, c(2.26295428488079, 0.673766639294351)) -}) test_that("derive.traits works",{ set.seed(0) diff --git a/base/db/tests/testthat/test.take.samples.R b/base/db/tests/testthat/test.take.samples.R index 738bd6fab61..dee2e479b44 100644 --- a/base/db/tests/testthat/test.take.samples.R +++ b/base/db/tests/testthat/test.take.samples.R @@ -7,4 +7,9 @@ test_that("`take.samples` returns a vector of length sample.size for given summa summary = list(mean = 10, stat = 10) sample.size = 10 expect_equal(length(take.samples(summary = summary, sample.size = sample.size)), sample.size) + + # Testing for exact return values for a simple example + test.sample <- take.samples(summary = data.frame(mean = 1, stat = 1), + sample.size = 2) + expect_equal(test.sample, c(2.26295428488079, 0.673766639294351)) }) \ No newline at end of file From b1a3b01a8aacd1b06cfe2b0e7b33292397f0c49d Mon Sep 17 00:00:00 2001 From: meetagrawal09 Date: Thu, 8 Jun 2023 11:21:11 +0530 Subject: [PATCH 03/32] tested check.lists, assign.treatments, drop.columns --- base/db/R/assign.treatments.R | 2 +- base/db/man/assign.treatments.Rd | 2 +- .../tests/testthat/test.assign.treatments.R | 33 +++++++++++++++++++ base/db/tests/testthat/test.check.lists.R | 23 +++++++++++++ 4 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 base/db/tests/testthat/test.assign.treatments.R create mode 100644 base/db/tests/testthat/test.check.lists.R diff --git a/base/db/R/assign.treatments.R b/base/db/R/assign.treatments.R index 5e22285e837..d75f6b20b3a 100644 --- a/base/db/R/assign.treatments.R +++ b/base/db/R/assign.treatments.R @@ -13,7 +13,7 @@ ##' Assigns all control treatments the same value, then assigns unique treatments ##' within each site. Each site is required to have a control treatment. ##' The algorithm (incorrectly) assumes that each site has a unique set of experimental -##' treatments. This assumption is required by the data in BETTdb that does not always consistently name treatments or quantity them in the managements table. Also it avoids having the need to estimate treatment by site interactions in the meta analysis model. This model uses data in the control treatment to estimate model parameters so the impact of the assumption is minimal. +##' treatments. This assumption is required by the data in BETYdb that does not always consistently name treatments or quantity them in the managements table. Also it avoids having the need to estimate treatment by site interactions in the meta analysis model. This model uses data in the control treatment to estimate model parameters so the impact of the assumption is minimal. ##' @name assign.treatments ##' @title assign.treatments ##' @param data input data diff --git a/base/db/man/assign.treatments.Rd b/base/db/man/assign.treatments.Rd index 30365dc8468..c1b6509217d 100644 --- a/base/db/man/assign.treatments.Rd +++ b/base/db/man/assign.treatments.Rd @@ -19,7 +19,7 @@ Change treatments to sequential integers Assigns all control treatments the same value, then assigns unique treatments within each site. Each site is required to have a control treatment. The algorithm (incorrectly) assumes that each site has a unique set of experimental -treatments. This assumption is required by the data in BETTdb that does not always consistently name treatments or quantity them in the managements table. Also it avoids having the need to estimate treatment by site interactions in the meta analysis model. This model uses data in the control treatment to estimate model parameters so the impact of the assumption is minimal. +treatments. This assumption is required by the data in BETYdb that does not always consistently name treatments or quantity them in the managements table. Also it avoids having the need to estimate treatment by site interactions in the meta analysis model. This model uses data in the control treatment to estimate model parameters so the impact of the assumption is minimal. } \author{ David LeBauer, Carl Davidson, Alexey Shiklomanov diff --git a/base/db/tests/testthat/test.assign.treatments.R b/base/db/tests/testthat/test.assign.treatments.R new file mode 100644 index 00000000000..a51c40f4804 --- /dev/null +++ b/base/db/tests/testthat/test.assign.treatments.R @@ -0,0 +1,33 @@ +test_that("`assign.treatments` correctly assigns control treatment", { + data <- data.frame( + site_id = c(1, 1, 2, 2, 3, 3), + citation_id = c(101, 101, 201, 201, 301, 301), + control = c(1, 0, 0, 1, 0, 0), + trt_id = NA + ) + + updated_data <- assign.treatments(data) + expect_equal(updated_data$trt_id, c("control", NA, NA, "control", "control", "control")) +}) + +test_that("`assign.treatments` gives an error if no control treatment is set for a site", { + data <- data.frame( + site_id = c(1, 1, 2, 2, 3, 3), + citation_id = c(101, 101, 201, 201, 301, 301), + control = c(0, 0, 0, 1, 0, 0), + trt_id = c(NA, NA, NA, NA, "not_control", NA) + ) + + expect_error(assign.treatments(data), "No control treatment set") +}) + +test_that("`drop.columns` able to drop specified columns from data", { + data <- data.frame( + id = c(1, 2, 3), + name = c("a", "b", "c"), + value = c(1.2, 4.5, 6.7) + ) + + updated_data <- drop.columns(data, c("name", "not_a_column")) + expect_equal(colnames(updated_data), c("id", "value")) +}) \ No newline at end of file diff --git a/base/db/tests/testthat/test.check.lists.R b/base/db/tests/testthat/test.check.lists.R new file mode 100644 index 00000000000..e434c29a2e7 --- /dev/null +++ b/base/db/tests/testthat/test.check.lists.R @@ -0,0 +1,23 @@ +test_that("`check.lists` returns false for appropriate cases", { + x <- data.frame(id = c(1, 2, 3)) + y <- data.frame(id = c(1, 2, 3, 4)) + + # for unequal number of rows + expect_false(check.lists(x, y)) + + # for wrong filename passed + expect_false(check.lists(x, y, filename = "wrong.csv")) + + # if x and y are actually unequal + y <- data.frame(id = c(1, 2, 4)) + expect_false(check.lists(x, y, filename = "species.csv")) +}) + +test_that("`check.lists` able to correctly work for matching data frames to lists read from csv files", { + withr::with_tempfile("tf", fileext = ".csv",{ + x <- data.frame(id = c(1, 2, 3)) + y <- data.frame(id = c(1, 2, 3)) + write.csv(y, file = tf) + expect_true(check.lists(x, read.csv(tf), filename = "species.csv")) + }) +}) \ No newline at end of file From 73698d50078f96af4562388f71428e2a7a2036dd Mon Sep 17 00:00:00 2001 From: meetagrawal09 Date: Sat, 10 Jun 2023 22:28:29 +0530 Subject: [PATCH 04/32] tested stamp_started, stamp_finished --- base/db/DESCRIPTION | 4 +++- base/db/tests/testthat/test.stamp.R | 15 +++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 base/db/tests/testthat/test.stamp.R diff --git a/base/db/DESCRIPTION b/base/db/DESCRIPTION index 0e302137980..37172d20f3d 100644 --- a/base/db/DESCRIPTION +++ b/base/db/DESCRIPTION @@ -66,13 +66,15 @@ Suggests: data.table, here, knitr, + mockery, RPostgreSQL, RPostgres, RSQLite, rcrossref, rmarkdown (>= 2.19), testthat (>= 2.0.0), - tidyverse + tidyverse, + withr X-Comment-Remotes: Installing markdown from GitHub because as of 2023-02-05, this is the easiest way to get version >= 2.19 onto Docker images that use older diff --git a/base/db/tests/testthat/test.stamp.R b/base/db/tests/testthat/test.stamp.R new file mode 100644 index 00000000000..c566e073ee4 --- /dev/null +++ b/base/db/tests/testthat/test.stamp.R @@ -0,0 +1,15 @@ +test_that("`stamp_started()` able to correctly update the query for run_id passed", { + mock_function <- mockery::mock() + mockery::stub(stamp_started, 'PEcAn.DB::db.query', mock_function) + stamp_started(1, 1) + args <- mockery::mock_args(mock_function) + expect_true(grepl("started_at .* WHERE id = 1", args[[1]]$query)) +}) + +test_that("`stamp_finished()` able to correctly update the query for run_id passed", { + mock_function <- mockery::mock() + mockery::stub(stamp_finished, 'PEcAn.DB::db.query', mock_function) + stamp_finished(1, 1) + args <- mockery::mock_args(mock_function) + expect_true(grepl("finished_at .* WHERE id = 1", args[[1]]$query)) +}) \ No newline at end of file From 27e06e3ae5b038114e2d038544b1d3c52b62e3b9 Mon Sep 17 00:00:00 2001 From: meetagrawal09 Date: Sun, 11 Jun 2023 23:34:02 +0530 Subject: [PATCH 05/32] query.priors, query.site --- base/db/tests/testthat/test.query.priors.R | 13 +++++++++++++ base/db/tests/testthat/test.query.site.R | 14 ++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 base/db/tests/testthat/test.query.priors.R create mode 100644 base/db/tests/testthat/test.query.site.R diff --git a/base/db/tests/testthat/test.query.priors.R b/base/db/tests/testthat/test.query.priors.R new file mode 100644 index 00000000000..4f0311a9e8f --- /dev/null +++ b/base/db/tests/testthat/test.query.priors.R @@ -0,0 +1,13 @@ +test_that("`query.priors()` correctly forms the query based on the parameters passed and returns priors",{ + mocked_function <- mockery::mock(data.frame(name = c("A", "B"), value = c(0.1, 0.2))) + mockery::stub(query.priors, 'db.query', mocked_function) + priors <- query.priors("ebifarm.pavi", c("SLA"), con = 1) + expect_equal(priors, c(0.1, 0.2)) + args <- mockery::mock_args(mocked_function) + expect_true( + grepl( + "WHERE pfts.id = ebifarm.pavi AND variables.name IN .* SLA", + args[[1]]$query + ) + ) +}) \ No newline at end of file diff --git a/base/db/tests/testthat/test.query.site.R b/base/db/tests/testthat/test.query.site.R new file mode 100644 index 00000000000..ec61c210581 --- /dev/null +++ b/base/db/tests/testthat/test.query.site.R @@ -0,0 +1,14 @@ +test_that("`query.site()` correctly forms the query and returns the site", { + mock_site_data <- data.frame(id = c(1), lon = c(1), lat = c(1)) + mocked_function <- mockery::mock(mock_site_data) + mockery::stub(query.site, 'db.query', mocked_function) + site <- query.site(1, con = 1) + expect_equal(site, mock_site_data) + args <- mockery::mock_args(mocked_function) + expect_true( + grepl( + "WHERE id = 1", + args[[1]]$query + ) + ) +}) \ No newline at end of file From a969778a9cd19da3d17fdd8258d8e363886fa4b8 Mon Sep 17 00:00:00 2001 From: meetagrawal09 Date: Tue, 13 Jun 2023 17:38:58 +0530 Subject: [PATCH 06/32] tested:query.data, query.yields & bug fix:query.yields --- base/db/R/query.yields.R | 11 +++++- base/db/tests/testthat/test.query.data.R | 20 ++++++++++ base/db/tests/testthat/test.query.yields.R | 43 ++++++++++++++++++++++ 3 files changed, 72 insertions(+), 2 deletions(-) create mode 100644 base/db/tests/testthat/test.query.data.R create mode 100644 base/db/tests/testthat/test.query.yields.R diff --git a/base/db/R/query.yields.R b/base/db/R/query.yields.R index d28c8bf10dd..416ba462420 100644 --- a/base/db/R/query.yields.R +++ b/base/db/R/query.yields.R @@ -24,13 +24,20 @@ query.yields <- function(trait = 'yield', spstr, extra.columns = '', con = NULL, ids_are_cultivars = FALSE, ...){ member_column <- if (ids_are_cultivars) {"cultivar_id"} else {"specie_id"} + + if(!is.null(extra.columns)) { + if(!is.character(extra.columns) || length(extra.columns) != 1) { + PEcAn.logger::logger.severe("`extra.columns` must be a string") + } + } + query <- paste("select yields.id, yields.citation_id, yields.site_id, treatments.name, yields.date, yields.time, yields.cultivar_id, yields.specie_id, yields.mean, yields.statname, yields.stat, yields.n, variables.name as vname, month(yields.date) as month,", - extra.columns, + if(extra.columns != '') { paste(extra.columns, ",", sep = "") } else {""}, "treatments.control, sites.greenhouse from yields left join treatments on (yields.treatment_id = treatments.id) @@ -38,7 +45,7 @@ query.yields <- function(trait = 'yield', spstr, extra.columns = '', con = NULL, left join variables on (yields.variable_id = variables.id) where ", member_column, " in (", spstr,");", sep = "") if(!trait == 'yield'){ - query <- gsub(");", paste(" and variables.name in ('", trait,"');", sep = ""), query) + query <- gsub(");", paste(") and variables.name in ('", trait,"');", sep = ""), query) } return(fetch.stats2se(connection = con, query = query)) diff --git a/base/db/tests/testthat/test.query.data.R b/base/db/tests/testthat/test.query.data.R new file mode 100644 index 00000000000..87cafba5787 --- /dev/null +++ b/base/db/tests/testthat/test.query.data.R @@ -0,0 +1,20 @@ +test_that("`query.data()` able to correctly form the query and return result in SE", { + mocked_function <- mockery::mock(data.frame(Y=rep(1,5), stat=rep(1,5), n=rep(4,5), mean = rep(3,5), statname=c('SD', 'MSE', 'LSD', 'HSD', 'MSD'))) + mockery::stub(query.data, 'db.query', mocked_function, 2) + result <- query.data(con = 1, trait = "test_trait", spstr = "test_spstr", store.unconverted = TRUE) + args <- mockery::mock_args(mocked_function) + expect_true( + grepl( + paste( + "ST_X\\(ST_CENTROID\\(sites\\.geometry\\)\\) AS lon,", + "ST_Y\\(ST_CENTROID\\(sites\\.geometry\\)\\) AS lat,.*", + "where specie_id in \\(test_spstr\\).*", + "variables.name in \\('test_trait'\\);" + ), + args[[1]]$query + ) + ) + expect_equal(result$mean_unconverted, result$mean) + expect_equal(result$stat_unconverted, result$stat) + expect_equal(result$statname, rep('SE', 5)) +}) \ No newline at end of file diff --git a/base/db/tests/testthat/test.query.yields.R b/base/db/tests/testthat/test.query.yields.R new file mode 100644 index 00000000000..c8a7905d51f --- /dev/null +++ b/base/db/tests/testthat/test.query.yields.R @@ -0,0 +1,43 @@ +test_that("`query.yields()` able to form the query correctly for trait set to 'yield' and with no extra columns", { + mocked_function <- mockery::mock(data.frame(Y=rep(1,5), stat=rep(1,5), n=rep(4,5), mean = rep(3,5), statname=c('SD', 'MSE', 'LSD', 'HSD', 'MSD'))) + mockery::stub(query.yields, 'db.query', mocked_function, 2) + result <- query.yields(spstr = "test_spstr", con = 1) + + args <- mockery::mock_args(mocked_function) + expect_true( + grepl( + paste0( + "month\\(yields.date\\) as month,treatments.control.*", + "where specie_id in \\(test_spstr\\);" + ), + args[[1]]$query + ) + ) +}) + +test_that("`query.yields()` throws an error if extra columns is not a string", { + expect_error( + query.yields(spstr = "test_spstr", con = 1, extra.columns = 1), + "`extra.columns` must be a string" + ) + expect_error( + query.yields(spstr = "test_spstr", con = 1, extra.columns = c("a","b")), + "`extra.columns` must be a string" + ) +}) + +test_that("`query.yields()` able to form the query correctly for trait not equal to 'yield' and with extra columns",{ + mocked_function <- mockery::mock(data.frame(Y=rep(1,5), stat=rep(1,5), n=rep(4,5), mean = rep(3,5), statname=c('SD', 'MSE', 'LSD', 'HSD', 'MSD'))) + mockery::stub(query.yields, 'db.query', mocked_function, 2) + result <- query.yields(trait = 'test_trait', spstr = "test_spstr", extra.columns = 'test_col', con = 1) + args <- mockery::mock_args(mocked_function) + expect_true( + grepl( + paste0( + "month\\(yields.date\\) as month,test_col,treatments.control.*", + "where specie_id in \\(test_spstr\\) and variables.name in \\('test_trait'\\)" + ), + args[[1]]$query + ) + ) +}) \ No newline at end of file From 1c435c5507cafe6ce45914b42e176e24a55cf5a5 Mon Sep 17 00:00:00 2001 From: meetagrawal09 Date: Thu, 15 Jun 2023 20:45:29 +0530 Subject: [PATCH 07/32] tested query.dplyr --- base/db/R/query.dplyr.R | 4 +- base/db/R/query.yields.R | 2 +- base/db/tests/testthat/test.query.dplyr.R | 77 +++++++++++++++++++++++ 3 files changed, 80 insertions(+), 3 deletions(-) create mode 100644 base/db/tests/testthat/test.query.dplyr.R diff --git a/base/db/R/query.dplyr.R b/base/db/R/query.dplyr.R index b0501a9abcb..d1f0a1a401a 100644 --- a/base/db/R/query.dplyr.R +++ b/base/db/R/query.dplyr.R @@ -61,7 +61,7 @@ dplyr.count <- function(df) { #' @param unit string containing CF-style time unit including origin (e.g. "days since 2010-01-01") #' @export ncdays2date <- function(time, unit) { - date <- lubridate::parse_date_time(unit, c("ymd_hms", "ymd_h", "ymd")) + date <- lubridate::parse_date_time(unit, c("ymd_HMS", "ymd_H", "ymd")) days <- PEcAn.utils::ud_convert(time, unit, paste("days since ", date)) seconds <- PEcAn.utils::ud_convert(days, "days", "seconds") return(as.POSIXct.numeric(seconds, origin = date, tz = "UTC")) @@ -124,7 +124,7 @@ workflows <- function(bety, ensemble = FALSE) { #' @export workflow <- function(bety, workflow_id) { workflows(bety) %>% - dplyr::filter(.data$workflow_id == !!.data$workflow_id) + dplyr::filter(.data$workflow_id == !!workflow_id) } # workflow diff --git a/base/db/R/query.yields.R b/base/db/R/query.yields.R index 416ba462420..4f07ef01b74 100644 --- a/base/db/R/query.yields.R +++ b/base/db/R/query.yields.R @@ -45,7 +45,7 @@ query.yields <- function(trait = 'yield', spstr, extra.columns = '', con = NULL, left join variables on (yields.variable_id = variables.id) where ", member_column, " in (", spstr,");", sep = "") if(!trait == 'yield'){ - query <- gsub(");", paste(") and variables.name in ('", trait,"');", sep = ""), query) + query <- gsub(";", paste(" and variables.name in ('", trait,"');", sep = ""), query) } return(fetch.stats2se(connection = con, query = query)) diff --git a/base/db/tests/testthat/test.query.dplyr.R b/base/db/tests/testthat/test.query.dplyr.R new file mode 100644 index 00000000000..52e15f959b4 --- /dev/null +++ b/base/db/tests/testthat/test.query.dplyr.R @@ -0,0 +1,77 @@ +test_that("`fancy_scientific()` converts numbers to scientific expressions with proper formatting", { + result <- fancy_scientific(1234567890) + expect_equal(result, expression("1.234568" %*% 10^+9)) + + result <- fancy_scientific(0.00000123) + expect_equal(result, expression("1.23" %*% 10^-6)) + + result <- fancy_scientific(1e-20) + expect_equal(result, expression("1" %*% 10^-20)) +}) + +test_that("`dplyr.count()` returns the correct count of rows in a dataframe", { + + df <- data.frame( + x = c(1, 2, 3, 2, 1, 3), + y = c("a", "b", "a", "b", "a", "b") + ) + result <- dplyr.count(df) + expect_equal(result, 6) + + df <- data.frame() + result <- dplyr.count(df) + expect_equal(result, 0) +}) + +test_that("`dbHostInfo()` able to return correct host information", { + mockery::stub(dbHostInfo, 'db.query', data.frame(floor = 10)) + mockery::stub( + dbHostInfo, + 'dplyr::tbl', + data.frame( + data.frame( + sync_host_id = c(10, 11), + hostname = c("test_host_1", "test_host_2"), + sync_start = c("20190201", "20190201"), + sync_end = c("20200101", "20200101"), + sync_url = c("http://test_url_1", "http://test_url_2"), + sync_contact = c("test_contact_1", "test_contact_2") + ) + ) + ) + result <- dbHostInfo(bety = 1) + expect_equal(result$hostid, 10) + expect_equal(result$hostname, "test_host_1") + expect_equal(result$start, "20190201") + expect_equal(result$end, "20200101") + expect_equal(result$sync_url, "http://test_url_1") + expect_equal(result$sync_contact, "test_contact_1") +}) + +test_that("`workflows()` able to correctly return a list of workflows", { + mockery::stub( + workflows, + 'dbHostInfo', + list( + hostid = 10, + hostname = "test_host_1", + start = 3, + end = 10, + sync_url = "http://test_url_1", + sync_contact = "test_contact_1" + ) + ) + mockery::stub(workflows, 'dplyr::tbl', data.frame(workflow_id = c(1, 2, 3, 4, 5, 6))) + result <- workflows(bety = 1, ensemble = TRUE) + expect_equal(result, data.frame(workflow_id = c(3, 4, 5, 6))) +}) + +test_that("`workflow()` able to get a workflow data by id", { + mockery::stub( + workflow, + 'workflows', + data.frame(workflow_id = c(1, 2, 3, 4, 5, 6), workflow_name = c("A", "B", "C", "D", "E", "F")) + ) + result <- workflow(bety = 1, workflow_id = 3) + expect_equal(result, data.frame(workflow_id = 3, workflow_name = "C")) +}) \ No newline at end of file From eadfb184b2b3615439c80f659ad82eb74a09ca4a Mon Sep 17 00:00:00 2001 From: meetagrawal09 Date: Thu, 15 Jun 2023 23:04:20 +0530 Subject: [PATCH 08/32] updated mockery version --- base/db/DESCRIPTION | 3 ++- docker/depends/pecan.depends.R | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/base/db/DESCRIPTION b/base/db/DESCRIPTION index 37172d20f3d..e8d4f273b84 100644 --- a/base/db/DESCRIPTION +++ b/base/db/DESCRIPTION @@ -82,7 +82,8 @@ X-Comment-Remotes: When building on a system that finds a new enough version on CRAN, OK to remove the Remotes line and this comment. Remotes: - github::rstudio/rmarkdown@v2.20 + github::rstudio/rmarkdown@v2.20, + github::r-lib/mockery@v0.4.3 License: BSD_3_clause + file LICENSE VignetteBuilder: knitr Copyright: Authors diff --git a/docker/depends/pecan.depends.R b/docker/depends/pecan.depends.R index 7b8986b2117..a6349e617eb 100644 --- a/docker/depends/pecan.depends.R +++ b/docker/depends/pecan.depends.R @@ -13,6 +13,7 @@ remotes::install_github(c( 'chuhousen/amerifluxr', 'ebimodeling/biocro@0.951', 'MikkoPeltoniemi/Rpreles', +'r-lib/mockery@v0.4.3' 'r-lib/testthat@v3.1.6', 'r-lib/vdiffr@v1.0.4', 'ropensci/geonames', From 487cbb4d6d8e6ebe4984271fdf6a0ce9e3b15a40 Mon Sep 17 00:00:00 2001 From: meetagrawal09 Date: Thu, 15 Jun 2023 23:06:22 +0530 Subject: [PATCH 09/32] typo fix --- docker/depends/pecan.depends.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/depends/pecan.depends.R b/docker/depends/pecan.depends.R index a6349e617eb..68986a3895c 100644 --- a/docker/depends/pecan.depends.R +++ b/docker/depends/pecan.depends.R @@ -13,7 +13,7 @@ remotes::install_github(c( 'chuhousen/amerifluxr', 'ebimodeling/biocro@0.951', 'MikkoPeltoniemi/Rpreles', -'r-lib/mockery@v0.4.3' +'r-lib/mockery@v0.4.3', 'r-lib/testthat@v3.1.6', 'r-lib/vdiffr@v1.0.4', 'ropensci/geonames', From 3f95e7ddd127f37ff139e652b18cea29d031ccd7 Mon Sep 17 00:00:00 2001 From: meetagrawal09 Date: Sat, 24 Jun 2023 10:22:21 +0530 Subject: [PATCH 10/32] tested runs, get_workflow_ids, get_users, get_run_ids --- base/db/tests/testthat/test.query.dplyr.R | 73 ++++++++++++++++++++++- 1 file changed, 72 insertions(+), 1 deletion(-) diff --git a/base/db/tests/testthat/test.query.dplyr.R b/base/db/tests/testthat/test.query.dplyr.R index 52e15f959b4..8be24e8f555 100644 --- a/base/db/tests/testthat/test.query.dplyr.R +++ b/base/db/tests/testthat/test.query.dplyr.R @@ -74,4 +74,75 @@ test_that("`workflow()` able to get a workflow data by id", { ) result <- workflow(bety = 1, workflow_id = 3) expect_equal(result, data.frame(workflow_id = 3, workflow_name = "C")) -}) \ No newline at end of file +}) + +test_that("`runs()` is able to get table of runs for a corresponding workflow", { + mockery::stub( + runs, + 'workflow', + data.frame( + workflow_id = c(1, 1), + folder = c("test_folder_1", "test_folder_2") + ) + ) + mocked_res <- mockery::mock( + data.frame( + id = c(1, 2, 3, 4, 5, 6), + workflow_id = c(1, 1, 3, 4, 5, 6) + ), + data.frame( + id = c(1, 2, 3), + ensemble_id = c(1, 1, 2) + ) + ) + mockery::stub(runs, 'dplyr::tbl', mocked_res) + result <- runs(bety = 1, workflow_id = 1) + expect_equal(result$run_id, c(1, 1, 2, 2, 3, 3)) + expect_equal(result$folder, c("test_folder_1", "test_folder_2", "test_folder_1", "test_folder_2", "test_folder_1", "test_folder_2")) +}) + +test_that("`get_workflow_ids()` able to get a vector of unique workflow IDs", { + mockery::stub( + get_workflow_ids, + 'workflows', + data.frame( + workflow_id = c(1, 2, 2, 3, 4, 4), + workflow_name = c("A", "B", "C", "D", "E", "F") + ) + ) + result <- get_workflow_ids(bety = 1, query = 1, all.ids = TRUE) + expect_equal(result, c(4, 3, 2, 1)) +}) + +test_that("`get_users()` ", { + mockery::stub(get_users, 'dplyr::tbl', data.frame(id = c(20200101, 20200102, 20240103))) + mockery::stub( + get_users, + 'dbHostInfo', + data.frame( + start = 20190201, + end = 20230101 + ) + ) + result <- get_users(bety = 1) + expect_equal(result, data.frame(id = c(20200101, 20200102))) +}) + +test_that("`get_run_ids()` able to get vector of run ids (in sorted order) for a given workflow ID", { + mockery::stub( + get_run_ids, + 'runs', + data.frame( + run_id = c(3, 1, 2), + folder = c("test_folder_1", "test_folder_2", "test_folder_3") + ) + ) + + result <- get_run_ids(bety = 1, workflow_id = 1) + expect_equal(result, c(1, 2, 3)) + + # if no run ids are found + mockery::stub(get_run_ids, 'runs', data.frame()) + result <- get_run_ids(bety = 1, workflow_id = 1) + expect_equal(result, c("No runs found")) +}) From 11152e6986c999bf1d8ad2bc4b58c5e9d583649e Mon Sep 17 00:00:00 2001 From: meetagrawal09 Date: Sat, 1 Jul 2023 19:08:58 +0530 Subject: [PATCH 11/32] query.file.path, utils_db --- base/db/R/dbfiles.R | 12 ++++----- base/db/R/query.file.path.R | 4 +-- base/db/tests/testthat/test.insert.R | 8 ++++++ base/db/tests/testthat/test.query.dplyr.R | 6 +++++ base/db/tests/testthat/test.query.file.path.R | 21 +++++++++++++++ base/db/tests/testthat/test.utils_db.R | 26 +++++++++++++++++++ 6 files changed, 69 insertions(+), 8 deletions(-) create mode 100644 base/db/tests/testthat/test.query.file.path.R create mode 100644 base/db/tests/testthat/test.utils_db.R diff --git a/base/db/R/dbfiles.R b/base/db/R/dbfiles.R index 28509daef8f..e0f9556779b 100644 --- a/base/db/R/dbfiles.R +++ b/base/db/R/dbfiles.R @@ -71,7 +71,7 @@ dbfile.input.insert <- function(in.path, in.prefix, siteid, startdate, enddate, "SELECT * FROM inputs WHERE site_id=", siteid, " AND name= '", name, "' AND format_id=", formatid, - parent + parent, ";" ), con = con ) @@ -120,26 +120,26 @@ dbfile.input.insert <- function(in.path, in.prefix, siteid, startdate, enddate, "INSERT INTO inputs ", "(site_id, format_id, name) VALUES (", siteid, ", ", formatid, ", '", name, - "'", ") RETURNING id" + "'", ") RETURNING id;" ) } else if (parent == "" && !is.null(startdate)) { cmd <- paste0( "INSERT INTO inputs ", "(site_id, format_id, start_date, end_date, name) VALUES (", siteid, ", ", formatid, ", '", startdate, "', '", enddate, "','", name, - "') RETURNING id" + "') RETURNING id;" ) } else if (is.null(startdate)) { cmd <- paste0( "INSERT INTO inputs ", "(site_id, format_id, name, parent_id) VALUES (", - siteid, ", ", formatid, ", '", name, "',", parentid, ") RETURNING id" + siteid, ", ", formatid, ", '", name, "',", parentid, ") RETURNING id;" ) } else { cmd <- paste0( "INSERT INTO inputs ", "(site_id, format_id, start_date, end_date, name, parent_id) VALUES (", - siteid, ", ", formatid, ", '", startdate, "', '", enddate, "','", name, "',", parentid, ") RETURNING id" + siteid, ", ", formatid, ", '", startdate, "', '", enddate, "','", name, "',", parentid, ") RETURNING id;" ) } # This is the id that we just registered @@ -150,7 +150,7 @@ dbfile.input.insert <- function(in.path, in.prefix, siteid, startdate, enddate, inputid <- db.query( query = paste0( "SELECT id FROM inputs WHERE site_id=", siteid, - " AND format_id=", formatid + " AND format_id=", formatid, ";" ), con = con )$id diff --git a/base/db/R/query.file.path.R b/base/db/R/query.file.path.R index 071b6af12b8..ceb79f99685 100644 --- a/base/db/R/query.file.path.R +++ b/base/db/R/query.file.path.R @@ -8,11 +8,11 @@ ##' @author Betsy Cowdery query.file.path <- function(input.id, host_name, con){ machine.host <- PEcAn.DB::default_hostname(host_name) - machine <- db.query(query = paste0("SELECT * from machines where hostname = '",machine.host,"'"), con = con) + machine <- db.query(query = paste0("SELECT * from machines where hostname = '",machine.host,"';"), con = con) dbfile <- db.query( query = paste( "SELECT file_name,file_path from dbfiles where container_id =", input.id, - " and container_type = 'Input' and machine_id =", machine$id + " and container_type = 'Input' and machine_id =", machine$id, ";" ), con = con ) diff --git a/base/db/tests/testthat/test.insert.R b/base/db/tests/testthat/test.insert.R index 95dd80a459e..168bb4f4ae6 100644 --- a/base/db/tests/testthat/test.insert.R +++ b/base/db/tests/testthat/test.insert.R @@ -47,3 +47,11 @@ test_that( } ) }) + +test_that("`match_colnames()` returns intersection of column names of a dataframe to a table", { + mockery::stub(match_colnames, 'dplyr::tbl', data.frame(id = 1, name = 'test', value = 1)) + expect_equal( + match_colnames(values = data.frame(id = 1, name = 'test'), table = 'test', con = 1), + c('id', 'name') + ) +}) \ No newline at end of file diff --git a/base/db/tests/testthat/test.query.dplyr.R b/base/db/tests/testthat/test.query.dplyr.R index 8be24e8f555..cc3e6436eea 100644 --- a/base/db/tests/testthat/test.query.dplyr.R +++ b/base/db/tests/testthat/test.query.dplyr.R @@ -146,3 +146,9 @@ test_that("`get_run_ids()` able to get vector of run ids (in sorted order) for a result <- get_run_ids(bety = 1, workflow_id = 1) expect_equal(result, c("No runs found")) }) + +test_that("`var_names_all()` able get vector of variable names for a particular workflow and run ID removing variables not to be shown to user", { + mockery::stub(var_names_all, 'get_var_names', c('A', 'B', 'C', 'Year','FracJulianDay')) + result <- var_names_all(bety = 1, workflow_id = 1, run_id = 1) + expect_equal(result, c('A', 'B', 'C')) +}) \ No newline at end of file diff --git a/base/db/tests/testthat/test.query.file.path.R b/base/db/tests/testthat/test.query.file.path.R new file mode 100644 index 00000000000..1da98a019e6 --- /dev/null +++ b/base/db/tests/testthat/test.query.file.path.R @@ -0,0 +1,21 @@ +test_that("`query.file.path()`", { + # mock responses for subsequent calls to db.query + mocked_res <- mockery::mock(data.frame(id = '20210101'), data.frame(file_name = 'test_file', file_path = 'test_path')) + mockery::stub(query.file.path, 'db.query', mocked_res) + mockery::stub(query.file.path, 'PEcAn.remote::remote.execute.R', TRUE) + res <- query.file.path(input.id = 1, host_name = "pecan", con = 1) + args <- mockery::mock_args(mocked_res) + expect_true( + grepl( + "where hostname = 'pecan'", + args[[1]]$query + ) + ) + expect_true( + grepl( + "container_id = 1.* machine_id = 20210101", + args[[2]]$query + ) + ) + expect_equal(res, 'test_path/test_file') +}) \ No newline at end of file diff --git a/base/db/tests/testthat/test.utils_db.R b/base/db/tests/testthat/test.utils_db.R new file mode 100644 index 00000000000..f6d83a748c9 --- /dev/null +++ b/base/db/tests/testthat/test.utils_db.R @@ -0,0 +1,26 @@ +test_that("`db.print.connections()` able to log out details about connections", { + PEcAn.logger::logger.setUseConsole(TRUE, FALSE) + on.exit(PEcAn.logger::logger.setUseConsole(TRUE, TRUE), add = TRUE) + expect_output( + db.print.connections(), + paste0( + ".* Created 0 connections and executed 0 queries .* ", + "Created 0 connections and executed 0 queries.*", + "No open database connections." + ) + ) +}) + +test_that("`db.showQueries()` and `db.getShowQueries()` able to set and get the value of the .db.utils$showquery variable respectively", { + showquery_old <- db.getShowQueries() + on.exit(db.showQueries(showquery_old)) + db.showQueries(TRUE) + expect_equal(db.getShowQueries(), TRUE) +}) + +test_that("`default_hostname()` fixes hostname if the host is localhost", { + expect_equal(default_hostname("localhost"), PEcAn.remote::fqdn()) + + # if not localhost + expect_equal(default_hostname("pecan"), "pecan") +}) \ No newline at end of file From 725f3f99c1c3c98c8c53b2d8240e3edd89fef321 Mon Sep 17 00:00:00 2001 From: meetagrawal09 Date: Sat, 8 Jul 2023 11:01:20 +0530 Subject: [PATCH 12/32] tested add_icon --- base/visualization/DESCRIPTION | 3 ++- base/visualization/tests/testthat/test.add_icon.R | 7 +++++++ 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 base/visualization/tests/testthat/test.add_icon.R diff --git a/base/visualization/DESCRIPTION b/base/visualization/DESCRIPTION index 201bffdada4..14d0297926f 100644 --- a/base/visualization/DESCRIPTION +++ b/base/visualization/DESCRIPTION @@ -45,7 +45,8 @@ Suggests: png, raster, sp, - testthat (>= 1.0.2) + testthat (>= 1.0.2), + withr License: BSD_3_clause + file LICENSE Copyright: Authors LazyLoad: yes diff --git a/base/visualization/tests/testthat/test.add_icon.R b/base/visualization/tests/testthat/test.add_icon.R new file mode 100644 index 00000000000..d487b87714e --- /dev/null +++ b/base/visualization/tests/testthat/test.add_icon.R @@ -0,0 +1,7 @@ +test_that("`add_icon()` able to create the correct output file", { + withr::with_dir(tempdir(), { + add_icon(1, 2, 3) + # check if file exists + expect_true(file.exists("Rplots.pdf")) + }) +}) \ No newline at end of file From 7cb18ca76fc1c647dc1586574aa6568a28d9c3df Mon Sep 17 00:00:00 2001 From: meetagrawal09 Date: Tue, 11 Jul 2023 14:58:06 +0530 Subject: [PATCH 13/32] rolled back test --- base/db/tests/testthat/test.utils_db.R | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/base/db/tests/testthat/test.utils_db.R b/base/db/tests/testthat/test.utils_db.R index f6d83a748c9..b859baea575 100644 --- a/base/db/tests/testthat/test.utils_db.R +++ b/base/db/tests/testthat/test.utils_db.R @@ -1,15 +1,15 @@ -test_that("`db.print.connections()` able to log out details about connections", { - PEcAn.logger::logger.setUseConsole(TRUE, FALSE) - on.exit(PEcAn.logger::logger.setUseConsole(TRUE, TRUE), add = TRUE) - expect_output( - db.print.connections(), - paste0( - ".* Created 0 connections and executed 0 queries .* ", - "Created 0 connections and executed 0 queries.*", - "No open database connections." - ) - ) -}) +# test_that("`db.print.connections()` able to log out details about connections", { +# PEcAn.logger::logger.setUseConsole(TRUE, FALSE) +# on.exit(PEcAn.logger::logger.setUseConsole(TRUE, TRUE), add = TRUE) +# expect_output( +# db.print.connections(), +# paste0( +# ".* Created 0 connections and executed 0 queries .* ", +# "Created 0 connections and executed 0 queries.*", +# "No open database connections." +# ) +# ) +# }) test_that("`db.showQueries()` and `db.getShowQueries()` able to set and get the value of the .db.utils$showquery variable respectively", { showquery_old <- db.getShowQueries() From 5f2537ebe4e345b7bb7a9131a5175de55cdd2389 Mon Sep 17 00:00:00 2001 From: meetagrawal09 Date: Sat, 29 Jul 2023 12:16:18 +0530 Subject: [PATCH 14/32] tested data.fetch, plot_netcdf --- .../testthat/data/urbana_subdaily_test.nc | Bin 0 -> 149519 bytes .../tests/testthat/test.plot_netcdf.R | 21 ++++++++++++++++++ 2 files changed, 21 insertions(+) create mode 100644 base/visualization/tests/testthat/data/urbana_subdaily_test.nc create mode 100644 base/visualization/tests/testthat/test.plot_netcdf.R diff --git a/base/visualization/tests/testthat/data/urbana_subdaily_test.nc b/base/visualization/tests/testthat/data/urbana_subdaily_test.nc new file mode 100644 index 0000000000000000000000000000000000000000..3e7ceef36711ef47f1d09faba4883af211d71615 GIT binary patch literal 149519 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 Date: Sat, 29 Jul 2023 12:22:42 +0530 Subject: [PATCH 15/32] updated desc --- base/visualization/DESCRIPTION | 1 + 1 file changed, 1 insertion(+) diff --git a/base/visualization/DESCRIPTION b/base/visualization/DESCRIPTION index 14d0297926f..2aece439bdd 100644 --- a/base/visualization/DESCRIPTION +++ b/base/visualization/DESCRIPTION @@ -42,6 +42,7 @@ Imports: stringr(>= 1.1.0) Suggests: grid, + mockery, png, raster, sp, From 3febddd459be0b88490270dcfde8d7ffae785057 Mon Sep 17 00:00:00 2001 From: meetagrawal09 Date: Tue, 1 Aug 2023 18:05:45 +0530 Subject: [PATCH 16/32] fixed typo, added test --- base/visualization/R/plots.R | 2 +- base/visualization/man/plot_data.Rd | 2 +- base/visualization/tests/testthat/test.ciEnvelope.R | 3 +++ base/visualization/tests/testthat/test.plot_data.R | 3 +++ 4 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 base/visualization/tests/testthat/test.ciEnvelope.R create mode 100644 base/visualization/tests/testthat/test.plot_data.R diff --git a/base/visualization/R/plots.R b/base/visualization/R/plots.R index f93f3eacf10..3f1395305bc 100644 --- a/base/visualization/R/plots.R +++ b/base/visualization/R/plots.R @@ -188,7 +188,7 @@ iqr <- function(x) { ##' ##' Used to add raw data or summary statistics to the plot of a distribution. ##' The height of Y is arbitrary, and can be set to optimize visualization. -##' If SE estimates are available, the se wil be plotted +##' If SE estimates are available, the SE will be plotted ##' @name plot_data ##' @aliases plot.data ##' @title Add data to plot diff --git a/base/visualization/man/plot_data.Rd b/base/visualization/man/plot_data.Rd index b54ad1dbca8..37d741dbd0b 100644 --- a/base/visualization/man/plot_data.Rd +++ b/base/visualization/man/plot_data.Rd @@ -24,7 +24,7 @@ Add data to an existing plot or create a new one \details{ Used to add raw data or summary statistics to the plot of a distribution. The height of Y is arbitrary, and can be set to optimize visualization. -If SE estimates are available, the se wil be plotted +If SE estimates are available, the SE will be plotted } \examples{ \dontrun{plot_data(data.frame(Y = c(1, 2), se = c(1,2)), base.plot = NULL, ymax = 10)} diff --git a/base/visualization/tests/testthat/test.ciEnvelope.R b/base/visualization/tests/testthat/test.ciEnvelope.R new file mode 100644 index 00000000000..a0b4548a702 --- /dev/null +++ b/base/visualization/tests/testthat/test.ciEnvelope.R @@ -0,0 +1,3 @@ +test_that("`ciEnvelope()`", { + +}) \ No newline at end of file diff --git a/base/visualization/tests/testthat/test.plot_data.R b/base/visualization/tests/testthat/test.plot_data.R new file mode 100644 index 00000000000..12fefd6c47f --- /dev/null +++ b/base/visualization/tests/testthat/test.plot_data.R @@ -0,0 +1,3 @@ +test_that("`plot_data()`", { + +}) \ No newline at end of file From 4385f2bd237271a9bce6eabf03af791213265bfd Mon Sep 17 00:00:00 2001 From: meetagrawal09 Date: Sun, 6 Aug 2023 10:19:03 +0530 Subject: [PATCH 17/32] added integration tests for ERA5 --- .../integrationTests/test.download.ERA5.R | 141 ++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 modules/data.atmosphere/inst/integrationTests/test.download.ERA5.R diff --git a/modules/data.atmosphere/inst/integrationTests/test.download.ERA5.R b/modules/data.atmosphere/inst/integrationTests/test.download.ERA5.R new file mode 100644 index 00000000000..01197d308ca --- /dev/null +++ b/modules/data.atmosphere/inst/integrationTests/test.download.ERA5.R @@ -0,0 +1,141 @@ +library(testthat) + +tmpdir <- tempfile(pattern = "era5Data") +dir.create(tmpdir) +on.exit(teardown(unlink(tmpdir, recursive = TRUE))) + +outfolder <- tmpdir +# outfolder <- "./era5Data" +start_date <- "2010-01-01" +end_date <- "2010-02-01" +lat.in <- 45.5594 +lon.in <- -84.6738 + +product_types <- "all" +overwrite <- FALSE +reticulate_python <- NULL + +PEcAn.utils:::need_packages("reticulate") + +if (!is.null(reticulate_python)) { + reticulate::use_python(reticulate_python) +} + +tryCatch({ + cdsapi <- reticulate::import("cdsapi") +}, error = function(e) { + PEcAn.logger::logger.severe( + "Failed to load `cdsapi` Python library. ", + "Please make sure it is installed to a location accessible to `reticulate`.", + "You should be able to install it with the following command: ", + "`pip install --user cdsapi`.", + "The following error was thrown by `reticulate::import(\"cdsapi\")`: ", + conditionMessage(e) + ) +}) + + +# check for the existence of the cdsapirc file +if (!file.exists(file.path(Sys.getenv("HOME"), ".cdsapirc"))) +PEcAn.logger::logger.severe( + "Please create a `${HOME}/.cdsapirc` file as described here:", + "https://cds.climate.copernicus.eu/api-how-to#install-the-cds-api-key ." +) + +# initialize the cdsapi client +tryCatch({ + cclient <- cdsapi$Client() +}, error = function(e) { + PEcAn.logger::logger.severe( + "The following error was thrown by `cdsapi$Client()`: ", + conditionMessage(e) + ) +}) + +all_products <- c("reanalysis", "ensemble_members", + "ensemble mean", "ensemble_spread") + +if (product_types == "all") { + product_types <- all_products +} + +# Check that all product types are valid +if (any(!product_types %in% all_products)) { + bad_products <- setdiff(product_types, all_products) + PEcAn.logger::logger.severe(sprintf( + "Invalid product types %s. Products must be one of the following: %s", + paste0("`", bad_products, "`", collapse = ", "), + paste0("`", all_products, "`", collapse = ", ") + )) +} + + +variables <- tibble::tribble( + ~cf_name, ~units, ~api_name, ~ncdf_name, + "air_temperature", "Kelvin", "2m_temperature", "t2m", + "air_pressure", "Pa", "surface_pressure", NA_character_, + NA_character_, "Kelvin", "2m_dewpoint_temperature", NA_character_, + "precipitation_flux", "kg/m2/s", "total_precipitation", NA_character_, + "eastward_wind", "m/s", "10m_u_component_of_wind", NA_character_, + "northward_wind", "m/s", "10m_v_component_of_wind", NA_character_, + "surface_downwelling_shortwave_flux_in_air", "W/m2", "surface_solar_radiation_downwards", NA_character_, + "surface_downwelling_longwave_flux_in_air", "W/m2", "surface_thermal_radiation_downwards", NA_character_ +) +nvar <- nrow(variables) + +area <- rep(round(c(lat.in, lon.in) * 4) / 4, 2) + +files <- character() +dir.create(outfolder, showWarnings = FALSE) + +# First, download all the files +for (i in seq_len(nvar)) { + var <- variables[["api_name"]][[i]] + PEcAn.logger::logger.debug(glue::glue( + "Downloading variable {i} of {nvar} ({var})." + )) + fname <- file.path(outfolder, paste("era5", var, "nc", sep = ".")) + if (file.exists(fname) && !overwrite) { + PEcAn.logger::logger.warn(glue::glue( + "File `{fname}` already exists, and `overwrite` is FALSE. ", + "Skipping to next variable." + )) + next + } + do_next <- tryCatch({ + cclient$retrieve( + "reanalysis-era5-single-levels", + list( + variable = var, + product_type = 'ensemble_members', + date = paste(start_date, end_date, sep = "/"), + time = "00/to/23/by/1", + area = area, + grid = c(0.25, 0.25), + format = "netcdf" + ), + fname + ) + FALSE + }, error = function(e) { + PEcAn.logger::logger.warn( + glue::glue( + "Failed to download variable `{var}`. ", + "Skipping to next variable. ", + "Error message was:\n", + conditionMessage(e) + ) + ) + TRUE + }) + + if (isTRUE(do_next)) next + files <- c(files, fname) +} + +test_that("Downloaded files exist in the specified directory", { + for (i in files) { + expect_true(file.exists(i)) + } +}) + From 693d230dfa9f82f971ae8c9dd815aeb51815b570 Mon Sep 17 00:00:00 2001 From: meetagrawal09 Date: Sun, 6 Aug 2023 10:43:05 +0530 Subject: [PATCH 18/32] added integration tests for AmerifluxLBL --- .../test.download.AmerifluxLBL.R | 165 ++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 modules/data.atmosphere/inst/integrationTests/test.download.AmerifluxLBL.R diff --git a/modules/data.atmosphere/inst/integrationTests/test.download.AmerifluxLBL.R b/modules/data.atmosphere/inst/integrationTests/test.download.AmerifluxLBL.R new file mode 100644 index 00000000000..144db86dea2 --- /dev/null +++ b/modules/data.atmosphere/inst/integrationTests/test.download.AmerifluxLBL.R @@ -0,0 +1,165 @@ +library(testthat) + +tmpdir <- tempfile(pattern = "amerifluxData") +dir.create(tmpdir) +on.exit(teardown(unlink(tmpdir, recursive = TRUE))) + +sitename <- "US-Akn" +# outfolder <- "./amerifluxData" +outfolder <- tmpdir +start_date <- "2011-01-01" +end_date <- "2011-12-31" +username <- "pecan" +useremail <- "@" +data_product <- "BASE-BADM" +data_policy <- "CCBY4.0" +overwrite <- FALSE +verbose <- FALSE + + +start_date <- as.POSIXlt(start_date, tz = "UTC") +end_date <- as.POSIXlt(end_date, tz = "UTC") + +start_year <- lubridate::year(start_date) +end_year <- lubridate::year(end_date) + + +site <- sub(".* \\((.*)\\)", "\\1", sitename) + +# make sure output folder exists +if (!file.exists(outfolder)) { + dir.create(outfolder, showWarnings = FALSE, recursive = TRUE) +} + +repeat { + tout <- options("timeout") + zip_file <- try(amerifluxr::amf_download_base( + user_id = username, + user_email = useremail, + site_id = site, + data_product = data_product, + data_policy = data_policy, + agree_policy = TRUE, + intended_use = "model", + intended_use_text = "PEcAn download", + verbose = verbose, + out_dir = outfolder + )) + if (!inherits(zip_file, "try-error")) { + break + } else if (tout$timeout > 250) { + PEcAn.logger::logger.severe("Download takes too long, check your connection.") + break + } + PEcAn.logger::logger.info("Added 100 seconds before the download timeouts") + options(timeout = tout$timeout + 100) +} + +# Path to created zip-file +ftplink <- zip_file +if (!grepl(".zip", ftplink)) { + PEcAn.logger::logger.info("Not able to download a zip-file. Check download.AmerifluxLBL inputs") +} + +# get zip and csv filenames +outfname <- strsplit(ftplink, "/") +outfname <- outfname[[1]][length(outfname[[1]])] +output_zip_file <- file.path(outfolder, outfname) +file_timestep_hh <- "HH" +file_timestep_hr <- "HR" +file_timestep <- file_timestep_hh + +endname <- strsplit(outfname, "_") +endname <- endname[[1]][length(endname[[1]])] +endname <- gsub("\\..*", "", endname) +outcsvname <- paste0(substr(outfname, 1, 15), "_", file_timestep_hh, "_", endname, ".csv") +output_csv_file <- file.path(outfolder, outcsvname) +outcsvname_hr <- paste0(substr(outfname, 1, 15), "_", file_timestep_hr, "_", endname, ".csv") +output_csv_file_hr <- file.path(outfolder, outcsvname_hr) + +download_file_flag <- TRUE +extract_file_flag <- TRUE +if (!overwrite && file.exists(output_zip_file)) { + PEcAn.logger::logger.debug("File '", output_zip_file, "' already exists, skipping download") + download_file_flag <- FALSE +} +if (!overwrite && file.exists(output_csv_file)) { + PEcAn.logger::logger.debug("File '", output_csv_file, "' already exists, skipping extraction.") + download_file_flag <- FALSE + extract_file_flag <- FALSE + file_timestep <- "HH" +} else { + if (!overwrite && file.exists(output_csv_file_hr)) { + PEcAn.logger::logger.debug("File '", output_csv_file_hr, "' already exists, skipping extraction.") + download_file_flag <- FALSE + extract_file_flag <- FALSE + file_timestep <- "HR" + outcsvname <- outcsvname_hr + output_csv_file <- output_csv_file_hr + } +} + +if (download_file_flag) { + extract_file_flag <- TRUE + PEcAn.utils::download_file(ftplink, output_zip_file, method) + if (!file.exists(output_zip_file)) { + PEcAn.logger::logger.severe("FTP did not download ", output_zip_file, " from ", ftplink) + } +} +if (extract_file_flag) { + avail_file <- utils::unzip(output_zip_file, list = TRUE) + if (length(grep("HH", avail_file)) > 0) { + file_timestep <- "HH" + } else { + if (length(grep("HR", avail_file)) > 0) { + file_timestep <- "HR" + output_csv_file <- output_csv_file_hr + outcsvname <- outcsvname_hr + } else { + PEcAn.logger::logger.severe("Half-hourly or Hourly data file was not found in ", output_zip_file) + } + } + utils::unzip(output_zip_file, outcsvname, exdir = outfolder) + if (!file.exists(output_csv_file)) { + PEcAn.logger::logger.severe("ZIP file ", output_zip_file, " did not contain CSV file ", outcsvname) + } +} + +dbfilename <- paste0(substr(outfname, 1, 15), "_", file_timestep, "_", endname) + +# get start and end year of data from file +firstline <- system(paste0("head -4 ", output_csv_file), intern = TRUE) +firstline <- firstline[4] +lastline <- system(paste0("tail -1 ", output_csv_file), intern = TRUE) + +firstdate_st <- paste0( + substr(firstline, 1, 4), "-", + substr(firstline, 5, 6), "-", + substr(firstline, 7, 8), " ", + substr(firstline, 9, 10), ":", + substr(firstline, 11, 12) +) +firstdate <- as.POSIXlt(firstdate_st) +lastdate_st <- paste0( + substr(lastline, 1, 4), "-", + substr(lastline, 5, 6), "-", + substr(lastline, 7, 8), " ", + substr(lastline, 9, 10), ":", + substr(lastline, 11, 12) +) +lastdate <- as.POSIXlt(lastdate_st) + +syear <- lubridate::year(firstdate) +eyear <- lubridate::year(lastdate) + +if (start_year > eyear) { + PEcAn.logger::logger.severe("Start_Year", start_year, "exceeds end of record ", eyear, " for ", site) +} +if (end_year < syear) { + PEcAn.logger::logger.severe("End_Year", end_year, "precedes start of record ", syear, " for ", site) +} + +test_that("Downloaded data files are present at the desired location", { + expect_true(file.exists(paste0(output_csv_file))) + expect_true(file.exists(paste0(output_zip_file))) +}) From 6108996e9e8bd3cea65e6c15dd3ac315257c3fb1 Mon Sep 17 00:00:00 2001 From: meetagrawal09 Date: Thu, 17 Aug 2023 18:35:34 +0530 Subject: [PATCH 19/32] tested convert_input, .get.file.deletion.commands --- base/db/tests/testthat/test.convert_input.R | 39 +++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 base/db/tests/testthat/test.convert_input.R diff --git a/base/db/tests/testthat/test.convert_input.R b/base/db/tests/testthat/test.convert_input.R new file mode 100644 index 00000000000..29513187c9e --- /dev/null +++ b/base/db/tests/testthat/test.convert_input.R @@ -0,0 +1,39 @@ +test_that("`convert_input()` able to call the respective download function for a data item with the correct arguments", { + mocked_res <- mockery::mock(list(c("A", "B"))) + + mockery::stub(convert_input, 'dbfile.input.check', data.frame()) + mockery::stub(convert_input, 'db.query', data.frame(id = 1)) + mockery::stub(convert_input, 'PEcAn.remote::remote.execute.R', mocked_res) + mockery::stub(convert_input, 'purrr::map_dfr', data.frame(missing = c(FALSE), empty = c(FALSE))) + + convert_input( + input.id = NA, + outfolder = "test", + formatname = NULL, + mimetype = NULL, + site.id = 1, + start_date = "2011-01-01", + end_date = "2011-12-31", + pkg = 'PEcAn.data.atmosphere', + fcn = 'download.AmerifluxLBL', + con = NULL, + host = data.frame(name = "localhost"), + browndog = NULL, + write = FALSE, + lat.in = 40, + lon.in = -88 + ) + + args <- mockery::mock_args(mocked_res) + expect_equal( + args[[1]]$script, + "PEcAn.data.atmosphere::download.AmerifluxLBL(lat.in=40, lon.in=-88, overwrite=FALSE, outfolder='test/', start_date='2011-01-01', end_date='2011-12-31')" + ) +}) + +test_that("`.get.file.deletion.commands()` able to return correct file deletion commands", { + res <- .get.file.deletion.commands(c("test")) + expect_equal(res$move.to.tmp, "dir.create(c('./tmp'), recursive=TRUE, showWarnings=FALSE); file.rename(from=c('test'), to=c('./tmp/test'))") + expect_equal(res$delete.tmp, "unlink(c('./tmp'), recursive=TRUE)") + expect_equal(res$replace.from.tmp, "file.rename(from=c('./tmp/test'), to=c('test'));unlink(c('./tmp'), recursive=TRUE)") +}) \ No newline at end of file From f794da8e2c0ddb88262bcfd57037ceeb97202e62 Mon Sep 17 00:00:00 2001 From: meetagrawal09 Date: Fri, 18 Aug 2023 15:47:18 +0530 Subject: [PATCH 20/32] tested dbfile.check, dbfile.file, dbfile.id --- base/db/tests/testthat/test.dbfiles.R | 44 +++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 base/db/tests/testthat/test.dbfiles.R diff --git a/base/db/tests/testthat/test.dbfiles.R b/base/db/tests/testthat/test.dbfiles.R new file mode 100644 index 00000000000..02fc2401fb9 --- /dev/null +++ b/base/db/tests/testthat/test.dbfiles.R @@ -0,0 +1,44 @@ +test_that("`dbfile.check()` able to return the most recent entries from `dbfiles` table associated with a container and machine", { + mockery::stub(dbfile.check, 'get.id', 1) + mockery::stub( + dbfile.check, + 'dplyr::tbl', + data.frame( + container_type = c('Input', 'Input', 'Model'), + container_id = c(7, 7, 7), + machine_id = c(1, 1, 2), + updated_at = c(20201112, 20210101, 20210102), + id = c(2, 3, 4), + filename = c('test_1', 'test_2', 'test_3'), + pathname = c('path_1', 'path_2', 'path_3') + ) + ) + res <- dbfile.check("Input", 7, con = NULL) + + expect_equal( + res, + data.frame(container_type = 'Input', container_id = 7, machine_id = 1, updated_at = 20210101, id = 3, filename = 'test_2', pathname = 'path_2') + ) +}) + +test_that("`dbfile.file()` able to return a correctly formed file path from entries in the `dbfiles` table for a particular container and machine", { + mockery::stub(dbfile.file, 'dbfile.check', data.frame(file_path = 'test/dir/path', file_name = 'test_file')) + expect_equal(dbfile.file('Input', 7, con = NULL), file.path('test/dir/path/test_file')) +}) + +test_that("`dbfile.id()` able to construct a correct database query to get id for a dbfile given the container type and filepath", { + mocked_res <- mockery::mock(data.frame(id = 1), data.frame(container_id = 2020)) + mockery::stub(dbfile.id, 'db.query', mocked_res) + + res <- dbfile.id('Model', '/usr/local/bin/sipnet', con = NULL) + args <- mockery::mock_args(mocked_res) + + expect_equal(res, 2020) + expect_true( + grepl( + "WHERE container_type='Model' AND file_path='/usr/local/bin' AND file_name='sipnet' AND machine_id=1", + args[[2]]$query + ) + ) +}) + From 92cada9a10b8525a4b75a23531c172b4739c23ca Mon Sep 17 00:00:00 2001 From: meetagrawal09 Date: Fri, 18 Aug 2023 17:35:43 +0530 Subject: [PATCH 21/32] tested dbfile.posterior.insert, dbfile.posterior.check, dbfile.insert --- base/db/tests/testthat/test.dbfiles.R | 41 ++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/base/db/tests/testthat/test.dbfiles.R b/base/db/tests/testthat/test.dbfiles.R index 02fc2401fb9..9dff33cc895 100644 --- a/base/db/tests/testthat/test.dbfiles.R +++ b/base/db/tests/testthat/test.dbfiles.R @@ -1,3 +1,43 @@ +test_that("`dbfile.posterior.insert()` able to make a correct query to insert a file into dbfiles table as a posterior", { + mocked_res <- mockery::mock(NULL, NULL, data.frame(id = 10)) + mockery::stub(dbfile.posterior.insert, 'get.id', 1) + mockery::stub(dbfile.posterior.insert, 'dbfile.insert', 1010) + mockery::stub(dbfile.posterior.insert, 'db.query', mocked_res) + + dbfile.posterior.insert('trait.data.Rdata', 'test-pft', 'application/x-RData', 'traits', con = NULL) + args <- mockery::mock_args(mocked_res) + expect_true(grepl("INSERT INTO posteriors \\(pft_id, format_id\\) VALUES \\(1, 1\\)", args[[2]]$query)) + +}) + +test_that("`dbfile.posterior.check()` able to form the correct query to retrieve correct posterior id to run further checks", { + mocked_res <- mockery::mock(data.frame(id = 2020)) + mockery::stub(dbfile.posterior.check, 'get.id', 1) + mockery::stub(dbfile.posterior.check, 'db.query', mocked_res) + mockery::stub(dbfile.posterior.check, 'dbfile.check', data.frame(id = 1, filename = 'test_1', pathname = 'path_1')) + + dbfile.posterior.check('testpft', 'application/x-RData', 'traits', con = NULL) + + args <- mockery::mock_args(mocked_res) + expect_true( + grepl( + "SELECT id FROM posteriors WHERE pft_id=1 AND format_id=1", + args[[1]]$query + ) + ) +}) + +test_that("`dbfile.insert()` able to add correct parameter values to the insert database query and return a file id", { + mocked_res <- mockery::mock(data.frame(), data.frame(id = 2020)) + mockery::stub(dbfile.insert, 'get.id', 1) + mockery::stub(dbfile.insert, 'db.query', mocked_res) + + res <- dbfile.insert(in.path = '/test/file/path', in.prefix = 'testfile.txt', 'Input', 7, con = NULL) + args <- mockery::mock_args(mocked_res) + expect_equal(res, 2020) + expect_true(grepl("VALUES \\('Input', 7, 'testfile.txt', '/test/file/path', 1\\) RETURNING id", args[[2]]$query)) +}) + test_that("`dbfile.check()` able to return the most recent entries from `dbfiles` table associated with a container and machine", { mockery::stub(dbfile.check, 'get.id', 1) mockery::stub( @@ -41,4 +81,3 @@ test_that("`dbfile.id()` able to construct a correct database query to get id fo ) ) }) - From 33c3f7fcc10dc8bb3a4439b347296d2ad0c29655 Mon Sep 17 00:00:00 2001 From: meetagrawal09 Date: Fri, 18 Aug 2023 18:57:04 +0530 Subject: [PATCH 22/32] tested dbfile.input.insert, dbfile.input.check --- base/db/tests/testthat/test.dbfiles.R | 69 ++++++++++++++++++++++++++- 1 file changed, 68 insertions(+), 1 deletion(-) diff --git a/base/db/tests/testthat/test.dbfiles.R b/base/db/tests/testthat/test.dbfiles.R index 9dff33cc895..4880a074cd8 100644 --- a/base/db/tests/testthat/test.dbfiles.R +++ b/base/db/tests/testthat/test.dbfiles.R @@ -1,3 +1,70 @@ +test_that("`dbfile.input.insert()` able to create correct sql queries to insert a file into dbfiles table", { + + mocked_res <- mockery::mock(data.frame(), 1, data.frame(id = 2023)) + mockery::stub(dbfile.input.insert, 'get.id', 1) + mockery::stub(dbfile.input.insert, 'db.query', mocked_res) + mockery::stub( + dbfile.input.insert, + 'dbfile.check', + data.frame(id = 101, file_name = 'test-file', file_path = 'trait.data.Rdata') + ) + + res <- dbfile.input.insert( + in.path = 'trait.data.Rdata', + in.prefix = 'test-file', + siteid = 'test-site', + startdate = '2021-01-01', + enddate = '2022-01-01', + mimetype = 'application/x-RData', + formatname = 'traits', + con = NULL + ) + + expect_equal(res$dbfile.id, 101) + expect_equal(res$input.id, 2023) + args <- mockery::mock_args(mocked_res) + + # finding appropriate input + expect_true( + grepl( + "WHERE site_id=test-site AND name= 'trait.data.Rdata' AND format_id=1;", + args[[1]]$query + ) + ) + + # parent == "" and startdate not NULL + expect_true( + grepl( + "VALUES \\(test-site, 1, '2021-01-01', '2022-01-01','trait.data.Rdata'\\)", + args[[2]]$query + ) + ) + + # startdate not NULL + expect_true( + grepl( + "WHERE site_id=test-site AND format_id=1 AND start_date='2021-01-01' AND end_date='2022-01-01'", + args[[3]]$query + ) + ) +}) + +test_that("`dbfile.input.check()` able to form the right query to check the dbfiles table to see if a file exists as an input", { + + mocked_res <- mockery::mock(NULL) + mockery::stub(dbfile.input.check, 'get.id', 1) + mockery::stub(dbfile.input.check, 'db.query', mocked_res) + + dbfile.input.check('US-Akn', '2021-01-01', '2022-01-01', 'application/x-RData', 'traits', con = NULL) + args <- mockery::mock_args(mocked_res) + expect_true( + grepl( + "WHERE site_id=US-Akn AND format_id=1", + args[[1]]$query + ) + ) +}) + test_that("`dbfile.posterior.insert()` able to make a correct query to insert a file into dbfiles table as a posterior", { mocked_res <- mockery::mock(NULL, NULL, data.frame(id = 10)) mockery::stub(dbfile.posterior.insert, 'get.id', 1) @@ -80,4 +147,4 @@ test_that("`dbfile.id()` able to construct a correct database query to get id fo args[[2]]$query ) ) -}) +}) \ No newline at end of file From f23fe001c548a48790bd7ba0150c0e01c1785f3f Mon Sep 17 00:00:00 2001 From: meetagrawal09 Date: Mon, 21 Aug 2023 14:15:19 +0530 Subject: [PATCH 23/32] updated download function --- .../test.download.AmerifluxLBL.R | 192 +++--------------- 1 file changed, 29 insertions(+), 163 deletions(-) diff --git a/modules/data.atmosphere/inst/integrationTests/test.download.AmerifluxLBL.R b/modules/data.atmosphere/inst/integrationTests/test.download.AmerifluxLBL.R index 144db86dea2..3ab07a98939 100644 --- a/modules/data.atmosphere/inst/integrationTests/test.download.AmerifluxLBL.R +++ b/modules/data.atmosphere/inst/integrationTests/test.download.AmerifluxLBL.R @@ -1,165 +1,31 @@ -library(testthat) - -tmpdir <- tempfile(pattern = "amerifluxData") -dir.create(tmpdir) -on.exit(teardown(unlink(tmpdir, recursive = TRUE))) - -sitename <- "US-Akn" -# outfolder <- "./amerifluxData" -outfolder <- tmpdir -start_date <- "2011-01-01" -end_date <- "2011-12-31" -username <- "pecan" -useremail <- "@" -data_product <- "BASE-BADM" -data_policy <- "CCBY4.0" -overwrite <- FALSE -verbose <- FALSE - - -start_date <- as.POSIXlt(start_date, tz = "UTC") -end_date <- as.POSIXlt(end_date, tz = "UTC") - -start_year <- lubridate::year(start_date) -end_year <- lubridate::year(end_date) - - -site <- sub(".* \\((.*)\\)", "\\1", sitename) - -# make sure output folder exists -if (!file.exists(outfolder)) { - dir.create(outfolder, showWarnings = FALSE, recursive = TRUE) -} - -repeat { - tout <- options("timeout") - zip_file <- try(amerifluxr::amf_download_base( - user_id = username, - user_email = useremail, - site_id = site, - data_product = data_product, - data_policy = data_policy, - agree_policy = TRUE, - intended_use = "model", - intended_use_text = "PEcAn download", - verbose = verbose, - out_dir = outfolder - )) - if (!inherits(zip_file, "try-error")) { - break - } else if (tout$timeout > 250) { - PEcAn.logger::logger.severe("Download takes too long, check your connection.") - break - } - PEcAn.logger::logger.info("Added 100 seconds before the download timeouts") - options(timeout = tout$timeout + 100) -} - -# Path to created zip-file -ftplink <- zip_file -if (!grepl(".zip", ftplink)) { - PEcAn.logger::logger.info("Not able to download a zip-file. Check download.AmerifluxLBL inputs") -} - -# get zip and csv filenames -outfname <- strsplit(ftplink, "/") -outfname <- outfname[[1]][length(outfname[[1]])] -output_zip_file <- file.path(outfolder, outfname) -file_timestep_hh <- "HH" -file_timestep_hr <- "HR" -file_timestep <- file_timestep_hh - -endname <- strsplit(outfname, "_") -endname <- endname[[1]][length(endname[[1]])] -endname <- gsub("\\..*", "", endname) -outcsvname <- paste0(substr(outfname, 1, 15), "_", file_timestep_hh, "_", endname, ".csv") -output_csv_file <- file.path(outfolder, outcsvname) -outcsvname_hr <- paste0(substr(outfname, 1, 15), "_", file_timestep_hr, "_", endname, ".csv") -output_csv_file_hr <- file.path(outfolder, outcsvname_hr) - -download_file_flag <- TRUE -extract_file_flag <- TRUE -if (!overwrite && file.exists(output_zip_file)) { - PEcAn.logger::logger.debug("File '", output_zip_file, "' already exists, skipping download") - download_file_flag <- FALSE -} -if (!overwrite && file.exists(output_csv_file)) { - PEcAn.logger::logger.debug("File '", output_csv_file, "' already exists, skipping extraction.") - download_file_flag <- FALSE - extract_file_flag <- FALSE - file_timestep <- "HH" -} else { - if (!overwrite && file.exists(output_csv_file_hr)) { - PEcAn.logger::logger.debug("File '", output_csv_file_hr, "' already exists, skipping extraction.") - download_file_flag <- FALSE - extract_file_flag <- FALSE - file_timestep <- "HR" - outcsvname <- outcsvname_hr - output_csv_file <- output_csv_file_hr - } -} - -if (download_file_flag) { - extract_file_flag <- TRUE - PEcAn.utils::download_file(ftplink, output_zip_file, method) - if (!file.exists(output_zip_file)) { - PEcAn.logger::logger.severe("FTP did not download ", output_zip_file, " from ", ftplink) - } -} -if (extract_file_flag) { - avail_file <- utils::unzip(output_zip_file, list = TRUE) - if (length(grep("HH", avail_file)) > 0) { - file_timestep <- "HH" - } else { - if (length(grep("HR", avail_file)) > 0) { - file_timestep <- "HR" - output_csv_file <- output_csv_file_hr - outcsvname <- outcsvname_hr - } else { - PEcAn.logger::logger.severe("Half-hourly or Hourly data file was not found in ", output_zip_file) - } - } - utils::unzip(output_zip_file, outcsvname, exdir = outfolder) - if (!file.exists(output_csv_file)) { - PEcAn.logger::logger.severe("ZIP file ", output_zip_file, " did not contain CSV file ", outcsvname) - } -} - -dbfilename <- paste0(substr(outfname, 1, 15), "_", file_timestep, "_", endname) - -# get start and end year of data from file -firstline <- system(paste0("head -4 ", output_csv_file), intern = TRUE) -firstline <- firstline[4] -lastline <- system(paste0("tail -1 ", output_csv_file), intern = TRUE) - -firstdate_st <- paste0( - substr(firstline, 1, 4), "-", - substr(firstline, 5, 6), "-", - substr(firstline, 7, 8), " ", - substr(firstline, 9, 10), ":", - substr(firstline, 11, 12) -) -firstdate <- as.POSIXlt(firstdate_st) -lastdate_st <- paste0( - substr(lastline, 1, 4), "-", - substr(lastline, 5, 6), "-", - substr(lastline, 7, 8), " ", - substr(lastline, 9, 10), ":", - substr(lastline, 11, 12) +# putting logger to debug mode +PEcAn.logger::logger.setUseConsole(TRUE, FALSE) +on.exit(PEcAn.logger::logger.setUseConsole(TRUE, TRUE), add = TRUE) +PEcAn.logger::logger.setLevel("DEBUG") + + +# mocking functions +mockery::stub(PEcAn.DB::convert_input, 'dbfile.input.check', data.frame()) +mockery::stub(PEcAn.DB::convert_input, 'db.query', data.frame(id = 1)) + +# calling download function +PEcAn.DB::convert_input( + input.id = NA, + outfolder = "testAmerifluxData", + formatname = NULL, + mimetype = NULL, + site.id = 1, + start_date = "2011-01-01", + end_date = "2011-12-31", + pkg = 'PEcAn.data.atmosphere', + fcn = 'download.AmerifluxLBL', + con = NULL, + host = data.frame(name = "localhost"), + browndog = NULL, + write = FALSE, + lat.in = 40, + lon.in = -88, + sitename = 'US-Akn' ) -lastdate <- as.POSIXlt(lastdate_st) - -syear <- lubridate::year(firstdate) -eyear <- lubridate::year(lastdate) - -if (start_year > eyear) { - PEcAn.logger::logger.severe("Start_Year", start_year, "exceeds end of record ", eyear, " for ", site) -} -if (end_year < syear) { - PEcAn.logger::logger.severe("End_Year", end_year, "precedes start of record ", syear, " for ", site) -} -test_that("Downloaded data files are present at the desired location", { - expect_true(file.exists(paste0(output_csv_file))) - expect_true(file.exists(paste0(output_zip_file))) -}) +# checking the downloaded files From 8c0eb713de8f0fd11d6f1eb4ebe50d3b19c37000 Mon Sep 17 00:00:00 2001 From: meetagrawal09 Date: Mon, 21 Aug 2023 15:27:11 +0530 Subject: [PATCH 24/32] updated download --- .../integrationTests/test.download.ERA5.R | 161 +++--------------- 1 file changed, 28 insertions(+), 133 deletions(-) diff --git a/modules/data.atmosphere/inst/integrationTests/test.download.ERA5.R b/modules/data.atmosphere/inst/integrationTests/test.download.ERA5.R index 01197d308ca..ab9b3a49845 100644 --- a/modules/data.atmosphere/inst/integrationTests/test.download.ERA5.R +++ b/modules/data.atmosphere/inst/integrationTests/test.download.ERA5.R @@ -1,141 +1,36 @@ library(testthat) -tmpdir <- tempfile(pattern = "era5Data") -dir.create(tmpdir) -on.exit(teardown(unlink(tmpdir, recursive = TRUE))) - -outfolder <- tmpdir -# outfolder <- "./era5Data" -start_date <- "2010-01-01" -end_date <- "2010-02-01" -lat.in <- 45.5594 -lon.in <- -84.6738 - -product_types <- "all" -overwrite <- FALSE -reticulate_python <- NULL - -PEcAn.utils:::need_packages("reticulate") - -if (!is.null(reticulate_python)) { - reticulate::use_python(reticulate_python) -} - -tryCatch({ - cdsapi <- reticulate::import("cdsapi") -}, error = function(e) { - PEcAn.logger::logger.severe( - "Failed to load `cdsapi` Python library. ", - "Please make sure it is installed to a location accessible to `reticulate`.", - "You should be able to install it with the following command: ", - "`pip install --user cdsapi`.", - "The following error was thrown by `reticulate::import(\"cdsapi\")`: ", - conditionMessage(e) - ) -}) - - -# check for the existence of the cdsapirc file -if (!file.exists(file.path(Sys.getenv("HOME"), ".cdsapirc"))) -PEcAn.logger::logger.severe( - "Please create a `${HOME}/.cdsapirc` file as described here:", - "https://cds.climate.copernicus.eu/api-how-to#install-the-cds-api-key ." -) +# putting logger to debug mode +PEcAn.logger::logger.setUseConsole(TRUE, FALSE) +on.exit(PEcAn.logger::logger.setUseConsole(TRUE, TRUE), add = TRUE) +PEcAn.logger::logger.setLevel("DEBUG") -# initialize the cdsapi client -tryCatch({ - cclient <- cdsapi$Client() -}, error = function(e) { - PEcAn.logger::logger.severe( - "The following error was thrown by `cdsapi$Client()`: ", - conditionMessage(e) - ) -}) -all_products <- c("reanalysis", "ensemble_members", - "ensemble mean", "ensemble_spread") - -if (product_types == "all") { - product_types <- all_products -} - -# Check that all product types are valid -if (any(!product_types %in% all_products)) { - bad_products <- setdiff(product_types, all_products) - PEcAn.logger::logger.severe(sprintf( - "Invalid product types %s. Products must be one of the following: %s", - paste0("`", bad_products, "`", collapse = ", "), - paste0("`", all_products, "`", collapse = ", ") - )) -} +# mocking functions +mockery::stub(PEcAn.DB::convert_input, 'dbfile.input.check', data.frame()) +mockery::stub(PEcAn.DB::convert_input, 'db.query', data.frame(id = 1)) +tmpdir <- tempfile(pattern = "era5Data") +dir.create(tmpdir) +on.exit(teardown(unlink(tmpdir, recursive = TRUE))) -variables <- tibble::tribble( - ~cf_name, ~units, ~api_name, ~ncdf_name, - "air_temperature", "Kelvin", "2m_temperature", "t2m", - "air_pressure", "Pa", "surface_pressure", NA_character_, - NA_character_, "Kelvin", "2m_dewpoint_temperature", NA_character_, - "precipitation_flux", "kg/m2/s", "total_precipitation", NA_character_, - "eastward_wind", "m/s", "10m_u_component_of_wind", NA_character_, - "northward_wind", "m/s", "10m_v_component_of_wind", NA_character_, - "surface_downwelling_shortwave_flux_in_air", "W/m2", "surface_solar_radiation_downwards", NA_character_, - "surface_downwelling_longwave_flux_in_air", "W/m2", "surface_thermal_radiation_downwards", NA_character_ +PEcAn.DB::convert_input( + input.id = NA, + outfolder = tmpdir, + formatname = NULL, + mimetype = NULL, + site.id = 1, + start_date = "2010-01-01", + end_date = "2010-02-01", + pkg = 'PEcAn.data.atmosphere', + fcn = 'download.ERA5.old', + con = NULL, + host = data.frame(name = "localhost"), + browndog = NULL, + write = FALSE, + lat.in = 45.5594, + lon.in = -84.6738, + product_types <- "all", + reticulate_python <- NULL ) -nvar <- nrow(variables) - -area <- rep(round(c(lat.in, lon.in) * 4) / 4, 2) - -files <- character() -dir.create(outfolder, showWarnings = FALSE) - -# First, download all the files -for (i in seq_len(nvar)) { - var <- variables[["api_name"]][[i]] - PEcAn.logger::logger.debug(glue::glue( - "Downloading variable {i} of {nvar} ({var})." - )) - fname <- file.path(outfolder, paste("era5", var, "nc", sep = ".")) - if (file.exists(fname) && !overwrite) { - PEcAn.logger::logger.warn(glue::glue( - "File `{fname}` already exists, and `overwrite` is FALSE. ", - "Skipping to next variable." - )) - next - } - do_next <- tryCatch({ - cclient$retrieve( - "reanalysis-era5-single-levels", - list( - variable = var, - product_type = 'ensemble_members', - date = paste(start_date, end_date, sep = "/"), - time = "00/to/23/by/1", - area = area, - grid = c(0.25, 0.25), - format = "netcdf" - ), - fname - ) - FALSE - }, error = function(e) { - PEcAn.logger::logger.warn( - glue::glue( - "Failed to download variable `{var}`. ", - "Skipping to next variable. ", - "Error message was:\n", - conditionMessage(e) - ) - ) - TRUE - }) - - if (isTRUE(do_next)) next - files <- c(files, fname) -} - -test_that("Downloaded files exist in the specified directory", { - for (i in files) { - expect_true(file.exists(i)) - } -}) From 17655acf08ed8143d37c3e7fbf662ee5e5e496ff Mon Sep 17 00:00:00 2001 From: meetagrawal09 Date: Wed, 23 Aug 2023 15:43:06 +0530 Subject: [PATCH 25/32] moved test inside a function --- .../integrationTests/test.download.ERA5.R | 61 +++++++++++-------- 1 file changed, 35 insertions(+), 26 deletions(-) diff --git a/modules/data.atmosphere/inst/integrationTests/test.download.ERA5.R b/modules/data.atmosphere/inst/integrationTests/test.download.ERA5.R index ab9b3a49845..ed8d6e99048 100644 --- a/modules/data.atmosphere/inst/integrationTests/test.download.ERA5.R +++ b/modules/data.atmosphere/inst/integrationTests/test.download.ERA5.R @@ -1,36 +1,45 @@ library(testthat) -# putting logger to debug mode -PEcAn.logger::logger.setUseConsole(TRUE, FALSE) -on.exit(PEcAn.logger::logger.setUseConsole(TRUE, TRUE), add = TRUE) -PEcAn.logger::logger.setLevel("DEBUG") +test_download_ERA5 <- function(start_date, end_date, lat.in, lon.in, product_types, reticulate_python) { + # putting logger to debug mode + PEcAn.logger::logger.setUseConsole(TRUE, FALSE) + on.exit(PEcAn.logger::logger.setUseConsole(TRUE, TRUE), add = TRUE) + PEcAn.logger::logger.setLevel("DEBUG") -# mocking functions -mockery::stub(PEcAn.DB::convert_input, 'dbfile.input.check', data.frame()) -mockery::stub(PEcAn.DB::convert_input, 'db.query', data.frame(id = 1)) + # mocking functions + mockery::stub(PEcAn.DB::convert_input, 'dbfile.input.check', data.frame()) + mockery::stub(PEcAn.DB::convert_input, 'db.query', data.frame(id = 1)) -tmpdir <- tempfile(pattern = "era5Data") -dir.create(tmpdir) -on.exit(teardown(unlink(tmpdir, recursive = TRUE))) + withr::with_dir(tempdir(), { + tmpdir <- getwd() + PEcAn.DB::convert_input( + input.id = NA, + outfolder = tmpdir, + formatname = NULL, + mimetype = NULL, + site.id = 1, + start_date = start_date, + end_date = end_date, + pkg = 'PEcAn.data.atmosphere', + fcn = 'download.ERA5.old', + con = NULL, + host = data.frame(name = "localhost"), + browndog = NULL, + write = FALSE, + lat.in = lat.in, + lon.in = lon.in, + product_types = product_types, + reticulate_python = reticulate_python + ) + }) +} -PEcAn.DB::convert_input( - input.id = NA, - outfolder = tmpdir, - formatname = NULL, - mimetype = NULL, - site.id = 1, +test_download_ERA5( start_date = "2010-01-01", end_date = "2010-02-01", - pkg = 'PEcAn.data.atmosphere', - fcn = 'download.ERA5.old', - con = NULL, - host = data.frame(name = "localhost"), - browndog = NULL, - write = FALSE, lat.in = 45.5594, lon.in = -84.6738, - product_types <- "all", - reticulate_python <- NULL -) - + product_types = "all", + reticulate_python = NULL +) \ No newline at end of file From acb23b4e9f73db64ad97467d15fe2b903c9865b1 Mon Sep 17 00:00:00 2001 From: meetagrawal09 Date: Wed, 23 Aug 2023 16:21:06 +0530 Subject: [PATCH 26/32] moved test to function, using withr for tempfile --- .../test.download.AmerifluxLBL.R | 62 +++++++++++-------- 1 file changed, 37 insertions(+), 25 deletions(-) diff --git a/modules/data.atmosphere/inst/integrationTests/test.download.AmerifluxLBL.R b/modules/data.atmosphere/inst/integrationTests/test.download.AmerifluxLBL.R index 3ab07a98939..4a0c4703b9f 100644 --- a/modules/data.atmosphere/inst/integrationTests/test.download.AmerifluxLBL.R +++ b/modules/data.atmosphere/inst/integrationTests/test.download.AmerifluxLBL.R @@ -1,31 +1,43 @@ -# putting logger to debug mode -PEcAn.logger::logger.setUseConsole(TRUE, FALSE) -on.exit(PEcAn.logger::logger.setUseConsole(TRUE, TRUE), add = TRUE) -PEcAn.logger::logger.setLevel("DEBUG") +library(testthat) +test_download_AmerifluxLBL <- function(start_date, end_date, sitename, lat.in, lon.in) { + # putting logger to debug mode + PEcAn.logger::logger.setUseConsole(TRUE, FALSE) + on.exit(PEcAn.logger::logger.setUseConsole(TRUE, TRUE), add = TRUE) + PEcAn.logger::logger.setLevel("DEBUG") -# mocking functions -mockery::stub(PEcAn.DB::convert_input, 'dbfile.input.check', data.frame()) -mockery::stub(PEcAn.DB::convert_input, 'db.query', data.frame(id = 1)) + # mocking functions + mockery::stub(PEcAn.DB::convert_input, 'dbfile.input.check', data.frame()) + mockery::stub(PEcAn.DB::convert_input, 'db.query', data.frame(id = 1)) -# calling download function -PEcAn.DB::convert_input( - input.id = NA, - outfolder = "testAmerifluxData", - formatname = NULL, - mimetype = NULL, - site.id = 1, + withr::with_dir(tempdir(), { + tmpdir <- getwd() + # calling download function + PEcAn.DB::convert_input( + input.id = NA, + outfolder = tmpdir, + formatname = NULL, + mimetype = NULL, + site.id = 1, + start_date = start_date, + end_date = end_date, + pkg = 'PEcAn.data.atmosphere', + fcn = 'download.AmerifluxLBL', + con = NULL, + host = data.frame(name = "localhost"), + browndog = NULL, + write = FALSE, + lat.in = lat.in, + lon.in = lon.in, + sitename = sitename + ) + }) +} + +test_download_AmerifluxLBL( start_date = "2011-01-01", end_date = "2011-12-31", - pkg = 'PEcAn.data.atmosphere', - fcn = 'download.AmerifluxLBL', - con = NULL, - host = data.frame(name = "localhost"), - browndog = NULL, - write = FALSE, + sitename = 'US-Akn', lat.in = 40, - lon.in = -88, - sitename = 'US-Akn' -) - -# checking the downloaded files + lon.in = -88 +) \ No newline at end of file From a009c1ad3079a4ff28b2abe0dfe40eff2d0c067b Mon Sep 17 00:00:00 2001 From: meetagrawal09 Date: Wed, 23 Aug 2023 18:50:41 +0530 Subject: [PATCH 27/32] added test : check if file exists --- .../inst/integrationTests/test.download.AmerifluxLBL.R | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/modules/data.atmosphere/inst/integrationTests/test.download.AmerifluxLBL.R b/modules/data.atmosphere/inst/integrationTests/test.download.AmerifluxLBL.R index 4a0c4703b9f..24401898180 100644 --- a/modules/data.atmosphere/inst/integrationTests/test.download.AmerifluxLBL.R +++ b/modules/data.atmosphere/inst/integrationTests/test.download.AmerifluxLBL.R @@ -32,6 +32,11 @@ test_download_AmerifluxLBL <- function(start_date, end_date, sitename, lat.in, l sitename = sitename ) }) + + # checking if the file is downloaded + test_that("Downloaded files are present in the desired location", { + expect_true(file.exists(paste0(tmpdir, "/AMF_US-Akn_BASE_HH_6-5.csv"))) + }) } test_download_AmerifluxLBL( From 7ced84e60d799f2a4fc28e82b234dc019b910b04 Mon Sep 17 00:00:00 2001 From: meetagrawal09 Date: Thu, 24 Aug 2023 15:13:53 +0530 Subject: [PATCH 28/32] added tests : check files, check units --- .../integrationTests/test.download.ERA5.R | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/modules/data.atmosphere/inst/integrationTests/test.download.ERA5.R b/modules/data.atmosphere/inst/integrationTests/test.download.ERA5.R index ed8d6e99048..90f37c8e0c0 100644 --- a/modules/data.atmosphere/inst/integrationTests/test.download.ERA5.R +++ b/modules/data.atmosphere/inst/integrationTests/test.download.ERA5.R @@ -1,4 +1,5 @@ library(testthat) +library(ncdf4) test_download_ERA5 <- function(start_date, end_date, lat.in, lon.in, product_types, reticulate_python) { # putting logger to debug mode @@ -11,6 +12,10 @@ test_download_ERA5 <- function(start_date, end_date, lat.in, lon.in, product_typ mockery::stub(PEcAn.DB::convert_input, 'dbfile.input.check', data.frame()) mockery::stub(PEcAn.DB::convert_input, 'db.query', data.frame(id = 1)) + # additional mocks needed since download.ERA5 does not return data as other download functions + mockery::stub(PEcAn.DB::convert_input, 'length', 2) + mockery::stub(PEcAn.DB::convert_input, 'purrr::map_dfr', data.frame(missing = c(FALSE), empty = c(FALSE))) + withr::with_dir(tempdir(), { tmpdir <- getwd() PEcAn.DB::convert_input( @@ -33,6 +38,51 @@ test_download_ERA5 <- function(start_date, end_date, lat.in, lon.in, product_typ reticulate_python = reticulate_python ) }) + + test_that("All the required files are downloaded and stored at desired location", { + expect_true(file.exists(paste0(tmpdir, "/era5.2m_dewpoint_temperature.nc"))) + expect_true(file.exists(paste0(tmpdir, "/era5.2m_temperature.nc"))) + expect_true(file.exists(paste0(tmpdir, "/era5.10m_u_component_of_wind.nc"))) + expect_true(file.exists(paste0(tmpdir, "/era5.10m_v_component_of_wind.nc"))) + expect_true(file.exists(paste0(tmpdir, "/era5.surface_pressure.nc"))) + expect_true(file.exists(paste0(tmpdir, "/era5.surface_solar_radiation_downwards.nc"))) + expect_true(file.exists(paste0(tmpdir, "/era5.surface_thermal_radiation_downwards.nc"))) + expect_true(file.exists(paste0(tmpdir, "/era5.total_precipitation.nc"))) + }) + + test_that("All ERA5 data files have the correct variable units", { + nc <- nc_open(paste0(tmpdir, "/era5.2m_dewpoint_temperature.nc")) + expect_equal(nc$var$d2m$units, "K") + nc_close(nc) + + nc <- nc_open(paste0(tmpdir, "/era5.2m_temperature.nc")) + expect_equal(nc$var$t2m$units, "K") + nc_close(nc) + + nc <- nc_open(paste0(tmpdir, "/era5.10m_u_component_of_wind.nc")) + expect_equal(nc$var$u10$units, "m s**-1") + nc_close(nc) + + nc <- nc_open(paste0(tmpdir, "/era5.10m_v_component_of_wind.nc")) + expect_equal(nc$var$v10$units, "m s**-1") + nc_close(nc) + + nc <- nc_open(paste0(tmpdir, "/era5.surface_pressure.nc")) + expect_equal(nc$var$sp$units, "Pa") + nc_close(nc) + + nc <- nc_open(paste0(tmpdir, "/era5.surface_solar_radiation_downwards.nc")) + expect_equal(nc$var$ssrd$units, "J m**-2") + nc_close(nc) + + nc <- nc_open(paste0(tmpdir, "/era5.surface_thermal_radiation_downwards.nc")) + expect_equal(nc$var$strd$units, "J m**-2") + nc_close(nc) + + nc <- nc_open(paste0(tmpdir, "/era5.total_precipitation.nc")) + expect_equal(nc$var$tp$units, "m") + nc_close(nc) + }) } test_download_ERA5( From 6c7988ef1e67d6bf1b1f7b2609219ded10e42fc0 Mon Sep 17 00:00:00 2001 From: meetagrawal09 Date: Thu, 24 Aug 2023 18:56:07 +0530 Subject: [PATCH 29/32] added tests : check files, check units --- .../test.download.AmerifluxLBL.R | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/modules/data.atmosphere/inst/integrationTests/test.download.AmerifluxLBL.R b/modules/data.atmosphere/inst/integrationTests/test.download.AmerifluxLBL.R index 24401898180..e46d1d4830f 100644 --- a/modules/data.atmosphere/inst/integrationTests/test.download.AmerifluxLBL.R +++ b/modules/data.atmosphere/inst/integrationTests/test.download.AmerifluxLBL.R @@ -13,7 +13,7 @@ test_download_AmerifluxLBL <- function(start_date, end_date, sitename, lat.in, l withr::with_dir(tempdir(), { tmpdir <- getwd() # calling download function - PEcAn.DB::convert_input( + res <- PEcAn.DB::convert_input( input.id = NA, outfolder = tmpdir, formatname = NULL, @@ -34,9 +34,21 @@ test_download_AmerifluxLBL <- function(start_date, end_date, sitename, lat.in, l }) # checking if the file is downloaded - test_that("Downloaded files are present in the desired location", { + test_that("Downloaded files are present at the desired location", { expect_true(file.exists(paste0(tmpdir, "/AMF_US-Akn_BASE_HH_6-5.csv"))) }) + + test_that("Downloaded data files have the right format", { + firstline <- system(paste0("head -4 ", paste0(tmpdir, "/AMF_US-Akn_BASE_HH_6-5.csv")), intern = TRUE) + lastline <- system(paste0("tail -1 ", paste0(tmpdir, "/AMF_US-Akn_BASE_HH_6-5.csv")), intern = TRUE) + + # checking if first line of CSV has the sitename + expect_true(grepl(sitename, firstline[1])) + + # fourth and last row checked to contain non-alphabetical data since these are used to verify start and end dates + expect_false(grepl("[A-Za-z]", firstline[4])) + expect_false(grepl("[A-Za-z]", lastline[1])) + }) } test_download_AmerifluxLBL( From c7dd2c6b47102b290cff8118b1511260bf2045d3 Mon Sep 17 00:00:00 2001 From: meetagrawal09 Date: Sat, 26 Aug 2023 19:32:08 +0530 Subject: [PATCH 30/32] tested plot_data --- base/visualization/tests/testthat/test.plot_data.R | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/base/visualization/tests/testthat/test.plot_data.R b/base/visualization/tests/testthat/test.plot_data.R index 12fefd6c47f..25264be0474 100644 --- a/base/visualization/tests/testthat/test.plot_data.R +++ b/base/visualization/tests/testthat/test.plot_data.R @@ -1,3 +1,7 @@ -test_that("`plot_data()`", { - +test_that("`plot_data()` able to create a new plot for data passed to it", { + withr::with_dir(tempdir(), { + res <- plot_data(data.frame(Y = c(1, 2), se = c(1,2), trt = c(1, 2)), base.plot = NULL, ymax = 10) + print(res) + expect_true(file.exists(paste0(getwd(), "/Rplots.pdf"))) + }) }) \ No newline at end of file From 87b91d6b85272ec137bf67d1412968fa6e77ea4b Mon Sep 17 00:00:00 2001 From: meetagrawal09 Date: Sat, 26 Aug 2023 20:00:37 +0530 Subject: [PATCH 31/32] clean up --- base/visualization/tests/testthat/test.ciEnvelope.R | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 base/visualization/tests/testthat/test.ciEnvelope.R diff --git a/base/visualization/tests/testthat/test.ciEnvelope.R b/base/visualization/tests/testthat/test.ciEnvelope.R deleted file mode 100644 index a0b4548a702..00000000000 --- a/base/visualization/tests/testthat/test.ciEnvelope.R +++ /dev/null @@ -1,3 +0,0 @@ -test_that("`ciEnvelope()`", { - -}) \ No newline at end of file From a5612f9312830c79df060faa8d28be0b8fe422f5 Mon Sep 17 00:00:00 2001 From: meetagrawal09 Date: Sun, 27 Aug 2023 10:01:38 +0530 Subject: [PATCH 32/32] tested met_inputs --- base/db/tests/testthat/test.met_inputs.R | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 base/db/tests/testthat/test.met_inputs.R diff --git a/base/db/tests/testthat/test.met_inputs.R b/base/db/tests/testthat/test.met_inputs.R new file mode 100644 index 00000000000..49d75b7b379 --- /dev/null +++ b/base/db/tests/testthat/test.met_inputs.R @@ -0,0 +1,10 @@ +test_that("`met_inputs()` able to correctly place input parameters in the database query to retrieve available met inputs", { + mocked_res <- mockery::mock(0) + mockery::stub(met_inputs, 'db.query', mocked_res) + met_inputs(dbcon = NULL, site_id = 100, model_id = 200, hostname = "pecan") + args <- mockery::mock_args(mocked_res) + + expect_true( + grepl("inputs.site_id = \\$1.*machines.hostname = \\$2.*models.id = \\$3", args[[1]][[1]]) + ) +}) \ No newline at end of file