before merge to main

This commit is contained in:
2026-03-04 16:29:24 +06:00
parent 0481cde1c3
commit 491ccbea89
21 changed files with 6776 additions and 47 deletions

View File

@@ -1,7 +1,9 @@
package tests
import (
"log"
"os"
"strings"
"testing"
"time"
@@ -472,3 +474,280 @@ func TestLineDbUniqueFieldsNoUniqueFields(t *testing.T) {
t.Fatalf("Expected 2 records when no unique fields, got %d", len(all))
}
}
// TestLineDbEncode проверяет шифрование записей при Encode
func TestLineDbEncode(t *testing.T) {
os.RemoveAll("./testdata")
initOptions := &linedb.LineDbInitOptions{
CacheSize: 100,
CacheTTL: time.Minute,
DBFolder: "./testdata",
Collections: []linedb.JSONLFileOptions{
{
CollectionName: "secret",
AllocSize: 512,
Encode: true,
EncodeKey: "test-secret-key",
},
},
}
db := linedb.NewLineDb(nil)
if err := db.Init(false, initOptions); err != nil {
t.Fatalf("Init failed: %v", err)
}
defer func() { db.Close(); os.RemoveAll("./testdata") }()
opts := linedb.LineDbAdapterOptions{}
if err := db.Insert(map[string]any{"name": "secret", "value": 42}, "secret", opts); err != nil {
t.Fatalf("Insert failed: %v", err)
}
all, err := db.Read("secret", opts)
if err != nil {
t.Fatalf("Read failed: %v", err)
}
if len(all) != 1 {
t.Fatalf("Expected 1 record, got %d", len(all))
}
r := all[0].(map[string]any)
if r["name"] != "secret" || r["value"] != float64(42) {
t.Fatalf("Expected name=secret value=42, got %+v", r)
}
}
// TestLineDbAllocSizeOverflow проверяет ошибку при записи, превышающей allocSize-1
func TestLineDbAllocSizeOverflow(t *testing.T) {
os.RemoveAll("./testdata")
initOptions := &linedb.LineDbInitOptions{
CacheSize: 100,
CacheTTL: time.Minute,
DBFolder: "./testdata",
Collections: []linedb.JSONLFileOptions{
{CollectionName: "tiny", AllocSize: 64},
},
}
db := linedb.NewLineDb(nil)
if err := db.Init(false, initOptions); err != nil {
t.Fatalf("Init failed: %v", err)
}
defer func() { db.Close(); os.RemoveAll("./testdata") }()
opts := linedb.LineDbAdapterOptions{}
// Короткая запись — поместится (allocSize-1=63)
if err := db.Insert(map[string]any{"x": "short"}, "tiny", opts); err != nil {
t.Fatalf("Insert short failed: %v", err)
}
// Длинная — не поместится (JSON > 63 символов)
longStr := strings.Repeat("a", 80)
err := db.Insert(map[string]any{"x": longStr}, "tiny", opts)
if err == nil {
t.Fatal("Expected error for record exceeding allocSize-1")
}
if !strings.Contains(err.Error(), "exceeds") {
t.Fatalf("Expected 'exceeds' in error, got: %v", err)
}
}
// TestLineDbInitNormalizeExistingFile проверяет нормализацию при Init существующего файла
func TestLineDbInitNormalizeExistingFile(t *testing.T) {
os.RemoveAll("./testdata")
os.MkdirAll("./testdata", 0755)
// Файл с записями разной длины
content := `{"id":1,"a":"x"}
{"id":2,"a":"yy"}
{"id":3,"a":"zzz"}`
if err := os.WriteFile("./testdata/norm.jsonl", []byte(content), 0644); err != nil {
t.Fatalf("Write file: %v", err)
}
defer os.RemoveAll("./testdata")
initOptions := &linedb.LineDbInitOptions{
CacheSize: 100,
CacheTTL: time.Minute,
DBFolder: "./testdata",
Collections: []linedb.JSONLFileOptions{
{CollectionName: "norm", AllocSize: 100},
},
}
db := linedb.NewLineDb(nil)
if err := db.Init(false, initOptions); err != nil {
t.Fatalf("Init failed: %v", err)
}
defer db.Close()
all, err := db.Read("norm", linedb.LineDbAdapterOptions{})
if err != nil {
t.Fatalf("Read failed: %v", err)
}
if len(all) != 3 {
t.Fatalf("Expected 3 records, got %d", len(all))
}
}
// setupCollectionWithFieldConstraints создаёт БД с FieldConstraints (required/unique)
func setupCollectionWithFieldConstraints(t *testing.T, constraints []linedb.FieldConstraint) (*linedb.LineDb, func()) {
t.Helper()
os.RemoveAll("./testdata")
initOptions := &linedb.LineDbInitOptions{
CacheSize: 100,
CacheTTL: time.Minute,
DBFolder: "./testdata",
Collections: []linedb.JSONLFileOptions{
{
CollectionName: "items",
AllocSize: 256,
FieldConstraints: constraints,
},
},
}
db := linedb.NewLineDb(nil)
if err := db.Init(false, initOptions); err != nil {
t.Fatalf("Failed to init database: %v", err)
}
return db, func() { db.Close(); os.RemoveAll("./testdata") }
}
// TestLineDbRequiredFieldsInsert проверяет, что required поля обязательны при Insert
func TestLineDbRequiredFieldsInsert(t *testing.T) {
db, cleanup := setupCollectionWithFieldConstraints(t, []linedb.FieldConstraint{
{Name: "email", Required: true},
{Name: "name", Required: true},
})
defer cleanup()
opts := linedb.LineDbAdapterOptions{}
// Успех — оба поля заполнены
if err := db.Insert(map[string]any{"name": "alice", "email": "a@test.com"}, "items", opts); err != nil {
t.Fatalf("Insert with required fields failed: %v", err)
}
// Ошибка — отсутствует email (AbsentKey)
err := db.Insert(map[string]any{"name": "bob"}, "items", opts)
if err == nil {
t.Fatal("Expected error for missing required field email")
} else {
log.Println("Error: ", err)
}
// Ошибка — email = nil
err = db.Insert(map[string]any{"name": "bob", "email": nil}, "items", opts)
if err == nil {
t.Fatal("Expected error for nil required field email")
} else {
log.Println("Error: ", err)
}
// Ошибка — email = ""
err = db.Insert(map[string]any{"name": "bob", "email": ""}, "items", opts)
if err == nil {
t.Fatal("Expected error for empty string in required field email")
} else {
log.Println("Error: ", err)
}
all, _ := db.Read("items", opts)
if len(all) != 1 {
t.Fatalf("Expected 1 record, got %d", len(all))
}
}
// TestLineDbRequiredFieldsEmptyMode проверяет разные режимы интерпретации пустоты
func TestLineDbRequiredFieldsEmptyMode(t *testing.T) {
// Режим: только отсутствие ключа — nil и "" допустимы
db, cleanup := setupCollectionWithFieldConstraints(t, []linedb.FieldConstraint{
{Name: "title", Required: true, EmptyMode: linedb.EmptyModeAbsentKey},
})
defer cleanup()
opts := linedb.LineDbAdapterOptions{}
// nil — допустимо (только AbsentKey проверяется)
if err := db.Insert(map[string]any{"title": nil}, "items", opts); err != nil {
t.Fatalf("Insert with nil (AbsentKey only) failed: %v", err)
}
// "" — допустимо
if err := db.Insert(map[string]any{"title": ""}, "items", opts); err != nil {
t.Fatalf("Insert with empty string (AbsentKey only) failed: %v", err)
}
// Отсутствие ключа — ошибка (поле не в записи)
err := db.Insert(map[string]any{"other": "x"}, "items", opts)
if err == nil {
t.Fatal("Expected error for absent required key")
}
// Режим: только nil — AbsentKey и "" допустимы? Нет: AbsentKey при отсутствии — пусто.
// При EmptyModeNil: пусто только когда v==nil. AbsentKey при отсутствии ключа: exists=false.
// isEmptyByMode: if mode&EmptyModeAbsentKey && !exists return true. So if we use only EmptyModeNil,
// AbsentKey is not in the mask, so we don't return true for !exists. Good.
db2, cleanup2 := setupCollectionWithFieldConstraints(t, []linedb.FieldConstraint{
{Name: "flag", Required: true, EmptyMode: linedb.EmptyModeNil},
})
defer cleanup2()
// Ключ отсутствует — при EmptyModeNil мы не проверяем AbsentKey, так что !exists не даёт return true.
// isEmptyByMode: проверяет mode&EmptyModeAbsentKey - если нет, то !exists не приводит к true.
// Потом mode&EmptyModeNil && v==nil - но v при !exists будет нулевым при v, exists := m[key]. v = nil, exists = false.
// Но мы заходим в v, exists := m[key]. Если !exists, v будет nil (zero value). So v==nil is true.
// And mode&EmptyModeNil - if we have only Nil, we'd return true. So absent key would still be "empty" if we access v.
// Actually no - we first check mode&EmptyModeAbsentKey && !exists - if that's not in mode, we skip.
// Then mode&EmptyModeNil && v==nil - for absent key, v is nil. So we'd return true. So absent key + Nil mode would still fail.
// The logic is: we return true (empty) if ANY of the conditions matching the mode are true. So:
// - AbsentKey + !exists -> empty
// - Nil + v==nil -> empty
// - ZeroValue + ("" or 0) -> empty
// For EmptyModeNil only: we don't check AbsentKey. We check Nil - and for absent key, v is nil (zero value from map lookup). So we'd return true. So absent key would still be empty. Good.
// For EmptyModeZeroValue only: we'd only check ZeroValue. Absent key: v is nil, we don't have a case for nil in the switch (we'd fall through and return false). So absent key would NOT be empty. And nil would not be empty. Only "" and 0 would be.
// Let me add a test for EmptyModeZeroValue: required field, value 0 should fail, value "" should fail, but absent key... would not fail? That might be surprising. Let me document it.
if err := db2.Insert(map[string]any{"flag": false}, "items", opts); err != nil {
t.Fatalf("Insert with false (bool, Nil mode) failed: %v", err)
}
}
// TestLineDbRequiredFieldsUpdate проверяет, что Update не может установить пустое в required
func TestLineDbRequiredFieldsUpdate(t *testing.T) {
db, cleanup := setupCollectionWithFieldConstraints(t, []linedb.FieldConstraint{
{Name: "email", Required: true},
})
defer cleanup()
opts := linedb.LineDbAdapterOptions{}
if err := db.Insert(map[string]any{"name": "a", "email": "a@test.com"}, "items", opts); err != nil {
t.Fatalf("Insert failed: %v", err)
}
// Ошибка — пытаемся установить email в ""
_, err := db.Update(map[string]any{"email": ""}, "items", map[string]any{"email": "a@test.com"}, opts)
if err == nil {
t.Fatal("Expected error when setting required field to empty")
}
// Успех — обновляем другое поле
_, err = db.Update(map[string]any{"name": "alice"}, "items", map[string]any{"email": "a@test.com"}, opts)
if err != nil {
t.Fatalf("Update non-required field failed: %v", err)
}
}
// TestLineDbRequiredAndUniqueCombined проверяет комбинацию required и unique
func TestLineDbRequiredAndUniqueCombined(t *testing.T) {
db, cleanup := setupCollectionWithFieldConstraints(t, []linedb.FieldConstraint{
{Name: "email", Required: true, Unique: true},
})
defer cleanup()
opts := linedb.LineDbAdapterOptions{}
if err := db.Insert(map[string]any{"email": "x@test.com"}, "items", opts); err != nil {
t.Fatalf("Insert failed: %v", err)
}
err := db.Insert(map[string]any{"email": "x@test.com"}, "items", opts)
if err == nil {
t.Fatal("Expected unique violation")
}
err = db.Insert(map[string]any{"email": ""}, "items", opts)
if err == nil {
t.Fatal("Expected required violation for empty email")
}
}