Added unit test for music module
Reorganized unit tests into unit_tests directory
This commit is contained in:
13
README.md
13
README.md
@@ -76,3 +76,16 @@ status ⏸ Dire Straits - Tunnel of Love [6% 14:23] 💾 24% 💻 1% 🔌 100%
|
|||||||
status ⏸ Dire Straits - Tunnel of Love [6% 14:23] 💾 24% 💻 1% 🔌 100% 📶 192.168.1.93 ☀️ 83°F 📅 2025 Jun 07 🕑 09:25PM -07:00
|
status ⏸ Dire Straits - Tunnel of Love [6% 14:23] 💾 24% 💻 1% 🔌 100% 📶 192.168.1.93 ☀️ 83°F 📅 2025 Jun 07 🕑 09:25PM -07:00
|
||||||
^C
|
^C
|
||||||
```
|
```
|
||||||
|
|
||||||
|
# Unit testing
|
||||||
|
|
||||||
|
Use the following command to run unit tests:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ cargo test --package unibar --bin unibar
|
||||||
|
```
|
||||||
|
To run a specific test, use the following:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ cargo test --package unibar --bin unibar -- bar_modules::bar_module_weather::tests::test_get_unicode_symbol_default --exact --show-output
|
||||||
|
```
|
||||||
|
@@ -135,3 +135,7 @@ impl bar_modules::BarModuleDebug for UnibarModuleMusic {
|
|||||||
fn post_debug(&self) {
|
fn post_debug(&self) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "../../unit_tests/bar_modules_music.test.rs"]
|
||||||
|
mod tests;
|
||||||
|
@@ -345,401 +345,5 @@ impl bar_modules::BarModuleDebug for UnibarModuleWeather {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
#[path = "../../unit_tests/bar_modules_weather.test.rs"]
|
||||||
use super::*;
|
mod tests;
|
||||||
use crate::bar_modules::BarModuleActions; // Add this line
|
|
||||||
use crate::common::AppOptions;
|
|
||||||
use crate::common::TemperatureUnits;
|
|
||||||
|
|
||||||
/// Creates a default AppOptions instance for testing
|
|
||||||
fn create_test_options(metric: bool) -> AppOptions {
|
|
||||||
AppOptions {
|
|
||||||
interval: 1,
|
|
||||||
weather_units: if metric { TemperatureUnits::Metric } else { TemperatureUnits::Imperial },
|
|
||||||
weather_station: "test_station".to_string(),
|
|
||||||
music_progress: false,
|
|
||||||
debug: false,
|
|
||||||
debug_modules: false,
|
|
||||||
debug_update: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
/// Test get_current_temperature returns correct value for metric and imperial units
|
|
||||||
fn test_get_current_temperature_units() {
|
|
||||||
let mut metric_module = UnibarModuleWeather::new(create_test_options(true));
|
|
||||||
let mut imperial_module = UnibarModuleWeather::new(create_test_options(false));
|
|
||||||
|
|
||||||
metric_module.weather_info = json!({
|
|
||||||
"features": [{
|
|
||||||
"properties": {
|
|
||||||
"temperature": {
|
|
||||||
"value": 25.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
imperial_module.weather_info = metric_module.weather_info.clone();
|
|
||||||
|
|
||||||
assert_eq!(metric_module.get_current_temperature().unwrap(), 25.0);
|
|
||||||
assert!((imperial_module.get_current_temperature().unwrap() - 77.0).abs() < 0.01);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
/// Test get_unicode_symbol returns default for unknown condition
|
|
||||||
fn test_get_unicode_symbol_default() {
|
|
||||||
let module = UnibarModuleWeather::new(create_test_options(true));
|
|
||||||
assert_eq!(module.get_unicode_symbol("not_a_real_condition"), "🌤️");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
/// Test clear resets weather_info to Null
|
|
||||||
fn test_clear_resets_weather_info() {
|
|
||||||
let mut module = UnibarModuleWeather::new(create_test_options(true));
|
|
||||||
module.weather_info = json!({"features": []});
|
|
||||||
module.clear();
|
|
||||||
assert_eq!(module.weather_info, json!(serde_json::Value::Null));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
/// Test get_content returns formatted temperature string
|
|
||||||
fn test_get_content_format() {
|
|
||||||
let mut module = UnibarModuleWeather::new(create_test_options(true));
|
|
||||||
module.weather_info = json!({
|
|
||||||
"features": [{
|
|
||||||
"properties": {
|
|
||||||
"temperature": {
|
|
||||||
"value": 18.7
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
let content = module.get_content();
|
|
||||||
assert!(content.starts_with("19°C"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
/// Test get_icon returns fallback when icon is missing
|
|
||||||
fn test_get_icon_missing_icon() {
|
|
||||||
let mut module = UnibarModuleWeather::new(create_test_options(true));
|
|
||||||
module.weather_info = json!({
|
|
||||||
"features": [{
|
|
||||||
"properties": {
|
|
||||||
"icon": serde_json::Value::Null
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
assert_eq!(module.get_icon(), "⁈");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
/// Test get_icon returns fallback when weather_info is Null
|
|
||||||
fn test_get_icon_null_weather_info() {
|
|
||||||
let module = UnibarModuleWeather::new(create_test_options(true));
|
|
||||||
assert_eq!(module.get_icon(), "⁈");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
/// Test weather module initialization
|
|
||||||
fn test_module_initialization() {
|
|
||||||
let module = UnibarModuleWeather::new(create_test_options(true));
|
|
||||||
assert_eq!(module.update_cnt, 0);
|
|
||||||
assert_eq!(module.weather_info, json!(serde_json::Value::Null));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
/// Test temperature unit display
|
|
||||||
fn test_temperature_unit_display() {
|
|
||||||
let metric_module = UnibarModuleWeather::new(create_test_options(true));
|
|
||||||
let imperial_module = UnibarModuleWeather::new(create_test_options(false));
|
|
||||||
|
|
||||||
assert_eq!(metric_module.get_temperature_unit(), "°C");
|
|
||||||
assert_eq!(imperial_module.get_temperature_unit(), "°F");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
/// Test weather condition to Unicode symbol mapping
|
|
||||||
fn test_unicode_symbol_mapping() {
|
|
||||||
let module = UnibarModuleWeather::new(create_test_options(true));
|
|
||||||
|
|
||||||
assert_eq!(module.get_unicode_symbol("snow"), "❄️");
|
|
||||||
assert_eq!(module.get_unicode_symbol("ovc"), "☁️");
|
|
||||||
assert_eq!(module.get_unicode_symbol("rain"), "🌧️");
|
|
||||||
assert_eq!(module.get_unicode_symbol("unknown"), "🌤️");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
/// Test error handling for invalid temperature data
|
|
||||||
fn test_error_handling() {
|
|
||||||
let mut module = UnibarModuleWeather::new(create_test_options(true));
|
|
||||||
|
|
||||||
// Test null weather info
|
|
||||||
assert!(module.get_current_temperature().is_ok());
|
|
||||||
assert_eq!(module.get_current_temperature().unwrap(), 0.0);
|
|
||||||
|
|
||||||
// Test invalid JSON structure
|
|
||||||
module.weather_info = json!({
|
|
||||||
"features": [{
|
|
||||||
"properties": {
|
|
||||||
"temperature": {
|
|
||||||
"value": "invalid"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
assert!(module.get_current_temperature().is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
/// Test update counter behavior. Update only when update_cnt==0
|
|
||||||
fn test_update_counter() {
|
|
||||||
let mut module = UnibarModuleWeather::new(create_test_options(true));
|
|
||||||
|
|
||||||
assert_eq!(module.update_cnt, 0);
|
|
||||||
assert_eq!(module.should_update(), UpdateResult::Update);
|
|
||||||
|
|
||||||
module.update_cnt = UPDATE_INTERVAL - 1;
|
|
||||||
assert_eq!(module.should_update(), UpdateResult::Skip);
|
|
||||||
|
|
||||||
module.update_cnt = UPDATE_INTERVAL;
|
|
||||||
assert_eq!(module.should_update(), UpdateResult::Skip);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
/// Test JSON parsing with valid weather data
|
|
||||||
fn test_valid_weather_data_parsing() {
|
|
||||||
let mut module = UnibarModuleWeather::new(create_test_options(true));
|
|
||||||
|
|
||||||
// Simulate valid weather data
|
|
||||||
module.weather_info = json!({
|
|
||||||
"features": [{
|
|
||||||
"properties": {
|
|
||||||
"temperature": {
|
|
||||||
"value": 20.5
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
|
|
||||||
// println!("{:#?}", module.weather_info);
|
|
||||||
let result = module.get_current_temperature();
|
|
||||||
// println!("{:#?}", result);
|
|
||||||
match result {
|
|
||||||
Ok(20.5) => {
|
|
||||||
// This is fine - we expect 20.5 for valid weather_info
|
|
||||||
assert_eq!(module.weather_info["features"][0]["properties"]["temperature"]["value"], 20.5);
|
|
||||||
},
|
|
||||||
Err(WeatherError::ApiResponseStrErr(_)) => {
|
|
||||||
assert!(false, "Expected valid temperature but got ApiResponseStrErr");
|
|
||||||
},
|
|
||||||
other => {
|
|
||||||
panic!("Unexpected result: {:?}", other);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
/// Test icon extraction from weather API URL
|
|
||||||
fn test_icon_extraction() {
|
|
||||||
let mut module = UnibarModuleWeather::new(create_test_options(true));
|
|
||||||
|
|
||||||
// Simulate weather data with icon URL
|
|
||||||
module.weather_info = json!({
|
|
||||||
"features": [{
|
|
||||||
"properties": {
|
|
||||||
"icon": "https://api.weather.gov/icons/land/night/ovc?size=medium"
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
let icon = module.get_icon();
|
|
||||||
// println!("Extracted icon: {}", icon);
|
|
||||||
// Check if the icon is correctly extracted
|
|
||||||
assert_eq!(icon, "☁️");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
/// Test handling of malformed/incomplete JSON structures
|
|
||||||
fn test_missing_json_structure() {
|
|
||||||
let mut module = UnibarModuleWeather::new(create_test_options(true));
|
|
||||||
|
|
||||||
// Test cases with increasingly incomplete JSON structures
|
|
||||||
let test_cases = vec![
|
|
||||||
// Missing value field
|
|
||||||
json!({
|
|
||||||
"features": [{
|
|
||||||
"properties": {
|
|
||||||
"temperature": {}
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
}),
|
|
||||||
// Missing temperature object
|
|
||||||
json!({
|
|
||||||
"features": [{
|
|
||||||
"properties": {}
|
|
||||||
}]
|
|
||||||
}),
|
|
||||||
// Missing properties
|
|
||||||
json!({
|
|
||||||
"features": [{}]
|
|
||||||
}),
|
|
||||||
// Empty features array
|
|
||||||
json!({
|
|
||||||
"features": []
|
|
||||||
}),
|
|
||||||
// Missing features
|
|
||||||
json!({}),
|
|
||||||
// Completely different structure
|
|
||||||
json!({
|
|
||||||
"unexpected": "structure"
|
|
||||||
}),
|
|
||||||
// Empty object
|
|
||||||
json!({}),
|
|
||||||
// Null value
|
|
||||||
json!(null),
|
|
||||||
// Array instead of object
|
|
||||||
json!([1, 2, 3]),
|
|
||||||
// Wrong data types
|
|
||||||
json!({
|
|
||||||
"features": [{
|
|
||||||
"properties": {
|
|
||||||
"temperature": {
|
|
||||||
"value": true // boolean instead of number
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
|
|
||||||
for (i, test_case) in test_cases.into_iter().enumerate() {
|
|
||||||
module.weather_info = test_case;
|
|
||||||
let result = module.get_current_temperature();
|
|
||||||
|
|
||||||
match result {
|
|
||||||
Ok(0.0) => {
|
|
||||||
// This is fine - we expect 0.0 for null weather_info
|
|
||||||
assert_eq!(module.weather_info, json!(null),
|
|
||||||
"Case {}: Should return 0.0 only for null weather_info", i);
|
|
||||||
},
|
|
||||||
Err(WeatherError::ApiResponseStrErr(_)) => {
|
|
||||||
// This is also fine - we expect ApiResponseStrErr for malformed data
|
|
||||||
},
|
|
||||||
other => {
|
|
||||||
panic!("Case {}: Unexpected result: {:?}", i, other);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
/// Test handling of malformed temperature values
|
|
||||||
fn test_invalid_temperature_values() {
|
|
||||||
let mut module = UnibarModuleWeather::new(create_test_options(true));
|
|
||||||
|
|
||||||
let test_cases = vec![
|
|
||||||
// Invalid temperature values
|
|
||||||
json!({
|
|
||||||
"features": [{
|
|
||||||
"properties": {
|
|
||||||
"temperature": {
|
|
||||||
"value": "not a number"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
}),
|
|
||||||
json!({
|
|
||||||
"features": [{
|
|
||||||
"properties": {
|
|
||||||
"temperature": {
|
|
||||||
"value": ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
}),
|
|
||||||
json!({
|
|
||||||
"features": [{
|
|
||||||
"properties": {
|
|
||||||
"temperature": {
|
|
||||||
"value": null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
}),
|
|
||||||
json!({
|
|
||||||
"features": [{
|
|
||||||
"properties": {
|
|
||||||
"temperature": {
|
|
||||||
"value": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
}),
|
|
||||||
json!({
|
|
||||||
"features": [{
|
|
||||||
"properties": {
|
|
||||||
"temperature": {
|
|
||||||
"value": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
})
|
|
||||||
];
|
|
||||||
|
|
||||||
for (i, test_case) in test_cases.into_iter().enumerate() {
|
|
||||||
module.weather_info = test_case;
|
|
||||||
let result = module.get_current_temperature();
|
|
||||||
|
|
||||||
assert!(matches!(result, Err(WeatherError::ApiResponseStrErr(_))),
|
|
||||||
"Case {}: Expected ApiResponseStrErr for invalid temperature value", i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
/// Test that deep nested access is safe
|
|
||||||
fn test_deep_nested_access() {
|
|
||||||
let mut module = UnibarModuleWeather::new(create_test_options(true));
|
|
||||||
|
|
||||||
// Test accessing nested fields that don't exist
|
|
||||||
let test_cases = vec![
|
|
||||||
// Deeper nesting than expected
|
|
||||||
json!({
|
|
||||||
"features": [{
|
|
||||||
"properties": {
|
|
||||||
"temperature": {
|
|
||||||
"value": {
|
|
||||||
"nested": "too deep"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
}),
|
|
||||||
// Missing intermediate fields
|
|
||||||
json!({
|
|
||||||
"features": [{
|
|
||||||
"properties": {
|
|
||||||
"temperature": {
|
|
||||||
"missing_value": 20.5
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
}),
|
|
||||||
// Wrong nesting structure
|
|
||||||
json!({
|
|
||||||
"features": { // Object instead of array
|
|
||||||
"properties": {
|
|
||||||
"temperature": {
|
|
||||||
"value": 20.5
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
];
|
|
||||||
|
|
||||||
for (i, test_case) in test_cases.into_iter().enumerate() {
|
|
||||||
module.weather_info = test_case;
|
|
||||||
let result = module.get_current_temperature();
|
|
||||||
|
|
||||||
assert!(matches!(result, Err(WeatherError::ApiResponseStrErr(_))),
|
|
||||||
"Case {}: Expected ApiResponseStrErr for invalid nested structure", i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
212
unit_tests/bar_modules_music.test.rs
Normal file
212
unit_tests/bar_modules_music.test.rs
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
// MIT License
|
||||||
|
// Copyright (c) 2025 Mahesh @ HeshApps
|
||||||
|
//
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
// of this software and associated documentation files (the "Software"), to deal
|
||||||
|
// in the Software without restriction, including without limitation the rights
|
||||||
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
// copies of the Software, and to permit persons to whom the Software is
|
||||||
|
// furnished to do so, subject to the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be included in all
|
||||||
|
// copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
// SOFTWARE.
|
||||||
|
|
||||||
|
//! Unit tests for Music module for Unibar
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use std::process::Command;
|
||||||
|
use crate::bar_modules::BarModuleActions;
|
||||||
|
|
||||||
|
/// Helper function to create test options
|
||||||
|
fn create_test_options(with_progress: bool) -> common::AppOptions {
|
||||||
|
common::AppOptions {
|
||||||
|
interval: 1,
|
||||||
|
weather_units: common::TemperatureUnits::Metric,
|
||||||
|
weather_station: "test".to_string(),
|
||||||
|
music_progress: with_progress,
|
||||||
|
debug: false,
|
||||||
|
debug_modules: false,
|
||||||
|
debug_update: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test that module handles missing mpc command gracefully.
|
||||||
|
/// Note: This needs ROOT permissions, hence excluding from normal
|
||||||
|
/// testing
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
fn test_missing_mpc_command() {
|
||||||
|
let mut module = UnibarModuleMusic::new(create_test_options(true));
|
||||||
|
|
||||||
|
// Temporarily rename mpc command if it exists
|
||||||
|
let has_mpc = Command::new("which")
|
||||||
|
.arg("mpc")
|
||||||
|
.output()
|
||||||
|
.map(|output| output.status.success())
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
let original_mpc_path = "/usr/bin/mpc";
|
||||||
|
let backup_mpc_path = "/tmp/mpc.bak";
|
||||||
|
|
||||||
|
if has_mpc {
|
||||||
|
// Create a backup
|
||||||
|
std::fs::copy(original_mpc_path, backup_mpc_path).expect("Failed to backup mpc");
|
||||||
|
// Rename the original
|
||||||
|
std::fs::rename(original_mpc_path, "/tmp/mpc_renamed").expect("Failed to rename mpc");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate data should not panic
|
||||||
|
module.generate_data();
|
||||||
|
|
||||||
|
// Should show appropriate error state
|
||||||
|
assert_eq!(module.get_icon(), "⏹");
|
||||||
|
assert!(module.get_content().contains("No player"));
|
||||||
|
|
||||||
|
// Restore mpc if it existed
|
||||||
|
if has_mpc {
|
||||||
|
// Remove the renamed file
|
||||||
|
std::fs::remove_file("/tmp/mpc_renamed").expect("Failed to remove renamed mpc");
|
||||||
|
// Restore from backup
|
||||||
|
std::fs::rename(backup_mpc_path, original_mpc_path).expect("Failed to restore mpc");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test handling of mpc command failing
|
||||||
|
#[test]
|
||||||
|
fn test_mpc_command_failure() {
|
||||||
|
let mut module = UnibarModuleMusic::new(create_test_options(true));
|
||||||
|
|
||||||
|
// Mock mpc command to always fail
|
||||||
|
module.current_stdout = "Error: Command not found".to_string();
|
||||||
|
module.progress_stdout = "".to_string();
|
||||||
|
module.state_stdout = "stopped".to_string();
|
||||||
|
|
||||||
|
// Generate data should not panic
|
||||||
|
// module.generate_data();
|
||||||
|
|
||||||
|
// Should show appropriate error state
|
||||||
|
assert_eq!(module.get_icon(), "⏹");
|
||||||
|
assert!(module.get_content().contains("Command not found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test handling of various mpc output formats
|
||||||
|
#[test]
|
||||||
|
fn test_unexpected_mpc_output() {
|
||||||
|
let mut module = UnibarModuleMusic::new(create_test_options(true));
|
||||||
|
|
||||||
|
// Test cases for current_stdout
|
||||||
|
let test_cases = vec![
|
||||||
|
"", // Empty output
|
||||||
|
"\n", // Just newline
|
||||||
|
//"invalid utf8 \x80", // Invalid UTF-8
|
||||||
|
"very\nlong\noutput\n", // Multiple lines
|
||||||
|
" ", // Just whitespace
|
||||||
|
"🎵 unicode 🎶", // Unicode characters
|
||||||
|
];
|
||||||
|
|
||||||
|
for test_case in test_cases {
|
||||||
|
module.current_stdout = test_case.to_string();
|
||||||
|
// get_content should not panic
|
||||||
|
let content = module.get_content();
|
||||||
|
assert!(!content.is_empty(), "Content should not be empty");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test state handling and icon selection
|
||||||
|
#[test]
|
||||||
|
fn test_player_states() {
|
||||||
|
let mut module = UnibarModuleMusic::new(create_test_options(false));
|
||||||
|
|
||||||
|
// Test valid states
|
||||||
|
let states = vec![
|
||||||
|
("playing", "𝄞"),
|
||||||
|
("paused", "⏸"),
|
||||||
|
("stopped", "⏹"),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (state, expected_icon) in states {
|
||||||
|
module.state_stdout = state.to_string();
|
||||||
|
assert_eq!(module.get_icon(), expected_icon);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test invalid states
|
||||||
|
let invalid_states = vec![
|
||||||
|
"",
|
||||||
|
"invalid",
|
||||||
|
"PLAYING",
|
||||||
|
"unknown",
|
||||||
|
"\n",
|
||||||
|
"playing\n",
|
||||||
|
];
|
||||||
|
|
||||||
|
for state in invalid_states {
|
||||||
|
module.state_stdout = state.to_string();
|
||||||
|
assert_eq!(module.get_icon(), "𝄞", "Invalid state should default to playing icon");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test progress display formatting
|
||||||
|
#[test]
|
||||||
|
fn test_progress_display() {
|
||||||
|
let mut module = UnibarModuleMusic::new(create_test_options(true));
|
||||||
|
|
||||||
|
// Test various progress formats
|
||||||
|
let test_cases = vec![
|
||||||
|
("50% 3:30", "[50% 3:30]"),
|
||||||
|
("", "[]"),
|
||||||
|
("invalid", "[invalid]"),
|
||||||
|
("100% 0:00", "[100% 0:00]"),
|
||||||
|
(" 50% 3:30 ", "[50% 3:30]"), // Extra whitespace
|
||||||
|
];
|
||||||
|
|
||||||
|
for (progress, expected) in test_cases {
|
||||||
|
module.progress_stdout = progress.to_string();
|
||||||
|
module.current_stdout = "Test Song".to_string();
|
||||||
|
let content = module.get_content();
|
||||||
|
assert!(content.contains(expected));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test with progress disabled
|
||||||
|
let mut module = UnibarModuleMusic::new(create_test_options(false));
|
||||||
|
module.progress_stdout = "50% 3:30".to_string();
|
||||||
|
module.current_stdout = "Test Song".to_string();
|
||||||
|
assert_eq!(module.get_content(), "Test Song");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test module initialization
|
||||||
|
#[test]
|
||||||
|
fn test_module_initialization() {
|
||||||
|
let module = UnibarModuleMusic::new(create_test_options(true));
|
||||||
|
|
||||||
|
assert_eq!(module.current_stdout, "");
|
||||||
|
assert_eq!(module.progress_stdout, "");
|
||||||
|
assert_eq!(module.state_stdout, "stopped");
|
||||||
|
assert_eq!(module.get_icon(), "⏹");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test clear functionality
|
||||||
|
#[test]
|
||||||
|
fn test_clear() {
|
||||||
|
let mut module = UnibarModuleMusic::new(create_test_options(true));
|
||||||
|
|
||||||
|
// Set some data
|
||||||
|
module.current_stdout = "Test Song".to_string();
|
||||||
|
module.progress_stdout = "50% 3:30".to_string();
|
||||||
|
module.state_stdout = "playing".to_string();
|
||||||
|
|
||||||
|
// Clear the module
|
||||||
|
module.clear();
|
||||||
|
|
||||||
|
// Verify cleared state
|
||||||
|
assert_eq!(module.current_stdout, "");
|
||||||
|
assert_eq!(module.progress_stdout, "");
|
||||||
|
assert_eq!(module.state_stdout, "stopped");
|
||||||
|
}
|
418
unit_tests/bar_modules_weather.test.rs
Normal file
418
unit_tests/bar_modules_weather.test.rs
Normal file
@@ -0,0 +1,418 @@
|
|||||||
|
// MIT License
|
||||||
|
// Copyright (c) 2025 Mahesh @ HeshApps
|
||||||
|
//
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
// of this software and associated documentation files (the "Software"), to deal
|
||||||
|
// in the Software without restriction, including without limitation the rights
|
||||||
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
// copies of the Software, and to permit persons to whom the Software is
|
||||||
|
// furnished to do so, subject to the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be included in all
|
||||||
|
// copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
// SOFTWARE.
|
||||||
|
|
||||||
|
//! Unit tests for Weather module for Unibar
|
||||||
|
use super::*;
|
||||||
|
use crate::bar_modules::BarModuleActions; // Add this line
|
||||||
|
use crate::common::AppOptions;
|
||||||
|
use crate::common::TemperatureUnits;
|
||||||
|
|
||||||
|
/// Creates a default AppOptions instance for testing
|
||||||
|
fn create_test_options(metric: bool) -> AppOptions {
|
||||||
|
AppOptions {
|
||||||
|
interval: 1,
|
||||||
|
weather_units: if metric { TemperatureUnits::Metric } else { TemperatureUnits::Imperial },
|
||||||
|
weather_station: "test_station".to_string(),
|
||||||
|
music_progress: false,
|
||||||
|
debug: false,
|
||||||
|
debug_modules: false,
|
||||||
|
debug_update: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// Test get_current_temperature returns correct value for metric and imperial units
|
||||||
|
fn test_get_current_temperature_units() {
|
||||||
|
let mut metric_module = UnibarModuleWeather::new(create_test_options(true));
|
||||||
|
let mut imperial_module = UnibarModuleWeather::new(create_test_options(false));
|
||||||
|
|
||||||
|
metric_module.weather_info = json!({
|
||||||
|
"features": [{
|
||||||
|
"properties": {
|
||||||
|
"temperature": {
|
||||||
|
"value": 25.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
imperial_module.weather_info = metric_module.weather_info.clone();
|
||||||
|
|
||||||
|
assert_eq!(metric_module.get_current_temperature().unwrap(), 25.0);
|
||||||
|
assert!((imperial_module.get_current_temperature().unwrap() - 77.0).abs() < 0.01);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// Test get_unicode_symbol returns default for unknown condition
|
||||||
|
fn test_get_unicode_symbol_default() {
|
||||||
|
let module = UnibarModuleWeather::new(create_test_options(true));
|
||||||
|
assert_eq!(module.get_unicode_symbol("not_a_real_condition"), "🌤️");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// Test clear resets weather_info to Null
|
||||||
|
fn test_clear_resets_weather_info() {
|
||||||
|
let mut module = UnibarModuleWeather::new(create_test_options(true));
|
||||||
|
module.weather_info = json!({"features": []});
|
||||||
|
module.clear();
|
||||||
|
assert_eq!(module.weather_info, json!(serde_json::Value::Null));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// Test get_content returns formatted temperature string
|
||||||
|
fn test_get_content_format() {
|
||||||
|
let mut module = UnibarModuleWeather::new(create_test_options(true));
|
||||||
|
module.weather_info = json!({
|
||||||
|
"features": [{
|
||||||
|
"properties": {
|
||||||
|
"temperature": {
|
||||||
|
"value": 18.7
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
let content = module.get_content();
|
||||||
|
assert!(content.starts_with("19°C"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// Test get_icon returns fallback when icon is missing
|
||||||
|
fn test_get_icon_missing_icon() {
|
||||||
|
let mut module = UnibarModuleWeather::new(create_test_options(true));
|
||||||
|
module.weather_info = json!({
|
||||||
|
"features": [{
|
||||||
|
"properties": {
|
||||||
|
"icon": serde_json::Value::Null
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
assert_eq!(module.get_icon(), "⁈");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// Test get_icon returns fallback when weather_info is Null
|
||||||
|
fn test_get_icon_null_weather_info() {
|
||||||
|
let module = UnibarModuleWeather::new(create_test_options(true));
|
||||||
|
assert_eq!(module.get_icon(), "⁈");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// Test weather module initialization
|
||||||
|
fn test_module_initialization() {
|
||||||
|
let module = UnibarModuleWeather::new(create_test_options(true));
|
||||||
|
assert_eq!(module.update_cnt, 0);
|
||||||
|
assert_eq!(module.weather_info, json!(serde_json::Value::Null));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// Test temperature unit display
|
||||||
|
fn test_temperature_unit_display() {
|
||||||
|
let metric_module = UnibarModuleWeather::new(create_test_options(true));
|
||||||
|
let imperial_module = UnibarModuleWeather::new(create_test_options(false));
|
||||||
|
|
||||||
|
assert_eq!(metric_module.get_temperature_unit(), "°C");
|
||||||
|
assert_eq!(imperial_module.get_temperature_unit(), "°F");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// Test weather condition to Unicode symbol mapping
|
||||||
|
fn test_unicode_symbol_mapping() {
|
||||||
|
let module = UnibarModuleWeather::new(create_test_options(true));
|
||||||
|
|
||||||
|
assert_eq!(module.get_unicode_symbol("snow"), "❄️");
|
||||||
|
assert_eq!(module.get_unicode_symbol("ovc"), "☁️");
|
||||||
|
assert_eq!(module.get_unicode_symbol("rain"), "🌧️");
|
||||||
|
assert_eq!(module.get_unicode_symbol("unknown"), "🌤️");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// Test error handling for invalid temperature data
|
||||||
|
fn test_error_handling() {
|
||||||
|
let mut module = UnibarModuleWeather::new(create_test_options(true));
|
||||||
|
|
||||||
|
// Test null weather info
|
||||||
|
assert!(module.get_current_temperature().is_ok());
|
||||||
|
assert_eq!(module.get_current_temperature().unwrap(), 0.0);
|
||||||
|
|
||||||
|
// Test invalid JSON structure
|
||||||
|
module.weather_info = json!({
|
||||||
|
"features": [{
|
||||||
|
"properties": {
|
||||||
|
"temperature": {
|
||||||
|
"value": "invalid"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
assert!(module.get_current_temperature().is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// Test update counter behavior. Update only when update_cnt==0
|
||||||
|
fn test_update_counter() {
|
||||||
|
let mut module = UnibarModuleWeather::new(create_test_options(true));
|
||||||
|
|
||||||
|
assert_eq!(module.update_cnt, 0);
|
||||||
|
assert_eq!(module.should_update(), UpdateResult::Update);
|
||||||
|
|
||||||
|
module.update_cnt = UPDATE_INTERVAL - 1;
|
||||||
|
assert_eq!(module.should_update(), UpdateResult::Skip);
|
||||||
|
|
||||||
|
module.update_cnt = UPDATE_INTERVAL;
|
||||||
|
assert_eq!(module.should_update(), UpdateResult::Skip);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// Test JSON parsing with valid weather data
|
||||||
|
fn test_valid_weather_data_parsing() {
|
||||||
|
let mut module = UnibarModuleWeather::new(create_test_options(true));
|
||||||
|
|
||||||
|
// Simulate valid weather data
|
||||||
|
module.weather_info = json!({
|
||||||
|
"features": [{
|
||||||
|
"properties": {
|
||||||
|
"temperature": {
|
||||||
|
"value": 20.5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
|
||||||
|
// println!("{:#?}", module.weather_info);
|
||||||
|
let result = module.get_current_temperature();
|
||||||
|
// println!("{:#?}", result);
|
||||||
|
match result {
|
||||||
|
Ok(20.5) => {
|
||||||
|
// This is fine - we expect 20.5 for valid weather_info
|
||||||
|
assert_eq!(module.weather_info["features"][0]["properties"]["temperature"]["value"], 20.5);
|
||||||
|
},
|
||||||
|
Err(WeatherError::ApiResponseStrErr(_)) => {
|
||||||
|
assert!(false, "Expected valid temperature but got ApiResponseStrErr");
|
||||||
|
},
|
||||||
|
other => {
|
||||||
|
panic!("Unexpected result: {:?}", other);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// Test icon extraction from weather API URL
|
||||||
|
fn test_icon_extraction() {
|
||||||
|
let mut module = UnibarModuleWeather::new(create_test_options(true));
|
||||||
|
|
||||||
|
// Simulate weather data with icon URL
|
||||||
|
module.weather_info = json!({
|
||||||
|
"features": [{
|
||||||
|
"properties": {
|
||||||
|
"icon": "https://api.weather.gov/icons/land/night/ovc?size=medium"
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
let icon = module.get_icon();
|
||||||
|
// println!("Extracted icon: {}", icon);
|
||||||
|
// Check if the icon is correctly extracted
|
||||||
|
assert_eq!(icon, "☁️");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// Test handling of malformed/incomplete JSON structures
|
||||||
|
fn test_missing_json_structure() {
|
||||||
|
let mut module = UnibarModuleWeather::new(create_test_options(true));
|
||||||
|
|
||||||
|
// Test cases with increasingly incomplete JSON structures
|
||||||
|
let test_cases = vec![
|
||||||
|
// Missing value field
|
||||||
|
json!({
|
||||||
|
"features": [{
|
||||||
|
"properties": {
|
||||||
|
"temperature": {}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}),
|
||||||
|
// Missing temperature object
|
||||||
|
json!({
|
||||||
|
"features": [{
|
||||||
|
"properties": {}
|
||||||
|
}]
|
||||||
|
}),
|
||||||
|
// Missing properties
|
||||||
|
json!({
|
||||||
|
"features": [{}]
|
||||||
|
}),
|
||||||
|
// Empty features array
|
||||||
|
json!({
|
||||||
|
"features": []
|
||||||
|
}),
|
||||||
|
// Missing features
|
||||||
|
json!({}),
|
||||||
|
// Completely different structure
|
||||||
|
json!({
|
||||||
|
"unexpected": "structure"
|
||||||
|
}),
|
||||||
|
// Empty object
|
||||||
|
json!({}),
|
||||||
|
// Null value
|
||||||
|
json!(null),
|
||||||
|
// Array instead of object
|
||||||
|
json!([1, 2, 3]),
|
||||||
|
// Wrong data types
|
||||||
|
json!({
|
||||||
|
"features": [{
|
||||||
|
"properties": {
|
||||||
|
"temperature": {
|
||||||
|
"value": true // boolean instead of number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (i, test_case) in test_cases.into_iter().enumerate() {
|
||||||
|
module.weather_info = test_case;
|
||||||
|
let result = module.get_current_temperature();
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(0.0) => {
|
||||||
|
// This is fine - we expect 0.0 for null weather_info
|
||||||
|
assert_eq!(module.weather_info, json!(null),
|
||||||
|
"Case {}: Should return 0.0 only for null weather_info", i);
|
||||||
|
},
|
||||||
|
Err(WeatherError::ApiResponseStrErr(_)) => {
|
||||||
|
// This is also fine - we expect ApiResponseStrErr for malformed data
|
||||||
|
},
|
||||||
|
other => {
|
||||||
|
panic!("Case {}: Unexpected result: {:?}", i, other);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// Test handling of malformed temperature values
|
||||||
|
fn test_invalid_temperature_values() {
|
||||||
|
let mut module = UnibarModuleWeather::new(create_test_options(true));
|
||||||
|
|
||||||
|
let test_cases = vec![
|
||||||
|
// Invalid temperature values
|
||||||
|
json!({
|
||||||
|
"features": [{
|
||||||
|
"properties": {
|
||||||
|
"temperature": {
|
||||||
|
"value": "not a number"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}),
|
||||||
|
json!({
|
||||||
|
"features": [{
|
||||||
|
"properties": {
|
||||||
|
"temperature": {
|
||||||
|
"value": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}),
|
||||||
|
json!({
|
||||||
|
"features": [{
|
||||||
|
"properties": {
|
||||||
|
"temperature": {
|
||||||
|
"value": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}),
|
||||||
|
json!({
|
||||||
|
"features": [{
|
||||||
|
"properties": {
|
||||||
|
"temperature": {
|
||||||
|
"value": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}),
|
||||||
|
json!({
|
||||||
|
"features": [{
|
||||||
|
"properties": {
|
||||||
|
"temperature": {
|
||||||
|
"value": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
for (i, test_case) in test_cases.into_iter().enumerate() {
|
||||||
|
module.weather_info = test_case;
|
||||||
|
let result = module.get_current_temperature();
|
||||||
|
|
||||||
|
assert!(matches!(result, Err(WeatherError::ApiResponseStrErr(_))),
|
||||||
|
"Case {}: Expected ApiResponseStrErr for invalid temperature value", i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// Test that deep nested access is safe
|
||||||
|
fn test_deep_nested_access() {
|
||||||
|
let mut module = UnibarModuleWeather::new(create_test_options(true));
|
||||||
|
|
||||||
|
// Test accessing nested fields that don't exist
|
||||||
|
let test_cases = vec![
|
||||||
|
// Deeper nesting than expected
|
||||||
|
json!({
|
||||||
|
"features": [{
|
||||||
|
"properties": {
|
||||||
|
"temperature": {
|
||||||
|
"value": {
|
||||||
|
"nested": "too deep"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}),
|
||||||
|
// Missing intermediate fields
|
||||||
|
json!({
|
||||||
|
"features": [{
|
||||||
|
"properties": {
|
||||||
|
"temperature": {
|
||||||
|
"missing_value": 20.5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}),
|
||||||
|
// Wrong nesting structure
|
||||||
|
json!({
|
||||||
|
"features": { // Object instead of array
|
||||||
|
"properties": {
|
||||||
|
"temperature": {
|
||||||
|
"value": 20.5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
for (i, test_case) in test_cases.into_iter().enumerate() {
|
||||||
|
module.weather_info = test_case;
|
||||||
|
let result = module.get_current_temperature();
|
||||||
|
|
||||||
|
assert!(matches!(result, Err(WeatherError::ApiResponseStrErr(_))),
|
||||||
|
"Case {}: Expected ApiResponseStrErr for invalid nested structure", i);
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user