mirror of
https://github.com/caddyserver/caddy.git
synced 2025-10-20 08:13:17 +00:00
351 lines
7.8 KiB
Go
351 lines
7.8 KiB
Go
// Copyright 2015 Matthew Holt and The Caddy Authors
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package caddy
|
|
|
|
import (
|
|
"fmt"
|
|
"io/fs"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// Mock filesystem implementation for testing
|
|
type mockFileSystem struct {
|
|
name string
|
|
files map[string]string
|
|
}
|
|
|
|
func (m *mockFileSystem) Open(name string) (fs.File, error) {
|
|
if content, exists := m.files[name]; exists {
|
|
return &mockFile{name: name, content: content}, nil
|
|
}
|
|
return nil, fs.ErrNotExist
|
|
}
|
|
|
|
type mockFile struct {
|
|
name string
|
|
content string
|
|
pos int
|
|
}
|
|
|
|
func (m *mockFile) Stat() (fs.FileInfo, error) {
|
|
return &mockFileInfo{name: m.name, size: int64(len(m.content))}, nil
|
|
}
|
|
|
|
func (m *mockFile) Read(b []byte) (int, error) {
|
|
if m.pos >= len(m.content) {
|
|
return 0, fs.ErrClosed
|
|
}
|
|
n := copy(b, m.content[m.pos:])
|
|
m.pos += n
|
|
return n, nil
|
|
}
|
|
|
|
func (m *mockFile) Close() error {
|
|
return nil
|
|
}
|
|
|
|
type mockFileInfo struct {
|
|
name string
|
|
size int64
|
|
}
|
|
|
|
func (m *mockFileInfo) Name() string { return m.name }
|
|
func (m *mockFileInfo) Size() int64 { return m.size }
|
|
func (m *mockFileInfo) Mode() fs.FileMode { return 0o644 }
|
|
func (m *mockFileInfo) ModTime() time.Time {
|
|
return time.Time{}
|
|
}
|
|
func (m *mockFileInfo) IsDir() bool { return false }
|
|
func (m *mockFileInfo) Sys() any { return nil }
|
|
|
|
// Mock FileSystems implementation for testing
|
|
type mockFileSystems struct {
|
|
mu sync.RWMutex
|
|
filesystems map[string]fs.FS
|
|
defaultFS fs.FS
|
|
}
|
|
|
|
func newMockFileSystems() *mockFileSystems {
|
|
return &mockFileSystems{
|
|
filesystems: make(map[string]fs.FS),
|
|
defaultFS: &mockFileSystem{name: "default", files: map[string]string{"default.txt": "default content"}},
|
|
}
|
|
}
|
|
|
|
func (m *mockFileSystems) Register(k string, v fs.FS) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.filesystems[k] = v
|
|
}
|
|
|
|
func (m *mockFileSystems) Unregister(k string) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
delete(m.filesystems, k)
|
|
}
|
|
|
|
func (m *mockFileSystems) Get(k string) (fs.FS, bool) {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
v, ok := m.filesystems[k]
|
|
return v, ok
|
|
}
|
|
|
|
func (m *mockFileSystems) Default() fs.FS {
|
|
return m.defaultFS
|
|
}
|
|
|
|
func TestFileSystems_Register_Get(t *testing.T) {
|
|
fsys := newMockFileSystems()
|
|
mockFS := &mockFileSystem{
|
|
name: "test",
|
|
files: map[string]string{"test.txt": "test content"},
|
|
}
|
|
|
|
// Register filesystem
|
|
fsys.Register("test", mockFS)
|
|
|
|
// Retrieve filesystem
|
|
retrieved, exists := fsys.Get("test")
|
|
if !exists {
|
|
t.Error("Expected filesystem to exist after registration")
|
|
}
|
|
if retrieved != mockFS {
|
|
t.Error("Retrieved filesystem is not the same as registered")
|
|
}
|
|
}
|
|
|
|
func TestFileSystems_Unregister(t *testing.T) {
|
|
fsys := newMockFileSystems()
|
|
mockFS := &mockFileSystem{name: "test"}
|
|
|
|
// Register then unregister
|
|
fsys.Register("test", mockFS)
|
|
fsys.Unregister("test")
|
|
|
|
// Should not exist after unregistration
|
|
_, exists := fsys.Get("test")
|
|
if exists {
|
|
t.Error("Filesystem should not exist after unregistration")
|
|
}
|
|
}
|
|
|
|
func TestFileSystems_Default(t *testing.T) {
|
|
fsys := newMockFileSystems()
|
|
|
|
defaultFS := fsys.Default()
|
|
if defaultFS == nil {
|
|
t.Error("Default filesystem should not be nil")
|
|
}
|
|
|
|
// Test that default filesystem works
|
|
file, err := defaultFS.Open("default.txt")
|
|
if err != nil {
|
|
t.Fatalf("Failed to open default file: %v", err)
|
|
}
|
|
defer file.Close()
|
|
|
|
data := make([]byte, 100)
|
|
n, err := file.Read(data)
|
|
if err != nil && err != fs.ErrClosed {
|
|
t.Fatalf("Failed to read default file: %v", err)
|
|
}
|
|
|
|
content := string(data[:n])
|
|
if content != "default content" {
|
|
t.Errorf("Expected 'default content', got '%s'", content)
|
|
}
|
|
}
|
|
|
|
func TestFileSystems_Concurrent_Access(t *testing.T) {
|
|
fsys := newMockFileSystems()
|
|
|
|
const numGoroutines = 50
|
|
const numOperations = 10
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
// Concurrent register/unregister/get operations
|
|
for i := 0; i < numGoroutines; i++ {
|
|
wg.Add(1)
|
|
go func(id int) {
|
|
defer wg.Done()
|
|
|
|
key := fmt.Sprintf("fs-%d", id)
|
|
mockFS := &mockFileSystem{
|
|
name: key,
|
|
files: map[string]string{key + ".txt": "content"},
|
|
}
|
|
|
|
for j := 0; j < numOperations; j++ {
|
|
// Register
|
|
fsys.Register(key, mockFS)
|
|
|
|
// Get
|
|
retrieved, exists := fsys.Get(key)
|
|
if !exists {
|
|
t.Errorf("Filesystem %s should exist", key)
|
|
continue
|
|
}
|
|
if retrieved != mockFS {
|
|
t.Errorf("Retrieved filesystem for %s is not correct", key)
|
|
}
|
|
|
|
// Test file access
|
|
file, err := retrieved.Open(key + ".txt")
|
|
if err != nil {
|
|
t.Errorf("Failed to open file in %s: %v", key, err)
|
|
continue
|
|
}
|
|
file.Close()
|
|
|
|
// Unregister
|
|
fsys.Unregister(key)
|
|
|
|
// Should not exist after unregister
|
|
_, stillExists := fsys.Get(key)
|
|
if stillExists {
|
|
t.Errorf("Filesystem %s should not exist after unregister", key)
|
|
}
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
wg.Wait()
|
|
}
|
|
|
|
func TestFileSystems_Get_NonExistent(t *testing.T) {
|
|
fsys := newMockFileSystems()
|
|
|
|
_, exists := fsys.Get("non-existent")
|
|
if exists {
|
|
t.Error("Non-existent filesystem should not exist")
|
|
}
|
|
}
|
|
|
|
func TestFileSystems_Register_Overwrite(t *testing.T) {
|
|
fsys := newMockFileSystems()
|
|
key := "overwrite-test"
|
|
|
|
// Register first filesystem
|
|
fs1 := &mockFileSystem{name: "fs1"}
|
|
fsys.Register(key, fs1)
|
|
|
|
// Register second filesystem with same key (should overwrite)
|
|
fs2 := &mockFileSystem{name: "fs2"}
|
|
fsys.Register(key, fs2)
|
|
|
|
// Should get the second filesystem
|
|
retrieved, exists := fsys.Get(key)
|
|
if !exists {
|
|
t.Error("Filesystem should exist")
|
|
}
|
|
if retrieved != fs2 {
|
|
t.Error("Should get the overwritten filesystem")
|
|
}
|
|
if retrieved == fs1 {
|
|
t.Error("Should not get the original filesystem")
|
|
}
|
|
}
|
|
|
|
func TestFileSystems_Concurrent_RegisterUnregister_SameKey(t *testing.T) {
|
|
fsys := newMockFileSystems()
|
|
key := "concurrent-key"
|
|
|
|
const numGoroutines = 20
|
|
var wg sync.WaitGroup
|
|
|
|
// Half the goroutines register, half unregister
|
|
for i := 0; i < numGoroutines; i++ {
|
|
wg.Add(1)
|
|
if i%2 == 0 {
|
|
go func(id int) {
|
|
defer wg.Done()
|
|
mockFS := &mockFileSystem{name: fmt.Sprintf("fs-%d", id)}
|
|
fsys.Register(key, mockFS)
|
|
}(i)
|
|
} else {
|
|
go func() {
|
|
defer wg.Done()
|
|
fsys.Unregister(key)
|
|
}()
|
|
}
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
// The final state is unpredictable due to race conditions,
|
|
// but the operations should not panic or cause corruption
|
|
// Test passes if we reach here without issues
|
|
}
|
|
|
|
func TestFileSystems_StressTest(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("Skipping stress test in short mode")
|
|
}
|
|
|
|
fsys := newMockFileSystems()
|
|
|
|
const numGoroutines = 100
|
|
const duration = 100 * time.Millisecond
|
|
|
|
var wg sync.WaitGroup
|
|
stopChan := make(chan struct{})
|
|
|
|
// Start timer
|
|
go func() {
|
|
time.Sleep(duration)
|
|
close(stopChan)
|
|
}()
|
|
|
|
// Stress test with continuous operations
|
|
for i := 0; i < numGoroutines; i++ {
|
|
wg.Add(1)
|
|
go func(id int) {
|
|
defer wg.Done()
|
|
|
|
key := fmt.Sprintf("stress-fs-%d", id%10) // Use limited set of keys
|
|
mockFS := &mockFileSystem{
|
|
name: key,
|
|
files: map[string]string{key + ".txt": "stress content"},
|
|
}
|
|
|
|
for {
|
|
select {
|
|
case <-stopChan:
|
|
return
|
|
default:
|
|
// Rapid register/get/unregister cycles
|
|
fsys.Register(key, mockFS)
|
|
|
|
if retrieved, exists := fsys.Get(key); exists {
|
|
// Try to use the filesystem
|
|
if file, err := retrieved.Open(key + ".txt"); err == nil {
|
|
file.Close()
|
|
}
|
|
}
|
|
|
|
fsys.Unregister(key)
|
|
}
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
// Test passes if we reach here without panics or deadlocks
|
|
}
|