processor/formatters_test.go GO 2,128 lines View on github.com → Search inside
File is large — showing lines 1–2,000 of 2,128.
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>&lt;script&gt;alert(&#34;x&#34;)&lt;/script&gt;</th>`) {1720		t.Error("expected escaped language cell, got", res)1721	}1722	if !strings.Contains(res, `<td>./a&lt;b&gt;&amp;&#34;c&#34;.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,

Code quality findings 2

Goroutine without waitgroup or channel; risks resource leaks or race conditions
warning correctness goroutine-without-sync
go func() {
Multiple appends without pre-allocation; use make() with capacity when size is known
info performance append-without-prealloc
sortedRecords = append(sortedRecords, data[i][0])

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.