From 700c86303aa3c6d4f5dbabc998203d62aca9be40 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Tue, 26 May 2026 12:41:58 +0200 Subject: [PATCH 01/43] Add generated TUS protocol contract canary --- .../java/io/tus/java/client/TusClient.java | 2 +- .../java/io/tus/java/client/TusProtocol.java | 17 + .../client/GeneratedTusProtocolContract.java | 461 ++++++++++++++++++ .../TestGeneratedTusProtocolContract.java | 115 +++++ 4 files changed, 594 insertions(+), 1 deletion(-) create mode 100644 src/main/java/io/tus/java/client/TusProtocol.java create mode 100644 src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java create mode 100644 src/test/java/io/tus/java/client/TestGeneratedTusProtocolContract.java diff --git a/src/main/java/io/tus/java/client/TusClient.java b/src/main/java/io/tus/java/client/TusClient.java index a5cac6ad..4b56a474 100644 --- a/src/main/java/io/tus/java/client/TusClient.java +++ b/src/main/java/io/tus/java/client/TusClient.java @@ -17,7 +17,7 @@ public class TusClient { * Version of the tus protocol used by the client. The remote server needs to support this * version, too. */ - public static final String TUS_VERSION = "1.0.0"; + public static final String TUS_VERSION = TusProtocol.DEFAULT_PROTOCOL_VERSION; private URL uploadCreationURL; private Proxy proxy; diff --git a/src/main/java/io/tus/java/client/TusProtocol.java b/src/main/java/io/tus/java/client/TusProtocol.java new file mode 100644 index 00000000..b4a53891 --- /dev/null +++ b/src/main/java/io/tus/java/client/TusProtocol.java @@ -0,0 +1,17 @@ +/* + * Code generated from Transloadit API2 TUS protocol contracts; DO NOT EDIT. + * If it looks wrong, please report the issue instead of editing this file by hand; + * the source fix belongs in the protocol contract generator so all TUS clients stay in sync. + */ + +package io.tus.java.client; + +/** + * Generated TUS protocol constants used by the runtime client. + */ +final class TusProtocol { + static final String DEFAULT_PROTOCOL_VERSION = "1.0.0"; + + private TusProtocol() { + } +} diff --git a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java new file mode 100644 index 00000000..a3e9b0bc --- /dev/null +++ b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java @@ -0,0 +1,461 @@ +/* + * Code generated from Transloadit API2 TUS protocol contracts; DO NOT EDIT. + * If it looks wrong, please report the issue instead of editing this file by hand; + * the source fix belongs in the protocol contract generator so all TUS clients stay in sync. + */ + +package io.tus.java.client; + +/** + * Generated TUS protocol contract fixture used by tests. + */ +final class GeneratedTusProtocolContract { + static final GeneratedTusWireVersion[] WIRE_VERSIONS = new GeneratedTusWireVersion[] { + new GeneratedTusWireVersion( + true, + "1.0.0" + ), + }; + + static final GeneratedTusProtocolOperation[] OPERATIONS = new GeneratedTusProtocolOperation[] { + new GeneratedTusProtocolOperation( + "discoverTusCapabilities", + "capability-discovery", + "OPTIONS", + "/resumable/files/", + new GeneratedTusRequestContract( + "empty", + null, + new GeneratedTusHeaderVariant[0] + ), + new GeneratedTusResponseContract[] { + new GeneratedTusResponseContract( + 200, + "empty", + new GeneratedTusHeaderVariant[] { + new GeneratedTusHeaderVariant( + new GeneratedTusHeaderField[] { + new GeneratedTusHeaderField( + "Tus-Extension", + "tus-extension", + true + ), + new GeneratedTusHeaderField( + "Tus-Max-Size", + "tus-max-size", + true + ), + new GeneratedTusHeaderField( + "Tus-Resumable", + "tus-resumable", + true + ), + new GeneratedTusHeaderField( + "Tus-Version", + "tus-version", + true + ), + } + ), + } + ), + } + ), + new GeneratedTusProtocolOperation( + "createTusUpload", + "creation", + "POST", + "/resumable/files/", + new GeneratedTusRequestContract( + "empty", + null, + new GeneratedTusHeaderVariant[] { + new GeneratedTusHeaderVariant( + new GeneratedTusHeaderField[] { + new GeneratedTusHeaderField( + "Tus-Resumable", + "tus-resumable", + true + ), + new GeneratedTusHeaderField( + "Upload-Length", + "upload-length", + true + ), + new GeneratedTusHeaderField( + "Upload-Metadata", + "upload-metadata", + true + ), + } + ), + new GeneratedTusHeaderVariant( + new GeneratedTusHeaderField[] { + new GeneratedTusHeaderField( + "Tus-Resumable", + "tus-resumable", + true + ), + new GeneratedTusHeaderField( + "Upload-Defer-Length", + "upload-defer-length", + true + ), + new GeneratedTusHeaderField( + "Upload-Metadata", + "upload-metadata", + true + ), + } + ), + } + ), + new GeneratedTusResponseContract[] { + new GeneratedTusResponseContract( + 201, + "empty", + new GeneratedTusHeaderVariant[] { + new GeneratedTusHeaderVariant( + new GeneratedTusHeaderField[] { + new GeneratedTusHeaderField( + "Location", + "location", + true + ), + new GeneratedTusHeaderField( + "Tus-Resumable", + "tus-resumable", + true + ), + } + ), + } + ), + } + ), + new GeneratedTusProtocolOperation( + "getTusUploadOffset", + "offset-discovery", + "HEAD", + "/resumable/files/{upload_id}", + new GeneratedTusRequestContract( + "empty", + null, + new GeneratedTusHeaderVariant[] { + new GeneratedTusHeaderVariant( + new GeneratedTusHeaderField[] { + new GeneratedTusHeaderField( + "Tus-Resumable", + "tus-resumable", + true + ), + } + ), + } + ), + new GeneratedTusResponseContract[] { + new GeneratedTusResponseContract( + 200, + "empty", + new GeneratedTusHeaderVariant[] { + new GeneratedTusHeaderVariant( + new GeneratedTusHeaderField[] { + new GeneratedTusHeaderField( + "Tus-Resumable", + "tus-resumable", + true + ), + new GeneratedTusHeaderField( + "Upload-Length", + "upload-length", + true + ), + new GeneratedTusHeaderField( + "Upload-Offset", + "upload-offset", + true + ), + } + ), + new GeneratedTusHeaderVariant( + new GeneratedTusHeaderField[] { + new GeneratedTusHeaderField( + "Tus-Resumable", + "tus-resumable", + true + ), + new GeneratedTusHeaderField( + "Upload-Defer-Length", + "upload-defer-length", + true + ), + new GeneratedTusHeaderField( + "Upload-Offset", + "upload-offset", + true + ), + } + ), + } + ), + } + ), + new GeneratedTusProtocolOperation( + "patchTusUpload", + "upload-chunk", + "PATCH", + "/resumable/files/{upload_id}", + new GeneratedTusRequestContract( + "binary", + "application/offset+octet-stream", + new GeneratedTusHeaderVariant[] { + new GeneratedTusHeaderVariant( + new GeneratedTusHeaderField[] { + new GeneratedTusHeaderField( + "Content-Type", + "content-type", + true + ), + new GeneratedTusHeaderField( + "Tus-Resumable", + "tus-resumable", + true + ), + new GeneratedTusHeaderField( + "Upload-Offset", + "upload-offset", + true + ), + } + ), + } + ), + new GeneratedTusResponseContract[] { + new GeneratedTusResponseContract( + 204, + "empty", + new GeneratedTusHeaderVariant[] { + new GeneratedTusHeaderVariant( + new GeneratedTusHeaderField[] { + new GeneratedTusHeaderField( + "Tus-Resumable", + "tus-resumable", + true + ), + new GeneratedTusHeaderField( + "Upload-Offset", + "upload-offset", + true + ), + } + ), + } + ), + } + ), + new GeneratedTusProtocolOperation( + "terminateTusUpload", + "termination", + "DELETE", + "/resumable/files/{upload_id}", + new GeneratedTusRequestContract( + "empty", + null, + new GeneratedTusHeaderVariant[] { + new GeneratedTusHeaderVariant( + new GeneratedTusHeaderField[] { + new GeneratedTusHeaderField( + "Tus-Resumable", + "tus-resumable", + true + ), + } + ), + } + ), + new GeneratedTusResponseContract[] { + new GeneratedTusResponseContract( + 204, + "empty", + new GeneratedTusHeaderVariant[] { + new GeneratedTusHeaderVariant( + new GeneratedTusHeaderField[] { + new GeneratedTusHeaderField( + "Tus-Resumable", + "tus-resumable", + true + ), + } + ), + } + ), + } + ), + new GeneratedTusProtocolOperation( + "downloadTusUpload", + "download", + "GET", + "/resumable/files/{upload_id}", + new GeneratedTusRequestContract( + "empty", + null, + new GeneratedTusHeaderVariant[0] + ), + new GeneratedTusResponseContract[] { + new GeneratedTusResponseContract( + 200, + "binary", + new GeneratedTusHeaderVariant[0] + ), + } + ), + }; + + static final GeneratedTusClientFeature[] CLIENT_FEATURES = new GeneratedTusClientFeature[] { + new GeneratedTusClientFeature( + "singleUploadLifecycle", + new String[] { + "createTusUpload", + "getTusUploadOffset", + "patchTusUpload", + }, + new String[] { + "open-input-source", + "fingerprint-input", + "store-resume-url", + "retry-with-backoff", + "emit-progress", + "abort-current-request", + } + ), + new GeneratedTusClientFeature( + "terminateUpload", + new String[] { + "terminateTusUpload", + }, + new String[] { + "retry-with-backoff", + } + ), + }; + + private GeneratedTusProtocolContract() { + } + + /** + * Generated wire-version fixture. + */ + static final class GeneratedTusWireVersion { + final boolean defaultVersion; + final String value; + + GeneratedTusWireVersion(boolean defaultVersion, String value) { + this.defaultVersion = defaultVersion; + this.value = value; + } + } + + /** + * Generated HTTP header field fixture. + */ + static final class GeneratedTusHeaderField { + final String displayName; + final String name; + final boolean required; + + GeneratedTusHeaderField(String displayName, String name, boolean required) { + this.displayName = displayName; + this.name = name; + this.required = required; + } + } + + /** + * Generated alternative HTTP header set fixture. + */ + static final class GeneratedTusHeaderVariant { + final GeneratedTusHeaderField[] fields; + + GeneratedTusHeaderVariant(GeneratedTusHeaderField[] fields) { + this.fields = fields; + } + } + + /** + * Generated request contract fixture. + */ + static final class GeneratedTusRequestContract { + final String bodyKind; + final String contentType; + final GeneratedTusHeaderVariant[] headerVariants; + + GeneratedTusRequestContract( + String bodyKind, + String contentType, + GeneratedTusHeaderVariant[] headerVariants) { + this.bodyKind = bodyKind; + this.contentType = contentType; + this.headerVariants = headerVariants; + } + } + + /** + * Generated response contract fixture. + */ + static final class GeneratedTusResponseContract { + final int statusCode; + final String bodyKind; + final GeneratedTusHeaderVariant[] headerVariants; + + GeneratedTusResponseContract( + int statusCode, + String bodyKind, + GeneratedTusHeaderVariant[] headerVariants) { + this.statusCode = statusCode; + this.bodyKind = bodyKind; + this.headerVariants = headerVariants; + } + } + + /** + * Generated protocol operation fixture. + */ + static final class GeneratedTusProtocolOperation { + final String operationId; + final String role; + final String method; + final String path; + final GeneratedTusRequestContract request; + final GeneratedTusResponseContract[] responses; + + GeneratedTusProtocolOperation( + String operationId, + String role, + String method, + String path, + GeneratedTusRequestContract request, + GeneratedTusResponseContract[] responses) { + this.operationId = operationId; + this.role = role; + this.method = method; + this.path = path; + this.request = request; + this.responses = responses; + } + } + + /** + * Generated client feature fixture. + */ + static final class GeneratedTusClientFeature { + final String featureId; + final String[] operationIds; + final String[] primitives; + + GeneratedTusClientFeature(String featureId, String[] operationIds, String[] primitives) { + this.featureId = featureId; + this.operationIds = operationIds; + this.primitives = primitives; + } + } +} diff --git a/src/test/java/io/tus/java/client/TestGeneratedTusProtocolContract.java b/src/test/java/io/tus/java/client/TestGeneratedTusProtocolContract.java new file mode 100644 index 00000000..9ab98919 --- /dev/null +++ b/src/test/java/io/tus/java/client/TestGeneratedTusProtocolContract.java @@ -0,0 +1,115 @@ +package io.tus.java.client; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +/** + * Tests the generated API2 protocol contract canary. + */ +public class TestGeneratedTusProtocolContract { + + /** + * Verifies the runtime constant is sourced from the generated protocol fixture. + */ + @Test + public void testDefaultProtocolVersionMatchesRuntimeConstant() { + String generatedDefault = null; + int defaultCount = 0; + + for (GeneratedTusProtocolContract.GeneratedTusWireVersion wireVersion + : GeneratedTusProtocolContract.WIRE_VERSIONS) { + if (wireVersion.defaultVersion) { + defaultCount++; + generatedDefault = wireVersion.value; + } + } + + assertEquals(1, defaultCount); + assertEquals("1.0.0", generatedDefault); + assertEquals(generatedDefault, TusProtocol.DEFAULT_PROTOCOL_VERSION); + assertEquals(generatedDefault, TusClient.TUS_VERSION); + } + + /** + * Verifies generated request-header variants retain creation requirements. + */ + @Test + public void testCreateUploadOperationKeepsRequiredHeaders() { + GeneratedTusProtocolContract.GeneratedTusProtocolOperation operation = + findOperation("createTusUpload"); + + assertEquals("POST", operation.method); + assertEquals("/resumable/files/", operation.path); + assertEquals(2, operation.request.headerVariants.length); + assertTrue(hasRequiredHeader(operation.request.headerVariants[0], "tus-resumable")); + assertTrue(hasRequiredHeader(operation.request.headerVariants[0], "upload-length")); + assertTrue(hasRequiredHeader(operation.request.headerVariants[1], "tus-resumable")); + assertTrue(hasRequiredHeader(operation.request.headerVariants[1], "upload-defer-length")); + } + + /** + * Verifies the generated high-level lifecycle feature points at raw protocol operations. + */ + @Test + public void testSingleUploadLifecycleFeatureReferencesProtocolOperations() { + GeneratedTusProtocolContract.GeneratedTusClientFeature feature = + findFeature("singleUploadLifecycle"); + + assertContains(feature.operationIds, "createTusUpload"); + assertContains(feature.operationIds, "getTusUploadOffset"); + assertContains(feature.operationIds, "patchTusUpload"); + assertContains(feature.primitives, "store-resume-url"); + assertContains(feature.primitives, "emit-progress"); + } + + private static GeneratedTusProtocolContract.GeneratedTusProtocolOperation findOperation( + String operationId) { + for (GeneratedTusProtocolContract.GeneratedTusProtocolOperation operation + : GeneratedTusProtocolContract.OPERATIONS) { + if (operation.operationId.equals(operationId)) { + return operation; + } + } + + throw new AssertionError("Missing generated TUS operation: " + operationId); + } + + private static GeneratedTusProtocolContract.GeneratedTusClientFeature findFeature( + String featureId) { + for (GeneratedTusProtocolContract.GeneratedTusClientFeature feature + : GeneratedTusProtocolContract.CLIENT_FEATURES) { + if (feature.featureId.equals(featureId)) { + return feature; + } + } + + throw new AssertionError("Missing generated TUS client feature: " + featureId); + } + + private static boolean hasRequiredHeader( + GeneratedTusProtocolContract.GeneratedTusHeaderVariant variant, + String headerName) { + assertNotNull(variant); + + for (GeneratedTusProtocolContract.GeneratedTusHeaderField field : variant.fields) { + if (field.required && field.name.equals(headerName)) { + return true; + } + } + + return false; + } + + private static void assertContains(String[] values, String expected) { + for (String value : values) { + if (value.equals(expected)) { + return; + } + } + + throw new AssertionError("Missing generated value: " + expected); + } +} From 8ad4983b9445ffbc9616b3d3658a84f6f714cc11 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Tue, 26 May 2026 13:10:01 +0200 Subject: [PATCH 02/43] Allow manual Java client workflow runs --- .github/workflows/lintChanges.yml | 2 ++ .github/workflows/tests.yml | 2 ++ 2 files changed, 4 insertions(+) diff --git a/.github/workflows/lintChanges.yml b/.github/workflows/lintChanges.yml index 2b7069dc..ab0b8cd9 100644 --- a/.github/workflows/lintChanges.yml +++ b/.github/workflows/lintChanges.yml @@ -1,11 +1,13 @@ name: Lint Java Code on: + workflow_dispatch: push: branches: - main pull_request: types: - opened + - ready_for_review - synchronize - unlabeled jobs: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 84d25cc7..e3722daa 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -4,12 +4,14 @@ name: Tests on: + workflow_dispatch: push: branches: - main pull_request: types: - opened + - ready_for_review - synchronize - unlabeled jobs: From 19adf55ea9341ae3e6839e04fd351bba27e5d699 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Tue, 26 May 2026 21:18:33 +0200 Subject: [PATCH 03/43] Regenerate TUS protocol contract fixture --- .../client/GeneratedTusProtocolContract.java | 110 ++++++++++++++++++ .../TestGeneratedTusProtocolContract.java | 36 +++++- 2 files changed, 141 insertions(+), 5 deletions(-) diff --git a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java index a3e9b0bc..fc8000f4 100644 --- a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java +++ b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java @@ -108,6 +108,49 @@ final class GeneratedTusProtocolContract { ), } ), + new GeneratedTusHeaderVariant( + new GeneratedTusHeaderField[] { + new GeneratedTusHeaderField( + "Tus-Resumable", + "tus-resumable", + true + ), + new GeneratedTusHeaderField( + "Upload-Concat", + "upload-concat", + true + ), + new GeneratedTusHeaderField( + "Upload-Length", + "upload-length", + true + ), + new GeneratedTusHeaderField( + "Upload-Metadata", + "upload-metadata", + false + ), + } + ), + new GeneratedTusHeaderVariant( + new GeneratedTusHeaderField[] { + new GeneratedTusHeaderField( + "Tus-Resumable", + "tus-resumable", + true + ), + new GeneratedTusHeaderField( + "Upload-Concat", + "upload-concat", + true + ), + new GeneratedTusHeaderField( + "Upload-Metadata", + "upload-metadata", + false + ), + } + ), } ), new GeneratedTusResponseContract[] { @@ -328,12 +371,79 @@ final class GeneratedTusProtocolContract { "abort-current-request", } ), + new GeneratedTusClientFeature( + "resumeUpload", + new String[] { + "getTusUploadOffset", + "patchTusUpload", + }, + new String[] { + "fingerprint-input", + "resume-from-previous-upload", + "store-resume-url", + } + ), + new GeneratedTusClientFeature( + "deferredLengthUpload", + new String[] { + "createTusUpload", + "patchTusUpload", + }, + new String[] { + "defer-upload-length", + "emit-progress", + } + ), + new GeneratedTusClientFeature( + "creationWithUpload", + new String[] { + "createTusUpload", + }, + new String[] { + "upload-during-creation", + "emit-progress", + } + ), + new GeneratedTusClientFeature( + "overridePatchMethod", + new String[] { + "getTusUploadOffset", + "patchTusUpload", + }, + new String[] { + "override-patch-method", + } + ), + new GeneratedTusClientFeature( + "parallelUploadConcat", + new String[] { + "createTusUpload", + "patchTusUpload", + }, + new String[] { + "concatenate-partial-uploads", + "emit-progress", + } + ), + new GeneratedTusClientFeature( + "retryOffsetRecovery", + new String[] { + "createTusUpload", + "getTusUploadOffset", + "patchTusUpload", + }, + new String[] { + "retry-with-backoff", + "recover-offset-after-error", + } + ), new GeneratedTusClientFeature( "terminateUpload", new String[] { "terminateTusUpload", }, new String[] { + "terminate-upload", "retry-with-backoff", } ), diff --git a/src/test/java/io/tus/java/client/TestGeneratedTusProtocolContract.java b/src/test/java/io/tus/java/client/TestGeneratedTusProtocolContract.java index 9ab98919..5c5a5cd2 100644 --- a/src/test/java/io/tus/java/client/TestGeneratedTusProtocolContract.java +++ b/src/test/java/io/tus/java/client/TestGeneratedTusProtocolContract.java @@ -43,11 +43,17 @@ public void testCreateUploadOperationKeepsRequiredHeaders() { assertEquals("POST", operation.method); assertEquals("/resumable/files/", operation.path); - assertEquals(2, operation.request.headerVariants.length); - assertTrue(hasRequiredHeader(operation.request.headerVariants[0], "tus-resumable")); - assertTrue(hasRequiredHeader(operation.request.headerVariants[0], "upload-length")); - assertTrue(hasRequiredHeader(operation.request.headerVariants[1], "tus-resumable")); - assertTrue(hasRequiredHeader(operation.request.headerVariants[1], "upload-defer-length")); + assertRequiredHeaderVariant( + operation.request.headerVariants, "tus-resumable", "upload-length"); + assertRequiredHeaderVariant( + operation.request.headerVariants, "tus-resumable", "upload-defer-length"); + assertRequiredHeaderVariant( + operation.request.headerVariants, + "tus-resumable", + "upload-concat", + "upload-length"); + assertRequiredHeaderVariant( + operation.request.headerVariants, "tus-resumable", "upload-concat"); } /** @@ -103,6 +109,26 @@ private static boolean hasRequiredHeader( return false; } + private static void assertRequiredHeaderVariant( + GeneratedTusProtocolContract.GeneratedTusHeaderVariant[] variants, + String... headerNames) { + for (GeneratedTusProtocolContract.GeneratedTusHeaderVariant variant : variants) { + boolean hasAllHeaders = true; + for (String headerName : headerNames) { + if (!hasRequiredHeader(variant, headerName)) { + hasAllHeaders = false; + break; + } + } + + if (hasAllHeaders) { + return; + } + } + + throw new AssertionError("Missing generated header variant"); + } + private static void assertContains(String[] values, String expected) { for (String value : values) { if (value.equals(expected)) { From 194b752ebbf7334b663e13afbc37fa943faff4c3 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Tue, 26 May 2026 21:22:43 +0200 Subject: [PATCH 04/43] Fix generated contract lint --- .../io/tus/java/client/TestGeneratedTusProtocolContract.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/java/io/tus/java/client/TestGeneratedTusProtocolContract.java b/src/test/java/io/tus/java/client/TestGeneratedTusProtocolContract.java index 5c5a5cd2..6d57686e 100644 --- a/src/test/java/io/tus/java/client/TestGeneratedTusProtocolContract.java +++ b/src/test/java/io/tus/java/client/TestGeneratedTusProtocolContract.java @@ -4,7 +4,6 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; /** * Tests the generated API2 protocol contract canary. From 9d1277f83c652b7dfbcc0fb87149a83ffa20d52a Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Tue, 26 May 2026 22:12:27 +0200 Subject: [PATCH 05/43] Regenerate TUS feature contract fixture --- .../client/GeneratedTusProtocolContract.java | 566 +++++++++++++++++- 1 file changed, 565 insertions(+), 1 deletion(-) diff --git a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java index fc8000f4..567d7385 100644 --- a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java +++ b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java @@ -356,7 +356,37 @@ final class GeneratedTusProtocolContract { static final GeneratedTusClientFeature[] CLIENT_FEATURES = new GeneratedTusClientFeature[] { new GeneratedTusClientFeature( + new GeneratedTusClientFeatureConformance( + new String[] { + "singleUploadLifecycle", + }, + "covered-by-generated-scenario" + ), + "Create an upload, store its URL, upload bytes, and finish successfully.", "singleUploadLifecycle", + new GeneratedTusClientFeatureFlowStep[] { + new GeneratedTusClientFeatureFlowStep( + "primitive", + "", + "open-input-source", + "", + "Open the caller input as a sliceable source." + ), + new GeneratedTusClientFeatureFlowStep( + "operation", + "createTusUpload", + "", + "", + "Create the remote upload resource." + ), + new GeneratedTusClientFeatureFlowStep( + "operation", + "patchTusUpload", + "", + "", + "Upload bytes until the accepted offset reaches the known length." + ), + }, new String[] { "createTusUpload", "getTusUploadOffset", @@ -372,7 +402,37 @@ final class GeneratedTusProtocolContract { } ), new GeneratedTusClientFeature( + new GeneratedTusClientFeatureConformance( + new String[] { + "resumeFromPreviousUpload", + }, + "covered-by-generated-scenario" + ), + "Resume a stored upload URL by discovering the remote offset before patching.", "resumeUpload", + new GeneratedTusClientFeatureFlowStep[] { + new GeneratedTusClientFeatureFlowStep( + "primitive", + "", + "resume-from-previous-upload", + "", + "Load a stored upload URL selected by fingerprint." + ), + new GeneratedTusClientFeatureFlowStep( + "operation", + "getTusUploadOffset", + "", + "", + "Read the server offset for the stored upload URL." + ), + new GeneratedTusClientFeatureFlowStep( + "operation", + "patchTusUpload", + "", + "", + "Continue uploading from the discovered offset." + ), + }, new String[] { "getTusUploadOffset", "patchTusUpload", @@ -384,7 +444,37 @@ final class GeneratedTusProtocolContract { } ), new GeneratedTusClientFeature( + new GeneratedTusClientFeatureConformance( + new String[] { + "deferredLengthUpload", + }, + "covered-by-generated-scenario" + ), + "Create an upload without a known length and declare the length on final PATCH.", "deferredLengthUpload", + new GeneratedTusClientFeatureFlowStep[] { + new GeneratedTusClientFeatureFlowStep( + "operation", + "createTusUpload", + "", + "", + "Create the upload with deferred length." + ), + new GeneratedTusClientFeatureFlowStep( + "primitive", + "", + "defer-upload-length", + "", + "Track the source until the final chunk reveals the total size." + ), + new GeneratedTusClientFeatureFlowStep( + "operation", + "patchTusUpload", + "", + "", + "Declare Upload-Length on the final chunk request." + ), + }, new String[] { "createTusUpload", "patchTusUpload", @@ -395,7 +485,30 @@ final class GeneratedTusProtocolContract { } ), new GeneratedTusClientFeature( + new GeneratedTusClientFeatureConformance( + new String[] { + "creationWithUpload", + }, + "covered-by-generated-scenario" + ), + "Send the first bytes on the creation request when the server/client support it.", "creationWithUpload", + new GeneratedTusClientFeatureFlowStep[] { + new GeneratedTusClientFeatureFlowStep( + "operation", + "createTusUpload", + "", + "", + "Create the upload while streaming the initial body." + ), + new GeneratedTusClientFeatureFlowStep( + "primitive", + "", + "upload-during-creation", + "", + "Interpret the creation response as an accepted offset." + ), + }, new String[] { "createTusUpload", }, @@ -405,7 +518,37 @@ final class GeneratedTusProtocolContract { } ), new GeneratedTusClientFeature( + new GeneratedTusClientFeatureConformance( + new String[] { + "overridePatchMethod", + }, + "covered-by-generated-scenario" + ), + "Tunnel PATCH through POST with the method-override header.", "overridePatchMethod", + new GeneratedTusClientFeatureFlowStep[] { + new GeneratedTusClientFeatureFlowStep( + "operation", + "getTusUploadOffset", + "", + "", + "Resume from the upload URL before sending bytes." + ), + new GeneratedTusClientFeatureFlowStep( + "primitive", + "", + "override-patch-method", + "", + "Replace PATCH with POST while preserving the protocol operation intent." + ), + new GeneratedTusClientFeatureFlowStep( + "operation", + "patchTusUpload", + "", + "", + "Upload bytes through the overridden request." + ), + }, new String[] { "getTusUploadOffset", "patchTusUpload", @@ -415,7 +558,37 @@ final class GeneratedTusProtocolContract { } ), new GeneratedTusClientFeature( + new GeneratedTusClientFeatureConformance( + new String[] { + "parallelUploadConcat", + }, + "covered-by-generated-scenario" + ), + "Split one input into partial uploads and concatenate their upload URLs.", "parallelUploadConcat", + new GeneratedTusClientFeatureFlowStep[] { + new GeneratedTusClientFeatureFlowStep( + "primitive", + "", + "split-parallel-upload-boundaries", + "", + "Split the input into stable byte ranges." + ), + new GeneratedTusClientFeatureFlowStep( + "operation", + "createTusUpload", + "", + "", + "Create partial uploads for each range." + ), + new GeneratedTusClientFeatureFlowStep( + "primitive", + "", + "concatenate-partial-uploads", + "", + "Create the final upload from completed partial upload URLs." + ), + }, new String[] { "createTusUpload", "patchTusUpload", @@ -423,10 +596,41 @@ final class GeneratedTusProtocolContract { new String[] { "concatenate-partial-uploads", "emit-progress", + "split-parallel-upload-boundaries", } ), new GeneratedTusClientFeature( + new GeneratedTusClientFeatureConformance( + new String[] { + "retryPatchAfterOffsetRecovery", + }, + "covered-by-generated-scenario" + ), + "Recover from a failed chunk by reading the server offset before retrying.", "retryOffsetRecovery", + new GeneratedTusClientFeatureFlowStep[] { + new GeneratedTusClientFeatureFlowStep( + "operation", + "patchTusUpload", + "", + "", + "Attempt the chunk upload." + ), + new GeneratedTusClientFeatureFlowStep( + "primitive", + "", + "recover-offset-after-error", + "", + "Discover the accepted offset after a retryable failure." + ), + new GeneratedTusClientFeatureFlowStep( + "operation", + "getTusUploadOffset", + "", + "", + "Use HEAD to recover the offset before retrying PATCH." + ), + }, new String[] { "createTusUpload", "getTusUploadOffset", @@ -438,7 +642,30 @@ final class GeneratedTusProtocolContract { } ), new GeneratedTusClientFeature( + new GeneratedTusClientFeatureConformance( + new String[] { + "terminateWithRetry", + }, + "covered-by-generated-scenario" + ), + "Terminate an upload resource and retry retryable termination failures.", "terminateUpload", + new GeneratedTusClientFeatureFlowStep[] { + new GeneratedTusClientFeatureFlowStep( + "primitive", + "", + "terminate-upload", + "", + "Choose server-side termination for an upload URL." + ), + new GeneratedTusClientFeatureFlowStep( + "operation", + "terminateTusUpload", + "", + "", + "Delete the upload resource." + ), + }, new String[] { "terminateTusUpload", }, @@ -447,6 +674,294 @@ final class GeneratedTusProtocolContract { "retry-with-backoff", } ), + new GeneratedTusClientFeature( + new GeneratedTusClientFeatureConformance( + new String[0], + "needs-generated-scenario" + ), + "Abort the active request, pending retry timer, and any partial uploads.", + "abortUpload", + new GeneratedTusClientFeatureFlowStep[] { + new GeneratedTusClientFeatureFlowStep( + "primitive", + "", + "abort-current-request", + "", + "Cancel in-flight transport work without emitting user callbacks after abort." + ), + }, + new String[0], + new String[] { + "abort-current-request", + } + ), + new GeneratedTusClientFeature( + new GeneratedTusClientFeatureConformance( + new String[0], + "needs-generated-scenario" + ), + "Expose progress and accepted-chunk callbacks from runtime upload activity.", + "uploadCallbacks", + new GeneratedTusClientFeatureFlowStep[] { + new GeneratedTusClientFeatureFlowStep( + "primitive", + "", + "emit-progress", + "", + "Report bytes sent against known or deferred length." + ), + new GeneratedTusClientFeatureFlowStep( + "primitive", + "", + "emit-chunk-complete", + "", + "Report chunk size, accepted offset, and total size after server acceptance." + ), + new GeneratedTusClientFeatureFlowStep( + "primitive", + "", + "emit-upload-url", + "", + "Notify once a usable upload URL is known." + ), + }, + new String[0], + new String[] { + "emit-progress", + "emit-chunk-complete", + "emit-upload-url", + } + ), + new GeneratedTusClientFeature( + new GeneratedTusClientFeatureConformance( + new String[0], + "needs-generated-scenario" + ), + "Run before-request, after-response, and custom retry hooks around transport.", + "requestLifecycleHooks", + new GeneratedTusClientFeatureFlowStep[] { + new GeneratedTusClientFeatureFlowStep( + "primitive", + "", + "run-request-hooks", + "", + "Call user hooks around each HTTP request/response pair." + ), + new GeneratedTusClientFeatureFlowStep( + "primitive", + "", + "customize-retry", + "", + "Let user retry policy override default retry decisions." + ), + }, + new String[0], + new String[] { + "customize-retry", + "run-request-hooks", + } + ), + new GeneratedTusClientFeature( + new GeneratedTusClientFeatureConformance( + new String[0], + "needs-generated-scenario" + ), + "Persist, find, resume, and optionally remove upload URLs by fingerprint.", + "resumeUrlStorage", + new GeneratedTusClientFeatureFlowStep[] { + new GeneratedTusClientFeatureFlowStep( + "primitive", + "", + "fingerprint-input", + "", + "Derive a stable key for the input when possible." + ), + new GeneratedTusClientFeatureFlowStep( + "primitive", + "", + "store-resume-url", + "", + "Persist upload URLs and partial-upload URLs for future resumption." + ), + new GeneratedTusClientFeatureFlowStep( + "primitive", + "", + "remove-stored-url-on-success", + "", + "Remove stored upload URLs when configured after success or invalidation." + ), + }, + new String[0], + new String[] { + "fingerprint-input", + "store-resume-url", + "remove-stored-url-on-success", + } + ), + new GeneratedTusClientFeature( + new GeneratedTusClientFeatureConformance( + new String[0], + "needs-generated-scenario" + ), + "Support the reference client input/source families across runtimes.", + "inputSources", + new GeneratedTusClientFeatureFlowStep[] { + new GeneratedTusClientFeatureFlowStep( + "primitive", + "", + "read-browser-file", + "", + "Read browser Blob/File and ArrayBuffer-family inputs." + ), + new GeneratedTusClientFeatureFlowStep( + "primitive", + "", + "read-node-stream", + "", + "Read Node streams when size and chunk constraints are satisfied." + ), + new GeneratedTusClientFeatureFlowStep( + "primitive", + "", + "read-web-stream", + "", + "Read Web Streams with deferred or configured size." + ), + new GeneratedTusClientFeatureFlowStep( + "primitive", + "", + "read-node-file", + "", + "Read filesystem paths and fs streams, including parallel ranges." + ), + }, + new String[0], + new String[] { + "read-browser-file", + "read-node-file", + "read-node-stream", + "read-web-stream", + } + ), + new GeneratedTusClientFeature( + new GeneratedTusClientFeatureConformance( + new String[0], + "needs-generated-scenario" + ), + "Support browser and file-backed URL storage implementations.", + "urlStorageBackends", + new GeneratedTusClientFeatureFlowStep[] { + new GeneratedTusClientFeatureFlowStep( + "primitive", + "", + "store-browser-url", + "", + "Persist upload records in browser localStorage." + ), + new GeneratedTusClientFeatureFlowStep( + "primitive", + "", + "store-file-url", + "", + "Persist upload records in the Node file store." + ), + }, + new String[0], + new String[] { + "store-browser-url", + "store-file-url", + } + ), + new GeneratedTusClientFeature( + new GeneratedTusClientFeatureConformance( + new String[0], + "needs-generated-scenario" + ), + "Select between tus v1 and supported IETF draft client protocol modes.", + "protocolVersionSelection", + new GeneratedTusClientFeatureFlowStep[] { + new GeneratedTusClientFeatureFlowStep( + "primitive", + "", + "select-client-protocol", + "", + "Choose request headers and response expectations for the selected protocol." + ), + }, + new String[] { + "createTusUpload", + "getTusUploadOffset", + "patchTusUpload", + }, + new String[] { + "select-client-protocol", + } + ), + new GeneratedTusClientFeature( + new GeneratedTusClientFeatureConformance( + new String[0], + "needs-generated-scenario" + ), + "Normalize relative Location headers against the request endpoint.", + "relativeLocationResolution", + new GeneratedTusClientFeatureFlowStep[] { + new GeneratedTusClientFeatureFlowStep( + "primitive", + "", + "resolve-relative-location", + "", + "Resolve server Location headers with the creation endpoint as origin." + ), + }, + new String[] { + "createTusUpload", + }, + new String[] { + "resolve-relative-location", + } + ), + new GeneratedTusClientFeature( + new GeneratedTusClientFeatureConformance( + new String[0], + "needs-generated-scenario" + ), + "Validate option combinations before starting runtime work.", + "startOptionValidation", + new GeneratedTusClientFeatureFlowStep[] { + new GeneratedTusClientFeatureFlowStep( + "primitive", + "", + "validate-start-options", + "", + "Reject missing inputs and incompatible parallel/deferred/resume options." + ), + }, + new String[0], + new String[] { + "validate-start-options", + } + ), + new GeneratedTusClientFeature( + new GeneratedTusClientFeatureConformance( + new String[0], + "needs-generated-scenario" + ), + "Attach request, response, status, body, and request ID context to errors.", + "detailedErrors", + new GeneratedTusClientFeatureFlowStep[] { + new GeneratedTusClientFeatureFlowStep( + "primitive", + "", + "report-detailed-errors", + "", + "Return user-facing errors with enough transport context for debugging." + ), + }, + new String[0], + new String[] { + "report-detailed-errors", + } + ), }; private GeneratedTusProtocolContract() { @@ -558,14 +1073,63 @@ static final class GeneratedTusProtocolOperation { * Generated client feature fixture. */ static final class GeneratedTusClientFeature { + final GeneratedTusClientFeatureConformance conformance; + final String description; final String featureId; + final GeneratedTusClientFeatureFlowStep[] flow; final String[] operationIds; final String[] primitives; - GeneratedTusClientFeature(String featureId, String[] operationIds, String[] primitives) { + GeneratedTusClientFeature( + GeneratedTusClientFeatureConformance conformance, + String description, + String featureId, + GeneratedTusClientFeatureFlowStep[] flow, + String[] operationIds, + String[] primitives) { + this.conformance = conformance; + this.description = description; this.featureId = featureId; + this.flow = flow; this.operationIds = operationIds; this.primitives = primitives; } } + + /** + * Generated client feature conformance coverage. + */ + static final class GeneratedTusClientFeatureConformance { + final String[] scenarioIds; + final String status; + + GeneratedTusClientFeatureConformance(String[] scenarioIds, String status) { + this.scenarioIds = scenarioIds; + this.status = status; + } + } + + /** + * Generated client feature flow step. + */ + static final class GeneratedTusClientFeatureFlowStep { + final String kind; + final String operationId; + final String primitive; + final String condition; + final String summary; + + GeneratedTusClientFeatureFlowStep( + String kind, + String operationId, + String primitive, + String condition, + String summary) { + this.kind = kind; + this.operationId = operationId; + this.primitive = primitive; + this.condition = condition; + this.summary = summary; + } + } } From 210974e28e1c8d0486e0ee84912b5bacac952ee5 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Wed, 27 May 2026 11:35:33 +0200 Subject: [PATCH 06/43] Regenerate upload body protocol fixture --- .../client/GeneratedTusProtocolContract.java | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java index 567d7385..4ac47ba2 100644 --- a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java +++ b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java @@ -517,6 +517,39 @@ final class GeneratedTusProtocolContract { "emit-progress", } ), + new GeneratedTusClientFeature( + new GeneratedTusClientFeatureConformance( + new String[] { + "uploadBodyHeaders", + }, + "covered-by-generated-scenario" + ), + "Send protocol-specific upload body headers whenever the client transmits file bytes.", + "uploadBodyHeaders", + new GeneratedTusClientFeatureFlowStep[] { + new GeneratedTusClientFeatureFlowStep( + "primitive", + "", + "send-upload-body-headers", + "", + "Attach the protocol-specific upload body content type when a request has bytes." + ), + new GeneratedTusClientFeatureFlowStep( + "operation", + "patchTusUpload", + "", + "", + "Upload bytes with the protocol-specific body headers." + ), + }, + new String[] { + "createTusUpload", + "patchTusUpload", + }, + new String[] { + "send-upload-body-headers", + } + ), new GeneratedTusClientFeature( new GeneratedTusClientFeatureConformance( new String[] { From 6e7a32a3aa6833131e9c40017f1ec289aab8e770 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Thu, 28 May 2026 22:39:50 +0200 Subject: [PATCH 07/43] Assert generated TUS upload events --- .../io/tus/java/client/GeneratedTusProtocolContract.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java index 4ac47ba2..00c3cc59 100644 --- a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java +++ b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java @@ -730,8 +730,12 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientFeature( new GeneratedTusClientFeatureConformance( - new String[0], - "needs-generated-scenario" + new String[] { + "singleUploadLifecycle", + "creationWithUpload", + "resumeFromPreviousUpload", + }, + "covered-by-generated-scenario" ), "Expose progress and accepted-chunk callbacks from runtime upload activity.", "uploadCallbacks", From d97c79c1f732f60a6b92c0d2866947a5d4bfcdec Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Thu, 28 May 2026 23:19:53 +0200 Subject: [PATCH 08/43] Cover TUS request lifecycle conformance --- .../io/tus/java/client/GeneratedTusProtocolContract.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java index 00c3cc59..08a16376 100644 --- a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java +++ b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java @@ -771,8 +771,11 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientFeature( new GeneratedTusClientFeatureConformance( - new String[0], - "needs-generated-scenario" + new String[] { + "requestLifecycleHooks", + "retryPatchAfterOffsetRecovery", + }, + "covered-by-generated-scenario" ), "Run before-request, after-response, and custom retry hooks around transport.", "requestLifecycleHooks", From d8b4e3856d4348596be53aab528538f6a880c6c0 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Fri, 29 May 2026 07:06:10 +0200 Subject: [PATCH 09/43] Cover TUS abort conformance --- .../io/tus/java/client/GeneratedTusProtocolContract.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java index 08a16376..74302ada 100644 --- a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java +++ b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java @@ -709,8 +709,10 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientFeature( new GeneratedTusClientFeatureConformance( - new String[0], - "needs-generated-scenario" + new String[] { + "abortUpload", + }, + "covered-by-generated-scenario" ), "Abort the active request, pending retry timer, and any partial uploads.", "abortUpload", From a211c170de64f9820abbd0c5b7e100bdaab2cdc3 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Fri, 29 May 2026 07:14:09 +0200 Subject: [PATCH 10/43] Cover TUS URL storage conformance --- .../io/tus/java/client/GeneratedTusProtocolContract.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java index 74302ada..5637c3e6 100644 --- a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java +++ b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java @@ -805,8 +805,11 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientFeature( new GeneratedTusClientFeatureConformance( - new String[0], - "needs-generated-scenario" + new String[] { + "singleUploadLifecycle", + "resumeFromPreviousUpload", + }, + "covered-by-generated-scenario" ), "Persist, find, resume, and optionally remove upload URLs by fingerprint.", "resumeUrlStorage", From 22e352beda1ed800c1b4d4ba3313e89af1315e2c Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Fri, 29 May 2026 07:19:28 +0200 Subject: [PATCH 11/43] Cover TUS relative Location conformance --- .../io/tus/java/client/GeneratedTusProtocolContract.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java index 5637c3e6..088580d5 100644 --- a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java +++ b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java @@ -944,8 +944,10 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientFeature( new GeneratedTusClientFeatureConformance( - new String[0], - "needs-generated-scenario" + new String[] { + "relativeLocationResolution", + }, + "covered-by-generated-scenario" ), "Normalize relative Location headers against the request endpoint.", "relativeLocationResolution", From 2c32dd5b5b211f5e4ea7cd524e9f19b61ffd796b Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Fri, 29 May 2026 07:48:41 +0200 Subject: [PATCH 12/43] Refresh TUS input source contract --- .../tus/java/client/GeneratedTusProtocolContract.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java index 088580d5..1af62d52 100644 --- a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java +++ b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java @@ -845,8 +845,14 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientFeature( new GeneratedTusClientFeatureConformance( - new String[0], - "needs-generated-scenario" + new String[] { + "arrayBufferInput", + "arrayBufferViewInput", + "webReadableStreamInput", + "nodeReadableStreamInput", + "nodePathInput", + }, + "covered-by-generated-scenario" ), "Support the reference client input/source families across runtimes.", "inputSources", From db63d33c96bc53f6b8a4d7e91f061f7c5341a67d Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Fri, 29 May 2026 18:10:38 +0200 Subject: [PATCH 13/43] Refresh TUS retry state contract --- .../client/GeneratedTusProtocolContract.java | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java index 1af62d52..9a50e54e 100644 --- a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java +++ b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java @@ -674,6 +674,41 @@ final class GeneratedTusProtocolContract { "recover-offset-after-error", } ), + new GeneratedTusClientFeature( + new GeneratedTusClientFeatureConformance( + new String[] { + "retryPatchAfterOffsetRecovery", + }, + "covered-by-generated-scenario" + ), + "Schedule retry timers and reset retry attempts after accepted progress.", + "retryStateTransitions", + new GeneratedTusClientFeatureFlowStep[] { + new GeneratedTusClientFeatureFlowStep( + "primitive", + "", + "schedule-retry-timer", + "", + "Consume the current retry delay and restart the upload after that timer fires." + ), + new GeneratedTusClientFeatureFlowStep( + "primitive", + "", + "reset-retry-attempt-after-progress", + "", + "Reset retry attempts once a later retry observes server-side offset progress." + ), + }, + new String[] { + "getTusUploadOffset", + "patchTusUpload", + }, + new String[] { + "retry-with-backoff", + "schedule-retry-timer", + "reset-retry-attempt-after-progress", + } + ), new GeneratedTusClientFeature( new GeneratedTusClientFeatureConformance( new String[] { From 296da7cd0e0b049e47bca7b7d1489bdc5e099b03 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Fri, 29 May 2026 20:09:07 +0200 Subject: [PATCH 14/43] Refresh TUS URL storage contract --- .../io/tus/java/client/GeneratedTusProtocolContract.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java index 9a50e54e..48580efe 100644 --- a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java +++ b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java @@ -931,8 +931,11 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientFeature( new GeneratedTusClientFeatureConformance( - new String[0], - "needs-generated-scenario" + new String[] { + "webStorageUrlStorageBackend", + "fileUrlStorageBackend", + }, + "covered-by-generated-scenario" ), "Support browser and file-backed URL storage implementations.", "urlStorageBackends", From c9338d9ed35c41c65e82875588812b353713524f Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Fri, 29 May 2026 21:06:25 +0200 Subject: [PATCH 15/43] Refresh TUS protocol selection contract --- .../io/tus/java/client/GeneratedTusProtocolContract.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java index 48580efe..c33f1f1c 100644 --- a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java +++ b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java @@ -963,8 +963,11 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientFeature( new GeneratedTusClientFeatureConformance( - new String[0], - "needs-generated-scenario" + new String[] { + "ietfDraft05CreationWithUpload", + "ietfDraft03ResumeWithoutKnownLength", + }, + "covered-by-generated-scenario" ), "Select between tus v1 and supported IETF draft client protocol modes.", "protocolVersionSelection", From bead997e74f2906dd572b23ffdb8fe72042bfb66 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Fri, 29 May 2026 22:26:26 +0200 Subject: [PATCH 16/43] Refresh TUS start validation contract --- .../java/client/GeneratedTusProtocolContract.java | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java index c33f1f1c..cbcc5661 100644 --- a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java +++ b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java @@ -1016,8 +1016,18 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientFeature( new GeneratedTusClientFeatureConformance( - new String[0], - "needs-generated-scenario" + new String[] { + "startValidationMissingInput", + "startValidationMissingEndpointOrUploadUrl", + "startValidationUnsupportedProtocol", + "startValidationRetryDelaysNotArray", + "startValidationParallelUploadsWithUploadUrl", + "startValidationParallelUploadsWithUploadSize", + "startValidationParallelUploadsWithDeferredLength", + "startValidationParallelBoundariesWithoutParallelUploads", + "startValidationParallelBoundariesLengthMismatch", + }, + "covered-by-generated-scenario" ), "Validate option combinations before starting runtime work.", "startOptionValidation", From 0c72eabab94c403cfc2b31522b7fff6790990601 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Fri, 29 May 2026 23:10:16 +0200 Subject: [PATCH 17/43] Update detailed error conformance --- .../io/tus/java/client/GeneratedTusProtocolContract.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java index cbcc5661..4466303d 100644 --- a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java +++ b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java @@ -1047,8 +1047,11 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientFeature( new GeneratedTusClientFeatureConformance( - new String[0], - "needs-generated-scenario" + new String[] { + "detailedCreateResponseError", + "detailedCreateRequestError", + }, + "covered-by-generated-scenario" ), "Attach request, response, status, body, and request ID context to errors.", "detailedErrors", From 84862fb8674ef6408b3cea5f1b70e31a07d92293 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Sun, 31 May 2026 13:41:05 +0200 Subject: [PATCH 18/43] Expose generated conformance scenarios --- .../client/GeneratedTusProtocolContract.java | 715 +++++++++++++++++- .../TestGeneratedTusProtocolContract.java | 32 + 2 files changed, 745 insertions(+), 2 deletions(-) diff --git a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java index 4466303d..652d718a 100644 --- a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java +++ b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java @@ -488,6 +488,7 @@ final class GeneratedTusProtocolContract { new GeneratedTusClientFeatureConformance( new String[] { "creationWithUpload", + "creationWithUploadPartialChunk", }, "covered-by-generated-scenario" ), @@ -511,6 +512,7 @@ final class GeneratedTusProtocolContract { }, new String[] { "createTusUpload", + "patchTusUpload", }, new String[] { "upload-during-creation", @@ -550,6 +552,46 @@ final class GeneratedTusProtocolContract { "send-upload-body-headers", } ), + new GeneratedTusClientFeature( + new GeneratedTusClientFeatureConformance( + new String[] { + "customRequestHeaders", + }, + "covered-by-generated-scenario" + ), + "Apply user-provided request headers to every upload request.", + "customRequestHeaders", + new GeneratedTusClientFeatureFlowStep[] { + new GeneratedTusClientFeatureFlowStep( + "primitive", + "", + "apply-custom-request-headers", + "", + "Merge user-provided headers after protocol headers are prepared." + ), + new GeneratedTusClientFeatureFlowStep( + "operation", + "createTusUpload", + "", + "", + "Create uploads with the configured custom headers." + ), + new GeneratedTusClientFeatureFlowStep( + "operation", + "patchTusUpload", + "", + "", + "Upload bytes with the configured custom headers." + ), + }, + new String[] { + "createTusUpload", + "patchTusUpload", + }, + new String[] { + "apply-custom-request-headers", + } + ), new GeneratedTusClientFeature( new GeneratedTusClientFeatureConformance( new String[] { @@ -594,10 +636,11 @@ final class GeneratedTusProtocolContract { new GeneratedTusClientFeatureConformance( new String[] { "parallelUploadConcat", + "parallelUploadAbortCleanup", }, "covered-by-generated-scenario" ), - "Split one input into partial uploads and concatenate their upload URLs.", + "Split one input into partial uploads, run the parts concurrently, clean up aborted parts, and concatenate their upload URLs.", "parallelUploadConcat", new GeneratedTusClientFeatureFlowStep[] { new GeneratedTusClientFeatureFlowStep( @@ -630,6 +673,7 @@ final class GeneratedTusProtocolContract { "concatenate-partial-uploads", "emit-progress", "split-parallel-upload-boundaries", + "terminate-upload", } ), new GeneratedTusClientFeature( @@ -746,6 +790,7 @@ final class GeneratedTusProtocolContract { new GeneratedTusClientFeatureConformance( new String[] { "abortUpload", + "abortUploadAfterStoredUrl", }, "covered-by-generated-scenario" ), @@ -760,9 +805,12 @@ final class GeneratedTusProtocolContract { "Cancel in-flight transport work without emitting user callbacks after abort." ), }, - new String[0], + new String[] { + "terminateTusUpload", + }, new String[] { "abort-current-request", + "terminate-upload", } ), new GeneratedTusClientFeature( @@ -1024,6 +1072,7 @@ final class GeneratedTusProtocolContract { "startValidationParallelUploadsWithUploadUrl", "startValidationParallelUploadsWithUploadSize", "startValidationParallelUploadsWithDeferredLength", + "startValidationParallelUploadsWithUploadDataDuringCreation", "startValidationParallelBoundariesWithoutParallelUploads", "startValidationParallelBoundariesLengthMismatch", }, @@ -1071,6 +1120,635 @@ final class GeneratedTusProtocolContract { ), }; + static final GeneratedTusClientConformanceScenario[] CLIENT_CONFORMANCE_SCENARIOS = + new GeneratedTusClientConformanceScenario[] { + new GeneratedTusClientConformanceScenario( + "single-upload-lifecycle", + "success", + null, + "singleUploadLifecycle", + "singleUploadLifecycle", + new String[] { + "createTusUpload", + "patchTusUpload", + }, + new String[] { + "open-input-source", + "fingerprint-input", + "store-resume-url", + "retry-with-backoff", + "emit-progress", + "abort-current-request", + }, + new String[] { + "fingerprint:contract-single-fingerprint", + "upload-url-available", + "url-storage-add:contract-single-fingerprint:https://tus.io/uploads/generated-contract", + "progress:0:11", + "progress:11:11", + "chunk-complete:11:11:11", + "success", + "source-close", + } + ), + new GeneratedTusClientConformanceScenario( + "creation-with-upload", + "success", + null, + "creationWithUpload", + "creationWithUpload", + new String[] { + "createTusUpload", + }, + new String[] { + "upload-during-creation", + "emit-progress", + }, + new String[] { + "progress:0:11", + "progress:11:11", + "upload-url-available", + "success", + "source-close", + } + ), + new GeneratedTusClientConformanceScenario( + "creation-with-upload-partial-chunk", + "success", + null, + "creationWithUpload", + "creationWithUploadPartialChunk", + new String[] { + "createTusUpload", + "patchTusUpload", + }, + new String[] { + "upload-during-creation", + "emit-progress", + }, + new String[] { + "progress:0:11", + "progress:5:11", + "upload-url-available", + "progress:5:11", + "progress:10:11", + "chunk-complete:5:10:11", + "progress:10:11", + "progress:11:11", + "chunk-complete:1:11:11", + "success", + "source-close", + } + ), + new GeneratedTusClientConformanceScenario( + "creation-with-upload", + "success", + null, + "protocolVersionSelection", + "ietfDraft05CreationWithUpload", + new String[] { + "createTusUpload", + }, + new String[] { + "select-client-protocol", + }, + new String[] { + "progress:0:11", + "progress:11:11", + "upload-url-available", + "success", + "source-close", + } + ), + new GeneratedTusClientConformanceScenario( + "upload-body-headers", + "success", + null, + "protocolVersionSelection", + "ietfDraft03ResumeWithoutKnownLength", + new String[] { + "getTusUploadOffset", + "patchTusUpload", + }, + new String[] { + "select-client-protocol", + }, + new String[] { + "upload-url-available", + "progress:5:11", + "progress:11:11", + "chunk-complete:6:11:11", + "success", + "source-close", + } + ), + new GeneratedTusClientConformanceScenario( + "start-option-validation", + "error", + "missingInput", + "startOptionValidation", + "startValidationMissingInput", + new String[0], + new String[] { + "validate-start-options", + }, + new String[0] + ), + new GeneratedTusClientConformanceScenario( + "start-option-validation", + "error", + "missingEndpointOrUploadUrl", + "startOptionValidation", + "startValidationMissingEndpointOrUploadUrl", + new String[0], + new String[] { + "validate-start-options", + }, + new String[0] + ), + new GeneratedTusClientConformanceScenario( + "start-option-validation", + "error", + "unsupportedProtocol", + "startOptionValidation", + "startValidationUnsupportedProtocol", + new String[0], + new String[] { + "validate-start-options", + }, + new String[0] + ), + new GeneratedTusClientConformanceScenario( + "start-option-validation", + "error", + "retryDelaysNotArray", + "startOptionValidation", + "startValidationRetryDelaysNotArray", + new String[0], + new String[] { + "validate-start-options", + }, + new String[0] + ), + new GeneratedTusClientConformanceScenario( + "start-option-validation", + "error", + "parallelUploadsWithUploadUrl", + "startOptionValidation", + "startValidationParallelUploadsWithUploadUrl", + new String[0], + new String[] { + "validate-start-options", + }, + new String[0] + ), + new GeneratedTusClientConformanceScenario( + "start-option-validation", + "error", + "parallelUploadsWithUploadSize", + "startOptionValidation", + "startValidationParallelUploadsWithUploadSize", + new String[0], + new String[] { + "validate-start-options", + }, + new String[0] + ), + new GeneratedTusClientConformanceScenario( + "start-option-validation", + "error", + "parallelUploadsWithDeferredLength", + "startOptionValidation", + "startValidationParallelUploadsWithDeferredLength", + new String[0], + new String[] { + "validate-start-options", + }, + new String[0] + ), + new GeneratedTusClientConformanceScenario( + "start-option-validation", + "error", + "parallelUploadsWithUploadDataDuringCreation", + "startOptionValidation", + "startValidationParallelUploadsWithUploadDataDuringCreation", + new String[0], + new String[] { + "validate-start-options", + }, + new String[0] + ), + new GeneratedTusClientConformanceScenario( + "start-option-validation", + "error", + "parallelBoundariesWithoutParallelUploads", + "startOptionValidation", + "startValidationParallelBoundariesWithoutParallelUploads", + new String[0], + new String[] { + "validate-start-options", + }, + new String[0] + ), + new GeneratedTusClientConformanceScenario( + "start-option-validation", + "error", + "parallelBoundariesLengthMismatch", + "startOptionValidation", + "startValidationParallelBoundariesLengthMismatch", + new String[0], + new String[] { + "validate-start-options", + }, + new String[0] + ), + new GeneratedTusClientConformanceScenario( + "detailed-error", + "error", + "unexpectedCreateResponse", + "detailedErrors", + "detailedCreateResponseError", + new String[] { + "createTusUpload", + }, + new String[] { + "report-detailed-errors", + }, + new String[0] + ), + new GeneratedTusClientConformanceScenario( + "detailed-error", + "error", + "createUploadRequestFailed", + "detailedErrors", + "detailedCreateRequestError", + new String[] { + "createTusUpload", + }, + new String[] { + "report-detailed-errors", + }, + new String[0] + ), + new GeneratedTusClientConformanceScenario( + "upload-body-headers", + "success", + null, + "uploadBodyHeaders", + "uploadBodyHeaders", + new String[] { + "createTusUpload", + "patchTusUpload", + }, + new String[] { + "send-upload-body-headers", + }, + new String[0] + ), + new GeneratedTusClientConformanceScenario( + "custom-request-headers", + "success", + null, + "customRequestHeaders", + "customRequestHeaders", + new String[] { + "createTusUpload", + "patchTusUpload", + }, + new String[] { + "apply-custom-request-headers", + }, + new String[0] + ), + new GeneratedTusClientConformanceScenario( + "resume-from-previous-upload", + "success", + null, + "resumeUpload", + "resumeFromPreviousUpload", + new String[] { + "getTusUploadOffset", + "patchTusUpload", + }, + new String[] { + "fingerprint-input", + "resume-from-previous-upload", + "store-resume-url", + }, + new String[] { + "fingerprint:contract-resume-fingerprint", + "url-storage-find:contract-resume-fingerprint:1", + "fingerprint:contract-resume-fingerprint", + "upload-url-available", + "progress:5:11", + "progress:11:11", + "chunk-complete:6:11:11", + "url-storage-remove:tus::contract-resume-fingerprint::1337", + "success", + "source-close", + } + ), + new GeneratedTusClientConformanceScenario( + "relative-location-resolution", + "success", + null, + "relativeLocationResolution", + "relativeLocationResolution", + new String[] { + "createTusUpload", + "patchTusUpload", + }, + new String[] { + "resolve-relative-location", + }, + new String[] { + "upload-url-available", + "progress:0:11", + "progress:11:11", + "chunk-complete:11:11:11", + "success", + "source-close", + } + ), + new GeneratedTusClientConformanceScenario( + "array-buffer-input", + "success", + null, + "inputSources", + "arrayBufferInput", + new String[] { + "createTusUpload", + "patchTusUpload", + }, + new String[] { + "read-browser-file", + }, + new String[] { + "source-open:array-buffer:11", + "success", + "source-close", + } + ), + new GeneratedTusClientConformanceScenario( + "array-buffer-view-input", + "success", + null, + "inputSources", + "arrayBufferViewInput", + new String[] { + "createTusUpload", + "patchTusUpload", + }, + new String[] { + "read-browser-file", + }, + new String[] { + "source-open:array-buffer-view:11", + "success", + "source-close", + } + ), + new GeneratedTusClientConformanceScenario( + "web-readable-stream-input", + "success", + null, + "inputSources", + "webReadableStreamInput", + new String[] { + "createTusUpload", + "patchTusUpload", + }, + new String[] { + "read-web-stream", + }, + new String[] { + "source-open:web-readable-stream:null", + "success", + "source-close", + } + ), + new GeneratedTusClientConformanceScenario( + "node-readable-stream-input", + "success", + null, + "inputSources", + "nodeReadableStreamInput", + new String[] { + "createTusUpload", + "patchTusUpload", + }, + new String[] { + "read-node-stream", + }, + new String[] { + "source-open:node-readable-stream:null", + "success", + "source-close", + } + ), + new GeneratedTusClientConformanceScenario( + "node-path-input", + "success", + null, + "inputSources", + "nodePathInput", + new String[] { + "createTusUpload", + "patchTusUpload", + }, + new String[] { + "read-node-file", + }, + new String[] { + "source-open:node-path-reference:11", + "success", + "source-close", + } + ), + new GeneratedTusClientConformanceScenario( + "deferred-length-upload", + "success", + null, + "deferredLengthUpload", + "deferredLengthUpload", + new String[] { + "createTusUpload", + "patchTusUpload", + }, + new String[] { + "defer-upload-length", + "emit-progress", + }, + new String[] { + "upload-url-available", + "progress:0:11", + "progress:11:11", + "chunk-complete:11:11:11", + "success", + "source-close", + } + ), + new GeneratedTusClientConformanceScenario( + "override-patch-method", + "success", + null, + "overridePatchMethod", + "overridePatchMethod", + new String[] { + "getTusUploadOffset", + "patchTusUpload", + }, + new String[] { + "override-patch-method", + }, + new String[0] + ), + new GeneratedTusClientConformanceScenario( + "parallel-upload-concat", + "success", + null, + "parallelUploadConcat", + "parallelUploadConcat", + new String[] { + "createTusUpload", + "createTusUpload", + "patchTusUpload", + "patchTusUpload", + "createTusUpload", + }, + new String[] { + "concatenate-partial-uploads", + "emit-progress", + }, + new String[] { + "progress:5:11", + "chunk-complete:5:5:11", + "progress:11:11", + "chunk-complete:6:11:11", + } + ), + new GeneratedTusClientConformanceScenario( + "parallel-upload-abort-cleanup", + "aborted", + null, + "parallelUploadConcat", + "parallelUploadAbortCleanup", + new String[] { + "createTusUpload", + "createTusUpload", + "patchTusUpload", + "patchTusUpload", + "terminateTusUpload", + "terminateTusUpload", + }, + new String[] { + "abort-current-request", + "terminate-upload", + "concatenate-partial-uploads", + }, + new String[] { + "request-abort:3", + } + ), + new GeneratedTusClientConformanceScenario( + "retry-patch-after-offset-recovery", + "success", + null, + "retryOffsetRecovery", + "retryPatchAfterOffsetRecovery", + new String[] { + "createTusUpload", + "patchTusUpload", + "getTusUploadOffset", + "patchTusUpload", + "getTusUploadOffset", + "patchTusUpload", + }, + new String[] { + "retry-with-backoff", + "recover-offset-after-error", + }, + new String[] { + "should-retry:0:true", + "retry-schedule:0", + "should-retry:0:true", + "retry-schedule:0", + } + ), + new GeneratedTusClientConformanceScenario( + "request-lifecycle-hooks", + "success", + null, + "requestLifecycleHooks", + "requestLifecycleHooks", + new String[] { + "getTusUploadOffset", + }, + new String[] { + "run-request-hooks", + }, + new String[] { + "before-request:0", + "after-response:0", + "success", + "source-close", + } + ), + new GeneratedTusClientConformanceScenario( + "abort-upload", + "aborted", + null, + "abortUpload", + "abortUpload", + new String[] { + "createTusUpload", + }, + new String[] { + "abort-current-request", + }, + new String[] { + "request-abort:0", + } + ), + new GeneratedTusClientConformanceScenario( + "abort-upload-after-stored-url", + "aborted", + null, + "abortUpload", + "abortUploadAfterStoredUrl", + new String[] { + "createTusUpload", + "patchTusUpload", + "terminateTusUpload", + }, + new String[] { + "abort-current-request", + "terminate-upload", + }, + new String[] { + "request-abort:1", + } + ), + new GeneratedTusClientConformanceScenario( + "terminate-with-retry", + "terminated", + null, + "terminateUpload", + "terminateWithRetry", + new String[] { + "createTusUpload", + "patchTusUpload", + "terminateTusUpload", + "terminateTusUpload", + }, + new String[] { + "terminate-upload", + "retry-with-backoff", + }, + new String[0] + ), + }; + private GeneratedTusProtocolContract() { } @@ -1239,4 +1917,37 @@ static final class GeneratedTusClientFeatureFlowStep { this.summary = summary; } } + + /** + * Generated client conformance scenario fixture. + */ + static final class GeneratedTusClientConformanceScenario { + final String behavior; + final String completionKind; + final String completionReason; + final String featureId; + final String scenarioId; + final String[] operationIds; + final String[] primitives; + final String[] eventKeys; + + GeneratedTusClientConformanceScenario( + String behavior, + String completionKind, + String completionReason, + String featureId, + String scenarioId, + String[] operationIds, + String[] primitives, + String[] eventKeys) { + this.behavior = behavior; + this.completionKind = completionKind; + this.completionReason = completionReason; + this.featureId = featureId; + this.scenarioId = scenarioId; + this.operationIds = operationIds; + this.primitives = primitives; + this.eventKeys = eventKeys; + } + } } diff --git a/src/test/java/io/tus/java/client/TestGeneratedTusProtocolContract.java b/src/test/java/io/tus/java/client/TestGeneratedTusProtocolContract.java index 6d57686e..ca098930 100644 --- a/src/test/java/io/tus/java/client/TestGeneratedTusProtocolContract.java +++ b/src/test/java/io/tus/java/client/TestGeneratedTusProtocolContract.java @@ -70,6 +70,26 @@ public void testSingleUploadLifecycleFeatureReferencesProtocolOperations() { assertContains(feature.primitives, "emit-progress"); } + /** + * Verifies generated high-level conformance scenarios expose projected event keys. + */ + @Test + public void testConformanceScenarioCarriesProjectedEventKeys() { + GeneratedTusProtocolContract.GeneratedTusClientFeature feature = + findFeature("creationWithUpload"); + GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario scenario = + findScenario("creationWithUploadPartialChunk"); + + assertContains(feature.conformance.scenarioIds, scenario.scenarioId); + assertEquals("creation-with-upload-partial-chunk", scenario.behavior); + assertEquals("success", scenario.completionKind); + assertContains(scenario.operationIds, "createTusUpload"); + assertContains(scenario.operationIds, "patchTusUpload"); + assertContains(scenario.primitives, "upload-during-creation"); + assertContains(scenario.eventKeys, "chunk-complete:5:10:11"); + assertContains(scenario.eventKeys, "chunk-complete:1:11:11"); + } + private static GeneratedTusProtocolContract.GeneratedTusProtocolOperation findOperation( String operationId) { for (GeneratedTusProtocolContract.GeneratedTusProtocolOperation operation @@ -94,6 +114,18 @@ private static GeneratedTusProtocolContract.GeneratedTusClientFeature findFeatur throw new AssertionError("Missing generated TUS client feature: " + featureId); } + private static GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario findScenario( + String scenarioId) { + for (GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario scenario + : GeneratedTusProtocolContract.CLIENT_CONFORMANCE_SCENARIOS) { + if (scenario.scenarioId.equals(scenarioId)) { + return scenario; + } + } + + throw new AssertionError("Missing generated TUS client scenario: " + scenarioId); + } + private static boolean hasRequiredHeader( GeneratedTusProtocolContract.GeneratedTusHeaderVariant variant, String headerName) { From 9a0d85a115c22e318c166717d1155fb54c2e2bb7 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Sun, 31 May 2026 13:55:53 +0200 Subject: [PATCH 19/43] Add generated conformance event canary --- .../TestGeneratedTusConformanceEvents.java | 285 ++++++++++++++++++ .../TestGeneratedTusProtocolContract.java | 32 -- 2 files changed, 285 insertions(+), 32 deletions(-) create mode 100644 src/test/java/io/tus/java/client/TestGeneratedTusConformanceEvents.java diff --git a/src/test/java/io/tus/java/client/TestGeneratedTusConformanceEvents.java b/src/test/java/io/tus/java/client/TestGeneratedTusConformanceEvents.java new file mode 100644 index 00000000..40526792 --- /dev/null +++ b/src/test/java/io/tus/java/client/TestGeneratedTusConformanceEvents.java @@ -0,0 +1,285 @@ +/* + * Code generated from Transloadit API2 TUS protocol contracts; DO NOT EDIT. + * If it looks wrong, please report the issue instead of editing this file by hand; + * the source fix belongs in the protocol contract generator so all TUS clients stay in sync. + */ + +package io.tus.java.client; + +import org.junit.Test; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + +/** + * Tests generated TUS client conformance event fixtures. + */ +public class TestGeneratedTusConformanceEvents { + private static final GeneratedTusEventCanaryCase[] CASES = + new GeneratedTusEventCanaryCase[] { + new GeneratedTusEventCanaryCase( + "singleUploadLifecycle", + "singleUploadLifecycle", + new String[] { + "fingerprint:contract-single-fingerprint", + "upload-url-available", + "url-storage-add:contract-single-fingerprint:https://tus.io/uploads/generated-contract", + "progress:0:11", + "progress:11:11", + "chunk-complete:11:11:11", + "success", + "source-close", + } + ), + new GeneratedTusEventCanaryCase( + "creationWithUpload", + "creationWithUpload", + new String[] { + "progress:0:11", + "progress:11:11", + "upload-url-available", + "success", + "source-close", + } + ), + new GeneratedTusEventCanaryCase( + "creationWithUpload", + "creationWithUploadPartialChunk", + new String[] { + "progress:0:11", + "progress:5:11", + "upload-url-available", + "progress:5:11", + "progress:10:11", + "chunk-complete:5:10:11", + "progress:10:11", + "progress:11:11", + "chunk-complete:1:11:11", + "success", + "source-close", + } + ), + new GeneratedTusEventCanaryCase( + "protocolVersionSelection", + "ietfDraft05CreationWithUpload", + new String[] { + "progress:0:11", + "progress:11:11", + "upload-url-available", + "success", + "source-close", + } + ), + new GeneratedTusEventCanaryCase( + "protocolVersionSelection", + "ietfDraft03ResumeWithoutKnownLength", + new String[] { + "upload-url-available", + "progress:5:11", + "progress:11:11", + "chunk-complete:6:11:11", + "success", + "source-close", + } + ), + new GeneratedTusEventCanaryCase( + "resumeUpload", + "resumeFromPreviousUpload", + new String[] { + "fingerprint:contract-resume-fingerprint", + "url-storage-find:contract-resume-fingerprint:1", + "fingerprint:contract-resume-fingerprint", + "upload-url-available", + "progress:5:11", + "progress:11:11", + "chunk-complete:6:11:11", + "url-storage-remove:tus::contract-resume-fingerprint::1337", + "success", + "source-close", + } + ), + new GeneratedTusEventCanaryCase( + "relativeLocationResolution", + "relativeLocationResolution", + new String[] { + "upload-url-available", + "progress:0:11", + "progress:11:11", + "chunk-complete:11:11:11", + "success", + "source-close", + } + ), + new GeneratedTusEventCanaryCase( + "inputSources", + "arrayBufferInput", + new String[] { + "source-open:array-buffer:11", + "success", + "source-close", + } + ), + new GeneratedTusEventCanaryCase( + "inputSources", + "arrayBufferViewInput", + new String[] { + "source-open:array-buffer-view:11", + "success", + "source-close", + } + ), + new GeneratedTusEventCanaryCase( + "inputSources", + "webReadableStreamInput", + new String[] { + "source-open:web-readable-stream:null", + "success", + "source-close", + } + ), + new GeneratedTusEventCanaryCase( + "inputSources", + "nodeReadableStreamInput", + new String[] { + "source-open:node-readable-stream:null", + "success", + "source-close", + } + ), + new GeneratedTusEventCanaryCase( + "inputSources", + "nodePathInput", + new String[] { + "source-open:node-path-reference:11", + "success", + "source-close", + } + ), + new GeneratedTusEventCanaryCase( + "deferredLengthUpload", + "deferredLengthUpload", + new String[] { + "upload-url-available", + "progress:0:11", + "progress:11:11", + "chunk-complete:11:11:11", + "success", + "source-close", + } + ), + new GeneratedTusEventCanaryCase( + "parallelUploadConcat", + "parallelUploadConcat", + new String[] { + "progress:5:11", + "chunk-complete:5:5:11", + "progress:11:11", + "chunk-complete:6:11:11", + } + ), + new GeneratedTusEventCanaryCase( + "parallelUploadConcat", + "parallelUploadAbortCleanup", + new String[] { + "request-abort:3", + } + ), + new GeneratedTusEventCanaryCase( + "retryOffsetRecovery", + "retryPatchAfterOffsetRecovery", + new String[] { + "should-retry:0:true", + "retry-schedule:0", + "should-retry:0:true", + "retry-schedule:0", + } + ), + new GeneratedTusEventCanaryCase( + "requestLifecycleHooks", + "requestLifecycleHooks", + new String[] { + "before-request:0", + "after-response:0", + "success", + "source-close", + } + ), + new GeneratedTusEventCanaryCase( + "abortUpload", + "abortUpload", + new String[] { + "request-abort:0", + } + ), + new GeneratedTusEventCanaryCase( + "abortUpload", + "abortUploadAfterStoredUrl", + new String[] { + "request-abort:1", + } + ), + }; + + /** + * Verifies generated feature-level event keys survive in the Java fixture. + */ + @Test + public void testGeneratedScenarioEventKeys() { + for (GeneratedTusEventCanaryCase testCase : CASES) { + GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario scenario = + findScenario(testCase.scenarioId); + GeneratedTusProtocolContract.GeneratedTusClientFeature feature = + findFeature(testCase.featureId); + + assertEquals(testCase.featureId, scenario.featureId); + assertContains(feature.conformance.scenarioIds, scenario.scenarioId); + assertArrayEquals(testCase.eventKeys, scenario.eventKeys); + } + } + + private static GeneratedTusProtocolContract.GeneratedTusClientFeature findFeature( + String featureId) { + for (GeneratedTusProtocolContract.GeneratedTusClientFeature feature + : GeneratedTusProtocolContract.CLIENT_FEATURES) { + if (feature.featureId.equals(featureId)) { + return feature; + } + } + + throw new AssertionError("Missing generated TUS client feature: " + featureId); + } + + private static GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario findScenario( + String scenarioId) { + for (GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario scenario + : GeneratedTusProtocolContract.CLIENT_CONFORMANCE_SCENARIOS) { + if (scenario.scenarioId.equals(scenarioId)) { + return scenario; + } + } + + throw new AssertionError("Missing generated TUS client scenario: " + scenarioId); + } + + private static void assertContains(String[] values, String expected) { + for (String value : values) { + if (value.equals(expected)) { + return; + } + } + + throw new AssertionError("Missing generated value: " + expected); + } + + private static final class GeneratedTusEventCanaryCase { + final String featureId; + final String scenarioId; + final String[] eventKeys; + + GeneratedTusEventCanaryCase(String featureId, String scenarioId, String[] eventKeys) { + this.featureId = featureId; + this.scenarioId = scenarioId; + this.eventKeys = eventKeys; + } + } +} diff --git a/src/test/java/io/tus/java/client/TestGeneratedTusProtocolContract.java b/src/test/java/io/tus/java/client/TestGeneratedTusProtocolContract.java index ca098930..6d57686e 100644 --- a/src/test/java/io/tus/java/client/TestGeneratedTusProtocolContract.java +++ b/src/test/java/io/tus/java/client/TestGeneratedTusProtocolContract.java @@ -70,26 +70,6 @@ public void testSingleUploadLifecycleFeatureReferencesProtocolOperations() { assertContains(feature.primitives, "emit-progress"); } - /** - * Verifies generated high-level conformance scenarios expose projected event keys. - */ - @Test - public void testConformanceScenarioCarriesProjectedEventKeys() { - GeneratedTusProtocolContract.GeneratedTusClientFeature feature = - findFeature("creationWithUpload"); - GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario scenario = - findScenario("creationWithUploadPartialChunk"); - - assertContains(feature.conformance.scenarioIds, scenario.scenarioId); - assertEquals("creation-with-upload-partial-chunk", scenario.behavior); - assertEquals("success", scenario.completionKind); - assertContains(scenario.operationIds, "createTusUpload"); - assertContains(scenario.operationIds, "patchTusUpload"); - assertContains(scenario.primitives, "upload-during-creation"); - assertContains(scenario.eventKeys, "chunk-complete:5:10:11"); - assertContains(scenario.eventKeys, "chunk-complete:1:11:11"); - } - private static GeneratedTusProtocolContract.GeneratedTusProtocolOperation findOperation( String operationId) { for (GeneratedTusProtocolContract.GeneratedTusProtocolOperation operation @@ -114,18 +94,6 @@ private static GeneratedTusProtocolContract.GeneratedTusClientFeature findFeatur throw new AssertionError("Missing generated TUS client feature: " + featureId); } - private static GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario findScenario( - String scenarioId) { - for (GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario scenario - : GeneratedTusProtocolContract.CLIENT_CONFORMANCE_SCENARIOS) { - if (scenario.scenarioId.equals(scenarioId)) { - return scenario; - } - } - - throw new AssertionError("Missing generated TUS client scenario: " + scenarioId); - } - private static boolean hasRequiredHeader( GeneratedTusProtocolContract.GeneratedTusHeaderVariant variant, String headerName) { From 6a8c8fadb98e0539b7978f9d0f150935091af576 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Sun, 31 May 2026 19:12:52 +0200 Subject: [PATCH 20/43] Regenerate TUS protocol fixture for lint --- .../client/GeneratedTusProtocolContract.java | 230 ++++++++++++------ 1 file changed, 156 insertions(+), 74 deletions(-) diff --git a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java index 652d718a..18e9d68d 100644 --- a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java +++ b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java @@ -1124,8 +1124,10 @@ final class GeneratedTusProtocolContract { new GeneratedTusClientConformanceScenario[] { new GeneratedTusClientConformanceScenario( "single-upload-lifecycle", - "success", - null, + new GeneratedTusClientConformanceCompletion( + "success", + null + ), "singleUploadLifecycle", "singleUploadLifecycle", new String[] { @@ -1153,8 +1155,10 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientConformanceScenario( "creation-with-upload", - "success", - null, + new GeneratedTusClientConformanceCompletion( + "success", + null + ), "creationWithUpload", "creationWithUpload", new String[] { @@ -1174,8 +1178,10 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientConformanceScenario( "creation-with-upload-partial-chunk", - "success", - null, + new GeneratedTusClientConformanceCompletion( + "success", + null + ), "creationWithUpload", "creationWithUploadPartialChunk", new String[] { @@ -1202,8 +1208,10 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientConformanceScenario( "creation-with-upload", - "success", - null, + new GeneratedTusClientConformanceCompletion( + "success", + null + ), "protocolVersionSelection", "ietfDraft05CreationWithUpload", new String[] { @@ -1222,8 +1230,10 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientConformanceScenario( "upload-body-headers", - "success", - null, + new GeneratedTusClientConformanceCompletion( + "success", + null + ), "protocolVersionSelection", "ietfDraft03ResumeWithoutKnownLength", new String[] { @@ -1244,8 +1254,10 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientConformanceScenario( "start-option-validation", - "error", - "missingInput", + new GeneratedTusClientConformanceCompletion( + "error", + "missingInput" + ), "startOptionValidation", "startValidationMissingInput", new String[0], @@ -1256,8 +1268,10 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientConformanceScenario( "start-option-validation", - "error", - "missingEndpointOrUploadUrl", + new GeneratedTusClientConformanceCompletion( + "error", + "missingEndpointOrUploadUrl" + ), "startOptionValidation", "startValidationMissingEndpointOrUploadUrl", new String[0], @@ -1268,8 +1282,10 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientConformanceScenario( "start-option-validation", - "error", - "unsupportedProtocol", + new GeneratedTusClientConformanceCompletion( + "error", + "unsupportedProtocol" + ), "startOptionValidation", "startValidationUnsupportedProtocol", new String[0], @@ -1280,8 +1296,10 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientConformanceScenario( "start-option-validation", - "error", - "retryDelaysNotArray", + new GeneratedTusClientConformanceCompletion( + "error", + "retryDelaysNotArray" + ), "startOptionValidation", "startValidationRetryDelaysNotArray", new String[0], @@ -1292,8 +1310,10 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientConformanceScenario( "start-option-validation", - "error", - "parallelUploadsWithUploadUrl", + new GeneratedTusClientConformanceCompletion( + "error", + "parallelUploadsWithUploadUrl" + ), "startOptionValidation", "startValidationParallelUploadsWithUploadUrl", new String[0], @@ -1304,8 +1324,10 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientConformanceScenario( "start-option-validation", - "error", - "parallelUploadsWithUploadSize", + new GeneratedTusClientConformanceCompletion( + "error", + "parallelUploadsWithUploadSize" + ), "startOptionValidation", "startValidationParallelUploadsWithUploadSize", new String[0], @@ -1316,8 +1338,10 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientConformanceScenario( "start-option-validation", - "error", - "parallelUploadsWithDeferredLength", + new GeneratedTusClientConformanceCompletion( + "error", + "parallelUploadsWithDeferredLength" + ), "startOptionValidation", "startValidationParallelUploadsWithDeferredLength", new String[0], @@ -1328,8 +1352,10 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientConformanceScenario( "start-option-validation", - "error", - "parallelUploadsWithUploadDataDuringCreation", + new GeneratedTusClientConformanceCompletion( + "error", + "parallelUploadsWithUploadDataDuringCreation" + ), "startOptionValidation", "startValidationParallelUploadsWithUploadDataDuringCreation", new String[0], @@ -1340,8 +1366,10 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientConformanceScenario( "start-option-validation", - "error", - "parallelBoundariesWithoutParallelUploads", + new GeneratedTusClientConformanceCompletion( + "error", + "parallelBoundariesWithoutParallelUploads" + ), "startOptionValidation", "startValidationParallelBoundariesWithoutParallelUploads", new String[0], @@ -1352,8 +1380,10 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientConformanceScenario( "start-option-validation", - "error", - "parallelBoundariesLengthMismatch", + new GeneratedTusClientConformanceCompletion( + "error", + "parallelBoundariesLengthMismatch" + ), "startOptionValidation", "startValidationParallelBoundariesLengthMismatch", new String[0], @@ -1364,8 +1394,10 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientConformanceScenario( "detailed-error", - "error", - "unexpectedCreateResponse", + new GeneratedTusClientConformanceCompletion( + "error", + "unexpectedCreateResponse" + ), "detailedErrors", "detailedCreateResponseError", new String[] { @@ -1378,8 +1410,10 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientConformanceScenario( "detailed-error", - "error", - "createUploadRequestFailed", + new GeneratedTusClientConformanceCompletion( + "error", + "createUploadRequestFailed" + ), "detailedErrors", "detailedCreateRequestError", new String[] { @@ -1392,8 +1426,10 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientConformanceScenario( "upload-body-headers", - "success", - null, + new GeneratedTusClientConformanceCompletion( + "success", + null + ), "uploadBodyHeaders", "uploadBodyHeaders", new String[] { @@ -1407,8 +1443,10 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientConformanceScenario( "custom-request-headers", - "success", - null, + new GeneratedTusClientConformanceCompletion( + "success", + null + ), "customRequestHeaders", "customRequestHeaders", new String[] { @@ -1422,8 +1460,10 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientConformanceScenario( "resume-from-previous-upload", - "success", - null, + new GeneratedTusClientConformanceCompletion( + "success", + null + ), "resumeUpload", "resumeFromPreviousUpload", new String[] { @@ -1450,8 +1490,10 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientConformanceScenario( "relative-location-resolution", - "success", - null, + new GeneratedTusClientConformanceCompletion( + "success", + null + ), "relativeLocationResolution", "relativeLocationResolution", new String[] { @@ -1472,8 +1514,10 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientConformanceScenario( "array-buffer-input", - "success", - null, + new GeneratedTusClientConformanceCompletion( + "success", + null + ), "inputSources", "arrayBufferInput", new String[] { @@ -1491,8 +1535,10 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientConformanceScenario( "array-buffer-view-input", - "success", - null, + new GeneratedTusClientConformanceCompletion( + "success", + null + ), "inputSources", "arrayBufferViewInput", new String[] { @@ -1510,8 +1556,10 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientConformanceScenario( "web-readable-stream-input", - "success", - null, + new GeneratedTusClientConformanceCompletion( + "success", + null + ), "inputSources", "webReadableStreamInput", new String[] { @@ -1529,8 +1577,10 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientConformanceScenario( "node-readable-stream-input", - "success", - null, + new GeneratedTusClientConformanceCompletion( + "success", + null + ), "inputSources", "nodeReadableStreamInput", new String[] { @@ -1548,8 +1598,10 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientConformanceScenario( "node-path-input", - "success", - null, + new GeneratedTusClientConformanceCompletion( + "success", + null + ), "inputSources", "nodePathInput", new String[] { @@ -1567,8 +1619,10 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientConformanceScenario( "deferred-length-upload", - "success", - null, + new GeneratedTusClientConformanceCompletion( + "success", + null + ), "deferredLengthUpload", "deferredLengthUpload", new String[] { @@ -1590,8 +1644,10 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientConformanceScenario( "override-patch-method", - "success", - null, + new GeneratedTusClientConformanceCompletion( + "success", + null + ), "overridePatchMethod", "overridePatchMethod", new String[] { @@ -1605,8 +1661,10 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientConformanceScenario( "parallel-upload-concat", - "success", - null, + new GeneratedTusClientConformanceCompletion( + "success", + null + ), "parallelUploadConcat", "parallelUploadConcat", new String[] { @@ -1629,8 +1687,10 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientConformanceScenario( "parallel-upload-abort-cleanup", - "aborted", - null, + new GeneratedTusClientConformanceCompletion( + "aborted", + null + ), "parallelUploadConcat", "parallelUploadAbortCleanup", new String[] { @@ -1652,8 +1712,10 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientConformanceScenario( "retry-patch-after-offset-recovery", - "success", - null, + new GeneratedTusClientConformanceCompletion( + "success", + null + ), "retryOffsetRecovery", "retryPatchAfterOffsetRecovery", new String[] { @@ -1677,8 +1739,10 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientConformanceScenario( "request-lifecycle-hooks", - "success", - null, + new GeneratedTusClientConformanceCompletion( + "success", + null + ), "requestLifecycleHooks", "requestLifecycleHooks", new String[] { @@ -1696,8 +1760,10 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientConformanceScenario( "abort-upload", - "aborted", - null, + new GeneratedTusClientConformanceCompletion( + "aborted", + null + ), "abortUpload", "abortUpload", new String[] { @@ -1712,8 +1778,10 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientConformanceScenario( "abort-upload-after-stored-url", - "aborted", - null, + new GeneratedTusClientConformanceCompletion( + "aborted", + null + ), "abortUpload", "abortUploadAfterStoredUrl", new String[] { @@ -1731,8 +1799,10 @@ final class GeneratedTusProtocolContract { ), new GeneratedTusClientConformanceScenario( "terminate-with-retry", - "terminated", - null, + new GeneratedTusClientConformanceCompletion( + "terminated", + null + ), "terminateUpload", "terminateWithRetry", new String[] { @@ -1933,16 +2003,15 @@ static final class GeneratedTusClientConformanceScenario { GeneratedTusClientConformanceScenario( String behavior, - String completionKind, - String completionReason, + GeneratedTusClientConformanceCompletion completion, String featureId, String scenarioId, String[] operationIds, String[] primitives, String[] eventKeys) { this.behavior = behavior; - this.completionKind = completionKind; - this.completionReason = completionReason; + this.completionKind = completion.kind; + this.completionReason = completion.reason; this.featureId = featureId; this.scenarioId = scenarioId; this.operationIds = operationIds; @@ -1950,4 +2019,17 @@ static final class GeneratedTusClientConformanceScenario { this.eventKeys = eventKeys; } } + + /** + * Generated client conformance completion fixture. + */ + static final class GeneratedTusClientConformanceCompletion { + final String kind; + final String reason; + + GeneratedTusClientConformanceCompletion(String kind, String reason) { + this.kind = kind; + this.reason = reason; + } + } } From 1a3a0852e12c527024b5b72859dd9593f92961cb Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Sun, 31 May 2026 20:17:51 +0200 Subject: [PATCH 21/43] Add generated runtime event canary --- .../java/io/tus/java/client/TusUploader.java | 72 ++ ...eneratedTusClientConformanceScenarios.java | 714 ++++++++++++++++++ .../client/GeneratedTusProtocolContract.java | 698 +---------------- .../client/TestGeneratedTusRuntimeEvents.java | 315 ++++++++ 4 files changed, 1102 insertions(+), 697 deletions(-) create mode 100644 src/test/java/io/tus/java/client/GeneratedTusClientConformanceScenarios.java create mode 100644 src/test/java/io/tus/java/client/TestGeneratedTusRuntimeEvents.java diff --git a/src/main/java/io/tus/java/client/TusUploader.java b/src/main/java/io/tus/java/client/TusUploader.java index d84bd8b4..8af4c462 100644 --- a/src/main/java/io/tus/java/client/TusUploader.java +++ b/src/main/java/io/tus/java/client/TusUploader.java @@ -21,6 +21,31 @@ * */ public class TusUploader { + /** + * Callback for upload progress events. + */ + public interface ProgressListener { + /** + * Called when upload progress changes. + * @param bytesSent Bytes accepted locally for the upload. + * @param bytesTotal Total upload size. + */ + void onProgress(long bytesSent, long bytesTotal); + } + + /** + * Callback for accepted chunk events. + */ + public interface ChunkCompleteListener { + /** + * Called after the server accepts an upload request. + * @param chunkSize Bytes accepted by the completed request. + * @param bytesAccepted Total bytes accepted by the server. + * @param bytesTotal Total upload size. + */ + void onChunkComplete(long chunkSize, long bytesAccepted, long bytesTotal); + } + private URL uploadURL; private Proxy proxy; private TusInputStream input; @@ -30,6 +55,10 @@ public class TusUploader { private byte[] buffer; private int requestPayloadSize = 10 * 1024 * 1024; private int bytesRemainingForRequest; + private long requestStartOffset; + private boolean requestProgressStarted; + private ProgressListener progressListener; + private ChunkCompleteListener chunkCompleteListener; private HttpURLConnection connection; private OutputStream output; @@ -65,6 +94,8 @@ private void openConnection() throws IOException, ProtocolException { } bytesRemainingForRequest = requestPayloadSize; + requestStartOffset = offset; + requestProgressStarted = false; input.mark(requestPayloadSize); if (proxy != null) { @@ -168,6 +199,24 @@ public int getRequestPayloadSize() { return requestPayloadSize; } + /** + * Set the listener used for upload progress events. + * + * @param listener Progress listener or null to disable events. + */ + public void setProgressListener(ProgressListener listener) { + progressListener = listener; + } + + /** + * Set the listener used for accepted chunk events. + * + * @param listener Chunk-complete listener or null to disable events. + */ + public void setChunkCompleteListener(ChunkCompleteListener listener) { + chunkCompleteListener = listener; + } + /** * Upload a part of the file by reading a chunk from the InputStream and writing * it to the HTTP request's body. If the number of available bytes is lower than the chunk's @@ -184,6 +233,7 @@ public int getRequestPayloadSize() { */ public int uploadChunk() throws IOException, ProtocolException { openConnection(); + notifyProgressAtRequestStart(); int bytesToRead = Math.min(getChunkSize(), bytesRemainingForRequest); @@ -201,6 +251,7 @@ public int uploadChunk() throws IOException, ProtocolException { offset += bytesRead; bytesRemainingForRequest -= bytesRead; + notifyProgress(offset); if (bytesRemainingForRequest <= 0) { finishConnection(); @@ -358,7 +409,28 @@ private void finishConnection() throws ProtocolException, IOException { connection); } + notifyChunkComplete(serverOffset - requestStartOffset, serverOffset); connection = null; + requestProgressStarted = false; + } + } + + private void notifyProgressAtRequestStart() { + if (!requestProgressStarted) { + notifyProgress(offset); + requestProgressStarted = true; + } + } + + private void notifyProgress(long bytesSent) { + if (progressListener != null) { + progressListener.onProgress(bytesSent, upload.getSize()); + } + } + + private void notifyChunkComplete(long chunkSize, long bytesAccepted) { + if (chunkCompleteListener != null) { + chunkCompleteListener.onChunkComplete(chunkSize, bytesAccepted, upload.getSize()); } } diff --git a/src/test/java/io/tus/java/client/GeneratedTusClientConformanceScenarios.java b/src/test/java/io/tus/java/client/GeneratedTusClientConformanceScenarios.java new file mode 100644 index 00000000..5775ae46 --- /dev/null +++ b/src/test/java/io/tus/java/client/GeneratedTusClientConformanceScenarios.java @@ -0,0 +1,714 @@ +/* + * Code generated from Transloadit API2 TUS protocol contracts; DO NOT EDIT. + * If it looks wrong, please report the issue instead of editing this file by hand; + * the source fix belongs in the protocol contract generator so all TUS clients stay in sync. + */ + +package io.tus.java.client; + +/** + * Generated TUS client conformance scenario fixture used by tests. + */ +final class GeneratedTusClientConformanceScenarios { + static final GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario[] CLIENT_CONFORMANCE_SCENARIOS = + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario[] { + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + "single-upload-lifecycle", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceCompletion( + "success", + null + ), + "singleUploadLifecycle", + "singleUploadLifecycle", + new String[] { + "createTusUpload", + "patchTusUpload", + }, + new String[] { + "open-input-source", + "fingerprint-input", + "store-resume-url", + "retry-with-backoff", + "emit-progress", + "abort-current-request", + }, + new String[] { + "fingerprint:contract-single-fingerprint", + "upload-url-available", + "url-storage-add:contract-single-fingerprint:https://tus.io/uploads/generated-contract", + "progress:0:11", + "progress:11:11", + "chunk-complete:11:11:11", + "success", + "source-close", + } + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + "creation-with-upload", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceCompletion( + "success", + null + ), + "creationWithUpload", + "creationWithUpload", + new String[] { + "createTusUpload", + }, + new String[] { + "upload-during-creation", + "emit-progress", + }, + new String[] { + "progress:0:11", + "progress:11:11", + "upload-url-available", + "success", + "source-close", + } + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + "creation-with-upload-partial-chunk", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceCompletion( + "success", + null + ), + "creationWithUpload", + "creationWithUploadPartialChunk", + new String[] { + "createTusUpload", + "patchTusUpload", + }, + new String[] { + "upload-during-creation", + "emit-progress", + }, + new String[] { + "progress:0:11", + "progress:5:11", + "upload-url-available", + "progress:5:11", + "progress:10:11", + "chunk-complete:5:10:11", + "progress:10:11", + "progress:11:11", + "chunk-complete:1:11:11", + "success", + "source-close", + } + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + "creation-with-upload", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceCompletion( + "success", + null + ), + "protocolVersionSelection", + "ietfDraft05CreationWithUpload", + new String[] { + "createTusUpload", + }, + new String[] { + "select-client-protocol", + }, + new String[] { + "progress:0:11", + "progress:11:11", + "upload-url-available", + "success", + "source-close", + } + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + "upload-body-headers", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceCompletion( + "success", + null + ), + "protocolVersionSelection", + "ietfDraft03ResumeWithoutKnownLength", + new String[] { + "getTusUploadOffset", + "patchTusUpload", + }, + new String[] { + "select-client-protocol", + }, + new String[] { + "upload-url-available", + "progress:5:11", + "progress:11:11", + "chunk-complete:6:11:11", + "success", + "source-close", + } + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + "start-option-validation", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceCompletion( + "error", + "missingInput" + ), + "startOptionValidation", + "startValidationMissingInput", + new String[0], + new String[] { + "validate-start-options", + }, + new String[0] + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + "start-option-validation", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceCompletion( + "error", + "missingEndpointOrUploadUrl" + ), + "startOptionValidation", + "startValidationMissingEndpointOrUploadUrl", + new String[0], + new String[] { + "validate-start-options", + }, + new String[0] + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + "start-option-validation", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceCompletion( + "error", + "unsupportedProtocol" + ), + "startOptionValidation", + "startValidationUnsupportedProtocol", + new String[0], + new String[] { + "validate-start-options", + }, + new String[0] + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + "start-option-validation", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceCompletion( + "error", + "retryDelaysNotArray" + ), + "startOptionValidation", + "startValidationRetryDelaysNotArray", + new String[0], + new String[] { + "validate-start-options", + }, + new String[0] + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + "start-option-validation", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceCompletion( + "error", + "parallelUploadsWithUploadUrl" + ), + "startOptionValidation", + "startValidationParallelUploadsWithUploadUrl", + new String[0], + new String[] { + "validate-start-options", + }, + new String[0] + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + "start-option-validation", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceCompletion( + "error", + "parallelUploadsWithUploadSize" + ), + "startOptionValidation", + "startValidationParallelUploadsWithUploadSize", + new String[0], + new String[] { + "validate-start-options", + }, + new String[0] + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + "start-option-validation", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceCompletion( + "error", + "parallelUploadsWithDeferredLength" + ), + "startOptionValidation", + "startValidationParallelUploadsWithDeferredLength", + new String[0], + new String[] { + "validate-start-options", + }, + new String[0] + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + "start-option-validation", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceCompletion( + "error", + "parallelUploadsWithUploadDataDuringCreation" + ), + "startOptionValidation", + "startValidationParallelUploadsWithUploadDataDuringCreation", + new String[0], + new String[] { + "validate-start-options", + }, + new String[0] + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + "start-option-validation", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceCompletion( + "error", + "parallelBoundariesWithoutParallelUploads" + ), + "startOptionValidation", + "startValidationParallelBoundariesWithoutParallelUploads", + new String[0], + new String[] { + "validate-start-options", + }, + new String[0] + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + "start-option-validation", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceCompletion( + "error", + "parallelBoundariesLengthMismatch" + ), + "startOptionValidation", + "startValidationParallelBoundariesLengthMismatch", + new String[0], + new String[] { + "validate-start-options", + }, + new String[0] + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + "detailed-error", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceCompletion( + "error", + "unexpectedCreateResponse" + ), + "detailedErrors", + "detailedCreateResponseError", + new String[] { + "createTusUpload", + }, + new String[] { + "report-detailed-errors", + }, + new String[0] + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + "detailed-error", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceCompletion( + "error", + "createUploadRequestFailed" + ), + "detailedErrors", + "detailedCreateRequestError", + new String[] { + "createTusUpload", + }, + new String[] { + "report-detailed-errors", + }, + new String[0] + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + "upload-body-headers", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceCompletion( + "success", + null + ), + "uploadBodyHeaders", + "uploadBodyHeaders", + new String[] { + "createTusUpload", + "patchTusUpload", + }, + new String[] { + "send-upload-body-headers", + }, + new String[0] + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + "custom-request-headers", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceCompletion( + "success", + null + ), + "customRequestHeaders", + "customRequestHeaders", + new String[] { + "createTusUpload", + "patchTusUpload", + }, + new String[] { + "apply-custom-request-headers", + }, + new String[0] + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + "resume-from-previous-upload", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceCompletion( + "success", + null + ), + "resumeUpload", + "resumeFromPreviousUpload", + new String[] { + "getTusUploadOffset", + "patchTusUpload", + }, + new String[] { + "fingerprint-input", + "resume-from-previous-upload", + "store-resume-url", + }, + new String[] { + "fingerprint:contract-resume-fingerprint", + "url-storage-find:contract-resume-fingerprint:1", + "fingerprint:contract-resume-fingerprint", + "upload-url-available", + "progress:5:11", + "progress:11:11", + "chunk-complete:6:11:11", + "url-storage-remove:tus::contract-resume-fingerprint::1337", + "success", + "source-close", + } + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + "relative-location-resolution", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceCompletion( + "success", + null + ), + "relativeLocationResolution", + "relativeLocationResolution", + new String[] { + "createTusUpload", + "patchTusUpload", + }, + new String[] { + "resolve-relative-location", + }, + new String[] { + "upload-url-available", + "progress:0:11", + "progress:11:11", + "chunk-complete:11:11:11", + "success", + "source-close", + } + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + "array-buffer-input", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceCompletion( + "success", + null + ), + "inputSources", + "arrayBufferInput", + new String[] { + "createTusUpload", + "patchTusUpload", + }, + new String[] { + "read-browser-file", + }, + new String[] { + "source-open:array-buffer:11", + "success", + "source-close", + } + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + "array-buffer-view-input", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceCompletion( + "success", + null + ), + "inputSources", + "arrayBufferViewInput", + new String[] { + "createTusUpload", + "patchTusUpload", + }, + new String[] { + "read-browser-file", + }, + new String[] { + "source-open:array-buffer-view:11", + "success", + "source-close", + } + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + "web-readable-stream-input", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceCompletion( + "success", + null + ), + "inputSources", + "webReadableStreamInput", + new String[] { + "createTusUpload", + "patchTusUpload", + }, + new String[] { + "read-web-stream", + }, + new String[] { + "source-open:web-readable-stream:null", + "success", + "source-close", + } + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + "node-readable-stream-input", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceCompletion( + "success", + null + ), + "inputSources", + "nodeReadableStreamInput", + new String[] { + "createTusUpload", + "patchTusUpload", + }, + new String[] { + "read-node-stream", + }, + new String[] { + "source-open:node-readable-stream:null", + "success", + "source-close", + } + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + "node-path-input", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceCompletion( + "success", + null + ), + "inputSources", + "nodePathInput", + new String[] { + "createTusUpload", + "patchTusUpload", + }, + new String[] { + "read-node-file", + }, + new String[] { + "source-open:node-path-reference:11", + "success", + "source-close", + } + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + "deferred-length-upload", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceCompletion( + "success", + null + ), + "deferredLengthUpload", + "deferredLengthUpload", + new String[] { + "createTusUpload", + "patchTusUpload", + }, + new String[] { + "defer-upload-length", + "emit-progress", + }, + new String[] { + "upload-url-available", + "progress:0:11", + "progress:11:11", + "chunk-complete:11:11:11", + "success", + "source-close", + } + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + "override-patch-method", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceCompletion( + "success", + null + ), + "overridePatchMethod", + "overridePatchMethod", + new String[] { + "getTusUploadOffset", + "patchTusUpload", + }, + new String[] { + "override-patch-method", + }, + new String[0] + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + "parallel-upload-concat", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceCompletion( + "success", + null + ), + "parallelUploadConcat", + "parallelUploadConcat", + new String[] { + "createTusUpload", + "createTusUpload", + "patchTusUpload", + "patchTusUpload", + "createTusUpload", + }, + new String[] { + "concatenate-partial-uploads", + "emit-progress", + }, + new String[] { + "progress:5:11", + "chunk-complete:5:5:11", + "progress:11:11", + "chunk-complete:6:11:11", + } + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + "parallel-upload-abort-cleanup", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceCompletion( + "aborted", + null + ), + "parallelUploadConcat", + "parallelUploadAbortCleanup", + new String[] { + "createTusUpload", + "createTusUpload", + "patchTusUpload", + "patchTusUpload", + "terminateTusUpload", + "terminateTusUpload", + }, + new String[] { + "abort-current-request", + "terminate-upload", + "concatenate-partial-uploads", + }, + new String[] { + "request-abort:3", + } + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + "retry-patch-after-offset-recovery", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceCompletion( + "success", + null + ), + "retryOffsetRecovery", + "retryPatchAfterOffsetRecovery", + new String[] { + "createTusUpload", + "patchTusUpload", + "getTusUploadOffset", + "patchTusUpload", + "getTusUploadOffset", + "patchTusUpload", + }, + new String[] { + "retry-with-backoff", + "recover-offset-after-error", + }, + new String[] { + "should-retry:0:true", + "retry-schedule:0", + "should-retry:0:true", + "retry-schedule:0", + } + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + "request-lifecycle-hooks", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceCompletion( + "success", + null + ), + "requestLifecycleHooks", + "requestLifecycleHooks", + new String[] { + "getTusUploadOffset", + }, + new String[] { + "run-request-hooks", + }, + new String[] { + "before-request:0", + "after-response:0", + "success", + "source-close", + } + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + "abort-upload", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceCompletion( + "aborted", + null + ), + "abortUpload", + "abortUpload", + new String[] { + "createTusUpload", + }, + new String[] { + "abort-current-request", + }, + new String[] { + "request-abort:0", + } + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + "abort-upload-after-stored-url", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceCompletion( + "aborted", + null + ), + "abortUpload", + "abortUploadAfterStoredUrl", + new String[] { + "createTusUpload", + "patchTusUpload", + "terminateTusUpload", + }, + new String[] { + "abort-current-request", + "terminate-upload", + }, + new String[] { + "request-abort:1", + } + ), + new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( + "terminate-with-retry", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceCompletion( + "terminated", + null + ), + "terminateUpload", + "terminateWithRetry", + new String[] { + "createTusUpload", + "patchTusUpload", + "terminateTusUpload", + "terminateTusUpload", + }, + new String[] { + "terminate-upload", + "retry-with-backoff", + }, + new String[0] + ), + }; + + private GeneratedTusClientConformanceScenarios() { + } +} diff --git a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java index 18e9d68d..e4108fbf 100644 --- a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java +++ b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java @@ -1121,703 +1121,7 @@ final class GeneratedTusProtocolContract { }; static final GeneratedTusClientConformanceScenario[] CLIENT_CONFORMANCE_SCENARIOS = - new GeneratedTusClientConformanceScenario[] { - new GeneratedTusClientConformanceScenario( - "single-upload-lifecycle", - new GeneratedTusClientConformanceCompletion( - "success", - null - ), - "singleUploadLifecycle", - "singleUploadLifecycle", - new String[] { - "createTusUpload", - "patchTusUpload", - }, - new String[] { - "open-input-source", - "fingerprint-input", - "store-resume-url", - "retry-with-backoff", - "emit-progress", - "abort-current-request", - }, - new String[] { - "fingerprint:contract-single-fingerprint", - "upload-url-available", - "url-storage-add:contract-single-fingerprint:https://tus.io/uploads/generated-contract", - "progress:0:11", - "progress:11:11", - "chunk-complete:11:11:11", - "success", - "source-close", - } - ), - new GeneratedTusClientConformanceScenario( - "creation-with-upload", - new GeneratedTusClientConformanceCompletion( - "success", - null - ), - "creationWithUpload", - "creationWithUpload", - new String[] { - "createTusUpload", - }, - new String[] { - "upload-during-creation", - "emit-progress", - }, - new String[] { - "progress:0:11", - "progress:11:11", - "upload-url-available", - "success", - "source-close", - } - ), - new GeneratedTusClientConformanceScenario( - "creation-with-upload-partial-chunk", - new GeneratedTusClientConformanceCompletion( - "success", - null - ), - "creationWithUpload", - "creationWithUploadPartialChunk", - new String[] { - "createTusUpload", - "patchTusUpload", - }, - new String[] { - "upload-during-creation", - "emit-progress", - }, - new String[] { - "progress:0:11", - "progress:5:11", - "upload-url-available", - "progress:5:11", - "progress:10:11", - "chunk-complete:5:10:11", - "progress:10:11", - "progress:11:11", - "chunk-complete:1:11:11", - "success", - "source-close", - } - ), - new GeneratedTusClientConformanceScenario( - "creation-with-upload", - new GeneratedTusClientConformanceCompletion( - "success", - null - ), - "protocolVersionSelection", - "ietfDraft05CreationWithUpload", - new String[] { - "createTusUpload", - }, - new String[] { - "select-client-protocol", - }, - new String[] { - "progress:0:11", - "progress:11:11", - "upload-url-available", - "success", - "source-close", - } - ), - new GeneratedTusClientConformanceScenario( - "upload-body-headers", - new GeneratedTusClientConformanceCompletion( - "success", - null - ), - "protocolVersionSelection", - "ietfDraft03ResumeWithoutKnownLength", - new String[] { - "getTusUploadOffset", - "patchTusUpload", - }, - new String[] { - "select-client-protocol", - }, - new String[] { - "upload-url-available", - "progress:5:11", - "progress:11:11", - "chunk-complete:6:11:11", - "success", - "source-close", - } - ), - new GeneratedTusClientConformanceScenario( - "start-option-validation", - new GeneratedTusClientConformanceCompletion( - "error", - "missingInput" - ), - "startOptionValidation", - "startValidationMissingInput", - new String[0], - new String[] { - "validate-start-options", - }, - new String[0] - ), - new GeneratedTusClientConformanceScenario( - "start-option-validation", - new GeneratedTusClientConformanceCompletion( - "error", - "missingEndpointOrUploadUrl" - ), - "startOptionValidation", - "startValidationMissingEndpointOrUploadUrl", - new String[0], - new String[] { - "validate-start-options", - }, - new String[0] - ), - new GeneratedTusClientConformanceScenario( - "start-option-validation", - new GeneratedTusClientConformanceCompletion( - "error", - "unsupportedProtocol" - ), - "startOptionValidation", - "startValidationUnsupportedProtocol", - new String[0], - new String[] { - "validate-start-options", - }, - new String[0] - ), - new GeneratedTusClientConformanceScenario( - "start-option-validation", - new GeneratedTusClientConformanceCompletion( - "error", - "retryDelaysNotArray" - ), - "startOptionValidation", - "startValidationRetryDelaysNotArray", - new String[0], - new String[] { - "validate-start-options", - }, - new String[0] - ), - new GeneratedTusClientConformanceScenario( - "start-option-validation", - new GeneratedTusClientConformanceCompletion( - "error", - "parallelUploadsWithUploadUrl" - ), - "startOptionValidation", - "startValidationParallelUploadsWithUploadUrl", - new String[0], - new String[] { - "validate-start-options", - }, - new String[0] - ), - new GeneratedTusClientConformanceScenario( - "start-option-validation", - new GeneratedTusClientConformanceCompletion( - "error", - "parallelUploadsWithUploadSize" - ), - "startOptionValidation", - "startValidationParallelUploadsWithUploadSize", - new String[0], - new String[] { - "validate-start-options", - }, - new String[0] - ), - new GeneratedTusClientConformanceScenario( - "start-option-validation", - new GeneratedTusClientConformanceCompletion( - "error", - "parallelUploadsWithDeferredLength" - ), - "startOptionValidation", - "startValidationParallelUploadsWithDeferredLength", - new String[0], - new String[] { - "validate-start-options", - }, - new String[0] - ), - new GeneratedTusClientConformanceScenario( - "start-option-validation", - new GeneratedTusClientConformanceCompletion( - "error", - "parallelUploadsWithUploadDataDuringCreation" - ), - "startOptionValidation", - "startValidationParallelUploadsWithUploadDataDuringCreation", - new String[0], - new String[] { - "validate-start-options", - }, - new String[0] - ), - new GeneratedTusClientConformanceScenario( - "start-option-validation", - new GeneratedTusClientConformanceCompletion( - "error", - "parallelBoundariesWithoutParallelUploads" - ), - "startOptionValidation", - "startValidationParallelBoundariesWithoutParallelUploads", - new String[0], - new String[] { - "validate-start-options", - }, - new String[0] - ), - new GeneratedTusClientConformanceScenario( - "start-option-validation", - new GeneratedTusClientConformanceCompletion( - "error", - "parallelBoundariesLengthMismatch" - ), - "startOptionValidation", - "startValidationParallelBoundariesLengthMismatch", - new String[0], - new String[] { - "validate-start-options", - }, - new String[0] - ), - new GeneratedTusClientConformanceScenario( - "detailed-error", - new GeneratedTusClientConformanceCompletion( - "error", - "unexpectedCreateResponse" - ), - "detailedErrors", - "detailedCreateResponseError", - new String[] { - "createTusUpload", - }, - new String[] { - "report-detailed-errors", - }, - new String[0] - ), - new GeneratedTusClientConformanceScenario( - "detailed-error", - new GeneratedTusClientConformanceCompletion( - "error", - "createUploadRequestFailed" - ), - "detailedErrors", - "detailedCreateRequestError", - new String[] { - "createTusUpload", - }, - new String[] { - "report-detailed-errors", - }, - new String[0] - ), - new GeneratedTusClientConformanceScenario( - "upload-body-headers", - new GeneratedTusClientConformanceCompletion( - "success", - null - ), - "uploadBodyHeaders", - "uploadBodyHeaders", - new String[] { - "createTusUpload", - "patchTusUpload", - }, - new String[] { - "send-upload-body-headers", - }, - new String[0] - ), - new GeneratedTusClientConformanceScenario( - "custom-request-headers", - new GeneratedTusClientConformanceCompletion( - "success", - null - ), - "customRequestHeaders", - "customRequestHeaders", - new String[] { - "createTusUpload", - "patchTusUpload", - }, - new String[] { - "apply-custom-request-headers", - }, - new String[0] - ), - new GeneratedTusClientConformanceScenario( - "resume-from-previous-upload", - new GeneratedTusClientConformanceCompletion( - "success", - null - ), - "resumeUpload", - "resumeFromPreviousUpload", - new String[] { - "getTusUploadOffset", - "patchTusUpload", - }, - new String[] { - "fingerprint-input", - "resume-from-previous-upload", - "store-resume-url", - }, - new String[] { - "fingerprint:contract-resume-fingerprint", - "url-storage-find:contract-resume-fingerprint:1", - "fingerprint:contract-resume-fingerprint", - "upload-url-available", - "progress:5:11", - "progress:11:11", - "chunk-complete:6:11:11", - "url-storage-remove:tus::contract-resume-fingerprint::1337", - "success", - "source-close", - } - ), - new GeneratedTusClientConformanceScenario( - "relative-location-resolution", - new GeneratedTusClientConformanceCompletion( - "success", - null - ), - "relativeLocationResolution", - "relativeLocationResolution", - new String[] { - "createTusUpload", - "patchTusUpload", - }, - new String[] { - "resolve-relative-location", - }, - new String[] { - "upload-url-available", - "progress:0:11", - "progress:11:11", - "chunk-complete:11:11:11", - "success", - "source-close", - } - ), - new GeneratedTusClientConformanceScenario( - "array-buffer-input", - new GeneratedTusClientConformanceCompletion( - "success", - null - ), - "inputSources", - "arrayBufferInput", - new String[] { - "createTusUpload", - "patchTusUpload", - }, - new String[] { - "read-browser-file", - }, - new String[] { - "source-open:array-buffer:11", - "success", - "source-close", - } - ), - new GeneratedTusClientConformanceScenario( - "array-buffer-view-input", - new GeneratedTusClientConformanceCompletion( - "success", - null - ), - "inputSources", - "arrayBufferViewInput", - new String[] { - "createTusUpload", - "patchTusUpload", - }, - new String[] { - "read-browser-file", - }, - new String[] { - "source-open:array-buffer-view:11", - "success", - "source-close", - } - ), - new GeneratedTusClientConformanceScenario( - "web-readable-stream-input", - new GeneratedTusClientConformanceCompletion( - "success", - null - ), - "inputSources", - "webReadableStreamInput", - new String[] { - "createTusUpload", - "patchTusUpload", - }, - new String[] { - "read-web-stream", - }, - new String[] { - "source-open:web-readable-stream:null", - "success", - "source-close", - } - ), - new GeneratedTusClientConformanceScenario( - "node-readable-stream-input", - new GeneratedTusClientConformanceCompletion( - "success", - null - ), - "inputSources", - "nodeReadableStreamInput", - new String[] { - "createTusUpload", - "patchTusUpload", - }, - new String[] { - "read-node-stream", - }, - new String[] { - "source-open:node-readable-stream:null", - "success", - "source-close", - } - ), - new GeneratedTusClientConformanceScenario( - "node-path-input", - new GeneratedTusClientConformanceCompletion( - "success", - null - ), - "inputSources", - "nodePathInput", - new String[] { - "createTusUpload", - "patchTusUpload", - }, - new String[] { - "read-node-file", - }, - new String[] { - "source-open:node-path-reference:11", - "success", - "source-close", - } - ), - new GeneratedTusClientConformanceScenario( - "deferred-length-upload", - new GeneratedTusClientConformanceCompletion( - "success", - null - ), - "deferredLengthUpload", - "deferredLengthUpload", - new String[] { - "createTusUpload", - "patchTusUpload", - }, - new String[] { - "defer-upload-length", - "emit-progress", - }, - new String[] { - "upload-url-available", - "progress:0:11", - "progress:11:11", - "chunk-complete:11:11:11", - "success", - "source-close", - } - ), - new GeneratedTusClientConformanceScenario( - "override-patch-method", - new GeneratedTusClientConformanceCompletion( - "success", - null - ), - "overridePatchMethod", - "overridePatchMethod", - new String[] { - "getTusUploadOffset", - "patchTusUpload", - }, - new String[] { - "override-patch-method", - }, - new String[0] - ), - new GeneratedTusClientConformanceScenario( - "parallel-upload-concat", - new GeneratedTusClientConformanceCompletion( - "success", - null - ), - "parallelUploadConcat", - "parallelUploadConcat", - new String[] { - "createTusUpload", - "createTusUpload", - "patchTusUpload", - "patchTusUpload", - "createTusUpload", - }, - new String[] { - "concatenate-partial-uploads", - "emit-progress", - }, - new String[] { - "progress:5:11", - "chunk-complete:5:5:11", - "progress:11:11", - "chunk-complete:6:11:11", - } - ), - new GeneratedTusClientConformanceScenario( - "parallel-upload-abort-cleanup", - new GeneratedTusClientConformanceCompletion( - "aborted", - null - ), - "parallelUploadConcat", - "parallelUploadAbortCleanup", - new String[] { - "createTusUpload", - "createTusUpload", - "patchTusUpload", - "patchTusUpload", - "terminateTusUpload", - "terminateTusUpload", - }, - new String[] { - "abort-current-request", - "terminate-upload", - "concatenate-partial-uploads", - }, - new String[] { - "request-abort:3", - } - ), - new GeneratedTusClientConformanceScenario( - "retry-patch-after-offset-recovery", - new GeneratedTusClientConformanceCompletion( - "success", - null - ), - "retryOffsetRecovery", - "retryPatchAfterOffsetRecovery", - new String[] { - "createTusUpload", - "patchTusUpload", - "getTusUploadOffset", - "patchTusUpload", - "getTusUploadOffset", - "patchTusUpload", - }, - new String[] { - "retry-with-backoff", - "recover-offset-after-error", - }, - new String[] { - "should-retry:0:true", - "retry-schedule:0", - "should-retry:0:true", - "retry-schedule:0", - } - ), - new GeneratedTusClientConformanceScenario( - "request-lifecycle-hooks", - new GeneratedTusClientConformanceCompletion( - "success", - null - ), - "requestLifecycleHooks", - "requestLifecycleHooks", - new String[] { - "getTusUploadOffset", - }, - new String[] { - "run-request-hooks", - }, - new String[] { - "before-request:0", - "after-response:0", - "success", - "source-close", - } - ), - new GeneratedTusClientConformanceScenario( - "abort-upload", - new GeneratedTusClientConformanceCompletion( - "aborted", - null - ), - "abortUpload", - "abortUpload", - new String[] { - "createTusUpload", - }, - new String[] { - "abort-current-request", - }, - new String[] { - "request-abort:0", - } - ), - new GeneratedTusClientConformanceScenario( - "abort-upload-after-stored-url", - new GeneratedTusClientConformanceCompletion( - "aborted", - null - ), - "abortUpload", - "abortUploadAfterStoredUrl", - new String[] { - "createTusUpload", - "patchTusUpload", - "terminateTusUpload", - }, - new String[] { - "abort-current-request", - "terminate-upload", - }, - new String[] { - "request-abort:1", - } - ), - new GeneratedTusClientConformanceScenario( - "terminate-with-retry", - new GeneratedTusClientConformanceCompletion( - "terminated", - null - ), - "terminateUpload", - "terminateWithRetry", - new String[] { - "createTusUpload", - "patchTusUpload", - "terminateTusUpload", - "terminateTusUpload", - }, - new String[] { - "terminate-upload", - "retry-with-backoff", - }, - new String[0] - ), - }; + GeneratedTusClientConformanceScenarios.CLIENT_CONFORMANCE_SCENARIOS; private GeneratedTusProtocolContract() { } diff --git a/src/test/java/io/tus/java/client/TestGeneratedTusRuntimeEvents.java b/src/test/java/io/tus/java/client/TestGeneratedTusRuntimeEvents.java new file mode 100644 index 00000000..41927ba2 --- /dev/null +++ b/src/test/java/io/tus/java/client/TestGeneratedTusRuntimeEvents.java @@ -0,0 +1,315 @@ +/* + * Code generated from Transloadit API2 TUS protocol contracts; DO NOT EDIT. + * If it looks wrong, please report the issue instead of editing this file by hand; + * the source fix belongs in the protocol contract generator so all TUS clients stay in sync. + */ + +package io.tus.java.client; + +import java.io.ByteArrayInputStream; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.junit.Test; +import org.mockserver.model.HttpRequest; +import org.mockserver.model.HttpResponse; + +import static org.junit.Assert.assertArrayEquals; + +/** + * Tests generated TUS client runtime event fixtures against the real uploader. + */ +public class TestGeneratedTusRuntimeEvents extends MockServerProvider { + private static final GeneratedTusRuntimeEventCase[] CASES = + new GeneratedTusRuntimeEventCase[] { + new GeneratedTusRuntimeEventCase( + "singleUploadLifecycle", + new GeneratedTusRuntimeEventInput( + "hello world", + "generated-contract", + "absolute", + false, + 11, + new GeneratedTusRuntimeEventMetadata[] { + new GeneratedTusRuntimeEventMetadata( + "filename", + "hello.txt" + ), + } + ), + new GeneratedTusRuntimeEventRequest[] { + new GeneratedTusRuntimeEventRequest( + "POST", + "endpoint", + 201, + new GeneratedTusRuntimeEventHeader[] { + new GeneratedTusRuntimeEventHeader( + "Location", + "https://tus.io/uploads/generated-contract" + ), + } + ), + new GeneratedTusRuntimeEventRequest( + "PATCH", + "upload", + 204, + new GeneratedTusRuntimeEventHeader[] { + new GeneratedTusRuntimeEventHeader( + "Upload-Offset", + "11" + ), + } + ), + }, + new String[] { + "progress:0:11", + "progress:11:11", + "chunk-complete:11:11:11", + } + ), + new GeneratedTusRuntimeEventCase( + "relativeLocationResolution", + new GeneratedTusRuntimeEventInput( + "hello world", + "relative-contract", + "relative", + true, + 11, + new GeneratedTusRuntimeEventMetadata[] { + new GeneratedTusRuntimeEventMetadata( + "filename", + "hello.txt" + ), + } + ), + new GeneratedTusRuntimeEventRequest[] { + new GeneratedTusRuntimeEventRequest( + "POST", + "endpoint", + 201, + new GeneratedTusRuntimeEventHeader[] { + new GeneratedTusRuntimeEventHeader( + "Location", + "relative-contract" + ), + } + ), + new GeneratedTusRuntimeEventRequest( + "PATCH", + "upload", + 204, + new GeneratedTusRuntimeEventHeader[] { + new GeneratedTusRuntimeEventHeader( + "Upload-Offset", + "11" + ), + } + ), + }, + new String[] { + "progress:0:11", + "progress:11:11", + "chunk-complete:11:11:11", + } + ), + }; + + /** + * Verifies the sync uploader emits generated progress and chunk-complete events. + */ + @Test + public void testSyncUploaderEmitsGeneratedProgressAndChunkEvents() throws Exception { + for (GeneratedTusRuntimeEventCase testCase : CASES) { + mockServer.reset(); + + final List events = new ArrayList(); + TusClient client = new TusClient(); + client.setUploadCreationURL(endpointUrlFor(testCase)); + + registerResponses(testCase); + + TusUploader uploader = client.createUpload(uploadFor(testCase)); + uploader.setChunkSize(testCase.input.chunkSize); + uploader.setProgressListener(new TusUploader.ProgressListener() { + @Override + public void onProgress(long bytesSent, long bytesTotal) { + events.add("progress:" + bytesSent + ":" + bytesTotal); + } + }); + uploader.setChunkCompleteListener(new TusUploader.ChunkCompleteListener() { + @Override + public void onChunkComplete(long chunkSize, long bytesAccepted, long bytesTotal) { + events.add("chunk-complete:" + chunkSize + ":" + bytesAccepted + ":" + bytesTotal); + } + }); + + while (uploader.uploadChunk() > -1) { + } + uploader.finish(); + + assertArrayEquals( + testCase.scenarioId, + testCase.eventKeys, + events.toArray(new String[events.size()])); + } + } + + private TusUpload uploadFor(GeneratedTusRuntimeEventCase testCase) { + byte[] content = testCase.input.content.getBytes(StandardCharsets.UTF_8); + TusUpload upload = new TusUpload(); + upload.setSize(content.length); + upload.setInputStream(new ByteArrayInputStream(content)); + upload.setMetadata(metadataFor(testCase.input.metadata)); + return upload; + } + + private Map metadataFor(GeneratedTusRuntimeEventMetadata[] metadata) { + Map result = new LinkedHashMap(); + for (GeneratedTusRuntimeEventMetadata entry : metadata) { + result.put(entry.name, entry.value); + } + return result; + } + + private void registerResponses(GeneratedTusRuntimeEventCase testCase) throws Exception { + for (GeneratedTusRuntimeEventRequest request : testCase.requests) { + HttpRequest httpRequest = new HttpRequest() + .withPath(pathFor(testCase, request)); + if (!"upload".equals(request.url)) { + httpRequest.withMethod(request.method); + } + + mockServer.when(httpRequest).respond(responseFor(testCase, request)); + } + } + + private String pathFor( + GeneratedTusRuntimeEventCase testCase, + GeneratedTusRuntimeEventRequest request) throws Exception { + if ("endpoint".equals(request.url)) { + return endpointUrlFor(testCase).getPath(); + } + + return uploadUrlFor(testCase).getPath(); + } + + private HttpResponse responseFor( + GeneratedTusRuntimeEventCase testCase, + GeneratedTusRuntimeEventRequest request) throws Exception { + HttpResponse response = new HttpResponse().withStatusCode(request.statusCode); + for (GeneratedTusRuntimeEventHeader header : request.headers) { + response.withHeader(header.name, headerValueFor(testCase, header)); + } + return response; + } + + private String headerValueFor( + GeneratedTusRuntimeEventCase testCase, + GeneratedTusRuntimeEventHeader header) throws Exception { + if (!"Location".equals(header.name)) { + return header.value; + } + + if ("relative".equals(testCase.input.locationHeaderKind)) { + return testCase.input.uploadPath; + } + + return uploadUrlFor(testCase).toString(); + } + + private URL uploadUrlFor(GeneratedTusRuntimeEventCase testCase) throws Exception { + return new URL(mockServerURL.toString() + "/" + testCase.input.uploadPath); + } + + private URL endpointUrlFor(GeneratedTusRuntimeEventCase testCase) throws Exception { + if (testCase.input.endpointHasTrailingSlash) { + return new URL(mockServerURL.toString() + "/"); + } + + return mockServerURL; + } + + private static final class GeneratedTusRuntimeEventCase { + final String scenarioId; + final GeneratedTusRuntimeEventInput input; + final GeneratedTusRuntimeEventRequest[] requests; + final String[] eventKeys; + + GeneratedTusRuntimeEventCase( + String scenarioId, + GeneratedTusRuntimeEventInput input, + GeneratedTusRuntimeEventRequest[] requests, + String[] eventKeys) { + this.scenarioId = scenarioId; + this.input = input; + this.requests = requests; + this.eventKeys = eventKeys; + } + } + + private static final class GeneratedTusRuntimeEventInput { + final String content; + final String uploadPath; + final String locationHeaderKind; + final boolean endpointHasTrailingSlash; + final int chunkSize; + final GeneratedTusRuntimeEventMetadata[] metadata; + + GeneratedTusRuntimeEventInput( + String content, + String uploadPath, + String locationHeaderKind, + boolean endpointHasTrailingSlash, + int chunkSize, + GeneratedTusRuntimeEventMetadata[] metadata) { + this.content = content; + this.uploadPath = uploadPath; + this.locationHeaderKind = locationHeaderKind; + this.endpointHasTrailingSlash = endpointHasTrailingSlash; + this.chunkSize = chunkSize; + this.metadata = metadata; + } + } + + private static final class GeneratedTusRuntimeEventRequest { + final String method; + final String url; + final int statusCode; + final GeneratedTusRuntimeEventHeader[] headers; + + GeneratedTusRuntimeEventRequest( + String method, + String url, + int statusCode, + GeneratedTusRuntimeEventHeader[] headers) { + this.method = method; + this.url = url; + this.statusCode = statusCode; + this.headers = headers; + } + } + + private static final class GeneratedTusRuntimeEventHeader { + final String name; + final String value; + + GeneratedTusRuntimeEventHeader(String name, String value) { + this.name = name; + this.value = value; + } + } + + private static final class GeneratedTusRuntimeEventMetadata { + final String name; + final String value; + + GeneratedTusRuntimeEventMetadata(String name, String value) { + this.name = name; + this.value = value; + } + } +} From e892ed197f1deceaa986409fd2bb5157da9adf7a Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Sun, 31 May 2026 20:27:30 +0200 Subject: [PATCH 22/43] Cover generated resume runtime events --- .../client/TestGeneratedTusRuntimeEvents.java | 136 +++++++++++++++++- 1 file changed, 134 insertions(+), 2 deletions(-) diff --git a/src/test/java/io/tus/java/client/TestGeneratedTusRuntimeEvents.java b/src/test/java/io/tus/java/client/TestGeneratedTusRuntimeEvents.java index 41927ba2..8897a811 100644 --- a/src/test/java/io/tus/java/client/TestGeneratedTusRuntimeEvents.java +++ b/src/test/java/io/tus/java/client/TestGeneratedTusRuntimeEvents.java @@ -19,6 +19,8 @@ import org.mockserver.model.HttpResponse; import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; /** * Tests generated TUS client runtime event fixtures against the real uploader. @@ -34,6 +36,8 @@ public class TestGeneratedTusRuntimeEvents extends MockServerProvider { "absolute", false, 11, + null, + false, new GeneratedTusRuntimeEventMetadata[] { new GeneratedTusRuntimeEventMetadata( "filename", @@ -71,6 +75,52 @@ public class TestGeneratedTusRuntimeEvents extends MockServerProvider { "chunk-complete:11:11:11", } ), + new GeneratedTusRuntimeEventCase( + "resumeFromPreviousUpload", + new GeneratedTusRuntimeEventInput( + "hello world", + "resume-contract", + "stored", + false, + 6, + "contract-resume-fingerprint", + true, + new GeneratedTusRuntimeEventMetadata[0] + ), + new GeneratedTusRuntimeEventRequest[] { + new GeneratedTusRuntimeEventRequest( + "HEAD", + "upload", + 200, + new GeneratedTusRuntimeEventHeader[] { + new GeneratedTusRuntimeEventHeader( + "Upload-Length", + "11" + ), + new GeneratedTusRuntimeEventHeader( + "Upload-Offset", + "5" + ), + } + ), + new GeneratedTusRuntimeEventRequest( + "PATCH", + "upload", + 204, + new GeneratedTusRuntimeEventHeader[] { + new GeneratedTusRuntimeEventHeader( + "Upload-Offset", + "11" + ), + } + ), + }, + new String[] { + "progress:5:11", + "progress:11:11", + "chunk-complete:6:11:11", + } + ), new GeneratedTusRuntimeEventCase( "relativeLocationResolution", new GeneratedTusRuntimeEventInput( @@ -79,6 +129,8 @@ public class TestGeneratedTusRuntimeEvents extends MockServerProvider { "relative", true, 11, + null, + false, new GeneratedTusRuntimeEventMetadata[] { new GeneratedTusRuntimeEventMetadata( "filename", @@ -129,10 +181,17 @@ public void testSyncUploaderEmitsGeneratedProgressAndChunkEvents() throws Except final List events = new ArrayList(); TusClient client = new TusClient(); client.setUploadCreationURL(endpointUrlFor(testCase)); + GeneratedTusRuntimeEventUrlStore urlStore = urlStoreFor(testCase); + if (urlStore != null) { + client.enableResuming(urlStore); + } + if (testCase.input.removeFingerprintOnSuccess) { + client.enableRemoveFingerprintOnSuccess(); + } registerResponses(testCase); - TusUploader uploader = client.createUpload(uploadFor(testCase)); + TusUploader uploader = uploaderFor(client, testCase); uploader.setChunkSize(testCase.input.chunkSize); uploader.setProgressListener(new TusUploader.ProgressListener() { @Override @@ -155,7 +214,17 @@ public void onChunkComplete(long chunkSize, long bytesAccepted, long bytesTotal) testCase.scenarioId, testCase.eventKeys, events.toArray(new String[events.size()])); + assertStoredUploadState(testCase, urlStore); + } + } + + private TusUploader uploaderFor(TusClient client, GeneratedTusRuntimeEventCase testCase) + throws Exception { + if (testCase.input.fingerprint != null) { + return client.resumeUpload(uploadFor(testCase)); } + + return client.createUpload(uploadFor(testCase)); } private TusUpload uploadFor(GeneratedTusRuntimeEventCase testCase) { @@ -164,6 +233,9 @@ private TusUpload uploadFor(GeneratedTusRuntimeEventCase testCase) { upload.setSize(content.length); upload.setInputStream(new ByteArrayInputStream(content)); upload.setMetadata(metadataFor(testCase.input.metadata)); + if (testCase.input.fingerprint != null) { + upload.setFingerprint(testCase.input.fingerprint); + } return upload; } @@ -179,7 +251,7 @@ private void registerResponses(GeneratedTusRuntimeEventCase testCase) throws Exc for (GeneratedTusRuntimeEventRequest request : testCase.requests) { HttpRequest httpRequest = new HttpRequest() .withPath(pathFor(testCase, request)); - if (!"upload".equals(request.url)) { + if (!"upload".equals(request.url) || "HEAD".equals(request.method)) { httpRequest.withMethod(request.method); } @@ -233,6 +305,41 @@ private URL endpointUrlFor(GeneratedTusRuntimeEventCase testCase) throws Excepti return mockServerURL; } + private GeneratedTusRuntimeEventUrlStore urlStoreFor( + GeneratedTusRuntimeEventCase testCase) throws Exception { + if (testCase.input.fingerprint == null) { + return null; + } + + GeneratedTusRuntimeEventUrlStore store = new GeneratedTusRuntimeEventUrlStore(); + store.set(testCase.input.fingerprint, uploadUrlFor(testCase)); + return store; + } + + private void assertStoredUploadState( + GeneratedTusRuntimeEventCase testCase, + GeneratedTusRuntimeEventUrlStore urlStore) { + if (urlStore == null) { + return; + } + + URL storedUrl = urlStore.get(testCase.input.fingerprint); + if (testCase.input.removeFingerprintOnSuccess) { + assertNull(testCase.scenarioId, storedUrl); + return; + } + + assertEquals(testCase.scenarioId, uploadUrlForUnchecked(testCase), storedUrl); + } + + private URL uploadUrlForUnchecked(GeneratedTusRuntimeEventCase testCase) { + try { + return uploadUrlFor(testCase); + } catch (Exception error) { + throw new AssertionError(error); + } + } + private static final class GeneratedTusRuntimeEventCase { final String scenarioId; final GeneratedTusRuntimeEventInput input; @@ -257,6 +364,8 @@ private static final class GeneratedTusRuntimeEventInput { final String locationHeaderKind; final boolean endpointHasTrailingSlash; final int chunkSize; + final String fingerprint; + final boolean removeFingerprintOnSuccess; final GeneratedTusRuntimeEventMetadata[] metadata; GeneratedTusRuntimeEventInput( @@ -265,12 +374,16 @@ private static final class GeneratedTusRuntimeEventInput { String locationHeaderKind, boolean endpointHasTrailingSlash, int chunkSize, + String fingerprint, + boolean removeFingerprintOnSuccess, GeneratedTusRuntimeEventMetadata[] metadata) { this.content = content; this.uploadPath = uploadPath; this.locationHeaderKind = locationHeaderKind; this.endpointHasTrailingSlash = endpointHasTrailingSlash; this.chunkSize = chunkSize; + this.fingerprint = fingerprint; + this.removeFingerprintOnSuccess = removeFingerprintOnSuccess; this.metadata = metadata; } } @@ -312,4 +425,23 @@ private static final class GeneratedTusRuntimeEventMetadata { this.value = value; } } + + private static final class GeneratedTusRuntimeEventUrlStore implements TusURLStore { + private final Map values = new LinkedHashMap(); + + @Override + public URL get(String fingerprint) { + return values.get(fingerprint); + } + + @Override + public void set(String fingerprint, URL url) { + values.put(fingerprint, url); + } + + @Override + public void remove(String fingerprint) { + values.remove(fingerprint); + } + } } From 420dea9ed968fe2224d5d131a3ca436317fb2785 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Sun, 31 May 2026 20:44:03 +0200 Subject: [PATCH 23/43] Keep generated runtime canary lint-clean --- .../client/TestGeneratedTusRuntimeEvents.java | 46 ++++++++++++------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/src/test/java/io/tus/java/client/TestGeneratedTusRuntimeEvents.java b/src/test/java/io/tus/java/client/TestGeneratedTusRuntimeEvents.java index 8897a811..768b6139 100644 --- a/src/test/java/io/tus/java/client/TestGeneratedTusRuntimeEvents.java +++ b/src/test/java/io/tus/java/client/TestGeneratedTusRuntimeEvents.java @@ -37,7 +37,6 @@ public class TestGeneratedTusRuntimeEvents extends MockServerProvider { false, 11, null, - false, new GeneratedTusRuntimeEventMetadata[] { new GeneratedTusRuntimeEventMetadata( "filename", @@ -83,8 +82,10 @@ public class TestGeneratedTusRuntimeEvents extends MockServerProvider { "stored", false, 6, - "contract-resume-fingerprint", - true, + new GeneratedTusRuntimeEventStoredUpload( + "contract-resume-fingerprint", + true + ), new GeneratedTusRuntimeEventMetadata[0] ), new GeneratedTusRuntimeEventRequest[] { @@ -130,7 +131,6 @@ public class TestGeneratedTusRuntimeEvents extends MockServerProvider { true, 11, null, - false, new GeneratedTusRuntimeEventMetadata[] { new GeneratedTusRuntimeEventMetadata( "filename", @@ -185,7 +185,9 @@ public void testSyncUploaderEmitsGeneratedProgressAndChunkEvents() throws Except if (urlStore != null) { client.enableResuming(urlStore); } - if (testCase.input.removeFingerprintOnSuccess) { + if ( + testCase.input.storedUpload != null + && testCase.input.storedUpload.removeFingerprintOnSuccess) { client.enableRemoveFingerprintOnSuccess(); } @@ -207,6 +209,7 @@ public void onChunkComplete(long chunkSize, long bytesAccepted, long bytesTotal) }); while (uploader.uploadChunk() > -1) { + continue; } uploader.finish(); @@ -220,7 +223,7 @@ public void onChunkComplete(long chunkSize, long bytesAccepted, long bytesTotal) private TusUploader uploaderFor(TusClient client, GeneratedTusRuntimeEventCase testCase) throws Exception { - if (testCase.input.fingerprint != null) { + if (testCase.input.storedUpload != null) { return client.resumeUpload(uploadFor(testCase)); } @@ -233,8 +236,8 @@ private TusUpload uploadFor(GeneratedTusRuntimeEventCase testCase) { upload.setSize(content.length); upload.setInputStream(new ByteArrayInputStream(content)); upload.setMetadata(metadataFor(testCase.input.metadata)); - if (testCase.input.fingerprint != null) { - upload.setFingerprint(testCase.input.fingerprint); + if (testCase.input.storedUpload != null) { + upload.setFingerprint(testCase.input.storedUpload.fingerprint); } return upload; } @@ -307,12 +310,12 @@ private URL endpointUrlFor(GeneratedTusRuntimeEventCase testCase) throws Excepti private GeneratedTusRuntimeEventUrlStore urlStoreFor( GeneratedTusRuntimeEventCase testCase) throws Exception { - if (testCase.input.fingerprint == null) { + if (testCase.input.storedUpload == null) { return null; } GeneratedTusRuntimeEventUrlStore store = new GeneratedTusRuntimeEventUrlStore(); - store.set(testCase.input.fingerprint, uploadUrlFor(testCase)); + store.set(testCase.input.storedUpload.fingerprint, uploadUrlFor(testCase)); return store; } @@ -323,8 +326,8 @@ private void assertStoredUploadState( return; } - URL storedUrl = urlStore.get(testCase.input.fingerprint); - if (testCase.input.removeFingerprintOnSuccess) { + URL storedUrl = urlStore.get(testCase.input.storedUpload.fingerprint); + if (testCase.input.storedUpload.removeFingerprintOnSuccess) { assertNull(testCase.scenarioId, storedUrl); return; } @@ -364,8 +367,7 @@ private static final class GeneratedTusRuntimeEventInput { final String locationHeaderKind; final boolean endpointHasTrailingSlash; final int chunkSize; - final String fingerprint; - final boolean removeFingerprintOnSuccess; + final GeneratedTusRuntimeEventStoredUpload storedUpload; final GeneratedTusRuntimeEventMetadata[] metadata; GeneratedTusRuntimeEventInput( @@ -374,17 +376,27 @@ private static final class GeneratedTusRuntimeEventInput { String locationHeaderKind, boolean endpointHasTrailingSlash, int chunkSize, - String fingerprint, - boolean removeFingerprintOnSuccess, + GeneratedTusRuntimeEventStoredUpload storedUpload, GeneratedTusRuntimeEventMetadata[] metadata) { this.content = content; this.uploadPath = uploadPath; this.locationHeaderKind = locationHeaderKind; this.endpointHasTrailingSlash = endpointHasTrailingSlash; this.chunkSize = chunkSize; + this.storedUpload = storedUpload; + this.metadata = metadata; + } + } + + private static final class GeneratedTusRuntimeEventStoredUpload { + final String fingerprint; + final boolean removeFingerprintOnSuccess; + + GeneratedTusRuntimeEventStoredUpload( + String fingerprint, + boolean removeFingerprintOnSuccess) { this.fingerprint = fingerprint; this.removeFingerprintOnSuccess = removeFingerprintOnSuccess; - this.metadata = metadata; } } From 40143841038f51a1dc56aa5db5e5b000e6d85ff3 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Sun, 31 May 2026 22:12:19 +0200 Subject: [PATCH 24/43] Support deferred length uploads --- .../java/io/tus/java/client/TusClient.java | 6 +- .../java/io/tus/java/client/TusUpload.java | 21 ++++ .../java/io/tus/java/client/TusUploader.java | 12 ++ .../client/GeneratedTusProtocolContract.java | 1 + .../client/TestGeneratedTusRuntimeEvents.java | 115 +++++++++++++++++- .../io/tus/java/client/TestTusClient.java | 34 ++++++ .../io/tus/java/client/TestTusUploader.java | 36 ++++++ 7 files changed, 220 insertions(+), 5 deletions(-) diff --git a/src/main/java/io/tus/java/client/TusClient.java b/src/main/java/io/tus/java/client/TusClient.java index 4b56a474..d4df183f 100644 --- a/src/main/java/io/tus/java/client/TusClient.java +++ b/src/main/java/io/tus/java/client/TusClient.java @@ -203,7 +203,11 @@ public TusUploader createUpload(@NotNull TusUpload upload) throws ProtocolExcept connection.setRequestProperty("Upload-Metadata", encodedMetadata); } - connection.addRequestProperty("Upload-Length", Long.toString(upload.getSize())); + if (upload.isUploadLengthDeferred()) { + connection.addRequestProperty("Upload-Defer-Length", "1"); + } else { + connection.addRequestProperty("Upload-Length", Long.toString(upload.getSize())); + } connection.connect(); int responseCode = connection.getResponseCode(); diff --git a/src/main/java/io/tus/java/client/TusUpload.java b/src/main/java/io/tus/java/client/TusUpload.java index 5559af59..ecd63732 100644 --- a/src/main/java/io/tus/java/client/TusUpload.java +++ b/src/main/java/io/tus/java/client/TusUpload.java @@ -21,6 +21,7 @@ public class TusUpload { private TusInputStream tusInputStream; private String fingerprint; private Map metadata; + private boolean uploadLengthDeferred; /** * Create a new TusUpload object. @@ -62,6 +63,26 @@ public void setSize(long size) { this.size = size; } + /** + * Returns whether upload creation should defer declaring the upload length. + * @return True if the Upload-Defer-Length creation header should be used. + */ + public boolean isUploadLengthDeferred() { + return uploadLengthDeferred; + } + + /** + * Set whether upload creation should defer declaring the upload length. + * + * When enabled, the upload is created with Upload-Defer-Length and the uploader declares + * Upload-Length on the first PATCH request. + * + * @param uploadLengthDeferred True to use deferred upload length creation. + */ + public void setUploadLengthDeferred(boolean uploadLengthDeferred) { + this.uploadLengthDeferred = uploadLengthDeferred; + } + /** * Returns the file specific fingerprint. * @return Fingerprint as String. diff --git a/src/main/java/io/tus/java/client/TusUploader.java b/src/main/java/io/tus/java/client/TusUploader.java index 8af4c462..d3b97d78 100644 --- a/src/main/java/io/tus/java/client/TusUploader.java +++ b/src/main/java/io/tus/java/client/TusUploader.java @@ -56,7 +56,9 @@ public interface ChunkCompleteListener { private int requestPayloadSize = 10 * 1024 * 1024; private int bytesRemainingForRequest; private long requestStartOffset; + private boolean requestDeclaresUploadLength; private boolean requestProgressStarted; + private boolean uploadLengthDeclared; private ProgressListener progressListener; private ChunkCompleteListener chunkCompleteListener; @@ -81,6 +83,7 @@ public TusUploader(TusClient client, TusUpload upload, URL uploadURL, TusInputSt this.offset = offset; this.client = client; this.upload = upload; + uploadLengthDeclared = !upload.isUploadLengthDeferred(); input.seekTo(offset); @@ -94,6 +97,7 @@ private void openConnection() throws IOException, ProtocolException { } bytesRemainingForRequest = requestPayloadSize; + requestDeclaresUploadLength = false; requestStartOffset = offset; requestProgressStarted = false; input.mark(requestPayloadSize); @@ -105,6 +109,10 @@ private void openConnection() throws IOException, ProtocolException { } client.prepareConnection(connection); connection.setRequestProperty("Upload-Offset", Long.toString(offset)); + if (!uploadLengthDeclared) { + connection.setRequestProperty("Upload-Length", Long.toString(upload.getSize())); + requestDeclaresUploadLength = true; + } connection.setRequestProperty("Content-Type", "application/offset+octet-stream"); connection.setRequestProperty("Expect", "100-continue"); @@ -409,8 +417,12 @@ private void finishConnection() throws ProtocolException, IOException { connection); } + if (requestDeclaresUploadLength) { + uploadLengthDeclared = true; + } notifyChunkComplete(serverOffset - requestStartOffset, serverOffset); connection = null; + requestDeclaresUploadLength = false; requestProgressStarted = false; } } diff --git a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java index e4108fbf..13c1127b 100644 --- a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java +++ b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java @@ -670,6 +670,7 @@ final class GeneratedTusProtocolContract { "patchTusUpload", }, new String[] { + "abort-current-request", "concatenate-partial-uploads", "emit-progress", "split-parallel-upload-boundaries", diff --git a/src/test/java/io/tus/java/client/TestGeneratedTusRuntimeEvents.java b/src/test/java/io/tus/java/client/TestGeneratedTusRuntimeEvents.java index 768b6139..284d1f83 100644 --- a/src/test/java/io/tus/java/client/TestGeneratedTusRuntimeEvents.java +++ b/src/test/java/io/tus/java/client/TestGeneratedTusRuntimeEvents.java @@ -30,6 +30,7 @@ public class TestGeneratedTusRuntimeEvents extends MockServerProvider { new GeneratedTusRuntimeEventCase[] { new GeneratedTusRuntimeEventCase( "singleUploadLifecycle", + false, new GeneratedTusRuntimeEventInput( "hello world", "generated-contract", @@ -49,6 +50,12 @@ public class TestGeneratedTusRuntimeEvents extends MockServerProvider { "POST", "endpoint", 201, + new GeneratedTusRuntimeEventHeader[] { + new GeneratedTusRuntimeEventHeader( + "Upload-Length", + "11" + ), + }, new GeneratedTusRuntimeEventHeader[] { new GeneratedTusRuntimeEventHeader( "Location", @@ -60,6 +67,12 @@ public class TestGeneratedTusRuntimeEvents extends MockServerProvider { "PATCH", "upload", 204, + new GeneratedTusRuntimeEventHeader[] { + new GeneratedTusRuntimeEventHeader( + "Upload-Offset", + "0" + ), + }, new GeneratedTusRuntimeEventHeader[] { new GeneratedTusRuntimeEventHeader( "Upload-Offset", @@ -76,6 +89,7 @@ public class TestGeneratedTusRuntimeEvents extends MockServerProvider { ), new GeneratedTusRuntimeEventCase( "resumeFromPreviousUpload", + false, new GeneratedTusRuntimeEventInput( "hello world", "resume-contract", @@ -93,6 +107,7 @@ public class TestGeneratedTusRuntimeEvents extends MockServerProvider { "HEAD", "upload", 200, + new GeneratedTusRuntimeEventHeader[0], new GeneratedTusRuntimeEventHeader[] { new GeneratedTusRuntimeEventHeader( "Upload-Length", @@ -108,6 +123,12 @@ public class TestGeneratedTusRuntimeEvents extends MockServerProvider { "PATCH", "upload", 204, + new GeneratedTusRuntimeEventHeader[] { + new GeneratedTusRuntimeEventHeader( + "Upload-Offset", + "5" + ), + }, new GeneratedTusRuntimeEventHeader[] { new GeneratedTusRuntimeEventHeader( "Upload-Offset", @@ -124,6 +145,7 @@ public class TestGeneratedTusRuntimeEvents extends MockServerProvider { ), new GeneratedTusRuntimeEventCase( "relativeLocationResolution", + false, new GeneratedTusRuntimeEventInput( "hello world", "relative-contract", @@ -143,6 +165,12 @@ public class TestGeneratedTusRuntimeEvents extends MockServerProvider { "POST", "endpoint", 201, + new GeneratedTusRuntimeEventHeader[] { + new GeneratedTusRuntimeEventHeader( + "Upload-Length", + "11" + ), + }, new GeneratedTusRuntimeEventHeader[] { new GeneratedTusRuntimeEventHeader( "Location", @@ -154,6 +182,75 @@ public class TestGeneratedTusRuntimeEvents extends MockServerProvider { "PATCH", "upload", 204, + new GeneratedTusRuntimeEventHeader[] { + new GeneratedTusRuntimeEventHeader( + "Upload-Offset", + "0" + ), + }, + new GeneratedTusRuntimeEventHeader[] { + new GeneratedTusRuntimeEventHeader( + "Upload-Offset", + "11" + ), + } + ), + }, + new String[] { + "progress:0:11", + "progress:11:11", + "chunk-complete:11:11:11", + } + ), + new GeneratedTusRuntimeEventCase( + "deferredLengthUpload", + true, + new GeneratedTusRuntimeEventInput( + "hello world", + "deferred-contract", + "absolute", + false, + 100, + null, + new GeneratedTusRuntimeEventMetadata[] { + new GeneratedTusRuntimeEventMetadata( + "filename", + "hello.txt" + ), + } + ), + new GeneratedTusRuntimeEventRequest[] { + new GeneratedTusRuntimeEventRequest( + "POST", + "endpoint", + 201, + new GeneratedTusRuntimeEventHeader[] { + new GeneratedTusRuntimeEventHeader( + "Upload-Defer-Length", + "1" + ), + }, + new GeneratedTusRuntimeEventHeader[] { + new GeneratedTusRuntimeEventHeader( + "Location", + "https://tus.io/uploads/deferred-contract" + ), + } + ), + new GeneratedTusRuntimeEventRequest( + "PATCH", + "upload", + 204, + new GeneratedTusRuntimeEventHeader[] { + new GeneratedTusRuntimeEventHeader( + "Upload-Length", + "11" + ), + new GeneratedTusRuntimeEventHeader( + "Upload-Offset", + "0" + ), + }, new GeneratedTusRuntimeEventHeader[] { new GeneratedTusRuntimeEventHeader( "Upload-Offset", @@ -239,6 +336,7 @@ private TusUpload uploadFor(GeneratedTusRuntimeEventCase testCase) { if (testCase.input.storedUpload != null) { upload.setFingerprint(testCase.input.storedUpload.fingerprint); } + upload.setUploadLengthDeferred(testCase.uploadLengthDeferred); return upload; } @@ -257,6 +355,9 @@ private void registerResponses(GeneratedTusRuntimeEventCase testCase) throws Exc if (!"upload".equals(request.url) || "HEAD".equals(request.method)) { httpRequest.withMethod(request.method); } + for (GeneratedTusRuntimeEventHeader header : request.requestHeaders) { + httpRequest.withHeader(header.name, header.value); + } mockServer.when(httpRequest).respond(responseFor(testCase, request)); } @@ -276,7 +377,7 @@ private HttpResponse responseFor( GeneratedTusRuntimeEventCase testCase, GeneratedTusRuntimeEventRequest request) throws Exception { HttpResponse response = new HttpResponse().withStatusCode(request.statusCode); - for (GeneratedTusRuntimeEventHeader header : request.headers) { + for (GeneratedTusRuntimeEventHeader header : request.responseHeaders) { response.withHeader(header.name, headerValueFor(testCase, header)); } return response; @@ -345,16 +446,19 @@ private URL uploadUrlForUnchecked(GeneratedTusRuntimeEventCase testCase) { private static final class GeneratedTusRuntimeEventCase { final String scenarioId; + final boolean uploadLengthDeferred; final GeneratedTusRuntimeEventInput input; final GeneratedTusRuntimeEventRequest[] requests; final String[] eventKeys; GeneratedTusRuntimeEventCase( String scenarioId, + boolean uploadLengthDeferred, GeneratedTusRuntimeEventInput input, GeneratedTusRuntimeEventRequest[] requests, String[] eventKeys) { this.scenarioId = scenarioId; + this.uploadLengthDeferred = uploadLengthDeferred; this.input = input; this.requests = requests; this.eventKeys = eventKeys; @@ -404,17 +508,20 @@ private static final class GeneratedTusRuntimeEventRequest { final String method; final String url; final int statusCode; - final GeneratedTusRuntimeEventHeader[] headers; + final GeneratedTusRuntimeEventHeader[] requestHeaders; + final GeneratedTusRuntimeEventHeader[] responseHeaders; GeneratedTusRuntimeEventRequest( String method, String url, int statusCode, - GeneratedTusRuntimeEventHeader[] headers) { + GeneratedTusRuntimeEventHeader[] requestHeaders, + GeneratedTusRuntimeEventHeader[] responseHeaders) { this.method = method; this.url = url; this.statusCode = statusCode; - this.headers = headers; + this.requestHeaders = requestHeaders; + this.responseHeaders = responseHeaders; } } diff --git a/src/test/java/io/tus/java/client/TestTusClient.java b/src/test/java/io/tus/java/client/TestTusClient.java index 9bd1ec5a..33211f2b 100644 --- a/src/test/java/io/tus/java/client/TestTusClient.java +++ b/src/test/java/io/tus/java/client/TestTusClient.java @@ -107,6 +107,40 @@ public void testCreateUpload() throws IOException, ProtocolException { assertEquals(uploader.getUploadURL(), new URL(mockServerURL + "/foo")); } + + /** + * Verifies if uploads can be created with deferred upload length. + * @throws IOException if upload data cannot be read. + * @throws ProtocolException if the upload cannot be constructed. + */ + @Test + public void testCreateUploadWithDeferredLength() throws IOException, ProtocolException { + mockServer.when(new HttpRequest() + .withMethod("POST") + .withPath("/files") + .withHeader("Tus-Resumable", TusClient.TUS_VERSION) + .withHeader("Upload-Defer-Length", "1")) + .respond(new HttpResponse() + .withStatusCode(201) + .withHeader("Tus-Resumable", TusClient.TUS_VERSION) + .withHeader("Location", mockServerURL + "/foo")); + + TusClient client = new TusClient(); + client.setUploadCreationURL(mockServerURL); + TusUpload upload = new TusUpload(); + upload.setSize(10); + upload.setUploadLengthDeferred(true); + upload.setInputStream(new ByteArrayInputStream(new byte[10])); + TusUploader uploader = client.createUpload(upload); + HttpRequest[] requests = mockServer.retrieveRecordedRequests(new HttpRequest() + .withMethod("POST") + .withPath("/files")); + + assertEquals(uploader.getUploadURL(), new URL(mockServerURL + "/foo")); + assertEquals(1, requests.length); + assertFalse(requests[0].containsHeader("Upload-Length")); + } + /** * Verifies if uploads can be created with the tus client through a proxy. * @throws IOException if upload data cannot be read. diff --git a/src/test/java/io/tus/java/client/TestTusUploader.java b/src/test/java/io/tus/java/client/TestTusUploader.java index b81e2b75..b1a7b70c 100644 --- a/src/test/java/io/tus/java/client/TestTusUploader.java +++ b/src/test/java/io/tus/java/client/TestTusUploader.java @@ -74,6 +74,42 @@ public void testTusUploader() throws IOException, ProtocolException { uploader.finish(); } + /** + * Tests if deferred-length uploads declare the upload length on the first PATCH request. + * @throws IOException + * @throws ProtocolException + */ + @Test + public void testTusUploaderDeclaresDeferredLength() throws IOException, ProtocolException { + byte[] content = "hello world".getBytes(); + + mockServer.when(new HttpRequest() + .withPath("/files/deferred") + .withHeader("Tus-Resumable", TusClient.TUS_VERSION) + .withHeader("Upload-Length", "11") + .withHeader("Upload-Offset", "0") + .withHeader("Content-Type", "application/offset+octet-stream") + .withBody(content)) + .respond(new HttpResponse() + .withStatusCode(204) + .withHeader("Tus-Resumable", TusClient.TUS_VERSION) + .withHeader("Upload-Offset", "11")); + + TusClient client = new TusClient(); + URL uploadUrl = new URL(mockServerURL + "/deferred"); + TusInputStream input = new TusInputStream(new ByteArrayInputStream(content)); + TusUpload upload = new TusUpload(); + upload.setSize(11); + upload.setUploadLengthDeferred(true); + + TusUploader uploader = new TusUploader(client, upload, uploadUrl, input, 0); + + uploader.setChunkSize(100); + assertEquals(11, uploader.uploadChunk()); + assertEquals(-1, uploader.uploadChunk()); + uploader.finish(); + } + /** * Tests if the {@link TusUploader} actually uploads files through a proxy. * @throws IOException From 78a231b9595a4a09f69b76ac81118231642fe172 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Mon, 1 Jun 2026 07:57:42 +0200 Subject: [PATCH 25/43] Regenerate TUS event contract --- .../tus/java/client/GeneratedTusClientConformanceScenarios.java | 1 + .../io/tus/java/client/TestGeneratedTusConformanceEvents.java | 1 + 2 files changed, 2 insertions(+) diff --git a/src/test/java/io/tus/java/client/GeneratedTusClientConformanceScenarios.java b/src/test/java/io/tus/java/client/GeneratedTusClientConformanceScenarios.java index 5775ae46..b7590b76 100644 --- a/src/test/java/io/tus/java/client/GeneratedTusClientConformanceScenarios.java +++ b/src/test/java/io/tus/java/client/GeneratedTusClientConformanceScenarios.java @@ -86,6 +86,7 @@ final class GeneratedTusClientConformanceScenarios { "progress:0:11", "progress:5:11", "upload-url-available", + "chunk-complete:5:5:11", "progress:5:11", "progress:10:11", "chunk-complete:5:10:11", diff --git a/src/test/java/io/tus/java/client/TestGeneratedTusConformanceEvents.java b/src/test/java/io/tus/java/client/TestGeneratedTusConformanceEvents.java index 40526792..b409cc71 100644 --- a/src/test/java/io/tus/java/client/TestGeneratedTusConformanceEvents.java +++ b/src/test/java/io/tus/java/client/TestGeneratedTusConformanceEvents.java @@ -49,6 +49,7 @@ public class TestGeneratedTusConformanceEvents { "progress:0:11", "progress:5:11", "upload-url-available", + "chunk-complete:5:5:11", "progress:5:11", "progress:10:11", "chunk-complete:5:10:11", From 5c1f6767eb85a13868d7b41a03586eaffed1ff1f Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Mon, 1 Jun 2026 08:18:22 +0200 Subject: [PATCH 26/43] Carry generated TUS event policy --- ...eneratedTusClientConformanceScenarios.java | 175 ++++++++++++++++++ .../client/GeneratedTusProtocolContract.java | 21 +++ .../TestGeneratedTusConformanceEvents.java | 112 ++++++++++- .../client/TestGeneratedTusRuntimeEvents.java | 69 ++++++- 4 files changed, 372 insertions(+), 5 deletions(-) diff --git a/src/test/java/io/tus/java/client/GeneratedTusClientConformanceScenarios.java b/src/test/java/io/tus/java/client/GeneratedTusClientConformanceScenarios.java index b7590b76..bcc84f1f 100644 --- a/src/test/java/io/tus/java/client/GeneratedTusClientConformanceScenarios.java +++ b/src/test/java/io/tus/java/client/GeneratedTusClientConformanceScenarios.java @@ -32,6 +32,11 @@ final class GeneratedTusClientConformanceScenarios { "emit-progress", "abort-current-request", }, + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact-except-extra-progress", + "milestone", + "may-emit-extra-samples" + ), new String[] { "fingerprint:contract-single-fingerprint", "upload-url-available", @@ -58,6 +63,11 @@ final class GeneratedTusClientConformanceScenarios { "upload-during-creation", "emit-progress", }, + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact-except-extra-progress", + "milestone", + "may-emit-extra-samples" + ), new String[] { "progress:0:11", "progress:11:11", @@ -82,6 +92,11 @@ final class GeneratedTusClientConformanceScenarios { "upload-during-creation", "emit-progress", }, + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact-except-extra-progress", + "milestone", + "may-emit-extra-samples" + ), new String[] { "progress:0:11", "progress:5:11", @@ -111,6 +126,11 @@ final class GeneratedTusClientConformanceScenarios { new String[] { "select-client-protocol", }, + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact-except-extra-progress", + "milestone", + "may-emit-extra-samples" + ), new String[] { "progress:0:11", "progress:11:11", @@ -134,6 +154,11 @@ final class GeneratedTusClientConformanceScenarios { new String[] { "select-client-protocol", }, + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact-except-extra-progress", + "milestone", + "may-emit-extra-samples" + ), new String[] { "upload-url-available", "progress:5:11", @@ -155,6 +180,11 @@ final class GeneratedTusClientConformanceScenarios { new String[] { "validate-start-options", }, + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null + ), new String[0] ), new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( @@ -169,6 +199,11 @@ final class GeneratedTusClientConformanceScenarios { new String[] { "validate-start-options", }, + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null + ), new String[0] ), new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( @@ -183,6 +218,11 @@ final class GeneratedTusClientConformanceScenarios { new String[] { "validate-start-options", }, + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null + ), new String[0] ), new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( @@ -197,6 +237,11 @@ final class GeneratedTusClientConformanceScenarios { new String[] { "validate-start-options", }, + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null + ), new String[0] ), new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( @@ -211,6 +256,11 @@ final class GeneratedTusClientConformanceScenarios { new String[] { "validate-start-options", }, + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null + ), new String[0] ), new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( @@ -225,6 +275,11 @@ final class GeneratedTusClientConformanceScenarios { new String[] { "validate-start-options", }, + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null + ), new String[0] ), new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( @@ -239,6 +294,11 @@ final class GeneratedTusClientConformanceScenarios { new String[] { "validate-start-options", }, + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null + ), new String[0] ), new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( @@ -253,6 +313,11 @@ final class GeneratedTusClientConformanceScenarios { new String[] { "validate-start-options", }, + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null + ), new String[0] ), new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( @@ -267,6 +332,11 @@ final class GeneratedTusClientConformanceScenarios { new String[] { "validate-start-options", }, + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null + ), new String[0] ), new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( @@ -281,6 +351,11 @@ final class GeneratedTusClientConformanceScenarios { new String[] { "validate-start-options", }, + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null + ), new String[0] ), new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( @@ -297,6 +372,11 @@ final class GeneratedTusClientConformanceScenarios { new String[] { "report-detailed-errors", }, + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null + ), new String[0] ), new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( @@ -313,6 +393,11 @@ final class GeneratedTusClientConformanceScenarios { new String[] { "report-detailed-errors", }, + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null + ), new String[0] ), new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( @@ -330,6 +415,11 @@ final class GeneratedTusClientConformanceScenarios { new String[] { "send-upload-body-headers", }, + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null + ), new String[0] ), new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( @@ -347,6 +437,11 @@ final class GeneratedTusClientConformanceScenarios { new String[] { "apply-custom-request-headers", }, + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null + ), new String[0] ), new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( @@ -366,6 +461,11 @@ final class GeneratedTusClientConformanceScenarios { "resume-from-previous-upload", "store-resume-url", }, + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact-except-extra-progress", + "milestone", + "may-emit-extra-samples" + ), new String[] { "fingerprint:contract-resume-fingerprint", "url-storage-find:contract-resume-fingerprint:1", @@ -394,6 +494,11 @@ final class GeneratedTusClientConformanceScenarios { new String[] { "resolve-relative-location", }, + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact-except-extra-progress", + "milestone", + "may-emit-extra-samples" + ), new String[] { "upload-url-available", "progress:0:11", @@ -418,6 +523,11 @@ final class GeneratedTusClientConformanceScenarios { new String[] { "read-browser-file", }, + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null + ), new String[] { "source-open:array-buffer:11", "success", @@ -439,6 +549,11 @@ final class GeneratedTusClientConformanceScenarios { new String[] { "read-browser-file", }, + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null + ), new String[] { "source-open:array-buffer-view:11", "success", @@ -460,6 +575,11 @@ final class GeneratedTusClientConformanceScenarios { new String[] { "read-web-stream", }, + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null + ), new String[] { "source-open:web-readable-stream:null", "success", @@ -481,6 +601,11 @@ final class GeneratedTusClientConformanceScenarios { new String[] { "read-node-stream", }, + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null + ), new String[] { "source-open:node-readable-stream:null", "success", @@ -502,6 +627,11 @@ final class GeneratedTusClientConformanceScenarios { new String[] { "read-node-file", }, + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null + ), new String[] { "source-open:node-path-reference:11", "success", @@ -524,6 +654,11 @@ final class GeneratedTusClientConformanceScenarios { "defer-upload-length", "emit-progress", }, + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact-except-extra-progress", + "milestone", + "may-emit-extra-samples" + ), new String[] { "upload-url-available", "progress:0:11", @@ -548,6 +683,11 @@ final class GeneratedTusClientConformanceScenarios { new String[] { "override-patch-method", }, + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null + ), new String[0] ), new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( @@ -569,6 +709,11 @@ final class GeneratedTusClientConformanceScenarios { "concatenate-partial-uploads", "emit-progress", }, + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact-except-extra-progress", + "milestone", + "may-emit-extra-samples" + ), new String[] { "progress:5:11", "chunk-complete:5:5:11", @@ -597,6 +742,11 @@ final class GeneratedTusClientConformanceScenarios { "terminate-upload", "concatenate-partial-uploads", }, + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null + ), new String[] { "request-abort:3", } @@ -621,6 +771,11 @@ final class GeneratedTusClientConformanceScenarios { "retry-with-backoff", "recover-offset-after-error", }, + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null + ), new String[] { "should-retry:0:true", "retry-schedule:0", @@ -642,6 +797,11 @@ final class GeneratedTusClientConformanceScenarios { new String[] { "run-request-hooks", }, + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null + ), new String[] { "before-request:0", "after-response:0", @@ -663,6 +823,11 @@ final class GeneratedTusClientConformanceScenarios { new String[] { "abort-current-request", }, + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null + ), new String[] { "request-abort:0", } @@ -684,6 +849,11 @@ final class GeneratedTusClientConformanceScenarios { "abort-current-request", "terminate-upload", }, + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null + ), new String[] { "request-abort:1", } @@ -706,6 +876,11 @@ final class GeneratedTusClientConformanceScenarios { "terminate-upload", "retry-with-backoff", }, + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null + ), new String[0] ), }; diff --git a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java index 13c1127b..dbaf8c53 100644 --- a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java +++ b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java @@ -1304,6 +1304,7 @@ static final class GeneratedTusClientConformanceScenario { final String scenarioId; final String[] operationIds; final String[] primitives; + final GeneratedTusClientConformanceEventPolicy eventPolicy; final String[] eventKeys; GeneratedTusClientConformanceScenario( @@ -1313,6 +1314,7 @@ static final class GeneratedTusClientConformanceScenario { String scenarioId, String[] operationIds, String[] primitives, + GeneratedTusClientConformanceEventPolicy eventPolicy, String[] eventKeys) { this.behavior = behavior; this.completionKind = completion.kind; @@ -1321,10 +1323,29 @@ static final class GeneratedTusClientConformanceScenario { this.scenarioId = scenarioId; this.operationIds = operationIds; this.primitives = primitives; + this.eventPolicy = eventPolicy; this.eventKeys = eventKeys; } } + /** + * Generated client conformance event policy fixture. + */ + static final class GeneratedTusClientConformanceEventPolicy { + final String matching; + final String progress; + final String transportProgress; + + GeneratedTusClientConformanceEventPolicy( + String matching, + String progress, + String transportProgress) { + this.matching = matching; + this.progress = progress; + this.transportProgress = transportProgress; + } + } + /** * Generated client conformance completion fixture. */ diff --git a/src/test/java/io/tus/java/client/TestGeneratedTusConformanceEvents.java b/src/test/java/io/tus/java/client/TestGeneratedTusConformanceEvents.java index b409cc71..ec0f1b20 100644 --- a/src/test/java/io/tus/java/client/TestGeneratedTusConformanceEvents.java +++ b/src/test/java/io/tus/java/client/TestGeneratedTusConformanceEvents.java @@ -20,6 +20,11 @@ public class TestGeneratedTusConformanceEvents { new GeneratedTusEventCanaryCase( "singleUploadLifecycle", "singleUploadLifecycle", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact-except-extra-progress", + "milestone", + "may-emit-extra-samples" + ), new String[] { "fingerprint:contract-single-fingerprint", "upload-url-available", @@ -34,6 +39,11 @@ public class TestGeneratedTusConformanceEvents { new GeneratedTusEventCanaryCase( "creationWithUpload", "creationWithUpload", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact-except-extra-progress", + "milestone", + "may-emit-extra-samples" + ), new String[] { "progress:0:11", "progress:11:11", @@ -45,6 +55,11 @@ public class TestGeneratedTusConformanceEvents { new GeneratedTusEventCanaryCase( "creationWithUpload", "creationWithUploadPartialChunk", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact-except-extra-progress", + "milestone", + "may-emit-extra-samples" + ), new String[] { "progress:0:11", "progress:5:11", @@ -63,6 +78,11 @@ public class TestGeneratedTusConformanceEvents { new GeneratedTusEventCanaryCase( "protocolVersionSelection", "ietfDraft05CreationWithUpload", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact-except-extra-progress", + "milestone", + "may-emit-extra-samples" + ), new String[] { "progress:0:11", "progress:11:11", @@ -74,6 +94,11 @@ public class TestGeneratedTusConformanceEvents { new GeneratedTusEventCanaryCase( "protocolVersionSelection", "ietfDraft03ResumeWithoutKnownLength", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact-except-extra-progress", + "milestone", + "may-emit-extra-samples" + ), new String[] { "upload-url-available", "progress:5:11", @@ -86,6 +111,11 @@ public class TestGeneratedTusConformanceEvents { new GeneratedTusEventCanaryCase( "resumeUpload", "resumeFromPreviousUpload", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact-except-extra-progress", + "milestone", + "may-emit-extra-samples" + ), new String[] { "fingerprint:contract-resume-fingerprint", "url-storage-find:contract-resume-fingerprint:1", @@ -102,6 +132,11 @@ public class TestGeneratedTusConformanceEvents { new GeneratedTusEventCanaryCase( "relativeLocationResolution", "relativeLocationResolution", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact-except-extra-progress", + "milestone", + "may-emit-extra-samples" + ), new String[] { "upload-url-available", "progress:0:11", @@ -114,6 +149,11 @@ public class TestGeneratedTusConformanceEvents { new GeneratedTusEventCanaryCase( "inputSources", "arrayBufferInput", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null + ), new String[] { "source-open:array-buffer:11", "success", @@ -123,6 +163,11 @@ public class TestGeneratedTusConformanceEvents { new GeneratedTusEventCanaryCase( "inputSources", "arrayBufferViewInput", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null + ), new String[] { "source-open:array-buffer-view:11", "success", @@ -132,6 +177,11 @@ public class TestGeneratedTusConformanceEvents { new GeneratedTusEventCanaryCase( "inputSources", "webReadableStreamInput", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null + ), new String[] { "source-open:web-readable-stream:null", "success", @@ -141,6 +191,11 @@ public class TestGeneratedTusConformanceEvents { new GeneratedTusEventCanaryCase( "inputSources", "nodeReadableStreamInput", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null + ), new String[] { "source-open:node-readable-stream:null", "success", @@ -150,6 +205,11 @@ public class TestGeneratedTusConformanceEvents { new GeneratedTusEventCanaryCase( "inputSources", "nodePathInput", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null + ), new String[] { "source-open:node-path-reference:11", "success", @@ -159,6 +219,11 @@ public class TestGeneratedTusConformanceEvents { new GeneratedTusEventCanaryCase( "deferredLengthUpload", "deferredLengthUpload", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact-except-extra-progress", + "milestone", + "may-emit-extra-samples" + ), new String[] { "upload-url-available", "progress:0:11", @@ -171,6 +236,11 @@ public class TestGeneratedTusConformanceEvents { new GeneratedTusEventCanaryCase( "parallelUploadConcat", "parallelUploadConcat", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact-except-extra-progress", + "milestone", + "may-emit-extra-samples" + ), new String[] { "progress:5:11", "chunk-complete:5:5:11", @@ -181,6 +251,11 @@ public class TestGeneratedTusConformanceEvents { new GeneratedTusEventCanaryCase( "parallelUploadConcat", "parallelUploadAbortCleanup", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null + ), new String[] { "request-abort:3", } @@ -188,6 +263,11 @@ public class TestGeneratedTusConformanceEvents { new GeneratedTusEventCanaryCase( "retryOffsetRecovery", "retryPatchAfterOffsetRecovery", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null + ), new String[] { "should-retry:0:true", "retry-schedule:0", @@ -198,6 +278,11 @@ public class TestGeneratedTusConformanceEvents { new GeneratedTusEventCanaryCase( "requestLifecycleHooks", "requestLifecycleHooks", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null + ), new String[] { "before-request:0", "after-response:0", @@ -208,6 +293,11 @@ public class TestGeneratedTusConformanceEvents { new GeneratedTusEventCanaryCase( "abortUpload", "abortUpload", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null + ), new String[] { "request-abort:0", } @@ -215,6 +305,11 @@ public class TestGeneratedTusConformanceEvents { new GeneratedTusEventCanaryCase( "abortUpload", "abortUploadAfterStoredUrl", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null + ), new String[] { "request-abort:1", } @@ -234,6 +329,7 @@ public void testGeneratedScenarioEventKeys() { assertEquals(testCase.featureId, scenario.featureId); assertContains(feature.conformance.scenarioIds, scenario.scenarioId); + assertEventPolicyEquals(testCase.eventPolicy, scenario.eventPolicy); assertArrayEquals(testCase.eventKeys, scenario.eventKeys); } } @@ -272,14 +368,28 @@ private static void assertContains(String[] values, String expected) { throw new AssertionError("Missing generated value: " + expected); } + private static void assertEventPolicyEquals( + GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy expected, + GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy actual) { + assertEquals(expected.matching, actual.matching); + assertEquals(expected.progress, actual.progress); + assertEquals(expected.transportProgress, actual.transportProgress); + } + private static final class GeneratedTusEventCanaryCase { final String featureId; final String scenarioId; + final GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy eventPolicy; final String[] eventKeys; - GeneratedTusEventCanaryCase(String featureId, String scenarioId, String[] eventKeys) { + GeneratedTusEventCanaryCase( + String featureId, + String scenarioId, + GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy eventPolicy, + String[] eventKeys) { this.featureId = featureId; this.scenarioId = scenarioId; + this.eventPolicy = eventPolicy; this.eventKeys = eventKeys; } } diff --git a/src/test/java/io/tus/java/client/TestGeneratedTusRuntimeEvents.java b/src/test/java/io/tus/java/client/TestGeneratedTusRuntimeEvents.java index 284d1f83..186e6ba1 100644 --- a/src/test/java/io/tus/java/client/TestGeneratedTusRuntimeEvents.java +++ b/src/test/java/io/tus/java/client/TestGeneratedTusRuntimeEvents.java @@ -30,6 +30,7 @@ public class TestGeneratedTusRuntimeEvents extends MockServerProvider { new GeneratedTusRuntimeEventCase[] { new GeneratedTusRuntimeEventCase( "singleUploadLifecycle", + "exact-except-extra-progress", false, new GeneratedTusRuntimeEventInput( "hello world", @@ -89,6 +90,7 @@ public class TestGeneratedTusRuntimeEvents extends MockServerProvider { ), new GeneratedTusRuntimeEventCase( "resumeFromPreviousUpload", + "exact-except-extra-progress", false, new GeneratedTusRuntimeEventInput( "hello world", @@ -145,6 +147,7 @@ public class TestGeneratedTusRuntimeEvents extends MockServerProvider { ), new GeneratedTusRuntimeEventCase( "relativeLocationResolution", + "exact-except-extra-progress", false, new GeneratedTusRuntimeEventInput( "hello world", @@ -204,6 +207,7 @@ public class TestGeneratedTusRuntimeEvents extends MockServerProvider { ), new GeneratedTusRuntimeEventCase( "deferredLengthUpload", + "exact-except-extra-progress", true, new GeneratedTusRuntimeEventInput( "hello world", @@ -310,10 +314,7 @@ public void onChunkComplete(long chunkSize, long bytesAccepted, long bytesTotal) } uploader.finish(); - assertArrayEquals( - testCase.scenarioId, - testCase.eventKeys, - events.toArray(new String[events.size()])); + assertEvents(testCase, events); assertStoredUploadState(testCase, urlStore); } } @@ -444,8 +445,66 @@ private URL uploadUrlForUnchecked(GeneratedTusRuntimeEventCase testCase) { } } + private void assertEvents(GeneratedTusRuntimeEventCase testCase, List events) { + if ("exact".equals(testCase.eventPolicyMatching)) { + assertArrayEquals( + testCase.scenarioId, + testCase.eventKeys, + events.toArray(new String[events.size()])); + return; + } + + if ("exact-except-extra-progress".equals(testCase.eventPolicyMatching)) { + assertEventsExactExceptExtraProgress(testCase, events); + return; + } + + throw new AssertionError( + "Unsupported generated event policy " + + testCase.eventPolicyMatching + + " for " + + testCase.scenarioId); + } + + private void assertEventsExactExceptExtraProgress( + GeneratedTusRuntimeEventCase testCase, + List events) { + int expectedIndex = 0; + for (String event : events) { + if ( + expectedIndex < testCase.eventKeys.length + && event.equals(testCase.eventKeys[expectedIndex])) { + expectedIndex += 1; + continue; + } + + if (event.startsWith("progress:")) { + continue; + } + + throw new AssertionError( + testCase.scenarioId + + " emitted unexpected non-progress event " + + event + + "; expected " + + java.util.Arrays.toString(testCase.eventKeys)); + } + + if (expectedIndex == testCase.eventKeys.length) { + return; + } + + throw new AssertionError( + testCase.scenarioId + + " did not emit every expected non-extra event; observed " + + events + + "; expected " + + java.util.Arrays.toString(testCase.eventKeys)); + } + private static final class GeneratedTusRuntimeEventCase { final String scenarioId; + final String eventPolicyMatching; final boolean uploadLengthDeferred; final GeneratedTusRuntimeEventInput input; final GeneratedTusRuntimeEventRequest[] requests; @@ -453,11 +512,13 @@ private static final class GeneratedTusRuntimeEventCase { GeneratedTusRuntimeEventCase( String scenarioId, + String eventPolicyMatching, boolean uploadLengthDeferred, GeneratedTusRuntimeEventInput input, GeneratedTusRuntimeEventRequest[] requests, String[] eventKeys) { this.scenarioId = scenarioId; + this.eventPolicyMatching = eventPolicyMatching; this.uploadLengthDeferred = uploadLengthDeferred; this.input = input; this.requests = requests; From 269dc2279c82122b87c5769f8e02105f20eb4da5 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Mon, 1 Jun 2026 08:27:25 +0200 Subject: [PATCH 27/43] Keep generated event fixtures lintable --- ...eneratedTusClientConformanceScenarios.java | 704 ++++++++++-------- .../client/GeneratedTusProtocolContract.java | 22 +- 2 files changed, 405 insertions(+), 321 deletions(-) diff --git a/src/test/java/io/tus/java/client/GeneratedTusClientConformanceScenarios.java b/src/test/java/io/tus/java/client/GeneratedTusClientConformanceScenarios.java index bcc84f1f..37e09050 100644 --- a/src/test/java/io/tus/java/client/GeneratedTusClientConformanceScenarios.java +++ b/src/test/java/io/tus/java/client/GeneratedTusClientConformanceScenarios.java @@ -32,21 +32,23 @@ final class GeneratedTusClientConformanceScenarios { "emit-progress", "abort-current-request", }, - new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( - "exact-except-extra-progress", - "milestone", - "may-emit-extra-samples" - ), - new String[] { - "fingerprint:contract-single-fingerprint", - "upload-url-available", - "url-storage-add:contract-single-fingerprint:https://tus.io/uploads/generated-contract", - "progress:0:11", - "progress:11:11", - "chunk-complete:11:11:11", - "success", - "source-close", - } + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEvents( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact-except-extra-progress", + "milestone", + "may-emit-extra-samples" + ), + new String[] { + "fingerprint:contract-single-fingerprint", + "upload-url-available", + "url-storage-add:contract-single-fingerprint:https://tus.io/uploads/generated-contract", + "progress:0:11", + "progress:11:11", + "chunk-complete:11:11:11", + "success", + "source-close", + } + ) ), new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( "creation-with-upload", @@ -63,18 +65,20 @@ final class GeneratedTusClientConformanceScenarios { "upload-during-creation", "emit-progress", }, - new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( - "exact-except-extra-progress", - "milestone", - "may-emit-extra-samples" - ), - new String[] { - "progress:0:11", - "progress:11:11", - "upload-url-available", - "success", - "source-close", - } + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEvents( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact-except-extra-progress", + "milestone", + "may-emit-extra-samples" + ), + new String[] { + "progress:0:11", + "progress:11:11", + "upload-url-available", + "success", + "source-close", + } + ) ), new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( "creation-with-upload-partial-chunk", @@ -92,25 +96,27 @@ final class GeneratedTusClientConformanceScenarios { "upload-during-creation", "emit-progress", }, - new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( - "exact-except-extra-progress", - "milestone", - "may-emit-extra-samples" - ), - new String[] { - "progress:0:11", - "progress:5:11", - "upload-url-available", - "chunk-complete:5:5:11", - "progress:5:11", - "progress:10:11", - "chunk-complete:5:10:11", - "progress:10:11", - "progress:11:11", - "chunk-complete:1:11:11", - "success", - "source-close", - } + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEvents( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact-except-extra-progress", + "milestone", + "may-emit-extra-samples" + ), + new String[] { + "progress:0:11", + "progress:5:11", + "upload-url-available", + "chunk-complete:5:5:11", + "progress:5:11", + "progress:10:11", + "chunk-complete:5:10:11", + "progress:10:11", + "progress:11:11", + "chunk-complete:1:11:11", + "success", + "source-close", + } + ) ), new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( "creation-with-upload", @@ -126,18 +132,20 @@ final class GeneratedTusClientConformanceScenarios { new String[] { "select-client-protocol", }, - new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( - "exact-except-extra-progress", - "milestone", - "may-emit-extra-samples" - ), - new String[] { - "progress:0:11", - "progress:11:11", - "upload-url-available", - "success", - "source-close", - } + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEvents( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact-except-extra-progress", + "milestone", + "may-emit-extra-samples" + ), + new String[] { + "progress:0:11", + "progress:11:11", + "upload-url-available", + "success", + "source-close", + } + ) ), new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( "upload-body-headers", @@ -154,19 +162,21 @@ final class GeneratedTusClientConformanceScenarios { new String[] { "select-client-protocol", }, - new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( - "exact-except-extra-progress", - "milestone", - "may-emit-extra-samples" - ), - new String[] { - "upload-url-available", - "progress:5:11", - "progress:11:11", - "chunk-complete:6:11:11", - "success", - "source-close", - } + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEvents( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact-except-extra-progress", + "milestone", + "may-emit-extra-samples" + ), + new String[] { + "upload-url-available", + "progress:5:11", + "progress:11:11", + "chunk-complete:6:11:11", + "success", + "source-close", + } + ) ), new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( "start-option-validation", @@ -180,12 +190,14 @@ final class GeneratedTusClientConformanceScenarios { new String[] { "validate-start-options", }, - new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( - "exact", - null, - null - ), - new String[0] + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEvents( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null + ), + new String[0] + ) ), new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( "start-option-validation", @@ -199,12 +211,14 @@ final class GeneratedTusClientConformanceScenarios { new String[] { "validate-start-options", }, - new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( - "exact", - null, - null - ), - new String[0] + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEvents( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null + ), + new String[0] + ) ), new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( "start-option-validation", @@ -218,12 +232,14 @@ final class GeneratedTusClientConformanceScenarios { new String[] { "validate-start-options", }, - new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( - "exact", - null, - null - ), - new String[0] + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEvents( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null + ), + new String[0] + ) ), new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( "start-option-validation", @@ -237,12 +253,14 @@ final class GeneratedTusClientConformanceScenarios { new String[] { "validate-start-options", }, - new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( - "exact", - null, - null - ), - new String[0] + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEvents( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null + ), + new String[0] + ) ), new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( "start-option-validation", @@ -256,12 +274,14 @@ final class GeneratedTusClientConformanceScenarios { new String[] { "validate-start-options", }, - new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( - "exact", - null, - null - ), - new String[0] + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEvents( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null + ), + new String[0] + ) ), new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( "start-option-validation", @@ -275,12 +295,14 @@ final class GeneratedTusClientConformanceScenarios { new String[] { "validate-start-options", }, - new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( - "exact", - null, - null - ), - new String[0] + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEvents( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null + ), + new String[0] + ) ), new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( "start-option-validation", @@ -294,12 +316,14 @@ final class GeneratedTusClientConformanceScenarios { new String[] { "validate-start-options", }, - new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( - "exact", - null, - null - ), - new String[0] + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEvents( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null + ), + new String[0] + ) ), new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( "start-option-validation", @@ -313,12 +337,14 @@ final class GeneratedTusClientConformanceScenarios { new String[] { "validate-start-options", }, - new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( - "exact", - null, - null - ), - new String[0] + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEvents( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null + ), + new String[0] + ) ), new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( "start-option-validation", @@ -332,12 +358,14 @@ final class GeneratedTusClientConformanceScenarios { new String[] { "validate-start-options", }, - new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( - "exact", - null, - null - ), - new String[0] + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEvents( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null + ), + new String[0] + ) ), new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( "start-option-validation", @@ -351,12 +379,14 @@ final class GeneratedTusClientConformanceScenarios { new String[] { "validate-start-options", }, - new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( - "exact", - null, - null - ), - new String[0] + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEvents( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null + ), + new String[0] + ) ), new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( "detailed-error", @@ -372,12 +402,14 @@ final class GeneratedTusClientConformanceScenarios { new String[] { "report-detailed-errors", }, - new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( - "exact", - null, - null - ), - new String[0] + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEvents( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null + ), + new String[0] + ) ), new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( "detailed-error", @@ -393,12 +425,14 @@ final class GeneratedTusClientConformanceScenarios { new String[] { "report-detailed-errors", }, - new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( - "exact", - null, - null - ), - new String[0] + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEvents( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null + ), + new String[0] + ) ), new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( "upload-body-headers", @@ -415,12 +449,14 @@ final class GeneratedTusClientConformanceScenarios { new String[] { "send-upload-body-headers", }, - new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( - "exact", - null, - null - ), - new String[0] + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEvents( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null + ), + new String[0] + ) ), new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( "custom-request-headers", @@ -437,12 +473,14 @@ final class GeneratedTusClientConformanceScenarios { new String[] { "apply-custom-request-headers", }, - new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( - "exact", - null, - null - ), - new String[0] + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEvents( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null + ), + new String[0] + ) ), new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( "resume-from-previous-upload", @@ -461,23 +499,25 @@ final class GeneratedTusClientConformanceScenarios { "resume-from-previous-upload", "store-resume-url", }, - new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( - "exact-except-extra-progress", - "milestone", - "may-emit-extra-samples" - ), - new String[] { - "fingerprint:contract-resume-fingerprint", - "url-storage-find:contract-resume-fingerprint:1", - "fingerprint:contract-resume-fingerprint", - "upload-url-available", - "progress:5:11", - "progress:11:11", - "chunk-complete:6:11:11", - "url-storage-remove:tus::contract-resume-fingerprint::1337", - "success", - "source-close", - } + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEvents( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact-except-extra-progress", + "milestone", + "may-emit-extra-samples" + ), + new String[] { + "fingerprint:contract-resume-fingerprint", + "url-storage-find:contract-resume-fingerprint:1", + "fingerprint:contract-resume-fingerprint", + "upload-url-available", + "progress:5:11", + "progress:11:11", + "chunk-complete:6:11:11", + "url-storage-remove:tus::contract-resume-fingerprint::1337", + "success", + "source-close", + } + ) ), new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( "relative-location-resolution", @@ -494,19 +534,21 @@ final class GeneratedTusClientConformanceScenarios { new String[] { "resolve-relative-location", }, - new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( - "exact-except-extra-progress", - "milestone", - "may-emit-extra-samples" - ), - new String[] { - "upload-url-available", - "progress:0:11", - "progress:11:11", - "chunk-complete:11:11:11", - "success", - "source-close", - } + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEvents( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact-except-extra-progress", + "milestone", + "may-emit-extra-samples" + ), + new String[] { + "upload-url-available", + "progress:0:11", + "progress:11:11", + "chunk-complete:11:11:11", + "success", + "source-close", + } + ) ), new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( "array-buffer-input", @@ -523,16 +565,18 @@ final class GeneratedTusClientConformanceScenarios { new String[] { "read-browser-file", }, - new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( - "exact", - null, - null - ), - new String[] { - "source-open:array-buffer:11", - "success", - "source-close", - } + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEvents( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null + ), + new String[] { + "source-open:array-buffer:11", + "success", + "source-close", + } + ) ), new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( "array-buffer-view-input", @@ -549,16 +593,18 @@ final class GeneratedTusClientConformanceScenarios { new String[] { "read-browser-file", }, - new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( - "exact", - null, - null - ), - new String[] { - "source-open:array-buffer-view:11", - "success", - "source-close", - } + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEvents( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null + ), + new String[] { + "source-open:array-buffer-view:11", + "success", + "source-close", + } + ) ), new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( "web-readable-stream-input", @@ -575,16 +621,18 @@ final class GeneratedTusClientConformanceScenarios { new String[] { "read-web-stream", }, - new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( - "exact", - null, - null - ), - new String[] { - "source-open:web-readable-stream:null", - "success", - "source-close", - } + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEvents( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null + ), + new String[] { + "source-open:web-readable-stream:null", + "success", + "source-close", + } + ) ), new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( "node-readable-stream-input", @@ -601,16 +649,18 @@ final class GeneratedTusClientConformanceScenarios { new String[] { "read-node-stream", }, - new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( - "exact", - null, - null - ), - new String[] { - "source-open:node-readable-stream:null", - "success", - "source-close", - } + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEvents( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null + ), + new String[] { + "source-open:node-readable-stream:null", + "success", + "source-close", + } + ) ), new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( "node-path-input", @@ -627,16 +677,18 @@ final class GeneratedTusClientConformanceScenarios { new String[] { "read-node-file", }, - new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( - "exact", - null, - null - ), - new String[] { - "source-open:node-path-reference:11", - "success", - "source-close", - } + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEvents( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null + ), + new String[] { + "source-open:node-path-reference:11", + "success", + "source-close", + } + ) ), new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( "deferred-length-upload", @@ -654,19 +706,21 @@ final class GeneratedTusClientConformanceScenarios { "defer-upload-length", "emit-progress", }, - new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( - "exact-except-extra-progress", - "milestone", - "may-emit-extra-samples" - ), - new String[] { - "upload-url-available", - "progress:0:11", - "progress:11:11", - "chunk-complete:11:11:11", - "success", - "source-close", - } + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEvents( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact-except-extra-progress", + "milestone", + "may-emit-extra-samples" + ), + new String[] { + "upload-url-available", + "progress:0:11", + "progress:11:11", + "chunk-complete:11:11:11", + "success", + "source-close", + } + ) ), new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( "override-patch-method", @@ -683,12 +737,14 @@ final class GeneratedTusClientConformanceScenarios { new String[] { "override-patch-method", }, - new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( - "exact", - null, - null - ), - new String[0] + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEvents( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null + ), + new String[0] + ) ), new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( "parallel-upload-concat", @@ -709,17 +765,19 @@ final class GeneratedTusClientConformanceScenarios { "concatenate-partial-uploads", "emit-progress", }, - new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( - "exact-except-extra-progress", - "milestone", - "may-emit-extra-samples" - ), - new String[] { - "progress:5:11", - "chunk-complete:5:5:11", - "progress:11:11", - "chunk-complete:6:11:11", - } + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEvents( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact-except-extra-progress", + "milestone", + "may-emit-extra-samples" + ), + new String[] { + "progress:5:11", + "chunk-complete:5:5:11", + "progress:11:11", + "chunk-complete:6:11:11", + } + ) ), new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( "parallel-upload-abort-cleanup", @@ -742,14 +800,16 @@ final class GeneratedTusClientConformanceScenarios { "terminate-upload", "concatenate-partial-uploads", }, - new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( - "exact", - null, - null - ), - new String[] { - "request-abort:3", - } + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEvents( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null + ), + new String[] { + "request-abort:3", + } + ) ), new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( "retry-patch-after-offset-recovery", @@ -771,17 +831,19 @@ final class GeneratedTusClientConformanceScenarios { "retry-with-backoff", "recover-offset-after-error", }, - new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( - "exact", - null, - null - ), - new String[] { - "should-retry:0:true", - "retry-schedule:0", - "should-retry:0:true", - "retry-schedule:0", - } + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEvents( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null + ), + new String[] { + "should-retry:0:true", + "retry-schedule:0", + "should-retry:0:true", + "retry-schedule:0", + } + ) ), new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( "request-lifecycle-hooks", @@ -797,17 +859,19 @@ final class GeneratedTusClientConformanceScenarios { new String[] { "run-request-hooks", }, - new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( - "exact", - null, - null - ), - new String[] { - "before-request:0", - "after-response:0", - "success", - "source-close", - } + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEvents( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null + ), + new String[] { + "before-request:0", + "after-response:0", + "success", + "source-close", + } + ) ), new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( "abort-upload", @@ -823,14 +887,16 @@ final class GeneratedTusClientConformanceScenarios { new String[] { "abort-current-request", }, - new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( - "exact", - null, - null - ), - new String[] { - "request-abort:0", - } + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEvents( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null + ), + new String[] { + "request-abort:0", + } + ) ), new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( "abort-upload-after-stored-url", @@ -849,14 +915,16 @@ final class GeneratedTusClientConformanceScenarios { "abort-current-request", "terminate-upload", }, - new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( - "exact", - null, - null - ), - new String[] { - "request-abort:1", - } + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEvents( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null + ), + new String[] { + "request-abort:1", + } + ) ), new GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario( "terminate-with-retry", @@ -876,12 +944,14 @@ final class GeneratedTusClientConformanceScenarios { "terminate-upload", "retry-with-backoff", }, - new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( - "exact", - null, - null - ), - new String[0] + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEvents( + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null + ), + new String[0] + ) ), }; diff --git a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java index dbaf8c53..b8ccc4f9 100644 --- a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java +++ b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java @@ -1314,8 +1314,7 @@ static final class GeneratedTusClientConformanceScenario { String scenarioId, String[] operationIds, String[] primitives, - GeneratedTusClientConformanceEventPolicy eventPolicy, - String[] eventKeys) { + GeneratedTusClientConformanceEvents events) { this.behavior = behavior; this.completionKind = completion.kind; this.completionReason = completion.reason; @@ -1323,8 +1322,23 @@ static final class GeneratedTusClientConformanceScenario { this.scenarioId = scenarioId; this.operationIds = operationIds; this.primitives = primitives; - this.eventPolicy = eventPolicy; - this.eventKeys = eventKeys; + this.eventPolicy = events.policy; + this.eventKeys = events.keys; + } + } + + /** + * Generated client conformance event fixture bundle. + */ + static final class GeneratedTusClientConformanceEvents { + final GeneratedTusClientConformanceEventPolicy policy; + final String[] keys; + + GeneratedTusClientConformanceEvents( + GeneratedTusClientConformanceEventPolicy policy, + String[] keys) { + this.policy = policy; + this.keys = keys; } } From bca3207f4bb1051dac7d22a3a2e72184f81b048d Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Mon, 1 Jun 2026 09:25:16 +0200 Subject: [PATCH 28/43] Update generated TUS retry events --- .../GeneratedTusClientConformanceScenarios.java | 5 ++++- .../client/TestGeneratedTusConformanceEvents.java | 13 +++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/test/java/io/tus/java/client/GeneratedTusClientConformanceScenarios.java b/src/test/java/io/tus/java/client/GeneratedTusClientConformanceScenarios.java index 37e09050..4eadec8a 100644 --- a/src/test/java/io/tus/java/client/GeneratedTusClientConformanceScenarios.java +++ b/src/test/java/io/tus/java/client/GeneratedTusClientConformanceScenarios.java @@ -950,7 +950,10 @@ final class GeneratedTusClientConformanceScenarios { null, null ), - new String[0] + new String[] { + "should-retry:0:true", + "retry-schedule:0", + } ) ), }; diff --git a/src/test/java/io/tus/java/client/TestGeneratedTusConformanceEvents.java b/src/test/java/io/tus/java/client/TestGeneratedTusConformanceEvents.java index ec0f1b20..2b2c91db 100644 --- a/src/test/java/io/tus/java/client/TestGeneratedTusConformanceEvents.java +++ b/src/test/java/io/tus/java/client/TestGeneratedTusConformanceEvents.java @@ -314,6 +314,19 @@ public class TestGeneratedTusConformanceEvents { "request-abort:1", } ), + new GeneratedTusEventCanaryCase( + "terminateUpload", + "terminateWithRetry", + new GeneratedTusProtocolContract.GeneratedTusClientConformanceEventPolicy( + "exact", + null, + null + ), + new String[] { + "should-retry:0:true", + "retry-schedule:0", + } + ), }; /** From 86cdecd453578d3b6a69035c19b7a23e7e7d5414 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Mon, 1 Jun 2026 09:58:03 +0200 Subject: [PATCH 29/43] Add generated TUS proof profile canaries --- .../TestGeneratedTusConformanceEvents.java | 128 ++++++++++++++++++ 1 file changed, 128 insertions(+) diff --git a/src/test/java/io/tus/java/client/TestGeneratedTusConformanceEvents.java b/src/test/java/io/tus/java/client/TestGeneratedTusConformanceEvents.java index 2b2c91db..15c88982 100644 --- a/src/test/java/io/tus/java/client/TestGeneratedTusConformanceEvents.java +++ b/src/test/java/io/tus/java/client/TestGeneratedTusConformanceEvents.java @@ -329,6 +329,87 @@ public class TestGeneratedTusConformanceEvents { ), }; + private static final GeneratedTusProofProfileCase[] PROOF_CASES = + new GeneratedTusProofProfileCase[] { + new GeneratedTusProofProfileCase( + "urlStorageCreateFlow", + "single-upload-lifecycle", + "success", + "singleUploadLifecycle", + "singleUploadLifecycle", + new String[] { + "createTusUpload", + "patchTusUpload", + }, + new String[] { + "open-input-source", + "fingerprint-input", + "store-resume-url", + "retry-with-backoff", + "emit-progress", + "abort-current-request", + } + ), + new GeneratedTusProofProfileCase( + "customRequestHeaders", + "custom-request-headers", + "success", + "customRequestHeaders", + "customRequestHeaders", + new String[] { + "createTusUpload", + "patchTusUpload", + }, + new String[] { + "apply-custom-request-headers", + } + ), + new GeneratedTusProofProfileCase( + "overridePatchMethod", + "override-patch-method", + "success", + "overridePatchMethod", + "overridePatchMethod", + new String[] { + "getTusUploadOffset", + "patchTusUpload", + }, + new String[] { + "override-patch-method", + } + ), + new GeneratedTusProofProfileCase( + "nodePathFileUpload", + "node-path-input", + "success", + "inputSources", + "nodePathInput", + new String[] { + "createTusUpload", + "patchTusUpload", + }, + new String[] { + "read-node-file", + } + ), + new GeneratedTusProofProfileCase( + "resumeFromPreviousUpload", + "resume-from-previous-upload", + "success", + "resumeUpload", + "resumeFromPreviousUpload", + new String[] { + "getTusUploadOffset", + "patchTusUpload", + }, + new String[] { + "fingerprint-input", + "resume-from-previous-upload", + "store-resume-url", + } + ), + }; + /** * Verifies generated feature-level event keys survive in the Java fixture. */ @@ -347,6 +428,26 @@ public void testGeneratedScenarioEventKeys() { } } + /** + * Verifies generated named proof-profile scenarios survive in the Java fixture. + */ + @Test + public void testGeneratedProofProfileScenarios() { + for (GeneratedTusProofProfileCase testCase : PROOF_CASES) { + GeneratedTusProtocolContract.GeneratedTusClientConformanceScenario scenario = + findScenario(testCase.scenarioId); + GeneratedTusProtocolContract.GeneratedTusClientFeature feature = + findFeature(testCase.featureId); + + assertEquals(testCase.behavior, scenario.behavior); + assertEquals(testCase.completionKind, scenario.completionKind); + assertEquals(testCase.featureId, scenario.featureId); + assertContains(feature.conformance.scenarioIds, scenario.scenarioId); + assertArrayEquals(testCase.operationIds, scenario.operationIds); + assertArrayEquals(testCase.primitives, scenario.primitives); + } + } + private static GeneratedTusProtocolContract.GeneratedTusClientFeature findFeature( String featureId) { for (GeneratedTusProtocolContract.GeneratedTusClientFeature feature @@ -406,4 +507,31 @@ private static final class GeneratedTusEventCanaryCase { this.eventKeys = eventKeys; } } + + private static final class GeneratedTusProofProfileCase { + final String profile; + final String behavior; + final String completionKind; + final String featureId; + final String scenarioId; + final String[] operationIds; + final String[] primitives; + + GeneratedTusProofProfileCase( + String profile, + String behavior, + String completionKind, + String featureId, + String scenarioId, + String[] operationIds, + String[] primitives) { + this.profile = profile; + this.behavior = behavior; + this.completionKind = completionKind; + this.featureId = featureId; + this.scenarioId = scenarioId; + this.operationIds = operationIds; + this.primitives = primitives; + } + } } From c34bd54274f12c34c6710df0be2e02c9a1daeca5 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Mon, 1 Jun 2026 10:26:45 +0200 Subject: [PATCH 30/43] Use generated TUS execution hints in runtime tests --- .../client/TestGeneratedTusRuntimeEvents.java | 75 ++++++++++++++++++- 1 file changed, 73 insertions(+), 2 deletions(-) diff --git a/src/test/java/io/tus/java/client/TestGeneratedTusRuntimeEvents.java b/src/test/java/io/tus/java/client/TestGeneratedTusRuntimeEvents.java index 186e6ba1..b900fc5a 100644 --- a/src/test/java/io/tus/java/client/TestGeneratedTusRuntimeEvents.java +++ b/src/test/java/io/tus/java/client/TestGeneratedTusRuntimeEvents.java @@ -32,6 +32,7 @@ public class TestGeneratedTusRuntimeEvents extends MockServerProvider { "singleUploadLifecycle", "exact-except-extra-progress", false, + new GeneratedTusRuntimeBeforeStartAction[0], new GeneratedTusRuntimeEventInput( "hello world", "generated-contract", @@ -92,6 +93,13 @@ public class TestGeneratedTusRuntimeEvents extends MockServerProvider { "resumeFromPreviousUpload", "exact-except-extra-progress", false, + new GeneratedTusRuntimeBeforeStartAction[] { + new GeneratedTusRuntimeBeforeStartAction( + "resume-from-previous-upload", + 1, + 0 + ), + }, new GeneratedTusRuntimeEventInput( "hello world", "resume-contract", @@ -149,6 +157,7 @@ public class TestGeneratedTusRuntimeEvents extends MockServerProvider { "relativeLocationResolution", "exact-except-extra-progress", false, + new GeneratedTusRuntimeBeforeStartAction[0], new GeneratedTusRuntimeEventInput( "hello world", "relative-contract", @@ -209,6 +218,7 @@ public class TestGeneratedTusRuntimeEvents extends MockServerProvider { "deferredLengthUpload", "exact-except-extra-progress", true, + new GeneratedTusRuntimeBeforeStartAction[0], new GeneratedTusRuntimeEventInput( "hello world", "deferred-contract", @@ -283,7 +293,11 @@ public void testSyncUploaderEmitsGeneratedProgressAndChunkEvents() throws Except TusClient client = new TusClient(); client.setUploadCreationURL(endpointUrlFor(testCase)); GeneratedTusRuntimeEventUrlStore urlStore = urlStoreFor(testCase); - if (urlStore != null) { + if (hasResumeBeforeStartAction(testCase)) { + if (urlStore == null) { + throw new AssertionError( + testCase.scenarioId + " cannot resume without generated URL storage"); + } client.enableResuming(urlStore); } if ( @@ -321,13 +335,52 @@ public void onChunkComplete(long chunkSize, long bytesAccepted, long bytesTotal) private TusUploader uploaderFor(TusClient client, GeneratedTusRuntimeEventCase testCase) throws Exception { - if (testCase.input.storedUpload != null) { + GeneratedTusRuntimeBeforeStartAction resumeAction = resumeBeforeStartAction(testCase); + if (resumeAction != null) { + assertStoredUploadAvailableForResume(testCase, resumeAction); return client.resumeUpload(uploadFor(testCase)); } return client.createUpload(uploadFor(testCase)); } + private boolean hasResumeBeforeStartAction(GeneratedTusRuntimeEventCase testCase) { + return resumeBeforeStartAction(testCase) != null; + } + + private GeneratedTusRuntimeBeforeStartAction resumeBeforeStartAction( + GeneratedTusRuntimeEventCase testCase) { + GeneratedTusRuntimeBeforeStartAction action = null; + for (GeneratedTusRuntimeBeforeStartAction candidate : testCase.beforeStartActions) { + if (!"resume-from-previous-upload".equals(candidate.kind)) { + throw new AssertionError( + testCase.scenarioId + + " uses unsupported generated beforeStart action " + + candidate.kind); + } + + if (action != null) { + throw new AssertionError( + testCase.scenarioId + " defines more than one resume beforeStart action"); + } + + action = candidate; + } + + return action; + } + + private void assertStoredUploadAvailableForResume( + GeneratedTusRuntimeEventCase testCase, + GeneratedTusRuntimeBeforeStartAction action) { + if (testCase.input.storedUpload == null) { + throw new AssertionError( + testCase.scenarioId + " cannot resume without a generated stored upload"); + } + assertEquals(testCase.scenarioId, 0, action.selectedPreviousUploadIndex); + assertEquals(testCase.scenarioId, 1, action.expectedPreviousUploadCount); + } + private TusUpload uploadFor(GeneratedTusRuntimeEventCase testCase) { byte[] content = testCase.input.content.getBytes(StandardCharsets.UTF_8); TusUpload upload = new TusUpload(); @@ -506,6 +559,7 @@ private static final class GeneratedTusRuntimeEventCase { final String scenarioId; final String eventPolicyMatching; final boolean uploadLengthDeferred; + final GeneratedTusRuntimeBeforeStartAction[] beforeStartActions; final GeneratedTusRuntimeEventInput input; final GeneratedTusRuntimeEventRequest[] requests; final String[] eventKeys; @@ -514,18 +568,35 @@ private static final class GeneratedTusRuntimeEventCase { String scenarioId, String eventPolicyMatching, boolean uploadLengthDeferred, + GeneratedTusRuntimeBeforeStartAction[] beforeStartActions, GeneratedTusRuntimeEventInput input, GeneratedTusRuntimeEventRequest[] requests, String[] eventKeys) { this.scenarioId = scenarioId; this.eventPolicyMatching = eventPolicyMatching; this.uploadLengthDeferred = uploadLengthDeferred; + this.beforeStartActions = beforeStartActions; this.input = input; this.requests = requests; this.eventKeys = eventKeys; } } + private static final class GeneratedTusRuntimeBeforeStartAction { + final String kind; + final int expectedPreviousUploadCount; + final int selectedPreviousUploadIndex; + + GeneratedTusRuntimeBeforeStartAction( + String kind, + int expectedPreviousUploadCount, + int selectedPreviousUploadIndex) { + this.kind = kind; + this.expectedPreviousUploadCount = expectedPreviousUploadCount; + this.selectedPreviousUploadIndex = selectedPreviousUploadIndex; + } + } + private static final class GeneratedTusRuntimeEventInput { final String content; final String uploadPath; From 25c42b7f47de287fe7875e4b6e5fbf2f73215b41 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Mon, 1 Jun 2026 11:37:34 +0200 Subject: [PATCH 31/43] Expose TUS managed upload contract --- .../java/io/tus/java/client/GeneratedTusProtocolContract.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java index b8ccc4f9..b0e72032 100644 --- a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java +++ b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java @@ -1121,6 +1121,8 @@ final class GeneratedTusProtocolContract { ), }; + static final String MANAGED_UPLOAD_JSON = "{\n \"capabilities\": {\n \"cleanup\": {\n \"policies\": [\n \"remove-owned-source-after-success\",\n \"remove-owned-source-after-cancel\",\n \"retain-owned-source-after-permanent-failure\",\n \"retain-source-after-retryable-failure\",\n \"remove-managed-state-after-terminal-retention\"\n ]\n },\n \"failureClassification\": {\n \"permanentFailures\": [\n \"source-unavailable\",\n \"unretryable-protocol-error\",\n \"retry-policy-exhausted\"\n ],\n \"retryableFailures\": [\n \"retryable-protocol-error\",\n \"io-error\",\n \"network-unavailable\"\n ]\n },\n \"networkConstraints\": {\n \"options\": [\n \"any-network\",\n \"unmetered-network\"\n ]\n },\n \"retryPolicy\": {\n \"controls\": [\n \"max-attempts\",\n \"deadline\",\n \"progress-sensitive-budget\",\n \"unbounded-until-permanent-failure\"\n ],\n \"permanentFailure\": \"stop-without-retry\",\n \"progressReset\": \"reset-budget-after-accepted-offset-advances\"\n },\n \"scheduling\": {\n \"strategies\": [\n \"foreground-task\",\n \"process-lifetime-worker-pool\",\n \"durable-os-scheduler\"\n ]\n },\n \"sourceDurability\": {\n \"ownedCopyCleanup\": \"after-success-or-cancel\",\n \"strategies\": [\n \"copy-to-owned-storage\",\n \"reference-original-source\",\n \"memory-only\"\n ]\n },\n \"stateReporting\": {\n \"states\": [\n \"pending\",\n \"running\",\n \"succeeded\",\n \"failed\"\n ],\n \"terminalRetention\": \"session-and-next-launch\",\n \"transientRetention\": \"until-terminal\"\n }\n },\n \"conformance\": {\n \"scenarioIds\": [\n \"managedUploadDurableRetry\",\n \"managedUploadPermanentFailure\",\n \"managedUploadNetworkConstraint\"\n ],\n \"status\": \"needs-generated-scenario\"\n },\n \"description\": \"Submit upload work that can make sources durable, schedule/resume execution, retry, report state, and clean up while reusing the raw TUS protocol features underneath.\",\n \"featureId\": \"managedUpload\",\n \"flow\": [\n {\n \"kind\": \"managed-primitive\",\n \"primitive\": \"accept-upload-submission\",\n \"summary\": \"Accept source, metadata, headers, endpoint, and retry/scheduling policy.\"\n },\n {\n \"kind\": \"managed-primitive\",\n \"primitive\": \"make-source-durable\",\n \"summary\": \"Keep the source readable according to the selected runtime durability strategy.\"\n },\n {\n \"kind\": \"managed-primitive\",\n \"primitive\": \"schedule-upload-work\",\n \"summary\": \"Run upload work according to the runtime scheduler capability.\"\n },\n {\n \"featureId\": \"singleUploadLifecycle\",\n \"kind\": \"protocol-feature\",\n \"summary\": \"Use the raw protocol upload lifecycle for each execution attempt.\"\n },\n {\n \"featureId\": \"retryOffsetRecovery\",\n \"kind\": \"protocol-feature\",\n \"summary\": \"Use protocol retry and offset recovery before classifying terminal failure.\"\n },\n {\n \"kind\": \"managed-primitive\",\n \"primitive\": \"publish-upload-state\",\n \"summary\": \"Expose pending, running, succeeded, and failed state snapshots.\"\n },\n {\n \"kind\": \"managed-primitive\",\n \"primitive\": \"cleanup-managed-upload\",\n \"summary\": \"Remove owned sources and terminal state according to cleanup policy.\"\n }\n ],\n \"layer\": \"feature-over-protocol\",\n \"primitives\": [\n \"accept-upload-submission\",\n \"make-source-durable\",\n \"schedule-upload-work\",\n \"run-protocol-upload\",\n \"apply-managed-retry-policy\",\n \"classify-failure\",\n \"publish-upload-state\",\n \"cleanup-managed-upload\"\n ],\n \"protocolPrimitives\": [\n \"store-resume-url\",\n \"resume-from-previous-upload\",\n \"recover-offset-after-error\",\n \"retry-with-backoff\",\n \"emit-progress\",\n \"emit-chunk-complete\",\n \"terminate-upload\"\n ],\n \"runtimeProfiles\": [\n {\n \"networkConstraints\": [\n \"any-network\",\n \"unmetered-network\"\n ],\n \"runtime\": \"android\",\n \"scheduler\": \"durable-os-scheduler\",\n \"sourceDurability\": [\n \"copy-to-owned-storage\",\n \"reference-original-source\"\n ],\n \"stateBackend\": \"platform-key-value-store\"\n },\n {\n \"networkConstraints\": [\n \"any-network\",\n \"unmetered-network\"\n ],\n \"runtime\": \"ios\",\n \"scheduler\": \"durable-os-scheduler\",\n \"sourceDurability\": [\n \"copy-to-owned-storage\",\n \"reference-original-source\"\n ],\n \"stateBackend\": \"platform-key-value-store\"\n },\n {\n \"networkConstraints\": [\n \"any-network\"\n ],\n \"runtime\": \"browser\",\n \"scheduler\": \"foreground-task\",\n \"sourceDurability\": [\n \"reference-original-source\",\n \"memory-only\"\n ],\n \"stateBackend\": \"web-storage\"\n },\n {\n \"networkConstraints\": [\n \"any-network\"\n ],\n \"runtime\": \"java\",\n \"scheduler\": \"process-lifetime-worker-pool\",\n \"sourceDurability\": [\n \"copy-to-owned-storage\",\n \"reference-original-source\"\n ],\n \"stateBackend\": \"filesystem\"\n },\n {\n \"networkConstraints\": [\n \"any-network\"\n ],\n \"runtime\": \"node\",\n \"scheduler\": \"process-lifetime-worker-pool\",\n \"sourceDurability\": [\n \"copy-to-owned-storage\",\n \"reference-original-source\",\n \"memory-only\"\n ],\n \"stateBackend\": \"filesystem\"\n },\n {\n \"networkConstraints\": [\n \"any-network\"\n ],\n \"runtime\": \"react-native\",\n \"scheduler\": \"foreground-task\",\n \"sourceDurability\": [\n \"reference-original-source\",\n \"memory-only\"\n ],\n \"stateBackend\": \"platform-key-value-store\"\n }\n ],\n \"scenarios\": [\n {\n \"requiredPrimitives\": [\n \"accept-upload-submission\",\n \"make-source-durable\",\n \"schedule-upload-work\",\n \"run-protocol-upload\",\n \"apply-managed-retry-policy\",\n \"publish-upload-state\",\n \"cleanup-managed-upload\"\n ],\n \"scenarioId\": \"managedUploadDurableRetry\",\n \"summary\": \"Submit a durable source, survive scheduler/process interruption, resume by stored upload URL, and finish with cleanup.\"\n },\n {\n \"requiredPrimitives\": [\n \"accept-upload-submission\",\n \"make-source-durable\",\n \"schedule-upload-work\",\n \"classify-failure\",\n \"publish-upload-state\"\n ],\n \"scenarioId\": \"managedUploadPermanentFailure\",\n \"summary\": \"Classify missing sources and unretryable protocol failures as terminal without further retry.\"\n },\n {\n \"requiredPrimitives\": [\n \"accept-upload-submission\",\n \"schedule-upload-work\",\n \"publish-upload-state\"\n ],\n \"scenarioId\": \"managedUploadNetworkConstraint\",\n \"summary\": \"Honor network constraints before starting or resuming upload work.\"\n }\n ]\n}\n"; + static final GeneratedTusClientConformanceScenario[] CLIENT_CONFORMANCE_SCENARIOS = GeneratedTusClientConformanceScenarios.CLIENT_CONFORMANCE_SCENARIOS; From fe0fbf6443d24d230e60a522e92a267491068f5d Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Mon, 1 Jun 2026 11:52:40 +0200 Subject: [PATCH 32/43] Expose managed upload proof cases --- .../client/GeneratedTusProtocolContract.java | 132 ++++++++++++++++++ .../TestGeneratedTusConformanceEvents.java | 25 ++++ 2 files changed, 157 insertions(+) diff --git a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java index b0e72032..a01c5927 100644 --- a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java +++ b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java @@ -1123,6 +1123,111 @@ final class GeneratedTusProtocolContract { static final String MANAGED_UPLOAD_JSON = "{\n \"capabilities\": {\n \"cleanup\": {\n \"policies\": [\n \"remove-owned-source-after-success\",\n \"remove-owned-source-after-cancel\",\n \"retain-owned-source-after-permanent-failure\",\n \"retain-source-after-retryable-failure\",\n \"remove-managed-state-after-terminal-retention\"\n ]\n },\n \"failureClassification\": {\n \"permanentFailures\": [\n \"source-unavailable\",\n \"unretryable-protocol-error\",\n \"retry-policy-exhausted\"\n ],\n \"retryableFailures\": [\n \"retryable-protocol-error\",\n \"io-error\",\n \"network-unavailable\"\n ]\n },\n \"networkConstraints\": {\n \"options\": [\n \"any-network\",\n \"unmetered-network\"\n ]\n },\n \"retryPolicy\": {\n \"controls\": [\n \"max-attempts\",\n \"deadline\",\n \"progress-sensitive-budget\",\n \"unbounded-until-permanent-failure\"\n ],\n \"permanentFailure\": \"stop-without-retry\",\n \"progressReset\": \"reset-budget-after-accepted-offset-advances\"\n },\n \"scheduling\": {\n \"strategies\": [\n \"foreground-task\",\n \"process-lifetime-worker-pool\",\n \"durable-os-scheduler\"\n ]\n },\n \"sourceDurability\": {\n \"ownedCopyCleanup\": \"after-success-or-cancel\",\n \"strategies\": [\n \"copy-to-owned-storage\",\n \"reference-original-source\",\n \"memory-only\"\n ]\n },\n \"stateReporting\": {\n \"states\": [\n \"pending\",\n \"running\",\n \"succeeded\",\n \"failed\"\n ],\n \"terminalRetention\": \"session-and-next-launch\",\n \"transientRetention\": \"until-terminal\"\n }\n },\n \"conformance\": {\n \"scenarioIds\": [\n \"managedUploadDurableRetry\",\n \"managedUploadPermanentFailure\",\n \"managedUploadNetworkConstraint\"\n ],\n \"status\": \"needs-generated-scenario\"\n },\n \"description\": \"Submit upload work that can make sources durable, schedule/resume execution, retry, report state, and clean up while reusing the raw TUS protocol features underneath.\",\n \"featureId\": \"managedUpload\",\n \"flow\": [\n {\n \"kind\": \"managed-primitive\",\n \"primitive\": \"accept-upload-submission\",\n \"summary\": \"Accept source, metadata, headers, endpoint, and retry/scheduling policy.\"\n },\n {\n \"kind\": \"managed-primitive\",\n \"primitive\": \"make-source-durable\",\n \"summary\": \"Keep the source readable according to the selected runtime durability strategy.\"\n },\n {\n \"kind\": \"managed-primitive\",\n \"primitive\": \"schedule-upload-work\",\n \"summary\": \"Run upload work according to the runtime scheduler capability.\"\n },\n {\n \"featureId\": \"singleUploadLifecycle\",\n \"kind\": \"protocol-feature\",\n \"summary\": \"Use the raw protocol upload lifecycle for each execution attempt.\"\n },\n {\n \"featureId\": \"retryOffsetRecovery\",\n \"kind\": \"protocol-feature\",\n \"summary\": \"Use protocol retry and offset recovery before classifying terminal failure.\"\n },\n {\n \"kind\": \"managed-primitive\",\n \"primitive\": \"publish-upload-state\",\n \"summary\": \"Expose pending, running, succeeded, and failed state snapshots.\"\n },\n {\n \"kind\": \"managed-primitive\",\n \"primitive\": \"cleanup-managed-upload\",\n \"summary\": \"Remove owned sources and terminal state according to cleanup policy.\"\n }\n ],\n \"layer\": \"feature-over-protocol\",\n \"primitives\": [\n \"accept-upload-submission\",\n \"make-source-durable\",\n \"schedule-upload-work\",\n \"run-protocol-upload\",\n \"apply-managed-retry-policy\",\n \"classify-failure\",\n \"publish-upload-state\",\n \"cleanup-managed-upload\"\n ],\n \"protocolPrimitives\": [\n \"store-resume-url\",\n \"resume-from-previous-upload\",\n \"recover-offset-after-error\",\n \"retry-with-backoff\",\n \"emit-progress\",\n \"emit-chunk-complete\",\n \"terminate-upload\"\n ],\n \"runtimeProfiles\": [\n {\n \"networkConstraints\": [\n \"any-network\",\n \"unmetered-network\"\n ],\n \"runtime\": \"android\",\n \"scheduler\": \"durable-os-scheduler\",\n \"sourceDurability\": [\n \"copy-to-owned-storage\",\n \"reference-original-source\"\n ],\n \"stateBackend\": \"platform-key-value-store\"\n },\n {\n \"networkConstraints\": [\n \"any-network\",\n \"unmetered-network\"\n ],\n \"runtime\": \"ios\",\n \"scheduler\": \"durable-os-scheduler\",\n \"sourceDurability\": [\n \"copy-to-owned-storage\",\n \"reference-original-source\"\n ],\n \"stateBackend\": \"platform-key-value-store\"\n },\n {\n \"networkConstraints\": [\n \"any-network\"\n ],\n \"runtime\": \"browser\",\n \"scheduler\": \"foreground-task\",\n \"sourceDurability\": [\n \"reference-original-source\",\n \"memory-only\"\n ],\n \"stateBackend\": \"web-storage\"\n },\n {\n \"networkConstraints\": [\n \"any-network\"\n ],\n \"runtime\": \"java\",\n \"scheduler\": \"process-lifetime-worker-pool\",\n \"sourceDurability\": [\n \"copy-to-owned-storage\",\n \"reference-original-source\"\n ],\n \"stateBackend\": \"filesystem\"\n },\n {\n \"networkConstraints\": [\n \"any-network\"\n ],\n \"runtime\": \"node\",\n \"scheduler\": \"process-lifetime-worker-pool\",\n \"sourceDurability\": [\n \"copy-to-owned-storage\",\n \"reference-original-source\",\n \"memory-only\"\n ],\n \"stateBackend\": \"filesystem\"\n },\n {\n \"networkConstraints\": [\n \"any-network\"\n ],\n \"runtime\": \"react-native\",\n \"scheduler\": \"foreground-task\",\n \"sourceDurability\": [\n \"reference-original-source\",\n \"memory-only\"\n ],\n \"stateBackend\": \"platform-key-value-store\"\n }\n ],\n \"scenarios\": [\n {\n \"requiredPrimitives\": [\n \"accept-upload-submission\",\n \"make-source-durable\",\n \"schedule-upload-work\",\n \"run-protocol-upload\",\n \"apply-managed-retry-policy\",\n \"publish-upload-state\",\n \"cleanup-managed-upload\"\n ],\n \"scenarioId\": \"managedUploadDurableRetry\",\n \"summary\": \"Submit a durable source, survive scheduler/process interruption, resume by stored upload URL, and finish with cleanup.\"\n },\n {\n \"requiredPrimitives\": [\n \"accept-upload-submission\",\n \"make-source-durable\",\n \"schedule-upload-work\",\n \"classify-failure\",\n \"publish-upload-state\"\n ],\n \"scenarioId\": \"managedUploadPermanentFailure\",\n \"summary\": \"Classify missing sources and unretryable protocol failures as terminal without further retry.\"\n },\n {\n \"requiredPrimitives\": [\n \"accept-upload-submission\",\n \"schedule-upload-work\",\n \"publish-upload-state\"\n ],\n \"scenarioId\": \"managedUploadNetworkConstraint\",\n \"summary\": \"Honor network constraints before starting or resuming upload work.\"\n }\n ]\n}\n"; + static final String[] MANAGED_UPLOAD_PRIMITIVES = + new String[] { + "accept-upload-submission", + "make-source-durable", + "schedule-upload-work", + "run-protocol-upload", + "apply-managed-retry-policy", + "classify-failure", + "publish-upload-state", + "cleanup-managed-upload", + }; + + static final String[] MANAGED_UPLOAD_RUNTIME_PROFILES = + new String[] { + "android", + "ios", + "browser", + "java", + "node", + "react-native", + }; + + static final String[] MANAGED_UPLOAD_SCENARIO_IDS = + new String[] { + "managedUploadDurableRetry", + "managedUploadPermanentFailure", + "managedUploadNetworkConstraint", + }; + + static final GeneratedTusManagedUploadProofCase[] MANAGED_UPLOAD_PROOF_CASES = + new GeneratedTusManagedUploadProofCase[] { + new GeneratedTusProtocolContract.GeneratedTusManagedUploadProofCase( + "managedUpload", + "feature-over-protocol", + "managedUploadDurableRetry", + new String[] { + "accept-upload-submission", + "make-source-durable", + "schedule-upload-work", + "run-protocol-upload", + "apply-managed-retry-policy", + "publish-upload-state", + "cleanup-managed-upload", + }, + new String[] { + "singleUploadLifecycle", + "retryOffsetRecovery", + }, + new String[] { + "android", + "ios", + "browser", + "java", + "node", + "react-native", + } + ), + new GeneratedTusProtocolContract.GeneratedTusManagedUploadProofCase( + "managedUpload", + "feature-over-protocol", + "managedUploadPermanentFailure", + new String[] { + "accept-upload-submission", + "make-source-durable", + "schedule-upload-work", + "classify-failure", + "publish-upload-state", + }, + new String[] { + "singleUploadLifecycle", + "retryOffsetRecovery", + }, + new String[] { + "android", + "ios", + "browser", + "java", + "node", + "react-native", + } + ), + new GeneratedTusProtocolContract.GeneratedTusManagedUploadProofCase( + "managedUpload", + "feature-over-protocol", + "managedUploadNetworkConstraint", + new String[] { + "accept-upload-submission", + "schedule-upload-work", + "publish-upload-state", + }, + new String[] { + "singleUploadLifecycle", + "retryOffsetRecovery", + }, + new String[] { + "android", + "ios", + "browser", + "java", + "node", + "react-native", + } + ), + }; + static final GeneratedTusClientConformanceScenario[] CLIENT_CONFORMANCE_SCENARIOS = GeneratedTusClientConformanceScenarios.CLIENT_CONFORMANCE_SCENARIOS; @@ -1295,6 +1400,33 @@ static final class GeneratedTusClientFeatureFlowStep { } } + /** + * Generated managed-upload feature proof fixture. + */ + static final class GeneratedTusManagedUploadProofCase { + final String featureId; + final String layer; + final String scenarioId; + final String[] requiredPrimitives; + final String[] protocolFeatureIds; + final String[] runtimeProfiles; + + GeneratedTusManagedUploadProofCase( + String featureId, + String layer, + String scenarioId, + String[] requiredPrimitives, + String[] protocolFeatureIds, + String[] runtimeProfiles) { + this.featureId = featureId; + this.layer = layer; + this.scenarioId = scenarioId; + this.requiredPrimitives = requiredPrimitives; + this.protocolFeatureIds = protocolFeatureIds; + this.runtimeProfiles = runtimeProfiles; + } + } + /** * Generated client conformance scenario fixture. */ diff --git a/src/test/java/io/tus/java/client/TestGeneratedTusConformanceEvents.java b/src/test/java/io/tus/java/client/TestGeneratedTusConformanceEvents.java index 15c88982..65b15407 100644 --- a/src/test/java/io/tus/java/client/TestGeneratedTusConformanceEvents.java +++ b/src/test/java/io/tus/java/client/TestGeneratedTusConformanceEvents.java @@ -448,6 +448,31 @@ public void testGeneratedProofProfileScenarios() { } } + /** + * Verifies managed-upload proof scenarios stay wired to protocol features and primitives. + */ + @Test + public void testGeneratedManagedUploadProofScenarios() { + for (GeneratedTusProtocolContract.GeneratedTusManagedUploadProofCase testCase + : GeneratedTusProtocolContract.MANAGED_UPLOAD_PROOF_CASES) { + assertEquals("managedUpload", testCase.featureId); + assertEquals("feature-over-protocol", testCase.layer); + assertContains( + GeneratedTusProtocolContract.MANAGED_UPLOAD_SCENARIO_IDS, + testCase.scenarioId); + assertArrayEquals( + GeneratedTusProtocolContract.MANAGED_UPLOAD_RUNTIME_PROFILES, + testCase.runtimeProfiles); + + for (String primitive : testCase.requiredPrimitives) { + assertContains(GeneratedTusProtocolContract.MANAGED_UPLOAD_PRIMITIVES, primitive); + } + for (String featureId : testCase.protocolFeatureIds) { + findFeature(featureId); + } + } + } + private static GeneratedTusProtocolContract.GeneratedTusClientFeature findFeature( String featureId) { for (GeneratedTusProtocolContract.GeneratedTusClientFeature feature From 2b623965259203d5d53ca6f3c3f60d36a3b5eef7 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Mon, 1 Jun 2026 12:17:07 +0200 Subject: [PATCH 33/43] Add managed upload runtime proof --- .../client/GeneratedTusProtocolContract.java | 2 +- .../TestGeneratedTusManagedUploadRuntime.java | 650 ++++++++++++++++++ .../client/TestGeneratedTusRuntimeEvents.java | 67 +- 3 files changed, 710 insertions(+), 9 deletions(-) create mode 100644 src/test/java/io/tus/java/client/TestGeneratedTusManagedUploadRuntime.java diff --git a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java index a01c5927..6539c66a 100644 --- a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java +++ b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java @@ -1121,7 +1121,7 @@ final class GeneratedTusProtocolContract { ), }; - static final String MANAGED_UPLOAD_JSON = "{\n \"capabilities\": {\n \"cleanup\": {\n \"policies\": [\n \"remove-owned-source-after-success\",\n \"remove-owned-source-after-cancel\",\n \"retain-owned-source-after-permanent-failure\",\n \"retain-source-after-retryable-failure\",\n \"remove-managed-state-after-terminal-retention\"\n ]\n },\n \"failureClassification\": {\n \"permanentFailures\": [\n \"source-unavailable\",\n \"unretryable-protocol-error\",\n \"retry-policy-exhausted\"\n ],\n \"retryableFailures\": [\n \"retryable-protocol-error\",\n \"io-error\",\n \"network-unavailable\"\n ]\n },\n \"networkConstraints\": {\n \"options\": [\n \"any-network\",\n \"unmetered-network\"\n ]\n },\n \"retryPolicy\": {\n \"controls\": [\n \"max-attempts\",\n \"deadline\",\n \"progress-sensitive-budget\",\n \"unbounded-until-permanent-failure\"\n ],\n \"permanentFailure\": \"stop-without-retry\",\n \"progressReset\": \"reset-budget-after-accepted-offset-advances\"\n },\n \"scheduling\": {\n \"strategies\": [\n \"foreground-task\",\n \"process-lifetime-worker-pool\",\n \"durable-os-scheduler\"\n ]\n },\n \"sourceDurability\": {\n \"ownedCopyCleanup\": \"after-success-or-cancel\",\n \"strategies\": [\n \"copy-to-owned-storage\",\n \"reference-original-source\",\n \"memory-only\"\n ]\n },\n \"stateReporting\": {\n \"states\": [\n \"pending\",\n \"running\",\n \"succeeded\",\n \"failed\"\n ],\n \"terminalRetention\": \"session-and-next-launch\",\n \"transientRetention\": \"until-terminal\"\n }\n },\n \"conformance\": {\n \"scenarioIds\": [\n \"managedUploadDurableRetry\",\n \"managedUploadPermanentFailure\",\n \"managedUploadNetworkConstraint\"\n ],\n \"status\": \"needs-generated-scenario\"\n },\n \"description\": \"Submit upload work that can make sources durable, schedule/resume execution, retry, report state, and clean up while reusing the raw TUS protocol features underneath.\",\n \"featureId\": \"managedUpload\",\n \"flow\": [\n {\n \"kind\": \"managed-primitive\",\n \"primitive\": \"accept-upload-submission\",\n \"summary\": \"Accept source, metadata, headers, endpoint, and retry/scheduling policy.\"\n },\n {\n \"kind\": \"managed-primitive\",\n \"primitive\": \"make-source-durable\",\n \"summary\": \"Keep the source readable according to the selected runtime durability strategy.\"\n },\n {\n \"kind\": \"managed-primitive\",\n \"primitive\": \"schedule-upload-work\",\n \"summary\": \"Run upload work according to the runtime scheduler capability.\"\n },\n {\n \"featureId\": \"singleUploadLifecycle\",\n \"kind\": \"protocol-feature\",\n \"summary\": \"Use the raw protocol upload lifecycle for each execution attempt.\"\n },\n {\n \"featureId\": \"retryOffsetRecovery\",\n \"kind\": \"protocol-feature\",\n \"summary\": \"Use protocol retry and offset recovery before classifying terminal failure.\"\n },\n {\n \"kind\": \"managed-primitive\",\n \"primitive\": \"publish-upload-state\",\n \"summary\": \"Expose pending, running, succeeded, and failed state snapshots.\"\n },\n {\n \"kind\": \"managed-primitive\",\n \"primitive\": \"cleanup-managed-upload\",\n \"summary\": \"Remove owned sources and terminal state according to cleanup policy.\"\n }\n ],\n \"layer\": \"feature-over-protocol\",\n \"primitives\": [\n \"accept-upload-submission\",\n \"make-source-durable\",\n \"schedule-upload-work\",\n \"run-protocol-upload\",\n \"apply-managed-retry-policy\",\n \"classify-failure\",\n \"publish-upload-state\",\n \"cleanup-managed-upload\"\n ],\n \"protocolPrimitives\": [\n \"store-resume-url\",\n \"resume-from-previous-upload\",\n \"recover-offset-after-error\",\n \"retry-with-backoff\",\n \"emit-progress\",\n \"emit-chunk-complete\",\n \"terminate-upload\"\n ],\n \"runtimeProfiles\": [\n {\n \"networkConstraints\": [\n \"any-network\",\n \"unmetered-network\"\n ],\n \"runtime\": \"android\",\n \"scheduler\": \"durable-os-scheduler\",\n \"sourceDurability\": [\n \"copy-to-owned-storage\",\n \"reference-original-source\"\n ],\n \"stateBackend\": \"platform-key-value-store\"\n },\n {\n \"networkConstraints\": [\n \"any-network\",\n \"unmetered-network\"\n ],\n \"runtime\": \"ios\",\n \"scheduler\": \"durable-os-scheduler\",\n \"sourceDurability\": [\n \"copy-to-owned-storage\",\n \"reference-original-source\"\n ],\n \"stateBackend\": \"platform-key-value-store\"\n },\n {\n \"networkConstraints\": [\n \"any-network\"\n ],\n \"runtime\": \"browser\",\n \"scheduler\": \"foreground-task\",\n \"sourceDurability\": [\n \"reference-original-source\",\n \"memory-only\"\n ],\n \"stateBackend\": \"web-storage\"\n },\n {\n \"networkConstraints\": [\n \"any-network\"\n ],\n \"runtime\": \"java\",\n \"scheduler\": \"process-lifetime-worker-pool\",\n \"sourceDurability\": [\n \"copy-to-owned-storage\",\n \"reference-original-source\"\n ],\n \"stateBackend\": \"filesystem\"\n },\n {\n \"networkConstraints\": [\n \"any-network\"\n ],\n \"runtime\": \"node\",\n \"scheduler\": \"process-lifetime-worker-pool\",\n \"sourceDurability\": [\n \"copy-to-owned-storage\",\n \"reference-original-source\",\n \"memory-only\"\n ],\n \"stateBackend\": \"filesystem\"\n },\n {\n \"networkConstraints\": [\n \"any-network\"\n ],\n \"runtime\": \"react-native\",\n \"scheduler\": \"foreground-task\",\n \"sourceDurability\": [\n \"reference-original-source\",\n \"memory-only\"\n ],\n \"stateBackend\": \"platform-key-value-store\"\n }\n ],\n \"scenarios\": [\n {\n \"requiredPrimitives\": [\n \"accept-upload-submission\",\n \"make-source-durable\",\n \"schedule-upload-work\",\n \"run-protocol-upload\",\n \"apply-managed-retry-policy\",\n \"publish-upload-state\",\n \"cleanup-managed-upload\"\n ],\n \"scenarioId\": \"managedUploadDurableRetry\",\n \"summary\": \"Submit a durable source, survive scheduler/process interruption, resume by stored upload URL, and finish with cleanup.\"\n },\n {\n \"requiredPrimitives\": [\n \"accept-upload-submission\",\n \"make-source-durable\",\n \"schedule-upload-work\",\n \"classify-failure\",\n \"publish-upload-state\"\n ],\n \"scenarioId\": \"managedUploadPermanentFailure\",\n \"summary\": \"Classify missing sources and unretryable protocol failures as terminal without further retry.\"\n },\n {\n \"requiredPrimitives\": [\n \"accept-upload-submission\",\n \"schedule-upload-work\",\n \"publish-upload-state\"\n ],\n \"scenarioId\": \"managedUploadNetworkConstraint\",\n \"summary\": \"Honor network constraints before starting or resuming upload work.\"\n }\n ]\n}\n"; + static final String MANAGED_UPLOAD_JSON = "{\n \"capabilities\": {\n \"cleanup\": {\n \"policies\": [\n \"remove-owned-source-after-success\",\n \"remove-owned-source-after-cancel\",\n \"retain-owned-source-after-permanent-failure\",\n \"retain-source-after-retryable-failure\",\n \"remove-managed-state-after-terminal-retention\"\n ]\n },\n \"failureClassification\": {\n \"permanentFailures\": [\n \"source-unavailable\",\n \"unretryable-protocol-error\",\n \"retry-policy-exhausted\"\n ],\n \"retryableFailures\": [\n \"retryable-protocol-error\",\n \"io-error\",\n \"network-unavailable\"\n ]\n },\n \"networkConstraints\": {\n \"options\": [\n \"any-network\",\n \"unmetered-network\"\n ]\n },\n \"retryPolicy\": {\n \"controls\": [\n \"max-attempts\",\n \"deadline\",\n \"progress-sensitive-budget\",\n \"unbounded-until-permanent-failure\"\n ],\n \"permanentFailure\": \"stop-without-retry\",\n \"progressReset\": \"reset-budget-after-accepted-offset-advances\"\n },\n \"scheduling\": {\n \"strategies\": [\n \"foreground-task\",\n \"process-lifetime-worker-pool\",\n \"durable-os-scheduler\"\n ]\n },\n \"sourceDurability\": {\n \"ownedCopyCleanup\": \"after-success-or-cancel\",\n \"strategies\": [\n \"copy-to-owned-storage\",\n \"reference-original-source\",\n \"memory-only\"\n ]\n },\n \"stateReporting\": {\n \"states\": [\n \"pending\",\n \"running\",\n \"succeeded\",\n \"failed\"\n ],\n \"terminalRetention\": \"session-and-next-launch\",\n \"transientRetention\": \"until-terminal\"\n }\n },\n \"conformance\": {\n \"scenarioIds\": [\n \"managedUploadDurableRetry\",\n \"managedUploadPermanentFailure\",\n \"managedUploadNetworkConstraint\"\n ],\n \"status\": \"needs-generated-scenario\"\n },\n \"description\": \"Submit upload work that can make sources durable, schedule/resume execution, retry, report state, and clean up while reusing the raw TUS protocol features underneath.\",\n \"featureId\": \"managedUpload\",\n \"flow\": [\n {\n \"kind\": \"managed-primitive\",\n \"primitive\": \"accept-upload-submission\",\n \"summary\": \"Accept source, metadata, headers, endpoint, and retry/scheduling policy.\"\n },\n {\n \"kind\": \"managed-primitive\",\n \"primitive\": \"make-source-durable\",\n \"summary\": \"Keep the source readable according to the selected runtime durability strategy.\"\n },\n {\n \"kind\": \"managed-primitive\",\n \"primitive\": \"schedule-upload-work\",\n \"summary\": \"Run upload work according to the runtime scheduler capability.\"\n },\n {\n \"featureId\": \"singleUploadLifecycle\",\n \"kind\": \"protocol-feature\",\n \"summary\": \"Use the raw protocol upload lifecycle for each execution attempt.\"\n },\n {\n \"featureId\": \"retryOffsetRecovery\",\n \"kind\": \"protocol-feature\",\n \"summary\": \"Use protocol retry and offset recovery before classifying terminal failure.\"\n },\n {\n \"kind\": \"managed-primitive\",\n \"primitive\": \"publish-upload-state\",\n \"summary\": \"Expose pending, running, succeeded, and failed state snapshots.\"\n },\n {\n \"kind\": \"managed-primitive\",\n \"primitive\": \"cleanup-managed-upload\",\n \"summary\": \"Remove owned sources and terminal state according to cleanup policy.\"\n }\n ],\n \"layer\": \"feature-over-protocol\",\n \"primitives\": [\n \"accept-upload-submission\",\n \"make-source-durable\",\n \"schedule-upload-work\",\n \"run-protocol-upload\",\n \"apply-managed-retry-policy\",\n \"classify-failure\",\n \"publish-upload-state\",\n \"cleanup-managed-upload\"\n ],\n \"protocolPrimitives\": [\n \"store-resume-url\",\n \"resume-from-previous-upload\",\n \"recover-offset-after-error\",\n \"retry-with-backoff\",\n \"emit-progress\",\n \"emit-chunk-complete\",\n \"terminate-upload\"\n ],\n \"runtimeProfiles\": [\n {\n \"networkConstraints\": [\n \"any-network\",\n \"unmetered-network\"\n ],\n \"runtime\": \"android\",\n \"scheduler\": \"durable-os-scheduler\",\n \"sourceDurability\": [\n \"copy-to-owned-storage\",\n \"reference-original-source\"\n ],\n \"stateBackend\": \"platform-key-value-store\"\n },\n {\n \"networkConstraints\": [\n \"any-network\",\n \"unmetered-network\"\n ],\n \"runtime\": \"ios\",\n \"scheduler\": \"durable-os-scheduler\",\n \"sourceDurability\": [\n \"copy-to-owned-storage\",\n \"reference-original-source\"\n ],\n \"stateBackend\": \"platform-key-value-store\"\n },\n {\n \"networkConstraints\": [\n \"any-network\"\n ],\n \"runtime\": \"browser\",\n \"scheduler\": \"foreground-task\",\n \"sourceDurability\": [\n \"reference-original-source\",\n \"memory-only\"\n ],\n \"stateBackend\": \"web-storage\"\n },\n {\n \"networkConstraints\": [\n \"any-network\"\n ],\n \"runtime\": \"java\",\n \"scheduler\": \"process-lifetime-worker-pool\",\n \"sourceDurability\": [\n \"copy-to-owned-storage\",\n \"reference-original-source\"\n ],\n \"stateBackend\": \"filesystem\"\n },\n {\n \"networkConstraints\": [\n \"any-network\"\n ],\n \"runtime\": \"node\",\n \"scheduler\": \"process-lifetime-worker-pool\",\n \"sourceDurability\": [\n \"copy-to-owned-storage\",\n \"reference-original-source\",\n \"memory-only\"\n ],\n \"stateBackend\": \"filesystem\"\n },\n {\n \"networkConstraints\": [\n \"any-network\"\n ],\n \"runtime\": \"react-native\",\n \"scheduler\": \"foreground-task\",\n \"sourceDurability\": [\n \"reference-original-source\",\n \"memory-only\"\n ],\n \"stateBackend\": \"platform-key-value-store\"\n }\n ],\n \"scenarios\": [\n {\n \"proof\": {\n \"attempts\": [\n {\n \"attemptIndex\": 0,\n \"failure\": {\n \"afterAcceptedOffset\": 7,\n \"kind\": \"io-error\"\n },\n \"requests\": [\n {\n \"bodySize\": 0,\n \"headers\": {\n \"Upload-Length\": \"14\"\n },\n \"operationId\": \"createTusUpload\",\n \"response\": {\n \"headers\": {\n \"Location\": \"https://tus.io/uploads/managed-durable-retry\"\n },\n \"statusCode\": 201\n },\n \"url\": \"endpoint\"\n },\n {\n \"bodySize\": 7,\n \"headers\": {\n \"Upload-Offset\": \"0\"\n },\n \"operationId\": \"patchTusUpload\",\n \"response\": {\n \"headers\": {\n \"Upload-Offset\": \"7\"\n },\n \"statusCode\": 204\n },\n \"url\": \"upload\"\n }\n ],\n \"stateAfterAttempt\": \"failed\"\n },\n {\n \"attemptIndex\": 1,\n \"requests\": [\n {\n \"headers\": {},\n \"operationId\": \"getTusUploadOffset\",\n \"response\": {\n \"headers\": {\n \"Upload-Length\": \"14\",\n \"Upload-Offset\": \"7\"\n },\n \"statusCode\": 200\n },\n \"url\": \"upload\"\n },\n {\n \"bodySize\": 7,\n \"headers\": {\n \"Upload-Offset\": \"7\"\n },\n \"operationId\": \"patchTusUpload\",\n \"response\": {\n \"headers\": {\n \"Upload-Offset\": \"14\"\n },\n \"statusCode\": 204\n },\n \"url\": \"upload\"\n }\n ],\n \"stateAfterAttempt\": \"succeeded\"\n }\n ],\n \"cleanup\": {\n \"ownedSource\": \"remove-owned-source-after-success\",\n \"resumeUrl\": \"remove-after-success\"\n },\n \"input\": {\n \"chunkSize\": 7,\n \"content\": \"hello managed!\",\n \"fingerprint\": \"managed-durable-retry-fingerprint\",\n \"metadata\": {\n \"filename\": \"managed.txt\"\n },\n \"uploadPath\": \"managed-durable-retry\"\n },\n \"retryDelays\": [\n 0\n ],\n \"runtime\": \"java\",\n \"scheduler\": \"process-lifetime-worker-pool\",\n \"sourceDurability\": \"copy-to-owned-storage\",\n \"stateBackend\": \"filesystem\",\n \"states\": [\n \"pending\",\n \"running\",\n \"failed\",\n \"running\",\n \"succeeded\"\n ]\n },\n \"requiredPrimitives\": [\n \"accept-upload-submission\",\n \"make-source-durable\",\n \"schedule-upload-work\",\n \"run-protocol-upload\",\n \"apply-managed-retry-policy\",\n \"publish-upload-state\",\n \"cleanup-managed-upload\"\n ],\n \"scenarioId\": \"managedUploadDurableRetry\",\n \"summary\": \"Submit a durable source, survive scheduler/process interruption, resume by stored upload URL, and finish with cleanup.\"\n },\n {\n \"requiredPrimitives\": [\n \"accept-upload-submission\",\n \"make-source-durable\",\n \"schedule-upload-work\",\n \"classify-failure\",\n \"publish-upload-state\"\n ],\n \"scenarioId\": \"managedUploadPermanentFailure\",\n \"summary\": \"Classify missing sources and unretryable protocol failures as terminal without further retry.\"\n },\n {\n \"requiredPrimitives\": [\n \"accept-upload-submission\",\n \"schedule-upload-work\",\n \"publish-upload-state\"\n ],\n \"scenarioId\": \"managedUploadNetworkConstraint\",\n \"summary\": \"Honor network constraints before starting or resuming upload work.\"\n }\n ]\n}\n"; static final String[] MANAGED_UPLOAD_PRIMITIVES = new String[] { diff --git a/src/test/java/io/tus/java/client/TestGeneratedTusManagedUploadRuntime.java b/src/test/java/io/tus/java/client/TestGeneratedTusManagedUploadRuntime.java new file mode 100644 index 00000000..41b9b36b --- /dev/null +++ b/src/test/java/io/tus/java/client/TestGeneratedTusManagedUploadRuntime.java @@ -0,0 +1,650 @@ +/* + * Code generated from Transloadit API2 TUS protocol contracts; DO NOT EDIT. + * If it looks wrong, please report the issue instead of editing this file by hand; + * the source fix belongs in the protocol contract generator so all TUS clients stay in sync. + */ + +package io.tus.java.client; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +import org.junit.Test; +import org.mockserver.model.HttpRequest; +import org.mockserver.model.HttpResponse; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +/** + * Tests generated managed-upload scenarios against the real Java client pieces. + */ +public class TestGeneratedTusManagedUploadRuntime extends MockServerProvider { + private static final GeneratedTusManagedUploadRuntimeCase[] CASES = + new GeneratedTusManagedUploadRuntimeCase[] { + new GeneratedTusManagedUploadRuntimeCase( + "managedUploadDurableRetry", + new GeneratedTusManagedUploadRuntimeProfile( + "java", + "process-lifetime-worker-pool", + "copy-to-owned-storage", + "filesystem" + ), + new GeneratedTusManagedUploadTransport( + "Location" + ), + new GeneratedTusManagedUploadCleanup( + "remove-owned-source-after-success", + "remove-after-success" + ), + new GeneratedTusManagedUploadRetryPlan( + new String[] { + "pending", + "running", + "failed", + "running", + "succeeded", + }, + new int[] { + 0, + } + ), + new GeneratedTusManagedUploadInput( + "hello managed!", + 7, + "managed-durable-retry-fingerprint", + "managed-durable-retry", + new GeneratedTusManagedUploadMetadata[] { + new GeneratedTusManagedUploadMetadata( + "filename", + "managed.txt" + ), + } + ), + new GeneratedTusManagedUploadAttempt[] { + new GeneratedTusManagedUploadAttempt( + 0, + "failed", + new GeneratedTusManagedUploadFailure( + "io-error", + 7 + ), + new GeneratedTusManagedUploadRequest[] { + new GeneratedTusManagedUploadRequest( + "POST", + "endpoint", + 0, + 201, + new GeneratedTusManagedUploadHeader[] { + new GeneratedTusManagedUploadHeader( + "Upload-Length", + "14" + ), + }, + new GeneratedTusManagedUploadHeader[] { + new GeneratedTusManagedUploadHeader( + "Location", + "https://tus.io/uploads/managed-durable-retry" + ), + } + ), + new GeneratedTusManagedUploadRequest( + "PATCH", + "upload", + 7, + 204, + new GeneratedTusManagedUploadHeader[] { + new GeneratedTusManagedUploadHeader( + "Upload-Offset", + "0" + ), + }, + new GeneratedTusManagedUploadHeader[] { + new GeneratedTusManagedUploadHeader( + "Upload-Offset", + "7" + ), + } + ), + } + ), + new GeneratedTusManagedUploadAttempt( + 1, + "succeeded", + null, + new GeneratedTusManagedUploadRequest[] { + new GeneratedTusManagedUploadRequest( + "HEAD", + "upload", + 0, + 200, + new GeneratedTusManagedUploadHeader[0], + new GeneratedTusManagedUploadHeader[] { + new GeneratedTusManagedUploadHeader( + "Upload-Length", + "14" + ), + new GeneratedTusManagedUploadHeader( + "Upload-Offset", + "7" + ), + } + ), + new GeneratedTusManagedUploadRequest( + "PATCH", + "upload", + 7, + 204, + new GeneratedTusManagedUploadHeader[] { + new GeneratedTusManagedUploadHeader( + "Upload-Offset", + "7" + ), + }, + new GeneratedTusManagedUploadHeader[] { + new GeneratedTusManagedUploadHeader( + "Upload-Offset", + "14" + ), + } + ), + } + ), + } + ), + }; + private static final GeneratedTusMethodOverride[] METHOD_OVERRIDES = + new GeneratedTusMethodOverride[] { + new GeneratedTusMethodOverride( + "PATCH", + "POST", + "X-HTTP-Method-Override", + "PATCH" + ), + }; + + /** + * Verifies a durable source can retry, resume, finish, and clean up from contract data. + */ + @Test + public void testManagedUploadDurableRetryRuntime() throws Exception { + for (GeneratedTusManagedUploadRuntimeCase testCase : CASES) { + mockServer.reset(); + registerResponses(testCase); + + List states = new ArrayList(); + File source = writeSourceFile(testCase); + File ownedSource = ownedSourceFile(testCase, source); + File stateFile = stateFile(testCase, source); + copyDurableSource(testCase, source, ownedSource); + recordState(testCase, states, stateFile, "pending"); + + final GeneratedTusManagedUploadUrlStore urlStore = new GeneratedTusManagedUploadUrlStore(); + final TusClient client = new TusClient(); + client.setUploadCreationURL(mockServerURL); + client.enableResuming(urlStore); + client.enableRemoveFingerprintOnSuccess(); + + TusExecutor executor = managedExecutorFor(testCase, client, ownedSource, states, stateFile); + ExecutorService worker = Executors.newSingleThreadExecutor(); + try { + Future future = worker.submit(new Callable() { + @Override + public Boolean call() throws Exception { + return executor.makeAttempts(); + } + }); + assertTrue(testCase.scenarioId, future.get()); + } finally { + worker.shutdownNow(); + } + + cleanupAfterSuccess(testCase, ownedSource); + + assertArrayEquals( + testCase.scenarioId, + testCase.expectedStates, + states.toArray(new String[states.size()])); + assertArrayEquals( + testCase.scenarioId, + testCase.expectedStates, + Files.readAllLines(stateFile.toPath(), StandardCharsets.UTF_8) + .toArray(new String[testCase.expectedStates.length])); + assertNull(testCase.scenarioId, urlStore.get(testCase.input.fingerprint)); + assertFalse(testCase.scenarioId, ownedSource.exists()); + assertTrue(testCase.scenarioId, source.exists()); + source.delete(); + stateFile.delete(); + } + } + + private TusExecutor managedExecutorFor( + final GeneratedTusManagedUploadRuntimeCase testCase, + final TusClient client, + final File ownedSource, + final List states, + final File stateFile) { + TusExecutor executor = new TusExecutor() { + private int attemptIndex; + + @Override + protected void makeAttempt() throws ProtocolException, IOException { + GeneratedTusManagedUploadAttempt attempt = testCase.attempts[attemptIndex]; + attemptIndex += 1; + recordState(testCase, states, stateFile, "running"); + + TusUpload upload = uploadFor(testCase, ownedSource); + TusUploader uploader = client.resumeOrCreateUpload(upload); + uploader.setChunkSize(testCase.input.chunkSize); + uploader.setRequestPayloadSize(testCase.input.chunkSize); + while (uploader.getOffset() < upload.getSize()) { + uploader.uploadChunk(); + if ( + attempt.failure != null + && uploader.getOffset() == attempt.failure.afterAcceptedOffset) { + uploader.finish(false); + recordState(testCase, states, stateFile, attempt.stateAfterAttempt); + throw new IOException(attempt.failure.kind); + } + } + uploader.finish(); + recordState(testCase, states, stateFile, attempt.stateAfterAttempt); + } + }; + executor.setDelays(testCase.retryDelays); + return executor; + } + + private TusUpload uploadFor( + GeneratedTusManagedUploadRuntimeCase testCase, + File ownedSource) throws IOException { + TusUpload upload = new TusUpload(ownedSource); + upload.setFingerprint(testCase.input.fingerprint); + upload.setMetadata(metadataFor(testCase.input.metadata)); + return upload; + } + + private Map metadataFor(GeneratedTusManagedUploadMetadata[] metadata) { + Map result = new LinkedHashMap(); + for (GeneratedTusManagedUploadMetadata entry : metadata) { + result.put(entry.name, entry.value); + } + return result; + } + + private void copyDurableSource( + GeneratedTusManagedUploadRuntimeCase testCase, + File source, + File ownedSource) throws IOException { + if (!"copy-to-owned-storage".equals(testCase.sourceDurability)) { + throw new AssertionError( + testCase.scenarioId + + " uses unsupported generated source durability " + + testCase.sourceDurability); + } + + Files.copy(source.toPath(), ownedSource.toPath(), StandardCopyOption.REPLACE_EXISTING); + assertTrue(testCase.scenarioId, ownedSource.exists()); + } + + private void cleanupAfterSuccess( + GeneratedTusManagedUploadRuntimeCase testCase, + File ownedSource) throws IOException { + if (!"remove-owned-source-after-success".equals(testCase.ownedSourceCleanup)) { + return; + } + + Files.deleteIfExists(ownedSource.toPath()); + } + + private void recordState( + GeneratedTusManagedUploadRuntimeCase testCase, + List states, + File stateFile, + String state) throws IOException { + if (!"filesystem".equals(testCase.stateBackend)) { + throw new AssertionError( + testCase.scenarioId + + " uses unsupported generated state backend " + + testCase.stateBackend); + } + + states.add(state); + Files.write(stateFile.toPath(), states, StandardCharsets.UTF_8); + } + + private File writeSourceFile(GeneratedTusManagedUploadRuntimeCase testCase) throws IOException { + File source = File.createTempFile(testCase.scenarioId, "-source.bin"); + Files.write( + source.toPath(), + testCase.input.content.getBytes(StandardCharsets.UTF_8)); + return source; + } + + private File ownedSourceFile( + GeneratedTusManagedUploadRuntimeCase testCase, + File source) { + return new File(source.getParentFile(), testCase.scenarioId + "-owned.bin"); + } + + private File stateFile( + GeneratedTusManagedUploadRuntimeCase testCase, + File source) { + return new File(source.getParentFile(), testCase.scenarioId + "-state.txt"); + } + + private void registerResponses(GeneratedTusManagedUploadRuntimeCase testCase) throws Exception { + for (GeneratedTusManagedUploadAttempt attempt : testCase.attempts) { + for (GeneratedTusManagedUploadRequest request : attempt.requests) { + mockServer.when(requestFor(testCase, request, request.method, null)) + .respond(responseFor(testCase, request)); + GeneratedTusMethodOverride methodOverride = methodOverrideFor(request.method); + if (methodOverride != null) { + mockServer.when(requestFor(testCase, request, methodOverride.method, methodOverride)) + .respond(responseFor(testCase, request)); + } + } + } + } + + private HttpRequest requestFor( + GeneratedTusManagedUploadRuntimeCase testCase, + GeneratedTusManagedUploadRequest request, + String method, + GeneratedTusMethodOverride methodOverride) throws Exception { + HttpRequest httpRequest = new HttpRequest() + .withMethod(method) + .withPath(pathFor(testCase, request)); + for (GeneratedTusManagedUploadHeader header : request.requestHeaders) { + httpRequest.withHeader(header.name, header.value); + } + if (methodOverride != null) { + httpRequest.withHeader(methodOverride.headerName, methodOverride.headerValue); + } + return httpRequest; + } + + private GeneratedTusMethodOverride methodOverrideFor(String originalMethod) { + for (GeneratedTusMethodOverride methodOverride : METHOD_OVERRIDES) { + if (methodOverride.originalMethod.equals(originalMethod)) { + return methodOverride; + } + } + + return null; + } + + private String pathFor( + GeneratedTusManagedUploadRuntimeCase testCase, + GeneratedTusManagedUploadRequest request) throws Exception { + if ("endpoint".equals(request.url)) { + return mockServerURL.getPath(); + } + + return uploadUrlFor(testCase).getPath(); + } + + private HttpResponse responseFor( + GeneratedTusManagedUploadRuntimeCase testCase, + GeneratedTusManagedUploadRequest request) throws Exception { + HttpResponse response = new HttpResponse().withStatusCode(request.statusCode); + for (GeneratedTusManagedUploadHeader header : request.responseHeaders) { + response.withHeader(header.name, headerValueFor(testCase, header)); + } + return response; + } + + private String headerValueFor( + GeneratedTusManagedUploadRuntimeCase testCase, + GeneratedTusManagedUploadHeader header) throws Exception { + if (!testCase.locationHeaderName.equals(header.name)) { + return header.value; + } + + return uploadUrlFor(testCase).toString(); + } + + private URL uploadUrlFor(GeneratedTusManagedUploadRuntimeCase testCase) throws Exception { + return new URL(mockServerURL.toString() + "/" + testCase.input.uploadPath); + } + + private static String offsetDiscoveryMethod() { + for (GeneratedTusProtocolContract.GeneratedTusProtocolOperation operation + : GeneratedTusProtocolContract.OPERATIONS) { + if ("offset-discovery".equals(operation.role)) { + return operation.method; + } + } + + throw new AssertionError("Missing generated offset-discovery operation"); + } + + private static final class GeneratedTusManagedUploadRuntimeCase { + final String scenarioId; + final String runtime; + final String scheduler; + final String sourceDurability; + final String stateBackend; + final String locationHeaderName; + final String ownedSourceCleanup; + final String resumeUrlCleanup; + final String[] expectedStates; + final int[] retryDelays; + final String offsetDiscoveryMethod; + final GeneratedTusManagedUploadInput input; + final GeneratedTusManagedUploadAttempt[] attempts; + + GeneratedTusManagedUploadRuntimeCase( + String scenarioId, + GeneratedTusManagedUploadRuntimeProfile profile, + GeneratedTusManagedUploadTransport transport, + GeneratedTusManagedUploadCleanup cleanup, + GeneratedTusManagedUploadRetryPlan retryPlan, + GeneratedTusManagedUploadInput input, + GeneratedTusManagedUploadAttempt[] attempts) { + this.scenarioId = scenarioId; + this.runtime = profile.runtime; + this.scheduler = profile.scheduler; + this.sourceDurability = profile.sourceDurability; + this.stateBackend = profile.stateBackend; + this.locationHeaderName = transport.locationHeaderName; + this.ownedSourceCleanup = cleanup.ownedSource; + this.resumeUrlCleanup = cleanup.resumeUrl; + this.expectedStates = retryPlan.expectedStates; + this.retryDelays = retryPlan.retryDelays; + this.offsetDiscoveryMethod = offsetDiscoveryMethod(); + this.input = input; + this.attempts = attempts; + } + } + + private static final class GeneratedTusManagedUploadRuntimeProfile { + final String runtime; + final String scheduler; + final String sourceDurability; + final String stateBackend; + + GeneratedTusManagedUploadRuntimeProfile( + String runtime, + String scheduler, + String sourceDurability, + String stateBackend) { + this.runtime = runtime; + this.scheduler = scheduler; + this.sourceDurability = sourceDurability; + this.stateBackend = stateBackend; + } + } + + private static final class GeneratedTusManagedUploadTransport { + final String locationHeaderName; + + GeneratedTusManagedUploadTransport(String locationHeaderName) { + this.locationHeaderName = locationHeaderName; + } + } + + private static final class GeneratedTusManagedUploadCleanup { + final String ownedSource; + final String resumeUrl; + + GeneratedTusManagedUploadCleanup(String ownedSource, String resumeUrl) { + this.ownedSource = ownedSource; + this.resumeUrl = resumeUrl; + } + } + + private static final class GeneratedTusManagedUploadRetryPlan { + final String[] expectedStates; + final int[] retryDelays; + + GeneratedTusManagedUploadRetryPlan(String[] expectedStates, int[] retryDelays) { + this.expectedStates = expectedStates; + this.retryDelays = retryDelays; + } + } + + private static final class GeneratedTusManagedUploadInput { + final String content; + final int chunkSize; + final String fingerprint; + final String uploadPath; + final GeneratedTusManagedUploadMetadata[] metadata; + + GeneratedTusManagedUploadInput( + String content, + int chunkSize, + String fingerprint, + String uploadPath, + GeneratedTusManagedUploadMetadata[] metadata) { + this.content = content; + this.chunkSize = chunkSize; + this.fingerprint = fingerprint; + this.uploadPath = uploadPath; + this.metadata = metadata; + } + } + + private static final class GeneratedTusManagedUploadAttempt { + final int attemptIndex; + final String stateAfterAttempt; + final GeneratedTusManagedUploadFailure failure; + final GeneratedTusManagedUploadRequest[] requests; + + GeneratedTusManagedUploadAttempt( + int attemptIndex, + String stateAfterAttempt, + GeneratedTusManagedUploadFailure failure, + GeneratedTusManagedUploadRequest[] requests) { + this.attemptIndex = attemptIndex; + this.stateAfterAttempt = stateAfterAttempt; + this.failure = failure; + this.requests = requests; + } + } + + private static final class GeneratedTusManagedUploadFailure { + final String kind; + final long afterAcceptedOffset; + + GeneratedTusManagedUploadFailure(String kind, long afterAcceptedOffset) { + this.kind = kind; + this.afterAcceptedOffset = afterAcceptedOffset; + } + } + + private static final class GeneratedTusManagedUploadRequest { + final String method; + final String url; + final int bodySize; + final int statusCode; + final GeneratedTusManagedUploadHeader[] requestHeaders; + final GeneratedTusManagedUploadHeader[] responseHeaders; + + GeneratedTusManagedUploadRequest( + String method, + String url, + int bodySize, + int statusCode, + GeneratedTusManagedUploadHeader[] requestHeaders, + GeneratedTusManagedUploadHeader[] responseHeaders) { + this.method = method; + this.url = url; + this.bodySize = bodySize; + this.statusCode = statusCode; + this.requestHeaders = requestHeaders; + this.responseHeaders = responseHeaders; + } + } + + private static final class GeneratedTusManagedUploadHeader { + final String name; + final String value; + + GeneratedTusManagedUploadHeader(String name, String value) { + this.name = name; + this.value = value; + } + } + + private static final class GeneratedTusManagedUploadMetadata { + final String name; + final String value; + + GeneratedTusManagedUploadMetadata(String name, String value) { + this.name = name; + this.value = value; + } + } + + private static final class GeneratedTusMethodOverride { + final String originalMethod; + final String method; + final String headerName; + final String headerValue; + + GeneratedTusMethodOverride( + String originalMethod, + String method, + String headerName, + String headerValue) { + this.originalMethod = originalMethod; + this.method = method; + this.headerName = headerName; + this.headerValue = headerValue; + } + } + + private static final class GeneratedTusManagedUploadUrlStore implements TusURLStore { + private final Map values = new LinkedHashMap(); + + @Override + public URL get(String fingerprint) { + return values.get(fingerprint); + } + + @Override + public void set(String fingerprint, URL url) { + values.put(fingerprint, url); + } + + @Override + public void remove(String fingerprint) { + values.remove(fingerprint); + } + } +} diff --git a/src/test/java/io/tus/java/client/TestGeneratedTusRuntimeEvents.java b/src/test/java/io/tus/java/client/TestGeneratedTusRuntimeEvents.java index b900fc5a..2dbcd7be 100644 --- a/src/test/java/io/tus/java/client/TestGeneratedTusRuntimeEvents.java +++ b/src/test/java/io/tus/java/client/TestGeneratedTusRuntimeEvents.java @@ -280,6 +280,15 @@ public class TestGeneratedTusRuntimeEvents extends MockServerProvider { } ), }; + private static final GeneratedTusMethodOverride[] METHOD_OVERRIDES = + new GeneratedTusMethodOverride[] { + new GeneratedTusMethodOverride( + "PATCH", + "POST", + "X-HTTP-Method-Override", + "PATCH" + ), + }; /** * Verifies the sync uploader emits generated progress and chunk-complete events. @@ -404,17 +413,41 @@ private Map metadataFor(GeneratedTusRuntimeEventMetadata[] metad private void registerResponses(GeneratedTusRuntimeEventCase testCase) throws Exception { for (GeneratedTusRuntimeEventRequest request : testCase.requests) { - HttpRequest httpRequest = new HttpRequest() - .withPath(pathFor(testCase, request)); - if (!"upload".equals(request.url) || "HEAD".equals(request.method)) { - httpRequest.withMethod(request.method); - } - for (GeneratedTusRuntimeEventHeader header : request.requestHeaders) { - httpRequest.withHeader(header.name, header.value); + mockServer.when(requestFor(testCase, request, request.method, null)) + .respond(responseFor(testCase, request)); + GeneratedTusMethodOverride methodOverride = methodOverrideFor(request.method); + if (methodOverride != null) { + mockServer.when(requestFor(testCase, request, methodOverride.method, methodOverride)) + .respond(responseFor(testCase, request)); } + } + } - mockServer.when(httpRequest).respond(responseFor(testCase, request)); + private HttpRequest requestFor( + GeneratedTusRuntimeEventCase testCase, + GeneratedTusRuntimeEventRequest request, + String method, + GeneratedTusMethodOverride methodOverride) throws Exception { + HttpRequest httpRequest = new HttpRequest() + .withMethod(method) + .withPath(pathFor(testCase, request)); + for (GeneratedTusRuntimeEventHeader header : request.requestHeaders) { + httpRequest.withHeader(header.name, header.value); + } + if (methodOverride != null) { + httpRequest.withHeader(methodOverride.headerName, methodOverride.headerValue); } + return httpRequest; + } + + private GeneratedTusMethodOverride methodOverrideFor(String originalMethod) { + for (GeneratedTusMethodOverride methodOverride : METHOD_OVERRIDES) { + if (methodOverride.originalMethod.equals(originalMethod)) { + return methodOverride; + } + } + + return null; } private String pathFor( @@ -677,6 +710,24 @@ private static final class GeneratedTusRuntimeEventMetadata { } } + private static final class GeneratedTusMethodOverride { + final String originalMethod; + final String method; + final String headerName; + final String headerValue; + + GeneratedTusMethodOverride( + String originalMethod, + String method, + String headerName, + String headerValue) { + this.originalMethod = originalMethod; + this.method = method; + this.headerName = headerName; + this.headerValue = headerValue; + } + } + private static final class GeneratedTusRuntimeEventUrlStore implements TusURLStore { private final Map values = new LinkedHashMap(); From c7e4c157b7a3d41af2e0cb71d26a60175ec09e68 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Mon, 1 Jun 2026 12:43:36 +0200 Subject: [PATCH 34/43] Update managed upload proof fixture --- .../tus/java/client/GeneratedTusProtocolContract.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java index 6539c66a..79f17bc7 100644 --- a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java +++ b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java @@ -1121,7 +1121,7 @@ final class GeneratedTusProtocolContract { ), }; - static final String MANAGED_UPLOAD_JSON = "{\n \"capabilities\": {\n \"cleanup\": {\n \"policies\": [\n \"remove-owned-source-after-success\",\n \"remove-owned-source-after-cancel\",\n \"retain-owned-source-after-permanent-failure\",\n \"retain-source-after-retryable-failure\",\n \"remove-managed-state-after-terminal-retention\"\n ]\n },\n \"failureClassification\": {\n \"permanentFailures\": [\n \"source-unavailable\",\n \"unretryable-protocol-error\",\n \"retry-policy-exhausted\"\n ],\n \"retryableFailures\": [\n \"retryable-protocol-error\",\n \"io-error\",\n \"network-unavailable\"\n ]\n },\n \"networkConstraints\": {\n \"options\": [\n \"any-network\",\n \"unmetered-network\"\n ]\n },\n \"retryPolicy\": {\n \"controls\": [\n \"max-attempts\",\n \"deadline\",\n \"progress-sensitive-budget\",\n \"unbounded-until-permanent-failure\"\n ],\n \"permanentFailure\": \"stop-without-retry\",\n \"progressReset\": \"reset-budget-after-accepted-offset-advances\"\n },\n \"scheduling\": {\n \"strategies\": [\n \"foreground-task\",\n \"process-lifetime-worker-pool\",\n \"durable-os-scheduler\"\n ]\n },\n \"sourceDurability\": {\n \"ownedCopyCleanup\": \"after-success-or-cancel\",\n \"strategies\": [\n \"copy-to-owned-storage\",\n \"reference-original-source\",\n \"memory-only\"\n ]\n },\n \"stateReporting\": {\n \"states\": [\n \"pending\",\n \"running\",\n \"succeeded\",\n \"failed\"\n ],\n \"terminalRetention\": \"session-and-next-launch\",\n \"transientRetention\": \"until-terminal\"\n }\n },\n \"conformance\": {\n \"scenarioIds\": [\n \"managedUploadDurableRetry\",\n \"managedUploadPermanentFailure\",\n \"managedUploadNetworkConstraint\"\n ],\n \"status\": \"needs-generated-scenario\"\n },\n \"description\": \"Submit upload work that can make sources durable, schedule/resume execution, retry, report state, and clean up while reusing the raw TUS protocol features underneath.\",\n \"featureId\": \"managedUpload\",\n \"flow\": [\n {\n \"kind\": \"managed-primitive\",\n \"primitive\": \"accept-upload-submission\",\n \"summary\": \"Accept source, metadata, headers, endpoint, and retry/scheduling policy.\"\n },\n {\n \"kind\": \"managed-primitive\",\n \"primitive\": \"make-source-durable\",\n \"summary\": \"Keep the source readable according to the selected runtime durability strategy.\"\n },\n {\n \"kind\": \"managed-primitive\",\n \"primitive\": \"schedule-upload-work\",\n \"summary\": \"Run upload work according to the runtime scheduler capability.\"\n },\n {\n \"featureId\": \"singleUploadLifecycle\",\n \"kind\": \"protocol-feature\",\n \"summary\": \"Use the raw protocol upload lifecycle for each execution attempt.\"\n },\n {\n \"featureId\": \"retryOffsetRecovery\",\n \"kind\": \"protocol-feature\",\n \"summary\": \"Use protocol retry and offset recovery before classifying terminal failure.\"\n },\n {\n \"kind\": \"managed-primitive\",\n \"primitive\": \"publish-upload-state\",\n \"summary\": \"Expose pending, running, succeeded, and failed state snapshots.\"\n },\n {\n \"kind\": \"managed-primitive\",\n \"primitive\": \"cleanup-managed-upload\",\n \"summary\": \"Remove owned sources and terminal state according to cleanup policy.\"\n }\n ],\n \"layer\": \"feature-over-protocol\",\n \"primitives\": [\n \"accept-upload-submission\",\n \"make-source-durable\",\n \"schedule-upload-work\",\n \"run-protocol-upload\",\n \"apply-managed-retry-policy\",\n \"classify-failure\",\n \"publish-upload-state\",\n \"cleanup-managed-upload\"\n ],\n \"protocolPrimitives\": [\n \"store-resume-url\",\n \"resume-from-previous-upload\",\n \"recover-offset-after-error\",\n \"retry-with-backoff\",\n \"emit-progress\",\n \"emit-chunk-complete\",\n \"terminate-upload\"\n ],\n \"runtimeProfiles\": [\n {\n \"networkConstraints\": [\n \"any-network\",\n \"unmetered-network\"\n ],\n \"runtime\": \"android\",\n \"scheduler\": \"durable-os-scheduler\",\n \"sourceDurability\": [\n \"copy-to-owned-storage\",\n \"reference-original-source\"\n ],\n \"stateBackend\": \"platform-key-value-store\"\n },\n {\n \"networkConstraints\": [\n \"any-network\",\n \"unmetered-network\"\n ],\n \"runtime\": \"ios\",\n \"scheduler\": \"durable-os-scheduler\",\n \"sourceDurability\": [\n \"copy-to-owned-storage\",\n \"reference-original-source\"\n ],\n \"stateBackend\": \"platform-key-value-store\"\n },\n {\n \"networkConstraints\": [\n \"any-network\"\n ],\n \"runtime\": \"browser\",\n \"scheduler\": \"foreground-task\",\n \"sourceDurability\": [\n \"reference-original-source\",\n \"memory-only\"\n ],\n \"stateBackend\": \"web-storage\"\n },\n {\n \"networkConstraints\": [\n \"any-network\"\n ],\n \"runtime\": \"java\",\n \"scheduler\": \"process-lifetime-worker-pool\",\n \"sourceDurability\": [\n \"copy-to-owned-storage\",\n \"reference-original-source\"\n ],\n \"stateBackend\": \"filesystem\"\n },\n {\n \"networkConstraints\": [\n \"any-network\"\n ],\n \"runtime\": \"node\",\n \"scheduler\": \"process-lifetime-worker-pool\",\n \"sourceDurability\": [\n \"copy-to-owned-storage\",\n \"reference-original-source\",\n \"memory-only\"\n ],\n \"stateBackend\": \"filesystem\"\n },\n {\n \"networkConstraints\": [\n \"any-network\"\n ],\n \"runtime\": \"react-native\",\n \"scheduler\": \"foreground-task\",\n \"sourceDurability\": [\n \"reference-original-source\",\n \"memory-only\"\n ],\n \"stateBackend\": \"platform-key-value-store\"\n }\n ],\n \"scenarios\": [\n {\n \"proof\": {\n \"attempts\": [\n {\n \"attemptIndex\": 0,\n \"failure\": {\n \"afterAcceptedOffset\": 7,\n \"kind\": \"io-error\"\n },\n \"requests\": [\n {\n \"bodySize\": 0,\n \"headers\": {\n \"Upload-Length\": \"14\"\n },\n \"operationId\": \"createTusUpload\",\n \"response\": {\n \"headers\": {\n \"Location\": \"https://tus.io/uploads/managed-durable-retry\"\n },\n \"statusCode\": 201\n },\n \"url\": \"endpoint\"\n },\n {\n \"bodySize\": 7,\n \"headers\": {\n \"Upload-Offset\": \"0\"\n },\n \"operationId\": \"patchTusUpload\",\n \"response\": {\n \"headers\": {\n \"Upload-Offset\": \"7\"\n },\n \"statusCode\": 204\n },\n \"url\": \"upload\"\n }\n ],\n \"stateAfterAttempt\": \"failed\"\n },\n {\n \"attemptIndex\": 1,\n \"requests\": [\n {\n \"headers\": {},\n \"operationId\": \"getTusUploadOffset\",\n \"response\": {\n \"headers\": {\n \"Upload-Length\": \"14\",\n \"Upload-Offset\": \"7\"\n },\n \"statusCode\": 200\n },\n \"url\": \"upload\"\n },\n {\n \"bodySize\": 7,\n \"headers\": {\n \"Upload-Offset\": \"7\"\n },\n \"operationId\": \"patchTusUpload\",\n \"response\": {\n \"headers\": {\n \"Upload-Offset\": \"14\"\n },\n \"statusCode\": 204\n },\n \"url\": \"upload\"\n }\n ],\n \"stateAfterAttempt\": \"succeeded\"\n }\n ],\n \"cleanup\": {\n \"ownedSource\": \"remove-owned-source-after-success\",\n \"resumeUrl\": \"remove-after-success\"\n },\n \"input\": {\n \"chunkSize\": 7,\n \"content\": \"hello managed!\",\n \"fingerprint\": \"managed-durable-retry-fingerprint\",\n \"metadata\": {\n \"filename\": \"managed.txt\"\n },\n \"uploadPath\": \"managed-durable-retry\"\n },\n \"retryDelays\": [\n 0\n ],\n \"runtime\": \"java\",\n \"scheduler\": \"process-lifetime-worker-pool\",\n \"sourceDurability\": \"copy-to-owned-storage\",\n \"stateBackend\": \"filesystem\",\n \"states\": [\n \"pending\",\n \"running\",\n \"failed\",\n \"running\",\n \"succeeded\"\n ]\n },\n \"requiredPrimitives\": [\n \"accept-upload-submission\",\n \"make-source-durable\",\n \"schedule-upload-work\",\n \"run-protocol-upload\",\n \"apply-managed-retry-policy\",\n \"publish-upload-state\",\n \"cleanup-managed-upload\"\n ],\n \"scenarioId\": \"managedUploadDurableRetry\",\n \"summary\": \"Submit a durable source, survive scheduler/process interruption, resume by stored upload URL, and finish with cleanup.\"\n },\n {\n \"requiredPrimitives\": [\n \"accept-upload-submission\",\n \"make-source-durable\",\n \"schedule-upload-work\",\n \"classify-failure\",\n \"publish-upload-state\"\n ],\n \"scenarioId\": \"managedUploadPermanentFailure\",\n \"summary\": \"Classify missing sources and unretryable protocol failures as terminal without further retry.\"\n },\n {\n \"requiredPrimitives\": [\n \"accept-upload-submission\",\n \"schedule-upload-work\",\n \"publish-upload-state\"\n ],\n \"scenarioId\": \"managedUploadNetworkConstraint\",\n \"summary\": \"Honor network constraints before starting or resuming upload work.\"\n }\n ]\n}\n"; + static final String MANAGED_UPLOAD_JSON = "{\n \"capabilities\": {\n \"cleanup\": {\n \"policies\": [\n \"remove-owned-source-after-success\",\n \"remove-owned-source-after-cancel\",\n \"retain-owned-source-after-permanent-failure\",\n \"retain-source-after-retryable-failure\",\n \"remove-managed-state-after-terminal-retention\"\n ]\n },\n \"failureClassification\": {\n \"permanentFailures\": [\n \"source-unavailable\",\n \"unretryable-protocol-error\",\n \"retry-policy-exhausted\"\n ],\n \"retryableFailures\": [\n \"retryable-protocol-error\",\n \"io-error\",\n \"network-unavailable\"\n ]\n },\n \"networkConstraints\": {\n \"options\": [\n \"any-network\",\n \"unmetered-network\"\n ]\n },\n \"retryPolicy\": {\n \"controls\": [\n \"max-attempts\",\n \"deadline\",\n \"progress-sensitive-budget\",\n \"unbounded-until-permanent-failure\"\n ],\n \"permanentFailure\": \"stop-without-retry\",\n \"progressReset\": \"reset-budget-after-accepted-offset-advances\"\n },\n \"scheduling\": {\n \"strategies\": [\n \"foreground-task\",\n \"process-lifetime-worker-pool\",\n \"durable-os-scheduler\"\n ]\n },\n \"sourceDurability\": {\n \"ownedCopyCleanup\": \"after-success-or-cancel\",\n \"strategies\": [\n \"copy-to-owned-storage\",\n \"reference-original-source\",\n \"memory-only\"\n ]\n },\n \"stateReporting\": {\n \"states\": [\n \"pending\",\n \"running\",\n \"succeeded\",\n \"failed\"\n ],\n \"terminalRetention\": \"session-and-next-launch\",\n \"transientRetention\": \"until-terminal\"\n }\n },\n \"conformance\": {\n \"scenarioIds\": [\n \"managedUploadDurableRetry\",\n \"managedUploadPermanentFailure\",\n \"managedUploadNetworkConstraint\"\n ],\n \"status\": \"needs-generated-scenario\"\n },\n \"description\": \"Submit upload work that can make sources durable, schedule/resume execution, retry, report state, and clean up while reusing the raw TUS protocol features underneath.\",\n \"featureId\": \"managedUpload\",\n \"flow\": [\n {\n \"kind\": \"managed-primitive\",\n \"primitive\": \"accept-upload-submission\",\n \"summary\": \"Accept source, metadata, headers, endpoint, and retry/scheduling policy.\"\n },\n {\n \"kind\": \"managed-primitive\",\n \"primitive\": \"make-source-durable\",\n \"summary\": \"Keep the source readable according to the selected runtime durability strategy.\"\n },\n {\n \"kind\": \"managed-primitive\",\n \"primitive\": \"schedule-upload-work\",\n \"summary\": \"Run upload work according to the runtime scheduler capability.\"\n },\n {\n \"featureId\": \"singleUploadLifecycle\",\n \"kind\": \"protocol-feature\",\n \"summary\": \"Use the raw protocol upload lifecycle for each execution attempt.\"\n },\n {\n \"featureId\": \"retryOffsetRecovery\",\n \"kind\": \"protocol-feature\",\n \"summary\": \"Use protocol retry and offset recovery before classifying terminal failure.\"\n },\n {\n \"kind\": \"managed-primitive\",\n \"primitive\": \"publish-upload-state\",\n \"summary\": \"Expose pending, running, succeeded, and failed state snapshots.\"\n },\n {\n \"kind\": \"managed-primitive\",\n \"primitive\": \"cleanup-managed-upload\",\n \"summary\": \"Remove owned sources and terminal state according to cleanup policy.\"\n }\n ],\n \"layer\": \"feature-over-protocol\",\n \"primitives\": [\n \"accept-upload-submission\",\n \"make-source-durable\",\n \"schedule-upload-work\",\n \"run-protocol-upload\",\n \"apply-managed-retry-policy\",\n \"classify-failure\",\n \"publish-upload-state\",\n \"cleanup-managed-upload\"\n ],\n \"protocolPrimitives\": [\n \"store-resume-url\",\n \"resume-from-previous-upload\",\n \"recover-offset-after-error\",\n \"retry-with-backoff\",\n \"emit-progress\",\n \"emit-chunk-complete\",\n \"terminate-upload\"\n ],\n \"runtimeProfiles\": [\n {\n \"networkConstraints\": [\n \"any-network\",\n \"unmetered-network\"\n ],\n \"runtime\": \"android\",\n \"scheduler\": \"durable-os-scheduler\",\n \"sourceDurability\": [\n \"copy-to-owned-storage\",\n \"reference-original-source\"\n ],\n \"stateBackend\": \"platform-key-value-store\"\n },\n {\n \"networkConstraints\": [\n \"any-network\",\n \"unmetered-network\"\n ],\n \"runtime\": \"ios\",\n \"scheduler\": \"durable-os-scheduler\",\n \"sourceDurability\": [\n \"copy-to-owned-storage\",\n \"reference-original-source\"\n ],\n \"stateBackend\": \"platform-key-value-store\"\n },\n {\n \"networkConstraints\": [\n \"any-network\"\n ],\n \"runtime\": \"browser\",\n \"scheduler\": \"foreground-task\",\n \"sourceDurability\": [\n \"reference-original-source\",\n \"memory-only\"\n ],\n \"stateBackend\": \"web-storage\"\n },\n {\n \"networkConstraints\": [\n \"any-network\"\n ],\n \"runtime\": \"java\",\n \"scheduler\": \"process-lifetime-worker-pool\",\n \"sourceDurability\": [\n \"copy-to-owned-storage\",\n \"reference-original-source\"\n ],\n \"stateBackend\": \"filesystem\"\n },\n {\n \"networkConstraints\": [\n \"any-network\"\n ],\n \"runtime\": \"node\",\n \"scheduler\": \"process-lifetime-worker-pool\",\n \"sourceDurability\": [\n \"copy-to-owned-storage\",\n \"reference-original-source\",\n \"memory-only\"\n ],\n \"stateBackend\": \"filesystem\"\n },\n {\n \"networkConstraints\": [\n \"any-network\"\n ],\n \"runtime\": \"react-native\",\n \"scheduler\": \"foreground-task\",\n \"sourceDurability\": [\n \"reference-original-source\",\n \"memory-only\"\n ],\n \"stateBackend\": \"platform-key-value-store\"\n }\n ],\n \"scenarios\": [\n {\n \"proofs\": [\n {\n \"attempts\": [\n {\n \"attemptIndex\": 0,\n \"failure\": {\n \"afterAcceptedOffset\": 7,\n \"kind\": \"io-error\"\n },\n \"requests\": [\n {\n \"bodySize\": 0,\n \"headers\": {\n \"Upload-Length\": \"14\"\n },\n \"operationId\": \"createTusUpload\",\n \"response\": {\n \"headers\": {\n \"Location\": \"https://tus.io/uploads/managed-durable-retry\"\n },\n \"statusCode\": 201\n },\n \"url\": \"endpoint\"\n },\n {\n \"bodySize\": 7,\n \"headers\": {\n \"Upload-Offset\": \"0\"\n },\n \"operationId\": \"patchTusUpload\",\n \"response\": {\n \"headers\": {\n \"Upload-Offset\": \"7\"\n },\n \"statusCode\": 204\n },\n \"url\": \"upload\"\n }\n ],\n \"stateAfterAttempt\": \"failed\"\n },\n {\n \"attemptIndex\": 1,\n \"requests\": [\n {\n \"headers\": {},\n \"operationId\": \"getTusUploadOffset\",\n \"response\": {\n \"headers\": {\n \"Upload-Length\": \"14\",\n \"Upload-Offset\": \"7\"\n },\n \"statusCode\": 200\n },\n \"url\": \"upload\"\n },\n {\n \"bodySize\": 7,\n \"headers\": {\n \"Upload-Offset\": \"7\"\n },\n \"operationId\": \"patchTusUpload\",\n \"response\": {\n \"headers\": {\n \"Upload-Offset\": \"14\"\n },\n \"statusCode\": 204\n },\n \"url\": \"upload\"\n }\n ],\n \"stateAfterAttempt\": \"succeeded\"\n }\n ],\n \"cleanup\": {\n \"ownedSource\": \"remove-owned-source-after-success\",\n \"resumeUrl\": \"remove-after-success\"\n },\n \"input\": {\n \"chunkSize\": 7,\n \"content\": \"hello managed!\",\n \"fingerprint\": \"managed-durable-retry-fingerprint\",\n \"metadata\": {\n \"filename\": \"managed.txt\"\n },\n \"uploadPath\": \"managed-durable-retry\"\n },\n \"retryDelays\": [\n 0\n ],\n \"sourceDurability\": \"copy-to-owned-storage\",\n \"states\": [\n \"pending\",\n \"running\",\n \"failed\",\n \"running\",\n \"succeeded\"\n ],\n \"runtime\": \"java\",\n \"scheduler\": \"process-lifetime-worker-pool\",\n \"stateBackend\": \"filesystem\"\n },\n {\n \"attempts\": [\n {\n \"attemptIndex\": 0,\n \"failure\": {\n \"afterAcceptedOffset\": 7,\n \"kind\": \"io-error\"\n },\n \"requests\": [\n {\n \"bodySize\": 0,\n \"headers\": {\n \"Upload-Length\": \"14\"\n },\n \"operationId\": \"createTusUpload\",\n \"response\": {\n \"headers\": {\n \"Location\": \"https://tus.io/uploads/managed-durable-retry\"\n },\n \"statusCode\": 201\n },\n \"url\": \"endpoint\"\n },\n {\n \"bodySize\": 7,\n \"headers\": {\n \"Upload-Offset\": \"0\"\n },\n \"operationId\": \"patchTusUpload\",\n \"response\": {\n \"headers\": {\n \"Upload-Offset\": \"7\"\n },\n \"statusCode\": 204\n },\n \"url\": \"upload\"\n }\n ],\n \"stateAfterAttempt\": \"failed\"\n },\n {\n \"attemptIndex\": 1,\n \"requests\": [\n {\n \"headers\": {},\n \"operationId\": \"getTusUploadOffset\",\n \"response\": {\n \"headers\": {\n \"Upload-Length\": \"14\",\n \"Upload-Offset\": \"7\"\n },\n \"statusCode\": 200\n },\n \"url\": \"upload\"\n },\n {\n \"bodySize\": 7,\n \"headers\": {\n \"Upload-Offset\": \"7\"\n },\n \"operationId\": \"patchTusUpload\",\n \"response\": {\n \"headers\": {\n \"Upload-Offset\": \"14\"\n },\n \"statusCode\": 204\n },\n \"url\": \"upload\"\n }\n ],\n \"stateAfterAttempt\": \"succeeded\"\n }\n ],\n \"cleanup\": {\n \"ownedSource\": \"remove-owned-source-after-success\",\n \"resumeUrl\": \"remove-after-success\"\n },\n \"input\": {\n \"chunkSize\": 7,\n \"content\": \"hello managed!\",\n \"fingerprint\": \"managed-durable-retry-fingerprint\",\n \"metadata\": {\n \"filename\": \"managed.txt\"\n },\n \"uploadPath\": \"managed-durable-retry\"\n },\n \"retryDelays\": [\n 0\n ],\n \"sourceDurability\": \"copy-to-owned-storage\",\n \"states\": [\n \"pending\",\n \"running\",\n \"failed\",\n \"running\",\n \"succeeded\"\n ],\n \"runtime\": \"android\",\n \"scheduler\": \"durable-os-scheduler\",\n \"stateBackend\": \"platform-key-value-store\"\n }\n ],\n \"requiredPrimitives\": [\n \"accept-upload-submission\",\n \"make-source-durable\",\n \"schedule-upload-work\",\n \"run-protocol-upload\",\n \"apply-managed-retry-policy\",\n \"publish-upload-state\",\n \"cleanup-managed-upload\"\n ],\n \"scenarioId\": \"managedUploadDurableRetry\",\n \"summary\": \"Submit a durable source, survive scheduler/process interruption, resume by stored upload URL, and finish with cleanup.\"\n },\n {\n \"requiredPrimitives\": [\n \"accept-upload-submission\",\n \"make-source-durable\",\n \"schedule-upload-work\",\n \"classify-failure\",\n \"publish-upload-state\"\n ],\n \"scenarioId\": \"managedUploadPermanentFailure\",\n \"summary\": \"Classify missing sources and unretryable protocol failures as terminal without further retry.\"\n },\n {\n \"requiredPrimitives\": [\n \"accept-upload-submission\",\n \"schedule-upload-work\",\n \"publish-upload-state\"\n ],\n \"scenarioId\": \"managedUploadNetworkConstraint\",\n \"summary\": \"Honor network constraints before starting or resuming upload work.\"\n }\n ]\n}\n"; static final String[] MANAGED_UPLOAD_PRIMITIVES = new String[] { @@ -1158,6 +1158,10 @@ final class GeneratedTusProtocolContract { "managedUpload", "feature-over-protocol", "managedUploadDurableRetry", + new String[] { + "java", + "android", + }, new String[] { "accept-upload-submission", "make-source-durable", @@ -1184,6 +1188,7 @@ final class GeneratedTusProtocolContract { "managedUpload", "feature-over-protocol", "managedUploadPermanentFailure", + new String[0], new String[] { "accept-upload-submission", "make-source-durable", @@ -1208,6 +1213,7 @@ final class GeneratedTusProtocolContract { "managedUpload", "feature-over-protocol", "managedUploadNetworkConstraint", + new String[0], new String[] { "accept-upload-submission", "schedule-upload-work", @@ -1407,6 +1413,7 @@ static final class GeneratedTusManagedUploadProofCase { final String featureId; final String layer; final String scenarioId; + final String[] proofRuntimes; final String[] requiredPrimitives; final String[] protocolFeatureIds; final String[] runtimeProfiles; @@ -1415,12 +1422,14 @@ static final class GeneratedTusManagedUploadProofCase { String featureId, String layer, String scenarioId, + String[] proofRuntimes, String[] requiredPrimitives, String[] protocolFeatureIds, String[] runtimeProfiles) { this.featureId = featureId; this.layer = layer; this.scenarioId = scenarioId; + this.proofRuntimes = proofRuntimes; this.requiredPrimitives = requiredPrimitives; this.protocolFeatureIds = protocolFeatureIds; this.runtimeProfiles = runtimeProfiles; From 0f840a9d2880a5a4df3094c5a8c78376cc4620e0 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Mon, 1 Jun 2026 13:09:50 +0200 Subject: [PATCH 35/43] Add managed upload permanent failure proof --- .../client/GeneratedTusProtocolContract.java | 9 +- .../TestGeneratedTusManagedUploadRuntime.java | 230 ++++++++++++++++-- 2 files changed, 217 insertions(+), 22 deletions(-) diff --git a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java index 79f17bc7..40426201 100644 --- a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java +++ b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java @@ -1121,7 +1121,7 @@ final class GeneratedTusProtocolContract { ), }; - static final String MANAGED_UPLOAD_JSON = "{\n \"capabilities\": {\n \"cleanup\": {\n \"policies\": [\n \"remove-owned-source-after-success\",\n \"remove-owned-source-after-cancel\",\n \"retain-owned-source-after-permanent-failure\",\n \"retain-source-after-retryable-failure\",\n \"remove-managed-state-after-terminal-retention\"\n ]\n },\n \"failureClassification\": {\n \"permanentFailures\": [\n \"source-unavailable\",\n \"unretryable-protocol-error\",\n \"retry-policy-exhausted\"\n ],\n \"retryableFailures\": [\n \"retryable-protocol-error\",\n \"io-error\",\n \"network-unavailable\"\n ]\n },\n \"networkConstraints\": {\n \"options\": [\n \"any-network\",\n \"unmetered-network\"\n ]\n },\n \"retryPolicy\": {\n \"controls\": [\n \"max-attempts\",\n \"deadline\",\n \"progress-sensitive-budget\",\n \"unbounded-until-permanent-failure\"\n ],\n \"permanentFailure\": \"stop-without-retry\",\n \"progressReset\": \"reset-budget-after-accepted-offset-advances\"\n },\n \"scheduling\": {\n \"strategies\": [\n \"foreground-task\",\n \"process-lifetime-worker-pool\",\n \"durable-os-scheduler\"\n ]\n },\n \"sourceDurability\": {\n \"ownedCopyCleanup\": \"after-success-or-cancel\",\n \"strategies\": [\n \"copy-to-owned-storage\",\n \"reference-original-source\",\n \"memory-only\"\n ]\n },\n \"stateReporting\": {\n \"states\": [\n \"pending\",\n \"running\",\n \"succeeded\",\n \"failed\"\n ],\n \"terminalRetention\": \"session-and-next-launch\",\n \"transientRetention\": \"until-terminal\"\n }\n },\n \"conformance\": {\n \"scenarioIds\": [\n \"managedUploadDurableRetry\",\n \"managedUploadPermanentFailure\",\n \"managedUploadNetworkConstraint\"\n ],\n \"status\": \"needs-generated-scenario\"\n },\n \"description\": \"Submit upload work that can make sources durable, schedule/resume execution, retry, report state, and clean up while reusing the raw TUS protocol features underneath.\",\n \"featureId\": \"managedUpload\",\n \"flow\": [\n {\n \"kind\": \"managed-primitive\",\n \"primitive\": \"accept-upload-submission\",\n \"summary\": \"Accept source, metadata, headers, endpoint, and retry/scheduling policy.\"\n },\n {\n \"kind\": \"managed-primitive\",\n \"primitive\": \"make-source-durable\",\n \"summary\": \"Keep the source readable according to the selected runtime durability strategy.\"\n },\n {\n \"kind\": \"managed-primitive\",\n \"primitive\": \"schedule-upload-work\",\n \"summary\": \"Run upload work according to the runtime scheduler capability.\"\n },\n {\n \"featureId\": \"singleUploadLifecycle\",\n \"kind\": \"protocol-feature\",\n \"summary\": \"Use the raw protocol upload lifecycle for each execution attempt.\"\n },\n {\n \"featureId\": \"retryOffsetRecovery\",\n \"kind\": \"protocol-feature\",\n \"summary\": \"Use protocol retry and offset recovery before classifying terminal failure.\"\n },\n {\n \"kind\": \"managed-primitive\",\n \"primitive\": \"publish-upload-state\",\n \"summary\": \"Expose pending, running, succeeded, and failed state snapshots.\"\n },\n {\n \"kind\": \"managed-primitive\",\n \"primitive\": \"cleanup-managed-upload\",\n \"summary\": \"Remove owned sources and terminal state according to cleanup policy.\"\n }\n ],\n \"layer\": \"feature-over-protocol\",\n \"primitives\": [\n \"accept-upload-submission\",\n \"make-source-durable\",\n \"schedule-upload-work\",\n \"run-protocol-upload\",\n \"apply-managed-retry-policy\",\n \"classify-failure\",\n \"publish-upload-state\",\n \"cleanup-managed-upload\"\n ],\n \"protocolPrimitives\": [\n \"store-resume-url\",\n \"resume-from-previous-upload\",\n \"recover-offset-after-error\",\n \"retry-with-backoff\",\n \"emit-progress\",\n \"emit-chunk-complete\",\n \"terminate-upload\"\n ],\n \"runtimeProfiles\": [\n {\n \"networkConstraints\": [\n \"any-network\",\n \"unmetered-network\"\n ],\n \"runtime\": \"android\",\n \"scheduler\": \"durable-os-scheduler\",\n \"sourceDurability\": [\n \"copy-to-owned-storage\",\n \"reference-original-source\"\n ],\n \"stateBackend\": \"platform-key-value-store\"\n },\n {\n \"networkConstraints\": [\n \"any-network\",\n \"unmetered-network\"\n ],\n \"runtime\": \"ios\",\n \"scheduler\": \"durable-os-scheduler\",\n \"sourceDurability\": [\n \"copy-to-owned-storage\",\n \"reference-original-source\"\n ],\n \"stateBackend\": \"platform-key-value-store\"\n },\n {\n \"networkConstraints\": [\n \"any-network\"\n ],\n \"runtime\": \"browser\",\n \"scheduler\": \"foreground-task\",\n \"sourceDurability\": [\n \"reference-original-source\",\n \"memory-only\"\n ],\n \"stateBackend\": \"web-storage\"\n },\n {\n \"networkConstraints\": [\n \"any-network\"\n ],\n \"runtime\": \"java\",\n \"scheduler\": \"process-lifetime-worker-pool\",\n \"sourceDurability\": [\n \"copy-to-owned-storage\",\n \"reference-original-source\"\n ],\n \"stateBackend\": \"filesystem\"\n },\n {\n \"networkConstraints\": [\n \"any-network\"\n ],\n \"runtime\": \"node\",\n \"scheduler\": \"process-lifetime-worker-pool\",\n \"sourceDurability\": [\n \"copy-to-owned-storage\",\n \"reference-original-source\",\n \"memory-only\"\n ],\n \"stateBackend\": \"filesystem\"\n },\n {\n \"networkConstraints\": [\n \"any-network\"\n ],\n \"runtime\": \"react-native\",\n \"scheduler\": \"foreground-task\",\n \"sourceDurability\": [\n \"reference-original-source\",\n \"memory-only\"\n ],\n \"stateBackend\": \"platform-key-value-store\"\n }\n ],\n \"scenarios\": [\n {\n \"proofs\": [\n {\n \"attempts\": [\n {\n \"attemptIndex\": 0,\n \"failure\": {\n \"afterAcceptedOffset\": 7,\n \"kind\": \"io-error\"\n },\n \"requests\": [\n {\n \"bodySize\": 0,\n \"headers\": {\n \"Upload-Length\": \"14\"\n },\n \"operationId\": \"createTusUpload\",\n \"response\": {\n \"headers\": {\n \"Location\": \"https://tus.io/uploads/managed-durable-retry\"\n },\n \"statusCode\": 201\n },\n \"url\": \"endpoint\"\n },\n {\n \"bodySize\": 7,\n \"headers\": {\n \"Upload-Offset\": \"0\"\n },\n \"operationId\": \"patchTusUpload\",\n \"response\": {\n \"headers\": {\n \"Upload-Offset\": \"7\"\n },\n \"statusCode\": 204\n },\n \"url\": \"upload\"\n }\n ],\n \"stateAfterAttempt\": \"failed\"\n },\n {\n \"attemptIndex\": 1,\n \"requests\": [\n {\n \"headers\": {},\n \"operationId\": \"getTusUploadOffset\",\n \"response\": {\n \"headers\": {\n \"Upload-Length\": \"14\",\n \"Upload-Offset\": \"7\"\n },\n \"statusCode\": 200\n },\n \"url\": \"upload\"\n },\n {\n \"bodySize\": 7,\n \"headers\": {\n \"Upload-Offset\": \"7\"\n },\n \"operationId\": \"patchTusUpload\",\n \"response\": {\n \"headers\": {\n \"Upload-Offset\": \"14\"\n },\n \"statusCode\": 204\n },\n \"url\": \"upload\"\n }\n ],\n \"stateAfterAttempt\": \"succeeded\"\n }\n ],\n \"cleanup\": {\n \"ownedSource\": \"remove-owned-source-after-success\",\n \"resumeUrl\": \"remove-after-success\"\n },\n \"input\": {\n \"chunkSize\": 7,\n \"content\": \"hello managed!\",\n \"fingerprint\": \"managed-durable-retry-fingerprint\",\n \"metadata\": {\n \"filename\": \"managed.txt\"\n },\n \"uploadPath\": \"managed-durable-retry\"\n },\n \"retryDelays\": [\n 0\n ],\n \"sourceDurability\": \"copy-to-owned-storage\",\n \"states\": [\n \"pending\",\n \"running\",\n \"failed\",\n \"running\",\n \"succeeded\"\n ],\n \"runtime\": \"java\",\n \"scheduler\": \"process-lifetime-worker-pool\",\n \"stateBackend\": \"filesystem\"\n },\n {\n \"attempts\": [\n {\n \"attemptIndex\": 0,\n \"failure\": {\n \"afterAcceptedOffset\": 7,\n \"kind\": \"io-error\"\n },\n \"requests\": [\n {\n \"bodySize\": 0,\n \"headers\": {\n \"Upload-Length\": \"14\"\n },\n \"operationId\": \"createTusUpload\",\n \"response\": {\n \"headers\": {\n \"Location\": \"https://tus.io/uploads/managed-durable-retry\"\n },\n \"statusCode\": 201\n },\n \"url\": \"endpoint\"\n },\n {\n \"bodySize\": 7,\n \"headers\": {\n \"Upload-Offset\": \"0\"\n },\n \"operationId\": \"patchTusUpload\",\n \"response\": {\n \"headers\": {\n \"Upload-Offset\": \"7\"\n },\n \"statusCode\": 204\n },\n \"url\": \"upload\"\n }\n ],\n \"stateAfterAttempt\": \"failed\"\n },\n {\n \"attemptIndex\": 1,\n \"requests\": [\n {\n \"headers\": {},\n \"operationId\": \"getTusUploadOffset\",\n \"response\": {\n \"headers\": {\n \"Upload-Length\": \"14\",\n \"Upload-Offset\": \"7\"\n },\n \"statusCode\": 200\n },\n \"url\": \"upload\"\n },\n {\n \"bodySize\": 7,\n \"headers\": {\n \"Upload-Offset\": \"7\"\n },\n \"operationId\": \"patchTusUpload\",\n \"response\": {\n \"headers\": {\n \"Upload-Offset\": \"14\"\n },\n \"statusCode\": 204\n },\n \"url\": \"upload\"\n }\n ],\n \"stateAfterAttempt\": \"succeeded\"\n }\n ],\n \"cleanup\": {\n \"ownedSource\": \"remove-owned-source-after-success\",\n \"resumeUrl\": \"remove-after-success\"\n },\n \"input\": {\n \"chunkSize\": 7,\n \"content\": \"hello managed!\",\n \"fingerprint\": \"managed-durable-retry-fingerprint\",\n \"metadata\": {\n \"filename\": \"managed.txt\"\n },\n \"uploadPath\": \"managed-durable-retry\"\n },\n \"retryDelays\": [\n 0\n ],\n \"sourceDurability\": \"copy-to-owned-storage\",\n \"states\": [\n \"pending\",\n \"running\",\n \"failed\",\n \"running\",\n \"succeeded\"\n ],\n \"runtime\": \"android\",\n \"scheduler\": \"durable-os-scheduler\",\n \"stateBackend\": \"platform-key-value-store\"\n }\n ],\n \"requiredPrimitives\": [\n \"accept-upload-submission\",\n \"make-source-durable\",\n \"schedule-upload-work\",\n \"run-protocol-upload\",\n \"apply-managed-retry-policy\",\n \"publish-upload-state\",\n \"cleanup-managed-upload\"\n ],\n \"scenarioId\": \"managedUploadDurableRetry\",\n \"summary\": \"Submit a durable source, survive scheduler/process interruption, resume by stored upload URL, and finish with cleanup.\"\n },\n {\n \"requiredPrimitives\": [\n \"accept-upload-submission\",\n \"make-source-durable\",\n \"schedule-upload-work\",\n \"classify-failure\",\n \"publish-upload-state\"\n ],\n \"scenarioId\": \"managedUploadPermanentFailure\",\n \"summary\": \"Classify missing sources and unretryable protocol failures as terminal without further retry.\"\n },\n {\n \"requiredPrimitives\": [\n \"accept-upload-submission\",\n \"schedule-upload-work\",\n \"publish-upload-state\"\n ],\n \"scenarioId\": \"managedUploadNetworkConstraint\",\n \"summary\": \"Honor network constraints before starting or resuming upload work.\"\n }\n ]\n}\n"; + static final String MANAGED_UPLOAD_JSON = "{\n \"capabilities\": {\n \"cleanup\": {\n \"policies\": [\n \"remove-owned-source-after-success\",\n \"remove-owned-source-after-cancel\",\n \"retain-owned-source-after-permanent-failure\",\n \"retain-source-after-retryable-failure\",\n \"remove-managed-state-after-terminal-retention\"\n ]\n },\n \"failureClassification\": {\n \"permanentFailures\": [\n \"source-unavailable\",\n \"unretryable-protocol-error\",\n \"retry-policy-exhausted\"\n ],\n \"retryableFailures\": [\n \"retryable-protocol-error\",\n \"io-error\",\n \"network-unavailable\"\n ]\n },\n \"networkConstraints\": {\n \"options\": [\n \"any-network\",\n \"unmetered-network\"\n ]\n },\n \"retryPolicy\": {\n \"controls\": [\n \"max-attempts\",\n \"deadline\",\n \"progress-sensitive-budget\",\n \"unbounded-until-permanent-failure\"\n ],\n \"permanentFailure\": \"stop-without-retry\",\n \"progressReset\": \"reset-budget-after-accepted-offset-advances\"\n },\n \"scheduling\": {\n \"strategies\": [\n \"foreground-task\",\n \"process-lifetime-worker-pool\",\n \"durable-os-scheduler\"\n ]\n },\n \"sourceDurability\": {\n \"ownedCopyCleanup\": \"after-success-or-cancel\",\n \"strategies\": [\n \"copy-to-owned-storage\",\n \"reference-original-source\",\n \"memory-only\"\n ]\n },\n \"stateReporting\": {\n \"states\": [\n \"pending\",\n \"running\",\n \"succeeded\",\n \"failed\"\n ],\n \"terminalRetention\": \"session-and-next-launch\",\n \"transientRetention\": \"until-terminal\"\n }\n },\n \"conformance\": {\n \"scenarioIds\": [\n \"managedUploadDurableRetry\",\n \"managedUploadPermanentFailure\",\n \"managedUploadNetworkConstraint\"\n ],\n \"status\": \"needs-generated-scenario\"\n },\n \"description\": \"Submit upload work that can make sources durable, schedule/resume execution, retry, report state, and clean up while reusing the raw TUS protocol features underneath.\",\n \"featureId\": \"managedUpload\",\n \"flow\": [\n {\n \"kind\": \"managed-primitive\",\n \"primitive\": \"accept-upload-submission\",\n \"summary\": \"Accept source, metadata, headers, endpoint, and retry/scheduling policy.\"\n },\n {\n \"kind\": \"managed-primitive\",\n \"primitive\": \"make-source-durable\",\n \"summary\": \"Keep the source readable according to the selected runtime durability strategy.\"\n },\n {\n \"kind\": \"managed-primitive\",\n \"primitive\": \"schedule-upload-work\",\n \"summary\": \"Run upload work according to the runtime scheduler capability.\"\n },\n {\n \"featureId\": \"singleUploadLifecycle\",\n \"kind\": \"protocol-feature\",\n \"summary\": \"Use the raw protocol upload lifecycle for each execution attempt.\"\n },\n {\n \"featureId\": \"retryOffsetRecovery\",\n \"kind\": \"protocol-feature\",\n \"summary\": \"Use protocol retry and offset recovery before classifying terminal failure.\"\n },\n {\n \"kind\": \"managed-primitive\",\n \"primitive\": \"publish-upload-state\",\n \"summary\": \"Expose pending, running, succeeded, and failed state snapshots.\"\n },\n {\n \"kind\": \"managed-primitive\",\n \"primitive\": \"cleanup-managed-upload\",\n \"summary\": \"Remove owned sources and terminal state according to cleanup policy.\"\n }\n ],\n \"layer\": \"feature-over-protocol\",\n \"primitives\": [\n \"accept-upload-submission\",\n \"make-source-durable\",\n \"schedule-upload-work\",\n \"run-protocol-upload\",\n \"apply-managed-retry-policy\",\n \"classify-failure\",\n \"publish-upload-state\",\n \"cleanup-managed-upload\"\n ],\n \"protocolPrimitives\": [\n \"store-resume-url\",\n \"resume-from-previous-upload\",\n \"recover-offset-after-error\",\n \"retry-with-backoff\",\n \"emit-progress\",\n \"emit-chunk-complete\",\n \"terminate-upload\"\n ],\n \"runtimeProfiles\": [\n {\n \"networkConstraints\": [\n \"any-network\",\n \"unmetered-network\"\n ],\n \"runtime\": \"android\",\n \"scheduler\": \"durable-os-scheduler\",\n \"sourceDurability\": [\n \"copy-to-owned-storage\",\n \"reference-original-source\"\n ],\n \"stateBackend\": \"platform-key-value-store\"\n },\n {\n \"networkConstraints\": [\n \"any-network\",\n \"unmetered-network\"\n ],\n \"runtime\": \"ios\",\n \"scheduler\": \"durable-os-scheduler\",\n \"sourceDurability\": [\n \"copy-to-owned-storage\",\n \"reference-original-source\"\n ],\n \"stateBackend\": \"platform-key-value-store\"\n },\n {\n \"networkConstraints\": [\n \"any-network\"\n ],\n \"runtime\": \"browser\",\n \"scheduler\": \"foreground-task\",\n \"sourceDurability\": [\n \"reference-original-source\",\n \"memory-only\"\n ],\n \"stateBackend\": \"web-storage\"\n },\n {\n \"networkConstraints\": [\n \"any-network\"\n ],\n \"runtime\": \"java\",\n \"scheduler\": \"process-lifetime-worker-pool\",\n \"sourceDurability\": [\n \"copy-to-owned-storage\",\n \"reference-original-source\"\n ],\n \"stateBackend\": \"filesystem\"\n },\n {\n \"networkConstraints\": [\n \"any-network\"\n ],\n \"runtime\": \"node\",\n \"scheduler\": \"process-lifetime-worker-pool\",\n \"sourceDurability\": [\n \"copy-to-owned-storage\",\n \"reference-original-source\",\n \"memory-only\"\n ],\n \"stateBackend\": \"filesystem\"\n },\n {\n \"networkConstraints\": [\n \"any-network\"\n ],\n \"runtime\": \"react-native\",\n \"scheduler\": \"foreground-task\",\n \"sourceDurability\": [\n \"reference-original-source\",\n \"memory-only\"\n ],\n \"stateBackend\": \"platform-key-value-store\"\n }\n ],\n \"scenarios\": [\n {\n \"proofs\": [\n {\n \"attempts\": [\n {\n \"attemptIndex\": 0,\n \"failure\": {\n \"afterAcceptedOffset\": 7,\n \"kind\": \"io-error\",\n \"phase\": \"after-accepted-offset\"\n },\n \"requests\": [\n {\n \"bodySize\": 0,\n \"headers\": {\n \"Upload-Length\": \"14\"\n },\n \"operationId\": \"createTusUpload\",\n \"response\": {\n \"headers\": {\n \"Location\": \"https://tus.io/uploads/managed-durable-retry\"\n },\n \"statusCode\": 201\n },\n \"url\": \"endpoint\"\n },\n {\n \"bodySize\": 7,\n \"headers\": {\n \"Upload-Offset\": \"0\"\n },\n \"operationId\": \"patchTusUpload\",\n \"response\": {\n \"headers\": {\n \"Upload-Offset\": \"7\"\n },\n \"statusCode\": 204\n },\n \"url\": \"upload\"\n }\n ],\n \"stateAfterAttempt\": \"failed\"\n },\n {\n \"attemptIndex\": 1,\n \"requests\": [\n {\n \"headers\": {},\n \"operationId\": \"getTusUploadOffset\",\n \"response\": {\n \"headers\": {\n \"Upload-Length\": \"14\",\n \"Upload-Offset\": \"7\"\n },\n \"statusCode\": 200\n },\n \"url\": \"upload\"\n },\n {\n \"bodySize\": 7,\n \"headers\": {\n \"Upload-Offset\": \"7\"\n },\n \"operationId\": \"patchTusUpload\",\n \"response\": {\n \"headers\": {\n \"Upload-Offset\": \"14\"\n },\n \"statusCode\": 204\n },\n \"url\": \"upload\"\n }\n ],\n \"stateAfterAttempt\": \"succeeded\"\n }\n ],\n \"cleanup\": {\n \"ownedSource\": \"remove-owned-source-after-success\",\n \"resumeUrl\": \"remove-after-success\"\n },\n \"input\": {\n \"chunkSize\": 7,\n \"content\": \"hello managed!\",\n \"fingerprint\": \"managed-durable-retry-fingerprint\",\n \"metadata\": {\n \"filename\": \"managed.txt\"\n },\n \"uploadPath\": \"managed-durable-retry\"\n },\n \"retryDelays\": [\n 0\n ],\n \"sourceDurability\": \"copy-to-owned-storage\",\n \"states\": [\n \"pending\",\n \"running\",\n \"failed\",\n \"running\",\n \"succeeded\"\n ],\n \"terminal\": {\n \"state\": \"succeeded\"\n },\n \"runtime\": \"java\",\n \"scheduler\": \"process-lifetime-worker-pool\",\n \"stateBackend\": \"filesystem\"\n },\n {\n \"attempts\": [\n {\n \"attemptIndex\": 0,\n \"failure\": {\n \"afterAcceptedOffset\": 7,\n \"kind\": \"io-error\",\n \"phase\": \"after-accepted-offset\"\n },\n \"requests\": [\n {\n \"bodySize\": 0,\n \"headers\": {\n \"Upload-Length\": \"14\"\n },\n \"operationId\": \"createTusUpload\",\n \"response\": {\n \"headers\": {\n \"Location\": \"https://tus.io/uploads/managed-durable-retry\"\n },\n \"statusCode\": 201\n },\n \"url\": \"endpoint\"\n },\n {\n \"bodySize\": 7,\n \"headers\": {\n \"Upload-Offset\": \"0\"\n },\n \"operationId\": \"patchTusUpload\",\n \"response\": {\n \"headers\": {\n \"Upload-Offset\": \"7\"\n },\n \"statusCode\": 204\n },\n \"url\": \"upload\"\n }\n ],\n \"stateAfterAttempt\": \"failed\"\n },\n {\n \"attemptIndex\": 1,\n \"requests\": [\n {\n \"headers\": {},\n \"operationId\": \"getTusUploadOffset\",\n \"response\": {\n \"headers\": {\n \"Upload-Length\": \"14\",\n \"Upload-Offset\": \"7\"\n },\n \"statusCode\": 200\n },\n \"url\": \"upload\"\n },\n {\n \"bodySize\": 7,\n \"headers\": {\n \"Upload-Offset\": \"7\"\n },\n \"operationId\": \"patchTusUpload\",\n \"response\": {\n \"headers\": {\n \"Upload-Offset\": \"14\"\n },\n \"statusCode\": 204\n },\n \"url\": \"upload\"\n }\n ],\n \"stateAfterAttempt\": \"succeeded\"\n }\n ],\n \"cleanup\": {\n \"ownedSource\": \"remove-owned-source-after-success\",\n \"resumeUrl\": \"remove-after-success\"\n },\n \"input\": {\n \"chunkSize\": 7,\n \"content\": \"hello managed!\",\n \"fingerprint\": \"managed-durable-retry-fingerprint\",\n \"metadata\": {\n \"filename\": \"managed.txt\"\n },\n \"uploadPath\": \"managed-durable-retry\"\n },\n \"retryDelays\": [\n 0\n ],\n \"sourceDurability\": \"copy-to-owned-storage\",\n \"states\": [\n \"pending\",\n \"running\",\n \"failed\",\n \"running\",\n \"succeeded\"\n ],\n \"terminal\": {\n \"state\": \"succeeded\"\n },\n \"runtime\": \"android\",\n \"scheduler\": \"durable-os-scheduler\",\n \"stateBackend\": \"platform-key-value-store\"\n }\n ],\n \"requiredPrimitives\": [\n \"accept-upload-submission\",\n \"make-source-durable\",\n \"schedule-upload-work\",\n \"run-protocol-upload\",\n \"apply-managed-retry-policy\",\n \"publish-upload-state\",\n \"cleanup-managed-upload\"\n ],\n \"scenarioId\": \"managedUploadDurableRetry\",\n \"summary\": \"Submit a durable source, survive scheduler/process interruption, resume by stored upload URL, and finish with cleanup.\"\n },\n {\n \"proofs\": [\n {\n \"attempts\": [\n {\n \"attemptIndex\": 0,\n \"failure\": {\n \"kind\": \"unretryable-protocol-error\",\n \"phase\": \"during-protocol-request\"\n },\n \"requests\": [\n {\n \"bodySize\": 0,\n \"headers\": {\n \"Upload-Length\": \"14\"\n },\n \"operationId\": \"createTusUpload\",\n \"response\": {\n \"headers\": {},\n \"statusCode\": 400\n },\n \"url\": \"endpoint\"\n }\n ],\n \"stateAfterAttempt\": \"failed\"\n }\n ],\n \"cleanup\": {\n \"ownedSource\": \"retain-owned-source-after-permanent-failure\",\n \"resumeUrl\": \"absent-after-permanent-failure\"\n },\n \"input\": {\n \"chunkSize\": 7,\n \"content\": \"hello failure!\",\n \"fingerprint\": \"managed-permanent-failure-fingerprint\",\n \"metadata\": {\n \"filename\": \"managed-permanent-failure.txt\"\n },\n \"uploadPath\": \"managed-permanent-failure\"\n },\n \"retryDelays\": [],\n \"sourceDurability\": \"copy-to-owned-storage\",\n \"states\": [\n \"pending\",\n \"running\",\n \"failed\"\n ],\n \"terminal\": {\n \"failure\": \"unretryable-protocol-error\",\n \"state\": \"failed\"\n },\n \"runtime\": \"java\",\n \"scheduler\": \"process-lifetime-worker-pool\",\n \"stateBackend\": \"filesystem\"\n },\n {\n \"attempts\": [\n {\n \"attemptIndex\": 0,\n \"failure\": {\n \"kind\": \"unretryable-protocol-error\",\n \"phase\": \"during-protocol-request\"\n },\n \"requests\": [\n {\n \"bodySize\": 0,\n \"headers\": {\n \"Upload-Length\": \"14\"\n },\n \"operationId\": \"createTusUpload\",\n \"response\": {\n \"headers\": {},\n \"statusCode\": 400\n },\n \"url\": \"endpoint\"\n }\n ],\n \"stateAfterAttempt\": \"failed\"\n }\n ],\n \"cleanup\": {\n \"ownedSource\": \"retain-owned-source-after-permanent-failure\",\n \"resumeUrl\": \"absent-after-permanent-failure\"\n },\n \"input\": {\n \"chunkSize\": 7,\n \"content\": \"hello failure!\",\n \"fingerprint\": \"managed-permanent-failure-fingerprint\",\n \"metadata\": {\n \"filename\": \"managed-permanent-failure.txt\"\n },\n \"uploadPath\": \"managed-permanent-failure\"\n },\n \"retryDelays\": [],\n \"sourceDurability\": \"copy-to-owned-storage\",\n \"states\": [\n \"pending\",\n \"running\",\n \"failed\"\n ],\n \"terminal\": {\n \"failure\": \"unretryable-protocol-error\",\n \"state\": \"failed\"\n },\n \"runtime\": \"android\",\n \"scheduler\": \"durable-os-scheduler\",\n \"stateBackend\": \"platform-key-value-store\"\n }\n ],\n \"requiredPrimitives\": [\n \"accept-upload-submission\",\n \"make-source-durable\",\n \"schedule-upload-work\",\n \"run-protocol-upload\",\n \"classify-failure\",\n \"publish-upload-state\",\n \"cleanup-managed-upload\"\n ],\n \"scenarioId\": \"managedUploadPermanentFailure\",\n \"summary\": \"Classify missing sources and unretryable protocol failures as terminal without further retry.\"\n },\n {\n \"requiredPrimitives\": [\n \"accept-upload-submission\",\n \"schedule-upload-work\",\n \"publish-upload-state\"\n ],\n \"scenarioId\": \"managedUploadNetworkConstraint\",\n \"summary\": \"Honor network constraints before starting or resuming upload work.\"\n }\n ]\n}\n"; static final String[] MANAGED_UPLOAD_PRIMITIVES = new String[] { @@ -1188,13 +1188,18 @@ final class GeneratedTusProtocolContract { "managedUpload", "feature-over-protocol", "managedUploadPermanentFailure", - new String[0], + new String[] { + "java", + "android", + }, new String[] { "accept-upload-submission", "make-source-durable", "schedule-upload-work", + "run-protocol-upload", "classify-failure", "publish-upload-state", + "cleanup-managed-upload", }, new String[] { "singleUploadLifecycle", diff --git a/src/test/java/io/tus/java/client/TestGeneratedTusManagedUploadRuntime.java b/src/test/java/io/tus/java/client/TestGeneratedTusManagedUploadRuntime.java index 41b9b36b..102dc411 100644 --- a/src/test/java/io/tus/java/client/TestGeneratedTusManagedUploadRuntime.java +++ b/src/test/java/io/tus/java/client/TestGeneratedTusManagedUploadRuntime.java @@ -17,6 +17,7 @@ import java.util.List; import java.util.Map; import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; @@ -47,6 +48,10 @@ public class TestGeneratedTusManagedUploadRuntime extends MockServerProvider { new GeneratedTusManagedUploadTransport( "Location" ), + new GeneratedTusManagedUploadTerminal( + "succeeded", + "" + ), new GeneratedTusManagedUploadCleanup( "remove-owned-source-after-success", "remove-after-success" @@ -80,6 +85,7 @@ public class TestGeneratedTusManagedUploadRuntime extends MockServerProvider { 0, "failed", new GeneratedTusManagedUploadFailure( + "after-accepted-offset", "io-error", 7 ), @@ -166,6 +172,72 @@ public class TestGeneratedTusManagedUploadRuntime extends MockServerProvider { ), } ), + new GeneratedTusManagedUploadRuntimeCase( + "managedUploadPermanentFailure", + new GeneratedTusManagedUploadRuntimeProfile( + "java", + "process-lifetime-worker-pool", + "copy-to-owned-storage", + "filesystem" + ), + new GeneratedTusManagedUploadTransport( + "Location" + ), + new GeneratedTusManagedUploadTerminal( + "failed", + "unretryable-protocol-error" + ), + new GeneratedTusManagedUploadCleanup( + "retain-owned-source-after-permanent-failure", + "absent-after-permanent-failure" + ), + new GeneratedTusManagedUploadRetryPlan( + new String[] { + "pending", + "running", + "failed", + }, + new int[0] + ), + new GeneratedTusManagedUploadInput( + "hello failure!", + 7, + "managed-permanent-failure-fingerprint", + "managed-permanent-failure", + new GeneratedTusManagedUploadMetadata[] { + new GeneratedTusManagedUploadMetadata( + "filename", + "managed-permanent-failure.txt" + ), + } + ), + new GeneratedTusManagedUploadAttempt[] { + new GeneratedTusManagedUploadAttempt( + 0, + "failed", + new GeneratedTusManagedUploadFailure( + "during-protocol-request", + "unretryable-protocol-error", + -1 + ), + new GeneratedTusManagedUploadRequest[] { + new GeneratedTusManagedUploadRequest( + "POST", + "endpoint", + 0, + 400, + new GeneratedTusManagedUploadHeader[] { + new GeneratedTusManagedUploadHeader( + "Upload-Length", + "14" + ), + }, + new GeneratedTusManagedUploadHeader[0] + ), + } + ), + } + ), }; private static final GeneratedTusMethodOverride[] METHOD_OVERRIDES = new GeneratedTusMethodOverride[] { @@ -208,12 +280,12 @@ public Boolean call() throws Exception { return executor.makeAttempts(); } }); - assertTrue(testCase.scenarioId, future.get()); + assertTerminalResult(testCase, future); } finally { worker.shutdownNow(); } - cleanupAfterSuccess(testCase, ownedSource); + cleanupAfterTerminalState(testCase, ownedSource); assertArrayEquals( testCase.scenarioId, @@ -224,14 +296,55 @@ public Boolean call() throws Exception { testCase.expectedStates, Files.readAllLines(stateFile.toPath(), StandardCharsets.UTF_8) .toArray(new String[testCase.expectedStates.length])); - assertNull(testCase.scenarioId, urlStore.get(testCase.input.fingerprint)); - assertFalse(testCase.scenarioId, ownedSource.exists()); + assertResumeUrlState(testCase, urlStore); + assertOwnedSourceState(testCase, ownedSource); assertTrue(testCase.scenarioId, source.exists()); source.delete(); stateFile.delete(); } } + private void assertTerminalResult( + GeneratedTusManagedUploadRuntimeCase testCase, + Future future) throws Exception { + try { + boolean result = future.get(); + if (!"succeeded".equals(testCase.terminalState)) { + throw new AssertionError(testCase.scenarioId + " expected terminal failure"); + } + assertTrue(testCase.scenarioId, result); + } catch (ExecutionException error) { + if (!"failed".equals(testCase.terminalState)) { + throw error; + } + assertTerminalFailure(testCase, error.getCause()); + } + } + + private void assertTerminalFailure( + GeneratedTusManagedUploadRuntimeCase testCase, + Throwable error) { + if ("unretryable-protocol-error".equals(testCase.terminalFailure)) { + assertTrue(testCase.scenarioId, error instanceof ProtocolException); + return; + } + if ("source-unavailable".equals(testCase.terminalFailure)) { + assertTrue(testCase.scenarioId, error instanceof IOException); + return; + } + if ("retry-policy-exhausted".equals(testCase.terminalFailure)) { + assertTrue( + testCase.scenarioId, + error instanceof ProtocolException || error instanceof IOException); + return; + } + + throw new AssertionError( + testCase.scenarioId + + " uses unsupported generated terminal failure " + + testCase.terminalFailure); + } + private TusExecutor managedExecutorFor( final GeneratedTusManagedUploadRuntimeCase testCase, final TusClient client, @@ -247,28 +360,53 @@ protected void makeAttempt() throws ProtocolException, IOException { attemptIndex += 1; recordState(testCase, states, stateFile, "running"); - TusUpload upload = uploadFor(testCase, ownedSource); - TusUploader uploader = client.resumeOrCreateUpload(upload); - uploader.setChunkSize(testCase.input.chunkSize); - uploader.setRequestPayloadSize(testCase.input.chunkSize); - while (uploader.getOffset() < upload.getSize()) { - uploader.uploadChunk(); - if ( - attempt.failure != null - && uploader.getOffset() == attempt.failure.afterAcceptedOffset) { - uploader.finish(false); - recordState(testCase, states, stateFile, attempt.stateAfterAttempt); - throw new IOException(attempt.failure.kind); + try { + TusUpload upload = uploadFor(testCase, ownedSource); + TusUploader uploader = client.resumeOrCreateUpload(upload); + uploader.setChunkSize(testCase.input.chunkSize); + uploader.setRequestPayloadSize(testCase.input.chunkSize); + while (uploader.getOffset() < upload.getSize()) { + uploader.uploadChunk(); + if ( + isAfterAcceptedOffsetFailure(attempt) + && uploader.getOffset() == attempt.failure.afterAcceptedOffset) { + uploader.finish(false); + recordState(testCase, states, stateFile, attempt.stateAfterAttempt); + throw new IOException(attempt.failure.kind); + } } + uploader.finish(); + recordState(testCase, states, stateFile, attempt.stateAfterAttempt); + } catch (ProtocolException error) { + recordDuringProtocolFailure(testCase, states, stateFile, attempt); + throw error; + } catch (IOException error) { + recordDuringProtocolFailure(testCase, states, stateFile, attempt); + throw error; } - uploader.finish(); - recordState(testCase, states, stateFile, attempt.stateAfterAttempt); } }; executor.setDelays(testCase.retryDelays); return executor; } + private boolean isAfterAcceptedOffsetFailure(GeneratedTusManagedUploadAttempt attempt) { + return attempt.failure != null + && "after-accepted-offset".equals(attempt.failure.phase); + } + + private void recordDuringProtocolFailure( + GeneratedTusManagedUploadRuntimeCase testCase, + List states, + File stateFile, + GeneratedTusManagedUploadAttempt attempt) throws IOException { + if (attempt.failure == null || !"during-protocol-request".equals(attempt.failure.phase)) { + return; + } + + recordState(testCase, states, stateFile, attempt.stateAfterAttempt); + } + private TusUpload uploadFor( GeneratedTusManagedUploadRuntimeCase testCase, File ownedSource) throws IOException { @@ -301,7 +439,7 @@ private void copyDurableSource( assertTrue(testCase.scenarioId, ownedSource.exists()); } - private void cleanupAfterSuccess( + private void cleanupAfterTerminalState( GeneratedTusManagedUploadRuntimeCase testCase, File ownedSource) throws IOException { if (!"remove-owned-source-after-success".equals(testCase.ownedSourceCleanup)) { @@ -311,6 +449,41 @@ private void cleanupAfterSuccess( Files.deleteIfExists(ownedSource.toPath()); } + private void assertOwnedSourceState( + GeneratedTusManagedUploadRuntimeCase testCase, + File ownedSource) { + if ("remove-owned-source-after-success".equals(testCase.ownedSourceCleanup)) { + assertFalse(testCase.scenarioId, ownedSource.exists()); + return; + } + if ("retain-owned-source-after-permanent-failure".equals(testCase.ownedSourceCleanup)) { + assertTrue(testCase.scenarioId, ownedSource.exists()); + ownedSource.delete(); + return; + } + + throw new AssertionError( + testCase.scenarioId + + " uses unsupported generated owned-source cleanup " + + testCase.ownedSourceCleanup); + } + + private void assertResumeUrlState( + GeneratedTusManagedUploadRuntimeCase testCase, + GeneratedTusManagedUploadUrlStore urlStore) { + if ( + "remove-after-success".equals(testCase.resumeUrlCleanup) + || "absent-after-permanent-failure".equals(testCase.resumeUrlCleanup)) { + assertNull(testCase.scenarioId, urlStore.get(testCase.input.fingerprint)); + return; + } + + throw new AssertionError( + testCase.scenarioId + + " uses unsupported generated resume URL cleanup " + + testCase.resumeUrlCleanup); + } + private void recordState( GeneratedTusManagedUploadRuntimeCase testCase, List states, @@ -440,6 +613,8 @@ private static final class GeneratedTusManagedUploadRuntimeCase { final String sourceDurability; final String stateBackend; final String locationHeaderName; + final String terminalState; + final String terminalFailure; final String ownedSourceCleanup; final String resumeUrlCleanup; final String[] expectedStates; @@ -452,6 +627,7 @@ private static final class GeneratedTusManagedUploadRuntimeCase { String scenarioId, GeneratedTusManagedUploadRuntimeProfile profile, GeneratedTusManagedUploadTransport transport, + GeneratedTusManagedUploadTerminal terminal, GeneratedTusManagedUploadCleanup cleanup, GeneratedTusManagedUploadRetryPlan retryPlan, GeneratedTusManagedUploadInput input, @@ -462,6 +638,8 @@ private static final class GeneratedTusManagedUploadRuntimeCase { this.sourceDurability = profile.sourceDurability; this.stateBackend = profile.stateBackend; this.locationHeaderName = transport.locationHeaderName; + this.terminalState = terminal.state; + this.terminalFailure = terminal.failure; this.ownedSourceCleanup = cleanup.ownedSource; this.resumeUrlCleanup = cleanup.resumeUrl; this.expectedStates = retryPlan.expectedStates; @@ -472,6 +650,16 @@ private static final class GeneratedTusManagedUploadRuntimeCase { } } + private static final class GeneratedTusManagedUploadTerminal { + final String state; + final String failure; + + GeneratedTusManagedUploadTerminal(String state, String failure) { + this.state = state; + this.failure = failure; + } + } + private static final class GeneratedTusManagedUploadRuntimeProfile { final String runtime; final String scheduler; @@ -558,10 +746,12 @@ private static final class GeneratedTusManagedUploadAttempt { } private static final class GeneratedTusManagedUploadFailure { + final String phase; final String kind; final long afterAcceptedOffset; - GeneratedTusManagedUploadFailure(String kind, long afterAcceptedOffset) { + GeneratedTusManagedUploadFailure(String phase, String kind, long afterAcceptedOffset) { + this.phase = phase; this.kind = kind; this.afterAcceptedOffset = afterAcceptedOffset; } From cf572c4e7aa75118ae87c3979442402f893d4c42 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Mon, 1 Jun 2026 13:20:44 +0200 Subject: [PATCH 36/43] Respect managed upload fixture lint --- .../client/TestGeneratedTusManagedUploadRuntime.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/test/java/io/tus/java/client/TestGeneratedTusManagedUploadRuntime.java b/src/test/java/io/tus/java/client/TestGeneratedTusManagedUploadRuntime.java index 102dc411..aaf02bfa 100644 --- a/src/test/java/io/tus/java/client/TestGeneratedTusManagedUploadRuntime.java +++ b/src/test/java/io/tus/java/client/TestGeneratedTusManagedUploadRuntime.java @@ -38,8 +38,8 @@ public class TestGeneratedTusManagedUploadRuntime extends MockServerProvider { private static final GeneratedTusManagedUploadRuntimeCase[] CASES = new GeneratedTusManagedUploadRuntimeCase[] { new GeneratedTusManagedUploadRuntimeCase( - "managedUploadDurableRetry", new GeneratedTusManagedUploadRuntimeProfile( + "managedUploadDurableRetry", "java", "process-lifetime-worker-pool", "copy-to-owned-storage", @@ -173,8 +173,8 @@ public class TestGeneratedTusManagedUploadRuntime extends MockServerProvider { } ), new GeneratedTusManagedUploadRuntimeCase( - "managedUploadPermanentFailure", new GeneratedTusManagedUploadRuntimeProfile( + "managedUploadPermanentFailure", "java", "process-lifetime-worker-pool", "copy-to-owned-storage", @@ -624,7 +624,6 @@ private static final class GeneratedTusManagedUploadRuntimeCase { final GeneratedTusManagedUploadAttempt[] attempts; GeneratedTusManagedUploadRuntimeCase( - String scenarioId, GeneratedTusManagedUploadRuntimeProfile profile, GeneratedTusManagedUploadTransport transport, GeneratedTusManagedUploadTerminal terminal, @@ -632,7 +631,7 @@ private static final class GeneratedTusManagedUploadRuntimeCase { GeneratedTusManagedUploadRetryPlan retryPlan, GeneratedTusManagedUploadInput input, GeneratedTusManagedUploadAttempt[] attempts) { - this.scenarioId = scenarioId; + this.scenarioId = profile.scenarioId; this.runtime = profile.runtime; this.scheduler = profile.scheduler; this.sourceDurability = profile.sourceDurability; @@ -661,16 +660,19 @@ private static final class GeneratedTusManagedUploadTerminal { } private static final class GeneratedTusManagedUploadRuntimeProfile { + final String scenarioId; final String runtime; final String scheduler; final String sourceDurability; final String stateBackend; GeneratedTusManagedUploadRuntimeProfile( + String scenarioId, String runtime, String scheduler, String sourceDurability, String stateBackend) { + this.scenarioId = scenarioId; this.runtime = runtime; this.scheduler = scheduler; this.sourceDurability = sourceDurability; From 862a262de5d6923f6479c4fb76f38ecdc27461ef Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Mon, 1 Jun 2026 13:32:43 +0200 Subject: [PATCH 37/43] Add managed upload retry exhaustion proof --- .../client/GeneratedTusProtocolContract.java | 34 ++++- .../TestGeneratedTusManagedUploadRuntime.java | 121 ++++++++++++++++++ 2 files changed, 154 insertions(+), 1 deletion(-) diff --git a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java index 40426201..af975e5c 100644 --- a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java +++ b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java @@ -1121,7 +1121,7 @@ final class GeneratedTusProtocolContract { ), }; - static final String MANAGED_UPLOAD_JSON = "{\n \"capabilities\": {\n \"cleanup\": {\n \"policies\": [\n \"remove-owned-source-after-success\",\n \"remove-owned-source-after-cancel\",\n \"retain-owned-source-after-permanent-failure\",\n \"retain-source-after-retryable-failure\",\n \"remove-managed-state-after-terminal-retention\"\n ]\n },\n \"failureClassification\": {\n \"permanentFailures\": [\n \"source-unavailable\",\n \"unretryable-protocol-error\",\n \"retry-policy-exhausted\"\n ],\n \"retryableFailures\": [\n \"retryable-protocol-error\",\n \"io-error\",\n \"network-unavailable\"\n ]\n },\n \"networkConstraints\": {\n \"options\": [\n \"any-network\",\n \"unmetered-network\"\n ]\n },\n \"retryPolicy\": {\n \"controls\": [\n \"max-attempts\",\n \"deadline\",\n \"progress-sensitive-budget\",\n \"unbounded-until-permanent-failure\"\n ],\n \"permanentFailure\": \"stop-without-retry\",\n \"progressReset\": \"reset-budget-after-accepted-offset-advances\"\n },\n \"scheduling\": {\n \"strategies\": [\n \"foreground-task\",\n \"process-lifetime-worker-pool\",\n \"durable-os-scheduler\"\n ]\n },\n \"sourceDurability\": {\n \"ownedCopyCleanup\": \"after-success-or-cancel\",\n \"strategies\": [\n \"copy-to-owned-storage\",\n \"reference-original-source\",\n \"memory-only\"\n ]\n },\n \"stateReporting\": {\n \"states\": [\n \"pending\",\n \"running\",\n \"succeeded\",\n \"failed\"\n ],\n \"terminalRetention\": \"session-and-next-launch\",\n \"transientRetention\": \"until-terminal\"\n }\n },\n \"conformance\": {\n \"scenarioIds\": [\n \"managedUploadDurableRetry\",\n \"managedUploadPermanentFailure\",\n \"managedUploadNetworkConstraint\"\n ],\n \"status\": \"needs-generated-scenario\"\n },\n \"description\": \"Submit upload work that can make sources durable, schedule/resume execution, retry, report state, and clean up while reusing the raw TUS protocol features underneath.\",\n \"featureId\": \"managedUpload\",\n \"flow\": [\n {\n \"kind\": \"managed-primitive\",\n \"primitive\": \"accept-upload-submission\",\n \"summary\": \"Accept source, metadata, headers, endpoint, and retry/scheduling policy.\"\n },\n {\n \"kind\": \"managed-primitive\",\n \"primitive\": \"make-source-durable\",\n \"summary\": \"Keep the source readable according to the selected runtime durability strategy.\"\n },\n {\n \"kind\": \"managed-primitive\",\n \"primitive\": \"schedule-upload-work\",\n \"summary\": \"Run upload work according to the runtime scheduler capability.\"\n },\n {\n \"featureId\": \"singleUploadLifecycle\",\n \"kind\": \"protocol-feature\",\n \"summary\": \"Use the raw protocol upload lifecycle for each execution attempt.\"\n },\n {\n \"featureId\": \"retryOffsetRecovery\",\n \"kind\": \"protocol-feature\",\n \"summary\": \"Use protocol retry and offset recovery before classifying terminal failure.\"\n },\n {\n \"kind\": \"managed-primitive\",\n \"primitive\": \"publish-upload-state\",\n \"summary\": \"Expose pending, running, succeeded, and failed state snapshots.\"\n },\n {\n \"kind\": \"managed-primitive\",\n \"primitive\": \"cleanup-managed-upload\",\n \"summary\": \"Remove owned sources and terminal state according to cleanup policy.\"\n }\n ],\n \"layer\": \"feature-over-protocol\",\n \"primitives\": [\n \"accept-upload-submission\",\n \"make-source-durable\",\n \"schedule-upload-work\",\n \"run-protocol-upload\",\n \"apply-managed-retry-policy\",\n \"classify-failure\",\n \"publish-upload-state\",\n \"cleanup-managed-upload\"\n ],\n \"protocolPrimitives\": [\n \"store-resume-url\",\n \"resume-from-previous-upload\",\n \"recover-offset-after-error\",\n \"retry-with-backoff\",\n \"emit-progress\",\n \"emit-chunk-complete\",\n \"terminate-upload\"\n ],\n \"runtimeProfiles\": [\n {\n \"networkConstraints\": [\n \"any-network\",\n \"unmetered-network\"\n ],\n \"runtime\": \"android\",\n \"scheduler\": \"durable-os-scheduler\",\n \"sourceDurability\": [\n \"copy-to-owned-storage\",\n \"reference-original-source\"\n ],\n \"stateBackend\": \"platform-key-value-store\"\n },\n {\n \"networkConstraints\": [\n \"any-network\",\n \"unmetered-network\"\n ],\n \"runtime\": \"ios\",\n \"scheduler\": \"durable-os-scheduler\",\n \"sourceDurability\": [\n \"copy-to-owned-storage\",\n \"reference-original-source\"\n ],\n \"stateBackend\": \"platform-key-value-store\"\n },\n {\n \"networkConstraints\": [\n \"any-network\"\n ],\n \"runtime\": \"browser\",\n \"scheduler\": \"foreground-task\",\n \"sourceDurability\": [\n \"reference-original-source\",\n \"memory-only\"\n ],\n \"stateBackend\": \"web-storage\"\n },\n {\n \"networkConstraints\": [\n \"any-network\"\n ],\n \"runtime\": \"java\",\n \"scheduler\": \"process-lifetime-worker-pool\",\n \"sourceDurability\": [\n \"copy-to-owned-storage\",\n \"reference-original-source\"\n ],\n \"stateBackend\": \"filesystem\"\n },\n {\n \"networkConstraints\": [\n \"any-network\"\n ],\n \"runtime\": \"node\",\n \"scheduler\": \"process-lifetime-worker-pool\",\n \"sourceDurability\": [\n \"copy-to-owned-storage\",\n \"reference-original-source\",\n \"memory-only\"\n ],\n \"stateBackend\": \"filesystem\"\n },\n {\n \"networkConstraints\": [\n \"any-network\"\n ],\n \"runtime\": \"react-native\",\n \"scheduler\": \"foreground-task\",\n \"sourceDurability\": [\n \"reference-original-source\",\n \"memory-only\"\n ],\n \"stateBackend\": \"platform-key-value-store\"\n }\n ],\n \"scenarios\": [\n {\n \"proofs\": [\n {\n \"attempts\": [\n {\n \"attemptIndex\": 0,\n \"failure\": {\n \"afterAcceptedOffset\": 7,\n \"kind\": \"io-error\",\n \"phase\": \"after-accepted-offset\"\n },\n \"requests\": [\n {\n \"bodySize\": 0,\n \"headers\": {\n \"Upload-Length\": \"14\"\n },\n \"operationId\": \"createTusUpload\",\n \"response\": {\n \"headers\": {\n \"Location\": \"https://tus.io/uploads/managed-durable-retry\"\n },\n \"statusCode\": 201\n },\n \"url\": \"endpoint\"\n },\n {\n \"bodySize\": 7,\n \"headers\": {\n \"Upload-Offset\": \"0\"\n },\n \"operationId\": \"patchTusUpload\",\n \"response\": {\n \"headers\": {\n \"Upload-Offset\": \"7\"\n },\n \"statusCode\": 204\n },\n \"url\": \"upload\"\n }\n ],\n \"stateAfterAttempt\": \"failed\"\n },\n {\n \"attemptIndex\": 1,\n \"requests\": [\n {\n \"headers\": {},\n \"operationId\": \"getTusUploadOffset\",\n \"response\": {\n \"headers\": {\n \"Upload-Length\": \"14\",\n \"Upload-Offset\": \"7\"\n },\n \"statusCode\": 200\n },\n \"url\": \"upload\"\n },\n {\n \"bodySize\": 7,\n \"headers\": {\n \"Upload-Offset\": \"7\"\n },\n \"operationId\": \"patchTusUpload\",\n \"response\": {\n \"headers\": {\n \"Upload-Offset\": \"14\"\n },\n \"statusCode\": 204\n },\n \"url\": \"upload\"\n }\n ],\n \"stateAfterAttempt\": \"succeeded\"\n }\n ],\n \"cleanup\": {\n \"ownedSource\": \"remove-owned-source-after-success\",\n \"resumeUrl\": \"remove-after-success\"\n },\n \"input\": {\n \"chunkSize\": 7,\n \"content\": \"hello managed!\",\n \"fingerprint\": \"managed-durable-retry-fingerprint\",\n \"metadata\": {\n \"filename\": \"managed.txt\"\n },\n \"uploadPath\": \"managed-durable-retry\"\n },\n \"retryDelays\": [\n 0\n ],\n \"sourceDurability\": \"copy-to-owned-storage\",\n \"states\": [\n \"pending\",\n \"running\",\n \"failed\",\n \"running\",\n \"succeeded\"\n ],\n \"terminal\": {\n \"state\": \"succeeded\"\n },\n \"runtime\": \"java\",\n \"scheduler\": \"process-lifetime-worker-pool\",\n \"stateBackend\": \"filesystem\"\n },\n {\n \"attempts\": [\n {\n \"attemptIndex\": 0,\n \"failure\": {\n \"afterAcceptedOffset\": 7,\n \"kind\": \"io-error\",\n \"phase\": \"after-accepted-offset\"\n },\n \"requests\": [\n {\n \"bodySize\": 0,\n \"headers\": {\n \"Upload-Length\": \"14\"\n },\n \"operationId\": \"createTusUpload\",\n \"response\": {\n \"headers\": {\n \"Location\": \"https://tus.io/uploads/managed-durable-retry\"\n },\n \"statusCode\": 201\n },\n \"url\": \"endpoint\"\n },\n {\n \"bodySize\": 7,\n \"headers\": {\n \"Upload-Offset\": \"0\"\n },\n \"operationId\": \"patchTusUpload\",\n \"response\": {\n \"headers\": {\n \"Upload-Offset\": \"7\"\n },\n \"statusCode\": 204\n },\n \"url\": \"upload\"\n }\n ],\n \"stateAfterAttempt\": \"failed\"\n },\n {\n \"attemptIndex\": 1,\n \"requests\": [\n {\n \"headers\": {},\n \"operationId\": \"getTusUploadOffset\",\n \"response\": {\n \"headers\": {\n \"Upload-Length\": \"14\",\n \"Upload-Offset\": \"7\"\n },\n \"statusCode\": 200\n },\n \"url\": \"upload\"\n },\n {\n \"bodySize\": 7,\n \"headers\": {\n \"Upload-Offset\": \"7\"\n },\n \"operationId\": \"patchTusUpload\",\n \"response\": {\n \"headers\": {\n \"Upload-Offset\": \"14\"\n },\n \"statusCode\": 204\n },\n \"url\": \"upload\"\n }\n ],\n \"stateAfterAttempt\": \"succeeded\"\n }\n ],\n \"cleanup\": {\n \"ownedSource\": \"remove-owned-source-after-success\",\n \"resumeUrl\": \"remove-after-success\"\n },\n \"input\": {\n \"chunkSize\": 7,\n \"content\": \"hello managed!\",\n \"fingerprint\": \"managed-durable-retry-fingerprint\",\n \"metadata\": {\n \"filename\": \"managed.txt\"\n },\n \"uploadPath\": \"managed-durable-retry\"\n },\n \"retryDelays\": [\n 0\n ],\n \"sourceDurability\": \"copy-to-owned-storage\",\n \"states\": [\n \"pending\",\n \"running\",\n \"failed\",\n \"running\",\n \"succeeded\"\n ],\n \"terminal\": {\n \"state\": \"succeeded\"\n },\n \"runtime\": \"android\",\n \"scheduler\": \"durable-os-scheduler\",\n \"stateBackend\": \"platform-key-value-store\"\n }\n ],\n \"requiredPrimitives\": [\n \"accept-upload-submission\",\n \"make-source-durable\",\n \"schedule-upload-work\",\n \"run-protocol-upload\",\n \"apply-managed-retry-policy\",\n \"publish-upload-state\",\n \"cleanup-managed-upload\"\n ],\n \"scenarioId\": \"managedUploadDurableRetry\",\n \"summary\": \"Submit a durable source, survive scheduler/process interruption, resume by stored upload URL, and finish with cleanup.\"\n },\n {\n \"proofs\": [\n {\n \"attempts\": [\n {\n \"attemptIndex\": 0,\n \"failure\": {\n \"kind\": \"unretryable-protocol-error\",\n \"phase\": \"during-protocol-request\"\n },\n \"requests\": [\n {\n \"bodySize\": 0,\n \"headers\": {\n \"Upload-Length\": \"14\"\n },\n \"operationId\": \"createTusUpload\",\n \"response\": {\n \"headers\": {},\n \"statusCode\": 400\n },\n \"url\": \"endpoint\"\n }\n ],\n \"stateAfterAttempt\": \"failed\"\n }\n ],\n \"cleanup\": {\n \"ownedSource\": \"retain-owned-source-after-permanent-failure\",\n \"resumeUrl\": \"absent-after-permanent-failure\"\n },\n \"input\": {\n \"chunkSize\": 7,\n \"content\": \"hello failure!\",\n \"fingerprint\": \"managed-permanent-failure-fingerprint\",\n \"metadata\": {\n \"filename\": \"managed-permanent-failure.txt\"\n },\n \"uploadPath\": \"managed-permanent-failure\"\n },\n \"retryDelays\": [],\n \"sourceDurability\": \"copy-to-owned-storage\",\n \"states\": [\n \"pending\",\n \"running\",\n \"failed\"\n ],\n \"terminal\": {\n \"failure\": \"unretryable-protocol-error\",\n \"state\": \"failed\"\n },\n \"runtime\": \"java\",\n \"scheduler\": \"process-lifetime-worker-pool\",\n \"stateBackend\": \"filesystem\"\n },\n {\n \"attempts\": [\n {\n \"attemptIndex\": 0,\n \"failure\": {\n \"kind\": \"unretryable-protocol-error\",\n \"phase\": \"during-protocol-request\"\n },\n \"requests\": [\n {\n \"bodySize\": 0,\n \"headers\": {\n \"Upload-Length\": \"14\"\n },\n \"operationId\": \"createTusUpload\",\n \"response\": {\n \"headers\": {},\n \"statusCode\": 400\n },\n \"url\": \"endpoint\"\n }\n ],\n \"stateAfterAttempt\": \"failed\"\n }\n ],\n \"cleanup\": {\n \"ownedSource\": \"retain-owned-source-after-permanent-failure\",\n \"resumeUrl\": \"absent-after-permanent-failure\"\n },\n \"input\": {\n \"chunkSize\": 7,\n \"content\": \"hello failure!\",\n \"fingerprint\": \"managed-permanent-failure-fingerprint\",\n \"metadata\": {\n \"filename\": \"managed-permanent-failure.txt\"\n },\n \"uploadPath\": \"managed-permanent-failure\"\n },\n \"retryDelays\": [],\n \"sourceDurability\": \"copy-to-owned-storage\",\n \"states\": [\n \"pending\",\n \"running\",\n \"failed\"\n ],\n \"terminal\": {\n \"failure\": \"unretryable-protocol-error\",\n \"state\": \"failed\"\n },\n \"runtime\": \"android\",\n \"scheduler\": \"durable-os-scheduler\",\n \"stateBackend\": \"platform-key-value-store\"\n }\n ],\n \"requiredPrimitives\": [\n \"accept-upload-submission\",\n \"make-source-durable\",\n \"schedule-upload-work\",\n \"run-protocol-upload\",\n \"classify-failure\",\n \"publish-upload-state\",\n \"cleanup-managed-upload\"\n ],\n \"scenarioId\": \"managedUploadPermanentFailure\",\n \"summary\": \"Classify missing sources and unretryable protocol failures as terminal without further retry.\"\n },\n {\n \"requiredPrimitives\": [\n \"accept-upload-submission\",\n \"schedule-upload-work\",\n \"publish-upload-state\"\n ],\n \"scenarioId\": \"managedUploadNetworkConstraint\",\n \"summary\": \"Honor network constraints before starting or resuming upload work.\"\n }\n ]\n}\n"; + static final String MANAGED_UPLOAD_JSON = "{\n \"capabilities\": {\n \"cleanup\": {\n \"policies\": [\n \"remove-owned-source-after-success\",\n \"remove-owned-source-after-cancel\",\n \"retain-owned-source-after-permanent-failure\",\n \"retain-source-after-retryable-failure\",\n \"remove-managed-state-after-terminal-retention\"\n ]\n },\n \"failureClassification\": {\n \"permanentFailures\": [\n \"source-unavailable\",\n \"unretryable-protocol-error\",\n \"retry-policy-exhausted\"\n ],\n \"retryableFailures\": [\n \"retryable-protocol-error\",\n \"io-error\",\n \"network-unavailable\"\n ]\n },\n \"networkConstraints\": {\n \"options\": [\n \"any-network\",\n \"unmetered-network\"\n ]\n },\n \"retryPolicy\": {\n \"controls\": [\n \"max-attempts\",\n \"deadline\",\n \"progress-sensitive-budget\",\n \"unbounded-until-permanent-failure\"\n ],\n \"permanentFailure\": \"stop-without-retry\",\n \"progressReset\": \"reset-budget-after-accepted-offset-advances\"\n },\n \"scheduling\": {\n \"strategies\": [\n \"foreground-task\",\n \"process-lifetime-worker-pool\",\n \"durable-os-scheduler\"\n ]\n },\n \"sourceDurability\": {\n \"ownedCopyCleanup\": \"after-success-or-cancel\",\n \"strategies\": [\n \"copy-to-owned-storage\",\n \"reference-original-source\",\n \"memory-only\"\n ]\n },\n \"stateReporting\": {\n \"states\": [\n \"pending\",\n \"running\",\n \"succeeded\",\n \"failed\"\n ],\n \"terminalRetention\": \"session-and-next-launch\",\n \"transientRetention\": \"until-terminal\"\n }\n },\n \"conformance\": {\n \"scenarioIds\": [\n \"managedUploadDurableRetry\",\n \"managedUploadPermanentFailure\",\n \"managedUploadRetryPolicyExhausted\",\n \"managedUploadNetworkConstraint\"\n ],\n \"status\": \"needs-generated-scenario\"\n },\n \"description\": \"Submit upload work that can make sources durable, schedule/resume execution, retry, report state, and clean up while reusing the raw TUS protocol features underneath.\",\n \"featureId\": \"managedUpload\",\n \"flow\": [\n {\n \"kind\": \"managed-primitive\",\n \"primitive\": \"accept-upload-submission\",\n \"summary\": \"Accept source, metadata, headers, endpoint, and retry/scheduling policy.\"\n },\n {\n \"kind\": \"managed-primitive\",\n \"primitive\": \"make-source-durable\",\n \"summary\": \"Keep the source readable according to the selected runtime durability strategy.\"\n },\n {\n \"kind\": \"managed-primitive\",\n \"primitive\": \"schedule-upload-work\",\n \"summary\": \"Run upload work according to the runtime scheduler capability.\"\n },\n {\n \"featureId\": \"singleUploadLifecycle\",\n \"kind\": \"protocol-feature\",\n \"summary\": \"Use the raw protocol upload lifecycle for each execution attempt.\"\n },\n {\n \"featureId\": \"retryOffsetRecovery\",\n \"kind\": \"protocol-feature\",\n \"summary\": \"Use protocol retry and offset recovery before classifying terminal failure.\"\n },\n {\n \"kind\": \"managed-primitive\",\n \"primitive\": \"publish-upload-state\",\n \"summary\": \"Expose pending, running, succeeded, and failed state snapshots.\"\n },\n {\n \"kind\": \"managed-primitive\",\n \"primitive\": \"cleanup-managed-upload\",\n \"summary\": \"Remove owned sources and terminal state according to cleanup policy.\"\n }\n ],\n \"layer\": \"feature-over-protocol\",\n \"primitives\": [\n \"accept-upload-submission\",\n \"make-source-durable\",\n \"schedule-upload-work\",\n \"run-protocol-upload\",\n \"apply-managed-retry-policy\",\n \"classify-failure\",\n \"publish-upload-state\",\n \"cleanup-managed-upload\"\n ],\n \"protocolPrimitives\": [\n \"store-resume-url\",\n \"resume-from-previous-upload\",\n \"recover-offset-after-error\",\n \"retry-with-backoff\",\n \"emit-progress\",\n \"emit-chunk-complete\",\n \"terminate-upload\"\n ],\n \"runtimeProfiles\": [\n {\n \"networkConstraints\": [\n \"any-network\",\n \"unmetered-network\"\n ],\n \"runtime\": \"android\",\n \"scheduler\": \"durable-os-scheduler\",\n \"sourceDurability\": [\n \"copy-to-owned-storage\",\n \"reference-original-source\"\n ],\n \"stateBackend\": \"platform-key-value-store\"\n },\n {\n \"networkConstraints\": [\n \"any-network\",\n \"unmetered-network\"\n ],\n \"runtime\": \"ios\",\n \"scheduler\": \"durable-os-scheduler\",\n \"sourceDurability\": [\n \"copy-to-owned-storage\",\n \"reference-original-source\"\n ],\n \"stateBackend\": \"platform-key-value-store\"\n },\n {\n \"networkConstraints\": [\n \"any-network\"\n ],\n \"runtime\": \"browser\",\n \"scheduler\": \"foreground-task\",\n \"sourceDurability\": [\n \"reference-original-source\",\n \"memory-only\"\n ],\n \"stateBackend\": \"web-storage\"\n },\n {\n \"networkConstraints\": [\n \"any-network\"\n ],\n \"runtime\": \"java\",\n \"scheduler\": \"process-lifetime-worker-pool\",\n \"sourceDurability\": [\n \"copy-to-owned-storage\",\n \"reference-original-source\"\n ],\n \"stateBackend\": \"filesystem\"\n },\n {\n \"networkConstraints\": [\n \"any-network\"\n ],\n \"runtime\": \"node\",\n \"scheduler\": \"process-lifetime-worker-pool\",\n \"sourceDurability\": [\n \"copy-to-owned-storage\",\n \"reference-original-source\",\n \"memory-only\"\n ],\n \"stateBackend\": \"filesystem\"\n },\n {\n \"networkConstraints\": [\n \"any-network\"\n ],\n \"runtime\": \"react-native\",\n \"scheduler\": \"foreground-task\",\n \"sourceDurability\": [\n \"reference-original-source\",\n \"memory-only\"\n ],\n \"stateBackend\": \"platform-key-value-store\"\n }\n ],\n \"scenarios\": [\n {\n \"proofs\": [\n {\n \"attempts\": [\n {\n \"attemptIndex\": 0,\n \"failure\": {\n \"afterAcceptedOffset\": 7,\n \"kind\": \"io-error\",\n \"phase\": \"after-accepted-offset\"\n },\n \"requests\": [\n {\n \"bodySize\": 0,\n \"headers\": {\n \"Upload-Length\": \"14\"\n },\n \"operationId\": \"createTusUpload\",\n \"response\": {\n \"headers\": {\n \"Location\": \"https://tus.io/uploads/managed-durable-retry\"\n },\n \"statusCode\": 201\n },\n \"url\": \"endpoint\"\n },\n {\n \"bodySize\": 7,\n \"headers\": {\n \"Upload-Offset\": \"0\"\n },\n \"operationId\": \"patchTusUpload\",\n \"response\": {\n \"headers\": {\n \"Upload-Offset\": \"7\"\n },\n \"statusCode\": 204\n },\n \"url\": \"upload\"\n }\n ],\n \"stateAfterAttempt\": \"failed\"\n },\n {\n \"attemptIndex\": 1,\n \"requests\": [\n {\n \"headers\": {},\n \"operationId\": \"getTusUploadOffset\",\n \"response\": {\n \"headers\": {\n \"Upload-Length\": \"14\",\n \"Upload-Offset\": \"7\"\n },\n \"statusCode\": 200\n },\n \"url\": \"upload\"\n },\n {\n \"bodySize\": 7,\n \"headers\": {\n \"Upload-Offset\": \"7\"\n },\n \"operationId\": \"patchTusUpload\",\n \"response\": {\n \"headers\": {\n \"Upload-Offset\": \"14\"\n },\n \"statusCode\": 204\n },\n \"url\": \"upload\"\n }\n ],\n \"stateAfterAttempt\": \"succeeded\"\n }\n ],\n \"cleanup\": {\n \"ownedSource\": \"remove-owned-source-after-success\",\n \"resumeUrl\": \"remove-after-success\"\n },\n \"input\": {\n \"chunkSize\": 7,\n \"content\": \"hello managed!\",\n \"fingerprint\": \"managed-durable-retry-fingerprint\",\n \"metadata\": {\n \"filename\": \"managed.txt\"\n },\n \"uploadPath\": \"managed-durable-retry\"\n },\n \"retryDelays\": [\n 0\n ],\n \"sourceDurability\": \"copy-to-owned-storage\",\n \"states\": [\n \"pending\",\n \"running\",\n \"failed\",\n \"running\",\n \"succeeded\"\n ],\n \"terminal\": {\n \"state\": \"succeeded\"\n },\n \"runtime\": \"java\",\n \"scheduler\": \"process-lifetime-worker-pool\",\n \"stateBackend\": \"filesystem\"\n },\n {\n \"attempts\": [\n {\n \"attemptIndex\": 0,\n \"failure\": {\n \"afterAcceptedOffset\": 7,\n \"kind\": \"io-error\",\n \"phase\": \"after-accepted-offset\"\n },\n \"requests\": [\n {\n \"bodySize\": 0,\n \"headers\": {\n \"Upload-Length\": \"14\"\n },\n \"operationId\": \"createTusUpload\",\n \"response\": {\n \"headers\": {\n \"Location\": \"https://tus.io/uploads/managed-durable-retry\"\n },\n \"statusCode\": 201\n },\n \"url\": \"endpoint\"\n },\n {\n \"bodySize\": 7,\n \"headers\": {\n \"Upload-Offset\": \"0\"\n },\n \"operationId\": \"patchTusUpload\",\n \"response\": {\n \"headers\": {\n \"Upload-Offset\": \"7\"\n },\n \"statusCode\": 204\n },\n \"url\": \"upload\"\n }\n ],\n \"stateAfterAttempt\": \"failed\"\n },\n {\n \"attemptIndex\": 1,\n \"requests\": [\n {\n \"headers\": {},\n \"operationId\": \"getTusUploadOffset\",\n \"response\": {\n \"headers\": {\n \"Upload-Length\": \"14\",\n \"Upload-Offset\": \"7\"\n },\n \"statusCode\": 200\n },\n \"url\": \"upload\"\n },\n {\n \"bodySize\": 7,\n \"headers\": {\n \"Upload-Offset\": \"7\"\n },\n \"operationId\": \"patchTusUpload\",\n \"response\": {\n \"headers\": {\n \"Upload-Offset\": \"14\"\n },\n \"statusCode\": 204\n },\n \"url\": \"upload\"\n }\n ],\n \"stateAfterAttempt\": \"succeeded\"\n }\n ],\n \"cleanup\": {\n \"ownedSource\": \"remove-owned-source-after-success\",\n \"resumeUrl\": \"remove-after-success\"\n },\n \"input\": {\n \"chunkSize\": 7,\n \"content\": \"hello managed!\",\n \"fingerprint\": \"managed-durable-retry-fingerprint\",\n \"metadata\": {\n \"filename\": \"managed.txt\"\n },\n \"uploadPath\": \"managed-durable-retry\"\n },\n \"retryDelays\": [\n 0\n ],\n \"sourceDurability\": \"copy-to-owned-storage\",\n \"states\": [\n \"pending\",\n \"running\",\n \"failed\",\n \"running\",\n \"succeeded\"\n ],\n \"terminal\": {\n \"state\": \"succeeded\"\n },\n \"runtime\": \"android\",\n \"scheduler\": \"durable-os-scheduler\",\n \"stateBackend\": \"platform-key-value-store\"\n }\n ],\n \"requiredPrimitives\": [\n \"accept-upload-submission\",\n \"make-source-durable\",\n \"schedule-upload-work\",\n \"run-protocol-upload\",\n \"apply-managed-retry-policy\",\n \"publish-upload-state\",\n \"cleanup-managed-upload\"\n ],\n \"scenarioId\": \"managedUploadDurableRetry\",\n \"summary\": \"Submit a durable source, survive scheduler/process interruption, resume by stored upload URL, and finish with cleanup.\"\n },\n {\n \"proofs\": [\n {\n \"attempts\": [\n {\n \"attemptIndex\": 0,\n \"failure\": {\n \"kind\": \"unretryable-protocol-error\",\n \"phase\": \"during-protocol-request\"\n },\n \"requests\": [\n {\n \"bodySize\": 0,\n \"headers\": {\n \"Upload-Length\": \"14\"\n },\n \"operationId\": \"createTusUpload\",\n \"response\": {\n \"headers\": {},\n \"statusCode\": 400\n },\n \"url\": \"endpoint\"\n }\n ],\n \"stateAfterAttempt\": \"failed\"\n }\n ],\n \"cleanup\": {\n \"ownedSource\": \"retain-owned-source-after-permanent-failure\",\n \"resumeUrl\": \"absent-after-permanent-failure\"\n },\n \"input\": {\n \"chunkSize\": 7,\n \"content\": \"hello failure!\",\n \"fingerprint\": \"managed-permanent-failure-fingerprint\",\n \"metadata\": {\n \"filename\": \"managed-permanent-failure.txt\"\n },\n \"uploadPath\": \"managed-permanent-failure\"\n },\n \"retryDelays\": [],\n \"sourceDurability\": \"copy-to-owned-storage\",\n \"states\": [\n \"pending\",\n \"running\",\n \"failed\"\n ],\n \"terminal\": {\n \"failure\": \"unretryable-protocol-error\",\n \"state\": \"failed\"\n },\n \"runtime\": \"java\",\n \"scheduler\": \"process-lifetime-worker-pool\",\n \"stateBackend\": \"filesystem\"\n },\n {\n \"attempts\": [\n {\n \"attemptIndex\": 0,\n \"failure\": {\n \"kind\": \"unretryable-protocol-error\",\n \"phase\": \"during-protocol-request\"\n },\n \"requests\": [\n {\n \"bodySize\": 0,\n \"headers\": {\n \"Upload-Length\": \"14\"\n },\n \"operationId\": \"createTusUpload\",\n \"response\": {\n \"headers\": {},\n \"statusCode\": 400\n },\n \"url\": \"endpoint\"\n }\n ],\n \"stateAfterAttempt\": \"failed\"\n }\n ],\n \"cleanup\": {\n \"ownedSource\": \"retain-owned-source-after-permanent-failure\",\n \"resumeUrl\": \"absent-after-permanent-failure\"\n },\n \"input\": {\n \"chunkSize\": 7,\n \"content\": \"hello failure!\",\n \"fingerprint\": \"managed-permanent-failure-fingerprint\",\n \"metadata\": {\n \"filename\": \"managed-permanent-failure.txt\"\n },\n \"uploadPath\": \"managed-permanent-failure\"\n },\n \"retryDelays\": [],\n \"sourceDurability\": \"copy-to-owned-storage\",\n \"states\": [\n \"pending\",\n \"running\",\n \"failed\"\n ],\n \"terminal\": {\n \"failure\": \"unretryable-protocol-error\",\n \"state\": \"failed\"\n },\n \"runtime\": \"android\",\n \"scheduler\": \"durable-os-scheduler\",\n \"stateBackend\": \"platform-key-value-store\"\n }\n ],\n \"requiredPrimitives\": [\n \"accept-upload-submission\",\n \"make-source-durable\",\n \"schedule-upload-work\",\n \"run-protocol-upload\",\n \"classify-failure\",\n \"publish-upload-state\",\n \"cleanup-managed-upload\"\n ],\n \"scenarioId\": \"managedUploadPermanentFailure\",\n \"summary\": \"Classify missing sources and unretryable protocol failures as terminal without further retry.\"\n },\n {\n \"proofs\": [\n {\n \"attempts\": [\n {\n \"attemptIndex\": 0,\n \"failure\": {\n \"kind\": \"retryable-protocol-error\",\n \"phase\": \"during-protocol-request\"\n },\n \"requests\": [\n {\n \"bodySize\": 0,\n \"headers\": {\n \"Upload-Length\": \"14\"\n },\n \"operationId\": \"createTusUpload\",\n \"response\": {\n \"headers\": {},\n \"statusCode\": 500\n },\n \"url\": \"endpoint\"\n }\n ],\n \"stateAfterAttempt\": \"failed\"\n },\n {\n \"attemptIndex\": 1,\n \"failure\": {\n \"kind\": \"retryable-protocol-error\",\n \"phase\": \"during-protocol-request\"\n },\n \"requests\": [\n {\n \"bodySize\": 0,\n \"headers\": {\n \"Upload-Length\": \"14\"\n },\n \"operationId\": \"createTusUpload\",\n \"response\": {\n \"headers\": {},\n \"statusCode\": 500\n },\n \"url\": \"endpoint\"\n }\n ],\n \"stateAfterAttempt\": \"failed\"\n },\n {\n \"attemptIndex\": 2,\n \"failure\": {\n \"kind\": \"retryable-protocol-error\",\n \"phase\": \"during-protocol-request\"\n },\n \"requests\": [\n {\n \"bodySize\": 0,\n \"headers\": {\n \"Upload-Length\": \"14\"\n },\n \"operationId\": \"createTusUpload\",\n \"response\": {\n \"headers\": {},\n \"statusCode\": 500\n },\n \"url\": \"endpoint\"\n }\n ],\n \"stateAfterAttempt\": \"failed\"\n }\n ],\n \"cleanup\": {\n \"ownedSource\": \"retain-owned-source-after-permanent-failure\",\n \"resumeUrl\": \"absent-after-permanent-failure\"\n },\n \"input\": {\n \"chunkSize\": 7,\n \"content\": \"hello retries!\",\n \"fingerprint\": \"managed-retry-exhausted-fingerprint\",\n \"metadata\": {\n \"filename\": \"managed-retry-exhausted.txt\"\n },\n \"uploadPath\": \"managed-retry-exhausted\"\n },\n \"retryDelays\": [\n 0,\n 0\n ],\n \"sourceDurability\": \"copy-to-owned-storage\",\n \"states\": [\n \"pending\",\n \"running\",\n \"failed\",\n \"running\",\n \"failed\",\n \"running\",\n \"failed\"\n ],\n \"terminal\": {\n \"failure\": \"retry-policy-exhausted\",\n \"state\": \"failed\"\n },\n \"runtime\": \"java\",\n \"scheduler\": \"process-lifetime-worker-pool\",\n \"stateBackend\": \"filesystem\"\n },\n {\n \"attempts\": [\n {\n \"attemptIndex\": 0,\n \"failure\": {\n \"kind\": \"retryable-protocol-error\",\n \"phase\": \"during-protocol-request\"\n },\n \"requests\": [\n {\n \"bodySize\": 0,\n \"headers\": {\n \"Upload-Length\": \"14\"\n },\n \"operationId\": \"createTusUpload\",\n \"response\": {\n \"headers\": {},\n \"statusCode\": 500\n },\n \"url\": \"endpoint\"\n }\n ],\n \"stateAfterAttempt\": \"failed\"\n },\n {\n \"attemptIndex\": 1,\n \"failure\": {\n \"kind\": \"retryable-protocol-error\",\n \"phase\": \"during-protocol-request\"\n },\n \"requests\": [\n {\n \"bodySize\": 0,\n \"headers\": {\n \"Upload-Length\": \"14\"\n },\n \"operationId\": \"createTusUpload\",\n \"response\": {\n \"headers\": {},\n \"statusCode\": 500\n },\n \"url\": \"endpoint\"\n }\n ],\n \"stateAfterAttempt\": \"failed\"\n },\n {\n \"attemptIndex\": 2,\n \"failure\": {\n \"kind\": \"retryable-protocol-error\",\n \"phase\": \"during-protocol-request\"\n },\n \"requests\": [\n {\n \"bodySize\": 0,\n \"headers\": {\n \"Upload-Length\": \"14\"\n },\n \"operationId\": \"createTusUpload\",\n \"response\": {\n \"headers\": {},\n \"statusCode\": 500\n },\n \"url\": \"endpoint\"\n }\n ],\n \"stateAfterAttempt\": \"failed\"\n }\n ],\n \"cleanup\": {\n \"ownedSource\": \"retain-owned-source-after-permanent-failure\",\n \"resumeUrl\": \"absent-after-permanent-failure\"\n },\n \"input\": {\n \"chunkSize\": 7,\n \"content\": \"hello retries!\",\n \"fingerprint\": \"managed-retry-exhausted-fingerprint\",\n \"metadata\": {\n \"filename\": \"managed-retry-exhausted.txt\"\n },\n \"uploadPath\": \"managed-retry-exhausted\"\n },\n \"retryDelays\": [\n 0,\n 0\n ],\n \"sourceDurability\": \"copy-to-owned-storage\",\n \"states\": [\n \"pending\",\n \"running\",\n \"failed\",\n \"running\",\n \"failed\",\n \"running\",\n \"failed\"\n ],\n \"terminal\": {\n \"failure\": \"retry-policy-exhausted\",\n \"state\": \"failed\"\n },\n \"runtime\": \"android\",\n \"scheduler\": \"durable-os-scheduler\",\n \"stateBackend\": \"platform-key-value-store\"\n }\n ],\n \"requiredPrimitives\": [\n \"accept-upload-submission\",\n \"make-source-durable\",\n \"schedule-upload-work\",\n \"run-protocol-upload\",\n \"apply-managed-retry-policy\",\n \"classify-failure\",\n \"publish-upload-state\",\n \"cleanup-managed-upload\"\n ],\n \"scenarioId\": \"managedUploadRetryPolicyExhausted\",\n \"summary\": \"Retry transient protocol failures up to the managed retry budget and then classify the upload as terminally failed.\"\n },\n {\n \"requiredPrimitives\": [\n \"accept-upload-submission\",\n \"schedule-upload-work\",\n \"publish-upload-state\"\n ],\n \"scenarioId\": \"managedUploadNetworkConstraint\",\n \"summary\": \"Honor network constraints before starting or resuming upload work.\"\n }\n ]\n}\n"; static final String[] MANAGED_UPLOAD_PRIMITIVES = new String[] { @@ -1149,6 +1149,7 @@ final class GeneratedTusProtocolContract { new String[] { "managedUploadDurableRetry", "managedUploadPermanentFailure", + "managedUploadRetryPolicyExhausted", "managedUploadNetworkConstraint", }; @@ -1214,6 +1215,37 @@ final class GeneratedTusProtocolContract { "react-native", } ), + new GeneratedTusProtocolContract.GeneratedTusManagedUploadProofCase( + "managedUpload", + "feature-over-protocol", + "managedUploadRetryPolicyExhausted", + new String[] { + "java", + "android", + }, + new String[] { + "accept-upload-submission", + "make-source-durable", + "schedule-upload-work", + "run-protocol-upload", + "apply-managed-retry-policy", + "classify-failure", + "publish-upload-state", + "cleanup-managed-upload", + }, + new String[] { + "singleUploadLifecycle", + "retryOffsetRecovery", + }, + new String[] { + "android", + "ios", + "browser", + "java", + "node", + "react-native", + } + ), new GeneratedTusProtocolContract.GeneratedTusManagedUploadProofCase( "managedUpload", "feature-over-protocol", diff --git a/src/test/java/io/tus/java/client/TestGeneratedTusManagedUploadRuntime.java b/src/test/java/io/tus/java/client/TestGeneratedTusManagedUploadRuntime.java index aaf02bfa..c8324f5a 100644 --- a/src/test/java/io/tus/java/client/TestGeneratedTusManagedUploadRuntime.java +++ b/src/test/java/io/tus/java/client/TestGeneratedTusManagedUploadRuntime.java @@ -238,6 +238,127 @@ public class TestGeneratedTusManagedUploadRuntime extends MockServerProvider { ), } ), + new GeneratedTusManagedUploadRuntimeCase( + new GeneratedTusManagedUploadRuntimeProfile( + "managedUploadRetryPolicyExhausted", + "java", + "process-lifetime-worker-pool", + "copy-to-owned-storage", + "filesystem" + ), + new GeneratedTusManagedUploadTransport( + "Location" + ), + new GeneratedTusManagedUploadTerminal( + "failed", + "retry-policy-exhausted" + ), + new GeneratedTusManagedUploadCleanup( + "retain-owned-source-after-permanent-failure", + "absent-after-permanent-failure" + ), + new GeneratedTusManagedUploadRetryPlan( + new String[] { + "pending", + "running", + "failed", + "running", + "failed", + "running", + "failed", + }, + new int[] { + 0, + 0, + } + ), + new GeneratedTusManagedUploadInput( + "hello retries!", + 7, + "managed-retry-exhausted-fingerprint", + "managed-retry-exhausted", + new GeneratedTusManagedUploadMetadata[] { + new GeneratedTusManagedUploadMetadata( + "filename", + "managed-retry-exhausted.txt" + ), + } + ), + new GeneratedTusManagedUploadAttempt[] { + new GeneratedTusManagedUploadAttempt( + 0, + "failed", + new GeneratedTusManagedUploadFailure( + "during-protocol-request", + "retryable-protocol-error", + -1 + ), + new GeneratedTusManagedUploadRequest[] { + new GeneratedTusManagedUploadRequest( + "POST", + "endpoint", + 0, + 500, + new GeneratedTusManagedUploadHeader[] { + new GeneratedTusManagedUploadHeader( + "Upload-Length", + "14" + ), + }, + new GeneratedTusManagedUploadHeader[0] + ), + } + ), + new GeneratedTusManagedUploadAttempt( + 1, + "failed", + new GeneratedTusManagedUploadFailure( + "during-protocol-request", + "retryable-protocol-error", + -1 + ), + new GeneratedTusManagedUploadRequest[] { + new GeneratedTusManagedUploadRequest( + "POST", + "endpoint", + 0, + 500, + new GeneratedTusManagedUploadHeader[] { + new GeneratedTusManagedUploadHeader( + "Upload-Length", + "14" + ), + }, + new GeneratedTusManagedUploadHeader[0] + ), + } + ), + new GeneratedTusManagedUploadAttempt( + 2, + "failed", + new GeneratedTusManagedUploadFailure( + "during-protocol-request", + "retryable-protocol-error", + -1 + ), + new GeneratedTusManagedUploadRequest[] { + new GeneratedTusManagedUploadRequest( + "POST", + "endpoint", + 0, + 500, + new GeneratedTusManagedUploadHeader[] { + new GeneratedTusManagedUploadHeader( + "Upload-Length", + "14" + ), + }, + new GeneratedTusManagedUploadHeader[0] + ), + } + ), + } + ), }; private static final GeneratedTusMethodOverride[] METHOD_OVERRIDES = new GeneratedTusMethodOverride[] { From 2d7600d1d4592ba97213f1c5fe5315b41ffd3bcc Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Mon, 1 Jun 2026 13:53:29 +0200 Subject: [PATCH 38/43] Add generated managed source unavailable proof --- .../client/GeneratedTusProtocolContract.java | 32 +++- .../TestGeneratedTusManagedUploadRuntime.java | 165 ++++++++++++++++-- 2 files changed, 182 insertions(+), 15 deletions(-) diff --git a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java index af975e5c..7b7b06c7 100644 --- a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java +++ b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java @@ -1121,7 +1121,7 @@ final class GeneratedTusProtocolContract { ), }; - static final String MANAGED_UPLOAD_JSON = "{\n \"capabilities\": {\n \"cleanup\": {\n \"policies\": [\n \"remove-owned-source-after-success\",\n \"remove-owned-source-after-cancel\",\n \"retain-owned-source-after-permanent-failure\",\n \"retain-source-after-retryable-failure\",\n \"remove-managed-state-after-terminal-retention\"\n ]\n },\n \"failureClassification\": {\n \"permanentFailures\": [\n \"source-unavailable\",\n \"unretryable-protocol-error\",\n \"retry-policy-exhausted\"\n ],\n \"retryableFailures\": [\n \"retryable-protocol-error\",\n \"io-error\",\n \"network-unavailable\"\n ]\n },\n \"networkConstraints\": {\n \"options\": [\n \"any-network\",\n \"unmetered-network\"\n ]\n },\n \"retryPolicy\": {\n \"controls\": [\n \"max-attempts\",\n \"deadline\",\n \"progress-sensitive-budget\",\n \"unbounded-until-permanent-failure\"\n ],\n \"permanentFailure\": \"stop-without-retry\",\n \"progressReset\": \"reset-budget-after-accepted-offset-advances\"\n },\n \"scheduling\": {\n \"strategies\": [\n \"foreground-task\",\n \"process-lifetime-worker-pool\",\n \"durable-os-scheduler\"\n ]\n },\n \"sourceDurability\": {\n \"ownedCopyCleanup\": \"after-success-or-cancel\",\n \"strategies\": [\n \"copy-to-owned-storage\",\n \"reference-original-source\",\n \"memory-only\"\n ]\n },\n \"stateReporting\": {\n \"states\": [\n \"pending\",\n \"running\",\n \"succeeded\",\n \"failed\"\n ],\n \"terminalRetention\": \"session-and-next-launch\",\n \"transientRetention\": \"until-terminal\"\n }\n },\n \"conformance\": {\n \"scenarioIds\": [\n \"managedUploadDurableRetry\",\n \"managedUploadPermanentFailure\",\n \"managedUploadRetryPolicyExhausted\",\n \"managedUploadNetworkConstraint\"\n ],\n \"status\": \"needs-generated-scenario\"\n },\n \"description\": \"Submit upload work that can make sources durable, schedule/resume execution, retry, report state, and clean up while reusing the raw TUS protocol features underneath.\",\n \"featureId\": \"managedUpload\",\n \"flow\": [\n {\n \"kind\": \"managed-primitive\",\n \"primitive\": \"accept-upload-submission\",\n \"summary\": \"Accept source, metadata, headers, endpoint, and retry/scheduling policy.\"\n },\n {\n \"kind\": \"managed-primitive\",\n \"primitive\": \"make-source-durable\",\n \"summary\": \"Keep the source readable according to the selected runtime durability strategy.\"\n },\n {\n \"kind\": \"managed-primitive\",\n \"primitive\": \"schedule-upload-work\",\n \"summary\": \"Run upload work according to the runtime scheduler capability.\"\n },\n {\n \"featureId\": \"singleUploadLifecycle\",\n \"kind\": \"protocol-feature\",\n \"summary\": \"Use the raw protocol upload lifecycle for each execution attempt.\"\n },\n {\n \"featureId\": \"retryOffsetRecovery\",\n \"kind\": \"protocol-feature\",\n \"summary\": \"Use protocol retry and offset recovery before classifying terminal failure.\"\n },\n {\n \"kind\": \"managed-primitive\",\n \"primitive\": \"publish-upload-state\",\n \"summary\": \"Expose pending, running, succeeded, and failed state snapshots.\"\n },\n {\n \"kind\": \"managed-primitive\",\n \"primitive\": \"cleanup-managed-upload\",\n \"summary\": \"Remove owned sources and terminal state according to cleanup policy.\"\n }\n ],\n \"layer\": \"feature-over-protocol\",\n \"primitives\": [\n \"accept-upload-submission\",\n \"make-source-durable\",\n \"schedule-upload-work\",\n \"run-protocol-upload\",\n \"apply-managed-retry-policy\",\n \"classify-failure\",\n \"publish-upload-state\",\n \"cleanup-managed-upload\"\n ],\n \"protocolPrimitives\": [\n \"store-resume-url\",\n \"resume-from-previous-upload\",\n \"recover-offset-after-error\",\n \"retry-with-backoff\",\n \"emit-progress\",\n \"emit-chunk-complete\",\n \"terminate-upload\"\n ],\n \"runtimeProfiles\": [\n {\n \"networkConstraints\": [\n \"any-network\",\n \"unmetered-network\"\n ],\n \"runtime\": \"android\",\n \"scheduler\": \"durable-os-scheduler\",\n \"sourceDurability\": [\n \"copy-to-owned-storage\",\n \"reference-original-source\"\n ],\n \"stateBackend\": \"platform-key-value-store\"\n },\n {\n \"networkConstraints\": [\n \"any-network\",\n \"unmetered-network\"\n ],\n \"runtime\": \"ios\",\n \"scheduler\": \"durable-os-scheduler\",\n \"sourceDurability\": [\n \"copy-to-owned-storage\",\n \"reference-original-source\"\n ],\n \"stateBackend\": \"platform-key-value-store\"\n },\n {\n \"networkConstraints\": [\n \"any-network\"\n ],\n \"runtime\": \"browser\",\n \"scheduler\": \"foreground-task\",\n \"sourceDurability\": [\n \"reference-original-source\",\n \"memory-only\"\n ],\n \"stateBackend\": \"web-storage\"\n },\n {\n \"networkConstraints\": [\n \"any-network\"\n ],\n \"runtime\": \"java\",\n \"scheduler\": \"process-lifetime-worker-pool\",\n \"sourceDurability\": [\n \"copy-to-owned-storage\",\n \"reference-original-source\"\n ],\n \"stateBackend\": \"filesystem\"\n },\n {\n \"networkConstraints\": [\n \"any-network\"\n ],\n \"runtime\": \"node\",\n \"scheduler\": \"process-lifetime-worker-pool\",\n \"sourceDurability\": [\n \"copy-to-owned-storage\",\n \"reference-original-source\",\n \"memory-only\"\n ],\n \"stateBackend\": \"filesystem\"\n },\n {\n \"networkConstraints\": [\n \"any-network\"\n ],\n \"runtime\": \"react-native\",\n \"scheduler\": \"foreground-task\",\n \"sourceDurability\": [\n \"reference-original-source\",\n \"memory-only\"\n ],\n \"stateBackend\": \"platform-key-value-store\"\n }\n ],\n \"scenarios\": [\n {\n \"proofs\": [\n {\n \"attempts\": [\n {\n \"attemptIndex\": 0,\n \"failure\": {\n \"afterAcceptedOffset\": 7,\n \"kind\": \"io-error\",\n \"phase\": \"after-accepted-offset\"\n },\n \"requests\": [\n {\n \"bodySize\": 0,\n \"headers\": {\n \"Upload-Length\": \"14\"\n },\n \"operationId\": \"createTusUpload\",\n \"response\": {\n \"headers\": {\n \"Location\": \"https://tus.io/uploads/managed-durable-retry\"\n },\n \"statusCode\": 201\n },\n \"url\": \"endpoint\"\n },\n {\n \"bodySize\": 7,\n \"headers\": {\n \"Upload-Offset\": \"0\"\n },\n \"operationId\": \"patchTusUpload\",\n \"response\": {\n \"headers\": {\n \"Upload-Offset\": \"7\"\n },\n \"statusCode\": 204\n },\n \"url\": \"upload\"\n }\n ],\n \"stateAfterAttempt\": \"failed\"\n },\n {\n \"attemptIndex\": 1,\n \"requests\": [\n {\n \"headers\": {},\n \"operationId\": \"getTusUploadOffset\",\n \"response\": {\n \"headers\": {\n \"Upload-Length\": \"14\",\n \"Upload-Offset\": \"7\"\n },\n \"statusCode\": 200\n },\n \"url\": \"upload\"\n },\n {\n \"bodySize\": 7,\n \"headers\": {\n \"Upload-Offset\": \"7\"\n },\n \"operationId\": \"patchTusUpload\",\n \"response\": {\n \"headers\": {\n \"Upload-Offset\": \"14\"\n },\n \"statusCode\": 204\n },\n \"url\": \"upload\"\n }\n ],\n \"stateAfterAttempt\": \"succeeded\"\n }\n ],\n \"cleanup\": {\n \"ownedSource\": \"remove-owned-source-after-success\",\n \"resumeUrl\": \"remove-after-success\"\n },\n \"input\": {\n \"chunkSize\": 7,\n \"content\": \"hello managed!\",\n \"fingerprint\": \"managed-durable-retry-fingerprint\",\n \"metadata\": {\n \"filename\": \"managed.txt\"\n },\n \"uploadPath\": \"managed-durable-retry\"\n },\n \"retryDelays\": [\n 0\n ],\n \"sourceDurability\": \"copy-to-owned-storage\",\n \"states\": [\n \"pending\",\n \"running\",\n \"failed\",\n \"running\",\n \"succeeded\"\n ],\n \"terminal\": {\n \"state\": \"succeeded\"\n },\n \"runtime\": \"java\",\n \"scheduler\": \"process-lifetime-worker-pool\",\n \"stateBackend\": \"filesystem\"\n },\n {\n \"attempts\": [\n {\n \"attemptIndex\": 0,\n \"failure\": {\n \"afterAcceptedOffset\": 7,\n \"kind\": \"io-error\",\n \"phase\": \"after-accepted-offset\"\n },\n \"requests\": [\n {\n \"bodySize\": 0,\n \"headers\": {\n \"Upload-Length\": \"14\"\n },\n \"operationId\": \"createTusUpload\",\n \"response\": {\n \"headers\": {\n \"Location\": \"https://tus.io/uploads/managed-durable-retry\"\n },\n \"statusCode\": 201\n },\n \"url\": \"endpoint\"\n },\n {\n \"bodySize\": 7,\n \"headers\": {\n \"Upload-Offset\": \"0\"\n },\n \"operationId\": \"patchTusUpload\",\n \"response\": {\n \"headers\": {\n \"Upload-Offset\": \"7\"\n },\n \"statusCode\": 204\n },\n \"url\": \"upload\"\n }\n ],\n \"stateAfterAttempt\": \"failed\"\n },\n {\n \"attemptIndex\": 1,\n \"requests\": [\n {\n \"headers\": {},\n \"operationId\": \"getTusUploadOffset\",\n \"response\": {\n \"headers\": {\n \"Upload-Length\": \"14\",\n \"Upload-Offset\": \"7\"\n },\n \"statusCode\": 200\n },\n \"url\": \"upload\"\n },\n {\n \"bodySize\": 7,\n \"headers\": {\n \"Upload-Offset\": \"7\"\n },\n \"operationId\": \"patchTusUpload\",\n \"response\": {\n \"headers\": {\n \"Upload-Offset\": \"14\"\n },\n \"statusCode\": 204\n },\n \"url\": \"upload\"\n }\n ],\n \"stateAfterAttempt\": \"succeeded\"\n }\n ],\n \"cleanup\": {\n \"ownedSource\": \"remove-owned-source-after-success\",\n \"resumeUrl\": \"remove-after-success\"\n },\n \"input\": {\n \"chunkSize\": 7,\n \"content\": \"hello managed!\",\n \"fingerprint\": \"managed-durable-retry-fingerprint\",\n \"metadata\": {\n \"filename\": \"managed.txt\"\n },\n \"uploadPath\": \"managed-durable-retry\"\n },\n \"retryDelays\": [\n 0\n ],\n \"sourceDurability\": \"copy-to-owned-storage\",\n \"states\": [\n \"pending\",\n \"running\",\n \"failed\",\n \"running\",\n \"succeeded\"\n ],\n \"terminal\": {\n \"state\": \"succeeded\"\n },\n \"runtime\": \"android\",\n \"scheduler\": \"durable-os-scheduler\",\n \"stateBackend\": \"platform-key-value-store\"\n }\n ],\n \"requiredPrimitives\": [\n \"accept-upload-submission\",\n \"make-source-durable\",\n \"schedule-upload-work\",\n \"run-protocol-upload\",\n \"apply-managed-retry-policy\",\n \"publish-upload-state\",\n \"cleanup-managed-upload\"\n ],\n \"scenarioId\": \"managedUploadDurableRetry\",\n \"summary\": \"Submit a durable source, survive scheduler/process interruption, resume by stored upload URL, and finish with cleanup.\"\n },\n {\n \"proofs\": [\n {\n \"attempts\": [\n {\n \"attemptIndex\": 0,\n \"failure\": {\n \"kind\": \"unretryable-protocol-error\",\n \"phase\": \"during-protocol-request\"\n },\n \"requests\": [\n {\n \"bodySize\": 0,\n \"headers\": {\n \"Upload-Length\": \"14\"\n },\n \"operationId\": \"createTusUpload\",\n \"response\": {\n \"headers\": {},\n \"statusCode\": 400\n },\n \"url\": \"endpoint\"\n }\n ],\n \"stateAfterAttempt\": \"failed\"\n }\n ],\n \"cleanup\": {\n \"ownedSource\": \"retain-owned-source-after-permanent-failure\",\n \"resumeUrl\": \"absent-after-permanent-failure\"\n },\n \"input\": {\n \"chunkSize\": 7,\n \"content\": \"hello failure!\",\n \"fingerprint\": \"managed-permanent-failure-fingerprint\",\n \"metadata\": {\n \"filename\": \"managed-permanent-failure.txt\"\n },\n \"uploadPath\": \"managed-permanent-failure\"\n },\n \"retryDelays\": [],\n \"sourceDurability\": \"copy-to-owned-storage\",\n \"states\": [\n \"pending\",\n \"running\",\n \"failed\"\n ],\n \"terminal\": {\n \"failure\": \"unretryable-protocol-error\",\n \"state\": \"failed\"\n },\n \"runtime\": \"java\",\n \"scheduler\": \"process-lifetime-worker-pool\",\n \"stateBackend\": \"filesystem\"\n },\n {\n \"attempts\": [\n {\n \"attemptIndex\": 0,\n \"failure\": {\n \"kind\": \"unretryable-protocol-error\",\n \"phase\": \"during-protocol-request\"\n },\n \"requests\": [\n {\n \"bodySize\": 0,\n \"headers\": {\n \"Upload-Length\": \"14\"\n },\n \"operationId\": \"createTusUpload\",\n \"response\": {\n \"headers\": {},\n \"statusCode\": 400\n },\n \"url\": \"endpoint\"\n }\n ],\n \"stateAfterAttempt\": \"failed\"\n }\n ],\n \"cleanup\": {\n \"ownedSource\": \"retain-owned-source-after-permanent-failure\",\n \"resumeUrl\": \"absent-after-permanent-failure\"\n },\n \"input\": {\n \"chunkSize\": 7,\n \"content\": \"hello failure!\",\n \"fingerprint\": \"managed-permanent-failure-fingerprint\",\n \"metadata\": {\n \"filename\": \"managed-permanent-failure.txt\"\n },\n \"uploadPath\": \"managed-permanent-failure\"\n },\n \"retryDelays\": [],\n \"sourceDurability\": \"copy-to-owned-storage\",\n \"states\": [\n \"pending\",\n \"running\",\n \"failed\"\n ],\n \"terminal\": {\n \"failure\": \"unretryable-protocol-error\",\n \"state\": \"failed\"\n },\n \"runtime\": \"android\",\n \"scheduler\": \"durable-os-scheduler\",\n \"stateBackend\": \"platform-key-value-store\"\n }\n ],\n \"requiredPrimitives\": [\n \"accept-upload-submission\",\n \"make-source-durable\",\n \"schedule-upload-work\",\n \"run-protocol-upload\",\n \"classify-failure\",\n \"publish-upload-state\",\n \"cleanup-managed-upload\"\n ],\n \"scenarioId\": \"managedUploadPermanentFailure\",\n \"summary\": \"Classify missing sources and unretryable protocol failures as terminal without further retry.\"\n },\n {\n \"proofs\": [\n {\n \"attempts\": [\n {\n \"attemptIndex\": 0,\n \"failure\": {\n \"kind\": \"retryable-protocol-error\",\n \"phase\": \"during-protocol-request\"\n },\n \"requests\": [\n {\n \"bodySize\": 0,\n \"headers\": {\n \"Upload-Length\": \"14\"\n },\n \"operationId\": \"createTusUpload\",\n \"response\": {\n \"headers\": {},\n \"statusCode\": 500\n },\n \"url\": \"endpoint\"\n }\n ],\n \"stateAfterAttempt\": \"failed\"\n },\n {\n \"attemptIndex\": 1,\n \"failure\": {\n \"kind\": \"retryable-protocol-error\",\n \"phase\": \"during-protocol-request\"\n },\n \"requests\": [\n {\n \"bodySize\": 0,\n \"headers\": {\n \"Upload-Length\": \"14\"\n },\n \"operationId\": \"createTusUpload\",\n \"response\": {\n \"headers\": {},\n \"statusCode\": 500\n },\n \"url\": \"endpoint\"\n }\n ],\n \"stateAfterAttempt\": \"failed\"\n },\n {\n \"attemptIndex\": 2,\n \"failure\": {\n \"kind\": \"retryable-protocol-error\",\n \"phase\": \"during-protocol-request\"\n },\n \"requests\": [\n {\n \"bodySize\": 0,\n \"headers\": {\n \"Upload-Length\": \"14\"\n },\n \"operationId\": \"createTusUpload\",\n \"response\": {\n \"headers\": {},\n \"statusCode\": 500\n },\n \"url\": \"endpoint\"\n }\n ],\n \"stateAfterAttempt\": \"failed\"\n }\n ],\n \"cleanup\": {\n \"ownedSource\": \"retain-owned-source-after-permanent-failure\",\n \"resumeUrl\": \"absent-after-permanent-failure\"\n },\n \"input\": {\n \"chunkSize\": 7,\n \"content\": \"hello retries!\",\n \"fingerprint\": \"managed-retry-exhausted-fingerprint\",\n \"metadata\": {\n \"filename\": \"managed-retry-exhausted.txt\"\n },\n \"uploadPath\": \"managed-retry-exhausted\"\n },\n \"retryDelays\": [\n 0,\n 0\n ],\n \"sourceDurability\": \"copy-to-owned-storage\",\n \"states\": [\n \"pending\",\n \"running\",\n \"failed\",\n \"running\",\n \"failed\",\n \"running\",\n \"failed\"\n ],\n \"terminal\": {\n \"failure\": \"retry-policy-exhausted\",\n \"state\": \"failed\"\n },\n \"runtime\": \"java\",\n \"scheduler\": \"process-lifetime-worker-pool\",\n \"stateBackend\": \"filesystem\"\n },\n {\n \"attempts\": [\n {\n \"attemptIndex\": 0,\n \"failure\": {\n \"kind\": \"retryable-protocol-error\",\n \"phase\": \"during-protocol-request\"\n },\n \"requests\": [\n {\n \"bodySize\": 0,\n \"headers\": {\n \"Upload-Length\": \"14\"\n },\n \"operationId\": \"createTusUpload\",\n \"response\": {\n \"headers\": {},\n \"statusCode\": 500\n },\n \"url\": \"endpoint\"\n }\n ],\n \"stateAfterAttempt\": \"failed\"\n },\n {\n \"attemptIndex\": 1,\n \"failure\": {\n \"kind\": \"retryable-protocol-error\",\n \"phase\": \"during-protocol-request\"\n },\n \"requests\": [\n {\n \"bodySize\": 0,\n \"headers\": {\n \"Upload-Length\": \"14\"\n },\n \"operationId\": \"createTusUpload\",\n \"response\": {\n \"headers\": {},\n \"statusCode\": 500\n },\n \"url\": \"endpoint\"\n }\n ],\n \"stateAfterAttempt\": \"failed\"\n },\n {\n \"attemptIndex\": 2,\n \"failure\": {\n \"kind\": \"retryable-protocol-error\",\n \"phase\": \"during-protocol-request\"\n },\n \"requests\": [\n {\n \"bodySize\": 0,\n \"headers\": {\n \"Upload-Length\": \"14\"\n },\n \"operationId\": \"createTusUpload\",\n \"response\": {\n \"headers\": {},\n \"statusCode\": 500\n },\n \"url\": \"endpoint\"\n }\n ],\n \"stateAfterAttempt\": \"failed\"\n }\n ],\n \"cleanup\": {\n \"ownedSource\": \"retain-owned-source-after-permanent-failure\",\n \"resumeUrl\": \"absent-after-permanent-failure\"\n },\n \"input\": {\n \"chunkSize\": 7,\n \"content\": \"hello retries!\",\n \"fingerprint\": \"managed-retry-exhausted-fingerprint\",\n \"metadata\": {\n \"filename\": \"managed-retry-exhausted.txt\"\n },\n \"uploadPath\": \"managed-retry-exhausted\"\n },\n \"retryDelays\": [\n 0,\n 0\n ],\n \"sourceDurability\": \"copy-to-owned-storage\",\n \"states\": [\n \"pending\",\n \"running\",\n \"failed\",\n \"running\",\n \"failed\",\n \"running\",\n \"failed\"\n ],\n \"terminal\": {\n \"failure\": \"retry-policy-exhausted\",\n \"state\": \"failed\"\n },\n \"runtime\": \"android\",\n \"scheduler\": \"durable-os-scheduler\",\n \"stateBackend\": \"platform-key-value-store\"\n }\n ],\n \"requiredPrimitives\": [\n \"accept-upload-submission\",\n \"make-source-durable\",\n \"schedule-upload-work\",\n \"run-protocol-upload\",\n \"apply-managed-retry-policy\",\n \"classify-failure\",\n \"publish-upload-state\",\n \"cleanup-managed-upload\"\n ],\n \"scenarioId\": \"managedUploadRetryPolicyExhausted\",\n \"summary\": \"Retry transient protocol failures up to the managed retry budget and then classify the upload as terminally failed.\"\n },\n {\n \"requiredPrimitives\": [\n \"accept-upload-submission\",\n \"schedule-upload-work\",\n \"publish-upload-state\"\n ],\n \"scenarioId\": \"managedUploadNetworkConstraint\",\n \"summary\": \"Honor network constraints before starting or resuming upload work.\"\n }\n ]\n}\n"; + static final String MANAGED_UPLOAD_JSON = "{\n \"capabilities\": {\n \"cleanup\": {\n \"policies\": [\n \"absent-after-source-unavailable\",\n \"remove-owned-source-after-success\",\n \"remove-owned-source-after-cancel\",\n \"retain-owned-source-after-permanent-failure\",\n \"retain-source-after-retryable-failure\",\n \"remove-managed-state-after-terminal-retention\"\n ]\n },\n \"failureClassification\": {\n \"permanentFailures\": [\n \"source-unavailable\",\n \"unretryable-protocol-error\",\n \"retry-policy-exhausted\"\n ],\n \"retryableFailures\": [\n \"retryable-protocol-error\",\n \"io-error\",\n \"network-unavailable\"\n ]\n },\n \"networkConstraints\": {\n \"options\": [\n \"any-network\",\n \"unmetered-network\"\n ]\n },\n \"retryPolicy\": {\n \"controls\": [\n \"max-attempts\",\n \"deadline\",\n \"progress-sensitive-budget\",\n \"unbounded-until-permanent-failure\"\n ],\n \"permanentFailure\": \"stop-without-retry\",\n \"progressReset\": \"reset-budget-after-accepted-offset-advances\"\n },\n \"scheduling\": {\n \"strategies\": [\n \"foreground-task\",\n \"process-lifetime-worker-pool\",\n \"durable-os-scheduler\"\n ]\n },\n \"sourceDurability\": {\n \"ownedCopyCleanup\": \"after-success-or-cancel\",\n \"strategies\": [\n \"copy-to-owned-storage\",\n \"reference-original-source\",\n \"memory-only\"\n ]\n },\n \"stateReporting\": {\n \"states\": [\n \"pending\",\n \"running\",\n \"succeeded\",\n \"failed\"\n ],\n \"terminalRetention\": \"session-and-next-launch\",\n \"transientRetention\": \"until-terminal\"\n }\n },\n \"conformance\": {\n \"scenarioIds\": [\n \"managedUploadDurableRetry\",\n \"managedUploadPermanentFailure\",\n \"managedUploadRetryPolicyExhausted\",\n \"managedUploadSourceUnavailable\",\n \"managedUploadNetworkConstraint\"\n ],\n \"status\": \"needs-generated-scenario\"\n },\n \"description\": \"Submit upload work that can make sources durable, schedule/resume execution, retry, report state, and clean up while reusing the raw TUS protocol features underneath.\",\n \"featureId\": \"managedUpload\",\n \"flow\": [\n {\n \"kind\": \"managed-primitive\",\n \"primitive\": \"accept-upload-submission\",\n \"summary\": \"Accept source, metadata, headers, endpoint, and retry/scheduling policy.\"\n },\n {\n \"kind\": \"managed-primitive\",\n \"primitive\": \"make-source-durable\",\n \"summary\": \"Keep the source readable according to the selected runtime durability strategy.\"\n },\n {\n \"kind\": \"managed-primitive\",\n \"primitive\": \"schedule-upload-work\",\n \"summary\": \"Run upload work according to the runtime scheduler capability.\"\n },\n {\n \"featureId\": \"singleUploadLifecycle\",\n \"kind\": \"protocol-feature\",\n \"summary\": \"Use the raw protocol upload lifecycle for each execution attempt.\"\n },\n {\n \"featureId\": \"retryOffsetRecovery\",\n \"kind\": \"protocol-feature\",\n \"summary\": \"Use protocol retry and offset recovery before classifying terminal failure.\"\n },\n {\n \"kind\": \"managed-primitive\",\n \"primitive\": \"publish-upload-state\",\n \"summary\": \"Expose pending, running, succeeded, and failed state snapshots.\"\n },\n {\n \"kind\": \"managed-primitive\",\n \"primitive\": \"cleanup-managed-upload\",\n \"summary\": \"Remove owned sources and terminal state according to cleanup policy.\"\n }\n ],\n \"layer\": \"feature-over-protocol\",\n \"primitives\": [\n \"accept-upload-submission\",\n \"make-source-durable\",\n \"schedule-upload-work\",\n \"run-protocol-upload\",\n \"apply-managed-retry-policy\",\n \"classify-failure\",\n \"publish-upload-state\",\n \"cleanup-managed-upload\"\n ],\n \"protocolPrimitives\": [\n \"store-resume-url\",\n \"resume-from-previous-upload\",\n \"recover-offset-after-error\",\n \"retry-with-backoff\",\n \"emit-progress\",\n \"emit-chunk-complete\",\n \"terminate-upload\"\n ],\n \"runtimeProfiles\": [\n {\n \"networkConstraints\": [\n \"any-network\",\n \"unmetered-network\"\n ],\n \"runtime\": \"android\",\n \"scheduler\": \"durable-os-scheduler\",\n \"sourceDurability\": [\n \"copy-to-owned-storage\",\n \"reference-original-source\"\n ],\n \"stateBackend\": \"platform-key-value-store\"\n },\n {\n \"networkConstraints\": [\n \"any-network\",\n \"unmetered-network\"\n ],\n \"runtime\": \"ios\",\n \"scheduler\": \"durable-os-scheduler\",\n \"sourceDurability\": [\n \"copy-to-owned-storage\",\n \"reference-original-source\"\n ],\n \"stateBackend\": \"platform-key-value-store\"\n },\n {\n \"networkConstraints\": [\n \"any-network\"\n ],\n \"runtime\": \"browser\",\n \"scheduler\": \"foreground-task\",\n \"sourceDurability\": [\n \"reference-original-source\",\n \"memory-only\"\n ],\n \"stateBackend\": \"web-storage\"\n },\n {\n \"networkConstraints\": [\n \"any-network\"\n ],\n \"runtime\": \"java\",\n \"scheduler\": \"process-lifetime-worker-pool\",\n \"sourceDurability\": [\n \"copy-to-owned-storage\",\n \"reference-original-source\"\n ],\n \"stateBackend\": \"filesystem\"\n },\n {\n \"networkConstraints\": [\n \"any-network\"\n ],\n \"runtime\": \"node\",\n \"scheduler\": \"process-lifetime-worker-pool\",\n \"sourceDurability\": [\n \"copy-to-owned-storage\",\n \"reference-original-source\",\n \"memory-only\"\n ],\n \"stateBackend\": \"filesystem\"\n },\n {\n \"networkConstraints\": [\n \"any-network\"\n ],\n \"runtime\": \"react-native\",\n \"scheduler\": \"foreground-task\",\n \"sourceDurability\": [\n \"reference-original-source\",\n \"memory-only\"\n ],\n \"stateBackend\": \"platform-key-value-store\"\n }\n ],\n \"scenarios\": [\n {\n \"proofs\": [\n {\n \"attempts\": [\n {\n \"attemptIndex\": 0,\n \"failure\": {\n \"afterAcceptedOffset\": 7,\n \"kind\": \"io-error\",\n \"phase\": \"after-accepted-offset\"\n },\n \"requests\": [\n {\n \"bodySize\": 0,\n \"headers\": {\n \"Upload-Length\": \"14\"\n },\n \"operationId\": \"createTusUpload\",\n \"response\": {\n \"headers\": {\n \"Location\": \"https://tus.io/uploads/managed-durable-retry\"\n },\n \"statusCode\": 201\n },\n \"url\": \"endpoint\"\n },\n {\n \"bodySize\": 7,\n \"headers\": {\n \"Upload-Offset\": \"0\"\n },\n \"operationId\": \"patchTusUpload\",\n \"response\": {\n \"headers\": {\n \"Upload-Offset\": \"7\"\n },\n \"statusCode\": 204\n },\n \"url\": \"upload\"\n }\n ],\n \"stateAfterAttempt\": \"failed\"\n },\n {\n \"attemptIndex\": 1,\n \"requests\": [\n {\n \"headers\": {},\n \"operationId\": \"getTusUploadOffset\",\n \"response\": {\n \"headers\": {\n \"Upload-Length\": \"14\",\n \"Upload-Offset\": \"7\"\n },\n \"statusCode\": 200\n },\n \"url\": \"upload\"\n },\n {\n \"bodySize\": 7,\n \"headers\": {\n \"Upload-Offset\": \"7\"\n },\n \"operationId\": \"patchTusUpload\",\n \"response\": {\n \"headers\": {\n \"Upload-Offset\": \"14\"\n },\n \"statusCode\": 204\n },\n \"url\": \"upload\"\n }\n ],\n \"stateAfterAttempt\": \"succeeded\"\n }\n ],\n \"cleanup\": {\n \"ownedSource\": \"remove-owned-source-after-success\",\n \"resumeUrl\": \"remove-after-success\"\n },\n \"input\": {\n \"chunkSize\": 7,\n \"content\": \"hello managed!\",\n \"fingerprint\": \"managed-durable-retry-fingerprint\",\n \"metadata\": {\n \"filename\": \"managed.txt\"\n },\n \"uploadPath\": \"managed-durable-retry\"\n },\n \"retryDelays\": [\n 0\n ],\n \"sourceAvailability\": \"available\",\n \"sourceDurability\": \"copy-to-owned-storage\",\n \"states\": [\n \"pending\",\n \"running\",\n \"failed\",\n \"running\",\n \"succeeded\"\n ],\n \"terminal\": {\n \"state\": \"succeeded\"\n },\n \"runtime\": \"java\",\n \"scheduler\": \"process-lifetime-worker-pool\",\n \"stateBackend\": \"filesystem\"\n },\n {\n \"attempts\": [\n {\n \"attemptIndex\": 0,\n \"failure\": {\n \"afterAcceptedOffset\": 7,\n \"kind\": \"io-error\",\n \"phase\": \"after-accepted-offset\"\n },\n \"requests\": [\n {\n \"bodySize\": 0,\n \"headers\": {\n \"Upload-Length\": \"14\"\n },\n \"operationId\": \"createTusUpload\",\n \"response\": {\n \"headers\": {\n \"Location\": \"https://tus.io/uploads/managed-durable-retry\"\n },\n \"statusCode\": 201\n },\n \"url\": \"endpoint\"\n },\n {\n \"bodySize\": 7,\n \"headers\": {\n \"Upload-Offset\": \"0\"\n },\n \"operationId\": \"patchTusUpload\",\n \"response\": {\n \"headers\": {\n \"Upload-Offset\": \"7\"\n },\n \"statusCode\": 204\n },\n \"url\": \"upload\"\n }\n ],\n \"stateAfterAttempt\": \"failed\"\n },\n {\n \"attemptIndex\": 1,\n \"requests\": [\n {\n \"headers\": {},\n \"operationId\": \"getTusUploadOffset\",\n \"response\": {\n \"headers\": {\n \"Upload-Length\": \"14\",\n \"Upload-Offset\": \"7\"\n },\n \"statusCode\": 200\n },\n \"url\": \"upload\"\n },\n {\n \"bodySize\": 7,\n \"headers\": {\n \"Upload-Offset\": \"7\"\n },\n \"operationId\": \"patchTusUpload\",\n \"response\": {\n \"headers\": {\n \"Upload-Offset\": \"14\"\n },\n \"statusCode\": 204\n },\n \"url\": \"upload\"\n }\n ],\n \"stateAfterAttempt\": \"succeeded\"\n }\n ],\n \"cleanup\": {\n \"ownedSource\": \"remove-owned-source-after-success\",\n \"resumeUrl\": \"remove-after-success\"\n },\n \"input\": {\n \"chunkSize\": 7,\n \"content\": \"hello managed!\",\n \"fingerprint\": \"managed-durable-retry-fingerprint\",\n \"metadata\": {\n \"filename\": \"managed.txt\"\n },\n \"uploadPath\": \"managed-durable-retry\"\n },\n \"retryDelays\": [\n 0\n ],\n \"sourceAvailability\": \"available\",\n \"sourceDurability\": \"copy-to-owned-storage\",\n \"states\": [\n \"pending\",\n \"running\",\n \"failed\",\n \"running\",\n \"succeeded\"\n ],\n \"terminal\": {\n \"state\": \"succeeded\"\n },\n \"runtime\": \"android\",\n \"scheduler\": \"durable-os-scheduler\",\n \"stateBackend\": \"platform-key-value-store\"\n }\n ],\n \"requiredPrimitives\": [\n \"accept-upload-submission\",\n \"make-source-durable\",\n \"schedule-upload-work\",\n \"run-protocol-upload\",\n \"apply-managed-retry-policy\",\n \"publish-upload-state\",\n \"cleanup-managed-upload\"\n ],\n \"scenarioId\": \"managedUploadDurableRetry\",\n \"summary\": \"Submit a durable source, survive scheduler/process interruption, resume by stored upload URL, and finish with cleanup.\"\n },\n {\n \"proofs\": [\n {\n \"attempts\": [\n {\n \"attemptIndex\": 0,\n \"failure\": {\n \"kind\": \"unretryable-protocol-error\",\n \"phase\": \"during-protocol-request\"\n },\n \"requests\": [\n {\n \"bodySize\": 0,\n \"headers\": {\n \"Upload-Length\": \"14\"\n },\n \"operationId\": \"createTusUpload\",\n \"response\": {\n \"headers\": {},\n \"statusCode\": 400\n },\n \"url\": \"endpoint\"\n }\n ],\n \"stateAfterAttempt\": \"failed\"\n }\n ],\n \"cleanup\": {\n \"ownedSource\": \"retain-owned-source-after-permanent-failure\",\n \"resumeUrl\": \"absent-after-permanent-failure\"\n },\n \"input\": {\n \"chunkSize\": 7,\n \"content\": \"hello failure!\",\n \"fingerprint\": \"managed-permanent-failure-fingerprint\",\n \"metadata\": {\n \"filename\": \"managed-permanent-failure.txt\"\n },\n \"uploadPath\": \"managed-permanent-failure\"\n },\n \"retryDelays\": [],\n \"sourceAvailability\": \"available\",\n \"sourceDurability\": \"copy-to-owned-storage\",\n \"states\": [\n \"pending\",\n \"running\",\n \"failed\"\n ],\n \"terminal\": {\n \"failure\": \"unretryable-protocol-error\",\n \"state\": \"failed\"\n },\n \"runtime\": \"java\",\n \"scheduler\": \"process-lifetime-worker-pool\",\n \"stateBackend\": \"filesystem\"\n },\n {\n \"attempts\": [\n {\n \"attemptIndex\": 0,\n \"failure\": {\n \"kind\": \"unretryable-protocol-error\",\n \"phase\": \"during-protocol-request\"\n },\n \"requests\": [\n {\n \"bodySize\": 0,\n \"headers\": {\n \"Upload-Length\": \"14\"\n },\n \"operationId\": \"createTusUpload\",\n \"response\": {\n \"headers\": {},\n \"statusCode\": 400\n },\n \"url\": \"endpoint\"\n }\n ],\n \"stateAfterAttempt\": \"failed\"\n }\n ],\n \"cleanup\": {\n \"ownedSource\": \"retain-owned-source-after-permanent-failure\",\n \"resumeUrl\": \"absent-after-permanent-failure\"\n },\n \"input\": {\n \"chunkSize\": 7,\n \"content\": \"hello failure!\",\n \"fingerprint\": \"managed-permanent-failure-fingerprint\",\n \"metadata\": {\n \"filename\": \"managed-permanent-failure.txt\"\n },\n \"uploadPath\": \"managed-permanent-failure\"\n },\n \"retryDelays\": [],\n \"sourceAvailability\": \"available\",\n \"sourceDurability\": \"copy-to-owned-storage\",\n \"states\": [\n \"pending\",\n \"running\",\n \"failed\"\n ],\n \"terminal\": {\n \"failure\": \"unretryable-protocol-error\",\n \"state\": \"failed\"\n },\n \"runtime\": \"android\",\n \"scheduler\": \"durable-os-scheduler\",\n \"stateBackend\": \"platform-key-value-store\"\n }\n ],\n \"requiredPrimitives\": [\n \"accept-upload-submission\",\n \"make-source-durable\",\n \"schedule-upload-work\",\n \"run-protocol-upload\",\n \"classify-failure\",\n \"publish-upload-state\",\n \"cleanup-managed-upload\"\n ],\n \"scenarioId\": \"managedUploadPermanentFailure\",\n \"summary\": \"Classify unretryable protocol failures as terminal without further retry.\"\n },\n {\n \"proofs\": [\n {\n \"attempts\": [\n {\n \"attemptIndex\": 0,\n \"failure\": {\n \"kind\": \"retryable-protocol-error\",\n \"phase\": \"during-protocol-request\"\n },\n \"requests\": [\n {\n \"bodySize\": 0,\n \"headers\": {\n \"Upload-Length\": \"14\"\n },\n \"operationId\": \"createTusUpload\",\n \"response\": {\n \"headers\": {},\n \"statusCode\": 500\n },\n \"url\": \"endpoint\"\n }\n ],\n \"stateAfterAttempt\": \"failed\"\n },\n {\n \"attemptIndex\": 1,\n \"failure\": {\n \"kind\": \"retryable-protocol-error\",\n \"phase\": \"during-protocol-request\"\n },\n \"requests\": [\n {\n \"bodySize\": 0,\n \"headers\": {\n \"Upload-Length\": \"14\"\n },\n \"operationId\": \"createTusUpload\",\n \"response\": {\n \"headers\": {},\n \"statusCode\": 500\n },\n \"url\": \"endpoint\"\n }\n ],\n \"stateAfterAttempt\": \"failed\"\n },\n {\n \"attemptIndex\": 2,\n \"failure\": {\n \"kind\": \"retryable-protocol-error\",\n \"phase\": \"during-protocol-request\"\n },\n \"requests\": [\n {\n \"bodySize\": 0,\n \"headers\": {\n \"Upload-Length\": \"14\"\n },\n \"operationId\": \"createTusUpload\",\n \"response\": {\n \"headers\": {},\n \"statusCode\": 500\n },\n \"url\": \"endpoint\"\n }\n ],\n \"stateAfterAttempt\": \"failed\"\n }\n ],\n \"cleanup\": {\n \"ownedSource\": \"retain-owned-source-after-permanent-failure\",\n \"resumeUrl\": \"absent-after-permanent-failure\"\n },\n \"input\": {\n \"chunkSize\": 7,\n \"content\": \"hello retries!\",\n \"fingerprint\": \"managed-retry-exhausted-fingerprint\",\n \"metadata\": {\n \"filename\": \"managed-retry-exhausted.txt\"\n },\n \"uploadPath\": \"managed-retry-exhausted\"\n },\n \"retryDelays\": [\n 0,\n 0\n ],\n \"sourceAvailability\": \"available\",\n \"sourceDurability\": \"copy-to-owned-storage\",\n \"states\": [\n \"pending\",\n \"running\",\n \"failed\",\n \"running\",\n \"failed\",\n \"running\",\n \"failed\"\n ],\n \"terminal\": {\n \"failure\": \"retry-policy-exhausted\",\n \"state\": \"failed\"\n },\n \"runtime\": \"java\",\n \"scheduler\": \"process-lifetime-worker-pool\",\n \"stateBackend\": \"filesystem\"\n },\n {\n \"attempts\": [\n {\n \"attemptIndex\": 0,\n \"failure\": {\n \"kind\": \"retryable-protocol-error\",\n \"phase\": \"during-protocol-request\"\n },\n \"requests\": [\n {\n \"bodySize\": 0,\n \"headers\": {\n \"Upload-Length\": \"14\"\n },\n \"operationId\": \"createTusUpload\",\n \"response\": {\n \"headers\": {},\n \"statusCode\": 500\n },\n \"url\": \"endpoint\"\n }\n ],\n \"stateAfterAttempt\": \"failed\"\n },\n {\n \"attemptIndex\": 1,\n \"failure\": {\n \"kind\": \"retryable-protocol-error\",\n \"phase\": \"during-protocol-request\"\n },\n \"requests\": [\n {\n \"bodySize\": 0,\n \"headers\": {\n \"Upload-Length\": \"14\"\n },\n \"operationId\": \"createTusUpload\",\n \"response\": {\n \"headers\": {},\n \"statusCode\": 500\n },\n \"url\": \"endpoint\"\n }\n ],\n \"stateAfterAttempt\": \"failed\"\n },\n {\n \"attemptIndex\": 2,\n \"failure\": {\n \"kind\": \"retryable-protocol-error\",\n \"phase\": \"during-protocol-request\"\n },\n \"requests\": [\n {\n \"bodySize\": 0,\n \"headers\": {\n \"Upload-Length\": \"14\"\n },\n \"operationId\": \"createTusUpload\",\n \"response\": {\n \"headers\": {},\n \"statusCode\": 500\n },\n \"url\": \"endpoint\"\n }\n ],\n \"stateAfterAttempt\": \"failed\"\n }\n ],\n \"cleanup\": {\n \"ownedSource\": \"retain-owned-source-after-permanent-failure\",\n \"resumeUrl\": \"absent-after-permanent-failure\"\n },\n \"input\": {\n \"chunkSize\": 7,\n \"content\": \"hello retries!\",\n \"fingerprint\": \"managed-retry-exhausted-fingerprint\",\n \"metadata\": {\n \"filename\": \"managed-retry-exhausted.txt\"\n },\n \"uploadPath\": \"managed-retry-exhausted\"\n },\n \"retryDelays\": [\n 0,\n 0\n ],\n \"sourceAvailability\": \"available\",\n \"sourceDurability\": \"copy-to-owned-storage\",\n \"states\": [\n \"pending\",\n \"running\",\n \"failed\",\n \"running\",\n \"failed\",\n \"running\",\n \"failed\"\n ],\n \"terminal\": {\n \"failure\": \"retry-policy-exhausted\",\n \"state\": \"failed\"\n },\n \"runtime\": \"android\",\n \"scheduler\": \"durable-os-scheduler\",\n \"stateBackend\": \"platform-key-value-store\"\n }\n ],\n \"requiredPrimitives\": [\n \"accept-upload-submission\",\n \"make-source-durable\",\n \"schedule-upload-work\",\n \"run-protocol-upload\",\n \"apply-managed-retry-policy\",\n \"classify-failure\",\n \"publish-upload-state\",\n \"cleanup-managed-upload\"\n ],\n \"scenarioId\": \"managedUploadRetryPolicyExhausted\",\n \"summary\": \"Retry transient protocol failures up to the managed retry budget and then classify the upload as terminally failed.\"\n },\n {\n \"proofs\": [\n {\n \"attempts\": [\n {\n \"attemptIndex\": 0,\n \"failure\": {\n \"kind\": \"source-unavailable\",\n \"phase\": \"before-protocol-request\"\n },\n \"requests\": [],\n \"stateAfterAttempt\": \"failed\"\n }\n ],\n \"cleanup\": {\n \"ownedSource\": \"absent-after-source-unavailable\",\n \"resumeUrl\": \"absent-after-permanent-failure\"\n },\n \"input\": {\n \"chunkSize\": 7,\n \"content\": \"hello missing!\",\n \"fingerprint\": \"managed-source-unavailable-fingerprint\",\n \"metadata\": {\n \"filename\": \"managed-source-unavailable.txt\"\n },\n \"uploadPath\": \"managed-source-unavailable\"\n },\n \"retryDelays\": [],\n \"sourceAvailability\": \"missing-before-durable-copy\",\n \"sourceDurability\": \"copy-to-owned-storage\",\n \"states\": [\n \"pending\",\n \"running\",\n \"failed\"\n ],\n \"terminal\": {\n \"failure\": \"source-unavailable\",\n \"state\": \"failed\"\n },\n \"runtime\": \"java\",\n \"scheduler\": \"process-lifetime-worker-pool\",\n \"stateBackend\": \"filesystem\"\n },\n {\n \"attempts\": [\n {\n \"attemptIndex\": 0,\n \"failure\": {\n \"kind\": \"source-unavailable\",\n \"phase\": \"before-protocol-request\"\n },\n \"requests\": [],\n \"stateAfterAttempt\": \"failed\"\n }\n ],\n \"cleanup\": {\n \"ownedSource\": \"absent-after-source-unavailable\",\n \"resumeUrl\": \"absent-after-permanent-failure\"\n },\n \"input\": {\n \"chunkSize\": 7,\n \"content\": \"hello missing!\",\n \"fingerprint\": \"managed-source-unavailable-fingerprint\",\n \"metadata\": {\n \"filename\": \"managed-source-unavailable.txt\"\n },\n \"uploadPath\": \"managed-source-unavailable\"\n },\n \"retryDelays\": [],\n \"sourceAvailability\": \"missing-before-durable-copy\",\n \"sourceDurability\": \"copy-to-owned-storage\",\n \"states\": [\n \"pending\",\n \"running\",\n \"failed\"\n ],\n \"terminal\": {\n \"failure\": \"source-unavailable\",\n \"state\": \"failed\"\n },\n \"runtime\": \"android\",\n \"scheduler\": \"durable-os-scheduler\",\n \"stateBackend\": \"platform-key-value-store\"\n }\n ],\n \"requiredPrimitives\": [\n \"accept-upload-submission\",\n \"make-source-durable\",\n \"schedule-upload-work\",\n \"classify-failure\",\n \"publish-upload-state\",\n \"cleanup-managed-upload\"\n ],\n \"scenarioId\": \"managedUploadSourceUnavailable\",\n \"summary\": \"Classify source disappearance before protocol requests as terminal without issuing a TUS request.\"\n },\n {\n \"requiredPrimitives\": [\n \"accept-upload-submission\",\n \"schedule-upload-work\",\n \"publish-upload-state\"\n ],\n \"scenarioId\": \"managedUploadNetworkConstraint\",\n \"summary\": \"Honor network constraints before starting or resuming upload work.\"\n }\n ]\n}\n"; static final String[] MANAGED_UPLOAD_PRIMITIVES = new String[] { @@ -1150,6 +1150,7 @@ final class GeneratedTusProtocolContract { "managedUploadDurableRetry", "managedUploadPermanentFailure", "managedUploadRetryPolicyExhausted", + "managedUploadSourceUnavailable", "managedUploadNetworkConstraint", }; @@ -1246,6 +1247,35 @@ final class GeneratedTusProtocolContract { "react-native", } ), + new GeneratedTusProtocolContract.GeneratedTusManagedUploadProofCase( + "managedUpload", + "feature-over-protocol", + "managedUploadSourceUnavailable", + new String[] { + "java", + "android", + }, + new String[] { + "accept-upload-submission", + "make-source-durable", + "schedule-upload-work", + "classify-failure", + "publish-upload-state", + "cleanup-managed-upload", + }, + new String[] { + "singleUploadLifecycle", + "retryOffsetRecovery", + }, + new String[] { + "android", + "ios", + "browser", + "java", + "node", + "react-native", + } + ), new GeneratedTusProtocolContract.GeneratedTusManagedUploadProofCase( "managedUpload", "feature-over-protocol", diff --git a/src/test/java/io/tus/java/client/TestGeneratedTusManagedUploadRuntime.java b/src/test/java/io/tus/java/client/TestGeneratedTusManagedUploadRuntime.java index c8324f5a..65ed0fa3 100644 --- a/src/test/java/io/tus/java/client/TestGeneratedTusManagedUploadRuntime.java +++ b/src/test/java/io/tus/java/client/TestGeneratedTusManagedUploadRuntime.java @@ -43,6 +43,7 @@ public class TestGeneratedTusManagedUploadRuntime extends MockServerProvider { "java", "process-lifetime-worker-pool", "copy-to-owned-storage", + "available", "filesystem" ), new GeneratedTusManagedUploadTransport( @@ -178,6 +179,7 @@ public class TestGeneratedTusManagedUploadRuntime extends MockServerProvider { "java", "process-lifetime-worker-pool", "copy-to-owned-storage", + "available", "filesystem" ), new GeneratedTusManagedUploadTransport( @@ -244,6 +246,7 @@ public class TestGeneratedTusManagedUploadRuntime extends MockServerProvider { "java", "process-lifetime-worker-pool", "copy-to-owned-storage", + "available", "filesystem" ), new GeneratedTusManagedUploadTransport( @@ -359,6 +362,61 @@ public class TestGeneratedTusManagedUploadRuntime extends MockServerProvider { ), } ), + new GeneratedTusManagedUploadRuntimeCase( + new GeneratedTusManagedUploadRuntimeProfile( + "managedUploadSourceUnavailable", + "java", + "process-lifetime-worker-pool", + "copy-to-owned-storage", + "missing-before-durable-copy", + "filesystem" + ), + new GeneratedTusManagedUploadTransport( + "Location" + ), + new GeneratedTusManagedUploadTerminal( + "failed", + "source-unavailable" + ), + new GeneratedTusManagedUploadCleanup( + "absent-after-source-unavailable", + "absent-after-permanent-failure" + ), + new GeneratedTusManagedUploadRetryPlan( + new String[] { + "pending", + "running", + "failed", + }, + new int[0] + ), + new GeneratedTusManagedUploadInput( + "hello missing!", + 7, + "managed-source-unavailable-fingerprint", + "managed-source-unavailable", + new GeneratedTusManagedUploadMetadata[] { + new GeneratedTusManagedUploadMetadata( + "filename", + "managed-source-unavailable.txt" + ), + } + ), + new GeneratedTusManagedUploadAttempt[] { + new GeneratedTusManagedUploadAttempt( + 0, + "failed", + new GeneratedTusManagedUploadFailure( + "before-protocol-request", + "source-unavailable", + -1 + ), + new GeneratedTusManagedUploadRequest[] { + + } + ), + } + ), }; private static final GeneratedTusMethodOverride[] METHOD_OVERRIDES = new GeneratedTusMethodOverride[] { @@ -383,7 +441,6 @@ public void testManagedUploadDurableRetryRuntime() throws Exception { File source = writeSourceFile(testCase); File ownedSource = ownedSourceFile(testCase, source); File stateFile = stateFile(testCase, source); - copyDurableSource(testCase, source, ownedSource); recordState(testCase, states, stateFile, "pending"); final GeneratedTusManagedUploadUrlStore urlStore = new GeneratedTusManagedUploadUrlStore(); @@ -392,18 +449,26 @@ public void testManagedUploadDurableRetryRuntime() throws Exception { client.enableResuming(urlStore); client.enableRemoveFingerprintOnSuccess(); - TusExecutor executor = managedExecutorFor(testCase, client, ownedSource, states, stateFile); - ExecutorService worker = Executors.newSingleThreadExecutor(); try { - Future future = worker.submit(new Callable() { - @Override - public Boolean call() throws Exception { - return executor.makeAttempts(); - } - }); - assertTerminalResult(testCase, future); - } finally { - worker.shutdownNow(); + prepareSourceBeforeProtocol(testCase, source, ownedSource, states, stateFile); + TusExecutor executor = managedExecutorFor(testCase, client, ownedSource, states, stateFile); + ExecutorService worker = Executors.newSingleThreadExecutor(); + try { + Future future = worker.submit(new Callable() { + @Override + public Boolean call() throws Exception { + return executor.makeAttempts(); + } + }); + assertTerminalResult(testCase, future); + } finally { + worker.shutdownNow(); + } + } catch (IOException error) { + if (!isSourceUnavailableBeforeProtocol(testCase)) { + throw error; + } + assertTerminalFailure(testCase, error); } cleanupAfterTerminalState(testCase, ownedSource); @@ -419,8 +484,8 @@ public Boolean call() throws Exception { .toArray(new String[testCase.expectedStates.length])); assertResumeUrlState(testCase, urlStore); assertOwnedSourceState(testCase, ownedSource); - assertTrue(testCase.scenarioId, source.exists()); - source.delete(); + assertInputSourceState(testCase, source); + assertProtocolRequestCount(testCase); stateFile.delete(); } } @@ -560,6 +625,42 @@ private void copyDurableSource( assertTrue(testCase.scenarioId, ownedSource.exists()); } + private void prepareSourceBeforeProtocol( + GeneratedTusManagedUploadRuntimeCase testCase, + File source, + File ownedSource, + List states, + File stateFile) throws IOException { + if ("available".equals(testCase.sourceAvailability)) { + copyDurableSource(testCase, source, ownedSource); + return; + } + if ("missing-before-durable-copy".equals(testCase.sourceAvailability)) { + GeneratedTusManagedUploadAttempt attempt = testCase.attempts[0]; + if (source.exists() && !source.delete()) { + throw new IOException("Could not remove generated input source " + source); + } + recordState(testCase, states, stateFile, "running"); + try { + copyDurableSource(testCase, source, ownedSource); + } catch (IOException error) { + recordState(testCase, states, stateFile, attempt.stateAfterAttempt); + throw error; + } + throw new AssertionError(testCase.scenarioId + " unexpectedly prepared missing source"); + } + + throw new AssertionError( + testCase.scenarioId + + " uses unsupported generated source availability " + + testCase.sourceAvailability); + } + + private boolean isSourceUnavailableBeforeProtocol(GeneratedTusManagedUploadRuntimeCase testCase) { + return "source-unavailable".equals(testCase.terminalFailure) + && "missing-before-durable-copy".equals(testCase.sourceAvailability); + } + private void cleanupAfterTerminalState( GeneratedTusManagedUploadRuntimeCase testCase, File ownedSource) throws IOException { @@ -582,6 +683,10 @@ private void assertOwnedSourceState( ownedSource.delete(); return; } + if ("absent-after-source-unavailable".equals(testCase.ownedSourceCleanup)) { + assertFalse(testCase.scenarioId, ownedSource.exists()); + return; + } throw new AssertionError( testCase.scenarioId @@ -589,6 +694,18 @@ private void assertOwnedSourceState( + testCase.ownedSourceCleanup); } + private void assertInputSourceState( + GeneratedTusManagedUploadRuntimeCase testCase, + File source) { + if ("missing-before-durable-copy".equals(testCase.sourceAvailability)) { + assertFalse(testCase.scenarioId, source.exists()); + return; + } + + assertTrue(testCase.scenarioId, source.exists()); + source.delete(); + } + private void assertResumeUrlState( GeneratedTusManagedUploadRuntimeCase testCase, GeneratedTusManagedUploadUrlStore urlStore) { @@ -605,6 +722,21 @@ private void assertResumeUrlState( + testCase.resumeUrlCleanup); } + private void assertProtocolRequestCount(GeneratedTusManagedUploadRuntimeCase testCase) { + HttpRequest[] requests = mockServer.retrieveRecordedRequests(new HttpRequest()); + assertTrue( + testCase.scenarioId, + requests.length == expectedProtocolRequestCount(testCase)); + } + + private int expectedProtocolRequestCount(GeneratedTusManagedUploadRuntimeCase testCase) { + int count = 0; + for (GeneratedTusManagedUploadAttempt attempt : testCase.attempts) { + count += attempt.requests.length; + } + return count; + } + private void recordState( GeneratedTusManagedUploadRuntimeCase testCase, List states, @@ -732,6 +864,7 @@ private static final class GeneratedTusManagedUploadRuntimeCase { final String runtime; final String scheduler; final String sourceDurability; + final String sourceAvailability; final String stateBackend; final String locationHeaderName; final String terminalState; @@ -756,6 +889,7 @@ private static final class GeneratedTusManagedUploadRuntimeCase { this.runtime = profile.runtime; this.scheduler = profile.scheduler; this.sourceDurability = profile.sourceDurability; + this.sourceAvailability = profile.sourceAvailability; this.stateBackend = profile.stateBackend; this.locationHeaderName = transport.locationHeaderName; this.terminalState = terminal.state; @@ -785,6 +919,7 @@ private static final class GeneratedTusManagedUploadRuntimeProfile { final String runtime; final String scheduler; final String sourceDurability; + final String sourceAvailability; final String stateBackend; GeneratedTusManagedUploadRuntimeProfile( @@ -792,11 +927,13 @@ private static final class GeneratedTusManagedUploadRuntimeProfile { String runtime, String scheduler, String sourceDurability, + String sourceAvailability, String stateBackend) { this.scenarioId = scenarioId; this.runtime = runtime; this.scheduler = scheduler; this.sourceDurability = sourceDurability; + this.sourceAvailability = sourceAvailability; this.stateBackend = stateBackend; } } From 729c107adc84f75a69953478866700707aa25726 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Mon, 1 Jun 2026 14:22:41 +0200 Subject: [PATCH 39/43] Add generated managed network deferral proof --- .../client/GeneratedTusProtocolContract.java | 7 +- .../TestGeneratedTusManagedUploadRuntime.java | 187 ++++++++++++++---- 2 files changed, 153 insertions(+), 41 deletions(-) diff --git a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java index 7b7b06c7..cba28960 100644 --- a/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java +++ b/src/test/java/io/tus/java/client/GeneratedTusProtocolContract.java @@ -1121,7 +1121,7 @@ final class GeneratedTusProtocolContract { ), }; - static final String MANAGED_UPLOAD_JSON = "{\n \"capabilities\": {\n \"cleanup\": {\n \"policies\": [\n \"absent-after-source-unavailable\",\n \"remove-owned-source-after-success\",\n \"remove-owned-source-after-cancel\",\n \"retain-owned-source-after-permanent-failure\",\n \"retain-source-after-retryable-failure\",\n \"remove-managed-state-after-terminal-retention\"\n ]\n },\n \"failureClassification\": {\n \"permanentFailures\": [\n \"source-unavailable\",\n \"unretryable-protocol-error\",\n \"retry-policy-exhausted\"\n ],\n \"retryableFailures\": [\n \"retryable-protocol-error\",\n \"io-error\",\n \"network-unavailable\"\n ]\n },\n \"networkConstraints\": {\n \"options\": [\n \"any-network\",\n \"unmetered-network\"\n ]\n },\n \"retryPolicy\": {\n \"controls\": [\n \"max-attempts\",\n \"deadline\",\n \"progress-sensitive-budget\",\n \"unbounded-until-permanent-failure\"\n ],\n \"permanentFailure\": \"stop-without-retry\",\n \"progressReset\": \"reset-budget-after-accepted-offset-advances\"\n },\n \"scheduling\": {\n \"strategies\": [\n \"foreground-task\",\n \"process-lifetime-worker-pool\",\n \"durable-os-scheduler\"\n ]\n },\n \"sourceDurability\": {\n \"ownedCopyCleanup\": \"after-success-or-cancel\",\n \"strategies\": [\n \"copy-to-owned-storage\",\n \"reference-original-source\",\n \"memory-only\"\n ]\n },\n \"stateReporting\": {\n \"states\": [\n \"pending\",\n \"running\",\n \"succeeded\",\n \"failed\"\n ],\n \"terminalRetention\": \"session-and-next-launch\",\n \"transientRetention\": \"until-terminal\"\n }\n },\n \"conformance\": {\n \"scenarioIds\": [\n \"managedUploadDurableRetry\",\n \"managedUploadPermanentFailure\",\n \"managedUploadRetryPolicyExhausted\",\n \"managedUploadSourceUnavailable\",\n \"managedUploadNetworkConstraint\"\n ],\n \"status\": \"needs-generated-scenario\"\n },\n \"description\": \"Submit upload work that can make sources durable, schedule/resume execution, retry, report state, and clean up while reusing the raw TUS protocol features underneath.\",\n \"featureId\": \"managedUpload\",\n \"flow\": [\n {\n \"kind\": \"managed-primitive\",\n \"primitive\": \"accept-upload-submission\",\n \"summary\": \"Accept source, metadata, headers, endpoint, and retry/scheduling policy.\"\n },\n {\n \"kind\": \"managed-primitive\",\n \"primitive\": \"make-source-durable\",\n \"summary\": \"Keep the source readable according to the selected runtime durability strategy.\"\n },\n {\n \"kind\": \"managed-primitive\",\n \"primitive\": \"schedule-upload-work\",\n \"summary\": \"Run upload work according to the runtime scheduler capability.\"\n },\n {\n \"featureId\": \"singleUploadLifecycle\",\n \"kind\": \"protocol-feature\",\n \"summary\": \"Use the raw protocol upload lifecycle for each execution attempt.\"\n },\n {\n \"featureId\": \"retryOffsetRecovery\",\n \"kind\": \"protocol-feature\",\n \"summary\": \"Use protocol retry and offset recovery before classifying terminal failure.\"\n },\n {\n \"kind\": \"managed-primitive\",\n \"primitive\": \"publish-upload-state\",\n \"summary\": \"Expose pending, running, succeeded, and failed state snapshots.\"\n },\n {\n \"kind\": \"managed-primitive\",\n \"primitive\": \"cleanup-managed-upload\",\n \"summary\": \"Remove owned sources and terminal state according to cleanup policy.\"\n }\n ],\n \"layer\": \"feature-over-protocol\",\n \"primitives\": [\n \"accept-upload-submission\",\n \"make-source-durable\",\n \"schedule-upload-work\",\n \"run-protocol-upload\",\n \"apply-managed-retry-policy\",\n \"classify-failure\",\n \"publish-upload-state\",\n \"cleanup-managed-upload\"\n ],\n \"protocolPrimitives\": [\n \"store-resume-url\",\n \"resume-from-previous-upload\",\n \"recover-offset-after-error\",\n \"retry-with-backoff\",\n \"emit-progress\",\n \"emit-chunk-complete\",\n \"terminate-upload\"\n ],\n \"runtimeProfiles\": [\n {\n \"networkConstraints\": [\n \"any-network\",\n \"unmetered-network\"\n ],\n \"runtime\": \"android\",\n \"scheduler\": \"durable-os-scheduler\",\n \"sourceDurability\": [\n \"copy-to-owned-storage\",\n \"reference-original-source\"\n ],\n \"stateBackend\": \"platform-key-value-store\"\n },\n {\n \"networkConstraints\": [\n \"any-network\",\n \"unmetered-network\"\n ],\n \"runtime\": \"ios\",\n \"scheduler\": \"durable-os-scheduler\",\n \"sourceDurability\": [\n \"copy-to-owned-storage\",\n \"reference-original-source\"\n ],\n \"stateBackend\": \"platform-key-value-store\"\n },\n {\n \"networkConstraints\": [\n \"any-network\"\n ],\n \"runtime\": \"browser\",\n \"scheduler\": \"foreground-task\",\n \"sourceDurability\": [\n \"reference-original-source\",\n \"memory-only\"\n ],\n \"stateBackend\": \"web-storage\"\n },\n {\n \"networkConstraints\": [\n \"any-network\"\n ],\n \"runtime\": \"java\",\n \"scheduler\": \"process-lifetime-worker-pool\",\n \"sourceDurability\": [\n \"copy-to-owned-storage\",\n \"reference-original-source\"\n ],\n \"stateBackend\": \"filesystem\"\n },\n {\n \"networkConstraints\": [\n \"any-network\"\n ],\n \"runtime\": \"node\",\n \"scheduler\": \"process-lifetime-worker-pool\",\n \"sourceDurability\": [\n \"copy-to-owned-storage\",\n \"reference-original-source\",\n \"memory-only\"\n ],\n \"stateBackend\": \"filesystem\"\n },\n {\n \"networkConstraints\": [\n \"any-network\"\n ],\n \"runtime\": \"react-native\",\n \"scheduler\": \"foreground-task\",\n \"sourceDurability\": [\n \"reference-original-source\",\n \"memory-only\"\n ],\n \"stateBackend\": \"platform-key-value-store\"\n }\n ],\n \"scenarios\": [\n {\n \"proofs\": [\n {\n \"attempts\": [\n {\n \"attemptIndex\": 0,\n \"failure\": {\n \"afterAcceptedOffset\": 7,\n \"kind\": \"io-error\",\n \"phase\": \"after-accepted-offset\"\n },\n \"requests\": [\n {\n \"bodySize\": 0,\n \"headers\": {\n \"Upload-Length\": \"14\"\n },\n \"operationId\": \"createTusUpload\",\n \"response\": {\n \"headers\": {\n \"Location\": \"https://tus.io/uploads/managed-durable-retry\"\n },\n \"statusCode\": 201\n },\n \"url\": \"endpoint\"\n },\n {\n \"bodySize\": 7,\n \"headers\": {\n \"Upload-Offset\": \"0\"\n },\n \"operationId\": \"patchTusUpload\",\n \"response\": {\n \"headers\": {\n \"Upload-Offset\": \"7\"\n },\n \"statusCode\": 204\n },\n \"url\": \"upload\"\n }\n ],\n \"stateAfterAttempt\": \"failed\"\n },\n {\n \"attemptIndex\": 1,\n \"requests\": [\n {\n \"headers\": {},\n \"operationId\": \"getTusUploadOffset\",\n \"response\": {\n \"headers\": {\n \"Upload-Length\": \"14\",\n \"Upload-Offset\": \"7\"\n },\n \"statusCode\": 200\n },\n \"url\": \"upload\"\n },\n {\n \"bodySize\": 7,\n \"headers\": {\n \"Upload-Offset\": \"7\"\n },\n \"operationId\": \"patchTusUpload\",\n \"response\": {\n \"headers\": {\n \"Upload-Offset\": \"14\"\n },\n \"statusCode\": 204\n },\n \"url\": \"upload\"\n }\n ],\n \"stateAfterAttempt\": \"succeeded\"\n }\n ],\n \"cleanup\": {\n \"ownedSource\": \"remove-owned-source-after-success\",\n \"resumeUrl\": \"remove-after-success\"\n },\n \"input\": {\n \"chunkSize\": 7,\n \"content\": \"hello managed!\",\n \"fingerprint\": \"managed-durable-retry-fingerprint\",\n \"metadata\": {\n \"filename\": \"managed.txt\"\n },\n \"uploadPath\": \"managed-durable-retry\"\n },\n \"retryDelays\": [\n 0\n ],\n \"sourceAvailability\": \"available\",\n \"sourceDurability\": \"copy-to-owned-storage\",\n \"states\": [\n \"pending\",\n \"running\",\n \"failed\",\n \"running\",\n \"succeeded\"\n ],\n \"terminal\": {\n \"state\": \"succeeded\"\n },\n \"runtime\": \"java\",\n \"scheduler\": \"process-lifetime-worker-pool\",\n \"stateBackend\": \"filesystem\"\n },\n {\n \"attempts\": [\n {\n \"attemptIndex\": 0,\n \"failure\": {\n \"afterAcceptedOffset\": 7,\n \"kind\": \"io-error\",\n \"phase\": \"after-accepted-offset\"\n },\n \"requests\": [\n {\n \"bodySize\": 0,\n \"headers\": {\n \"Upload-Length\": \"14\"\n },\n \"operationId\": \"createTusUpload\",\n \"response\": {\n \"headers\": {\n \"Location\": \"https://tus.io/uploads/managed-durable-retry\"\n },\n \"statusCode\": 201\n },\n \"url\": \"endpoint\"\n },\n {\n \"bodySize\": 7,\n \"headers\": {\n \"Upload-Offset\": \"0\"\n },\n \"operationId\": \"patchTusUpload\",\n \"response\": {\n \"headers\": {\n \"Upload-Offset\": \"7\"\n },\n \"statusCode\": 204\n },\n \"url\": \"upload\"\n }\n ],\n \"stateAfterAttempt\": \"failed\"\n },\n {\n \"attemptIndex\": 1,\n \"requests\": [\n {\n \"headers\": {},\n \"operationId\": \"getTusUploadOffset\",\n \"response\": {\n \"headers\": {\n \"Upload-Length\": \"14\",\n \"Upload-Offset\": \"7\"\n },\n \"statusCode\": 200\n },\n \"url\": \"upload\"\n },\n {\n \"bodySize\": 7,\n \"headers\": {\n \"Upload-Offset\": \"7\"\n },\n \"operationId\": \"patchTusUpload\",\n \"response\": {\n \"headers\": {\n \"Upload-Offset\": \"14\"\n },\n \"statusCode\": 204\n },\n \"url\": \"upload\"\n }\n ],\n \"stateAfterAttempt\": \"succeeded\"\n }\n ],\n \"cleanup\": {\n \"ownedSource\": \"remove-owned-source-after-success\",\n \"resumeUrl\": \"remove-after-success\"\n },\n \"input\": {\n \"chunkSize\": 7,\n \"content\": \"hello managed!\",\n \"fingerprint\": \"managed-durable-retry-fingerprint\",\n \"metadata\": {\n \"filename\": \"managed.txt\"\n },\n \"uploadPath\": \"managed-durable-retry\"\n },\n \"retryDelays\": [\n 0\n ],\n \"sourceAvailability\": \"available\",\n \"sourceDurability\": \"copy-to-owned-storage\",\n \"states\": [\n \"pending\",\n \"running\",\n \"failed\",\n \"running\",\n \"succeeded\"\n ],\n \"terminal\": {\n \"state\": \"succeeded\"\n },\n \"runtime\": \"android\",\n \"scheduler\": \"durable-os-scheduler\",\n \"stateBackend\": \"platform-key-value-store\"\n }\n ],\n \"requiredPrimitives\": [\n \"accept-upload-submission\",\n \"make-source-durable\",\n \"schedule-upload-work\",\n \"run-protocol-upload\",\n \"apply-managed-retry-policy\",\n \"publish-upload-state\",\n \"cleanup-managed-upload\"\n ],\n \"scenarioId\": \"managedUploadDurableRetry\",\n \"summary\": \"Submit a durable source, survive scheduler/process interruption, resume by stored upload URL, and finish with cleanup.\"\n },\n {\n \"proofs\": [\n {\n \"attempts\": [\n {\n \"attemptIndex\": 0,\n \"failure\": {\n \"kind\": \"unretryable-protocol-error\",\n \"phase\": \"during-protocol-request\"\n },\n \"requests\": [\n {\n \"bodySize\": 0,\n \"headers\": {\n \"Upload-Length\": \"14\"\n },\n \"operationId\": \"createTusUpload\",\n \"response\": {\n \"headers\": {},\n \"statusCode\": 400\n },\n \"url\": \"endpoint\"\n }\n ],\n \"stateAfterAttempt\": \"failed\"\n }\n ],\n \"cleanup\": {\n \"ownedSource\": \"retain-owned-source-after-permanent-failure\",\n \"resumeUrl\": \"absent-after-permanent-failure\"\n },\n \"input\": {\n \"chunkSize\": 7,\n \"content\": \"hello failure!\",\n \"fingerprint\": \"managed-permanent-failure-fingerprint\",\n \"metadata\": {\n \"filename\": \"managed-permanent-failure.txt\"\n },\n \"uploadPath\": \"managed-permanent-failure\"\n },\n \"retryDelays\": [],\n \"sourceAvailability\": \"available\",\n \"sourceDurability\": \"copy-to-owned-storage\",\n \"states\": [\n \"pending\",\n \"running\",\n \"failed\"\n ],\n \"terminal\": {\n \"failure\": \"unretryable-protocol-error\",\n \"state\": \"failed\"\n },\n \"runtime\": \"java\",\n \"scheduler\": \"process-lifetime-worker-pool\",\n \"stateBackend\": \"filesystem\"\n },\n {\n \"attempts\": [\n {\n \"attemptIndex\": 0,\n \"failure\": {\n \"kind\": \"unretryable-protocol-error\",\n \"phase\": \"during-protocol-request\"\n },\n \"requests\": [\n {\n \"bodySize\": 0,\n \"headers\": {\n \"Upload-Length\": \"14\"\n },\n \"operationId\": \"createTusUpload\",\n \"response\": {\n \"headers\": {},\n \"statusCode\": 400\n },\n \"url\": \"endpoint\"\n }\n ],\n \"stateAfterAttempt\": \"failed\"\n }\n ],\n \"cleanup\": {\n \"ownedSource\": \"retain-owned-source-after-permanent-failure\",\n \"resumeUrl\": \"absent-after-permanent-failure\"\n },\n \"input\": {\n \"chunkSize\": 7,\n \"content\": \"hello failure!\",\n \"fingerprint\": \"managed-permanent-failure-fingerprint\",\n \"metadata\": {\n \"filename\": \"managed-permanent-failure.txt\"\n },\n \"uploadPath\": \"managed-permanent-failure\"\n },\n \"retryDelays\": [],\n \"sourceAvailability\": \"available\",\n \"sourceDurability\": \"copy-to-owned-storage\",\n \"states\": [\n \"pending\",\n \"running\",\n \"failed\"\n ],\n \"terminal\": {\n \"failure\": \"unretryable-protocol-error\",\n \"state\": \"failed\"\n },\n \"runtime\": \"android\",\n \"scheduler\": \"durable-os-scheduler\",\n \"stateBackend\": \"platform-key-value-store\"\n }\n ],\n \"requiredPrimitives\": [\n \"accept-upload-submission\",\n \"make-source-durable\",\n \"schedule-upload-work\",\n \"run-protocol-upload\",\n \"classify-failure\",\n \"publish-upload-state\",\n \"cleanup-managed-upload\"\n ],\n \"scenarioId\": \"managedUploadPermanentFailure\",\n \"summary\": \"Classify unretryable protocol failures as terminal without further retry.\"\n },\n {\n \"proofs\": [\n {\n \"attempts\": [\n {\n \"attemptIndex\": 0,\n \"failure\": {\n \"kind\": \"retryable-protocol-error\",\n \"phase\": \"during-protocol-request\"\n },\n \"requests\": [\n {\n \"bodySize\": 0,\n \"headers\": {\n \"Upload-Length\": \"14\"\n },\n \"operationId\": \"createTusUpload\",\n \"response\": {\n \"headers\": {},\n \"statusCode\": 500\n },\n \"url\": \"endpoint\"\n }\n ],\n \"stateAfterAttempt\": \"failed\"\n },\n {\n \"attemptIndex\": 1,\n \"failure\": {\n \"kind\": \"retryable-protocol-error\",\n \"phase\": \"during-protocol-request\"\n },\n \"requests\": [\n {\n \"bodySize\": 0,\n \"headers\": {\n \"Upload-Length\": \"14\"\n },\n \"operationId\": \"createTusUpload\",\n \"response\": {\n \"headers\": {},\n \"statusCode\": 500\n },\n \"url\": \"endpoint\"\n }\n ],\n \"stateAfterAttempt\": \"failed\"\n },\n {\n \"attemptIndex\": 2,\n \"failure\": {\n \"kind\": \"retryable-protocol-error\",\n \"phase\": \"during-protocol-request\"\n },\n \"requests\": [\n {\n \"bodySize\": 0,\n \"headers\": {\n \"Upload-Length\": \"14\"\n },\n \"operationId\": \"createTusUpload\",\n \"response\": {\n \"headers\": {},\n \"statusCode\": 500\n },\n \"url\": \"endpoint\"\n }\n ],\n \"stateAfterAttempt\": \"failed\"\n }\n ],\n \"cleanup\": {\n \"ownedSource\": \"retain-owned-source-after-permanent-failure\",\n \"resumeUrl\": \"absent-after-permanent-failure\"\n },\n \"input\": {\n \"chunkSize\": 7,\n \"content\": \"hello retries!\",\n \"fingerprint\": \"managed-retry-exhausted-fingerprint\",\n \"metadata\": {\n \"filename\": \"managed-retry-exhausted.txt\"\n },\n \"uploadPath\": \"managed-retry-exhausted\"\n },\n \"retryDelays\": [\n 0,\n 0\n ],\n \"sourceAvailability\": \"available\",\n \"sourceDurability\": \"copy-to-owned-storage\",\n \"states\": [\n \"pending\",\n \"running\",\n \"failed\",\n \"running\",\n \"failed\",\n \"running\",\n \"failed\"\n ],\n \"terminal\": {\n \"failure\": \"retry-policy-exhausted\",\n \"state\": \"failed\"\n },\n \"runtime\": \"java\",\n \"scheduler\": \"process-lifetime-worker-pool\",\n \"stateBackend\": \"filesystem\"\n },\n {\n \"attempts\": [\n {\n \"attemptIndex\": 0,\n \"failure\": {\n \"kind\": \"retryable-protocol-error\",\n \"phase\": \"during-protocol-request\"\n },\n \"requests\": [\n {\n \"bodySize\": 0,\n \"headers\": {\n \"Upload-Length\": \"14\"\n },\n \"operationId\": \"createTusUpload\",\n \"response\": {\n \"headers\": {},\n \"statusCode\": 500\n },\n \"url\": \"endpoint\"\n }\n ],\n \"stateAfterAttempt\": \"failed\"\n },\n {\n \"attemptIndex\": 1,\n \"failure\": {\n \"kind\": \"retryable-protocol-error\",\n \"phase\": \"during-protocol-request\"\n },\n \"requests\": [\n {\n \"bodySize\": 0,\n \"headers\": {\n \"Upload-Length\": \"14\"\n },\n \"operationId\": \"createTusUpload\",\n \"response\": {\n \"headers\": {},\n \"statusCode\": 500\n },\n \"url\": \"endpoint\"\n }\n ],\n \"stateAfterAttempt\": \"failed\"\n },\n {\n \"attemptIndex\": 2,\n \"failure\": {\n \"kind\": \"retryable-protocol-error\",\n \"phase\": \"during-protocol-request\"\n },\n \"requests\": [\n {\n \"bodySize\": 0,\n \"headers\": {\n \"Upload-Length\": \"14\"\n },\n \"operationId\": \"createTusUpload\",\n \"response\": {\n \"headers\": {},\n \"statusCode\": 500\n },\n \"url\": \"endpoint\"\n }\n ],\n \"stateAfterAttempt\": \"failed\"\n }\n ],\n \"cleanup\": {\n \"ownedSource\": \"retain-owned-source-after-permanent-failure\",\n \"resumeUrl\": \"absent-after-permanent-failure\"\n },\n \"input\": {\n \"chunkSize\": 7,\n \"content\": \"hello retries!\",\n \"fingerprint\": \"managed-retry-exhausted-fingerprint\",\n \"metadata\": {\n \"filename\": \"managed-retry-exhausted.txt\"\n },\n \"uploadPath\": \"managed-retry-exhausted\"\n },\n \"retryDelays\": [\n 0,\n 0\n ],\n \"sourceAvailability\": \"available\",\n \"sourceDurability\": \"copy-to-owned-storage\",\n \"states\": [\n \"pending\",\n \"running\",\n \"failed\",\n \"running\",\n \"failed\",\n \"running\",\n \"failed\"\n ],\n \"terminal\": {\n \"failure\": \"retry-policy-exhausted\",\n \"state\": \"failed\"\n },\n \"runtime\": \"android\",\n \"scheduler\": \"durable-os-scheduler\",\n \"stateBackend\": \"platform-key-value-store\"\n }\n ],\n \"requiredPrimitives\": [\n \"accept-upload-submission\",\n \"make-source-durable\",\n \"schedule-upload-work\",\n \"run-protocol-upload\",\n \"apply-managed-retry-policy\",\n \"classify-failure\",\n \"publish-upload-state\",\n \"cleanup-managed-upload\"\n ],\n \"scenarioId\": \"managedUploadRetryPolicyExhausted\",\n \"summary\": \"Retry transient protocol failures up to the managed retry budget and then classify the upload as terminally failed.\"\n },\n {\n \"proofs\": [\n {\n \"attempts\": [\n {\n \"attemptIndex\": 0,\n \"failure\": {\n \"kind\": \"source-unavailable\",\n \"phase\": \"before-protocol-request\"\n },\n \"requests\": [],\n \"stateAfterAttempt\": \"failed\"\n }\n ],\n \"cleanup\": {\n \"ownedSource\": \"absent-after-source-unavailable\",\n \"resumeUrl\": \"absent-after-permanent-failure\"\n },\n \"input\": {\n \"chunkSize\": 7,\n \"content\": \"hello missing!\",\n \"fingerprint\": \"managed-source-unavailable-fingerprint\",\n \"metadata\": {\n \"filename\": \"managed-source-unavailable.txt\"\n },\n \"uploadPath\": \"managed-source-unavailable\"\n },\n \"retryDelays\": [],\n \"sourceAvailability\": \"missing-before-durable-copy\",\n \"sourceDurability\": \"copy-to-owned-storage\",\n \"states\": [\n \"pending\",\n \"running\",\n \"failed\"\n ],\n \"terminal\": {\n \"failure\": \"source-unavailable\",\n \"state\": \"failed\"\n },\n \"runtime\": \"java\",\n \"scheduler\": \"process-lifetime-worker-pool\",\n \"stateBackend\": \"filesystem\"\n },\n {\n \"attempts\": [\n {\n \"attemptIndex\": 0,\n \"failure\": {\n \"kind\": \"source-unavailable\",\n \"phase\": \"before-protocol-request\"\n },\n \"requests\": [],\n \"stateAfterAttempt\": \"failed\"\n }\n ],\n \"cleanup\": {\n \"ownedSource\": \"absent-after-source-unavailable\",\n \"resumeUrl\": \"absent-after-permanent-failure\"\n },\n \"input\": {\n \"chunkSize\": 7,\n \"content\": \"hello missing!\",\n \"fingerprint\": \"managed-source-unavailable-fingerprint\",\n \"metadata\": {\n \"filename\": \"managed-source-unavailable.txt\"\n },\n \"uploadPath\": \"managed-source-unavailable\"\n },\n \"retryDelays\": [],\n \"sourceAvailability\": \"missing-before-durable-copy\",\n \"sourceDurability\": \"copy-to-owned-storage\",\n \"states\": [\n \"pending\",\n \"running\",\n \"failed\"\n ],\n \"terminal\": {\n \"failure\": \"source-unavailable\",\n \"state\": \"failed\"\n },\n \"runtime\": \"android\",\n \"scheduler\": \"durable-os-scheduler\",\n \"stateBackend\": \"platform-key-value-store\"\n }\n ],\n \"requiredPrimitives\": [\n \"accept-upload-submission\",\n \"make-source-durable\",\n \"schedule-upload-work\",\n \"classify-failure\",\n \"publish-upload-state\",\n \"cleanup-managed-upload\"\n ],\n \"scenarioId\": \"managedUploadSourceUnavailable\",\n \"summary\": \"Classify source disappearance before protocol requests as terminal without issuing a TUS request.\"\n },\n {\n \"requiredPrimitives\": [\n \"accept-upload-submission\",\n \"schedule-upload-work\",\n \"publish-upload-state\"\n ],\n \"scenarioId\": \"managedUploadNetworkConstraint\",\n \"summary\": \"Honor network constraints before starting or resuming upload work.\"\n }\n ]\n}\n"; + static final String MANAGED_UPLOAD_JSON = "{\n \"capabilities\": {\n \"cleanup\": {\n \"policies\": [\n \"absent-after-source-unavailable\",\n \"remove-owned-source-after-success\",\n \"remove-owned-source-after-cancel\",\n \"retain-owned-source-while-deferred\",\n \"retain-owned-source-after-permanent-failure\",\n \"retain-source-after-retryable-failure\",\n \"remove-managed-state-after-terminal-retention\"\n ]\n },\n \"failureClassification\": {\n \"permanentFailures\": [\n \"source-unavailable\",\n \"unretryable-protocol-error\",\n \"retry-policy-exhausted\"\n ],\n \"retryableFailures\": [\n \"retryable-protocol-error\",\n \"io-error\",\n \"network-unavailable\"\n ]\n },\n \"networkConstraints\": {\n \"options\": [\n \"any-network\",\n \"unmetered-network\"\n ]\n },\n \"retryPolicy\": {\n \"controls\": [\n \"max-attempts\",\n \"deadline\",\n \"progress-sensitive-budget\",\n \"unbounded-until-permanent-failure\"\n ],\n \"permanentFailure\": \"stop-without-retry\",\n \"progressReset\": \"reset-budget-after-accepted-offset-advances\"\n },\n \"scheduling\": {\n \"strategies\": [\n \"foreground-task\",\n \"process-lifetime-worker-pool\",\n \"durable-os-scheduler\"\n ]\n },\n \"sourceDurability\": {\n \"ownedCopyCleanup\": \"after-success-or-cancel\",\n \"strategies\": [\n \"copy-to-owned-storage\",\n \"reference-original-source\",\n \"memory-only\"\n ]\n },\n \"stateReporting\": {\n \"states\": [\n \"pending\",\n \"running\",\n \"succeeded\",\n \"failed\"\n ],\n \"terminalRetention\": \"session-and-next-launch\",\n \"transientRetention\": \"until-terminal\"\n }\n },\n \"conformance\": {\n \"scenarioIds\": [\n \"managedUploadDurableRetry\",\n \"managedUploadPermanentFailure\",\n \"managedUploadRetryPolicyExhausted\",\n \"managedUploadSourceUnavailable\",\n \"managedUploadNetworkConstraint\"\n ],\n \"status\": \"covered-by-generated-scenario\"\n },\n \"description\": \"Submit upload work that can make sources durable, schedule/resume execution, retry, report state, and clean up while reusing the raw TUS protocol features underneath.\",\n \"featureId\": \"managedUpload\",\n \"flow\": [\n {\n \"kind\": \"managed-primitive\",\n \"primitive\": \"accept-upload-submission\",\n \"summary\": \"Accept source, metadata, headers, endpoint, and retry/scheduling policy.\"\n },\n {\n \"kind\": \"managed-primitive\",\n \"primitive\": \"make-source-durable\",\n \"summary\": \"Keep the source readable according to the selected runtime durability strategy.\"\n },\n {\n \"kind\": \"managed-primitive\",\n \"primitive\": \"schedule-upload-work\",\n \"summary\": \"Run upload work according to the runtime scheduler capability.\"\n },\n {\n \"featureId\": \"singleUploadLifecycle\",\n \"kind\": \"protocol-feature\",\n \"summary\": \"Use the raw protocol upload lifecycle for each execution attempt.\"\n },\n {\n \"featureId\": \"retryOffsetRecovery\",\n \"kind\": \"protocol-feature\",\n \"summary\": \"Use protocol retry and offset recovery before classifying terminal failure.\"\n },\n {\n \"kind\": \"managed-primitive\",\n \"primitive\": \"publish-upload-state\",\n \"summary\": \"Expose pending, running, succeeded, and failed state snapshots.\"\n },\n {\n \"kind\": \"managed-primitive\",\n \"primitive\": \"cleanup-managed-upload\",\n \"summary\": \"Remove owned sources and terminal state according to cleanup policy.\"\n }\n ],\n \"layer\": \"feature-over-protocol\",\n \"primitives\": [\n \"accept-upload-submission\",\n \"make-source-durable\",\n \"schedule-upload-work\",\n \"run-protocol-upload\",\n \"apply-managed-retry-policy\",\n \"classify-failure\",\n \"publish-upload-state\",\n \"cleanup-managed-upload\"\n ],\n \"protocolPrimitives\": [\n \"store-resume-url\",\n \"resume-from-previous-upload\",\n \"recover-offset-after-error\",\n \"retry-with-backoff\",\n \"emit-progress\",\n \"emit-chunk-complete\",\n \"terminate-upload\"\n ],\n \"runtimeProfiles\": [\n {\n \"networkConstraints\": [\n \"any-network\",\n \"unmetered-network\"\n ],\n \"runtime\": \"android\",\n \"scheduler\": \"durable-os-scheduler\",\n \"sourceDurability\": [\n \"copy-to-owned-storage\",\n \"reference-original-source\"\n ],\n \"stateBackend\": \"platform-key-value-store\"\n },\n {\n \"networkConstraints\": [\n \"any-network\",\n \"unmetered-network\"\n ],\n \"runtime\": \"ios\",\n \"scheduler\": \"durable-os-scheduler\",\n \"sourceDurability\": [\n \"copy-to-owned-storage\",\n \"reference-original-source\"\n ],\n \"stateBackend\": \"platform-key-value-store\"\n },\n {\n \"networkConstraints\": [\n \"any-network\"\n ],\n \"runtime\": \"browser\",\n \"scheduler\": \"foreground-task\",\n \"sourceDurability\": [\n \"reference-original-source\",\n \"memory-only\"\n ],\n \"stateBackend\": \"web-storage\"\n },\n {\n \"networkConstraints\": [\n \"any-network\"\n ],\n \"runtime\": \"java\",\n \"scheduler\": \"process-lifetime-worker-pool\",\n \"sourceDurability\": [\n \"copy-to-owned-storage\",\n \"reference-original-source\"\n ],\n \"stateBackend\": \"filesystem\"\n },\n {\n \"networkConstraints\": [\n \"any-network\"\n ],\n \"runtime\": \"node\",\n \"scheduler\": \"process-lifetime-worker-pool\",\n \"sourceDurability\": [\n \"copy-to-owned-storage\",\n \"reference-original-source\",\n \"memory-only\"\n ],\n \"stateBackend\": \"filesystem\"\n },\n {\n \"networkConstraints\": [\n \"any-network\"\n ],\n \"runtime\": \"react-native\",\n \"scheduler\": \"foreground-task\",\n \"sourceDurability\": [\n \"reference-original-source\",\n \"memory-only\"\n ],\n \"stateBackend\": \"platform-key-value-store\"\n }\n ],\n \"scenarios\": [\n {\n \"proofs\": [\n {\n \"attempts\": [\n {\n \"attemptIndex\": 0,\n \"failure\": {\n \"afterAcceptedOffset\": 7,\n \"kind\": \"io-error\",\n \"phase\": \"after-accepted-offset\"\n },\n \"requests\": [\n {\n \"bodySize\": 0,\n \"headers\": {\n \"Upload-Length\": \"14\"\n },\n \"operationId\": \"createTusUpload\",\n \"response\": {\n \"headers\": {\n \"Location\": \"https://tus.io/uploads/managed-durable-retry\"\n },\n \"statusCode\": 201\n },\n \"url\": \"endpoint\"\n },\n {\n \"bodySize\": 7,\n \"headers\": {\n \"Upload-Offset\": \"0\"\n },\n \"operationId\": \"patchTusUpload\",\n \"response\": {\n \"headers\": {\n \"Upload-Offset\": \"7\"\n },\n \"statusCode\": 204\n },\n \"url\": \"upload\"\n }\n ],\n \"stateAfterAttempt\": \"failed\"\n },\n {\n \"attemptIndex\": 1,\n \"requests\": [\n {\n \"headers\": {},\n \"operationId\": \"getTusUploadOffset\",\n \"response\": {\n \"headers\": {\n \"Upload-Length\": \"14\",\n \"Upload-Offset\": \"7\"\n },\n \"statusCode\": 200\n },\n \"url\": \"upload\"\n },\n {\n \"bodySize\": 7,\n \"headers\": {\n \"Upload-Offset\": \"7\"\n },\n \"operationId\": \"patchTusUpload\",\n \"response\": {\n \"headers\": {\n \"Upload-Offset\": \"14\"\n },\n \"statusCode\": 204\n },\n \"url\": \"upload\"\n }\n ],\n \"stateAfterAttempt\": \"succeeded\"\n }\n ],\n \"cleanup\": {\n \"ownedSource\": \"remove-owned-source-after-success\",\n \"resumeUrl\": \"remove-after-success\"\n },\n \"input\": {\n \"chunkSize\": 7,\n \"content\": \"hello managed!\",\n \"fingerprint\": \"managed-durable-retry-fingerprint\",\n \"metadata\": {\n \"filename\": \"managed.txt\"\n },\n \"uploadPath\": \"managed-durable-retry\"\n },\n \"network\": {\n \"current\": \"unmetered-network\",\n \"decision\": \"start-upload-work\",\n \"required\": \"any-network\"\n },\n \"outcome\": {\n \"kind\": \"terminal\",\n \"state\": \"succeeded\"\n },\n \"retryDelays\": [\n 0\n ],\n \"sourceAvailability\": \"available\",\n \"sourceDurability\": \"copy-to-owned-storage\",\n \"states\": [\n \"pending\",\n \"running\",\n \"failed\",\n \"running\",\n \"succeeded\"\n ],\n \"runtime\": \"java\",\n \"scheduler\": \"process-lifetime-worker-pool\",\n \"stateBackend\": \"filesystem\"\n },\n {\n \"attempts\": [\n {\n \"attemptIndex\": 0,\n \"failure\": {\n \"afterAcceptedOffset\": 7,\n \"kind\": \"io-error\",\n \"phase\": \"after-accepted-offset\"\n },\n \"requests\": [\n {\n \"bodySize\": 0,\n \"headers\": {\n \"Upload-Length\": \"14\"\n },\n \"operationId\": \"createTusUpload\",\n \"response\": {\n \"headers\": {\n \"Location\": \"https://tus.io/uploads/managed-durable-retry\"\n },\n \"statusCode\": 201\n },\n \"url\": \"endpoint\"\n },\n {\n \"bodySize\": 7,\n \"headers\": {\n \"Upload-Offset\": \"0\"\n },\n \"operationId\": \"patchTusUpload\",\n \"response\": {\n \"headers\": {\n \"Upload-Offset\": \"7\"\n },\n \"statusCode\": 204\n },\n \"url\": \"upload\"\n }\n ],\n \"stateAfterAttempt\": \"failed\"\n },\n {\n \"attemptIndex\": 1,\n \"requests\": [\n {\n \"headers\": {},\n \"operationId\": \"getTusUploadOffset\",\n \"response\": {\n \"headers\": {\n \"Upload-Length\": \"14\",\n \"Upload-Offset\": \"7\"\n },\n \"statusCode\": 200\n },\n \"url\": \"upload\"\n },\n {\n \"bodySize\": 7,\n \"headers\": {\n \"Upload-Offset\": \"7\"\n },\n \"operationId\": \"patchTusUpload\",\n \"response\": {\n \"headers\": {\n \"Upload-Offset\": \"14\"\n },\n \"statusCode\": 204\n },\n \"url\": \"upload\"\n }\n ],\n \"stateAfterAttempt\": \"succeeded\"\n }\n ],\n \"cleanup\": {\n \"ownedSource\": \"remove-owned-source-after-success\",\n \"resumeUrl\": \"remove-after-success\"\n },\n \"input\": {\n \"chunkSize\": 7,\n \"content\": \"hello managed!\",\n \"fingerprint\": \"managed-durable-retry-fingerprint\",\n \"metadata\": {\n \"filename\": \"managed.txt\"\n },\n \"uploadPath\": \"managed-durable-retry\"\n },\n \"network\": {\n \"current\": \"unmetered-network\",\n \"decision\": \"start-upload-work\",\n \"required\": \"any-network\"\n },\n \"outcome\": {\n \"kind\": \"terminal\",\n \"state\": \"succeeded\"\n },\n \"retryDelays\": [\n 0\n ],\n \"sourceAvailability\": \"available\",\n \"sourceDurability\": \"copy-to-owned-storage\",\n \"states\": [\n \"pending\",\n \"running\",\n \"failed\",\n \"running\",\n \"succeeded\"\n ],\n \"runtime\": \"android\",\n \"scheduler\": \"durable-os-scheduler\",\n \"stateBackend\": \"platform-key-value-store\"\n }\n ],\n \"requiredPrimitives\": [\n \"accept-upload-submission\",\n \"make-source-durable\",\n \"schedule-upload-work\",\n \"run-protocol-upload\",\n \"apply-managed-retry-policy\",\n \"publish-upload-state\",\n \"cleanup-managed-upload\"\n ],\n \"scenarioId\": \"managedUploadDurableRetry\",\n \"summary\": \"Submit a durable source, survive scheduler/process interruption, resume by stored upload URL, and finish with cleanup.\"\n },\n {\n \"proofs\": [\n {\n \"attempts\": [\n {\n \"attemptIndex\": 0,\n \"failure\": {\n \"kind\": \"unretryable-protocol-error\",\n \"phase\": \"during-protocol-request\"\n },\n \"requests\": [\n {\n \"bodySize\": 0,\n \"headers\": {\n \"Upload-Length\": \"14\"\n },\n \"operationId\": \"createTusUpload\",\n \"response\": {\n \"headers\": {},\n \"statusCode\": 400\n },\n \"url\": \"endpoint\"\n }\n ],\n \"stateAfterAttempt\": \"failed\"\n }\n ],\n \"cleanup\": {\n \"ownedSource\": \"retain-owned-source-after-permanent-failure\",\n \"resumeUrl\": \"absent-after-permanent-failure\"\n },\n \"input\": {\n \"chunkSize\": 7,\n \"content\": \"hello failure!\",\n \"fingerprint\": \"managed-permanent-failure-fingerprint\",\n \"metadata\": {\n \"filename\": \"managed-permanent-failure.txt\"\n },\n \"uploadPath\": \"managed-permanent-failure\"\n },\n \"network\": {\n \"current\": \"unmetered-network\",\n \"decision\": \"start-upload-work\",\n \"required\": \"any-network\"\n },\n \"outcome\": {\n \"failure\": \"unretryable-protocol-error\",\n \"kind\": \"terminal\",\n \"state\": \"failed\"\n },\n \"retryDelays\": [],\n \"sourceAvailability\": \"available\",\n \"sourceDurability\": \"copy-to-owned-storage\",\n \"states\": [\n \"pending\",\n \"running\",\n \"failed\"\n ],\n \"runtime\": \"java\",\n \"scheduler\": \"process-lifetime-worker-pool\",\n \"stateBackend\": \"filesystem\"\n },\n {\n \"attempts\": [\n {\n \"attemptIndex\": 0,\n \"failure\": {\n \"kind\": \"unretryable-protocol-error\",\n \"phase\": \"during-protocol-request\"\n },\n \"requests\": [\n {\n \"bodySize\": 0,\n \"headers\": {\n \"Upload-Length\": \"14\"\n },\n \"operationId\": \"createTusUpload\",\n \"response\": {\n \"headers\": {},\n \"statusCode\": 400\n },\n \"url\": \"endpoint\"\n }\n ],\n \"stateAfterAttempt\": \"failed\"\n }\n ],\n \"cleanup\": {\n \"ownedSource\": \"retain-owned-source-after-permanent-failure\",\n \"resumeUrl\": \"absent-after-permanent-failure\"\n },\n \"input\": {\n \"chunkSize\": 7,\n \"content\": \"hello failure!\",\n \"fingerprint\": \"managed-permanent-failure-fingerprint\",\n \"metadata\": {\n \"filename\": \"managed-permanent-failure.txt\"\n },\n \"uploadPath\": \"managed-permanent-failure\"\n },\n \"network\": {\n \"current\": \"unmetered-network\",\n \"decision\": \"start-upload-work\",\n \"required\": \"any-network\"\n },\n \"outcome\": {\n \"failure\": \"unretryable-protocol-error\",\n \"kind\": \"terminal\",\n \"state\": \"failed\"\n },\n \"retryDelays\": [],\n \"sourceAvailability\": \"available\",\n \"sourceDurability\": \"copy-to-owned-storage\",\n \"states\": [\n \"pending\",\n \"running\",\n \"failed\"\n ],\n \"runtime\": \"android\",\n \"scheduler\": \"durable-os-scheduler\",\n \"stateBackend\": \"platform-key-value-store\"\n }\n ],\n \"requiredPrimitives\": [\n \"accept-upload-submission\",\n \"make-source-durable\",\n \"schedule-upload-work\",\n \"run-protocol-upload\",\n \"classify-failure\",\n \"publish-upload-state\",\n \"cleanup-managed-upload\"\n ],\n \"scenarioId\": \"managedUploadPermanentFailure\",\n \"summary\": \"Classify unretryable protocol failures as terminal without further retry.\"\n },\n {\n \"proofs\": [\n {\n \"attempts\": [\n {\n \"attemptIndex\": 0,\n \"failure\": {\n \"kind\": \"retryable-protocol-error\",\n \"phase\": \"during-protocol-request\"\n },\n \"requests\": [\n {\n \"bodySize\": 0,\n \"headers\": {\n \"Upload-Length\": \"14\"\n },\n \"operationId\": \"createTusUpload\",\n \"response\": {\n \"headers\": {},\n \"statusCode\": 500\n },\n \"url\": \"endpoint\"\n }\n ],\n \"stateAfterAttempt\": \"failed\"\n },\n {\n \"attemptIndex\": 1,\n \"failure\": {\n \"kind\": \"retryable-protocol-error\",\n \"phase\": \"during-protocol-request\"\n },\n \"requests\": [\n {\n \"bodySize\": 0,\n \"headers\": {\n \"Upload-Length\": \"14\"\n },\n \"operationId\": \"createTusUpload\",\n \"response\": {\n \"headers\": {},\n \"statusCode\": 500\n },\n \"url\": \"endpoint\"\n }\n ],\n \"stateAfterAttempt\": \"failed\"\n },\n {\n \"attemptIndex\": 2,\n \"failure\": {\n \"kind\": \"retryable-protocol-error\",\n \"phase\": \"during-protocol-request\"\n },\n \"requests\": [\n {\n \"bodySize\": 0,\n \"headers\": {\n \"Upload-Length\": \"14\"\n },\n \"operationId\": \"createTusUpload\",\n \"response\": {\n \"headers\": {},\n \"statusCode\": 500\n },\n \"url\": \"endpoint\"\n }\n ],\n \"stateAfterAttempt\": \"failed\"\n }\n ],\n \"cleanup\": {\n \"ownedSource\": \"retain-owned-source-after-permanent-failure\",\n \"resumeUrl\": \"absent-after-permanent-failure\"\n },\n \"input\": {\n \"chunkSize\": 7,\n \"content\": \"hello retries!\",\n \"fingerprint\": \"managed-retry-exhausted-fingerprint\",\n \"metadata\": {\n \"filename\": \"managed-retry-exhausted.txt\"\n },\n \"uploadPath\": \"managed-retry-exhausted\"\n },\n \"network\": {\n \"current\": \"unmetered-network\",\n \"decision\": \"start-upload-work\",\n \"required\": \"any-network\"\n },\n \"outcome\": {\n \"failure\": \"retry-policy-exhausted\",\n \"kind\": \"terminal\",\n \"state\": \"failed\"\n },\n \"retryDelays\": [\n 0,\n 0\n ],\n \"sourceAvailability\": \"available\",\n \"sourceDurability\": \"copy-to-owned-storage\",\n \"states\": [\n \"pending\",\n \"running\",\n \"failed\",\n \"running\",\n \"failed\",\n \"running\",\n \"failed\"\n ],\n \"runtime\": \"java\",\n \"scheduler\": \"process-lifetime-worker-pool\",\n \"stateBackend\": \"filesystem\"\n },\n {\n \"attempts\": [\n {\n \"attemptIndex\": 0,\n \"failure\": {\n \"kind\": \"retryable-protocol-error\",\n \"phase\": \"during-protocol-request\"\n },\n \"requests\": [\n {\n \"bodySize\": 0,\n \"headers\": {\n \"Upload-Length\": \"14\"\n },\n \"operationId\": \"createTusUpload\",\n \"response\": {\n \"headers\": {},\n \"statusCode\": 500\n },\n \"url\": \"endpoint\"\n }\n ],\n \"stateAfterAttempt\": \"failed\"\n },\n {\n \"attemptIndex\": 1,\n \"failure\": {\n \"kind\": \"retryable-protocol-error\",\n \"phase\": \"during-protocol-request\"\n },\n \"requests\": [\n {\n \"bodySize\": 0,\n \"headers\": {\n \"Upload-Length\": \"14\"\n },\n \"operationId\": \"createTusUpload\",\n \"response\": {\n \"headers\": {},\n \"statusCode\": 500\n },\n \"url\": \"endpoint\"\n }\n ],\n \"stateAfterAttempt\": \"failed\"\n },\n {\n \"attemptIndex\": 2,\n \"failure\": {\n \"kind\": \"retryable-protocol-error\",\n \"phase\": \"during-protocol-request\"\n },\n \"requests\": [\n {\n \"bodySize\": 0,\n \"headers\": {\n \"Upload-Length\": \"14\"\n },\n \"operationId\": \"createTusUpload\",\n \"response\": {\n \"headers\": {},\n \"statusCode\": 500\n },\n \"url\": \"endpoint\"\n }\n ],\n \"stateAfterAttempt\": \"failed\"\n }\n ],\n \"cleanup\": {\n \"ownedSource\": \"retain-owned-source-after-permanent-failure\",\n \"resumeUrl\": \"absent-after-permanent-failure\"\n },\n \"input\": {\n \"chunkSize\": 7,\n \"content\": \"hello retries!\",\n \"fingerprint\": \"managed-retry-exhausted-fingerprint\",\n \"metadata\": {\n \"filename\": \"managed-retry-exhausted.txt\"\n },\n \"uploadPath\": \"managed-retry-exhausted\"\n },\n \"network\": {\n \"current\": \"unmetered-network\",\n \"decision\": \"start-upload-work\",\n \"required\": \"any-network\"\n },\n \"outcome\": {\n \"failure\": \"retry-policy-exhausted\",\n \"kind\": \"terminal\",\n \"state\": \"failed\"\n },\n \"retryDelays\": [\n 0,\n 0\n ],\n \"sourceAvailability\": \"available\",\n \"sourceDurability\": \"copy-to-owned-storage\",\n \"states\": [\n \"pending\",\n \"running\",\n \"failed\",\n \"running\",\n \"failed\",\n \"running\",\n \"failed\"\n ],\n \"runtime\": \"android\",\n \"scheduler\": \"durable-os-scheduler\",\n \"stateBackend\": \"platform-key-value-store\"\n }\n ],\n \"requiredPrimitives\": [\n \"accept-upload-submission\",\n \"make-source-durable\",\n \"schedule-upload-work\",\n \"run-protocol-upload\",\n \"apply-managed-retry-policy\",\n \"classify-failure\",\n \"publish-upload-state\",\n \"cleanup-managed-upload\"\n ],\n \"scenarioId\": \"managedUploadRetryPolicyExhausted\",\n \"summary\": \"Retry transient protocol failures up to the managed retry budget and then classify the upload as terminally failed.\"\n },\n {\n \"proofs\": [\n {\n \"attempts\": [\n {\n \"attemptIndex\": 0,\n \"failure\": {\n \"kind\": \"source-unavailable\",\n \"phase\": \"before-protocol-request\"\n },\n \"requests\": [],\n \"stateAfterAttempt\": \"failed\"\n }\n ],\n \"cleanup\": {\n \"ownedSource\": \"absent-after-source-unavailable\",\n \"resumeUrl\": \"absent-after-permanent-failure\"\n },\n \"input\": {\n \"chunkSize\": 7,\n \"content\": \"hello missing!\",\n \"fingerprint\": \"managed-source-unavailable-fingerprint\",\n \"metadata\": {\n \"filename\": \"managed-source-unavailable.txt\"\n },\n \"uploadPath\": \"managed-source-unavailable\"\n },\n \"network\": {\n \"current\": \"unmetered-network\",\n \"decision\": \"start-upload-work\",\n \"required\": \"any-network\"\n },\n \"outcome\": {\n \"failure\": \"source-unavailable\",\n \"kind\": \"terminal\",\n \"state\": \"failed\"\n },\n \"retryDelays\": [],\n \"sourceAvailability\": \"missing-before-durable-copy\",\n \"sourceDurability\": \"copy-to-owned-storage\",\n \"states\": [\n \"pending\",\n \"running\",\n \"failed\"\n ],\n \"runtime\": \"java\",\n \"scheduler\": \"process-lifetime-worker-pool\",\n \"stateBackend\": \"filesystem\"\n },\n {\n \"attempts\": [\n {\n \"attemptIndex\": 0,\n \"failure\": {\n \"kind\": \"source-unavailable\",\n \"phase\": \"before-protocol-request\"\n },\n \"requests\": [],\n \"stateAfterAttempt\": \"failed\"\n }\n ],\n \"cleanup\": {\n \"ownedSource\": \"absent-after-source-unavailable\",\n \"resumeUrl\": \"absent-after-permanent-failure\"\n },\n \"input\": {\n \"chunkSize\": 7,\n \"content\": \"hello missing!\",\n \"fingerprint\": \"managed-source-unavailable-fingerprint\",\n \"metadata\": {\n \"filename\": \"managed-source-unavailable.txt\"\n },\n \"uploadPath\": \"managed-source-unavailable\"\n },\n \"network\": {\n \"current\": \"unmetered-network\",\n \"decision\": \"start-upload-work\",\n \"required\": \"any-network\"\n },\n \"outcome\": {\n \"failure\": \"source-unavailable\",\n \"kind\": \"terminal\",\n \"state\": \"failed\"\n },\n \"retryDelays\": [],\n \"sourceAvailability\": \"missing-before-durable-copy\",\n \"sourceDurability\": \"copy-to-owned-storage\",\n \"states\": [\n \"pending\",\n \"running\",\n \"failed\"\n ],\n \"runtime\": \"android\",\n \"scheduler\": \"durable-os-scheduler\",\n \"stateBackend\": \"platform-key-value-store\"\n }\n ],\n \"requiredPrimitives\": [\n \"accept-upload-submission\",\n \"make-source-durable\",\n \"schedule-upload-work\",\n \"classify-failure\",\n \"publish-upload-state\",\n \"cleanup-managed-upload\"\n ],\n \"scenarioId\": \"managedUploadSourceUnavailable\",\n \"summary\": \"Classify source disappearance before protocol requests as terminal without issuing a TUS request.\"\n },\n {\n \"proofs\": [\n {\n \"attempts\": [],\n \"cleanup\": {\n \"ownedSource\": \"retain-owned-source-while-deferred\",\n \"resumeUrl\": \"absent-while-deferred\"\n },\n \"input\": {\n \"chunkSize\": 7,\n \"content\": \"hello later!\",\n \"fingerprint\": \"managed-network-constraint-fingerprint\",\n \"metadata\": {\n \"filename\": \"managed-network-constraint.txt\"\n },\n \"uploadPath\": \"managed-network-constraint\"\n },\n \"network\": {\n \"current\": \"metered-network\",\n \"decision\": \"defer-until-network-constraint-satisfied\",\n \"required\": \"unmetered-network\"\n },\n \"outcome\": {\n \"kind\": \"deferred\",\n \"reason\": \"network-constraint-unsatisfied\",\n \"state\": \"pending\"\n },\n \"retryDelays\": [],\n \"sourceAvailability\": \"available\",\n \"sourceDurability\": \"copy-to-owned-storage\",\n \"states\": [\n \"pending\"\n ],\n \"runtime\": \"android\",\n \"scheduler\": \"durable-os-scheduler\",\n \"stateBackend\": \"platform-key-value-store\"\n }\n ],\n \"requiredPrimitives\": [\n \"accept-upload-submission\",\n \"make-source-durable\",\n \"schedule-upload-work\",\n \"publish-upload-state\"\n ],\n \"scenarioId\": \"managedUploadNetworkConstraint\",\n \"summary\": \"Honor network constraints before starting or resuming upload work.\"\n }\n ]\n}\n"; static final String[] MANAGED_UPLOAD_PRIMITIVES = new String[] { @@ -1280,9 +1280,12 @@ final class GeneratedTusProtocolContract { "managedUpload", "feature-over-protocol", "managedUploadNetworkConstraint", - new String[0], + new String[] { + "android", + }, new String[] { "accept-upload-submission", + "make-source-durable", "schedule-upload-work", "publish-upload-state", }, diff --git a/src/test/java/io/tus/java/client/TestGeneratedTusManagedUploadRuntime.java b/src/test/java/io/tus/java/client/TestGeneratedTusManagedUploadRuntime.java index 65ed0fa3..ddb01db0 100644 --- a/src/test/java/io/tus/java/client/TestGeneratedTusManagedUploadRuntime.java +++ b/src/test/java/io/tus/java/client/TestGeneratedTusManagedUploadRuntime.java @@ -44,13 +44,20 @@ public class TestGeneratedTusManagedUploadRuntime extends MockServerProvider { "process-lifetime-worker-pool", "copy-to-owned-storage", "available", - "filesystem" + "filesystem", + new GeneratedTusManagedUploadNetwork( + "any-network", + "unmetered-network", + "start-upload-work" + ) ), new GeneratedTusManagedUploadTransport( "Location" ), - new GeneratedTusManagedUploadTerminal( + new GeneratedTusManagedUploadOutcome( + "terminal", "succeeded", + "", "" ), new GeneratedTusManagedUploadCleanup( @@ -180,14 +187,21 @@ public class TestGeneratedTusManagedUploadRuntime extends MockServerProvider { "process-lifetime-worker-pool", "copy-to-owned-storage", "available", - "filesystem" + "filesystem", + new GeneratedTusManagedUploadNetwork( + "any-network", + "unmetered-network", + "start-upload-work" + ) ), new GeneratedTusManagedUploadTransport( "Location" ), - new GeneratedTusManagedUploadTerminal( + new GeneratedTusManagedUploadOutcome( + "terminal", "failed", - "unretryable-protocol-error" + "unretryable-protocol-error", + "" ), new GeneratedTusManagedUploadCleanup( "retain-owned-source-after-permanent-failure", @@ -247,14 +261,21 @@ public class TestGeneratedTusManagedUploadRuntime extends MockServerProvider { "process-lifetime-worker-pool", "copy-to-owned-storage", "available", - "filesystem" + "filesystem", + new GeneratedTusManagedUploadNetwork( + "any-network", + "unmetered-network", + "start-upload-work" + ) ), new GeneratedTusManagedUploadTransport( "Location" ), - new GeneratedTusManagedUploadTerminal( + new GeneratedTusManagedUploadOutcome( + "terminal", "failed", - "retry-policy-exhausted" + "retry-policy-exhausted", + "" ), new GeneratedTusManagedUploadCleanup( "retain-owned-source-after-permanent-failure", @@ -369,14 +390,21 @@ public class TestGeneratedTusManagedUploadRuntime extends MockServerProvider { "process-lifetime-worker-pool", "copy-to-owned-storage", "missing-before-durable-copy", - "filesystem" + "filesystem", + new GeneratedTusManagedUploadNetwork( + "any-network", + "unmetered-network", + "start-upload-work" + ) ), new GeneratedTusManagedUploadTransport( "Location" ), - new GeneratedTusManagedUploadTerminal( + new GeneratedTusManagedUploadOutcome( + "terminal", "failed", - "source-unavailable" + "source-unavailable", + "" ), new GeneratedTusManagedUploadCleanup( "absent-after-source-unavailable", @@ -451,18 +479,23 @@ public void testManagedUploadDurableRetryRuntime() throws Exception { try { prepareSourceBeforeProtocol(testCase, source, ownedSource, states, stateFile); - TusExecutor executor = managedExecutorFor(testCase, client, ownedSource, states, stateFile); - ExecutorService worker = Executors.newSingleThreadExecutor(); - try { - Future future = worker.submit(new Callable() { - @Override - public Boolean call() throws Exception { - return executor.makeAttempts(); - } - }); - assertTerminalResult(testCase, future); - } finally { - worker.shutdownNow(); + if (shouldDeferBeforeProtocol(testCase)) { + assertDeferredResult(testCase); + } else { + TusExecutor executor = + managedExecutorFor(testCase, client, ownedSource, states, stateFile); + ExecutorService worker = Executors.newSingleThreadExecutor(); + try { + Future future = worker.submit(new Callable() { + @Override + public Boolean call() throws Exception { + return executor.makeAttempts(); + } + }); + assertTerminalResult(testCase, future); + } finally { + worker.shutdownNow(); + } } } catch (IOException error) { if (!isSourceUnavailableBeforeProtocol(testCase)) { @@ -493,14 +526,18 @@ public Boolean call() throws Exception { private void assertTerminalResult( GeneratedTusManagedUploadRuntimeCase testCase, Future future) throws Exception { + if (!"terminal".equals(testCase.outcomeKind)) { + throw new AssertionError(testCase.scenarioId + " expected deferred outcome"); + } + try { boolean result = future.get(); - if (!"succeeded".equals(testCase.terminalState)) { + if (!"succeeded".equals(testCase.outcomeState)) { throw new AssertionError(testCase.scenarioId + " expected terminal failure"); } assertTrue(testCase.scenarioId, result); } catch (ExecutionException error) { - if (!"failed".equals(testCase.terminalState)) { + if (!"failed".equals(testCase.outcomeState)) { throw error; } assertTerminalFailure(testCase, error.getCause()); @@ -510,15 +547,15 @@ private void assertTerminalResult( private void assertTerminalFailure( GeneratedTusManagedUploadRuntimeCase testCase, Throwable error) { - if ("unretryable-protocol-error".equals(testCase.terminalFailure)) { + if ("unretryable-protocol-error".equals(testCase.outcomeFailure)) { assertTrue(testCase.scenarioId, error instanceof ProtocolException); return; } - if ("source-unavailable".equals(testCase.terminalFailure)) { + if ("source-unavailable".equals(testCase.outcomeFailure)) { assertTrue(testCase.scenarioId, error instanceof IOException); return; } - if ("retry-policy-exhausted".equals(testCase.terminalFailure)) { + if ("retry-policy-exhausted".equals(testCase.outcomeFailure)) { assertTrue( testCase.scenarioId, error instanceof ProtocolException || error instanceof IOException); @@ -528,7 +565,36 @@ private void assertTerminalFailure( throw new AssertionError( testCase.scenarioId + " uses unsupported generated terminal failure " - + testCase.terminalFailure); + + testCase.outcomeFailure); + } + + private void assertDeferredResult(GeneratedTusManagedUploadRuntimeCase testCase) { + if ( + !"deferred".equals(testCase.outcomeKind) + || !"pending".equals(testCase.outcomeState) + || !"network-constraint-unsatisfied".equals(testCase.outcomeReason) + || !"defer-until-network-constraint-satisfied".equals(testCase.networkDecision) + || networkConstraintSatisfied(testCase)) { + throw new AssertionError(testCase.scenarioId + " expected deferred network outcome"); + } + } + + private boolean networkConstraintSatisfied(GeneratedTusManagedUploadRuntimeCase testCase) { + if ("offline".equals(testCase.currentNetwork)) { + return false; + } + if ("any-network".equals(testCase.networkRequired)) { + return "metered-network".equals(testCase.currentNetwork) + || "unmetered-network".equals(testCase.currentNetwork); + } + if ("unmetered-network".equals(testCase.networkRequired)) { + return "unmetered-network".equals(testCase.currentNetwork); + } + + throw new AssertionError( + testCase.scenarioId + + " uses unsupported generated network requirement " + + testCase.networkRequired); } private TusExecutor managedExecutorFor( @@ -657,10 +723,14 @@ private void prepareSourceBeforeProtocol( } private boolean isSourceUnavailableBeforeProtocol(GeneratedTusManagedUploadRuntimeCase testCase) { - return "source-unavailable".equals(testCase.terminalFailure) + return "source-unavailable".equals(testCase.outcomeFailure) && "missing-before-durable-copy".equals(testCase.sourceAvailability); } + private boolean shouldDeferBeforeProtocol(GeneratedTusManagedUploadRuntimeCase testCase) { + return "defer-until-network-constraint-satisfied".equals(testCase.networkDecision); + } + private void cleanupAfterTerminalState( GeneratedTusManagedUploadRuntimeCase testCase, File ownedSource) throws IOException { @@ -683,6 +753,11 @@ private void assertOwnedSourceState( ownedSource.delete(); return; } + if ("retain-owned-source-while-deferred".equals(testCase.ownedSourceCleanup)) { + assertTrue(testCase.scenarioId, ownedSource.exists()); + ownedSource.delete(); + return; + } if ("absent-after-source-unavailable".equals(testCase.ownedSourceCleanup)) { assertFalse(testCase.scenarioId, ownedSource.exists()); return; @@ -711,7 +786,8 @@ private void assertResumeUrlState( GeneratedTusManagedUploadUrlStore urlStore) { if ( "remove-after-success".equals(testCase.resumeUrlCleanup) - || "absent-after-permanent-failure".equals(testCase.resumeUrlCleanup)) { + || "absent-after-permanent-failure".equals(testCase.resumeUrlCleanup) + || "absent-while-deferred".equals(testCase.resumeUrlCleanup)) { assertNull(testCase.scenarioId, urlStore.get(testCase.input.fingerprint)); return; } @@ -866,9 +942,14 @@ private static final class GeneratedTusManagedUploadRuntimeCase { final String sourceDurability; final String sourceAvailability; final String stateBackend; + final String networkRequired; + final String currentNetwork; + final String networkDecision; final String locationHeaderName; - final String terminalState; - final String terminalFailure; + final String outcomeKind; + final String outcomeState; + final String outcomeFailure; + final String outcomeReason; final String ownedSourceCleanup; final String resumeUrlCleanup; final String[] expectedStates; @@ -880,7 +961,7 @@ private static final class GeneratedTusManagedUploadRuntimeCase { GeneratedTusManagedUploadRuntimeCase( GeneratedTusManagedUploadRuntimeProfile profile, GeneratedTusManagedUploadTransport transport, - GeneratedTusManagedUploadTerminal terminal, + GeneratedTusManagedUploadOutcome outcome, GeneratedTusManagedUploadCleanup cleanup, GeneratedTusManagedUploadRetryPlan retryPlan, GeneratedTusManagedUploadInput input, @@ -891,9 +972,14 @@ private static final class GeneratedTusManagedUploadRuntimeCase { this.sourceDurability = profile.sourceDurability; this.sourceAvailability = profile.sourceAvailability; this.stateBackend = profile.stateBackend; + this.networkRequired = profile.networkRequired; + this.currentNetwork = profile.currentNetwork; + this.networkDecision = profile.networkDecision; this.locationHeaderName = transport.locationHeaderName; - this.terminalState = terminal.state; - this.terminalFailure = terminal.failure; + this.outcomeKind = outcome.kind; + this.outcomeState = outcome.state; + this.outcomeFailure = outcome.failure; + this.outcomeReason = outcome.reason; this.ownedSourceCleanup = cleanup.ownedSource; this.resumeUrlCleanup = cleanup.resumeUrl; this.expectedStates = retryPlan.expectedStates; @@ -904,13 +990,17 @@ private static final class GeneratedTusManagedUploadRuntimeCase { } } - private static final class GeneratedTusManagedUploadTerminal { + private static final class GeneratedTusManagedUploadOutcome { + final String kind; final String state; final String failure; + final String reason; - GeneratedTusManagedUploadTerminal(String state, String failure) { + GeneratedTusManagedUploadOutcome(String kind, String state, String failure, String reason) { + this.kind = kind; this.state = state; this.failure = failure; + this.reason = reason; } } @@ -921,6 +1011,9 @@ private static final class GeneratedTusManagedUploadRuntimeProfile { final String sourceDurability; final String sourceAvailability; final String stateBackend; + final String networkRequired; + final String currentNetwork; + final String networkDecision; GeneratedTusManagedUploadRuntimeProfile( String scenarioId, @@ -928,13 +1021,29 @@ private static final class GeneratedTusManagedUploadRuntimeProfile { String scheduler, String sourceDurability, String sourceAvailability, - String stateBackend) { + String stateBackend, + GeneratedTusManagedUploadNetwork network) { this.scenarioId = scenarioId; this.runtime = runtime; this.scheduler = scheduler; this.sourceDurability = sourceDurability; this.sourceAvailability = sourceAvailability; this.stateBackend = stateBackend; + this.networkRequired = network.required; + this.currentNetwork = network.current; + this.networkDecision = network.decision; + } + } + + private static final class GeneratedTusManagedUploadNetwork { + final String required; + final String current; + final String decision; + + GeneratedTusManagedUploadNetwork(String required, String current, String decision) { + this.required = required; + this.current = current; + this.decision = decision; } } From a165b6bdf13adfa2ae14f4dd032b9d129754a0d9 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Mon, 1 Jun 2026 21:59:29 +0200 Subject: [PATCH 40/43] Add devdock TUS upload example --- example/build.gradle | 6 + .../java/example/Api2DevdockTusUpload.java | 175 ++++++++++++++++++ 2 files changed, 181 insertions(+) create mode 100644 example/src/main/java/io/tus/java/example/Api2DevdockTusUpload.java diff --git a/example/build.gradle b/example/build.gradle index aeeb70b7..87b0c476 100644 --- a/example/build.gradle +++ b/example/build.gradle @@ -2,5 +2,11 @@ apply plugin: 'java' dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation 'org.json:json:20240303' implementation rootProject } + +tasks.register('api2DevdockTusUpload', JavaExec) { + classpath = sourceSets.main.runtimeClasspath + mainClass = 'io.tus.java.example.Api2DevdockTusUpload' +} diff --git a/example/src/main/java/io/tus/java/example/Api2DevdockTusUpload.java b/example/src/main/java/io/tus/java/example/Api2DevdockTusUpload.java new file mode 100644 index 00000000..44d7254b --- /dev/null +++ b/example/src/main/java/io/tus/java/example/Api2DevdockTusUpload.java @@ -0,0 +1,175 @@ +package io.tus.java.example; + +import io.tus.java.client.ProtocolException; +import io.tus.java.client.TusClient; +import io.tus.java.client.TusURLMemoryStore; +import io.tus.java.client.TusUpload; +import io.tus.java.client.TusUploader; +import org.json.JSONArray; +import org.json.JSONObject; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.LinkedHashMap; +import java.util.Map; + +public final class Api2DevdockTusUpload { + public static void main(String[] args) { + try { + System.setProperty("http.strictPostRedirect", "true"); + + final JSONObject scenario = loadScenario(); + final JSONObject createResponse = scenario.getJSONObject("prepared").getJSONObject("createResponse"); + final String uploadUrl = uploadWithTus(scenario, createResponse); + + System.out.println( + "Java TUS SDK devdock scenario " + + scenario.getString("scenarioId") + + " uploaded to " + + uploadUrl + ); + } catch (Exception e) { + e.printStackTrace(); + System.exit(1); + } + } + + private static JSONObject loadScenario() throws IOException { + String scenarioPath = System.getenv("API2_SDK_EXAMPLE_SCENARIO"); + if (scenarioPath == null || scenarioPath.isEmpty()) { + scenarioPath = "example/api2-scenario.json"; + } + + final byte[] contents = Files.readAllBytes(Paths.get(scenarioPath)); + return new JSONObject(new String(contents, StandardCharsets.UTF_8)); + } + + private static String uploadWithTus( + JSONObject scenario, + JSONObject createResponse + ) throws IOException, ProtocolException { + final JSONObject uploadConfig = scenario.getJSONObject("upload"); + final Object endpointValue = resolveValue(uploadConfig.getJSONObject("tusUrl"), scenario, createResponse); + final byte[] content = scenarioBytes(uploadConfig); + + final TusClient client = new TusClient(); + client.setUploadCreationURL(new URL(scalarString(endpointValue))); + client.enableResuming(new TusURLMemoryStore()); + + final TusUpload upload = new TusUpload(); + upload.setInputStream(new ByteArrayInputStream(content)); + upload.setSize(content.length); + upload.setFingerprint(scenario.getString("scenarioId") + "-java-devdock-example"); + upload.setMetadata(uploadMetadata(uploadConfig, scenario, createResponse)); + + final TusUploader uploader = client.resumeOrCreateUpload(upload); + uploader.setChunkSize(content.length); + while (uploader.uploadChunk() > -1) { + // Continue until the client reports that the source is fully uploaded. + } + uploader.finish(); + + if (uploader.getOffset() != content.length) { + throw new IllegalStateException( + "remote offset " + uploader.getOffset() + ", expected " + content.length + ); + } + if (uploader.getUploadURL() == null) { + throw new IllegalStateException("upload did not return a URL"); + } + + return uploader.getUploadURL().toString(); + } + + private static byte[] scenarioBytes(JSONObject uploadConfig) { + final JSONObject source = uploadConfig.getJSONObject("source"); + final String kind = source.getString("kind"); + if (!"bytes".equals(kind)) { + throw new IllegalArgumentException("unsupported source kind " + kind); + } + + final String encoding = source.getString("encoding"); + if (!"utf8".equals(encoding)) { + throw new IllegalArgumentException("unsupported source encoding " + encoding); + } + + return source.getString("value").getBytes(StandardCharsets.UTF_8); + } + + private static Map uploadMetadata( + JSONObject uploadConfig, + JSONObject scenario, + JSONObject createResponse + ) { + final JSONArray fields = uploadConfig.getJSONArray("metadata"); + final Map metadata = new LinkedHashMap(); + for (int index = 0; index < fields.length(); index++) { + final JSONObject field = fields.getJSONObject(index); + metadata.put( + field.getString("name"), + scalarString(resolveValue(field.getJSONObject("value"), scenario, createResponse)) + ); + } + + return metadata; + } + + private static Object resolveValue( + JSONObject valueSpec, + JSONObject scenario, + JSONObject createResponse + ) { + if (valueSpec.has("value")) { + return valueSpec.get("value"); + } + + final JSONObject source = valueSpec.getJSONObject("source"); + final String root = source.getString("root"); + final Object rootValue; + if ("scenario".equals(root)) { + rootValue = scenario; + } else if ("createResponse".equals(root)) { + rootValue = createResponse; + } else { + throw new IllegalArgumentException("unsupported scenario value root " + root); + } + + return readPath(rootValue, source.getJSONArray("path")); + } + + private static Object readPath(Object value, JSONArray pathParts) { + Object current = value; + for (int index = 0; index < pathParts.length(); index++) { + final Object part = pathParts.get(index); + if (current instanceof JSONObject && part instanceof String) { + current = ((JSONObject) current).get((String) part); + continue; + } + + if (current instanceof JSONArray && part instanceof Number) { + current = ((JSONArray) current).get(((Number) part).intValue()); + continue; + } + + throw new IllegalArgumentException("cannot read scenario path part " + part); + } + + return current; + } + + private static String scalarString(Object value) { + if (JSONObject.NULL.equals(value)) { + return "null"; + } + + return String.valueOf(value); + } + + private Api2DevdockTusUpload() { + throw new IllegalStateException("Utility class"); + } +} From bd7aaa1a082b0989cc74dac73919019260f6523b Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Mon, 1 Jun 2026 22:08:35 +0200 Subject: [PATCH 41/43] Run devdock example from repo root --- example/build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/example/build.gradle b/example/build.gradle index 87b0c476..558931d9 100644 --- a/example/build.gradle +++ b/example/build.gradle @@ -9,4 +9,5 @@ dependencies { tasks.register('api2DevdockTusUpload', JavaExec) { classpath = sourceSets.main.runtimeClasspath mainClass = 'io.tus.java.example.Api2DevdockTusUpload' + workingDir = rootProject.projectDir } From f444876412b73c4b47ebefc8acc0f1dc49c5ae19 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Mon, 1 Jun 2026 22:14:40 +0200 Subject: [PATCH 42/43] Satisfy lint for devdock example --- .../io/tus/java/example/Api2DevdockTusUpload.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/example/src/main/java/io/tus/java/example/Api2DevdockTusUpload.java b/example/src/main/java/io/tus/java/example/Api2DevdockTusUpload.java index 44d7254b..fa5d2128 100644 --- a/example/src/main/java/io/tus/java/example/Api2DevdockTusUpload.java +++ b/example/src/main/java/io/tus/java/example/Api2DevdockTusUpload.java @@ -18,6 +18,11 @@ import java.util.Map; public final class Api2DevdockTusUpload { + /** + * Run the API2 devdock TUS upload example. + * + * @param args ignored + */ public static void main(String[] args) { try { System.setProperty("http.strictPostRedirect", "true"); @@ -68,9 +73,10 @@ private static String uploadWithTus( final TusUploader uploader = client.resumeOrCreateUpload(upload); uploader.setChunkSize(content.length); - while (uploader.uploadChunk() > -1) { - // Continue until the client reports that the source is fully uploaded. - } + int uploadedChunkSize; + do { + uploadedChunkSize = uploader.uploadChunk(); + } while (uploadedChunkSize > -1); uploader.finish(); if (uploader.getOffset() != content.length) { From 9108bda356c58883a5e2e30886383939e14b6876 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Mon, 1 Jun 2026 22:38:18 +0200 Subject: [PATCH 43/43] Emit devdock example result --- .../io/tus/java/example/Api2DevdockTusUpload.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/example/src/main/java/io/tus/java/example/Api2DevdockTusUpload.java b/example/src/main/java/io/tus/java/example/Api2DevdockTusUpload.java index fa5d2128..33845e69 100644 --- a/example/src/main/java/io/tus/java/example/Api2DevdockTusUpload.java +++ b/example/src/main/java/io/tus/java/example/Api2DevdockTusUpload.java @@ -30,6 +30,7 @@ public static void main(String[] args) { final JSONObject scenario = loadScenario(); final JSONObject createResponse = scenario.getJSONObject("prepared").getJSONObject("createResponse"); final String uploadUrl = uploadWithTus(scenario, createResponse); + writeResult(uploadUrl); System.out.println( "Java TUS SDK devdock scenario " @@ -53,6 +54,20 @@ private static JSONObject loadScenario() throws IOException { return new JSONObject(new String(contents, StandardCharsets.UTF_8)); } + private static void writeResult(String uploadUrl) throws IOException { + final String resultPath = System.getenv("API2_SDK_EXAMPLE_RESULT"); + if (resultPath == null || resultPath.isEmpty()) { + return; + } + + final JSONObject result = new JSONObject(); + result.put("uploadUrl", uploadUrl); + Files.write( + Paths.get(resultPath), + (result.toString(2) + "\n").getBytes(StandardCharsets.UTF_8) + ); + } + private static String uploadWithTus( JSONObject scenario, JSONObject createResponse