From f3043df46516787890288c849e063db0f29e3c7f Mon Sep 17 00:00:00 2001 From: Jason McInerney Date: Mon, 30 Sep 2024 21:38:15 -0700 Subject: [PATCH] Add unit tests --- .gitignore | 1 + database.go | 52 ++++++++++----- database_test.go | 142 +++++++++++++++++++++++++++++++++++++++++ helpers.go | 14 ++-- helpers_test.go | 30 +++++++++ main.go | 81 +++++++---------------- reports.go | 43 +++++++++++-- test/load-test-data.go | 2 +- work.sh | 21 ++++++ 9 files changed, 299 insertions(+), 87 deletions(-) create mode 100644 database_test.go create mode 100644 work.sh diff --git a/.gitignore b/.gitignore index 95c1732..85a33ce 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ *.db *.txt *.out +test/manualcheck.sql \ No newline at end of file diff --git a/database.go b/database.go index a222e92..437b0b0 100644 --- a/database.go +++ b/database.go @@ -72,7 +72,7 @@ func initializeDatabase() *sql.DB { date DATE NOT NULL, half_hour INTEGER NOT NULL CHECK (half_hour BETWEEN 0 AND 47), task_name TEXT, - status TEXT DEFAULT 'in progress', + status TEXT DEFAULT 'active', FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE UNIQUE(task_id, date, half_hour) );` @@ -85,6 +85,37 @@ func initializeDatabase() *sql.DB { return db } +func getDailyTasks(db *sql.DB) ([]Task, error){ + query := ` + SELECT id, name, estimate, actual, created_at, updated_at, done + FROM tasks + WHERE DATE(datetime(created_at, 'localtime')) = DATE('now', 'localtime') + ORDER BY created_at; + ` + + rows, err := db.Query(query) + if err != nil { + log.Fatal(err) + } + defer rows.Close() + + var tasks []Task + for rows.Next() { + var id, estimate, actual int + var name string + var createdAt, updatedAt time.Time + var done bool + + err := rows.Scan(&id, &name, &estimate, &actual, &createdAt, &updatedAt, &done) + if err != nil { + log.Fatal(err) + } + + tasks = append(tasks, Task{ ID: id, Name: name, Estimate: estimate, Actual: actual, CreatedAt: createdAt, UpdatedAt: updatedAt, Done: done }) + } + return tasks, nil +} + func getMonthlyData(db *sql.DB) ([]DayAggregate, error) { query := ` SELECT @@ -133,18 +164,6 @@ func getAllDaysOfMonth() []string { return days } -// func getAllDaysOfWeek() []string { -// var days []string -// now := time.Now() - -// for i := 0; i < 7; i++ { -// day := now.AddDate(0, 0, -int(now.Weekday())+i) -// days = append(days, day.Format("2006-01-02")) -// } - -// return days -// } - func getYearlyData(db *sql.DB, year int) ([]TaskTrackingAggregate, error) { // aggregate half_hours completed for each day for the year query := fmt.Sprintf(` @@ -293,17 +312,19 @@ func addTask(db *sql.DB, name string, estimate int) error { func activateTask(id int) { // get current date in yyyy-mm-dd format // insert into task_tracking table - currentDate := time.Now().Format("2006-01-02") half_hour := getHalfHour(time.Now().Hour(), time.Now().Minute()) + insertTrackingTask(id, currentDate, half_hour) +} +func insertTrackingTask(id int, currentDate string, halfHour int) { query := ` INSERT INTO task_tracking (task_id, date, half_hour, status) VALUES (?, ?, ?, 'active') ON CONFLICT(task_id, date, half_hour) DO UPDATE SET status = 'done'; ` - _, err := db.Exec(query, id, currentDate, half_hour) + _, err := db.Exec(query, id, currentDate, halfHour) if err != nil { log.Fatal(err) } @@ -316,7 +337,6 @@ func updateActual(id int) { if err != nil { log.Fatal(err) } - rowsAffected, err := result.RowsAffected() if err != nil { diff --git a/database_test.go b/database_test.go new file mode 100644 index 0000000..71167e2 --- /dev/null +++ b/database_test.go @@ -0,0 +1,142 @@ +package main + +import ( + "database/sql" + "fmt" + "time" + + //"log" + "testing" + //"time" + + _ "github.com/mattn/go-sqlite3" // SQLite driver +) + +// Mock database connection for tests +var testDB *sql.DB + +// Setup test database (in-memory SQLite) +func setupTestDB2(t *testing.T) { + var err error + testDB, err = sql.Open("sqlite3", ":memory:") + if err != nil { + t.Fatalf("failed to open test database: %v", err) + } + + // Create tasks table if it doesn't exist + createTableQuery := ` + CREATE TABLE IF NOT EXISTS tasks ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + estimate INTEGER NOT NULL, + actual INTEGER DEFAULT 0, + created_at DATETIME DEFAULT (datetime('now', 'localtime')), + updated_at DATETIME DEFAULT (datetime('now', 'localtime')), + done BOOLEAN DEFAULT 0 + );` + _, err = testDB.Exec(createTableQuery) + if err != nil { + t.Fatalf("failed to create task_tracking table: %v", err) + } + + // Create the task_tracking table schema for testing + query := ` + CREATE TABLE task_tracking ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + task_id INTEGER NOT NULL, + date TEXT NOT NULL, + half_hour INTEGER NOT NULL, + status TEXT NOT NULL, + UNIQUE(task_id, date, half_hour) + );` + _, err = testDB.Exec(query) + if err != nil { + t.Fatalf("failed to create task_tracking table: %v", err) + } +} + +// Helper function to check if a task exists in the database +func taskExists(t *testing.T, taskID int, date string, halfHour int, status string) bool { + var count int + query := ` + SELECT COUNT(*) FROM task_tracking + WHERE task_id = ? AND date = ? AND half_hour = ? AND status = ?` + + err := testDB.QueryRow(query, taskID, date, halfHour, status).Scan(&count) + if err != nil { + t.Fatalf("failed to query task: %v", err) + } + return count > 0 +} + +// Function to test activateTask +func TestActivateTask(t *testing.T) { + setupTestDB2(t) // Initialize the test database + + db = testDB + + mockDate := time.Now().Format("2006-01-02") + mockHalfHour := getHalfHour(time.Now().Hour(), time.Now().Minute()) + + fmt.Println(mockDate) + fmt.Println(mockHalfHour) + + insertTrackingTask(1, mockDate, mockHalfHour) + + // Check if the task was inserted with 'active' status + if !taskExists(t, 1, mockDate, mockHalfHour, "active") { + t.Errorf("expected task to be 'active', but it wasn't found") + } + + // Test: Update the task to 'done' status + insertTrackingTask(1, mockDate, mockHalfHour) + + // Check if the task was updated to 'done' status + if !taskExists(t, 1, mockDate, mockHalfHour, "done") { + t.Errorf("expected task to be updated to 'done', but it wasn't") + } +} + +func TestInsertTrackingTask(t *testing.T) { + setupTestDB2(t) // Initialize the test database + + db = testDB + + mockDate := time.Now().Format("2006-01-02") + mockHalfHour := getHalfHour(time.Now().Hour(), time.Now().Minute()) + + // Test: Insert a new task with 'active' status + insertTrackingTask(1, mockDate, mockHalfHour) + + // Check if the task was inserted with 'active' status + if !taskExists(t, 1, mockDate, mockHalfHour, "active") { + t.Errorf("expected task to be 'active', but it wasn't found") + } + + // Test: Update the task to 'done' status + insertTrackingTask(1, mockDate, mockHalfHour) + + // Check if the task was updated to 'done' status + if !taskExists(t, 1, mockDate, mockHalfHour, "done") { + t.Errorf("expected task to be updated to 'done', but it wasn't") + } +} + +// add test for getDailyTasks +func TestGetDailyTasks(t *testing.T) { + setupTestDB2(t) // Initialize the test database + + db = testDB + + // Test: Insert a new task with 'active' status + err := addTask(db, "Task 1", 1) + if err != nil { + t.Fatalf("failed to add task: %v", err) + } + + // Test: Get all tasks for the day + tasks, _ := getDailyTasks(db) + if len(tasks) != 1 { + t.Errorf("expected 1 task, got %d", len(tasks)) + } +} \ No newline at end of file diff --git a/helpers.go b/helpers.go index 6c4a6aa..565e8a2 100644 --- a/helpers.go +++ b/helpers.go @@ -6,14 +6,16 @@ import ( // Get start and end dates for the current week (Monday to Sunday) func getCurrentWeek() (time.Time, time.Time) { - // Get the current date now := time.Now().Local() - // Find the Monday of the current week - sunday := now.AddDate(0, 0, -int(now.Weekday())) - // Get the Sunday of the current week - saturday := sunday.AddDate(0, 0, 6) + + sunday, saturday := getWeek(now) + return sunday, saturday +} - // Format as YYYY-MM-DD +func getWeek(mytime time.Time) (time.Time, time.Time) { + + sunday := mytime.AddDate(0, 0, -int(mytime.Weekday())) + saturday := sunday.AddDate(0, 0, 6) return sunday, saturday } diff --git a/helpers_test.go b/helpers_test.go index 7ce3f9e..3154005 100644 --- a/helpers_test.go +++ b/helpers_test.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "testing" + "time" _ "github.com/mattn/go-sqlite3" ) @@ -181,6 +182,35 @@ func TestHalfHour(t *testing.T) { t.Errorf("getHalfHour(%d, %d) = %d; want %d", tt.hour, tt.minute, result, tt.expected) } } +} + +// Mock version of time.Now() to simulate a specific date +func mockTimeNow(mockedTime time.Time) func() time.Time { + return func() time.Time { + return mockedTime + } +} + +// Test for getCurrentWeek +func TestGetCurrentWeek(t *testing.T) { + // Mock the current date to be a Wednesday, September 20, 2024 + mockDate := time.Date(2024, time.September, 20, 0, 0, 0, 0, time.Local) + // Replace time.Now with the mock + timeNow := mockTimeNow(mockDate) + // Call the function to get the week range + sunday, saturday := getWeek(timeNow()) + + // Expected Sunday and Saturday for the week of September 15–21, 2024 + expectedSunday := time.Date(2024, time.September, 15, 0, 0, 0, 0, time.Local) + expectedSaturday := time.Date(2024, time.September, 21, 0, 0, 0, 0, time.Local) + + // Check if the calculated Sunday and Saturday match the expected values + if !sunday.Equal(expectedSunday) { + t.Errorf("expected Sunday to be %v, but got %v", expectedSunday, sunday) + } + if !saturday.Equal(expectedSaturday) { + t.Errorf("expected Saturday to be %v, but got %v", expectedSaturday, saturday) + } } \ No newline at end of file diff --git a/main.go b/main.go index c05c14e..b5e98d9 100644 --- a/main.go +++ b/main.go @@ -9,7 +9,6 @@ import ( "os" "strconv" "strings" - ) var db *sql.DB @@ -42,16 +41,20 @@ func main() { handleDeleteCommand(os.Args[2:]) case "activate": handleActivateCommand(os.Args[2:]) + case "backfill": + handleBackfillCommand(os.Args[2:]) case "load": handleLoadTasksCommand(db, os.Args[2:]) - case "simple": - handlesimpleReport(db) + case "today": + // use the handle report command with the --type flag set to today + os.Args = append(os.Args, "--type", "today") + handleReportCommand(os.Args[2:]) case "version": fmt.Println("tomatillo v0.1") case "help": handleHelpCommand() default: - fmt.Println("expected 'add', 'activate', 'simple', 'list', 'update', 'done', 'edit', 'delete', 'load', version', or 'report' subcommands") + fmt.Println("expected 'add', 'activate', 'today', 'list', 'update', 'done', 'edit', 'delete', 'load', version', or 'report' subcommands") os.Exit(1) } } @@ -67,7 +70,6 @@ func handleHelpCommand() { fmt.Println(" report Generate a report") fmt.Println(" delete Delete a task") fmt.Println(" load Load tasks from a file") - fmt.Println(" simple Generate a simple report") fmt.Println(" version Print the version of the application") } @@ -88,55 +90,6 @@ func handleAddCommand(db *sql.DB, args []string) error { return nil } -func handlesimpleReport(db *sql.DB) { - query := ` - SELECT id, name, estimate, actual, created_at, updated_at, done - FROM tasks - WHERE DATE(datetime(created_at, 'localtime')) = DATE('now', 'localtime') - ORDER BY created_at; - ` - - rows, err := db.Query(query) - if err != nil { - log.Fatal(err) - } - defer rows.Close() - - var totalEstimate, totalActual, totalTasks, completedTasks int - - fmt.Printf("| %-3s | %-5s | %-54s | %-4s | %-4s |\n", "ID", "Done?", "Task", "Est.", "Act.") - fmt.Println("| --- | ----- | ------------------------------------------------------ | ---- | ---- |") - - for rows.Next() { - var id, estimate, actual int - var name, createdAt, updatedAt string - var done bool - - err := rows.Scan(&id, &name, &estimate, &actual, &createdAt, &updatedAt, &done) - if err != nil { - log.Fatal(err) - } - - status := "No" - if done { - status = "Yes" - completedTasks++ - } - - totalEstimate += estimate - totalActual += actual - totalTasks++ - - fmt.Printf("| %-3d | %-5s | %-54s | %-4d | %-4d |\n", id, status, name, estimate, actual) - } - - fmt.Println("\n**Summary:**") - fmt.Printf("\n- Estimated: %d\n- Actual: %d\n", - totalEstimate, totalActual) - fmt.Printf("- Tasks Completed/Total: %d of %d\n", completedTasks, totalTasks) -} - - func handleLoadTasksCommand(db *sql.DB, args []string) error { loadTasksFlag := flag.NewFlagSet("load", flag.ExitOnError) filePath := loadTasksFlag.String("file", "", "Path to the file containing tasks and estimates") @@ -211,6 +164,20 @@ func handleActivateCommand(args []string) { activateTask(*activateTaskId) } +func handleBackfillCommand(args []string) { + backfillFlag := flag.NewFlagSet("backfill", flag.ExitOnError) + backfillTaskId := backfillFlag.Int("id", 0, "Task ID to backfill") + backfillTaskDate := backfillFlag.String("date", "", "Date to backfill the task") + backfillTaskHalfHour := backfillFlag.Int("halfhour", 0, "Half hour to backfill the task") + backfillFlag.Parse(args) + + if *backfillTaskId <= 0 { + log.Println("Please provide a valid task ID.") + os.Exit(1) + } + insertTrackingTask(*backfillTaskId, *backfillTaskDate, *backfillTaskHalfHour) +} + // Helper function to handle the 'update' command func handleUpdateCommand(args []string) { @@ -268,12 +235,12 @@ func handleReportCommand(args []string) { generateMonthlyReport(db) } else if *reportType == "blockmonth" { generateMonthlyBlockReport() - } else if *reportType == "daily" { - generateDailyReport("2024-09-21") + } else if *reportType == "today" { + generateTodayReport() } else if *reportType == "blockweek" { generateWeeklyBlockReport() } else { - generateWeeklyBlockReport() + generateTodayReport() } } diff --git a/reports.go b/reports.go index 83698e3..9ceaccd 100644 --- a/reports.go +++ b/reports.go @@ -64,7 +64,7 @@ func generateMonthlyReport(db *sql.DB) { } } -func generateDailyReport(date string) { +func generateDailyBlock(date string) { tasks, err := getTasksForDay(date) if err != nil { fmt.Println("Error fetching tasks:", err) @@ -108,8 +108,7 @@ func generateWeeklyBlockReport() { // Iterate through each day of the week for i := 0; i < 7; i++ { day := time.Now().Local().AddDate(0, 0, -int(time.Now().Weekday())+i).Format("2006-01-02") - //fmt.Printf("\n%s\n", day) - generateDailyReport(day) // Reuse your daily report generation + generateDailyBlock(day) // Reuse your daily report generation } fmt.Println("╚════════════════════════════════════════════════════════════════════════════════════╝ ") @@ -128,7 +127,7 @@ func generateMonthlyBlockReport() { for day := startOfMonth; !day.After(endOfMonth); day = day.AddDate(0, 0, 1) { dayStr := day.Format("2006-01-02") //fmt.Printf("\n%s\n", day) - generateDailyReport(dayStr) // Reuse your daily report generation + generateDailyBlock(dayStr) // Reuse your daily report generation } fmt.Println("╚════════════════════════════════════════════════════════════════════════════════════╝ ") @@ -137,12 +136,13 @@ func generateMonthlyBlockReport() { // generate a report for yearly data of tasks completed. each row is a month and each column is a day func generateYearlyCountReport() { - reports, _ := getYearlyData(db, 2024) + t := time.Now().Local() + year := t.Year() + + reports, _ := getYearlyData(db, year) var currentMonth time.Month var lastDay int startOfYear, endOfYear := getCurrentYear() - fmt.Println("start of year", startOfYear) - fmt.Println("end of year", endOfYear) fmt.Println("╔═══════════════════════════════════════════╗ ") fmt.Printf( "║ Yearly Report (%s to %s) ║ \n", startOfYear.Format("2006-01-02"), endOfYear.Format("2006-01-02")) fmt.Println("╠═══════════════════════════════════════════╩═══════════════════════════════════════════════════════╗ ") @@ -195,6 +195,35 @@ func generateTaskReport(tasks []Task) { } } +func generateTodayReport() { + tasks, err := getDailyTasks(db) + if err != nil { + log.Fatal(err) + } + + fmt.Println("╔════════════════════════════════════════════════════════════════════════════════════╗ ") + fmt.Printf( "║ %-3s %-5s %-54s %-4s %-4s ║\n", "ID", "Done?", "Task", "Est.", "Act.") + fmt.Println("╠════════════════════════════════════════════════════════════════════════════════════╣ ") + + for _, task := range tasks { + id := task.ID + name := task.Name + estimate := task.Estimate + actual := task.Actual + done := task.Done + completedTasks := 0 + + status := "No" + if done { + status = "Yes" + completedTasks++ + } + fmt.Printf("║ %-3d %-5s %-54s %-4d %-4d ║\n", id, status, name, estimate, actual) + } + fmt.Println("╚════════════════════════════════════════════════════════════════════════════════════╝ ") +} + + func listTasks(days int, status string) { tasks, err := getTasks(days, status) if err != nil { diff --git a/test/load-test-data.go b/test/load-test-data.go index 2416041..d22ff23 100644 --- a/test/load-test-data.go +++ b/test/load-test-data.go @@ -11,7 +11,7 @@ import ( ) func main() { - db, err := sql.Open("sqlite3", "./tomatillo.db") + db, err := sql.Open("sqlite3", ":memory:") if err != nil { log.Fatal(err) } diff --git a/work.sh b/work.sh new file mode 100644 index 0000000..d735cb2 --- /dev/null +++ b/work.sh @@ -0,0 +1,21 @@ +work() { + # usage: work 10m 42, work 10m on task 42. Default is 25m + + duration="${1:-25m}" + task_id="$2" + + if [ -n "$task_id" ]; then + tomatillo activate --id="$task_id" + fi + + timer "$duration" && echo -e "\a" + # Update the tomatillo task if a task id is provided + if [ -n "$task_id" ]; then + tomatillo update --id="$task_id" + fi +} + +rest() { + # usage: rest 10m, rest 60s etc. Default is 5m + timer "${1:-5m}" && echo -e "\a" +} \ No newline at end of file