Merge pull request #706 from MartinKavik/fix/webdriver_versions
Select correct webdriver versionmaster
commit
15e354f19c
@ -0,0 +1,58 @@ |
||||
//! Key-value store in `*.stamps` file.
|
||||
|
||||
use failure::{self, ResultExt}; |
||||
use std::{env, fs, path::PathBuf}; |
||||
|
||||
/// Get a value corresponding to the key from the JSON value.
|
||||
///
|
||||
/// You should use return value of function `read_stamps_file_to_json()` as `json` argument.
|
||||
pub fn get_stamp_value( |
||||
key: impl AsRef<str>, |
||||
json: &serde_json::Value, |
||||
) -> Result<String, failure::Error> { |
||||
json.get(key.as_ref()) |
||||
.and_then(|value| value.as_str().map(ToOwned::to_owned)) |
||||
.ok_or_else(|| { |
||||
failure::err_msg(format!("cannot get stamp value for key '{}'", key.as_ref())) |
||||
}) |
||||
} |
||||
|
||||
/// Save the key-value pair to the store.
|
||||
pub fn save_stamp_value( |
||||
key: impl Into<String>, |
||||
value: impl AsRef<str>, |
||||
) -> Result<(), failure::Error> { |
||||
let mut json = read_stamps_file_to_json().unwrap_or_else(|_| serde_json::Map::new().into()); |
||||
|
||||
let stamps = json |
||||
.as_object_mut() |
||||
.ok_or_else(|| failure::err_msg("stamps file doesn't contain JSON object"))?; |
||||
stamps.insert(key.into(), value.as_ref().into()); |
||||
|
||||
write_to_stamps_file(json) |
||||
} |
||||
|
||||
/// Get the path of the `*.stamps` file that is used as the store.
|
||||
pub fn get_stamps_file_path() -> Result<PathBuf, failure::Error> { |
||||
let path = env::current_exe() |
||||
.map(|path| path.with_extension("stamps")) |
||||
.context("cannot get stamps file path")?; |
||||
Ok(path) |
||||
} |
||||
|
||||
/// Read `*.stamps` file and convert its content to the JSON value.
|
||||
pub fn read_stamps_file_to_json() -> Result<serde_json::Value, failure::Error> { |
||||
let stamps_file_path = get_stamps_file_path()?; |
||||
let stamps_file_content = |
||||
fs::read_to_string(stamps_file_path).context("cannot find or read stamps file")?; |
||||
let json: serde_json::Value = serde_json::from_str(&stamps_file_content) |
||||
.context("stamps file doesn't contain valid JSON")?; |
||||
Ok(json) |
||||
} |
||||
|
||||
fn write_to_stamps_file(json: serde_json::Value) -> Result<(), failure::Error> { |
||||
let stamps_file_path = get_stamps_file_path()?; |
||||
let pretty_json = serde_json::to_string_pretty(&json).context("JSON serialization failed")?; |
||||
fs::write(stamps_file_path, pretty_json).context("cannot write to stamps file")?; |
||||
Ok(()) |
||||
} |
@ -0,0 +1,146 @@ |
||||
use super::{get_and_notify, Collector}; |
||||
use binary_install::Cache; |
||||
use chrono::DateTime; |
||||
use failure::{self, ResultExt}; |
||||
use install::InstallMode; |
||||
use stamps; |
||||
use std::path::PathBuf; |
||||
use target; |
||||
|
||||
// Keep it up to date with each `wasm-pack` release.
|
||||
// https://chromedriver.storage.googleapis.com/LATEST_RELEASE
|
||||
const DEFAULT_CHROMEDRIVER_VERSION: &str = "76.0.3809.126"; |
||||
|
||||
const CHROMEDRIVER_LAST_UPDATED_STAMP: &str = "chromedriver_last_updated"; |
||||
const CHROMEDRIVER_VERSION_STAMP: &str = "chromedriver_version"; |
||||
|
||||
/// 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<PathBuf, failure::Error> { |
||||
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<PathBuf, failure::Error> { |
||||
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.
|
||||
///
|
||||
/// _Algorithm_:
|
||||
/// 1. Try to open `*.stamps` file and deserialize its content to JSON object.
|
||||
/// 2. Try to compare current time with the saved one.
|
||||
/// 3. If the saved time is older than 1 day or something failed
|
||||
/// => fetch a new version and save version & time.
|
||||
/// 4. If everything failed, use the default version.
|
||||
/// 5. Return URL.
|
||||
///
|
||||
/// _Notes:_
|
||||
///
|
||||
/// 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) -> String { |
||||
let fetch_and_save_version = |
||||
|| fetch_chromedriver_version().and_then(save_chromedriver_version); |
||||
|
||||
let chromedriver_version = match stamps::read_stamps_file_to_json() { |
||||
Ok(json) => { |
||||
if should_load_chromedriver_version_from_stamp(&json) { |
||||
stamps::get_stamp_value(CHROMEDRIVER_VERSION_STAMP, &json) |
||||
} else { |
||||
fetch_and_save_version() |
||||
} |
||||
} |
||||
Err(_) => fetch_and_save_version(), |
||||
} |
||||
.unwrap_or_else(|error| { |
||||
log::warn!( |
||||
"Cannot load or fetch chromedriver's latest version data, \ |
||||
the default version {} will be used. Error: {}", |
||||
DEFAULT_CHROMEDRIVER_VERSION, |
||||
error |
||||
); |
||||
DEFAULT_CHROMEDRIVER_VERSION.to_owned() |
||||
}); |
||||
assemble_chromedriver_url(&chromedriver_version, target) |
||||
} |
||||
|
||||
// ------ `get_chromedriver_url` helpers ------
|
||||
|
||||
fn save_chromedriver_version(version: String) -> Result<String, failure::Error> { |
||||
stamps::save_stamp_value(CHROMEDRIVER_VERSION_STAMP, &version)?; |
||||
|
||||
let current_time = chrono::offset::Local::now().to_rfc3339(); |
||||
stamps::save_stamp_value(CHROMEDRIVER_LAST_UPDATED_STAMP, current_time)?; |
||||
|
||||
Ok(version) |
||||
} |
||||
|
||||
fn should_load_chromedriver_version_from_stamp(json: &serde_json::Value) -> bool { |
||||
let last_updated = stamps::get_stamp_value(CHROMEDRIVER_LAST_UPDATED_STAMP, json) |
||||
.ok() |
||||
.and_then(|last_updated| DateTime::parse_from_rfc3339(&last_updated).ok()); |
||||
|
||||
match last_updated { |
||||
None => false, |
||||
Some(last_updated) => { |
||||
let current_time = chrono::offset::Local::now(); |
||||
current_time.signed_duration_since(last_updated).num_hours() < 24 |
||||
} |
||||
} |
||||
} |
||||
|
||||
fn fetch_chromedriver_version() -> Result<String, failure::Error> { |
||||
let mut handle = curl::easy::Easy2::new(Collector(Vec::new())); |
||||
handle |
||||
.url("https://chromedriver.storage.googleapis.com/LATEST_RELEASE") |
||||
.context("URL to fetch chromedriver's LATEST_RELEASE is invalid")?; |
||||
handle |
||||
.perform() |
||||
.context("fetching of chromedriver's LATEST_RELEASE failed")?; |
||||
|
||||
let content = handle.get_mut().take_content(); |
||||
let version = |
||||
String::from_utf8(content).context("chromedriver's LATEST_RELEASE is not valid UTF-8")?; |
||||
Ok(version) |
||||
} |
||||
|
||||
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, |
||||
) |
||||
} |
@ -0,0 +1,174 @@ |
||||
use super::{get_and_notify, Collector}; |
||||
use binary_install::Cache; |
||||
use chrono::DateTime; |
||||
use failure::{self, ResultExt}; |
||||
use install::InstallMode; |
||||
use stamps; |
||||
use std::path::PathBuf; |
||||
use target; |
||||
|
||||
// Keep it up to date with each `wasm-pack` release.
|
||||
// https://github.com/mozilla/geckodriver/releases/latest
|
||||
const DEFAULT_GECKODRIVER_VERSION: &str = "v0.24.0"; |
||||
|
||||
const GECKODRIVER_LAST_UPDATED_STAMP: &str = "geckodriver_last_updated"; |
||||
const GECKODRIVER_VERSION_STAMP: &str = "geckodriver_version"; |
||||
|
||||
/// 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<PathBuf, failure::Error> { |
||||
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<PathBuf, failure::Error> { |
||||
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.
|
||||
///
|
||||
/// _Algorithm_:
|
||||
/// 1. Try to open `*.stamps` file and deserialize its content to JSON object.
|
||||
/// 2. Try to compare current time with the saved one.
|
||||
/// 3. If the saved time is older than 1 day or something failed
|
||||
/// => fetch a new version and save version & time.
|
||||
/// 4. If everything failed, use the default version.
|
||||
/// 5. Return URL.
|
||||
///
|
||||
/// _Notes:_
|
||||
///
|
||||
/// 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) -> String { |
||||
let fetch_and_save_version = || { |
||||
fetch_latest_geckodriver_tag_json() |
||||
.and_then(get_version_from_json) |
||||
.and_then(save_geckodriver_version) |
||||
}; |
||||
|
||||
let geckodriver_version = match stamps::read_stamps_file_to_json() { |
||||
Ok(json) => { |
||||
if should_load_geckodriver_version_from_stamp(&json) { |
||||
stamps::get_stamp_value(GECKODRIVER_VERSION_STAMP, &json) |
||||
} else { |
||||
fetch_and_save_version() |
||||
} |
||||
} |
||||
Err(_) => fetch_and_save_version(), |
||||
} |
||||
.unwrap_or_else(|error| { |
||||
log::warn!( |
||||
"Cannot load or fetch geckodriver's latest version data, \ |
||||
the default version {} will be used. Error: {}", |
||||
DEFAULT_GECKODRIVER_VERSION, |
||||
error |
||||
); |
||||
DEFAULT_GECKODRIVER_VERSION.to_owned() |
||||
}); |
||||
assemble_geckodriver_url(&geckodriver_version, target, ext) |
||||
} |
||||
|
||||
// ------ `get_geckodriver_url` helpers ------
|
||||
|
||||
fn save_geckodriver_version(version: String) -> Result<String, failure::Error> { |
||||
stamps::save_stamp_value(GECKODRIVER_VERSION_STAMP, &version)?; |
||||
|
||||
let current_time = chrono::offset::Local::now().to_rfc3339(); |
||||
stamps::save_stamp_value(GECKODRIVER_LAST_UPDATED_STAMP, current_time)?; |
||||
|
||||
Ok(version) |
||||
} |
||||
|
||||
fn should_load_geckodriver_version_from_stamp(json: &serde_json::Value) -> bool { |
||||
let last_updated = stamps::get_stamp_value(GECKODRIVER_LAST_UPDATED_STAMP, json) |
||||
.ok() |
||||
.and_then(|last_updated| DateTime::parse_from_rfc3339(&last_updated).ok()); |
||||
|
||||
match last_updated { |
||||
None => false, |
||||
Some(last_updated) => { |
||||
let current_time = chrono::offset::Local::now(); |
||||
current_time.signed_duration_since(last_updated).num_hours() < 24 |
||||
} |
||||
} |
||||
} |
||||
|
||||
fn fetch_latest_geckodriver_tag_json() -> Result<String, failure::Error> { |
||||
let mut headers = curl::easy::List::new(); |
||||
headers |
||||
.append("Accept: application/json") |
||||
.context("cannot fetch geckodriver's latest release data - appending header failed")?; |
||||
|
||||
let mut handle = curl::easy::Easy2::new(Collector(Vec::new())); |
||||
handle |
||||
.url("https://github.com/mozilla/geckodriver/releases/latest") |
||||
.context("URL to fetch geckodriver's latest release data is invalid")?; |
||||
handle |
||||
.http_headers(headers) |
||||
.context("cannot fetch geckodriver's latest release data - setting headers failed")?; |
||||
// We will be redirected from the `latest` placeholder to the specific tag name.
|
||||
handle |
||||
.follow_location(true) |
||||
.context("cannot fetch geckodriver's latest release data - enabling redirects failed")?; |
||||
handle |
||||
.perform() |
||||
.context("fetching of geckodriver's latest release data failed")?; |
||||
|
||||
let content = handle.get_mut().take_content(); |
||||
let version = String::from_utf8(content) |
||||
.context("geckodriver's latest release data is not valid UTF-8")?; |
||||
|
||||
Ok(version) |
||||
} |
||||
|
||||
/// JSON example: `{"id":15227534,"tag_name":"v0.24.0","update_url":"/mozzila...`
|
||||
fn get_version_from_json(json: impl AsRef<str>) -> Result<String, failure::Error> { |
||||
let json: serde_json::Value = serde_json::from_str(json.as_ref()) |
||||
.context("geckodriver's latest release data is not valid 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 geckodriver's latest release data") |
||||
}) |
||||
} |
||||
|
||||
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, |
||||
) |
||||
} |
@ -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<PathBuf, failure::Error> { |
||||
match which::which("safaridriver") { |
||||
Ok(p) => Ok(p), |
||||
Err(_) => bail!("could not find `safaridriver` on the `$PATH`"), |
||||
} |
||||
} |
@ -0,0 +1,71 @@ |
||||
use std::{fs, panic}; |
||||
use wasm_pack::stamps; |
||||
|
||||
fn run_test<T>(test: T) -> () |
||||
where |
||||
T: FnOnce() -> () + panic::UnwindSafe, |
||||
{ |
||||
before(); |
||||
let result = panic::catch_unwind(|| test()); |
||||
after(); |
||||
assert!(result.is_ok()) |
||||
} |
||||
|
||||
fn before() { |
||||
remove_stamps_file() |
||||
} |
||||
|
||||
fn after() { |
||||
remove_stamps_file() |
||||
} |
||||
|
||||
fn remove_stamps_file() { |
||||
let stamps_file_path = stamps::get_stamps_file_path().unwrap(); |
||||
if stamps_file_path.exists() { |
||||
fs::remove_file(stamps_file_path).unwrap(); |
||||
} |
||||
} |
||||
|
||||
#[test] |
||||
#[should_panic] |
||||
#[serial] |
||||
fn load_stamp_from_non_existent_file() { |
||||
run_test(|| { |
||||
// ACT
|
||||
let json = stamps::read_stamps_file_to_json().unwrap(); |
||||
stamps::get_stamp_value("Foo", &json).unwrap(); |
||||
}) |
||||
} |
||||
|
||||
#[test] |
||||
#[serial] |
||||
fn load_stamp() { |
||||
run_test(|| { |
||||
// ARRANGE
|
||||
stamps::save_stamp_value("Foo", "Bar").unwrap(); |
||||
|
||||
// ACT
|
||||
let json = stamps::read_stamps_file_to_json().unwrap(); |
||||
let stamp_value = stamps::get_stamp_value("Foo", &json).unwrap(); |
||||
|
||||
// ASSERT
|
||||
assert_eq!(stamp_value, "Bar"); |
||||
}) |
||||
} |
||||
|
||||
#[test] |
||||
#[serial] |
||||
fn update_stamp() { |
||||
run_test(|| { |
||||
// ARRANGE
|
||||
stamps::save_stamp_value("Foo", "Bar").unwrap(); |
||||
|
||||
// ACT
|
||||
stamps::save_stamp_value("Foo", "John").unwrap(); |
||||
|
||||
// ASSERT
|
||||
let json = stamps::read_stamps_file_to_json().unwrap(); |
||||
let stamp_value = stamps::get_stamp_value("Foo", &json).unwrap(); |
||||
assert_eq!(stamp_value, "John"); |
||||
}) |
||||
} |
Loading…
Reference in new issue