Repository: kopia/kopia
Pull request #4362 (2025-01-23 00:35:02)

build(deps): bump codecov/codecov-action to 5.2.0
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5.1.2 to 5.2.0.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v5.1.2...5a605bd92782ce0810fa3b8acc235c921b497052)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
- dependency-type: direct:production
-  update-type: version-update:semver-major ...

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/kopia/kopia/pull/4362?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 76.31%. Comparing base [(`cb455c6`)](https://app.codecov.io/gh/kopia/kopia/commit/cb455c699a04e54cb47e25a676d7d65063ce883e?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia) to head [(`440180f`)](https://app.codecov.io/gh/kopia/kopia/commit/440180ffdafbacae865247d6e6f60344a5d53dea?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia).
> Report is 400 commits behind head on master.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #4362      +/-   ##
==========================================
+ Coverage   75.86%   76.31%   +0.44%     
==========================================
  Files         470      527      +57     
  Lines       37301    39920    +2619     
==========================================
+ Hits        28299    30463    +2164     
- Misses       7071     7451     +380     
- Partials     1931     2006      +75     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/kopia/kopia/pull/4362?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia).


Patch:
diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml
index c88cbd4c..fff04b0e 100644
--- a/.github/workflows/code-coverage.yml
+++ b/.github/workflows/code-coverage.yml
@@ -24,7 +24,7 @@ jobs:
     - name: Run Tests
       run: make test-with-coverage
     - name: Upload Coverage
-      uses: codecov/codecov-action@1e68e06f1dbfde0e4cefc87efeba9e4643565303 # v5.1.2
+      uses: codecov/codecov-action@5a605bd92782ce0810fa3b8acc235c921b497052 # v5.2.0
       with:
         files: coverage.txt
     - name: Upload Logs
diff --git a/.github/workflows/make.yml b/.github/workflows/make.yml
index cef35301..dc40f185 100644
--- a/.github/workflows/make.yml
+++ b/.github/workflows/make.yml
@@ -141,7 +141,7 @@ jobs:
     steps:
     - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
     - name: Set up QEMU
-      uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf # v3.2.0
+      uses: docker/setup-qemu-action@53851d14592bedcffcf25ea515637cff71ef929a # v3.3.0
     - name: Set up Docker Buildx
       uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 # v3.8.0
     - name: Install Linux-specific packages
diff --git a/go.mod b/go.mod
index eef1da8e..771f4bf7 100644
--- a/go.mod
+++ b/go.mod
@@ -8,7 +8,7 @@ require (
 	cloud.google.com/go/storage v1.50.0
 	github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0
 	github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.1
-	github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.5.0
+	github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.0
 	github.com/alecthomas/kingpin/v2 v2.4.0
 	github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137
 	github.com/chmduquesne/rollinghash v4.0.0+incompatible
diff --git a/go.sum b/go.sum
index 545dc3b8..c4872827 100644
--- a/go.sum
+++ b/go.sum
@@ -33,8 +33,8 @@ github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xP
 github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY=
 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.6.0 h1:PiSrjRPpkQNjrM8H0WwKMnZUdu1RGMtd/LdGKUrOo+c=
 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.6.0/go.mod h1:oDrbWx4ewMylP7xHivfgixbfGBT6APAwsSoHRKotnIc=
-github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.5.0 h1:mlmW46Q0B79I+Aj4azKC6xDMFN9a9SyZWESlGWYXbFs=
-github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.5.0/go.mod h1:PXe2h+LKcWTX9afWdZoHyODqR4fBa5boUM/8uJfZ0Jo=
+github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.0 h1:UXT0o77lXQrikd1kgwIPQOUect7EoR/+sbP4wQKdzxM=
+github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.0/go.mod h1:cTvi54pg19DoT07ekoeMgE/taAwNtCShVeZqA+Iv2xI=
 github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM=
 github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE=
 github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2 h1:kYRSnvJju5gYVyhkij+RTJ/VR6QIUaCfWeaFm2ycsjQ=
diff --git a/internal/repodiag/log_manager.go b/internal/repodiag/log_manager.go
index 2ee78485..c05f260c 100644
--- a/internal/repodiag/log_manager.go
+++ b/internal/repodiag/log_manager.go
@@ -78,7 +78,7 @@ func (m *LogManager) Enable() {
 // NewLogManager creates a new LogManager that will emit logs as repository blobs.
 func NewLogManager(ctx context.Context, w *BlobWriter) *LogManager {
 	return &LogManager{
-		ctx:            ctx,
+		ctx:            context.WithoutCancel(ctx),
 		writer:         w,
 		flushThreshold: blobLoggerFlushThreshold,
 		timeFunc:       clock.Now,


Test patch:
diff --git a/internal/blobtesting/map.go b/internal/blobtesting/map.go
index b5f4ccba..db06376b 100644
--- a/internal/blobtesting/map.go
+++ b/internal/blobtesting/map.go
@@ -33,6 +33,10 @@ type mapStorage struct {
 }
 
 func (s *mapStorage) GetCapacity(ctx context.Context) (blob.Capacity, error) {
+	if err := ctx.Err(); err != nil {
+		return blob.Capacity{}, errors.Wrap(err, "get capacity failed")
+	}
+
 	if s.limit < 0 {
 		return blob.Capacity{}, blob.ErrNotAVolume
 	}
@@ -47,6 +51,10 @@ func (s *mapStorage) GetCapacity(ctx context.Context) (blob.Capacity, error) {
 }
 
 func (s *mapStorage) GetBlob(ctx context.Context, id blob.ID, offset, length int64, output blob.OutputBuffer) error {
+	if err := ctx.Err(); err != nil {
+		return errors.Wrap(err, "get blob failed")
+	}
+
 	s.mutex.RLock()
 	defer s.mutex.RUnlock()
 
@@ -82,6 +90,10 @@ func (s *mapStorage) GetBlob(ctx context.Context, id blob.ID, offset, length int
 }
 
 func (s *mapStorage) GetMetadata(ctx context.Context, id blob.ID) (blob.Metadata, error) {
+	if err := ctx.Err(); err != nil {
+		return blob.Metadata{}, errors.Wrap(err, "get metadata failed")
+	}
+
 	s.mutex.RLock()
 	defer s.mutex.RUnlock()
 
@@ -98,6 +110,10 @@ func (s *mapStorage) GetMetadata(ctx context.Context, id blob.ID) (blob.Metadata
 }
 
 func (s *mapStorage) PutBlob(ctx context.Context, id blob.ID, data blob.Bytes, opts blob.PutOptions) error {
+	if err := ctx.Err(); err != nil {
+		return errors.Wrap(err, "pub blob failed")
+	}
+
 	switch {
 	case opts.HasRetentionOptions():
 		return errors.Wrap(blob.ErrUnsupportedPutBlobOption, "blob-retention")
@@ -134,6 +150,10 @@ func (s *mapStorage) PutBlob(ctx context.Context, id blob.ID, data blob.Bytes, o
 }
 
 func (s *mapStorage) DeleteBlob(ctx context.Context, id blob.ID) error {
+	if err := ctx.Err(); err != nil {
+		return errors.Wrap(err, "delete blob failed")
+	}
+
 	s.mutex.Lock()
 	defer s.mutex.Unlock()
 
@@ -145,6 +165,10 @@ func (s *mapStorage) DeleteBlob(ctx context.Context, id blob.ID) error {
 }
 
 func (s *mapStorage) ListBlobs(ctx context.Context, prefix blob.ID, callback func(blob.Metadata) error) error {
+	if err := ctx.Err(); err != nil {
+		return errors.Wrap(err, "list blobs failed")
+	}
+
 	s.mutex.RLock()
 
 	keys := []blob.ID{}
@@ -184,6 +208,10 @@ func (s *mapStorage) ListBlobs(ctx context.Context, prefix blob.ID, callback fun
 }
 
 func (s *mapStorage) TouchBlob(ctx context.Context, blobID blob.ID, threshold time.Duration) (time.Time, error) {
+	if err := ctx.Err(); err != nil {
+		return time.Time{}, errors.Wrap(err, "touch blob failed")
+	}
+
 	s.mutex.Lock()
 	defer s.mutex.Unlock()
 
diff --git a/internal/repodiag/log_manager_test.go b/internal/repodiag/log_manager_test.go
index 47530fbd..4cf79719 100644
--- a/internal/repodiag/log_manager_test.go
+++ b/internal/repodiag/log_manager_test.go
@@ -1,6 +1,7 @@
 package repodiag_test
 
 import (
+	"context"
 	"crypto/rand"
 	"encoding/hex"
 	"strings"
@@ -84,6 +85,30 @@ func TestLogManager_NotEnabled(t *testing.T) {
 	require.Empty(t, d)
 }
 
+func TestLogManager_CancelledContext(t *testing.T) {
+	d := blobtesting.DataMap{}
+	st := blobtesting.NewMapStorage(d, nil, nil)
+	w := repodiag.NewWriter(st, newStaticCrypter(t))
+	ctx := testlogging.Context(t)
+	cctx, cancel := context.WithCancel(ctx)
+	lm := repodiag.NewLogManager(cctx, w)
+
+	// cancel context, logs should still be written
+	cancel()
+
+	lm.Enable()
+	l := lm.NewLogger()
+	l.Info("hello")
+
+	require.Empty(t, d)
+
+	l.Sync()
+	w.Wait(ctx)
+
+	// make sure log messages are written
+	require.Len(t, d, 1)
+}
+
 func TestLogManager_Null(t *testing.T) {
 	var lm *repodiag.LogManager
 
diff --git a/tests/end_to_end_test/server_repo_logs_test.go b/tests/end_to_end_test/server_repo_logs_test.go
new file mode 100644
index 00000000..163b7416
--- /dev/null
+++ b/tests/end_to_end_test/server_repo_logs_test.go
@@ -0,0 +1,108 @@
+package endtoend_test
+
+import (
+	"net/http"
+	"testing"
+	"time"
+
+	"github.com/pkg/errors"
+	"github.com/stretchr/testify/require"
+
+	"github.com/kopia/kopia/internal/apiclient"
+	"github.com/kopia/kopia/internal/serverapi"
+	"github.com/kopia/kopia/internal/testlogging"
+	"github.com/kopia/kopia/internal/testutil"
+	"github.com/kopia/kopia/snapshot/policy"
+	"github.com/kopia/kopia/tests/testenv"
+)
+
+// Verify that the "diagnostic/log" blobs are uploaded to the  repository when
+// the server exits.
+// Approach / steps:
+// - initialize a repo, note this uploads logs to the repo
+// - start the server
+// - create a "snapshot source" on the server via the server-control API
+// - remove all log blobs from the repo and check that there are 0.
+// - stop the server
+// - check whether or not the server uploaded the logs.
+func TestServerRepoLogsUploadedOnShutdown(t *testing.T) {
+	t.Parallel()
+
+	ctx := testlogging.Context(t)
+
+	runner := testenv.NewInProcRunner(t)
+	e := testenv.NewCLITest(t, testenv.RepoFormatNotImportant, runner)
+
+	e.RunAndExpectSuccess(t, "repo", "create", "filesystem", "--path", e.RepoDir, "--override-hostname=fake-hostname", "--override-username=fake-username")
+	defer e.RunAndExpectSuccess(t, "repo", "disconnect")
+
+	logs := e.RunAndExpectSuccess(t, "logs", "list")
+	require.Len(t, logs, 1, "repo create did not upload logs")
+
+	var sp testutil.ServerParameters
+
+	wait, _ := e.RunAndProcessStderr(t, sp.ProcessOutput,
+		"server", "start",
+		"--address=localhost:0",
+		"--insecure",
+		"--without-password",
+		"--tls-generate-rsa-key-size=2048", // use shorter key size to speed up generation,
+	)
+
+	require.NotEmpty(t, sp.BaseURL, "server base URL")
+
+	controlCli, err := apiclient.NewKopiaAPIClient(apiclient.Options{
+		BaseURL:  sp.BaseURL,
+		Username: defaultServerControlUsername,
+		Password: sp.ServerControlPassword,
+	})
+	require.NoError(t, err)
+
+	checkServerStartedOrFailed := func() bool {
+		var hs apiclient.HTTPStatusError
+
+		_, err := serverapi.Status(ctx, controlCli)
+
+		if errors.As(err, &hs) {
+			switch hs.HTTPStatusCode {
+			case http.StatusBadRequest:
+				return false
+			case http.StatusForbidden:
+				return false
+			}
+		}
+
+		return true
+	}
+
+	require.Eventually(t, checkServerStartedOrFailed, 10*time.Second, 100*time.Millisecond)
+	require.NoError(t, controlCli.FetchCSRFTokenForTesting(ctx))
+
+	keepDaily := policy.OptionalInt(3)
+
+	_, err = serverapi.CreateSnapshotSource(ctx, controlCli, &serverapi.CreateSnapshotSourceRequest{
+		Path: sharedTestDataDir1,
+		Policy: &policy.Policy{
+			RetentionPolicy: policy.RetentionPolicy{
+				KeepDaily: &keepDaily,
+			},
+		},
+		CreateSnapshot: false,
+	})
+
+	require.NoError(t, err)
+
+	lines := e.RunAndExpectSuccess(t, "server", "status", "--address", sp.BaseURL, "--server-control-password", sp.ServerControlPassword)
+	t.Logf("lines: %v", lines)
+
+	e.RunAndExpectSuccess(t, "logs", "cleanup", "--max-age=1ns")
+	logs = e.RunAndExpectSuccess(t, "logs", "list")
+	require.Empty(t, logs, "new logs were uploaded unexpectedly:", logs)
+
+	require.NoError(t, serverapi.Shutdown(ctx, controlCli))
+	require.NoError(t, wait())
+
+	logs = e.RunAndExpectSuccess(t, "logs", "list")
+
+	require.NotEmpty(t, logs, "server logs were not uploaded")
+}