diff --git a/.test-mutations.json b/.test-mutations.json new file mode 100644 index 0000000..bd26bd8 --- /dev/null +++ b/.test-mutations.json @@ -0,0 +1,105 @@ +{ + "version": "1.0", + "test_command": "go test ./...", + "last_updated": "2026-02-15T00:00:00Z", + "modules": { + "internal/sheet/parse.go": { + "status": "completed", + "covering_tests": ["internal/sheet/parse_test.go", "internal/sheet/parse_extended_test.go"], + "last_tested": "2026-02-15T00:00:00Z", + "mutations_applied": 8, + "mutations_killed": 8, + "mutation_score": 100.0, + "notes": "Originally 7/8 (87.5%). Added TestHasMalformedYAML to kill YAML unmarshal error survivor." + }, + "internal/config/validate.go": { + "status": "completed", + "covering_tests": ["internal/config/validate_test.go"], + "last_tested": "2026-02-15T00:00:00Z", + "mutations_applied": 8, + "mutations_killed": 8, + "mutation_score": 100.0, + "notes": "Originally 7/8 (87.5%). Added TestInvalidateInvalidCheatpath to kill cheatpath.Validate() delegation survivor." + }, + "internal/sheets/filter.go": { + "status": "completed", + "covering_tests": ["internal/sheets/filter_test.go"], + "last_tested": "2026-02-15T00:00:00Z", + "mutations_applied": 7, + "mutations_killed": 5, + "mutation_score": 71.4, + "notes": "Survivors relate to UTF-8 condition ordering and OR→AND on dead code path. Not actionable — logically equivalent mutations." + }, + "internal/config/paths.go": { + "status": "completed", + "covering_tests": ["internal/config/paths_test.go"], + "last_tested": "2026-02-15T00:00:00Z", + "mutations_applied": 8, + "mutations_killed": 8, + "mutation_score": 100.0, + "notes": "Perfect score. Excellent existing coverage." + }, + "internal/sheet/colorize.go": { + "status": "completed", + "covering_tests": ["internal/sheet/colorize_test.go"], + "last_tested": "2026-02-15T00:00:00Z", + "mutations_applied": 5, + "mutations_killed": 5, + "mutation_score": 100.0, + "notes": "Originally 2/5 (40%). Added TestColorizeDefaultSyntax and TestColorizeExplicitSyntax. All 5 mutations now killed." + }, + "internal/sheets/consolidate.go": { + "status": "completed", + "covering_tests": ["internal/sheets/consolidate_test.go"], + "last_tested": "2026-02-15T00:00:00Z", + "mutations_applied": 2, + "mutations_killed": 2, + "mutation_score": 100.0, + "notes": "Override semantics well-tested." + }, + "internal/display/indent.go": { + "status": "completed", + "covering_tests": ["internal/display/indent_test.go"], + "last_tested": "2026-02-15T00:00:00Z", + "mutations_applied": 3, + "mutations_killed": 3, + "mutation_score": 100.0, + "notes": "Originally 2/3 (66.7%). Added TestIndentTrimsWhitespace to kill TrimSpace survivor." + }, + "internal/display/faint.go": { + "status": "completed", + "covering_tests": ["internal/display/faint_test.go"], + "last_tested": "2026-02-15T00:00:00Z", + "mutations_applied": 3, + "mutations_killed": 3, + "mutation_score": 100.0, + "notes": "Perfect score." + }, + "internal/sheets/tags.go": { + "status": "completed", + "covering_tests": ["internal/sheets/tags_test.go"], + "last_tested": "2026-02-15T00:00:00Z", + "mutations_applied": 2, + "mutations_killed": 2, + "mutation_score": 100.0, + "notes": "UTF-8 validation and sort order both tested." + }, + "internal/sheet/validate.go": { + "status": "completed", + "covering_tests": ["internal/sheet/validate_test.go"], + "last_tested": "2026-02-15T00:00:00Z", + "mutations_applied": 10, + "mutations_killed": 10, + "mutation_score": 100.0, + "notes": "Perfect score. All security checks well-tested." + } + }, + "global_statistics": { + "total_modules": 10, + "completed_modules": 10, + "total_mutations": 56, + "total_killed": 54, + "total_survived": 2, + "overall_score": 96.4 + } +} diff --git a/internal/config/validate_test.go b/internal/config/validate_test.go index b838772..2f150db 100644 --- a/internal/config/validate_test.go +++ b/internal/config/validate_test.go @@ -157,3 +157,28 @@ func TestInvalidateDuplicateCheatpathPaths(t *testing.T) { t.Errorf("failed to invalidate config with cheatpaths with duplicate paths") } } + +// TestInvalidateInvalidCheatpath asserts that configs containing a cheatpath +// with an empty name are invalidated +func TestInvalidateInvalidCheatpath(t *testing.T) { + + // mock a config with a cheatpath that has an empty name + conf := Config{ + Colorize: true, + Editor: "vim", + Formatter: "terminal16m", + Cheatpaths: []cheatpath.Path{ + cheatpath.Path{ + Name: "", + Path: "/foo", + ReadOnly: false, + Tags: []string{}, + }, + }, + } + + // assert that an error is returned + if err := conf.Validate(); err == nil { + t.Errorf("failed to invalidate config with invalid cheatpath (empty name)") + } +} diff --git a/internal/display/indent_test.go b/internal/display/indent_test.go index 94b1ac9..2d17ef2 100644 --- a/internal/display/indent_test.go +++ b/internal/display/indent_test.go @@ -10,3 +10,13 @@ func TestIndent(t *testing.T) { t.Errorf("failed to indent: want: %s, got: %s", want, got) } } + +// TestIndentTrimsWhitespace asserts that Indent trims leading and trailing +// whitespace before indenting +func TestIndentTrimsWhitespace(t *testing.T) { + got := Indent(" foo\nbar\nbaz \n") + want := "\tfoo\n\tbar\n\tbaz\n" + if got != want { + t.Errorf("failed to trim and indent: want: %q, got: %q", want, got) + } +} diff --git a/internal/sheet/colorize_test.go b/internal/sheet/colorize_test.go index 553a2e7..8b49426 100644 --- a/internal/sheet/colorize_test.go +++ b/internal/sheet/colorize_test.go @@ -40,3 +40,55 @@ func TestColorize(t *testing.T) { t.Errorf("colorized text lost original content: %q", s.Text) } } + +// TestColorizeDefaultSyntax asserts that when no syntax is specified, the +// default ("bash") is used and produces the same output as an explicit "bash" +func TestColorizeDefaultSyntax(t *testing.T) { + + conf := config.Config{ + Formatter: "terminal16m", + Style: "monokai", + } + + // use bash-specific content that tokenizes differently across lexers + code := "if [[ -f /etc/passwd ]]; then\n echo \"found\" | grep -o found\nfi" + + // colorize with empty syntax (should default to "bash") + noSyntax := Sheet{Text: code} + noSyntax.Colorize(conf) + + // colorize with explicit "bash" syntax + bashSyntax := Sheet{Text: code, Syntax: "bash"} + bashSyntax.Colorize(conf) + + // both should produce the same output + if noSyntax.Text != bashSyntax.Text { + t.Errorf( + "default syntax does not match explicit bash:\ndefault: %q\nexplicit: %q", + noSyntax.Text, + bashSyntax.Text, + ) + } +} + +// TestColorizeExplicitSyntax asserts that a specified syntax is used +func TestColorizeExplicitSyntax(t *testing.T) { + + conf := config.Config{ + Formatter: "terminal16m", + Style: "monokai", + } + + // colorize as bash + bashSheet := Sheet{Text: "def hello():\n pass", Syntax: "bash"} + bashSheet.Colorize(conf) + + // colorize as python + pySheet := Sheet{Text: "def hello():\n pass", Syntax: "python"} + pySheet.Colorize(conf) + + // different lexers should produce different output for Python code + if bashSheet.Text == pySheet.Text { + t.Error("bash and python syntax produced identical output") + } +} diff --git a/internal/sheet/parse_test.go b/internal/sheet/parse_test.go index b43f46e..b3f98f0 100644 --- a/internal/sheet/parse_test.go +++ b/internal/sheet/parse_test.go @@ -93,3 +93,20 @@ To foo the bar: baz` t.Errorf("failed to parse text: want: %s, got: %s", markdown, text) } } + +// TestHasMalformedYAML asserts that an error is returned when the frontmatter +// contains invalid YAML that cannot be unmarshalled +func TestHasMalformedYAML(t *testing.T) { + + // stub cheatsheet content with syntactically invalid YAML between the + // delimiters (a bare tab character followed by unquoted colon) + markdown := "---\n\t:\t:\n---\nBody text here" + + // parse the frontmatter + _, _, err := parse(markdown) + + // assert that an error was returned due to YAML unmarshal failure + if err == nil { + t.Error("failed to error on malformed YAML frontmatter") + } +}