|
16 | 16 | package software.amazon.awssdk.http.apache.internal;
|
17 | 17 |
|
18 | 18 | import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
| 19 | +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; |
19 | 20 | import static org.junit.jupiter.api.Assertions.assertEquals;
|
20 | 21 | import static org.junit.jupiter.api.Assertions.assertFalse;
|
21 | 22 | import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
|
35 | 36 | import java.io.InputStream;
|
36 | 37 | import java.io.InterruptedIOException;
|
37 | 38 | import java.net.URI;
|
38 |
| -import java.util.ArrayList; |
39 |
| -import java.util.Collections; |
40 |
| -import java.util.List; |
41 | 39 | import java.util.Random;
|
42 |
| -import java.util.concurrent.CountDownLatch; |
43 |
| -import java.util.concurrent.TimeUnit; |
44 | 40 | import java.util.concurrent.atomic.AtomicInteger;
|
45 | 41 | import org.junit.jupiter.api.BeforeEach;
|
46 | 42 | import org.junit.jupiter.api.DisplayName;
|
@@ -343,7 +339,9 @@ public int read() throws IOException {
|
343 | 339 | return -1;
|
344 | 340 | }
|
345 | 341 | hasBeenRead = true;
|
346 |
| - return data[position++] & 0xFF; |
| 342 | + int i = data[position] & 0xFF; |
| 343 | + position++; |
| 344 | + return i; |
347 | 345 | }
|
348 | 346 |
|
349 | 347 | @Override
|
@@ -670,51 +668,6 @@ void constructor_WithoutContentType_HandlesGracefully() {
|
670 | 668 | assertEquals(100L, entity.getContentLength());
|
671 | 669 | }
|
672 | 670 |
|
673 |
| - @Test |
674 |
| - @DisplayName("Entity should handle concurrent write attempts") |
675 |
| - void writeTo_ConcurrentWrites_HandlesCorrectly() throws Exception { |
676 |
| - // Given |
677 |
| - String content = "Concurrent test content"; |
678 |
| - ContentStreamProvider provider = () -> new ByteArrayInputStream(content.getBytes()); |
679 |
| - SdkHttpRequest httpRequest = httpRequestBuilder.build(); |
680 |
| - HttpExecuteRequest request = HttpExecuteRequest.builder() |
681 |
| - .request(httpRequest) |
682 |
| - .contentStreamProvider(provider) |
683 |
| - .build(); |
684 |
| - |
685 |
| - entity = new RepeatableInputStreamRequestEntity(request); |
686 |
| - |
687 |
| - // Simulate concurrent writes |
688 |
| - int threadCount = 5; |
689 |
| - CountDownLatch latch = new CountDownLatch(threadCount); |
690 |
| - List<ByteArrayOutputStream> outputs = Collections.synchronizedList(new ArrayList<>()); |
691 |
| - List<Exception> exceptions = Collections.synchronizedList(new ArrayList<>()); |
692 |
| - |
693 |
| - for (int i = 0; i < threadCount; i++) { |
694 |
| - new Thread(() -> { |
695 |
| - try { |
696 |
| - ByteArrayOutputStream output = new ByteArrayOutputStream(); |
697 |
| - entity.writeTo(output); |
698 |
| - outputs.add(output); |
699 |
| - } catch (Exception e) { |
700 |
| - exceptions.add(e); |
701 |
| - } finally { |
702 |
| - latch.countDown(); |
703 |
| - } |
704 |
| - }).start(); |
705 |
| - } |
706 |
| - |
707 |
| - latch.await(5, TimeUnit.SECONDS); |
708 |
| - |
709 |
| - // At least one should succeed, others may fail due to stream state |
710 |
| - assertFalse(outputs.isEmpty(), "At least one write should succeed"); |
711 |
| - for (ByteArrayOutputStream output : outputs) { |
712 |
| - if (output.size() > 0) { |
713 |
| - assertEquals(content, output.toString()); |
714 |
| - } |
715 |
| - } |
716 |
| - } |
717 |
| - |
718 | 671 | @Test
|
719 | 672 | @DisplayName("Entity should handle interrupted IO operations")
|
720 | 673 | void writeTo_InterruptedStream_ThrowsIOException() throws IOException {
|
@@ -790,5 +743,161 @@ void multipleOperations_StatePreservation_WorksCorrectly() throws IOException {
|
790 | 743 | assertEquals(contentLength1, contentLength2);
|
791 | 744 | assertEquals(contentLength2, contentLength3);
|
792 | 745 | }
|
| 746 | + |
| 747 | + @Test |
| 748 | + @DisplayName("markSupported should be be called everytime") |
| 749 | + void markSupported_NotCachedDuringConstruction() { |
| 750 | + // Given |
| 751 | + AtomicInteger markSupportedCalls = new AtomicInteger(0); |
| 752 | + InputStream trackingStream = new ByteArrayInputStream("test".getBytes()) { |
| 753 | + @Override |
| 754 | + public boolean markSupported() { |
| 755 | + markSupportedCalls.incrementAndGet(); |
| 756 | + return true; |
| 757 | + } |
| 758 | + }; |
| 759 | + |
| 760 | + entity = createEntity(trackingStream); |
| 761 | + assertEquals(0, markSupportedCalls.get()); |
| 762 | + |
| 763 | + // Multiple isRepeatable calls trigger new markSupported calls |
| 764 | + assertTrue(entity.isRepeatable()); |
| 765 | + assertTrue(entity.isRepeatable()); |
| 766 | + assertEquals(2, markSupportedCalls.get()); |
| 767 | + } |
| 768 | + |
| 769 | + @Test |
| 770 | + @DisplayName("ContentStreamProvider.newStream() should only be called once") |
| 771 | + void contentStreamProvider_NewStreamCalledOnce() { |
| 772 | + AtomicInteger newStreamCalls = new AtomicInteger(0); |
| 773 | + ContentStreamProvider provider = () -> { |
| 774 | + if (newStreamCalls.incrementAndGet() > 1) { |
| 775 | + throw new RuntimeException("Could not create new stream: Already created"); |
| 776 | + } |
| 777 | + return new ByteArrayInputStream("test".getBytes()); |
| 778 | + }; |
| 779 | + |
| 780 | + entity = createEntity(provider); |
| 781 | + |
| 782 | + assertEquals(1, newStreamCalls.get()); |
| 783 | + assertTrue(entity.isRepeatable()); |
| 784 | + assertFalse(entity.isChunked()); |
| 785 | + } |
| 786 | + |
| 787 | + @Test |
| 788 | + @DisplayName("writeTo should use cached markSupported for reset decision") |
| 789 | + void writeTo_UsesCachedMarkSupported() throws IOException { |
| 790 | + // Given - Stream that changes markSupported behavior |
| 791 | + AtomicInteger markSupportedCalls = new AtomicInteger(0); |
| 792 | + ByteArrayInputStream baseStream = new ByteArrayInputStream("test".getBytes()); |
| 793 | + InputStream stream = new InputStream() { |
| 794 | + @Override |
| 795 | + public int read() throws IOException { |
| 796 | + return baseStream.read(); |
| 797 | + } |
| 798 | + |
| 799 | + @Override |
| 800 | + public boolean markSupported() { |
| 801 | + return markSupportedCalls.incrementAndGet() == 1; // Only first call returns true |
| 802 | + } |
| 803 | + |
| 804 | + @Override |
| 805 | + public synchronized void reset() throws IOException { |
| 806 | + baseStream.reset(); |
| 807 | + } |
| 808 | + }; |
| 809 | + |
| 810 | + entity = createEntity(stream); |
| 811 | + |
| 812 | + // Write twice |
| 813 | + ByteArrayOutputStream output1 = new ByteArrayOutputStream(); |
| 814 | + entity.writeTo(output1); |
| 815 | + |
| 816 | + ByteArrayOutputStream output2 = new ByteArrayOutputStream(); |
| 817 | + entity.writeTo(output2); |
| 818 | + |
| 819 | + // Then - Both writes succeed using cached markSupported value |
| 820 | + assertEquals("test", output1.toString()); |
| 821 | + assertEquals("test", output2.toString()); |
| 822 | + assertEquals(1, markSupportedCalls.get()); |
| 823 | + } |
| 824 | + |
| 825 | + @Test |
| 826 | + @DisplayName("Non-repeatable stream should not attempt reset") |
| 827 | + void nonRepeatableStream_NoResetAttempt() throws IOException { |
| 828 | + // Given |
| 829 | + AtomicInteger resetCalls = new AtomicInteger(0); |
| 830 | + InputStream nonRepeatableStream = new ByteArrayInputStream("test".getBytes()) { |
| 831 | + @Override |
| 832 | + public boolean markSupported() { |
| 833 | + return false; |
| 834 | + } |
| 835 | + |
| 836 | + @Override |
| 837 | + public synchronized void reset() { |
| 838 | + resetCalls.incrementAndGet(); |
| 839 | + throw new RuntimeException("Reset not supported"); |
| 840 | + } |
| 841 | + }; |
| 842 | + |
| 843 | + entity = createEntity(nonRepeatableStream); |
| 844 | + assertFalse(entity.isRepeatable()); |
| 845 | + entity.writeTo(new ByteArrayOutputStream()); |
| 846 | + entity.writeTo(new ByteArrayOutputStream()); |
| 847 | + assertEquals(0, resetCalls.get()); |
| 848 | + } |
| 849 | + |
| 850 | + @Test |
| 851 | + @DisplayName("Stream should not be read during construction") |
| 852 | + void constructor_DoesNotReadStream() { |
| 853 | + // Given |
| 854 | + InputStream nonReadableStream = new InputStream() { |
| 855 | + @Override |
| 856 | + public int read() throws IOException { |
| 857 | + throw new IOException("Stream should not be read during construction"); |
| 858 | + } |
| 859 | + |
| 860 | + @Override |
| 861 | + public boolean markSupported() { |
| 862 | + return true; |
| 863 | + } |
| 864 | + }; |
| 865 | + assertDoesNotThrow(() -> entity = createEntity(nonReadableStream)); |
| 866 | + assertTrue(entity.isRepeatable()); |
| 867 | + } |
| 868 | + |
| 869 | + @Test |
| 870 | + @DisplayName("getContent should reuse existing stream") |
| 871 | + void getContent_ReusesExistingStream() throws IOException { |
| 872 | + InputStream originalStream = new ByteArrayInputStream("content".getBytes()); |
| 873 | + entity = createEntity(originalStream); |
| 874 | + InputStream content1 = entity.getContent(); |
| 875 | + InputStream content2 = entity.getContent(); |
| 876 | + assertSame(content1, content2); |
| 877 | + } |
| 878 | + |
| 879 | + @Test |
| 880 | + @DisplayName("Empty stream should be repeatable") |
| 881 | + void emptyStream_IsRepeatable() { |
| 882 | + // Given - No content provider |
| 883 | + HttpExecuteRequest request = HttpExecuteRequest.builder() |
| 884 | + .request(httpRequestBuilder.build()) |
| 885 | + .build(); |
| 886 | + entity = new RepeatableInputStreamRequestEntity(request); |
| 887 | + assertTrue(entity.isRepeatable()); |
| 888 | + } |
| 889 | + |
| 890 | + // Helper methods |
| 891 | + private RepeatableInputStreamRequestEntity createEntity(InputStream stream) { |
| 892 | + return createEntity(() -> stream); |
| 893 | + } |
| 894 | + |
| 895 | + private RepeatableInputStreamRequestEntity createEntity(ContentStreamProvider provider) { |
| 896 | + HttpExecuteRequest request = HttpExecuteRequest.builder() |
| 897 | + .request(httpRequestBuilder.build()) |
| 898 | + .contentStreamProvider(provider) |
| 899 | + .build(); |
| 900 | + return new RepeatableInputStreamRequestEntity(request); |
| 901 | + } |
793 | 902 | }
|
794 | 903 |
|
0 commit comments