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