blob: 5d0c0b0a06ca150f4845b6c2e94d841969067a02 [file]
// Copyright 2026 Google LLC
//
// 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
//
// https://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 "utils/byte_instruction.h"
#include <cstdint>
#include <string>
#include <vector>
#include "absl/status/status.h"
#include "absl/status/status_matchers.h"
#include "absl/strings/string_view.h"
#include "absl/types/span.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "riegeli/bytes/string_reader.h"
#include "riegeli/bytes/string_writer.h"
namespace credentio {
namespace {
using ::absl_testing::IsOk;
using ::absl_testing::StatusIs;
using ::testing::HasSubstr;
struct SuccessfulApplicationTestCase {
std::string name;
std::string input;
std::vector<ByteInstruction> instructions;
std::string expected;
};
using SuccessfulApplicationTest =
testing::TestWithParam<SuccessfulApplicationTestCase>;
INSTANTIATE_TEST_SUITE_P(
ParameterizedTests, SuccessfulApplicationTest,
testing::ValuesIn<SuccessfulApplicationTestCase>({
{
.name = "NoInstructions",
.input = "hello world",
.instructions = {},
.expected = "hello world",
},
{
.name = "EmptyInsert",
.input = "",
.instructions =
{
{.operation = ByteInstruction::Operation::kInsert,
.offset = 0,
.bytes = "foo"},
},
.expected = "foo",
},
{
.name = "EmptyReplace",
.input = "",
.instructions =
{
{.operation = ByteInstruction::Operation::kReplace,
.offset = 0,
.bytes = "foo"},
},
.expected = "foo",
},
{
.name = "EmptyInstruction",
.input = "bar",
.instructions =
{
{.operation = ByteInstruction::Operation::kInsert,
.offset = 0,
.bytes = ""},
},
.expected = "bar",
},
{
.name = "InsertAtStart",
.input = "bar",
.instructions =
{
{.operation = ByteInstruction::Operation::kInsert,
.offset = 0,
.bytes = "foo"},
},
.expected = "foobar",
},
{
.name = "InsertAtEnd",
.input = "bar",
.instructions =
{
{.operation = ByteInstruction::Operation::kInsert,
.offset = 3,
.bytes = "foo"},
},
.expected = "barfoo",
},
{
.name = "ReplaceAtStart",
.input = "bar",
.instructions =
{
{.operation = ByteInstruction::Operation::kReplace,
.offset = 0,
.bytes = "foo"},
},
.expected = "foo",
},
{
.name = "ReplaceAtEnd",
.input = "bar",
.instructions =
{
{.operation = ByteInstruction::Operation::kReplace,
.offset = 3,
.bytes = "foo"},
},
.expected = "barfoo",
},
{
.name = "InsertAtMiddle",
.input = "bar",
.instructions =
{
{.operation = ByteInstruction::Operation::kInsert,
.offset = 1,
.bytes = "foo"},
},
.expected = "bfooar",
},
{
.name = "ReplaceAtMiddle",
.input = "bar",
.instructions =
{
{.operation = ByteInstruction::Operation::kReplace,
.offset = 1,
.bytes = "foo"},
},
.expected = "bfoo",
},
{
.name = "ReplaceInsertReplace",
.input = "0_________0_________0_________0_________",
.instructions =
{
{.operation = ByteInstruction::Operation::kReplace,
.offset = 10,
.bytes = "replace"},
{.operation = ByteInstruction::Operation::kInsert,
.offset = 17,
.bytes = "insert"},
{.operation = ByteInstruction::Operation::kReplace,
.offset = 30,
.bytes = "second"},
},
.expected = "0_________replaceinsert___0_________second____",
},
{
.name = "InsertPastEndOfInput",
.input = "hello world",
.instructions =
{
{.operation = ByteInstruction::Operation::kInsert,
.offset = 30,
.bytes = " goodnight"},
},
.expected = "hello world goodnight",
},
{
.name = "ReplacePastEndOfInput",
.input = "hello world",
.instructions =
{
{.operation = ByteInstruction::Operation::kReplace,
.offset = 30,
.bytes = " goodnight"},
},
.expected = "hello world goodnight",
},
{
.name = "ReplacePastEndOfInputWithTrailingInstructions",
.input = "hello world",
.instructions =
{
{.operation = ByteInstruction::Operation::kReplace,
.offset = 6,
.bytes = "world goodnight"},
{.operation = ByteInstruction::Operation::kInsert,
.offset = 30,
.bytes = " and goodbye"},
},
.expected = "hello world goodnight and goodbye",
},
}),
[](const testing::TestParamInfo<SuccessfulApplicationTest::ParamType>&
info) { return info.param.name; });
TEST_P(SuccessfulApplicationTest, AppliesByteInstructions) {
riegeli::StringReader<std::string> reader(GetParam().input);
ASSERT_THAT(reader.status(), IsOk());
riegeli::StringWriter<std::string> writer;
ASSERT_THAT(writer.status(), IsOk());
EXPECT_THAT(ApplyByteInstructions(&reader, GetParam().instructions, &writer),
IsOk());
ASSERT_TRUE(reader.VerifyEndAndClose());
ASSERT_TRUE(writer.Close());
EXPECT_EQ(writer.dest(), GetParam().expected);
}
TEST_P(SuccessfulApplicationTest, AppliesByteInstructionsInPlace) {
riegeli::StringWriter<std::string> writer(
GetParam().input, riegeli::StringWriterBase::Options().set_append(true));
ASSERT_THAT(writer.status(), IsOk());
EXPECT_THAT(ApplyByteInstructions(GetParam().instructions, &writer), IsOk());
ASSERT_TRUE(writer.Close());
EXPECT_EQ(writer.dest(), GetParam().expected);
}
TEST(SuccessfulApplicationTest, ReaderNotAtStart) {
riegeli::StringReader<> reader("hello world");
ASSERT_THAT(reader.status(), IsOk());
ASSERT_TRUE(reader.Seek(8));
riegeli::StringWriter<std::string> writer;
ASSERT_THAT(writer.status(), IsOk());
EXPECT_THAT(
ApplyByteInstructions(&reader,
{{.operation = ByteInstruction::Operation::kReplace,
.offset = 6,
.bytes = "W"}},
&writer),
IsOk());
ASSERT_TRUE(reader.VerifyEndAndClose());
ASSERT_TRUE(writer.Close());
EXPECT_EQ(writer.dest(), "hello World");
}
TEST(SuccessfulApplicationTest, WriterNotAtStart) {
riegeli::StringWriter<std::string> writer(
"hello world", riegeli::StringWriterBase::Options().set_append(true));
ASSERT_THAT(writer.status(), IsOk());
ASSERT_TRUE(writer.Seek(8));
EXPECT_THAT(
ApplyByteInstructions({{.operation = ByteInstruction::Operation::kReplace,
.offset = 6,
.bytes = "W"}},
&writer),
IsOk());
ASSERT_TRUE(writer.Close());
EXPECT_EQ(writer.dest(), "hello World");
}
TEST(FailedApplicationTest, ReaderClosed) {
riegeli::StringReader<> reader("hello world");
ASSERT_THAT(reader.status(), IsOk());
reader.Close();
riegeli::StringWriter<std::string> writer;
ASSERT_THAT(writer.status(), IsOk());
EXPECT_THAT(ApplyByteInstructions(&reader, {}, &writer),
StatusIs(absl::StatusCode::kFailedPrecondition,
HasSubstr("Object closed")));
}
TEST(FailedApplicationTest, WriterClosed) {
riegeli::StringReader<> reader("hello world");
ASSERT_THAT(reader.status(), IsOk());
riegeli::StringWriter<std::string> writer;
ASSERT_THAT(writer.status(), IsOk());
writer.Close();
EXPECT_THAT(ApplyByteInstructions(&reader, {}, &writer),
StatusIs(absl::StatusCode::kFailedPrecondition,
HasSubstr("Object closed")));
}
TEST(FailedApplicationTest, InstructionOutOfOrder) {
riegeli::StringReader<> reader("hello world");
ASSERT_THAT(reader.status(), IsOk());
riegeli::StringWriter<std::string> writer;
ASSERT_THAT(writer.status(), IsOk());
EXPECT_THAT(
ApplyByteInstructions(&reader,
{{.operation = ByteInstruction::Operation::kReplace,
.offset = 5,
.bytes = "da"},
{.operation = ByteInstruction::Operation::kReplace,
.offset = 2,
.bytes = "da"}},
&writer),
StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("must be in ascending order")));
}
class PrematureEofReader : public riegeli::StringReader<> {
public:
PrematureEofReader(absl::string_view data, uint64_t fake_size)
: riegeli::StringReader<>(data), fake_size_(fake_size) {}
std::optional<riegeli::Position> SizeImpl() override { return fake_size_; }
private:
uint64_t fake_size_;
};
TEST(FailedApplicationTest, UnexpectedEofDuringCopy) {
PrematureEofReader reader("hello", 100);
ASSERT_THAT(reader.status(), IsOk());
riegeli::StringWriter<std::string> writer;
ASSERT_THAT(writer.status(), IsOk());
EXPECT_THAT(
ApplyByteInstructions(&reader,
{{.operation = ByteInstruction::Operation::kReplace,
.offset = 10,
.bytes = "x"}},
&writer),
StatusIs(absl::StatusCode::kDataLoss,
HasSubstr("Unexpected EOF while copying source data")));
}
TEST(FailedApplicationTest, UnexpectedEofDuringSkip) {
PrematureEofReader reader("hello", 100);
ASSERT_THAT(reader.status(), IsOk());
riegeli::StringWriter<std::string> writer;
ASSERT_THAT(writer.status(), IsOk());
EXPECT_THAT(
ApplyByteInstructions(&reader,
{{.operation = ByteInstruction::Operation::kReplace,
.offset = 0,
.bytes = "xxxxxxxxxx"}},
&writer),
StatusIs(absl::StatusCode::kDataLoss,
HasSubstr("Unexpected EOF while skipping source data")));
}
TEST(FailedApplicationInPlaceTest, WriterClosed) {
riegeli::StringWriter<std::string> writer(
"hello world", riegeli::StringWriterBase::Options().set_append(true));
ASSERT_THAT(writer.status(), IsOk());
writer.Close();
EXPECT_THAT(ApplyByteInstructions({}, &writer),
StatusIs(absl::StatusCode::kDataLoss,
HasSubstr("failed to seek to start of file")));
}
TEST(FailedApplicationInPlaceTest, InstructionOutOfOrder) {
riegeli::StringWriter<std::string> writer(
"hello world", riegeli::StringWriterBase::Options().set_append(true));
ASSERT_THAT(writer.status(), IsOk());
EXPECT_THAT(
ApplyByteInstructions({{.operation = ByteInstruction::Operation::kReplace,
.offset = 5,
.bytes = "da"},
{.operation = ByteInstruction::Operation::kReplace,
.offset = 2,
.bytes = "da"}},
&writer),
StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("must be in ascending order")));
}
} // namespace
} // namespace credentio