Skip to content

Commit 80fae25

Browse files
committed
fix: Increase test coverage
1 parent 6064cd3 commit 80fae25

5 files changed

Lines changed: 150 additions & 1 deletion

File tree

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
# Build command with common flags
44
BUILD_CMD = CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -tags=prod
55
PACKAGE = ./backup/main.go
6-
COVERAGE_THRESHOLD = 90
6+
COVERAGE_THRESHOLD = 98
77

88
.PHONY: build clean test lint tidy checksums release sanity-check check-mod-tidy lint-config-check lint-fix format check-clean check-coverage
99

backup/internal/test/check_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,3 +281,57 @@ func TestListUncoveredPaths_UnreadableDirectory(t *testing.T) {
281281
assert.Equal(t, []string{"/data"}, result)
282282
assert.Contains(t, logBuf.String(), "ADD: Path '/data' is uncovered")
283283
}
284+
285+
// Test that a child path matching a job exclusion is marked as excluded
286+
// (covers isExcluded true + isCoveredByJob excluded log).
287+
func TestListUncoveredPaths_ChildPathExcludedByJob(t *testing.T) {
288+
fs := afero.NewMemMapFs()
289+
_ = fs.MkdirAll("/data/stuff/docs", 0755)
290+
_ = fs.MkdirAll("/data/stuff/cache", 0755)
291+
292+
var logBuf bytes.Buffer
293+
294+
checker := newTestChecker(fs, &logBuf)
295+
296+
cfg := Config{
297+
Sources: []Path{
298+
{Path: "/data/stuff"},
299+
},
300+
Jobs: []Job{
301+
// Source "/data" with exclusion "stuff/cache" so exclusionPath = "/data/stuff/cache"
302+
{Name: "data-backup", Source: "/data", Exclusions: []string{"stuff/cache"}},
303+
// Covers the /data/stuff/docs child directly
304+
{Name: "docs", Source: "/data/stuff/docs"},
305+
},
306+
}
307+
308+
result := checker.ListUncoveredPaths(cfg)
309+
310+
assert.Empty(t, result)
311+
assert.Contains(t, logBuf.String(), "EXCLUDED: Path '/data/stuff/cache' is excluded by job 'data-backup'")
312+
}
313+
314+
// Test that a source path that is globally excluded is skipped in checkPath.
315+
func TestListUncoveredPaths_GloballyExcludedSourceSkipped(t *testing.T) {
316+
fs := afero.NewMemMapFs()
317+
_ = fs.MkdirAll("/data/cache", 0755)
318+
319+
var logBuf bytes.Buffer
320+
321+
checker := newTestChecker(fs, &logBuf)
322+
323+
cfg := Config{
324+
Sources: []Path{
325+
{Path: "/data", Exclusions: []string{"cache"}},
326+
{Path: "/data/cache"},
327+
},
328+
Jobs: []Job{
329+
{Name: "backup", Source: "/data"},
330+
},
331+
}
332+
333+
result := checker.ListUncoveredPaths(cfg)
334+
335+
assert.Empty(t, result)
336+
assert.Contains(t, logBuf.String(), "SKIP: Path '/data/cache' is globally excluded")
337+
}

backup/internal/test/config_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,50 @@ jobs:
452452
assert.Contains(t, err.Error(), "job source path validation failed")
453453
}
454454

455+
func TestLoadResolvedConfig_OverlappingSourcePathsAllowedByExclusion(t *testing.T) {
456+
yaml := `
457+
sources:
458+
- path: "/home"
459+
targets:
460+
- path: "/backup"
461+
jobs:
462+
- name: "parent"
463+
source: "/home/user"
464+
target: "/backup/user"
465+
exclusions:
466+
- "docs"
467+
- name: "child"
468+
source: "/home/user/docs"
469+
target: "/backup/docs"
470+
`
471+
path := writeTestConfig(t, yaml)
472+
473+
cfg, err := LoadResolvedConfig(path)
474+
require.NoError(t, err)
475+
assert.Len(t, cfg.Jobs, 2)
476+
}
477+
478+
func TestLoadResolvedConfig_OverlappingTargetPaths(t *testing.T) {
479+
yaml := `
480+
sources:
481+
- path: "/home"
482+
targets:
483+
- path: "/backup"
484+
jobs:
485+
- name: "job1"
486+
source: "/home/docs"
487+
target: "/backup/all"
488+
- name: "job2"
489+
source: "/home/photos"
490+
target: "/backup/all/photos"
491+
`
492+
path := writeTestConfig(t, yaml)
493+
494+
_, err := LoadResolvedConfig(path)
495+
require.Error(t, err)
496+
assert.Contains(t, err.Error(), "job target path validation failed")
497+
}
498+
455499
func TestLoadResolvedConfig_ValidConfig(t *testing.T) {
456500
yaml := `
457501
sources:

backup/internal/test/helper_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package internal_test
22

33
import (
4+
"os"
45
"testing"
56
"time"
67

@@ -78,3 +79,36 @@ func TestCreateMainLogger_DeterministicLogPath_NoSimulate(t *testing.T) {
7879

7980
assert.Equal(t, "logs/sync-2025-06-15T14-30-45-sync", logPath)
8081
}
82+
83+
func TestCreateMainLogger_MkdirError(t *testing.T) {
84+
// Use t.Chdir to a temp dir so we control the filesystem
85+
tmpDir := t.TempDir()
86+
t.Chdir(tmpDir)
87+
88+
// Create "logs" as a regular file to block MkdirAll
89+
err := os.WriteFile("logs", []byte("block"), 0600)
90+
require.NoError(t, err)
91+
92+
_, _, cleanup, err := CreateMainLogger("test.yaml", false, fixedTime())
93+
_ = cleanup
94+
95+
require.Error(t, err)
96+
assert.Contains(t, err.Error(), "failed to create log directory")
97+
}
98+
99+
func TestCreateMainLogger_OpenFileError(t *testing.T) {
100+
tmpDir := t.TempDir()
101+
t.Chdir(tmpDir)
102+
103+
// Pre-create the log path directory and make summary.log a directory to block OpenFile
104+
logDir := "logs/sync-2025-06-15T14-30-45-test"
105+
106+
err := os.MkdirAll(logDir+"/summary.log", 0750)
107+
require.NoError(t, err)
108+
109+
_, _, cleanup, err := CreateMainLogger("test.yaml", false, fixedTime())
110+
_ = cleanup
111+
112+
require.Error(t, err)
113+
assert.Contains(t, err.Error(), "failed to open overall log file")
114+
}

backup/internal/test/job_test.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import (
55
"testing"
66

77
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
9+
"gopkg.in/yaml.v3"
810
)
911

1012
func newJob() Job {
@@ -56,3 +58,18 @@ func TestApply_JobSucceeds_RunIsCalledAndReturnsSuccess(t *testing.T) {
5658

5759
assert.Equal(t, Success, status)
5860
}
61+
62+
func TestUnmarshalYAML_InvalidNode(t *testing.T) {
63+
// A scalar node cannot be decoded into the JobYAML struct
64+
node := &yaml.Node{
65+
Kind: yaml.ScalarNode,
66+
Value: "not a mapping",
67+
}
68+
69+
var job Job
70+
71+
err := node.Decode(&job)
72+
73+
require.Error(t, err)
74+
assert.Contains(t, err.Error(), "failed to decode YAML node")
75+
}

0 commit comments

Comments
 (0)