From 23a2be331d5ca84713cb2a059085088ccb6eb3e3 Mon Sep 17 00:00:00 2001 From: Abdullah Alaadine <125296663+knbr13@users.noreply.github.com> Date: Mon, 19 Feb 2024 19:48:08 +0200 Subject: [PATCH] Add unit tests (#24) * Add test data submodules * return error instead of log.Fatal * modify local since variable instead of the glabal one * create setTimeFlags func * handle setTimeFlags * add unit tests * remove submodules * update expected results in tests for stats.go * add script to setup test data * add test workflow --- .github/workflows/test.yaml | 29 ++++ input.go | 8 +- input_test.go | 43 ++++++ main.go | 35 ++--- print.go | 10 +- print_test.go | 199 ++++++++++++++++++++++++++ scan_test.go | 50 +++++++ setup-test.sh | 21 +++ stats_test.go | 110 +++++++++++++++ utils.go | 30 ++++ utils_test.go | 273 ++++++++++++++++++++++++++++++++++++ 11 files changed, 774 insertions(+), 34 deletions(-) create mode 100644 .github/workflows/test.yaml create mode 100644 input_test.go create mode 100644 print_test.go create mode 100644 scan_test.go create mode 100755 setup-test.sh create mode 100644 stats_test.go create mode 100644 utils_test.go diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..41b0886 --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,29 @@ +name: Go Test + +on: + pull_request: + branches: + - main + +jobs: + test: + name: Test + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Set up Go + uses: actions/setup-go@v2 + with: + go-version: 1.22 + + - name: chmod + run: chmod +x ./setup-test.sh + + - name: setup tests + run: ./setup-test.sh + + - name: Run tests + run: go test ./... diff --git a/input.go b/input.go index 274d4e9..54cc2c2 100644 --- a/input.go +++ b/input.go @@ -3,23 +3,21 @@ package main import ( "bufio" "fmt" - "os" "strings" "github.com/gookit/color" ) -func getPathFromUser(reader *bufio.Reader) string { +func getPathFromUser(reader *bufio.Reader) (string, error) { for { fmt.Print("enter the folder path to scan for Git repositories: ") input, err := reader.ReadString('\n') if err != nil { - fmt.Fprintf(os.Stderr, "gitcs: error reading input: %s\n", err.Error()) - os.Exit(1) + return "", err } input = strings.TrimSpace(input) if isValidFolderPath(input) { - return input + return input, nil } fmt.Println(color.Yellow.Sprintf("gitcs: path %q is not found, please enter a valid folder path", input)) } diff --git a/input_test.go b/input_test.go new file mode 100644 index 0000000..4cdc914 --- /dev/null +++ b/input_test.go @@ -0,0 +1,43 @@ +package main + +import ( + "bufio" + "strings" + "testing" +) + +func TestGetPathFromUser(t *testing.T) { + testCases := []struct { + description string + input string + expectedPath string + expectedError bool + }{ + { + description: "Valid folder path", + input: "./test_data", + expectedPath: "./test_data", + expectedError: false, + }, + { + description: "Invalid folder path", + input: "./path/to/invalid/folder", + expectedPath: "./", + expectedError: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.description, func(t *testing.T) { + reader := bufio.NewReader(strings.NewReader(tc.input + "\n")) + actualPath, err := getPathFromUser(reader) + if err != nil && !tc.expectedError { + t.Errorf("getPathFromUser() error = %v, want %v", err, tc.expectedError) + } + + if !tc.expectedError && actualPath != tc.expectedPath { + t.Errorf("Expected path: %s, got: %s", tc.expectedPath, actualPath) + } + }) + } +} diff --git a/main.go b/main.go index 0c523c0..53baa2b 100644 --- a/main.go +++ b/main.go @@ -4,7 +4,6 @@ import ( "bufio" "flag" "fmt" - "net/mail" "os" "strings" "time" @@ -23,37 +22,23 @@ func main() { flag.StringVar(&email, "email", strings.TrimSpace(getGlobalEmailFromGit()), "you Git email") flag.Parse() - var err error - if untilflag != "" { - until, err = time.Parse("2006-01-02", untilflag) - if err != nil { - fmt.Fprintln(os.Stderr, color.Red.Sprintf("gitcs: invalid 'until' date format. please use the format: 2006-01-02")) - os.Exit(1) - } - if until.After(now) { - until = now - } - } else { - until = now - } - if sinceflag != "" { - since, err = time.Parse("2006-01-02", sinceflag) - if err != nil { - fmt.Fprintln(os.Stderr, color.Red.Sprintf("gitcs: invalid 'since' date format. please use the format: 2006-01-02")) - os.Exit(1) - } - } else { - since = time.Date(until.Year(), until.Month(), until.Day(), 0, 0, 0, 0, until.Location()).AddDate(0, 0, -sixMonthsInDays) + err := setTimeFlags(sinceflag, untilflag) + if err != nil { + fmt.Fprint(os.Stderr, color.Red.Sprintf("gitcs: %s\n", err.Error())) + os.Exit(1) } - _, err = mail.ParseAddress(strings.TrimSpace(email)) - if err != nil { + if valid := isValidEmail(email); !valid { fmt.Fprintln(os.Stderr, color.Red.Sprintf("gitcs: invalid 'email' address")) os.Exit(1) } reader := bufio.NewReader(os.Stdin) - folder := getPathFromUser(reader) + folder, err := getPathFromUser(reader) + if err != nil { + fmt.Fprint(os.Stderr, color.Red.Sprintf("gitcs: error reading input: %s\n", err.Error())) + os.Exit(1) + } s := spinner.New(spinner.CharSets[6], 100*time.Millisecond, spinner.WithSuffix(" loading...")) diff --git a/print.go b/print.go index 39bd63f..0cb6dd1 100644 --- a/print.go +++ b/print.go @@ -31,8 +31,6 @@ func getDay(i int) string { } func printTable(commits map[int]int) { - fmt.Printf("%s %s\n", sixEmptySpaces, buildHeader(since, until)) - max := getMaxValue(commits) for since.Weekday() != time.Sunday { since = since.AddDate(0, 0, -1) } @@ -40,17 +38,21 @@ func printTable(commits map[int]int) { until = until.AddDate(0, 0, 1) } + fmt.Printf("%s %s\n", sixEmptySpaces, buildHeader(since, until)) + max := getMaxValue(commits) + s := strings.Builder{} + s1 := since for i := 0; i < 7; i++ { s.WriteString(fmt.Sprintf("%-5s", getDay(i))) - sn2 := since + sn2 := s1 for !sn2.After(until) { d := daysAgo(sn2) s.WriteString(printCell(commits[d], max)) sn2 = sn2.AddDate(0, 0, 7) } - since = since.AddDate(0, 0, 1) + s1 = s1.AddDate(0, 0, 1) fmt.Println(s.String()) s.Reset() } diff --git a/print_test.go b/print_test.go new file mode 100644 index 0000000..adb1bdf --- /dev/null +++ b/print_test.go @@ -0,0 +1,199 @@ +package main + +import ( + "fmt" + "io" + "os" + "strings" + "testing" + "time" + + "github.com/gookit/color" +) + +func TestGetDay(t *testing.T) { + tests := []struct { + name string + day int + want string + }{ + { + name: "Monday", + day: 1, + want: "Mon", + }, + { + name: "Wednesday", + day: 3, + want: "Wed", + }, + { + name: "Friday", + day: 5, + want: "Fri", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := getDay(tt.day); got != tt.want { + t.Errorf("getDay() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestBuildHeader(t *testing.T) { + type args struct { + start time.Time + end time.Time + } + tests := []struct { + name string + args args + want string + }{ + { + name: "test 1", + args: args{ + start: time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC), + end: time.Date(2023, 2, 1, 0, 0, 0, 0, time.UTC), + }, + want: "Jan Feb ", + }, + { + name: "test 2", + args: args{ + start: time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC), + end: time.Date(2023, 3, 1, 0, 0, 0, 0, time.UTC), + }, + want: "Jan Feb Mar ", + }, + { + name: "test 2", + args: args{ + start: time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC), + end: time.Date(2023, 5, 1, 0, 0, 0, 0, time.UTC), + }, + want: "Jan Feb Mar Apr May ", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := buildHeader(tt.args.start, tt.args.end); got != tt.want { + t.Errorf("buildHeader() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestPrintCell(t *testing.T) { + testCases := []struct { + description string + val int + maxValue int + expectedMessage string + }{ + { + description: "Zero value (/8)", + val: 0, + maxValue: 10, + expectedMessage: color.New(color.FgWhite, color.BgBlack).Sprintf(" - "), + }, + { + description: "Lower bound value (/4)", + val: 2, + maxValue: 10, + expectedMessage: color.New(color.FgBlack, color.BgLightCyan).Sprintf(" 2 "), + }, + { + description: "Middle value (/2)", + val: 4, + maxValue: 10, + expectedMessage: color.New(color.FgBlack, color.BgHiBlue).Sprintf(" 4 "), + }, + { + description: "Upper bound value 1", + val: 8, + maxValue: 10, + expectedMessage: color.New(color.FgBlack, color.BgBlue).Sprintf(" 8 "), + }, + { + description: "Upper bound value 2", + val: 10, + maxValue: 10, + expectedMessage: color.New(color.FgBlack, color.BgBlue).Sprintf(" 10 "), + }, + } + + for _, tc := range testCases { + t.Run(tc.description, func(t *testing.T) { + actualMessage := printCell(tc.val, tc.maxValue) + if actualMessage != tc.expectedMessage { + t.Errorf("Expected message: %s, got: %s", tc.expectedMessage, actualMessage) + } + }) + } +} + +func TestPrintTable(t *testing.T) { + commits := map[int]int{ + 0: 5, + 1: 8, + 2: 12, + 3: 3, + 4: 0, + 5: 10, + 6: 7, + 7: 4, + 8: 6, + 9: 9, + 10: 2, + 11: 15, + 12: 1, + 13: 0, + } + + since = time.Date(2024, 2, 7, 0, 0, 0, 0, time.UTC) + until = time.Date(2024, 2, 19, 0, 0, 0, 0, time.UTC) + + oldStdout := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + + printTable(commits) + w.Close() + + dat, err := io.ReadAll(r) + if err != nil { + t.Errorf("Error reading from pipe: %s", err.Error()) + } + + os.Stdout = oldStdout + + var buf strings.Builder + _, _ = fmt.Fprint(&buf, string(dat)) + + s := strings.Builder{} + s1 := since + + s.WriteString(fmt.Sprintf("%s %s\n", sixEmptySpaces, buildHeader(since, until))) + + max := getMaxValue(commits) + for i := 0; i < 7; i++ { + s.WriteString(fmt.Sprintf("%-5s", getDay(i))) + sn2 := s1 + for !sn2.After(until) { + d := daysAgo(sn2) + s.WriteString(printCell(commits[d], max)) + sn2 = sn2.AddDate(0, 0, 7) + } + s1 = s1.AddDate(0, 0, 1) + s.WriteRune('\n') + } + + expectedOutput := s.String() + if strings.TrimSpace(buf.String()) != strings.TrimSpace(expectedOutput) { + t.Errorf("Expected output: %q\n\n, got: %q", expectedOutput, buf.String()) + } + +} diff --git a/scan_test.go b/scan_test.go new file mode 100644 index 0000000..6ba15bd --- /dev/null +++ b/scan_test.go @@ -0,0 +1,50 @@ +package main + +import ( + "os" + "path" + "testing" +) + +func TestScanGitFolders(t *testing.T) { + wd, err := os.Getwd() + if err != nil { + t.Fatalf("failed to get working directory: %v", err) + } + + test := []struct { + Name string + Root string + Want []string + }{ + { + Name: "3 expected repos", + Root: path.Join(wd, "test_data"), + Want: []string{path.Join(wd, "test_data", "project_1"), path.Join(wd, "test_data", "project_2"), path.Join(wd, "test_data", "project_3")}, + }, + { + Name: "no expected repos", + Root: path.Join(wd, ".github"), + Want: []string{}, + }, + } + + for _, tt := range test { + t.Run(tt.Name, func(t *testing.T) { + got, err := scanGitFolders(tt.Root) + if err != nil { + t.Fatalf("failed to scan git folders: %v", err) + } + + if len(got) != len(tt.Want) { + t.Fatalf("expected %d git folders, got %d", len(tt.Want), len(got)) + } + + for i := range got { + if got[i] != tt.Want[i] { + t.Fatalf("expected %s, got %s", tt.Want[i], got[i]) + } + } + }) + } +} diff --git a/setup-test.sh b/setup-test.sh new file mode 100755 index 0000000..9269bde --- /dev/null +++ b/setup-test.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +projects=("project_1" "project_2" "project_3") + +for project in "${projects[@]}"; do + mkdir -p "test_data/$project" + cd "test_data/$project" || exit + git init + git config user.name "tester" + git config user.email "tester@test.com" + + for ((i = 1; i <= 3; i++)); do + echo "Content $i" > "file_$i.txt" + + git add "file_$i.txt" + + git commit -m "Commit $i" + done + + cd ../../ || exit +done diff --git a/stats_test.go b/stats_test.go new file mode 100644 index 0000000..0288fb6 --- /dev/null +++ b/stats_test.go @@ -0,0 +1,110 @@ +package main + +import ( + "os" + "path" + "testing" + "time" +) + +func TestFillCommits(t *testing.T) { + now := time.Now() + wd, err := os.Getwd() + if err != nil { + t.Fatalf("failed to get working directory: %v", err) + } + + commitsDate := time.Date(now.Year(), now.Month(), now.Day(), 4, 0, 0, 0, now.Location()) + days := daysAgo(commitsDate) + tests := []struct { + Name string + Path string + Email string + Expected map[int]int + }{ + { + Name: "test 1", + Path: path.Join(wd, "test_data", "project_1"), + Email: "tester@test.com", + Expected: map[int]int{days: 3}, + }, + { + Name: "test 2", + Path: path.Join(wd, "test_data", "project_2"), + Email: "tester@test.com", + Expected: map[int]int{days: 3}, + }, + { + Name: "test 3", + Path: path.Join(wd, "test_data", "project_3"), + Email: "tester@test.com", + Expected: map[int]int{days: 3}, + }, + } + + for _, tt := range tests { + t.Run(tt.Name, func(t *testing.T) { + commits := map[int]int{} + err = fillCommits(tt.Path, tt.Email, commits) + if err != nil { + t.Fatalf("failed to fill commits in %q: %v", tt.Path, err) + } + if len(commits) != len(tt.Expected) { + t.Errorf("fillCommits() = %v, want %v", commits, tt.Expected) + } + for k, v := range tt.Expected { + if commits[k] != v { + t.Errorf("fillCommits() = %v, want %v", commits[k], v) + } + } + }) + } +} + +func TestProcessRepos(t *testing.T) { + wd, err := os.Getwd() + if err != nil { + t.Fatalf("failed to get working directory: %v", err) + } + + commitsDate := time.Date(now.Year(), now.Month(), now.Day(), 4, 0, 0, 0, now.Location()) + days := daysAgo(commitsDate) + + tests := []struct { + Name string + Repos []string + Email string + Expected map[int]int + }{ + { + Name: "test 1", + Repos: []string{ + path.Join(wd, "test_data", "project_1"), + path.Join(wd, "test_data", "project_2"), + path.Join(wd, "test_data", "project_3"), + }, + Email: "tester@test.com", + Expected: map[int]int{days: 9}, + }, + { + Name: "test 2", + Repos: []string{}, + Email: "tester@test.com", + Expected: map[int]int{}, + }, + } + + for _, tt := range tests { + t.Run(tt.Name, func(t *testing.T) { + commits := processRepos(tt.Repos, tt.Email) + if len(commits) != len(tt.Expected) { + t.Errorf("processRepos11() = %v, want %v", commits, tt.Expected) + } + for k, v := range tt.Expected { + if commits[k] != v { + t.Errorf("processRepos(22) = %v, want %v", commits[k], v) + } + } + }) + } +} diff --git a/utils.go b/utils.go index 30b6e0f..65570fe 100644 --- a/utils.go +++ b/utils.go @@ -3,11 +3,17 @@ package main import ( "fmt" "math" + "net/mail" "os" "os/exec" "time" ) +func isValidEmail(email string) bool { + _, err := mail.ParseAddress(email) + return err == nil +} + func isValidFolderPath(folder string) bool { // Check if the folder exists and is a directory info, err := os.Stat(folder) @@ -54,3 +60,27 @@ func getGlobalEmailFromGit() string { return string(localEmail) } + +func setTimeFlags(sinceflag, untilflag string) error { + var err error + if untilflag != "" { + until, err = time.Parse("2006-01-02", untilflag) + if err != nil { + return fmt.Errorf("invalid 'until' date format. please use the format: 2006-01-02") + } + if until.After(now) { + until = now + } + } else { + until = now + } + if sinceflag != "" { + since, err = time.Parse("2006-01-02", sinceflag) + if err != nil { + return fmt.Errorf("invalid 'since' date format. please use the format: 2006-01-02") + } + } else { + since = time.Date(until.Year(), until.Month(), until.Day(), 0, 0, 0, 0, until.Location()).AddDate(0, 0, -sixMonthsInDays) + } + return nil +} diff --git a/utils_test.go b/utils_test.go new file mode 100644 index 0000000..cdba8f0 --- /dev/null +++ b/utils_test.go @@ -0,0 +1,273 @@ +package main + +import ( + "math" + "testing" + "time" +) + +func TestIsValidEmail(t *testing.T) { + tests := []struct { + name string + email string + want bool + }{ + { + name: "valid - test 1", + email: "tester@test.com", + want: true, + }, + { + name: "not valid - test 2", + email: "tester", + want: false, + }, + { + name: "not valid - test 3", + email: "jane@.com", + want: false, + }, + { + name: "not valid - test 4", + email: "jane.com", + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := isValidEmail(tt.email); got != tt.want { + t.Errorf("isValidEmail() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestIsValidFolderPath(t *testing.T) { + tests := []struct { + name string + folder string + expected bool + }{ + { + name: "valid folder#1", + folder: "./test_data", + expected: true, + }, + { + name: "valid folder#2", + folder: "./test_data/project_1", + expected: true, + }, + { + name: "valid folder#2", + folder: "./test_data/project_3", + expected: true, + }, + { + name: "non-existent folder", + folder: "/path/to/non-existent/folder", + expected: false, + }, + { + name: "file", + folder: "./test_data/project_1/main.go", + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := isValidFolderPath(tt.folder) + if actual != tt.expected { + t.Errorf("isValidFolderPath() = %v, want %v", actual, tt.expected) + } + }) + } +} + +func TestDaysAgo(t *testing.T) { + now := time.Now() + tests := []struct { + name string + time time.Time + expected int + }{ + { + name: "Today", + time: now, + expected: 0, + }, + { + name: "Yesterday", + time: now.Add(-24 * time.Hour), + expected: 1, + }, + { + name: "Two Days Ago", + time: now.Add(-48 * time.Hour), + expected: 2, + }, + { + name: "Three Days Ago", + time: now.Add(-72 * time.Hour), + expected: 3, + }, + { + name: "Future Date", + time: now.Add(24 * time.Hour), + expected: -1, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := daysAgo(tt.time) + if actual != tt.expected { + t.Errorf("Expected %d, got %d", tt.expected, actual) + } + }) + } +} + +func TestGetMaxValue(t *testing.T) { + type args struct { + m map[int]int + } + tests := []struct { + name string + args args + want int + }{ + { + name: "empty map", + args: args{ + m: map[int]int{}, + }, + want: math.MinInt, + }, + { + name: "one element map", + args: args{ + m: map[int]int{ + 1: 1, + }, + }, + want: 1, + }, + { + name: "two elements map", + args: args{ + m: map[int]int{ + 1: 1, + 2: 2, + }, + }, + want: 2, + }, + { + name: "ten elements map", + args: args{ + m: map[int]int{ + 1: 1, + 2: 2, + 10: 3245, + 23: 4653, + 29: 431509, + 32: 34, + 8: 35, + 12: 12, + 19: 86, + 43: 17, + }, + }, + want: 431509, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := getMaxValue(tt.args.m); got != tt.want { + t.Errorf("getMaxValue() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestSetTimeFlags(t *testing.T) { + originalNow := now + defer func() { + now = originalNow + }() + + now = time.Date(2023, time.January, 1, 0, 0, 0, 0, time.UTC) + + tests := []struct { + name string + sinceflag string + untilflag string + expectedSince time.Time + expectedUntil time.Time + expectedError string + }{ + { + name: "Valid since and until flags provided", + sinceflag: "2022-01-01", + untilflag: "2022-12-31", + expectedSince: time.Date(2022, time.January, 1, 0, 0, 0, 0, time.UTC), + expectedUntil: time.Date(2022, time.December, 31, 0, 0, 0, 0, time.UTC), + expectedError: "", + }, + { + name: "Valid since flag provided, until flag not provided", + sinceflag: "2022-01-01", + untilflag: "", + expectedSince: time.Date(2022, time.January, 1, 0, 0, 0, 0, time.UTC), + expectedUntil: time.Date(2023, time.January, 1, 0, 0, 0, 0, time.UTC), + expectedError: "", + }, + { + name: "Valid until flag provided, since flag not provided", + sinceflag: "", + untilflag: "2022-12-31", + expectedSince: time.Date(2022, time.July, 2, 0, 0, 0, 0, time.UTC), + expectedUntil: time.Date(2022, time.December, 31, 0, 0, 0, 0, time.UTC), + expectedError: "", + }, + { + name: "Invalid since flag format", + sinceflag: "01-01-2022", + untilflag: "", + expectedSince: time.Time{}, + expectedUntil: time.Date(2023, time.January, 1, 0, 0, 0, 0, time.UTC), + expectedError: "invalid 'since' date format. please use the format: 2006-01-02", + }, + { + name: "Invalid until flag format", + sinceflag: "", + untilflag: "2022/12/31", + expectedSince: time.Date(2022, time.July, 4, 0, 0, 0, 0, time.UTC), + expectedUntil: time.Time{}, + expectedError: "invalid 'until' date format. please use the format: 2006-01-02", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := setTimeFlags(tt.sinceflag, tt.untilflag) + + if err != nil { + if err.Error() != tt.expectedError { + t.Errorf("Unexpected error message. Expected: %v, Got: %v", tt.expectedError, err.Error()) + } + return + } + + if since != tt.expectedSince { + t.Errorf("Unexpected value of 'since'. Expected: %v, Got: %v", tt.expectedSince, since) + } + + if until != tt.expectedUntil { + t.Errorf("Unexpected value of 'until'. Expected: %v, Got: %v", tt.expectedUntil, until) + } + }) + } +}