Gives HistoryDb an in-memory mode and reconcile() an injectable deployments list so HistoryService can be exercised hermetically, without touching the real history.db or shelling out to rpm-ostree. Covers bootstrap, idempotent reconcile, install-then-reboot, manual/detected changes, last_known_safe, and chain walking. Also fixes the pre-existing widget boot-smoke test: startup now makes a second real Process.run call (via HistoryService.reconcile), which tipped flutter_test's fake-time zone into flagging a dangling Timer from that real I/O. Wrapping the pump in tester.runAsync() is the standard fix. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
90 lines
2.7 KiB
Dart
90 lines
2.7 KiB
Dart
import 'dart:ffi';
|
|
import 'dart:io';
|
|
|
|
import 'package:sqlite3/open.dart';
|
|
import 'package:sqlite3/sqlite3.dart';
|
|
|
|
/// Opens the local layer-history database.
|
|
///
|
|
/// Fedora/Aurora ship `libsqlite3.so.0` but not the unversioned
|
|
/// `libsqlite3.so` symlink that `package:sqlite3` looks for by default —
|
|
/// that symlink only shows up with `sqlite-devel`, a layer we don't want to
|
|
/// require just to run this app. Point `dlopen` at the versioned name
|
|
/// instead, same lib, no extra layer needed.
|
|
class HistoryDb {
|
|
static Database? _instance;
|
|
static bool _useInMemory = false;
|
|
|
|
static String get _stateDir => '${Platform.environment['HOME']}/.local/state/layer_wizard';
|
|
static String get _dbPath => '$_stateDir/history.db';
|
|
|
|
static Database database() {
|
|
final existing = _instance;
|
|
if (existing != null) return existing;
|
|
|
|
if (Platform.isLinux) {
|
|
open.overrideFor(OperatingSystem.linux, _openLinuxLibSqlite3);
|
|
}
|
|
|
|
final db = _useInMemory ? sqlite3.openInMemory() : _openOnDisk();
|
|
_migrate(db);
|
|
_instance = db;
|
|
return db;
|
|
}
|
|
|
|
static Database _openOnDisk() {
|
|
final dir = Directory(_stateDir);
|
|
if (!dir.existsSync()) {
|
|
dir.createSync(recursive: true);
|
|
}
|
|
return sqlite3.open(_dbPath);
|
|
}
|
|
|
|
/// Test-only: discards any open connection and makes the next
|
|
/// [database] call return a fresh in-memory database instead of touching
|
|
/// the real `~/.local/state/layer_wizard/history.db`. Call this from
|
|
/// `setUp` in every test that exercises [HistoryService].
|
|
static void resetForTesting() {
|
|
_instance?.dispose();
|
|
_instance = null;
|
|
_useInMemory = true;
|
|
}
|
|
|
|
static DynamicLibrary _openLinuxLibSqlite3() {
|
|
for (final name in ['libsqlite3.so.0', 'libsqlite3.so']) {
|
|
try {
|
|
return DynamicLibrary.open(name);
|
|
} catch (_) {
|
|
// Try the next candidate.
|
|
}
|
|
}
|
|
throw ArgumentError('Could not find libsqlite3.so.0 or libsqlite3.so');
|
|
}
|
|
|
|
static void _migrate(Database db) {
|
|
db.execute('''
|
|
CREATE TABLE IF NOT EXISTS nodes (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
checksum TEXT NOT NULL UNIQUE,
|
|
base_checksum TEXT,
|
|
parent_id INTEGER REFERENCES nodes(id),
|
|
action TEXT NOT NULL,
|
|
package_name TEXT,
|
|
version TEXT,
|
|
container_image_ref TEXT,
|
|
requested_packages TEXT,
|
|
source TEXT NOT NULL,
|
|
created_at TEXT NOT NULL,
|
|
applied_at TEXT,
|
|
notes TEXT
|
|
);
|
|
''');
|
|
db.execute('CREATE INDEX IF NOT EXISTS idx_nodes_parent ON nodes(parent_id);');
|
|
db.execute('''
|
|
CREATE TABLE IF NOT EXISTS pointers (
|
|
name TEXT PRIMARY KEY,
|
|
node_id INTEGER NOT NULL REFERENCES nodes(id)
|
|
);
|
|
''');
|
|
}
|
|
}
|