@@ -1141,20 +1141,22 @@ void testProgressListener_prescribedStateTransitions() throws Exception {
11411141 ResumableUploadFuture <String > future =
11421142 callable .futureCall ("resource-path" , payloadOf ("01234567890123456789" ), options );
11431143
1144+ assertThat (future .getEffectiveChunkSize ()).isNull ();
1145+
11441146 String url = "https://upload.url/progress-transitions" ;
11451147 startFuture .set (ResumableUploadSession .newBuilder ().setUploadUrl (url ).build ());
11461148
11471149 assertThat (future .get ()).isEqualTo ("done" );
11481150
11491151 assertThat (listener .awaitTerminal ())
11501152 .containsExactly (
1151- createProgress (ResumableUploadProgress .STATE_STARTING , 0L , null ),
1152- createProgress (ResumableUploadProgress .STATE_STARTED , 0L , url ),
1153- createProgress (ResumableUploadProgress .STATE_UPLOADING , 8L , url ),
1154- createProgress (ResumableUploadProgress .STATE_RECOVERING , 8L , url ),
1155- createProgress (ResumableUploadProgress .STATE_OFFSET_RECEIVED , 8L , url ),
1156- createProgress (ResumableUploadProgress .STATE_UPLOADING , 16L , url ),
1157- createProgress (ResumableUploadProgress .STATE_FINALIZED , 20L , url ))
1153+ createProgress (ResumableUploadProgress .STATE_STARTING , 0L , null , null ),
1154+ createProgress (ResumableUploadProgress .STATE_STARTED , 0L , url , 8 ),
1155+ createProgress (ResumableUploadProgress .STATE_UPLOADING , 8L , url , 8 ),
1156+ createProgress (ResumableUploadProgress .STATE_RECOVERING , 8L , url , 8 ),
1157+ createProgress (ResumableUploadProgress .STATE_OFFSET_RECEIVED , 8L , url , 8 ),
1158+ createProgress (ResumableUploadProgress .STATE_UPLOADING , 16L , url , 8 ),
1159+ createProgress (ResumableUploadProgress .STATE_FINALIZED , 20L , url , 8 ))
11581160 .inOrder ();
11591161 }
11601162
@@ -1175,8 +1177,8 @@ void testProgressListener_uploadFailure_transitionsToFailed() throws Exception {
11751177
11761178 assertThat (listener .awaitTerminal ())
11771179 .containsExactly (
1178- createProgress (ResumableUploadProgress .STATE_STARTING , 0L , null ),
1179- createProgress (ResumableUploadProgress .STATE_FAILED , 0L , null ))
1180+ createProgress (ResumableUploadProgress .STATE_STARTING , 0L , null , null ),
1181+ createProgress (ResumableUploadProgress .STATE_FAILED , 0L , null , null ))
11801182 .inOrder ();
11811183 }
11821184
@@ -1199,7 +1201,7 @@ void testProgressListener_cancel_transitionsToFailed() throws Exception {
11991201 assertThat (receivedStatuses .get (receivedStatuses .size () - 1 ))
12001202 .isEqualTo (
12011203 createProgress (
1202- ResumableUploadProgress .STATE_FAILED , 0L , "https://upload.url/cancel-progress" ));
1204+ ResumableUploadProgress .STATE_FAILED , 0L , "https://upload.url/cancel-progress" , 8 ));
12031205 }
12041206
12051207 @ Test
@@ -1228,15 +1230,20 @@ void testProgressListener_queryFinal_transitionsToFinalized() throws Exception {
12281230 createProgress (
12291231 ResumableUploadProgress .STATE_FINALIZED ,
12301232 5L ,
1231- "https://upload.url/query-final-progress" ));
1233+ "https://upload.url/query-final-progress" ,
1234+ 8 ));
12321235 }
12331236
12341237 private static ResumableUploadProgress createProgress (
1235- String state , long bytesUploaded , @ Nullable String uploadUrl ) {
1238+ String state ,
1239+ long bytesUploaded ,
1240+ @ Nullable String uploadUrl ,
1241+ @ Nullable Integer effectiveChunkSize ) {
12361242 return ResumableUploadProgress .newBuilder ()
12371243 .setState (state )
12381244 .setBytesUploaded (bytesUploaded )
12391245 .setUploadUrl (uploadUrl )
1246+ .setEffectiveChunkSize (effectiveChunkSize )
12401247 .build ();
12411248 }
12421249
@@ -1273,6 +1280,7 @@ void testActionableErrors_startFailure_preservesOriginalException() {
12731280 ExecutionException ex = assertThrows (ExecutionException .class , future ::get );
12741281 assertThat (ex .getCause ()).isSameInstanceAs (startError );
12751282 assertThat (future .getUploadSessionUrl ()).isNull ();
1283+ assertThat (future .getEffectiveChunkSize ()).isNull ();
12761284 }
12771285
12781286 @ Test
@@ -1298,9 +1306,52 @@ void testActionableErrors_preservesErrorDetailsAndCauseChain() {
12981306 assertThat (ex .getCause ()).isInstanceOf (ApiException .class );
12991307 ApiException cause = (ApiException ) ex .getCause ();
13001308 assertThat (cause .getMessage ()).contains (sessionUrl );
1309+ assertThat (cause .getMessage ()).contains ("effective chunk size: 8" );
13011310 assertThat (cause .getCause ()).isSameInstanceAs (transportCause );
13021311 assertThat (cause .getErrorDetails ()).isSameInstanceAs (errorDetails );
13031312 assertThat (future .getUploadSessionUrl ()).isEqualTo (sessionUrl );
1313+ assertThat (future .getEffectiveChunkSize ()).isEqualTo (8 );
1314+ }
1315+
1316+ @ Test
1317+ void testAlignChunkSize () {
1318+ assertThat (ResumableUploadFutureImpl .alignChunkSize (8 , 1L )).isEqualTo (8 );
1319+ assertThat (ResumableUploadFutureImpl .alignChunkSize (8 , 4L )).isEqualTo (8 );
1320+ assertThat (ResumableUploadFutureImpl .alignChunkSize (10 , 4L )).isEqualTo (8 );
1321+ assertThat (ResumableUploadFutureImpl .alignChunkSize (4 , 6L )).isEqualTo (6 );
1322+ }
1323+
1324+ @ Test
1325+ void testChunkGranularity_alignsChunkSizeAndSurfacesEffectiveSize () throws Exception {
1326+ when (mockStartCallable .futureCall (any (), any ()))
1327+ .thenReturn (
1328+ ApiFutures .immediateFuture (
1329+ ResumableUploadSession .newBuilder ()
1330+ .setUploadUrl ("https://upload.url/granularity-align" )
1331+ .setChunkGranularity (4L )
1332+ .build ()));
1333+ when (mockChunkCallable .futureCall (any (ChunkUploadRequest .class ), any ()))
1334+ .thenReturn (
1335+ ApiFutures .immediateFuture (
1336+ ChunkUploadResponse .create (ResumableUploadStatus .ACTIVE , null )))
1337+ .thenReturn (
1338+ ApiFutures .immediateFuture (
1339+ ChunkUploadResponse .create (ResumableUploadStatus .FINAL , "done" )));
1340+
1341+ // Configured chunkSize = 10, server granularity = 4 -> aligned down to 8
1342+ ResumableUploadOptions options = defaultSettings .toBuilder ().setChunkSize (10 ).build ();
1343+
1344+ ResumableUploadFuture <String > future =
1345+ callable .futureCall ("resource-path" , payloadOf ("0123456789ab" ), options );
1346+
1347+ assertThat (future .get ()).isEqualTo ("done" );
1348+ assertThat (future .getEffectiveChunkSize ()).isEqualTo (8 );
1349+
1350+ ArgumentCaptor <ChunkUploadRequest > captor = ArgumentCaptor .forClass (ChunkUploadRequest .class );
1351+ verify (mockChunkCallable , times (2 )).futureCall (captor .capture (), any ());
1352+ List <ChunkUploadRequest > requests = captor .getAllValues ();
1353+ assertChunk (requests .get (0 ), 0 , 8 , false );
1354+ assertChunk (requests .get (1 ), 8 , 4 , true );
13041355 }
13051356
13061357 @ Test
0 commit comments