This is an automated email from the ASF dual-hosted git repository.

bcall pushed a commit to branch unit-test-coverage
in repository https://gitbox.apache.org/repos/asf/trafficserver.git

commit a993b11aabffec7add2b8b949489540edb72aa1d
Author: Bryan Call <[email protected]>
AuthorDate: Tue Jan 20 19:32:43 2026 -0800

    Add mock framework for unit testing (MockNetVC, MockCacheVC)
    
    Add test utilities library with mock objects for unit testing:
    - MockNetVC: Mock NetVConnection for testing network code
    - MockCacheVC: Mock CacheVConnection for testing cache code
    - MockIOBuffer: Helper for creating/managing IOBuffers in tests
    - TestEventProcessor: Controllable event processor for tests
    
    This is part of the unit test coverage improvement initiative.
---
 src/test_utils/CMakeLists.txt |   3 +
 src/test_utils/MockCacheVC.cc | 322 ++++++++++++++++++++++++++++++++++++
 src/test_utils/MockCacheVC.h  | 155 +++++++++++++++++
 src/test_utils/MockNetVC.cc   | 376 ++++++++++++++++++++++++++++++++++++++++++
 src/test_utils/MockNetVC.h    | 176 ++++++++++++++++++++
 5 files changed, 1032 insertions(+)

diff --git a/src/test_utils/CMakeLists.txt b/src/test_utils/CMakeLists.txt
index 339994e283..7ace822e9d 100644
--- a/src/test_utils/CMakeLists.txt
+++ b/src/test_utils/CMakeLists.txt
@@ -21,6 +21,8 @@
 add_library(
   ats_test_utils STATIC
   MockIOBuffer.cc
+  MockNetVC.cc
+  MockCacheVC.cc
   TestEventProcessor.cc
 )
 
@@ -33,6 +35,7 @@ target_include_directories(
 target_link_libraries(
   ats_test_utils
   PUBLIC ts::inkevent
+         ts::inknet
          ts::tscore
 )
 
diff --git a/src/test_utils/MockCacheVC.cc b/src/test_utils/MockCacheVC.cc
new file mode 100644
index 0000000000..af7b6c945b
--- /dev/null
+++ b/src/test_utils/MockCacheVC.cc
@@ -0,0 +1,322 @@
+/** @file
+
+  Mock CacheVConnection implementation for unit testing
+
+  @section license License
+
+  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 "test_utils/MockCacheVC.h"
+#include "tscore/ink_memory.h"
+#include <cstring>
+
+MockCacheVC::MockCacheVC()
+{
+  // Initialize read buffer for simulating cached data
+  read_buffer_        = new_MIOBuffer(BUFFER_SIZE_INDEX_4K);
+  read_buffer_reader_ = read_buffer_->alloc_reader();
+
+  // Initialize write capture buffer
+  write_capture_        = new_MIOBuffer(BUFFER_SIZE_INDEX_4K);
+  write_capture_reader_ = write_capture_->alloc_reader();
+}
+
+MockCacheVC::~MockCacheVC()
+{
+  if (read_buffer_) {
+    free_MIOBuffer(read_buffer_);
+  }
+  if (write_capture_) {
+    free_MIOBuffer(write_capture_);
+  }
+  if (header_) {
+    ats_free(header_);
+  }
+}
+
+// ============== Test Setup Methods ==============
+
+void
+MockCacheVC::set_read_data(std::string_view data)
+{
+  // Clear existing data
+  read_buffer_reader_->consume(read_buffer_reader_->read_avail());
+  // Write new data
+  read_buffer_->write(data.data(), data.size());
+  object_size_ = data.size();
+}
+
+std::string
+MockCacheVC::get_written_data() const
+{
+  std::string result;
+  int64_t     avail = write_capture_reader_->read_avail();
+  if (avail > 0) {
+    result.resize(avail);
+    IOBufferBlock *block  = write_capture_reader_->get_current_block();
+    int64_t        offset = write_capture_reader_->start_offset;
+    size_t         pos    = 0;
+
+    while (block && pos < static_cast<size_t>(avail)) {
+      const char *start = block->start() + offset;
+      int64_t     len   = block->read_avail() - offset;
+      if (len > 0) {
+        if (pos + len > static_cast<size_t>(avail)) {
+          len = avail - pos;
+        }
+        memcpy(&result[pos], start, len);
+        pos += len;
+      }
+      block  = block->next.get();
+      offset = 0;
+    }
+  }
+  return result;
+}
+
+void
+MockCacheVC::clear_written_data()
+{
+  write_capture_reader_->consume(write_capture_reader_->read_avail());
+}
+
+void
+MockCacheVC::set_http_info_for_test(CacheHTTPInfo *info)
+{
+  http_info_ = info;
+}
+
+void
+MockCacheVC::set_ram_cache_hit(bool is_hit)
+{
+  ram_cache_hit_ = is_hit;
+}
+
+void
+MockCacheVC::set_object_size(int64_t size)
+{
+  object_size_ = size;
+}
+
+void
+MockCacheVC::simulate_cache_miss()
+{
+  if (read_vio_.cont) {
+    read_vio_.cont->handleEvent(CACHE_EVENT_OPEN_READ_FAILED, this);
+  }
+}
+
+void
+MockCacheVC::simulate_error(int lerrno)
+{
+  if (read_vio_.cont) {
+    read_vio_.cont->handleEvent(VC_EVENT_ERROR, this);
+  }
+  close_error_ = lerrno;
+}
+
+// ============== CacheVConnection Interface Implementation ==============
+
+VIO *
+MockCacheVC::do_io_read(Continuation *c, int64_t nbytes, MIOBuffer *buf)
+{
+  read_vio_.op        = VIO::READ;
+  read_vio_.cont      = c;
+  read_vio_.nbytes    = nbytes;
+  read_vio_.ndone     = 0;
+  read_vio_.buffer.writer_for(buf);
+  read_vio_.vc_server = this;
+
+  // Copy available data from our read_buffer to the provided buffer
+  int64_t avail = read_buffer_reader_->read_avail();
+  if (avail > 0) {
+    int64_t to_copy = (nbytes == INT64_MAX) ? avail : std::min(avail, nbytes);
+    buf->write(read_buffer_reader_, to_copy);
+    read_buffer_reader_->consume(to_copy);
+    read_vio_.ndone = to_copy;
+  }
+
+  return &read_vio_;
+}
+
+VIO *
+MockCacheVC::do_io_pread(Continuation *c, int64_t nbytes, MIOBuffer *buf, 
int64_t offset)
+{
+  // For simplicity, pread behaves like read but skips 'offset' bytes first
+  if (offset > 0) {
+    int64_t avail = read_buffer_reader_->read_avail();
+    if (offset < avail) {
+      read_buffer_reader_->consume(offset);
+    } else {
+      read_buffer_reader_->consume(avail);
+    }
+  }
+  return do_io_read(c, nbytes, buf);
+}
+
+VIO *
+MockCacheVC::do_io_write(Continuation *c, int64_t nbytes, IOBufferReader *buf, 
bool /* owner ATS_UNUSED */)
+{
+  write_vio_.op        = VIO::WRITE;
+  write_vio_.cont      = c;
+  write_vio_.nbytes    = nbytes;
+  write_vio_.ndone     = 0;
+  write_vio_.buffer.reader_for(buf);
+  write_vio_.vc_server = this;
+
+  // Capture any data that's already available
+  int64_t avail = buf->read_avail();
+  if (avail > 0) {
+    int64_t to_copy = (nbytes == INT64_MAX) ? avail : std::min(avail, nbytes);
+    write_capture_->write(buf, to_copy);
+    buf->consume(to_copy);
+    write_vio_.ndone = to_copy;
+  }
+
+  return &write_vio_;
+}
+
+void
+MockCacheVC::do_io_close(int lerrno)
+{
+  closed_       = true;
+  close_error_  = lerrno;
+  read_vio_.op  = VIO::NONE;
+  write_vio_.op = VIO::NONE;
+}
+
+void
+MockCacheVC::reenable(VIO *vio)
+{
+  if (vio == &read_vio_ && read_vio_.op == VIO::READ) {
+    int64_t avail = read_buffer_reader_->read_avail();
+    if (avail > 0 && read_vio_.cont) {
+      int64_t remaining = read_vio_.nbytes - read_vio_.ndone;
+      int64_t to_copy   = (remaining == INT64_MAX) ? avail : std::min(avail, 
remaining);
+      if (to_copy > 0) {
+        read_vio_.buffer.writer()->write(read_buffer_reader_, to_copy);
+        read_buffer_reader_->consume(to_copy);
+        read_vio_.ndone += to_copy;
+        read_vio_.cont->handleEvent(VC_EVENT_READ_READY, &read_vio_);
+      }
+    }
+  } else if (vio == &write_vio_ && write_vio_.op == VIO::WRITE) {
+    IOBufferReader *reader = write_vio_.buffer.reader();
+    if (reader) {
+      int64_t avail     = reader->read_avail();
+      int64_t remaining = write_vio_.nbytes - write_vio_.ndone;
+      int64_t to_copy   = (remaining == INT64_MAX) ? avail : std::min(avail, 
remaining);
+      if (to_copy > 0) {
+        write_capture_->write(reader, to_copy);
+        reader->consume(to_copy);
+        write_vio_.ndone += to_copy;
+        if (write_vio_.cont) {
+          write_vio_.cont->handleEvent(VC_EVENT_WRITE_READY, &write_vio_);
+        }
+      }
+    }
+  }
+}
+
+void
+MockCacheVC::reenable_re(VIO *vio)
+{
+  reenable(vio);
+}
+
+int
+MockCacheVC::get_header(void **ptr, int *len)
+{
+  if (header_ && header_len_ > 0) {
+    *ptr = header_;
+    *len = header_len_;
+    return 0;
+  }
+  return -1;
+}
+
+int
+MockCacheVC::set_header(void *ptr, int len)
+{
+  if (header_) {
+    ats_free(header_);
+  }
+  header_     = ats_malloc(len);
+  header_len_ = len;
+  memcpy(header_, ptr, len);
+  return 0;
+}
+
+int
+MockCacheVC::get_single_data(void **ptr, int *len)
+{
+  // Return available data as a single block
+  int64_t avail = read_buffer_reader_->read_avail();
+  if (avail > 0) {
+    IOBufferBlock *block = read_buffer_reader_->get_current_block();
+    if (block) {
+      *ptr = const_cast<char *>(block->start() + 
read_buffer_reader_->start_offset);
+      *len = static_cast<int>(avail);
+      return 0;
+    }
+  }
+  return -1;
+}
+
+void
+MockCacheVC::set_http_info(CacheHTTPInfo *info)
+{
+  http_info_ = info;
+}
+
+void
+MockCacheVC::get_http_info(CacheHTTPInfo **info)
+{
+  *info = http_info_;
+}
+
+bool
+MockCacheVC::is_ram_cache_hit() const
+{
+  return ram_cache_hit_;
+}
+
+bool
+MockCacheVC::set_pin_in_cache(time_t t)
+{
+  pin_in_cache_ = t;
+  return true;
+}
+
+time_t
+MockCacheVC::get_pin_in_cache()
+{
+  return pin_in_cache_;
+}
+
+int64_t
+MockCacheVC::get_object_size()
+{
+  return object_size_;
+}
+
+bool
+MockCacheVC::is_pread_capable()
+{
+  return true; // Mock supports pread
+}
diff --git a/src/test_utils/MockCacheVC.h b/src/test_utils/MockCacheVC.h
new file mode 100644
index 0000000000..94ef3fffc1
--- /dev/null
+++ b/src/test_utils/MockCacheVC.h
@@ -0,0 +1,155 @@
+/** @file
+
+  Mock CacheVConnection for unit testing
+
+  @section license License
+
+  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.
+*/
+
+#pragma once
+
+#include "iocore/cache/Cache.h"
+#include "iocore/eventsystem/IOBuffer.h"
+#include <string>
+#include <string_view>
+
+/**
+ * @class MockCacheVC
+ * @brief Mock CacheVConnection for unit testing
+ *
+ * This class provides a testable mock for CacheVConnection that allows:
+ * - Simulating cache reads (what would be read from cache)
+ * - Capturing cache writes (what would be written to cache)
+ * - Simulating cache miss/hit scenarios
+ * - Testing HTTP header handling
+ *
+ * Usage:
+ * @code
+ * MockCacheVC mock_cache;
+ * mock_cache.set_read_data("cached content");
+ * mock_cache.set_http_info(&cached_http_info);
+ * // ... use mock_cache in test ...
+ * std::string written = mock_cache.get_written_data();
+ * @endcode
+ */
+class MockCacheVC : public CacheVConnection
+{
+public:
+  MockCacheVC();
+  ~MockCacheVC() override;
+
+  // ============== Test Setup Methods ==============
+
+  /**
+   * Set the data that will be available for reading (cache hit scenario)
+   * @param data Data to be read from cache
+   */
+  void set_read_data(std::string_view data);
+
+  /**
+   * Get data that has been written to cache
+   * @return All data written to this mock cache
+   */
+  std::string get_written_data() const;
+
+  /**
+   * Clear the write buffer (reset captured writes)
+   */
+  void clear_written_data();
+
+  /**
+   * Set the HTTP info for this cache entry
+   * @param info HTTP info to associate with this cache entry
+   */
+  void set_http_info_for_test(CacheHTTPInfo *info);
+
+  /**
+   * Set whether this is a RAM cache hit
+   * @param is_hit true if this should simulate a RAM cache hit
+   */
+  void set_ram_cache_hit(bool is_hit);
+
+  /**
+   * Set the object size
+   * @param size Size of the cached object
+   */
+  void set_object_size(int64_t size);
+
+  /**
+   * Simulate a cache miss (sends CACHE_EVENT_OPEN_READ_FAILED)
+   */
+  void simulate_cache_miss();
+
+  /**
+   * Simulate a cache error
+   * @param lerrno Error number
+   */
+  void simulate_error(int lerrno);
+
+  // ============== CacheVConnection Interface Implementation ==============
+
+  VIO *do_io_read(Continuation *c, int64_t nbytes, MIOBuffer *buf) override;
+  VIO *do_io_pread(Continuation *c, int64_t nbytes, MIOBuffer *buf, int64_t 
offset) override;
+  VIO *do_io_write(Continuation *c, int64_t nbytes, IOBufferReader *buf, bool 
owner = false) override;
+  void do_io_close(int lerrno = -1) override;
+  void reenable(VIO *vio) override;
+  void reenable_re(VIO *vio) override;
+
+  int get_header(void **ptr, int *len) override;
+  int set_header(void *ptr, int len) override;
+  int get_single_data(void **ptr, int *len) override;
+
+  void set_http_info(CacheHTTPInfo *info) override;
+  void get_http_info(CacheHTTPInfo **info) override;
+
+  bool    is_ram_cache_hit() const override;
+  bool    set_pin_in_cache(time_t t) override;
+  time_t  get_pin_in_cache() override;
+  int64_t get_object_size() override;
+  bool    is_pread_capable() override;
+
+  // ============== Test State Access ==============
+
+  bool is_closed() const { return closed_; }
+  int  get_close_error() const { return close_error_; }
+
+private:
+  // Read side
+  MIOBuffer      *read_buffer_        = nullptr;
+  IOBufferReader *read_buffer_reader_ = nullptr;
+  VIO             read_vio_;
+
+  // Write side
+  MIOBuffer      *write_capture_        = nullptr;
+  IOBufferReader *write_capture_reader_ = nullptr;
+  VIO             write_vio_;
+
+  // Header storage
+  void  *header_     = nullptr;
+  int    header_len_ = 0;
+
+  // HTTP info
+  CacheHTTPInfo *http_info_ = nullptr;
+
+  // State
+  bool    closed_        = false;
+  int     close_error_   = 0;
+  bool    ram_cache_hit_ = false;
+  time_t  pin_in_cache_  = 0;
+  int64_t object_size_   = 0;
+};
diff --git a/src/test_utils/MockNetVC.cc b/src/test_utils/MockNetVC.cc
new file mode 100644
index 0000000000..145bbfe750
--- /dev/null
+++ b/src/test_utils/MockNetVC.cc
@@ -0,0 +1,376 @@
+/** @file
+
+  Mock NetVConnection implementation for unit testing
+
+  @section license License
+
+  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 "test_utils/MockNetVC.h"
+#include "tscore/ink_memory.h"
+
+MockNetVC::MockNetVC()
+{
+  // Initialize read buffer for simulating incoming data
+  read_buffer_        = new_MIOBuffer(BUFFER_SIZE_INDEX_4K);
+  read_buffer_reader_ = read_buffer_->alloc_reader();
+
+  // Initialize write capture buffer
+  write_capture_        = new_MIOBuffer(BUFFER_SIZE_INDEX_4K);
+  write_capture_reader_ = write_capture_->alloc_reader();
+}
+
+MockNetVC::~MockNetVC()
+{
+  if (read_buffer_) {
+    free_MIOBuffer(read_buffer_);
+  }
+  if (write_capture_) {
+    free_MIOBuffer(write_capture_);
+  }
+}
+
+// ============== Test Setup Methods ==============
+
+void
+MockNetVC::set_read_data(std::string_view data)
+{
+  // Clear existing data
+  read_buffer_reader_->consume(read_buffer_reader_->read_avail());
+  // Write new data
+  read_buffer_->write(data.data(), data.size());
+}
+
+void
+MockNetVC::append_read_data(std::string_view data)
+{
+  read_buffer_->write(data.data(), data.size());
+}
+
+std::string
+MockNetVC::get_written_data() const
+{
+  std::string result;
+  int64_t     avail = write_capture_reader_->read_avail();
+  if (avail > 0) {
+    result.resize(avail);
+    // We need to read without consuming, so copy block by block
+    IOBufferBlock *block = write_capture_reader_->get_current_block();
+    int64_t        offset = write_capture_reader_->start_offset;
+    size_t         pos    = 0;
+
+    while (block && pos < static_cast<size_t>(avail)) {
+      const char *start = block->start() + offset;
+      int64_t     len   = block->read_avail() - offset;
+      if (len > 0) {
+        if (pos + len > static_cast<size_t>(avail)) {
+          len = avail - pos;
+        }
+        memcpy(&result[pos], start, len);
+        pos += len;
+      }
+      block  = block->next.get();
+      offset = 0;
+    }
+  }
+  return result;
+}
+
+void
+MockNetVC::clear_written_data()
+{
+  write_capture_reader_->consume(write_capture_reader_->read_avail());
+}
+
+void
+MockNetVC::simulate_eos()
+{
+  eos_ = true;
+  if (read_vio_.cont) {
+    read_vio_.cont->handleEvent(VC_EVENT_EOS, &read_vio_);
+  }
+}
+
+void
+MockNetVC::simulate_error(int lerrno)
+{
+  error_ = lerrno;
+  if (read_vio_.cont) {
+    read_vio_.cont->handleEvent(VC_EVENT_ERROR, &read_vio_);
+  }
+}
+
+void
+MockNetVC::simulate_inactivity_timeout()
+{
+  if (read_vio_.cont) {
+    read_vio_.cont->handleEvent(VC_EVENT_INACTIVITY_TIMEOUT, &read_vio_);
+  }
+}
+
+void
+MockNetVC::simulate_active_timeout()
+{
+  if (read_vio_.cont) {
+    read_vio_.cont->handleEvent(VC_EVENT_ACTIVE_TIMEOUT, &read_vio_);
+  }
+}
+
+void
+MockNetVC::set_local_address(const sockaddr *addr)
+{
+  ats_ip_copy(&local_addr, addr);
+  got_local_addr = true;
+}
+
+void
+MockNetVC::set_remote_address(const sockaddr *addr)
+{
+  ats_ip_copy(&remote_addr, addr);
+  got_remote_addr = true;
+}
+
+// ============== NetVConnection Interface Implementation ==============
+
+VIO *
+MockNetVC::do_io_read(Continuation *c, int64_t nbytes, MIOBuffer *buf)
+{
+  read_vio_.op        = VIO::READ;
+  read_vio_.cont      = c;
+  read_vio_.nbytes    = nbytes;
+  read_vio_.ndone     = 0;
+  read_vio_.buffer.writer_for(buf);
+  read_vio_.vc_server = this;
+
+  // Copy available data from our read_buffer to the provided buffer
+  int64_t avail = read_buffer_reader_->read_avail();
+  if (avail > 0) {
+    int64_t to_copy = (nbytes == INT64_MAX) ? avail : std::min(avail, nbytes);
+    buf->write(read_buffer_reader_, to_copy);
+    read_buffer_reader_->consume(to_copy);
+    read_vio_.ndone = to_copy;
+  }
+
+  return &read_vio_;
+}
+
+VIO *
+MockNetVC::do_io_write(Continuation *c, int64_t nbytes, IOBufferReader *buf, 
bool /* owner ATS_UNUSED */)
+{
+  write_vio_.op        = VIO::WRITE;
+  write_vio_.cont      = c;
+  write_vio_.nbytes    = nbytes;
+  write_vio_.ndone     = 0;
+  write_vio_.buffer.reader_for(buf);
+  write_vio_.vc_server = this;
+
+  // Capture any data that's already available
+  int64_t avail = buf->read_avail();
+  if (avail > 0) {
+    int64_t to_copy = (nbytes == INT64_MAX) ? avail : std::min(avail, nbytes);
+    write_capture_->write(buf, to_copy);
+    buf->consume(to_copy);
+    write_vio_.ndone = to_copy;
+  }
+
+  return &write_vio_;
+}
+
+void
+MockNetVC::do_io_close(int lerrno)
+{
+  closed_      = true;
+  close_error_ = lerrno;
+  read_vio_.op  = VIO::NONE;
+  write_vio_.op = VIO::NONE;
+}
+
+void
+MockNetVC::do_io_shutdown(ShutdownHowTo_t howto)
+{
+  switch (howto) {
+  case IO_SHUTDOWN_READ:
+    read_shutdown_ = true;
+    read_vio_.op   = VIO::NONE;
+    break;
+  case IO_SHUTDOWN_WRITE:
+    write_shutdown_ = true;
+    write_vio_.op   = VIO::NONE;
+    break;
+  case IO_SHUTDOWN_READWRITE:
+    read_shutdown_  = true;
+    write_shutdown_ = true;
+    read_vio_.op    = VIO::NONE;
+    write_vio_.op   = VIO::NONE;
+    break;
+  }
+}
+
+void
+MockNetVC::reenable(VIO *vio)
+{
+  if (vio == &read_vio_ && read_vio_.op == VIO::READ) {
+    // Check if there's more data to read
+    int64_t avail = read_buffer_reader_->read_avail();
+    if (avail > 0 && read_vio_.cont) {
+      int64_t remaining = read_vio_.nbytes - read_vio_.ndone;
+      int64_t to_copy   = (remaining == INT64_MAX) ? avail : std::min(avail, 
remaining);
+      if (to_copy > 0) {
+        read_vio_.buffer.writer()->write(read_buffer_reader_, to_copy);
+        read_buffer_reader_->consume(to_copy);
+        read_vio_.ndone += to_copy;
+        read_vio_.cont->handleEvent(VC_EVENT_READ_READY, &read_vio_);
+      }
+    } else if (eos_ && read_vio_.cont) {
+      read_vio_.cont->handleEvent(VC_EVENT_EOS, &read_vio_);
+    }
+  } else if (vio == &write_vio_ && write_vio_.op == VIO::WRITE) {
+    // Capture any newly available write data
+    IOBufferReader *reader = write_vio_.buffer.reader();
+    if (reader) {
+      int64_t avail     = reader->read_avail();
+      int64_t remaining = write_vio_.nbytes - write_vio_.ndone;
+      int64_t to_copy   = (remaining == INT64_MAX) ? avail : std::min(avail, 
remaining);
+      if (to_copy > 0) {
+        write_capture_->write(reader, to_copy);
+        reader->consume(to_copy);
+        write_vio_.ndone += to_copy;
+        if (write_vio_.cont) {
+          write_vio_.cont->handleEvent(VC_EVENT_WRITE_READY, &write_vio_);
+        }
+      }
+    }
+  }
+}
+
+void
+MockNetVC::reenable_re(VIO *vio)
+{
+  reenable(vio);
+}
+
+void
+MockNetVC::set_active_timeout(ink_hrtime timeout_in)
+{
+  active_timeout_ = timeout_in;
+}
+
+void
+MockNetVC::set_inactivity_timeout(ink_hrtime timeout_in)
+{
+  inactivity_timeout_         = timeout_in;
+  default_inactivity_timeout_ = false;
+}
+
+void
+MockNetVC::set_default_inactivity_timeout(ink_hrtime timeout_in)
+{
+  inactivity_timeout_         = timeout_in;
+  default_inactivity_timeout_ = true;
+}
+
+bool
+MockNetVC::is_default_inactivity_timeout()
+{
+  return default_inactivity_timeout_;
+}
+
+void
+MockNetVC::cancel_active_timeout()
+{
+  active_timeout_ = 0;
+}
+
+void
+MockNetVC::cancel_inactivity_timeout()
+{
+  inactivity_timeout_ = 0;
+}
+
+ink_hrtime
+MockNetVC::get_active_timeout()
+{
+  return active_timeout_;
+}
+
+ink_hrtime
+MockNetVC::get_inactivity_timeout()
+{
+  return inactivity_timeout_;
+}
+
+void
+MockNetVC::apply_options()
+{
+  // No-op for mock
+}
+
+SOCKET
+MockNetVC::get_socket()
+{
+  return -1; // Invalid socket for mock
+}
+
+int
+MockNetVC::set_tcp_congestion_control(tcp_congestion_control_side /* side 
ATS_UNUSED */)
+{
+  return 0; // Success for mock
+}
+
+void
+MockNetVC::set_local_addr()
+{
+  // No-op for mock - use set_local_address() instead
+}
+
+void
+MockNetVC::set_remote_addr()
+{
+  // No-op for mock - use set_remote_address() instead
+}
+
+void
+MockNetVC::set_remote_addr(const sockaddr *addr)
+{
+  set_remote_address(addr);
+}
+
+void
+MockNetVC::set_mptcp_state()
+{
+  // No-op for mock
+}
+
+int
+MockNetVC::populate_protocol(std::string_view *results, int n) const
+{
+  if (n > 0) {
+    results[0] = "tcp";
+    return 1;
+  }
+  return 0;
+}
+
+const char *
+MockNetVC::protocol_contains(std::string_view tag) const
+{
+  if (tag == "tcp") {
+    return "tcp";
+  }
+  return nullptr;
+}
diff --git a/src/test_utils/MockNetVC.h b/src/test_utils/MockNetVC.h
new file mode 100644
index 0000000000..6fb7d0f2d7
--- /dev/null
+++ b/src/test_utils/MockNetVC.h
@@ -0,0 +1,176 @@
+/** @file
+
+  Mock NetVConnection for unit testing
+
+  @section license License
+
+  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.
+*/
+
+#pragma once
+
+#include "iocore/net/NetVConnection.h"
+#include "iocore/eventsystem/IOBuffer.h"
+#include <string>
+#include <string_view>
+
+/**
+ * @class MockNetVC
+ * @brief Mock NetVConnection for unit testing
+ *
+ * This class provides a testable mock for NetVConnection that allows:
+ * - Simulating incoming data (what would be read from a network connection)
+ * - Capturing outgoing data (what would be written to a network connection)
+ * - Simulating network events (EOS, errors, timeouts)
+ *
+ * Usage:
+ * @code
+ * MockNetVC mock_vc;
+ * mock_vc.set_read_data("GET / HTTP/1.1\r\n\r\n");
+ * // ... use mock_vc in test ...
+ * std::string written = mock_vc.get_written_data();
+ * @endcode
+ */
+class MockNetVC : public NetVConnection
+{
+public:
+  MockNetVC();
+  ~MockNetVC() override;
+
+  // ============== Test Setup Methods ==============
+
+  /**
+   * Set the data that will be available for reading
+   * @param data Data to be read from this mock connection
+   */
+  void set_read_data(std::string_view data);
+
+  /**
+   * Append more data to be read (simulates data arriving later)
+   * @param data Additional data to be read
+   */
+  void append_read_data(std::string_view data);
+
+  /**
+   * Get data that has been written to this connection
+   * @return All data written to this mock connection
+   */
+  std::string get_written_data() const;
+
+  /**
+   * Clear the write buffer (reset captured writes)
+   */
+  void clear_written_data();
+
+  /**
+   * Simulate an end-of-stream condition
+   */
+  void simulate_eos();
+
+  /**
+   * Simulate an error condition
+   * @param lerrno The error number to simulate
+   */
+  void simulate_error(int lerrno);
+
+  /**
+   * Simulate an inactivity timeout
+   */
+  void simulate_inactivity_timeout();
+
+  /**
+   * Simulate an active timeout
+   */
+  void simulate_active_timeout();
+
+  /**
+   * Set the local address for this connection
+   */
+  void set_local_address(const sockaddr *addr);
+
+  /**
+   * Set the remote address for this connection
+   */
+  void set_remote_address(const sockaddr *addr);
+
+  // ============== NetVConnection Interface Implementation ==============
+
+  VIO *do_io_read(Continuation *c, int64_t nbytes, MIOBuffer *buf) override;
+  VIO *do_io_write(Continuation *c, int64_t nbytes, IOBufferReader *buf, bool 
owner = false) override;
+  void do_io_close(int lerrno = -1) override;
+  void do_io_shutdown(ShutdownHowTo_t howto) override;
+
+  void reenable(VIO *vio) override;
+  void reenable_re(VIO *vio) override;
+
+  // Timeout methods (store values but don't actually time out)
+  void         set_active_timeout(ink_hrtime timeout_in) override;
+  void         set_inactivity_timeout(ink_hrtime timeout_in) override;
+  void         set_default_inactivity_timeout(ink_hrtime timeout_in) override;
+  bool         is_default_inactivity_timeout() override;
+  void         cancel_active_timeout() override;
+  void         cancel_inactivity_timeout() override;
+  ink_hrtime   get_active_timeout() override;
+  ink_hrtime   get_inactivity_timeout() override;
+  virtual void apply_options() override;
+
+  // Note: Address methods are not virtual in NetVConnection.
+  // They use the protected local_addr/remote_addr members directly.
+  // We set these via set_local_address() and set_remote_address() above.
+
+  // Misc methods
+  SOCKET get_socket() override;
+  int    set_tcp_congestion_control(tcp_congestion_control_side side) override;
+  void   set_local_addr() override;
+  void   set_remote_addr() override;
+  void   set_remote_addr(const sockaddr *) override;
+  void   set_mptcp_state() override;
+
+  int populate_protocol(std::string_view *results, int n) const override;
+  const char  *protocol_contains(std::string_view tag) const override;
+
+  // ============== Test State Access ==============
+
+  bool is_closed() const { return closed_; }
+  int  get_close_error() const { return close_error_; }
+  bool is_read_shutdown() const { return read_shutdown_; }
+  bool is_write_shutdown() const { return write_shutdown_; }
+
+private:
+  // Read side
+  MIOBuffer      *read_buffer_       = nullptr;
+  IOBufferReader *read_buffer_reader_ = nullptr;
+  VIO             read_vio_;
+
+  // Write side
+  MIOBuffer      *write_capture_     = nullptr;
+  IOBufferReader *write_capture_reader_ = nullptr;
+  VIO             write_vio_;
+
+  // State
+  bool closed_          = false;
+  int  close_error_     = 0;
+  bool read_shutdown_   = false;
+  bool write_shutdown_  = false;
+  bool eos_             = false;
+  int  error_           = 0;
+
+  // Timeouts
+  ink_hrtime active_timeout_     = 0;
+  ink_hrtime inactivity_timeout_ = 0;
+  bool       default_inactivity_timeout_ = false;
+};

Reply via email to