Goroutine without waitgroup or channel; risks resource leaks or race conditions
go func() {
1// SPDX-License-Identifier: MIT23package processor45import (6 "fmt"7 "io"8 "os"9 "slices"10 "strings"11 "testing"12 "time"1314 "github.com/mattn/go-runewidth"15)1617func TestCalculateCocomo(t *testing.T) {18 var str strings.Builder19 calculateCocomo(1, &str)2021 if !strings.Contains(str.String(), "Estimated Schedule Effort (organic) 0.22 months") {22 t.Error("expected to match got", str.String())23 }24}2526func TestCalculateSizeSingleByte(t *testing.T) {27 t.Setenv("LANG", "en_US.UTF-8")28 var str strings.Builder29 calculateSize(1, &str)3031 if !strings.Contains(str.String(), "Processed 1 bytes, 0.000 megabytes (SI)") {32 t.Error("expected to match got", str.String())33 }34}3536func TestCalculateSize(t *testing.T) {37 t.Setenv("LANG", "en_US.UTF-8")38 var str strings.Builder39 calculateSize(1000000, &str)4041 // The byte count is locale-grouped like every other number in the output.42 if !strings.Contains(str.String(), "Processed 1,000,000 bytes, 1.000 megabytes (SI)") {43 t.Error("expected to match got", str.String())44 }45}4647func TestCalculateSizeXkcdKb(t *testing.T) {48 t.Setenv("LANG", "en_US.UTF-8")4950 prevSizeUnit := SizeUnit51 SizeUnit = "xkcd-kb"52 t.Cleanup(func() { SizeUnit = prevSizeUnit })5354 // The divisor mirrors the case's help text "1000 bytes during leap55 // years, 1024 otherwise": 1_000_000 (1000-based) in leap years and56 // 1_048_576 (1024-based) otherwise. Asserting it directly keeps the57 // expectation year-independent.58 if got := xkcdKbDivisor(2026); got != float64(1024*1024) {59 t.Errorf("xkcdKbDivisor(2026) = %v, want %d (non-leap, 1024-based)", got, 1024*1024)60 }61 if got := xkcdKbDivisor(2024); got != 1_000_000.0 {62 t.Errorf("xkcdKbDivisor(2024) = %v, want 1000000 (leap, 1000-based)", got)63 }6465 var str strings.Builder66 calculateSize(10_000_000, &str)6768 // Regression guard: before the fix `size` was left at 0.0 in non-leap69 // years, so the line read "0.000 megabytes" for any byte count.70 if strings.Contains(str.String(), "0.000 megabytes") {71 t.Errorf("expected a non-zero megabyte size for xkcd-kb, got:\n%s", str.String())72 }7374 // The rendered value must equal bytes/divisor for the current year75 // ("9.537 megabytes" in a non-leap year such as 2026).76 want := fmt.Sprintf("%.3f megabytes", 10_000_000.0/xkcdKbDivisor(time.Now().Year()))77 if !strings.Contains(str.String(), want) {78 t.Errorf("expected output to contain %q, got:\n%s", want, str.String())79 }80}8182func TestSortSummaryFilesEmpty(t *testing.T) {83 summary := LanguageSummary{}84 sortSummaryFiles(&summary)85}8687func TestSortSummaryFiles(t *testing.T) {88 files := []*FileJob{}89 files = append(files, &FileJob{90 Language: "Go",91 Filename: "bbbb.go",92 Extension: "go",93 Location: "./bbbb.go",94 Bytes: 1000,95 Lines: 1000,96 Code: 1000,97 Comment: 1000,98 Blank: 1000,99 Complexity: 1000,100 WeightedComplexity: 1000,101 Binary: false,102 })103 files = append(files, &FileJob{104 Language: "Go",105 Filename: "aaaa.go",106 Extension: "go",107 Location: "./aaaa.go",108 Bytes: 2000,109 Lines: 2000,110 Code: 2000,111 Comment: 2000,112 Blank: 2000,113 Complexity: 2000,114 WeightedComplexity: 2000,115 Binary: false,116 })117118 summary := LanguageSummary{119 Name: "Go",120 Bytes: 1000,121 Lines: 1000,122 Code: 1000,123 Comment: 1000,124 Blank: 1000,125 Complexity: 1000,126 Count: 1000,127 WeightedComplexity: 1000,128 Files: files,129 }130131 lineSort := []string{"name", "names", "language", "languages", "line", "lines", "RANDOMTHING"}132 for _, val := range lineSort {133 SortBy = val134 sortSummaryFiles(&summary)135136 if summary.Files[0].Filename != "aaaa.go" {137 t.Error("Sorting on lines failed", val)138 }139 }140141 blankSort := []string{"blank", "blanks"}142 for _, val := range blankSort {143 SortBy = val144 sortSummaryFiles(&summary)145146 if summary.Files[0].Filename != "aaaa.go" {147 t.Error("Sorting on blank failed", val)148 }149 }150151 codeSort := []string{"code", "codes"}152 for _, val := range codeSort {153 SortBy = val154 sortSummaryFiles(&summary)155156 if summary.Files[0].Filename != "aaaa.go" {157 t.Error("Sorting on code failed", val)158 }159 }160161 commentSort := []string{"comment", "comments"}162 for _, val := range commentSort {163 SortBy = val164 sortSummaryFiles(&summary)165166 if summary.Files[0].Filename != "aaaa.go" {167 t.Error("Sorting on comment failed", val)168 }169 }170171 complexitySort := []string{"complexity", "complexitys"}172 for _, val := range complexitySort {173 SortBy = val174 sortSummaryFiles(&summary)175176 if summary.Files[0].Filename != "aaaa.go" {177 t.Error("Sorting on complexity failed", val)178 }179 }180}181182func TestSortSummaryFilesName(t *testing.T) {183 goFiles := []*FileJob{}184 goFiles = append(goFiles, &FileJob{185 Language: "Go",186 Location: "bbbb.go",187 })188189 goFiles = append(goFiles, &FileJob{190 Language: "Go",191 Location: "aaaa.go",192 })193194 goFiles = append(goFiles, &FileJob{195 Language: "Go",196 Location: "cccc.go",197 })198199 summary := LanguageSummary{200 Name: "Go",201 Files: goFiles,202 }203204 lineSort := []string{"name", "names", "language", "languages"}205 for _, val := range lineSort {206 SortBy = val207 sortSummaryFiles(&summary)208209 if summary.Files[0].Location != "aaaa.go" {210 t.Error("Sorting on lines failed", val)211 }212 }213 SortBy = ""214}215216func TestSortLanguageSummaryName(t *testing.T) {217 SortBy = "name"218 ls := []LanguageSummary{219 {220 Name: "b",221 Lines: 1,222 },223 {224 Name: "a",225 Lines: 1,226 },227 }228229 ls = sortLanguageSummary(ls)230231 if ls[0].Name != "a" {232 t.Error("Expected a to be first")233 }234}235236func TestSortLanguageSummaryLine(t *testing.T) {237 SortBy = "line"238 ls := []LanguageSummary{239 {240 Name: "a",241 Lines: 1,242 },243 {244 Name: "b",245 Lines: 1,246 },247 {248 Name: "c",249 Lines: 2,250 },251 }252253 ls = sortLanguageSummary(ls)254255 if ls[0].Name != "c" || ls[1].Name != "a" {256 t.Error("Expected c to be first and a second")257 }258}259260func TestSortLanguageSummaryBlank(t *testing.T) {261 SortBy = "blank"262 ls := []LanguageSummary{263 {264 Name: "a",265 Blank: 1,266 },267 {268 Name: "b",269 Blank: 1,270 },271 {272 Name: "c",273 Blank: 2,274 },275 }276277 ls = sortLanguageSummary(ls)278279 if ls[0].Name != "c" || ls[1].Name != "a" {280 t.Error("Expected c to be first and a second")281 }282}283284func TestSortLanguageSummaryCode(t *testing.T) {285 SortBy = "code"286 ls := []LanguageSummary{287 {288 Name: "a",289 Code: 1,290 },291 {292 Name: "b",293 Code: 1,294 },295 {296 Name: "c",297 Code: 2,298 },299 }300301 ls = sortLanguageSummary(ls)302303 if ls[0].Name != "c" || ls[1].Name != "a" {304 t.Error("Expected c to be first and a second")305 }306}307308func TestSortLanguageSummaryComment(t *testing.T) {309 SortBy = "comment"310 ls := []LanguageSummary{311 {312 Name: "a",313 Comment: 1,314 },315 {316 Name: "b",317 Comment: 1,318 },319 {320 Name: "c",321 Comment: 2,322 },323 }324325 ls = sortLanguageSummary(ls)326327 if ls[0].Name != "c" || ls[1].Name != "a" {328 t.Error("Expected c to be first and a second")329 }330}331332func TestSortLanguageSummaryComplexity(t *testing.T) {333 SortBy = "complexity"334 ls := []LanguageSummary{335 {336 Name: "a",337 Complexity: 1,338 },339 {340 Name: "b",341 Complexity: 1,342 },343 {344 Name: "c",345 Complexity: 2,346 },347 }348349 ls = sortLanguageSummary(ls)350351 if ls[0].Name != "c" || ls[1].Name != "a" {352 t.Error("Expected c to be first and a second")353 }354}355356func TestSortLanguageSummaryBytes(t *testing.T) {357 SortBy = "bytes"358 ls := []LanguageSummary{359 {360 Name: "a",361 Bytes: 1,362 },363 {364 Name: "b",365 Bytes: 1,366 },367 {368 Name: "c",369 Bytes: 2,370 },371 }372373 ls = sortLanguageSummary(ls)374375 if ls[0].Name != "c" || ls[1].Name != "a" {376 t.Error("Expected c to be first and a second")377 }378}379380func TestSortLanguageSummaryFiles(t *testing.T) {381 SortBy = "files"382 ls := []LanguageSummary{383 {384 Name: "a",385 Count: 1,386 },387 {388 Name: "b",389 Count: 1,390 },391 {392 Name: "c",393 Count: 2,394 },395 }396397 ls = sortLanguageSummary(ls)398399 if ls[0].Name != "c" || ls[1].Name != "a" {400 t.Error("Expected c to be first and a second")401 }402}403404func TestSortSummaryNames(t *testing.T) {405 SortBy = "name"406 ls := []LanguageSummary{407 {408 Name: "a",409 Complexity: 1,410 },411 {412 Name: "b",413 Complexity: 1,414 },415 {416 Name: "c",417 Complexity: 2,418 },419 }420421 ls = sortLanguageSummary(ls)422423 if ls[0].Name != "a" || ls[1].Name != "b" || ls[2].Name != "c" {424 t.Error("Expected a to be first and b second and c third")425 }426}427428func TestToJSONEmpty(t *testing.T) {429 inputChan := make(chan *FileJob, 1000)430 close(inputChan)431 res := toJSON(inputChan)432433 if res != "[]" {434 t.Error("Expected empty JSON return", res)435 }436}437438func TestToJSONSingle(t *testing.T) {439 inputChan := make(chan *FileJob, 1000)440 inputChan <- &FileJob{441 Language: "Go",442 Filename: "bbbb.go",443 Extension: "go",444 Location: "./",445 Bytes: 1000,446 Lines: 1000,447 Code: 1000,448 Comment: 1000,449 Blank: 1000,450 Complexity: 1000,451 WeightedComplexity: 1000,452 Binary: false,453 }454 close(inputChan)455 Debug = true // Increase coverage slightly456 Files = true457 res := toJSON(inputChan)458 Debug = false459460 if !strings.Contains(res, `"Name":"Go"`) || !strings.Contains(res, `"Code":1000`) || !strings.Contains(res, `"Filename":"bbbb.go"`) {461 t.Error("Expected JSON return", res)462 }463 if strings.Contains(res, `"Content":`) {464 t.Error("Expected JSON return", res)465 }466}467468func TestToJSONSingleWithoutFiles(t *testing.T) {469 inputChan := make(chan *FileJob, 1000)470 inputChan <- &FileJob{471 Language: "Go",472 Filename: "bbbb.go",473 Extension: "go",474 Location: "./",475 Bytes: 1000,476 Lines: 1000,477 Code: 1000,478 Comment: 1000,479 Blank: 1000,480 Complexity: 1000,481 WeightedComplexity: 1000,482 Binary: false,483 }484 close(inputChan)485 Debug = true // Increase coverage slightly486 Files = false487 res := toJSON(inputChan)488 Debug = false489490 if !strings.Contains(res, `"Name":"Go"`) || !strings.Contains(res, `"Code":1000`) {491 t.Error("Expected JSON return", res)492 }493 if strings.Contains(res, `"Filename":"bbbb.go"`) {494 t.Error("Expected JSON return", res)495 }496}497498func TestToJSONMultiple(t *testing.T) {499 inputChan := make(chan *FileJob, 1000)500 inputChan <- &FileJob{501 Language: "Go",502 Filename: "bbbb.go",503 Extension: "go",504 Location: "./",505 Bytes: 1000,506 Lines: 1000,507 Code: 1000,508 Comment: 1000,509 Blank: 1000,510 Complexity: 1000,511 WeightedComplexity: 1000,512 Binary: false,513 }514 inputChan <- &FileJob{515 Language: "Go",516 Filename: "aaaa.go",517 Extension: "go",518 Location: "./",519 Bytes: 1000,520 Lines: 1000,521 Code: 1000,522 Comment: 1000,523 Blank: 1000,524 Complexity: 1000,525 WeightedComplexity: 1000,526 Binary: false,527 }528 close(inputChan)529 Debug = true // Increase coverage slightly530 Files = true531 res := toJSON(inputChan)532 Debug = false533534 if !strings.Contains(res, `aaaa.go`) || !strings.Contains(res, `bbbb.go`) {535 t.Error("Expected JSON return", res)536 }537}538539func TestToYAMLEmpty(t *testing.T) {540 inputChan := make(chan *FileJob, 1000)541 close(inputChan)542 res := toClocYAML(inputChan)543544 if !strings.Contains(res, "{}") || !strings.Contains(res, "header:") || !strings.Contains(res, "n_files: 0") {545 t.Error("Expected empty Cloc YAML return", res)546 }547}548549func TestToYAMLSingle(t *testing.T) {550 inputChan := make(chan *FileJob, 1000)551 inputChan <- &FileJob{552 Language: "Go",553 Filename: "bbbb.go",554 Extension: "go",555 Location: "./",556 Bytes: 1000,557 Lines: 1000,558 Code: 1000,559 Comment: 1000,560 Blank: 1000,561 Complexity: 1000,562 WeightedComplexity: 1000,563 Binary: false,564 }565 close(inputChan)566 Debug = true // Increase coverage slightly567 res := toClocYAML(inputChan)568 Debug = false569570 if !strings.Contains(res, `n_lines: 1000`) {571 t.Error("Expected Cloc YAML return", res)572 }573}574575func TestToYAMLMultiple(t *testing.T) {576 inputChan := make(chan *FileJob, 1000)577 inputChan <- &FileJob{578 Language: "Go",579 Filename: "bbbb.go",580 Extension: "go",581 Location: "./",582 Bytes: 1000,583 Lines: 1000,584 Code: 1000,585 Comment: 1000,586 Blank: 1000,587 Complexity: 1000,588 WeightedComplexity: 1000,589 Binary: false,590 }591 inputChan <- &FileJob{592 Language: "Go",593 Filename: "aaaa.go",594 Extension: "go",595 Location: "./",596 Bytes: 1000,597 Lines: 1000,598 Code: 1000,599 Comment: 1000,600 Blank: 1000,601 Complexity: 1000,602 WeightedComplexity: 1000,603 Binary: false,604 }605 close(inputChan)606 Debug = true // Increase coverage slightly607 res := toClocYAML(inputChan)608 Debug = false609610 if !strings.Contains(res, `code: 2000`) || !strings.Contains(res, `n_lines: 2000`) {611 t.Error("Expected Cloc JSON return", res)612 }613}614615func TestToCsvMultiple(t *testing.T) {616 inputChan := make(chan *FileJob, 1000)617 inputChan <- &FileJob{618 Language: "Go",619 Filename: "bbbb.go",620 Extension: "go",621 Location: "./",622 Bytes: 1000,623 Lines: 1000,624 Code: 1000,625 Comment: 1000,626 Blank: 1000,627 Complexity: 1000,628 WeightedComplexity: 1000,629 Binary: false,630 }631 inputChan <- &FileJob{632 Language: "Go",633 Filename: "aaaa.go",634 Extension: "go",635 Location: "./",636 Bytes: 1000,637 Lines: 1000,638 Code: 1000,639 Comment: 1000,640 Blank: 1000,641 Complexity: 1000,642 WeightedComplexity: 1000,643 Binary: false,644 }645 close(inputChan)646 Debug = true // Increase coverage slightly647 res := toCSV(inputChan)648 Debug = false649650 if !strings.Contains(res, `aaaa.go,`) || !strings.Contains(res, `bbbb.go`) {651 t.Error("Expected CSV return", res)652 }653}654655func TestToCsvStreamMultiple(t *testing.T) {656 inputChan := make(chan *FileJob, 1000)657 inputChan <- &FileJob{658 Language: "Go",659 Filename: "bbbb.go",660 Extension: "go",661 Location: "./",662 Bytes: 1000,663 Lines: 1000,664 Code: 1000,665 Comment: 1000,666 Blank: 1000,667 Complexity: 1000,668 WeightedComplexity: 1000,669 Binary: false,670 }671 inputChan <- &FileJob{672 Language: "Go",673 Filename: "aaaa.go",674 Extension: "go",675 Location: "./",676 Bytes: 1000,677 Lines: 1000,678 Code: 1000,679 Comment: 1000,680 Blank: 1000,681 Complexity: 1000,682 WeightedComplexity: 1000,683 Binary: false,684 }685 close(inputChan)686 Debug = true // Increase coverage slightly687 res := toCSVStream(inputChan)688 Debug = false689690 if res != "" {691 t.Error("Expected CSV return", res)692 }693}694695func TestToCsvFilesSorted(t *testing.T) {696 fj1 := &FileJob{697 Language: "Go",698 Filename: "bbbb.go",699 Extension: "go",700 Location: "./",701 Bytes: 90,702 Lines: 90,703 Code: 90,704 Comment: 90,705 Blank: 90,706 Complexity: 90,707 WeightedComplexity: 90,708 Binary: false,709 }710 fj2 := &FileJob{711 Language: "Go",712 Filename: "aaaa.go",713 Extension: "go",714 Location: "./",715 Bytes: 1000,716 Lines: 1000,717 Code: 1000,718 Comment: 1000,719 Blank: 1000,720 Complexity: 1000,721 WeightedComplexity: 1000,722 Binary: false,723 }724725 Files = true726 SortBy = "lines"727728 inputChan1 := make(chan *FileJob, 1000)729 inputChan1 <- fj1730 inputChan1 <- fj2731 close(inputChan1)732 res1 := toCSV(inputChan1)733734 inputChan2 := make(chan *FileJob, 1000)735 inputChan2 <- fj2736 inputChan2 <- fj1737 close(inputChan2)738 res2 := toCSV(inputChan2)739740 Files = false741742 if res1 != res2 {743 t.Error("Should be sorted to be the same")744 }745}746747func TestToOpenMetricsMultiple(t *testing.T) {748 inputChan := make(chan *FileJob, 1000)749 inputChan <- &FileJob{750 Language: "Go",751 Filename: "bbbb.go",752 Extension: "go",753 Location: "./",754 Bytes: 1000,755 Lines: 1000,756 Code: 1000,757 Comment: 1000,758 Blank: 1000,759 Complexity: 1000,760 WeightedComplexity: 1000,761 Binary: false,762 }763 inputChan <- &FileJob{764 Language: "Go",765 Filename: "aaaa.go",766 Extension: "go",767 Location: "./",768 Bytes: 1000,769 Lines: 1000,770 Code: 1000,771 Comment: 1000,772 Blank: 1000,773 Complexity: 1000,774 WeightedComplexity: 1000,775 Binary: false,776 }777 close(inputChan)778 Files = false779 Debug = true // Increase coverage slightly780 res := toOpenMetrics(inputChan)781 Debug = false782783 var expectedResult = `# TYPE scc_files gauge784# HELP scc_files Number of sourcecode files.785scc_files{language="Go"} 2786# TYPE scc_lines gauge787# HELP scc_lines Number of lines.788scc_lines{language="Go"} 2000789# TYPE scc_code gauge790# HELP scc_code Number of lines of actual code.791scc_code{language="Go"} 2000792# TYPE scc_comments gauge793# HELP scc_comments Number of comments.794scc_comments{language="Go"} 2000795# TYPE scc_blanks gauge796# HELP scc_blanks Number of blank lines.797scc_blanks{language="Go"} 2000798# TYPE scc_complexity gauge799# HELP scc_complexity Code complexity.800scc_complexity{language="Go"} 2000801# TYPE scc_bytes gauge802# UNIT scc_bytes bytes803# HELP scc_bytes Size in bytes.804scc_bytes{language="Go"} 2000805# EOF806`807808 if res != expectedResult {809 t.Error("Expected OpenMetrics return", res)810 }811}812813// assertOpenMetricsFamiliesContiguous checks that every sample line of a metric family appears814// in one unbroken run, which OpenMetrics requires, and that the output ends with the # EOF815// terminator.816func assertOpenMetricsFamiliesContiguous(t *testing.T, res string) {817 t.Helper()818819 if !strings.HasSuffix(res, "# EOF\n") {820 t.Error("Expected output to be terminated by # EOF", res)821 }822823 var order []string824 seen := map[string]bool{}825 previous := ""826827 for _, line := range strings.Split(res, "\n") {828 if line == "" || strings.HasPrefix(line, "#") {829 continue830 }831832 name, _, found := strings.Cut(line, "{")833 if !found {834 t.Error("Expected a labelled sample line", line)835 continue836 }837838 if name != previous {839 if seen[name] {840 t.Error("Samples of family are not contiguous: "+name, res)841 }842 seen[name] = true843 order = append(order, name)844 previous = name845 }846 }847848 if len(order) == 0 {849 t.Error("Expected at least one sample", res)850 }851}852853func TestToOpenMetricsFamiliesContiguous(t *testing.T) {854 inputChan := make(chan *FileJob, 1000)855 for _, language := range []string{"Go", "Python", "Rust"} {856 for i := 0; i < 2; i++ {857 inputChan <- &FileJob{858 Language: language,859 Filename: fmt.Sprintf("a%d.x", i),860 Location: fmt.Sprintf("./a%d.x", i),861 Bytes: 1000,862 Lines: 1000,863 Code: 1000,864 Comment: 1000,865 Blank: 1000,866 Complexity: 1000,867 }868 }869 }870 close(inputChan)871872 Files = false873 res := toOpenMetrics(inputChan)874875 assertOpenMetricsFamiliesContiguous(t, res)876}877878func TestToOpenMetricsFilesFamiliesContiguous(t *testing.T) {879 inputChan := make(chan *FileJob, 1000)880 for _, language := range []string{"Go", "Python", "Rust"} {881 for i := 0; i < 2; i++ {882 inputChan <- &FileJob{883 Language: language,884 Filename: fmt.Sprintf("a%d.x", i),885 Location: fmt.Sprintf("./a%d.x", i),886 Bytes: 1000,887 Lines: 1000,888 Code: 1000,889 Comment: 1000,890 Blank: 1000,891 Complexity: 1000,892 }893 }894 }895 close(inputChan)896897 Files = true898 res := toOpenMetrics(inputChan)899 Files = false900901 assertOpenMetricsFamiliesContiguous(t, res)902}903904func TestToOpenMetricsFilesEscapesLabelValues(t *testing.T) {905 inputChan := make(chan *FileJob, 1000)906 inputChan <- &FileJob{907 Language: `we"ird`,908 Filename: `we"ird.py`,909 Location: "./we\"ird\\new\nline.py",910 Bytes: 1000,911 Lines: 1000,912 Code: 1000,913 Comment: 1000,914 Blank: 1000,915 Complexity: 1000,916 }917 close(inputChan)918919 Files = true920 res := toOpenMetrics(inputChan)921 Files = false922923 if !strings.Contains(res, `scc_lines{language="we\"ird",file="./we\"ird\\new\nline.py"} 1000`) {924 t.Error("Expected quote, backslash and newline to be escaped in label values", res)925 }926}927928func TestToSQLSingle(t *testing.T) {929 inputChan := make(chan *FileJob, 1000)930 inputChan <- &FileJob{931 Language: "Go",932 Filename: "bbbb.go",933 Extension: "go",934 Location: "./",935 Bytes: 1000,936 Lines: 1000,937 Code: 1000,938 Comment: 1000,939 Blank: 1000,940 Complexity: 1000,941 WeightedComplexity: 1000,942 Binary: false,943 Uloc: 99,944 }945 close(inputChan)946 Files = false947 Debug = true // Increase coverage slightly948 res := toSql(inputChan)949 Debug = false950951 if !strings.Contains(res, `create table metadata`) {952 t.Error("Expected create table return", res)953 }954955 if !strings.Contains(res, `create table t`) {956 t.Error("Expected create table return", res)957 }958959 if !strings.Contains(res, `begin transaction`) {960 t.Error("Expected begin transaction return", res)961 }962963 if !strings.Contains(res, `insert into t values('', 'Go', './', './', 'bbbb.go', 1000, 1000, 1000, 1000, 1000, 99);`) {964 t.Error("Expected insert return", res)965 }966967 if !strings.Contains(res, `insert into metadata values`) {968 t.Error("Expected insert return", res)969 }970}971972func TestToSQLSingleEscapesProjectName(t *testing.T) {973 inputChan := make(chan *FileJob, 1000)974 inputChan <- &FileJob{975 Language: "Go",976 Filename: "bbbb.go",977 Extension: "go",978 Location: "./",979 Bytes: 1000,980 Lines: 1000,981 Code: 1000,982 Comment: 1000,983 Blank: 1000,984 Complexity: 1000,985 Binary: false,986 Uloc: 99,987 }988 close(inputChan)989990 SQLProject = "it's mine"991 defer func() { SQLProject = "" }()992993 res := toSql(inputChan)994995 if !strings.Contains(res, `insert into metadata values('`) {996 t.Error("Expected metadata insert", res)997 }998999 if !strings.Contains(res, `, 'it''s mine', `) {1000 t.Error("Expected escaped project name in metadata insert", res)1001 }10021003 if strings.Contains(res, `'it's mine'`) {1004 t.Error("Expected no unescaped project name", res)1005 }1006}10071008func TestToSQLLocomoCreatesLocomoMetadataTable(t *testing.T) {1009 inputChan := make(chan *FileJob, 1000)1010 inputChan <- &FileJob{1011 Language: "Go",1012 Filename: "bbbb.go",1013 Extension: "go",1014 Location: "./",1015 Bytes: 1000,1016 Lines: 1000,1017 Code: 1000,1018 Comment: 1000,1019 Blank: 1000,1020 Complexity: 1000,1021 Uloc: 99,1022 }1023 close(inputChan)10241025 Locomo = true1026 defer func() { Locomo = false }()10271028 res := toSql(inputChan)10291030 if !strings.Contains(res, `insert into locomo_metadata values(`) {1031 t.Error("Expected locomo_metadata insert", res)1032 }10331034 if !strings.Contains(res, `create table locomo_metadata`) {1035 t.Error("Expected create table locomo_metadata so the script can be replayed", res)1036 }10371038 if strings.Index(res, `create table locomo_metadata`) > strings.Index(res, `insert into locomo_metadata`) {1039 t.Error("Expected create table locomo_metadata before its insert", res)1040 }1041}10421043func TestFileSummarizeWide(t *testing.T) {1044 inputChan := make(chan *FileJob, 1000)1045 inputChan <- &FileJob{1046 Language: "Go",1047 Filename: "bbbb.go",1048 Extension: "go",1049 Location: "./",1050 Bytes: 1000,1051 Lines: 1000,1052 Code: 1000,1053 Comment: 1000,1054 Blank: 1000,1055 Complexity: 1000,1056 WeightedComplexity: 1000,1057 Binary: false,1058 }10591060 close(inputChan)1061 Format = "wide"1062 More = true1063 res := fileSummarize(inputChan)1064 More = false10651066 if !strings.Contains(res, `Language`) {1067 t.Error("Expected CSV return", res)1068 }1069}10701071func TestFileSummarizeJson(t *testing.T) {1072 inputChan := make(chan *FileJob, 1000)1073 inputChan <- &FileJob{1074 Language: "Go",1075 Filename: "bbbb.go",1076 Extension: "go",1077 Location: "./",1078 Bytes: 1000,1079 Lines: 1000,1080 Code: 1000,1081 Comment: 1000,1082 Blank: 1000,1083 Complexity: 1000,1084 WeightedComplexity: 1000,1085 Binary: false,1086 }10871088 close(inputChan)1089 Format = "JSON"1090 More = false1091 Files = true1092 res := fileSummarize(inputChan)10931094 if !strings.Contains(res, `bbbb.go`) || !strings.HasPrefix(res, "[") {1095 t.Error("Expected JSON return", res)1096 }1097}10981099func TestFileSummarizeCsv(t *testing.T) {1100 inputChan := make(chan *FileJob, 1000)1101 inputChan <- &FileJob{1102 Language: "Go",1103 Filename: "bbbb.go",1104 Extension: "go",1105 Location: "./",1106 Bytes: 1000,1107 Lines: 1000,1108 Code: 1000,1109 Comment: 1000,1110 Blank: 1000,1111 Complexity: 1000,1112 WeightedComplexity: 1000,1113 Binary: false,1114 }11151116 close(inputChan)1117 Format = "CSV"1118 More = false1119 res := fileSummarize(inputChan)11201121 if !strings.Contains(res, `bbbb.go`) {1122 t.Error("Expected CSV return", res)1123 }1124}11251126func TestFileSummarizeYaml(t *testing.T) {1127 inputChan := make(chan *FileJob, 1000)1128 inputChan <- &FileJob{1129 Language: "Go",1130 Filename: "bbbb.go",1131 Extension: "go",1132 Location: "./",1133 Bytes: 1000,1134 Lines: 1000,1135 Code: 1000,1136 Comment: 1000,1137 Blank: 1000,1138 Complexity: 1000,1139 WeightedComplexity: 1000,1140 Binary: false,1141 }11421143 close(inputChan)1144 Format = "cloc-yml"1145 More = false1146 res := fileSummarize(inputChan)11471148 if !strings.Contains(res, `code: 1000`) {1149 t.Error("Expected YAML return", res)1150 }1151}11521153func TestFileSummarizeYml(t *testing.T) {1154 inputChan := make(chan *FileJob, 1000)1155 inputChan <- &FileJob{1156 Language: "Go",1157 Filename: "bbbb.go",1158 Extension: "go",1159 Location: "./",1160 Bytes: 1000,1161 Lines: 1000,1162 Code: 1000,1163 Comment: 1000,1164 Blank: 1000,1165 Complexity: 1000,1166 WeightedComplexity: 1000,1167 Binary: false,1168 }11691170 close(inputChan)1171 Format = "cloc-YAML"1172 More = false1173 res := fileSummarize(inputChan)11741175 if !strings.Contains(res, `code: 1000`) {1176 t.Error("Expected YML return", res)1177 }1178}11791180func TestFileSummarizeOpenMetrics(t *testing.T) {1181 inputChan := make(chan *FileJob, 1000)1182 inputChan <- &FileJob{1183 Language: "Go",1184 Filename: "bbbb.go",1185 Extension: "go",1186 Location: "./",1187 Bytes: 1000,1188 Lines: 1000,1189 Code: 1000,1190 Comment: 1000,1191 Blank: 1000,1192 Complexity: 1000,1193 WeightedComplexity: 1000,1194 Binary: false,1195 }11961197 close(inputChan)1198 Files = false1199 Format = "OpenMetrics"1200 More = false1201 res := fileSummarize(inputChan)12021203 var expectedResult = `# TYPE scc_files gauge1204# HELP scc_files Number of sourcecode files.1205scc_files{language="Go"} 11206# TYPE scc_lines gauge1207# HELP scc_lines Number of lines.1208scc_lines{language="Go"} 10001209# TYPE scc_code gauge1210# HELP scc_code Number of lines of actual code.1211scc_code{language="Go"} 10001212# TYPE scc_comments gauge1213# HELP scc_comments Number of comments.1214scc_comments{language="Go"} 10001215# TYPE scc_blanks gauge1216# HELP scc_blanks Number of blank lines.1217scc_blanks{language="Go"} 10001218# TYPE scc_complexity gauge1219# HELP scc_complexity Code complexity.1220scc_complexity{language="Go"} 10001221# TYPE scc_bytes gauge1222# UNIT scc_bytes bytes1223# HELP scc_bytes Size in bytes.1224scc_bytes{language="Go"} 10001225# EOF1226`12271228 if res != expectedResult {1229 t.Error("Expected OpenMetrics return", res)1230 }1231}12321233func TestFileSummarizeOpenMetricsPerFile(t *testing.T) {1234 inputChan := make(chan *FileJob, 1000)1235 inputChan <- &FileJob{1236 Language: "Go",1237 Filename: "bbbb.go",1238 Extension: "go",1239 Location: "C:\\bbbb.go", // to test escaping of the backslash1240 Bytes: 1000,1241 Lines: 1000,1242 Code: 1000,1243 Comment: 1000,1244 Blank: 1000,1245 Complexity: 1000,1246 WeightedComplexity: 1000,1247 Binary: false,1248 }12491250 close(inputChan)1251 Format = "OpenMetrics"1252 More = false1253 Files = true1254 res := fileSummarize(inputChan)12551256 var expectedResult = `# TYPE scc_files gauge1257# HELP scc_files Number of sourcecode files.1258# TYPE scc_lines gauge1259# HELP scc_lines Number of lines.1260scc_lines{language="Go",file="C:\\bbbb.go"} 10001261# TYPE scc_code gauge1262# HELP scc_code Number of lines of actual code.1263scc_code{language="Go",file="C:\\bbbb.go"} 10001264# TYPE scc_comments gauge1265# HELP scc_comments Number of comments.1266scc_comments{language="Go",file="C:\\bbbb.go"} 10001267# TYPE scc_blanks gauge1268# HELP scc_blanks Number of blank lines.1269scc_blanks{language="Go",file="C:\\bbbb.go"} 10001270# TYPE scc_complexity gauge1271# HELP scc_complexity Code complexity.1272scc_complexity{language="Go",file="C:\\bbbb.go"} 10001273# TYPE scc_bytes gauge1274# UNIT scc_bytes bytes1275# HELP scc_bytes Size in bytes.1276scc_bytes{language="Go",file="C:\\bbbb.go"} 10001277# EOF1278`12791280 if res != expectedResult {1281 t.Error("Expected OpenMetrics return", res)1282 }1283}12841285func TestFileSummarizeHtml(t *testing.T) {1286 inputChan := make(chan *FileJob, 1000)1287 inputChan <- &FileJob{1288 Language: "Go",1289 Filename: "bbbb.go",1290 Extension: "go",1291 Location: "./",1292 Bytes: 1000,1293 Lines: 1000,1294 Code: 1000,1295 Comment: 1000,1296 Blank: 1000,1297 Complexity: 1000,1298 WeightedComplexity: 1000,1299 Binary: false,1300 }13011302 close(inputChan)1303 Format = "html"1304 More = false1305 res := fileSummarize(inputChan)13061307 if !strings.Contains(res, `<th>1000`) {1308 t.Error("Expected HTML return", res)1309 }1310}13111312func TestFileSummarizeHtmlTable(t *testing.T) {1313 inputChan := make(chan *FileJob, 1000)1314 inputChan <- &FileJob{1315 Language: "Go",1316 Filename: "bbbb.go",1317 Extension: "go",1318 Location: "./",1319 Bytes: 1000,1320 Lines: 1000,1321 Code: 1000,1322 Comment: 1000,1323 Blank: 1000,1324 Complexity: 1000,1325 WeightedComplexity: 1000,1326 Binary: false,1327 }13281329 close(inputChan)1330 Format = "html-table"1331 More = false1332 res := fileSummarize(inputChan)13331334 if !strings.Contains(res, `<th>1000`) {1335 t.Error("Expected HTML-table return", res)1336 }1337}13381339func TestFileSummarizeDefault(t *testing.T) {1340 inputChan := make(chan *FileJob, 1000)1341 inputChan <- &FileJob{1342 Language: "Go",1343 Filename: "bbbb.go",1344 Extension: "go",1345 Location: "./",1346 Bytes: 1000,1347 Lines: 1000,1348 Code: 1000,1349 Comment: 1000,1350 Blank: 1000,1351 Complexity: 1000,1352 WeightedComplexity: 1000,1353 Binary: false,1354 }13551356 close(inputChan)1357 Format = ""1358 More = false1359 res := fileSummarize(inputChan)13601361 if !strings.Contains(res, `Estimated Cost to Develop`) {1362 t.Error("Expected summary return", res)1363 }1364}13651366func TestFileSummarizeLong(t *testing.T) {1367 inputChan := make(chan *FileJob, 1000)1368 inputChan <- &FileJob{1369 Language: "Go",1370 Filename: "bbbb.go",1371 Extension: "go",1372 Location: "./",1373 Bytes: 1000,1374 Lines: 1000,1375 Code: 1000,1376 Comment: 1000,1377 Blank: 1000,1378 Complexity: 1000,1379 WeightedComplexity: 1000,1380 Binary: false,1381 }1382 inputChan <- &FileJob{1383 Language: "Go",1384 Filename: "aaaa.go",1385 Extension: "go",1386 Location: "./",1387 Bytes: 1000,1388 Lines: 1000,1389 Code: 1000,1390 Comment: 1000,1391 Blank: 1000,1392 Complexity: 1000,1393 WeightedComplexity: 1000,1394 Binary: false,1395 }1396 close(inputChan)1397 res := fileSummarizeLong(inputChan)13981399 if !strings.Contains(res, `Language`) {1400 t.Error("Expected Summary return", res)1401 }1402}14031404// TestFileSummarizeLongWeightedComplexity guards against regression of issue #412 where the1405// language and total Complexity/Lines columns summed each file's ratio instead of recomputing1406// from the aggregated complexity and code totals.1407func TestFileSummarizeLongWeightedComplexity(t *testing.T) {1408 Files = false14091410 inputChan := make(chan *FileJob, 1000)1411 inputChan <- &FileJob{1412 Language: "Go",1413 Filename: "aaaa.go",1414 Extension: "go",1415 Location: "./",1416 Lines: 1000,1417 Code: 1000,1418 Complexity: 1000,1419 }1420 inputChan <- &FileJob{1421 Language: "Go",1422 Filename: "bbbb.go",1423 Extension: "go",1424 Location: "./",1425 Lines: 1000,1426 Code: 1000,1427 Complexity: 500,1428 }1429 close(inputChan)1430 res := fileSummarizeLong(inputChan)14311432 // Aggregate is complexity 1500 / code 2000 * 100 = 75.00 for both the language and total rows.1433 // The old buggy behaviour summed the per-file ratios (100.00 + 50.00 = 150.00).1434 if !strings.Contains(res, "75.00") {1435 t.Errorf("Expected aggregated Complexity/Lines of 75.00, got:\n%s", res)1436 }1437 if strings.Contains(res, "150.00") {1438 t.Errorf("Complexity/Lines was summed across files instead of recomputed:\n%s", res)1439 }1440}14411442func TestFileSummarizeShort(t *testing.T) {1443 inputChan := make(chan *FileJob, 1000)1444 inputChan <- &FileJob{1445 Language: "Go",1446 Filename: "bbbb.go",1447 Extension: "go",1448 Location: "./",1449 Bytes: 1000,1450 Lines: 1000,1451 Code: 1000,1452 Comment: 1000,1453 Blank: 1000,1454 Complexity: 1000,1455 WeightedComplexity: 1000,1456 Binary: false,1457 }1458 inputChan <- &FileJob{1459 Language: "Go",1460 Filename: "aaaa.go",1461 Extension: "go",1462 Location: "./",1463 Bytes: 1000,1464 Lines: 1000,1465 Code: 1000,1466 Comment: 1000,1467 Blank: 1000,1468 Complexity: 1000,1469 WeightedComplexity: 1000,1470 Binary: false,1471 }1472 close(inputChan)1473 res := fileSummarizeShort(inputChan)14741475 if !strings.Contains(res, `Language`) {1476 t.Error("Expected Summary return", res)1477 }1478}14791480func TestFileSummarizeShortSort(t *testing.T) {1481 inputChan := make(chan *FileJob, 1000)1482 inputChan <- &FileJob{1483 Language: "Go",1484 Filename: "bbbb.go",1485 Extension: "go",1486 Location: "./",1487 Bytes: 1000,1488 Lines: 1000,1489 Code: 1000,1490 Comment: 1000,1491 Blank: 1000,1492 Complexity: 1000,1493 WeightedComplexity: 1000,1494 Binary: false,1495 }1496 inputChan <- &FileJob{1497 Language: "Go",1498 Filename: "bbbb.go",1499 Extension: "go",1500 Location: "./",1501 Bytes: 1000,1502 Lines: 1000,1503 Code: 1000,1504 Comment: 1000,1505 Blank: 1000,1506 Complexity: 1000,1507 WeightedComplexity: 1000,1508 Binary: false,1509 }1510 close(inputChan)15111512 sortBy := []string{"name", "line", "blank", "code", "comment"}15131514 Files = true1515 for _, sort := range sortBy {1516 SortBy = sort1517 res := fileSummarizeShort(inputChan)15181519 if !strings.Contains(res, `Language`) {1520 t.Error("Expected Summary return", res)1521 }1522 }1523}15241525func TestFileSummarizeLongSort(t *testing.T) {1526 inputChan := make(chan *FileJob, 1000)1527 inputChan <- &FileJob{1528 Language: "Go",1529 Filename: "bbbb.go",1530 Extension: "go",1531 Location: "./",1532 Bytes: 1000,1533 Lines: 1000,1534 Code: 1000,1535 Comment: 1000,1536 Blank: 1000,1537 Complexity: 1000,1538 WeightedComplexity: 1000,1539 Binary: false,1540 }1541 inputChan <- &FileJob{1542 Language: "Go",1543 Filename: "bbbb.go",1544 Extension: "go",1545 Location: "./",1546 Bytes: 1000,1547 Lines: 1000,1548 Code: 1000,1549 Comment: 1000,1550 Blank: 1000,1551 Complexity: 1000,1552 WeightedComplexity: 1000,1553 Binary: false,1554 }1555 close(inputChan)15561557 sortBy := []string{"name", "line", "blank", "code", "comment"}15581559 Files = true1560 for _, sort := range sortBy {1561 SortBy = sort1562 res := fileSummarizeLong(inputChan)15631564 if !strings.Contains(res, `Language`) {1565 t.Error("Expected Summary return", res)1566 }1567 }1568}15691570func TestGetTabularShortBreak(t *testing.T) {1571 Ci = false1572 r := getTabularShortBreak()15731574 if !strings.Contains(r, "─") {1575 t.Errorf("Expected to have box line")1576 }15771578 Ci = true1579 r = getTabularShortBreak()15801581 if !strings.Contains(r, "-") {1582 t.Errorf("Expected to have hyphen")1583 }15841585 Ci = false1586}15871588func TestGetTabularWideBreak(t *testing.T) {1589 {1590 Ci, HBorder = false, false1591 r := getTabularWideBreak()1592 if !strings.Contains(r, "─") {1593 t.Errorf("Expected to have box line")1594 }1595 }1596 {1597 Ci, HBorder = false, true1598 r := getTabularWideBreak()1599 if strings.Contains(r, "─") {1600 t.Errorf("Didn't expect to have box line")1601 }1602 }1603 {1604 Ci, HBorder = true, false1605 r := getTabularWideBreak()1606 if !strings.Contains(r, "-") {1607 t.Errorf("Expected to have hyphen")1608 }1609 }1610 {1611 Ci, HBorder = true, true1612 r := getTabularWideBreak()1613 if strings.Contains(r, "-") {1614 t.Errorf("Didn't expect to have hyphen")1615 }1616 }16171618 Ci, HBorder = false, false1619}16201621func TestToHTML(t *testing.T) {1622 inputChan := make(chan *FileJob, 1000)1623 inputChan <- &FileJob{1624 Language: "Go",1625 Filename: "bbbb.go",1626 Extension: "go",1627 Location: "./",1628 Bytes: 1000,1629 Lines: 1000,1630 Code: 1000,1631 Comment: 1000,1632 Blank: 1000,1633 Complexity: 1000,1634 WeightedComplexity: 1000,1635 Binary: false,1636 }1637 close(inputChan)1638 res := toHtml(inputChan)16391640 if !strings.Contains(res, `<html lang="en">`) {1641 t.Error("Expected to have HTML wrapper")1642 }16431644 if !strings.Contains(res, "<th>Language</th>") {1645 t.Error("html Language check failed")1646 }1647 if !strings.Contains(res, "<th>Files</th>") {1648 t.Error("html Files check failed")1649 }1650 if !strings.Contains(res, "<th>Lines</th>") {1651 t.Error("html Lines check failed")1652 }1653 if !strings.Contains(res, "<th>Blank</th>") {1654 t.Error("html Blank check failed")1655 }1656 if !strings.Contains(res, "<th>Comment</th>") {1657 t.Error("html Comment check failed")1658 }1659 if !strings.Contains(res, "<th>Code</th>") {1660 t.Error("html Code check failed")1661 }1662 if !strings.Contains(res, "<th>Complexity</th>") {1663 t.Error("html Complexity check failed")1664 }1665 if !strings.Contains(res, "<th>Bytes</th>") {1666 t.Error("html Bytes check failed")1667 }1668 if !strings.Contains(res, "<th>Uloc</th>") {1669 t.Error("html Uloc check failed")1670 }1671}16721673func TestToHTMLTable(t *testing.T) {1674 inputChan := make(chan *FileJob, 1000)1675 inputChan <- &FileJob{1676 Language: "Go",1677 Filename: "bbbb.go",1678 Extension: "go",1679 Location: "./",1680 Bytes: 1000,1681 Lines: 1000,1682 Code: 1000,1683 Comment: 1000,1684 Blank: 1000,1685 Complexity: 1000,1686 WeightedComplexity: 1000,1687 Binary: false,1688 }1689 close(inputChan)1690 res := toHtmlTable(inputChan)16911692 if strings.Contains(res, `<html lang="en">`) {1693 t.Error("Expected to not have wrapper")1694 }16951696 if !strings.Contains(res, `<table id="scc-table">`) {1697 t.Error("Expected to have table element")1698 }1699}17001701// Language names and file paths are attacker/user controlled, so they have to be1702// escaped or they break out of the cell and produce invalid HTML.1703func TestToHTMLTableEscapesLanguageAndLocation(t *testing.T) {1704 t.Cleanup(func() { Files = false })1705 Files = true17061707 inputChan := make(chan *FileJob, 1000)1708 inputChan <- &FileJob{1709 Language: `<script>alert("x")</script>`,1710 Filename: "b&b.go",1711 Location: `./a<b>&"c".go`,1712 }1713 close(inputChan)1714 res := toHtmlTable(inputChan)17151716 if strings.Contains(res, "<script>") {1717 t.Error("expected the language name to be escaped, got", res)1718 }1719 if !strings.Contains(res, `<th><script>alert("x")</script></th>`) {1720 t.Error("expected escaped language cell, got", res)1721 }1722 if !strings.Contains(res, `<td>./a<b>&"c".go</td>`) {1723 t.Error("expected escaped location cell, got", res)1724 }1725}17261727func TestUnicodeAwareTrimAscii(t *testing.T) {1728 tmp := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.md"1729 res := unicodeAwareTrim(tmp, shortFormatFileTruncate)1730 if res != "~aaaaaaaaaaaaaaaaaaaaaaa.md" {1731 t.Error("expected ~aaaaaaaaaaaaaaaaaaaaaaa.md got", res)1732 }1733}17341735func TestUnicodeAwareTrimExactSizeAscii(t *testing.T) {1736 tmp := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.md"1737 res := unicodeAwareTrim(tmp, len(tmp))1738 if res != tmp {1739 t.Errorf("expected %s got %s", tmp, res)1740 }1741}17421743func TestUnicodeAwareTrimUnicode(t *testing.T) {1744 tmp := "中文中文中文中文中文中文中文中文中文中文中文中文中文中文中文中文.md"1745 res := unicodeAwareTrim(tmp, shortFormatFileTruncate)1746 if res != "~文中文中文中文中文中文.md" {1747 t.Error("expected ~文中文中文中文中文中文.md got", res)1748 }1749}17501751func TestUnicodeAwareRightPad(t *testing.T) {1752 tmp := unicodeAwareRightPad("", 10)1753 if runewidth.StringWidth(tmp) != 10 {1754 t.Errorf("expected length of 10")1755 }1756}17571758func TestUnicodeAwareRightPadUnicode(t *testing.T) {1759 tmp := unicodeAwareRightPad("中文", 10)1760 if runewidth.StringWidth(tmp) != 10 {1761 t.Errorf("expected length of 10")1762 }1763}17641765func BenchmarkUnicodeAwareTrimExactSizeAscii(b *testing.B) {1766 tmp := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.md"1767 for b.Loop() {1768 res := unicodeAwareTrim(tmp, len(tmp))1769 if res != tmp {1770 b.Fatalf("expected %s got %s", tmp, res)1771 }1772 }1773}17741775func BenchmarkUnicodeAwareTrimUnicode(b *testing.B) {1776 tmp := "中文中文中文中文中文中文中文中文中文中文中文中文中文中文中文中文.md"1777 for b.Loop() {1778 res := unicodeAwareTrim(tmp, shortFormatFileTruncate)1779 if res != "~文中文中文中文中文中文.md" {1780 b.Fatalf("expected ~文中文中文中文中文中文.md got %s", res)1781 }1782 }1783}17841785func BenchmarkUnicodeAwareRightPad(b *testing.B) {1786 for b.Loop() {1787 tmp := unicodeAwareRightPad("", 10)1788 if runewidth.StringWidth(tmp) != 10 {1789 b.Fatal("expected length of 10")1790 }1791 }1792}17931794func BenchmarkUnicodeAwareRightPadUnicode(b *testing.B) {1795 for b.Loop() {1796 tmp := unicodeAwareRightPad("中文", 10)1797 if runewidth.StringWidth(tmp) != 10 {1798 b.Fatal("expected length of 10")1799 }1800 }1801}18021803// When using columise ~28726 ns/op1804// When using optimised ~14293 ns/op1805func BenchmarkFileSummerize(b *testing.B) {1806 for i := 0; i < b.N; i++ {1807 b.StopTimer()1808 fileSummaryJobQueue := make(chan *FileJob, 1000)18091810 fileSummaryJobQueue <- &FileJob{1811 Blank: 1,1812 Bytes: 1,1813 Code: 1,1814 Comment: 1,1815 Complexity: 1,1816 Language: "Go",1817 Lines: 10,1818 }1819 fileSummaryJobQueue <- &FileJob{1820 Blank: 2,1821 Bytes: 2,1822 Code: 2,1823 Comment: 2,1824 Complexity: 2,1825 Language: "Python",1826 Lines: 20,1827 }1828 close(fileSummaryJobQueue)1829 b.StartTimer()18301831 fileSummarize(fileSummaryJobQueue)1832 }1833}18341835func TestGetCSVFilesSortFunc(t *testing.T) {1836 records := [][]string{1837 // Language,Provider,Filename,Lines,Code,Comments,Blanks,Complexity,Bytes,ULOC1838 {"Go", "/path/to/file", "go.go", "10", "10", "0", "1", "1", "1024", "0"},1839 {"Python", "/path/to/file", "python.py", "20", "20", "1", "2", "2", "2048", "0"},1840 {"C#", "/path/to/file", "csharp.cs", "30", "30", "2", "3", "3", "4096", "0"},1841 {"C++", "/path/to/file", "cpp.cpp", "40", "40", "3", "4", "4", "8192", "0"},1842 }1843 testCases := []struct {1844 sortBy string1845 expected []string1846 }{1847 {1848 sortBy: "names",1849 expected: []string{"C++", "C#", "Go", "Python"},1850 },1851 {1852 sortBy: "langs",1853 expected: []string{"C#", "C++", "Go", "Python"},1854 },1855 {1856 sortBy: "lines",1857 expected: []string{"C++", "C#", "Python", "Go"},1858 },1859 {1860 sortBy: "code",1861 expected: []string{"C++", "C#", "Python", "Go"},1862 },1863 {1864 sortBy: "comments",1865 expected: []string{"C++", "C#", "Python", "Go"},1866 },1867 {1868 sortBy: "blanks",1869 expected: []string{"C++", "C#", "Python", "Go"},1870 },1871 {1872 sortBy: "complexity",1873 expected: []string{"C++", "C#", "Python", "Go"},1874 },1875 {1876 sortBy: "bytes",1877 expected: []string{"C++", "C#", "Python", "Go"},1878 },1879 {1880 sortBy: "default",1881 expected: []string{"C++", "C#", "Go", "Python"},1882 },1883 }1884 for _, tc := range testCases {1885 data := slices.Clone(records) // always use an unordered records1886 slices.SortFunc(data, getCSVFilesSortFunc(tc.sortBy))1887 sortedRecords := make([]string, 0, len(data))1888 for i := range data {1889 sortedRecords = append(sortedRecords, data[i][0])1890 }1891 if !slices.Equal(sortedRecords, tc.expected) {1892 t.Errorf("sortBy: %s failed, expected: %v, got: %v", tc.sortBy, tc.expected, sortedRecords)1893 }1894 }1895}18961897func TestToCSVFilesHeader(t *testing.T) {1898 inputChan := make(chan *FileJob, 1000)1899 inputChan <- &FileJob{1900 Language: "Go",1901 Filename: "bbbb.go",1902 Extension: "go",1903 Location: "./",1904 Bytes: 1000,1905 Lines: 1000,1906 Code: 1000,1907 Comment: 1000,1908 Blank: 1000,1909 Complexity: 1000,1910 WeightedComplexity: 1000,1911 Binary: false,1912 }1913 inputChan <- &FileJob{1914 Language: "Go",1915 Filename: "aaaa.go",1916 Extension: "go",1917 Location: "./",1918 Bytes: 1000,1919 Lines: 1000,1920 Code: 1000,1921 Comment: 1000,1922 Blank: 1000,1923 Complexity: 1000,1924 WeightedComplexity: 1000,1925 Binary: false,1926 }1927 close(inputChan)1928 res := toCSVFiles(inputChan)1929 header, _, _ := strings.Cut(res, "\n")1930 const expected = "Language,Provider,Filename,Lines,Code,Comments,Blanks,Complexity,Bytes,ULOC"1931 if header != expected {1932 t.Errorf("check toCSVFiles header failed, expected: %v, got: %v", expected, header)1933 }1934}19351936func TestToCSVStreamHeader(t *testing.T) {1937 inputChan := make(chan *FileJob, 1000)1938 inputChan <- &FileJob{1939 Language: "Go",1940 Filename: "bbbb.go",1941 Extension: "go",1942 Location: "./",1943 Bytes: 1000,1944 Lines: 1000,1945 Code: 1000,1946 Comment: 1000,1947 Blank: 1000,1948 Complexity: 1000,1949 WeightedComplexity: 1000,1950 Binary: false,1951 }1952 inputChan <- &FileJob{1953 Language: "Go",1954 Filename: "aaaa.go",1955 Extension: "go",1956 Location: "./",1957 Bytes: 1000,1958 Lines: 1000,1959 Code: 1000,1960 Comment: 1000,1961 Blank: 1000,1962 Complexity: 1000,1963 WeightedComplexity: 1000,1964 Binary: false,1965 }1966 close(inputChan)19671968 originStdout := os.Stdout1969 t.Cleanup(func() {1970 os.Stdout = originStdout1971 })1972 r, w, err := os.Pipe()1973 if err != nil {1974 t.Fatal(err)1975 }1976 os.Stdout = w1977 go func() {1978 toCSVStream(inputChan)1979 _ = w.Close()1980 }()1981 output, err := io.ReadAll(r)1982 if err != nil {1983 t.Fatal(err)1984 }19851986 header, _, _ := strings.Cut(string(output), "\n")1987 const expected = "Language,Provider,Filename,Lines,Code,Comments,Blanks,Complexity,Bytes,Uloc"1988 if header != expected {1989 t.Errorf("check toCSVStream header failed, expected: %v, got: %v", expected, header)1990 }1991}19921993func TestToJSONKeys(t *testing.T) {1994 inputChan := make(chan *FileJob, 1000)1995 inputChan <- &FileJob{1996 Language: "Go",1997 Filename: "bbbb.go",1998 Extension: "go",1999 Location: "./",2000 Bytes: 1000,
Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.