2020-01-17 21:05:03 +08:00
|
|
|
// Copyright 2019 Google LLC
|
2019-03-19 00:21:48 +08:00
|
|
|
//
|
|
|
|
// 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
|
|
|
|
//
|
2022-01-28 17:38:27 +08:00
|
|
|
// https://www.apache.org/licenses/LICENSE-2.0
|
2019-03-19 00:21:48 +08:00
|
|
|
//
|
|
|
|
// 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 <fcntl.h>
|
2024-02-29 02:38:26 +08:00
|
|
|
#include <sys/types.h>
|
2019-03-19 00:21:48 +08:00
|
|
|
|
2024-02-29 02:38:26 +08:00
|
|
|
#include <cstdint>
|
2022-10-12 20:22:51 +08:00
|
|
|
#include <memory>
|
2023-08-24 21:23:03 +08:00
|
|
|
#include <string>
|
2022-03-23 18:34:56 +08:00
|
|
|
#include <thread> // NOLINT(build/c++11)
|
2024-02-29 02:38:26 +08:00
|
|
|
#include <vector>
|
2022-03-23 18:34:56 +08:00
|
|
|
|
2019-03-19 00:21:48 +08:00
|
|
|
#include "benchmark/benchmark.h"
|
|
|
|
#include "gmock/gmock.h"
|
|
|
|
#include "gtest/gtest.h"
|
2024-02-29 02:38:26 +08:00
|
|
|
#include "absl/log/log.h"
|
2020-02-28 01:23:44 +08:00
|
|
|
#include "absl/status/status.h"
|
2023-08-24 21:23:03 +08:00
|
|
|
#include "absl/status/statusor.h"
|
2024-02-29 02:38:26 +08:00
|
|
|
#include "absl/strings/string_view.h"
|
2023-08-24 21:23:03 +08:00
|
|
|
#include "absl/time/clock.h"
|
|
|
|
#include "absl/time/time.h"
|
2024-02-29 02:38:26 +08:00
|
|
|
#include "absl/types/span.h"
|
2021-10-11 22:59:01 +08:00
|
|
|
#include "sandboxed_api/examples/stringop/stringop-sapi.sapi.h"
|
|
|
|
#include "sandboxed_api/examples/stringop/stringop_params.pb.h"
|
|
|
|
#include "sandboxed_api/examples/sum/sum-sapi.sapi.h"
|
2023-08-30 17:20:25 +08:00
|
|
|
#include "sandboxed_api/sandbox.h"
|
2021-02-01 23:10:43 +08:00
|
|
|
#include "sandboxed_api/testing.h"
|
2019-03-19 00:21:48 +08:00
|
|
|
#include "sandboxed_api/transaction.h"
|
|
|
|
#include "sandboxed_api/util/status_matchers.h"
|
2024-02-29 02:38:26 +08:00
|
|
|
#include "sandboxed_api/var_array.h"
|
2019-03-19 00:21:48 +08:00
|
|
|
|
2021-02-01 23:10:43 +08:00
|
|
|
namespace sapi {
|
|
|
|
namespace {
|
|
|
|
|
2019-03-19 00:21:48 +08:00
|
|
|
using ::sapi::IsOk;
|
|
|
|
using ::sapi::StatusIs;
|
2024-02-29 02:38:26 +08:00
|
|
|
using ::testing::ContainerEq;
|
2019-03-19 00:21:48 +08:00
|
|
|
using ::testing::Eq;
|
2023-08-30 17:20:25 +08:00
|
|
|
using ::testing::Gt;
|
2019-03-19 00:21:48 +08:00
|
|
|
using ::testing::HasSubstr;
|
2024-02-29 02:38:26 +08:00
|
|
|
using ::testing::NotNull;
|
2019-03-19 00:21:48 +08:00
|
|
|
|
|
|
|
// Functions that will be used during the benchmarks:
|
|
|
|
|
|
|
|
// Function causing no load in the sandboxee.
|
2020-02-28 01:23:44 +08:00
|
|
|
absl::Status InvokeNop(Sandbox* sandbox) {
|
2019-03-19 00:21:48 +08:00
|
|
|
StringopApi api(sandbox);
|
|
|
|
return api.nop();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Function that makes use of our special protobuf (de)-serialization code
|
|
|
|
// inside SAPI (including the back-synchronization of the structure).
|
2020-02-28 01:23:44 +08:00
|
|
|
absl::Status InvokeStringReversal(Sandbox* sandbox) {
|
2019-03-19 00:21:48 +08:00
|
|
|
StringopApi api(sandbox);
|
|
|
|
stringop::StringReverse proto;
|
|
|
|
proto.set_input("Hello");
|
2022-01-04 23:00:51 +08:00
|
|
|
absl::StatusOr<v::Proto<stringop::StringReverse>> pp(
|
|
|
|
v::Proto<stringop::StringReverse>::FromMessage(proto));
|
|
|
|
SAPI_RETURN_IF_ERROR(pp.status());
|
|
|
|
SAPI_ASSIGN_OR_RETURN(int return_code, api.pb_reverse_string(pp->PtrBoth()));
|
2019-03-19 00:21:48 +08:00
|
|
|
TRANSACTION_FAIL_IF_NOT(return_code != 0, "pb_reverse_string failed");
|
2022-01-04 23:00:51 +08:00
|
|
|
SAPI_ASSIGN_OR_RETURN(auto pb_result, pp->GetMessage());
|
2019-09-13 17:28:09 +08:00
|
|
|
TRANSACTION_FAIL_IF_NOT(pb_result.output() == "olleH", "Incorrect output");
|
2020-02-28 01:23:44 +08:00
|
|
|
return absl::OkStatus();
|
2019-03-19 00:21:48 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// Benchmark functions:
|
|
|
|
|
|
|
|
// Restart SAPI sandbox by letting the sandbox object go out of scope.
|
|
|
|
// Minimal case for measuring the minimum overhead of restarting the sandbox.
|
|
|
|
void BenchmarkSandboxRestartOverhead(benchmark::State& state) {
|
|
|
|
for (auto _ : state) {
|
2022-10-12 20:22:51 +08:00
|
|
|
BasicTransaction st(std::make_unique<StringopSandbox>());
|
2019-03-19 00:21:48 +08:00
|
|
|
// Invoke nop() to make sure that our sandbox is running.
|
|
|
|
EXPECT_THAT(st.Run(InvokeNop), IsOk());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
BENCHMARK(BenchmarkSandboxRestartOverhead);
|
|
|
|
|
|
|
|
void BenchmarkSandboxRestartForkserverOverhead(benchmark::State& state) {
|
2022-10-12 20:22:51 +08:00
|
|
|
sapi::BasicTransaction st(std::make_unique<StringopSandbox>());
|
2019-03-19 00:21:48 +08:00
|
|
|
for (auto _ : state) {
|
|
|
|
EXPECT_THAT(st.Run(InvokeNop), IsOk());
|
2020-02-18 21:27:08 +08:00
|
|
|
EXPECT_THAT(st.sandbox()->Restart(true), IsOk());
|
2019-03-19 00:21:48 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
BENCHMARK(BenchmarkSandboxRestartForkserverOverhead);
|
|
|
|
|
|
|
|
void BenchmarkSandboxRestartForkserverOverheadForced(benchmark::State& state) {
|
2022-10-12 20:22:51 +08:00
|
|
|
sapi::BasicTransaction st{std::make_unique<StringopSandbox>()};
|
2019-03-19 00:21:48 +08:00
|
|
|
for (auto _ : state) {
|
|
|
|
EXPECT_THAT(st.Run(InvokeNop), IsOk());
|
2020-02-18 21:27:08 +08:00
|
|
|
EXPECT_THAT(st.sandbox()->Restart(false), IsOk());
|
2019-03-19 00:21:48 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
BENCHMARK(BenchmarkSandboxRestartForkserverOverheadForced);
|
|
|
|
|
|
|
|
// Reuse the sandbox. Used to measure the overhead of the call invocation.
|
|
|
|
void BenchmarkCallOverhead(benchmark::State& state) {
|
2022-10-12 20:22:51 +08:00
|
|
|
BasicTransaction st(std::make_unique<StringopSandbox>());
|
2019-03-19 00:21:48 +08:00
|
|
|
for (auto _ : state) {
|
|
|
|
EXPECT_THAT(st.Run(InvokeNop), IsOk());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
BENCHMARK(BenchmarkCallOverhead);
|
|
|
|
|
|
|
|
// Make use of protobufs.
|
|
|
|
void BenchmarkProtobufHandling(benchmark::State& state) {
|
2022-10-12 20:22:51 +08:00
|
|
|
BasicTransaction st(std::make_unique<StringopSandbox>());
|
2019-03-19 00:21:48 +08:00
|
|
|
for (auto _ : state) {
|
|
|
|
EXPECT_THAT(st.Run(InvokeStringReversal), IsOk());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
BENCHMARK(BenchmarkProtobufHandling);
|
|
|
|
|
|
|
|
// Measure overhead of synchronizing data.
|
|
|
|
void BenchmarkIntDataSynchronization(benchmark::State& state) {
|
2022-10-12 20:22:51 +08:00
|
|
|
auto sandbox = std::make_unique<StringopSandbox>();
|
2019-03-19 00:21:48 +08:00
|
|
|
ASSERT_THAT(sandbox->Init(), IsOk());
|
|
|
|
|
|
|
|
long current_val = 0; // NOLINT
|
|
|
|
v::Long long_var;
|
|
|
|
// Allocate remote memory.
|
|
|
|
ASSERT_THAT(sandbox->Allocate(&long_var, false), IsOk());
|
|
|
|
|
|
|
|
for (auto _ : state) {
|
|
|
|
// Write current_val to the process.
|
|
|
|
long_var.SetValue(current_val);
|
|
|
|
EXPECT_THAT(sandbox->TransferToSandboxee(&long_var), IsOk());
|
|
|
|
// Invalidate value to make sure that the next call
|
|
|
|
// is not simply a noop.
|
|
|
|
long_var.SetValue(-1);
|
|
|
|
// Read value back.
|
|
|
|
EXPECT_THAT(sandbox->TransferFromSandboxee(&long_var), IsOk());
|
|
|
|
EXPECT_THAT(long_var.GetValue(), Eq(current_val));
|
|
|
|
|
|
|
|
current_val++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
BENCHMARK(BenchmarkIntDataSynchronization);
|
|
|
|
|
|
|
|
// Test whether stack trace generation works.
|
2021-02-01 23:10:43 +08:00
|
|
|
TEST(SapiTest, HasStackTraces) {
|
|
|
|
SKIP_SANITIZERS_AND_COVERAGE;
|
|
|
|
|
2022-10-12 20:22:51 +08:00
|
|
|
auto sandbox = std::make_unique<StringopSandbox>();
|
2019-03-19 00:21:48 +08:00
|
|
|
ASSERT_THAT(sandbox->Init(), IsOk());
|
|
|
|
StringopApi api(sandbox.get());
|
2020-02-28 01:23:44 +08:00
|
|
|
EXPECT_THAT(api.violate(), StatusIs(absl::StatusCode::kUnavailable));
|
2019-03-19 00:21:48 +08:00
|
|
|
const auto& result = sandbox->AwaitResult();
|
2021-05-06 22:35:41 +08:00
|
|
|
EXPECT_THAT(
|
|
|
|
result.GetStackTrace(),
|
|
|
|
// Check that at least one expected function is present in the stack
|
|
|
|
// trace.
|
|
|
|
// Note: Typically, in optimized builds, on x86-64, only
|
|
|
|
// "ViolateIndirect()" will be present in the stack trace. On POWER, all
|
|
|
|
// stack frames are generated, but libunwind will be unable to track
|
|
|
|
// "ViolateIndirect()" on the stack and instead show its IP as zero.
|
|
|
|
AnyOf(HasSubstr("ViolateIndirect"), HasSubstr("violate")));
|
2019-03-19 00:21:48 +08:00
|
|
|
EXPECT_THAT(result.final_status(), Eq(sandbox2::Result::VIOLATION));
|
|
|
|
}
|
|
|
|
|
|
|
|
// Various tests:
|
|
|
|
|
|
|
|
// Leaks a file descriptor inside the sandboxee.
|
2020-07-20 18:07:15 +08:00
|
|
|
int LeakFileDescriptor(sapi::Sandbox* sandbox, const char* path) {
|
2019-03-19 00:21:48 +08:00
|
|
|
int raw_fd = open(path, O_RDONLY);
|
|
|
|
sapi::v::Fd fd(raw_fd); // Takes ownership of the raw fd.
|
|
|
|
EXPECT_THAT(sandbox->TransferToSandboxee(&fd), IsOk());
|
|
|
|
// We want to leak the remote FD. The local FD will still be closed.
|
|
|
|
fd.OwnRemoteFd(false);
|
|
|
|
return fd.GetRemoteFd();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Make sure that restarting the sandboxee works (= fresh set of FDs).
|
|
|
|
TEST(SandboxTest, RestartSandboxFD) {
|
2022-10-12 20:22:51 +08:00
|
|
|
sapi::BasicTransaction st{std::make_unique<SumSandbox>()};
|
2019-03-19 00:21:48 +08:00
|
|
|
|
2020-02-28 01:23:44 +08:00
|
|
|
auto test_body = [](sapi::Sandbox* sandbox) -> absl::Status {
|
2019-03-19 00:21:48 +08:00
|
|
|
// Open some FDs and check their value.
|
2021-09-22 21:57:15 +08:00
|
|
|
int first_remote_fd = LeakFileDescriptor(sandbox, "/proc/self/exe");
|
|
|
|
EXPECT_THAT(LeakFileDescriptor(sandbox, "/proc/self/exe"),
|
|
|
|
Eq(first_remote_fd + 1));
|
2019-03-19 00:21:48 +08:00
|
|
|
SAPI_RETURN_IF_ERROR(sandbox->Restart(false));
|
|
|
|
// We should have a fresh sandbox now = FDs open previously should be
|
|
|
|
// closed now.
|
2021-09-22 21:57:15 +08:00
|
|
|
EXPECT_THAT(LeakFileDescriptor(sandbox, "/proc/self/exe"),
|
|
|
|
Eq(first_remote_fd));
|
2020-02-28 01:23:44 +08:00
|
|
|
return absl::OkStatus();
|
2019-03-19 00:21:48 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
EXPECT_THAT(st.Run(test_body), IsOk());
|
|
|
|
}
|
|
|
|
|
|
|
|
TEST(SandboxTest, RestartTransactionSandboxFD) {
|
2022-10-12 20:22:51 +08:00
|
|
|
sapi::BasicTransaction st{std::make_unique<SumSandbox>()};
|
2019-03-19 00:21:48 +08:00
|
|
|
|
2023-08-30 17:20:25 +08:00
|
|
|
int fd_no = -1;
|
|
|
|
ASSERT_THAT(st.Run([&fd_no](sapi::Sandbox* sandbox) -> absl::Status {
|
|
|
|
fd_no = LeakFileDescriptor(sandbox, "/proc/self/exe");
|
2020-02-28 01:23:44 +08:00
|
|
|
return absl::OkStatus();
|
2019-03-19 00:21:48 +08:00
|
|
|
}),
|
|
|
|
IsOk());
|
|
|
|
|
2023-08-30 17:20:25 +08:00
|
|
|
EXPECT_THAT(st.Run([fd_no](sapi::Sandbox* sandbox) -> absl::Status {
|
|
|
|
EXPECT_THAT(LeakFileDescriptor(sandbox, "/proc/self/exe"), Gt(fd_no));
|
2020-02-28 01:23:44 +08:00
|
|
|
return absl::OkStatus();
|
2019-03-19 00:21:48 +08:00
|
|
|
}),
|
|
|
|
IsOk());
|
|
|
|
|
|
|
|
EXPECT_THAT(st.Restart(), IsOk());
|
|
|
|
|
2023-08-30 17:20:25 +08:00
|
|
|
EXPECT_THAT(st.Run([fd_no](sapi::Sandbox* sandbox) -> absl::Status {
|
|
|
|
EXPECT_THAT(LeakFileDescriptor(sandbox, "/proc/self/exe"), Eq(fd_no));
|
2020-02-28 01:23:44 +08:00
|
|
|
return absl::OkStatus();
|
2019-03-19 00:21:48 +08:00
|
|
|
}),
|
|
|
|
IsOk());
|
|
|
|
}
|
|
|
|
|
|
|
|
// Make sure we can recover from a dying sandbox.
|
|
|
|
TEST(SandboxTest, RestartSandboxAfterCrash) {
|
2020-04-02 22:42:17 +08:00
|
|
|
SumSandbox sandbox;
|
|
|
|
ASSERT_THAT(sandbox.Init(), IsOk());
|
|
|
|
SumApi api(&sandbox);
|
|
|
|
|
|
|
|
// Crash the sandbox.
|
|
|
|
EXPECT_THAT(api.crash(), StatusIs(absl::StatusCode::kUnavailable));
|
|
|
|
EXPECT_THAT(api.sum(1, 2).status(), StatusIs(absl::StatusCode::kUnavailable));
|
|
|
|
EXPECT_THAT(sandbox.AwaitResult().final_status(),
|
|
|
|
Eq(sandbox2::Result::SIGNALED));
|
|
|
|
|
|
|
|
// Restart the sandbox.
|
|
|
|
ASSERT_THAT(sandbox.Restart(false), IsOk());
|
|
|
|
|
|
|
|
// The sandbox should now be responsive again.
|
|
|
|
SAPI_ASSERT_OK_AND_ASSIGN(int result, api.sum(1, 2));
|
|
|
|
EXPECT_THAT(result, Eq(3));
|
2019-03-19 00:21:48 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
TEST(SandboxTest, RestartSandboxAfterViolation) {
|
2020-04-02 22:42:17 +08:00
|
|
|
SumSandbox sandbox;
|
|
|
|
ASSERT_THAT(sandbox.Init(), IsOk());
|
|
|
|
SumApi api(&sandbox);
|
2019-03-19 00:21:48 +08:00
|
|
|
|
2020-04-02 22:42:17 +08:00
|
|
|
// Violate the sandbox policy.
|
|
|
|
EXPECT_THAT(api.violate(), StatusIs(absl::StatusCode::kUnavailable));
|
|
|
|
EXPECT_THAT(api.sum(1, 2).status(), StatusIs(absl::StatusCode::kUnavailable));
|
|
|
|
EXPECT_THAT(sandbox.AwaitResult().final_status(),
|
|
|
|
Eq(sandbox2::Result::VIOLATION));
|
2019-03-19 00:21:48 +08:00
|
|
|
|
2020-04-02 22:42:17 +08:00
|
|
|
// Restart the sandbox.
|
|
|
|
ASSERT_THAT(sandbox.Restart(false), IsOk());
|
|
|
|
|
|
|
|
// The sandbox should now be responsive again.
|
|
|
|
SAPI_ASSERT_OK_AND_ASSIGN(int result, api.sum(1, 2));
|
|
|
|
EXPECT_THAT(result, Eq(3));
|
2019-03-19 00:21:48 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
TEST(SandboxTest, NoRaceInAwaitResult) {
|
2020-04-02 22:42:17 +08:00
|
|
|
StringopSandbox sandbox;
|
|
|
|
ASSERT_THAT(sandbox.Init(), IsOk());
|
|
|
|
StringopApi api(&sandbox);
|
|
|
|
|
2020-02-28 01:23:44 +08:00
|
|
|
EXPECT_THAT(api.violate(), StatusIs(absl::StatusCode::kUnavailable));
|
2020-04-02 22:42:17 +08:00
|
|
|
absl::SleepFor(absl::Milliseconds(200)); // Make sure we lose the race
|
|
|
|
const auto& result = sandbox.AwaitResult();
|
2019-03-19 00:21:48 +08:00
|
|
|
EXPECT_THAT(result.final_status(), Eq(sandbox2::Result::VIOLATION));
|
|
|
|
}
|
|
|
|
|
2022-03-23 18:34:56 +08:00
|
|
|
TEST(SandboxTest, NoRaceInConcurrentTerminate) {
|
|
|
|
SumSandbox sandbox;
|
|
|
|
ASSERT_THAT(sandbox.Init(), IsOk());
|
|
|
|
SumApi api(&sandbox);
|
|
|
|
std::thread th([&sandbox] {
|
|
|
|
// Sleep so that the call already starts
|
|
|
|
absl::SleepFor(absl::Seconds(1));
|
|
|
|
sandbox.Terminate(/*attempt_graceful_exit=*/false);
|
|
|
|
});
|
|
|
|
EXPECT_THAT(api.sleep_for_sec(10), StatusIs(absl::StatusCode::kUnavailable));
|
|
|
|
th.join();
|
|
|
|
const auto& result = sandbox.AwaitResult();
|
|
|
|
EXPECT_THAT(result.final_status(), Eq(sandbox2::Result::EXTERNAL_KILL));
|
|
|
|
}
|
|
|
|
|
2023-12-14 16:47:34 +08:00
|
|
|
TEST(SandboxTest, UseUnotifyMonitor) {
|
|
|
|
SumSandbox sandbox;
|
|
|
|
ASSERT_THAT(sandbox.Init(/*use_unotify_monitor=*/true), IsOk());
|
|
|
|
SumApi api(&sandbox);
|
|
|
|
|
|
|
|
// Violate the sandbox policy.
|
|
|
|
EXPECT_THAT(api.violate(), StatusIs(absl::StatusCode::kUnavailable));
|
|
|
|
EXPECT_THAT(api.sum(1, 2).status(), StatusIs(absl::StatusCode::kUnavailable));
|
|
|
|
EXPECT_THAT(sandbox.AwaitResult().final_status(),
|
|
|
|
Eq(sandbox2::Result::VIOLATION));
|
|
|
|
|
|
|
|
// Restart the sandbox.
|
|
|
|
ASSERT_THAT(sandbox.Restart(false), IsOk());
|
|
|
|
|
|
|
|
// The sandbox should now be responsive again.
|
|
|
|
SAPI_ASSERT_OK_AND_ASSIGN(int result, api.sum(1, 2));
|
|
|
|
EXPECT_THAT(result, Eq(3));
|
|
|
|
}
|
|
|
|
|
2024-02-29 02:38:26 +08:00
|
|
|
TEST(SandboxTest, AllocateAndTransferTest) {
|
|
|
|
std::string test_string("This is a test");
|
|
|
|
std::vector<uint8_t> test_string_vector(test_string.begin(),
|
|
|
|
test_string.end());
|
|
|
|
|
|
|
|
absl::Span<uint8_t> buffer_input(
|
|
|
|
reinterpret_cast<uint8_t*>(test_string_vector.data()),
|
|
|
|
test_string_vector.size());
|
|
|
|
std::vector<uint8_t> buffer_output(test_string_vector.size());
|
|
|
|
|
|
|
|
SumSandbox sandbox;
|
|
|
|
ASSERT_THAT(sandbox.Init(), IsOk());
|
|
|
|
SumApi api(&sandbox);
|
|
|
|
|
|
|
|
SAPI_ASSERT_OK_AND_ASSIGN(
|
|
|
|
auto sapi_array, sandbox.AllocateAndTransferToSandboxee(buffer_input));
|
|
|
|
ASSERT_THAT(sapi_array, NotNull());
|
|
|
|
sapi::v::Array<const uint8_t> sapi_buffer_output(
|
|
|
|
reinterpret_cast<const uint8_t*>(buffer_output.data()),
|
|
|
|
buffer_output.size());
|
|
|
|
sapi_buffer_output.SetRemote(sapi_array->GetRemote());
|
|
|
|
ASSERT_THAT(sandbox.TransferFromSandboxee(&sapi_buffer_output), IsOk());
|
|
|
|
EXPECT_THAT(test_string_vector, ContainerEq(buffer_output));
|
|
|
|
}
|
|
|
|
|
2019-03-19 00:21:48 +08:00
|
|
|
} // namespace
|
|
|
|
} // namespace sapi
|