Skip to main content

videre_core/
home.rs

1use anyhow::{bail, Context, Result};
2use std::path::{Path, PathBuf};
3
4/// Root of videre's per-user state: $VIDERE_HOME if set, else $HOME/.videre.
5pub fn videre_home() -> Result<PathBuf> {
6    if let Some(h) = std::env::var_os("VIDERE_HOME") {
7        return Ok(PathBuf::from(h));
8    }
9    match std::env::var_os("HOME") {
10        Some(h) => Ok(PathBuf::from(h).join(".videre")),
11        None => bail!("cannot locate videre home: neither VIDERE_HOME nor HOME is set"),
12    }
13}
14
15/// Default JSONL output path (used by `dedupe --output` with no value).
16pub fn default_jsonl() -> Result<PathBuf> {
17    Ok(videre_home()?.join("hashes.jsonl"))
18}
19
20/// Directory holding `flock` sidecar lock files: `<home>/locks`.
21///
22/// Locks used to sit next to the database as `<db path>.<command>.lock`, which
23/// scattered them into whatever directory the database lived in, cluttering
24/// `~/.videre` for the default database, and the user's own folders for any
25/// `--db` elsewhere. Collecting them here keeps that state in one place.
26///
27/// Only the path is computed; creating the directory is the caller's job, so
28/// readers (`videre stats` probing liveness) never bring videre's home into
29/// existence just by looking, same lazily-created-by-writers rule the rest of
30/// the home directory follows.
31pub fn locks_dir() -> Result<PathBuf> {
32    Ok(videre_home()?.join("locks"))
33}
34
35#[derive(Debug, Default, PartialEq)]
36pub struct Config {
37    pub default_db: Option<PathBuf>,
38    pub default_path: Option<PathBuf>,
39    /// Embedding model id, e.g. `google/siglip-base-patch16-224`. A plain
40    /// string, not a path: it must never be absolutized.
41    pub default_model: Option<String>,
42    /// Assumed floor read rate in MB/s, used to scale the I/O timeout to file
43    /// size. Only needed on a mount slower than the default.
44    pub min_read_rate_mb_s: Option<u64>,
45}
46
47/// Path of the config file inside a given home dir: <home>/config.toml.
48pub fn config_path(home: &Path) -> PathBuf {
49    home.join("config.toml")
50}
51
52fn path_key(table: &toml::Table, file: &Path, key: &str) -> Result<Option<PathBuf>> {
53    match table.get(key) {
54        None => Ok(None),
55        Some(toml::Value::String(s)) => Ok(Some(PathBuf::from(s))),
56        Some(other) => bail!(
57            "malformed config {}: {} must be a string, got {}",
58            file.display(),
59            key,
60            other.type_str()
61        ),
62    }
63}
64
65/// Read a positive-integer-valued key.
66///
67/// Stored as a TOML integer rather than a string, so it round-trips as a
68/// number. Zero is rejected here rather than clamped: as a read rate it means
69/// an unbounded timeout, which is the hang the timeout exists to prevent, and
70/// silently substituting a different number would hide a typo in the config.
71fn positive_int_key(table: &toml::Table, file: &Path, key: &str) -> Result<Option<u64>> {
72    match table.get(key) {
73        None => Ok(None),
74        Some(toml::Value::Integer(n)) if *n > 0 => Ok(Some(*n as u64)),
75        Some(toml::Value::Integer(n)) => bail!(
76            "malformed config {}: {} must be greater than 0, got {}",
77            file.display(),
78            key,
79            n
80        ),
81        Some(other) => bail!(
82            "malformed config {}: {} must be an integer, got {}",
83            file.display(),
84            key,
85            other.type_str()
86        ),
87    }
88}
89
90/// Read a string-valued key. Separate from `path_key` because a model id is
91/// not a path and must survive verbatim.
92fn string_key(table: &toml::Table, file: &Path, key: &str) -> Result<Option<String>> {
93    match table.get(key) {
94        None => Ok(None),
95        Some(toml::Value::String(s)) => Ok(Some(s.clone())),
96        Some(other) => bail!(
97            "malformed config {}: {} must be a string, got {}",
98            file.display(),
99            key,
100            other.type_str()
101        ),
102    }
103}
104
105/// Load <home>/config.toml. A missing file is the default config; a file that
106/// does not parse is a hard error (silent fallback would mask typos).
107pub fn load_config(home: &Path) -> Result<Config> {
108    let path = config_path(home);
109    let text = match std::fs::read_to_string(&path) {
110        Ok(t) => t,
111        Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(Config::default()),
112        Err(e) => return Err(e).with_context(|| format!("read {}", path.display())),
113    };
114    let table: toml::Table = text
115        .parse()
116        .with_context(|| format!("malformed config {}", path.display()))?;
117    Ok(Config {
118        default_db: path_key(&table, &path, "default_db")?,
119        default_path: path_key(&table, &path, "default_path")?,
120        default_model: string_key(&table, &path, "default_model")?,
121        min_read_rate_mb_s: positive_int_key(&table, &path, "min_read_rate_mb_s")?,
122    })
123}
124
125/// Whether the home came from `VIDERE_HOME` rather than the built-in default.
126///
127/// The distinction decides whether a config's `default_db` applies: see
128/// `resolve_db`.
129pub fn home_is_explicit() -> bool {
130    std::env::var_os("VIDERE_HOME").is_some()
131}
132
133/// Resolution for a given home: config default_db, else <home>/hashes.db.
134///
135/// Does not consider `VIDERE_HOME`; callers wanting the full precedence rule
136/// want `resolve_db`.
137pub fn resolve_db_in(home: &Path) -> Result<PathBuf> {
138    Ok(load_config(home)?
139        .default_db
140        .unwrap_or_else(|| home.join("hashes.db")))
141}
142
143/// Full chain: explicit CLI path > `VIDERE_HOME` > config `default_db` >
144/// `<home>/hashes.db`.
145///
146/// **`VIDERE_HOME` outranks the config file**, which is the ordinary
147/// precedence for an environment variable against persisted settings, and it
148/// was not always so. Before 0.14.1 the config won, so a home whose
149/// `config.toml` named an absolute `default_db` wrote there no matter what
150/// `VIDERE_HOME` said. That silently defeats the isolation the variable exists
151/// to provide: every copied home carries the original's absolute path, so
152/// pointing `VIDERE_HOME` at a copy still wrote into the source database.
153/// Reported after a 428GB scan aimed at one home landed in another.
154///
155/// A divergence is announced rather than applied silently, so a deliberate
156/// `default_db` is not quietly ignored either.
157/// Pure precedence decision, split out so it is testable without touching the
158/// process-global `VIDERE_HOME`.
159///
160/// Mutating that variable in a test corrupts every *other* test that resolves a
161/// home concurrently - a `Mutex` protects such tests from each other but not
162/// from the rest of the suite, which is exactly how an unrelated
163/// `embeddings_db` test started failing. Same split as
164/// `heic::resolve_qlmanage_concurrency`.
165///
166/// Returns the database to use, and the configured path being overridden when
167/// there is one to report.
168pub(crate) fn decide_db(
169    home: &Path,
170    home_is_explicit: bool,
171    configured: Option<PathBuf>,
172) -> (PathBuf, Option<PathBuf>) {
173    let in_home = home.join("hashes.db");
174    match (home_is_explicit, configured) {
175        // The env var is the more immediate signal and outranks the file.
176        (true, Some(c)) if c != in_home => (in_home, Some(c)),
177        (true, _) => (in_home, None),
178        (false, Some(c)) => (c, None),
179        (false, None) => (in_home, None),
180    }
181}
182
183pub fn resolve_db(explicit: Option<&Path>) -> Result<PathBuf> {
184    if let Some(p) = explicit {
185        // An explicit path is used verbatim and never consults home or config.
186        return Ok(p.to_path_buf());
187    }
188    let home = videre_home()?;
189    let (chosen, overridden) = decide_db(&home, home_is_explicit(), load_config(&home)?.default_db);
190    {
191        let in_home = chosen.clone();
192        if let Some(configured) = overridden {
193            eprintln!("videre: VIDERE_HOME is set, using {}", in_home.display());
194            eprintln!(
195                "  ignoring default_db = {} from that home's config.toml; pass --db to override",
196                configured.display()
197            );
198        }
199    }
200    Ok(chosen)
201}
202
203/// Write one string-valued key into <home>/config.toml, creating the home
204/// dir. Unknown keys already in the file are preserved.
205fn set_string_key(home: &Path, key: &str, value: String) -> Result<()> {
206    std::fs::create_dir_all(home).with_context(|| format!("create {}", home.display()))?;
207    let path = config_path(home);
208    let mut table: toml::Table = match std::fs::read_to_string(&path) {
209        Ok(t) => t
210            .parse()
211            .with_context(|| format!("malformed config {}", path.display()))?,
212        Err(e) if e.kind() == std::io::ErrorKind::NotFound => toml::Table::new(),
213        Err(e) => return Err(e).with_context(|| format!("read {}", path.display())),
214    };
215    table.insert(key.to_string(), toml::Value::String(value));
216    std::fs::write(&path, toml::to_string_pretty(&table)?)
217        .with_context(|| format!("write {}", path.display()))?;
218    Ok(())
219}
220
221/// Write one path-valued key, absolutized. The target need not exist yet (you
222/// may set it before the first scan).
223fn set_path_key(home: &Path, key: &str, value: &Path) -> Result<()> {
224    let abs = std::path::absolute(value)
225        .with_context(|| format!("cannot absolutize {}", value.display()))?;
226    set_string_key(home, key, abs.to_string_lossy().into_owned())
227}
228
229/// Remove one key from <home>/config.toml. Missing file or key is a no-op.
230fn unset_key(home: &Path, key: &str) -> Result<()> {
231    let path = config_path(home);
232    let text = match std::fs::read_to_string(&path) {
233        Ok(t) => t,
234        Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(()),
235        Err(e) => return Err(e).with_context(|| format!("read {}", path.display())),
236    };
237    let mut table: toml::Table = text
238        .parse()
239        .with_context(|| format!("malformed config {}", path.display()))?;
240    if table.remove(key).is_some() {
241        std::fs::write(&path, toml::to_string_pretty(&table)?)
242            .with_context(|| format!("write {}", path.display()))?;
243    }
244    Ok(())
245}
246
247pub fn set_default_db(home: &Path, db: &Path) -> Result<()> {
248    set_path_key(home, "default_db", db)
249}
250
251pub fn unset_default_db(home: &Path) -> Result<()> {
252    unset_key(home, "default_db")
253}
254
255pub fn set_default_path(home: &Path, dir: &Path) -> Result<()> {
256    set_path_key(home, "default_path", dir)
257}
258
259pub fn unset_default_path(home: &Path) -> Result<()> {
260    unset_key(home, "default_path")
261}
262
263/// Write one integer-valued key, as a TOML integer rather than a string so it
264/// round-trips through `positive_int_key`.
265fn set_int_key(home: &Path, key: &str, value: i64) -> Result<()> {
266    std::fs::create_dir_all(home).with_context(|| format!("create {}", home.display()))?;
267    let path = config_path(home);
268    let mut table: toml::Table = match std::fs::read_to_string(&path) {
269        Ok(t) => t
270            .parse()
271            .with_context(|| format!("malformed config {}", path.display()))?,
272        Err(e) if e.kind() == std::io::ErrorKind::NotFound => toml::Table::new(),
273        Err(e) => return Err(e).with_context(|| format!("read {}", path.display())),
274    };
275    table.insert(key.to_string(), toml::Value::Integer(value));
276    std::fs::write(&path, toml::to_string_pretty(&table)?)
277        .with_context(|| format!("write {}", path.display()))?;
278    Ok(())
279}
280
281/// Assumed floor read rate in MB/s. Rejects zero: as a read rate it means an
282/// unbounded timeout, which is exactly the hang the timeout exists to prevent.
283pub fn set_min_read_rate(home: &Path, mb_s: u64) -> Result<()> {
284    if mb_s == 0 {
285        bail!("min read rate must be greater than 0 MB/s");
286    }
287    set_int_key(home, "min_read_rate_mb_s", mb_s as i64)
288}
289
290pub fn unset_min_read_rate(home: &Path) -> Result<()> {
291    unset_key(home, "min_read_rate_mb_s")
292}
293
294pub fn set_default_model(home: &Path, model_id: &str) -> Result<()> {
295    set_string_key(home, "default_model", model_id.to_string())
296}
297
298pub fn unset_default_model(home: &Path) -> Result<()> {
299    unset_key(home, "default_model")
300}
301
302/// The configured default embedding model, if any. None means the built-in
303/// default applies (see `videre_core::embeddings::DEFAULT_MODEL_ID`).
304pub fn default_model() -> Result<Option<String>> {
305    Ok(load_config(&videre_home()?)?.default_model)
306}
307
308/// The configured default scan/watch directory, if any (config `path` key,
309/// stored as `default_path`). There is no built-in fallback: None means the
310/// user must pass a directory explicitly.
311pub fn default_path() -> Result<Option<PathBuf>> {
312    Ok(load_config(&videre_home()?)?.default_path)
313}
314
315#[cfg(test)]
316mod tests {
317    use super::*;
318    use std::path::{Path, PathBuf};
319
320    fn tmp_home(tag: &str) -> PathBuf {
321        let dir = std::env::temp_dir().join(format!("videre_home_{}_{}", tag, std::process::id()));
322        let _ = std::fs::remove_dir_all(&dir);
323        std::fs::create_dir_all(&dir).unwrap();
324        dir
325    }
326
327    #[test]
328    fn missing_config_yields_defaults() {
329        let home = tmp_home("missing");
330        assert_eq!(load_config(&home).unwrap(), Config::default());
331        assert_eq!(resolve_db_in(&home).unwrap(), home.join("hashes.db"));
332        let _ = std::fs::remove_dir_all(&home);
333    }
334
335    #[test]
336    fn config_default_db_wins_over_builtin_default() {
337        let home = tmp_home("wins");
338        set_default_db(&home, Path::new("/tmp/custom.db")).unwrap();
339        assert_eq!(
340            resolve_db_in(&home).unwrap(),
341            PathBuf::from("/tmp/custom.db")
342        );
343        let _ = std::fs::remove_dir_all(&home);
344    }
345
346    #[test]
347    fn explicit_path_wins_verbatim() {
348        // Explicit paths never consult home or config.
349        assert_eq!(
350            resolve_db(Some(Path::new("/x/y.db"))).unwrap(),
351            PathBuf::from("/x/y.db")
352        );
353    }
354
355    #[test]
356    fn set_default_db_absolutizes_relative_paths() {
357        let home = tmp_home("abs");
358        set_default_db(&home, Path::new("rel.db")).unwrap();
359        let db = load_config(&home).unwrap().default_db.unwrap();
360        assert!(
361            db.is_absolute(),
362            "saved path must be absolute: {}",
363            db.display()
364        );
365        assert!(db.ends_with("rel.db"));
366        let _ = std::fs::remove_dir_all(&home);
367    }
368
369    #[test]
370    fn set_preserves_unknown_keys() {
371        let home = tmp_home("preserve");
372        std::fs::write(home.join("config.toml"), "future_key = \"x\"\n").unwrap();
373        set_default_db(&home, Path::new("/tmp/a.db")).unwrap();
374        let text = std::fs::read_to_string(home.join("config.toml")).unwrap();
375        assert!(
376            text.contains("future_key"),
377            "unknown keys must survive a rewrite: {text}"
378        );
379        assert!(text.contains("default_db"));
380        let _ = std::fs::remove_dir_all(&home);
381    }
382
383    #[test]
384    fn unset_removes_key_and_is_noop_when_missing() {
385        let home = tmp_home("unset");
386        unset_default_db(&home).unwrap(); // no file: no-op, Ok
387        set_default_db(&home, Path::new("/tmp/a.db")).unwrap();
388        unset_default_db(&home).unwrap();
389        assert_eq!(load_config(&home).unwrap(), Config::default());
390        let _ = std::fs::remove_dir_all(&home);
391    }
392
393    #[test]
394    fn malformed_config_is_error() {
395        let home = tmp_home("malformed");
396        std::fs::write(home.join("config.toml"), "not = = toml").unwrap();
397        let err = load_config(&home).unwrap_err();
398        assert!(format!("{err:#}").contains("malformed config"), "{err:#}");
399        let _ = std::fs::remove_dir_all(&home);
400    }
401
402    #[test]
403    fn default_path_roundtrips_and_absolutizes() {
404        let home = tmp_home("path_roundtrip");
405        set_default_path(&home, Path::new("photos")).unwrap();
406        let dir = load_config(&home).unwrap().default_path.unwrap();
407        assert!(
408            dir.is_absolute(),
409            "saved path must be absolute: {}",
410            dir.display()
411        );
412        assert!(dir.ends_with("photos"));
413        unset_default_path(&home).unwrap();
414        assert_eq!(load_config(&home).unwrap().default_path, None);
415        let _ = std::fs::remove_dir_all(&home);
416    }
417
418    #[test]
419    fn default_model_round_trips_verbatim_without_absolutizing() {
420        // The regression that reusing set_path_key would cause: a model id
421        // contains a slash, so absolutize() turns it into a filesystem path.
422        let home = tmp_home("model_roundtrip");
423        set_default_model(&home, "google/siglip-base-patch16-224").unwrap();
424        assert_eq!(
425            load_config(&home).unwrap().default_model,
426            Some("google/siglip-base-patch16-224".to_string())
427        );
428        let text = std::fs::read_to_string(config_path(&home)).unwrap();
429        assert!(
430            !text.contains("/Users") && !text.contains("//"),
431            "model id must be stored verbatim, got: {text}"
432        );
433        unset_default_model(&home).unwrap();
434        assert_eq!(load_config(&home).unwrap().default_model, None);
435        let _ = std::fs::remove_dir_all(&home);
436    }
437
438    #[test]
439    fn all_three_keys_coexist_independently() {
440        let home = tmp_home("three_keys");
441        set_default_db(&home, Path::new("/tmp/a.db")).unwrap();
442        set_default_path(&home, Path::new("/tmp/photos")).unwrap();
443        set_default_model(&home, "owner/model-224").unwrap();
444
445        let c = load_config(&home).unwrap();
446        assert_eq!(c.default_db, Some(PathBuf::from("/tmp/a.db")));
447        assert_eq!(c.default_path, Some(PathBuf::from("/tmp/photos")));
448        assert_eq!(c.default_model, Some("owner/model-224".to_string()));
449
450        // Unsetting one must not disturb the others.
451        unset_default_model(&home).unwrap();
452        let c = load_config(&home).unwrap();
453        assert_eq!(c.default_db, Some(PathBuf::from("/tmp/a.db")));
454        assert_eq!(c.default_path, Some(PathBuf::from("/tmp/photos")));
455        assert_eq!(c.default_model, None);
456        let _ = std::fs::remove_dir_all(&home);
457    }
458
459    #[test]
460    fn default_model_is_read_as_a_plain_string() {
461        let home = tmp_home("model_read");
462        std::fs::write(
463            config_path(&home),
464            "default_model = \"google/siglip-base-patch16-224\"\n",
465        )
466        .unwrap();
467        assert_eq!(
468            load_config(&home).unwrap().default_model,
469            Some("google/siglip-base-patch16-224".to_string())
470        );
471        let _ = std::fs::remove_dir_all(&home);
472    }
473
474    #[test]
475    fn a_non_string_default_model_is_a_hard_error() {
476        // Same treatment as the path keys: silent fallback would mask a typo.
477        let home = tmp_home("model_badtype");
478        std::fs::write(config_path(&home), "default_model = 42\n").unwrap();
479        let err = load_config(&home).unwrap_err();
480        assert!(format!("{err:#}").contains("must be a string"), "{err:#}");
481        let _ = std::fs::remove_dir_all(&home);
482    }
483
484    #[test]
485    fn db_and_path_keys_coexist_independently() {
486        let home = tmp_home("coexist");
487        set_default_db(&home, Path::new("/tmp/a.db")).unwrap();
488        set_default_path(&home, Path::new("/tmp/photos")).unwrap();
489        let config = load_config(&home).unwrap();
490        assert_eq!(config.default_db, Some(PathBuf::from("/tmp/a.db")));
491        assert_eq!(config.default_path, Some(PathBuf::from("/tmp/photos")));
492        // unsetting one must not disturb the other
493        unset_default_db(&home).unwrap();
494        let config = load_config(&home).unwrap();
495        assert_eq!(config.default_db, None);
496        assert_eq!(config.default_path, Some(PathBuf::from("/tmp/photos")));
497        let _ = std::fs::remove_dir_all(&home);
498    }
499}
500
501#[cfg(test)]
502mod read_rate_tests {
503    use super::*;
504
505    fn home() -> std::path::PathBuf {
506        let d = std::env::temp_dir().join(format!(
507            "videre-rate-{}-{:?}",
508            std::process::id(),
509            std::thread::current().id()
510        ));
511        let _ = std::fs::remove_dir_all(&d);
512        std::fs::create_dir_all(&d).unwrap();
513        d
514    }
515
516    #[test]
517    fn a_rate_round_trips_as_a_number_not_a_string() {
518        // Stored as a TOML string it would parse back as the wrong type and
519        // the key would silently do nothing.
520        let h = home();
521        set_min_read_rate(&h, 50).unwrap();
522        let raw = std::fs::read_to_string(config_path(&h)).unwrap();
523        assert!(raw.contains("min_read_rate_mb_s = 50"), "got: {raw}");
524        assert_eq!(load_config(&h).unwrap().min_read_rate_mb_s, Some(50));
525        let _ = std::fs::remove_dir_all(&h);
526    }
527
528    #[test]
529    fn zero_is_refused_rather_than_written() {
530        // A zero rate means an unbounded timeout, which is the hang the
531        // timeout exists to prevent.
532        let h = home();
533        assert!(set_min_read_rate(&h, 0).is_err());
534        let _ = std::fs::remove_dir_all(&h);
535    }
536
537    #[test]
538    fn a_zero_already_in_the_file_is_rejected_on_read() {
539        // Hand-edited configs exist; the reader cannot trust the writer.
540        let h = home();
541        std::fs::write(config_path(&h), "min_read_rate_mb_s = 0\n").unwrap();
542        assert!(load_config(&h).is_err());
543        let _ = std::fs::remove_dir_all(&h);
544    }
545
546    #[test]
547    fn a_non_integer_is_rejected_with_a_clear_error() {
548        let h = home();
549        std::fs::write(config_path(&h), "min_read_rate_mb_s = \"fast\"\n").unwrap();
550        let e = load_config(&h).unwrap_err().to_string();
551        assert!(e.contains("must be an integer"), "got: {e}");
552        let _ = std::fs::remove_dir_all(&h);
553    }
554
555    #[test]
556    fn absent_means_the_built_in_default_applies() {
557        let h = home();
558        assert_eq!(load_config(&h).unwrap().min_read_rate_mb_s, None);
559        let _ = std::fs::remove_dir_all(&h);
560    }
561
562    #[test]
563    fn unset_removes_it() {
564        let h = home();
565        set_min_read_rate(&h, 33).unwrap();
566        unset_min_read_rate(&h).unwrap();
567        assert_eq!(load_config(&h).unwrap().min_read_rate_mb_s, None);
568        let _ = std::fs::remove_dir_all(&h);
569    }
570}
571
572#[cfg(test)]
573mod db_precedence_tests {
574    use super::*;
575
576    // These test `decide_db` rather than setting VIDERE_HOME, deliberately.
577    // The variable is process-global, so mutating it from a test corrupts every
578    // other test resolving a home at the same moment; an earlier version of
579    // this module did exactly that and made an unrelated embeddings_db test
580    // fail. A pure function needs no such coordination.
581
582    #[test]
583    fn videre_home_outranks_a_config_default_db() {
584        // The reported bug: a home copied from another carries the original's
585        // absolute default_db, so pointing VIDERE_HOME at the copy still wrote
586        // into the source. A 428GB scan landed in the wrong database this way.
587        let home = Path::new("/homes/copy");
588        let configured = Some(PathBuf::from("/homes/original/hashes.db"));
589        let (chosen, overridden) = decide_db(home, true, configured.clone());
590        assert_eq!(chosen, home.join("hashes.db"));
591        assert_eq!(
592            overridden, configured,
593            "the ignored setting must be reportable"
594        );
595    }
596
597    #[test]
598    fn without_the_env_var_the_config_still_wins() {
599        // Unchanged for anyone not using VIDERE_HOME: `videre config set db`
600        // behaves exactly as before.
601        let home = Path::new("/homes/default");
602        let configured = Some(PathBuf::from("/elsewhere/hashes.db"));
603        let (chosen, overridden) = decide_db(home, false, configured);
604        assert_eq!(chosen, PathBuf::from("/elsewhere/hashes.db"));
605        assert!(overridden.is_none(), "nothing was overridden");
606    }
607
608    #[test]
609    fn no_config_falls_back_to_the_home_either_way() {
610        let home = Path::new("/homes/x");
611        assert_eq!(decide_db(home, true, None).0, home.join("hashes.db"));
612        assert_eq!(decide_db(home, false, None).0, home.join("hashes.db"));
613    }
614
615    #[test]
616    fn a_config_naming_the_home_database_is_not_a_divergence() {
617        // Nothing to report when both agree, or every command would print a
618        // notice about a setting that changes nothing.
619        let home = Path::new("/homes/x");
620        let same = Some(home.join("hashes.db"));
621        assert!(decide_db(home, true, same).1.is_none());
622    }
623}