//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// In addition, as a special exception, the copyright holders give
// permission to link the code of portions of this program with the
// OpenSSL library under certain conditions as described in each
// individual source file, and distribute linked combinations including
// the two.
//
// You must obey the GNU General Public License in all respects for all
// of the code used other than OpenSSL. If you modify file(s) with this
// exception, you may extend this exception to your version of the
// file(s), but you are not obligated to do so. If you do not wish to do
// so, delete this exception statement from your version. If you delete
// this exception statement from all source files in the program, then
// also delete it here.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
// moonfire-db-test.cc: tests of the moonfire-db.h interface.
#include
#include
#include
#include
#include
#include "moonfire-db.h"
#include "sqlite.h"
#include "string.h"
#include "testutil.h"
DECLARE_bool(alsologtostderr);
using testing::_;
using testing::HasSubstr;
using testing::DoAll;
using testing::Return;
using testing::SetArgPointee;
namespace moonfire_nvr {
namespace {
class MoonfireDbTest : public testing::Test {
protected:
MoonfireDbTest() {
tmpdir_ = PrepareTempDirOrDie("moonfire-db-test");
std::string error_message;
CHECK(db_.Open(StrCat(tmpdir_, "/db").c_str(),
SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, &error_message))
<< error_message;
std::string create_sql = ReadFileOrDie("../src/schema.sql");
DatabaseContext ctx(&db_);
CHECK(RunStatements(&ctx, create_sql, &error_message)) << error_message;
}
int64_t AddCamera(Uuid uuid, re2::StringPiece short_name) {
DatabaseContext ctx(&db_);
auto run = ctx.UseOnce(
R"(
insert into camera (uuid, short_name, host, username, password,
main_rtsp_path, sub_rtsp_path, retain_bytes)
values (:uuid, :short_name, :host, :username, :password,
:main_rtsp_path, :sub_rtsp_path, :retain_bytes);
)");
run.BindBlob(":uuid", uuid.binary_view());
run.BindText(":short_name", short_name);
run.BindText(":host", "test-camera");
run.BindText(":username", "foo");
run.BindText(":password", "bar");
run.BindText(":main_rtsp_path", "/main");
run.BindText(":sub_rtsp_path", "/sub");
run.BindInt64(":retain_bytes", 42);
CHECK_EQ(SQLITE_DONE, run.Step()) << run.error_message();
if (run.Step() != SQLITE_DONE) {
ADD_FAILURE() << run.error_message();
return -1;
}
return ctx.last_insert_rowid();
}
void ExpectNoRecordings(Uuid camera_uuid) {
int rows = 0;
mdb_->ListCameras([&](const ListCamerasRow &row) {
++rows;
EXPECT_EQ(camera_uuid, row.uuid);
EXPECT_EQ("test-camera", row.host);
EXPECT_EQ("foo", row.username);
EXPECT_EQ("bar", row.password);
EXPECT_EQ("/main", row.main_rtsp_path);
EXPECT_EQ("/sub", row.sub_rtsp_path);
EXPECT_EQ(42, row.retain_bytes);
EXPECT_EQ(-1, row.min_start_time_90k);
EXPECT_EQ(-1, row.max_end_time_90k);
EXPECT_EQ(0, row.total_duration_90k);
EXPECT_EQ(0, row.total_sample_file_bytes);
return IterationControl::kContinue;
});
EXPECT_EQ(1, rows);
std::string error_message;
rows = 0;
EXPECT_TRUE(mdb_->ListCameraRecordings(
camera_uuid, 0, std::numeric_limits::max(),
[&](const ListCameraRecordingsRow &row) {
++rows;
return IterationControl::kBreak;
},
&error_message))
<< error_message;
EXPECT_EQ(0, rows);
rows = 0;
EXPECT_TRUE(mdb_->ListMp4Recordings(
camera_uuid, 0, std::numeric_limits::max(),
[&](Recording &recording, const VideoSampleEntry &entry) {
++rows;
return IterationControl::kBreak;
},
&error_message))
<< error_message;
EXPECT_EQ(0, rows);
}
void ExpectSingleRecording(Uuid camera_uuid, const Recording &recording,
const VideoSampleEntry &entry,
ListOldestSampleFilesRow *save_oldest_row) {
std::string error_message;
int rows = 0;
mdb_->ListCameras([&](const ListCamerasRow &row) {
++rows;
EXPECT_EQ(camera_uuid, row.uuid);
EXPECT_EQ(recording.start_time_90k, row.min_start_time_90k);
EXPECT_EQ(recording.end_time_90k, row.max_end_time_90k);
EXPECT_EQ(recording.end_time_90k - recording.start_time_90k,
row.total_duration_90k);
EXPECT_EQ(recording.sample_file_bytes, row.total_sample_file_bytes);
return IterationControl::kContinue;
});
EXPECT_EQ(1, rows);
GetCameraRow camera_row;
EXPECT_TRUE(mdb_->GetCamera(camera_uuid, &camera_row));
EXPECT_EQ(recording.start_time_90k, camera_row.min_start_time_90k);
EXPECT_EQ(recording.end_time_90k, camera_row.max_end_time_90k);
EXPECT_EQ(recording.end_time_90k - recording.start_time_90k,
camera_row.total_duration_90k);
EXPECT_EQ(recording.sample_file_bytes, camera_row.total_sample_file_bytes);
rows = 0;
EXPECT_TRUE(mdb_->ListCameraRecordings(
camera_uuid, 0, std::numeric_limits::max(),
[&](const ListCameraRecordingsRow &row) {
++rows;
EXPECT_EQ(recording.start_time_90k, row.start_time_90k);
EXPECT_EQ(recording.end_time_90k, row.end_time_90k);
EXPECT_EQ(recording.video_samples, row.video_samples);
EXPECT_EQ(recording.sample_file_bytes, row.sample_file_bytes);
EXPECT_EQ(entry.sha1, row.video_sample_entry_sha1);
EXPECT_EQ(entry.width, row.width);
EXPECT_EQ(entry.height, row.height);
return IterationControl::kContinue;
},
&error_message))
<< error_message;
EXPECT_EQ(1, rows);
rows = 0;
EXPECT_TRUE(mdb_->ListOldestSampleFiles(
camera_uuid,
[&](const ListOldestSampleFilesRow &row) {
++rows;
EXPECT_EQ(recording.id, row.recording_id);
EXPECT_EQ(recording.sample_file_uuid, row.sample_file_uuid);
EXPECT_EQ(recording.end_time_90k - recording.start_time_90k,
row.duration_90k);
EXPECT_EQ(recording.sample_file_bytes, row.sample_file_bytes);
*save_oldest_row = row;
return IterationControl::kContinue;
},
&error_message))
<< error_message;
EXPECT_EQ(1, rows);
rows = 0;
EXPECT_TRUE(mdb_->ListMp4Recordings(
camera_uuid, 0, std::numeric_limits::max(),
[&](Recording &some_recording, const VideoSampleEntry &some_entry) {
++rows;
EXPECT_EQ(recording.id, some_recording.id);
EXPECT_EQ(recording.camera_id, some_recording.camera_id);
EXPECT_EQ(recording.sample_file_sha1,
some_recording.sample_file_sha1);
EXPECT_EQ(recording.sample_file_uuid,
some_recording.sample_file_uuid);
EXPECT_EQ(recording.video_sample_entry_id,
some_recording.video_sample_entry_id);
EXPECT_EQ(recording.start_time_90k, some_recording.start_time_90k);
EXPECT_EQ(recording.end_time_90k, some_recording.end_time_90k);
EXPECT_EQ(recording.sample_file_bytes,
some_recording.sample_file_bytes);
EXPECT_EQ(recording.video_samples, some_recording.video_samples);
EXPECT_EQ(recording.video_sync_samples,
some_recording.video_sync_samples);
EXPECT_EQ(recording.video_index, some_recording.video_index);
EXPECT_EQ(entry.id, some_entry.id);
EXPECT_EQ(entry.sha1, some_entry.sha1);
EXPECT_EQ(entry.data, some_entry.data);
EXPECT_EQ(entry.width, some_entry.width);
EXPECT_EQ(entry.height, some_entry.height);
return IterationControl::kContinue;
},
&error_message))
<< error_message;
EXPECT_EQ(1, rows);
}
std::string tmpdir_;
Database db_;
std::unique_ptr mdb_;
};
TEST(AdjustDaysMapTest, Basic) {
std::map days;
// Create a day.
const int64_t kTestTime = INT64_C(130647162600000); // 2015-12-31 23:59:00
moonfire_nvr::internal::AdjustDaysMap(
kTestTime, kTestTime + 60 * kTimeUnitsPerSecond, 1, &days);
EXPECT_THAT(days, testing::ElementsAre(std::make_pair(
"2015-12-31", 60 * kTimeUnitsPerSecond)));
// Add to a day.
moonfire_nvr::internal::AdjustDaysMap(
kTestTime, kTestTime + 60 * kTimeUnitsPerSecond, 1, &days);
EXPECT_THAT(days, testing::ElementsAre(std::make_pair(
"2015-12-31", 120 * kTimeUnitsPerSecond)));
// Subtract from a day.
moonfire_nvr::internal::AdjustDaysMap(
kTestTime, kTestTime + 60 * kTimeUnitsPerSecond, -1, &days);
EXPECT_THAT(days, testing::ElementsAre(std::make_pair(
"2015-12-31", 60 * kTimeUnitsPerSecond)));
// Remove a day.
moonfire_nvr::internal::AdjustDaysMap(
kTestTime, kTestTime + 60 * kTimeUnitsPerSecond, -1, &days);
EXPECT_THAT(days, testing::ElementsAre());
// Create two days.
moonfire_nvr::internal::AdjustDaysMap(
kTestTime, kTestTime + 3 * 60 * kTimeUnitsPerSecond, 1, &days);
EXPECT_THAT(days,
testing::ElementsAre(
std::make_pair("2015-12-31", 1 * 60 * kTimeUnitsPerSecond),
std::make_pair("2016-01-01", 2 * 60 * kTimeUnitsPerSecond)));
// Add to two days.
moonfire_nvr::internal::AdjustDaysMap(
kTestTime, kTestTime + 3 * 60 * kTimeUnitsPerSecond, 1, &days);
EXPECT_THAT(days,
testing::ElementsAre(
std::make_pair("2015-12-31", 2 * 60 * kTimeUnitsPerSecond),
std::make_pair("2016-01-01", 4 * 60 * kTimeUnitsPerSecond)));
// Subtract from two days.
moonfire_nvr::internal::AdjustDaysMap(
kTestTime, kTestTime + 3 * 60 * kTimeUnitsPerSecond, -1, &days);
EXPECT_THAT(days,
testing::ElementsAre(
std::make_pair("2015-12-31", 1 * 60 * kTimeUnitsPerSecond),
std::make_pair("2016-01-01", 2 * 60 * kTimeUnitsPerSecond)));
// Remove two days.
moonfire_nvr::internal::AdjustDaysMap(
kTestTime, kTestTime + 3 * 60 * kTimeUnitsPerSecond, -1, &days);
EXPECT_THAT(days, testing::ElementsAre());
}
TEST(GetDayBoundsTest, Basic) {
int64_t start_90k;
int64_t end_90k;
std::string error_msg;
// Normal day.
EXPECT_TRUE(GetDayBounds("2015-12-31", &start_90k, &end_90k, &error_msg))
<< error_msg;
EXPECT_EQ(INT64_C(130639392000000), start_90k);
EXPECT_EQ(INT64_C(130647168000000), end_90k);
// Spring forward (23-hour day).
EXPECT_TRUE(GetDayBounds("2016-03-13", &start_90k, &end_90k, &error_msg));
EXPECT_EQ(INT64_C(131207040000000), start_90k);
EXPECT_EQ(INT64_C(131214492000000), end_90k);
// Fall back (25-hour day).
EXPECT_TRUE(GetDayBounds("2016-11-06", &start_90k, &end_90k, &error_msg));
EXPECT_EQ(INT64_C(133057404000000), start_90k);
EXPECT_EQ(INT64_C(133065504000000), end_90k);
// Unparseable day.
EXPECT_FALSE(GetDayBounds("xxxx-xx-xx", &start_90k, &end_90k, &error_msg));
}
// Basic test of running some queries on an empty database.
TEST_F(MoonfireDbTest, EmptyDatabase) {
std::string error_message;
mdb_.reset(new MoonfireDatabase);
ASSERT_TRUE(mdb_->Init(&db_, &error_message)) << error_message;
mdb_->ListCameras([&](const ListCamerasRow &row) {
ADD_FAILURE() << "row unexpected";
return IterationControl::kBreak;
});
GetCameraRow get_camera_row;
EXPECT_FALSE(mdb_->GetCamera(Uuid(), &get_camera_row));
EXPECT_FALSE(
mdb_->ListCameraRecordings(Uuid(), 0, std::numeric_limits::max(),
[&](const ListCameraRecordingsRow &row) {
ADD_FAILURE() << "row unexpected";
return IterationControl::kBreak;
},
&error_message));
EXPECT_FALSE(mdb_->ListMp4Recordings(
Uuid(), 0, std::numeric_limits::max(),
[&](Recording &recording, const VideoSampleEntry &entry) {
ADD_FAILURE() << "row unexpected";
return IterationControl::kBreak;
},
&error_message));
}
// Basic test of the full lifecycle of recording.
// Does not exercise many error cases.
TEST_F(MoonfireDbTest, FullLifecycle) {
std::string error_message;
const char kCameraShortName[] = "testcam";
Uuid camera_uuid = GetRealUuidGenerator()->Generate();
int64_t camera_id = AddCamera(camera_uuid, kCameraShortName);
ASSERT_GT(camera_id, 0);
mdb_.reset(new MoonfireDatabase);
ASSERT_TRUE(mdb_->Init(&db_, &error_message)) << error_message;
ExpectNoRecordings(camera_uuid);
std::vector reserved;
EXPECT_TRUE(mdb_->ListReservedSampleFiles(&reserved, &error_message))
<< error_message;
EXPECT_THAT(reserved, testing::IsEmpty());
std::vector uuids = mdb_->ReserveSampleFiles(2, &error_message);
ASSERT_THAT(uuids, testing::SizeIs(2)) << error_message;
EXPECT_TRUE(mdb_->ListReservedSampleFiles(&reserved, &error_message))
<< error_message;
EXPECT_THAT(reserved, testing::UnorderedElementsAre(uuids[0], uuids[1]));
VideoSampleEntry entry;
entry.sha1.resize(20);
entry.width = 768;
entry.height = 512;
entry.data.resize(100);
ASSERT_TRUE(mdb_->InsertVideoSampleEntry(&entry, &error_message))
<< error_message;
ASSERT_GT(entry.id, 0);
Recording recording;
recording.camera_id = camera_id;
recording.sample_file_uuid = GetRealUuidGenerator()->Generate();
recording.video_sample_entry_id = entry.id;
SampleIndexEncoder encoder;
encoder.Init(&recording, UINT64_C(1430006400) * kTimeUnitsPerSecond);
encoder.AddSample(kTimeUnitsPerSecond, 42, true);
// Inserting a recording should succeed and remove its uuid from the
// reserved table.
ASSERT_FALSE(mdb_->InsertRecording(&recording, &error_message));
EXPECT_THAT(error_message, testing::HasSubstr("not reserved"));
recording.sample_file_uuid = uuids.back();
recording.sample_file_sha1.resize(20);
ASSERT_TRUE(mdb_->InsertRecording(&recording, &error_message))
<< error_message;
ASSERT_GT(recording.id, 0);
EXPECT_TRUE(mdb_->ListReservedSampleFiles(&reserved, &error_message))
<< error_message;
EXPECT_THAT(reserved, testing::ElementsAre(uuids[0]));
// Queries should return the correct result (with caches updated on insert).
ListOldestSampleFilesRow oldest;
ExpectSingleRecording(camera_uuid, recording, entry, &oldest);
// Queries on a fresh database should return the correct result (with caches
// populated from existing database contents).
mdb_.reset(new MoonfireDatabase);
ASSERT_TRUE(mdb_->Init(&db_, &error_message)) << error_message;
ExpectSingleRecording(camera_uuid, recording, entry, &oldest);
// Deleting a recording should succeed, update the min/max times, and mark
// the uuid as reserved.
std::vector to_delete;
to_delete.push_back(oldest);
ASSERT_TRUE(mdb_->DeleteRecordings(to_delete, &error_message))
<< error_message;
EXPECT_TRUE(mdb_->ListReservedSampleFiles(&reserved, &error_message))
<< error_message;
EXPECT_THAT(reserved, testing::UnorderedElementsAre(uuids[0], uuids[1]));
ExpectNoRecordings(camera_uuid);
EXPECT_TRUE(mdb_->MarkSampleFilesDeleted(uuids, &error_message))
<< error_message;
EXPECT_TRUE(mdb_->ListReservedSampleFiles(&reserved, &error_message))
<< error_message;
EXPECT_THAT(reserved, testing::IsEmpty());
}
} // namespace
} // namespace moonfire_nvr
int main(int argc, char **argv) {
FLAGS_alsologtostderr = true;
google::ParseCommandLineFlags(&argc, &argv, true);
testing::InitGoogleTest(&argc, argv);
google::InitGoogleLogging(argv[0]);
// The calendar day math assumes this timezone.
CHECK_EQ(0, setenv("TZ", "America/Los_Angeles", 1)) << strerror(errno);
tzset();
return RUN_ALL_TESTS();
}