Repository: kopia/kopia
Pull request #2707 (2023-01-23 23:04:50)

build(deps): bump github.com/klauspost/reedsolomon from 1.11.3 to 1.11.5
Bumps [github.com/klauspost/reedsolomon](https://github.com/klauspost/reedsolomon) from 1.11.3 to 1.11.5.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/klauspost/reedsolomon/releases">github.com/klauspost/reedsolomon's releases</a>.</em></p>
<blockquote>
<h2>v1.11.5</h2>
<h2>What's Changed</h2>
<ul>
<li>zero memory padding on stream split, fix potential OOM issues. by <a href="https://github.com/0xbillw"><code>@​0xbillw</code></a> in <a href="https://github-redirect.dependabot.com/klauspost/reedsolomon/pull/233">klauspost/reedsolomon#233</a></li>
<li>Fix Split function with 0 erasure by <a href="https://github.com/klauspost"><code>@​klauspost</code></a> in <a href="https://github-redirect.dependabot.com/klauspost/reedsolomon/pull/234">klauspost/reedsolomon#234</a></li>
<li>Retract v1.11.2 by <a href="https://github.com/klauspost"><code>@​klauspost</code></a> in <a href="https://github-redirect.dependabot.com/klauspost/reedsolomon/pull/231">klauspost/reedsolomon#231</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/0xbillw"><code>@​0xbillw</code></a> made their first contribution in <a href="https://github-redirect.dependabot.com/klauspost/reedsolomon/pull/233">klauspost/reedsolomon#233</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a href="https://github.com/klauspost/reedsolomon/compare/v1.11.4...v1.11.5">https://github.com/klauspost/reedsolomon/compare/v1.11.4...v1.11.5</a></p>
<h2>v1.11.4</h2>
<h2>What's Changed</h2>
<ul>
<li>Use GFNI broadcast by <a href="https://github.com/klauspost"><code>@​klauspost</code></a> in <a href="https://github-redirect.dependabot.com/klauspost/reedsolomon/pull/228">klauspost/reedsolomon#228</a></li>
<li>Align allocations by <a href="https://github.com/klauspost"><code>@​klauspost</code></a> in <a href="https://github-redirect.dependabot.com/klauspost/reedsolomon/pull/230">klauspost/reedsolomon#230</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a href="https://github.com/klauspost/reedsolomon/compare/v1.11.3...v1.11.4">https://github.com/klauspost/reedsolomon/compare/v1.11.3...v1.11.4</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/klauspost/reedsolomon/commit/7e59db9eaa4948ed5473befddf939927c57ef65f"><code>7e59db9</code></a> Fix Split function with 0 erasure (<a href="https://github-redirect.dependabot.com/klauspost/reedsolomon/issues/234">#234</a>)</li>
<li><a href="https://github.com/klauspost/reedsolomon/commit/6580cd4c9e5adaac64fd100157acf9b113cc7a93"><code>6580cd4</code></a> zero memery padding on stream split, fix potential OOM issues. (<a href="https://github-redirect.dependabot.com/klauspost/reedsolomon/issues/233">#233</a>)</li>
<li><a href="https://github.com/klauspost/reedsolomon/commit/b413df32aec2953ebc9e85167dc4b4969be4a3a6"><code>b413df3</code></a> Retract v1.11.2 (<a href="https://github-redirect.dependabot.com/klauspost/reedsolomon/issues/231">#231</a>)</li>
<li><a href="https://github.com/klauspost/reedsolomon/commit/c5147e0f93fe991713cc0ba10e43985e063e897f"><code>c5147e0</code></a> Update README.md</li>
<li><a href="https://github.com/klauspost/reedsolomon/commit/e4bf56114a5adc7eec3df9c91d1ad667b5a00109"><code>e4bf561</code></a> Align allocations (<a href="https://github-redirect.dependabot.com/klauspost/reedsolomon/issues/230">#230</a>)</li>
<li><a href="https://github.com/klauspost/reedsolomon/commit/76765c1d282d91cf958cc385042b982cff0ecb62"><code>76765c1</code></a> Use GFNI broadcast (<a href="https://github-redirect.dependabot.com/klauspost/reedsolomon/issues/228">#228</a>)</li>
<li>See full diff in <a href="https://github.com/klauspost/reedsolomon/compare/v1.11.3...v1.11.5">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github.com/klauspost/reedsolomon&package-manager=go_modules&previous-version=1.11.3&new-version=1.11.5)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>

codecov[bot]: # [Codecov](https://codecov.io/gh/kopia/kopia/pull/2707?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia) Report
Base: **74.95**% // Head: **74.94**% // Decreases project coverage by **`-0.01%`** :warning:
> Coverage data is based on head [(`5bc1ec1`)](https://codecov.io/gh/kopia/kopia/pull/2707?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia) compared to base [(`e5387ce`)](https://codecov.io/gh/kopia/kopia/commit/e5387cec0acbb701912acb1e852d1f3fa98deca1?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia).
> Patch has no changes to coverable lines.

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


```diff
@@            Coverage Diff             @@
##           master    #2707      +/-   ##
==========================================
- Coverage   74.95%   74.94%   -0.01%     
==========================================
  Files         457      457              
  Lines       35823    35823              
==========================================
- Hits        26850    26848       -2     
- Misses       7071     7074       +3     
+ Partials     1902     1901       -1     
```


| [Impacted Files](https://codecov.io/gh/kopia/kopia/pull/2707?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia) | Coverage Δ | |
|---|---|---|
| [repo/content/index/index\_v1.go](https://codecov.io/gh/kopia/kopia/pull/2707?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia#diff-cmVwby9jb250ZW50L2luZGV4L2luZGV4X3YxLmdv) | `75.00% <0.00%> (-1.18%)` | :arrow_down: |
| [repo/open.go](https://codecov.io/gh/kopia/kopia/pull/2707?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia#diff-cmVwby9vcGVuLmdv) | `78.33% <0.00%> (-1.00%)` | :arrow_down: |
| [internal/server/source\_manager.go](https://codecov.io/gh/kopia/kopia/pull/2707?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia#diff-aW50ZXJuYWwvc2VydmVyL3NvdXJjZV9tYW5hZ2VyLmdv) | `80.66% <0.00%> (-0.77%)` | :arrow_down: |
| [internal/providervalidation/providervalidation.go](https://codecov.io/gh/kopia/kopia/pull/2707?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia#diff-aW50ZXJuYWwvcHJvdmlkZXJ2YWxpZGF0aW9uL3Byb3ZpZGVydmFsaWRhdGlvbi5nbw==) | `65.68% <0.00%> (+2.58%)` | :arrow_up: |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia)

</details>

[:umbrella: View full report at Codecov](https://codecov.io/gh/kopia/kopia/pull/2707?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](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/race-detector.yml b/.github/workflows/race-detector.yml
index fc6075cd..faa3fa7a 100644
--- a/.github/workflows/race-detector.yml
+++ b/.github/workflows/race-detector.yml
@@ -22,5 +22,3 @@ jobs:
         fetch-depth: 0
     - name: Unit Tests
       run: make -j2 test UNIT_TEST_RACE_FLAGS=-race UNIT_TESTS_TIMEOUT=1200s
-    - name: Integration Tests
-      run: make -j2 ci-integration-tests INTEGRATION_TEST_RACE_FLAGS=-race
diff --git a/Makefile b/Makefile
index f007b4f6..44e184ad 100644
--- a/Makefile
+++ b/Makefile
@@ -14,7 +14,6 @@ all_go_sources=$(foreach d,$(go_source_dirs),$(call rwildcard,$d/,*.go)) $(wildc
 all:
 	$(MAKE) test
 	$(MAKE) lint
-	$(MAKE) integration-tests
 
 include tools/tools.mk
 
@@ -42,7 +41,7 @@ GOTESTSUM_FLAGS=--format=$(GOTESTSUM_FORMAT) --no-summary=skipped
 GO_TEST?=$(gotestsum) $(GOTESTSUM_FLAGS) --
 
 LINTER_DEADLINE=600s
-UNIT_TESTS_TIMEOUT=600s
+UNIT_TESTS_TIMEOUT=1200s
 
 ifeq ($(GOARCH),amd64)
 PARALLEL=8
@@ -213,7 +212,6 @@ download-rclone:
 ci-tests: lint vet test 
 
 ci-integration-tests:
-	$(MAKE) integration-tests
 	$(MAKE) robustness-tool-tests
 
 ci-publish-coverage:
@@ -252,7 +250,7 @@ dev-deps:
 test-with-coverage: export KOPIA_COVERAGE_TEST=1
 test-with-coverage: export TESTING_ACTION_EXE ?= $(TESTING_ACTION_EXE)
 test-with-coverage: $(gotestsum) $(TESTING_ACTION_EXE)
-	$(GO_TEST) $(UNIT_TEST_RACE_FLAGS) -tags testing -count=$(REPEAT_TEST) -short -covermode=atomic -coverprofile=coverage.txt --coverpkg $(COVERAGE_PACKAGES) -timeout 300s ./...
+	$(GO_TEST) $(UNIT_TEST_RACE_FLAGS) -tags testing -count=$(REPEAT_TEST) -short -covermode=atomic -coverprofile=coverage.txt --coverpkg $(COVERAGE_PACKAGES) -timeout $(UNIT_TESTS_TIMEOUT) ./...
 
 test: GOTESTSUM_FLAGS=--format=$(GOTESTSUM_FORMAT) --no-summary=skipped --jsonfile=.tmp.unit-tests.json
 test: export TESTING_ACTION_EXE ?= $(TESTING_ACTION_EXE)
@@ -286,15 +284,6 @@ build-integration-test-binary:
 $(TESTING_ACTION_EXE): tests/testingaction/main.go
 	go build -o $(TESTING_ACTION_EXE) -tags testing github.com/kopia/kopia/tests/testingaction
 
-integration-tests: export KOPIA_EXE ?= $(KOPIA_INTEGRATION_EXE)
-integration-tests: export KOPIA_08_EXE=$(kopia08)
-integration-tests: export KOPIA_TRACK_CHUNK_ALLOC=1
-integration-tests: export TESTING_ACTION_EXE ?= $(TESTING_ACTION_EXE)
-integration-tests: GOTESTSUM_FLAGS=--format=testname --no-summary=skipped --jsonfile=.tmp.integration-tests.json
-integration-tests: build-integration-test-binary $(gotestsum) $(TESTING_ACTION_EXE) $(kopia08)
-	$(GO_TEST) $(TEST_FLAGS) -count=$(REPEAT_TEST) -parallel $(PARALLEL) -timeout 3600s github.com/kopia/kopia/tests/end_to_end_test
-	-$(gotestsum) tool slowest --jsonfile .tmp.integration-tests.json  --threshold 1000ms
-
 compat-tests: export KOPIA_CURRENT_EXE=$(CURDIR)/$(kopia_ui_embedded_exe)
 compat-tests: export KOPIA_08_EXE=$(kopia08)
 compat-tests: GOTESTSUM_FLAGS=--format=testname --no-summary=skipped --jsonfile=.tmp.compat-tests.json
diff --git a/app/package-lock.json b/app/package-lock.json
index 082b97ae..1b798c4c 100644
--- a/app/package-lock.json
+++ b/app/package-lock.json
@@ -20,7 +20,7 @@
       "devDependencies": {
         "@playwright/test": "^1.26.1",
         "asar": "^3.2.0",
-        "concurrently": "^7.4.0",
+        "concurrently": "^7.6.0",
         "dotenv": "^16.0.3",
         "electron": "^19.0.8",
         "electron-builder": "^23.3.3",
@@ -1105,9 +1105,9 @@
       }
     },
     "node_modules/concurrently": {
-      "version": "7.4.0",
-      "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-7.4.0.tgz",
-      "integrity": "sha512-M6AfrueDt/GEna/Vg9BqQ+93yuvzkSKmoTixnwEJkH0LlcGrRC2eCmjeG1tLLHIYfpYJABokqSGyMcXjm96AFA==",
+      "version": "7.6.0",
+      "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-7.6.0.tgz",
+      "integrity": "sha512-BKtRgvcJGeZ4XttiDiNcFiRlxoAeZOseqUvyYRUp/Vtd+9p1ULmeoSqGsDA+2ivdeDFpqrJvGvmI+StKfKl5hw==",
       "dev": true,
       "dependencies": {
         "chalk": "^4.1.0",
@@ -4583,9 +4583,9 @@
       }
     },
     "concurrently": {
-      "version": "7.4.0",
-      "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-7.4.0.tgz",
-      "integrity": "sha512-M6AfrueDt/GEna/Vg9BqQ+93yuvzkSKmoTixnwEJkH0LlcGrRC2eCmjeG1tLLHIYfpYJABokqSGyMcXjm96AFA==",
+      "version": "7.6.0",
+      "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-7.6.0.tgz",
+      "integrity": "sha512-BKtRgvcJGeZ4XttiDiNcFiRlxoAeZOseqUvyYRUp/Vtd+9p1ULmeoSqGsDA+2ivdeDFpqrJvGvmI+StKfKl5hw==",
       "dev": true,
       "requires": {
         "chalk": "^4.1.0",
diff --git a/app/package.json b/app/package.json
index 8d995627..aee137df 100644
--- a/app/package.json
+++ b/app/package.json
@@ -112,7 +112,7 @@
   "devDependencies": {
     "@playwright/test": "^1.26.1",
     "asar": "^3.2.0",
-    "concurrently": "^7.4.0",
+    "concurrently": "^7.6.0",
     "dotenv": "^16.0.3",
     "electron": "^19.0.8",
     "electron-builder": "^23.3.3",
diff --git a/cli/command_server_start.go b/cli/command_server_start.go
index 97621c42..367f1f40 100644
--- a/cli/command_server_start.go
+++ b/cli/command_server_start.go
@@ -63,6 +63,8 @@ type commandServerStart struct {
 	uiPreferencesFile                   string
 	asyncRepoConnect                    bool
 
+	shutdownGracePeriod time.Duration
+
 	logServerRequests bool
 
 	disableCSRFTokenChecks bool // disable CSRF token checks - used for development/debugging only
@@ -112,6 +114,8 @@ func (c *commandServerStart) setup(svc advancedAppServices, parent commandParent
 	cmd.Flag("log-server-requests", "Log server requests").Hidden().BoolVar(&c.logServerRequests)
 	cmd.Flag("disable-csrf-token-checks", "Disable CSRF token").Hidden().BoolVar(&c.disableCSRFTokenChecks)
 
+	cmd.Flag("shutdown-grace-period", "Grace period for shutting down the server").Default("5s").DurationVar(&c.shutdownGracePeriod)
+
 	c.sf.setup(svc, cmd)
 	c.co.setup(svc, cmd)
 	c.svc = svc
@@ -191,7 +195,23 @@ func (c *commandServerStart) run(ctx context.Context) error {
 		},
 	}
 
-	srv.OnShutdown = httpServer.Shutdown
+	srv.OnShutdown = func(ctx context.Context) error {
+		ctx2, cancel := context.WithTimeout(ctx, c.shutdownGracePeriod)
+		defer cancel()
+
+		// wait for all connections to finish for up to 5 seconds
+		log(ctx2).Debugf("attempting graceful shutdown for %v", c.shutdownGracePeriod)
+
+		if serr := httpServer.Shutdown(ctx2); serr != nil {
+			// graceful shutdown unsuccessful, force close
+			log(ctx2).Debugf("unable to shut down gracefully - closing: %v", serr)
+			return errors.Wrap(httpServer.Close(), "close")
+		}
+
+		log(ctx2).Debugf("graceful shutdown succeeded")
+
+		return nil
+	}
 
 	c.svc.onCtrlC(func() {
 		log(ctx).Infof("Shutting down...")
diff --git a/go.mod b/go.mod
index 9e882ba7..26ae2701 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,7 @@ module github.com/kopia/kopia
 go 1.19
 
 require (
-	cloud.google.com/go/storage v1.28.1
+	cloud.google.com/go/storage v1.29.0
 	github.com/Azure/azure-pipeline-go v0.2.3 // indirect
 	github.com/Azure/azure-storage-blob-go v0.15.0
 	github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962 // indirect
@@ -16,7 +16,7 @@ require (
 	github.com/foomo/htpasswd v0.0.0-20200116085101-e3a90e78da9c
 	github.com/frankban/quicktest v1.13.1 // indirect
 	github.com/gofrs/flock v0.8.1
-	github.com/golang-jwt/jwt/v4 v4.4.2
+	github.com/golang-jwt/jwt/v4 v4.4.3
 	github.com/golang/protobuf v1.5.2
 	github.com/google/fswalker v0.2.1-0.20200214223026-f0e929ba4126
 	github.com/google/go-cmp v0.5.9
@@ -66,7 +66,7 @@ require (
 )
 
 require (
-	cloud.google.com/go v0.105.0 // indirect
+	cloud.google.com/go v0.107.0 // indirect
 	github.com/Azure/azure-sdk-for-go/sdk/azcore v0.21.1 // indirect
 	github.com/Azure/azure-sdk-for-go/sdk/internal v0.8.3 // indirect
 	github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.3.0
@@ -98,7 +98,7 @@ require (
 	go.uber.org/multierr v1.8.0
 	golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
 	google.golang.org/appengine v1.6.7 // indirect
-	google.golang.org/genproto v0.0.0-20221227171554-f9683d7f8bef // indirect
+	google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f // indirect
 )
 
 require (
@@ -106,7 +106,7 @@ require (
 	github.com/chromedp/chromedp v0.8.7
 	github.com/edsrzf/mmap-go v1.1.0
 	github.com/hashicorp/golang-lru v0.5.4
-	github.com/klauspost/reedsolomon v1.11.3
+	github.com/klauspost/reedsolomon v1.11.5
 	github.com/prometheus/client_model v0.3.0
 	go.opentelemetry.io/otel v1.11.2
 	go.opentelemetry.io/otel/exporters/jaeger v1.11.1
diff --git a/go.sum b/go.sum
index 63b247b4..0c477868 100644
--- a/go.sum
+++ b/go.sum
@@ -13,8 +13,8 @@ cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKV
 cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
 cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
 cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
-cloud.google.com/go v0.105.0 h1:DNtEKRBAAzeS4KyIory52wWHuClNaXJ5x1F7xa4q+5Y=
-cloud.google.com/go v0.105.0/go.mod h1:PrLgOJNe5nfE9UMxKxgXj4mD3voiP+YQ6gdt6KMFOKM=
+cloud.google.com/go v0.107.0 h1:qkj22L7bgkl6vIeZDlOY2po43Mx/TIa2Wsa7VR+PEww=
+cloud.google.com/go v0.107.0/go.mod h1:wpc2eNrD7hXUTy8EKS10jkxpZBjASrORK7goS+3YX2I=
 cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
 cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
 cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
@@ -39,8 +39,8 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo
 cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
 cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
 cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
-cloud.google.com/go/storage v1.28.1 h1:F5QDG5ChchaAVQhINh24U99OWHURqrW8OmQcGKXcbgI=
-cloud.google.com/go/storage v1.28.1/go.mod h1:Qnisd4CqDdo6BGs2AD5LLnEsmSQ80wQ5ogcBBKhU86Y=
+cloud.google.com/go/storage v1.29.0 h1:6weCgzRvMg7lzuUurI4697AqIRPU1SvzHhynwpW31jI=
+cloud.google.com/go/storage v1.29.0/go.mod h1:4puEjyTKnku6gfKoTfNOU/W+a9JyuVNxjpS5GBrB8h4=
 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
 github.com/Azure/azure-pipeline-go v0.2.3 h1:7U9HBg1JFK3jHl5qmo4CTZKFTVgMwdFHMVtCdfBE21U=
 github.com/Azure/azure-pipeline-go v0.2.3/go.mod h1:x841ezTBIMG6O3lAcl8ATHnsOPVl2bqk7S3ta6S6u4k=
@@ -164,8 +164,8 @@ github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
 github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
 github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
 github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
-github.com/golang-jwt/jwt/v4 v4.4.2 h1:rcc4lwaZgFMCZ5jxF9ABolDcIHdBytAFgqFPbSJQAYs=
-github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
+github.com/golang-jwt/jwt/v4 v4.4.3 h1:Hxl6lhQFj4AnOX6MLrsCb/+7tCj7DxP7VA+2rDIq5AU=
+github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
 github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ=
 github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4=
@@ -279,8 +279,8 @@ github.com/klauspost/cpuid/v2 v2.1.1 h1:t0wUqjowdm8ezddV5k0tLWVklVuvLJpoHeb4WBdy
 github.com/klauspost/cpuid/v2 v2.1.1/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
 github.com/klauspost/pgzip v1.2.5 h1:qnWYvvKqedOF2ulHpMG72XQol4ILEJ8k2wwRl/Km8oE=
 github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
-github.com/klauspost/reedsolomon v1.11.3 h1:rX9UNNvDhJ0Bq45y6uBy/eYehcjyz5faokTuZmu1Q9U=
-github.com/klauspost/reedsolomon v1.11.3/go.mod h1:FXLZzlJIdfqEnQLdUKWNRuMZg747hZ4oYp2Ml60Lb/k=
+github.com/klauspost/reedsolomon v1.11.5 h1:8ebqrZbby2dplht2gUPplizNlvYGCghRRfq5F9SFYKM=
+github.com/klauspost/reedsolomon v1.11.5/go.mod h1:lbYSjK96uTl22BD+PzgzlsCal+PW/0yLKlQP4MMT+i4=
 github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/kopia/htmluibuild v0.0.0-20220928042710-9fdd02afb1e7 h1:WP5VfIQL7AaYkO4zTNSCsVOawTzudbc4tvLojvg0RKc=
@@ -724,8 +724,8 @@ google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7Fc
 google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
 google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
 google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20221227171554-f9683d7f8bef h1:uQ2vjV/sHTsWSqdKeLqmwitzgvjMl7o4IdtHwUDXSJY=
-google.golang.org/genproto v0.0.0-20221227171554-f9683d7f8bef/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM=
+google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f h1:BWUVssLB0HVOSY78gIdvk1dTVYtT1y8SBWtPYuTJ/6w=
+google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
 google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
diff --git a/repo/grpc_repository_client.go b/repo/grpc_repository_client.go
index 3ed90bb6..0aefcb47 100644
--- a/repo/grpc_repository_client.go
+++ b/repo/grpc_repository_client.go
@@ -96,10 +96,14 @@ type grpcInnerSession struct {
 
 	cli        apipb.KopiaRepository_SessionClient
 	repoParams *apipb.RepositoryParameters
+
+	wg sync.WaitGroup
 }
 
 // readLoop runs in a goroutine and consumes all messages in session and forwards them to appropriate channels.
 func (r *grpcInnerSession) readLoop(ctx context.Context) {
+	defer r.wg.Done()
+
 	msg, err := r.cli.Recv()
 
 	for ; err == nil; msg, err = r.cli.Recv() {
@@ -122,6 +126,8 @@ func (r *grpcInnerSession) readLoop(ctx context.Context) {
 	for id := range r.activeRequests {
 		r.sendStreamBrokenAndClose(r.getAndDeleteResponseChannelLocked(id), err)
 	}
+
+	log(ctx).Debugf("finished closing active requests")
 }
 
 // sendRequest sends the provided request to the server and returns a channel on which the
@@ -834,6 +840,8 @@ func (r *grpcRepositoryClient) getOrEstablishInnerSession(ctx context.Context) (
 				nextRequestID:  1,
 			}
 
+			newSess.wg.Add(1)
+
 			go newSess.readLoop(ctx)
 
 			newSess.repoParams, err = newSess.initializeSession(ctx, r.opt.Purpose, r.isReadOnly)
@@ -859,6 +867,7 @@ func (r *grpcRepositoryClient) killInnerSession() {
 
 	if r.innerSession != nil {
 		r.innerSession.cli.CloseSend() //nolint:errcheck
+		r.innerSession.wg.Wait()
 		r.innerSession = nil
 	}
 }


Test patch:
diff --git a/cli/command_repository_upgrade_test.go b/cli/command_repository_upgrade_test.go
index 432faaf4..86a38d4c 100644
--- a/cli/command_repository_upgrade_test.go
+++ b/cli/command_repository_upgrade_test.go
@@ -205,6 +205,8 @@ func lockRepositoryForUpgrade(t *testing.T, env *testenv.CLITest) {
 }
 
 func (s *formatSpecificTestSuite) TestRepositoryUpgradeStatusWhileLocked(t *testing.T) {
+	t.Parallel()
+
 	env := testenv.NewCLITest(t, s.formatFlags, testenv.NewInProcRunner(t))
 
 	env.RunAndExpectSuccess(t, "repo", "create", "filesystem", "--path", env.RepoDir)
diff --git a/internal/logfile/logfile_test.go b/internal/logfile/logfile_test.go
index 34e7c576..3bfdd329 100644
--- a/internal/logfile/logfile_test.go
+++ b/internal/logfile/logfile_test.go
@@ -147,6 +147,8 @@ func TestLogFileRotation(t *testing.T) {
 }
 
 func TestLogFileMaxTotalSize(t *testing.T) {
+	t.Parallel()
+
 	runner := testenv.NewInProcRunner(t)
 	runner.CustomizeApp = logfile.Attach
 
@@ -156,11 +158,11 @@ func TestLogFileMaxTotalSize(t *testing.T) {
 	srcDir := testutil.TempDirectory(t)
 	tmpLogDir := testutil.TempDirectory(t)
 
-	// 5-level directory with <=10 files and <=10 subdirectories at each level
+	// 2-level directory with <=10 files and <=10 subdirectories at each level
 	testdirtree.CreateDirectoryTree(srcDir, testdirtree.MaybeSimplifyFilesystem(testdirtree.DirectoryTreeOptions{
-		Depth:                  3,
+		Depth:                  2,
 		MaxSubdirsPerDirectory: 10,
-		MaxFilesPerDirectory:   100,
+		MaxFilesPerDirectory:   10,
 		MaxFileSize:            10,
 	}), &testdirtree.DirectoryTreeCounters{})
 
@@ -178,12 +180,16 @@ func TestLogFileMaxTotalSize(t *testing.T) {
 		flag := flag
 
 		t.Run(subdir, func(t *testing.T) {
-			env.RunAndExpectSuccess(t, "snap", "ls", "--file-log-level=debug", "--log-dir", tmpLogDir, flag+"=40000")
+			size0 := getTotalDirSize(t, logSubdir)
+			size0MB := float64(size0) / 1e6
+
+			env.RunAndExpectSuccess(t, "snap", "ls", "--file-log-level=debug", "--log-dir", tmpLogDir, fmt.Sprintf("%s=%v", flag, size0MB/2))
 			size1 := getTotalDirSize(t, logSubdir)
 			size1MB := float64(size1) / 1e6
 
 			env.RunAndExpectSuccess(t, "snap", "ls", "--file-log-level=debug", "--log-dir", tmpLogDir, fmt.Sprintf("%s=%v", flag, size1MB/2))
 			size2 := getTotalDirSize(t, logSubdir)
+			require.Less(t, size1, size0/2)
 			require.Less(t, size2, size1/2)
 			require.Greater(t, size2, size1/4)
 		})
diff --git a/tests/end_to_end_test/api_server_repository_test.go b/tests/end_to_end_test/api_server_repository_test.go
index 85529aa1..968060be 100644
--- a/tests/end_to_end_test/api_server_repository_test.go
+++ b/tests/end_to_end_test/api_server_repository_test.go
@@ -62,16 +62,21 @@ func testAPIServerRepository(t *testing.T, serverStartArgs []string, useGRPC, al
 		connectArgs = []string{"--no-grpc"}
 	}
 
-	runner := testenv.NewExeRunner(t)
+	runner := testenv.NewInProcRunner(t)
 	e := testenv.NewCLITest(t, testenv.RepoFormatNotImportant, runner)
 
 	defer e.RunAndExpectSuccess(t, "repo", "disconnect")
 
-	// create one snapshot as foo@bar
+	// create 5 snapshots as foo@bar
 	e.RunAndExpectSuccess(t, "repo", "create", "filesystem", "--path", e.RepoDir, "--override-username", "foo", "--override-hostname", "bar")
 	e.RunAndExpectSuccess(t, "snapshot", "create", sharedTestDataDir1)
+	e.RunAndExpectSuccess(t, "snapshot", "create", sharedTestDataDir1)
+	e.RunAndExpectSuccess(t, "snapshot", "create", sharedTestDataDir1)
+	e.RunAndExpectSuccess(t, "snapshot", "create", sharedTestDataDir1)
+	e.RunAndExpectSuccess(t, "snapshot", "create", sharedTestDataDir1)
 
 	e1 := testenv.NewCLITest(t, testenv.RepoFormatNotImportant, runner)
+
 	defer e1.RunAndExpectSuccess(t, "repo", "disconnect")
 
 	// create one snapshot as not-foo@bar
@@ -99,7 +104,12 @@ func testAPIServerRepository(t *testing.T, serverStartArgs []string, useGRPC, al
 
 	var sp testutil.ServerParameters
 
-	e.RunAndProcessStderr(t, sp.ProcessOutput,
+	e.LogOutput = true
+	e.LogOutputPrefix = "<first> "
+
+	t.Logf("******** first server startup ********")
+
+	wait, _ := e.RunAndProcessStderr(t, sp.ProcessOutput,
 		append([]string{
 			"server", "start",
 			"--address=localhost:0",
@@ -110,7 +120,9 @@ func testAPIServerRepository(t *testing.T, serverStartArgs []string, useGRPC, al
 			"--server-password", uiPassword,
 			"--server-control-username", controlUsername,
 			"--server-control-password", controlPassword,
+			"--shutdown-grace-period", "100ms",
 		}, serverStartArgs...)...)
+
 	t.Logf("detected server parameters %#v", sp)
 
 	controlClient, err := apiclient.NewKopiaAPIClient(apiclient.Options{
@@ -120,12 +132,12 @@ func testAPIServerRepository(t *testing.T, serverStartArgs []string, useGRPC, al
 		TrustedServerCertificateFingerprint: sp.SHA256Fingerprint,
 		LogRequests:                         true,
 	})
-	if err != nil {
-		t.Fatalf("unable to create API apiclient")
-	}
+	require.NoError(t, err)
 
 	waitUntilServerStarted(ctx, t, controlClient)
 
+	t.Logf("******** first server completed startup ********")
+
 	// open repository client.
 	ctx2, cancel := context.WithCancel(ctx)
 	rep, err := servertesting.ConnectAndOpenAPIServer(t, ctx2, &repo.APIServerInfo{
@@ -148,16 +160,22 @@ func testAPIServerRepository(t *testing.T, serverStartArgs []string, useGRPC, al
 	_, writeSess, err := rep.NewWriter(ctx, repo.WriteSessionOptions{Purpose: "some writer"})
 	require.NoError(t, err)
 
-	logErrorAndIgnore(t, serverapi.Shutdown(ctx, controlClient))
+	defer writeSess.Close(ctx)
 
-	// give the server a moment to wind down.
-	time.Sleep(1 * time.Second)
+	t.Logf("******** server shutdown ********")
+	require.NoError(t, serverapi.Shutdown(ctx, controlClient))
+	// wait for the server to wind down.
+	wait()
+	t.Logf("******** finished server shutdown ********")
 
 	defer rep.Close(ctx)
 
+	e.LogOutput = true
+	e.LogOutputPrefix = "<second> "
+
 	// start the server again, using the same address & TLS key+cert, so existing connection
 	// should be re-established.
-	e.RunAndProcessStderr(t, sp.ProcessOutput,
+	wait2, _ := e.RunAndProcessStderr(t, sp.ProcessOutput,
 		append([]string{
 			"server", "start",
 			"--address=" + sp.BaseURL,
@@ -168,12 +186,11 @@ func testAPIServerRepository(t *testing.T, serverStartArgs []string, useGRPC, al
 			"--server-control-username", controlUsername,
 			"--server-control-password", controlPassword,
 		}, serverStartArgs...)...)
+
 	t.Logf("detected server parameters %#v", sp)
 
 	waitUntilServerStarted(ctx, t, controlClient)
 
-	defer serverapi.Shutdown(ctx, controlClient)
-
 	someLabels := map[string]string{
 		"type":     "snapshot",
 		"username": "foo",
@@ -181,21 +198,22 @@ func testAPIServerRepository(t *testing.T, serverStartArgs []string, useGRPC, al
 	}
 
 	// invoke some read method, the repository will automatically reconnect to the server.
-	verifyFindManifestCount(ctx, t, rep, someLabels, 1)
+	verifyFindManifestCount(ctx, t, rep, someLabels, 5)
 
 	if useGRPC {
 		// the same method on a GRPC write session should fail because the stream was broken.
-		if _, err := writeSess.FindManifests(ctx, someLabels); err == nil {
-			t.Fatalf("expected failure on write session method, got success.")
-		}
+		_, err := writeSess.FindManifests(ctx, someLabels)
+		require.Error(t, err)
 	} else {
 		// invoke some method on write session, this will succeed because legacy API is stateless
 		// (also incorrect in this case).
-		verifyFindManifestCount(ctx, t, writeSess, someLabels, 1)
+		verifyFindManifestCount(ctx, t, writeSess, someLabels, 5)
 	}
 
-	runner2 := testenv.NewExeRunner(t)
+	runner2 := testenv.NewInProcRunner(t)
 	e2 := testenv.NewCLITest(t, testenv.RepoFormatNotImportant, runner2)
+	e2.LogOutput = true
+	e2.LogOutputPrefix = "client2 "
 
 	defer e2.RunAndExpectSuccess(t, "repo", "disconnect")
 
@@ -215,9 +233,7 @@ func testAPIServerRepository(t *testing.T, serverStartArgs []string, useGRPC, al
 
 	// should see one snapshot
 	snapshots := clitestutil.ListSnapshotsAndExpectSuccess(t, e2)
-	if got, want := len(snapshots), 1; got != want {
-		t.Errorf("invalid number of snapshots for foo@bar")
-	}
+	require.Len(t, snapshots, 1)
 
 	// create very small directory
 	smallDataDir := filepath.Join(sharedTestDataDirBase, "dir-small")
@@ -234,25 +250,20 @@ func testAPIServerRepository(t *testing.T, serverStartArgs []string, useGRPC, al
 
 	// make sure snapshot created by the client resulted in blobs being created by the server
 	// as opposed to buffering it in memory
-	if got, want := len(e.RunAndExpectSuccess(t, "blob", "list", "--prefix=p")), originalPBlobCount; got <= want {
-		t.Errorf("unexpected number of P blobs on the server: %v, wanted > %v", got, want)
-	}
-
-	if got, want := len(e.RunAndExpectSuccess(t, "blob", "list", "--prefix=q")), originalQBlobCount; got <= want {
-		t.Errorf("unexpected number of Q blobs on the server: %v, wanted > %v", got, want)
-	}
+	require.Greater(t, len(e.RunAndExpectSuccess(t, "blob", "list", "--prefix=p")), originalPBlobCount)
+	require.Greater(t, len(e.RunAndExpectSuccess(t, "blob", "list", "--prefix=q")), originalQBlobCount)
 
 	// create snapshot using remote repository client
 	e2.RunAndExpectSuccess(t, "snapshot", "create", sharedTestDataDir2)
 
 	// now should see two snapshots
 	snapshots = clitestutil.ListSnapshotsAndExpectSuccess(t, e2)
-	if got, want := len(snapshots), 3; got != want {
-		t.Errorf("invalid number of snapshots for foo@bar")
-	}
+	require.Len(t, snapshots, 3)
 
 	// shutdown the server
-	logErrorAndIgnore(t, serverapi.Shutdown(ctx, controlClient))
+	require.NoError(t, serverapi.Shutdown(ctx, controlClient))
+
+	wait2()
 
 	// open repository client to a dead server, this should fail quickly instead of retrying forever.
 	timer := timetrack.StartTimer()
@@ -267,28 +278,13 @@ func testAPIServerRepository(t *testing.T, serverStartArgs []string, useGRPC, al
 	}, content.CachingOptions{}, "baz", &repo.Options{})
 
 	//nolint:forbidigo
-	if dur := timer.Elapsed(); dur > 15*time.Second {
-		t.Fatalf("failed connection took %v", dur)
-	}
+	require.Less(t, timer.Elapsed(), 15*time.Second)
 }
 
 func verifyFindManifestCount(ctx context.Context, t *testing.T, rep repo.Repository, labels map[string]string, wantCount int) {
 	t.Helper()
 
 	man, err := rep.FindManifests(ctx, labels)
-	if err != nil {
-		t.Fatalf("unable to list manifests using repository %v", err)
-	}
-
-	if got, want := len(man), wantCount; got != want {
-		t.Fatalf("invalid number of manifests: %v, want %v", got, want)
-	}
-}
-
-func logErrorAndIgnore(t *testing.T, err error) {
-	t.Helper()
-
-	if err != nil {
-		t.Log(err)
-	}
+	require.NoError(t, err)
+	require.Len(t, man, wantCount)
 }
diff --git a/tests/htmlui_e2e_test/htmlui_e2e_test.go b/tests/htmlui_e2e_test/htmlui_e2e_test.go
index 5b6e17d3..678584c3 100644
--- a/tests/htmlui_e2e_test/htmlui_e2e_test.go
+++ b/tests/htmlui_e2e_test/htmlui_e2e_test.go
@@ -60,8 +60,9 @@ func runInBrowser(t *testing.T, run func(ctx context.Context, sp *testutil.Serve
 		args = append(args, "--html="+os.Getenv("HTMLUI_BUILD_DIR"))
 	}
 
-	_, kill := e.RunAndProcessStderr(t, sp.ProcessOutput, args...)
+	wait, kill := e.RunAndProcessStderr(t, sp.ProcessOutput, args...)
 
+	defer wait()
 	defer kill()
 
 	t.Logf("detected server parameters %#v", sp)
diff --git a/tests/testenv/cli_test_env.go b/tests/testenv/cli_test_env.go
index 07d1cc21..4dd721b6 100644
--- a/tests/testenv/cli_test_env.go
+++ b/tests/testenv/cli_test_env.go
@@ -42,6 +42,9 @@ type CLITest struct {
 
 	Runner CLIRunner
 
+	LogOutput       bool
+	LogOutputPrefix string
+
 	fixedArgs   []string
 	Environment map[string]string
 
@@ -145,7 +148,17 @@ func (e *CLITest) RunAndProcessStderr(t *testing.T, callback func(line string) b
 	t.Helper()
 
 	stdout, stderr, wait, kill := e.Runner.Start(t, e.cmdArgs(args), e.Environment)
-	go io.Copy(io.Discard, stdout)
+
+	go func() {
+		scanner := bufio.NewScanner(stdout)
+		for scanner.Scan() {
+			if os.Getenv("KOPIA_TEST_LOG_OUTPUT") != "" || e.LogOutput {
+				t.Logf("[%vstdout] %v", e.LogOutputPrefix, scanner.Text())
+			}
+		}
+
+		t.Logf("[%vstdout] EOF", e.LogOutputPrefix)
+	}()
 
 	scanner := bufio.NewScanner(stderr)
 	for scanner.Scan() {
@@ -157,8 +170,12 @@ func (e *CLITest) RunAndProcessStderr(t *testing.T, callback func(line string) b
 	// complete the scan in background without processing lines.
 	go func() {
 		for scanner.Scan() {
-			// ignore
+			if os.Getenv("KOPIA_TEST_LOG_OUTPUT") != "" || e.LogOutput {
+				t.Logf("[%vstderr] %v", e.LogOutputPrefix, scanner.Text())
+			}
 		}
+
+		t.Logf("[%vstderr] EOF", e.LogOutputPrefix)
 	}()
 
 	return wait, kill
@@ -219,7 +236,7 @@ func (e *CLITest) Run(t *testing.T, expectedError bool, args ...string) (stdout,
 	t.Helper()
 
 	args = e.cmdArgs(args)
-	t.Logf("running 'kopia %v' with %v", strings.Join(args, " "), e.Environment)
+	t.Logf("%vrunning 'kopia %v' with %v", e.LogOutputPrefix, strings.Join(args, " "), e.Environment)
 
 	timer := timetrack.StartTimer()
 
@@ -234,8 +251,8 @@ func (e *CLITest) Run(t *testing.T, expectedError bool, args ...string) (stdout,
 
 		scanner := bufio.NewScanner(stdoutReader)
 		for scanner.Scan() {
-			if os.Getenv("KOPIA_TEST_LOG_OUTPUT") != "" {
-				t.Logf("[stdout] %v", scanner.Text())
+			if os.Getenv("KOPIA_TEST_LOG_OUTPUT") != "" || e.LogOutput {
+				t.Logf("[%vstdout] %v", e.LogOutputPrefix, scanner.Text())
 			}
 
 			stdout = append(stdout, scanner.Text())
@@ -249,8 +266,8 @@ func (e *CLITest) Run(t *testing.T, expectedError bool, args ...string) (stdout,
 
 		scanner := bufio.NewScanner(stderrReader)
 		for scanner.Scan() {
-			if os.Getenv("KOPIA_TEST_LOG_OUTPUT") != "" {
-				t.Logf("[stderr] %v", scanner.Text())
+			if os.Getenv("KOPIA_TEST_LOG_OUTPUT") != "" || e.LogOutput {
+				t.Logf("[%vstderr] %v", e.LogOutputPrefix, scanner.Text())
 			}
 
 			stderr = append(stderr, scanner.Text())
@@ -268,7 +285,7 @@ func (e *CLITest) Run(t *testing.T, expectedError bool, args ...string) (stdout,
 	}
 
 	//nolint:forbidigo
-	t.Logf("finished in %v: 'kopia %v'", timer.Elapsed().Milliseconds(), strings.Join(args, " "))
+	t.Logf("%vfinished in %v: 'kopia %v'", e.LogOutputPrefix, timer.Elapsed().Milliseconds(), strings.Join(args, " "))
 
 	return stdout, stderr, gotErr
 }