]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
test: unit test cases for rbd mirror image sync
authorJason Dillaman <dillaman@redhat.com>
Sat, 12 Mar 2016 05:16:40 +0000 (00:16 -0500)
committerJason Dillaman <dillaman@redhat.com>
Sun, 13 Mar 2016 03:40:17 +0000 (22:40 -0500)
Signed-off-by: Jason Dillaman <dillaman@redhat.com>
src/test/Makefile-client.am
src/test/rbd_mirror/image_sync/test_mock_ImageCopyRequest.cc
src/test/rbd_mirror/image_sync/test_mock_ObjectCopyRequest.cc
src/test/rbd_mirror/test_ImageSync.cc [new file with mode: 0644]
src/test/rbd_mirror/test_fixture.cc
src/test/rbd_mirror/test_fixture.h
src/test/rbd_mirror/test_main.cc
src/test/rbd_mirror/test_mock_ImageSync.cc [new file with mode: 0644]

index 333a2f312ef93ce28e4b21b127f47f58a87f2304..8de2988a36dab47c678c05862aab586faf68140e 100644 (file)
@@ -442,6 +442,7 @@ librbd_mirror_test_la_SOURCES = \
        test/rbd_mirror/test_ClusterWatcher.cc \
        test/rbd_mirror/test_PoolWatcher.cc \
        test/rbd_mirror/test_ImageReplayer.cc \
+       test/rbd_mirror/test_ImageSync.cc \
        test/rbd_mirror/test_fixture.cc
 
 noinst_HEADERS += \
@@ -455,6 +456,7 @@ noinst_LTLIBRARIES += librbd_mirror_test.la
 unittest_rbd_mirror_SOURCES = \
        test/rbd_mirror/test_main.cc \
        test/rbd_mirror/test_mock_fixture.cc \
+       test/rbd_mirror/test_mock_ImageSync.cc \
        test/rbd_mirror/image_sync/test_mock_ImageCopyRequest.cc \
        test/rbd_mirror/image_sync/test_mock_ObjectCopyRequest.cc \
        test/rbd_mirror/image_sync/test_mock_SnapshotCopyRequest.cc \
index c8baae60948e1316bd19a02408125ee9aead3205..302a4d62500b828e119b3c8b40f97d57850dbfe7 100644 (file)
@@ -152,25 +152,7 @@ public:
                                     &sync_point, ctx);
   }
 
-  int create_snap(librbd::ImageCtx *image_ctx, const char* snap_name,
-                  librados::snap_t *snap_id) {
-    int r = image_ctx->operations->snap_create(snap_name);
-    if (r < 0) {
-      return r;
-    }
-
-    r = image_ctx->state->refresh();
-    if (r < 0) {
-      return r;
-    }
-
-    if (image_ctx->snap_ids.count(snap_name) == 0) {
-      return -ENOENT;
-    }
-    *snap_id = image_ctx->snap_ids[snap_name];
-    return 0;
-  }
-
+  using TestFixture::create_snap;
   int create_snap(const char* snap_name) {
     librados::snap_t remote_snap_id;
     int r = create_snap(m_remote_image_ctx, snap_name, &remote_snap_id);
@@ -344,7 +326,7 @@ TEST_F(TestMockImageSyncImageCopyRequest, RestartCatchup) {
 
 TEST_F(TestMockImageSyncImageCopyRequest, RestartPartialSync) {
   ASSERT_EQ(0, create_snap("snap1"));
-  m_client_meta.sync_points = {{"snap1", 0}};
+  m_client_meta.sync_points = {{"snap1", librbd::journal::MirrorPeerSyncPoint::ObjectNumber{0U}}};
 
   librbd::MockImageCtx mock_remote_image_ctx(*m_remote_image_ctx);
   librbd::MockImageCtx mock_local_image_ctx(*m_local_image_ctx);
index 14997b48a592bff13cf041f187e8a2b70ff5e86c..7df68723e429e071f93e8440fe014d56b5e251f7 100644 (file)
@@ -152,25 +152,7 @@ public:
     }
   }
 
-  int create_snap(librbd::ImageCtx *image_ctx, const char* snap_name,
-                  librados::snap_t *snap_id) {
-    int r = image_ctx->operations->snap_create(snap_name);
-    if (r < 0) {
-      return r;
-    }
-
-    r = image_ctx->state->refresh();
-    if (r < 0) {
-      return r;
-    }
-
-    if (image_ctx->snap_ids.count(snap_name) == 0) {
-      return -ENOENT;
-    }
-    *snap_id = image_ctx->snap_ids[snap_name];
-    return 0;
-  }
-
+  using TestFixture::create_snap;
   int create_snap(const char* snap_name) {
     librados::snap_t remote_snap_id;
     int r = create_snap(m_remote_image_ctx, snap_name, &remote_snap_id);
diff --git a/src/test/rbd_mirror/test_ImageSync.cc b/src/test/rbd_mirror/test_ImageSync.cc
new file mode 100644 (file)
index 0000000..7846fca
--- /dev/null
@@ -0,0 +1,140 @@
+// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*-
+// vim: ts=8 sw=2 smarttab
+
+#include "test/rbd_mirror/test_fixture.h"
+#include "include/rbd/librbd.hpp"
+#include "journal/Journaler.h"
+#include "librbd/AioImageRequestWQ.h"
+#include "librbd/ExclusiveLock.h"
+#include "librbd/ImageCtx.h"
+#include "librbd/ImageState.h"
+#include "tools/rbd_mirror/ImageSync.h"
+#include "tools/rbd_mirror/Threads.h"
+
+void register_test_image_sync() {
+}
+
+namespace rbd {
+namespace mirror {
+
+namespace {
+
+void scribble(librbd::ImageCtx *image_ctx, int num_ops, size_t max_size)
+{
+  for (int i=0; i<num_ops; i++) {
+    uint64_t off = rand() % (image_ctx->size - max_size + 1);
+    uint64_t len = 1 + rand() % max_size;
+
+    if (rand() % 4 == 0) {
+      ASSERT_EQ((int)len, image_ctx->aio_work_queue->discard(off, len));
+    } else {
+      std::string str(len, '1');
+      ASSERT_EQ((int)len, image_ctx->aio_work_queue->write(off, len,
+                                                           str.c_str(), 0));
+    }
+  }
+}
+
+} // anonymous namespace
+class TestImageSync : public TestFixture {
+public:
+
+  virtual void SetUp() {
+    TestFixture::SetUp();
+    create_and_open(m_local_io_ctx, &m_local_image_ctx);
+    create_and_open(m_remote_io_ctx, &m_remote_image_ctx);
+
+    m_threads = new rbd::mirror::Threads(reinterpret_cast<CephContext*>(
+      m_local_io_ctx.cct()));
+
+    m_remote_journaler = new ::journal::Journaler(
+      m_threads->work_queue, m_threads->timer, &m_threads->timer_lock,
+      m_remote_io_ctx, m_remote_image_ctx->id, "mirror-uuid", 5);
+
+    m_client_meta = {"image-id"};
+
+    librbd::journal::ClientData client_data(m_client_meta);
+    bufferlist client_data_bl;
+    ::encode(client_data, client_data_bl);
+
+    ASSERT_EQ(0, m_remote_journaler->register_client(client_data_bl));
+  }
+
+  virtual void TearDown() {
+    delete m_threads;
+    TestFixture::TearDown();
+  }
+
+  void create_and_open(librados::IoCtx &io_ctx, librbd::ImageCtx **image_ctx) {
+    librbd::RBD rbd;
+    ASSERT_EQ(0, create_image(rbd, io_ctx, m_image_name, m_image_size));
+    ASSERT_EQ(0, open_image(io_ctx, m_image_name, image_ctx));
+
+    C_SaferCond ctx;
+    {
+      RWLock::RLocker owner_locker((*image_ctx)->owner_lock);
+      (*image_ctx)->exclusive_lock->try_lock(&ctx);
+    }
+    ASSERT_EQ(0, ctx.wait());
+    ASSERT_TRUE((*image_ctx)->exclusive_lock->is_lock_owner());
+  }
+
+  ImageSync<> *create_request(Context *ctx) {
+    return new ImageSync<>(m_local_image_ctx, m_remote_image_ctx,
+                           m_threads->timer, &m_threads->timer_lock,
+                           "mirror-uuid", m_remote_journaler, &m_client_meta,
+                           ctx);
+  }
+
+  librbd::ImageCtx *m_remote_image_ctx;
+  librbd::ImageCtx *m_local_image_ctx;
+  ::journal::Journaler *m_remote_journaler;
+  librbd::journal::MirrorPeerClientMeta m_client_meta;
+
+  rbd::mirror::Threads *m_threads = nullptr;
+};
+
+TEST_F(TestImageSync, Empty) {
+  C_SaferCond ctx;
+  ImageSync<> *request = create_request(&ctx);
+  request->start();
+  ASSERT_EQ(0, ctx.wait());
+
+  ASSERT_EQ(0U, m_client_meta.sync_points.size());
+  ASSERT_EQ(0, m_remote_image_ctx->state->refresh());
+  ASSERT_EQ(0U, m_remote_image_ctx->snap_ids.size());
+  ASSERT_EQ(0, m_local_image_ctx->state->refresh());
+  ASSERT_EQ(1U, m_local_image_ctx->snap_ids.size()); // deleted on journal replay
+}
+
+TEST_F(TestImageSync, Simple) {
+  scribble(m_remote_image_ctx, 10, 102400);
+  {
+    RWLock::RLocker owner_locker(m_remote_image_ctx->owner_lock);
+    ASSERT_EQ(0, m_remote_image_ctx->flush());
+  }
+
+  C_SaferCond ctx;
+  ImageSync<> *request = create_request(&ctx);
+  request->start();
+  ASSERT_EQ(0, ctx.wait());
+
+  int64_t object_size = std::min<int64_t>(
+    m_remote_image_ctx->size, 1 << m_remote_image_ctx->order);
+  bufferlist read_remote_bl;
+  read_remote_bl.append(std::string(object_size, '1'));
+  bufferlist read_local_bl;
+  read_local_bl.append(std::string(object_size, '1'));
+
+  for (uint64_t offset = 0; offset < m_remote_image_ctx->size;
+       offset += object_size) {
+    ASSERT_LE(0, m_remote_image_ctx->aio_work_queue->read(
+                   offset, object_size, read_remote_bl.c_str(), 0));
+    ASSERT_LE(0, m_local_image_ctx->aio_work_queue->read(
+                   offset, object_size, read_local_bl.c_str(), 0));
+    ASSERT_TRUE(read_remote_bl.contents_equal(read_local_bl));
+  }
+}
+
+} // namespace mirror
+} // namespace rbd
index 51d25c27857e226907a3d56d69329d165b3a7034..ab24353bd1c330a902a11f146197ca3ce294cae2 100644 (file)
@@ -6,6 +6,7 @@
 #include "include/rbd/librbd.hpp"
 #include "librbd/ImageCtx.h"
 #include "librbd/ImageState.h"
+#include "librbd/Operations.h"
 #include "test/librados/test.h"
 
 namespace rbd {
@@ -50,7 +51,7 @@ void TestFixture::TearDown() {
 
 int TestFixture::create_image(librbd::RBD &rbd, librados::IoCtx &ioctx,
                               const std::string &name, uint64_t size) {
-  int order = 0;
+  int order = 18;
   return rbd.create2(ioctx, name.c_str(), size, RBD_FEATURES_ALL, &order);
 }
 
@@ -63,6 +64,28 @@ int TestFixture::open_image(librados::IoCtx &io_ctx,
   return (*image_ctx)->state->open();
 }
 
+int TestFixture::create_snap(librbd::ImageCtx *image_ctx, const char* snap_name,
+                             librados::snap_t *snap_id) {
+  int r = image_ctx->operations->snap_create(snap_name);
+  if (r < 0) {
+    return r;
+  }
+
+  r = image_ctx->state->refresh();
+  if (r < 0) {
+    return r;
+  }
+
+  if (image_ctx->snap_ids.count(snap_name) == 0) {
+    return -ENOENT;
+  }
+
+  if (snap_id != nullptr) {
+    *snap_id = image_ctx->snap_ids[snap_name];
+  }
+  return 0;
+}
+
 std::string TestFixture::get_temp_image_name() {
   ++_image_number;
   return "image" + stringify(_image_number);
index 4297615aedbfd0f3ae9a17c160e49466e9687b74..730ffc5311366943d4de88bdc9ffba0a16cea922 100644 (file)
@@ -31,7 +31,7 @@ public:
   librados::IoCtx m_remote_io_ctx;
 
   std::string m_image_name;
-  uint64_t m_image_size = 2 << 20;
+  uint64_t m_image_size = 1 << 24;
 
   std::set<librbd::ImageCtx *> m_image_ctxs;
 
@@ -40,6 +40,9 @@ public:
   int open_image(librados::IoCtx &io_ctx, const std::string &image_name,
                  librbd::ImageCtx **image_ctx);
 
+  int create_snap(librbd::ImageCtx *image_ctx, const char* snap_name,
+                  librados::snap_t *snap_id = nullptr);
+
   static std::string get_temp_image_name();
 
   static std::string _local_pool_name;
index 23fa0dad548525923b23aaac8bca2609319c6661..71fe23a4334a6ea3d230f90807568ad5af79ce55 100644 (file)
 extern void register_test_cluster_watcher();
 extern void register_test_pool_watcher();
 extern void register_test_rbd_mirror();
+extern void register_test_image_sync();
 
 int main(int argc, char **argv)
 {
   register_test_cluster_watcher();
   register_test_pool_watcher();
   register_test_rbd_mirror();
+  register_test_image_sync();
 
   ::testing::InitGoogleTest(&argc, argv);
 
diff --git a/src/test/rbd_mirror/test_mock_ImageSync.cc b/src/test/rbd_mirror/test_mock_ImageSync.cc
new file mode 100644 (file)
index 0000000..21892cf
--- /dev/null
@@ -0,0 +1,303 @@
+// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*-
+// vim: ts=8 sw=2 smarttab
+
+#include "test/rbd_mirror/test_mock_fixture.h"
+#include "include/rbd/librbd.hpp"
+#include "librbd/journal/Types.h"
+#include "test/librados_test_stub/MockTestMemIoCtxImpl.h"
+#include "test/librbd/mock/MockImageCtx.h"
+#include "test/rbd_mirror/mock/MockJournaler.h"
+#include "tools/rbd_mirror/ImageSync.h"
+#include "tools/rbd_mirror/Threads.h"
+#include "tools/rbd_mirror/image_sync/ImageCopyRequest.h"
+#include "tools/rbd_mirror/image_sync/SnapshotCopyRequest.h"
+#include "tools/rbd_mirror/image_sync/SyncPointCreateRequest.h"
+#include "tools/rbd_mirror/image_sync/SyncPointPruneRequest.h"
+
+// template definitions
+#include "tools/rbd_mirror/ImageSync.cc"
+template class rbd::mirror::ImageSync<librbd::MockImageCtx>;
+
+namespace rbd {
+namespace mirror {
+
+namespace image_sync {
+
+template <>
+class ImageCopyRequest<librbd::MockImageCtx> {
+public:
+  static ImageCopyRequest* s_instance;
+  Context *on_finish;
+
+  static ImageCopyRequest* create(librbd::MockImageCtx *local_image_ctx,
+                                  librbd::MockImageCtx *remote_image_ctx,
+                                  SafeTimer *timer, Mutex *timer_lock,
+                                  journal::MockJournaler *journaler,
+                                  librbd::journal::MirrorPeerClientMeta *client_meta,
+                                  librbd::journal::MirrorPeerSyncPoint *sync_point,
+                                  Context *on_finish) {
+    assert(s_instance != nullptr);
+    s_instance->on_finish = on_finish;
+    return s_instance;
+  }
+
+  ImageCopyRequest() {
+    s_instance = this;
+  }
+  MOCK_METHOD0(cancel, void());
+  MOCK_METHOD0(send, void());
+};
+
+template <>
+class SnapshotCopyRequest<librbd::MockImageCtx> {
+public:
+  static SnapshotCopyRequest* s_instance;
+  Context *on_finish;
+
+  static SnapshotCopyRequest* create(librbd::MockImageCtx *local_image_ctx,
+                                     librbd::MockImageCtx *remote_image_ctx,
+                                     SnapshotCopyRequest<librbd::ImageCtx>::SnapMap *snap_map,
+                                     journal::MockJournaler *journaler,
+                                     librbd::journal::MirrorPeerClientMeta *client_meta,
+                                     Context *on_finish) {
+    assert(s_instance != nullptr);
+    s_instance->on_finish = on_finish;
+    return s_instance;
+  }
+
+  SnapshotCopyRequest() {
+    s_instance = this;
+  }
+  MOCK_METHOD0(send, void());
+};
+
+template <>
+class SyncPointCreateRequest<librbd::MockImageCtx> {
+public:
+  static SyncPointCreateRequest *s_instance;
+  Context *on_finish;
+
+  static SyncPointCreateRequest* create(librbd::MockImageCtx *remote_image_ctx,
+                                        const std::string &mirror_uuid,
+                                        journal::MockJournaler *journaler,
+                                        librbd::journal::MirrorPeerClientMeta *client_meta,
+                                        Context *on_finish) {
+    assert(s_instance != nullptr);
+    s_instance->on_finish = on_finish;
+    return s_instance;
+  }
+
+  SyncPointCreateRequest() {
+    s_instance = this;
+  }
+  MOCK_METHOD0(send, void());
+};
+
+template <>
+class SyncPointPruneRequest<librbd::MockImageCtx> {
+public:
+  static SyncPointPruneRequest *s_instance;
+  Context *on_finish;
+  bool sync_complete;
+
+  static SyncPointPruneRequest* create(librbd::MockImageCtx *remote_image_ctx,
+                                       bool sync_complete,
+                                       journal::MockJournaler *journaler,
+                                       librbd::journal::MirrorPeerClientMeta *client_meta,
+                                       Context *on_finish) {
+    assert(s_instance != nullptr);
+    s_instance->on_finish = on_finish;
+    s_instance->sync_complete = sync_complete;
+    return s_instance;
+  }
+
+  SyncPointPruneRequest() {
+    s_instance = this;
+  }
+  MOCK_METHOD0(send, void());
+};
+
+ImageCopyRequest<librbd::MockImageCtx>* ImageCopyRequest<librbd::MockImageCtx>::s_instance = nullptr;
+SnapshotCopyRequest<librbd::MockImageCtx>* SnapshotCopyRequest<librbd::MockImageCtx>::s_instance = nullptr;
+SyncPointCreateRequest<librbd::MockImageCtx>* SyncPointCreateRequest<librbd::MockImageCtx>::s_instance = nullptr;
+SyncPointPruneRequest<librbd::MockImageCtx>* SyncPointPruneRequest<librbd::MockImageCtx>::s_instance = nullptr;
+
+} // namespace image_sync
+
+using ::testing::_;
+using ::testing::InSequence;
+using ::testing::Invoke;
+
+class TestMockImageSync : public TestMockFixture {
+public:
+  typedef ImageSync<librbd::MockImageCtx> MockImageSync;
+  typedef image_sync::ImageCopyRequest<librbd::MockImageCtx> MockImageCopyRequest;
+  typedef image_sync::SnapshotCopyRequest<librbd::MockImageCtx> MockSnapshotCopyRequest;
+  typedef image_sync::SyncPointCreateRequest<librbd::MockImageCtx> MockSyncPointCreateRequest;
+  typedef image_sync::SyncPointPruneRequest<librbd::MockImageCtx> MockSyncPointPruneRequest;
+
+  virtual void SetUp() {
+    TestMockFixture::SetUp();
+
+    librbd::RBD rbd;
+    ASSERT_EQ(0, create_image(rbd, m_remote_io_ctx, m_image_name, m_image_size));
+    ASSERT_EQ(0, open_image(m_remote_io_ctx, m_image_name, &m_remote_image_ctx));
+
+    ASSERT_EQ(0, create_image(rbd, m_local_io_ctx, m_image_name, m_image_size));
+    ASSERT_EQ(0, open_image(m_local_io_ctx, m_image_name, &m_local_image_ctx));
+
+    m_threads = new rbd::mirror::Threads(reinterpret_cast<CephContext*>(
+      m_local_io_ctx.cct()));
+  }
+
+  virtual void TearDown() {
+    delete m_threads;
+    TestMockFixture::TearDown();
+  }
+
+  void expect_create_sync_point(MockSyncPointCreateRequest &mock_sync_point_create_request,
+                                int r) {
+    EXPECT_CALL(mock_sync_point_create_request, send())
+      .WillOnce(Invoke([this, &mock_sync_point_create_request, r]() {
+          if (r == 0) {
+            m_client_meta.sync_points.emplace_back("snap1", boost::none);
+          }
+          m_threads->work_queue->queue(mock_sync_point_create_request.on_finish, r);
+        }));
+  }
+
+  void expect_copy_snapshots(MockSnapshotCopyRequest &mock_snapshot_copy_request, int r) {
+    EXPECT_CALL(mock_snapshot_copy_request, send())
+      .WillOnce(Invoke([this, &mock_snapshot_copy_request, r]() {
+          m_threads->work_queue->queue(mock_snapshot_copy_request.on_finish, r);
+        }));
+  }
+
+  void expect_copy_image(MockImageCopyRequest &mock_image_copy_request, int r) {
+    EXPECT_CALL(mock_image_copy_request, send())
+      .WillOnce(Invoke([this, &mock_image_copy_request, r]() {
+          m_threads->work_queue->queue(mock_image_copy_request.on_finish, r);
+        }));
+  }
+
+  void expect_prune_sync_point(MockSyncPointPruneRequest &mock_sync_point_prune_request,
+                               bool sync_complete, int r) {
+    EXPECT_CALL(mock_sync_point_prune_request, send())
+      .WillOnce(Invoke([this, &mock_sync_point_prune_request, sync_complete, r]() {
+          ASSERT_EQ(sync_complete, mock_sync_point_prune_request.sync_complete);
+          if (r == 0 && !m_client_meta.sync_points.empty()) {
+            if (sync_complete) {
+              m_client_meta.sync_points.pop_front();
+            } else {
+              while (m_client_meta.sync_points.size() > 1) {
+                m_client_meta.sync_points.pop_back();
+              }
+            }
+          }
+          m_threads->work_queue->queue(mock_sync_point_prune_request.on_finish, r);
+        }));
+  }
+
+  MockImageSync *create_request(librbd::MockImageCtx &mock_remote_image_ctx,
+                                librbd::MockImageCtx &mock_local_image_ctx,
+                                journal::MockJournaler &mock_journaler,
+                                Context *ctx) {
+    return new MockImageSync(&mock_local_image_ctx, &mock_remote_image_ctx,
+                             m_threads->timer, &m_threads->timer_lock,
+                             "mirror-uuid", &mock_journaler, &m_client_meta,
+                             ctx);
+  }
+
+  librbd::ImageCtx *m_remote_image_ctx;
+  librbd::ImageCtx *m_local_image_ctx;
+  librbd::journal::MirrorPeerClientMeta m_client_meta;
+
+  rbd::mirror::Threads *m_threads = nullptr;
+};
+
+TEST_F(TestMockImageSync, SimpleSync) {
+  librbd::MockImageCtx mock_remote_image_ctx(*m_remote_image_ctx);
+  librbd::MockImageCtx mock_local_image_ctx(*m_local_image_ctx);
+  journal::MockJournaler mock_journaler;
+  MockImageCopyRequest mock_image_copy_request;
+  MockSnapshotCopyRequest mock_snapshot_copy_request;
+  MockSyncPointCreateRequest mock_sync_point_create_request;
+  MockSyncPointPruneRequest mock_sync_point_prune_request;
+
+  InSequence seq;
+  expect_create_sync_point(mock_sync_point_create_request, 0);
+  expect_copy_snapshots(mock_snapshot_copy_request, 0);
+  expect_copy_image(mock_image_copy_request, 0);
+  expect_prune_sync_point(mock_sync_point_prune_request, true, 0);
+
+  C_SaferCond ctx;
+  MockImageSync *request = create_request(mock_remote_image_ctx,
+                                          mock_local_image_ctx,
+                                          mock_journaler, &ctx);
+  request->start();
+  ASSERT_EQ(0, ctx.wait());
+}
+
+TEST_F(TestMockImageSync, RestartSync) {
+  librbd::MockImageCtx mock_remote_image_ctx(*m_remote_image_ctx);
+  librbd::MockImageCtx mock_local_image_ctx(*m_local_image_ctx);
+  journal::MockJournaler mock_journaler;
+  MockImageCopyRequest mock_image_copy_request;
+  MockSnapshotCopyRequest mock_snapshot_copy_request;
+  MockSyncPointCreateRequest mock_sync_point_create_request;
+  MockSyncPointPruneRequest mock_sync_point_prune_request;
+
+  m_client_meta.sync_points = {{"snap1", boost::none},
+                               {"snap2", "snap1", boost::none}};
+
+  InSequence seq;
+  expect_prune_sync_point(mock_sync_point_prune_request, false, 0);
+  expect_copy_snapshots(mock_snapshot_copy_request, 0);
+  expect_copy_image(mock_image_copy_request, 0);
+  expect_prune_sync_point(mock_sync_point_prune_request, true, 0);
+
+  C_SaferCond ctx;
+  MockImageSync *request = create_request(mock_remote_image_ctx,
+                                          mock_local_image_ctx,
+                                          mock_journaler, &ctx);
+  request->start();
+  ASSERT_EQ(0, ctx.wait());
+}
+
+TEST_F(TestMockImageSync, CancelImageCopy) {
+  librbd::MockImageCtx mock_remote_image_ctx(*m_remote_image_ctx);
+  librbd::MockImageCtx mock_local_image_ctx(*m_local_image_ctx);
+  journal::MockJournaler mock_journaler;
+  MockImageCopyRequest mock_image_copy_request;
+  MockSnapshotCopyRequest mock_snapshot_copy_request;
+  MockSyncPointCreateRequest mock_sync_point_create_request;
+  MockSyncPointPruneRequest mock_sync_point_prune_request;
+
+  m_client_meta.sync_points = {{"snap1", boost::none}};
+
+  InSequence seq;
+  expect_copy_snapshots(mock_snapshot_copy_request, 0);
+
+  C_SaferCond image_copy_ctx;
+  EXPECT_CALL(mock_image_copy_request, send())
+    .WillOnce(Invoke([&image_copy_ctx]() {
+        image_copy_ctx.complete(0);
+      }));
+  EXPECT_CALL(mock_image_copy_request, cancel());
+
+  C_SaferCond ctx;
+  MockImageSync *request = create_request(mock_remote_image_ctx,
+                                          mock_local_image_ctx,
+                                          mock_journaler, &ctx);
+  request->start();
+
+  // cancel the image copy once it starts
+  ASSERT_EQ(0, image_copy_ctx.wait());
+  request->cancel();
+  m_threads->work_queue->queue(mock_image_copy_request.on_finish, 0);
+
+  ASSERT_EQ(-ECANCELED, ctx.wait());
+}
+
+} // namespace mirror
+} // namespace rbd