blob: 5a7fad5e8b8630510cc219bebecfcabc078c6503 [file] [log] [blame]
// Copyright 2015 The Android Open Source Project
//
// This software is licensed under the terms of the GNU General Public
// License version 2, as published by the Free Software Foundation, and
// may be copied, distributed, and modified under those terms.
//
// 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.
#include "android/base/files/IniFile.h"
#include "android/base/testing/TestTempDir.h"
#include <gtest/gtest.h>
#include <algorithm>
#include <fstream>
#include <limits>
#include <memory>
#include <string>
#include <unordered_map>
#include <vector>
namespace android {
namespace base {
using std::endl;
using std::numeric_limits;
using std::string;
using std::unique_ptr;
using std::unordered_map;
using std::vector;
namespace {
class IniFileTest : public ::testing::Test {
public:
void SetUp() override {
mTempDir.reset(new TestTempDir("inifiletest"));
mIniFilePath = mTempDir->makeSubPath("test.ini").c_str();
mIni.reset(new IniFile(mIniFilePath));
}
void TearDown() override {
mIni.reset();
mTempDir.reset();
}
protected:
void writeIniFileData(const vector<string>& lines) {
std::ofstream outFile(mIniFilePath,
std::ios_base::out | std::ios_base::trunc);
ASSERT_FALSE(outFile.fail());
for (const auto& line : lines) {
outFile << line << endl;
}
}
void verifyFileContents(const vector<string>& lines) {
std::ifstream inFile(mIniFilePath);
ASSERT_FALSE(inFile.fail());
vector<string> fileLines;
string line;
while (std::getline(inFile, line)) {
fileLines.push_back(line);
}
ASSERT_EQ(lines.size(), fileLines.size());
for (size_t i = 0; i < lines.size(); ++i) {
EXPECT_EQ(lines[i], fileLines[i]);
}
}
// Verifies that the underlying file for |ini| is |updated| when
// |writeIfChanged| is called.
// Note that this changes |ini|. After this function is called, |ini| is
// always
// clean, and the data has been changed arbitrarily.
void verifyFileUpdated(bool updated) {
static int counter = 0;
string key = "key" + std::to_string(counter++);
vector<string> lines = {key + " = somevalue"};
writeIniFileData(lines);
EXPECT_TRUE(mIni->writeIfChanged());
EXPECT_TRUE(mIni->read());
// If the file was updated, then the new uniquely written data must have
// disappeared, not otherwise.
if (updated) {
EXPECT_FALSE(mIni->hasKey(key));
} else {
EXPECT_TRUE(mIni->hasKey(key));
}
}
unique_ptr<TestTempDir> mTempDir;
string mIniFilePath;
unique_ptr<IniFile> mIni;
};
} // namespace
TEST_F(IniFileTest, readWrite) {
static const unordered_map<string, int> intData = {
{"zeroInt", 0},
{"positiveInt", 1},
{"negativeInt", -1},
{"maxInt", numeric_limits<int>::max()},
{"minInt", numeric_limits<int>::min()},
{"lowestInt", numeric_limits<int>::lowest()}};
static const unordered_map<string, int64_t> int64Data = {
{"zeroInt64", 0ULL},
{"positiveInt64", 1ULL},
{"negativeInt64", -1ULL},
{"maxInt64", numeric_limits<int64_t>::max()},
{"minInt64", numeric_limits<int64_t>::min()},
{"lowestInt64", numeric_limits<int64_t>::lowest()}};
static const unordered_map<string, double> doubleData = {
{"zeroDouble", 0.0},
{"positiveDouble", 1.5},
{"negativeDouble", -1.5},
{"maxDouble", numeric_limits<double>::max()},
// minDouble fails because of rounding errors.
// {"minDouble", numeric_limits<double>::min()},
{"lowestDouble", numeric_limits<double>::lowest()}};
// This doesn't actually test the format in which values are persisted.
// But it does test that serialize-deserialize are consistent.
static const unordered_map<string, bool> boolData = {{"trueKey", true},
{"falseKey", false}};
static const unordered_map<string, IniFile::DiskSize> diskSizeData = {
{"ds0", 0ULL},
{"ds1000B", 1000ULL},
{"ds1K", 1024ULL},
{"ds5k", 5 * 1024ULL},
{"ds1M", 1024 * 1024ULL},
{"ds3G", 3 * 1024 * 1024 * 1024ULL}};
for (const auto& keyval : intData) {
mIni->setInt(keyval.first, keyval.second);
}
for (const auto& keyval : int64Data) {
mIni->setInt64(keyval.first, keyval.second);
}
for (const auto& keyval : doubleData) {
mIni->setDouble(keyval.first, keyval.second);
}
for (const auto& keyval : boolData) {
mIni->setBool(keyval.first, keyval.second);
}
for (const auto& keyval : diskSizeData) {
mIni->setDiskSize(keyval.first, keyval.second);
}
ASSERT_TRUE(mIni->write());
mIni.reset(new IniFile(mIniFilePath));
ASSERT_EQ(0, mIni->size());
ASSERT_TRUE(mIni->read());
EXPECT_EQ(intData.size() + int64Data.size() + doubleData.size() +
boolData.size() + diskSizeData.size(),
mIni->size());
// Mix-up the order a bit.
for (const auto& keyval : boolData) {
EXPECT_TRUE(mIni->hasKey(keyval.first));
EXPECT_EQ(keyval.second, mIni->getBool(keyval.first, 99));
}
for (const auto& keyval : diskSizeData) {
EXPECT_TRUE(mIni->hasKey(keyval.first));
EXPECT_EQ(keyval.second, mIni->getDiskSize(keyval.first, 99));
}
for (const auto& keyval : int64Data) {
EXPECT_TRUE(mIni->hasKey(keyval.first));
EXPECT_EQ(keyval.second, mIni->getInt64(keyval.first, 99));
}
for (const auto& keyval : intData) {
EXPECT_TRUE(mIni->hasKey(keyval.first));
EXPECT_EQ(keyval.second, mIni->getInt(keyval.first, 99));
}
for (const auto& keyval : doubleData) {
EXPECT_TRUE(mIni->hasKey(keyval.first));
EXPECT_EQ(keyval.second, mIni->getDouble(keyval.first, 99));
}
}
TEST_F(IniFileTest, duplicateAndMisingKeys) {
mIni->setInt("int", 0);
mIni->setInt("int", 1);
mIni->setInt64("int64", 0ULL);
mIni->setInt64("int64", 1ULL);
mIni->setDouble("double", 0.0);
mIni->setDouble("double", 1.1);
mIni->setBool("bool", false);
mIni->setBool("bool", true);
mIni->setDiskSize("ds", 0ULL);
mIni->setDiskSize("ds", 1ULL);
ASSERT_TRUE(mIni->write());
mIni.reset(new IniFile(mIniFilePath));
ASSERT_EQ(0, mIni->size());
ASSERT_TRUE(mIni->read());
EXPECT_NE(0, mIni->size());
EXPECT_EQ(1, mIni->getInt("int", 99));
EXPECT_EQ(1ULL, mIni->getInt64("int64", 99));
EXPECT_EQ(1.1, mIni->getDouble("double", 99));
EXPECT_EQ(true, mIni->getBool("bool", false));
EXPECT_EQ(1ULL, mIni->getDiskSize("ds", 99ULL));
EXPECT_EQ(-11, mIni->getInt("missing", -11));
EXPECT_EQ(22ULL, mIni->getInt64("missing", 22ULL));
EXPECT_EQ(3.3, mIni->getDouble("missing", 3.3));
EXPECT_EQ(true, mIni->getBool("missing", true));
EXPECT_EQ(44ULL, mIni->getInt("missing", 44ULL));
}
TEST_F(IniFileTest, valueFormat) {
static const vector<string> fileData = {
"key1 = value with spaces", "key2 = value with trailing spaces ",
"key3 = \"value with redundant quotes\"", "keyAllSpaces = "};
writeIniFileData(fileData);
ASSERT_TRUE(mIni->read());
EXPECT_EQ(fileData.size(), mIni->size());
EXPECT_EQ("value with spaces", mIni->getString("key1", ""));
EXPECT_EQ("value with trailing spaces", mIni->getString("key2", ""));
EXPECT_EQ("\"value with redundant quotes\"", mIni->getString("key3", ""));
EXPECT_EQ("", mIni->getString("keyAllSpaces", "nonemptydefault"));
}
TEST_F(IniFileTest, readMalformedFile) {
static const int defaultInt = -99;
static const vector<string> fileData = {
"a = 5",
"; This comment will be skipped",
" b = 4",
" # So will this: irrelevant ; and #",
"c=43",
"d= 37malformedint,otherwiseOK",
"This is actually malformed, and will be skipped with warning",
" d = 45.6 now this becomes malformed here",
"d = 43 ; hanging comments are not supported.",
" ee = 546",
"f=\"56\"",
"f32ASDF_-.dfae3=1",
"90=KeyMustStartWithAlpha",
"a9%0=KeyCanNotContainPercent",
""};
static const unordered_map<string, int> validEntries = {
{"a", 5},
{"b", 4},
{"c", 43},
{"d", defaultInt},
{"ee", 546},
{"f", defaultInt},
{"f32ASDF_-.dfae3", 1}};
writeIniFileData(fileData);
ASSERT_TRUE(mIni->read());
EXPECT_EQ(validEntries.size(), mIni->size());
for (const auto& keyval : validEntries) {
EXPECT_TRUE(mIni->hasKey(keyval.first));
EXPECT_EQ(keyval.second, mIni->getInt(keyval.first, defaultInt));
}
}
static void formatToLines(vector<string>* lines,
const unordered_map<string, string>& dataMap) {
for (const auto& keyval : dataMap) {
lines->push_back(keyval.first + " = " + keyval.second);
}
}
TEST_F(IniFileTest, boolFormat) {
static const unordered_map<string, string> validTrues = {{"true1", "yes"},
{"true2", "YES"},
{"true3", "true"},
{"true4", "TRUE"},
{"true5", "1"}};
static const unordered_map<string, string> validFalses = {
{"false1", "no"},
{"false2", "NO"},
{"false3", "false"},
{"flase4", "FALSE"},
{"false5", "0"}};
static const unordered_map<string, string> invalidTrues = {
{"true12", "blah"}, {"true13", "\"1\""}};
static const unordered_map<string, string> invalidFalses = {
{"false12", "blah"}, {"false13", "\"0\""}};
vector<string> lines;
formatToLines(&lines, validTrues);
formatToLines(&lines, invalidTrues);
formatToLines(&lines, validFalses);
formatToLines(&lines, invalidFalses);
writeIniFileData(lines);
ASSERT_TRUE(mIni->read());
EXPECT_EQ(lines.size(), mIni->size());
for (const auto& keyval : validTrues) {
EXPECT_TRUE(mIni->hasKey(keyval.first));
EXPECT_TRUE(mIni->getBool(keyval.first, false));
}
for (const auto& keyval : validFalses) {
EXPECT_TRUE(mIni->hasKey(keyval.first));
EXPECT_FALSE(mIni->getBool(keyval.first, true));
}
for (const auto& keyval : invalidTrues) {
// The keyval exists, it's just not a valid bool value.
EXPECT_TRUE(mIni->hasKey(keyval.first));
EXPECT_FALSE(mIni->getBool(keyval.first, false));
}
for (const auto& keyval : invalidFalses) {
// The keyval exists, it's just not a valid bool value.
EXPECT_TRUE(mIni->hasKey(keyval.first));
EXPECT_TRUE(mIni->getBool(keyval.first, true));
}
}
TEST_F(IniFileTest, diskSizeFormat) {
static const unordered_map<string, string> validDiskSizes = {
{"ThirtyB", "30"}, {"OneKilo", "1k"}, {"FiveKilo", "5K"},
{"OneMega", "1m"}, {"FiveMega", "5M"}, {"OneGiga", "1g"},
{"FiveGiga", "5G"}};
static const unordered_map<string, string> invalidDiskSizes = {
{"WrongUnit", "30hertz"},
{"FractionalNumber", "2.14142135423"},
{"FractionalKilo", "3.14K"},
{"smiley_really", ";-)"}};
vector<string> lines;
formatToLines(&lines, validDiskSizes);
formatToLines(&lines, invalidDiskSizes);
writeIniFileData(lines);
ASSERT_TRUE(mIni->read());
EXPECT_EQ(lines.size(), mIni->size());
EXPECT_EQ(30ULL, mIni->getDiskSize("ThirtyB", 99));
EXPECT_EQ(1024ULL, mIni->getDiskSize("OneKilo", 99));
EXPECT_EQ(1024 * 1024ULL, mIni->getDiskSize("OneMega", 99));
EXPECT_EQ(1024 * 1024 * 1024ULL, mIni->getDiskSize("OneGiga", 99));
EXPECT_EQ(5 * 1024ULL, mIni->getDiskSize("FiveKilo", 99));
EXPECT_EQ(5 * 1024 * 1024ULL, mIni->getDiskSize("FiveMega", 99));
EXPECT_EQ(5 * 1024 * 1024 * 1024ULL, mIni->getDiskSize("FiveGiga", 99));
EXPECT_EQ(99L, mIni->getDiskSize("WrongUnit", 99L));
EXPECT_EQ(99L, mIni->getDiskSize("FractionalNumber", 99L));
EXPECT_EQ(99L, mIni->getDiskSize("FractionalKilo", 99L));
EXPECT_EQ(99L, mIni->getDiskSize("smiley_really", 99L));
}
TEST_F(IniFileTest, discardEmpty) {
mIni->setString("nonEmpty", "someValue");
mIni->setString("empty", "");
ASSERT_TRUE(mIni->write());
mIni.reset(new IniFile(mIniFilePath));
ASSERT_EQ(0, mIni->size());
ASSERT_TRUE(mIni->read());
EXPECT_EQ(2, mIni->size());
EXPECT_EQ("someValue", mIni->getString("nonEmpty", "defaultString"));
EXPECT_EQ("", mIni->getString("empty", "defaultString"));
EXPECT_TRUE(mIni->writeDiscardingEmpty());
mIni.reset(new IniFile(mIniFilePath));
ASSERT_EQ(0, mIni->size());
ASSERT_TRUE(mIni->read());
EXPECT_EQ(1, mIni->size());
EXPECT_EQ("someValue", mIni->getString("nonEmpty", "defaultString"));
EXPECT_EQ("defaultString", mIni->getString("empty", "defaultString"));
}
TEST_F(IniFileTest, writeIfChanged) {
// Initially, we must treat the object as dirty.
// Hence, we'll clear the lines we'd written previously.
mIni.reset(new IniFile(mIniFilePath));
verifyFileUpdated(true);
mIni->write();
// Now we consider the object as clean, so write shouldn't modify the
// underlying file.
verifyFileUpdated(false);
// Now let's change the data.
mIni->setString("random", "yippeeee");
verifyFileUpdated(true);
mIni->read();
// No changes, so write shouldn't do anything.
verifyFileUpdated(false);
// But resetting the file path means we should flush.
mIni->setBackingFile(mIniFilePath);
verifyFileUpdated(true);
}
TEST_F(IniFileTest, noBackingFile) {
mIni.reset(new IniFile());
ASSERT_FALSE(mIni->read());
mIni->setBackingFile(mIniFilePath);
ASSERT_FALSE(mIni->read());
ASSERT_TRUE(mIni->write());
ASSERT_TRUE(mIni->read());
}
TEST_F(IniFileTest, iterator) {
mIni->setString("firstKey", "firstValue");
mIni->setString("secondKey", "secondValue");
// Const iterators, also verify order.
const IniFile& cIni = *mIni;
vector<string> keys = {"firstKey", "secondKey"};
ASSERT_EQ(keys.size(), mIni->size());
size_t i = 0;
for (const auto& key : cIni) {
EXPECT_EQ(keys[i], key);
++i;
}
}
TEST_F(IniFileTest, diskFileOrder) {
vector<string> lines = {
"first = some valid value", "second line is invalid",
"; Third line is a comment", "fourth = is valid",
"# Fifth is also a comment", "",
";Sixth was a comment", "3p0 = is an invalid key",
"lets = finish with valid"};
vector<string> rewritten_lines = {"first = some valid value",
"; Third line is a comment",
"fourth = is valid",
"# Fifth is also a comment",
"",
";Sixth was a comment",
"lets = finish with valid"};
writeIniFileData(lines);
ASSERT_TRUE(mIni->read());
ASSERT_TRUE(mIni->write());
verifyFileContents(rewritten_lines);
// This is order indpendent. Let's try the reverse
std::reverse(std::begin(lines), std::end(lines));
std::reverse(std::begin(rewritten_lines), std::end(rewritten_lines));
writeIniFileData(lines);
ASSERT_TRUE(mIni->read());
ASSERT_TRUE(mIni->write());
verifyFileContents(rewritten_lines);
// Extra keys are appended to the file.
mIni->setString("extraKey", "extraValue");
ASSERT_TRUE(mIni->write());
rewritten_lines.push_back("extraKey = extraValue");
verifyFileContents(rewritten_lines);
// Test order of iteration in this complicated case.
// Remeber we reversed the lines.
vector<string> keys = {"lets", "fourth", "first", "extraKey"};
ASSERT_EQ(keys.size(), mIni->size());
size_t i = 0;
for (const auto& key : *mIni) {
EXPECT_EQ(keys[i], key);
++i;
}
}
TEST_F(IniFileTest, strDefaultValues) {
ASSERT_EQ(0, mIni->size());
EXPECT_TRUE(mIni->getBoolStr("missingKey", "yes"));
EXPECT_FALSE(mIni->getBoolStr("missingKey", "no"));
EXPECT_EQ(1024ULL, mIni->getDiskSizeStr("missingKey", "1k"));
}
TEST_F(IniFileTest, makeValidKey) {
EXPECT_STREQ("_key", IniFile::makeValidKey("key").c_str());
EXPECT_STREQ("_", IniFile::makeValidKey("").c_str());
EXPECT_STREQ("_.3D", IniFile::makeValidKey("=").c_str());
EXPECT_STREQ("_a.20sign.20.23", IniFile::makeValidKey("a sign #").c_str());
EXPECT_STREQ("_some.20number.2010.20within",
IniFile::makeValidKey("some number 10 within").c_str());
}
} // namespace base
} // namespace android