mirror of
https://github.com/aptly-dev/aptly.git
synced 2026-07-15 11:57:59 +00:00
Add comprehensive CI/CD improvements and test coverage
This commit introduces major enhancements to the CI/CD pipeline and testing infrastructure: CI/CD Improvements: - Consolidated modern and legacy CI workflows into a single comprehensive pipeline - Removed all publishing functionality from CI (no longer needed) - Added 8 new advanced testing jobs for pull requests: * advanced-coverage: Detailed coverage analysis with base branch comparison * performance-profile: CPU and memory profiling with benchmarks * fuzz-test: Automated fuzz testing for supported packages * deep-analysis: Multiple static analysis tools (shadow, ineffassign, gosec, staticcheck) * mutation-test: Tests effectiveness of test suite on changed files * dependency-audit: Security vulnerabilities and outdated dependency checks * stress-test: Race detection with 100 iterations and parallel testing * test-report-summary: Aggregates all reports into a single PR comment - Enabled RUN_LONG_TESTS by default for thorough testing - Added automatic PR comment generation with all test results Testing Infrastructure: - Added comprehensive test files across all packages to improve coverage - Implemented unit tests for previously untested packages - Added race condition tests for concurrent operations - Created integration tests for API endpoints - Added storage backend tests (etcd, goleveldb) - Implemented command-line interface tests Local Testing Support: - Added act configuration for testing GitHub Actions locally - Created docker-compose.ci.yml for full CI environment simulation - Updated CONTRIBUTING.md with detailed local testing instructions Documentation Updates: - Added comprehensive CI documentation to CONTRIBUTING.md - Removed obsolete references to Travis CI - Updated Go version requirements to 1.24 - Added act usage instructions and examples Other Improvements: - Updated .gitignore to exclude coverage reports and build artifacts - Added test-act.yml workflow for testing act functionality - Created CI_SUMMARY.md documenting all CI capabilities These changes transform aptly's CI from a basic testing pipeline into a comprehensive quality assurance system that provides immediate feedback on code quality, performance, security, and test effectiveness.
This commit is contained in:
@@ -0,0 +1,210 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
check "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
// Hook up gocheck into the "go test" runner.
|
||||
func Test(t *testing.T) { check.TestingT(t) }
|
||||
|
||||
type DatabaseSuite struct{}
|
||||
|
||||
var _ = check.Suite(&DatabaseSuite{})
|
||||
|
||||
func (s *DatabaseSuite) TestErrNotFound(c *check.C) {
|
||||
// Test that ErrNotFound is properly defined
|
||||
c.Check(ErrNotFound, check.NotNil)
|
||||
c.Check(ErrNotFound.Error(), check.Equals, "key not found")
|
||||
|
||||
// Test that it's an actual error
|
||||
var err error = ErrNotFound
|
||||
c.Check(err, check.NotNil)
|
||||
|
||||
// Test comparison with errors.New
|
||||
newErr := errors.New("key not found")
|
||||
c.Check(ErrNotFound.Error(), check.Equals, newErr.Error())
|
||||
|
||||
// Test that it's not equal to other errors
|
||||
otherErr := errors.New("other error")
|
||||
c.Check(ErrNotFound.Error(), check.Not(check.Equals), otherErr.Error())
|
||||
}
|
||||
|
||||
func (s *DatabaseSuite) TestStorageProcessor(c *check.C) {
|
||||
// Test StorageProcessor function type
|
||||
called := false
|
||||
var processor StorageProcessor = func(key []byte, value []byte) error {
|
||||
called = true
|
||||
c.Check(key, check.DeepEquals, []byte("test-key"))
|
||||
c.Check(value, check.DeepEquals, []byte("test-value"))
|
||||
return nil
|
||||
}
|
||||
|
||||
err := processor([]byte("test-key"), []byte("test-value"))
|
||||
c.Check(err, check.IsNil)
|
||||
c.Check(called, check.Equals, true)
|
||||
}
|
||||
|
||||
func (s *DatabaseSuite) TestStorageProcessorWithError(c *check.C) {
|
||||
// Test StorageProcessor that returns an error
|
||||
testError := errors.New("processing error")
|
||||
var processor StorageProcessor = func(key []byte, value []byte) error {
|
||||
return testError
|
||||
}
|
||||
|
||||
err := processor([]byte("key"), []byte("value"))
|
||||
c.Check(err, check.Equals, testError)
|
||||
}
|
||||
|
||||
func (s *DatabaseSuite) TestStorageProcessorNilInputs(c *check.C) {
|
||||
// Test StorageProcessor with nil inputs
|
||||
var processor StorageProcessor = func(key []byte, value []byte) error {
|
||||
c.Check(key, check.IsNil)
|
||||
c.Check(value, check.DeepEquals, []byte("value"))
|
||||
return nil
|
||||
}
|
||||
|
||||
err := processor(nil, []byte("value"))
|
||||
c.Check(err, check.IsNil)
|
||||
}
|
||||
|
||||
func (s *DatabaseSuite) TestStorageProcessorEmptyInputs(c *check.C) {
|
||||
// Test StorageProcessor with empty inputs
|
||||
var processor StorageProcessor = func(key []byte, value []byte) error {
|
||||
c.Check(len(key), check.Equals, 0)
|
||||
c.Check(len(value), check.Equals, 0)
|
||||
return nil
|
||||
}
|
||||
|
||||
err := processor([]byte{}, []byte{})
|
||||
c.Check(err, check.IsNil)
|
||||
}
|
||||
|
||||
// Mock implementations to test interface compliance
|
||||
type mockReader struct {
|
||||
data map[string][]byte
|
||||
}
|
||||
|
||||
func (m *mockReader) Get(key []byte) ([]byte, error) {
|
||||
if value, exists := m.data[string(key)]; exists {
|
||||
return value, nil
|
||||
}
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
|
||||
type mockWriter struct {
|
||||
data map[string][]byte
|
||||
}
|
||||
|
||||
func (m *mockWriter) Put(key []byte, value []byte) error {
|
||||
m.data[string(key)] = value
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockWriter) Delete(key []byte) error {
|
||||
delete(m.data, string(key))
|
||||
return nil
|
||||
}
|
||||
|
||||
type mockReaderWriter struct {
|
||||
*mockReader
|
||||
*mockWriter
|
||||
}
|
||||
|
||||
func (s *DatabaseSuite) TestReaderInterface(c *check.C) {
|
||||
// Test Reader interface implementation
|
||||
data := map[string][]byte{
|
||||
"key1": []byte("value1"),
|
||||
"key2": []byte("value2"),
|
||||
}
|
||||
|
||||
var reader Reader = &mockReader{data: data}
|
||||
|
||||
// Test existing key
|
||||
value, err := reader.Get([]byte("key1"))
|
||||
c.Check(err, check.IsNil)
|
||||
c.Check(value, check.DeepEquals, []byte("value1"))
|
||||
|
||||
// Test non-existing key
|
||||
value, err = reader.Get([]byte("nonexistent"))
|
||||
c.Check(err, check.Equals, ErrNotFound)
|
||||
c.Check(value, check.IsNil)
|
||||
}
|
||||
|
||||
func (s *DatabaseSuite) TestWriterInterface(c *check.C) {
|
||||
// Test Writer interface implementation
|
||||
data := make(map[string][]byte)
|
||||
var writer Writer = &mockWriter{data: data}
|
||||
|
||||
// Test Put
|
||||
err := writer.Put([]byte("key1"), []byte("value1"))
|
||||
c.Check(err, check.IsNil)
|
||||
c.Check(data["key1"], check.DeepEquals, []byte("value1"))
|
||||
|
||||
// Test Delete
|
||||
err = writer.Delete([]byte("key1"))
|
||||
c.Check(err, check.IsNil)
|
||||
_, exists := data["key1"]
|
||||
c.Check(exists, check.Equals, false)
|
||||
}
|
||||
|
||||
func (s *DatabaseSuite) TestReaderWriterInterface(c *check.C) {
|
||||
// Test ReaderWriter interface implementation
|
||||
data := make(map[string][]byte)
|
||||
|
||||
var rw ReaderWriter = &mockReaderWriter{
|
||||
mockReader: &mockReader{data: data},
|
||||
mockWriter: &mockWriter{data: data},
|
||||
}
|
||||
|
||||
// Test write then read
|
||||
err := rw.Put([]byte("test"), []byte("value"))
|
||||
c.Check(err, check.IsNil)
|
||||
|
||||
value, err := rw.Get([]byte("test"))
|
||||
c.Check(err, check.IsNil)
|
||||
c.Check(value, check.DeepEquals, []byte("value"))
|
||||
|
||||
// Test delete
|
||||
err = rw.Delete([]byte("test"))
|
||||
c.Check(err, check.IsNil)
|
||||
|
||||
value, err = rw.Get([]byte("test"))
|
||||
c.Check(err, check.Equals, ErrNotFound)
|
||||
c.Check(value, check.IsNil)
|
||||
}
|
||||
|
||||
// Test that all interfaces are properly defined
|
||||
func (s *DatabaseSuite) TestInterfaceDefinitions(c *check.C) {
|
||||
// This test ensures that all interfaces are properly defined
|
||||
// and can be used as interface types
|
||||
|
||||
var reader Reader
|
||||
var prefixReader PrefixReader
|
||||
var writer Writer
|
||||
var readerWriter ReaderWriter
|
||||
var storage Storage
|
||||
var batch Batch
|
||||
var transaction Transaction
|
||||
|
||||
// Test that they are nil by default
|
||||
c.Check(reader, check.IsNil)
|
||||
c.Check(prefixReader, check.IsNil)
|
||||
c.Check(writer, check.IsNil)
|
||||
c.Check(readerWriter, check.IsNil)
|
||||
c.Check(storage, check.IsNil)
|
||||
c.Check(batch, check.IsNil)
|
||||
c.Check(transaction, check.IsNil)
|
||||
}
|
||||
|
||||
func (s *DatabaseSuite) TestErrorConstants(c *check.C) {
|
||||
// Test that error constants are immutable and consistently defined
|
||||
original := ErrNotFound
|
||||
c.Check(original, check.NotNil)
|
||||
|
||||
// Verify it maintains its identity
|
||||
c.Check(ErrNotFound, check.Equals, original)
|
||||
c.Check(ErrNotFound.Error(), check.Equals, original.Error())
|
||||
}
|
||||
@@ -0,0 +1,519 @@
|
||||
package goleveldb_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
. "gopkg.in/check.v1"
|
||||
|
||||
"github.com/aptly-dev/aptly/database"
|
||||
"github.com/aptly-dev/aptly/database/goleveldb"
|
||||
)
|
||||
|
||||
type ExtendedLevelDBSuite struct {
|
||||
tempDir string
|
||||
}
|
||||
|
||||
var _ = Suite(&ExtendedLevelDBSuite{})
|
||||
|
||||
func (s *ExtendedLevelDBSuite) SetUpTest(c *C) {
|
||||
s.tempDir = c.MkDir()
|
||||
}
|
||||
|
||||
func (s *ExtendedLevelDBSuite) TestNewDB(c *C) {
|
||||
// Test NewDB function
|
||||
dbPath := filepath.Join(s.tempDir, "test-db")
|
||||
|
||||
db, err := goleveldb.NewDB(dbPath)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(db, NotNil)
|
||||
|
||||
// DB should not be open yet
|
||||
_, err = db.Get([]byte("test"))
|
||||
c.Check(err, NotNil) // Should error because DB is not open
|
||||
|
||||
// Open the database
|
||||
err = db.Open()
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// Now should work
|
||||
_, err = db.Get([]byte("test"))
|
||||
c.Check(err, Equals, database.ErrNotFound) // Key not found but no open error
|
||||
|
||||
err = db.Close()
|
||||
c.Check(err, IsNil)
|
||||
}
|
||||
|
||||
func (s *ExtendedLevelDBSuite) TestNewOpenDB(c *C) {
|
||||
// Test NewOpenDB function
|
||||
dbPath := filepath.Join(s.tempDir, "test-open-db")
|
||||
|
||||
db, err := goleveldb.NewOpenDB(dbPath)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(db, NotNil)
|
||||
|
||||
// DB should be open and ready to use
|
||||
_, err = db.Get([]byte("test"))
|
||||
c.Check(err, Equals, database.ErrNotFound) // Key not found but no open error
|
||||
|
||||
err = db.Close()
|
||||
c.Check(err, IsNil)
|
||||
}
|
||||
|
||||
func (s *ExtendedLevelDBSuite) TestRecoverDBError(c *C) {
|
||||
// Test RecoverDB with invalid path
|
||||
invalidPath := "/invalid/nonexistent/path"
|
||||
|
||||
err := goleveldb.RecoverDB(invalidPath)
|
||||
c.Check(err, NotNil) // Should error with invalid path
|
||||
}
|
||||
|
||||
func (s *ExtendedLevelDBSuite) TestRecoverDBValidPath(c *C) {
|
||||
// Test RecoverDB with valid database
|
||||
dbPath := filepath.Join(s.tempDir, "recover-test")
|
||||
|
||||
// First create a database
|
||||
db, err := goleveldb.NewOpenDB(dbPath)
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// Add some data
|
||||
err = db.Put([]byte("key1"), []byte("value1"))
|
||||
c.Check(err, IsNil)
|
||||
|
||||
err = db.Close()
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// Now recover it
|
||||
err = goleveldb.RecoverDB(dbPath)
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// Verify data is still there after recovery
|
||||
db2, err := goleveldb.NewOpenDB(dbPath)
|
||||
c.Check(err, IsNil)
|
||||
|
||||
value, err := db2.Get([]byte("key1"))
|
||||
c.Check(err, IsNil)
|
||||
c.Check(value, DeepEquals, []byte("value1"))
|
||||
|
||||
err = db2.Close()
|
||||
c.Check(err, IsNil)
|
||||
}
|
||||
|
||||
func (s *ExtendedLevelDBSuite) TestCreateTemporaryError(c *C) {
|
||||
// Test CreateTemporary with limited permissions (if possible)
|
||||
dbPath := filepath.Join(s.tempDir, "test-temp")
|
||||
|
||||
db, err := goleveldb.NewOpenDB(dbPath)
|
||||
c.Check(err, IsNil)
|
||||
|
||||
tempDB, err := db.CreateTemporary()
|
||||
c.Check(err, IsNil)
|
||||
c.Check(tempDB, NotNil)
|
||||
|
||||
// Temporary DB should be usable
|
||||
err = tempDB.Put([]byte("temp-key"), []byte("temp-value"))
|
||||
c.Check(err, IsNil)
|
||||
|
||||
value, err := tempDB.Get([]byte("temp-key"))
|
||||
c.Check(err, IsNil)
|
||||
c.Check(value, DeepEquals, []byte("temp-value"))
|
||||
|
||||
err = tempDB.Close()
|
||||
c.Check(err, IsNil)
|
||||
|
||||
err = tempDB.Drop()
|
||||
c.Check(err, IsNil)
|
||||
|
||||
err = db.Close()
|
||||
c.Check(err, IsNil)
|
||||
}
|
||||
|
||||
func (s *ExtendedLevelDBSuite) TestStoragePutOptimization(c *C) {
|
||||
// Test Put optimization (doesn't save if value is same)
|
||||
dbPath := filepath.Join(s.tempDir, "put-optimization")
|
||||
|
||||
db, err := goleveldb.NewOpenDB(dbPath)
|
||||
c.Check(err, IsNil)
|
||||
|
||||
key := []byte("optimization-key")
|
||||
value := []byte("same-value")
|
||||
|
||||
// First put
|
||||
err = db.Put(key, value)
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// Second put with same value (should be optimized)
|
||||
err = db.Put(key, value)
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// Third put with different value
|
||||
newValue := []byte("different-value")
|
||||
err = db.Put(key, newValue)
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// Verify final value
|
||||
result, err := db.Get(key)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(result, DeepEquals, newValue)
|
||||
|
||||
err = db.Close()
|
||||
c.Check(err, IsNil)
|
||||
}
|
||||
|
||||
func (s *ExtendedLevelDBSuite) TestStorageCloseMultiple(c *C) {
|
||||
// Test calling Close multiple times
|
||||
dbPath := filepath.Join(s.tempDir, "close-multiple")
|
||||
|
||||
db, err := goleveldb.NewOpenDB(dbPath)
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// First close should work
|
||||
err = db.Close()
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// Second close should not error
|
||||
err = db.Close()
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// Third close should not error
|
||||
err = db.Close()
|
||||
c.Check(err, IsNil)
|
||||
}
|
||||
|
||||
func (s *ExtendedLevelDBSuite) TestStorageOpenMultiple(c *C) {
|
||||
// Test calling Open multiple times
|
||||
dbPath := filepath.Join(s.tempDir, "open-multiple")
|
||||
|
||||
db, err := goleveldb.NewDB(dbPath)
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// First open should work
|
||||
err = db.Open()
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// Second open should not error (already open)
|
||||
err = db.Open()
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// Should still be functional
|
||||
err = db.Put([]byte("test"), []byte("value"))
|
||||
c.Check(err, IsNil)
|
||||
|
||||
err = db.Close()
|
||||
c.Check(err, IsNil)
|
||||
}
|
||||
|
||||
func (s *ExtendedLevelDBSuite) TestStorageDropError(c *C) {
|
||||
// Test Drop when database is still open
|
||||
dbPath := filepath.Join(s.tempDir, "drop-error")
|
||||
|
||||
db, err := goleveldb.NewOpenDB(dbPath)
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// Try to drop while DB is open (should error)
|
||||
err = db.Drop()
|
||||
c.Check(err, NotNil)
|
||||
c.Check(err.Error(), Equals, "DB is still open")
|
||||
|
||||
// Close and then drop should work
|
||||
err = db.Close()
|
||||
c.Check(err, IsNil)
|
||||
|
||||
err = db.Drop()
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// Verify directory is gone
|
||||
_, err = os.Stat(dbPath)
|
||||
c.Check(os.IsNotExist(err), Equals, true)
|
||||
}
|
||||
|
||||
func (s *ExtendedLevelDBSuite) TestTransactionInterface(c *C) {
|
||||
// Test transaction functionality
|
||||
dbPath := filepath.Join(s.tempDir, "transaction-test")
|
||||
|
||||
db, err := goleveldb.NewOpenDB(dbPath)
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// Create transaction
|
||||
tx, err := db.OpenTransaction()
|
||||
c.Check(err, IsNil)
|
||||
c.Check(tx, NotNil)
|
||||
|
||||
// Test transaction operations
|
||||
key := []byte("tx-key")
|
||||
value := []byte("tx-value")
|
||||
|
||||
err = tx.Put(key, value)
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// Value should not be visible outside transaction yet
|
||||
_, err = db.Get(key)
|
||||
c.Check(err, Equals, database.ErrNotFound)
|
||||
|
||||
// But should be visible within transaction
|
||||
txValue, err := tx.Get(key)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(txValue, DeepEquals, value)
|
||||
|
||||
// Commit transaction
|
||||
err = tx.Commit()
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// Now value should be visible
|
||||
finalValue, err := db.Get(key)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(finalValue, DeepEquals, value)
|
||||
|
||||
err = db.Close()
|
||||
c.Check(err, IsNil)
|
||||
}
|
||||
|
||||
func (s *ExtendedLevelDBSuite) TestTransactionDiscard(c *C) {
|
||||
// Test transaction discard functionality
|
||||
dbPath := filepath.Join(s.tempDir, "transaction-discard")
|
||||
|
||||
db, err := goleveldb.NewOpenDB(dbPath)
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// Create transaction
|
||||
tx, err := db.OpenTransaction()
|
||||
c.Check(err, IsNil)
|
||||
|
||||
key := []byte("discard-key")
|
||||
value := []byte("discard-value")
|
||||
|
||||
err = tx.Put(key, value)
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// Discard transaction
|
||||
tx.Discard()
|
||||
|
||||
// Value should not be visible
|
||||
_, err = db.Get(key)
|
||||
c.Check(err, Equals, database.ErrNotFound)
|
||||
|
||||
err = db.Close()
|
||||
c.Check(err, IsNil)
|
||||
}
|
||||
|
||||
func (s *ExtendedLevelDBSuite) TestProcessByPrefixError(c *C) {
|
||||
// Test ProcessByPrefix with processor that returns error
|
||||
dbPath := filepath.Join(s.tempDir, "process-error")
|
||||
|
||||
db, err := goleveldb.NewOpenDB(dbPath)
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// Add some data
|
||||
prefix := []byte("error-")
|
||||
err = db.Put(append(prefix, []byte("key1")...), []byte("value1"))
|
||||
c.Check(err, IsNil)
|
||||
err = db.Put(append(prefix, []byte("key2")...), []byte("value2"))
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// Process with error-returning function
|
||||
testError := errors.New("processing error")
|
||||
processedCount := 0
|
||||
|
||||
err = db.ProcessByPrefix(prefix, func(key, value []byte) error {
|
||||
processedCount++
|
||||
if processedCount == 1 {
|
||||
return testError
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
c.Check(err, Equals, testError)
|
||||
c.Check(processedCount, Equals, 1) // Should stop at first error
|
||||
|
||||
err = db.Close()
|
||||
c.Check(err, IsNil)
|
||||
}
|
||||
|
||||
func (s *ExtendedLevelDBSuite) TestPrefixOperationsEmptyDB(c *C) {
|
||||
// Test prefix operations on empty database
|
||||
dbPath := filepath.Join(s.tempDir, "empty-prefix")
|
||||
|
||||
db, err := goleveldb.NewOpenDB(dbPath)
|
||||
c.Check(err, IsNil)
|
||||
|
||||
prefix := []byte("empty")
|
||||
|
||||
// All prefix operations should return empty results
|
||||
c.Check(db.HasPrefix(prefix), Equals, false)
|
||||
c.Check(db.KeysByPrefix(prefix), DeepEquals, [][]byte{})
|
||||
c.Check(db.FetchByPrefix(prefix), DeepEquals, [][]byte{})
|
||||
|
||||
processedCount := 0
|
||||
err = db.ProcessByPrefix(prefix, func(key, value []byte) error {
|
||||
processedCount++
|
||||
return nil
|
||||
})
|
||||
c.Check(err, IsNil)
|
||||
c.Check(processedCount, Equals, 0)
|
||||
|
||||
err = db.Close()
|
||||
c.Check(err, IsNil)
|
||||
}
|
||||
|
||||
func (s *ExtendedLevelDBSuite) TestBatchOperations(c *C) {
|
||||
// Test batch operations in detail
|
||||
dbPath := filepath.Join(s.tempDir, "batch-ops")
|
||||
|
||||
db, err := goleveldb.NewOpenDB(dbPath)
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// Create batch
|
||||
batch := db.CreateBatch()
|
||||
c.Check(batch, NotNil)
|
||||
|
||||
// Add multiple operations to batch
|
||||
keys := [][]byte{
|
||||
[]byte("batch-key-1"),
|
||||
[]byte("batch-key-2"),
|
||||
[]byte("batch-key-3"),
|
||||
}
|
||||
values := [][]byte{
|
||||
[]byte("batch-value-1"),
|
||||
[]byte("batch-value-2"),
|
||||
[]byte("batch-value-3"),
|
||||
}
|
||||
|
||||
for i, key := range keys {
|
||||
err = batch.Put(key, values[i])
|
||||
c.Check(err, IsNil)
|
||||
}
|
||||
|
||||
// Values should not be visible before Write
|
||||
for _, key := range keys {
|
||||
_, err = db.Get(key)
|
||||
c.Check(err, Equals, database.ErrNotFound)
|
||||
}
|
||||
|
||||
// Write batch
|
||||
err = batch.Write()
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// Now all values should be visible
|
||||
for i, key := range keys {
|
||||
value, err := db.Get(key)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(value, DeepEquals, values[i])
|
||||
}
|
||||
|
||||
err = db.Close()
|
||||
c.Check(err, IsNil)
|
||||
}
|
||||
|
||||
func (s *ExtendedLevelDBSuite) TestIteratorEdgeCases(c *C) {
|
||||
// Test iterator edge cases in prefix operations
|
||||
dbPath := filepath.Join(s.tempDir, "iterator-edge")
|
||||
|
||||
db, err := goleveldb.NewOpenDB(dbPath)
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// Add data with similar but different prefixes
|
||||
prefixes := [][]byte{
|
||||
[]byte("test"),
|
||||
[]byte("test-"),
|
||||
[]byte("test-a"),
|
||||
[]byte("test-ab"),
|
||||
[]byte("testing"),
|
||||
[]byte("totally-different"),
|
||||
}
|
||||
|
||||
for i, prefix := range prefixes {
|
||||
key := append(prefix, []byte("key")...)
|
||||
value := []byte{byte(i)}
|
||||
err = db.Put(key, value)
|
||||
c.Check(err, IsNil)
|
||||
}
|
||||
|
||||
// Test exact prefix matching
|
||||
targetPrefix := []byte("test-")
|
||||
keys := db.KeysByPrefix(targetPrefix)
|
||||
values := db.FetchByPrefix(targetPrefix)
|
||||
|
||||
// Should only match keys that start with "test-"
|
||||
expectedCount := 0
|
||||
for _, prefix := range prefixes {
|
||||
testKey := append(prefix, []byte("key")...)
|
||||
if len(testKey) >= len(targetPrefix) {
|
||||
if string(testKey[:len(targetPrefix)]) == string(targetPrefix) {
|
||||
expectedCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.Check(len(keys), Equals, expectedCount)
|
||||
c.Check(len(values), Equals, expectedCount)
|
||||
|
||||
err = db.Close()
|
||||
c.Check(err, IsNil)
|
||||
}
|
||||
|
||||
func (s *ExtendedLevelDBSuite) TestCompactDBError(c *C) {
|
||||
// Test CompactDB on closed database
|
||||
dbPath := filepath.Join(s.tempDir, "compact-error")
|
||||
|
||||
db, err := goleveldb.NewOpenDB(dbPath)
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// Close database
|
||||
err = db.Close()
|
||||
c.Check(err, IsNil)
|
||||
|
||||
// CompactDB should error on closed database
|
||||
err = db.CompactDB()
|
||||
c.Check(err, NotNil)
|
||||
}
|
||||
|
||||
func (s *ExtendedLevelDBSuite) TestInterface(c *C) {
|
||||
// Test that storage implements database.Storage interface
|
||||
dbPath := filepath.Join(s.tempDir, "interface-test")
|
||||
|
||||
var storage database.Storage
|
||||
storage, err := goleveldb.NewOpenDB(dbPath)
|
||||
c.Check(err, IsNil)
|
||||
c.Check(storage, NotNil)
|
||||
|
||||
// Test that all interface methods are available
|
||||
_, err = storage.Get([]byte("test"))
|
||||
c.Check(err, Equals, database.ErrNotFound)
|
||||
|
||||
err = storage.Put([]byte("test"), []byte("value"))
|
||||
c.Check(err, IsNil)
|
||||
|
||||
err = storage.Delete([]byte("test"))
|
||||
c.Check(err, IsNil)
|
||||
|
||||
c.Check(storage.HasPrefix([]byte("test")), Equals, false)
|
||||
c.Check(storage.KeysByPrefix([]byte("test")), DeepEquals, [][]byte{})
|
||||
c.Check(storage.FetchByPrefix([]byte("test")), DeepEquals, [][]byte{})
|
||||
|
||||
err = storage.ProcessByPrefix([]byte("test"), func(k, v []byte) error { return nil })
|
||||
c.Check(err, IsNil)
|
||||
|
||||
batch := storage.CreateBatch()
|
||||
c.Check(batch, NotNil)
|
||||
|
||||
tx, err := storage.OpenTransaction()
|
||||
c.Check(err, IsNil)
|
||||
c.Check(tx, NotNil)
|
||||
tx.Discard()
|
||||
|
||||
temp, err := storage.CreateTemporary()
|
||||
c.Check(err, IsNil)
|
||||
c.Check(temp, NotNil)
|
||||
temp.Close()
|
||||
temp.Drop()
|
||||
|
||||
err = storage.CompactDB()
|
||||
c.Check(err, IsNil)
|
||||
|
||||
err = storage.Close()
|
||||
c.Check(err, IsNil)
|
||||
|
||||
err = storage.Drop()
|
||||
c.Check(err, IsNil)
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
package goleveldb
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Test for database close race condition
|
||||
func TestStorageCloseRace(t *testing.T) {
|
||||
// Create temporary storage
|
||||
tempdir, err := os.MkdirTemp("", "aptly-race-test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tempdir)
|
||||
|
||||
db, err := internalOpen(tempdir, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
storage := &storage{db: db, path: tempdir}
|
||||
|
||||
// Put some initial data
|
||||
err = storage.Put([]byte("test-key"), []byte("test-value"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
errors := make(chan error, 100)
|
||||
panics := make(chan string, 100)
|
||||
|
||||
// Start multiple goroutines doing database operations
|
||||
for i := 0; i < 10; i++ {
|
||||
wg.Add(1)
|
||||
go func(id int) {
|
||||
defer wg.Done()
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
panics <- fmt.Sprintf("goroutine %d panicked: %v", id, r)
|
||||
}
|
||||
}()
|
||||
|
||||
// Continuously perform operations
|
||||
for j := 0; j < 100; j++ {
|
||||
// Try Get operation
|
||||
_, err := storage.Get([]byte("test-key"))
|
||||
if err != nil && err.Error() != "database is nil" {
|
||||
errors <- fmt.Errorf("get error in goroutine %d: %v", id, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Try Put operation
|
||||
err = storage.Put([]byte(fmt.Sprintf("key-%d-%d", id, j)), []byte("value"))
|
||||
if err != nil && err.Error() != "database is nil" {
|
||||
errors <- fmt.Errorf("put error in goroutine %d: %v", id, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Small delay to increase race window
|
||||
time.Sleep(time.Microsecond)
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
// Start goroutines that close the database
|
||||
for i := 0; i < 5; i++ {
|
||||
wg.Add(1)
|
||||
go func(id int) {
|
||||
defer wg.Done()
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
panics <- fmt.Sprintf("close goroutine %d panicked: %v", id, r)
|
||||
}
|
||||
}()
|
||||
|
||||
// Wait a bit then close
|
||||
time.Sleep(time.Duration(id*10) * time.Millisecond)
|
||||
err := storage.Close()
|
||||
if err != nil {
|
||||
errors <- fmt.Errorf("close error in goroutine %d: %v", id, err)
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(errors)
|
||||
close(panics)
|
||||
|
||||
// Check for panics (indicates race condition bug)
|
||||
for panic := range panics {
|
||||
t.Errorf("Race condition caused panic: %s", panic)
|
||||
}
|
||||
|
||||
// Some errors are expected (database closed), but panics are not
|
||||
errorCount := 0
|
||||
for err := range errors {
|
||||
errorCount++
|
||||
if errorCount < 10 { // Only log first few errors
|
||||
t.Logf("Expected error during race: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test concurrent operations vs close
|
||||
func TestStorageConcurrentOpsVsClose(t *testing.T) {
|
||||
for attempt := 0; attempt < 5; attempt++ {
|
||||
// Create fresh storage for each attempt
|
||||
tempdir, err := os.MkdirTemp("", "aptly-concurrent-test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tempdir)
|
||||
|
||||
db, err := internalOpen(tempdir, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
storage := &storage{db: db, path: tempdir}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
panicked := make(chan bool, 1)
|
||||
|
||||
// Goroutine performing operations
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
panicked <- true
|
||||
}
|
||||
}()
|
||||
|
||||
for i := 0; i < 1000; i++ {
|
||||
storage.Get([]byte("key"))
|
||||
storage.Put([]byte("key"), []byte("value"))
|
||||
storage.Delete([]byte("key"))
|
||||
}
|
||||
}()
|
||||
|
||||
// Goroutine closing database
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
time.Sleep(50 * time.Millisecond) // Let operations start
|
||||
storage.Close()
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// Check if panic occurred
|
||||
select {
|
||||
case <-panicked:
|
||||
t.Errorf("Attempt %d: Panic occurred during concurrent ops vs close", attempt)
|
||||
default:
|
||||
// No panic - good
|
||||
}
|
||||
|
||||
close(panicked)
|
||||
}
|
||||
}
|
||||
|
||||
// Test multiple concurrent close attempts
|
||||
func TestStorageMultipleClose(t *testing.T) {
|
||||
tempdir, err := os.MkdirTemp("", "aptly-close-test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tempdir)
|
||||
|
||||
db, err := internalOpen(tempdir, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
storage := &storage{db: db, path: tempdir}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
panics := make(chan string, 20)
|
||||
|
||||
// Multiple goroutines trying to close
|
||||
for i := 0; i < 20; i++ {
|
||||
wg.Add(1)
|
||||
go func(id int) {
|
||||
defer wg.Done()
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
panics <- fmt.Sprintf("close %d panicked: %v", id, r)
|
||||
}
|
||||
}()
|
||||
|
||||
err := storage.Close()
|
||||
if err != nil {
|
||||
// Error is ok, panic is not
|
||||
t.Logf("Close %d got error (expected): %v", id, err)
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(panics)
|
||||
|
||||
// Check for panics
|
||||
for panic := range panics {
|
||||
t.Errorf("Multiple close caused panic: %s", panic)
|
||||
}
|
||||
}
|
||||
|
||||
// Test iterator operations during close
|
||||
func TestStorageIteratorRace(t *testing.T) {
|
||||
tempdir, err := os.MkdirTemp("", "aptly-iterator-test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tempdir)
|
||||
|
||||
db, err := internalOpen(tempdir, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
storage := &storage{db: db, path: tempdir}
|
||||
|
||||
// Add some data
|
||||
for i := 0; i < 100; i++ {
|
||||
storage.Put([]byte(fmt.Sprintf("key-%03d", i)), []byte("value"))
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
panics := make(chan string, 10)
|
||||
|
||||
// Goroutines using iterators
|
||||
for i := 0; i < 5; i++ {
|
||||
wg.Add(1)
|
||||
go func(id int) {
|
||||
defer wg.Done()
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
panics <- fmt.Sprintf("iterator %d panicked: %v", id, r)
|
||||
}
|
||||
}()
|
||||
|
||||
// Use methods that create iterators
|
||||
storage.KeysByPrefix([]byte("key-"))
|
||||
storage.FetchByPrefix([]byte("key-"))
|
||||
storage.HasPrefix([]byte("key-"))
|
||||
}(i)
|
||||
}
|
||||
|
||||
// Close database while iterators are running
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
storage.Close()
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
close(panics)
|
||||
|
||||
// Check for panics
|
||||
for panic := range panics {
|
||||
t.Errorf("Iterator race caused panic: %s", panic)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package goleveldb
|
||||
|
||||
import (
|
||||
"github.com/aptly-dev/aptly/database"
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
type LevelDBStorageSuite struct {
|
||||
storage *storage
|
||||
tempDir string
|
||||
}
|
||||
|
||||
var _ = Suite(&LevelDBStorageSuite{})
|
||||
|
||||
func (s *LevelDBStorageSuite) SetUpTest(c *C) {
|
||||
s.tempDir = c.MkDir()
|
||||
s.storage = &storage{
|
||||
path: s.tempDir,
|
||||
db: nil, // Not opened for unit tests
|
||||
}
|
||||
}
|
||||
|
||||
func (s *LevelDBStorageSuite) TestCreateTemporary(c *C) {
|
||||
// Test creating temporary storage
|
||||
tempStorage, err := s.storage.CreateTemporary()
|
||||
if err != nil {
|
||||
// Expected to fail without real leveldb setup
|
||||
c.Check(err, NotNil)
|
||||
return
|
||||
}
|
||||
|
||||
c.Check(tempStorage, NotNil)
|
||||
levelStorage, ok := tempStorage.(*storage)
|
||||
c.Check(ok, Equals, true)
|
||||
c.Check(len(levelStorage.path) > 0, Equals, true)
|
||||
c.Check(levelStorage.path, Not(Equals), s.storage.path)
|
||||
}
|
||||
|
||||
func (s *LevelDBStorageSuite) TestCloseNilDB(c *C) {
|
||||
// Test closing storage with nil DB
|
||||
err := s.storage.Close()
|
||||
c.Check(err, IsNil)
|
||||
}
|
||||
|
||||
func (s *LevelDBStorageSuite) TestOpenNilDB(c *C) {
|
||||
// Test opening storage - should succeed with valid path
|
||||
err := s.storage.Open()
|
||||
// Should succeed with valid temporary directory
|
||||
c.Check(err, IsNil)
|
||||
// Clean up
|
||||
s.storage.Close()
|
||||
}
|
||||
|
||||
func (s *LevelDBStorageSuite) TestCreateBatchNilDB(c *C) {
|
||||
// Test creating batch with nil DB
|
||||
batch := s.storage.CreateBatch()
|
||||
c.Check(batch, IsNil)
|
||||
}
|
||||
|
||||
func (s *LevelDBStorageSuite) TestCompactDB(c *C) {
|
||||
// Test CompactDB with nil DB - should handle gracefully
|
||||
err := s.storage.CompactDB()
|
||||
c.Check(err, NotNil) // Expected to fail with nil DB
|
||||
}
|
||||
|
||||
func (s *LevelDBStorageSuite) TestDropNilDB(c *C) {
|
||||
// Test dropping storage with nil DB
|
||||
err := s.storage.Drop()
|
||||
c.Check(err, IsNil) // Should succeed (removes directory)
|
||||
}
|
||||
|
||||
func (s *LevelDBStorageSuite) TestInterfaceCompliance(c *C) {
|
||||
// Test that storage implements database.Storage interface
|
||||
var dbStorage database.Storage = &storage{}
|
||||
c.Check(dbStorage, NotNil)
|
||||
}
|
||||
|
||||
func (s *LevelDBStorageSuite) TestGetNilDB(c *C) {
|
||||
// Test Get with nil DB - should fail
|
||||
_, err := s.storage.Get([]byte("key"))
|
||||
c.Check(err, NotNil) // Expected to fail with nil DB
|
||||
}
|
||||
|
||||
// Note: storage does not implement Has method - it uses Get and checks for ErrNotFound
|
||||
|
||||
func (s *LevelDBStorageSuite) TestPutNilDB(c *C) {
|
||||
// Test Put with nil DB - should fail
|
||||
err := s.storage.Put([]byte("key"), []byte("value"))
|
||||
c.Check(err, NotNil) // Expected to fail with nil DB
|
||||
}
|
||||
|
||||
func (s *LevelDBStorageSuite) TestDeleteNilDB(c *C) {
|
||||
// Test Delete with nil DB - should fail
|
||||
err := s.storage.Delete([]byte("key"))
|
||||
c.Check(err, NotNil) // Expected to fail with nil DB
|
||||
}
|
||||
|
||||
func (s *LevelDBStorageSuite) TestKeysByPrefixNilDB(c *C) {
|
||||
// Test KeysByPrefix with nil DB - should return nil
|
||||
keys := s.storage.KeysByPrefix([]byte("prefix/"))
|
||||
c.Check(keys, IsNil)
|
||||
}
|
||||
|
||||
func (s *LevelDBStorageSuite) TestFetchByPrefixNilDB(c *C) {
|
||||
// Test FetchByPrefix with nil DB - should return nil
|
||||
values := s.storage.FetchByPrefix([]byte("prefix/"))
|
||||
c.Check(values, IsNil)
|
||||
}
|
||||
|
||||
func (s *LevelDBStorageSuite) TestHasPrefixNilDB(c *C) {
|
||||
// Test HasPrefix with nil DB - should return false
|
||||
result := s.storage.HasPrefix([]byte("prefix/"))
|
||||
c.Check(result, Equals, false)
|
||||
}
|
||||
|
||||
func (s *LevelDBStorageSuite) TestProcessByPrefixNilDB(c *C) {
|
||||
// Test ProcessByPrefix with nil DB - should fail
|
||||
processor := func(key, value []byte) error { return nil }
|
||||
err := s.storage.ProcessByPrefix([]byte("prefix/"), processor)
|
||||
c.Check(err, NotNil) // Expected to fail with nil DB
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package goleveldb
|
||||
|
||||
import (
|
||||
"github.com/aptly-dev/aptly/database"
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
type TransactionSuite struct {
|
||||
storage *storage
|
||||
tempDir string
|
||||
}
|
||||
|
||||
var _ = Suite(&TransactionSuite{})
|
||||
|
||||
func (s *TransactionSuite) SetUpTest(c *C) {
|
||||
s.tempDir = c.MkDir()
|
||||
s.storage = &storage{
|
||||
path: s.tempDir,
|
||||
db: nil, // Not opened for unit tests
|
||||
}
|
||||
}
|
||||
|
||||
func (s *TransactionSuite) TestOpenTransactionNilDB(c *C) {
|
||||
// Test opening transaction with nil DB - should fail
|
||||
transaction, err := s.storage.OpenTransaction()
|
||||
c.Check(err, NotNil) // Expected to fail with nil DB
|
||||
c.Check(transaction, IsNil)
|
||||
}
|
||||
|
||||
func (s *TransactionSuite) TestInterfaceCompliance(c *C) {
|
||||
// Test that storage implements the transaction interface
|
||||
var storageInterface database.Storage = &storage{}
|
||||
c.Check(storageInterface, NotNil)
|
||||
|
||||
// Test that we can call OpenTransaction method
|
||||
_, err := storageInterface.OpenTransaction()
|
||||
c.Check(err, NotNil) // Expected to fail without proper setup
|
||||
}
|
||||
Reference in New Issue
Block a user