blob: 5b43784eb1056a7cbb1e5bd4fc75cf1ecdde9664 [file] [log] [blame]
// Copyright 2019 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <lib/async-loop/cpp/loop.h>
#include <lib/async-loop/default.h>
#include <stream_provider.h>
#include <algorithm>
#include <iostream>
#include <random>
#include <vector>
#include <gtest/gtest.h>
#include <openssl/sha.h>
#include <src/lib/syslog/cpp/logger.h>
#include <trace-provider/provider.h>
#include <trace/event.h>
static uint32_t g_gtest_iteration = 0;
class SampledHasher {
public:
explicit SampledHasher(size_t image_size) : sample_indices_(kSampleBytes) {
std::mt19937 gen(0);
std::uniform_int_distribution<uint32_t> dist(0, image_size - 1);
for (auto& index : sample_indices_) {
index = dist(gen);
}
std::sort(sample_indices_.begin(), sample_indices_.end());
}
// Returns the formatted sha512 string of randomly sampled bytes of an image.
std::string Hash(const void* data) {
auto bytes = reinterpret_cast<const uint8_t*>(data);
std::vector<uint8_t> sample_data(kSampleBytes);
auto it = sample_data.begin();
for (auto index : sample_indices_) {
*it++ = bytes[index];
}
constexpr char table[] = "0123456789abcdef";
uint8_t md[SHA512_DIGEST_LENGTH]{};
SHA512(sample_data.data(), sample_data.size(), md);
std::string ret(2 * sizeof(md), 0);
for (uint32_t i = 0; i < SHA512_DIGEST_LENGTH; ++i) {
ret[2 * i] = table[(md[i] >> 4) & 0xF];
ret[2 * i + 1] = table[md[i] & 0xF];
}
return ret;
}
private:
static constexpr auto kSampleBytes = 16384;
std::vector<uint32_t> sample_indices_;
};
class StreamProviderTest : public testing::TestWithParam<StreamProvider::Source> {
protected:
StreamProviderTest()
: loop_(&kAsyncLoopConfigAttachToCurrentThread), trace_provider_(loop_.dispatcher()) {}
~StreamProviderTest() override {
if (GetParam() == StreamProvider::Source::MANAGER) {
++g_gtest_iteration;
}
}
virtual void SetUp() override {
if (g_gtest_iteration > 0) {
constexpr char kRepeatErrorMessage[] =
"This test relies on run_test_component tearing down the parent environment between "
"runs, which does not occur when using gtest_repeat. Run the test multiple times "
"manually instead.";
FX_LOGS(ERROR) << kRepeatErrorMessage;
std::cerr << kRepeatErrorMessage << std::endl;
std::cerr.flush();
std::cout.flush();
exit(EXIT_FAILURE);
}
auto source = GetParam();
provider_ = StreamProvider::Create(source);
ASSERT_NE(provider_, nullptr);
RunLoopUntilIdle();
}
virtual void TearDown() override {
provider_ = nullptr;
RunLoopUntilIdle();
}
void RunLoopUntilIdle() { ASSERT_EQ(loop_.RunUntilIdle(), ZX_OK); }
async::Loop loop_;
std::unique_ptr<StreamProvider> provider_;
trace::TraceProviderWithFdio trace_provider_;
};
// Read and validate frames from each provider type.
TEST_P(StreamProviderTest, ValidateFrames) {
// Pick something large enough that it's likely larger than any internal ring buffers, but small
// enough that the test completes relatively quickly.
constexpr auto kFramesToCheck = 42u;
// Connect to the stream.
fuchsia::camera2::StreamPtr stream;
auto result = provider_->ConnectToStream(stream.NewRequest());
ASSERT_TRUE(result.is_ok());
auto [format, buffers, should_rotate] = result.take_value();
const auto& buffer_size = buffers.settings.buffer_settings.size_bytes;
SampledHasher hasher(buffer_size);
bool stream_alive = true;
stream.set_error_handler([&](zx_status_t status) {
FX_PLOGS(ERROR, status) << provider_->GetName() << " disconnected";
ADD_FAILURE();
stream_alive = false;
});
// Sanity check some of the returned values.
ASSERT_GT(buffers.buffer_count, 0u);
ASSERT_NE(format.coded_width, 0u);
ASSERT_NE(format.coded_height, 0u);
ASSERT_GE(format.bytes_per_row, format.coded_width);
// Populate a set of known hashes to constant-value frame data. The provider should nver return
// frames matching these.
std::map<std::string, uint8_t> known_hashes;
{
// Try known transients 0x00 and 0xFF, as well as other likely transients near values k*2^N.
constexpr std::array kValuesToCheck{0x00, 0xFF, 0x01, 0xFE, 0x7F, 0x80, 0x3F, 0x40, 0xBF, 0xC0};
std::vector<uint8_t> known_frame(buffer_size);
for (size_t i = 0; i < kValuesToCheck.size(); ++i) {
auto value = kValuesToCheck[i];
std::cout << "\rCalculating hash for fixed value " << static_cast<uint32_t>(value) << " ("
<< i + 1 << "/" << kValuesToCheck.size() << ")";
std::cout.flush();
memset(known_frame.data(), value, known_frame.size());
known_hashes[hasher.Hash(known_frame.data())] = value;
}
std::cout << std::endl;
}
// Register a frame event handler.
std::map<std::string, uint32_t> frame_hashes;
std::vector<bool> buffer_owned(buffers.buffer_count, false);
uint32_t frames_received = 0;
stream.events().OnFrameAvailable = [&, buffers =
&buffers](fuchsia::camera2::FrameAvailableInfo info) {
ASSERT_EQ(info.frame_status, fuchsia::camera2::FrameStatus::OK);
ASSERT_LT(info.buffer_id, buffers->buffer_count);
TRACE_DURATION_BEGIN("camera", "FrameHeld", info.buffer_id);
if (++frames_received > kFramesToCheck) {
stream->ReleaseFrame(info.buffer_id);
TRACE_DURATION_END("camera", "FrameHeld", info.buffer_id);
return;
}
// Check ownership validity of the buffer.
ASSERT_LT(info.buffer_id, buffers->buffer_count);
EXPECT_FALSE(buffer_owned[info.buffer_id])
<< "Server sent frame " << info.buffer_id << " again without the client releasing it.";
buffer_owned[info.buffer_id] = true;
// Map and hash the entire contents of the buffer.
uintptr_t mapped_addr = 0;
ASSERT_EQ(zx::vmar::root_self()->map(0, buffers->buffers[info.buffer_id].vmo, 0, buffer_size,
ZX_VM_PERM_READ, &mapped_addr),
ZX_OK);
std::cout << "\rCalculating hash for frame " << frames_received << "/" << kFramesToCheck;
std::cout.flush();
auto hash = hasher.Hash(reinterpret_cast<void*>(mapped_addr));
ASSERT_EQ(zx::vmar::root_self()->unmap(mapped_addr, buffer_size), ZX_OK);
// Verify the hash does not match a prior or known hash. Even with a static scene, thermal
// noise should prevent any perfectly identical frames. As a result, this check should only
// fail if the frames are not actually coming from the sensor, or are being recycled
// incorrectly.
auto it = known_hashes.find(hash);
if (it != known_hashes.end()) {
// Frame hash matches a known constant-value hash, indicating the buffer was not correctly
// populated.
ADD_FAILURE_AT(__FILE__, __LINE__)
<< "Frame " << frames_received
<< " does not contain valid image data - it is just the constant byte value "
<< static_cast<uint32_t>(it->second);
} else {
auto it = frame_hashes.find(hash);
if (it == frame_hashes.end()) {
frame_hashes.emplace(hash, frames_received);
} else {
// Frame hash matches a prior frame's hash, indicating buffers are being recycled
// incorrectly.
ADD_FAILURE_AT(__FILE__, __LINE__)
<< "Duplicate frame - the contents of frames " << it->second << " and "
<< frames_received << " both hash to 0x" << hash;
}
}
buffer_owned[info.buffer_id] = false;
stream->ReleaseFrame(info.buffer_id);
TRACE_DURATION_END("camera", "FrameHeld", info.buffer_id);
};
stream->Start();
while (stream_alive && frames_received < kFramesToCheck) {
RunLoopUntilIdle();
}
std::cout << std::endl;
ASSERT_TRUE(stream_alive);
stream->Stop();
RunLoopUntilIdle();
}
static std::string ParamToString(testing::TestParamInfo<StreamProvider::Source> param) {
switch (param.param) {
case StreamProvider::Source::ISP:
return "ISP";
case StreamProvider::Source::CONTROLLER:
return "CONTROLLER";
case StreamProvider::Source::MANAGER:
return "MANAGER";
default:
return "UNKNOWN";
}
}
INSTANTIATE_TEST_SUITE_P(StreamProviderTestSuite, StreamProviderTest,
testing::Values(StreamProvider::Source::ISP,
StreamProvider::Source::CONTROLLER,
StreamProvider::Source::MANAGER),
ParamToString);