diff --git a/src/test/webdriver.rs b/src/test/webdriver.rs index 8384bbe..b9619e1 100644 --- a/src/test/webdriver.rs +++ b/src/test/webdriver.rs @@ -1,12 +1,22 @@ //! Getting WebDriver client binaries. +mod chromedriver; +mod geckodriver; +mod safaridriver; + use binary_install::Cache; use failure; -use install::InstallMode; use std::path::PathBuf; -use target; use PBAR; +pub use self::{ + chromedriver::{get_or_install_chromedriver, install_chromedriver}, + geckodriver::{get_or_install_geckodriver, install_geckodriver}, + safaridriver::get_safaridriver, +}; + +// ------ driver helpers ------ + fn get_and_notify( cache: &Cache, installation_allowed: bool, @@ -25,100 +35,11 @@ fn get_and_notify( } } -/// Get the path to an existing `chromedriver`, or install it if no existing -/// binary is found. -pub fn get_or_install_chromedriver( - cache: &Cache, - mode: InstallMode, -) -> Result { - if let Ok(path) = which::which("chromedriver") { - return Ok(path); - } - install_chromedriver(cache, mode.install_permitted()) -} - -/// Download and install a pre-built `chromedriver` binary. -pub fn install_chromedriver( - cache: &Cache, - installation_allowed: bool, -) -> Result { - let target = if target::LINUX && target::x86_64 { - "linux64" - } else if target::MACOS && target::x86_64 { - "mac64" - } else if target::WINDOWS { - "win32" - } else { - bail!("chromedriver binaries are unavailable for this target") - }; - - let url = format!( - "https://chromedriver.storage.googleapis.com/2.46/chromedriver_{}.zip", - target - ); - match get_and_notify(cache, installation_allowed, "chromedriver", &url)? { - Some(path) => Ok(path), - None => bail!( - "No cached `chromedriver` binary found, and could not find a global \ - `chromedriver` on the `$PATH`. Not installing `chromedriver` because of noinstall \ - mode." - ), - } -} - -/// Get the path to an existing `geckodriver`, or install it if no existing -/// binary is found. -pub fn get_or_install_geckodriver( - cache: &Cache, - mode: InstallMode, -) -> Result { - if let Ok(path) = which::which("geckodriver") { - return Ok(path); - } - install_geckodriver(cache, mode.install_permitted()) -} - -/// Download and install a pre-built `geckodriver` binary. -pub fn install_geckodriver( - cache: &Cache, - installation_allowed: bool, -) -> Result { - let (target, ext) = if target::LINUX && target::x86 { - ("linux32", "tar.gz") - } else if target::LINUX && target::x86_64 { - ("linux64", "tar.gz") - } else if target::MACOS { - ("macos", "tar.gz") - } else if target::WINDOWS && target::x86 { - ("win32", "zip") - } else if target::WINDOWS && target::x86_64 { - ("win64", "zip") - } else { - bail!("geckodriver binaries are unavailable for this target") - }; - - let url = format!( - "https://github.com/mozilla/geckodriver/releases/download/v0.24.0/geckodriver-v0.24.0-{}.{}", - target, - ext, - ); - match get_and_notify(cache, installation_allowed, "geckodriver", &url)? { - Some(path) => Ok(path), - None => bail!( - "No cached `geckodriver` binary found, and could not find a global `geckodriver` \ - on the `$PATH`. Not installing `geckodriver` because of noinstall mode." - ), - } -} +struct Collector(Vec); -/// Get the path to an existing `safaridriver`. -/// -/// We can't install `safaridriver` if an existing one is not found because -/// Apple does not provide pre-built binaries. However, `safaridriver` *should* -/// be present by default. -pub fn get_safaridriver() -> Result { - match which::which("safaridriver") { - Ok(p) => Ok(p), - Err(_) => bail!("could not find `safaridriver` on the `$PATH`"), +impl curl::easy::Handler for Collector { + fn write(&mut self, data: &[u8]) -> Result { + self.0.extend_from_slice(data); + Ok(data.len()) } } diff --git a/src/test/webdriver/chromedriver.rs b/src/test/webdriver/chromedriver.rs new file mode 100644 index 0000000..def432c --- /dev/null +++ b/src/test/webdriver/chromedriver.rs @@ -0,0 +1,77 @@ +use super::{get_and_notify, Collector}; +use binary_install::Cache; +use failure; +use install::InstallMode; +use std::path::PathBuf; +use target; + +/// Get the path to an existing `chromedriver`, or install it if no existing +/// binary is found or if there is a new binary version. +pub fn get_or_install_chromedriver( + cache: &Cache, + mode: InstallMode, +) -> Result { + if let Ok(path) = which::which("chromedriver") { + return Ok(path); + } + install_chromedriver(cache, mode.install_permitted()) +} + +/// Download and install a pre-built `chromedriver` binary. +pub fn install_chromedriver( + cache: &Cache, + installation_allowed: bool, +) -> Result { + let target = if target::LINUX && target::x86_64 { + "linux64" + } else if target::MACOS && target::x86_64 { + "mac64" + } else if target::WINDOWS { + "win32" + } else { + bail!("chromedriver binaries are unavailable for this target") + }; + + let url = get_chromedriver_url(target)?; + + match get_and_notify(cache, installation_allowed, "chromedriver", &url)? { + Some(path) => Ok(path), + None => bail!( + "No cached `chromedriver` binary found, and could not find a global \ + `chromedriver` on the `$PATH`. Not installing `chromedriver` because of noinstall \ + mode." + ), + } +} + +/// Get `chromedriver` download URL. +/// +/// It returns the latest one without checking the installed `Chrome` version +/// because it's not easy to find out `Chrome` version on `Windows` - +/// https://bugs.chromium.org/p/chromium/issues/detail?id=158372 +/// +/// The official algorithm for `chromedriver` version selection: +/// https://chromedriver.chromium.org/downloads/version-selection +fn get_chromedriver_url(target: &str) -> Result { + let chromedriver_version = fetch_chromedriver_version()?; + Ok(assemble_chromedriver_url(&chromedriver_version, target)) +} + +// ------ `get_chromedriver_url` steps ------ + +fn fetch_chromedriver_version() -> Result { + let mut handle = curl::easy::Easy2::new(Collector(Vec::new())); + handle.url("https://chromedriver.storage.googleapis.com/LATEST_RELEASE")?; + handle.perform()?; + + let contents = handle.get_ref(); + Ok(String::from_utf8_lossy(&contents.0).into_owned()) +} + +fn assemble_chromedriver_url(chromedriver_version: &str, target: &str) -> String { + format!( + "https://chromedriver.storage.googleapis.com/{version}/chromedriver_{target}.zip", + version = chromedriver_version, + target = target, + ) +} diff --git a/src/test/webdriver/geckodriver.rs b/src/test/webdriver/geckodriver.rs new file mode 100644 index 0000000..2a86871 --- /dev/null +++ b/src/test/webdriver/geckodriver.rs @@ -0,0 +1,93 @@ +use super::{get_and_notify, Collector}; +use binary_install::Cache; +use failure; +use install::InstallMode; +use std::path::PathBuf; +use target; + +/// Get the path to an existing `geckodriver`, or install it if no existing +/// binary is found or if there is a new binary version. +pub fn get_or_install_geckodriver( + cache: &Cache, + mode: InstallMode, +) -> Result { + if let Ok(path) = which::which("geckodriver") { + return Ok(path); + } + install_geckodriver(cache, mode.install_permitted()) +} + +/// Download and install a pre-built `geckodriver` binary. +pub fn install_geckodriver( + cache: &Cache, + installation_allowed: bool, +) -> Result { + let (target, ext) = if target::LINUX && target::x86 { + ("linux32", "tar.gz") + } else if target::LINUX && target::x86_64 { + ("linux64", "tar.gz") + } else if target::MACOS { + ("macos", "tar.gz") + } else if target::WINDOWS && target::x86 { + ("win32", "zip") + } else if target::WINDOWS && target::x86_64 { + ("win64", "zip") + } else { + bail!("geckodriver binaries are unavailable for this target") + }; + + let url = get_geckodriver_url(target, ext)?; + + match get_and_notify(cache, installation_allowed, "geckodriver", &url)? { + Some(path) => Ok(path), + None => bail!( + "No cached `geckodriver` binary found, and could not find a global `geckodriver` \ + on the `$PATH`. Not installing `geckodriver` because of noinstall mode." + ), + } +} + +/// Get `geckodriver` download URL. +/// +/// It returns the latest one without checking the installed `Firefox` version +/// - it should be relatively safe because each `geckodriver` supports many `Firefox` versions: +/// https://firefox-source-docs.mozilla.org/testing/geckodriver/Support.html#supported-platforms +fn get_geckodriver_url(target: &str, ext: &str) -> Result { + // JSON example: `{"id":15227534,"tag_name":"v0.24.0","update_url":"/mozzila...` + let latest_tag_json = fetch_latest_geckodriver_tag_json()?; + let latest_tag = get_tag_name_from_json(&latest_tag_json)?; + Ok(assemble_geckodriver_url(&latest_tag, target, ext)) +} + +// ------ `get_geckodriver_url` steps ------ + +fn fetch_latest_geckodriver_tag_json() -> Result { + let mut headers = curl::easy::List::new(); + headers.append("Accept: application/json")?; + + let mut handle = curl::easy::Easy2::new(Collector(Vec::new())); + handle.url("https://github.com/mozilla/geckodriver/releases/latest")?; + handle.http_headers(headers)?; + // We will be redirected from the `latest` placeholder to the specific tag name. + handle.follow_location(true)?; + handle.perform()?; + + let contents = handle.get_ref(); + Ok(String::from_utf8_lossy(&contents.0).into_owned()) +} + +fn get_tag_name_from_json(json: &str) -> Result { + let json: serde_json::Value = serde_json::from_str(json)?; + json.get("tag_name") + .and_then(|tag_name| tag_name.as_str().map(ToOwned::to_owned)) + .ok_or_else(|| failure::err_msg("cannot get `tag_name` from JSON response")) +} + +fn assemble_geckodriver_url(tag: &str, target: &str, ext: &str) -> String { + format!( + "https://github.com/mozilla/geckodriver/releases/download/{tag}/geckodriver-{tag}-{target}.{ext}", + tag=tag, + target=target, + ext=ext, + ) +} diff --git a/src/test/webdriver/safaridriver.rs b/src/test/webdriver/safaridriver.rs new file mode 100644 index 0000000..867a14d --- /dev/null +++ b/src/test/webdriver/safaridriver.rs @@ -0,0 +1,13 @@ +use std::path::PathBuf; + +/// Get the path to an existing `safaridriver`. +/// +/// We can't install `safaridriver` if an existing one is not found because +/// Apple does not provide pre-built binaries. However, `safaridriver` *should* +/// be present by default. +pub fn get_safaridriver() -> Result { + match which::which("safaridriver") { + Ok(p) => Ok(p), + Err(_) => bail!("could not find `safaridriver` on the `$PATH`"), + } +}