coverage: exercise config error fallback and name accessor
Every error a config can produce is a DeserializeError; only a
Rust-side serialization failure reaches the fallback arm.

Assisted-by: GLM-5.3 via pi
change vtwxpzxktrsotytnyyzprkzkvtuqlrtk
commit f85a1405a6b1bded737d77db8acddec58d8f83bd
author Alpha Chen <alpha@kejadlen.dev>
date
parent lonyrkqu
diff --git a/src/parallax.rs b/src/parallax.rs
index 8c14e1f..8bfac56 100644
--- a/src/parallax.rs
+++ b/src/parallax.rs
@@ -1,3 +1,5 @@
+#![cfg_attr(test, allow(clippy::expect_used))]
+
 use camino::Utf8Path;
 use camino::Utf8PathBuf;
 
@@ -16,6 +18,7 @@ const FENNEL_LUA: &str = include_str!("../vendor/fennel.lua");
 
 /// The table a config returns: the setup spec plus the server and flow
 /// functions.
+#[derive(Debug)]
 struct Config {
     server: Function,
     flow: Function,
@@ -154,3 +157,61 @@ fn run_config(lua: &Lua, source: &str, filename: &str) -> Result<Value, mlua::Er
     let compiled: String = compile.call((source, options))?;
     lua.load(compiled).set_name(format!("@{filename}")).eval()
 }
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use camino::Utf8Path;
+
+    /// A type whose serialization always fails, so deserializing a
+    /// value containing it errors without going through serde's
+    /// validation messages.
+    struct FailsSerialize;
+
+    impl serde::Serialize for FailsSerialize {
+        fn serialize<S>(&self, _serializer: S) -> Result<S::Ok, S::Error>
+        where
+            S: serde::Serializer,
+        {
+            Err(serde::ser::Error::custom("boom"))
+        }
+    }
+
+    #[test]
+    fn non_validation_deserialization_errors_become_config_errors() {
+        let lua = Lua::new();
+        let table = lua.create_table().expect("root table");
+        let variants = lua.create_table().expect("variants table");
+        let desktop = lua.create_table().expect("variant table");
+        let viewport = lua.create_table().expect("viewport table");
+        viewport.push(1280).expect("width");
+        viewport.push(900).expect("height");
+        desktop.set("viewport", viewport).expect("viewport");
+        variants.set("desktop", desktop).expect("variant");
+        table.set("variants", variants).expect("variants");
+        table
+            .set(
+                "name",
+                lua.create_ser_any_userdata(FailsSerialize)
+                    .expect("userdata"),
+            )
+            .expect("name");
+        table
+            .set(
+                "server",
+                lua.create_function(|_, ()| Ok(()))
+                    .expect("server function"),
+            )
+            .expect("server");
+        table
+            .set(
+                "flow",
+                lua.create_function(|_, ()| Ok(())).expect("flow function"),
+            )
+            .expect("flow");
+
+        let error = Config::from_value(Utf8Path::new("config.fnl"), Value::Table(table))
+            .expect_err("deserialization must fail");
+        assert!(matches!(error, Error::Config(_)));
+    }
+}
diff --git a/tests/parallax.rs b/tests/parallax.rs
new file mode 100644
index 0000000..b3d6a7e
--- /dev/null
+++ b/tests/parallax.rs
@@ -0,0 +1,40 @@
+#![cfg_attr(test, allow(clippy::expect_used, clippy::unwrap_used))]
+
+use camino::Utf8PathBuf;
+use parallax::Parallax;
+use tempfile::TempDir;
+
+const VALID: &str = r#"{:variants {:desktop {:viewport [1280 900]}}
+ :server (fn [] {:cmd ["true"]})
+ :flow (fn [] nil)}"#;
+
+const NAMED: &str = r#"{:name "ketchup"
+ :variants {:desktop {:viewport [1280 900]}}
+ :server (fn [] {:cmd ["true"]})
+ :flow (fn [] nil)}"#;
+
+fn config_file(source: &str) -> (TempDir, Utf8PathBuf) {
+    let dir = tempfile::tempdir().expect("tempdir");
+    let path: Utf8PathBuf = dir
+        .path()
+        .join("config.fnl")
+        .try_into()
+        .expect("utf-8 path");
+    fs_err::write(&path, source).expect("write config");
+    (dir, path)
+}
+
+#[test]
+fn name_defaults_to_the_config_directory() {
+    let (dir, path) = config_file(VALID);
+    let parallax = Parallax::load(&path).expect("config loads");
+    let directory = dir.path().file_name().unwrap().to_str().unwrap();
+    assert_eq!(parallax.name(), directory);
+}
+
+#[test]
+fn name_field_overrides_the_default() {
+    let (_dir, path) = config_file(NAMED);
+    let parallax = Parallax::load(&path).expect("config loads");
+    assert_eq!(parallax.name(), "ketchup");
+}