use tga::core::db::Database;
use tga::report::period_trends::query_author_period_trends;
use tracing::debug;
use super::error::{ProfileError, Result};
use super::types::PeriodBatch;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Window {
Quarterly,
Monthly,
Weekly,
Custom(u32),
}
impl Window {
pub fn window_weeks(self) -> u32 {
match self {
Window::Quarterly => 13,
Window::Monthly => 4,
Window::Weekly => 1,
Window::Custom(n) => n.max(1),
}
}
}
pub fn assemble_period_batches(
db: &Database,
canonical_email: &str,
window: Window,
since: Option<&str>,
until: Option<&str>,
) -> Result<Vec<PeriodBatch>> {
let window_weeks = window.window_weeks();
debug!(
canonical_email,
window_weeks, since, until, "assembling period batches"
);
let summaries = query_author_period_trends(db, canonical_email, window_weeks, since, until)
.map_err(ProfileError::Report)?;
let batches = summaries.into_iter().map(PeriodBatch::from_stats).collect();
Ok(batches)
}
#[cfg(test)]
mod tests {
use super::*;
use rusqlite::params;
use tga::core::db::Database;
fn seed_author(db: &Database, name: &str, email: &str) -> i64 {
db.connection()
.execute(
"INSERT INTO authors (canonical_name, canonical_email, aliases) \
VALUES (?1, ?2, '[]')",
params![name, email],
)
.expect("insert author");
db.connection().last_insert_rowid()
}
fn seed_commit(db: &Database, sha: &str, author_id: i64, timestamp: &str) {
db.connection()
.execute(
"INSERT INTO commits (sha, author_id, author_name, author_email, \
timestamp, message, repository, insertions, deletions) \
VALUES (?1, ?2, 'n', 'e', ?3, 'm', 'repo-a', 5, 2)",
params![sha, author_id, timestamp],
)
.expect("insert commit");
}
#[test]
fn window_to_weeks() {
assert_eq!(Window::Quarterly.window_weeks(), 13);
assert_eq!(Window::Monthly.window_weeks(), 4);
assert_eq!(Window::Weekly.window_weeks(), 1);
assert_eq!(Window::Custom(8).window_weeks(), 8);
assert_eq!(
Window::Custom(0).window_weeks(),
1,
"Custom(0) should floor to 1"
);
}
#[test]
fn assemble_quarterly_batches() {
let db = Database::open_in_memory().expect("open");
let aid = seed_author(&db, "Alice", "alice@example.com");
let weeks = [
"2024-01-01T00:00:00Z",
"2024-01-08T00:00:00Z",
"2024-01-15T00:00:00Z",
"2024-01-22T00:00:00Z",
"2024-01-29T00:00:00Z",
"2024-02-05T00:00:00Z",
"2024-02-12T00:00:00Z",
"2024-02-19T00:00:00Z",
"2024-02-26T00:00:00Z",
"2024-03-04T00:00:00Z",
"2024-03-11T00:00:00Z",
"2024-03-18T00:00:00Z",
"2024-03-25T00:00:00Z",
];
for (i, ts) in weeks.iter().enumerate() {
seed_commit(&db, &format!("sha{i}"), aid, ts);
}
let batches =
assemble_period_batches(&db, "alice@example.com", Window::Quarterly, None, None)
.expect("assemble");
assert_eq!(batches.len(), 1, "13 weeks in one quarterly bucket");
assert_eq!(
batches[0].stats.commit_count, 13,
"all 13 commits in the period"
);
assert!(
batches[0].sampled_diffs.is_empty(),
"sampled_diffs must be empty before diff sampler runs"
);
}
#[test]
fn assemble_monthly_batches() {
let db = Database::open_in_memory().expect("open");
let aid = seed_author(&db, "Bob", "bob@example.com");
let weeks = [
"2024-01-01T00:00:00Z",
"2024-01-08T00:00:00Z",
"2024-01-15T00:00:00Z",
"2024-01-22T00:00:00Z",
"2024-01-29T00:00:00Z",
"2024-02-05T00:00:00Z",
"2024-02-12T00:00:00Z",
"2024-02-19T00:00:00Z",
];
for (i, ts) in weeks.iter().enumerate() {
seed_commit(&db, &format!("bsha{i}"), aid, ts);
}
let batches = assemble_period_batches(&db, "bob@example.com", Window::Monthly, None, None)
.expect("assemble");
assert_eq!(batches.len(), 2, "8 weeks → 2 monthly batches");
assert_eq!(
batches[0].stats.commit_count + batches[1].stats.commit_count,
8,
"total commit count must be 8"
);
}
#[test]
fn assemble_with_date_filter() {
let db = Database::open_in_memory().expect("open");
let aid = seed_author(&db, "Carol", "carol@example.com");
seed_commit(&db, "c1", aid, "2024-01-08T00:00:00Z");
seed_commit(&db, "c2", aid, "2024-01-15T00:00:00Z");
seed_commit(&db, "c3", aid, "2024-02-05T00:00:00Z");
let batches = assemble_period_batches(
&db,
"carol@example.com",
Window::Monthly,
Some("2024-01-01"),
Some("2024-01-31"),
)
.expect("assemble");
let total: u64 = batches.iter().map(|b| b.stats.commit_count).sum();
assert_eq!(total, 2, "filter should yield only the 2 January commits");
}
#[test]
fn assemble_empty_for_no_commits() {
let db = Database::open_in_memory().expect("open");
seed_author(&db, "Dave", "dave@example.com");
let batches =
assemble_period_batches(&db, "dave@example.com", Window::Quarterly, None, None)
.expect("assemble");
assert!(batches.is_empty(), "no commits → empty Vec");
}
#[test]
fn assemble_period_label_propagated() {
let db = Database::open_in_memory().expect("open");
let aid = seed_author(&db, "Eve", "eve@example.com");
seed_commit(&db, "e1", aid, "2024-01-08T00:00:00Z");
let batches = assemble_period_batches(&db, "eve@example.com", Window::Weekly, None, None)
.expect("assemble");
assert!(!batches.is_empty());
let p = &batches[0].stats;
assert!(
p.period_label.contains("-W"),
"period_label must contain '-W': {}",
p.period_label
);
assert_eq!(p.since.len(), 10, "since must be YYYY-MM-DD: {}", p.since);
assert_eq!(p.until.len(), 10, "until must be YYYY-MM-DD: {}", p.until);
}
#[test]
fn assemble_custom_window() {
let db = Database::open_in_memory().expect("open");
let aid = seed_author(&db, "Frank", "frank@example.com");
let weeks = [
"2024-01-01T00:00:00Z",
"2024-01-08T00:00:00Z",
"2024-01-15T00:00:00Z",
"2024-01-22T00:00:00Z",
];
for (i, ts) in weeks.iter().enumerate() {
seed_commit(&db, &format!("fsha{i}"), aid, ts);
}
let batches =
assemble_period_batches(&db, "frank@example.com", Window::Custom(2), None, None)
.expect("assemble");
assert_eq!(batches.len(), 2, "4 weeks with Custom(2) → 2 batches");
}
}