file: Update design and create a new unit test

Signed-off-by: Anna Schumaker <schumaker.anna@gmail.com>
This commit is contained in:
Anna Schumaker 2014-03-02 14:44:24 -05:00 committed by Anna Schumaker
parent 3359cbae6f
commit 336024bae9
13 changed files with 373 additions and 477 deletions

24
DESIGN
View File

@ -132,18 +132,13 @@ Callbacks: (lib/callback.cpp)
On-disk files: (lib/file.cpp)
Data will be stored in the user's home directory according to the
XDG / freedesktop.org specification. This means storing data in
$XDG_DATA_HOME/ocarina{-debug|-test}/ and storing configuration in
$XDG_CONFIG_HOME/ocarina{-debug|-test}/. In addition, I will support
importing data from Ocarina 5.10 for conversion to the new format.
XDG / freedesktop.org specification. This means data will be stored
in a subdirectory of $XDG_DATA_HOME.
In theory my file format will not change often, so it should be
possible to use multiple Ocarina versions with the same data. However,
should the format need to change I will only support forward
compatibility. This means that downgrades will not be possible after
a file format change. To keep the code even cleaner, I will only
support updating from the previous file format version. This means
that legacy support will be removed after the first file format change.
The filse class will support reading and writing files in the users
$XDG_CONFIG_HOME/ocarina{-debug|test}. In addition, Ocarina 6.0 will
support reading library files from the Ocarina 5.10 directory:
$HOME/.ocarina{-debug}.
Items should be written to a file with either a space or new line
separating multiple values.
@ -156,7 +151,6 @@ On-disk files: (lib/file.cpp)
- Hint where the file is located:
enum FileLocHint {
FILE_TYPE_CONFIG,
FILE_TYPE_DATA,
FILE_TYPE_LEGACY,
FILE_TYPE_INVALID,
@ -195,9 +189,9 @@ On-disk files: (lib/file.cpp)
- API:
File :: File(string filepath, FileLocHint hint);
Resolve filepath to one of:
XDG_{CONFIG|DATA}_HOME/ocarina/filepath
XDG_{CONFIG|DATA}_HOME/ocarina-debug/filepath
XDG_{CONFIG|DATA}_HOME/ocarina-test/filepath
XDG_DATA_HOME/ocarina/filepath
XDG_DATA_HOME/ocarina-debug/filepath
XDG_DATA_HOME/ocarina-test/filepath
$HOME/.ocarina/
$HOME/.ocarina-debug/
$HOME/.ocarina-test/

1
config
View File

@ -26,6 +26,7 @@ Export("env", "use_package", "CONFIG_DEBUG", "CONFIG_VERSION")
include = SConscript("include/Sconscript")
lib = SConscript("lib/Sconscript")
Export("lib")
tests = SConscript("tests/Sconscript")

View File

@ -10,7 +10,6 @@
#define FILE_VERSION 0
enum FileLocHint {
FILE_TYPE_CONFIG,
FILE_TYPE_DATA,
FILE_TYPE_LEGACY,
FILE_TYPE_INVALID,
@ -29,7 +28,7 @@ private:
std::string filepath;
unsigned int version;
void find_dir(std::string &);
std::string find_dir();
void open_read();
void open_write();

View File

@ -15,49 +15,16 @@
#define OCARINA_DIR "ocarina"
#endif
void File :: find_dir(std::string &res)
{
if (hint == FILE_TYPE_INVALID)
return;
switch (hint) {
case FILE_TYPE_CONFIG:
res = g_get_user_config_dir();
break;
case FILE_TYPE_DATA:
res = g_get_user_data_dir();
break;
default: /* FILE_TYPE_LEGACY */
res = g_get_home_dir();
break;
}
res += "/";
switch (hint) {
case FILE_TYPE_CONFIG:
case FILE_TYPE_DATA:
res += OCARINA_DIR;
break;
default: /* FILE_TYPE_LEGACY */
res += ".";
res += OCARINA_DIR;
res += "/library";
}
}
File :: File(const std::string &path, FileLocHint file_hint)
: mode(NOT_OPEN), hint(file_hint)
: mode(NOT_OPEN), hint(file_hint), version(FILE_VERSION)
{
std::string dir;
if (path == "") {
if (path.size() == 0)
hint = FILE_TYPE_INVALID;
filepath = "INVALID";
return;
}
find_dir(dir);
filepath = dir + "/" + path;
if (hint == FILE_TYPE_INVALID)
filepath = "INVALID";
else
filepath = find_dir() + "/" + path;
}
File :: ~File()
@ -80,16 +47,34 @@ bool File :: exists()
return g_file_test(filepath.c_str(), G_FILE_TEST_EXISTS);
}
std::string File :: find_dir()
{
std::string res;
if (hint == FILE_TYPE_DATA) {
res = g_get_user_data_dir();
res += "/";
res += OCARINA_DIR;
} else /* FILE_TYPE_LEGACY */ {
res = g_get_home_dir();
res += "/.";
res += OCARINA_DIR;
res += "/library";
}
return res;
}
void File :: open_read()
{
if (!exists()) {
dprint("ERROR: File does not exist (%s)\n", filepath.c_str());
dprint("ERROR: File does not exist\n");
throw -E_EXIST;
}
std::fstream::open(filepath.c_str(), std::fstream::in);
if (std::fstream::fail()) {
dprint("ERROR: Could not open file for reading (%s)\n", filepath.c_str());
dprint("ERROR: File could not be opened for reading\n");
throw -E_IO;
}
@ -100,21 +85,20 @@ void File :: open_read()
void File :: open_write()
{
std::string dir;
if (hint == FILE_TYPE_LEGACY) {
dprint("ERROR: Cannot write to legacy files (%s)\n", filepath.c_str());
dprint("ERROR: Cannot write to legacy files\n");
throw -E_IO;
}
find_dir(dir);
std::string dir = find_dir();
if (g_mkdir_with_parents(dir.c_str(), 0755) != 0) {
dprint("ERROR: Could not make directory (%s)\n", dir.c_str());
dprint("ERROR: Could not make directory\n");
throw -E_IO;
}
std::fstream::open(filepath.c_str(), std::fstream::out);
if (std::fstream::fail()) {
dprint("ERROR: Could not open file for writing (%s)\n", filepath.c_str());
dprint("ERROR: Could not open file for writing\n");
throw -E_IO;
}
@ -124,18 +108,18 @@ void File :: open_write()
void File :: open(OpenMode m)
{
if (mode != NOT_OPEN) {
dprint("ERROR: File is already open (%s)\n", filepath.c_str());
if (hint == FILE_TYPE_INVALID) {
dprint("ERROR: A file with hint = FILE_TYPE_INVALID cannot be opened\n");
throw -E_INVAL;
} else if (m == NOT_OPEN) {
dprint("ERROR: NOT_OPEN is not a legal OpenMode\n");
throw -E_INVAL;
} else if (mode != NOT_OPEN) {
dprint("ERROR: File is already open\n");
throw -E_IO;
}
if (m == NOT_OPEN) {
dprint("ERROR: NOT_OPEN is not a legal OpenMode (%s)\n", filepath.c_str());
throw -E_INVAL;
} else if (hint == FILE_TYPE_INVALID) {
dprint("ERROR: A file with hint = FILE_TYPE_INVALID cannot be opened\n");
throw -E_INVAL;
} else if (m == OPEN_READ)
else if (m == OPEN_READ)
open_read();
else /* m == OPEN_WRITE */
open_write();

View File

@ -1,21 +1,20 @@
#!/usr/bin/python
Import("env")
env.Append( CCFLAGS = [ "-DCONFIG_TEST" ] )
src = SConscript("src/Sconscript")
tests = [ "version" ]
def Test(name):
return Command("%s.out" % name, [], "./tests/%s" % name)
for test in tests:
t = Test(test)
Depends(t, src)
AlwaysBuild(t)
#AlwaysBuild(test)
#Depends(test, src)
tests = [ "version", "file" ]
#scripts = [ "print", "file", "database", "index", "filter", "idle", "playlist",
# "library", "playqueue", "deck", "audio", "gui" ]
prev = None
for test in tests:
t = Command("%s.out" % test, [], "./tests/%s" % test)
if prev:
Depends(t, prev)
Depends(t, src)
AlwaysBuild(t)
prev = t

View File

@ -16,18 +16,51 @@ function config_debug
read_config CONFIG_DEBUG
}
function test_success
CUR_TEST=0
function new_test
{
echo "Success!"
exit 0
echo "$1"
CUR_TEST=0
}
function test_failed
function start_test
{
echo "FAILED =("
on_failed
exit 1
echo -n " $CUR_TEST: "
let CUR_TEST=($CUR_TEST + 1)
}
function assert_equal
{
if [ "$1" == "$2" ]; then
echo "Success!"
return 0
else
echo "FAILED =("
echo " Expected: $2"
echo " Actual: $1"
return 1
fi
}
function test_equal
{
start_test
assert_equal "$($1)" "$2"
}
function on_exit
{
ret=$?
echo
return $ret
}; trap "on_exit" EXIT
[ -z $HOME ] && HOME=$(cat /etc/passwd | grep $(whoami) | awk -F: '{print $6}')
[ -z $XDG_DATA_HOME] && XDG_DATA_HOME="$HOME/.local/share"
DATA_DIR="$XDG_DATA_HOME/ocarina-test"
LEGACY_DIR="$HOME/.ocarina-test/library"
rm -rf $DATA_DIR 2>/dev/null || true
set -e
cd $(dirname $0)
echo -n "Testing $(basename $0) ... "

113
tests/file Executable file
View File

@ -0,0 +1,113 @@
#!/bin/bash
# Copyright 2014 (c) Anna Schumaker
. $(dirname $0)/_functions
function test_file
{
test_equal "./src/file $1" "$2"
}
###
#
# Test filepaths
#
new_test "Filepath Test"
test_file -D INVALID
test_file -L INVALID
test_file "-D file.txt" "$DATA_DIR/file.txt"
test_file "-L file.txt" "$LEGACY_DIR/file.txt"
test_file "file.txt" INVALID
test_file "-D -v file.txt" "0"
test_file "-L -v file.txt" "0"
if [ -d $DATA_DIR ]; then
echo "ERROR: $DATA_DIR should not exist!"
exit 1
fi
###
#
# Test opening files
#
echo
new_test "File Open Test"
# Generic open testing
test_file "-o N file.txt" "ERROR: A file with hint = FILE_TYPE_INVALID cannot be opened"
test_file "-D -o N file.txt" "ERROR: NOT_OPEN is not a legal OpenMode"
test_file "-D -o W -O file.txt" "ERROR: File is already open" # This test creates a file
test_file "-D -o R -O file.txt" "ERROR: File is already open"
rm $DATA_DIR/*
# Test opening for read
test_file "-D -o R file.txt" "ERROR: File does not exist"
touch $DATA_DIR/file.txt
chmod -r $DATA_DIR/file.txt
test_file "-D -o R file.txt" "ERROR: File could not be opened for reading"
rm -r $DATA_DIR
# Test opening for write
test_file "-L -o W file.txt" "ERROR: Cannot write to legacy files"
touch $DATA_DIR
test_file "-D -o W file.txt" "ERROR: Could not make directory"
rm $DATA_DIR
mkdir -p $DATA_DIR
touch $DATA_DIR/file.txt
chmod -w $DATA_DIR/file.txt
test_file "-D -o W file.txt" "ERROR: Could not open file for writing"
rm -rf $DATA_DIR
###
#
# Test closing files
#
echo
new_test "File Close Test"
test_file "-D -c file.txt" ""
###
#
# Test FILE IO
#
data="ABCDE FGHIJ KLMNO PQRST UVWXYZ"
echo
new_test "File IO Test"
# Write to file
./src/file -D -w file.txt "$data"
start_test
assert_equal "$(cat $DATA_DIR/file.txt)" "0
$data"
# Read data back from file
test_file "-D -r file.txt" "ABCDE
FGHIJ
KLMNO
PQRST
UVWXYZ"
# Write different data to file
./src/file -D -w file.txt " $data"
start_test
assert_equal "$(cat $DATA_DIR/file.txt)" "0
$data"
# Read data back in a single line
test_file "-D -r -g file.txt" "$data"

View File

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

View File

@ -1,287 +0,0 @@
/*
* Copyright 2013 (c) Anna Schumaker.
*/
#include <error.h>
#include <file.h>
#include <print.h>
#include <sstream>
bool test_result(bool passed, bool print_spaces)
{
if (print_spaces)
print(" ");
if (passed == true)
print("Passed\n");
else
print("Failed\n");
return passed;
}
File *get_file(int testno, char subtest, bool valid, FileLocHint hint)
{
std::stringstream s;
std::string path = "";
if (valid == true) {
path = "file.";
s << testno;
path += s.str() + subtest;
}
print("Test %d%c: ", testno, subtest);
return new File(path, hint);
}
static inline bool inval_or_legacy(bool valid, FileLocHint hint)
{
return (valid == false) || (hint == FILE_TYPE_LEGACY);
}
/*
* Check how filepath is expanded
* An empty filepath == INVALID
*/
void test_a(int testno, bool valid, FileLocHint hint)
{
File *f = get_file(testno, 'a', valid, hint);
print("File path is %s\n", f->get_filepath());
}
/*
* NOT_OPEN is not a valid openmode
*/
void test_b(int testno, bool valid, FileLocHint hint)
{
int status = 0;
File *f = get_file(testno, 'b', valid, hint);
try {
f->open(NOT_OPEN);
} catch (int error) {
status = error;
}
test_result(status == -E_INVAL, true);
}
/*
* If a file doesn't exist, it can't be opened for reading
*/
void test_c(int testno, bool valid, FileLocHint hint)
{
int status = 0;
File *f = get_file(testno, 'c', valid, hint);
try {
f->open(OPEN_READ);
} catch (int error) {
status = error;
}
if (valid == false)
test_result(status == -E_INVAL, true);
else
test_result(status == -E_EXIST, true);
}
/*
* Writing to FILE_TYPE_LEGACY is not supported
*/
void test_d(int testno, bool valid, FileLocHint hint)
{
int status = 0;
File *f = get_file(testno, 'd', valid, hint);
try {
f->open(OPEN_WRITE);
} catch (int error) {
status = error;
}
if (valid == false)
test_result(status == -E_INVAL, true);
else if (hint == FILE_TYPE_LEGACY)
test_result(status == -E_IO, true);
else
test_result(status == 0, false);
}
/*
* Attempt to close a file that was never opened
*/
void test_e(int testno, bool valid, FileLocHint hint)
{
int status = 0;
File *f = get_file(testno, 'e', valid, hint);
try {
f->close();
} catch (int error) {
status = error;
}
test_result(status == 0, false);
}
/*
* Open for writing, then for reading
*/
void test_f(int testno, bool valid, FileLocHint hint)
{
File *f;
int status = 0;
if (inval_or_legacy(valid, hint))
return;
f = get_file(testno, 'f', valid, hint);
try {
f->open(OPEN_WRITE);
} catch (int error) {
status = error;
}
test_result(status == 0, false);
print(" ");
status = 0;
try {
f->open(OPEN_READ);
} catch (int error) {
status = error;
}
test_result(status == -E_IO, true);
}
/*
* Create a file, then open for reading
*/
void test_g(int testno, bool valid, FileLocHint hint)
{
File *f;
int status = 0;
if (inval_or_legacy(valid, hint))
return;
f = get_file(testno, 'g', valid, hint);
try {
f->open(OPEN_WRITE);
} catch (int error) {
status = error;
}
if (!test_result(status == 0, false))
return;
try {
f->close();
} catch (int error) {
status = error;
}
if (!test_result(status == 0, true))
return;
try {
f->open(OPEN_READ);
} catch (int error) {
status = error;
}
if (!test_result(status == 0, true))
return;
f->close();
}
/*
* Test doing IO to a file
*/
void test_h(int testno, bool valid, FileLocHint hint)
{
File *f;
int a, b, c, d, e, status = 0;
if (inval_or_legacy(valid, hint))
return;
f = get_file(testno, 'h', valid, hint);
/*
* Write data to file
*/
try {
f->open(OPEN_WRITE);
} catch (int error) {
status = error;
}
if (!test_result(status == 0, false))
return;
(*f) << "Hello, World!" << std::endl;
(*f) << "This is a multi-line file =)" << std::endl;
(*f) << "1 2 3 4 5" << std::endl;
try {
f->close();
} catch (int error) {
status = error;
}
if (!test_result(status == 0, true))
return;
/*
* Read data back from disk
*/
try {
f->open(OPEN_READ);
} catch (int error) {
status = error;
}
if (!test_result(status == 0, true))
return;
print(" File version is: %u\n", f->get_version());
print(" \"%s\"\n", f->getline().c_str());
if (!test_result(f->good(), true))
return;
print(" \"%s\"\n", f->getline().c_str());
if (!test_result(f->good(), true))
return;
(*f) >> a >> b >> c >> d >> e;
if (!test_result(f->good(), true))
return;
print(" a: %d, b: %d, c: %d, d: %d, e: %d\n", a, b, c, d, e);
/*
* Close file
*/
try {
f->close();
} catch (int error) {
status = error;
}
test_result(status == 0, true);
}
static void(*tests[])(int, bool, FileLocHint) = {
test_a,
test_b,
test_c,
test_d,
test_e,
test_f,
test_g,
test_h,
};
static unsigned int num_tests = sizeof(tests) / sizeof(void (*)(int, bool, FileLocHint));
void run_test(int testno, bool valid, FileLocHint hint)
{
for (unsigned int i = 0; i < num_tests; i++)
tests[i](testno, valid, hint);
print("\n");
}
int main(int argc, char **argv)
{
run_test(1, true, FILE_TYPE_CONFIG);
run_test(2, false, FILE_TYPE_CONFIG);
run_test(3, true, FILE_TYPE_DATA);
run_test(4, false, FILE_TYPE_DATA);
run_test(5, true, FILE_TYPE_LEGACY);
run_test(6, false, FILE_TYPE_LEGACY);
return 0;
}

View File

@ -1,86 +0,0 @@
Test 1a: File path is /home/anna/.config/ocarina-test/file.1a
Test 1b: ERROR: NOT_OPEN is not a legal OpenMode (/home/anna/.config/ocarina-test/file.1b)
Passed
Test 1c: ERROR: File does not exist (/home/anna/.config/ocarina-test/file.1c)
Passed
Test 1d: Passed
Test 1e: Passed
Test 1f: Passed
ERROR: File is already open (/home/anna/.config/ocarina-test/file.1f)
Passed
Test 1g: Passed
Passed
Passed
Test 1h: Passed
Passed
Passed
File version is: 0
"Hello, World!"
Passed
"This is a multi-line file =)"
Passed
Passed
a: 1, b: 2, c: 3, d: 4, e: 5
Passed
Test 2a: File path is INVALID
Test 2b: ERROR: NOT_OPEN is not a legal OpenMode (INVALID)
Passed
Test 2c: ERROR: A file with hint = FILE_TYPE_INVALID cannot be opened
Passed
Test 2d: ERROR: A file with hint = FILE_TYPE_INVALID cannot be opened
Passed
Test 2e: Passed
Test 3a: File path is /home/anna/.local/share/ocarina-test/file.3a
Test 3b: ERROR: NOT_OPEN is not a legal OpenMode (/home/anna/.local/share/ocarina-test/file.3b)
Passed
Test 3c: ERROR: File does not exist (/home/anna/.local/share/ocarina-test/file.3c)
Passed
Test 3d: Passed
Test 3e: Passed
Test 3f: Passed
ERROR: File is already open (/home/anna/.local/share/ocarina-test/file.3f)
Passed
Test 3g: Passed
Passed
Passed
Test 3h: Passed
Passed
Passed
File version is: 0
"Hello, World!"
Passed
"This is a multi-line file =)"
Passed
Passed
a: 1, b: 2, c: 3, d: 4, e: 5
Passed
Test 4a: File path is INVALID
Test 4b: ERROR: NOT_OPEN is not a legal OpenMode (INVALID)
Passed
Test 4c: ERROR: A file with hint = FILE_TYPE_INVALID cannot be opened
Passed
Test 4d: ERROR: A file with hint = FILE_TYPE_INVALID cannot be opened
Passed
Test 4e: Passed
Test 5a: File path is /home/anna/.ocarina-test/library/file.5a
Test 5b: ERROR: NOT_OPEN is not a legal OpenMode (/home/anna/.ocarina-test/library/file.5b)
Passed
Test 5c: ERROR: File does not exist (/home/anna/.ocarina-test/library/file.5c)
Passed
Test 5d: ERROR: Cannot write to legacy files (/home/anna/.ocarina-test/library/file.5d)
Passed
Test 5e: Passed
Test 6a: File path is INVALID
Test 6b: ERROR: NOT_OPEN is not a legal OpenMode (INVALID)
Passed
Test 6c: ERROR: A file with hint = FILE_TYPE_INVALID cannot be opened
Passed
Test 6d: ERROR: A file with hint = FILE_TYPE_INVALID cannot be opened
Passed
Test 6e: Passed

View File

@ -1,8 +1,10 @@
#!/usr/bin/python
Import("env")
Import("env", "lib")
def compile_utility(name):
return env.Program(name, "%s.cpp" % name)
build = []
for f in Glob("*.cpp"):
src = str(f)
name = src.split(".")[0]
build += [ env.Program(name, [src] + lib) ]
build = compile_utility("version")
Return("build")

150
tests/src/file.cpp Normal file
View File

@ -0,0 +1,150 @@
/*
* Copyright 2014 (c) Anna Schumaker.
* Do stuff with files
*
* Usage: files -D|-L [-c -g -o {R, W, N} -O -r -v -w] name [DATA]
*
* -D: FILE_TYPE_DATA
* -L: FILE_TYPE_LEGACY
*
* -c: Test closing the file
* -g: Read file using getline()
* -o: Open the file for READ, WRITE, or NOT_OPEN
* -O: Open the file a second time
* -r: Read data from file
* -v: Print version and exit
* -w: Write data to file
*
*/
#include <file.h>
#include <print.h>
#include <unistd.h>
enum action_t { CLOSE, OPEN, PATHS, READ, VERSION, WRITE };
int print_version(File &f)
{
print("%u\n", f.get_version());
return 0;
}
int print_filepath(File &f)
{
print("%s\n", f.get_filepath());
return 0;
}
int open_file(File &f, OpenMode mode)
{
try {
f.open(mode);
return 0;
} catch (int err) {
return err;
}
}
int main(int argc, char **argv)
{
int c, ret;
action_t action = PATHS;
FileLocHint hint = FILE_TYPE_INVALID;
OpenMode mode = NOT_OPEN;
bool second_open = false;
bool getline = false;
std::string file;
std::string data;
while ((c = getopt(argc, argv, "cDgLo:Orvw")) != -1) {
switch (c) {
case 'c':
action = CLOSE;
break;
case 'D':
hint = FILE_TYPE_DATA;
break;
case 'g':
getline = true;
break;
case 'L':
hint = FILE_TYPE_LEGACY;
break;
case 'o':
action = OPEN;
switch (optarg[0]) {
case 'R':
mode = OPEN_READ;
break;
case 'W':
mode = OPEN_WRITE;
break;
case 'N':
mode = NOT_OPEN;
break;
default:
print("Invalid open mode\n");
return 1;
}
break;
case 'O':
second_open = true;
break;
case 'r':
action = READ;
break;
case 'v':
action = VERSION;
break;
case 'w':
action = WRITE;
break;
default:
return 1;
}
}
if (optind < argc)
file = argv[optind++];
if (optind < argc)
data = argv[optind++];
File f(file, hint);
switch (action) {
case CLOSE:
ret = open_file(f, OPEN_WRITE);
if (ret == 0) {
f.close();
ret = open_file(f, OPEN_WRITE);
}
return ret;
case OPEN:
ret = open_file(f, mode);
if ((ret == 0) && (second_open == true))
ret = open_file(f, mode);
return ret;
case PATHS:
return print_filepath(f);
case READ:
ret = open_file(f, OPEN_READ);
if (ret == 0) {
do {
if (getline == true)
data = f.getline();
else
f >> data;
if (f.good() == false)
break;
print("%s\n", data.c_str());
} while (true);
}
return ret;
case VERSION:
return print_version(f);
case WRITE:
ret = open_file(f, OPEN_WRITE);
if (ret == 0)
f << data << std::endl;
return ret;
}
}

Binary file not shown.