Skip to content

Commit ec3ab63

Browse files
committed
ci: add comprehensive unit test coverage for contrib components
Add extensive unit tests for nydusify and nydus-overlayfs contrib packages to improve code coverage and reliability. The new tests cover builder operations, workflow management, checker rules, and various utility functions. Introduce a new Makefile target `contrib-ut-coverage` that runs all Go unit tests in contrib directories with coverage collection. Coverage profiles are merged into a combined report for easy analysis. The coverage target runs tests with atomic coverage mode and 20-minute timeout, supporting parallel execution for faster CI feedback. Signed-off-by: Peng Tao <bergwolf@hyper.sh>
1 parent d4baa7b commit ec3ab63

40 files changed

+11628
-274
lines changed

Makefile

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,42 @@ coverage-codecov:
161161
TEST_WORKDIR_PREFIX=$(TEST_WORKDIR_PREFIX) ${RUSTUP} run stable cargo llvm-cov --codecov --output-path codecov.json --workspace $(EXCLUDE_PACKAGES) $(CARGO_COMMON) $(CARGO_BUILD_FLAGS) -- --skip integration --nocapture --test-threads=8
162162

163163

164+
CONTRIB_GO_COV_DIR := $(PWD)/coverage/contrib-go
165+
166+
# Run all golang unit tests in contrib directory and collect code coverage.
167+
# Coverage profiles are written to coverage/contrib-go/ and merged into combined.cov.
168+
.PHONY: contrib-ut-coverage
169+
contrib-ut-coverage:
170+
@mkdir -p $(CONTRIB_GO_COV_DIR)
171+
@echo "Running Go unit tests with coverage for contrib/nydusify..."
172+
@cd $(NYDUSIFY_PATH) && go test -buildvcs=false -gcflags=all=-l -covermode=atomic \
173+
-coverprofile=$(CONTRIB_GO_COV_DIR)/nydusify.cov \
174+
-count=1 -v -timeout 20m -parallel 16 \
175+
$$(go list -buildvcs=false ./... | grep -v /vendor/)
176+
@echo "Running Go unit tests with coverage for contrib/nydus-overlayfs..."
177+
@cd ${NYDUS-OVERLAYFS_PATH} && go test -buildvcs=false -gcflags=all=-l -covermode=atomic \
178+
-coverprofile=$(CONTRIB_GO_COV_DIR)/nydus-overlayfs.cov \
179+
-count=1 -v -timeout 20m -parallel 16 \
180+
$$(go list -buildvcs=false ./... | grep -v /vendor/)
181+
@echo "Merging coverage profiles..."
182+
@echo "mode: atomic" > $(CONTRIB_GO_COV_DIR)/combined.cov
183+
@tail -n +2 $(CONTRIB_GO_COV_DIR)/nydusify.cov >> $(CONTRIB_GO_COV_DIR)/combined.cov 2>/dev/null || true
184+
@tail -n +2 $(CONTRIB_GO_COV_DIR)/nydus-overlayfs.cov >> $(CONTRIB_GO_COV_DIR)/combined.cov 2>/dev/null || true
185+
@echo "Coverage summary:"
186+
@go tool cover -func=$(CONTRIB_GO_COV_DIR)/combined.cov | tail -1
187+
@echo "Generating coverage markdown report..."
188+
@echo "# Go Unit Test Coverage Report" > $(CONTRIB_GO_COV_DIR)/coverage.md
189+
@echo "" >> $(CONTRIB_GO_COV_DIR)/coverage.md
190+
@echo "| File | Function | Coverage |" >> $(CONTRIB_GO_COV_DIR)/coverage.md
191+
@echo "|------|----------|----------|" >> $(CONTRIB_GO_COV_DIR)/coverage.md
192+
@go tool cover -func=$(CONTRIB_GO_COV_DIR)/combined.cov | \
193+
sed 's/\t\t*/\t/g' | \
194+
awk -F'\t' '{printf "| %s | %s | %s |\n", $$1, $$2, $$3}' \
195+
>> $(CONTRIB_GO_COV_DIR)/coverage.md
196+
@echo "" >> $(CONTRIB_GO_COV_DIR)/coverage.md
197+
@echo "Full coverage profile: $(CONTRIB_GO_COV_DIR)/combined.cov"
198+
@echo "Coverage markdown report: $(CONTRIB_GO_COV_DIR)/coverage.md"
199+
164200
contrib-build: nydusify nydus-overlayfs
165201

166202
contrib-release: nydusify-release nydus-overlayfs-release

contrib/nydusify/cmd/nydusify_test.go

Lines changed: 52 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ import (
1010
"flag"
1111
"fmt"
1212
"os"
13+
"path/filepath"
1314
"testing"
1415

15-
"github.com/agiledragon/gomonkey/v2"
1616
"github.com/sirupsen/logrus"
1717
"github.com/stretchr/testify/assert"
1818
"github.com/stretchr/testify/require"
@@ -350,46 +350,66 @@ func TestGetGlobalFlags(t *testing.T) {
350350
require.Equal(t, 3, len(flags))
351351
}
352352

353-
func TestSetupLogLevelWithLogFile(t *testing.T) {
354-
logFilePath := "test_log_file.log"
355-
defer os.Remove(logFilePath)
353+
// TestSetupLogLevel tests setupLogLevel using real cli.Context objects
354+
// with proper flag sets, avoiding gomonkey patches that are unreliable
355+
// on ARM64.
356+
func TestSetupLogLevel(t *testing.T) {
357+
originalLevel := logrus.GetLevel()
358+
originalOutput := logrus.StandardLogger().Out
359+
defer func() {
360+
logrus.SetLevel(originalLevel)
361+
logrus.SetOutput(originalOutput)
362+
}()
356363

357-
c := &cli.Context{}
364+
t.Run("with log file", func(t *testing.T) {
365+
logFilePath := filepath.Join(t.TempDir(), "test_log_file.log")
358366

359-
patches := gomonkey.ApplyMethodSeq(c, "String", []gomonkey.OutputCell{
360-
{Values: []interface{}{"info"}, Times: 1},
361-
{Values: []interface{}{"test_log_file.log"}, Times: 2},
362-
})
363-
defer patches.Reset()
364-
setupLogLevel(c)
367+
app := &cli.App{
368+
Flags: []cli.Flag{
369+
&cli.BoolFlag{Name: "D", Value: false},
370+
&cli.StringFlag{Name: "log-level", Value: "info"},
371+
&cli.StringFlag{Name: "log-file", Value: ""},
372+
},
373+
}
374+
flagSet := flag.NewFlagSet("log-test", flag.PanicOnError)
375+
flagSet.String("log-level", "info", "")
376+
flagSet.String("log-file", logFilePath, "")
377+
ctx := cli.NewContext(app, flagSet, nil)
365378

366-
file, err := os.Open(logFilePath)
367-
assert.NoError(t, err)
368-
assert.NotNil(t, file)
369-
file.Close()
379+
setupLogLevel(ctx)
370380

371-
logrusOutput := logrus.StandardLogger().Out
372-
assert.NotNil(t, logrusOutput)
381+
file, err := os.Open(logFilePath)
382+
assert.NoError(t, err)
383+
assert.NotNil(t, file)
384+
file.Close()
373385

374-
logrus.Info("This is a test log message")
375-
content, err := os.ReadFile(logFilePath)
376-
assert.NoError(t, err)
377-
assert.Contains(t, string(content), "This is a test log message")
378-
}
386+
logrusOutput := logrus.StandardLogger().Out
387+
assert.NotNil(t, logrusOutput)
379388

380-
func TestSetupLogLevelWithInvalidLogFile(t *testing.T) {
389+
logrus.Info("This is a test log message")
390+
content, err := os.ReadFile(logFilePath)
391+
assert.NoError(t, err)
392+
assert.Contains(t, string(content), "This is a test log message")
393+
})
381394

382-
c := &cli.Context{}
395+
t.Run("with invalid log file", func(t *testing.T) {
396+
app := &cli.App{
397+
Flags: []cli.Flag{
398+
&cli.BoolFlag{Name: "D", Value: false},
399+
&cli.StringFlag{Name: "log-level", Value: "info"},
400+
&cli.StringFlag{Name: "log-file", Value: ""},
401+
},
402+
}
403+
flagSet := flag.NewFlagSet("log-test-invalid", flag.PanicOnError)
404+
flagSet.String("log-level", "info", "")
405+
flagSet.String("log-file", "test/test_log_file.log", "")
406+
ctx := cli.NewContext(app, flagSet, nil)
383407

384-
patches := gomonkey.ApplyMethodSeq(c, "String", []gomonkey.OutputCell{
385-
{Values: []interface{}{"info"}, Times: 1},
386-
{Values: []interface{}{"test/test_log_file.log"}, Times: 2},
387-
})
388-
defer patches.Reset()
389-
setupLogLevel(c)
408+
setupLogLevel(ctx)
390409

391-
logrusOutput := logrus.StandardLogger().Out
392-
assert.NotNil(t, logrusOutput)
410+
logrusOutput := logrus.StandardLogger().Out
411+
assert.NotNil(t, logrusOutput)
412+
})
393413
}
394414

395415
func TestValidateSourceAndTargetArchives(t *testing.T) {

contrib/nydusify/pkg/build/builder_test.go

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
package build
66

77
import (
8+
"bytes"
9+
"os"
810
"testing"
911

1012
"github.com/stretchr/testify/require"
@@ -203,3 +205,119 @@ func TestNewBuilderDifferentPaths(t *testing.T) {
203205
})
204206
}
205207
}
208+
209+
func TestRunBuildsCorrectArgsNoParent(t *testing.T) {
210+
builder := NewBuilder("/nonexistent/binary")
211+
// No parent bootstrap - should use "create" without --parent-bootstrap
212+
err := builder.Run(BuilderOption{
213+
BootstrapPath: "/tmp/bootstrap",
214+
RootfsPath: "/tmp/rootfs",
215+
WhiteoutSpec: "oci",
216+
OutputJSONPath: "/tmp/output.json",
217+
BlobPath: "/tmp/blob",
218+
FsVersion: "6",
219+
})
220+
require.Error(t, err)
221+
}
222+
223+
func TestRunBuildsCorrectArgsWithAllFlags(t *testing.T) {
224+
builder := NewBuilder("/nonexistent/binary")
225+
err := builder.Run(BuilderOption{
226+
ParentBootstrapPath: "/tmp/parent",
227+
BootstrapPath: "/tmp/bootstrap",
228+
RootfsPath: "/tmp/rootfs",
229+
WhiteoutSpec: "oci",
230+
OutputJSONPath: "/tmp/output.json",
231+
BlobPath: "/tmp/blob",
232+
FsVersion: "6",
233+
AlignedChunk: true,
234+
ChunkDict: "/tmp/chunkdict",
235+
Compressor: "lz4_block",
236+
PrefetchPatterns: "/bin /lib",
237+
ChunkSize: "0x200000",
238+
})
239+
require.Error(t, err)
240+
}
241+
242+
func TestCompactNoOptional(t *testing.T) {
243+
builder := NewBuilder("/nonexistent/binary")
244+
err := builder.Compact(CompactOption{
245+
BootstrapPath: "/tmp/bootstrap",
246+
BlobsDir: "/tmp/blobs",
247+
MinUsedRatio: "10",
248+
CompactBlobSize: "20971520",
249+
MaxCompactSize: "209715200",
250+
LayersToCompact: "64",
251+
BackendType: "oss",
252+
BackendConfigPath: "/tmp/backend.json",
253+
OutputJSONPath: "/tmp/output.json",
254+
})
255+
require.Error(t, err)
256+
}
257+
258+
func TestGenerateEmptyBootstraps(t *testing.T) {
259+
builder := NewBuilder("/nonexistent/binary")
260+
err := builder.Generate(GenerateOption{
261+
BootstrapPaths: []string{},
262+
DatabasePath: "/tmp/db",
263+
ChunkdictBootstrapPath: "/tmp/chunkdict",
264+
OutputPath: "/tmp/output",
265+
})
266+
require.Error(t, err)
267+
}
268+
269+
func TestBuilderCustomStdoutStderr(t *testing.T) {
270+
builder := NewBuilder("/usr/bin/nydus-image")
271+
require.Equal(t, os.Stdout, builder.stdout)
272+
require.Equal(t, os.Stderr, builder.stderr)
273+
274+
// Verify builder can be used with custom writers
275+
var buf bytes.Buffer
276+
builder.stdout = &buf
277+
builder.stderr = &buf
278+
require.Equal(t, &buf, builder.stdout)
279+
require.Equal(t, &buf, builder.stderr)
280+
}
281+
282+
func TestRunPrefetchPatternsNotEmpty(t *testing.T) {
283+
builder := NewBuilder("/nonexistent/binary")
284+
// When PrefetchPatterns is set, --prefetch-policy fs should be added
285+
err := builder.Run(BuilderOption{
286+
BootstrapPath: "/tmp/bootstrap",
287+
RootfsPath: "/tmp/rootfs",
288+
WhiteoutSpec: "oci",
289+
OutputJSONPath: "/tmp/output.json",
290+
BlobPath: "/tmp/blob",
291+
FsVersion: "5",
292+
PrefetchPatterns: "/",
293+
})
294+
require.Error(t, err)
295+
}
296+
297+
func TestRunEmptyCompressor(t *testing.T) {
298+
builder := NewBuilder("/nonexistent/binary")
299+
err := builder.Run(BuilderOption{
300+
BootstrapPath: "/tmp/bootstrap",
301+
RootfsPath: "/tmp/rootfs",
302+
WhiteoutSpec: "oci",
303+
OutputJSONPath: "/tmp/output.json",
304+
BlobPath: "/tmp/blob",
305+
FsVersion: "6",
306+
Compressor: "",
307+
})
308+
require.Error(t, err)
309+
}
310+
311+
func TestRunEmptyChunkSize(t *testing.T) {
312+
builder := NewBuilder("/nonexistent/binary")
313+
err := builder.Run(BuilderOption{
314+
BootstrapPath: "/tmp/bootstrap",
315+
RootfsPath: "/tmp/rootfs",
316+
WhiteoutSpec: "oci",
317+
OutputJSONPath: "/tmp/output.json",
318+
BlobPath: "/tmp/blob",
319+
FsVersion: "6",
320+
ChunkSize: "",
321+
})
322+
require.Error(t, err)
323+
}

0 commit comments

Comments
 (0)