Skip to main content

tga_core/
lib.rs

1//! # tga-core
2//!
3//! Shared types, configuration, database schema, and error definitions for
4//! `trusty-git-analytics`. This crate is the foundation that all other
5//! crates in the workspace (`tga-collect`, `tga-classify`, `tga-report`,
6//! `tga-cli`) depend on.
7//!
8//! ## Modules
9//!
10//! - [`config`] — YAML configuration loading and types
11//! - [`db`] — SQLite database wrapper with WAL mode and versioned migrations
12//! - [`errors`] — crate-wide error enum and `Result` alias
13//! - [`models`] — domain structs for commits, authors, classifications, etc.
14
15#![warn(missing_docs)]
16
17pub mod config;
18pub mod db;
19pub mod errors;
20pub mod models;
21
22pub use errors::{Result, TgaError};
23
24#[cfg(test)]
25mod tests {
26    use std::io::Write;
27
28    use super::*;
29
30    #[test]
31    fn config_loads_from_yaml_file() {
32        let mut tmp = tempfile_like("config.yaml");
33        writeln!(
34            tmp.file,
35            "repositories:\n  - path: /tmp/repo-a\n    name: repo-a\noutput:\n  format: csv\n"
36        )
37        .expect("write");
38        let cfg = config::Config::load(&tmp.path).expect("load");
39        assert_eq!(cfg.repositories.len(), 1);
40        assert_eq!(cfg.repositories[0].name.as_deref(), Some("repo-a"));
41        assert_eq!(
42            cfg.output.as_ref().and_then(|o| o.format.as_deref()),
43            Some("csv")
44        );
45        cfg.validate().expect("validate");
46    }
47
48    #[test]
49    fn config_validate_requires_repositories() {
50        let cfg = config::Config::default();
51        let err = cfg.validate().expect_err("should fail");
52        match err {
53            TgaError::ValidationError(_) => {}
54            other => panic!("unexpected error: {other:?}"),
55        }
56    }
57
58    #[test]
59    fn database_opens_with_wal_and_migrations_apply() {
60        let dir = tempfile_dir();
61        let db_path = dir.path.join("test.db");
62        let db = db::Database::open(&db_path).expect("open");
63
64        // WAL mode must be active for on-disk databases.
65        let mode = db.journal_mode().expect("journal mode");
66        assert_eq!(mode.to_lowercase(), "wal");
67
68        // v1 migration must have been applied.
69        assert!(db.schema_version().expect("version") >= 1);
70
71        // Core tables must exist.
72        for table in [
73            "commits",
74            "authors",
75            "classifications",
76            "files",
77            "pull_requests",
78            "schema_migrations",
79        ] {
80            let n: i64 = db
81                .connection()
82                .query_row(
83                    "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name=?1",
84                    [table],
85                    |row| row.get(0),
86                )
87                .expect("query");
88            assert_eq!(n, 1, "table {table} should exist");
89        }
90    }
91
92    #[test]
93    fn migrations_are_idempotent() {
94        let dir = tempfile_dir();
95        let db_path = dir.path.join("idempotent.db");
96        let _db1 = db::Database::open(&db_path).expect("first open");
97        let db2 = db::Database::open(&db_path).expect("second open");
98        // Running again must not duplicate or fail.
99        assert_eq!(db2.schema_version().expect("version"), 1);
100    }
101
102    // --- minimal tempfile helpers (avoid pulling in a `tempfile` dep) ---
103
104    struct TempFile {
105        path: std::path::PathBuf,
106        file: std::fs::File,
107    }
108
109    impl Drop for TempFile {
110        fn drop(&mut self) {
111            let _ = std::fs::remove_file(&self.path);
112        }
113    }
114
115    fn tempfile_like(name: &str) -> TempFile {
116        let mut path = std::env::temp_dir();
117        let unique = format!(
118            "tga-core-{}-{}-{}",
119            std::process::id(),
120            std::time::SystemTime::now()
121                .duration_since(std::time::UNIX_EPOCH)
122                .map(|d| d.as_nanos())
123                .unwrap_or(0),
124            name
125        );
126        path.push(unique);
127        let file = std::fs::File::create(&path).expect("create temp file");
128        TempFile { path, file }
129    }
130
131    struct TempDir {
132        path: std::path::PathBuf,
133    }
134
135    impl Drop for TempDir {
136        fn drop(&mut self) {
137            let _ = std::fs::remove_dir_all(&self.path);
138        }
139    }
140
141    fn tempfile_dir() -> TempDir {
142        let mut path = std::env::temp_dir();
143        let unique = format!(
144            "tga-core-dir-{}-{}",
145            std::process::id(),
146            std::time::SystemTime::now()
147                .duration_since(std::time::UNIX_EPOCH)
148                .map(|d| d.as_nanos())
149                .unwrap_or(0),
150        );
151        path.push(unique);
152        std::fs::create_dir_all(&path).expect("mkdir");
153        TempDir { path }
154    }
155}