blob: d94b09197cce2b8423225e33170211e9aa847211 [file] [log] [blame]
// Copyright 2016 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 "android/metrics/FileMetricsWriter.h"
#include "android/base/Optional.h"
#include "android/base/StringView.h"
#include "android/base/testing/TestLooper.h"
#include "android/base/testing/TestSystem.h"
#include "android/base/testing/TestTempDir.h"
#include "android/base/Uuid.h"
#include "android/metrics/proto/clientanalytics.pb.h"
#include "android/metrics/proto/studio_stats.pb.h"
#include "android/protobuf/DelimitedSerialization.h"
#include "android/utils/path.h"
#include <google/protobuf/util/message_differencer.h>
#include <gtest/gtest.h>
#include <algorithm>
#include <array>
#include <numeric>
using namespace android::base;
using namespace android::metrics;
static constexpr StringView kSpoolDir = ".android/metrics/spool";
namespace {
class FileMetricsWriterTest : public ::testing::Test {
public:
TestSystem mSystem{"/", System::kProgramBitness, "/home"};
TestTempDir* mRoot = mSystem.getTempRoot();
std::unique_ptr<TestLooper> mLooper{new TestLooper()};
FileMetricsWriter::Ptr mWriter;
void SetUp() override {
ASSERT_TRUE(mRoot != nullptr);
ASSERT_TRUE(mLooper != nullptr);
mSystem.envSet("ANDROID_SDK_HOME", mRoot->pathString());
}
std::string spoolDir() { return mRoot->makeSubPath(kSpoolDir.c_str()); }
static std::string sessionId() { return Uuid::nullUuidStr; }
void makeSpoolDir() { path_mkdir_if_needed(spoolDir().c_str(), 0700); }
};
} // namespace
TEST_F(FileMetricsWriterTest, createDestroy) {
EXPECT_FALSE(mSystem.pathIsDir(spoolDir()));
mWriter = FileMetricsWriter::create(spoolDir(), sessionId(), 0, nullptr, 0);
EXPECT_TRUE(mWriter != nullptr);
EXPECT_TRUE(mSystem.pathIsDir(spoolDir()));
auto files = mSystem.scanDirEntries(spoolDir());
ASSERT_EQ(2, (int)files.size());
// one of the files is the open metrics file, another one is the lock.
EXPECT_TRUE(PathUtils::extension(files[0]) == ".lock" ||
PathUtils::extension(files[1]) == ".lock");
EXPECT_TRUE(PathUtils::extension(files[0]) == ".open" ||
PathUtils::extension(files[1]) == ".open");
mWriter.reset();
// on destruction it finalizes and unlocks the metrics file.
files = mSystem.scanDirEntries(spoolDir());
ASSERT_FALSE(files.empty());
ASSERT_EQ(1, (int)files.size());
EXPECT_TRUE(PathUtils::extension(files[0]) == ".trx");
}
TEST_F(FileMetricsWriterTest, createDestroyWithTimer) {
EXPECT_FALSE(mSystem.pathIsDir(spoolDir()));
mWriter = FileMetricsWriter::create(spoolDir(), sessionId(), 0,
mLooper.get(), 10);
EXPECT_TRUE(mWriter != nullptr);
EXPECT_TRUE(mSystem.pathIsDir(spoolDir()));
auto files = mSystem.scanDirEntries(spoolDir());
ASSERT_EQ(2, (int)files.size());
// make sure we have no timer, as it is only created on the first message
// write.
EXPECT_EQ(0, mLooper->timers().size());
// one of the files is the open metrics file, another one is the lock.
EXPECT_TRUE(PathUtils::extension(files[0]) == ".lock" ||
PathUtils::extension(files[1]) == ".lock");
EXPECT_TRUE(PathUtils::extension(files[0]) == ".open" ||
PathUtils::extension(files[1]) == ".open");
mWriter.reset();
// on destruction it finalizes and unlocks the metrics file.
files = mSystem.scanDirEntries(spoolDir());
ASSERT_FALSE(files.empty());
ASSERT_EQ(1, (int)files.size());
EXPECT_TRUE(PathUtils::extension(files[0]) == ".trx");
// we should still have no timers
EXPECT_EQ(0, mLooper->timers().size());
}
TEST_F(FileMetricsWriterTest, finalizeAbandonedSessionFilesNoDir) {
// no spool directory - no sessions
auto sessions =
FileMetricsWriter::finalizeAbandonedSessionFiles(spoolDir());
EXPECT_TRUE(sessions.empty());
}
TEST_F(FileMetricsWriterTest, finalizeAbandonedSessionFilesEmptyDir) {
makeSpoolDir();
// empty spool directory - no sessions
auto sessions =
FileMetricsWriter::finalizeAbandonedSessionFiles(spoolDir());
EXPECT_TRUE(sessions.empty());
}
TEST_F(FileMetricsWriterTest, finalizeAbandonedSessionFilesManyFiles) {
// create several session-like files now in the spool directory
makeSpoolDir();
std::vector<std::string> testSessions;
for (int i = 0; i < 10; ++i) {
testSessions.push_back(Uuid::generateFast().toString());
ASSERT_TRUE(mRoot->makeSubFile(PathUtils::join(
kSpoolDir, StringFormat("emulator-metrics-%s-1-1.open",
testSessions.back()))));
ASSERT_TRUE(mRoot->makeSubFile(PathUtils::join(
kSpoolDir, StringFormat("emulator-metrics-%s-2-1.open",
testSessions.back()))));
}
// now finalize them
auto sessions =
FileMetricsWriter::finalizeAbandonedSessionFiles(spoolDir());
// all test sessions should be present in |sessions|
EXPECT_EQ(sessions.size(), testSessions.size());
for (const auto& session : testSessions) {
EXPECT_NE(sessions.end(), sessions.find(session));
}
// make sure the open files are renamed and there are no other files
auto files = mSystem.scanDirEntries(spoolDir());
// there were twice as many files as sessions
EXPECT_EQ(2 * sessions.size(), files.size());
for (const auto& file : files) {
EXPECT_STREQ(PathUtils::extension(file).c_str(), ".trx");
// make sure all files have a known session ID in the name
EXPECT_NE(sessions.end(),
std::find_if(sessions.begin(), sessions.end(),
[&file](const std::string& session) {
return file.find(session) !=
std::string::npos;
}));
}
}
TEST_F(FileMetricsWriterTest, finalizeAbandonedSessionFilesWrongNames) {
makeSpoolDir();
// create some non-open-like files
ASSERT_TRUE(mRoot->makeSubFile(
PathUtils::join(kSpoolDir, "emulator-metrics.trx")));
ASSERT_TRUE(
mRoot->makeSubFile(PathUtils::join(kSpoolDir, "emulator-metrics")));
ASSERT_TRUE(mRoot->makeSubFile(
PathUtils::join(kSpoolDir, "emulator-metrics.open.closed")));
ASSERT_TRUE(mRoot->makeSubFile(
PathUtils::join(kSpoolDir, "emulator-metrics.lock")));
// no sessions
auto sessions =
FileMetricsWriter::finalizeAbandonedSessionFiles(spoolDir());
EXPECT_TRUE(sessions.empty());
}
TEST_F(FileMetricsWriterTest, finalizeAbandonedSessionFilesLockedSession) {
makeSpoolDir();
// create a locked open session file and make sure it is not finalized
auto openSessionName = PathUtils::join(
kSpoolDir,
StringFormat("emulator-metrics-%s-1-1.open", sessionId()));
ASSERT_TRUE(mRoot->makeSubFile(openSessionName));
{
// lock the file and try finalizing
auto scopedLock = makeCustomScopedPtr(
filelock_create(mRoot->makeSubPath(openSessionName).c_str()),
filelock_release);
ASSERT_TRUE(scopedLock);
auto sessions =
FileMetricsWriter::finalizeAbandonedSessionFiles(spoolDir());
// no sessions
EXPECT_TRUE(sessions.empty());
}
// check that now, when there's no lock, the only file left in the directory
// is the open session file
auto files = mSystem.scanDirEntries(spoolDir());
ASSERT_EQ(1, files.size());
EXPECT_TRUE(openSessionName.find(files[0]) != std::string::npos)
<< "Expected file name '" << files[0] << "' to be a file name from"
" session file path '"
<< openSessionName << '\'';
}
TEST_F(FileMetricsWriterTest, writeSimple) {
mWriter = FileMetricsWriter::create(spoolDir(), sessionId(), 0, nullptr, 0);
// create and write some event
wireless_android_play_playlog::LogEvent event;
event.set_is_user_initiated(true);
event.set_tag("tag");
event.set_source_extension("se");
mWriter->write(event);
mWriter.reset();
// read the event back.
auto files = mSystem.scanDirEntries(spoolDir(), true);
ASSERT_EQ(1, files.size());
EXPECT_STREQ(PathUtils::extension(files[0]).c_str(), ".trx");
std::ifstream in(files[0], std::ios_base::binary);
EXPECT_TRUE(in);
wireless_android_play_playlog::LogEvent readEvent;
google::protobuf::io::IstreamInputStream inStream(&in);
ASSERT_TRUE(android::protobuf::readOneDelimited(&readEvent, &inStream));
// make sure it's the same event.
ASSERT_TRUE(google::protobuf::util::MessageDifferencer::Equals(event,
readEvent));
// and that there are no more events
ASSERT_FALSE(android::protobuf::readOneDelimited(&readEvent, &inStream));
}
TEST_F(FileMetricsWriterTest, writeMultiple) {
mWriter = FileMetricsWriter::create(spoolDir(), sessionId(), 0, nullptr, 0);
// create and write some events
std::vector<wireless_android_play_playlog::LogEvent> events;
for (int i = 0; i < 100; ++i) {
wireless_android_play_playlog::LogEvent event;
event.set_is_user_initiated(true);
event.set_tag("tag");
event.set_source_extension("se");
event.set_event_code(i);
mWriter->write(event);
events.push_back(event);
}
mWriter.reset();
// read the event back.
auto files = mSystem.scanDirEntries(spoolDir(), true);
ASSERT_EQ(1, files.size());
EXPECT_STREQ(PathUtils::extension(files[0]).c_str(), ".trx");
std::ifstream in(files[0], std::ios_base::binary);
EXPECT_TRUE(in);
google::protobuf::io::IstreamInputStream inStream(&in);
for (const auto& event : events) {
wireless_android_play_playlog::LogEvent readEvent;
ASSERT_TRUE(android::protobuf::readOneDelimited(&readEvent, &inStream));
// make sure it's the same event.
ASSERT_TRUE(google::protobuf::util::MessageDifferencer::Equals(event,
readEvent));
}
// and that there are no more events
wireless_android_play_playlog::LogEvent readEvent;
ASSERT_FALSE(android::protobuf::readOneDelimited(&readEvent, &inStream));
}
TEST_F(FileMetricsWriterTest, writeLimited) {
mWriter = FileMetricsWriter::create(spoolDir(), sessionId(),
1, // records per file
nullptr, 0);
// create and write some event
std::array<wireless_android_play_playlog::LogEvent, 2> event;
event[0].set_is_user_initiated(true);
event[0].set_tag("tag");
event[0].set_source_extension("se");
mWriter->write(event[0]);
{
auto files = mSystem.scanDirEntries(spoolDir());
// make sure we have one open and one finalized file as of now
ASSERT_EQ(1, std::count_if(files.begin(), files.end(),
[](const std::string& name) {
return PathUtils::extension(name) ==
".open";
}));
ASSERT_EQ(1, std::count_if(files.begin(), files.end(),
[](const std::string& name) {
return PathUtils::extension(name) ==
".trx";
}));
}
event[1].set_event_uptime_ms(100);
event[1].add_test_code(100);
event[1].set_store("store");
mWriter->write(event[1]);
mWriter.reset();
// read the events back and make sure they're correct.
auto files = mSystem.scanDirEntries(spoolDir(), true);
ASSERT_EQ(3, files.size()) << std::accumulate(files.begin(), files.end(),
std::string());
// the last file is an empty one: it's the file created for the following
// event.
System::FileSize size;
ASSERT_TRUE(mSystem.pathFileSize(files.back(), &size));
ASSERT_EQ(0, size);
for (size_t i = 0; i < event.size(); ++i) {
EXPECT_STREQ(PathUtils::extension(files[i]).c_str(), ".trx");
std::ifstream in(files[i], std::ios_base::binary);
EXPECT_TRUE(in);
wireless_android_play_playlog::LogEvent readEvent;
google::protobuf::io::IstreamInputStream inStream(&in);
ASSERT_TRUE(android::protobuf::readOneDelimited(&readEvent, &inStream));
// make sure it's the same event.
ASSERT_TRUE(google::protobuf::util::MessageDifferencer::Equals(
event[i], readEvent));
// and that there are no more events
ASSERT_FALSE(
android::protobuf::readOneDelimited(&readEvent, &inStream));
}
}
TEST_F(FileMetricsWriterTest, writeTimered) {
mWriter = FileMetricsWriter::create(spoolDir(), sessionId(), 0,
mLooper.get(),
1000); // 1 sec per file.
EXPECT_EQ(0, mLooper->timers().size());
// create and write some event
wireless_android_play_playlog::LogEvent event;
event.set_is_user_initiated(true);
event.set_tag("tag");
event.set_source_extension("se");
mWriter->write(event);
EXPECT_EQ(1, mLooper->timers().size());
EXPECT_EQ(1, mLooper->activeTimers().size());
EXPECT_EQ(0, mLooper->pendingTimers().size());
{
auto files = mSystem.scanDirEntries(spoolDir());
// make sure we have one open and no finalized files as of now
ASSERT_EQ(1, std::count_if(files.begin(), files.end(),
[](const std::string& name) {
return PathUtils::extension(name) ==
".open";
}));
ASSERT_EQ(0, std::count_if(files.begin(), files.end(),
[](const std::string& name) {
return PathUtils::extension(name) ==
".trx";
}));
}
// now update the timestamp and run the looper to trigger the timer
mSystem.setUnixTimeUs(2000000);
mLooper->runOneIterationWithDeadlineMs(2000);
// there's still one active timer..
EXPECT_EQ(1, mLooper->timers().size());
EXPECT_EQ(1, mLooper->activeTimers().size());
EXPECT_EQ(0, mLooper->pendingTimers().size());
// now we should have one open and one finalized file as
auto files = mSystem.scanDirEntries(spoolDir(), true);
ASSERT_EQ(1, std::count_if(files.begin(), files.end(),
[](const std::string& name) {
return PathUtils::extension(name) ==
".open";
}));
ASSERT_EQ(1, std::count_if(files.begin(), files.end(),
[](const std::string& name) {
return PathUtils::extension(name) ==
".trx";
}));
EXPECT_STREQ(PathUtils::extension(files.front()).c_str(), ".trx");
// read the event back and make sure they're correct.
std::ifstream in(files.front(), std::ios_base::binary);
EXPECT_TRUE(in);
wireless_android_play_playlog::LogEvent readEvent;
google::protobuf::io::IstreamInputStream inStream(&in);
ASSERT_TRUE(android::protobuf::readOneDelimited(&readEvent, &inStream));
ASSERT_TRUE(google::protobuf::util::MessageDifferencer::Equals(
event, readEvent));
ASSERT_FALSE(
android::protobuf::readOneDelimited(&readEvent, &inStream));
mWriter.reset();
// no timers anymore
EXPECT_EQ(0, mLooper->timers().size());
EXPECT_EQ(0, mLooper->activeTimers().size());
EXPECT_EQ(0, mLooper->pendingTimers().size());
}