diff --git a/rust/src/chrome.rs b/rust/src/chrome.rs
index 61224379a30f6..2c6583ba8442e 100644
--- a/rust/src/chrome.rs
+++ b/rust/src/chrome.rs
@@ -22,7 +22,8 @@ use crate::downloads::{parse_json_from_url, read_version_from_link};
use crate::files::{BrowserPath, compose_driver_path_in_cache};
use crate::logger::Logger;
use crate::metadata::{
- create_driver_metadata, get_driver_version_from_metadata, get_metadata, write_metadata,
+ create_driver_metadata, get_driver_version_from_metadata, get_metadata,
+ should_cache_driver_version, write_metadata,
};
use crate::{
BETA, DASH_DASH_VERSION, DEV, NIGHTLY, OFFLINE_REQUEST_ERR_MSG, REG_VERSION_ARG, STABLE,
@@ -339,7 +340,7 @@ impl SeleniumManager for ChromeManager {
};
let driver_ttl = self.get_ttl();
- if driver_ttl > 0 && !major_browser_version.is_empty() && !driver_version.is_empty()
+ if should_cache_driver_version(driver_ttl, major_browser_version, &driver_version)
{
metadata.drivers.push(create_driver_metadata(
major_browser_version,
diff --git a/rust/src/config.rs b/rust/src/config.rs
index 60973a0c7da9e..1cb274e8fab58 100644
--- a/rust/src/config.rs
+++ b/rust/src/config.rs
@@ -188,8 +188,8 @@ pub enum ARCH {
impl ARCH {
pub fn to_str_vector(&self) -> Vec<&str> {
match self {
- ARCH::X32 => vec![ARCH_X86, "i386", "x32"],
- ARCH::X64 => vec![ARCH_X64, "amd64", "x64", "i686", "ia64"],
+ ARCH::X32 => vec![ARCH_X86, "i386", "x32", "i686"],
+ ARCH::X64 => vec![ARCH_X64, "amd64", "x64", "ia64"],
ARCH::ARM64 => vec![ARCH_ARM64, "aarch64", "arm"],
ARCH::ARMV7 => vec![ARCH_ARM7L, "armv7l"],
}
@@ -269,6 +269,31 @@ fn get_env_name(suffix: &str) -> String {
concat(ENV_PREFIX, suffix_uppercase.as_str())
}
+#[cfg(test)]
+mod env_name_tests {
+ use super::*;
+
+ #[test]
+ fn get_env_name_simple_key() {
+ assert_eq!(get_env_name("browser"), "SE_BROWSER");
+ }
+
+ #[test]
+ fn get_env_name_dashes_become_underscores() {
+ assert_eq!(get_env_name("browser-version"), "SE_BROWSER_VERSION");
+ }
+
+ #[test]
+ fn get_env_name_mixed_case_uppercased() {
+ assert_eq!(get_env_name("Cache-Path"), "SE_CACHE_PATH");
+ }
+
+ #[test]
+ fn get_env_name_empty_suffix() {
+ assert_eq!(get_env_name(""), "SE_");
+ }
+}
+
fn get_config() -> Result
{
let cache_path = read_cache_path();
let config_path = Path::new(&cache_path).to_path_buf().join(CONFIG_FILE);
diff --git a/rust/src/edge.rs b/rust/src/edge.rs
index 042f805244dd0..fb04e94f5f4cd 100644
--- a/rust/src/edge.rs
+++ b/rust/src/edge.rs
@@ -21,7 +21,8 @@ use crate::config::OS::{LINUX, MACOS, WINDOWS};
use crate::downloads::{parse_json_from_url, read_version_from_link};
use crate::files::{BrowserPath, compose_driver_path_in_cache};
use crate::metadata::{
- create_driver_metadata, get_driver_version_from_metadata, get_metadata, write_metadata,
+ create_driver_metadata, get_driver_version_from_metadata, get_metadata,
+ should_cache_driver_version, write_metadata,
};
use crate::{
BETA, DASH_DASH_VERSION, DEV, ENV_PROGRAM_FILES, ENV_PROGRAM_FILES_X86, Logger, NIGHTLY,
@@ -252,7 +253,8 @@ impl SeleniumManager for EdgeManager {
read_version_from_link(self.get_http_client(), &driver_url, self.get_logger())?;
let driver_ttl = self.get_ttl();
- if driver_ttl > 0 && !major_browser_version.is_empty() {
+ if should_cache_driver_version(driver_ttl, major_browser_version, &driver_version)
+ {
metadata.drivers.push(create_driver_metadata(
major_browser_version.as_str(),
self.driver_name,
diff --git a/rust/src/firefox.rs b/rust/src/firefox.rs
index 19b9f4b3b014d..ea3276b76ddaa 100644
--- a/rust/src/firefox.rs
+++ b/rust/src/firefox.rs
@@ -21,7 +21,8 @@ use crate::config::OS::{LINUX, MACOS, WINDOWS};
use crate::downloads::{parse_json_from_url, read_content_from_link, read_redirect_from_link};
use crate::files::{BrowserPath, compose_driver_path_in_cache};
use crate::metadata::{
- create_driver_metadata, get_driver_version_from_metadata, get_metadata, write_metadata,
+ create_driver_metadata, get_driver_version_from_metadata, get_metadata,
+ should_cache_driver_version, write_metadata,
};
use crate::{
BETA, DASH_VERSION, DEV, ESR, LATEST_RELEASE, Logger, NIGHTLY, OFFLINE_REQUEST_ERR_MSG, STABLE,
@@ -296,7 +297,8 @@ impl SeleniumManager for FirefoxManager {
};
let driver_ttl = self.get_ttl();
- if driver_ttl > 0 && !major_browser_version.is_empty() {
+ if should_cache_driver_version(driver_ttl, major_browser_version, &driver_version)
+ {
metadata.drivers.push(create_driver_metadata(
major_browser_version,
self.driver_name,
diff --git a/rust/src/lib.rs b/rust/src/lib.rs
index 2186054c409b3..c5f41bef25506 100644
--- a/rust/src/lib.rs
+++ b/rust/src/lib.rs
@@ -1972,3 +1972,40 @@ fn get_index_version(full_version: &str, index: usize) -> Result
.ok_or(anyhow!(format!("Wrong version: {}", full_version)))?
.to_string())
}
+
+#[cfg(test)]
+mod index_version_tests {
+ use super::*;
+
+ #[test]
+ fn get_index_version_major() {
+ assert_eq!(get_index_version("120.0.6099.109", 0).unwrap(), "120");
+ }
+
+ #[test]
+ fn get_index_version_minor() {
+ assert_eq!(get_index_version("120.0.6099.109", 1).unwrap(), "0");
+ }
+
+ #[test]
+ fn get_index_version_patch() {
+ assert_eq!(get_index_version("120.0.6099.109", 2).unwrap(), "6099");
+ }
+
+ #[test]
+ fn get_index_version_single_component() {
+ assert_eq!(get_index_version("115", 0).unwrap(), "115");
+ }
+
+ #[test]
+ fn get_index_version_out_of_bounds_errors() {
+ let result = get_index_version("115", 1);
+ assert!(result.is_err());
+ }
+
+ #[test]
+ fn get_index_version_empty_string_errors() {
+ let result = get_index_version("", 0);
+ assert!(result.is_err());
+ }
+}
diff --git a/rust/src/metadata.rs b/rust/src/metadata.rs
index 8d9888d77839c..90f234bc044fd 100644
--- a/rust/src/metadata.rs
+++ b/rust/src/metadata.rs
@@ -181,6 +181,18 @@ pub fn create_browser_metadata(
}
}
+/// Determines whether a discovered driver version should be written to the metadata cache.
+///
+/// Returns `false` when the TTL is zero, the major browser version is empty, or the
+/// driver version itself is empty (e.g. a version-less HTTP response).
+pub fn should_cache_driver_version(
+ driver_ttl: u64,
+ major_browser_version: &str,
+ driver_version: &str,
+) -> bool {
+ driver_ttl > 0 && !major_browser_version.is_empty() && !driver_version.is_empty()
+}
+
pub fn create_driver_metadata(
major_browser_version: &str,
driver_name: &str,
diff --git a/rust/tests/cache_unit_tests.rs b/rust/tests/cache_unit_tests.rs
new file mode 100644
index 0000000000000..53e53fac98d92
--- /dev/null
+++ b/rust/tests/cache_unit_tests.rs
@@ -0,0 +1,317 @@
+// 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 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 selenium_manager::files::{collect_files_from_cache, find_latest_from_cache};
+use selenium_manager::get_manager_by_browser;
+use selenium_manager::metadata::{
+ create_driver_metadata, get_driver_version_from_metadata, get_metadata,
+ should_cache_driver_version, write_metadata, Metadata,
+};
+use selenium_manager::SeleniumManager;
+
+use rstest::rstest;
+use std::fs;
+use std::path::PathBuf;
+use tempfile::tempdir;
+use walkdir::WalkDir;
+
+fn create_driver_in_cache(
+ cache: &PathBuf,
+ driver_name: &str,
+ version: &str,
+) {
+ let manager = get_manager_by_browser("chrome".to_string()).unwrap();
+ let platform = manager.get_platform_label().to_string();
+ let os = manager.get_os().to_string();
+ let ext = if selenium_manager::config::OS::WINDOWS.is(&os) {
+ ".exe"
+ } else {
+ ""
+ };
+ let dir = cache
+ .join(driver_name)
+ .join(os)
+ .join(&platform)
+ .join(version);
+ fs::create_dir_all(&dir).unwrap();
+ fs::write(
+ dir.join(format!("{}{}", driver_name, ext)),
+ b"fake binary",
+ )
+ .unwrap();
+}
+
+fn create_driver_in_cache_custom(
+ cache: &PathBuf,
+ driver_name: &str,
+ os_label: &str,
+ arch_label: &str,
+ version: &str,
+) {
+ let dir = cache
+ .join(driver_name)
+ .join(os_label)
+ .join(arch_label)
+ .join(version);
+ fs::create_dir_all(&dir).unwrap();
+ let ext = if os_label == "windows" { ".exe" } else { "" };
+ fs::write(
+ dir.join(format!("{}{}", driver_name, ext)),
+ b"fake binary",
+ )
+ .unwrap();
+}
+
+#[test]
+fn find_latest_from_cache_returns_none_when_empty() {
+ let tmp = tempdir().unwrap();
+ let cache = tmp.path().to_path_buf();
+ let result = find_latest_from_cache(&cache, |_| true).unwrap();
+ assert!(result.is_none());
+}
+
+#[test]
+fn find_latest_from_cache_returns_matching_file() {
+ let tmp = tempdir().unwrap();
+ let cache = tmp.path().to_path_buf();
+ create_driver_in_cache(&cache, "chromedriver", "120.0.6099.109");
+
+ let result = find_latest_from_cache(&cache, |entry| {
+ entry
+ .file_name()
+ .to_str()
+ .map(|s| s.contains("chromedriver"))
+ .unwrap_or(false)
+ })
+ .unwrap();
+
+ assert!(result.is_some());
+ let path = result.unwrap();
+ assert!(path.exists());
+ assert!(path.to_str().unwrap().contains("chromedriver"));
+}
+
+#[test]
+fn find_latest_from_cache_returns_last_when_multiple_matches() {
+ let tmp = tempdir().unwrap();
+ let cache = tmp.path().to_path_buf();
+ create_driver_in_cache(&cache, "chromedriver", "115.0.5790.110");
+ create_driver_in_cache(&cache, "chromedriver", "120.0.6099.109");
+
+ let result = find_latest_from_cache(&cache, |entry| {
+ entry
+ .file_name()
+ .to_str()
+ .map(|s| s.contains("chromedriver"))
+ .unwrap_or(false)
+ })
+ .unwrap();
+
+ assert!(result.is_some());
+ let path = result.unwrap();
+ assert!(
+ path.to_str().unwrap().contains("120.0.6099.109"),
+ "expected last sorted entry to be 120.x, got: {:?}",
+ path
+ );
+}
+
+#[test]
+fn find_latest_from_cache_lexical_sort_misorders_across_digit_boundaries() {
+ let tmp = tempdir().unwrap();
+ let cache = tmp.path().to_path_buf();
+ create_driver_in_cache_custom(&cache, "chromedriver", "linux", "x86_64", "9.0.0");
+ create_driver_in_cache_custom(&cache, "chromedriver", "linux", "x86_64", "10.0.0");
+
+ let result = find_latest_from_cache(&cache, |entry| {
+ entry
+ .file_name()
+ .to_str()
+ .map(|s| s.contains("chromedriver"))
+ .unwrap_or(false)
+ })
+ .unwrap()
+ .unwrap();
+
+ assert!(
+ result.to_str().unwrap().contains("9.0.0"),
+ "lexical sort puts '9' after '10'; this test documents the latent bug"
+ );
+}
+
+#[test]
+fn collect_files_from_cache_returns_empty_when_no_match() {
+ let tmp = tempdir().unwrap();
+ let cache = tmp.path().to_path_buf();
+ create_driver_in_cache(&cache, "chromedriver", "120.0.6099.109");
+
+ let result = collect_files_from_cache(&cache, |_| false);
+ assert!(result.is_empty());
+}
+
+#[test]
+fn collect_files_from_cache_returns_all_matches() {
+ let tmp = tempdir().unwrap();
+ let cache = tmp.path().to_path_buf();
+ create_driver_in_cache(&cache, "chromedriver", "115.0.5790.110");
+ create_driver_in_cache(&cache, "chromedriver", "120.0.6099.109");
+
+ let result = collect_files_from_cache(&cache, |entry| {
+ entry
+ .file_name()
+ .to_str()
+ .map(|s| s.contains("chromedriver"))
+ .unwrap_or(false)
+ });
+
+ assert_eq!(result.len(), 2);
+}
+
+#[test]
+fn is_driver_and_matches_browser_version_matches_major() {
+ let tmp = tempdir().unwrap();
+ let cache = tmp.path().to_path_buf();
+ create_driver_in_cache(&cache, "chromedriver", "120.0.6099.109");
+
+ let mut manager = get_manager_by_browser("chrome".to_string()).unwrap();
+ manager.set_browser_version("120".to_string());
+ let entries: Vec<_> = WalkDir::new(&cache)
+ .into_iter()
+ .filter_map(|e| e.ok())
+ .filter(|e| e.file_type().is_file())
+ .collect();
+
+ let matching: Vec<_> = entries
+ .iter()
+ .filter(|e| manager.is_driver_and_matches_browser_version(e))
+ .collect();
+
+ assert_eq!(matching.len(), 1);
+}
+
+#[test]
+fn is_driver_and_matches_browser_version_empty_major_matches_all() {
+ let tmp = tempdir().unwrap();
+ let cache = tmp.path().to_path_buf();
+ create_driver_in_cache(&cache, "chromedriver", "115.0.5790.110");
+ create_driver_in_cache(&cache, "chromedriver", "120.0.6099.109");
+
+ let manager = get_manager_by_browser("chrome".to_string()).unwrap();
+ let entries: Vec<_> = WalkDir::new(&cache)
+ .into_iter()
+ .filter_map(|e| e.ok())
+ .filter(|e| e.file_type().is_file())
+ .collect();
+
+ let matching: Vec<_> = entries
+ .iter()
+ .filter(|e| manager.is_driver_and_matches_browser_version(e))
+ .collect();
+
+ assert_eq!(
+ matching.len(),
+ 2,
+ "starts_with(\"\") matches every cached driver — documents the latent bug"
+ );
+}
+
+#[test]
+fn find_best_driver_from_cache_returns_matching_version() {
+ let tmp = tempdir().unwrap();
+ let cache = tmp.path().to_path_buf();
+ create_driver_in_cache(&cache, "chromedriver", "115.0.5790.110");
+ create_driver_in_cache(&cache, "chromedriver", "120.0.6099.109");
+
+ let mut manager = get_manager_by_browser("chrome".to_string()).unwrap();
+ manager.set_cache_path(cache.to_str().unwrap().to_string());
+ manager.set_browser_version("120".to_string());
+
+ let result = manager.find_best_driver_from_cache().unwrap();
+ assert!(result.is_some());
+ let path = result.unwrap();
+ assert!(path.to_str().unwrap().contains("120.0.6099.109"));
+}
+
+#[test]
+fn find_best_driver_from_cache_falls_back_to_latest_when_no_match() {
+ let tmp = tempdir().unwrap();
+ let cache = tmp.path().to_path_buf();
+ create_driver_in_cache(&cache, "chromedriver", "115.0.5790.110");
+
+ let mut manager = get_manager_by_browser("chrome".to_string()).unwrap();
+ manager.set_cache_path(cache.to_str().unwrap().to_string());
+ manager.set_browser_version("130".to_string());
+
+ let result = manager.find_best_driver_from_cache().unwrap();
+ assert!(result.is_some());
+ let path = result.unwrap();
+ assert!(path.to_str().unwrap().contains("chromedriver"));
+}
+
+#[test]
+fn find_best_driver_from_cache_returns_none_when_cache_empty() {
+ let tmp = tempdir().unwrap();
+ let cache = tmp.path().to_path_buf();
+
+ let mut manager = get_manager_by_browser("chrome".to_string()).unwrap();
+ manager.set_cache_path(cache.to_str().unwrap().to_string());
+ manager.set_browser_version("120".to_string());
+
+ let result = manager.find_best_driver_from_cache().unwrap();
+ assert!(result.is_none());
+}
+
+#[test]
+fn empty_driver_version_is_not_cached_in_metadata() {
+ let tmp = tempdir().unwrap();
+ let cache = tmp.path().to_path_buf();
+ let log = selenium_manager::logger::Logger::default();
+
+ let mut metadata = Metadata {
+ browsers: Vec::new(),
+ drivers: Vec::new(),
+ stats: Vec::new(),
+ cached_assets: Vec::new(),
+ };
+
+ let major_browser_version = "120";
+ let driver_name = "edgedriver";
+ let driver_ttl = 3600;
+
+ let driver_version = "";
+
+ if should_cache_driver_version(driver_ttl, major_browser_version, driver_version) {
+ metadata.drivers.push(create_driver_metadata(
+ major_browser_version,
+ driver_name,
+ driver_version,
+ driver_ttl,
+ ));
+ write_metadata(&metadata, &log, Some(cache.clone()));
+ }
+
+ let read_back = get_metadata(&log, &Some(cache.clone()));
+ let result = get_driver_version_from_metadata(
+ &read_back.drivers,
+ driver_name,
+ major_browser_version,
+ );
+
+ assert!(
+ result.is_none(),
+ "empty driver_version must not be cached in metadata — this test fails if the !driver_version.is_empty() guard is removed from should_cache_driver_version()"
+ );
+}
diff --git a/rust/tests/config_unit_tests.rs b/rust/tests/config_unit_tests.rs
new file mode 100644
index 0000000000000..7367275aae24b
--- /dev/null
+++ b/rust/tests/config_unit_tests.rs
@@ -0,0 +1,100 @@
+// 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 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 selenium_manager::config::{OS, ARCH, str_to_os};
+use selenium_manager::config::OS::{LINUX, MACOS, WINDOWS};
+use selenium_manager::config::ARCH::{ARM64, ARMV7, X32, X64};
+
+use rstest::rstest;
+
+#[rstest]
+#[case("windows", WINDOWS)]
+#[case("win", WINDOWS)]
+#[case("Windows", WINDOWS)]
+#[case("WINDOWS", WINDOWS)]
+#[case("macos", MACOS)]
+#[case("mac", MACOS)]
+#[case("MacOS", MACOS)]
+#[case("linux", LINUX)]
+#[case("gnu/linux", LINUX)]
+#[case("Linux", LINUX)]
+fn str_to_os_parses_valid_os(#[case] input: &str, #[case] expected: OS) {
+ let result = str_to_os(input).unwrap();
+ assert_eq!(result, expected);
+}
+
+#[rstest]
+#[case("solaris")]
+#[case("")]
+#[case("darwin")]
+#[case("unix")]
+fn str_to_os_rejects_invalid_os(#[case] input: &str) {
+ let result = str_to_os(input);
+ assert!(result.is_err());
+}
+
+#[rstest]
+#[case(WINDOWS, "windows")]
+#[case(WINDOWS, "win")]
+#[case(WINDOWS, "Windows")]
+#[case(MACOS, "macos")]
+#[case(MACOS, "mac")]
+#[case(LINUX, "linux")]
+#[case(LINUX, "gnu/linux")]
+fn os_is_matches(#[case] os: OS, #[case] candidate: &str) {
+ assert!(os.is(candidate));
+}
+
+#[rstest]
+#[case(WINDOWS, "linux")]
+#[case(WINDOWS, "macos")]
+#[case(MACOS, "windows")]
+#[case(MACOS, "linux")]
+#[case(LINUX, "windows")]
+#[case(LINUX, "macos")]
+fn os_is_does_not_match(#[case] os: OS, #[case] candidate: &str) {
+ assert!(!os.is(candidate));
+}
+
+#[rstest]
+#[case(X32, "x86")]
+#[case(X32, "i386")]
+#[case(X32, "x32")]
+#[case(X32, "i686")]
+#[case(X64, "x86_64")]
+#[case(X64, "amd64")]
+#[case(X64, "x64")]
+#[case(X64, "ia64")]
+#[case(ARM64, "arm64")]
+#[case(ARM64, "aarch64")]
+#[case(ARM64, "arm")]
+#[case(ARMV7, "arm7l")]
+#[case(ARMV7, "armv7l")]
+fn arch_is_matches(#[case] arch: ARCH, #[case] candidate: &str) {
+ assert!(arch.is(candidate));
+}
+
+#[rstest]
+#[case(X32, "x86_64")]
+#[case(X32, "arm64")]
+#[case(X64, "x86")]
+#[case(X64, "i686")]
+#[case(X64, "arm7l")]
+#[case(ARM64, "x86_64")]
+#[case(ARMV7, "aarch64")]
+fn arch_is_does_not_match(#[case] arch: ARCH, #[case] candidate: &str) {
+ assert!(!arch.is(candidate));
+}
diff --git a/rust/tests/version_unit_tests.rs b/rust/tests/version_unit_tests.rs
new file mode 100644
index 0000000000000..61f3a2919c315
--- /dev/null
+++ b/rust/tests/version_unit_tests.rs
@@ -0,0 +1,185 @@
+// 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 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 selenium_manager::files::parse_version;
+use selenium_manager::logger::Logger;
+use selenium_manager::SeleniumManager;
+use selenium_manager::get_manager_by_browser;
+
+use rstest::rstest;
+
+#[rstest]
+#[case("Google Chrome 120.0.6099.109", "120.0.6099.109")]
+#[case("Chromium 115.0.5790.110", "115.0.5790.110")]
+#[case("Microsoft Edge 120.0.2210.91", "120.0.2210.91")]
+#[case("Mozilla Firefox 121.0", "121.0")]
+#[case("115", "115")]
+#[case(" 130.0.6723.91 ", "130.0.6723.91")]
+fn parse_version_extracts_version_string(#[case] input: &str, #[case] expected: &str) {
+ let log = Logger::new();
+ let result = parse_version(input.to_string(), &log).unwrap();
+ assert_eq!(result, expected);
+}
+
+#[test]
+fn parse_version_strips_trailing_dot() {
+ let log = Logger::new();
+ let result = parse_version("120.".to_string(), &log).unwrap();
+ assert_eq!(result, "120");
+}
+
+#[rstest]
+#[case("error: command not found")]
+#[case("ERROR: something went wrong")]
+#[case("Error: unable to locate browser")]
+fn parse_version_returns_error_on_error_string(#[case] input: &str) {
+ let log = Logger::new();
+ let result = parse_version(input.to_string(), &log);
+ assert!(result.is_err());
+}
+
+#[test]
+fn parse_version_returns_empty_on_garbage() {
+ let log = Logger::new();
+ let result = parse_version("no version here".to_string(), &log).unwrap();
+ assert_eq!(result, "");
+}
+
+#[test]
+fn parse_version_handles_esr_suffix() {
+ let log = Logger::new();
+ let result = parse_version("Mozilla Firefox 115.12.0esr".to_string(), &log).unwrap();
+ assert_eq!(result, "115.12.0");
+}
+
+#[test]
+fn parse_version_handles_snap_prefix() {
+ let log = Logger::new();
+ let result = parse_version("snap 121.0.1".to_string(), &log).unwrap();
+ assert_eq!(result, "121.0.1");
+}
+
+#[rstest]
+#[case("stable", true)]
+#[case("STABLE", true)]
+#[case("Stable", true)]
+#[case("beta", false)]
+#[case("120.0.6099.109", false)]
+fn is_stable_predicate(#[case] version: &str, #[case] expected: bool) {
+ let manager = get_manager_by_browser("chrome".to_string()).unwrap();
+ assert_eq!(manager.is_stable(version), expected);
+}
+
+#[rstest]
+#[case("beta", true)]
+#[case("BETA", true)]
+#[case("Beta", true)]
+#[case("stable", false)]
+#[case("120.0", false)]
+fn is_beta_predicate(#[case] version: &str, #[case] expected: bool) {
+ let manager = get_manager_by_browser("chrome".to_string()).unwrap();
+ assert_eq!(manager.is_beta(version), expected);
+}
+
+#[rstest]
+#[case("dev", true)]
+#[case("DEV", true)]
+#[case("Dev", true)]
+#[case("stable", false)]
+fn is_dev_predicate(#[case] version: &str, #[case] expected: bool) {
+ let manager = get_manager_by_browser("chrome".to_string()).unwrap();
+ assert_eq!(manager.is_dev(version), expected);
+}
+
+#[rstest]
+#[case("nightly", true)]
+#[case("NIGHTLY", true)]
+#[case("canary", true)]
+#[case("CANARY", true)]
+#[case("Canary", true)]
+#[case("stable", false)]
+#[case("120.0", false)]
+fn is_nightly_predicate(#[case] version: &str, #[case] expected: bool) {
+ let manager = get_manager_by_browser("chrome".to_string()).unwrap();
+ assert_eq!(manager.is_nightly(version), expected);
+}
+
+#[rstest]
+#[case("esr", true)]
+#[case("ESR", true)]
+#[case("Esr", true)]
+#[case("stable", false)]
+#[case("115.12.0", false)]
+fn is_esr_predicate(#[case] version: &str, #[case] expected: bool) {
+ let manager = get_manager_by_browser("firefox".to_string()).unwrap();
+ assert_eq!(manager.is_esr(version), expected);
+}
+
+#[rstest]
+#[case("120.0.6099.109", true)]
+#[case("121.0", true)]
+#[case("stable", false)]
+#[case("beta", false)]
+#[case("", false)]
+fn is_version_specific_predicate(#[case] version: &str, #[case] expected: bool) {
+ let manager = get_manager_by_browser("chrome".to_string()).unwrap();
+ assert_eq!(manager.is_version_specific(version), expected);
+}
+
+#[rstest]
+#[case("120.0.6099.109", "120")]
+#[case("115", "115")]
+#[case("130.0.6723.91", "130")]
+fn get_major_version_extracts_major(#[case] version: &str, #[case] expected: &str) {
+ let manager = get_manager_by_browser("chrome".to_string()).unwrap();
+ let result = manager.get_major_version(version).unwrap();
+ assert_eq!(result, expected);
+}
+
+#[test]
+fn get_major_version_errors_on_empty() {
+ let manager = get_manager_by_browser("chrome".to_string()).unwrap();
+ let result = manager.get_major_version("");
+ assert!(result.is_err());
+}
+
+#[test]
+fn get_major_version_returns_first_component_for_non_numeric() {
+ let manager = get_manager_by_browser("chrome".to_string()).unwrap();
+ let result = manager.get_major_version("not-a-version");
+ assert_eq!(result.unwrap(), "not-a-version");
+}
+
+#[rstest]
+#[case("stable", "stable")]
+#[case("beta", "beta")]
+#[case("dev", "dev")]
+#[case("nightly", "nightly")]
+#[case("canary", "canary")]
+#[case("esr", "esr")]
+#[case("120.0.6099.109", "120")]
+#[case("115", "115")]
+fn get_major_browser_version_returns_expected(#[case] version: &str, #[case] expected: &str) {
+ let mut manager = get_manager_by_browser("chrome".to_string()).unwrap();
+ manager.set_browser_version(version.to_string());
+ assert_eq!(manager.get_major_browser_version(), expected);
+}
+
+#[test]
+fn get_major_browser_version_empty_returns_empty() {
+ let manager = get_manager_by_browser("chrome".to_string()).unwrap();
+ assert_eq!(manager.get_major_browser_version(), "");
+}