| /* | |
| * Licensed to the Apache Software Foundation (ASF) under one | |
| * or more contributor license agreements. See the NOTICE file | |
| * distributed with this work for additional information | |
| * regarding copyright ownership. The ASF licenses this file | |
| * to you under the Apache License, Version 2.0 (the | |
| * "License"); you may not use this file except in compliance | |
| * with the License. You may obtain a copy of the License at | |
| * | |
| * http://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, | |
| * software distributed under the License is distributed on an | |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
| * KIND, either express or implied. See the License for the | |
| * specific language governing permissions and limitations | |
| * under the License. | |
| */ | |
| #include "paimon/io/buffered_input_stream.h" | |
| #include <utility> | |
| #include "gtest/gtest.h" | |
| #include "paimon/common/io/memory_segment_output_stream.h" | |
| #include "paimon/common/memory/memory_segment_utils.h" | |
| #include "paimon/io/byte_array_input_stream.h" | |
| #include "paimon/memory/bytes.h" | |
| #include "paimon/memory/memory_pool.h" | |
| #include "paimon/testing/utils/testharness.h" | |
| namespace paimon::test { | |
| TEST(BufferedInputStreamTest, TestSimple) { | |
| auto pool = GetDefaultPool(); | |
| auto output_stream = std::make_unique<MemorySegmentOutputStream>(/*segment_size=*/8, pool); | |
| std::string str = "abcdef"; | |
| auto bytes = std::make_shared<Bytes>(str, pool.get()); | |
| output_stream->WriteBytes(bytes); | |
| auto out_bytes = MemorySegmentUtils::CopyToBytes(output_stream->Segments(), 0, | |
| output_stream->CurrentSize(), pool.get()); | |
| auto in = std::make_shared<ByteArrayInputStream>(out_bytes->data(), out_bytes->size()); | |
| auto input_stream = std::make_shared<BufferedInputStream>(in, /*buffer_size=*/4, pool.get()); | |
| ASSERT_EQ(6, input_stream->Length().value()); | |
| ASSERT_TRUE(input_stream->GetUri().value().empty()); | |
| // read from pos 0 | |
| std::string value(3, '\0'); | |
| ASSERT_EQ(3, input_stream->Read(value.data(), value.size()).value()); | |
| ASSERT_EQ("abc", value); | |
| ASSERT_EQ(3, input_stream->GetPos().value()); | |
| ASSERT_EQ(3, input_stream->Read(value.data(), value.size()).value()); | |
| ASSERT_EQ("def", value); | |
| ASSERT_EQ(6, input_stream->GetPos().value()); | |
| // read from pos 1 | |
| ASSERT_NOK_WITH_MSG(input_stream->Read(value.data(), value.size(), /*offset=*/1), | |
| "BufferedInputStream does not support Read from offset"); | |
| ASSERT_EQ(6, input_stream->GetPos().value()); | |
| // seek to pos 3 | |
| ASSERT_OK(input_stream->Seek(3, SeekOrigin::FS_SEEK_SET)); | |
| ASSERT_EQ(3, input_stream->Read(value.data(), value.size()).value()); | |
| ASSERT_EQ("def", value); | |
| ASSERT_EQ(6, input_stream->GetPos().value()); | |
| // seek to pos 2 | |
| ASSERT_OK(input_stream->Seek(-4, SeekOrigin::FS_SEEK_END)); | |
| ASSERT_EQ(3, input_stream->Read(value.data(), value.size()).value()); | |
| ASSERT_EQ("cde", value); | |
| ASSERT_EQ(5, input_stream->GetPos().value()); | |
| // seek to pos 1 | |
| ASSERT_OK(input_stream->Seek(-4, SeekOrigin::FS_SEEK_CUR)); | |
| ASSERT_EQ(3, input_stream->Read(value.data(), value.size()).value()); | |
| ASSERT_EQ("bcd", value); | |
| ASSERT_EQ(4, input_stream->GetPos().value()); | |
| // test exceed eof, seek to pos 4, want to read 4 bytes | |
| ASSERT_OK(input_stream->Seek(-2, SeekOrigin::FS_SEEK_END)); | |
| ASSERT_NOK_WITH_MSG(input_stream->Read(value.data(), value.size()), | |
| "InnerRead failed, after Fill(), still no bytes available (may read eof), " | |
| "but expect read 1 bytes"); | |
| // test invalid seek | |
| ASSERT_NOK_WITH_MSG(input_stream->Seek(100, SeekOrigin::FS_SEEK_CUR), | |
| "invalid seek, after seek, current pos 106, length 6"); | |
| // test ReadAsync not implemented | |
| bool read_finished = false; | |
| auto callback = [&](Status status) { | |
| ASSERT_TRUE(status.IsNotImplemented()); | |
| read_finished = true; | |
| }; | |
| input_stream->ReadAsync(value.data(), value.size(), /*offset=*/0, callback); | |
| ASSERT_TRUE(read_finished); | |
| ASSERT_OK(input_stream->Close()); | |
| } | |
| TEST(BufferedInputStreamTest, TestSeek) { | |
| // Data: "0123456789abcdef" (16 bytes), buffer_size = 8 | |
| auto pool = GetDefaultPool(); | |
| std::string data = "0123456789abcdef"; | |
| auto in = std::make_shared<ByteArrayInputStream>(reinterpret_cast<const char*>(data.data()), | |
| data.size()); | |
| auto stream = std::make_shared<BufferedInputStream>(in, /*buffer_size=*/8, pool.get()); | |
| // Helper: verify buffer_ content matches expected substring of data | |
| auto check_buffer = [&](const BufferedInputStream& s, const std::string& expected_content, | |
| int32_t expected_pos, int32_t expected_count) { | |
| ASSERT_EQ(s.pos_, expected_pos); | |
| ASSERT_EQ(s.count_, expected_count); | |
| std::string actual(s.buffer_->data(), s.buffer_->data() + expected_count); | |
| ASSERT_EQ(actual, expected_content) << "buffer content mismatch: actual=\"" << actual | |
| << "\", expected=\"" << expected_content << "\""; | |
| }; | |
| std::string buf(4, '\0'); | |
| // Initial state: buffer empty | |
| ASSERT_EQ(stream->pos_, 0); | |
| ASSERT_EQ(stream->count_, 0); | |
| // FS_SEEK_SET slow path (buffer empty, count_==0): seek to pos 4, read "4567" | |
| // First Read calls Fill: reads 8 bytes from pos 4 -> buffer = "456789ab", count_=8. | |
| // Then consumes 4 bytes -> pos_=4. | |
| { | |
| ASSERT_OK(stream->Seek(4, SeekOrigin::FS_SEEK_SET)); | |
| // After seek: slow path, buffer invalidated | |
| ASSERT_EQ(stream->pos_, 0); | |
| ASSERT_EQ(stream->count_, 0); | |
| ASSERT_EQ(4, stream->GetPos().value()); | |
| ASSERT_EQ(4, stream->Read(buf.data(), 4).value()); | |
| ASSERT_EQ("4567", buf); | |
| check_buffer(*stream, "456789ab", 4, 8); | |
| } | |
| // FS_SEEK_CUR buffer hit: pos=8, seek -4 -> target=4, inside buffer [4..12) | |
| // Fast path: only adjusts pos_ to 0, count_ stays 8, buffer unchanged. | |
| { | |
| ASSERT_OK(stream->Seek(-4, SeekOrigin::FS_SEEK_CUR)); | |
| check_buffer(*stream, "456789ab", 0, 8); | |
| ASSERT_EQ(4, stream->GetPos().value()); | |
| ASSERT_EQ(4, stream->Read(buf.data(), 4).value()); | |
| ASSERT_EQ("4567", buf); | |
| check_buffer(*stream, "456789ab", 4, 8); | |
| } | |
| // FS_SEEK_SET buffer hit: pos=8, seek to 5, inside buffer [4..12) | |
| // Fast path: pos_ = 5 - 4 = 1, count_ stays 8, buffer unchanged. | |
| { | |
| ASSERT_OK(stream->Seek(5, SeekOrigin::FS_SEEK_SET)); | |
| check_buffer(*stream, "456789ab", 1, 8); | |
| ASSERT_EQ(5, stream->GetPos().value()); | |
| ASSERT_EQ(4, stream->Read(buf.data(), 4).value()); | |
| ASSERT_EQ("5678", buf); | |
| check_buffer(*stream, "456789ab", 5, 8); | |
| } | |
| // FS_SEEK_END buffer miss: target = 16 + (-6) = 10 | |
| // Current buffer covers [4..12). pos 10 is inside [4..12) -> actually buffer hit! | |
| // Fast path: pos_ = 10 - 4 = 6, count_ stays 8. | |
| { | |
| ASSERT_OK(stream->Seek(-6, SeekOrigin::FS_SEEK_END)); | |
| check_buffer(*stream, "456789ab", 6, 8); | |
| ASSERT_EQ(10, stream->GetPos().value()); | |
| // Read 4 bytes from pos 10: 2 left in buffer ("ab"), then refill. | |
| ASSERT_EQ(4, stream->Read(buf.data(), 4).value()); | |
| ASSERT_EQ("abcd", buf); | |
| // After consuming "ab" (pos_==count_==8), InnerRead calls Fill from in_ pos 12, | |
| // reads [12..16) -> buffer = "cdef", count_=4, then consumes 2 -> pos_=2. | |
| check_buffer(*stream, "cdef", 2, 4); | |
| } | |
| // Buffer miss slow path: seek to pos 0, current buffer covers [12..16). | |
| // Target 0 is outside [12..16) -> slow path, buffer invalidated. | |
| { | |
| ASSERT_OK(stream->Seek(0, SeekOrigin::FS_SEEK_SET)); | |
| ASSERT_EQ(stream->pos_, 0); | |
| ASSERT_EQ(stream->count_, 0); | |
| ASSERT_EQ(0, stream->GetPos().value()); | |
| ASSERT_EQ(4, stream->Read(buf.data(), 4).value()); | |
| ASSERT_EQ("0123", buf); | |
| // Fill reads [0..8) -> buffer = "01234567", count_=8, pos_=4 | |
| check_buffer(*stream, "01234567", 4, 8); | |
| } | |
| // Buffer hit at exact boundary start: seek to pos 0 (= buf_start_abs=0) | |
| // Fast path: pos_ = 0, count_ stays 8, buffer unchanged. | |
| { | |
| ASSERT_OK(stream->Seek(0, SeekOrigin::FS_SEEK_SET)); | |
| check_buffer(*stream, "01234567", 0, 8); | |
| ASSERT_EQ(4, stream->Read(buf.data(), 4).value()); | |
| ASSERT_EQ("0123", buf); | |
| check_buffer(*stream, "01234567", 4, 8); | |
| } | |
| // Buffer hit at exact boundary end: seek to pos 8 (= buf_end_abs=8) | |
| // Fast path: pos_ = 8 == count_, buffer unchanged but next Read triggers refill. | |
| { | |
| ASSERT_OK(stream->Seek(8, SeekOrigin::FS_SEEK_SET)); | |
| check_buffer(*stream, "01234567", 8, 8); | |
| ASSERT_EQ(8, stream->GetPos().value()); | |
| ASSERT_EQ(4, stream->Read(buf.data(), 4).value()); | |
| ASSERT_EQ("89ab", buf); | |
| // pos_==count_ triggered Fill: buffer = "89abcdef", count_=8, pos_=4 | |
| check_buffer(*stream, "89abcdef", 4, 8); | |
| } | |
| // FS_SEEK_CUR buffer miss: pos=12, seek -12 -> target=0, outside [8..16) | |
| { | |
| ASSERT_OK(stream->Seek(-12, SeekOrigin::FS_SEEK_CUR)); | |
| ASSERT_EQ(stream->pos_, 0); | |
| ASSERT_EQ(stream->count_, 0); | |
| ASSERT_EQ(4, stream->Read(buf.data(), 4).value()); | |
| ASSERT_EQ("0123", buf); | |
| check_buffer(*stream, "01234567", 4, 8); | |
| } | |
| // FS_SEEK_END buffer hit: fill buffer near file end, then seek within via FS_SEEK_END. | |
| { | |
| ASSERT_OK(stream->Seek(12, SeekOrigin::FS_SEEK_SET)); | |
| // pos 12 is outside current buffer [0..8) -> slow path | |
| ASSERT_EQ(stream->pos_, 0); | |
| ASSERT_EQ(stream->count_, 0); | |
| ASSERT_EQ(4, stream->Read(buf.data(), 4).value()); | |
| ASSERT_EQ("cdef", buf); | |
| // Fill from 12: buffer = "cdef", count_=4, pos_=4 | |
| check_buffer(*stream, "cdef", 4, 4); | |
| // Seek -4 from end -> target = 12, buffer covers [12..16), 12 is inside. | |
| // Fast path: pos_ = 12 - 12 = 0 | |
| ASSERT_OK(stream->Seek(-4, SeekOrigin::FS_SEEK_END)); | |
| check_buffer(*stream, "cdef", 0, 4); | |
| ASSERT_EQ(12, stream->GetPos().value()); | |
| ASSERT_EQ(4, stream->Read(buf.data(), 4).value()); | |
| ASSERT_EQ("cdef", buf); | |
| check_buffer(*stream, "cdef", 4, 4); | |
| } | |
| // Empty buffer (count_==0): fresh stream, seek always takes slow path. | |
| { | |
| auto in2 = std::make_shared<ByteArrayInputStream>( | |
| reinterpret_cast<const char*>(data.data()), data.size()); | |
| auto fresh = std::make_shared<BufferedInputStream>(in2, /*buffer_size=*/8, pool.get()); | |
| ASSERT_EQ(fresh->pos_, 0); | |
| ASSERT_EQ(fresh->count_, 0); | |
| ASSERT_OK(fresh->Seek(10, SeekOrigin::FS_SEEK_SET)); | |
| ASSERT_EQ(fresh->pos_, 0); | |
| ASSERT_EQ(fresh->count_, 0); | |
| ASSERT_EQ(10, fresh->GetPos().value()); | |
| ASSERT_EQ(4, fresh->Read(buf.data(), 4).value()); | |
| ASSERT_EQ("abcd", buf); | |
| // Fill from 10: buffer = "abcdef", count_=6, pos_=4 | |
| check_buffer(*fresh, "abcdef", 4, 6); | |
| } | |
| } | |
| } // namespace paimon::test |