]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
test/librbd: initial unit test for copyup state machine
authorJason Dillaman <dillaman@redhat.com>
Tue, 2 Apr 2019 18:10:42 +0000 (14:10 -0400)
committerJason Dillaman <dillaman@redhat.com>
Mon, 8 Apr 2019 17:20:07 +0000 (13:20 -0400)
Signed-off-by: Jason Dillaman <dillaman@redhat.com>
src/librbd/io/CopyupRequest.cc
src/librbd/io/CopyupRequest.h
src/test/librbd/CMakeLists.txt
src/test/librbd/deep_copy/test_mock_ObjectCopyRequest.cc
src/test/librbd/io/test_mock_CopyupRequest.cc [new file with mode: 0644]
src/test/librbd/mock/MockObjectMap.h

index 4eeee3d4d5d7bbf3500f660ebac5bad9a0f6a918..37fa3d00e0ca9700b0124cbf2f6ec352035296de 100644 (file)
@@ -32,27 +32,29 @@ namespace io {
 
 namespace {
 
-class C_UpdateObjectMap : public C_AsyncObjectThrottle<> {
+template <typename I>
+class C_UpdateObjectMap : public C_AsyncObjectThrottle<I> {
 public:
-  C_UpdateObjectMap(AsyncObjectThrottle<> &throttle, ImageCtx *image_ctx,
+  C_UpdateObjectMap(AsyncObjectThrottle<I> &throttle, I *image_ctx,
                     uint64_t object_no, uint8_t head_object_map_state,
                     const std::vector<uint64_t> *snap_ids,
                     const ZTracer::Trace &trace, size_t snap_id_idx)
-    : C_AsyncObjectThrottle(throttle, *image_ctx), m_object_no(object_no),
+    : C_AsyncObjectThrottle<I>(throttle, *image_ctx), m_object_no(object_no),
       m_head_object_map_state(head_object_map_state), m_snap_ids(*snap_ids),
       m_trace(trace), m_snap_id_idx(snap_id_idx)
   {
   }
 
   int send() override {
-    ceph_assert(m_image_ctx.owner_lock.is_locked());
-    if (m_image_ctx.exclusive_lock == nullptr) {
+    auto& image_ctx = this->m_image_ctx;
+    ceph_assert(image_ctx.owner_lock.is_locked());
+    if (image_ctx.exclusive_lock == nullptr) {
       return 1;
     }
-    ceph_assert(m_image_ctx.exclusive_lock->is_lock_owner());
+    ceph_assert(image_ctx.exclusive_lock->is_lock_owner());
 
-    RWLock::RLocker snap_locker(m_image_ctx.snap_lock);
-    if (m_image_ctx.object_map == nullptr) {
+    RWLock::RLocker snap_locker(image_ctx.snap_lock);
+    if (image_ctx.object_map == nullptr) {
       return 1;
     }
 
@@ -65,25 +67,26 @@ public:
   }
 
   int update_head() {
-    RWLock::WLocker object_map_locker(m_image_ctx.object_map_lock);
-    bool sent = m_image_ctx.object_map->aio_update<Context>(
+    auto& image_ctx = this->m_image_ctx;
+    RWLock::WLocker object_map_locker(image_ctx.object_map_lock);
+    bool sent = image_ctx.object_map->template aio_update<Context>(
       CEPH_NOSNAP, m_object_no, m_head_object_map_state, {}, m_trace, false,
       this);
     return (sent ? 0 : 1);
   }
 
   int update_snapshot(uint64_t snap_id) {
+    auto& image_ctx = this->m_image_ctx;
     uint8_t state = OBJECT_EXISTS;
-    if (m_image_ctx.test_features(RBD_FEATURE_FAST_DIFF,
-                                  m_image_ctx.snap_lock) &&
+    if (image_ctx.test_features(RBD_FEATURE_FAST_DIFF, image_ctx.snap_lock) &&
         m_snap_id_idx > 0) {
       // first snapshot should be exists+dirty since it contains
       // the copyup data -- later snapshots inherit the data.
       state = OBJECT_EXISTS_CLEAN;
     }
 
-    RWLock::RLocker object_map_locker(m_image_ctx.object_map_lock);
-    bool sent = m_image_ctx.object_map->aio_update<Context>(
+    RWLock::RLocker object_map_locker(image_ctx.object_map_lock);
+    bool sent = image_ctx.object_map->template aio_update<Context>(
       snap_id, m_object_no, state, {}, m_trace, true, this);
     ceph_assert(sent);
     return 0;
@@ -103,12 +106,12 @@ template <typename I>
 CopyupRequest<I>::CopyupRequest(I *ictx, const std::string &oid,
                                 uint64_t objectno, Extents &&image_extents,
                                 const ZTracer::Trace &parent_trace)
-  : m_ictx(util::get_image_ctx(ictx)), m_oid(oid), m_object_no(objectno),
+  : m_image_ctx(ictx), m_oid(oid), m_object_no(objectno),
     m_image_extents(image_extents),
-    m_trace(util::create_trace(*m_ictx, "copy-up", parent_trace)),
+    m_trace(util::create_trace(*m_image_ctx, "copy-up", parent_trace)),
     m_lock("CopyupRequest", false, false)
 {
-  m_async_op.start_op(*m_ictx);
+  m_async_op.start_op(*util::get_image_ctx(m_image_ctx));
 }
 
 template <typename I>
@@ -121,9 +124,10 @@ template <typename I>
 void CopyupRequest<I>::append_request(AbstractObjectWriteRequest<I> *req) {
   Mutex::Locker locker(m_lock);
 
-  ldout(m_ictx->cct, 20) << "oid=" << m_oid << ", "
-                         << "object_request=" << req << ", "
-                         << "append=" << m_append_request_permitted << dendl;
+  auto cct = m_image_ctx->cct;
+  ldout(cct, 20) << "oid=" << m_oid << ", "
+                 << "object_request=" << req << ", "
+                 << "append=" << m_append_request_permitted << dendl;
   if (m_append_request_permitted) {
     m_pending_requests.push_back(req);
   } else {
@@ -138,15 +142,16 @@ void CopyupRequest<I>::send() {
 
 template <typename I>
 void CopyupRequest<I>::read_from_parent() {
-  m_ictx->snap_lock.get_read();
-  m_ictx->parent_lock.get_read();
+  auto cct = m_image_ctx->cct;
+  m_image_ctx->snap_lock.get_read();
+  m_image_ctx->parent_lock.get_read();
 
-  if (m_ictx->parent == nullptr) {
-    ldout(m_ictx->cct, 5) << "parent detached" << dendl;
-    m_ictx->parent_lock.put_read();
-    m_ictx->snap_lock.put_read();
+  if (m_image_ctx->parent == nullptr) {
+    ldout(cct, 5) << "parent detached" << dendl;
+    m_image_ctx->parent_lock.put_read();
+    m_image_ctx->snap_lock.put_read();
 
-    m_ictx->op_work_queue->queue(
+    m_image_ctx->op_work_queue->queue(
       util::create_context_callback<
         CopyupRequest<I>, &CopyupRequest<I>::handle_read_from_parent>(this),
       -ENOENT);
@@ -160,23 +165,25 @@ void CopyupRequest<I>::read_from_parent() {
   m_deep_copy = false;
   auto comp = AioCompletion::create_and_start<
     CopyupRequest<I>,
-    &CopyupRequest<I>::handle_read_from_parent>(this, m_ictx, AIO_TYPE_READ);
-
-  ldout(m_ictx->cct, 20) << "oid=" << m_oid << ", "
-                         << "completion=" << comp << ", "
-                         << "extents=" << m_image_extents
-                         << dendl;
-  ImageRequest<>::aio_read(m_ictx->parent, comp, std::move(m_image_extents),
-                           ReadResult{&m_copyup_data}, 0, m_trace);
-
-  m_ictx->parent_lock.put_read();
-  m_ictx->snap_lock.put_read();
+    &CopyupRequest<I>::handle_read_from_parent>(
+      this, util::get_image_ctx(m_image_ctx), AIO_TYPE_READ);
+
+  ldout(cct, 20) << "oid=" << m_oid << ", "
+                 << "completion=" << comp << ", "
+                 << "extents=" << m_image_extents
+                 << dendl;
+  ImageRequest<I>::aio_read(m_image_ctx->parent, comp,
+                            std::move(m_image_extents),
+                            ReadResult{&m_copyup_data}, 0, m_trace);
+
+  m_image_ctx->parent_lock.put_read();
+  m_image_ctx->snap_lock.put_read();
 }
 
 template <typename I>
 void CopyupRequest<I>::handle_read_from_parent(int r) {
-  ldout(m_ictx->cct, 20) << "oid=" << m_oid << ", "
-                         << "r=" << r << dendl;
+  auto cct = m_image_ctx->cct;
+  ldout(cct, 20) << "oid=" << m_oid << ", r=" << r << dendl;
 
   m_lock.Lock();
   m_copyup_required = is_copyup_required();
@@ -185,8 +192,7 @@ void CopyupRequest<I>::handle_read_from_parent(int r) {
   if (r < 0 && r != -ENOENT) {
     m_lock.Unlock();
 
-    lderr(m_ictx->cct) << "error reading from parent: " << cpp_strerror(r)
-                       << dendl;
+    lderr(cct) << "error reading from parent: " << cpp_strerror(r) << dendl;
     finish(r);
     return;
   }
@@ -194,7 +200,7 @@ void CopyupRequest<I>::handle_read_from_parent(int r) {
   if (!m_copyup_required) {
     m_lock.Unlock();
 
-    ldout(m_ictx->cct, 20) << "no-op, skipping" << dendl;
+    ldout(cct, 20) << "no-op, skipping" << dendl;
     finish(0);
     return;
   }
@@ -205,42 +211,42 @@ void CopyupRequest<I>::handle_read_from_parent(int r) {
 
 template <typename I>
 void CopyupRequest<I>::deep_copy() {
-  ceph_assert(m_ictx->snap_lock.is_locked());
-  ceph_assert(m_ictx->parent_lock.is_locked());
-  ceph_assert(m_ictx->parent != nullptr);
+  auto cct = m_image_ctx->cct;
+  ceph_assert(m_image_ctx->snap_lock.is_locked());
+  ceph_assert(m_image_ctx->parent_lock.is_locked());
+  ceph_assert(m_image_ctx->parent != nullptr);
 
   m_lock.Lock();
-  m_flatten = is_copyup_required() ? true : m_ictx->migration_info.flatten;
+  m_flatten = is_copyup_required() ? true : m_image_ctx->migration_info.flatten;
   m_lock.Unlock();
 
-  ldout(m_ictx->cct, 20) << "oid=" << m_oid << ", "
-                         << "flatten=" << m_flatten << dendl;
+  ldout(cct, 20) << "oid=" << m_oid << ", flatten=" << m_flatten << dendl;
 
   m_deep_copy = true;
   auto ctx = util::create_context_callback<
     CopyupRequest<I>, &CopyupRequest<I>::handle_deep_copy>(this);
   auto req = deep_copy::ObjectCopyRequest<I>::create(
-      m_ictx->parent, m_ictx, m_ictx->migration_info.snap_map, m_object_no,
-      m_flatten, ctx);
+    m_image_ctx->parent, m_image_ctx, m_image_ctx->migration_info.snap_map,
+    m_object_no, m_flatten, ctx);
 
   req->send();
-  m_ictx->parent_lock.put_read();
-  m_ictx->snap_lock.put_read();
+  m_image_ctx->parent_lock.put_read();
+  m_image_ctx->snap_lock.put_read();
 }
 
 template <typename I>
 void CopyupRequest<I>::handle_deep_copy(int r) {
-  ldout(m_ictx->cct, 20) << "oid=" << m_oid << ", "
-                         << "r=" << r << dendl;
+  auto cct = m_image_ctx->cct;
+  ldout(cct, 20) << "oid=" << m_oid << ", r=" << r << dendl;
 
-  m_ictx->snap_lock.get_read();
+  m_image_ctx->snap_lock.get_read();
   m_lock.Lock();
   m_copyup_required = is_copyup_required();
   if (r == -ENOENT && !m_flatten && m_copyup_required) {
     m_lock.Unlock();
-    m_ictx->snap_lock.put_read();
+    m_image_ctx->snap_lock.put_read();
 
-    ldout(m_ictx->cct, 10) << "restart deep-copy with flatten" << dendl;
+    ldout(cct, 10) << "restart deep-copy with flatten" << dendl;
     send();
     return;
   }
@@ -249,38 +255,38 @@ void CopyupRequest<I>::handle_deep_copy(int r) {
 
   if (r < 0 && r != -ENOENT) {
     m_lock.Unlock();
-    m_ictx->snap_lock.put_read();
+    m_image_ctx->snap_lock.put_read();
 
-    lderr(m_ictx->cct) << "error encountered during deep-copy: "
-                       << cpp_strerror(r) << dendl;
+    lderr(cct) << "error encountered during deep-copy: " << cpp_strerror(r)
+               << dendl;
     finish(r);
     return;
   }
 
   if (!m_copyup_required && !is_update_object_map_required(r)) {
     m_lock.Unlock();
-    m_ictx->snap_lock.put_read();
+    m_image_ctx->snap_lock.put_read();
 
     if (r == -ENOENT) {
       r = 0;
     }
 
-    ldout(m_ictx->cct, 20) << "skipping" << dendl;
+    ldout(cct, 20) << "skipping" << dendl;
     finish(r);
     return;
   }
 
   m_lock.Unlock();
-  m_ictx->snap_lock.put_read();
+  m_image_ctx->snap_lock.put_read();
 
   update_object_maps();
 }
 
 template <typename I>
 void CopyupRequest<I>::update_object_maps() {
-  RWLock::RLocker owner_locker(m_ictx->owner_lock);
-  RWLock::RLocker snap_locker(m_ictx->snap_lock);
-  if (m_ictx->object_map == nullptr) {
+  RWLock::RLocker owner_locker(m_image_ctx->owner_lock);
+  RWLock::RLocker snap_locker(m_image_ctx->snap_lock);
+  if (m_image_ctx->object_map == nullptr) {
     snap_locker.unlock();
     owner_locker.unlock();
 
@@ -288,22 +294,23 @@ void CopyupRequest<I>::update_object_maps() {
     return;
   }
 
-  auto cct = m_ictx->cct;
+  auto cct = m_image_ctx->cct;
   ldout(cct, 20) << "oid=" << m_oid << dendl;
 
-  if (!m_ictx->snaps.empty()) {
+  if (!m_image_ctx->snaps.empty()) {
     if (m_deep_copy) {
       compute_deep_copy_snap_ids();
     } else {
-      m_snap_ids.insert(m_snap_ids.end(), m_ictx->snaps.rbegin(),
-                        m_ictx->snaps.rend());
+      m_snap_ids.insert(m_snap_ids.end(), m_image_ctx->snaps.rbegin(),
+                        m_image_ctx->snaps.rend());
     }
   }
 
   bool copy_on_read = m_pending_requests.empty();
   uint8_t head_object_map_state = OBJECT_EXISTS;
   if (copy_on_read && !m_snap_ids.empty() &&
-      m_ictx->test_features(RBD_FEATURE_FAST_DIFF, m_ictx->snap_lock)) {
+      m_image_ctx->test_features(RBD_FEATURE_FAST_DIFF,
+                                 m_image_ctx->snap_lock)) {
     // HEAD is non-dirty since data is tied to first snapshot
     head_object_map_state = OBJECT_EXISTS_CLEAN;
   }
@@ -314,35 +321,35 @@ void CopyupRequest<I>::update_object_maps() {
     head_object_map_state = (*r_it)->get_pre_write_object_map_state();
   }
 
-  RWLock::WLocker object_map_locker(m_ictx->object_map_lock);
-  if ((*m_ictx->object_map)[m_object_no] != head_object_map_state) {
+  RWLock::WLocker object_map_locker(m_image_ctx->object_map_lock);
+  if ((*m_image_ctx->object_map)[m_object_no] != head_object_map_state) {
     // (maybe) need to update the HEAD object map state
     m_snap_ids.push_back(CEPH_NOSNAP);
   }
   object_map_locker.unlock();
   snap_locker.unlock();
 
-  ceph_assert(m_ictx->exclusive_lock->is_lock_owner());
-  AsyncObjectThrottle<>::ContextFactory context_factory(
-    boost::lambda::bind(boost::lambda::new_ptr<C_UpdateObjectMap>(),
-    boost::lambda::_1, m_ictx, m_object_no, head_object_map_state, &m_snap_ids,
-    m_trace, boost::lambda::_2));
+  ceph_assert(m_image_ctx->exclusive_lock->is_lock_owner());
+  typename AsyncObjectThrottle<I>::ContextFactory context_factory(
+    boost::lambda::bind(boost::lambda::new_ptr<C_UpdateObjectMap<I>>(),
+    boost::lambda::_1, m_image_ctx, m_object_no, head_object_map_state,
+    &m_snap_ids, m_trace, boost::lambda::_2));
   auto ctx = util::create_context_callback<
     CopyupRequest<I>, &CopyupRequest<I>::handle_update_object_maps>(this);
-  auto throttle = new AsyncObjectThrottle<>(
-    nullptr, *m_ictx, context_factory, ctx, nullptr, 0, m_snap_ids.size());
+  auto throttle = new AsyncObjectThrottle<I>(
+    nullptr, *m_image_ctx, context_factory, ctx, nullptr, 0, m_snap_ids.size());
   throttle->start_ops(
-    m_ictx->config.template get_val<uint64_t>("rbd_concurrent_management_ops"));
+    m_image_ctx->config.template get_val<uint64_t>("rbd_concurrent_management_ops"));
 }
 
 template <typename I>
 void CopyupRequest<I>::handle_update_object_maps(int r) {
-  ldout(m_ictx->cct, 20) << "oid=" << m_oid << ", "
-                         << "r=" << r << dendl;
+  auto cct = m_image_ctx->cct;
+  ldout(cct, 20) << "oid=" << m_oid << ", r=" << r << dendl;
 
   if (r < 0) {
-    lderr(m_ictx->cct) << "failed to update object map: " << cpp_strerror(r)
-                       << dendl;
+    lderr(m_image_ctx->cct) << "failed to update object map: "
+                            << cpp_strerror(r) << dendl;
 
     finish(r);
     return;
@@ -353,23 +360,21 @@ void CopyupRequest<I>::handle_update_object_maps(int r) {
 
 template <typename I>
 void CopyupRequest<I>::copyup() {
+  auto cct = m_image_ctx->cct;
+  m_image_ctx->snap_lock.get_read();
+  auto snapc = m_image_ctx->snapc;
+  m_image_ctx->snap_lock.put_read();
+
   m_lock.Lock();
   if (!m_copyup_required) {
     m_lock.Unlock();
 
-    ldout(m_ictx->cct, 20) << "skipping copyup" << dendl;
+    ldout(cct, 20) << "skipping copyup" << dendl;
     finish(0);
     return;
   }
-  m_lock.Unlock();
-
-  ldout(m_ictx->cct, 20) << "oid=" << m_oid << dendl;
 
-  m_ictx->snap_lock.get_read();
-  ::SnapContext snapc = m_ictx->snapc;
-  m_ictx->snap_lock.put_read();
-
-  std::vector<librados::snap_t> snaps;
+  ldout(cct, 20) << "oid=" << m_oid << dendl;
 
   bool copy_on_read = m_pending_requests.empty();
   bool deep_copyup = !snapc.snaps.empty() && !m_copyup_data.is_zero();
@@ -377,59 +382,63 @@ void CopyupRequest<I>::copyup() {
     m_copyup_data.clear();
   }
 
-  Mutex::Locker locker(m_lock);
   int r;
+  librados::ObjectWriteOperation copyup_op;
   if (copy_on_read || deep_copyup) {
-    librados::ObjectWriteOperation copyup_op;
     copyup_op.exec("rbd", "copyup", m_copyup_data);
-    ObjectRequest<I>::add_write_hint(*m_ictx, &copyup_op);
+    ObjectRequest<I>::add_write_hint(*m_image_ctx, &copyup_op);
+    ++m_pending_copyups;
+  }
+
+  librados::ObjectWriteOperation write_op;
+  if (!copy_on_read) {
+    if (!deep_copyup) {
+      write_op.exec("rbd", "copyup", m_copyup_data);
+      ObjectRequest<I>::add_write_hint(*m_image_ctx, &write_op);
+    }
+
+    // merge all pending write ops into this single RADOS op
+    for (auto req : m_pending_requests) {
+      ldout(cct, 20) << "add_copyup_ops " << req << dendl;
+      req->add_copyup_ops(&write_op);
+    }
+
+    if (write_op.size() > 0) {
+      ++m_pending_copyups;
+    }
+  }
+  m_lock.Unlock();
 
+  // issue librados ops at the end to simplify test cases
+  std::vector<librados::snap_t> snaps;
+  if (copyup_op.size() > 0) {
     // send only the copyup request with a blank snapshot context so that
     // all snapshots are detected from the parent for this object.  If
     // this is a CoW request, a second request will be created for the
     // actual modification.
-    m_pending_copyups++;
-    ldout(m_ictx->cct, 20) << "copyup with empty snapshot context" << dendl;
+    ldout(cct, 20) << "copyup with empty snapshot context" << dendl;
 
     auto comp = util::create_rados_callback<
       CopyupRequest<I>, &CopyupRequest<I>::handle_copyup>(this);
-    r = m_ictx->data_ctx.aio_operate(
+    r = m_image_ctx->data_ctx.aio_operate(
       m_oid, comp, &copyup_op, 0, snaps,
       (m_trace.valid() ? m_trace.get_info() : nullptr));
     ceph_assert(r == 0);
     comp->release();
   }
 
-  if (!copy_on_read) {
-    librados::ObjectWriteOperation write_op;
-    if (!deep_copyup) {
-      write_op.exec("rbd", "copyup", m_copyup_data);
-      ObjectRequest<I>::add_write_hint(*m_ictx, &write_op);
-    }
-
-    // merge all pending write ops into this single RADOS op
-    for (auto req : m_pending_requests) {
-      ldout(m_ictx->cct, 20) << "add_copyup_ops " << req << dendl;
-      req->add_copyup_ops(&write_op);
-    }
-
+  if (write_op.size() > 0) {
     // compare-and-write doesn't add any write ops (copyup+cmpext+write
     // can't be executed in the same RADOS op because, unless the object
     // was already present in the clone, cmpext wouldn't see it)
-    if (!write_op.size()) {
-      return;
-    }
-
-    m_pending_copyups++;
-    ldout(m_ictx->cct, 20) << (!deep_copyup && write_op.size() > 2 ?
-                               "copyup + ops" : !deep_copyup ?
-                                                "copyup" : "ops")
-                           << " with current snapshot context" << dendl;
+    ldout(cct, 20) << (!deep_copyup && write_op.size() > 2 ?
+                        "copyup + ops" : !deep_copyup ? "copyup" : "ops")
+                   << " with current snapshot context" << dendl;
 
     snaps.insert(snaps.end(), snapc.snaps.begin(), snapc.snaps.end());
     auto comp = util::create_rados_callback<
       CopyupRequest<I>, &CopyupRequest<I>::handle_copyup>(this);
-    r = m_ictx->data_ctx.aio_operate(
+    r = m_image_ctx->data_ctx.aio_operate(
       m_oid, comp, &write_op, snapc.seq, snaps,
       (m_trace.valid() ? m_trace.get_info() : nullptr));
     ceph_assert(r == 0);
@@ -439,6 +448,7 @@ void CopyupRequest<I>::copyup() {
 
 template <typename I>
 void CopyupRequest<I>::handle_copyup(int r) {
+  auto cct = m_image_ctx->cct;
   unsigned pending_copyups;
   {
     Mutex::Locker locker(m_lock);
@@ -446,30 +456,23 @@ void CopyupRequest<I>::handle_copyup(int r) {
     pending_copyups = --m_pending_copyups;
   }
 
-  ldout(m_ictx->cct, 20) << "oid=" << m_oid << ", "
-                         << "r=" << r << ", "
-                         << "pending=" << pending_copyups << dendl;
+  ldout(cct, 20) << "oid=" << m_oid << ", " << "r=" << r << ", "
+                 << "pending=" << pending_copyups << dendl;
 
-  if (r == -ENOENT) {
-    if (pending_copyups == 0) {
-      // hide the -ENOENT error if this is the last op
-      r = 0;
-    }
-  } else if (r < 0) {
-    lderr(m_ictx->cct) << "failed to copyup object: "
-                       << cpp_strerror(r) << dendl;
+  if (r < 0 && r != -ENOENT) {
+    lderr(cct) << "failed to copyup object: " << cpp_strerror(r) << dendl;
     complete_requests(false, r);
   }
 
   if (pending_copyups == 0) {
-    finish(r);
+    finish(0);
   }
 }
 
 template <typename I>
 void CopyupRequest<I>::finish(int r) {
-  ldout(m_ictx->cct, 20) << "oid=" << m_oid << ", "
-                         << "r=" << r << dendl;
+  auto cct = m_image_ctx->cct;
+  ldout(cct, 20) << "oid=" << m_oid << ", r=" << r << dendl;
 
   complete_requests(true, r);
   delete this;
@@ -477,12 +480,13 @@ void CopyupRequest<I>::finish(int r) {
 
 template <typename I>
 void CopyupRequest<I>::complete_requests(bool override_restart_retval, int r) {
+  auto cct = m_image_ctx->cct;
   remove_from_list();
 
   while (!m_pending_requests.empty()) {
     auto it = m_pending_requests.begin();
     auto req = *it;
-    ldout(m_ictx->cct, 20) << "completing request " << req << dendl;
+    ldout(cct, 20) << "completing request " << req << dendl;
     req->handle_copyup(r);
     m_pending_requests.erase(it);
   }
@@ -494,7 +498,7 @@ void CopyupRequest<I>::complete_requests(bool override_restart_retval, int r) {
   while (!m_restart_requests.empty()) {
     auto it = m_restart_requests.begin();
     auto req = *it;
-    ldout(m_ictx->cct, 20) << "restarting request " << req << dendl;
+    ldout(cct, 20) << "restarting request " << req << dendl;
     req->handle_copyup(r);
     m_restart_requests.erase(it);
   }
@@ -508,11 +512,11 @@ void CopyupRequest<I>::disable_append_requests() {
 
 template <typename I>
 void CopyupRequest<I>::remove_from_list() {
-  Mutex::Locker copyup_list_locker(m_ictx->copyup_list_lock);
+  Mutex::Locker copyup_list_locker(m_image_ctx->copyup_list_lock);
 
-  auto it = m_ictx->copyup_list.find(m_object_no);
-  if (it != m_ictx->copyup_list.end()) {
-    m_ictx->copyup_list.erase(it);
+  auto it = m_image_ctx->copyup_list.find(m_object_no);
+  if (it != m_image_ctx->copyup_list.end()) {
+    m_image_ctx->copyup_list.erase(it);
   }
 }
 
@@ -540,55 +544,50 @@ bool CopyupRequest<I>::is_copyup_required() {
 
 template <typename I>
 bool CopyupRequest<I>::is_deep_copy() const {
-  ceph_assert(m_ictx->snap_lock.is_locked());
-  return !m_ictx->migration_info.empty();
+  ceph_assert(m_image_ctx->snap_lock.is_locked());
+  return !m_image_ctx->migration_info.empty();
 }
 
 template <typename I>
 bool CopyupRequest<I>::is_update_object_map_required(int r) {
-  ceph_assert(m_ictx->snap_lock.is_locked());
+  ceph_assert(m_image_ctx->snap_lock.is_locked());
 
   if (r < 0) {
     return false;
   }
 
-  if (m_ictx->object_map == nullptr) {
-    return false;
-  }
-
-  if (!is_deep_copy()) {
+  if (m_image_ctx->object_map == nullptr) {
     return false;
   }
 
-  auto it = m_ictx->migration_info.snap_map.find(CEPH_NOSNAP);
-  ceph_assert(it != m_ictx->migration_info.snap_map.end());
+  auto it = m_image_ctx->migration_info.snap_map.find(CEPH_NOSNAP);
+  ceph_assert(it != m_image_ctx->migration_info.snap_map.end());
   return it->second[0] != CEPH_NOSNAP;
 }
 
 template <typename I>
 void CopyupRequest<I>::compute_deep_copy_snap_ids() {
-  ceph_assert(m_ictx->snap_lock.is_locked());
+  ceph_assert(m_image_ctx->snap_lock.is_locked());
 
   // don't copy ids for the snaps updated by object deep copy or
   // that don't overlap
   std::set<uint64_t> deep_copied;
-  for (auto &it : m_ictx->migration_info.snap_map) {
+  for (auto &it : m_image_ctx->migration_info.snap_map) {
     if (it.first != CEPH_NOSNAP) {
       deep_copied.insert(it.second.front());
     }
   }
 
-  RWLock::RLocker parent_locker(m_ictx->parent_lock);
-  std::copy_if(m_ictx->snaps.rbegin(), m_ictx->snaps.rend(),
+  RWLock::RLocker parent_locker(m_image_ctx->parent_lock);
+  std::copy_if(m_image_ctx->snaps.rbegin(), m_image_ctx->snaps.rend(),
                std::back_inserter(m_snap_ids),
-               [this, cct=m_ictx->cct, &deep_copied](uint64_t snap_id) {
+               [this, cct=m_image_ctx->cct, &deep_copied](uint64_t snap_id) {
       if (deep_copied.count(snap_id)) {
         return false;
       }
 
       uint64_t parent_overlap = 0;
-      int r = m_ictx->get_parent_overlap(snap_id,
-                                         &parent_overlap);
+      int r = m_image_ctx->get_parent_overlap(snap_id, &parent_overlap);
       if (r < 0) {
         ldout(cct, 5) << "failed getting parent overlap for snap_id: "
                       << snap_id << ": " << cpp_strerror(r) << dendl;
@@ -597,11 +596,11 @@ void CopyupRequest<I>::compute_deep_copy_snap_ids() {
         return false;
       }
       std::vector<std::pair<uint64_t, uint64_t>> extents;
-      Striper::extent_to_file(cct, &m_ictx->layout,
+      Striper::extent_to_file(cct, &m_image_ctx->layout,
                               m_object_no, 0,
-                              m_ictx->layout.object_size,
+                              m_image_ctx->layout.object_size,
                               extents);
-      auto overlap = m_ictx->prune_parent_extents(
+      auto overlap = m_image_ctx->prune_parent_extents(
           extents, parent_overlap);
       return overlap > 0;
     });
index 5043ada66c5aff6c67b2ac87e1584f4c04193238..c30d5d409cb311f8f6da31326ed92efe7311e8c3 100644 (file)
@@ -78,7 +78,7 @@ private:
 
   typedef std::vector<AbstractObjectWriteRequest<ImageCtxT> *> WriteRequests;
 
-  ImageCtx *m_ictx;
+  ImageCtxT *m_image_ctx;
   std::string m_oid;
   uint64_t m_object_no;
   Extents m_image_extents;
index 2894bf94351c8676216bf4f3867c796307223433..326511bccf8f45b86d4ace7441cdf7ba6da1c678 100644 (file)
@@ -69,6 +69,7 @@ set(unittest_librbd_srcs
   image/test_mock_RefreshRequest.cc
   image/test_mock_RemoveRequest.cc
   image/test_mock_ValidatePoolRequest.cc
+  io/test_mock_CopyupRequest.cc
   io/test_mock_ImageRequest.cc
   io/test_mock_ImageRequestWQ.cc
   io/test_mock_ObjectRequest.cc
index 6119f7b385bfd087840ea63af9224f6378af4496..7816a507e90d7d90fc22adc041b09f3a089077d8 100644 (file)
@@ -64,7 +64,7 @@ ImageRequest<MockTestImageCtx> *ImageRequest<MockTestImageCtx>::s_instance = nul
 #include "librbd/deep_copy/ObjectCopyRequest.cc"
 template class librbd::deep_copy::ObjectCopyRequest<librbd::MockTestImageCtx>;
 
-bool operator==(const SnapContext& rhs, const SnapContext& lhs) {
+static bool operator==(const SnapContext& rhs, const SnapContext& lhs) {
   return (rhs.seq == lhs.seq && rhs.snaps == lhs.snaps);
 }
 
diff --git a/src/test/librbd/io/test_mock_CopyupRequest.cc b/src/test/librbd/io/test_mock_CopyupRequest.cc
new file mode 100644 (file)
index 0000000..823e5b0
--- /dev/null
@@ -0,0 +1,987 @@
+// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*-
+// vim: ts=8 sw=2 smarttab
+
+#include "test/librbd/test_mock_fixture.h"
+#include "test/librbd/test_support.h"
+#include "test/librbd/mock/MockExclusiveLock.h"
+#include "test/librbd/mock/MockImageCtx.h"
+#include "test/librbd/mock/MockJournal.h"
+#include "test/librbd/mock/MockObjectMap.h"
+#include "test/librados_test_stub/MockTestMemIoCtxImpl.h"
+#include "test/librados_test_stub/MockTestMemRadosClient.h"
+#include "include/rbd/librbd.hpp"
+#include "librbd/deep_copy/ObjectCopyRequest.h"
+#include "librbd/io/CopyupRequest.h"
+#include "librbd/io/ImageRequest.h"
+#include "librbd/io/ObjectRequest.h"
+#include "librbd/io/ReadResult.h"
+
+namespace librbd {
+namespace {
+
+struct MockTestImageCtx : public MockImageCtx {
+  MockTestImageCtx(ImageCtx &image_ctx,
+                   MockTestImageCtx* mock_parent_image_ctx = nullptr)
+    : MockImageCtx(image_ctx) {
+    parent = mock_parent_image_ctx;
+  }
+
+  std::map<uint64_t, librbd::io::CopyupRequest<librbd::MockTestImageCtx>*> copyup_list;
+};
+
+} // anonymous namespace
+
+namespace util {
+
+inline ImageCtx *get_image_ctx(MockImageCtx *image_ctx) {
+  return image_ctx->image_ctx;
+}
+
+} // namespace util
+
+namespace deep_copy {
+
+template <>
+struct ObjectCopyRequest<librbd::MockTestImageCtx> {
+  static ObjectCopyRequest* s_instance;
+  static ObjectCopyRequest* create(librbd::MockImageCtx* parent_image_ctx,
+                                   librbd::MockTestImageCtx* image_ctx,
+                                   const SnapMap &snap_map,
+                                   uint64_t object_number, bool flatten,
+                                   Context *on_finish) {
+    ceph_assert(s_instance != nullptr);
+    s_instance->object_number = object_number;
+    s_instance->flatten = flatten;
+    s_instance->on_finish = on_finish;
+    return s_instance;
+  }
+
+  uint64_t object_number;
+  bool flatten;
+  Context *on_finish;
+
+  ObjectCopyRequest() {
+    s_instance = this;
+  }
+
+  MOCK_METHOD0(send, void());
+};
+
+ObjectCopyRequest<librbd::MockTestImageCtx>* ObjectCopyRequest<librbd::MockTestImageCtx>::s_instance = nullptr;
+
+} // namespace deep_copy
+
+namespace io {
+
+template <>
+struct ObjectRequest<librbd::MockTestImageCtx> {
+  static void add_write_hint(librbd::MockTestImageCtx&,
+                             librados::ObjectWriteOperation*) {
+  }
+};
+
+template <>
+struct AbstractObjectWriteRequest<librbd::MockTestImageCtx> {
+  C_SaferCond ctx;
+  void handle_copyup(int r) {
+    ctx.complete(r);
+  }
+
+  MOCK_CONST_METHOD0(get_pre_write_object_map_state, uint8_t());
+  MOCK_CONST_METHOD0(is_empty_write_op, bool());
+
+  MOCK_METHOD1(add_copyup_ops, void(librados::ObjectWriteOperation*));
+};
+
+template <>
+struct ImageRequest<librbd::MockTestImageCtx> {
+  static ImageRequest *s_instance;
+  static void aio_read(librbd::MockImageCtx *ictx, AioCompletion *c,
+                       Extents &&image_extents, ReadResult &&read_result,
+                       int op_flags, const ZTracer::Trace &parent_trace) {
+    s_instance->aio_read(c, image_extents, &read_result);
+  }
+
+  MOCK_METHOD3(aio_read, void(AioCompletion *, const Extents&, ReadResult*));
+
+  ImageRequest() {
+    s_instance = this;
+  }
+};
+
+ImageRequest<librbd::MockTestImageCtx>* ImageRequest<librbd::MockTestImageCtx>::s_instance = nullptr;
+
+} // namespace io
+} // namespace librbd
+
+static bool operator==(const SnapContext& rhs, const SnapContext& lhs) {
+  return (rhs.seq == lhs.seq && rhs.snaps == lhs.snaps);
+}
+
+#include "librbd/AsyncObjectThrottle.cc"
+#include "librbd/io/CopyupRequest.cc"
+
+namespace librbd {
+namespace io {
+
+using ::testing::_;
+using ::testing::InSequence;
+using ::testing::Invoke;
+using ::testing::Return;
+using ::testing::StrEq;
+using ::testing::WithArg;
+using ::testing::WithArgs;
+using ::testing::WithoutArgs;
+
+struct TestMockIoCopyupRequest : public TestMockFixture {
+  typedef CopyupRequest<librbd::MockTestImageCtx> MockCopyupRequest;
+  typedef ImageRequest<librbd::MockTestImageCtx> MockImageRequest;
+  typedef ObjectRequest<librbd::MockTestImageCtx> MockObjectRequest;
+  typedef AbstractObjectWriteRequest<librbd::MockTestImageCtx> MockAbstractObjectWriteRequest;
+  typedef deep_copy::ObjectCopyRequest<librbd::MockTestImageCtx> MockObjectCopyRequest;
+
+  void SetUp() override {
+    TestMockFixture::SetUp();
+    if (!is_feature_enabled(RBD_FEATURE_LAYERING)) {
+      return;
+    }
+
+    m_parent_image_name = m_image_name;
+    m_image_name = get_temp_image_name();
+
+    librbd::Image image;
+    librbd::RBD rbd;
+    ASSERT_EQ(0, rbd.open(m_ioctx, image, m_parent_image_name.c_str(),
+                          nullptr));
+    ASSERT_EQ(0, image.snap_create("one"));
+    ASSERT_EQ(0, image.snap_protect("one"));
+
+    uint64_t features;
+    ASSERT_EQ(0, image.features(&features));
+    image.close();
+
+    int order = 0;
+    ASSERT_EQ(0, rbd.clone(m_ioctx, m_parent_image_name.c_str(), "one", m_ioctx,
+                           m_image_name.c_str(), features, &order));
+  }
+
+  void expect_get_parent_overlap(MockTestImageCtx& mock_image_ctx,
+                                 librados::snap_t snap_id, uint64_t overlap,
+                                 int r) {
+    if (mock_image_ctx.object_map != nullptr) {
+      EXPECT_CALL(mock_image_ctx, get_parent_overlap(snap_id, _))
+        .WillOnce(WithArg<1>(Invoke([overlap, r](uint64_t *o) {
+                               *o = overlap;
+                               return r;
+                             })));
+    }
+  }
+
+  void expect_prune_parent_extents(MockTestImageCtx& mock_image_ctx,
+                                   uint64_t overlap, uint64_t object_overlap) {
+    if (mock_image_ctx.object_map != nullptr) {
+      EXPECT_CALL(mock_image_ctx, prune_parent_extents(_, overlap))
+        .WillOnce(WithoutArgs(Invoke([object_overlap]() {
+                                return object_overlap;
+                              })));
+    }
+  }
+
+  void expect_read_parent(MockTestImageCtx& mock_image_ctx,
+                          MockImageRequest& mock_image_request,
+                          const Extents& image_extents,
+                          const std::string& data, int r) {
+    EXPECT_CALL(mock_image_request, aio_read(_, image_extents, _))
+      .WillOnce(WithArgs<0, 2>(Invoke(
+        [&mock_image_ctx, image_extents, data, r](
+            AioCompletion* aio_comp, ReadResult* read_result) {
+          aio_comp->read_result = std::move(*read_result);
+          aio_comp->set_request_count(1);
+          auto ctx = new ReadResult::C_ImageReadRequest(aio_comp,
+                                                        image_extents);
+          ctx->bl.append(data);
+          mock_image_ctx.image_ctx->op_work_queue->queue(ctx, r);
+        })));
+  }
+
+  void expect_copyup(MockTestImageCtx& mock_image_ctx, uint64_t snap_id,
+                     const std::string& oid, const std::string& data, int r) {
+    bufferlist in_bl;
+    in_bl.append(data);
+
+    SnapContext snapc;
+    if (snap_id == CEPH_NOSNAP) {
+      snapc = mock_image_ctx.snapc;
+    }
+
+    EXPECT_CALL(get_mock_io_ctx(mock_image_ctx.data_ctx),
+                exec(oid, _, StrEq("rbd"), StrEq("copyup"),
+                     ContentsEqual(in_bl), _, snapc))
+      .WillOnce(Return(r));
+  }
+
+  void expect_write(MockTestImageCtx& mock_image_ctx, uint64_t snap_id,
+                    const std::string& oid, int r) {
+    SnapContext snapc;
+    if (snap_id == CEPH_NOSNAP) {
+      snapc = mock_image_ctx.snapc;
+    }
+
+    EXPECT_CALL(get_mock_io_ctx(mock_image_ctx.data_ctx),
+                write(oid, _, 0, 0, snapc))
+      .WillOnce(Return(r));
+  }
+
+  void expect_test_features(MockTestImageCtx& mock_image_ctx) {
+    EXPECT_CALL(mock_image_ctx, test_features(_, _))
+      .WillRepeatedly(WithArg<0>(Invoke([&mock_image_ctx](uint64_t features) {
+              return (mock_image_ctx.features & features) != 0;
+            })));
+  }
+
+  void expect_is_lock_owner(MockTestImageCtx& mock_image_ctx) {
+    if (mock_image_ctx.exclusive_lock != nullptr) {
+      EXPECT_CALL(*mock_image_ctx.exclusive_lock,
+                  is_lock_owner()).WillRepeatedly(Return(true));
+    }
+  }
+
+  void expect_is_empty_write_op(MockAbstractObjectWriteRequest& mock_write_request,
+                                bool is_empty) {
+    EXPECT_CALL(mock_write_request, is_empty_write_op())
+      .WillOnce(Return(is_empty));
+  }
+
+  void expect_add_copyup_ops(MockAbstractObjectWriteRequest& mock_write_request) {
+    EXPECT_CALL(mock_write_request, add_copyup_ops(_))
+      .WillOnce(Invoke([](librados::ObjectWriteOperation* op) {
+                  op->write(0, bufferlist{});
+                }));
+  }
+
+  void expect_get_pre_write_object_map_state(MockTestImageCtx& mock_image_ctx,
+                                             MockAbstractObjectWriteRequest& mock_write_request,
+                                             uint8_t state) {
+    if (mock_image_ctx.object_map != nullptr) {
+      EXPECT_CALL(mock_write_request, get_pre_write_object_map_state())
+        .WillOnce(Return(state));
+    }
+  }
+
+  void expect_object_map_at(MockTestImageCtx& mock_image_ctx,
+                            uint64_t object_no, uint8_t state) {
+    if (mock_image_ctx.object_map != nullptr) {
+      EXPECT_CALL(*mock_image_ctx.object_map, at(object_no))
+        .WillOnce(Return(state));
+    }
+  }
+
+  void expect_object_map_update(MockTestImageCtx& mock_image_ctx,
+                                uint64_t snap_id, uint64_t object_no,
+                                uint8_t state, bool updated, int ret_val) {
+    if (mock_image_ctx.object_map != nullptr) {
+      if (!mock_image_ctx.image_ctx->test_features(RBD_FEATURE_FAST_DIFF) &&
+          state == OBJECT_EXISTS_CLEAN) {
+        state = OBJECT_EXISTS;
+      }
+
+      EXPECT_CALL(*mock_image_ctx.object_map,
+                  aio_update(snap_id, object_no, object_no + 1, state,
+                             boost::optional<uint8_t>(), _,
+                             (snap_id != CEPH_NOSNAP), _))
+        .WillOnce(WithArg<7>(Invoke([&mock_image_ctx, updated, ret_val](Context *ctx) {
+                               if (updated) {
+                                 mock_image_ctx.op_work_queue->queue(ctx, ret_val);
+                               }
+                               return updated;
+                             })));
+    }
+  }
+
+  void expect_object_copy(MockTestImageCtx& mock_image_ctx,
+                          MockObjectCopyRequest& mock_object_copy_request,
+                          bool flatten, int r) {
+    EXPECT_CALL(mock_object_copy_request, send())
+      .WillOnce(Invoke(
+        [&mock_image_ctx, &mock_object_copy_request, flatten, r]() {
+          ASSERT_EQ(flatten, mock_object_copy_request.flatten);
+          mock_image_ctx.op_work_queue->queue(
+            mock_object_copy_request.on_finish, r);
+        }));
+  }
+
+  std::string m_parent_image_name;
+};
+
+TEST_F(TestMockIoCopyupRequest, Standard) {
+  REQUIRE_FEATURE(RBD_FEATURE_LAYERING);
+
+  librbd::ImageCtx *ictx;
+  ASSERT_EQ(0, open_image(m_image_name, &ictx));
+
+  MockTestImageCtx mock_parent_image_ctx(*ictx->parent);
+  MockTestImageCtx mock_image_ctx(*ictx, &mock_parent_image_ctx);
+
+  MockExclusiveLock mock_exclusive_lock;
+  MockJournal mock_journal;
+  MockObjectMap mock_object_map;
+  initialize_features(ictx, mock_image_ctx, mock_exclusive_lock, mock_journal,
+                      mock_object_map);
+
+  expect_op_work_queue(mock_image_ctx);
+  expect_is_lock_owner(mock_image_ctx);
+
+  InSequence seq;
+
+  MockImageRequest mock_image_request;
+  std::string data(4096, '1');
+  expect_read_parent(mock_parent_image_ctx, mock_image_request, {{0, 4096}},
+                     data, 0);
+
+  MockAbstractObjectWriteRequest mock_write_request;
+  expect_get_pre_write_object_map_state(mock_image_ctx, mock_write_request,
+                                        OBJECT_EXISTS);
+  expect_object_map_at(mock_image_ctx, 0, OBJECT_NONEXISTENT);
+  expect_object_map_update(mock_image_ctx, CEPH_NOSNAP, 0, OBJECT_EXISTS, true,
+                           0);
+
+  expect_add_copyup_ops(mock_write_request);
+  expect_copyup(mock_image_ctx, CEPH_NOSNAP, "oid", data, 0);
+  expect_write(mock_image_ctx, CEPH_NOSNAP, "oid", 0);
+
+  auto req = new MockCopyupRequest(&mock_image_ctx, "oid", 0,
+                                   {{0, 4096}}, {});
+  mock_image_ctx.copyup_list[0] = req;
+  req->append_request(&mock_write_request);
+  req->send();
+
+  ASSERT_EQ(0, mock_write_request.ctx.wait());
+}
+
+TEST_F(TestMockIoCopyupRequest, StandardWithSnaps) {
+  REQUIRE_FEATURE(RBD_FEATURE_LAYERING);
+
+  librbd::ImageCtx *ictx;
+  ASSERT_EQ(0, open_image(m_image_name, &ictx));
+  ictx->snap_lock.get_write();
+  ictx->add_snap(cls::rbd::UserSnapshotNamespace(), "2", 2, ictx->size,
+                 ictx->parent_md, RBD_PROTECTION_STATUS_UNPROTECTED,
+                 0, {});
+  ictx->add_snap(cls::rbd::UserSnapshotNamespace(), "1", 1, ictx->size,
+                 ictx->parent_md, RBD_PROTECTION_STATUS_UNPROTECTED,
+                 0, {});
+  ictx->snapc = {2, {2, 1}};
+  ictx->snap_lock.put_write();
+
+  MockTestImageCtx mock_parent_image_ctx(*ictx->parent);
+  MockTestImageCtx mock_image_ctx(*ictx, &mock_parent_image_ctx);
+
+  MockExclusiveLock mock_exclusive_lock;
+  MockJournal mock_journal;
+  MockObjectMap mock_object_map;
+  initialize_features(ictx, mock_image_ctx, mock_exclusive_lock, mock_journal,
+                      mock_object_map);
+
+  expect_test_features(mock_image_ctx);
+  expect_op_work_queue(mock_image_ctx);
+  expect_is_lock_owner(mock_image_ctx);
+
+  InSequence seq;
+
+  MockImageRequest mock_image_request;
+  std::string data(4096, '1');
+  expect_read_parent(mock_parent_image_ctx, mock_image_request, {{0, 4096}},
+                     data, 0);
+
+  MockAbstractObjectWriteRequest mock_write_request;
+  expect_get_pre_write_object_map_state(mock_image_ctx, mock_write_request,
+                                        OBJECT_EXISTS);
+  expect_object_map_at(mock_image_ctx, 0, OBJECT_NONEXISTENT);
+  expect_object_map_update(mock_image_ctx, 1, 0, OBJECT_EXISTS, true, 0);
+  expect_object_map_update(mock_image_ctx, 2, 0, OBJECT_EXISTS_CLEAN, true, 0);
+  expect_object_map_update(mock_image_ctx, CEPH_NOSNAP, 0, OBJECT_EXISTS, true,
+                           0);
+
+  expect_add_copyup_ops(mock_write_request);
+  expect_copyup(mock_image_ctx, 0, "oid", data, 0);
+  expect_write(mock_image_ctx, CEPH_NOSNAP, "oid", 0);
+
+  auto req = new MockCopyupRequest(&mock_image_ctx, "oid", 0,
+                                   {{0, 4096}}, {});
+  mock_image_ctx.copyup_list[0] = req;
+  req->append_request(&mock_write_request);
+  req->send();
+
+  ASSERT_EQ(0, mock_write_request.ctx.wait());
+}
+
+TEST_F(TestMockIoCopyupRequest, CopyOnRead) {
+  REQUIRE_FEATURE(RBD_FEATURE_LAYERING);
+
+  librbd::ImageCtx *ictx;
+  ASSERT_EQ(0, open_image(m_image_name, &ictx));
+
+  MockTestImageCtx mock_parent_image_ctx(*ictx->parent);
+  MockTestImageCtx mock_image_ctx(*ictx, &mock_parent_image_ctx);
+
+  MockExclusiveLock mock_exclusive_lock;
+  MockJournal mock_journal;
+  MockObjectMap mock_object_map;
+  initialize_features(ictx, mock_image_ctx, mock_exclusive_lock, mock_journal,
+                      mock_object_map);
+
+  expect_op_work_queue(mock_image_ctx);
+  expect_is_lock_owner(mock_image_ctx);
+
+  InSequence seq;
+
+  MockImageRequest mock_image_request;
+  std::string data(4096, '1');
+  expect_read_parent(mock_parent_image_ctx, mock_image_request, {{0, 4096}},
+                     data, 0);
+
+  expect_object_map_at(mock_image_ctx, 0, OBJECT_NONEXISTENT);
+  expect_object_map_update(mock_image_ctx, CEPH_NOSNAP, 0, OBJECT_EXISTS, true,
+                           0);
+
+  expect_copyup(mock_image_ctx, CEPH_NOSNAP, "oid", data, 0);
+
+  auto req = new MockCopyupRequest(&mock_image_ctx, "oid", 0,
+                                   {{0, 4096}}, {});
+  mock_image_ctx.copyup_list[0] = req;
+  req->send();
+  ictx->flush_async_operations();
+}
+
+TEST_F(TestMockIoCopyupRequest, CopyOnReadWithSnaps) {
+  REQUIRE_FEATURE(RBD_FEATURE_LAYERING);
+
+  librbd::ImageCtx *ictx;
+  ASSERT_EQ(0, open_image(m_image_name, &ictx));
+  ictx->snap_lock.get_write();
+  ictx->add_snap(cls::rbd::UserSnapshotNamespace(), "1", 1, ictx->size,
+                 ictx->parent_md, RBD_PROTECTION_STATUS_UNPROTECTED,
+                 0, {});
+  ictx->snapc = {1, {1}};
+  ictx->snap_lock.put_write();
+
+  MockTestImageCtx mock_parent_image_ctx(*ictx->parent);
+  MockTestImageCtx mock_image_ctx(*ictx, &mock_parent_image_ctx);
+
+  MockExclusiveLock mock_exclusive_lock;
+  MockJournal mock_journal;
+  MockObjectMap mock_object_map;
+  initialize_features(ictx, mock_image_ctx, mock_exclusive_lock, mock_journal,
+                      mock_object_map);
+
+  expect_test_features(mock_image_ctx);
+  expect_op_work_queue(mock_image_ctx);
+  expect_is_lock_owner(mock_image_ctx);
+
+  InSequence seq;
+
+  MockImageRequest mock_image_request;
+  std::string data(4096, '1');
+  expect_read_parent(mock_parent_image_ctx, mock_image_request, {{0, 4096}},
+                     data, 0);
+
+  expect_object_map_at(mock_image_ctx, 0, OBJECT_NONEXISTENT);
+  expect_object_map_update(mock_image_ctx, 1, 0, OBJECT_EXISTS, true, 0);
+  expect_object_map_update(mock_image_ctx, CEPH_NOSNAP, 0, OBJECT_EXISTS_CLEAN,
+                           true, 0);
+
+  expect_copyup(mock_image_ctx, 0, "oid", data, 0);
+
+  auto req = new MockCopyupRequest(&mock_image_ctx, "oid", 0,
+                                   {{0, 4096}}, {});
+  mock_image_ctx.copyup_list[0] = req;
+  req->send();
+  ictx->flush_async_operations();
+}
+
+TEST_F(TestMockIoCopyupRequest, DeepCopy) {
+  REQUIRE_FEATURE(RBD_FEATURE_LAYERING);
+
+  librbd::ImageCtx *ictx;
+  ASSERT_EQ(0, open_image(m_image_name, &ictx));
+
+  MockTestImageCtx mock_parent_image_ctx(*ictx->parent);
+  MockTestImageCtx mock_image_ctx(*ictx, &mock_parent_image_ctx);
+
+  MockExclusiveLock mock_exclusive_lock;
+  MockJournal mock_journal;
+  MockObjectMap mock_object_map;
+  initialize_features(ictx, mock_image_ctx, mock_exclusive_lock, mock_journal,
+                      mock_object_map);
+
+  expect_op_work_queue(mock_image_ctx);
+  expect_is_lock_owner(mock_image_ctx);
+
+  InSequence seq;
+
+  MockAbstractObjectWriteRequest mock_write_request;
+  MockObjectCopyRequest mock_object_copy_request;
+  mock_image_ctx.migration_info = {1, "", "", "image id", {}, ictx->size, true};
+  expect_is_empty_write_op(mock_write_request, false);
+  expect_object_copy(mock_image_ctx, mock_object_copy_request, true, 0);
+
+  expect_is_empty_write_op(mock_write_request, false);
+  expect_get_pre_write_object_map_state(mock_image_ctx, mock_write_request,
+                                        OBJECT_EXISTS);
+  expect_object_map_at(mock_image_ctx, 0, OBJECT_NONEXISTENT);
+  expect_object_map_update(mock_image_ctx, CEPH_NOSNAP, 0, OBJECT_EXISTS, true,
+                           0);
+
+  expect_add_copyup_ops(mock_write_request);
+  expect_copyup(mock_image_ctx, CEPH_NOSNAP, "oid", "", 0);
+  expect_write(mock_image_ctx, CEPH_NOSNAP, "oid", 0);
+
+  auto req = new MockCopyupRequest(&mock_image_ctx, "oid", 0,
+                                   {{0, 4096}}, {});
+  mock_image_ctx.copyup_list[0] = req;
+  req->append_request(&mock_write_request);
+  req->send();
+
+  ASSERT_EQ(0, mock_write_request.ctx.wait());
+}
+
+TEST_F(TestMockIoCopyupRequest, DeepCopyOnRead) {
+  REQUIRE_FEATURE(RBD_FEATURE_LAYERING);
+
+  librbd::ImageCtx *ictx;
+  ASSERT_EQ(0, open_image(m_image_name, &ictx));
+
+  MockTestImageCtx mock_parent_image_ctx(*ictx->parent);
+  MockTestImageCtx mock_image_ctx(*ictx, &mock_parent_image_ctx);
+
+  MockExclusiveLock mock_exclusive_lock;
+  MockJournal mock_journal;
+  MockObjectMap mock_object_map;
+  initialize_features(ictx, mock_image_ctx, mock_exclusive_lock, mock_journal,
+                      mock_object_map);
+
+  expect_op_work_queue(mock_image_ctx);
+  expect_is_lock_owner(mock_image_ctx);
+
+  InSequence seq;
+
+  MockObjectCopyRequest mock_object_copy_request;
+  mock_image_ctx.migration_info = {1, "", "", "image id", {}, ictx->size,
+                                   false};
+  expect_object_copy(mock_image_ctx, mock_object_copy_request, true, 0);
+
+  expect_object_map_at(mock_image_ctx, 0, OBJECT_NONEXISTENT);
+  expect_object_map_update(mock_image_ctx, CEPH_NOSNAP, 0, OBJECT_EXISTS, true,
+                           0);
+
+  expect_copyup(mock_image_ctx, CEPH_NOSNAP, "oid", "", 0);
+
+  auto req = new MockCopyupRequest(&mock_image_ctx, "oid", 0,
+                                   {{0, 4096}}, {});
+  mock_image_ctx.copyup_list[0] = req;
+  req->send();
+  ictx->flush_async_operations();
+}
+
+TEST_F(TestMockIoCopyupRequest, DeepCopyWithSnaps) {
+  REQUIRE_FEATURE(RBD_FEATURE_LAYERING);
+
+  librbd::ImageCtx *ictx;
+  ASSERT_EQ(0, open_image(m_image_name, &ictx));
+  ictx->snap_lock.get_write();
+  ictx->add_snap(cls::rbd::UserSnapshotNamespace(), "3", 3, ictx->size,
+                 ictx->parent_md, RBD_PROTECTION_STATUS_UNPROTECTED,
+                 0, {});
+  ictx->add_snap(cls::rbd::UserSnapshotNamespace(), "2", 2, ictx->size,
+                 ictx->parent_md, RBD_PROTECTION_STATUS_UNPROTECTED,
+                 0, {});
+  ictx->add_snap(cls::rbd::UserSnapshotNamespace(), "1", 1, ictx->size,
+                 ictx->parent_md, RBD_PROTECTION_STATUS_UNPROTECTED,
+                 0, {});
+  ictx->snapc = {3, {3, 2, 1}};
+  ictx->snap_lock.put_write();
+
+  MockTestImageCtx mock_parent_image_ctx(*ictx->parent);
+  MockTestImageCtx mock_image_ctx(*ictx, &mock_parent_image_ctx);
+
+  MockExclusiveLock mock_exclusive_lock;
+  MockJournal mock_journal;
+  MockObjectMap mock_object_map;
+  initialize_features(ictx, mock_image_ctx, mock_exclusive_lock, mock_journal,
+                      mock_object_map);
+
+  expect_test_features(mock_image_ctx);
+  expect_op_work_queue(mock_image_ctx);
+  expect_is_lock_owner(mock_image_ctx);
+
+  InSequence seq;
+
+  MockAbstractObjectWriteRequest mock_write_request;
+  MockObjectCopyRequest mock_object_copy_request;
+  mock_image_ctx.migration_info = {1, "", "", "image id",
+                                   {{CEPH_NOSNAP, {2, 1}}, {10, {1}}},
+                                   ictx->size, true};
+  expect_is_empty_write_op(mock_write_request, false);
+  expect_object_copy(mock_image_ctx, mock_object_copy_request, true, 0);
+
+  expect_is_empty_write_op(mock_write_request, false);
+  expect_get_parent_overlap(mock_image_ctx, 2, 0, 0);
+  expect_get_parent_overlap(mock_image_ctx, 3, 1, 0);
+  expect_prune_parent_extents(mock_image_ctx, 1, 1);
+  expect_get_pre_write_object_map_state(mock_image_ctx, mock_write_request,
+                                        OBJECT_EXISTS);
+  expect_object_map_at(mock_image_ctx, 0, OBJECT_NONEXISTENT);
+  expect_object_map_update(mock_image_ctx, 3, 0, OBJECT_EXISTS, true, 0);
+  expect_object_map_update(mock_image_ctx, CEPH_NOSNAP, 0, OBJECT_EXISTS, true,
+                           0);
+
+  expect_add_copyup_ops(mock_write_request);
+  expect_copyup(mock_image_ctx, CEPH_NOSNAP, "oid", "", 0);
+  expect_write(mock_image_ctx, CEPH_NOSNAP, "oid", 0);
+
+  auto req = new MockCopyupRequest(&mock_image_ctx, "oid", 0,
+                                   {{0, 4096}}, {});
+  mock_image_ctx.copyup_list[0] = req;
+  req->append_request(&mock_write_request);
+  req->send();
+
+  ASSERT_EQ(0, mock_write_request.ctx.wait());
+}
+
+TEST_F(TestMockIoCopyupRequest, ZeroedCopyup) {
+  REQUIRE_FEATURE(RBD_FEATURE_LAYERING);
+
+  librbd::ImageCtx *ictx;
+  ASSERT_EQ(0, open_image(m_image_name, &ictx));
+
+  MockTestImageCtx mock_image_ctx(*ictx);
+
+  MockExclusiveLock mock_exclusive_lock;
+  MockJournal mock_journal;
+  MockObjectMap mock_object_map;
+  initialize_features(ictx, mock_image_ctx, mock_exclusive_lock, mock_journal,
+                      mock_object_map);
+
+  expect_op_work_queue(mock_image_ctx);
+  expect_is_lock_owner(mock_image_ctx);
+
+  InSequence seq;
+
+  MockAbstractObjectWriteRequest mock_write_request;
+  expect_is_empty_write_op(mock_write_request, false);
+  expect_get_pre_write_object_map_state(mock_image_ctx, mock_write_request,
+                                        OBJECT_EXISTS);
+  expect_object_map_at(mock_image_ctx, 0, OBJECT_NONEXISTENT);
+  expect_object_map_update(mock_image_ctx, CEPH_NOSNAP, 0, OBJECT_EXISTS, true,
+                           0);
+
+  expect_add_copyup_ops(mock_write_request);
+  expect_copyup(mock_image_ctx, CEPH_NOSNAP, "oid", "", 0);
+  expect_write(mock_image_ctx, CEPH_NOSNAP, "oid", 0);
+
+  auto req = new MockCopyupRequest(&mock_image_ctx, "oid", 0,
+                                   {{0, 4096}}, {});
+  mock_image_ctx.copyup_list[0] = req;
+  req->append_request(&mock_write_request);
+  req->send();
+
+  ASSERT_EQ(0, mock_write_request.ctx.wait());
+}
+
+TEST_F(TestMockIoCopyupRequest, ZeroedCopyOnRead) {
+  REQUIRE_FEATURE(RBD_FEATURE_LAYERING);
+
+  librbd::ImageCtx *ictx;
+  ASSERT_EQ(0, open_image(m_image_name, &ictx));
+
+  MockTestImageCtx mock_parent_image_ctx(*ictx->parent);
+  MockTestImageCtx mock_image_ctx(*ictx, &mock_parent_image_ctx);
+
+  MockExclusiveLock mock_exclusive_lock;
+  MockJournal mock_journal;
+  MockObjectMap mock_object_map;
+  initialize_features(ictx, mock_image_ctx, mock_exclusive_lock, mock_journal,
+                      mock_object_map);
+
+  expect_op_work_queue(mock_image_ctx);
+  expect_is_lock_owner(mock_image_ctx);
+
+  InSequence seq;
+
+  MockImageRequest mock_image_request;
+  std::string data(4096, '\0');
+  expect_read_parent(mock_parent_image_ctx, mock_image_request, {{0, 4096}},
+                     data, 0);
+
+  expect_object_map_at(mock_image_ctx, 0, OBJECT_NONEXISTENT);
+  expect_object_map_update(mock_image_ctx, CEPH_NOSNAP, 0, OBJECT_EXISTS, true,
+                           0);
+
+  expect_copyup(mock_image_ctx, CEPH_NOSNAP, "oid", "", 0);
+
+  auto req = new MockCopyupRequest(&mock_image_ctx, "oid", 0,
+                                   {{0, 4096}}, {});
+  mock_image_ctx.copyup_list[0] = req;
+  req->send();
+  ictx->flush_async_operations();
+}
+
+TEST_F(TestMockIoCopyupRequest, NoOpCopyup) {
+  REQUIRE_FEATURE(RBD_FEATURE_LAYERING);
+
+  librbd::ImageCtx *ictx;
+  ASSERT_EQ(0, open_image(m_image_name, &ictx));
+
+  MockTestImageCtx mock_parent_image_ctx(*ictx->parent);
+  MockTestImageCtx mock_image_ctx(*ictx, &mock_parent_image_ctx);
+
+  MockExclusiveLock mock_exclusive_lock;
+  MockJournal mock_journal;
+  MockObjectMap mock_object_map;
+  initialize_features(ictx, mock_image_ctx, mock_exclusive_lock, mock_journal,
+                      mock_object_map);
+
+  expect_op_work_queue(mock_image_ctx);
+  expect_is_lock_owner(mock_image_ctx);
+
+  InSequence seq;
+
+  MockImageRequest mock_image_request;
+  expect_read_parent(mock_parent_image_ctx, mock_image_request, {{0, 4096}},
+                     "", -ENOENT);
+
+  MockAbstractObjectWriteRequest mock_write_request;
+  expect_is_empty_write_op(mock_write_request, true);
+
+  auto req = new MockCopyupRequest(&mock_image_ctx, "oid", 0,
+                                   {{0, 4096}}, {});
+  mock_image_ctx.copyup_list[0] = req;
+  req->append_request(&mock_write_request);
+  req->send();
+
+  ASSERT_EQ(0, mock_write_request.ctx.wait());
+}
+
+TEST_F(TestMockIoCopyupRequest, RestartWrite) {
+  REQUIRE_FEATURE(RBD_FEATURE_LAYERING);
+
+  librbd::ImageCtx *ictx;
+  ASSERT_EQ(0, open_image(m_image_name, &ictx));
+
+  MockTestImageCtx mock_parent_image_ctx(*ictx->parent);
+  MockTestImageCtx mock_image_ctx(*ictx, &mock_parent_image_ctx);
+
+  MockExclusiveLock mock_exclusive_lock;
+  MockJournal mock_journal;
+  MockObjectMap mock_object_map;
+  initialize_features(ictx, mock_image_ctx, mock_exclusive_lock, mock_journal,
+                      mock_object_map);
+
+  expect_op_work_queue(mock_image_ctx);
+  expect_is_lock_owner(mock_image_ctx);
+
+  InSequence seq;
+
+  MockImageRequest mock_image_request;
+  std::string data(4096, '1');
+  expect_read_parent(mock_parent_image_ctx, mock_image_request, {{0, 4096}},
+                     data, 0);
+
+  MockAbstractObjectWriteRequest mock_write_request1;
+  expect_get_pre_write_object_map_state(mock_image_ctx, mock_write_request1,
+                                        OBJECT_EXISTS);
+  expect_object_map_at(mock_image_ctx, 0, OBJECT_NONEXISTENT);
+  expect_object_map_update(mock_image_ctx, CEPH_NOSNAP, 0, OBJECT_EXISTS, true,
+                           0);
+
+  auto req = new MockCopyupRequest(&mock_image_ctx, "oid", 0,
+                                   {{0, 4096}}, {});
+  expect_add_copyup_ops(mock_write_request1);
+  expect_copyup(mock_image_ctx, CEPH_NOSNAP, "oid", data, 0);
+
+  MockAbstractObjectWriteRequest mock_write_request2;
+  EXPECT_CALL(get_mock_io_ctx(mock_image_ctx.data_ctx),
+              write("oid", _, 0, 0, _))
+    .WillOnce(WithoutArgs(Invoke([req, &mock_write_request2]() {
+                            req->append_request(&mock_write_request2);
+                            return 0;
+                          })));
+
+  mock_image_ctx.copyup_list[0] = req;
+  req->append_request(&mock_write_request1);
+  req->send();
+
+  ASSERT_EQ(0, mock_write_request1.ctx.wait());
+  ASSERT_EQ(-ERESTART, mock_write_request2.ctx.wait());
+}
+
+TEST_F(TestMockIoCopyupRequest, ReadFromParentError) {
+  REQUIRE_FEATURE(RBD_FEATURE_LAYERING);
+
+  librbd::ImageCtx *ictx;
+  ASSERT_EQ(0, open_image(m_image_name, &ictx));
+
+  MockTestImageCtx mock_parent_image_ctx(*ictx->parent);
+  MockTestImageCtx mock_image_ctx(*ictx, &mock_parent_image_ctx);
+
+  MockExclusiveLock mock_exclusive_lock;
+  MockJournal mock_journal;
+  MockObjectMap mock_object_map;
+  initialize_features(ictx, mock_image_ctx, mock_exclusive_lock, mock_journal,
+                      mock_object_map);
+
+  expect_op_work_queue(mock_image_ctx);
+  expect_is_lock_owner(mock_image_ctx);
+
+  InSequence seq;
+
+  MockImageRequest mock_image_request;
+  expect_read_parent(mock_parent_image_ctx, mock_image_request, {{0, 4096}},
+                     "", -EPERM);
+
+  MockAbstractObjectWriteRequest mock_write_request;
+  expect_is_empty_write_op(mock_write_request, false);
+
+  auto req = new MockCopyupRequest(&mock_image_ctx, "oid", 0,
+                                   {{0, 4096}}, {});
+  mock_image_ctx.copyup_list[0] = req;
+  req->append_request(&mock_write_request);
+  req->send();
+
+  ASSERT_EQ(-EPERM, mock_write_request.ctx.wait());
+}
+
+TEST_F(TestMockIoCopyupRequest, DeepCopyError) {
+  REQUIRE_FEATURE(RBD_FEATURE_LAYERING);
+
+  librbd::ImageCtx *ictx;
+  ASSERT_EQ(0, open_image(m_image_name, &ictx));
+
+  MockTestImageCtx mock_parent_image_ctx(*ictx->parent);
+  MockTestImageCtx mock_image_ctx(*ictx, &mock_parent_image_ctx);
+
+  MockExclusiveLock mock_exclusive_lock;
+  MockJournal mock_journal;
+  MockObjectMap mock_object_map;
+  initialize_features(ictx, mock_image_ctx, mock_exclusive_lock, mock_journal,
+                      mock_object_map);
+
+  expect_op_work_queue(mock_image_ctx);
+  expect_is_lock_owner(mock_image_ctx);
+
+  InSequence seq;
+
+  MockAbstractObjectWriteRequest mock_write_request;
+  MockObjectCopyRequest mock_object_copy_request;
+  mock_image_ctx.migration_info = {1, "", "", "image id", {}, ictx->size, true};
+  expect_is_empty_write_op(mock_write_request, false);
+  expect_object_copy(mock_image_ctx, mock_object_copy_request, true, -EPERM);
+
+  expect_is_empty_write_op(mock_write_request, false);
+
+  auto req = new MockCopyupRequest(&mock_image_ctx, "oid", 0,
+                                   {{0, 4096}}, {});
+  mock_image_ctx.copyup_list[0] = req;
+  req->append_request(&mock_write_request);
+  req->send();
+
+  ASSERT_EQ(-EPERM, mock_write_request.ctx.wait());
+}
+
+TEST_F(TestMockIoCopyupRequest, UpdateObjectMapError) {
+  REQUIRE_FEATURE(RBD_FEATURE_LAYERING | RBD_FEATURE_OBJECT_MAP);
+
+  librbd::ImageCtx *ictx;
+  ASSERT_EQ(0, open_image(m_image_name, &ictx));
+
+  MockTestImageCtx mock_parent_image_ctx(*ictx->parent);
+  MockTestImageCtx mock_image_ctx(*ictx, &mock_parent_image_ctx);
+
+  MockExclusiveLock mock_exclusive_lock;
+  MockJournal mock_journal;
+  MockObjectMap mock_object_map;
+  initialize_features(ictx, mock_image_ctx, mock_exclusive_lock, mock_journal,
+                      mock_object_map);
+
+  expect_op_work_queue(mock_image_ctx);
+  expect_is_lock_owner(mock_image_ctx);
+
+  InSequence seq;
+
+  MockImageRequest mock_image_request;
+  std::string data(4096, '1');
+  expect_read_parent(mock_parent_image_ctx, mock_image_request, {{0, 4096}},
+                     data, 0);
+
+  MockAbstractObjectWriteRequest mock_write_request;
+  expect_get_pre_write_object_map_state(mock_image_ctx, mock_write_request,
+                                        OBJECT_EXISTS);
+  expect_object_map_at(mock_image_ctx, 0, OBJECT_NONEXISTENT);
+  expect_object_map_update(mock_image_ctx, CEPH_NOSNAP, 0, OBJECT_EXISTS, true,
+                           -EINVAL);
+
+  auto req = new MockCopyupRequest(&mock_image_ctx, "oid", 0,
+                                   {{0, 4096}}, {});
+  mock_image_ctx.copyup_list[0] = req;
+  req->append_request(&mock_write_request);
+  req->send();
+
+  ASSERT_EQ(-EINVAL, mock_write_request.ctx.wait());
+}
+
+TEST_F(TestMockIoCopyupRequest, CopyupError) {
+  REQUIRE_FEATURE(RBD_FEATURE_LAYERING);
+
+  librbd::ImageCtx *ictx;
+  ASSERT_EQ(0, open_image(m_image_name, &ictx));
+  ictx->snap_lock.get_write();
+  ictx->add_snap(cls::rbd::UserSnapshotNamespace(), "1", 1, ictx->size,
+                 ictx->parent_md, RBD_PROTECTION_STATUS_UNPROTECTED,
+                 0, {});
+  ictx->snapc = {1, {1}};
+  ictx->snap_lock.put_write();
+
+  MockTestImageCtx mock_parent_image_ctx(*ictx->parent);
+  MockTestImageCtx mock_image_ctx(*ictx, &mock_parent_image_ctx);
+
+  MockExclusiveLock mock_exclusive_lock;
+  MockJournal mock_journal;
+  MockObjectMap mock_object_map;
+  initialize_features(ictx, mock_image_ctx, mock_exclusive_lock, mock_journal,
+                      mock_object_map);
+
+  expect_test_features(mock_image_ctx);
+  expect_op_work_queue(mock_image_ctx);
+  expect_is_lock_owner(mock_image_ctx);
+
+  InSequence seq;
+
+  MockImageRequest mock_image_request;
+  std::string data(4096, '1');
+  expect_read_parent(mock_parent_image_ctx, mock_image_request, {{0, 4096}},
+                     data, 0);
+
+  MockAbstractObjectWriteRequest mock_write_request;
+  expect_get_pre_write_object_map_state(mock_image_ctx, mock_write_request,
+                                        OBJECT_EXISTS);
+  expect_object_map_at(mock_image_ctx, 0, OBJECT_NONEXISTENT);
+  expect_object_map_update(mock_image_ctx, 1, 0, OBJECT_EXISTS, true, 0);
+  expect_object_map_update(mock_image_ctx, CEPH_NOSNAP, 0, OBJECT_EXISTS, true,
+                           0);
+
+  expect_add_copyup_ops(mock_write_request);
+  expect_copyup(mock_image_ctx, 0, "oid", data, -EPERM);
+  expect_write(mock_image_ctx, CEPH_NOSNAP, "oid", 0);
+
+  auto req = new MockCopyupRequest(&mock_image_ctx, "oid", 0,
+                                   {{0, 4096}}, {});
+  mock_image_ctx.copyup_list[0] = req;
+  req->append_request(&mock_write_request);
+  req->send();
+
+  ASSERT_EQ(-EPERM, mock_write_request.ctx.wait());
+  ictx->flush_async_operations();
+}
+
+} // namespace io
+} // namespace librbd
index e756178b3a94644812ca08a6bb9aba679d55a8c7..2692a30ff0b8555aa6afc23c97ce08658abda6cd 100644 (file)
 namespace librbd {
 
 struct MockObjectMap {
+  MOCK_METHOD1(at, uint8_t(uint64_t));
+  uint8_t operator[](uint64_t object_no) {
+    return at(object_no);
+  }
+
   MOCK_CONST_METHOD1(enabled, bool(const RWLock &object_map_lock));
 
   MOCK_CONST_METHOD0(size, uint64_t());