From 3fb3b6128adcfa6e9b2283b05981d1cc9ba7f244 Mon Sep 17 00:00:00 2001 From: Boni Garcia Date: Wed, 28 Jun 2023 15:28:24 +0200 Subject: [PATCH] [rust] Automated management of Selenium Grid binaries (selenium-server.jar) --- rust/README.md | 2 + rust/src/downloads.rs | 9 +- rust/src/files.rs | 10 +- rust/src/grid.rs | 208 +++++++++++++++++++++++++++++++++++++++ rust/src/iexplorer.rs | 21 ++-- rust/src/lib.rs | 45 ++++++++- rust/src/main.rs | 11 +++ rust/tests/grid_tests.rs | 80 +++++++++++++++ 8 files changed, 361 insertions(+), 25 deletions(-) create mode 100644 rust/src/grid.rs create mode 100644 rust/tests/grid_tests.rs diff --git a/rust/README.md b/rust/README.md index 1196bf12005a8..4df408ddc375d 100644 --- a/rust/README.md +++ b/rust/README.md @@ -25,6 +25,8 @@ Options: Browser name (chrome, firefox, edge, iexplorer, safari, or safaritp) --driver Driver name (chromedriver, geckodriver, msedgedriver, IEDriverServer, or safaridriver) + --grid [] + Selenium Grid. If version is not provided, the latest version is downloaded --driver-version Driver version (e.g., 106.0.5249.61, 0.31.0, etc.) --browser-version diff --git a/rust/src/downloads.rs b/rust/src/downloads.rs index a543e2e47f69e..50d37c625a3e0 100644 --- a/rust/src/downloads.rs +++ b/rust/src/downloads.rs @@ -15,7 +15,7 @@ // specific language governing permissions and limitations // under the License. -use reqwest::Client; +use reqwest::{Client, StatusCode}; use serde::{Deserialize, Serialize}; use std::error::Error; use std::fs::File; @@ -40,7 +40,12 @@ pub async fn download_driver_to_tmp_folder( tmp_dir.path() )); - let response = http_client.get(url).send().await?; + let response = http_client.get(&url).send().await?; + let status_code = response.status(); + if status_code != StatusCode::OK { + return Err(format!("Unsuccessful response ({}) for URL {}", status_code, url).into()); + } + let target_path; let mut tmp_file = { let target_name = response diff --git a/rust/src/files.rs b/rust/src/files.rs index 72586ad75b8ca..c1c4c87256d0f 100644 --- a/rust/src/files.rs +++ b/rust/src/files.rs @@ -54,6 +54,12 @@ impl BrowserPath { } } +pub fn create_parent_path_if_not_exists(path: &Path) { + if let Some(p) = path.parent() { + create_path_if_not_exists(p); + } +} + pub fn create_path_if_not_exists(path: &Path) { if !path.exists() { fs::create_dir_all(path).unwrap(); @@ -123,9 +129,7 @@ pub fn unzip(file: File, target: &Path, log: &Logger) -> Result<(), Box, +} + +impl GridManager { + pub fn new(driver_version: String) -> Result, Box> { + let browser_name = GRID_NAME; + let driver_name = GRID_RELEASE; + let mut config = ManagerConfig::default(browser_name, driver_name); + config.driver_version = driver_version; + let default_timeout = config.timeout.to_owned(); + let default_proxy = &config.proxy; + Ok(Box::new(GridManager { + browser_name, + driver_name, + http_client: create_http_client(default_timeout, default_proxy)?, + config, + log: Logger::default(), + driver_url: None, + })) + } +} + +impl SeleniumManager for GridManager { + fn get_browser_name(&self) -> &str { + self.browser_name + } + + fn get_http_client(&self) -> &Client { + &self.http_client + } + + fn set_http_client(&mut self, http_client: Client) { + self.http_client = http_client; + } + + fn get_browser_path_map(&self) -> HashMap { + HashMap::new() + } + + fn discover_browser_version(&self) -> Option { + None + } + + fn get_driver_name(&self) -> &str { + self.driver_name + } + + fn request_driver_version(&mut self) -> Result> { + let browser_version_binding = self.get_major_browser_version(); + let browser_version = browser_version_binding.as_str(); + let mut metadata = get_metadata(self.get_logger()); + + match get_driver_version_from_metadata(&metadata.drivers, self.driver_name, browser_version) + { + Some(driver_version) => { + self.log.trace(format!( + "Driver TTL is valid. Getting {} version from metadata", + &self.driver_name + )); + Ok(driver_version) + } + _ => { + let selenium_releases = parse_json_from_url::>( + self.get_http_client(), + MIRROR_URL.to_string(), + )?; + + let filtered_releases: Vec = selenium_releases + .into_iter() + .filter(|r| { + r.assets.iter().any(|url| { + url.browser_download_url.contains(GRID_RELEASE) + && !url.browser_download_url.contains(SNAPSHOT) + }) + }) + .collect(); + + if !filtered_releases.is_empty() { + let assets = &filtered_releases.get(0).unwrap().assets; + let driver_releases: Vec<&Assets> = assets + .iter() + .filter(|url| { + url.browser_download_url.contains(GRID_RELEASE) + && url.browser_download_url.contains(GRID_EXTENSION) + }) + .collect(); + let driver_url = &driver_releases.last().unwrap().browser_download_url; + self.driver_url = Some(driver_url.to_string()); + + let index_release = + driver_url.rfind(GRID_RELEASE).unwrap() + GRID_RELEASE.len() + 1; + let driver_version = parse_version( + driver_url.as_str()[index_release..].to_string(), + self.get_logger(), + )?; + + let driver_ttl = self.get_driver_ttl(); + if driver_ttl > 0 { + metadata.drivers.push(create_driver_metadata( + browser_version, + self.driver_name, + &driver_version, + driver_ttl, + )); + write_metadata(&metadata, self.get_logger()); + } + + Ok(driver_version) + } else { + Err(format!("{} release not available", self.get_driver_name()).into()) + } + } + } + } + + fn get_driver_url(&mut self) -> Result> { + if self.driver_url.is_some() { + return Ok(self.driver_url.as_ref().unwrap().to_string()); + } + + let release_version = self.get_selenium_release_version()?; + Ok(format!( + "{}download/{}/{}-{}.{}", + DRIVER_URL, + release_version, + GRID_RELEASE, + self.get_driver_version(), + GRID_EXTENSION + )) + } + + fn get_driver_path_in_cache(&self) -> PathBuf { + let browser_name = self.get_browser_name(); + let driver_name = self.get_driver_name(); + let driver_version = self.get_driver_version(); + get_cache_folder() + .join(browser_name) + .join(driver_version) + .join(format!("{driver_name}-{driver_version}.{GRID_EXTENSION}")) + } + + fn get_config(&self) -> &ManagerConfig { + &self.config + } + + fn get_config_mut(&mut self) -> &mut ManagerConfig { + &mut self.config + } + + fn set_config(&mut self, config: ManagerConfig) { + self.config = config; + } + + fn get_logger(&self) -> &Logger { + &self.log + } + + fn set_logger(&mut self, log: Logger) { + self.log = log; + } +} diff --git a/rust/src/iexplorer.rs b/rust/src/iexplorer.rs index 139d8f241e535..d2f12376dd7c5 100644 --- a/rust/src/iexplorer.rs +++ b/rust/src/iexplorer.rs @@ -164,22 +164,13 @@ impl SeleniumManager for IExplorerManager { return Ok(self.driver_url.as_ref().unwrap().to_string()); } - let driver_version = self.get_driver_version(); - let mut release_version = driver_version.to_string(); - if !driver_version.ends_with('0') { - // E.g.: version 4.8.1 is shipped within release 4.8.0 - let error_message = format!( - "Wrong {} version: '{}'", - self.get_driver_name(), - driver_version - ); - let index = release_version.rfind('.').ok_or(error_message)? + 1; - release_version = release_version[..index].to_string(); - release_version.push('0'); - } + let release_version = self.get_selenium_release_version()?; Ok(format!( - "{}download/selenium-{}/{}{}.zip", - DRIVER_URL, release_version, IEDRIVER_RELEASE, driver_version + "{}download/{}/{}{}.zip", + DRIVER_URL, + release_version, + IEDRIVER_RELEASE, + self.get_driver_version() )) } diff --git a/rust/src/lib.rs b/rust/src/lib.rs index d43652e3591e3..e740a3fa84723 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -17,7 +17,7 @@ use crate::chrome::{ChromeManager, CHROMEDRIVER_NAME, CHROME_NAME}; use crate::edge::{EdgeManager, EDGEDRIVER_NAME, EDGE_NAMES}; -use crate::files::compose_cache_folder; +use crate::files::{compose_cache_folder, create_parent_path_if_not_exists}; use crate::firefox::{FirefoxManager, FIREFOX_NAME, GECKODRIVER_NAME}; use crate::iexplorer::{IExplorerManager, IEDRIVER_NAME, IE_NAMES}; use crate::safari::{SafariManager, SAFARIDRIVER_NAME, SAFARI_NAME}; @@ -35,6 +35,7 @@ use std::time::Duration; use crate::downloads::download_driver_to_tmp_folder; use crate::files::{parse_version, uncompress, BrowserPath}; +use crate::grid::GRID_NAME; use crate::logger::Logger; use crate::metadata::{ create_browser_metadata, get_browser_version_from_metadata, get_metadata, write_metadata, @@ -47,6 +48,7 @@ pub mod downloads; pub mod edge; pub mod files; pub mod firefox; +pub mod grid; pub mod iexplorer; pub mod logger; pub mod metadata; @@ -85,6 +87,7 @@ pub const TTL_DRIVERS_SEC: u64 = 86400; pub const UNAME_COMMAND: &str = "uname -{}"; pub const CRLF: &str = "\r\n"; pub const LF: &str = "\n"; +pub const SNAPSHOT: &str = "SNAPSHOT"; pub trait SeleniumManager { // ---------------------------------------------------------- @@ -129,8 +132,15 @@ pub trait SeleniumManager { .debug(format!("Driver URL: {}", driver_url)); let (_tmp_folder, driver_zip_file) = download_driver_to_tmp_folder(self.get_http_client(), driver_url, self.get_logger())?; - let driver_path_in_cache = Self::get_driver_path_in_cache(self); - uncompress(&driver_zip_file, &driver_path_in_cache, self.get_logger()) + + if self.is_grid() { + let driver_path_in_cache = Self::get_driver_path_in_cache(self); + create_parent_path_if_not_exists(&driver_path_in_cache); + Ok(fs::rename(driver_zip_file, driver_path_in_cache)?) + } else { + let driver_path_in_cache = Self::get_driver_path_in_cache(self); + uncompress(&driver_zip_file, &driver_path_in_cache, self.get_logger()) + } } fn detect_browser_path(&self) -> Option<&str> { @@ -213,7 +223,7 @@ pub trait SeleniumManager { None => { if self.is_browser_version_unstable() { return Err(format!("Browser version '{browser_version}' not found")); - } else if !self.is_iexplorer() { + } else if !self.is_iexplorer() && !self.is_grid() { self.get_logger().warn(format!( "The version of {} cannot be detected. Trying with latest driver version", self.get_browser_name() @@ -294,6 +304,10 @@ pub trait SeleniumManager { self.get_browser_name().eq(IE_NAMES[0]) } + fn is_grid(&self) -> bool { + self.get_browser_name().eq(GRID_NAME) + } + fn is_browser_version_unstable(&self) -> bool { let browser_version = self.get_browser_version(); browser_version.eq_ignore_ascii_case(BETA) @@ -308,7 +322,7 @@ pub trait SeleniumManager { self.set_driver_version(driver_version); } - if !self.is_safari() { + if !self.is_safari() && !self.is_grid() { if let (Some(version), Some(path)) = self.find_driver_in_path() { if version == self.get_driver_version() { self.get_logger().debug(format!( @@ -355,6 +369,27 @@ pub trait SeleniumManager { get_index_version(full_version, 1) } + fn get_selenium_release_version(&self) -> Result> { + let driver_version = self.get_driver_version(); + if driver_version.contains(SNAPSHOT) { + return Ok(NIGHTLY.to_string()); + } + + let mut release_version = driver_version.to_string(); + if !driver_version.ends_with('0') { + // E.g.: version 4.8.1 is shipped within release 4.8.0 + let error_message = format!( + "Wrong {} version: '{}'", + self.get_driver_name(), + driver_version + ); + let index = release_version.rfind('.').ok_or(error_message)? + 1; + release_version = release_version[..index].to_string(); + release_version.push('0'); + } + Ok(format!("selenium-{release_version}")) + } + // ---------------------------------------------------------- // Getters and setters for configuration parameters // ---------------------------------------------------------- diff --git a/rust/src/main.rs b/rust/src/main.rs index 34c292a32f938..e85365df4af60 100644 --- a/rust/src/main.rs +++ b/rust/src/main.rs @@ -23,6 +23,7 @@ use exitcode::DATAERR; use exitcode::OK; use selenium_manager::config::BooleanKey; +use selenium_manager::grid::GridManager; use selenium_manager::logger::Logger; use selenium_manager::REQUEST_TIMEOUT_SEC; use selenium_manager::TTL_BROWSERS_SEC; @@ -49,6 +50,10 @@ struct Cli { #[clap(long, value_parser)] driver: Option, + /// Selenium Grid. If version is not provided, the latest version is downloaded + #[clap(long, value_parser, num_args = 0..=1, default_missing_value = "", value_name = "GRID_VERSION")] + grid: Option, + /// Driver version (e.g., 106.0.5249.61, 0.31.0, etc.) #[clap(long, value_parser)] driver_version: Option, @@ -105,6 +110,7 @@ fn main() { let debug = cli.debug || BooleanKey("debug", false).get_value(); let trace = cli.trace || BooleanKey("trace", false).get_value(); let log = Logger::create(&cli.output, debug, trace); + let grid = cli.grid; if cli.clear_cache || BooleanKey("clear-cache", false).get_value() { clear_cache(&log); @@ -127,6 +133,11 @@ fn main() { log.error(err); flush_and_exit(DATAERR, &log); }) + } else if grid.is_some() { + GridManager::new(grid.as_ref().unwrap().to_string()).unwrap_or_else(|err| { + log.error(err); + flush_and_exit(DATAERR, &log); + }) } else { log.error("You need to specify a browser or driver"); flush_and_exit(DATAERR, &log); diff --git a/rust/tests/grid_tests.rs b/rust/tests/grid_tests.rs new file mode 100644 index 0000000000000..7d2385b3a6ad5 --- /dev/null +++ b/rust/tests/grid_tests.rs @@ -0,0 +1,80 @@ +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +use assert_cmd::Command; +use exitcode::DATAERR; +use rstest::rstest; +use selenium_manager::logger::JsonOutput; +use std::path::Path; +use std::str; + +#[test] +fn grid_latest_test() { + let mut cmd = Command::new(env!("CARGO_BIN_EXE_selenium-manager")); + cmd.args(["--grid", "--output", "json"]) + .assert() + .success() + .code(0); + + let stdout = &cmd.unwrap().stdout; + let output = str::from_utf8(stdout).unwrap(); + println!("{}", output); + + let json: JsonOutput = serde_json::from_str(output).unwrap(); + assert!(!json.logs.is_empty()); + + let output_code = json.result.code; + assert_eq!(output_code, 0); + + let jar = Path::new(&json.result.message); + assert!(jar.exists()); + + let jar_name = jar.file_name().unwrap().to_str().unwrap(); + assert!(jar_name.contains("selenium-server")); +} + +#[rstest] +#[case("4.8.0")] +#[case("4.9.0")] +#[case("4.10.0")] +fn grid_version_test(#[case] grid_version: &str) { + let mut cmd = Command::new(env!("CARGO_BIN_EXE_selenium-manager")); + cmd.args(["--grid", grid_version, "--output", "json"]) + .assert() + .success() + .code(0); + + let stdout = &cmd.unwrap().stdout; + let output = str::from_utf8(stdout).unwrap(); + println!("{}", output); + + let json: JsonOutput = serde_json::from_str(output).unwrap(); + let jar = Path::new(&json.result.message); + let jar_name = jar.file_name().unwrap().to_str().unwrap(); + assert!(jar_name.contains(grid_version)); +} + +#[rstest] +#[case("bad-version")] +#[case("99.99.99")] +fn grid_error_test(#[case] grid_version: &str) { + let mut cmd = Command::new(env!("CARGO_BIN_EXE_selenium-manager")); + cmd.args(["--grid", grid_version]) + .assert() + .failure() + .code(DATAERR); +}