summaryrefslogblamecommitdiffstats
path: root/tests/unit/commands_test.cpp
blob: 8a54df7038a27908428a4cc456fe8469e6672ee9 (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16















                                                                           
                    

                 
                                 
                        
                        
 
                               
                             










                                                                   
                                                                                       





                                                            


























































































































                                                                                                    




















                                                   






















































































































































                                                                                                   
 


















                                                                                                    
                                               

                                 


                                                                                       
                
             
                                                                    


















                                                 
















































































































                                                                                                   






















































                                                                                                 
/*
 * Copyright (C) 2018 The Android Open Source Project
 *
 * 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 <algorithm>
#include <string>

#include <android-base/strings.h>
#include <gtest/gtest.h>
#include <openssl/sha.h>

#include "otautil/print_sha1.h"
#include "otautil/rangeset.h"
#include "private/commands.h"

TEST(CommandsTest, ParseType) {
  ASSERT_EQ(Command::Type::ZERO, Command::ParseType("zero"));
  ASSERT_EQ(Command::Type::NEW, Command::ParseType("new"));
  ASSERT_EQ(Command::Type::ERASE, Command::ParseType("erase"));
  ASSERT_EQ(Command::Type::MOVE, Command::ParseType("move"));
  ASSERT_EQ(Command::Type::BSDIFF, Command::ParseType("bsdiff"));
  ASSERT_EQ(Command::Type::IMGDIFF, Command::ParseType("imgdiff"));
  ASSERT_EQ(Command::Type::STASH, Command::ParseType("stash"));
  ASSERT_EQ(Command::Type::FREE, Command::ParseType("free"));
  ASSERT_EQ(Command::Type::COMPUTE_HASH_TREE, Command::ParseType("compute_hash_tree"));
}

TEST(CommandsTest, ParseType_InvalidCommand) {
  ASSERT_EQ(Command::Type::LAST, Command::ParseType("foo"));
  ASSERT_EQ(Command::Type::LAST, Command::ParseType("bar"));
}

TEST(CommandsTest, ParseTargetInfoAndSourceInfo_SourceBlocksOnly) {
  const std::vector<std::string> tokens{
    "4,569884,569904,591946,592043",
    "117",
    "4,566779,566799,591946,592043",
  };
  TargetInfo target;
  SourceInfo source;
  std::string err;
  ASSERT_TRUE(Command::ParseTargetInfoAndSourceInfo(
      tokens, "1d74d1a60332fd38cf9405f1bae67917888da6cb", &target,
      "1d74d1a60332fd38cf9405f1bae67917888da6cb", &source, &err));
  ASSERT_EQ(TargetInfo("1d74d1a60332fd38cf9405f1bae67917888da6cb",
                       RangeSet({ { 569884, 569904 }, { 591946, 592043 } })),
            target);
  ASSERT_EQ(SourceInfo("1d74d1a60332fd38cf9405f1bae67917888da6cb",
                       RangeSet({ { 566779, 566799 }, { 591946, 592043 } }), {}, {}),
            source);
  ASSERT_EQ(117, source.blocks());
}

TEST(CommandsTest, ParseTargetInfoAndSourceInfo_StashesOnly) {
  const std::vector<std::string> tokens{
    "2,350729,350731",
    "2",
    "-",
    "6ebcf8cf1f6be0bc49e7d4a864214251925d1d15:2,0,2",
  };
  TargetInfo target;
  SourceInfo source;
  std::string err;
  ASSERT_TRUE(Command::ParseTargetInfoAndSourceInfo(
      tokens, "6ebcf8cf1f6be0bc49e7d4a864214251925d1d15", &target,
      "1c25ba04d3278d6b65a1b9f17abac78425ec8b8d", &source, &err));
  ASSERT_EQ(
      TargetInfo("6ebcf8cf1f6be0bc49e7d4a864214251925d1d15", RangeSet({ { 350729, 350731 } })),
      target);
  ASSERT_EQ(
      SourceInfo("1c25ba04d3278d6b65a1b9f17abac78425ec8b8d", {}, {},
                 {
                     StashInfo("6ebcf8cf1f6be0bc49e7d4a864214251925d1d15", RangeSet({ { 0, 2 } })),
                 }),
      source);
  ASSERT_EQ(2, source.blocks());
}

TEST(CommandsTest, ParseTargetInfoAndSourceInfo_SourceBlocksAndStashes) {
  const std::vector<std::string> tokens{
    "4,611641,611643,636981,637075",
    "96",
    "4,636981,637075,770665,770666",
    "4,0,94,95,96",
    "9eedf00d11061549e32503cadf054ec6fbfa7a23:2,94,95",
  };
  TargetInfo target;
  SourceInfo source;
  std::string err;
  ASSERT_TRUE(Command::ParseTargetInfoAndSourceInfo(
      tokens, "4734d1b241eb3d0f993714aaf7d665fae43772b6", &target,
      "a6cbdf3f416960f02189d3a814ec7e9e95c44a0d", &source, &err));
  ASSERT_EQ(TargetInfo("4734d1b241eb3d0f993714aaf7d665fae43772b6",
                       RangeSet({ { 611641, 611643 }, { 636981, 637075 } })),
            target);
  ASSERT_EQ(SourceInfo(
                "a6cbdf3f416960f02189d3a814ec7e9e95c44a0d",
                RangeSet({ { 636981, 637075 }, { 770665, 770666 } }),  // source ranges
                RangeSet({ { 0, 94 }, { 95, 96 } }),                   // source location
                {
                    StashInfo("9eedf00d11061549e32503cadf054ec6fbfa7a23", RangeSet({ { 94, 95 } })),
                }),
            source);
  ASSERT_EQ(96, source.blocks());
}

TEST(CommandsTest, ParseTargetInfoAndSourceInfo_InvalidInput) {
  const std::vector<std::string> tokens{
    "4,611641,611643,636981,637075",
    "96",
    "4,636981,637075,770665,770666",
    "4,0,94,95,96",
    "9eedf00d11061549e32503cadf054ec6fbfa7a23:2,94,95",
  };
  TargetInfo target;
  SourceInfo source;
  std::string err;

  // Mismatching block count.
  {
    std::vector<std::string> tokens_copy(tokens);
    tokens_copy[1] = "97";
    ASSERT_FALSE(Command::ParseTargetInfoAndSourceInfo(
        tokens_copy, "1d74d1a60332fd38cf9405f1bae67917888da6cb", &target,
        "1d74d1a60332fd38cf9405f1bae67917888da6cb", &source, &err));
  }

  // Excess stashes (causing block count mismatch).
  {
    std::vector<std::string> tokens_copy(tokens);
    tokens_copy.push_back("e145a2f83a33334714ac65e34969c1f115e54a6f:2,0,22");
    ASSERT_FALSE(Command::ParseTargetInfoAndSourceInfo(
        tokens_copy, "1d74d1a60332fd38cf9405f1bae67917888da6cb", &target,
        "1d74d1a60332fd38cf9405f1bae67917888da6cb", &source, &err));
  }

  // Invalid args.
  for (size_t i = 0; i < tokens.size(); i++) {
    TargetInfo target;
    SourceInfo source;
    std::string err;
    ASSERT_FALSE(Command::ParseTargetInfoAndSourceInfo(
        std::vector<std::string>(tokens.cbegin() + i + 1, tokens.cend()),
        "1d74d1a60332fd38cf9405f1bae67917888da6cb", &target,
        "1d74d1a60332fd38cf9405f1bae67917888da6cb", &source, &err));
  }
}

TEST(CommandsTest, Parse_EmptyInput) {
  std::string err;
  ASSERT_FALSE(Command::Parse("", 0, &err));
  ASSERT_EQ("invalid type", err);
}

TEST(CommandsTest, Parse_ABORT_Allowed) {
  Command::abort_allowed_ = true;

  const std::string input{ "abort" };
  std::string err;
  Command command = Command::Parse(input, 0, &err);
  ASSERT_TRUE(command);

  ASSERT_EQ(TargetInfo(), command.target());
  ASSERT_EQ(SourceInfo(), command.source());
  ASSERT_EQ(StashInfo(), command.stash());
  ASSERT_EQ(PatchInfo(), command.patch());
}

TEST(CommandsTest, Parse_ABORT_NotAllowed) {
  const std::string input{ "abort" };
  std::string err;
  Command command = Command::Parse(input, 0, &err);
  ASSERT_FALSE(command);
}

TEST(CommandsTest, Parse_BSDIFF) {
  const std::string input{
    "bsdiff 0 148 "
    "f201a4e04bd3860da6ad47b957ef424d58a58f8c 9d5d223b4bc5c45dbd25a799c4f1a98466731599 "
    "4,565704,565752,566779,566799 "
    "68 4,64525,64545,565704,565752"
  };
  std::string err;
  Command command = Command::Parse(input, 1, &err);
  ASSERT_TRUE(command);

  ASSERT_EQ(Command::Type::BSDIFF, command.type());
  ASSERT_EQ(1, command.index());
  ASSERT_EQ(input, command.cmdline());

  ASSERT_EQ(TargetInfo("9d5d223b4bc5c45dbd25a799c4f1a98466731599",
                       RangeSet({ { 565704, 565752 }, { 566779, 566799 } })),
            command.target());
  ASSERT_EQ(SourceInfo("f201a4e04bd3860da6ad47b957ef424d58a58f8c",
                       RangeSet({ { 64525, 64545 }, { 565704, 565752 } }), RangeSet(), {}),
            command.source());
  ASSERT_EQ(StashInfo(), command.stash());
  ASSERT_EQ(PatchInfo(0, 148), command.patch());
}

TEST(CommandsTest, Parse_ERASE) {
  const std::string input{ "erase 2,5,10" };
  std::string err;
  Command command = Command::Parse(input, 2, &err);
  ASSERT_TRUE(command);

  ASSERT_EQ(Command::Type::ERASE, command.type());
  ASSERT_EQ(2, command.index());
  ASSERT_EQ(input, command.cmdline());

  ASSERT_EQ(TargetInfo("unknown-hash", RangeSet({ { 5, 10 } })), command.target());
  ASSERT_EQ(SourceInfo(), command.source());
  ASSERT_EQ(StashInfo(), command.stash());
  ASSERT_EQ(PatchInfo(), command.patch());
}

TEST(CommandsTest, Parse_FREE) {
  const std::string input{ "free hash1" };
  std::string err;
  Command command = Command::Parse(input, 3, &err);
  ASSERT_TRUE(command);

  ASSERT_EQ(Command::Type::FREE, command.type());
  ASSERT_EQ(3, command.index());
  ASSERT_EQ(input, command.cmdline());

  ASSERT_EQ(TargetInfo(), command.target());
  ASSERT_EQ(SourceInfo(), command.source());
  ASSERT_EQ(StashInfo("hash1", RangeSet()), command.stash());
  ASSERT_EQ(PatchInfo(), command.patch());
}

TEST(CommandsTest, Parse_IMGDIFF) {
  const std::string input{
    "imgdiff 29629269 185 "
    "a6b1c49aed1b57a2aab1ec3e1505b945540cd8db 51978f65035f584a8ef7afa941dacb6d5e862164 "
    "2,90851,90852 "
    "1 2,90851,90852"
  };
  std::string err;
  Command command = Command::Parse(input, 4, &err);
  ASSERT_TRUE(command);

  ASSERT_EQ(Command::Type::IMGDIFF, command.type());
  ASSERT_EQ(4, command.index());
  ASSERT_EQ(input, command.cmdline());

  ASSERT_EQ(TargetInfo("51978f65035f584a8ef7afa941dacb6d5e862164", RangeSet({ { 90851, 90852 } })),
            command.target());
  ASSERT_EQ(SourceInfo("a6b1c49aed1b57a2aab1ec3e1505b945540cd8db", RangeSet({ { 90851, 90852 } }),
                       RangeSet(), {}),
            command.source());
  ASSERT_EQ(StashInfo(), command.stash());
  ASSERT_EQ(PatchInfo(29629269, 185), command.patch());
}

TEST(CommandsTest, Parse_MOVE) {
  const std::string input{
    "move 1d74d1a60332fd38cf9405f1bae67917888da6cb "
    "4,569884,569904,591946,592043 117 4,566779,566799,591946,592043"
  };
  std::string err;
  Command command = Command::Parse(input, 5, &err);
  ASSERT_TRUE(command);

  ASSERT_EQ(Command::Type::MOVE, command.type());
  ASSERT_EQ(5, command.index());
  ASSERT_EQ(input, command.cmdline());

  ASSERT_EQ(TargetInfo("1d74d1a60332fd38cf9405f1bae67917888da6cb",
                       RangeSet({ { 569884, 569904 }, { 591946, 592043 } })),
            command.target());
  ASSERT_EQ(SourceInfo("1d74d1a60332fd38cf9405f1bae67917888da6cb",
                       RangeSet({ { 566779, 566799 }, { 591946, 592043 } }), RangeSet(), {}),
            command.source());
  ASSERT_EQ(StashInfo(), command.stash());
  ASSERT_EQ(PatchInfo(), command.patch());
}

TEST(CommandsTest, Parse_NEW) {
  const std::string input{ "new 4,3,5,10,12" };
  std::string err;
  Command command = Command::Parse(input, 6, &err);
  ASSERT_TRUE(command);

  ASSERT_EQ(Command::Type::NEW, command.type());
  ASSERT_EQ(6, command.index());
  ASSERT_EQ(input, command.cmdline());

  ASSERT_EQ(TargetInfo("unknown-hash", RangeSet({ { 3, 5 }, { 10, 12 } })), command.target());
  ASSERT_EQ(SourceInfo(), command.source());
  ASSERT_EQ(StashInfo(), command.stash());
  ASSERT_EQ(PatchInfo(), command.patch());
}

TEST(CommandsTest, Parse_STASH) {
  const std::string input{ "stash hash1 2,5,10" };
  std::string err;
  Command command = Command::Parse(input, 7, &err);
  ASSERT_TRUE(command);

  ASSERT_EQ(Command::Type::STASH, command.type());
  ASSERT_EQ(7, command.index());
  ASSERT_EQ(input, command.cmdline());

  ASSERT_EQ(TargetInfo(), command.target());
  ASSERT_EQ(SourceInfo(), command.source());
  ASSERT_EQ(StashInfo("hash1", RangeSet({ { 5, 10 } })), command.stash());
  ASSERT_EQ(PatchInfo(), command.patch());
}

TEST(CommandsTest, Parse_ZERO) {
  const std::string input{ "zero 2,1,5" };
  std::string err;
  Command command = Command::Parse(input, 8, &err);
  ASSERT_TRUE(command);

  ASSERT_EQ(Command::Type::ZERO, command.type());
  ASSERT_EQ(8, command.index());
  ASSERT_EQ(input, command.cmdline());

  ASSERT_EQ(TargetInfo("unknown-hash", RangeSet({ { 1, 5 } })), command.target());
  ASSERT_EQ(SourceInfo(), command.source());
  ASSERT_EQ(StashInfo(), command.stash());
  ASSERT_EQ(PatchInfo(), command.patch());
}

TEST(CommandsTest, Parse_COMPUTE_HASH_TREE) {
  const std::string input{ "compute_hash_tree 2,0,1 2,3,4 sha1 unknown-salt unknown-root-hash" };
  std::string err;
  Command command = Command::Parse(input, 9, &err);
  ASSERT_TRUE(command);

  ASSERT_EQ(Command::Type::COMPUTE_HASH_TREE, command.type());
  ASSERT_EQ(9, command.index());
  ASSERT_EQ(input, command.cmdline());

  HashTreeInfo expected_info(RangeSet({ { 0, 1 } }), RangeSet({ { 3, 4 } }), "sha1", "unknown-salt",
                             "unknown-root-hash");
  ASSERT_EQ(expected_info, command.hash_tree_info());
  ASSERT_EQ(TargetInfo(), command.target());
  ASSERT_EQ(SourceInfo(), command.source());
  ASSERT_EQ(StashInfo(), command.stash());
  ASSERT_EQ(PatchInfo(), command.patch());
}

TEST(CommandsTest, Parse_InvalidNumberOfArgs) {
  Command::abort_allowed_ = true;

  // Note that the case of having excess args in BSDIFF, IMGDIFF and MOVE is covered by
  // ParseTargetInfoAndSourceInfo_InvalidInput.
  std::vector<std::string> inputs{
    "abort foo",
    "bsdiff",
    "compute_hash_tree, 2,0,1 2,0,1 unknown-algorithm unknown-salt",
    "erase",
    "erase 4,3,5,10,12 hash1",
    "free",
    "free id1 id2",
    "imgdiff",
    "move",
    "new",
    "new 4,3,5,10,12 hash1",
    "stash",
    "stash id1",
    "stash id1 4,3,5,10,12 id2",
    "zero",
    "zero 4,3,5,10,12 hash2",
  };
  for (const auto& input : inputs) {
    std::string err;
    ASSERT_FALSE(Command::Parse(input, 0, &err));
  }
}

TEST(SourceInfoTest, Overlaps) {
  ASSERT_TRUE(SourceInfo("1d74d1a60332fd38cf9405f1bae67917888da6cb",
                         RangeSet({ { 7, 9 }, { 16, 20 } }), {}, {})
                  .Overlaps(TargetInfo("1d74d1a60332fd38cf9405f1bae67917888da6cb",
                                       RangeSet({ { 7, 9 }, { 16, 20 } }))));

  ASSERT_TRUE(SourceInfo("1d74d1a60332fd38cf9405f1bae67917888da6cb",
                         RangeSet({ { 7, 9 }, { 16, 20 } }), {}, {})
                  .Overlaps(TargetInfo("1d74d1a60332fd38cf9405f1bae67917888da6cb",
                                       RangeSet({ { 4, 7 }, { 16, 23 } }))));

  ASSERT_FALSE(SourceInfo("1d74d1a60332fd38cf9405f1bae67917888da6cb",
                          RangeSet({ { 7, 9 }, { 16, 20 } }), {}, {})
                   .Overlaps(TargetInfo("1d74d1a60332fd38cf9405f1bae67917888da6cb",
                                        RangeSet({ { 9, 16 } }))));
}

TEST(SourceInfoTest, Overlaps_EmptySourceOrTarget) {
  ASSERT_FALSE(SourceInfo().Overlaps(TargetInfo()));

  ASSERT_FALSE(SourceInfo().Overlaps(
      TargetInfo("1d74d1a60332fd38cf9405f1bae67917888da6cb", RangeSet({ { 7, 9 }, { 16, 20 } }))));

  ASSERT_FALSE(SourceInfo("1d74d1a60332fd38cf9405f1bae67917888da6cb",
                          RangeSet({ { 7, 9 }, { 16, 20 } }), {}, {})
                   .Overlaps(TargetInfo()));
}

TEST(SourceInfoTest, Overlaps_WithStashes) {
  ASSERT_FALSE(SourceInfo("a6cbdf3f416960f02189d3a814ec7e9e95c44a0d",
                          RangeSet({ { 81, 175 }, { 265, 266 } }),  // source ranges
                          RangeSet({ { 0, 94 }, { 95, 96 } }),      // source location
                          { StashInfo("9eedf00d11061549e32503cadf054ec6fbfa7a23",
                                      RangeSet({ { 94, 95 } })) })
                   .Overlaps(TargetInfo("1d74d1a60332fd38cf9405f1bae67917888da6cb",
                                        RangeSet({ { 175, 265 } }))));

  ASSERT_TRUE(SourceInfo("a6cbdf3f416960f02189d3a814ec7e9e95c44a0d",
                         RangeSet({ { 81, 175 }, { 265, 266 } }),  // source ranges
                         RangeSet({ { 0, 94 }, { 95, 96 } }),      // source location
                         { StashInfo("9eedf00d11061549e32503cadf054ec6fbfa7a23",
                                     RangeSet({ { 94, 95 } })) })
                  .Overlaps(TargetInfo("1d74d1a60332fd38cf9405f1bae67917888da6cb",
                                       RangeSet({ { 265, 266 } }))));
}

// The block size should be specified by the caller of ReadAll (i.e. from Command instance during
// normal run).
constexpr size_t kBlockSize = 4096;

TEST(SourceInfoTest, ReadAll) {
  // "2727756cfee3fbfe24bf5650123fd7743d7b3465" is the SHA-1 hex digest of 8192 * 'a'.
  const SourceInfo source("2727756cfee3fbfe24bf5650123fd7743d7b3465", RangeSet({ { 0, 2 } }), {},
                          {});
  auto block_reader = [](const RangeSet& src, std::vector<uint8_t>* block_buffer) -> int {
    std::fill_n(block_buffer->begin(), src.blocks() * kBlockSize, 'a');
    return 0;
  };
  auto stash_reader = [](const std::string&, std::vector<uint8_t>*) -> int { return 0; };
  std::vector<uint8_t> buffer(source.blocks() * kBlockSize);
  ASSERT_TRUE(source.ReadAll(&buffer, kBlockSize, block_reader, stash_reader));
  ASSERT_EQ(source.blocks() * kBlockSize, buffer.size());

  uint8_t digest[SHA_DIGEST_LENGTH];
  SHA1(buffer.data(), buffer.size(), digest);
  ASSERT_EQ(source.hash(), print_sha1(digest));
}

TEST(SourceInfoTest, ReadAll_WithStashes) {
  const SourceInfo source(
      // SHA-1 hex digest of 8192 * 'a' + 4096 * 'b'.
      "ee3ebea26130769c10ad13604712100346d48660", RangeSet({ { 0, 2 } }), RangeSet({ { 0, 2 } }),
      { StashInfo("1e41f7a59e80c6eb4dc043caae80d273f130bed8", RangeSet({ { 2, 3 } })) });
  auto block_reader = [](const RangeSet& src, std::vector<uint8_t>* block_buffer) -> int {
    std::fill_n(block_buffer->begin(), src.blocks() * kBlockSize, 'a');
    return 0;
  };
  auto stash_reader = [](const std::string&, std::vector<uint8_t>* stash_buffer) -> int {
    std::fill_n(stash_buffer->begin(), kBlockSize, 'b');
    return 0;
  };
  std::vector<uint8_t> buffer(source.blocks() * kBlockSize);
  ASSERT_TRUE(source.ReadAll(&buffer, kBlockSize, block_reader, stash_reader));
  ASSERT_EQ(source.blocks() * kBlockSize, buffer.size());

  uint8_t digest[SHA_DIGEST_LENGTH];
  SHA1(buffer.data(), buffer.size(), digest);
  ASSERT_EQ(source.hash(), print_sha1(digest));
}

TEST(SourceInfoTest, ReadAll_BufferTooSmall) {
  const SourceInfo source("2727756cfee3fbfe24bf5650123fd7743d7b3465", RangeSet({ { 0, 2 } }), {},
                          {});
  auto block_reader = [](const RangeSet&, std::vector<uint8_t>*) -> int { return 0; };
  auto stash_reader = [](const std::string&, std::vector<uint8_t>*) -> int { return 0; };
  std::vector<uint8_t> buffer(source.blocks() * kBlockSize - 1);
  ASSERT_FALSE(source.ReadAll(&buffer, kBlockSize, block_reader, stash_reader));
}

TEST(SourceInfoTest, ReadAll_FailingReader) {
  const SourceInfo source(
      "ee3ebea26130769c10ad13604712100346d48660", RangeSet({ { 0, 2 } }), RangeSet({ { 0, 2 } }),
      { StashInfo("1e41f7a59e80c6eb4dc043caae80d273f130bed8", RangeSet({ { 2, 3 } })) });
  std::vector<uint8_t> buffer(source.blocks() * kBlockSize);
  auto failing_block_reader = [](const RangeSet&, std::vector<uint8_t>*) -> int { return -1; };
  auto stash_reader = [](const std::string&, std::vector<uint8_t>*) -> int { return 0; };
  ASSERT_FALSE(source.ReadAll(&buffer, kBlockSize, failing_block_reader, stash_reader));

  auto block_reader = [](const RangeSet&, std::vector<uint8_t>*) -> int { return 0; };
  auto failing_stash_reader = [](const std::string&, std::vector<uint8_t>*) -> int { return -1; };
  ASSERT_FALSE(source.ReadAll(&buffer, kBlockSize, block_reader, failing_stash_reader));
}

TEST(TransferListTest, Parse) {
  std::vector<std::string> input_lines{
    "4",  // version
    "2",  // total blocks
    "1",  // max stashed entries
    "1",  // max stashed blocks
    "stash 1d74d1a60332fd38cf9405f1bae67917888da6cb 2,0,1",
    "move 1d74d1a60332fd38cf9405f1bae67917888da6cb 2,0,1 1 2,0,1",
  };

  std::string err;
  TransferList transfer_list = TransferList::Parse(android::base::Join(input_lines, '\n'), &err);
  ASSERT_TRUE(static_cast<bool>(transfer_list));
  ASSERT_EQ(4, transfer_list.version());
  ASSERT_EQ(2, transfer_list.total_blocks());
  ASSERT_EQ(1, transfer_list.stash_max_entries());
  ASSERT_EQ(1, transfer_list.stash_max_blocks());
  ASSERT_EQ(2U, transfer_list.commands().size());
  ASSERT_EQ(Command::Type::STASH, transfer_list.commands()[0].type());
  ASSERT_EQ(Command::Type::MOVE, transfer_list.commands()[1].type());
}

TEST(TransferListTest, Parse_InvalidCommand) {
  std::vector<std::string> input_lines{
    "4",  // version
    "2",  // total blocks
    "1",  // max stashed entries
    "1",  // max stashed blocks
    "stash 1d74d1a60332fd38cf9405f1bae67917888da6cb 2,0,1",
    "move 1d74d1a60332fd38cf9405f1bae67917888da6cb 2,0,1 1",
  };

  std::string err;
  TransferList transfer_list = TransferList::Parse(android::base::Join(input_lines, '\n'), &err);
  ASSERT_FALSE(static_cast<bool>(transfer_list));
}

TEST(TransferListTest, Parse_ZeroTotalBlocks) {
  std::vector<std::string> input_lines{
    "4",  // version
    "0",  // total blocks
    "0",  // max stashed entries
    "0",  // max stashed blocks
  };

  std::string err;
  TransferList transfer_list = TransferList::Parse(android::base::Join(input_lines, '\n'), &err);
  ASSERT_TRUE(static_cast<bool>(transfer_list));
  ASSERT_EQ(4, transfer_list.version());
  ASSERT_EQ(0, transfer_list.total_blocks());
  ASSERT_EQ(0, transfer_list.stash_max_entries());
  ASSERT_EQ(0, transfer_list.stash_max_blocks());
  ASSERT_TRUE(transfer_list.commands().empty());
}