loading up the forgejo repo on tangled to test page performance

Add artifacts test fixture (#30300)

Closes https://github.com/go-gitea/gitea/issues/30296

- Adds a DB fixture for actions artifacts
- Adds artifacts test files
- Clears artifacts test files between each run
- Note: I initially initialized the artifacts only for artifacts tests,
but because the files are small it only takes ~8ms, so I changed it to
always run in test setup for simplicity
- Fix some otherwise flaky tests by making them not depend on previous
tests

(cherry picked from commit 66971e591e5dddd5b6dc1572ac48f4e4ab29b8e0)

Conflicts:
- tests/integration/api_actions_artifact_test.go
Conflict resolved by manually changing the tested artifact
name from "artifact" to "artifact-download"
- tests/integration/api_actions_artifact_v4_test.go
Conflict resolved by manually updating the tested artifact
names, and adjusting the test case only present in our tree.
- tests/test_utils.go
Resolved by manually copying the added function.

authored by Kyle D. and committed by Gergely Nagy 748ae10e 6b74043b

+71
models/fixtures/action_artifact.yml
··· 1 + - 2 + id: 1 3 + run_id: 791 4 + runner_id: 1 5 + repo_id: 4 6 + owner_id: 1 7 + commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 8 + storage_path: "26/1/1712166500347189545.chunk" 9 + file_size: 1024 10 + file_compressed_size: 1024 11 + content_encoding: "" 12 + artifact_path: "abc.txt" 13 + artifact_name: "artifact-download" 14 + status: 1 15 + created_unix: 1712338649 16 + updated_unix: 1712338649 17 + expired_unix: 1720114649 18 + 19 + - 20 + id: 19 21 + run_id: 791 22 + runner_id: 1 23 + repo_id: 4 24 + owner_id: 1 25 + commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 26 + storage_path: "26/19/1712348022422036662.chunk" 27 + file_size: 1024 28 + file_compressed_size: 1024 29 + content_encoding: "" 30 + artifact_path: "abc.txt" 31 + artifact_name: "multi-file-download" 32 + status: 2 33 + created_unix: 1712348022 34 + updated_unix: 1712348022 35 + expired_unix: 1720124022 36 + 37 + - 38 + id: 20 39 + run_id: 791 40 + runner_id: 1 41 + repo_id: 4 42 + owner_id: 1 43 + commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 44 + storage_path: "26/20/1712348022423431524.chunk" 45 + file_size: 1024 46 + file_compressed_size: 1024 47 + content_encoding: "" 48 + artifact_path: "xyz/def.txt" 49 + artifact_name: "multi-file-download" 50 + status: 2 51 + created_unix: 1712348022 52 + updated_unix: 1712348022 53 + expired_unix: 1720124022 54 + 55 + - 56 + id: 22 57 + run_id: 792 58 + runner_id: 1 59 + repo_id: 4 60 + owner_id: 1 61 + commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 62 + storage_path: "27/5/1730330775594233150.chunk" 63 + file_size: 1024 64 + file_compressed_size: 1024 65 + content_encoding: "application/zip" 66 + artifact_path: "artifact-v4-download.zip" 67 + artifact_name: "artifact-v4-download" 68 + status: 2 69 + created_unix: 1730330775 70 + updated_unix: 1730330775 71 + expired_unix: 1738106775
+1 -1
modules/storage/storage.go
··· 131 131 ActionsArtifacts ObjectStorage = UninitializedStorage 132 132 ) 133 133 134 - // Init init the stoarge 134 + // Init init the storage 135 135 func Init() error { 136 136 for _, f := range []func() error{ 137 137 initAttachments,
+42 -32
tests/integration/api_actions_artifact_test.go
··· 38 38 39 39 // get upload url 40 40 idx := strings.Index(uploadResp.FileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/") 41 - url := uploadResp.FileContainerResourceURL[idx:] + "?itemPath=artifact/abc.txt" 41 + url := uploadResp.FileContainerResourceURL[idx:] + "?itemPath=artifact/abc-2.txt" 42 42 43 43 // upload artifact chunk 44 - body := strings.Repeat("A", 1024) 44 + body := strings.Repeat("C", 1024) 45 45 req = NewRequestWithBody(t, "PUT", url, strings.NewReader(body)). 46 46 AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a"). 47 47 SetHeader("Content-Range", "bytes 0-1023/1024"). 48 48 SetHeader("x-tfs-filelength", "1024"). 49 - SetHeader("x-actions-results-md5", "1HsSe8LeLWh93ILaw1TEFQ==") // base64(md5(body)) 49 + SetHeader("x-actions-results-md5", "XVlf820rMInUi64wmMi6EA==") // base64(md5(body)) 50 50 MakeRequest(t, req, http.StatusOK) 51 51 52 52 t.Logf("Create artifact confirm") 53 53 54 54 // confirm artifact upload 55 - req = NewRequest(t, "PATCH", "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts?artifactName=artifact"). 55 + req = NewRequest(t, "PATCH", "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts?artifactName=artifact-single"). 56 56 AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a") 57 57 MakeRequest(t, req, http.StatusOK) 58 58 } ··· 115 115 resp := MakeRequest(t, req, http.StatusOK) 116 116 var listResp listArtifactsResponse 117 117 DecodeJSON(t, resp, &listResp) 118 - assert.Equal(t, int64(1), listResp.Count) 119 - assert.Equal(t, "artifact", listResp.Value[0].Name) 120 - assert.Contains(t, listResp.Value[0].FileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts") 118 + assert.Equal(t, int64(2), listResp.Count) 119 + 120 + // Return list might be in any order. Get one file. 121 + var artifactIdx int 122 + for i, artifact := range listResp.Value { 123 + if artifact.Name == "artifact-download" { 124 + artifactIdx = i 125 + break 126 + } 127 + } 128 + assert.NotNil(t, artifactIdx) 129 + assert.Equal(t, listResp.Value[artifactIdx].Name, "artifact-download") 130 + assert.Contains(t, listResp.Value[artifactIdx].FileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts") 121 131 122 - idx := strings.Index(listResp.Value[0].FileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/") 123 - url := listResp.Value[0].FileContainerResourceURL[idx+1:] + "?itemPath=artifact" 132 + idx := strings.Index(listResp.Value[artifactIdx].FileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/") 133 + url := listResp.Value[artifactIdx].FileContainerResourceURL[idx+1:] + "?itemPath=artifact-download" 124 134 req = NewRequest(t, "GET", url). 125 135 AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a") 126 136 resp = MakeRequest(t, req, http.StatusOK) 127 137 var downloadResp downloadArtifactResponse 128 138 DecodeJSON(t, resp, &downloadResp) 129 139 assert.Len(t, downloadResp.Value, 1) 130 - assert.Equal(t, "artifact/abc.txt", downloadResp.Value[0].Path) 131 - assert.Equal(t, "file", downloadResp.Value[0].ItemType) 132 - assert.Contains(t, downloadResp.Value[0].ContentLocation, "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts") 140 + assert.Equal(t, "artifact-download/abc.txt", downloadResp.Value[artifactIdx].Path) 141 + assert.Equal(t, "file", downloadResp.Value[artifactIdx].ItemType) 142 + assert.Contains(t, downloadResp.Value[artifactIdx].ContentLocation, "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts") 133 143 134 - idx = strings.Index(downloadResp.Value[0].ContentLocation, "/api/actions_pipeline/_apis/pipelines/") 135 - url = downloadResp.Value[0].ContentLocation[idx:] 144 + idx = strings.Index(downloadResp.Value[artifactIdx].ContentLocation, "/api/actions_pipeline/_apis/pipelines/") 145 + url = downloadResp.Value[artifactIdx].ContentLocation[idx:] 136 146 req = NewRequest(t, "GET", url). 137 147 AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a") 138 148 resp = MakeRequest(t, req, http.StatusOK) 149 + 139 150 body := strings.Repeat("A", 1024) 140 - assert.Equal(t, resp.Body.String(), body) 151 + assert.Equal(t, body, resp.Body.String()) 141 152 } 142 153 143 154 func TestActionsArtifactUploadMultipleFile(t *testing.T) { ··· 163 174 164 175 files := []uploadingFile{ 165 176 { 166 - Path: "abc.txt", 167 - Content: strings.Repeat("A", 1024), 168 - MD5: "1HsSe8LeLWh93ILaw1TEFQ==", 177 + Path: "abc-3.txt", 178 + Content: strings.Repeat("D", 1024), 179 + MD5: "9nqj7E8HZmfQtPifCJ5Zww==", 169 180 }, 170 181 { 171 - Path: "xyz/def.txt", 172 - Content: strings.Repeat("B", 1024), 173 - MD5: "6fgADK/7zjadf+6cB9Q1CQ==", 182 + Path: "xyz/def-2.txt", 183 + Content: strings.Repeat("E", 1024), 184 + MD5: "/s1kKvxeHlUX85vaTaVxuA==", 174 185 }, 175 186 } 176 187 ··· 199 210 func TestActionsArtifactDownloadMultiFiles(t *testing.T) { 200 211 defer tests.PrepareTestEnv(t)() 201 212 202 - const testArtifactName = "multi-files" 213 + const testArtifactName = "multi-file-download" 203 214 204 215 req := NewRequest(t, "GET", "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts"). 205 216 AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a") ··· 226 237 DecodeJSON(t, resp, &downloadResp) 227 238 assert.Len(t, downloadResp.Value, 2) 228 239 229 - downloads := [][]string{{"multi-files/abc.txt", "A"}, {"multi-files/xyz/def.txt", "B"}} 240 + downloads := [][]string{{"multi-file-download/abc.txt", "B"}, {"multi-file-download/xyz/def.txt", "C"}} 230 241 for _, v := range downloadResp.Value { 231 242 var bodyChar string 232 243 var path string ··· 247 258 req = NewRequest(t, "GET", url). 248 259 AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a") 249 260 resp = MakeRequest(t, req, http.StatusOK) 250 - body := strings.Repeat(bodyChar, 1024) 251 - assert.Equal(t, resp.Body.String(), body) 261 + assert.Equal(t, strings.Repeat(bodyChar, 1024), resp.Body.String()) 252 262 } 253 263 } 254 264 ··· 300 310 DecodeJSON(t, resp, &listResp) 301 311 302 312 idx := strings.Index(listResp.Value[0].FileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/") 303 - url := listResp.Value[0].FileContainerResourceURL[idx+1:] + "?itemPath=artifact" 313 + url := listResp.Value[0].FileContainerResourceURL[idx+1:] + "?itemPath=artifact-download" 304 314 req = NewRequest(t, "GET", url). 305 315 AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a") 306 316 resp = MakeRequest(t, req, http.StatusOK) ··· 320 330 // upload same artifact, it uses 4096 B 321 331 req := NewRequestWithJSON(t, "POST", "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts", getUploadArtifactRequest{ 322 332 Type: "actions_storage", 323 - Name: "artifact", 333 + Name: "artifact-download", 324 334 }).AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a") 325 335 resp := MakeRequest(t, req, http.StatusOK) 326 336 var uploadResp uploadArtifactResponse 327 337 DecodeJSON(t, resp, &uploadResp) 328 338 329 339 idx := strings.Index(uploadResp.FileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/") 330 - url := uploadResp.FileContainerResourceURL[idx:] + "?itemPath=artifact/abc.txt" 340 + url := uploadResp.FileContainerResourceURL[idx:] + "?itemPath=artifact-download/abc.txt" 331 341 body := strings.Repeat("B", 4096) 332 342 req = NewRequestWithBody(t, "PUT", url, strings.NewReader(body)). 333 343 AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a"). ··· 337 347 MakeRequest(t, req, http.StatusOK) 338 348 339 349 // confirm artifact upload 340 - req = NewRequest(t, "PATCH", "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts?artifactName=artifact"). 350 + req = NewRequest(t, "PATCH", "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts?artifactName=artifact-download"). 341 351 AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a") 342 352 MakeRequest(t, req, http.StatusOK) 343 353 } ··· 352 362 353 363 var uploadedItem listArtifactsResponseItem 354 364 for _, item := range listResp.Value { 355 - if item.Name == "artifact" { 365 + if item.Name == "artifact-download" { 356 366 uploadedItem = item 357 367 break 358 368 } 359 369 } 360 - assert.Equal(t, "artifact", uploadedItem.Name) 370 + assert.Equal(t, "artifact-download", uploadedItem.Name) 361 371 362 372 idx := strings.Index(uploadedItem.FileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/") 363 - url := uploadedItem.FileContainerResourceURL[idx+1:] + "?itemPath=artifact" 373 + url := uploadedItem.FileContainerResourceURL[idx+1:] + "?itemPath=artifact-download" 364 374 req = NewRequest(t, "GET", url). 365 375 AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a") 366 376 resp = MakeRequest(t, req, http.StatusOK)
+12 -12
tests/integration/api_actions_artifact_v4_test.go
··· 313 313 314 314 // acquire artifact upload url 315 315 req := NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/ListArtifacts", toProtoJSON(&actions.ListArtifactsRequest{ 316 - NameFilter: wrapperspb.String("artifact"), 316 + NameFilter: wrapperspb.String("artifact-v4-download"), 317 317 WorkflowRunBackendId: "792", 318 318 WorkflowJobRunBackendId: "193", 319 319 })).AddTokenAuth(token) ··· 324 324 325 325 // confirm artifact upload 326 326 req = NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/GetSignedArtifactURL", toProtoJSON(&actions.GetSignedArtifactURLRequest{ 327 - Name: "artifact", 327 + Name: "artifact-v4-download", 328 328 WorkflowRunBackendId: "792", 329 329 WorkflowJobRunBackendId: "193", 330 330 })). ··· 336 336 337 337 req = NewRequest(t, "GET", finalizeResp.SignedUrl) 338 338 resp = MakeRequest(t, req, http.StatusOK) 339 - body := strings.Repeat("A", 1024) 339 + body := strings.Repeat("D", 1024) 340 340 assert.Equal(t, "bytes", resp.Header().Get("accept-ranges")) 341 341 assert.Equal(t, body, resp.Body.String()) 342 342 343 343 // Download artifact via user-facing URL 344 - req = NewRequest(t, "GET", "/user5/repo4/actions/runs/188/artifacts/artifact") 344 + req = NewRequest(t, "GET", "/user5/repo4/actions/runs/188/artifacts/artifact-v4-download") 345 345 resp = MakeRequest(t, req, http.StatusOK) 346 346 assert.Equal(t, "bytes", resp.Header().Get("accept-ranges")) 347 347 assert.Equal(t, body, resp.Body.String()) 348 348 349 349 // Partial artifact download 350 - req = NewRequest(t, "GET", "/user5/repo4/actions/runs/188/artifacts/artifact").SetHeader("range", "bytes=0-99") 350 + req = NewRequest(t, "GET", "/user5/repo4/actions/runs/188/artifacts/artifact-v4-download").SetHeader("range", "bytes=0-99") 351 351 resp = MakeRequest(t, req, http.StatusPartialContent) 352 - body = strings.Repeat("A", 100) 352 + body = strings.Repeat("D", 100) 353 353 assert.Equal(t, "bytes 0-99/1024", resp.Header().Get("content-range")) 354 354 assert.Equal(t, body, resp.Body.String()) 355 355 } ··· 357 357 func TestActionsArtifactV4DownloadRange(t *testing.T) { 358 358 defer tests.PrepareTestEnv(t)() 359 359 360 - bstr := strings.Repeat("B", 100) 360 + bstr := strings.Repeat("D", 100) 361 361 body := strings.Repeat("A", 100) + bstr 362 362 token := uploadArtifact(t, body) 363 363 364 364 // Download (Actions API) 365 365 req := NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/GetSignedArtifactURL", toProtoJSON(&actions.GetSignedArtifactURLRequest{ 366 - Name: "artifact", 366 + Name: "artifact-v4-download", 367 367 WorkflowRunBackendId: "792", 368 368 WorkflowJobRunBackendId: "193", 369 369 })). ··· 375 375 376 376 req = NewRequest(t, "GET", finalizeResp.SignedUrl).SetHeader("range", "bytes=100-199") 377 377 resp = MakeRequest(t, req, http.StatusPartialContent) 378 - assert.Equal(t, "bytes 100-199/200", resp.Header().Get("content-range")) 378 + assert.Equal(t, "bytes 100-199/1024", resp.Header().Get("content-range")) 379 379 assert.Equal(t, bstr, resp.Body.String()) 380 380 381 381 // Download (user-facing API) 382 - req = NewRequest(t, "GET", "/user5/repo4/actions/runs/188/artifacts/artifact").SetHeader("range", "bytes=100-199") 382 + req = NewRequest(t, "GET", "/user5/repo4/actions/runs/188/artifacts/artifact-v4-download").SetHeader("range", "bytes=100-199") 383 383 resp = MakeRequest(t, req, http.StatusPartialContent) 384 - assert.Equal(t, "bytes 100-199/200", resp.Header().Get("content-range")) 384 + assert.Equal(t, "bytes 100-199/1024", resp.Header().Get("content-range")) 385 385 assert.Equal(t, bstr, resp.Body.String()) 386 386 } 387 387 ··· 393 393 394 394 // delete artifact by name 395 395 req := NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/DeleteArtifact", toProtoJSON(&actions.DeleteArtifactRequest{ 396 - Name: "artifact", 396 + Name: "artifact-v4-download", 397 397 WorkflowRunBackendId: "792", 398 398 WorkflowJobRunBackendId: "193", 399 399 })).AddTokenAuth(token)
+17
tests/test_utils.go
··· 224 224 t.Logf("PrepareTestEnv: all processes cancelled within %s", time.Since(start)) 225 225 } 226 226 227 + func PrepareArtifactsStorage(t testing.TB) { 228 + // prepare actions artifacts directory and files 229 + assert.NoError(t, storage.Clean(storage.ActionsArtifacts)) 230 + 231 + s, err := storage.NewStorage(setting.LocalStorageType, &setting.Storage{ 232 + Path: filepath.Join(filepath.Dir(setting.AppPath), "tests", "testdata", "data", "artifacts"), 233 + }) 234 + assert.NoError(t, err) 235 + assert.NoError(t, s.IterateObjects("", func(p string, obj storage.Object) error { 236 + _, err = storage.Copy(storage.ActionsArtifacts, p, s, p) 237 + return err 238 + })) 239 + } 240 + 227 241 func PrepareTestEnv(t testing.TB, skip ...int) func() { 228 242 t.Helper() 229 243 ourSkip := 1 ··· 262 276 _ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "refs", "tag"), 0o755) 263 277 } 264 278 } 279 + 280 + // Initialize actions artifact data 281 + PrepareArtifactsStorage(t) 265 282 266 283 // load LFS object fixtures 267 284 // (LFS storage can be on any of several backends, including remote servers, so we init it with the storage API)
+1
tests/testdata/data/artifacts/26/1/1712166500347189545.chunk
··· 1 + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+1
tests/testdata/data/artifacts/26/19/1712348022422036662.chunk
··· 1 + BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
+1
tests/testdata/data/artifacts/26/20/1712348022423431524.chunk
··· 1 + CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC
+1
tests/testdata/data/artifacts/27/5/1730330775594233150.chunk
··· 1 + DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD