-
Notifications
You must be signed in to change notification settings - Fork 26
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
chore: use v1 timezones API #315
Conversation
- use na instead of null for no value - update function name - add tests for version getting
made possible by caching version on the client object
tests/testthat/test-schedule.R
Outdated
MockConnect <- R6Class( | ||
"MockConnect", | ||
inherit = Connect, | ||
public = list( | ||
initialize = function(version = NA) { | ||
self$server <- "https://connect.example" | ||
self$api_key <- "fake" | ||
private$.version <- version | ||
}, | ||
request = function(method, url, ..., parser = "parsed") { | ||
# Record call | ||
self$log_call(paste(method, url)) | ||
|
||
# Look for response | ||
if (!(url %in% names(self$responses))) { | ||
stop("Unexpected URL") | ||
} | ||
res <- self$responses[[url]] | ||
|
||
if (is.null(parser)) { | ||
res | ||
} else { | ||
self$raise_error(res) | ||
httr::content(res, as = parser) | ||
} | ||
}, | ||
responses = list(), | ||
add_mock_response = function(path, content, status_code = 200L, headers = c("Content-Type" = "application/json; charset=utf-8")) { | ||
url <- self$api_url(path) | ||
res <- mock_response(url, content, status_code, headers) | ||
self$responses[[url]] <- res | ||
}, | ||
call_log = character(), | ||
log_call = function(call) { | ||
self$call_log <- c(self$call_log, call) | ||
} | ||
) | ||
) | ||
|
||
mock_response <- function(url, content, status_code, headers = character()) { | ||
# Headers in responses are case-insensitive lists. | ||
names(headers) <- tolower(names(headers)) | ||
headers <- as.list(headers) | ||
headers <- structure(as.list(headers), class = c("insensitive", class(headers))) | ||
|
||
# Treat content similarly to httr::POST, with a subset of behaviors | ||
if (is.character(content) && length(content) == 1) { | ||
content <- charToRaw(content) | ||
} else if (is.list(content)) { | ||
content <- charToRaw(toJSON(content, auto_unbox = TRUE)) | ||
} | ||
|
||
structure( | ||
list( | ||
url = url, | ||
status_code = status_code, | ||
request = structure(list(url = url), class = "request"), | ||
headers = headers, | ||
content = content | ||
), | ||
class = "response" | ||
) | ||
} | ||
|
||
mock_response_404 <- function(url) { | ||
mock_response(url, content = "404 page not found", status_code = 404L, headers = c("Content-Type" = "text/plain")) | ||
} | ||
|
||
|
||
mock_response( | ||
url = "v1/timezones", | ||
body = list( | ||
list(timezone = "Africa/Abidjan", offset = "+00:00"), | ||
list(timezone = "Africa/Accra", offset = "+00:00") | ||
) | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this something you're having trouble getting httptest
to do? This pattern should be possible:
https://github.com/Crunch-io/rcrunch/blob/b709e282172ee6b813b26299c28514ea840c47a6/tests/testthat/test-api.R#L117-L124 and the fixture https://github.com/Crunch-io/rcrunch/blob/b709e282172ee6b813b26299c28514ea840c47a6/tests/testthat/app.crunch.io/404.R#L1-L18 are one example of this.
I know that you mentioned wanting to experiment with possible other test runners / setups for the http testing, but I don't think this is the place we should do that. Let's keep using what we have and defer taking a new testing approach until after we have at least a few PRs on our journey of improving the recipes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This commit was pushed up in a state I had not intended to receive review. I had a 1-on-1 with @nealrichardson earlier today and asked some questions about the approach to testing that I had taken with this PR, and he gave some thoughts on streamlining it that I implemented after our chat.
I was motivated to explore this approach as I was adding fixtures for the tests yesterday. There are three scenarios I want to test:
- Both endpoints respond
- The new endpoint returns a 404
- The new and old endpoints returns a 404
So far, we have two mock dirs that are used for different versions of Connect. I can use those to mark out the two scenarios, but to test a third scenario requires adding another mock directory, and — I'm loathe to move to a model where we have additional mock dirs for each testing scenario, and managing tests of simple scenarios that are split out across an increasingly complex file tree has been a headache.
I threw this together in a few hours. It seems to work, and I @nealrichardson's suggestions made it much tighter. I also have found the workflow a good fit for me personally, and I think the tests are nicely concise and contained — it feels like a good fit for this kind of narrowly-scoped unit test.
I'm actually really excited about this approach — I feel like I've solved a piece of workflow friction that has been slowing me down.
a7f7b1d
to
adc2041
Compare
@@ -337,7 +337,13 @@ schedule_describe <- function(.schedule) { | |||
#' @family schedule functions | |||
#' @export | |||
get_timezones <- function(connect) { | |||
raw_tz <- connect$GET(unversioned_url("timezones")) | |||
raw_tz <- tryCatch( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jonkeane One problem using tryCatch is that connectapi
emits a message, not just an error, when it hits an HTTP error. This means that even if we catch the error, a message is emitted saying 404 page not found
when the incorrect URL is tried. This may have been our original motivation for not parsing the responses in the thumbnail
API. I'll add a comment to this thread with a code ref for the error handling.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lines 79 to 96 in adc2041
raise_error = function(res) { | |
if (httr::http_error(res)) { | |
err <- sprintf( | |
"%s request failed with %s", | |
res$request$url, | |
httr::http_status(res)$message | |
) | |
tryCatch( | |
{ | |
message(capture.output(str(httr::content(res)))) | |
}, | |
error = function(e) { | |
message(e) | |
} | |
) | |
stop(err) | |
} | |
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll go ahead and create an issue to track this so we don't need to treat it as blocking this work.
[EDIT] #317
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@toph-allen and I discussed this over Zoom. Since it isn't directly relevant to this feature, we're going to create a separate issue to capture the oddities.
Generally speaking, a console write should always be accomplished through a logging interface. That way the user has some control over the messages (e.g.., disabling them). And even then, a logging interface is usually overkill. In my opinion, this type of message should just be part of the raised error. Then the program can handle the error as needed.
tests/testthat/test-schedule.R
Outdated
client$mock_response( | ||
path = "v1/timezones", | ||
content = "404 page not found", | ||
status_code = 404L, | ||
headers = c("Content-Type" = "text/plain; charset=utf-8") | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I personally like this abstraction quite a bit. It removes a lot of the boilerplate and clarifies what's being augmented for the purpose of the test.
Also, This looks fairly similar to the responses
package we are using in Python.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Implementation looks good to me. Have we had a chance to test this against live instances of the relevant Connect versions?
I've tested it against Dogfood. But -- what I also am reminded is that I should try to add an integration test for this. |
The I manually built Connect 2024.08.0 locally and tested against that to manually verify against relevant versions. |
Co-authored-by: Neal Richardson <[email protected]>
Intent
This PR contains two sets of changes, which were interrelated at the start of this PR but are now less so:
Fixes #300
Approach
Time zones
The time zones change is relatively simple.
The server-version-related refactoring is in here because I originally was using the time zones API switch to think about how the logic for choosing a URL would look if it also keyed off the server version. It made the logic much more complicated than was necessary, so I removed that logic from the
get_timezones()
function, but I have kept some of the version-getting-related refactoring. Another option would be moving it to a different PR.Server version
The gist of the refactoring is:
Connect$version
is now an active binding on theConnect
object, a private property that isNULL
by default, and a character vector when fetched.NA
indicates that the version is hidden.This could easily be removed from this PR and tabled or submitted in a separate PR.
Testing
I've been finding it difficult to manage our testing fixtures here — we have 79 files in our fixtures dirs right now, and managing tests when the required context is distributed in that file hierarchy has been a cognitive challenge. For simple unit tests like this, I thought an approach where I stub out the
request
method in the subclass would provide a few benefits.Checklist
NEWS.md
(referencing the connected issue if necessary)?