diff --git a/dash-frontend/src/lib.rs b/dash-frontend/src/lib.rs index 916ed0cf..ce508442 100644 --- a/dash-frontend/src/lib.rs +++ b/dash-frontend/src/lib.rs @@ -1,6 +1,7 @@ mod assets; pub mod frontend; mod tab; +mod test; mod util; mod various; mod views; diff --git a/dash-frontend/src/tab/settings/tab_features.rs b/dash-frontend/src/tab/settings/tab_features.rs index 100e941f..cc2c9161 100644 --- a/dash-frontend/src/tab/settings/tab_features.rs +++ b/dash-frontend/src/tab/settings/tab_features.rs @@ -58,13 +58,14 @@ impl SettingsTab for State { match task { Task::WhisperDownloadClosed => { if let Some(model) = self.pending_download.take() - && !whisper_model_path(model.file_name).exists() { - // download failed, set to selection to none - par.general_config.whisper_model = "".into(); - par.config_change_kind.replace(ConfigChangeKind::Other); - // reload the tab - self.parent_tasks.push(ParentTask::SetTab(TabNameEnum::Features)); - } + && !whisper_model_path(model.file_name).exists() + { + // download failed, set to selection to none + par.general_config.whisper_model = "".into(); + par.config_change_kind.replace(ConfigChangeKind::Other); + // reload the tab + self.parent_tasks.push(ParentTask::SetTab(TabNameEnum::Features)); + } } Task::WhisperRemoveUnused => { let _ = whisper_delete_all_models().log_err("could not remove whisper models"); diff --git a/dash-frontend/src/test.rs b/dash-frontend/src/test.rs new file mode 100644 index 00000000..0d781cfd --- /dev/null +++ b/dash-frontend/src/test.rs @@ -0,0 +1,104 @@ +#[cfg(test)] +mod tests { + use crate::assets::Asset; + + #[test] + fn lang_json_files_are_valid() { + for file in Asset::iter() { + if file.ends_with(".json") { + let data = Asset::get(&file).unwrap_or_else(|| panic!("embedded file {file} not found")); + let text = + String::from_utf8(data.data.to_vec()).unwrap_or_else(|e| panic!("file {file} is not valid UTF-8: {e}")); + serde_json::from_str::(&text) + .unwrap_or_else(|e| panic!("file {file} is not valid JSON: {e}")); + } + } + } + + fn parse_csv_keys(data: &[u8]) -> Vec { + let text = std::str::from_utf8(data).expect("en.csv is not valid UTF-8"); + let mut keys = Vec::new(); + let mut chars = text.chars().peekable(); + while chars.peek().is_some() { + let key = read_csv_field(&mut chars); + // skip comma separator + if chars.peek() == Some(&',') { + chars.next(); + } + // skip value field + read_csv_field(&mut chars); + // skip comma separator + if chars.peek() == Some(&',') { + chars.next(); + } + // skip description field (may contain newlines via quotes) + read_csv_field(&mut chars); + // skip newline + if chars.peek() == Some(&'\n') { + chars.next(); + } + keys.push(key); + } + keys + } + + fn read_csv_field(chars: &mut std::iter::Peekable) -> String { + let mut field = String::new(); + if chars.peek() == Some(&'"') { + // quoted field + chars.next(); // consume opening quote + loop { + match chars.next() { + Some('"') => { + if chars.peek() == Some(&'"') { + field.push('"'); // escaped quote + chars.next(); + } else { + break; // closing quote + } + } + Some(c) => field.push(c), + None => break, + } + } + } else { + // unquoted field + for c in chars.by_ref() { + if c == ',' || c == '\n' { + break; + } + field.push(c); + } + } + field + } + + fn get_nested<'a>(value: &'a serde_json::Value, key: &str) -> Option<&'a serde_json::Value> { + let mut current = value; + for segment in key.split('.') { + current = current.get(segment)?; + } + Some(current) + } + + #[test] + fn lang_json_files_contain_all_keys() { + let en_data = Asset::get("lang/en.csv").expect("en.csv not found"); + let expected_keys = parse_csv_keys(&en_data.data); + + for file in Asset::iter() { + if !file.ends_with(".json") { + continue; + } + let data = Asset::get(&file).unwrap_or_else(|| panic!("embedded file {file} not found")); + let text = + String::from_utf8(data.data.to_vec()).unwrap_or_else(|e| panic!("file {file} is not valid UTF-8: {e}")); + let json: serde_json::Value = + serde_json::from_str(&text).unwrap_or_else(|e| panic!("file {file} is not valid JSON: {e}")); + + for key in &expected_keys { + assert!(get_nested(&json, key).is_some(), "missing key {key} in {file}"); + } + } + } +} diff --git a/wayvr/src/main.rs b/wayvr/src/main.rs index 3e3e356a..d85a0ddc 100644 --- a/wayvr/src/main.rs +++ b/wayvr/src/main.rs @@ -28,6 +28,7 @@ mod overlays; mod shaders; mod state; mod subsystem; +mod test; mod windowing; use std::{ diff --git a/wayvr/src/test.rs b/wayvr/src/test.rs new file mode 100644 index 00000000..9e7b6f7a --- /dev/null +++ b/wayvr/src/test.rs @@ -0,0 +1,109 @@ +#[cfg(test)] +mod tests { + use crate::gui::asset::GuiAsset; + + #[test] + fn lang_json_files_are_valid() { + for file in GuiAsset::iter() { + if file.ends_with(".json") { + let data = GuiAsset::get(&file) + .unwrap_or_else(|| panic!("embedded file {file} not found")); + let text = String::from_utf8(data.data.to_vec()) + .unwrap_or_else(|e| panic!("file {file} is not valid UTF-8: {e}")); + serde_json::from_str::(&text) + .unwrap_or_else(|e| panic!("file {file} is not valid JSON: {e}")); + } + } + } + + fn parse_csv_keys(data: &[u8]) -> Vec { + let text = std::str::from_utf8(data).expect("en.csv is not valid UTF-8"); + let mut keys = Vec::new(); + let mut chars = text.chars().peekable(); + while let Some(_) = chars.peek() { + let key = read_csv_field(&mut chars); + // skip comma separator + if chars.peek() == Some(&',') { + chars.next(); + } + // skip value field + read_csv_field(&mut chars); + // skip comma separator + if chars.peek() == Some(&',') { + chars.next(); + } + // skip description field (may contain newlines via quotes) + read_csv_field(&mut chars); + // skip newline + if chars.peek() == Some(&'\n') { + chars.next(); + } + keys.push(key); + } + keys + } + + fn read_csv_field(chars: &mut std::iter::Peekable) -> String { + let mut field = String::new(); + if chars.peek() == Some(&'"') { + // quoted field + chars.next(); // consume opening quote + loop { + match chars.next() { + Some('"') => { + if chars.peek() == Some(&'"') { + field.push('"'); // escaped quote + chars.next(); + } else { + break; // closing quote + } + } + Some(c) => field.push(c), + None => break, + } + } + } else { + // unquoted field + for c in chars.by_ref() { + if c == ',' || c == '\n' { + break; + } + field.push(c); + } + } + field + } + + fn get_nested<'a>(value: &'a serde_json::Value, key: &str) -> Option<&'a serde_json::Value> { + let mut current = value; + for segment in key.split('.') { + current = current.get(segment)?; + } + Some(current) + } + + #[test] + fn lang_json_files_contain_all_keys() { + let en_data = GuiAsset::get("lang/en.csv").expect("en.csv not found"); + let expected_keys = parse_csv_keys(&en_data.data); + + for file in GuiAsset::iter() { + if !file.ends_with(".json") { + continue; + } + let data = + GuiAsset::get(&file).unwrap_or_else(|| panic!("embedded file {file} not found")); + let text = String::from_utf8(data.data.to_vec()) + .unwrap_or_else(|e| panic!("file {file} is not valid UTF-8: {e}")); + let json: serde_json::Value = serde_json::from_str(&text) + .unwrap_or_else(|e| panic!("file {file} is not valid JSON: {e}")); + + for key in &expected_keys { + assert!( + get_nested(&json, key).is_some(), + "missing key {key} in {file}" + ); + } + } + } +}