Add unittests for dawn::native::Blob

Bug: dawn:549
Change-Id: Ie3db7e8f1aa720c8513480688611cadd9b72647d
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/92600
Kokoro: Kokoro <noreply+kokoro@google.com>
Reviewed-by: Shrek Shao <shrekshao@google.com>
Commit-Queue: Austin Eng <enga@chromium.org>
diff --git a/src/dawn/native/Blob.cpp b/src/dawn/native/Blob.cpp
index 66ba0d7..a3ac2b2 100644
--- a/src/dawn/native/Blob.cpp
+++ b/src/dawn/native/Blob.cpp
@@ -14,6 +14,7 @@
 
 #include <utility>
 
+#include "dawn/common/Assert.h"
 #include "dawn/native/Blob.h"
 
 namespace dawn::native {
@@ -35,11 +36,24 @@
 Blob::Blob() : mData(nullptr), mSize(0), mDeleter({}) {}
 
 Blob::Blob(uint8_t* data, size_t size, std::function<void()> deleter)
-    : mData(data), mSize(size), mDeleter(std::move(deleter)) {}
+    : mData(data), mSize(size), mDeleter(std::move(deleter)) {
+    // It is invalid to make a blob that has null data unless its size is also zero.
+    ASSERT(data != nullptr || size == 0);
+}
 
-Blob::Blob(Blob&&) = default;
+Blob::Blob(Blob&& rhs) : mData(rhs.mData), mSize(rhs.mSize) {
+    mDeleter = std::move(rhs.mDeleter);
+}
 
-Blob& Blob::operator=(Blob&&) = default;
+Blob& Blob::operator=(Blob&& rhs) {
+    mData = rhs.mData;
+    mSize = rhs.mSize;
+    if (mDeleter) {
+        mDeleter();
+    }
+    mDeleter = std::move(rhs.mDeleter);
+    return *this;
+}
 
 Blob::~Blob() {
     if (mDeleter) {
diff --git a/src/dawn/tests/BUILD.gn b/src/dawn/tests/BUILD.gn
index 9323299..69c41a2 100644
--- a/src/dawn/tests/BUILD.gn
+++ b/src/dawn/tests/BUILD.gn
@@ -252,6 +252,7 @@
     "unittests/ToBackendTests.cpp",
     "unittests/TypedIntegerTests.cpp",
     "unittests/VersionTests.cpp",
+    "unittests/native/BlobTests.cpp",
     "unittests/native/CacheKeyTests.cpp",
     "unittests/native/CommandBufferEncodingTests.cpp",
     "unittests/native/CreatePipelineAsyncTaskTests.cpp",
diff --git a/src/dawn/tests/unittests/native/BlobTests.cpp b/src/dawn/tests/unittests/native/BlobTests.cpp
new file mode 100644
index 0000000..580f0b7
--- /dev/null
+++ b/src/dawn/tests/unittests/native/BlobTests.cpp
@@ -0,0 +1,168 @@
+// Copyright 2022 The Dawn Authors
+//
+// Licensed 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 <utility>
+
+#include "dawn/native/Blob.h"
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+
+namespace dawn::native {
+
+namespace {
+
+// Test that a blob starts empty.
+TEST(BlobTests, DefaultEmpty) {
+    Blob b;
+    EXPECT_TRUE(b.Empty());
+    EXPECT_EQ(b.Data(), nullptr);
+    EXPECT_EQ(b.Size(), 0u);
+}
+
+// Test that you can create a blob with a size in bytes and write/read its contents.
+TEST(BlobTests, SizedCreation) {
+    Blob b = CreateBlob(10);
+    EXPECT_FALSE(b.Empty());
+    EXPECT_EQ(b.Size(), 10u);
+    ASSERT_NE(b.Data(), nullptr);
+    // We should be able to copy 10 bytes into the blob.
+    char data[10] = {'1', '2', '3', '4', '5', '6', '7', '8', '9', '0'};
+    memcpy(b.Data(), data, sizeof(data));
+    // And retrieve the exact contents back.
+    EXPECT_EQ(memcmp(b.Data(), data, sizeof(data)), 0);
+}
+
+// Test that you can create a zero-sized blob.
+TEST(BlobTests, EmptySizedCreation) {
+    Blob b = CreateBlob(0);
+    EXPECT_TRUE(b.Empty());
+    EXPECT_EQ(b.Data(), nullptr);
+    EXPECT_EQ(b.Size(), 0u);
+}
+
+// Test that you can create a blob with UnsafeCreateWithDeleter, and the deleter is
+// called on destruction.
+TEST(BlobTests, UnsafeCreateWithDeleter) {
+    unsigned char data[13] = "hello world!";
+    testing::StrictMock<testing::MockFunction<void()>> mockDeleter;
+    {
+        // Make a blob with a mock deleter.
+        Blob b = Blob::UnsafeCreateWithDeleter(data, sizeof(data), [&]() { mockDeleter.Call(); });
+        // Check the contents.
+        EXPECT_FALSE(b.Empty());
+        EXPECT_EQ(b.Size(), sizeof(data));
+        ASSERT_EQ(b.Data(), data);
+        EXPECT_EQ(memcmp(b.Data(), data, sizeof(data)), 0);
+
+        // |b| is deleted when this scope exits.
+        EXPECT_CALL(mockDeleter, Call());
+    }
+}
+
+// Test that you can create a blob with UnsafeCreateWithDeleter with zero size but non-null data.
+// The deleter is still called on destruction, and the blob is normalized to be empty.
+TEST(BlobTests, UnsafeCreateWithDeleterZeroSize) {
+    unsigned char data[13] = "hello world!";
+    testing::StrictMock<testing::MockFunction<void()>> mockDeleter;
+    {
+        // Make a blob with a mock deleter.
+        Blob b = Blob::UnsafeCreateWithDeleter(data, 0, [&]() { mockDeleter.Call(); });
+        // Check the contents.
+        EXPECT_TRUE(b.Empty());
+        EXPECT_EQ(b.Size(), 0u);
+        // Data still points to the data.
+        EXPECT_EQ(b.Data(), data);
+
+        // |b| is deleted when this scope exits.
+        EXPECT_CALL(mockDeleter, Call());
+    }
+}
+
+// Test that you can create a blob with UnsafeCreateWithDeleter that points to nothing.
+// The deleter should still be called.
+TEST(BlobTests, UnsafeCreateWithDeleterEmpty) {
+    testing::StrictMock<testing::MockFunction<void()>> mockDeleter;
+    {
+        // Make a blob with a mock deleter.
+        Blob b = Blob::UnsafeCreateWithDeleter(nullptr, 0, [&]() { mockDeleter.Call(); });
+        // Check the contents.
+        EXPECT_TRUE(b.Empty());
+        EXPECT_EQ(b.Size(), 0u);
+        EXPECT_EQ(b.Data(), nullptr);
+
+        // |b| is deleted when this scope exits.
+        EXPECT_CALL(mockDeleter, Call());
+    }
+}
+
+// Test that move construction moves the data from one blob into the new one.
+TEST(BlobTests, MoveConstruct) {
+    // Create the blob.
+    Blob b1 = CreateBlob(10);
+    char data[10] = {'1', '2', '3', '4', '5', '6', '7', '8', '9', '0'};
+    memcpy(b1.Data(), data, sizeof(data));
+
+    // Move construct b2 from b1.
+    Blob b2(std::move(b1));
+
+    // Data should be moved.
+    EXPECT_FALSE(b2.Empty());
+    EXPECT_EQ(b2.Size(), 10u);
+    ASSERT_NE(b2.Data(), nullptr);
+    EXPECT_EQ(memcmp(b2.Data(), data, sizeof(data)), 0);
+}
+
+// Test that move assignment moves the data from one blob into another.
+TEST(BlobTests, MoveAssign) {
+    // Create the blob.
+    Blob b1 = CreateBlob(10);
+    char data[10] = {'1', '2', '3', '4', '5', '6', '7', '8', '9', '0'};
+    memcpy(b1.Data(), data, sizeof(data));
+
+    // Move assign b2 from b1.
+    Blob b2;
+    b2 = std::move(b1);
+
+    // Data should be moved.
+    EXPECT_FALSE(b2.Empty());
+    EXPECT_EQ(b2.Size(), 10u);
+    ASSERT_NE(b2.Data(), nullptr);
+    EXPECT_EQ(memcmp(b2.Data(), data, sizeof(data)), 0);
+}
+
+// Test that move assignment can replace the contents of the moved-to blob.
+TEST(BlobTests, MoveAssignOver) {
+    // Create the blob.
+    Blob b1 = CreateBlob(10);
+    char data[10] = {'1', '2', '3', '4', '5', '6', '7', '8', '9', '0'};
+    memcpy(b1.Data(), data, sizeof(data));
+
+    // Create another blob with a mock deleter.
+    testing::StrictMock<testing::MockFunction<void()>> mockDeleter;
+    Blob b2 = Blob::UnsafeCreateWithDeleter(nullptr, 0, [&]() { mockDeleter.Call(); });
+
+    // Move b1 into b2, replacing b2's contents, and expect the deleter to be called.
+    EXPECT_CALL(mockDeleter, Call());
+    b2 = std::move(b1);
+
+    // Data should be moved.
+    EXPECT_FALSE(b2.Empty());
+    EXPECT_EQ(b2.Size(), 10u);
+    ASSERT_NE(b2.Data(), nullptr);
+    EXPECT_EQ(memcmp(b2.Data(), data, sizeof(data)), 0);
+}
+
+}  // namespace
+
+}  // namespace dawn::native