diff --git a/.gitignore b/.gitignore index 69b344d068..26dde1619f 100644 --- a/.gitignore +++ b/.gitignore @@ -38,6 +38,9 @@ vendor-php # API acceptance tests - auto-generated files .php-cs-fixer.cache +# QA activity reports +tests/qa-activity-report/reports/ + # drone CI is in .drone.star, do not let someone accidentally commit a local .drone.yml .drone.yml diff --git a/Makefile b/Makefile index 23467a7444..c68f9b1ad3 100644 --- a/Makefile +++ b/Makefile @@ -355,3 +355,12 @@ vendor-bin/php_codesniffer/vendor: vendor/bamarni/composer-bin-plugin vendor-bin vendor-bin/php_codesniffer/composer.lock: vendor-bin/php_codesniffer/composer.json @echo php_codesniffer composer.lock is not up to date. + +.PHONY: generate-qa-activity-report +generate-qa-activity-report: node_modules + @if [ -z "${MONTH}" ] || [ -z "${YEAR}" ]; then \ + echo "Please set the MONTH and YEAR environment variables. Usage: make generate-qa-activity-report MONTH= YEAR="; \ + exit 1; \ + fi + go run tests/qa-activity-report/generate-qa-activity-report.go --month ${MONTH} --year ${YEAR} + diff --git a/tests/qa-activity-report/generate-qa-activity-report.go b/tests/qa-activity-report/generate-qa-activity-report.go new file mode 100644 index 0000000000..3c764f6c40 --- /dev/null +++ b/tests/qa-activity-report/generate-qa-activity-report.go @@ -0,0 +1,225 @@ +package main + +import ( + "bytes" + "encoding/csv" + "fmt" + "os" + "os/exec" + "strconv" + "strings" + "time" +) + +func main() { + since, until, period, err := getTimeframe() + if err != nil { + fmt.Println("Error:", err) + return + } + + logs, err := getGitLog(since, until) + if err != nil { + fmt.Println("Error:", err) + return + } + + csvRows := make([][]string, 0, 1000) + + for _, logLine := range logs { + logParts := strings.Split(logLine, " ") + if len(logParts) < 2 { + continue + } + + commit := logParts[0] + date := logParts[1] + + diffLines, err := getGitDiff(commit) + if err != nil { + fmt.Println("Error:", err) + return + } + + addedTests, changedTests, deletedTests := 0, 0, 0 + // var inScenarios bool + + for i, line := range diffLines { + switch { + case strings.HasPrefix(line, "+") && !strings.HasPrefix(line, "+++"): + if strings.Contains(line, "Scenario:") { + addedTests++ + } else if strings.Contains(line, "Scenario Outline:") { + addedTests += countAddedTestsInExamples(diffLines, i) + } + case strings.HasPrefix(line, "-") && !strings.HasPrefix(line, "---"): + if strings.Contains(line, "Scenario") { + deletedTests++ + } + case strings.Contains(line, "@@ Feature:"): + changedTests := 0 + for i, line := range diffLines { + if strings.Contains(line, "@@ Feature:") { + inScenarios, changed := checkChangedTests(diffLines, i) + if !inScenarios { + changedTests += changed + } + } + } + } + } + + csvRows = append(csvRows, []string{"API Test", date, strconv.Itoa(addedTests), strconv.Itoa(changedTests), strconv.Itoa(deletedTests), commit}) + } + + // Ensure the directory exists + reportDir := "tests/qa-activity-report/reports" + if err := os.MkdirAll(reportDir, os.ModePerm); err != nil { + fmt.Println("Error creating directory:", err) + return + } + + // Define the path for the CSV file + filePath := fmt.Sprintf("%s/QA_Activity_Report_%s.csv", reportDir, period) + if err := generateCSV(csvRows, filePath); err != nil { + fmt.Println("Error writing CSV report:", err) + } else { + fmt.Println("CSV report generated successfully. You can find it in", filePath) + } +} + +func getTimeframe() (since string, until string, period string, err error) { + monthStr := os.Getenv("MONTH") + yearStr := os.Getenv("YEAR") + daysStr := os.Getenv("DAYS") + + if monthStr != "" && yearStr != "" { + month, err := strconv.Atoi(monthStr) + if err != nil { + return "", "", "", fmt.Errorf("invalid month: %w", err) + } + year, err := strconv.Atoi(yearStr) + if err != nil { + return "", "", "", fmt.Errorf("invalid year: %w", err) + } + startDate := time.Date(year, time.Month(month), 1, 0, 0, 0, 0, time.UTC) + endDate := startDate.AddDate(0, 1, -1) + since = startDate.Format("2006-01-02") + until = endDate.Format("2006-01-02") + period = fmt.Sprintf("%02d_%04d", month, year) + } else if daysStr != "" { + days, err := strconv.Atoi(daysStr) + if err != nil { + return "", "", "", fmt.Errorf("invalid number of days: %w", err) + } + until = time.Now().Format("2006-01-02") + since = time.Now().AddDate(0, 0, -days).Format("2006-01-02") + period = fmt.Sprintf("Last_%d_days", days) + } else { + return "", "", "", fmt.Errorf("please provide either MONTH and YEAR or DAYS") + } + + return since, until, period, nil +} + +func getGitLog(since, until string) ([]string, error) { + cmd := exec.Command("git", "log", "--since="+since, "--until="+until, "--pretty=format:%H %ad", "--date=short", "--", "tests/acceptance/features", ":(exclude)tests/acceptance/features/bootstrap/") + var out bytes.Buffer + cmd.Stdout = &out + if err := cmd.Run(); err != nil { + return nil, err + } + return strings.Split(out.String(), "\n"), nil +} + +func getGitDiff(commit string) ([]string, error) { + cmd := exec.Command("git", "diff", commit+"~1", commit, "--", "tests/acceptance/features", ":(exclude)tests/acceptance/features/bootstrap/") + var out bytes.Buffer + cmd.Stdout = &out + err := cmd.Run() + if err != nil { + return nil, err + } + return strings.Split(out.String(), "\n"), nil +} + +func countAddedTestsInExamples(diffLines []string, startIndex int) int { + var inExamples bool + addedTests := 0 + + for j := startIndex + 1; j < len(diffLines); j++ { + exampleLine := diffLines[j] + if strings.HasPrefix(exampleLine, "+") && strings.Contains(exampleLine, "Examples:") { + inExamples = true + continue + } else if inExamples { + trimmedLine := strings.TrimSpace(exampleLine) + if strings.HasPrefix(trimmedLine, "+") && strings.Contains(trimmedLine, "|") { + // Count a string if it starts with "+" and contains "|" + addedTests++ + } else if strings.TrimSpace(exampleLine) == "" || !strings.HasPrefix(trimmedLine, "+") || !strings.HasPrefix(trimmedLine, "|") { + // Abort counting when a row that does not belong to the table is encountered + break + } + } + } + + // We have one line | resource | which is not a test line. So we deleted one line from addedTest + // Examples: + // | resource | + // | testfile.txt | + // | FolderToShare | + if inExamples { + addedTests-- + } + return addedTests +} + +func checkChangedTests(diffLines []string, startIndex int) (bool, int) { + var inScenarios bool + changedTests := 0 + + for j := startIndex + 1; j < len(diffLines); j++ { + scenarioLine := diffLines[j] + if strings.HasPrefix(scenarioLine, "+") || strings.HasPrefix(scenarioLine, "-") { + // If there are changes and the string contains the word "Scenario", set inScenarios to true + if strings.Contains(scenarioLine, "Scenario") { + inScenarios = true + break + } + // If the line no longer starts with "-" "+", then the change block has ended + if !strings.HasPrefix(scenarioLine, "-") && !strings.HasPrefix(scenarioLine, "+") { + break + } + } + } + + // If we didn't find "Scenario" in the changes, increase the changedTests counter + if !inScenarios { + changedTests++ + } + return inScenarios, changedTests +} + +func generateCSV(csvRows [][]string, filePath string) error { + file, err := os.Create(filePath) + if err != nil { + return err + } + defer file.Close() + + writer := csv.NewWriter(file) + defer writer.Flush() + + header := []string{"Test-Type", "Date", "Tests Added", "Tests Changed", "Tests Deleted", "commit-ID"} + if err := writer.Write(header); err != nil { + return err + } + for _, row := range csvRows { + if err := writer.Write(row); err != nil { + return err + } + } + + return nil +}