Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion internal/services/covratchet/.covgate
Original file line number Diff line number Diff line change
@@ -1 +1 @@
99.5
95.3
30 changes: 28 additions & 2 deletions internal/services/covratchet/covratchet.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"io"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
Expand Down Expand Up @@ -203,8 +204,33 @@ func readCovgate(path string) string {
return strings.TrimSpace(strings.Split(string(data), "\n")[0])
}

func writeCovgate(path string, coverage float64) error {
func writeCovgate(path string, coverage float64) (err error) {
content := fmt.Sprintf("%.1f\n", coverage)
dir := filepath.Dir(path)
tmpFile, err := os.CreateTemp(dir, ".covgate.tmp-*")
if err != nil {
return fmt.Errorf("create tempfile for %s: %w", path, err)
}
tmpName := tmpFile.Name()
defer func() {
if err != nil {
_ = os.Remove(tmpName)
}
}()
//nolint:gosec // G306: intentional 0644
return os.WriteFile(path, []byte(content), 0o644)
if err = tmpFile.Chmod(0o644); err != nil {
_ = tmpFile.Close()
return fmt.Errorf("chmod tempfile %s: %w", tmpName, err)
}
if _, err = tmpFile.WriteString(content); err != nil {
_ = tmpFile.Close()
return fmt.Errorf("write tempfile %s: %w", tmpName, err)
}
if err = tmpFile.Close(); err != nil {
return fmt.Errorf("close tempfile %s: %w", tmpName, err)
}
if err = os.Rename(tmpName, path); err != nil {
return fmt.Errorf("rename tempfile to %s: %w", path, err)
}
return nil
}
89 changes: 86 additions & 3 deletions internal/services/covratchet/covratchet_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,90 @@ func TestWriteCovgate(t *testing.T) {
}
}

func TestWriteCovgate_NoLeftoverTempfile(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, ".covgate")
if err := writeCovgate(path, 42.5); err != nil {
t.Fatalf("writeCovgate: %v", err)
}
//nolint:gosec // G304: test file read
content, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read .covgate: %v", err)
}
if string(content) != "42.5\n" {
t.Fatalf("got %q, want %q", string(content), "42.5\n")
}
entries, err := os.ReadDir(dir)
if err != nil {
t.Fatalf("read dir: %v", err)
}
for _, e := range entries {
if strings.HasPrefix(e.Name(), ".covgate.tmp-") {
t.Fatalf("tempfile leaked: %s", e.Name())
}
}
}

func TestWriteCovgate_PreservesExistingOnFailure(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, ".covgate")
//nolint:gosec // G306: test file
if err := os.WriteFile(path, []byte("55.0\n"), 0o644); err != nil {
t.Fatalf("seed .covgate: %v", err)
}
//nolint:gosec // G302: test file permissions
if err := os.Chmod(dir, 0o555); err != nil {
t.Fatalf("chmod dir: %v", err)
}
//nolint:gosec // G302: test file permissions
t.Cleanup(func() { _ = os.Chmod(dir, 0o755) })
err := writeCovgate(path, 99.9)
if err == nil {
t.Fatalf("expected error on read-only dir, got nil")
}
if !strings.Contains(err.Error(), "create tempfile") {
t.Errorf("error %q does not mention create tempfile", err)
}
//nolint:gosec // G302: test file permissions
if err := os.Chmod(dir, 0o755); err != nil {
t.Fatalf("restore dir mode: %v", err)
}
//nolint:gosec // G304: test file read
content, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read .covgate: %v", err)
}
if string(content) != "55.0\n" {
t.Fatalf("original .covgate clobbered: got %q", string(content))
}
}

func TestWriteCovgate_RenameFails(t *testing.T) {
parent := t.TempDir()
path := filepath.Join(parent, ".covgate")
//nolint:gosec // G301: test dir
if err := os.Mkdir(path, 0o755); err != nil {
t.Fatalf("mkdir target dir: %v", err)
}
err := writeCovgate(path, 42.0)
if err == nil {
t.Fatalf("expected error renaming over directory, got nil")
}
if !strings.Contains(err.Error(), "rename") {
t.Errorf("error %q does not mention rename", err)
}
entries, readErr := os.ReadDir(parent)
if readErr != nil {
t.Fatalf("read parent dir: %v", readErr)
}
for _, e := range entries {
if strings.HasPrefix(e.Name(), ".covgate.tmp-") {
t.Fatalf("tempfile leaked after failed rename: %s", e.Name())
}
}
}

func TestPrintHeader(t *testing.T) {
var buf bytes.Buffer
printHeader(&buf)
Expand Down Expand Up @@ -217,13 +301,12 @@ func TestRatchetPackage_Up_WriteError(t *testing.T) {
dir := testutil.MakePkgDir(t, pkgRel)
testutil.WriteCovgateFile(t, dir, "70.0\n")

covFile := filepath.Join(dir, ".covgate")
//nolint:gosec // G302: test file permissions
if err := os.Chmod(covFile, 0o444); err != nil {
if err := os.Chmod(dir, 0o555); err != nil {
t.Fatal(err)
}
//nolint:gosec // G302: test file permissions
t.Cleanup(func() { _ = os.Chmod(covFile, 0o644) })
t.Cleanup(func() { _ = os.Chmod(dir, 0o755) })

//nolint:exhaustruct // test uses partial initialization
r := runner{measure: fakeMeasure(85.0)}
Expand Down
Loading