1 Commits

Author SHA1 Message Date
Spencer Brower
f0b12582ba chore(dev): release 0.4.0 2026-06-18 10:35:49 -04:00
8 changed files with 132 additions and 127 deletions

View File

@@ -2,6 +2,8 @@ on:
push: push:
branches: branches:
- main - main
- dev
- odin
permissions: permissions:
contents: write contents: write

View File

@@ -1,5 +1,22 @@
# Changelog # Changelog
## [0.4.0](https://github.com/sbrow/envr/compare/v0.3.0...v0.4.0) (2026-06-18)
### Features
* Removed runtime git dependency. ([12574e1](https://github.com/sbrow/envr/commit/12574e123bdedba3aca813143e906ec5e0b95719))
### Bug Fixes
* Fixed memory leaks in the db. ([5059572](https://github.com/sbrow/envr/commit/5059572951b3ec20b3d2027032a9c3be5cb14dba))
### Performance Improvements
* Replaced `fd` with custom internals. ([2ef733f](https://github.com/sbrow/envr/commit/2ef733fe58594b0a0b6e3ef85142b74af445ccb8))
## [0.3.0](https://github.com/sbrow/envr/compare/v0.2.1...v0.3.0) (2026-06-16) ## [0.3.0](https://github.com/sbrow/envr/compare/v0.2.1...v0.3.0) (2026-06-16)
Version 0.3.0 represents a significant departure (and improvement) for envr. Version 0.3.0 represents a significant departure (and improvement) for envr.

View File

@@ -54,6 +54,8 @@ cmd_check :: proc(cmd: ^Command) {
if !list_ok { if !list_ok {
return return
} }
defer delete(db_files)
defer for &file in db_files {delete_envfile(&file)}
not_backed := find_unbacked(files_in_path[:], db_files[:]) not_backed := find_unbacked(files_in_path[:], db_files[:])

View File

@@ -25,6 +25,8 @@ cmd_list :: proc(cmd: ^Command) {
if !list_ok { if !list_ok {
return return
} }
defer delete(rows)
defer for &row in rows {delete_envfile(&row)}
if terminal.is_terminal(os.stdout) { if terminal.is_terminal(os.stdout) {
headers := []string{"Directory", "Path"} headers := []string{"Directory", "Path"}

View File

@@ -25,16 +25,17 @@ Config :: struct {
config_path: string `json:"-"`, config_path: string `json:"-"`,
} }
load_config :: proc(config_path: string, allocator := context.allocator) -> (Config, bool) { load_config :: proc(config_path: string) -> (Config, bool) {
// TODO: Should we use context.allocator + defer delete()? data, read_err := os.read_entire_file_from_path(config_path, context.allocator)
data, read_err := os.read_entire_file_from_path(config_path, context.temp_allocator)
if read_err != nil { if read_err != nil {
fmt.println("No config file found. Please run `envr init` to generate one.") fmt.println("No config file found. Please run `envr init` to generate one.")
return Config{}, false return Config{}, false
} }
defer delete(data)
cfg: Config cfg: Config
err := json.unmarshal(data, &cfg, .JSON5, allocator) // TODO: use json 5
err := json.unmarshal(data, &cfg)
if err != nil { if err != nil {
fmt.printf("Error parsing config: %v\n", err) fmt.printf("Error parsing config: %v\n", err)
return Config{}, false return Config{}, false
@@ -52,22 +53,22 @@ default_config_path :: proc(home: string, allocator := context.allocator) -> str
return path return path
} }
delete_config :: proc(cfg: ^Config, allocator := context.allocator) { delete_config :: proc(cfg: ^Config) {
for key in cfg.Keys { for key in cfg.Keys {
delete(key.Private, allocator) delete(key.Private)
delete(key.Public, allocator) delete(key.Public)
} }
delete(cfg.Keys) delete(cfg.Keys)
delete(cfg.ScanConfig.Matcher, allocator) delete(cfg.ScanConfig.Matcher)
for exclude in cfg.ScanConfig.Exclude { for exclude in cfg.ScanConfig.Exclude {
delete(exclude, allocator) delete(exclude)
} }
delete(cfg.ScanConfig.Exclude) delete(cfg.ScanConfig.Exclude)
for include in cfg.ScanConfig.Include { for include in cfg.ScanConfig.Include {
delete(include, allocator) delete(include)
} }
delete(cfg.ScanConfig.Include) delete(cfg.ScanConfig.Include)
} }

110
db.odin
View File

@@ -5,7 +5,6 @@ import "core:encoding/hex"
import "core:encoding/ini" import "core:encoding/ini"
import "core:encoding/json" import "core:encoding/json"
import "core:fmt" import "core:fmt"
import "core:mem"
import "core:os" import "core:os"
import "core:path/filepath" import "core:path/filepath"
import "core:strings" import "core:strings"
@@ -32,7 +31,6 @@ Db :: struct {
db: ^rawptr, db: ^rawptr,
cfg: Config, cfg: Config,
changed: bool, changed: bool,
arena: mem.Dynamic_Arena,
} }
EnvFile :: struct { EnvFile :: struct {
@@ -53,15 +51,25 @@ delete_envfile :: proc(f: ^EnvFile) {
delete(f.contents) delete(f.contents)
} }
db_open :: proc(cfg_path: string) -> (Db, bool) { db_open :: proc(cfg_path: string) -> (database: Db, ok: bool) {
database, ok := db_init() database.cfg = load_config(cfg_path) or_return
if !ok {
return database, ok
}
database.cfg, ok = load_config(cfg_path, db_allocator(&database)) {
if !ok { db: ^rawptr
return database, ok rc := sqlite.db_open(":memory:", &db)
if rc != sqlite.OK {
fmt.printf("Error opening in-memory database: %s\n", sqlite.db_errmsg(db))
return
}
create_sql: cstring = "CREATE TABLE IF NOT EXISTS envr_env_files (path TEXT PRIMARY KEY NOT NULL, remotes TEXT, sha256 TEXT NOT NULL, contents TEXT NOT NULL)"
rc = sqlite.db_exec(db, create_sql, nil, nil, nil)
if rc != sqlite.OK {
fmt.printf("Error creating table: %s\n", sqlite.db_errmsg(db))
sqlite.db_close(db)
return
}
database.db = db
} }
// TODO: Use different allocators? // TODO: Use different allocators?
@@ -69,7 +77,7 @@ db_open :: proc(cfg_path: string) -> (Db, bool) {
if os.exists(data_path) { if os.exists(data_path) {
if ok = db_restore_from_encrypted(&database, data_path); !ok { if ok = db_restore_from_encrypted(&database, data_path); !ok {
sqlite.db_close(database.db) sqlite.db_close(database.db)
return database, ok return
} }
} else { } else {
// DB was created // DB was created
@@ -79,36 +87,6 @@ db_open :: proc(cfg_path: string) -> (Db, bool) {
return database, true return database, true
} }
// Creates a database an allocator and fresh, empty table, with zero encryption.
// In production, you most likely want to use `db_open`.
db_init :: proc() -> (database: Db, ok: bool) {
db: ^rawptr
rc := sqlite.db_open(":memory:", &db)
if rc != sqlite.OK {
fmt.printf("Error opening in-memory database: %s\n", sqlite.db_errmsg(db))
return
}
create_sql: cstring = "CREATE TABLE IF NOT EXISTS envr_env_files (path TEXT PRIMARY KEY NOT NULL, remotes TEXT, sha256 TEXT NOT NULL, contents TEXT NOT NULL)"
rc = sqlite.db_exec(db, create_sql, nil, nil, nil)
if rc != sqlite.OK {
fmt.printf("Error creating table: %s\n", sqlite.db_errmsg(db))
sqlite.db_close(db)
return
}
database.db = db
mem.dynamic_arena_init(&database.arena)
return database, true
}
// TODO: Should allocator live on the db?
db_allocator :: proc(db: ^Db) -> mem.Allocator {
return mem.dynamic_arena_allocator(&db.arena)
}
// TODO: use db allocator?
db_restore_from_encrypted :: proc(db: ^Db, data_path: string) -> bool { db_restore_from_encrypted :: proc(db: ^Db, data_path: string) -> bool {
encrypted_data, read_err := os.read_entire_file_from_path(data_path, context.allocator) encrypted_data, read_err := os.read_entire_file_from_path(data_path, context.allocator)
defer delete(encrypted_data) defer delete(encrypted_data)
@@ -117,7 +95,6 @@ db_restore_from_encrypted :: proc(db: ^Db, data_path: string) -> bool {
return false return false
} }
// TODO: allow allocator selection?
plaintext, dec_ok := decrypt(encrypted_data, db.cfg.Keys[:]) plaintext, dec_ok := decrypt(encrypted_data, db.cfg.Keys[:])
if !dec_ok { if !dec_ok {
fmt.println("Error: decryption failed") fmt.println("Error: decryption failed")
@@ -150,15 +127,9 @@ db_restore_from_encrypted :: proc(db: ^Db, data_path: string) -> bool {
return true return true
} }
// TODO: Return error enum?
db_close :: proc(d: ^Db) { db_close :: proc(d: ^Db) {
allocator := db_allocator(d) defer sqlite.db_close(d.db)
defer delete_config(&d.cfg)
defer {
sqlite.db_close(d.db)
delete_config(&d.cfg, allocator)
mem.dynamic_arena_destroy(&d.arena)
}
if d.changed { if d.changed {
rc := sqlite.db_exec(d.db, "VACUUM", nil, nil, nil) rc := sqlite.db_exec(d.db, "VACUUM", nil, nil, nil)
@@ -176,15 +147,13 @@ db_close :: proc(d: ^Db) {
defer sqlite.free(data) defer sqlite.free(data)
sqlite_data := data[:sz] sqlite_data := data[:sz]
// TODO: PAss allocator chain
// FIXME Warning gets printed in tests.
encrypted, enc_ok := encrypt(sqlite_data, d.cfg.Keys[:]) encrypted, enc_ok := encrypt(sqlite_data, d.cfg.Keys[:])
if !enc_ok { if !enc_ok {
fmt.println("Error: encryption failed") fmt.println("Error: encryption failed")
return return
} }
data_path := data_path(d.cfg.config_path, allocator) data_path := data_path(d.cfg.config_path)
envr_d := envr_dir(d.cfg.config_path) envr_d := envr_dir(d.cfg.config_path)
os.mkdir_all(envr_d) os.mkdir_all(envr_d)
@@ -199,8 +168,7 @@ db_close :: proc(d: ^Db) {
} }
} }
// Results will be freed when `db_close` is called. db_list :: proc(d: ^Db, allocator := context.allocator) -> (results: [dynamic]EnvFile, ok: bool) {
db_list :: proc(d: ^Db) -> ([]EnvFile, bool) {
stmt: ^rawptr stmt: ^rawptr
rc := sqlite.prepare_v2( rc := sqlite.prepare_v2(
d.db, d.db,
@@ -211,22 +179,18 @@ db_list :: proc(d: ^Db) -> ([]EnvFile, bool) {
) )
if rc != sqlite.OK { if rc != sqlite.OK {
fmt.printf("Error preparing query: %s\n", sqlite.db_errmsg(d.db)) fmt.printf("Error preparing query: %s\n", sqlite.db_errmsg(d.db))
return []EnvFile{}, false return
} }
defer sqlite.finalize(stmt) defer sqlite.finalize(stmt)
allocator := db_allocator(d)
// TODO: Is 10 a good size?
results := make([dynamic]EnvFile, 0, 10, allocator)
for { for {
rc = sqlite.step(stmt) rc = sqlite.step(stmt)
if rc == sqlite.DONE { if rc == sqlite.DONE {
break break
} }
#no_bounds_check if rc != sqlite.ROW { if rc != sqlite.ROW {
fmt.printf("Error stepping query: %s\n", sqlite.db_errmsg(d.db)) fmt.printf("Error stepping query: %s\n", sqlite.db_errmsg(d.db))
return results[:], false return
} }
remotes_json := string(sqlite.column_text(stmt, 1)) remotes_json := string(sqlite.column_text(stmt, 1))
@@ -248,10 +212,10 @@ db_list :: proc(d: ^Db) -> ([]EnvFile, bool) {
) )
} }
#no_bounds_check return results[:], true ok = true
return
} }
// FIXME: Needs to use the allocator
db_insert :: proc(d: ^Db, file: EnvFile) -> bool { db_insert :: proc(d: ^Db, file: EnvFile) -> bool {
remotes_json, marshal_err := json.marshal(file.Remotes) remotes_json, marshal_err := json.marshal(file.Remotes)
if marshal_err != nil { if marshal_err != nil {
@@ -314,8 +278,7 @@ db_insert :: proc(d: ^Db, file: EnvFile) -> bool {
return true return true
} }
// Result will be freed when `db_close` is called. db_fetch :: proc(d: ^Db, path: string, allocator := context.allocator) -> (EnvFile, bool) {
db_fetch :: proc(d: ^Db, path: string) -> (EnvFile, bool) {
sql: cstring = "SELECT path, remotes, sha256, contents FROM envr_env_files WHERE path = ?" sql: cstring = "SELECT path, remotes, sha256, contents FROM envr_env_files WHERE path = ?"
stmt: ^rawptr stmt: ^rawptr
rc := sqlite.prepare_v2(d.db, sql, -1, &stmt, nil) rc := sqlite.prepare_v2(d.db, sql, -1, &stmt, nil)
@@ -325,8 +288,8 @@ db_fetch :: proc(d: ^Db, path: string) -> (EnvFile, bool) {
} }
defer sqlite.finalize(stmt) defer sqlite.finalize(stmt)
// TODO: Use standard allocator + delete here? cpath := to_cstring(path, allocator)
cpath := to_cstring(path, context.temp_allocator) defer delete(cpath, allocator)
rc = sqlite.bind_text(stmt, 1, cpath, -1, nil) rc = sqlite.bind_text(stmt, 1, cpath, -1, nil)
if rc != sqlite.OK { if rc != sqlite.OK {
fmt.printf("Error binding path: %s\n", sqlite.db_errmsg(d.db)) fmt.printf("Error binding path: %s\n", sqlite.db_errmsg(d.db))
@@ -342,15 +305,13 @@ db_fetch :: proc(d: ^Db, path: string) -> (EnvFile, bool) {
return EnvFile{}, false return EnvFile{}, false
} }
allocator := db_allocator(d)
remotes_json := string(sqlite.column_text(stmt, 1)) remotes_json := string(sqlite.column_text(stmt, 1))
remotes: [dynamic]string = --- remotes: [dynamic]string = ---
if len(remotes_json) > 0 { if len(remotes_json) > 0 {
json.unmarshal_string(remotes_json, &remotes, allocator = allocator) json.unmarshal_string(remotes_json, &remotes, allocator = allocator)
} }
file_path := clone_cstring(sqlite.column_text(stmt, 0), allocator) file_path := clone_cstring(sqlite.column_text(stmt, 0))
return EnvFile { return EnvFile {
Path = file_path, Path = file_path,
@@ -372,7 +333,6 @@ db_delete :: proc(d: ^Db, path: string) -> bool {
} }
defer sqlite.finalize(stmt) defer sqlite.finalize(stmt)
// TODO: Use db allocator here?
cpath := to_cstring(path) cpath := to_cstring(path)
defer delete(cpath) defer delete(cpath)
rc = sqlite.bind_text(stmt, 1, cpath, -1, nil) rc = sqlite.bind_text(stmt, 1, cpath, -1, nil)
@@ -432,7 +392,6 @@ db_sync :: proc(d: ^Db, f: ^EnvFile) -> (SyncFlag, string) {
} }
// If SyncFlag is .BackedUp, Caller is responsible for calling delete on f.contents and f.Sha256 // If SyncFlag is .BackedUp, Caller is responsible for calling delete on f.contents and f.Sha256
// TODO: Should this use the db allocator?
env_file_sync :: proc(f: ^EnvFile, dir: SyncDirection, d: ^Db) -> (SyncFlag, string) { env_file_sync :: proc(f: ^EnvFile, dir: SyncDirection, d: ^Db) -> (SyncFlag, string) {
result: SyncFlag = {} result: SyncFlag = {}
@@ -504,7 +463,6 @@ env_file_sync :: proc(f: ^EnvFile, dir: SyncDirection, d: ^Db) -> (SyncFlag, str
return result, "" return result, ""
} }
// TODO: Should this use the db allocator?
find_moved_dirs :: proc(d: ^Db, f: ^EnvFile) -> ([dynamic]string, bool) { find_moved_dirs :: proc(d: ^Db, f: ^EnvFile) -> ([dynamic]string, bool) {
roots, roots_ok := find_git_roots(d.cfg) roots, roots_ok := find_git_roots(d.cfg)
if !roots_ok { if !roots_ok {
@@ -522,7 +480,6 @@ find_moved_dirs :: proc(d: ^Db, f: ^EnvFile) -> ([dynamic]string, bool) {
return moved, true return moved, true
} }
// TODO: Should this use the db allocator?
update_dir :: proc(f: ^EnvFile, new_dir: string) { update_dir :: proc(f: ^EnvFile, new_dir: string) {
f.Dir = new_dir f.Dir = new_dir
base := filepath.base(f.Path) base := filepath.base(f.Path)
@@ -534,8 +491,6 @@ update_dir :: proc(f: ^EnvFile, new_dir: string) {
// Loads the contents of the the file at f.Path into f.contents // Loads the contents of the the file at f.Path into f.contents
// //
// Caller is responsible for calling delete on f.contents and f.Sha256 // Caller is responsible for calling delete on f.contents and f.Sha256
//
// TODO: Should this use the db allocator?
env_file_backup :: proc(f: ^EnvFile) -> bool { env_file_backup :: proc(f: ^EnvFile) -> bool {
data, read_err := os.read_entire_file_from_path(f.Path, context.allocator) data, read_err := os.read_entire_file_from_path(f.Path, context.allocator)
if read_err != nil { if read_err != nil {
@@ -565,7 +520,6 @@ shares_remote :: proc(f: ^EnvFile, remotes: []string) -> bool {
return false return false
} }
// TODO: Should this use the db allocator?
get_git_remotes :: proc(dir: string) -> [dynamic]string { get_git_remotes :: proc(dir: string) -> [dynamic]string {
remotes: [dynamic]string remotes: [dynamic]string
remote_set: map[string]bool remote_set: map[string]bool

View File

@@ -8,6 +8,23 @@ import "core:testing"
import "sqlite" import "sqlite"
make_test_db :: proc() -> (Db, bool) {
db: ^rawptr
rc := sqlite.db_open(":memory:", &db)
if rc != sqlite.OK {
return Db{}, false
}
create_sql: cstring = "CREATE TABLE IF NOT EXISTS envr_env_files (path TEXT PRIMARY KEY NOT NULL, remotes TEXT, sha256 TEXT NOT NULL, contents TEXT NOT NULL)"
rc = sqlite.db_exec(db, create_sql, nil, nil, nil)
if rc != sqlite.OK {
sqlite.db_close(db)
return Db{}, false
}
return Db{db = db}, true
}
make_test_env_file :: proc(path, sha, contents: string, remotes: []string = {}) -> EnvFile { make_test_env_file :: proc(path, sha, contents: string, remotes: []string = {}) -> EnvFile {
f := EnvFile { f := EnvFile {
Path = path, Path = path,
@@ -24,10 +41,10 @@ make_test_env_file :: proc(path, sha, contents: string, remotes: []string = {})
@(test) @(test)
test_db_insert_and_fetch :: proc(t: ^testing.T) { test_db_insert_and_fetch :: proc(t: ^testing.T) {
d, ok := db_init() d, ok := make_test_db()
testing.expect(t, ok, "failed to create test db") testing.expect(t, ok, "failed to create test db")
if !ok do return if !ok do return
defer db_close(&d) defer sqlite.db_close(d.db)
path := "/project/.env" path := "/project/.env"
sha := "abc123" sha := "abc123"
@@ -39,7 +56,7 @@ test_db_insert_and_fetch :: proc(t: ^testing.T) {
testing.expect(t, db_insert(&d, f), "insert should succeed") testing.expect(t, db_insert(&d, f), "insert should succeed")
fetched, fetch_ok := db_fetch(&d, "/project/.env") fetched, fetch_ok := db_fetch(&d, "/project/.env")
// defer delete_envfile(&fetched) defer delete_envfile(&fetched)
testing.expect(t, fetch_ok, "fetch should succeed") testing.expect(t, fetch_ok, "fetch should succeed")
if !fetch_ok do return if !fetch_ok do return
@@ -52,10 +69,10 @@ test_db_insert_and_fetch :: proc(t: ^testing.T) {
@(test) @(test)
test_db_fetch_missing :: proc(t: ^testing.T) { test_db_fetch_missing :: proc(t: ^testing.T) {
d, ok := db_init() d, ok := make_test_db()
testing.expect(t, ok, "failed to create test db") testing.expect(t, ok, "failed to create test db")
if !ok do return if !ok do return
defer db_close(&d) defer sqlite.db_close(d.db)
_, fetch_ok := db_fetch(&d, "/nonexistent/.env") _, fetch_ok := db_fetch(&d, "/nonexistent/.env")
testing.expect(t, !fetch_ok, "fetch missing should return false") testing.expect(t, !fetch_ok, "fetch missing should return false")
@@ -63,9 +80,10 @@ test_db_fetch_missing :: proc(t: ^testing.T) {
@(test) @(test)
test_db_insert_or_replace :: proc(t: ^testing.T) { test_db_insert_or_replace :: proc(t: ^testing.T) {
d, ok := db_init() d, ok := make_test_db()
defer db_close(&d)
testing.expect(t, ok, "failed to create test db") testing.expect(t, ok, "failed to create test db")
if !ok do return
defer sqlite.db_close(d.db)
f1 := make_test_env_file("/project/.env", "sha1", "KEY=old") f1 := make_test_env_file("/project/.env", "sha1", "KEY=old")
defer delete(f1.Remotes) defer delete(f1.Remotes)
@@ -77,11 +95,18 @@ test_db_insert_or_replace :: proc(t: ^testing.T) {
results, list_ok := db_list(&d) results, list_ok := db_list(&d)
testing.expect(t, list_ok, "list should succeed") testing.expect(t, list_ok, "list should succeed")
if !list_ok do return
defer delete(results)
for &result in results {
defer delete_envfile(&result)
}
testing.expect(t, len(results) == 1, "should have 1 row, not 2") testing.expect(t, len(results) == 1, "should have 1 row, not 2")
fetched, fetch_ok := db_fetch(&d, "/project/.env") fetched, fetch_ok := db_fetch(&d, "/project/.env")
testing.expect(t, fetch_ok, "fetch should succeed") testing.expect(t, fetch_ok, "fetch should succeed")
if !fetch_ok do return
defer delete_envfile(&fetched)
testing.expect_value(t, fetched.contents, "KEY=new") testing.expect_value(t, fetched.contents, "KEY=new")
testing.expect_value(t, fetched.Sha256, "sha2") testing.expect_value(t, fetched.Sha256, "sha2")
@@ -89,10 +114,10 @@ test_db_insert_or_replace :: proc(t: ^testing.T) {
@(test) @(test)
test_db_delete_existing :: proc(t: ^testing.T) { test_db_delete_existing :: proc(t: ^testing.T) {
d, ok := db_init() d, ok := make_test_db()
testing.expect(t, ok, "failed to create test db") testing.expect(t, ok, "failed to create test db")
if !ok do return if !ok do return
defer db_close(&d) defer sqlite.db_close(d.db)
f := make_test_env_file("/project/.env", "sha", "KEY=val") f := make_test_env_file("/project/.env", "sha", "KEY=val")
defer delete(f.Remotes) defer delete(f.Remotes)
@@ -106,19 +131,20 @@ test_db_delete_existing :: proc(t: ^testing.T) {
@(test) @(test)
test_db_delete_missing :: proc(t: ^testing.T) { test_db_delete_missing :: proc(t: ^testing.T) {
d, ok := db_init() d, ok := make_test_db()
testing.expect(t, ok, "failed to create test db") testing.expect(t, ok, "failed to create test db")
if !ok do return if !ok do return
defer db_close(&d) defer sqlite.db_close(d.db)
testing.expect(t, !db_delete(&d, "/nonexistent/.env"), "delete missing should return false") testing.expect(t, !db_delete(&d, "/nonexistent/.env"), "delete missing should return false")
} }
@(test) @(test)
test_db_list_multiple :: proc(t: ^testing.T) { test_db_list_multiple :: proc(t: ^testing.T) {
d, ok := db_init() d, ok := make_test_db()
testing.expect(t, ok, "failed to create test db") testing.expect(t, ok, "failed to create test db")
defer db_close(&d) if !ok do return
defer sqlite.db_close(d.db)
f1 := make_test_env_file("/proj1/.env", "sha1", "A=1", []string{"git@github.com:a/repo.git"}) f1 := make_test_env_file("/proj1/.env", "sha1", "A=1", []string{"git@github.com:a/repo.git"})
defer delete(f1.Remotes) defer delete(f1.Remotes)
@@ -132,27 +158,36 @@ test_db_list_multiple :: proc(t: ^testing.T) {
results, list_ok := db_list(&d) results, list_ok := db_list(&d)
testing.expect(t, list_ok, "list should succeed") testing.expect(t, list_ok, "list should succeed")
if !list_ok do return
defer delete(results)
defer {
for &result in results {
delete_envfile(&result)
}
}
testing.expect_value(t, len(results), 3) testing.expect_value(t, len(results), 3)
} }
@(test) @(test)
test_db_list_empty :: proc(t: ^testing.T) { test_db_list_empty :: proc(t: ^testing.T) {
d, ok := db_init() d, ok := make_test_db()
testing.expect(t, ok, "failed to create test db") testing.expect(t, ok, "failed to create test db")
defer db_close(&d) if !ok do return
defer sqlite.db_close(d.db)
results, list_ok := db_list(&d) results, list_ok := db_list(&d)
testing.expect(t, list_ok, "list should succeed on empty db") testing.expect(t, list_ok, "list should succeed on empty db")
testing.expect(t, len(results) == 0, "should have 0 rows") testing.expect(t, len(results) == 0, "should have 0 rows")
if list_ok do delete(results)
} }
@(test) @(test)
test_db_insert_sets_changed :: proc(t: ^testing.T) { test_db_insert_sets_changed :: proc(t: ^testing.T) {
d, ok := db_init() d, ok := make_test_db()
testing.expect(t, ok, "failed to create test db") testing.expect(t, ok, "failed to create test db")
if !ok do return if !ok do return
defer db_close(&d) defer sqlite.db_close(d.db)
testing.expect(t, !d.changed, "changed should start false") testing.expect(t, !d.changed, "changed should start false")
@@ -165,10 +200,10 @@ test_db_insert_sets_changed :: proc(t: ^testing.T) {
@(test) @(test)
test_db_delete_sets_changed :: proc(t: ^testing.T) { test_db_delete_sets_changed :: proc(t: ^testing.T) {
d, ok := db_init() d, ok := make_test_db()
testing.expect(t, ok, "failed to create test db") testing.expect(t, ok, "failed to create test db")
if !ok do return if !ok do return
defer db_close(&d) defer sqlite.db_close(d.db)
f := make_test_env_file("/project/.env", "sha", "KEY=val") f := make_test_env_file("/project/.env", "sha", "KEY=val")
defer delete(f.Remotes) defer delete(f.Remotes)
@@ -181,10 +216,10 @@ test_db_delete_sets_changed :: proc(t: ^testing.T) {
@(test) @(test)
test_db_serialize :: proc(t: ^testing.T) { test_db_serialize :: proc(t: ^testing.T) {
d, ok := db_init() d, ok := make_test_db()
testing.expect(t, ok, "failed to create test db") testing.expect(t, ok, "failed to create test db")
if !ok do return if !ok do return
defer db_close(&d) defer sqlite.db_close(d.db)
f := make_test_env_file("/project/.env", "sha", "KEY=val") f := make_test_env_file("/project/.env", "sha", "KEY=val")
defer delete(f.Remotes) defer delete(f.Remotes)
@@ -429,7 +464,7 @@ test_update_dir :: proc(t: ^testing.T) {
Dir = "/old/project", Dir = "/old/project",
Remotes = make([dynamic]string, 0), Remotes = make([dynamic]string, 0),
} }
defer delete(f.Remotes) defer delete_envfile(&f)
update_dir(&f, "/new/location") update_dir(&f, "/new/location")
@@ -446,14 +481,13 @@ test_closing_db_has_no_leaks :: proc(t: ^testing.T) {
cfg_path, err := filepath.join([]string{base, "config.json"}, context.temp_allocator) cfg_path, err := filepath.join([]string{base, "config.json"}, context.temp_allocator)
testing.expect(t, err == nil, "cfgPath should build successfully") testing.expect(t, err == nil, "cfgPath should build successfully")
{ cfg := new_config([]string{"fixtures/keys/insecure-test-key"}, cfg_path)
cfg := new_config([]string{"fixtures/keys/insecure-test-key"}, cfg_path) defer delete_config(&cfg)
testing.expect(t, save_config(cfg, force = true), "save should succeed") testing.expect(t, save_config(cfg, force = true), "save should succeed")
delete_config(&cfg)
}
db, ok := db_open(cfg_path) db, ok := db_open(cfg_path)
testing.expect(t, ok, "db should open") testing.expect(t, ok, "db should open")
if !ok do return
db_close(&db) db_close(&db)
} }
@@ -466,22 +500,15 @@ test_open_existing_db_has_no_leaks :: proc(t: ^testing.T) {
cfg_path, err := filepath.join([]string{base, "config.json"}, context.temp_allocator) cfg_path, err := filepath.join([]string{base, "config.json"}, context.temp_allocator)
testing.expect(t, err == nil, "cfgPath should build successfully") testing.expect(t, err == nil, "cfgPath should build successfully")
{ cfg := new_config([]string{"fixtures/keys/insecure-test-key"}, cfg_path)
cfg := new_config([]string{"fixtures/keys/insecure-test-key"}, cfg_path) defer delete_config(&cfg)
testing.expect(t, save_config(cfg, force = true), "save should succeed") testing.expect(t, save_config(cfg, force = true), "save should succeed")
delete_config(&cfg)
}
// First open/close creates data.envr on disk // First open/close creates data.envr on disk
db, ok := db_open(cfg_path) db, ok := db_open(cfg_path)
testing.expect(t, ok, "db should open") testing.expect(t, ok, "db should open")
if !ok do return if !ok do return
f := make_test_env_file( f := make_test_env_file("/project/.env", "abc123", "SECRET=value", []string{"git@github.com:user/repo.git"})
"/project/.env",
"abc123",
"SECRET=value",
[]string{"git@github.com:user/repo.git"},
)
defer delete(f.Remotes) defer delete(f.Remotes)
testing.expect(t, db_insert(&db, f), "insert should succeed") testing.expect(t, db_insert(&db, f), "insert should succeed")
db_close(&db) db_close(&db)

View File

@@ -1 +1 @@
0.3.0 0.4.0