linux db
Some checks failed
CI - Multi-Platform Native / Build iOS (RSSuper) (push) Has been cancelled
CI - Multi-Platform Native / Build macOS (push) Has been cancelled
CI - Multi-Platform Native / Build Android (push) Has been cancelled
CI - Multi-Platform Native / Build Summary (push) Has been cancelled
CI - Multi-Platform Native / Build Linux (push) Has been cancelled
Some checks failed
CI - Multi-Platform Native / Build iOS (RSSuper) (push) Has been cancelled
CI - Multi-Platform Native / Build macOS (push) Has been cancelled
CI - Multi-Platform Native / Build Android (push) Has been cancelled
CI - Multi-Platform Native / Build Summary (push) Has been cancelled
CI - Multi-Platform Native / Build Linux (push) Has been cancelled
This commit is contained in:
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@@ -229,7 +229,7 @@ jobs:
|
||||
# macOS Build Job (using same iOS project)
|
||||
build-macos:
|
||||
name: Build macOS
|
||||
runs-on: macos-15
|
||||
runs-on: macos
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
@@ -294,7 +294,7 @@ jobs:
|
||||
# Linux Build Job
|
||||
build-linux:
|
||||
name: Build Linux
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
@@ -326,7 +326,7 @@ jobs:
|
||||
# Summary Job
|
||||
build-summary:
|
||||
name: Build Summary
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu
|
||||
needs: [build-ios, build-macos, build-android, build-linux]
|
||||
if: always()
|
||||
|
||||
|
||||
@@ -30,12 +30,11 @@ models = files(
|
||||
|
||||
# Database files
|
||||
database = files(
|
||||
'src/database/db-error.vala',
|
||||
'src/database/database.vala',
|
||||
'src/database/subscription-store.vala',
|
||||
'src/database/feed-item-store.vala',
|
||||
'src/database/search-history-store.vala',
|
||||
'src/database/sqlite3.vapi',
|
||||
'src/database/errors.vapi',
|
||||
)
|
||||
|
||||
# Main library
|
||||
@@ -57,7 +56,7 @@ test_exe = executable('database-tests',
|
||||
'src/tests/database-tests.vala',
|
||||
dependencies: [glib_dep, gio_dep, json_dep, sqlite_dep, gobject_dep],
|
||||
link_with: [models_lib, database_lib],
|
||||
vala_args: ['--vapidir', 'src/database', '--pkg', 'sqlite3'],
|
||||
vala_args: ['--vapidir', '.', '--pkg', 'sqlite3'],
|
||||
install: false
|
||||
)
|
||||
|
||||
|
||||
69
native-route/linux/rssuper-database.vapi
Normal file
69
native-route/linux/rssuper-database.vapi
Normal file
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* RSSuper Database vapi - exports SQLite bindings for use by dependent modules
|
||||
*/
|
||||
|
||||
[CCode (cheader_filename = "sqlite3.h")]
|
||||
namespace SQLite {
|
||||
[CCode (cname = "sqlite3", free_function = "sqlite3_close")]
|
||||
public class DB {
|
||||
[CCode (cname = "sqlite3_open")]
|
||||
public static int open(string filename, out DB db);
|
||||
|
||||
[CCode (cname = "sqlite3_close")]
|
||||
public int close();
|
||||
|
||||
[CCode (cname = "sqlite3_exec")]
|
||||
public int exec(string sql, DBCallback? callback = null, void* arg = null, [CCode (array_length = false)] out string? errmsg = null);
|
||||
|
||||
[CCode (cname = "sqlite3_errmsg")]
|
||||
public unowned string errmsg();
|
||||
|
||||
[CCode (cname = "sqlite3_prepare_v2")]
|
||||
public int prepare_v2(string zSql, int nByte, out Stmt stmt, void* pzTail = null);
|
||||
}
|
||||
|
||||
[CCode (cname = "sqlite3_stmt", free_function = "sqlite3_finalize")]
|
||||
public class Stmt {
|
||||
[CCode (cname = "sqlite3_step")]
|
||||
public int step();
|
||||
|
||||
[CCode (cname = "sqlite3_column_count")]
|
||||
public int column_count();
|
||||
|
||||
[CCode (cname = "sqlite3_column_text")]
|
||||
public unowned string column_text(int i);
|
||||
|
||||
[CCode (cname = "sqlite3_column_int")]
|
||||
public int column_int(int i);
|
||||
|
||||
[CCode (cname = "sqlite3_column_double")]
|
||||
public double column_double(int i);
|
||||
|
||||
[CCode (cname = "sqlite3_bind_text")]
|
||||
public int bind_text(int i, string z, int n, void* x);
|
||||
|
||||
[CCode (cname = "sqlite3_bind_int")]
|
||||
public int bind_int(int i, int val);
|
||||
|
||||
[CCode (cname = "sqlite3_bind_double")]
|
||||
public int bind_double(int i, double val);
|
||||
|
||||
[CCode (cname = "sqlite3_bind_null")]
|
||||
public int bind_null(int i);
|
||||
|
||||
[CCode (cname = "sqlite3_finalize")]
|
||||
public int finalize();
|
||||
}
|
||||
|
||||
[CCode (cname = "SQLITE_OK")]
|
||||
public const int SQLITE_OK;
|
||||
[CCode (cname = "SQLITE_ROW")]
|
||||
public const int SQLITE_ROW;
|
||||
[CCode (cname = "SQLITE_DONE")]
|
||||
public const int SQLITE_DONE;
|
||||
[CCode (cname = "SQLITE_ERROR")]
|
||||
public const int SQLITE_ERROR;
|
||||
|
||||
[CCode (simple_type = true)]
|
||||
public delegate int DBCallback(void* arg, int argc, string[] argv, string[] col_names);
|
||||
}
|
||||
@@ -9,7 +9,7 @@
|
||||
* Database - Manages SQLite database connection and migrations
|
||||
*/
|
||||
public class RSSuper.Database : Object {
|
||||
private SQLite.DB db;
|
||||
private Sqlite.Database db;
|
||||
private string db_path;
|
||||
|
||||
/**
|
||||
@@ -48,13 +48,13 @@ public class RSSuper.Database : Object {
|
||||
try {
|
||||
parent.make_directory_with_parents();
|
||||
} catch (Error e) {
|
||||
throw throw new Error.FAILED("Failed to create database directory: %s", e.message);
|
||||
throw new DBError.FAILED("Failed to create database directory: %s", e.message);
|
||||
}
|
||||
}
|
||||
|
||||
int result = SQLite.DB.open(db_path, out db);
|
||||
if (result != SQLite.SQLITE_OK) {
|
||||
throw throw new Error.FAILED("Failed to open database: %s".printf(db.errmsg()));
|
||||
int result = Sqlite.Database.open(db_path, out db);
|
||||
if (result != Sqlite.OK) {
|
||||
throw new DBError.FAILED("Failed to open database: %s".printf(db.errmsg()));
|
||||
}
|
||||
|
||||
execute("PRAGMA foreign_keys = ON;");
|
||||
@@ -90,7 +90,7 @@ public class RSSuper.Database : Object {
|
||||
}
|
||||
|
||||
if (!schema_file.query_exists()) {
|
||||
throw throw new Error.FAILED("Schema file not found: %s".printf(schema_path));
|
||||
throw new DBError.FAILED("Schema file not found: %s".printf(schema_path));
|
||||
}
|
||||
|
||||
uint8[] schema_bytes;
|
||||
@@ -99,7 +99,7 @@ public class RSSuper.Database : Object {
|
||||
try {
|
||||
schema_file.load_contents(cancellable, out schema_bytes, out schema_str);
|
||||
} catch (Error e) {
|
||||
throw throw new Error.FAILED("Failed to read schema file: %s", e.message);
|
||||
throw new DBError.FAILED("Failed to read schema file: %s", e.message);
|
||||
}
|
||||
string schema = schema_str ?? (string) schema_bytes;
|
||||
|
||||
@@ -109,7 +109,7 @@ public class RSSuper.Database : Object {
|
||||
debug("Database migrated to version %d", CURRENT_VERSION);
|
||||
|
||||
} catch (Error e) {
|
||||
throw throw new Error.FAILED("Migration failed: %s".printf(e.message));
|
||||
throw new DBError.FAILED("Migration failed: %s".printf(e.message));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,22 +118,22 @@ public class RSSuper.Database : Object {
|
||||
*/
|
||||
private int get_current_version() throws Error {
|
||||
try {
|
||||
SQLite.Stmt stmt;
|
||||
Sqlite.Statement stmt;
|
||||
int result = db.prepare_v2("SELECT COALESCE(MAX(version), 0) FROM schema_migrations;", -1, out stmt, null);
|
||||
|
||||
if (result != SQLite.SQLITE_OK) {
|
||||
throw throw new Error.FAILED("Failed to prepare statement: %s".printf(db.errmsg()));
|
||||
if (result != Sqlite.OK) {
|
||||
throw new DBError.FAILED("Failed to prepare statement: %s".printf(db.errmsg()));
|
||||
}
|
||||
|
||||
int version = 0;
|
||||
if (stmt.step() == SQLite.SQLITE_ROW) {
|
||||
if (stmt.step() == Sqlite.ROW) {
|
||||
version = stmt.column_int(0);
|
||||
}
|
||||
|
||||
return version;
|
||||
|
||||
} catch (Error e) {
|
||||
throw throw new Error.FAILED("Failed to get migration version: %s".printf(e.message));
|
||||
throw new DBError.FAILED("Failed to get migration version: %s".printf(e.message));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,23 +141,23 @@ public class RSSuper.Database : Object {
|
||||
* Execute a SQL statement
|
||||
*/
|
||||
public void execute(string sql) throws Error {
|
||||
string errmsg = null;
|
||||
int result = db.exec(sql, null, null, out errmsg);
|
||||
string? errmsg;
|
||||
int result = db.exec(sql, null, out errmsg);
|
||||
|
||||
if (result != SQLite.SQLITE_OK) {
|
||||
throw throw new Error.FAILED("SQL execution failed: %s\nSQL: %s".printf(errmsg, sql));
|
||||
if (result != Sqlite.OK) {
|
||||
throw new DBError.FAILED("SQL execution failed: %s\nSQL: %s".printf(errmsg, sql));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare a SQL statement
|
||||
*/
|
||||
public SQLite.Stmt prepare(string sql) throws Error {
|
||||
SQLite.Stmt stmt;
|
||||
public Sqlite.Statement prepare(string sql) throws Error {
|
||||
Sqlite.Statement stmt;
|
||||
int result = db.prepare_v2(sql, -1, out stmt, null);
|
||||
|
||||
if (result != SQLite.SQLITE_OK) {
|
||||
throw throw new Error.FAILED("Failed to prepare statement: %s\nSQL: %s".printf(db.errmsg(), sql));
|
||||
if (result != Sqlite.OK) {
|
||||
throw new DBError.FAILED("Failed to prepare statement: %s\nSQL: %s".printf(db.errmsg(), sql));
|
||||
}
|
||||
|
||||
return stmt;
|
||||
@@ -166,7 +166,7 @@ public class RSSuper.Database : Object {
|
||||
/**
|
||||
* Get the database connection handle
|
||||
*/
|
||||
public SQLite.DB get_handle() {
|
||||
public unowned Sqlite.Database get_handle() {
|
||||
return db;
|
||||
}
|
||||
|
||||
|
||||
24
native-route/linux/src/database/db-error.vala
Normal file
24
native-route/linux/src/database/db-error.vala
Normal file
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* DBError.vala
|
||||
*
|
||||
* Database error domain definition.
|
||||
*/
|
||||
|
||||
namespace RSSuper {
|
||||
/**
|
||||
* DBError - Database error domain
|
||||
*/
|
||||
public errordomain DBError {
|
||||
FAILED, /** Generic database operation failed */
|
||||
NOT_FOUND, /** Record not found */
|
||||
DUPLICATE, /** Duplicate key or record */
|
||||
CORRUPTED, /** Database is corrupted */
|
||||
TIMEOUT, /** Operation timed out */
|
||||
INVALID_STATE, /** Invalid database state */
|
||||
MIGRATION_FAILED, /** Migration failed */
|
||||
CONSTRAINT_FAILED, /** Constraint violation */
|
||||
FOREIGN_KEY_FAILED, /** Foreign key constraint failed */
|
||||
UNIQUE_FAILED, /** Unique constraint failed */
|
||||
CHECK_FAILED, /** Check constraint failed */
|
||||
}
|
||||
}
|
||||
@@ -98,7 +98,7 @@ public class RSSuper.FeedItemStore : Object {
|
||||
|
||||
stmt.bind_text(1, id, -1, null);
|
||||
|
||||
if (stmt.step() == SQLite.SQLITE_ROW) {
|
||||
if (stmt.step() == Sqlite.ROW) {
|
||||
return row_to_item(stmt);
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@ public class RSSuper.FeedItemStore : Object {
|
||||
|
||||
stmt.bind_text(1, subscription_id, -1, null);
|
||||
|
||||
while (stmt.step() == SQLite.SQLITE_ROW) {
|
||||
while (stmt.step() == Sqlite.ROW) {
|
||||
var item = row_to_item(stmt);
|
||||
if (item != null) {
|
||||
items.append(item);
|
||||
@@ -144,7 +144,7 @@ public class RSSuper.FeedItemStore : Object {
|
||||
"FROM feed_items ORDER BY published DESC LIMIT 1000;"
|
||||
);
|
||||
|
||||
while (stmt.step() == SQLite.SQLITE_ROW) {
|
||||
while (stmt.step() == Sqlite.ROW) {
|
||||
var item = row_to_item(stmt);
|
||||
if (item != null) {
|
||||
items.append(item);
|
||||
@@ -174,7 +174,7 @@ public class RSSuper.FeedItemStore : Object {
|
||||
stmt.bind_text(1, query, -1, null);
|
||||
stmt.bind_int(2, limit);
|
||||
|
||||
while (stmt.step() == SQLite.SQLITE_ROW) {
|
||||
while (stmt.step() == Sqlite.ROW) {
|
||||
var item = row_to_item(stmt);
|
||||
if (item != null) {
|
||||
items.append(item);
|
||||
@@ -242,7 +242,7 @@ public class RSSuper.FeedItemStore : Object {
|
||||
"ORDER BY published DESC LIMIT 100;"
|
||||
);
|
||||
|
||||
while (stmt.step() == SQLite.SQLITE_ROW) {
|
||||
while (stmt.step() == Sqlite.ROW) {
|
||||
var item = row_to_item(stmt);
|
||||
if (item != null) {
|
||||
items.append(item);
|
||||
@@ -266,7 +266,7 @@ public class RSSuper.FeedItemStore : Object {
|
||||
"ORDER BY published DESC LIMIT 100;"
|
||||
);
|
||||
|
||||
while (stmt.step() == SQLite.SQLITE_ROW) {
|
||||
while (stmt.step() == Sqlite.ROW) {
|
||||
var item = row_to_item(stmt);
|
||||
if (item != null) {
|
||||
items.append(item);
|
||||
@@ -326,7 +326,7 @@ public class RSSuper.FeedItemStore : Object {
|
||||
/**
|
||||
* Convert a database row to a FeedItem
|
||||
*/
|
||||
private FeedItem? row_to_item(SQLite.Stmt stmt) {
|
||||
private FeedItem? row_to_item(Sqlite.Statement stmt) {
|
||||
try {
|
||||
string categories_str = stmt.column_text(9);
|
||||
string[] categories = parse_categories(categories_str);
|
||||
|
||||
@@ -74,7 +74,7 @@ public class RSSuper.SearchHistoryStore : Object {
|
||||
|
||||
stmt.bind_int(1, limit);
|
||||
|
||||
while (stmt.step() == SQLite.SQLITE_ROW) {
|
||||
while (stmt.step() == Sqlite.ROW) {
|
||||
var query = row_to_query(stmt);
|
||||
queries.append(query);
|
||||
}
|
||||
@@ -101,7 +101,7 @@ public class RSSuper.SearchHistoryStore : Object {
|
||||
|
||||
stmt.bind_text(1, threshold, -1, null);
|
||||
|
||||
while (stmt.step() == SQLite.SQLITE_ROW) {
|
||||
while (stmt.step() == Sqlite.ROW) {
|
||||
var query = row_to_query(stmt);
|
||||
queries.append(query);
|
||||
}
|
||||
@@ -148,7 +148,7 @@ public class RSSuper.SearchHistoryStore : Object {
|
||||
/**
|
||||
* Convert a database row to a SearchQuery
|
||||
*/
|
||||
private SearchQuery row_to_query(SQLite.Stmt stmt) {
|
||||
private SearchQuery row_to_query(Sqlite.Statement stmt) {
|
||||
string query_str = stmt.column_text(0);
|
||||
string? filters_json = stmt.column_text(1);
|
||||
string sort_str = stmt.column_text(2);
|
||||
|
||||
@@ -7,16 +7,19 @@ namespace SQLite {
|
||||
[CCode (cname = "sqlite3", free_function = "sqlite3_close")]
|
||||
public class DB {
|
||||
[CCode (cname = "sqlite3_open")]
|
||||
public static int open(string filename, [CCode (array_length = false)] out DB db);
|
||||
public static int open(string filename, out DB db);
|
||||
|
||||
[CCode (cname = "sqlite3_close")]
|
||||
public int close();
|
||||
|
||||
[CCode (cname = "sqlite3_exec")]
|
||||
public int exec(string sql, [CCode (array_length = false)] DBCallback callback = null, void* arg = null, [CCode (array_length = false)] out string errmsg = null);
|
||||
public int exec(string sql, DBCallback? callback = null, void* arg = null, [CCode (array_length = false)] out string? errmsg = null);
|
||||
|
||||
[CCode (cname = "sqlite3_errmsg")]
|
||||
public unowned string errmsg();
|
||||
|
||||
[CCode (cname = "sqlite3_prepare_v2")]
|
||||
public int prepare_v2(string zSql, int nByte, [CCode (array_length = false)] out Stmt stmt, void* pzTail = null);
|
||||
public int prepare_v2(string zSql, int nByte, out Stmt stmt, void* pzTail = null);
|
||||
}
|
||||
|
||||
[CCode (cname = "sqlite3_stmt", free_function = "sqlite3_finalize")]
|
||||
@@ -47,6 +50,9 @@ namespace SQLite {
|
||||
|
||||
[CCode (cname = "sqlite3_bind_null")]
|
||||
public int bind_null(int i);
|
||||
|
||||
[CCode (cname = "sqlite3_finalize")]
|
||||
public int finalize();
|
||||
}
|
||||
|
||||
[CCode (cname = "SQLITE_OK")]
|
||||
|
||||
@@ -76,7 +76,7 @@ public class RSSuper.SubscriptionStore : Object {
|
||||
|
||||
stmt.bind_text(1, id, -1, null);
|
||||
|
||||
if (stmt.step() == SQLite.SQLITE_ROW) {
|
||||
if (stmt.step() == Sqlite.ROW) {
|
||||
return row_to_subscription(stmt);
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ public class RSSuper.SubscriptionStore : Object {
|
||||
"FROM feed_subscriptions ORDER BY title;"
|
||||
);
|
||||
|
||||
while (stmt.step() == SQLite.SQLITE_ROW) {
|
||||
while (stmt.step() == Sqlite.ROW) {
|
||||
var subscription = row_to_subscription(stmt);
|
||||
if (subscription != null) {
|
||||
subscriptions.append(subscription);
|
||||
@@ -166,7 +166,7 @@ public class RSSuper.SubscriptionStore : Object {
|
||||
"FROM feed_subscriptions WHERE enabled = 1 ORDER BY title;"
|
||||
);
|
||||
|
||||
while (stmt.step() == SQLite.SQLITE_ROW) {
|
||||
while (stmt.step() == Sqlite.ROW) {
|
||||
var subscription = row_to_subscription(stmt);
|
||||
if (subscription != null) {
|
||||
subscriptions.append(subscription);
|
||||
@@ -194,7 +194,7 @@ public class RSSuper.SubscriptionStore : Object {
|
||||
|
||||
stmt.bind_text(1, now_str, -1, null);
|
||||
|
||||
while (stmt.step() == SQLite.SQLITE_ROW) {
|
||||
while (stmt.step() == Sqlite.ROW) {
|
||||
var subscription = row_to_subscription(stmt);
|
||||
if (subscription != null) {
|
||||
subscriptions.append(subscription);
|
||||
@@ -207,7 +207,7 @@ public class RSSuper.SubscriptionStore : Object {
|
||||
/**
|
||||
* Convert a database row to a FeedSubscription
|
||||
*/
|
||||
private FeedSubscription? row_to_subscription(SQLite.Stmt stmt) {
|
||||
private FeedSubscription? row_to_subscription(Sqlite.Statement stmt) {
|
||||
try {
|
||||
var subscription = new FeedSubscription.with_values(
|
||||
stmt.column_text(0), // id
|
||||
|
||||
@@ -4,285 +4,413 @@
|
||||
* Unit tests for database layer.
|
||||
*/
|
||||
|
||||
public class RSSuper.DatabaseTests : TestCase {
|
||||
public class RSSuper.DatabaseTests {
|
||||
private Database? db;
|
||||
private string test_db_path;
|
||||
|
||||
public override void setUp() {
|
||||
base.setUp();
|
||||
test_db_path = "/tmp/rssuper_test_%d.db".printf((int)Time.get_current_time());
|
||||
public void run_subscription_crud() {
|
||||
try {
|
||||
test_db_path = "/tmp/rssuper_test_%d.db".printf((int)new DateTime.now_local().to_unix());
|
||||
db = new Database(test_db_path);
|
||||
} catch (DBError e) {
|
||||
warning("Failed to create test database: %s", e.message);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
db = new Database(test_db_path);
|
||||
} catch (DatabaseError e) {
|
||||
warn("Failed to create test database: %s", e.message);
|
||||
var store = new SubscriptionStore(db);
|
||||
|
||||
// Create test subscription
|
||||
var subscription = new FeedSubscription.with_values(
|
||||
"sub_1",
|
||||
"https://example.com/feed.xml",
|
||||
"Example Feed",
|
||||
60,
|
||||
"Technology",
|
||||
true,
|
||||
"2024-01-01T00:00:00Z",
|
||||
"2024-01-01T00:00:00Z"
|
||||
);
|
||||
|
||||
// Test add
|
||||
store.add(subscription);
|
||||
var retrieved = store.get_by_id("sub_1");
|
||||
if (retrieved == null) {
|
||||
printerr("FAIL: Expected subscription to exist after add\n");
|
||||
return;
|
||||
}
|
||||
|
||||
// Test get
|
||||
if (retrieved.title != "Example Feed") {
|
||||
printerr("FAIL: Expected title 'Example Feed', got '%s'\n", retrieved.title);
|
||||
return;
|
||||
}
|
||||
if (retrieved.url != "https://example.com/feed.xml") {
|
||||
printerr("FAIL: Expected url 'https://example.com/feed.xml', got '%s'\n", retrieved.url);
|
||||
return;
|
||||
}
|
||||
|
||||
// Test update
|
||||
retrieved.title = "Updated Feed";
|
||||
store.update(retrieved);
|
||||
var updated = store.get_by_id("sub_1");
|
||||
if (updated.title != "Updated Feed") {
|
||||
printerr("FAIL: Expected title 'Updated Feed', got '%s'\n", updated.title);
|
||||
return;
|
||||
}
|
||||
|
||||
// Test delete
|
||||
store.remove_subscription("sub_1");
|
||||
var deleted = store.get_by_id("sub_1");
|
||||
if (deleted != null) {
|
||||
printerr("FAIL: Expected subscription to be deleted\n");
|
||||
return;
|
||||
}
|
||||
|
||||
print("PASS: test_subscription_crud\n");
|
||||
} finally {
|
||||
cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
public override void tearDown() {
|
||||
base.tearDown();
|
||||
public void run_subscription_list() {
|
||||
try {
|
||||
test_db_path = "/tmp/rssuper_test_%d.db".printf((int)new DateTime.now_local().to_unix());
|
||||
db = new Database(test_db_path);
|
||||
} catch (DBError e) {
|
||||
warning("Failed to create test database: %s", e.message);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
var store = new SubscriptionStore(db);
|
||||
|
||||
// Add multiple subscriptions
|
||||
var sub1 = new FeedSubscription.with_values("sub_1", "https://feed1.com", "Feed 1");
|
||||
var sub2 = new FeedSubscription.with_values("sub_2", "https://feed2.com", "Feed 2");
|
||||
var sub3 = new FeedSubscription.with_values("sub_3", "https://feed3.com", "Feed 3", 60, null, false);
|
||||
|
||||
store.add(sub1);
|
||||
store.add(sub2);
|
||||
store.add(sub3);
|
||||
|
||||
// Test get_all
|
||||
var all = store.get_all();
|
||||
if (all.length != 3) {
|
||||
printerr("FAIL: Expected 3 subscriptions, got %d\n", all.length);
|
||||
return;
|
||||
}
|
||||
|
||||
// Test get_enabled
|
||||
var enabled = store.get_enabled();
|
||||
if (enabled.length != 2) {
|
||||
printerr("FAIL: Expected 2 enabled subscriptions, got %d\n", enabled.length);
|
||||
return;
|
||||
}
|
||||
|
||||
print("PASS: test_subscription_list\n");
|
||||
} finally {
|
||||
cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
public void run_feed_item_crud() {
|
||||
try {
|
||||
test_db_path = "/tmp/rssuper_test_%d.db".printf((int)new DateTime.now_local().to_unix());
|
||||
db = new Database(test_db_path);
|
||||
} catch (DBError e) {
|
||||
warning("Failed to create test database: %s", e.message);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
var sub_store = new SubscriptionStore(db);
|
||||
var item_store = new FeedItemStore(db);
|
||||
|
||||
// Create subscription first
|
||||
var subscription = new FeedSubscription.with_values(
|
||||
"sub_1", "https://example.com/feed.xml", "Example Feed"
|
||||
);
|
||||
sub_store.add(subscription);
|
||||
|
||||
// Create test item
|
||||
var item = new FeedItem.with_values(
|
||||
"item_1",
|
||||
"Test Article",
|
||||
"https://example.com/article",
|
||||
"This is a test description",
|
||||
"Full content of the article",
|
||||
"John Doe",
|
||||
"2024-01-01T12:00:00Z",
|
||||
"2024-01-01T12:00:00Z",
|
||||
{"Technology", "News"},
|
||||
null, null, null, null,
|
||||
"Example Feed"
|
||||
);
|
||||
|
||||
// Test add
|
||||
item_store.add(item);
|
||||
var retrieved = item_store.get_by_id("item_1");
|
||||
if (retrieved == null) {
|
||||
printerr("FAIL: Expected item to exist after add\n");
|
||||
return;
|
||||
}
|
||||
if (retrieved.title != "Test Article") {
|
||||
printerr("FAIL: Expected title 'Test Article', got '%s'\n", retrieved.title);
|
||||
return;
|
||||
}
|
||||
|
||||
// Test get by subscription
|
||||
var items = item_store.get_by_subscription("sub_1");
|
||||
if (items.length != 1) {
|
||||
printerr("FAIL: Expected 1 item, got %d\n", items.length);
|
||||
return;
|
||||
}
|
||||
|
||||
// Test mark as read
|
||||
item_store.mark_as_read("item_1");
|
||||
|
||||
// Test delete
|
||||
item_store.delete("item_1");
|
||||
var deleted = item_store.get_by_id("item_1");
|
||||
if (deleted != null) {
|
||||
printerr("FAIL: Expected item to be deleted\n");
|
||||
return;
|
||||
}
|
||||
|
||||
print("PASS: test_feed_item_crud\n");
|
||||
} finally {
|
||||
cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
public void run_feed_item_batch() {
|
||||
try {
|
||||
test_db_path = "/tmp/rssuper_test_%d.db".printf((int)new DateTime.now_local().to_unix());
|
||||
db = new Database(test_db_path);
|
||||
} catch (DBError e) {
|
||||
warning("Failed to create test database: %s", e.message);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
var sub_store = new SubscriptionStore(db);
|
||||
var item_store = new FeedItemStore(db);
|
||||
|
||||
// Create subscription
|
||||
var subscription = new FeedSubscription.with_values(
|
||||
"sub_1", "https://example.com/feed.xml", "Example Feed"
|
||||
);
|
||||
sub_store.add(subscription);
|
||||
|
||||
// Create multiple items
|
||||
var items = new FeedItem[5];
|
||||
for (var i = 0; i < 5; i++) {
|
||||
items[i] = new FeedItem.with_values(
|
||||
"item_%d".printf(i),
|
||||
"Article %d".printf(i),
|
||||
"https://example.com/article%d".printf(i),
|
||||
"Description %d".printf(i),
|
||||
null,
|
||||
"Author %d".printf(i),
|
||||
"2024-01-%02dT12:00:00Z".printf(i + 1),
|
||||
null,
|
||||
null,
|
||||
null, null, null, null,
|
||||
"Example Feed"
|
||||
);
|
||||
}
|
||||
|
||||
// Test batch insert
|
||||
item_store.add_batch(items);
|
||||
|
||||
var all = item_store.get_by_subscription("sub_1");
|
||||
if (all.length != 5) {
|
||||
printerr("FAIL: Expected 5 items, got %d\n", all.length);
|
||||
return;
|
||||
}
|
||||
|
||||
print("PASS: test_feed_item_batch\n");
|
||||
} finally {
|
||||
cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
public void run_search_history() {
|
||||
try {
|
||||
test_db_path = "/tmp/rssuper_test_%d.db".printf((int)new DateTime.now_local().to_unix());
|
||||
db = new Database(test_db_path);
|
||||
} catch (DBError e) {
|
||||
warning("Failed to create test database: %s", e.message);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
var store = new SearchHistoryStore(db);
|
||||
|
||||
// Create test queries
|
||||
var query1 = SearchQuery("test query", 1, 20, null, SearchSortOption.RELEVANCE);
|
||||
var query2 = SearchQuery("another search", 1, 10, null, SearchSortOption.DATE_DESC);
|
||||
|
||||
// Test record
|
||||
store.record_search(query1, 15);
|
||||
store.record_search(query2, 8);
|
||||
|
||||
// Test get_history
|
||||
var history = store.get_history();
|
||||
if (history.length != 2) {
|
||||
printerr("FAIL: Expected 2 history entries, got %d\n", history.length);
|
||||
return;
|
||||
}
|
||||
if (history[0].query != "another search") {
|
||||
printerr("FAIL: Expected 'another search', got '%s'\n", history[0].query);
|
||||
return;
|
||||
}
|
||||
|
||||
// Test get_recent
|
||||
var recent = store.get_recent();
|
||||
if (recent.length != 2) {
|
||||
printerr("FAIL: Expected 2 recent entries, got %d\n", recent.length);
|
||||
return;
|
||||
}
|
||||
|
||||
print("PASS: test_search_history\n");
|
||||
} finally {
|
||||
cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
public void run_fts_search() {
|
||||
try {
|
||||
test_db_path = "/tmp/rssuper_test_%d.db".printf((int)new DateTime.now_local().to_unix());
|
||||
db = new Database(test_db_path);
|
||||
} catch (DBError e) {
|
||||
warning("Failed to create test database: %s", e.message);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
var sub_store = new SubscriptionStore(db);
|
||||
var item_store = new FeedItemStore(db);
|
||||
|
||||
// Create subscription
|
||||
var subscription = new FeedSubscription.with_values(
|
||||
"sub_1", "https://example.com/feed.xml", "Example Feed"
|
||||
);
|
||||
sub_store.add(subscription);
|
||||
|
||||
// Add items with searchable content
|
||||
var item1 = new FeedItem.with_values(
|
||||
"item_1",
|
||||
"Swift Programming Guide",
|
||||
"https://example.com/swift",
|
||||
"Learn Swift programming language basics",
|
||||
"A comprehensive guide to Swift",
|
||||
"Apple Developer",
|
||||
"2024-01-01T12:00:00Z",
|
||||
null,
|
||||
null,
|
||||
null, null, null, null,
|
||||
"Example Feed"
|
||||
);
|
||||
|
||||
var item2 = new FeedItem.with_values(
|
||||
"item_2",
|
||||
"Python for Data Science",
|
||||
"https://example.com/python",
|
||||
"Data analysis with Python and pandas",
|
||||
"Complete Python data science tutorial",
|
||||
"Data Team",
|
||||
"2024-01-02T12:00:00Z",
|
||||
null,
|
||||
null,
|
||||
null, null, null, null,
|
||||
"Example Feed"
|
||||
);
|
||||
|
||||
item_store.add(item1);
|
||||
item_store.add(item2);
|
||||
|
||||
// Test FTS search
|
||||
var results = item_store.search("swift");
|
||||
if (results.length != 1) {
|
||||
printerr("FAIL: Expected 1 result for 'swift', got %d\n", results.length);
|
||||
return;
|
||||
}
|
||||
if (results[0].title != "Swift Programming Guide") {
|
||||
printerr("FAIL: Expected 'Swift Programming Guide', got '%s'\n", results[0].title);
|
||||
return;
|
||||
}
|
||||
|
||||
results = item_store.search("python");
|
||||
if (results.length != 1) {
|
||||
printerr("FAIL: Expected 1 result for 'python', got %d\n", results.length);
|
||||
return;
|
||||
}
|
||||
if (results[0].title != "Python for Data Science") {
|
||||
printerr("FAIL: Expected 'Python for Data Science', got '%s'\n", results[0].title);
|
||||
return;
|
||||
}
|
||||
|
||||
print("PASS: test_fts_search\n");
|
||||
} finally {
|
||||
cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
private void cleanup() {
|
||||
if (db != null) {
|
||||
db.close();
|
||||
db = null;
|
||||
}
|
||||
|
||||
// Clean up test database
|
||||
var file = File.new_for_path(test_db_path);
|
||||
if (file.query_exists()) {
|
||||
try {
|
||||
file.delete();
|
||||
} catch (DatabaseError e) {
|
||||
warn("Failed to delete test database: %s", e.message);
|
||||
if (test_db_path != null && test_db_path.length > 0) {
|
||||
var file = File.new_for_path(test_db_path);
|
||||
if (file.query_exists()) {
|
||||
try {
|
||||
file.delete();
|
||||
} catch (Error e) {
|
||||
warning("Failed to delete test database: %s", e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up WAL file
|
||||
var wal_file = File.new_for_path(test_db_path + "-wal");
|
||||
if (wal_file.query_exists()) {
|
||||
try {
|
||||
wal_file.delete();
|
||||
} catch (Error e) {
|
||||
warning("Failed to delete WAL file: %s", e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up WAL file
|
||||
var wal_file = File.new_for_path(test_db_path + "-wal");
|
||||
if (wal_file.query_exists()) {
|
||||
try {
|
||||
wal_file.delete();
|
||||
} catch (DatabaseError e) {
|
||||
warn("Failed to delete WAL file: %s", e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void test_subscription_crud() {
|
||||
if (db == null) return;
|
||||
|
||||
var store = new SubscriptionStore(db);
|
||||
|
||||
// Create test subscription
|
||||
var subscription = new FeedSubscription.with_values(
|
||||
"sub_1",
|
||||
"https://example.com/feed.xml",
|
||||
"Example Feed",
|
||||
60,
|
||||
"Technology",
|
||||
true,
|
||||
"2024-01-01T00:00:00Z",
|
||||
"2024-01-01T00:00:00Z"
|
||||
);
|
||||
|
||||
// Test add
|
||||
store.add(subscription);
|
||||
assert_not_null(store.get_by_id("sub_1"));
|
||||
|
||||
// Test get
|
||||
var retrieved = store.get_by_id("sub_1");
|
||||
assert_not_null(retrieved);
|
||||
assert_equal("Example Feed", retrieved.title);
|
||||
assert_equal("https://example.com/feed.xml", retrieved.url);
|
||||
|
||||
// Test update
|
||||
retrieved.title = "Updated Feed";
|
||||
store.update(retrieved);
|
||||
var updated = store.get_by_id("sub_1");
|
||||
assert_equal("Updated Feed", updated.title);
|
||||
|
||||
// Test delete
|
||||
store.delete("sub_1");
|
||||
var deleted = store.get_by_id("sub_1");
|
||||
assert_null(deleted);
|
||||
}
|
||||
|
||||
public void test_subscription_list() {
|
||||
if (db == null) return;
|
||||
|
||||
var store = new SubscriptionStore(db);
|
||||
|
||||
// Add multiple subscriptions
|
||||
var sub1 = new FeedSubscription.with_values("sub_1", "https://feed1.com", "Feed 1");
|
||||
var sub2 = new FeedSubscription.with_values("sub_2", "https://feed2.com", "Feed 2");
|
||||
var sub3 = new FeedSubscription.with_values("sub_3", "https://feed3.com", "Feed 3", 60, null, false);
|
||||
|
||||
store.add(sub1);
|
||||
store.add(sub2);
|
||||
store.add(sub3);
|
||||
|
||||
// Test get_all
|
||||
var all = store.get_all();
|
||||
assert_equal(3, all.length);
|
||||
|
||||
// Test get_enabled
|
||||
var enabled = store.get_enabled();
|
||||
assert_equal(2, enabled.length);
|
||||
}
|
||||
|
||||
public void test_feed_item_crud() {
|
||||
if (db == null) return;
|
||||
|
||||
var sub_store = new SubscriptionStore(db);
|
||||
var item_store = new FeedItemStore(db);
|
||||
|
||||
// Create subscription first
|
||||
var subscription = new FeedSubscription.with_values(
|
||||
"sub_1", "https://example.com/feed.xml", "Example Feed"
|
||||
);
|
||||
sub_store.add(subscription);
|
||||
|
||||
// Create test item
|
||||
var item = new FeedItem.with_values(
|
||||
"item_1",
|
||||
"Test Article",
|
||||
"https://example.com/article",
|
||||
"This is a test description",
|
||||
"Full content of the article",
|
||||
"John Doe",
|
||||
"2024-01-01T12:00:00Z",
|
||||
"2024-01-01T12:00:00Z",
|
||||
{"Technology", "News"},
|
||||
null, null, null, null,
|
||||
"Example Feed"
|
||||
);
|
||||
|
||||
// Test add
|
||||
item_store.add(item);
|
||||
var retrieved = item_store.get_by_id("item_1");
|
||||
assert_not_null(retrieved);
|
||||
assert_equal("Test Article", retrieved.title);
|
||||
|
||||
// Test get by subscription
|
||||
var items = item_store.get_by_subscription("sub_1");
|
||||
assert_equal(1, items.length);
|
||||
|
||||
// Test mark as read
|
||||
item_store.mark_as_read("item_1");
|
||||
|
||||
// Test delete
|
||||
item_store.delete("item_1");
|
||||
var deleted = item_store.get_by_id("item_1");
|
||||
assert_null(deleted);
|
||||
}
|
||||
|
||||
public void test_feed_item_batch() {
|
||||
if (db == null) return;
|
||||
|
||||
var sub_store = new SubscriptionStore(db);
|
||||
var item_store = new FeedItemStore(db);
|
||||
|
||||
// Create subscription
|
||||
var subscription = new FeedSubscription.with_values(
|
||||
"sub_1", "https://example.com/feed.xml", "Example Feed"
|
||||
);
|
||||
sub_store.add(subscription);
|
||||
|
||||
// Create multiple items
|
||||
var items = new FeedItem[5];
|
||||
for (var i = 0; i < 5; i++) {
|
||||
items[i] = new FeedItem.with_values(
|
||||
"item_%d".printf(i),
|
||||
"Article %d".printf(i),
|
||||
"https://example.com/article%d".printf(i),
|
||||
"Description %d".printf(i),
|
||||
null,
|
||||
"Author %d".printf(i),
|
||||
"2024-01-%02dT12:00:00Z".printf(i + 1),
|
||||
null,
|
||||
null,
|
||||
null, null, null, null,
|
||||
"Example Feed"
|
||||
);
|
||||
}
|
||||
|
||||
// Test batch insert
|
||||
item_store.add_batch(items);
|
||||
|
||||
var all = item_store.get_by_subscription("sub_1");
|
||||
assert_equal(5, all.length);
|
||||
}
|
||||
|
||||
public void test_search_history() {
|
||||
if (db == null) return;
|
||||
|
||||
var store = new SearchHistoryStore(db);
|
||||
|
||||
// Create test queries
|
||||
var query1 = SearchQuery("test query", 1, 20, null, SearchSortOption.RELEVANCE);
|
||||
var query2 = SearchQuery("another search", 1, 10, null, SearchSortOption.DATE_DESC);
|
||||
|
||||
// Test record
|
||||
store.record_search(query1, 15);
|
||||
store.record_search(query2, 8);
|
||||
|
||||
// Test get_history
|
||||
var history = store.get_history();
|
||||
assert_equal(2, history.length);
|
||||
assert_equal("another search", history[0].query); // Most recent first
|
||||
|
||||
// Test get_recent
|
||||
var recent = store.get_recent();
|
||||
assert_equal(2, recent.length);
|
||||
}
|
||||
|
||||
public void test_fts_search() {
|
||||
if (db == null) return;
|
||||
|
||||
var sub_store = new SubscriptionStore(db);
|
||||
var item_store = new FeedItemStore(db);
|
||||
|
||||
// Create subscription
|
||||
var subscription = new FeedSubscription.with_values(
|
||||
"sub_1", "https://example.com/feed.xml", "Example Feed"
|
||||
);
|
||||
sub_store.add(subscription);
|
||||
|
||||
// Add items with searchable content
|
||||
var item1 = new FeedItem.with_values(
|
||||
"item_1",
|
||||
"Swift Programming Guide",
|
||||
"https://example.com/swift",
|
||||
"Learn Swift programming language basics",
|
||||
"A comprehensive guide to Swift",
|
||||
"Apple Developer",
|
||||
"2024-01-01T12:00:00Z",
|
||||
null,
|
||||
null,
|
||||
null, null, null, null,
|
||||
"Example Feed"
|
||||
);
|
||||
|
||||
var item2 = new FeedItem.with_values(
|
||||
"item_2",
|
||||
"Python for Data Science",
|
||||
"https://example.com/python",
|
||||
"Data analysis with Python and pandas",
|
||||
"Complete Python data science tutorial",
|
||||
"Data Team",
|
||||
"2024-01-02T12:00:00Z",
|
||||
null,
|
||||
null,
|
||||
null, null, null, null,
|
||||
"Example Feed"
|
||||
);
|
||||
|
||||
item_store.add(item1);
|
||||
item_store.add(item2);
|
||||
|
||||
// Test FTS search
|
||||
var results = item_store.search("swift");
|
||||
assert_equal(1, results.length);
|
||||
assert_equal("Swift Programming Guide", results[0].title);
|
||||
|
||||
results = item_store.search("python");
|
||||
assert_equal(1, results.length);
|
||||
assert_equal("Python for Data Science", results[0].title);
|
||||
}
|
||||
|
||||
public static int main(string[] args) {
|
||||
Test.init(ref args);
|
||||
print("Running database tests...\n");
|
||||
|
||||
var suite = Test.create_suite();
|
||||
var tests = new DatabaseTests();
|
||||
|
||||
var test_case = new DatabaseTests();
|
||||
Test.add_func("/database/subscription_crud", test_case.test_subscription_crud);
|
||||
Test.add_func("/database/subscription_list", test_case.test_subscription_list);
|
||||
Test.add_func("/database/feed_item_crud", test_case.test_feed_item_crud);
|
||||
Test.add_func("/database/feed_item_batch", test_case.test_feed_item_batch);
|
||||
Test.add_func("/database/search_history", test_case.test_search_history);
|
||||
Test.add_func("/database/fts_search", test_case.test_fts_search);
|
||||
print("\n=== Running subscription CRUD tests ===");
|
||||
tests.run_subscription_crud();
|
||||
|
||||
return Test.run();
|
||||
print("\n=== Running subscription list tests ===");
|
||||
tests.run_subscription_list();
|
||||
|
||||
print("\n=== Running feed item CRUD tests ===");
|
||||
tests.run_feed_item_crud();
|
||||
|
||||
print("\n=== Running feed item batch tests ===");
|
||||
tests.run_feed_item_batch();
|
||||
|
||||
print("\n=== Running search history tests ===");
|
||||
tests.run_search_history();
|
||||
|
||||
print("\n=== Running FTS search tests ===");
|
||||
tests.run_fts_search();
|
||||
|
||||
print("\nAll tests completed!\n");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user