fix: cross-platform CI test fixes and parse bug fix

- Add .gitattributes to force LF in mock files (Windows autocrlf)
- Fix parse.go: detect line endings from content instead of runtime.GOOS
- Add fail-fast: false to CI matrix; trigger on all branch pushes
- Skip chmod-based tests on Windows (permissions work differently)
- Use filepath.Join for expected paths in Windows path tests
- Use platform-appropriate invalid paths in error tests
- Add Windows absolute path test case for ValidateSheetName
- Skip Unix-specific integration tests on Windows

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Christopher Allen Lane
2026-02-14 21:31:26 -05:00
parent b604027205
commit 8eafa5adfe
13 changed files with 83 additions and 31 deletions

3
.gitattributes vendored Normal file
View File

@@ -0,0 +1,3 @@
# Force LF line endings for mock/test data files to ensure consistent
# behavior across platforms (Windows git autocrlf converts to CRLF otherwise)
mocks/** text eol=lf

View File

@@ -3,9 +3,6 @@ name: CI
on: on:
push: push:
branches: [master]
pull_request:
branches: [master]
jobs: jobs:
lint: lint:
@@ -26,6 +23,7 @@ jobs:
test: test:
strategy: strategy:
fail-fast: false
matrix: matrix:
os: [ubuntu-latest, macos-latest, windows-latest] os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}

View File

@@ -5,6 +5,7 @@ import (
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"runtime"
"strings" "strings"
"testing" "testing"
) )
@@ -12,6 +13,10 @@ import (
// TestPathTraversalIntegration tests that the cheat binary properly blocks // TestPathTraversalIntegration tests that the cheat binary properly blocks
// path traversal attempts when invoked as a subprocess. // path traversal attempts when invoked as a subprocess.
func TestPathTraversalIntegration(t *testing.T) { func TestPathTraversalIntegration(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("integration test uses Unix-specific env and tools")
}
// Build the cheat binary // Build the cheat binary
binPath := filepath.Join(t.TempDir(), "cheat_test") binPath := filepath.Join(t.TempDir(), "cheat_test")
if output, err := exec.Command("go", "build", "-o", binPath, ".").CombinedOutput(); err != nil { if output, err := exec.Command("go", "build", "-o", binPath, ".").CombinedOutput(); err != nil {
@@ -146,6 +151,10 @@ cheatpaths:
// TestPathTraversalRealWorld tests with more realistic scenarios // TestPathTraversalRealWorld tests with more realistic scenarios
func TestPathTraversalRealWorld(t *testing.T) { func TestPathTraversalRealWorld(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("integration test uses Unix-specific env and tools")
}
// This test ensures our protection works with actual file operations // This test ensures our protection works with actual file operations
// Build cheat // Build cheat

View File

@@ -1,6 +1,7 @@
package cheatpath package cheatpath
import ( import (
"runtime"
"strings" "strings"
"testing" "testing"
) )
@@ -53,9 +54,15 @@ func TestValidateSheetName(t *testing.T) {
errMsg: "'..'", errMsg: "'..'",
}, },
{ {
name: "absolute path", name: "absolute path unix",
input: "/etc/passwd", input: "/etc/passwd",
wantErr: true, wantErr: runtime.GOOS != "windows", // /etc/passwd is not absolute on Windows
errMsg: "absolute",
},
{
name: "absolute path windows",
input: `C:\evil`,
wantErr: runtime.GOOS == "windows", // C:\evil is not absolute on Unix
errMsg: "absolute", errMsg: "absolute",
}, },
{ {

View File

@@ -3,6 +3,7 @@ package config
import ( import (
"os" "os"
"path/filepath" "path/filepath"
"runtime"
"testing" "testing"
"github.com/cheat/cheat/internal/mock" "github.com/cheat/cheat/internal/mock"
@@ -226,6 +227,10 @@ cheatpaths:
// TestConfigGetCwdError tests error handling when os.Getwd fails // TestConfigGetCwdError tests error handling when os.Getwd fails
func TestConfigGetCwdError(t *testing.T) { func TestConfigGetCwdError(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("Windows does not allow removing the current directory")
}
// This is difficult to test without being able to break os.Getwd // This is difficult to test without being able to break os.Getwd
// We'll create a scenario where the current directory is removed // We'll create a scenario where the current directory is removed

View File

@@ -4,6 +4,7 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"reflect" "reflect"
"runtime"
"testing" "testing"
"github.com/davecgh/go-spew/spew" "github.com/davecgh/go-spew/spew"
@@ -78,6 +79,9 @@ func TestConfigFailure(t *testing.T) {
// TestEmptyEditor asserts that envvars are respected if an editor is not // TestEmptyEditor asserts that envvars are respected if an editor is not
// specified in the configs // specified in the configs
func TestEmptyEditor(t *testing.T) { func TestEmptyEditor(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("Editor() returns notepad on Windows before checking env vars")
}
// clear the environment variables // clear the environment variables
os.Setenv("VISUAL", "") os.Setenv("VISUAL", "")

View File

@@ -3,6 +3,7 @@ package config
import ( import (
"os" "os"
"path/filepath" "path/filepath"
"runtime"
"strings" "strings"
"testing" "testing"
) )
@@ -74,12 +75,18 @@ func TestInitCreateDirectory(t *testing.T) {
// TestInitWriteError tests error handling when file write fails // TestInitWriteError tests error handling when file write fails
func TestInitWriteError(t *testing.T) { func TestInitWriteError(t *testing.T) {
// Skip this test if running as root (can write anywhere) // Skip this test if running as root (can write anywhere)
if os.Getuid() == 0 { if runtime.GOOS != "windows" && os.Getuid() == 0 {
t.Skip("Cannot test write errors as root") t.Skip("Cannot test write errors as root")
} }
// Use a platform-appropriate invalid path
invalidPath := "/dev/null/impossible/path/conf.yml"
if runtime.GOOS == "windows" {
invalidPath = `NUL\impossible\path\conf.yml`
}
// Try to write to a read-only directory // Try to write to a read-only directory
err := Init("/dev/null/impossible/path/conf.yml", "test") err := Init(invalidPath, "test")
if err == nil { if err == nil {
t.Error("expected error when writing to invalid path, got nil") t.Error("expected error when writing to invalid path, got nil")
} }

View File

@@ -1,7 +1,9 @@
package config package config
import ( import (
"path/filepath"
"reflect" "reflect"
"runtime"
"testing" "testing"
"github.com/davecgh/go-spew/spew" "github.com/davecgh/go-spew/spew"
@@ -10,6 +12,9 @@ import (
// TestValidatePathsNix asserts that the proper config paths are returned on // TestValidatePathsNix asserts that the proper config paths are returned on
// *nix platforms // *nix platforms
func TestValidatePathsNix(t *testing.T) { func TestValidatePathsNix(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("filepath.Join uses backslashes on Windows")
}
// mock the user's home directory // mock the user's home directory
home := "/home/foo" home := "/home/foo"
@@ -57,6 +62,9 @@ func TestValidatePathsNix(t *testing.T) {
// TestValidatePathsNixNoXDG asserts that the proper config paths are returned // TestValidatePathsNixNoXDG asserts that the proper config paths are returned
// on *nix platforms when `XDG_CONFIG_HOME is not set // on *nix platforms when `XDG_CONFIG_HOME is not set
func TestValidatePathsNixNoXDG(t *testing.T) { func TestValidatePathsNixNoXDG(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("filepath.Join uses backslashes on Windows")
}
// mock the user's home directory // mock the user's home directory
home := "/home/foo" home := "/home/foo"
@@ -106,8 +114,8 @@ func TestValidatePathsWindows(t *testing.T) {
// mock some envvars // mock some envvars
envvars := map[string]string{ envvars := map[string]string{
"APPDATA": "/apps", "APPDATA": filepath.Join("C:", "apps"),
"PROGRAMDATA": "/programs", "PROGRAMDATA": filepath.Join("C:", "programs"),
} }
// get the paths for the platform // get the paths for the platform
@@ -118,8 +126,8 @@ func TestValidatePathsWindows(t *testing.T) {
// specify the expected output // specify the expected output
want := []string{ want := []string{
"/apps/cheat/conf.yml", filepath.Join("C:", "apps", "cheat", "conf.yml"),
"/programs/cheat/conf.yml", filepath.Join("C:", "programs", "cheat", "conf.yml"),
} }
// assert that output matches expectations // assert that output matches expectations

View File

@@ -5,6 +5,7 @@ import (
"io" "io"
"os" "os"
"path/filepath" "path/filepath"
"runtime"
"strings" "strings"
"testing" "testing"
) )
@@ -71,21 +72,29 @@ cheatpaths:
{ {
name: "invalid config path", name: "invalid config path",
configs: "test", configs: "test",
confpath: "/nonexistent/path/conf.yml", // /dev/null/... is truly uncreatable on Unix;
// NUL\... is uncreatable on Windows
confpath: func() string {
if runtime.GOOS == "windows" {
return `NUL\impossible\conf.yml`
}
return "/dev/null/impossible/conf.yml"
}(),
userInput: "n\n", userInput: "n\n",
wantErr: true, wantErr: true,
wantInErr: "failed to create config file", wantInErr: "failed to create",
}, },
} }
// Pre-create a non-empty community dir so PlainClone fails reliably // Pre-create a .git dir inside the community path so go-git's PlainClone
// (otherwise, on CI runners with network access, the clone succeeds) // returns ErrRepositoryAlreadyExists (otherwise, on CI runners with
cloneBlocker := filepath.Join(tempDir, "conf2", "cheatsheets", "community") // network access, the real clone succeeds and the test fails)
if err := os.MkdirAll(cloneBlocker, 0755); err != nil { fakeGitDir := filepath.Join(tempDir, "conf2", "cheatsheets", "community", ".git")
t.Fatalf("failed to create clone blocker dir: %v", err) if err := os.MkdirAll(fakeGitDir, 0755); err != nil {
t.Fatalf("failed to create fake .git dir: %v", err)
} }
if err := os.WriteFile(filepath.Join(cloneBlocker, ".gitkeep"), []byte(""), 0644); err != nil { if err := os.WriteFile(filepath.Join(fakeGitDir, "HEAD"), []byte("ref: refs/heads/main\n"), 0644); err != nil {
t.Fatalf("failed to write clone blocker file: %v", err) t.Fatalf("failed to write fake HEAD: %v", err)
} }
for _, tt := range tests { for _, tt := range tests {

View File

@@ -3,6 +3,7 @@ package repo
import ( import (
"os" "os"
"path/filepath" "path/filepath"
"runtime"
"testing" "testing"
) )
@@ -12,6 +13,9 @@ func TestClone(t *testing.T) {
// that don't require actual cloning // that don't require actual cloning
t.Run("clone to read-only directory", func(t *testing.T) { t.Run("clone to read-only directory", func(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("chmod does not restrict writes on Windows")
}
if os.Getuid() == 0 { if os.Getuid() == 0 {
t.Skip("Cannot test read-only directory as root") t.Skip("Cannot test read-only directory as root")
} }

View File

@@ -3,6 +3,7 @@ package sheet
import ( import (
"os" "os"
"path/filepath" "path/filepath"
"runtime"
"testing" "testing"
) )
@@ -130,6 +131,10 @@ func TestCopyIOError(t *testing.T) {
// TestCopyCleanupOnError verifies that partially written files are cleaned up on error // TestCopyCleanupOnError verifies that partially written files are cleaned up on error
func TestCopyCleanupOnError(t *testing.T) { func TestCopyCleanupOnError(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("chmod does not restrict reads on Windows")
}
// Create a source file that we'll make unreadable after opening // Create a source file that we'll make unreadable after opening
src, err := os.CreateTemp("", "copy-test-cleanup-*") src, err := os.CreateTemp("", "copy-test-cleanup-*")
if err != nil { if err != nil {

View File

@@ -2,7 +2,6 @@ package sheet
import ( import (
"fmt" "fmt"
"runtime"
"strings" "strings"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
@@ -11,9 +10,9 @@ import (
// Parse parses cheatsheet frontmatter // Parse parses cheatsheet frontmatter
func parse(markdown string) (frontmatter, string, error) { func parse(markdown string) (frontmatter, string, error) {
// determine the appropriate line-break for the platform // detect the line-break style used in the content
linebreak := "\n" linebreak := "\n"
if runtime.GOOS == "windows" { if strings.Contains(markdown, "\r\n") {
linebreak = "\r\n" linebreak = "\r\n"
} }

View File

@@ -1,17 +1,11 @@
package sheet package sheet
import ( import (
"runtime"
"testing" "testing"
) )
// TestParseWindowsLineEndings tests parsing with Windows line endings // TestParseWindowsLineEndings tests parsing with Windows line endings
func TestParseWindowsLineEndings(t *testing.T) { func TestParseWindowsLineEndings(t *testing.T) {
// Only test Windows line endings on Windows
if runtime.GOOS != "windows" {
t.Skip("Skipping Windows line ending test on non-Windows platform")
}
// stub our cheatsheet content with Windows line endings // stub our cheatsheet content with Windows line endings
markdown := "---\r\nsyntax: go\r\ntags: [ test ]\r\n---\r\nTo foo the bar: baz" markdown := "---\r\nsyntax: go\r\ntags: [ test ]\r\n---\r\nTo foo the bar: baz"