database: Design and test updates

I add:
- Autosaving option
- Real iterators
- Better accessor functions

The new design and unit test also allows me to remove the 300,000+ line
"database.good" file that the old tests were based on.

Signed-off-by: Anna Schumaker <schumaker.anna@gmail.com>
This commit is contained in:
Anna Schumaker 2014-03-09 14:58:06 -04:00 committed by Anna Schumaker
parent b251f27bb5
commit fc8dc9d55e
13 changed files with 573 additions and 300728 deletions

258
DESIGN
View File

@ -115,7 +115,7 @@ On-disk files:
}
- File:
class File : std::fstream {
class File : public std::fstream {
private:
unsigned int version;
OpenMode mode;
@ -207,8 +207,7 @@ Database Entry:
virtual void write(File &) = 0;
virtual void read(File &) = 0;
virtual void print() = 0;
}
};
- API:
DatabaseEntry :: DatabaseEntry():
@ -227,33 +226,138 @@ Database Entry:
void DatabaseEntry :: read(File &);
This function is called to read a DatabaseEntry from a file.
void DatabaseEntry :: print();
This function will only exist if CONFIG_TEST is enabled and is
called to print a DatabaseEntry to console.
Database: (lib/database.cpp)
Ocarina 5.x created a different save file format for each type of
data that needed to be stored (preferences, library paths, playlists).
I intend to unify everything into a generic file format that can be
accessed through a generic database interface. The database code will
be in charge of printing the "valid" bit for each DatabaseEntry so that
child classes do not need to call into the parent class. If valid ==
true, the DatabaseEntry will be streamed out followed by a newline. If
valid == false the database will print the next entry in the vector.
Modules should inherit from the DatabasEntry class and implement their
own read() and write() functions. The "valid" field will be stored
before these functions are called, and the entry will be skipped if
valid is set to false.
Database:
Databases are a generic store for information used by Ocarina. Users
need to inherit from a DatabaseEntry class (above) to properly use a
database.
The Database class is a templated class, so code could potentially
get messy. Normal class declarations can still exist in the file
include/database.h and member functions can be written in the file
include/database.h and member functions are written in the file
include/database.hpp, which will be included by database.h. Any
function not relying on a template can be written in lib/database.cpp.
- Automatic saving
Databases can save automatically whenever a new value is inserted or
deleted. This will be more efficient for Databases that do not change
often but will be a huge performance hit for Databases that have many
changes at once. All databases will be loaded automatically from disk.
- Primary keys
Databases use the primary_key() function of a DatabaseEntry to enforce
uniqueness. This key is used when inserting a new value into the
Database, and will not be updated after.
- Valid bit
The "valid" bit of a DatabaseEntry is completely managed by the entry's
Database container. It will be set to true when an entry is inserted
and false when deleted. The Database is also in charge of writing the
valid bit to file.
- Database:
template <class T>
class Database {
private:
std::vector<T> _db;
std::map<std::string, unsigned int> _keys;
unsigned int _size; /* Number of valid rows */
bool _autosave;
File _file;
public:
typedef std::vector<T>::iterator iterator;
typedef std::vector<T>::const_iterator const_iterator;
Database(std::string, bool);
void save();
void load();
unsigned int insert(T);
void remove(unsigned int);
unsigned int size();
unsigned int actual_size();
iterator begin();
iterator end();
iterator next(iterator &);
iterator at(unsigned int);
iterator find(const std::string &);
};
- File format:
File << db.size() << endl
File << INDEX_0 << db[INDEX_0].valid << db[INDEX_0] << endl;
File << INDEX_1 << db[INDEX_1].valid << db[INDEX_1] << endl;
...
- API:
Database :: Database(std::string filepath, bool autosave);
Initialize a database using "filepath" as a location to store
data on disk. If the file already exists, read the data into
the backing vector.
void Database :: save();
Save the database to disk.
void Database :: load();
Load the database from disk.
unsigned int Database :: insert(T &item);
Look up the item in the _keys map.
If we find an item with the same key:
- Return the index of the item to the caller.
Otherwise:
- Add the new item to the end of the _db.
- Add the new item to _keys.
- Set item.valid = true.
_ Increment _size.
- If autosave == true: save().
- Return the index of the new item.
unsigned int Database :: remove();
- Remove the item from the _keys map.
- Set item.valid = false.
- If autosave == true: save().
- Decrement _size.
unsigned int Database :: size();
return _size;
unsigned int Database :: actual_size();
return _db.size();
iterator Database :: begin();
Return _db.end() if there are no valid entries
If the first entry is valid:
- return _db.begin();
Otherwise:
- return an iterator to the first valid entry.
iterator Database :: end();
return _db.end();
iterator Database :: next(iterator &cur);
Return the next DatabaseEntry with valid == true or _db.end()
if there are no valid entries left.
iterator Database :: at(unsigned int i);
If _db[i].valid == true:
Return an iterator pointing to _db[i];
Otherwise:
Return _db.end();
iterator Database :: find(const std::string &key);
If key is in the _keys map:
Return an iterator pointing to the corresponding entry.
Otherwise:
Return _db.end();
- IndexEntry:
class IndexEntry : public DatabaseEntry {
public:
@ -269,116 +373,6 @@ Database: (lib/database.cpp)
File << key << endl;
File << values.size() << values[0] << .. << values[N] << endl;
- Database:
template <class T>
class Database {
private:
vector<T> db;
map<std::string, unsigned int> keys;
unsigned int _size; /* Number of valid rows */
File file;
public:
Database::Database(filename, flags);
void load();
void save();
void clear();
void print();
void print_keys();
unsigned int insert(T);
void remove(unsigned int);
unsigned int size();
unsigned int num_rows();
unsigned int first();
unsigned int last();
unsigned int next(unsigned int &);
bool has_key(const std :: string &);
unsigned int find_index(const std::string &);
T &find(const std::string &);
T &operator[](unsigned int);
};
File << db.size() << endl
File << INDEX_0 << db[INDEX_0].valid << db[INDEX_0] << endl;
File << INDEX_1 << db[INDEX_1].valid << db[INDEX_1] << endl;
...
- API:
Database :: Database(filename, flags);
Initializes database to use ~/.ocarina{-debug}/filename.
Set up flags.
void Database :: load();
Reads a saved database from disk.
void Database :: save();
Saves the database to disk.
void Database :: clear();
This function exists only if CONFIG_TEST is enabled.
Clear the database contents in-memory, but do NOT write
to disk.
void Database :: print();
This function exists only If CONFIG_TEST is enabled.
Following a similar format for writing to disk, print the
database to the console in a human-readable format.
void Database :: print_keys();
This function exists only if CONFIG_TEST is enabled.
Print out the collected primary keys in the database.
template <class T>
unsigned int Database :: insert(T &);
Adds a new item to the db and marks it as valid. A reverse
mapping is also created into the keys map to map the primary
key back to the id of the newly added item.
void Database :: remove(unsigned int index);
Mark db[index] as invalid (quick deletion).
unsigned int Database :: size();
Returns number of valid rows in the database.
unsigned int Database :: num_rows();
Return db.size().
unsigned int Database :: first();
Return the id to the first valid row. Return db.size() if
there are no valid rows.
unsigned int Database :: last();
Return the id of the last valid row. Return db.size() if
there are no valid rows.
unsigned int Database :: next(unsigned int &id);
Return the id of the next valid row or return db.size() if
there are no valid rows.
bool Database :: has_key(const std::string &key);
Return true if an item with primary key "key" exists in the
database, and false otherwise.
unsigned int Database :: find_index(const std::string key);
If the key exists in the database, return the index of the
database item with that key. Throw -EEXIST if the key is not
in the database.
template <class T>
T &Database :: find(const std::string &key);
Search for primary key "key" in the database. The reverse
mapping should be used to make this operation faster.
Throw -EEXIST if the key is not found in the mapping.
Throw -EINVAL if the key points to an invalid entry.
template <class T>
T &Database :: operator[](unsigned int index);
Return a reference to db[index].
Throw -EEXIST if index is out of range.
Throw -EINVAL if index is invalid.
Filter: (lib/filter.cpp)

View File

@ -20,9 +20,6 @@ public:
virtual std::string primary_key() = 0;
virtual void write(File &) = 0;
virtual void read(File &) = 0;
#ifdef CONFIG_TEST
virtual void print() = 0;
#endif /* CONFIG_TEST */
};
@ -48,55 +45,57 @@ public:
template <class T>
class Database {
private:
std::vector<T> db;
std::map<const std::string, unsigned int> keys;
std::vector<T> _db;
std::map<const std::string, unsigned int> _keys;
unsigned int _size;
File file;
bool _autosave;
File _file;
void autosave();
public:
Database(std::string);
typedef typename std::vector<T>::iterator iterator;
typedef typename std::vector<T>::const_iterator const_iterator;
Database(std::string, bool);
~Database();
void save();
void load();
#ifdef CONFIG_TEST
void clear();
void print();
void print_keys();
#endif /* CONFIG_TEST */
unsigned int insert(T);
void remove(unsigned int);
unsigned int size();
unsigned int num_rows();
unsigned int actual_size();
unsigned int first();
unsigned int last();
unsigned int next(unsigned int);
bool has_key(const std::string &);
unsigned int find_index(const std::string &);
T &find(const std::string &);
T &operator[](unsigned int);
iterator begin();
iterator end();
iterator next(iterator &);
iterator at(unsigned int);
iterator find(const std::string &);
};
static inline void index_insert(Database<IndexEntry> &db,
const std::string &key,
unsigned int val)
{
try {
db.find(key).insert(val);
} catch (int error) {
Database<IndexEntry>::iterator it = db.find(key);
if (it == db.end()) {
db.insert(IndexEntry(key, val));
}
} else
it->insert(val);
}
static inline void index_remove(Database<IndexEntry> &db,
const std::string &key,
unsigned int val)
{
unsigned int i = db.find_index(key);
db[i].remove(val);
if (db[i].values.size() == 0)
db.remove(db.find_index(key));
Database<IndexEntry>::iterator it = db.find(key);
if (it != db.end()) {
it->remove(val);
if (it->values.size() == 0)
db.remove(it - db.begin());
}
}
#include "database.hpp"

View File

@ -10,8 +10,8 @@
#include <error.h>
template <class T>
Database<T> :: Database(std::string filepath)
: _size(0), file(filepath, FILE_TYPE_DATA)
Database<T> :: Database(std::string filepath, bool autosave)
: _size(0), _autosave(autosave), _file(filepath, FILE_TYPE_DATA)
{
}
@ -23,21 +23,25 @@ Database<T> :: ~Database()
template <class T>
void Database<T> :: save()
{
try {
file.open(OPEN_WRITE);
} catch (int error) {
if (_file.open(OPEN_WRITE) == false)
return;
_file << _db.size() << std::endl;
for (unsigned int i = 0; i < _db.size(); i++) {
_file << _db[i].valid << " ";
if (_db[i].valid == true)
_db[i].write(_file);
_file << std::endl;
}
file << db.size() << std::endl;
for (unsigned int i = 0; i < db.size(); i++) {
file << db[i].valid << " ";
if (db[i].valid == true)
db[i].write(file);
file << std::endl;
}
_file.close();
}
file.close();
template <class T>
void Database<T> :: autosave()
{
if (_autosave == true)
save();
}
template <class T>
@ -45,90 +49,55 @@ void Database<T> :: load()
{
unsigned int db_size;
if (file.exists() == false)
if (_file.exists() == false)
return;
else if (_file.open(OPEN_READ) == false)
return;
try {
file.open(OPEN_READ);
} catch (int error) {
return;
}
file >> db_size;
db.resize(db_size);
_file >> db_size;
_db.resize(db_size);
for (unsigned int i = 0; i < db_size; i++) {
file >> db[i].valid;
if (db[i].valid == true) {
db[i].read(file);
keys.insert(std::pair<std::string, unsigned int>(db[i].primary_key(), i));
_file >> _db[i].valid;
if (_db[i].valid == true) {
_db[i].read(_file);
_keys.insert(std::pair<std::string, unsigned int>(_db[i].primary_key(), i));
_size++;
}
}
file.close();
_file.close();
}
#ifdef CONFIG_TEST
template <class T>
void Database<T> :: clear()
{
db.clear();
keys.clear();
_size = 0;
}
template <class T>
void Database<T> :: print()
{
:: print("Allocated rows: %u\n", db.size());
:: print("Valid rows: %u\n", _size);
for (unsigned int i = 0; i < db.size(); i++) {
if (db[i].valid == true) {
:: print("db[%u] = ", i);
db[i].print();
:: print("\n");
}
}
}
template <class T>
void Database<T> :: print_keys()
{
std::map<const std::string, unsigned int>::iterator it;
:: print("Found keys:");
for (it = keys.begin(); it != keys.end(); it++)
:: print(" %s", it->first.c_str());
:: print("\n");
}
#endif /* CONFIG_TEST */
template <class T>
unsigned int Database<T> :: insert(T val)
{
unsigned int id;
typename std::map<const std::string, unsigned int>::iterator it;
iterator it = find(val.primary_key());
it = keys.find(val.primary_key());
if (it != keys.end())
return it->second;
if (it != end())
return it - _db.begin();
/*
* Check primary key stuff here
*/
id = db.size();
db.push_back(val);
keys.insert(std::pair<const std::string, unsigned int>(val.primary_key(), id));
db[id].valid = true;
id = _db.size();
_db.push_back(val);
_keys[val.primary_key()] = id;
_db[id].valid = true;
_size++;
autosave();
return id;
}
template <class T>
void Database<T> :: remove(unsigned int id)
{
keys.erase(db[id].primary_key());
db[id].valid = false;
if (id >= actual_size())
return;
_keys.erase(_db[id].primary_key());
_db[id].valid = false;
_size--;
autosave();
}
template <class T>
@ -138,82 +107,53 @@ unsigned int Database<T> :: size()
}
template <class T>
unsigned int Database<T> :: num_rows()
unsigned int Database<T> :: actual_size()
{
return db.size();
return _db.size();
}
template <class T>
unsigned int Database<T> :: first()
typename Database<T>::iterator Database<T> :: begin()
{
for (unsigned int i = 0; i < db.size(); i++) {
if (db[i].valid == true)
return i;
}
return db.size();
iterator it = _db.begin();
if ( (*it).valid == true )
return it;
return next(it);
}
template <class T>
unsigned int Database<T> :: last()
typename Database<T>::iterator Database<T> :: end()
{
if (_size == 0)
return db.size();
for (unsigned int i = db.size() - 1; i >= 0; i--) {
if (db[i].valid == true)
return i;
}
return db.size();
return _db.end();
}
template <class T>
unsigned int Database<T> :: next(unsigned int id)
typename Database<T>::iterator Database<T> :: next(iterator &it)
{
for (unsigned int i = id + 1; i < db.size(); i++) {
if (db[i].valid == true)
return i;
}
return db.size();
do {
it++;
} while ((it != end()) && ((*it).valid == false));
return it;
}
template <class T>
bool Database<T> :: has_key(const std::string &key)
typename Database<T>::iterator Database<T> :: at(unsigned int id)
{
if (id >= actual_size())
return end();
if (_db[id].valid == false)
return end();
return _db.begin() + id;
}
template <class T>
typename Database<T>::iterator Database<T> :: find(const std::string &key)
{
std::map<const std::string, unsigned int>::iterator it;
it = keys.find(key);
return it != keys.end();
it = _keys.find(key);
if (it == _keys.end())
return end();
return _db.begin() + it->second;
}
template <class T>
unsigned int Database<T> :: find_index(const std::string &key)
{
std::map<const std::string, unsigned int>::iterator it;
it = keys.find(key);
if (it == keys.end())
throw -E_EXIST;
return it->second;
}
template <class T>
T &Database<T> :: find(const std::string &key)
{
unsigned int index = find_index(key);
if (db[index].valid == false)
throw -E_INVAL;
return db[index];
}
template <class T>
T &Database<T> :: operator[](unsigned int id)
{
if (id >= db.size())
throw -E_EXIST;
else if (db[id].valid == false)
throw -E_INVAL;
return db[id];
}
#endif /* OCARINA_DATABASE_HPP */

View File

@ -36,9 +36,6 @@ namespace library
std::string primary_key();
void read(File &);
void write(File &);
#ifdef CONFIG_TEST
void print();
#endif /* CONFIG_TEST */
};
@ -55,9 +52,6 @@ namespace library
std::string primary_key();
void read(File &);
void write(File &);
#ifdef CONFIG_TEST
void print();
#endif /* CONFIG_TEST */
};
@ -72,9 +66,6 @@ namespace library
std::string primary_key();
void read(File &);
void write(File &);
#ifdef CONFIG_TEST
void print();
#endif /* CONFIG_TEST */
};
@ -107,9 +98,6 @@ namespace library
std::string primary_key();
void read(File &);
void write(File &);
#ifdef CONFIG_TEST
void print();
#endif /* CONFIG_TEST */
};

View File

@ -10,7 +10,7 @@
#include <map>
#include <set>
static Database<IndexEntry> filter_index("");
static Database<IndexEntry> filter_index("", false);
static std::map<std::string, std::string> lowercase_cache;
static unsigned int lowercase_cache_hits = 0;
@ -107,10 +107,10 @@ void filter :: add(const std::string &text, unsigned int track_id)
static void find_intersection(std::string &text, std::set<unsigned int> &res)
{
IndexEntry *terms = &filter_index.find(text);
Database<IndexEntry>::iterator it = filter_index.find(text);
std::set<unsigned int> tmp;
set_intersection(terms->values.begin(), terms->values.end(),
set_intersection(it->values.begin(), it->values.end(),
res.begin(), res.end(),
std::inserter<std::set<unsigned int> >(tmp, tmp.begin()));
res.swap(tmp);
@ -127,7 +127,7 @@ void filter :: search(const std::string &text, std::set<unsigned int> &res)
it = parsed.begin();
try {
res = filter_index.find(*it).values;
res = filter_index.find(*it)->values;
} catch (...) {
return;
}

View File

@ -11,11 +11,11 @@
#include <sstream>
#include <time.h>
static Database<library :: Album> album_db("album.db");
static Database<library :: AGInfo> artist_db("artist.db");
static Database<library :: AGInfo> genre_db("genre.db");
static Database<library :: Track> track_db("track.db");
static Database<library :: Library> library_db("library.db");
static Database<library :: Album> album_db("album.db", false);
static Database<library :: AGInfo> artist_db("artist.db", false);
static Database<library :: AGInfo> genre_db("genre.db", false);
static Database<library :: Track> track_db("track.db", false);
static Database<library :: Library> library_db("library.db", false);
struct ImportData {
std::string filepath;
@ -76,16 +76,6 @@ void library :: AGInfo :: write(File &f)
f << name;
}
#ifdef CONFIG_TEST
void library :: AGInfo :: print()
{
if (db_type == DB_ARTIST)
:: print("Artist: %s", name.c_str());
else
:: print("Genre: %s", name.c_str());
}
#endif /* CONFIG_TEST */
/*
@ -129,13 +119,6 @@ void library :: Album :: write(File &f)
f << artist_id << " " << year << " " << name;
}
#ifdef CONFIG_TEST
void library :: Album :: print()
{
:: print("Album: %s (%u) by %s", name.c_str(), year, artist_db[artist_id].name.c_str());
}
#endif /* CONFIG_TEST */
/*
@ -169,18 +152,6 @@ void library :: Library :: write(File &f)
f << enabled << " " << root_path;
}
#ifdef CONFIG_TEST
void library :: Library :: print()
{
:: print("%s", root_path.c_str());
if (enabled == true)
:: print(" (enabled)");
else
:: print(" (disabled)");
:: print(", size = %u", size);
}
#endif /* CONFIG_TEST */
/*
@ -204,7 +175,7 @@ library :: Track :: Track(TagLib :: Tag *tag, TagLib :: AudioProperties *audio,
unsigned int minutes, seconds;
full_path = path;
filepath = path.substr(library_db[library_id].root_path.size() + 1);
filepath = path.substr(library_db.at(library_id)->root_path.size() + 1);
title_lower = filter :: to_lowercase(title);
minutes = length / 60;
@ -228,7 +199,7 @@ library :: Track :: Track(struct ImportData *data, unsigned int lib,
unsigned int minutes, seconds;
full_path = data->filepath;
filepath = full_path.substr(library_db[library_id].root_path.size() + 1);
filepath = full_path.substr(library_db.at(library_id)->root_path.size() + 1);
title_lower = filter :: to_lowercase(title);
minutes = length / 60;
@ -254,8 +225,8 @@ void library :: Track :: read(File &f)
title = f.getline();
filepath = f.getline();
title_lower = filter :: to_lowercase(title);
full_path = library_db[library_id].root_path + "/" + filepath;
library_db[library_id].size++;
full_path = library_db.at(library_id)->root_path + "/" + filepath;
library_db.at(library_id)->size++;
}
void library :: Track :: write(File &f)
@ -267,20 +238,6 @@ void library :: Track :: write(File &f)
f << filepath;
}
#ifdef CONFIG_TEST
void library :: Track :: print()
{
:: print("%u. %s by %s from %s (%u)\n", track, title.c_str(),
artist_db[artist_id].name.c_str(),
album_db[album_id].name.c_str(), album_db[album_id].year);
:: print(" Genre: %s, Length: %u (seconds)\n",
genre_db[genre_id].name.c_str(), length);
:: print(" Play count: %u, last played %u/%u/%u\n", play_count,
last_day, last_month, last_year);
:: print(" %s", filepath.c_str());
}
#endif /* CONFIG_TEST */
/*
@ -313,11 +270,11 @@ static void read_tags(unsigned int lib_id, const std :: string &path)
genre_id = genre_db.insert(library :: AGInfo(library :: DB_GENRE, tag));
track_id = track_db.insert(library :: Track(tag, audio, lib_id,
artist_id, album_id, genre_id, path));
library_db[lib_id].size++;
library_db.at(lib_id)->size++;
filter::add(artist_db[artist_id].name, track_id);
filter::add(album_db[album_id].name, track_id);
filter::add(track_db[track_id].title, track_id);
filter::add(artist_db.at(artist_id)->name, track_id);
filter::add(album_db.at(album_id)->name, track_id);
filter::add(track_db.at(track_id)->title, track_id);
get_callbacks()->on_library_track_add(track_id);
}
@ -333,7 +290,7 @@ static bool process_path(unsigned int lib_id, const std :: string &dir,
scan.path = path;
idle :: schedule (do_scan_path, scan);
} else {
if (track_db.has_key(path) == false) {
if (track_db.find(path) != track_db.end()) {
read_tags(lib_id, path);
changed = true;
}
@ -370,7 +327,7 @@ static void do_scan_path(struct scan_info &scan)
if (changed == true) {
save_all_dbs();
get_callbacks()->on_library_update(scan.lib_id,
&library_db[scan.lib_id]);
&(*library_db.at(scan.lib_id)));
}
}
@ -382,26 +339,27 @@ static void do_validate_library(unsigned int &lib_id)
if (track_db.size() == 0)
return;
for (unsigned int i = track_db.first(); i <= track_db.last(); i = track_db.next(i)) {
if (track_db[i].library_id != lib_id)
Database<library :: Track>::iterator it;
for (it = track_db.begin(); it != track_db.end(); it = track_db.next(it)) {
if ((*it).library_id != lib_id)
continue;
path = library_db[lib_id].root_path + "/" + track_db[i].filepath;
path = library_db.at(lib_id)->root_path + "/" + (*it).filepath;
if (g_file_test(path.c_str(), G_FILE_TEST_EXISTS) == false) {
dprint("Removing file: %s\n", path.c_str());
track_db.remove(i);
library_db[lib_id].size--;
track_db.remove(it - track_db.begin());
library_db.at(lib_id)->size--;
changed = true;
}
}
if (changed == true)
get_callbacks()->on_library_update(lib_id, &library_db[lib_id]);
get_callbacks()->on_library_update(lib_id, &(*library_db.at(lib_id)));
}
static void do_update_library(unsigned int lib_id)
{
struct scan_info scan = { lib_id, library_db[lib_id].root_path };
struct scan_info scan = { lib_id, library_db.at(lib_id)->root_path };
idle :: schedule(do_validate_library, lib_id);
idle :: schedule(do_scan_path, scan);
}
@ -430,11 +388,11 @@ static void do_import_track(File &f, unsigned int lib_id)
genre_id = genre_db.insert(library :: AGInfo(library :: DB_GENRE, genre));
track_id = track_db.insert(library :: Track(&data, lib_id, artist_id,
album_id, genre_id));
library_db[lib_id].size++;
library_db.at(lib_id)->size++;
filter::add(artist_db[artist_id].name, track_id);
filter::add(album_db[album_id].name, track_id);
filter::add(track_db[track_id].title, track_id);
filter::add(artist_db.at(artist_id)->name, track_id);
filter::add(album_db.at(album_id)->name, track_id);
filter::add(track_db.at(track_id)->title, track_id);
get_callbacks()->on_library_track_add(track_id);
if (banned == true)
@ -460,20 +418,20 @@ static void do_import_library(std::string &s)
f >> id >> enabled >> next_id >> size;
/* Assign this path a new id */
if (library_db.has_key(path)) {
if (library_db.find(path) != library_db.end()) {
print("Library already contains path: %s, skipping\n", path.c_str());
return;
}
print("Adding path: %s\n", path.c_str());
id = library_db.insert(library :: Library(path, enabled));
get_callbacks()->on_library_add(id, &library_db[id]);
get_callbacks()->on_library_add(id, &(*library_db.at(id)));
library_db.save();
f.getline(); /* Get rest of line */
for (unsigned int i = 0; i < size; i++)
do_import_track(f, id);
save_all_dbs();
get_callbacks()->on_library_update(id, &library_db[id]);
get_callbacks()->on_library_update(id, &(*library_db.at(id)));
library :: update_path(id);
}
@ -494,16 +452,22 @@ void library :: init()
library_db.load();
track_db.load();
for (i = track_db.first(); i < track_db.num_rows(); i = track_db.next(i)) {
filter::add(artist_db[track_db[i].artist_id].name, i);
filter::add(album_db[track_db[i].album_id].name, i);
filter::add(track_db[i].title, i);
Database<Track>::iterator it;
for (it = track_db.begin(); it != track_db.end(); it = track_db.next(it)) {
i = (it - track_db.begin());
filter::add(artist_db.at((*it).artist_id)->name, i);
filter::add(album_db.at((*it).album_id)->name, i);
filter::add((*it).title, i);
if (library_db[track_db[i].library_id].enabled)
if (library_db.at((*it).library_id)->enabled)
get_callbacks()->on_library_track_add(i);
}
for (i = library_db.first(); i < library_db.num_rows(); i = library_db.next(i))
get_callbacks()->on_library_add(i, &library_db[i]);
Database<Library>::iterator l_it;
for (l_it = library_db.begin(); l_it != library_db.end(); l_it = library_db.next(l_it)) {
i = l_it - library_db.begin();
get_callbacks()->on_library_add(i, &(*library_db.at(i)));
}
}
void library :: add_path(const std::string &dir)
@ -511,24 +475,25 @@ void library :: add_path(const std::string &dir)
unsigned int id;
if (g_file_test(dir.c_str(), G_FILE_TEST_IS_DIR) == false)
throw -E_INVAL;
if (library_db.has_key(dir))
if (library_db.find(dir) != library_db.end())
return;
id = library_db.insert(library :: Library(dir, true));
library_db.save();
get_callbacks()->on_library_add(id, &library_db[id]);
get_callbacks()->on_library_add(id, &(*library_db.at(id)));
update_path(id);
}
void library :: del_path(unsigned int id)
{
unsigned int t;
Database<Track>::iterator it;
for (t = track_db.first(); t < track_db.num_rows(); t = track_db.next(t)) {
if (track_db[t].library_id == id) {
get_callbacks()->on_library_track_del(t);
track_db.remove(t);
for (it = track_db.begin(); it != track_db.end(); it = track_db.next(it)) {
if ((*it).library_id == id) {
unsigned int track_id = it - track_db.begin();
get_callbacks()->on_library_track_del(track_id);
track_db.remove(track_id);
}
}
@ -542,27 +507,30 @@ void library :: update_path(unsigned int id)
{
if (id > library_db.size())
return;
if (library_db[id].valid == false)
if (library_db.at(id)->valid == false)
return;
do_update_library(id);
}
void library :: update_all()
{
unsigned int i;
for (i = library_db.first(); i < library_db.num_rows(); i = library_db.next(i))
update_path(i);
Database<Library>::iterator it;
for (it = library_db.begin(); it != library_db.end(); it = library_db.next(it))
update_path(it - library_db.begin());
}
void library :: set_enabled(unsigned int id, bool enabled)
{
unsigned int t;
Database<Track>::iterator it;
library_db[id].enabled = enabled;
library_db.at(id)->enabled = enabled;
library_db.save();
for (t = track_db.first(); t < track_db.num_rows(); t = track_db.next(t)) {
if (track_db[t].library_id == id) {
for (it = track_db.begin(); it != track_db.end(); it = track_db.next(it)) {
if ((*it).library_id == id) {
t = it - track_db.begin();
if (enabled)
get_callbacks()->on_library_track_add(t);
else
@ -573,27 +541,27 @@ void library :: set_enabled(unsigned int id, bool enabled)
void library :: lookup(unsigned int id, library :: Song *song)
{
if (id >= track_db.num_rows())
if (id >= track_db.actual_size())
throw -E_EXIST;
song->track = &track_db[id];
song->track = &(*track_db.at(id));
if (song->track->valid == false)
throw -E_EXIST;
song->track_id = id;
song->artist = &artist_db[song->track->artist_id];
song->album = &album_db[song->track->album_id];
song->genre = &genre_db[song->track->genre_id];
song->library = &library_db[song->track->library_id];
song->artist = &(*artist_db.at(song->track->artist_id));
song->album = &(*album_db.at(song->track->album_id));
song->genre = &(*genre_db.at(song->track->genre_id));
song->library = &(*library_db.at(song->track->library_id));
}
library :: Library *library :: lookup_path(unsigned int id)
{
if (id >= library_db.num_rows())
if (id >= library_db.actual_size())
throw -E_EXIST;
if (library_db[id].valid == false)
if (library_db.at(id)->valid == false)
throw -E_EXIST;
return &library_db[id];
return &(*library_db.at(id));
}
void library :: import()
@ -622,10 +590,10 @@ void library :: track_played(unsigned int id)
time_t the_time = time(NULL);
struct tm *now = localtime(&the_time);
track_db[id].play_count++;
track_db[id].last_day = now->tm_mday;
track_db[id].last_month = now->tm_mon + 1;
track_db[id].last_year = now->tm_year + 1900;
track_db.at(id)->play_count++;
track_db.at(id)->last_day = now->tm_mday;
track_db.at(id)->last_month = now->tm_mon + 1;
track_db.at(id)->last_year = now->tm_year + 1900;
track_db.save();
get_callbacks()->on_library_track_updated(id);
@ -636,28 +604,19 @@ void library :: print_db(DB_Type type)
{
switch (type) {
case DB_ALBUM:
album_db.print();
break;
case DB_ARTIST:
artist_db.print();
break;
case DB_GENRE:
genre_db.print();
break;
case DB_LIBRARY:
library_db.print();
break;
case DB_TRACK:
track_db.print();
break;
}
}
void library :: reset()
{
album_db.clear();
artist_db.clear();
genre_db.clear();
library_db.clear();
track_db.clear();
}
#endif /* CONFIG_TEST */

View File

@ -7,7 +7,7 @@
#include <playlist.h>
static std::set<unsigned int> empty_set;
static Database<IndexEntry> playlist_db("playlist.db");
static Database<IndexEntry> playlist_db("playlist.db", false);
static Playqueue playlist_pq(PQ_ENABLED);
static std::string cur_pq;
@ -85,11 +85,10 @@ void playlist :: select(const std::string &name)
const std::set<unsigned int> &playlist :: get_tracks(const std::string &name)
{
if ((name == "Banned") || (name == "Favorites")) {
try {
return playlist_db.find(name).values;
} catch (int error) {
return empty_set;
}
Database<IndexEntry>::iterator it = playlist_db.find(name);
if (it != playlist_db.end())
return it->values;
return empty_set;
}
throw -E_EXIST;
}
@ -102,6 +101,5 @@ Playqueue *playlist :: get_pq()
#ifdef CONFIG_TEST
void playlist :: clear()
{
playlist_db.clear();
}
#endif /* CONFIG_TEST */

View File

@ -8,7 +8,7 @@ if sys.argv.count("tests") > 0:
src = SConscript("src/Sconscript")
tests = [ "version", "file", "db_entry" ]
tests = [ "version", "file", "db_entry", "database" ]
#scripts = [ "print", "file", "database", "index", "filter", "idle", "playlist",
# "library", "playqueue", "deck", "audio", "gui" ]

45
tests/database Executable file
View File

@ -0,0 +1,45 @@
#!/bin/bash
# Copyright 2014 (c) Anna Schumaker
. $(dirname $0)/_functions
function test_autosave
{
new_test "Database Test (n = $1, autosave = true)"
src/database.run -a $1
if [ ! -f $DATA_DIR/database.db ]; then
echo "ERROR: $DATA_DIR/database.db doesn't exist!"
exit 1
fi
}
function test_noautosave
{
new_test "Database Test (n = $1, autosave = false)"
src/database.run $1
if [ -f $DATA_DIR/database.db ]; then
echo "ERROR: $DATA_DIR/database.db exists!"
exit 1
fi
}
function run_test
{
rm $DATA_DIR/* 2>/dev/null || true
if [ $1 -le 1000 ]; then
test_autosave $1
else
test_noautosave $1
fi
}
run_test 10
echo
run_test 100
echo
run_test 1000
echo
run_test 10000
echo
run_test 100000

View File

@ -1,6 +0,0 @@
#!/usr/bin/python
Import("Test", "CONFIG")
CONFIG.DATABASE = True
Test("database", "database.cpp")

View File

@ -1,195 +0,0 @@
/*
* Copyright 2013 (c) Anna Schumaker.
*/
#include <database.h>
#include <print.h>
#include <sstream>
class DBTest : public DatabaseEntry {
public:
unsigned int value;
DBTest();
DBTest(unsigned int);
void write(File &);
void read(File &);
void print();
};
DBTest :: DBTest()
{
}
DBTest :: DBTest(unsigned int val)
{
std::stringstream ss;
value = val;
ss << value;
primary_key = ss.str();
}
void DBTest :: write(File &file)
{
file << value;
}
void DBTest :: read(File &file)
{
file >> value;
}
void DBTest :: print()
{
:: print("%u", value);
}
void print_db(Database<DBTest> &db)
{
print("Database size: %u\n", db.size());
print("Num rows: %u\n", db.num_rows());
print("First: %u\n", db.first());
print("Last: %u\n", db.last());
for (unsigned int i = db.first(); i != db.num_rows(); i = db.next(i))
print("db[%u] = %u\n", i, db[i].value);
}
void test_insertion(Database<DBTest> &db)
{
for (unsigned int i = 1; i <= 100000; i++)
db.insert(DBTest(i));
print_db(db);
}
void test_deletion(Database<DBTest> &db)
{
for (unsigned int i = db.first(); i < db.num_rows(); i = db.next(i)) {
db.remove(i);
i = db.next(i);
}
print_db(db);
}
/*
* Test everything except reloading.
*/
void test_0()
{
print("Test 0\n");
Database<DBTest> db("test.db");
test_insertion(db);
test_deletion(db);
test_deletion(db);
db.save();
}
/*
* Reload the database from disk.
*/
void test_1()
{
print("\nTest 1\n");
Database<DBTest> db("test.db");
db.load();
print_db(db);
}
/*
* Attempt to save a db with an empty filepath.
*/
void test_2()
{
print("\nTest 2\n");
Database<DBTest> db("");
test_insertion(db);
db.save();
}
/*
* Attempt to load a db with an empty filepath
*/
void test_3()
{
print("\nTest 3\n");
Database<DBTest> db("");
db.load();
}
/*
* Test unique insertion
*/
void test_4()
{
print("\nTest 4\n");
Database<DBTest> db("");
for (unsigned int i = 0; i < 100; i++)
db.insert(DBTest(i % 5));
print_db(db);
}
/*
* Test the database's built-in print() function
*/
void test_5()
{
print("\nTest 5\n");
Database<DBTest> db("");
for (unsigned int i = 0; i < 10; i++)
db.insert(DBTest(i));
db.print();
}
/*
* Test the database's clear() function
*/
void test_6()
{
print("\nTest 6\n");
Database<DBTest> db("");
for (unsigned int i = 0; i < 10; i++)
db.insert(DBTest(i));
print_db(db);
db.clear();
print_db(db);
}
/*
* Print databases with no valid rows
*/
void test_7()
{
print("\nTest 7\n");
Database<DBTest> db("");
print_db(db);
for (unsigned int i = 0; i < 10; i++)
db.insert(DBTest(i));
print_db(db);
for (unsigned int i = 0; i < 10; i++)
db.remove(i);
print_db(db);
}
int main(int argc, char **argv)
{
test_0();
test_1();
test_2();
test_3();
test_4();
test_5();
test_6();
test_7();
return 0;
}

File diff suppressed because it is too large Load Diff

220
tests/src/database.cpp Normal file
View File

@ -0,0 +1,220 @@
/*
* Copyright 2014 (c) Anna Schumaker.
* Test a Database
*/
#include <database.h>
#include <print.h>
#include <sstream>
#include <stdlib.h>
#include <unistd.h>
unsigned int test_num = 0;
class IntEntry : public DatabaseEntry {
public:
unsigned int val;
IntEntry();
IntEntry(unsigned int);
std::string primary_key();
void write(File &);
void read(File &);
void print();
};
IntEntry :: IntEntry() : val(0) {}
IntEntry :: IntEntry(unsigned int v) : val(v) {}
std::string IntEntry :: primary_key()
{
std::stringstream ss;
ss << val;
return ss.str();
}
void IntEntry :: write(File &f) { f << val; }
void IntEntry :: read(File &f) { f >> val; }
void IntEntry :: print() { :: print(primary_key().c_str()); }
void test_results(bool success, unsigned int line)
{
print(" %u: ", test_num);
if (success)
print("Success!\n");
else {
print("FAILED (%u) =(\n", line);
exit(1);
}
test_num++;
}
void test_size(Database<IntEntry> &db, unsigned int size,
unsigned int actual, unsigned int line)
{
test_results( (db.size() == size) || (db.actual_size() == actual), line );
}
int main(int argc, char **argv)
{
bool autosave = false;
unsigned int n = 0, size = 0, actual = 0;
char c;
while ((c = getopt(argc, argv, "a")) != -1) {
switch (c) {
case 'a':
autosave = true;
break;
}
}
n = atoi(argv[optind]);
Database<IntEntry> db("database.db", autosave);
/**
* 0: Test initial size
*/
test_size(db, size, actual, __LINE__);
/**
* 1: Test insertion
*/
for (unsigned int i = 0; i < n; i++) {
if (db.insert(IntEntry(i)) != i)
test_results(false, __LINE__);
}
test_results(true, __LINE__);
size += n;
actual += n;
/**
* 2: Test that size changes
*/
test_size(db, size, actual, __LINE__);
/**
* 3: Test inserting ... again.
*/
for (unsigned int i = 0; i < n; i++) {
if (db.insert(IntEntry(i)) != i)
test_results(false, __LINE__);
}
test_results(true, __LINE__);
/**
* 4: Test that size didn't change
*/
test_size(db, size, actual, __LINE__);
/**
* 5: Test that size changes when removing
* Also test out-of-bounds removal
*
* Note: This test removes all even-index entries
*/
for (unsigned int i = 0; i < n + 10; i+=2) {
db.remove(i);
size--;
}
test_size(db, size, actual, __LINE__);
/**
* 6: Test that removing again doesn't change anything
*/
for (unsigned int i = 0; i < n; i+=2)
db.remove(i);
test_size(db, size, actual, __LINE__);
/**
* 7: Test iterating
*/
Database<IntEntry>::iterator it;
unsigned int index = 1;
for (it = db.begin(); it != db.end(); it = db.next(it)) {
if ((*it).val != index)
test_results(false, __LINE__);
index+=2;
};
test_results(true, __LINE__);
/**
* 8. Test access by id
*/
for (unsigned int i = 0; i < n + 10; i++) {
Database<IntEntry>::iterator it = db.at(i);
if (((i % 2) == 0) && (it != db.end()))
test_results(false, __LINE__);
if ((i >= n) && (it != db.end()))
test_results(false, __LINE__);
}
test_results(true, __LINE__);
/**
* 9. Test inserting once again
*/
for (unsigned int i = 0; i < n; i++) {
index = db.insert(i);
if ((i % 2) == 0) {
size++;
actual++;
if (index != (n + (i / 2)))
test_results(false, __LINE__);
} else {
if (index != i)
test_results(false, __LINE__);
}
}
test_results(true, __LINE__);
/**
* 10. Test that size changed for every other insert
*/
test_size(db, size, actual, __LINE__);
/**
* Everything after this point tests loading from a file
*/
if (autosave == false)
return 0;
Database<IntEntry> db2("database.db", autosave);
db2.load();
/**
* 11. Sizes should match
*/
test_size(db2, db.size(), db.actual_size(), __LINE__);
/**
* 12. Values should match
*/
Database<IntEntry>::iterator it2 = db2.begin();
for (it = db.begin(); it != db.end(); it++) {
if (it->valid != it2->valid)
test_results(false, __LINE__);
if (it->valid == true) {
if (it->val != it2->val)
test_results(false, __LINE__);
}
it2++;
}
test_results(true, __LINE__);
return 0;
}