main_test.go GO 1,169 lines View on github.com → Search inside
1package main23import (4	"fmt"5	"os"6	"os/exec"7	"path/filepath"8	"regexp"9	"runtime"10	"slices"11	"strconv"12	"strings"13	"testing"14)1516const sccTestFlag string = "-test.main"1718var sccBinPath = os.Args[0]1920func TestMain(m *testing.M) {21	idx := slices.Index(os.Args, sccTestFlag)22	if idx != -1 {23		os.Args = slices.Delete(os.Args, idx, idx+1)24		main()25		return26	}2728	os.Exit(m.Run())29}3031func runSCC(args ...string) (string, error) {32	args = slices.Insert(args, 0, sccTestFlag)33	cmd := exec.Command(sccBinPath, args...)34	// Force SCC_CONFIG_PATH empty (treated as unset) so a global config in the35	// developer's environment cannot pollute these runs - an unreadable one would36	// otherwise make scc exit non-zero and fail every test here.37	cmd.Env = append(os.Environ(), SccConfigEnv+"=")38	res, err := cmd.CombinedOutput()39	return string(res), err40}4142func TestNoGitIgnore(t *testing.T) {43	tmpDir := t.TempDir()44	ignoreFileName := filepath.Join(tmpDir, ".gitignore")45	err := os.WriteFile(ignoreFileName, []byte("ignored.xml\n"), 0644)46	if err != nil {47		t.Fatal(err)48	}49	xmlFileName := filepath.Join(tmpDir, "ignored.xml")50	err = os.WriteFile(xmlFileName, []byte(`<?xml version="1.0" encoding="UTF-8"?>`), 0644)51	if err != nil {52		t.Fatal(err)53	}5455	output, err := runSCC(tmpDir)56	if err != nil {57		t.Fatal(err)58	}59	if strings.Contains(output, "XML") {60		t.Fatalf("test --no-gitignore failed, output:\n%s", output)61	}6263	output, err = runSCC("--no-gitignore", tmpDir)64	if err != nil {65		t.Fatal(err)66	}67	if !strings.Contains(output, "XML") {68		t.Fatalf("test --no-gitignore failed, output:\n%s", output)69	}70}7172func TestIssue82(t *testing.T) {73	t.Parallel()74	// Regression issue https://github.com/boyter/scc/issues/8275	output1, err := runSCC(".")76	if err != nil {77		t.Fatal(err)78	}7980	pwd, err := os.Getwd()81	if err != nil {82		t.Fatal(err)83	}84	output2, err := runSCC(pwd)85	if err != nil {86		t.Fatal(err)87	}8889	if output1 != output2 {90		t.Fatalf("`./scc .` not equal to `./scc ${PWD}`")91	}92}9394func TestIncludeExt(t *testing.T) {95	t.Parallel()96	// Regression issue https://github.com/boyter/scc/issues/10897	output, err := runSCC("--include-ext", "go", "examples/language")98	if err != nil {99		t.Fatal(err)100	}101	if !strings.Contains(output, "Go") || strings.Contains(output, "Java") {102		t.Fatalf("include-ext check failed, output:\n%s", output)103	}104}105106func TestIssue115(t *testing.T) {107	t.Parallel()108	// Regression issue https://github.com/boyter/scc/issues/115109	output, err := runSCC("examples/issue115/.test/file")110	if err != nil {111		t.Fatal(err)112	}113	if strings.Contains(output, "Perl") {114		t.Fatalf("Should not print Perl, output:\n%s", output)115	}116}117118func TestIssue120(t *testing.T) {119	t.Parallel()120	// Regression issue https://github.com/boyter/scc/issues/120121	output, err := runSCC("-i", "java", "./examples/issue120")122	if err != nil {123		t.Fatal(err)124	}125	if strings.Contains(output, "Perl") {126		t.Fatal("extension param should ignore Shebang")127	}128}129130func TestIssue152(t *testing.T) {131	t.Parallel()132	// Regression issue https://github.com/boyter/scc/issues/152133	output, err := runSCC("-i", "css", "./examples/issue152/")134	if err != nil {135		t.Fatal(err)136	}137	if !strings.Contains(output, "CSS") {138		t.Fatalf("`-i css` extension check failed, output:\n%s", output)139	}140}141142func TestIssue250(t *testing.T) {143	// Regression issue https://github.com/boyter/scc/issues/250144	output1, err := runSCC("--exclude-dir", "examples/")145	if err != nil {146		t.Fatal(err)147	}148	output2, err := runSCC("--exclude-dir", "examples")149	if err != nil {150		t.Fatal(err)151	}152153	if output1 != output2 {154		t.Fatalf("examples exclude-dir check failed, output1:\n%s, output2:\n%s", output1, output2)155	}156}157158func TestIssue259(t *testing.T) {159	// Regression issue https://github.com/boyter/scc/issues/259160	output, err := runSCC("-f", "csv", "--exclude-ext", "go")161	if err != nil {162		t.Fatal(err)163	}164165	if strings.Contains(output, "Go,") {166		t.Fatalf("exclude-ext check failed, output:\n%s", output)167	}168}169170func TestIssue260(t *testing.T) {171	t.Parallel()172	// Regression issue https://github.com/boyter/scc/issues/260173	_, err := runSCC("-d", "examples/issue260/")174	if err != nil {175		t.Fatalf("duplicate empty crash: %v", err)176	}177}178179func TestIssue345(t *testing.T) {180	t.Parallel()181	// Regression issue https://github.com/boyter/scc/issues/345182	const expectedOutput = "C++,4,3,1,0,0,76,1,0"183	output, err := runSCC("-f", "csv", "--no-scc-ignore", "examples/issue345/")184	if err != nil {185		t.Fatal(err)186	}187	lines := strings.Split(output, "\n")188	if len(lines) < 2 {189		t.Fatalf("wrong output: %s", output)190	}191	if lines[1] != expectedOutput {192		t.Fatalf("got: %s, want: %s", lines[1], expectedOutput)193	}194}195196func TestIssue379(t *testing.T) {197	t.Parallel()198	// Regression issue https://github.com/boyter/scc/issues/379199	const expectedOutput = "Python,7,4,2,1,1,83,1,0"200	output, err := runSCC("-f", "csv", "--no-scc-ignore", "examples/issue379/")201	if err != nil {202		t.Fatal(err)203	}204	lines := strings.Split(output, "\n")205	if len(lines) < 2 {206		t.Fatalf("wrong output: %s", output)207	}208	if lines[1] != expectedOutput {209		t.Fatalf("got: %s, want: %s", lines[1], expectedOutput)210	}211}212213func TestIssue457(t *testing.T) {214	t.Parallel()215	// Regression issue https://github.com/boyter/scc/issues/457216	output, err := runSCC("-M", ".*")217	if err != nil {218		t.Fatal(err)219	}220	if !strings.Contains(output, "0.000 megabytes") {221		t.Fatalf("Issue 457 test failed, output:\n%s", output)222	}223}224225func TestIssue564(t *testing.T) {226	t.Parallel()227	// Regression issue https://github.com/boyter/scc/issues/564228	const expectedPythonOutput = "Python,3,3,0,0,0,84,3,0"229	const expectedGoOutput = "Go,6,4,0,2,0,58,2,0"230	output, err := runSCC("-f", "csv", "--no-scc-ignore", "examples/issue564/")231	if err != nil {232		t.Fatal(err)233	}234	lines := strings.Split(output, "\n")235	if len(lines) < 3 {236		t.Fatalf("wrong output: %s", output)237	}238	if lines[1] != expectedPythonOutput {239		t.Fatalf("got: %s, want: %s", lines[1], expectedPythonOutput)240	}241	if lines[2] != expectedGoOutput {242		t.Fatalf("got: %s, want: %s", lines[2], expectedGoOutput)243	}244}245246func TestIssue610(t *testing.T) {247	t.Parallel()248	// Regression issue https://github.com/boyter/scc/issues/610249	const expectedOutput = "TypeScript,11,7,2,2,1,214,1,0"250	output, err := runSCC("-f", "csv", "--no-scc-ignore", "examples/issue610/")251	if err != nil {252		t.Fatal(err)253	}254	lines := strings.Split(output, "\n")255	if len(lines) < 2 {256		t.Fatalf("wrong output: %s", output)257	}258	if lines[1] != expectedOutput {259		t.Fatalf("got: %s, want: %s", lines[1], expectedOutput)260	}261}262263func TestIssue339(t *testing.T) {264	t.Parallel()265	// Regression issue https://github.com/boyter/scc/issues/339266	output, err := runSCC("-f", "csv", "--no-scc-ignore", "examples/issue339/")267	if err != nil {268		t.Fatal(err)269	}270	if !strings.Contains(output, "MATLAB") {271		t.Errorf("can not find MATLAB, output: %s", output)272	}273	if !strings.Contains(output, "Objective C") {274		t.Errorf("can not find Objective C, output:\n%s", output)275	}276}277278func TestInvalidOption(t *testing.T) {279	t.Parallel()280	output, err := runSCC("--not-a-real-option")281	if err == nil {282		t.Fatal("scc should exit with error code")283	}284	if !strings.Contains(output, "Error: unknown flag: --not-a-real-option") {285		t.Fatalf("scc should report invalid options, output:\n%s", output)286	}287}288289func TestFileFlagSyntax(t *testing.T) {290	tmpDir := t.TempDir()291	flagsFileName := filepath.Join(tmpDir, "flags.txt")292	// include \n, \r\n and no line terminators293	testCases := []string{294		"go.mod\ngo.sum\nLICENSE\n",295		"go.mod\r\ngo.sum\r\nLICENSE\r\n",296		"go.mod\ngo.sum\nLICENSE",297		"go.mod\r\ngo.sum\r\nLICENSE",298		"go.mod\ngo.sum\r\nLICENSE",299	}300301	for _, tc := range testCases {302		err := os.WriteFile(flagsFileName, []byte(tc), 0644)303		if err != nil {304			t.Fatal(err)305		}306		_, err = runSCC("@" + flagsFileName)307		if err != nil {308			t.Errorf("flag syntax faild: %q, %v", tc, err)309		}310	}311}312313func TestLineLength(t *testing.T) {314	t.Parallel()315	output, err := runSCC("-m")316	if err != nil {317		t.Fatal(err)318	}319	if strings.Count(output, "MaxLine / MeanLine") < 2 {320		t.Fatalf("line length test failed, output:\n%s", output)321	}322}323324func TestFormatHTML(t *testing.T) {325	t.Parallel()326	output, err := runSCC("--format", "html")327	if err != nil {328		t.Fatal(err)329	}330	if !strings.Contains(output, "<title>scc html output</title>") {331		t.Fatalf("html format test failed, output:\n%s", output)332	}333}334335func TestFormatHTMLTable(t *testing.T) {336	t.Parallel()337	output, err := runSCC("--format", "html-table")338	if err != nil {339		t.Fatal(err)340	}341	if !strings.Contains(output, `<table id="scc-table">`) {342		t.Fatalf("html-table format test failed, output:\n%s", output)343	}344}345346func TestFormatSQL(t *testing.T) {347	t.Parallel()348	output, err := runSCC("--format", "sql")349	if err != nil {350		t.Fatal(err)351	}352	if !strings.Contains(output, "create table metadata (   -- github.com/boyter/scc") {353		t.Fatalf("sql format test failed, output:\n%s", output)354	}355}356357func TestFormatSQLInsert(t *testing.T) {358	t.Parallel()359	output, err := runSCC("--format", "sql-insert")360	if err != nil {361		t.Fatal(err)362	}363	if !strings.Contains(output, "begin transaction;\ninsert into t values(") {364		t.Fatalf("sql-insert format test failed, output:\n%s", output)365	}366}367368func TestMultipleFormatStdout(t *testing.T) {369	output, err := runSCC("--format-multi", "tabular:stdout,html:stdout,csv:stdout,sql:stdout")370	if err != nil {371		t.Fatal(err)372	}373374	tabularPattern := regexp.MustCompile(`Processed .+? bytes, .+? megabytes \(SI\)`)375	if !tabularPattern.MatchString(output) {376		t.Errorf("multi-format tabular failed, output:\n%s", output)377	}378379	if !strings.Contains(output, `<html lang="en"><head><meta charset="utf-8" /><title>scc html output</title>`) {380		t.Errorf("multi-format html failed, output:\n%s", output)381	}382383	if !strings.Contains(output, "Language,Lines,Code,Comments,Blanks,Complexity,Bytes,Files,ULOC") {384		t.Errorf("multi-format csv failed, output:\n%s", output)385	}386387	sqlPattern := regexp.MustCompile(`insert into t values\(.+?\);`)388	if !sqlPattern.MatchString(output) {389		t.Errorf("multi-format sql failed, output:\n%s", output)390	}391}392393func TestMultipleFormatWriteFile(t *testing.T) {394	tmpDir := t.TempDir()395	outputTabular := filepath.Join(tmpDir, "output.tab")396	outputWide := filepath.Join(tmpDir, "output.wide")397	outputJSON1 := filepath.Join(tmpDir, "output.json")398	outputJSON2 := filepath.Join(tmpDir, "output2.json")399	outputCSV := filepath.Join(tmpDir, "output.csv")400	outputYAML := filepath.Join(tmpDir, "output.yaml")401	outputHTML := filepath.Join(tmpDir, "output.html")402	outputHTMLTable := filepath.Join(tmpDir, "output_table.html")403	outputSQL := filepath.Join(tmpDir, "output.sql")404405	multiFormatArgs := fmt.Sprintf(406		"tabular:%s,wide:%s,json:%s,json2:%s,csv:%s,cloc-yaml:%s,html:%s,html-table:%s,sql:%s",407		outputTabular,408		outputWide,409		outputJSON1,410		outputJSON2,411		outputCSV,412		outputYAML,413		outputHTML,414		outputHTMLTable,415		outputSQL,416	)417418	_, err := runSCC("--format-multi", multiFormatArgs)419	if err != nil {420		t.Fatal(err)421	}422423	if info, err := os.Stat(outputTabular); err != nil || info.Size() <= 0 {424		t.Fatal("tabular write file test failed")425	}426	if info, err := os.Stat(outputWide); err != nil || info.Size() <= 0 {427		t.Fatal("wide write file test failed")428	}429	if info, err := os.Stat(outputJSON1); err != nil || info.Size() <= 0 {430		t.Fatal("json write file test failed")431	}432	if info, err := os.Stat(outputJSON2); err != nil || info.Size() <= 0 {433		t.Fatal("json2 write file test failed")434	}435	if info, err := os.Stat(outputCSV); err != nil || info.Size() <= 0 {436		t.Fatal("csv write file test failed")437	}438	if info, err := os.Stat(outputYAML); err != nil || info.Size() <= 0 {439		t.Fatal("cloc-yaml write file test failed")440	}441	if info, err := os.Stat(outputHTML); err != nil || info.Size() <= 0 {442		t.Fatal("html write file test failed")443	}444	if info, err := os.Stat(outputHTMLTable); err != nil || info.Size() <= 0 {445		t.Fatal("html-table write file test failed")446	}447	if info, err := os.Stat(outputSQL); err != nil || info.Size() <= 0 {448		t.Fatal("sql write file test failed")449	}450}451452// TestMultipleFormatWriteFileColonPath is a regression test for453// https://github.com/boyter/scc/issues/290 where --format-multi silently454// discarded any target containing a colon, such as a Windows absolute path455// like csv:C:\folder\out.csv, because the format:target pair was split on456// every colon rather than only the first one.457func TestMultipleFormatWriteFileColonPath(t *testing.T) {458	tmpDir := t.TempDir()459	outputCSV := filepath.Join(tmpDir, "C:output.csv")460461	_, err := runSCC("--format-multi", "csv:"+outputCSV)462	if err != nil {463		t.Fatal(err)464	}465466	if info, err := os.Stat(outputCSV); err != nil || info.Size() <= 0 {467		t.Fatalf("csv write to colon containing path failed for %s", outputCSV)468	}469}470471func TestRecursivelyIgnore(t *testing.T) {472	tmpDir := t.TempDir()473	err := os.WriteFile(filepath.Join(tmpDir, ".gitignore"), []byte("ignore-git.txt\n"), 0644)474	if err != nil {475		t.Fatal(err)476	}477	err = os.WriteFile(filepath.Join(tmpDir, ".ignore"), []byte("vendor/\nignore.txt\n"), 0644)478	if err != nil {479		t.Fatal(err)480	}481	err = os.Mkdir(filepath.Join(tmpDir, "ignore"), 0755)482	if err != nil {483		t.Fatal(err)484	}485	err = os.WriteFile(filepath.Join(tmpDir, "ignore", "README.md"), []byte("Files in here are to ensure that .ignore and .gitignore work recursively\n"), 0644)486	if err != nil {487		t.Fatal(err)488	}489	err = os.WriteFile(filepath.Join(tmpDir, "ignore", "ignore.txt"), []byte("testing\n"), 0644)490	if err != nil {491		t.Fatal(err)492	}493	err = os.WriteFile(filepath.Join(tmpDir, "ignore", "ignore-git.txt"), []byte("git\ntesting\n"), 0644)494	if err != nil {495		t.Fatal(err)496	}497498	output, err := runSCC("--by-file", "--no-scc-ignore", tmpDir)499	if err != nil {500		t.Fatal(err)501	}502	if strings.Contains(output, "ignore.txt") || strings.Contains(output, "ignore-git.txt") {503		t.Errorf("ignore recursive filter failed, output:\n%s", output)504	}505506	output, err = runSCC("--by-file", "--no-scc-ignore", "--no-ignore", tmpDir)507	if err != nil {508		t.Fatal(err)509	}510	if !strings.Contains(output, "ignore.txt") || strings.Contains(output, "ignore-git.txt") {511		t.Errorf("ignore recursive filter failed, output:\n%s", output)512	}513514	output, err = runSCC("--by-file", "--no-scc-ignore", "--no-gitignore", tmpDir)515	if err != nil {516		t.Fatal(err)517	}518	if strings.Contains(output, "ignore.txt") || !strings.Contains(output, "ignore-git.txt") {519		t.Errorf("ignore recursive filter failed, output:\n%s", output)520	}521522	output, err = runSCC("--by-file", "--no-scc-ignore", "--no-ignore", "--no-gitignore", tmpDir)523	if err != nil {524		t.Fatal(err)525	}526	if !strings.Contains(output, "ignore.txt") || !strings.Contains(output, "ignore-git.txt") {527		t.Errorf("ignore recursive filter failed, output:\n%s", output)528	}529}530531func TestMultipleGitIgnore(t *testing.T) {532	tmpDir := t.TempDir()533	err := os.WriteFile(filepath.Join(tmpDir, ".gitignore"), []byte("ignore.txt\n"), 0644)534	if err != nil {535		t.Fatal(err)536	}537	err = os.Mkdir(filepath.Join(tmpDir, "ignore"), 0755)538	if err != nil {539		t.Fatal(err)540	}541	err = os.WriteFile(filepath.Join(tmpDir, "ignore", ".gitignore"), []byte("ignore.java\n"), 0644)542	if err != nil {543		t.Fatal(err)544	}545	err = os.WriteFile(filepath.Join(tmpDir, "ignore", "ignore.java"), []byte("//test\n"), 0644)546	if err != nil {547		t.Fatal(err)548	}549550	output, err := runSCC(tmpDir)551	if err != nil {552		t.Fatal(err)553	}554	if strings.Contains(output, "Java") {555		t.Fatalf("multiple gitignore failed, output:\n%s", output)556	}557}558559func TestFlagSuggestion(t *testing.T) {560	t.Parallel()561	testCases := []struct {562		args           []string563		expectedOutput string564	}{565		{566			args:           []string{"--farmat"},567			expectedOutput: "The most similar flag of --farmat is:\n\t--format\n",568		},569		{570			args:           []string{"--no-gignore"},571			expectedOutput: "The most similar flags of --no-gignore are:\n\t--no-ignore\n\t--no-gitignore\n",572		},573	}574575	for _, tc := range testCases {576		output, err := runSCC(tc.args...)577		if err == nil {578			t.Fatal("scc should exit with error code")579		}580		if !strings.Contains(output, tc.expectedOutput) {581			t.Errorf("wrong suggestion for %v, want: %s, got: %s", tc.args, tc.expectedOutput, output)582		}583	}584}585586func TestDeterministicOutput(t *testing.T) {587	output, err := runSCC(".")588	if err != nil {589		t.Fatal(err)590	}591	for range 10 {592		output2, err := runSCC(".")593		if err != nil {594			t.Fatal(err)595		}596		if output != output2 {597			t.Fatalf("want:\n%s, got:\n%s", output, output2)598		}599	}600}601602func TestDuplicates(t *testing.T) {603	for range 10 {604		output, err := runSCC("-f", "json", "-d", "./examples/duplicates/")605		if err != nil {606			t.Fatal(err)607		}608		if !strings.Contains(output, `"Count":1`) {609			t.Fatalf("duplicates check failed, output:\n%s", output)610		}611	}612}613614func TestCountAs(t *testing.T) {615	testCases := []struct {616		countAs  string617		expected []string618	}{619		{620			countAs:  "jsp:html",621			expected: []string{"HTML"},622		},623		{624			countAs:  "JsP:html",625			expected: []string{"HTML"},626		},627		{628			countAs:  "jsp:j2",629			expected: []string{"Jinja"},630		},631		{632			countAs:  "jsp:html,new:java",633			expected: []string{"HTML", "Java"},634		},635		{636			countAs:  "jsp:html,new:C Header",637			expected: []string{"HTML", "C Header"},638		},639	}640641	for _, tc := range testCases {642		output, err := runSCC("-f", "csv", "--count-as", tc.countAs, "./examples/countas/")643		if err != nil {644			t.Fatal(err)645		}646		for _, expectedLang := range tc.expected {647			if !strings.Contains(output, expectedLang+",") {648				t.Errorf("count as failed, count as: %s, output:\n%s", tc.countAs, output)649			}650		}651	}652}653654func TestCountAsPattern(t *testing.T) {655	// foo_spec.rb is relabelled to the new Ruby Spec category, app.rb stays Ruby656	output, err := runSCC("-f", "csv", "--count-as-pattern", "glob:*_spec.rb:Ruby Spec:Ruby", "./examples/countaspattern/")657	if err != nil {658		t.Fatal(err)659	}660661	// The minted category row, with non-zero comment and complexity counts which662	// proves the counting rules were cloned from the Ruby base language663	if !strings.Contains(output, "Ruby Spec,9,7,1,1,1,") {664		t.Errorf("count-as-pattern failed to produce Ruby Spec row, output:\n%s", output)665	}666	// The non matching file is still counted as plain Ruby667	if !strings.Contains(output, "Ruby,7,5,1,1,1,") {668		t.Errorf("count-as-pattern should leave app.rb as Ruby, output:\n%s", output)669	}670671	// The regex engine should produce the same relabelling672	output, err = runSCC("-f", "csv", "--count-as-pattern", `re:_spec\.rb$:Ruby Spec:Ruby`, "./examples/countaspattern/")673	if err != nil {674		t.Fatal(err)675	}676	if !strings.Contains(output, "Ruby Spec,") {677		t.Errorf("regex count-as-pattern failed, output:\n%s", output)678	}679}680681func TestCountAsPatternInvalidSkipped(t *testing.T) {682	// An unresolvable base language is reported and skipped, the run still works683	output, err := runSCC("-f", "csv", "--count-as-pattern", "glob:*_spec.rb:Ruby Spec:Nonexistent", "./examples/countaspattern/")684	if err != nil {685		t.Fatal(err)686	}687	if !strings.Contains(output, "is not a known language or extension") {688		t.Errorf("expected error message for unknown base language, output:\n%s", output)689	}690	if strings.Contains(output, "Ruby Spec,") {691		t.Errorf("Ruby Spec should not appear when the rule was skipped, output:\n%s", output)692	}693}694695func TestRemapUnknown(t *testing.T) {696	t.Parallel()697	output, err := runSCC("-f", "csv", "--remap-unknown", "-*- C++ -*-:C Header", "./examples/remap/unknown")698	if err != nil {699		t.Fatal(err)700	}701	if !strings.Contains(output, "C Header,") {702		t.Fatalf("remap unknown failed, output:\n%s", output)703	}704}705706func TestRemapAll(t *testing.T) {707	t.Parallel()708	output, err := runSCC("-f", "csv", "--remap-all", "-*- C++ -*-:C Header", "./examples/remap/java.java")709	if err != nil {710		t.Fatal(err)711	}712	if !strings.Contains(output, "C Header,") {713		t.Fatalf("remap all failed, output:\n%s", output)714	}715}716717func TestCocomoProjectType(t *testing.T) {718	projectTypes := []string{"organic", "semi-detached", "embedded", "custom,1,1,1,1"}719	for _, typ := range projectTypes {720		output, err := runSCC("--cocomo-project-type", typ)721		if err != nil {722			t.Fatal(err)723		}724		if !strings.Contains(output, fmt.Sprintf("Estimated Cost to Develop (%s)", typ)) ||725			!strings.Contains(output, fmt.Sprintf("Estimated Schedule Effort (%s)", typ)) ||726			!strings.Contains(output, fmt.Sprintf("Estimated People Required (%s)", typ)) {727			t.Errorf("check cocomo project type failed: %s", typ)728		}729	}730}731732func TestCocomoProjectTypeFallback(t *testing.T) {733	unknownTypes := []string{"doesnotexist", "custom,1,1,1"}734	for _, typ := range unknownTypes {735		output, err := runSCC("--cocomo-project-type", typ)736		if err != nil {737			t.Fatal(err)738		}739		if !strings.Contains(output, "Estimated Cost to Develop (organic)") ||740			!strings.Contains(output, "Estimated Schedule Effort (organic)") ||741			!strings.Contains(output, "Estimated People Required (organic)") {742			t.Errorf("check cocomo project type fallback failed: %s", typ)743		}744	}745}746747func TestOutputBytes(t *testing.T) {748	jsonOutput, err := runSCC("-f", "json")749	if err != nil {750		t.Fatal(err)751	}752	if !strings.Contains(jsonOutput, `"Bytes":`) {753		t.Errorf("json output does not contain `Bytes` field, output:\n%s", jsonOutput)754	}755756	output, err := runSCC()757	if err != nil {758		t.Fatal(err)759	}760	if !strings.Contains(output, "megabytes") {761		t.Errorf("output does not contain `megabytes`, output:\n%s", output)762	}763}764765func TestFileGCCount(t *testing.T) {766	const target = "./examples/duplicates"767	files, err := os.ReadDir(target)768	if err != nil {769		t.Fatal(err)770	}771772	output, err := runSCC("--file-gc-count", strconv.Itoa(len(files)-1), "-v", target)773	if err != nil {774		t.Fatal(err)775	}776	if !strings.Contains(output, "read file limit exceeded GC re-enabled") {777		t.Errorf("test file GC count failed, file count: %d, limit: %d", len(files), len(files)-1)778	}779780	output, err = runSCC("--file-gc-count", strconv.Itoa(len(files)+1), "-v", target)781	if err != nil {782		t.Fatal(err)783	}784	if strings.Contains(output, "read file limit exceeded GC re-enabled") {785		t.Errorf("test file GC count failed, file count: %d, limit: %d", len(files), len(files)+1)786	}787}788789func TestIncludeSymlinks(t *testing.T) {790	if runtime.GOOS == "windows" {791		t.Skip("skipping symlink test on Windows due to privilege requirements")792	}793794	tmpDir, err := filepath.EvalSymlinks(t.TempDir())795	if err != nil {796		t.Fatal(err)797	}798799	dirA := filepath.Join(tmpDir, "a")800	dirB := filepath.Join(tmpDir, "b")801	if err := os.Mkdir(dirA, 0755); err != nil {802		t.Fatal(err)803	}804	if err := os.Mkdir(dirB, 0755); err != nil {805		t.Fatal(err)806	}807808	const fileName = "source.go"809	if err := os.WriteFile(filepath.Join(dirA, fileName), []byte("package main\n"), 0644); err != nil {810		t.Fatal(err)811	}812	if err := os.WriteFile(filepath.Join(dirB, fileName), []byte("package main\n"), 0644); err != nil {813		t.Fatal(err)814	}815	// link to another dir, should be counted under --include-symlinks816	if err := os.Symlink(filepath.Join(dirB, fileName), filepath.Join(dirA, "link1.go")); err != nil {817		t.Fatal(err)818	}819	// link to the same dir, this should be ignored in all times820	if err := os.Symlink(filepath.Join(dirA, fileName), filepath.Join(dirA, "link2.go")); err != nil {821		t.Fatal(err)822	}823824	output, err := runSCC("-f", "json", "--no-scc-ignore", dirA)825	if err != nil {826		t.Fatal(err)827	}828	if !strings.Contains(output, `"Count":1`) {829		t.Errorf("count without symlink failed, output:\n%s", output)830	}831832	output, err = runSCC("-f", "json", "--no-scc-ignore", "--include-symlinks", dirA)833	if err != nil {834		t.Fatal(err)835	}836	if !strings.Contains(output, `"Count":2`) {837		t.Errorf("count includes symlink failed, output:\n%s", output)838	}839}840841func TestLanguageNameTruncate(t *testing.T) {842	output, err := runSCC("examples/language")843	if err != nil {844		t.Fatal(err)845	}846	if strings.Count(output, "Bitbucket Pipe…") != 1 {847		t.Errorf("`Bitbucket Pipeline` truncate test failed")848	}849	if strings.Count(output, "CloudFormation…") != 2 {850		t.Errorf("`CloudFormation (JSON)` and `CloudFormation (YAML)` truncate test failed")851	}852}853854func TestSpecificLanguages(t *testing.T) {855	languages := [...]string{856		"ABNF",857		"AL",858		"Alchemist",859		"Algol 68",860		"Alloy",861		"Amber",862		"Apex",863		"ArkTs",864		"Arturo",865		"Astro",866		"AWK",867		"BASH",868		"Bean",869		"Bicep",870		"Bitbucket Pipeline",871		"Blueprint",872		"Boo",873		"Bosque",874		"Bru",875		"C",876		"C3",877		"C Header",878		"C Shell",879		"C#",880		"C++",881		"C++ Header",882		"Cairo",883		"Cangjie",884		"Chapel",885		"Circom",886		"Clipper",887		"Clojure",888		"CMake",889		"Cuda",890		"Cypher",891		"D2",892		"DAML",893		"DM",894		"Docker ignore",895		"Dockerfile",896		"DOT",897		"Elixir Template",898		"Elm",899		"EmiT",900		"F#",901		"Factor",902		"Flow9",903		"FSL",904		"Futhark",905		"FXML",906		"Gemfile",907		"Gleam",908		"Go",909		"Go+",910		"Godot Scene",911		"GraphQL",912		"Gremlin",913		"Gwion",914		"HAML",915		"Hare",916		"Haskell",917		"HCL",918		"IEC61131-3",919		"ignore",920		"INI",921		"Java",922		"JavaScript",923		"JCL",924		"JSON5",925		"JSONC",926		"jq",927		"Korn Shell",928		"Kotlin",929		"Koto",930		"LALRPOP",931		"License",932		"Liquid",933		"LiveScript",934		"LLVM IR",935		"Lua",936		"Luau",937		"Luna",938		"MLIR",939		"Makefile",940		"Metal",941		"Mojo",942		"Monkey C",943		"Moonbit",944		"Move",945		"Nature",946		"Nushell",947		"OpenQASM",948		"OpenTofu",949		"Patch",950		"Perl",951		"PHP",952		"Pkl",953		"Plain Text",954		"POML",955		"PostScript",956		"ProGuard",957		"Proto",958		"Python",959		"Q#",960		"Qt Translation Source",961		"R",962		"Racket",963		"Rakefile",964		"RAML",965		"Rebol",966		"Redscript",967		"Rich Text Format",968		"Ruby",969		"Rust",970		"Scala",971		"Scallop",972		"Seed7",973		"Shell",974		"Sieve",975		"Slang",976		"Slint",977		"Smalltalk",978		"Snakemake",979		"Stan",980		"Swift",981		"Systemd",982		"Tact",983		"Teal",984		"Tera",985		"Templ",986		"Terraform",987		"TOML",988		"TOON",989		"TTCN-3",990		"Txtar",991		"TypeScript",992		"TypeSpec",993		"Typst",994		"Up",995		"Vala",996		"Vim Script",997		"Web Services Description Language",998		"WebGPU Enhanced Shading Language",999		"WebGPU Shading Language",1000		"wenyan",1001		"Wren",1002		"XHTML",1003		"XMake",1004		"XML Schema",1005		"YAML",1006		"Yarn",1007		"Zen C",1008		"Zig",1009		"ZoKrates",1010		"Zsh",1011	}10121013	output, err := runSCC("-f", "csv", "examples/language")1014	if err != nil {1015		t.Fatal(err)1016	}10171018	for _, language := range languages {1019		if !strings.Contains(output, language+",") {1020			t.Errorf("language not found in output: %v", language)1021		}1022	}1023}10241025// runHelp returns the `scc --help` text. --no-config keeps a developer's1026// ./.sccconfig out of it: runSCC reports combined output, so a warning from a1027// malformed one would be compared against the README as if it were help.1028func runHelp() (string, error) {1029	return runSCC("--help", "--no-config")1030}10311032var (1033	// cpuDefaultFlag matches the four flags defaulting to runtime.NumCPU(),1034	// the only ones whose default depends on the machine the paste was taken1035	// on. Every other default is a literal and stays under comparison.1036	cpuDefaultFlag = regexp.MustCompile(`^\s+--(file-list-job-workers|file-list-queue-size|file-process-job-workers|file-summary-job-queue-size) `)1037	helpDefault    = regexp.MustCompile(`\(default \d+\)`)1038)10391040// normaliseHelp drops the parts of the help output that differ legitimately1041// between machines and releases: the version line and the CPU-count defaults.1042func normaliseHelp(s string) string {1043	var kept []string1044	for _, line := range strings.Split(strings.ReplaceAll(s, "\r\n", "\n"), "\n") {1045		if strings.HasPrefix(line, "Version ") {1046			continue1047		}1048		if cpuDefaultFlag.MatchString(line) {1049			line = helpDefault.ReplaceAllString(line, "(default N)")1050		}1051		kept = append(kept, line)1052	}1053	return strings.TrimSpace(strings.Join(kept, "\n"))1054}10551056// readmeHelpBlock returns the `scc -h` paste, which runs from the line after1057// the prompt down to the fence that closes the block.1058func readmeHelpBlock(t *testing.T, readme string) string {1059	t.Helper()1060	const anchor = "$ scc -h\n"1061	readme = strings.ReplaceAll(readme, "\r\n", "\n")1062	start := strings.Index(readme, anchor)1063	if start == -1 {1064		t.Fatal("README.md has no `$ scc -h` line to anchor the help paste to")1065	}1066	block := readme[start+len(anchor):]1067	end := strings.Index(block, "\n```")1068	if end == -1 {1069		t.Fatal("the `$ scc -h` block in README.md is never closed by a fence")1070	}1071	return block[:end]1072}10731074// TestReadmeHelpMatchesBinary holds the `scc -h` paste against the flags1075// cobra actually registers, so a new or reworded option cannot land with the1076// documented set left behind.1077func TestReadmeHelpMatchesBinary(t *testing.T) {1078	output, err := runHelp()1079	if err != nil {1080		t.Fatal(err)1081	}10821083	readme, err := os.ReadFile("README.md")1084	if err != nil {1085		t.Skipf("README.md is not readable, nothing to compare against: %v", err)1086	}10871088	want := normaliseHelp(output)1089	got := normaliseHelp(readmeHelpBlock(t, string(readme)))1090	if got == want {1091		return1092	}10931094	wantLines := strings.Split(want, "\n")1095	gotLines := strings.Split(got, "\n")1096	for i := 0; i < max(len(wantLines), len(gotLines)); i++ {1097		var w, g string1098		if i < len(wantLines) {1099			w = wantLines[i]1100		}1101		if i < len(gotLines) {1102			g = gotLines[i]1103		}1104		if w == g {1105			continue1106		}1107		t.Errorf("the `scc -h` block in README.md is out of date - paste the current `scc --help` output in under `$ scc -h`\nfirst difference at line %d:\n  README: %s\n  binary: %s", i+1, g, w)1108		return1109	}1110}11111112// reportSkipHelpRe pulls the recognised section names out of the1113// --report-skip line of `scc --help`, and backtickedRe the ones the README1114// table claims.1115var (1116	reportSkipHelpRe = regexp.MustCompile(`--report-skip string\s+[^(\n]+\(([a-z,]+)\)`)1117	backtickedRe     = regexp.MustCompile("`([a-z]+)`")1118)11191120// TestReadmeReportSkipSections holds the --report-skip row of the report1121// options table against the names the flag itself lists, the two having1122// drifted apart once already.1123func TestReadmeReportSkipSections(t *testing.T) {1124	output, err := runHelp()1125	if err != nil {1126		t.Fatal(err)1127	}11281129	readme, err := os.ReadFile("README.md")1130	if err != nil {1131		t.Skipf("README.md is not readable, nothing to compare against: %v", err)1132	}11331134	sections := reportSkipHelpRe.FindStringSubmatch(output)1135	if sections == nil {1136		t.Fatal("no --report-skip section list found in `scc --help`")1137	}11381139	const prefix = "| `--report-skip LIST` |"1140	row := ""1141	for _, line := range strings.Split(string(readme), "\n") {1142		if strings.HasPrefix(line, prefix) {1143			row = line1144			break1145		}1146	}1147	if row == "" {1148		t.Fatalf("README.md has no %q row to check", prefix)1149	}11501151	recognised := map[string]bool{}1152	for _, name := range strings.Split(sections[1], ",") {1153		recognised[name] = true1154		if !strings.Contains(row, "`"+name+"`") {1155			t.Errorf("the --report-skip row in README.md does not list %q", name)1156		}1157	}11581159	cells := strings.Split(row, "|")1160	if len(cells) < 4 {1161		t.Fatalf("could not read a description cell out of %q", row)1162	}1163	for _, listed := range backtickedRe.FindAllStringSubmatch(cells[2], -1) {1164		if !recognised[listed[1]] {1165			t.Errorf("the --report-skip row in README.md lists %q, which the flag does not accept", listed[1])1166		}1167	}1168}

Code quality findings 7

String to byte slice conversion inside loop allocates a new slice each iteration; convert once before the loop
info correctness string-to-byte-in-loop
err := os.WriteFile(flagsFileName, []byte(tc), 0644)
Regexp compiled inside function; compile once at package level to avoid recompilation on each call
info performance regexp-compile-in-func
tabularPattern := regexp.MustCompile(`Processed .+? bytes, .+? megabytes \(SI\)`)
Regexp compiled inside function; compile once at package level to avoid recompilation on each call
info performance regexp-compile-in-func
sqlPattern := regexp.MustCompile(`insert into t values\(.+?\);`)
Regexp compiled inside function; compile once at package level to avoid recompilation on each call
info performance regexp-compile-in-func
cpuDefaultFlag = regexp.MustCompile(`^\s+--(file-list-job-workers|file-list-queue-size|file-process-job-workers|file-summary-job-queue-size) `)
Regexp compiled inside function; compile once at package level to avoid recompilation on each call
info performance regexp-compile-in-func
helpDefault = regexp.MustCompile(`\(default \d+\)`)
Regexp compiled inside function; compile once at package level to avoid recompilation on each call
info performance regexp-compile-in-func
reportSkipHelpRe = regexp.MustCompile(`--report-skip string\s+[^(\n]+\(([a-z,]+)\)`)
Regexp compiled inside function; compile once at package level to avoid recompilation on each call
info performance regexp-compile-in-func
backtickedRe = regexp.MustCompile("`([a-z]+)`")

Get this view in your editor

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

Generated by searchcode.com · powered by code_get_file + code_get_findings