1#![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 let mode = db.journal_mode().expect("journal mode");
66 assert_eq!(mode.to_lowercase(), "wal");
67
68 assert!(db.schema_version().expect("version") >= 1);
70
71 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 assert_eq!(db2.schema_version().expect("version"), 1);
100 }
101
102 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}