From 134be0d0eda13e35f97ff84e37932f444b713268 Mon Sep 17 00:00:00 2001 From: avallete Date: Fri, 13 Jun 2025 12:29:39 +0200 Subject: [PATCH 1/7] chore: add watcher tests and main.ts --- internal/functions/serve/templates/main.ts | 34 ++- internal/functions/serve/watcher_test.go | 332 +++++++++++++++++++++ 2 files changed, 363 insertions(+), 3 deletions(-) create mode 100644 internal/functions/serve/watcher_test.go diff --git a/internal/functions/serve/templates/main.ts b/internal/functions/serve/templates/main.ts index 534409a98..568c547f8 100644 --- a/internal/functions/serve/templates/main.ts +++ b/internal/functions/serve/templates/main.ts @@ -48,6 +48,7 @@ const DENO_SB_ERROR_MAP = new Map([ SB_SPECIFIC_ERROR_CODE.WorkerLimit, ], ]); +const GENERIC_FUNCTION_SERVE_MESSAGE = `Serving functions on http://127.0.0.1:${HOST_PORT}/functions/v1/` interface FunctionConfig { entrypointPath: string; @@ -228,9 +229,36 @@ Deno.serve({ }, onListen: () => { - console.log( - `Serving functions on http://127.0.0.1:${HOST_PORT}/functions/v1/\nUsing ${Deno.version.deno}`, - ); + try { + const functionsConfigString = Deno.env.get( + "SUPABASE_INTERNAL_FUNCTIONS_CONFIG" + ); + if (functionsConfigString) { + const MAX_FUNCTIONS_URL_EXAMPLES = 5 + const functionsConfig = JSON.parse(functionsConfigString) as Record< + string, + unknown + >; + const functionNames = Object.keys(functionsConfig); + const exampleFunctions = functionNames.slice(0, MAX_FUNCTIONS_URL_EXAMPLES); + const functionsUrls = exampleFunctions.map( + (fname) => ` - http://127.0.0.1:${HOST_PORT}/functions/v1/${fname}` + ); + const functionsExamplesMessages = functionNames.length > 0 + // Show some functions urls examples + ? `\n${functionsUrls.join(`\n`)}${functionNames.length > MAX_FUNCTIONS_URL_EXAMPLES + // If we have more than 10 functions to serve, then show examples for first 10 + // and a count for the remaining ones + ? `\n... and ${functionNames.length - MAX_FUNCTIONS_URL_EXAMPLES} more functions` + : ''}` + : '' + console.log(`${GENERIC_FUNCTION_SERVE_MESSAGE}${functionsExamplesMessages}\nUsing ${Deno.version.deno}`); + } + } catch (e) { + console.log( + `${GENERIC_FUNCTION_SERVE_MESSAGE}\nUsing ${Deno.version.deno}` + ); + } }, onError: e => { diff --git a/internal/functions/serve/watcher_test.go b/internal/functions/serve/watcher_test.go new file mode 100644 index 000000000..f5a953ccf --- /dev/null +++ b/internal/functions/serve/watcher_test.go @@ -0,0 +1,332 @@ +package serve + +import ( + "context" + "fmt" + "os" + "path/filepath" + "testing" + "time" + + "github.com/spf13/afero" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// Integration test setup for watcher functionality +type WatcherIntegrationSetup struct { + T *testing.T + Context context.Context + Cancel context.CancelFunc + TempDir string +} + +func NewWatcherIntegrationSetup(t *testing.T) *WatcherIntegrationSetup { + ctx, cancel := context.WithCancel(context.Background()) + tempDir := t.TempDir() + + setup := &WatcherIntegrationSetup{ + T: t, + Context: ctx, + Cancel: cancel, + TempDir: tempDir, + } + + return setup +} + +func (s *WatcherIntegrationSetup) Cleanup() { + s.Cancel() +} + +// SetupFunctionsDirectory creates a functions directory with test functions +func (s *WatcherIntegrationSetup) SetupFunctionsDirectory() string { + functionsDir := filepath.Join(s.TempDir, "supabase", "functions") + require.NoError(s.T, os.MkdirAll(functionsDir, 0755)) + + // Set up test functions + s.createFunction("hello", `export default () => new Response("Hello World")`) + s.createFunction("protected", `export default () => new Response("Protected")`) + + return functionsDir +} + +func (s *WatcherIntegrationSetup) SetupSupabaseDirectory() string { + supabaseDir := filepath.Join(s.TempDir, "supabase") + require.NoError(s.T, os.MkdirAll(supabaseDir, 0755)) + + return supabaseDir +} + +func (s *WatcherIntegrationSetup) createFunction(name, content string) { + funcDir := filepath.Join(s.TempDir, "supabase", "functions", name) + require.NoError(s.T, os.MkdirAll(funcDir, 0755)) + require.NoError(s.T, os.WriteFile(filepath.Join(funcDir, "index.ts"), []byte(content), 0644)) +} + +// CreateFileWatcher creates and configures a debounce file watcher for testing +func (s *WatcherIntegrationSetup) CreateFileWatcher() (*debounceFileWatcher, error) { + watcher, err := NewDebounceFileWatcher() + if err != nil { + return nil, err + } + + // Set up watch paths to include our test directory + fsys := afero.NewOsFs() + watchPaths := []string{s.TempDir} + + if err := watcher.SetWatchPaths(watchPaths, fsys); err != nil { + watcher.Close() + return nil, err + } + + return watcher, nil +} + +func TestFileWatcherIntegration(t *testing.T) { + t.Run("detects TypeScript function changes and triggers restart", func(t *testing.T) { + setup := NewWatcherIntegrationSetup(t) + defer setup.Cleanup() + + functionsDir := setup.SetupFunctionsDirectory() + + watcher, err := setup.CreateFileWatcher() + require.NoError(t, err) + defer watcher.Close() + + // Start the watcher + ctx, cancel := context.WithTimeout(setup.Context, 5*time.Second) + defer cancel() + + go watcher.Start(ctx) + + // Give watcher time to initialize + time.Sleep(100 * time.Millisecond) + + // Modify a function file + funcFile := filepath.Join(functionsDir, "hello", "index.ts") + newContent := `export default () => new Response("Hello Modified World")` + require.NoError(t, os.WriteFile(funcFile, []byte(newContent), 0644)) + + // Wait for restart signal + select { + case <-watcher.RestartCh: + // Expected - file change should trigger restart + case <-time.After(2 * time.Second): + t.Error("Expected restart signal after modifying TypeScript file") + } + }) + + t.Run("ignores editor temporary files", func(t *testing.T) { + setup := NewWatcherIntegrationSetup(t) + defer setup.Cleanup() + + functionsDir := setup.SetupFunctionsDirectory() + + watcher, err := setup.CreateFileWatcher() + require.NoError(t, err) + defer watcher.Close() + + ctx, cancel := context.WithTimeout(setup.Context, 3*time.Second) + defer cancel() + + go watcher.Start(ctx) + + // Give watcher time to initialize + time.Sleep(100 * time.Millisecond) + + // Create various temporary/editor files that should be ignored + tempFiles := []string{ + filepath.Join(functionsDir, "hello", "test.txt~"), // Backup file + filepath.Join(functionsDir, "hello", ".test.swp"), // Vim swap + filepath.Join(functionsDir, "hello", ".#test.ts"), // Emacs lock + filepath.Join(functionsDir, "hello", "test.tmp"), // Temp file + filepath.Join(functionsDir, "hello", "___deno_temp___"), // Deno temp + } + + for _, tempFile := range tempFiles { + require.NoError(t, os.WriteFile(tempFile, []byte("temp content"), 0644)) + time.Sleep(50 * time.Millisecond) + } + + // Wait for debounce period + time.Sleep(600 * time.Millisecond) + + // Should not receive any restart signals from ignored files + select { + case <-watcher.RestartCh: + t.Error("Received unexpected restart signal from ignored files") + case <-time.After(100 * time.Millisecond): + // Expected - no restart for ignored files + } + }) + + t.Run("detects config file changes and triggers restart", func(t *testing.T) { + setup := NewWatcherIntegrationSetup(t) + defer setup.Cleanup() + + supabaseDir := setup.SetupSupabaseDirectory() + + watcher, err := setup.CreateFileWatcher() + require.NoError(t, err) + defer watcher.Close() + + ctx, cancel := context.WithTimeout(setup.Context, 3*time.Second) + defer cancel() + + go watcher.Start(ctx) + + // Give watcher time to initialize + time.Sleep(100 * time.Millisecond) + + // Create and modify a config.toml file + configFile := filepath.Join(supabaseDir, "config.toml") + configContent := `[functions.hello] +enabled = true +verify_jwt = false` + require.NoError(t, os.WriteFile(configFile, []byte(configContent), 0644)) + + // Wait for restart signal + select { + case <-watcher.RestartCh: + // Expected - config change should trigger restart + case <-time.After(2 * time.Second): + t.Error("Expected restart signal after modifying config file") + } + }) + + t.Run("handles file watcher errors gracefully", func(t *testing.T) { + setup := NewWatcherIntegrationSetup(t) + defer setup.Cleanup() + + watcher, err := setup.CreateFileWatcher() + require.NoError(t, err) + defer watcher.Close() + + ctx, cancel := context.WithTimeout(setup.Context, 1*time.Second) + defer cancel() + + // Start watcher + go watcher.Start(ctx) + + // Monitor for errors + select { + case err := <-watcher.ErrCh: + // If we get an error, it should be handled gracefully + t.Logf("Watcher error (handled gracefully): %v", err) + case <-ctx.Done(): + // Expected - timeout without critical errors + } + }) + + t.Run("debounces rapid file changes", func(t *testing.T) { + setup := NewWatcherIntegrationSetup(t) + defer setup.Cleanup() + + functionsDir := setup.SetupFunctionsDirectory() + + watcher, err := setup.CreateFileWatcher() + require.NoError(t, err) + defer watcher.Close() + + ctx, cancel := context.WithTimeout(setup.Context, 5*time.Second) + defer cancel() + + go watcher.Start(ctx) + + // Give watcher time to initialize + time.Sleep(100 * time.Millisecond) + + // Make rapid changes to a file + funcFile := filepath.Join(functionsDir, "hello", "index.ts") + for i := 0; i < 5; i++ { + content := fmt.Sprintf(`export default () => new Response("Hello %d")`, i) + require.NoError(t, os.WriteFile(funcFile, []byte(content), 0644)) + time.Sleep(50 * time.Millisecond) // Less than debounce duration + } + + // Should only get one restart signal due to debouncing + restartCount := 0 + timeout := time.After(1 * time.Second) + + for { + select { + case <-watcher.RestartCh: + restartCount++ + // Continue to see if more signals come through + case <-timeout: + // Done counting + goto done + } + } + + done: + // Should have only one restart signal due to debouncing + assert.Equal(t, 1, restartCount, "Expected exactly one restart signal due to debouncing") + }) + + t.Run("watches multiple directories", func(t *testing.T) { + setup := NewWatcherIntegrationSetup(t) + defer setup.Cleanup() + + // Create multiple directories with functions + functionsDir := setup.SetupFunctionsDirectory() + libDir := filepath.Join(setup.TempDir, "lib") + require.NoError(t, os.MkdirAll(libDir, 0755)) + + // Create a utility file in lib directory + utilFile := filepath.Join(libDir, "utils.ts") + require.NoError(t, os.WriteFile(utilFile, []byte(`export function util() { return "utility"; }`), 0644)) + + watcher, err := NewDebounceFileWatcher() + require.NoError(t, err) + defer watcher.Close() + + // Set up watch paths to include both directories + fsys := afero.NewOsFs() + watchPaths := []string{functionsDir, libDir} + require.NoError(t, watcher.SetWatchPaths(watchPaths, fsys)) + + ctx, cancel := context.WithTimeout(setup.Context, 3*time.Second) + defer cancel() + + go watcher.Start(ctx) + + // Give watcher time to initialize + time.Sleep(100 * time.Millisecond) + + // Modify file in lib directory + require.NoError(t, os.WriteFile(utilFile, []byte(`export function util() { return "modified utility"; }`), 0644)) + + // Wait for restart signal + select { + case <-watcher.RestartCh: + // Expected - change in watched lib directory should trigger restart + case <-time.After(2 * time.Second): + t.Error("Expected restart signal after modifying file in watched lib directory") + } + }) + + t.Run("stops watching when context is cancelled", func(t *testing.T) { + setup := NewWatcherIntegrationSetup(t) + defer setup.Cleanup() + + setup.SetupFunctionsDirectory() + + watcher, err := setup.CreateFileWatcher() + require.NoError(t, err) + defer watcher.Close() + + ctx, cancel := context.WithTimeout(setup.Context, 500*time.Millisecond) + defer cancel() + + // Start watcher - it should respect context cancellation + go watcher.Start(ctx) + + // Wait for context to be cancelled + <-ctx.Done() + + // Watcher should have stopped gracefully + // This test mainly ensures no goroutine leaks or panics occur + }) +} From 9ff7cc3aa4b1168ceb4b937419fe4e332f14a0ba Mon Sep 17 00:00:00 2001 From: avallete Date: Fri, 13 Jun 2025 13:26:46 +0200 Subject: [PATCH 2/7] chore: add serve tests --- internal/functions/serve/serve_test.go | 280 +++++++++++++++++++++++- internal/utils/container_output.go | 35 --- internal/utils/container_output_test.go | 37 ---- 3 files changed, 275 insertions(+), 77 deletions(-) diff --git a/internal/functions/serve/serve_test.go b/internal/functions/serve/serve_test.go index 7dbd75377..80794ae9e 100644 --- a/internal/functions/serve/serve_test.go +++ b/internal/functions/serve/serve_test.go @@ -1,15 +1,21 @@ package serve import ( + "bytes" "context" "net/http" "os" "path/filepath" "testing" + "time" + + "encoding/json" "github.com/docker/docker/api/types/container" + "github.com/docker/docker/pkg/stdcopy" "github.com/h2non/gock" "github.com/spf13/afero" + "github.com/spf13/viper" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/supabase/cli/internal/testing/apitest" @@ -17,6 +23,80 @@ import ( "github.com/supabase/cli/pkg/cast" ) +// Test helper functions +type TestSetup struct { + T *testing.T + Fsys afero.Fs + Context context.Context + Cancel context.CancelFunc + ProjectId string + RootPath string +} + +func NewTestSetup(t *testing.T) *TestSetup { + fsys := afero.NewMemMapFs() + ctx, cancel := context.WithCancel(context.Background()) + + setup := &TestSetup{ + T: t, + Fsys: fsys, + Context: ctx, + Cancel: cancel, + ProjectId: "test", + RootPath: "/project", + } + + // Initialize basic config + require.NoError(t, utils.InitConfig(utils.InitParams{ProjectId: setup.ProjectId}, fsys)) + + return setup +} + +func (s *TestSetup) Cleanup() { + s.Cancel() + gock.OffAll() +} + +// SetupFunction creates a test function with given name and content +func (s *TestSetup) SetupFunction(name, content string) { + funcDir := filepath.Join(utils.FunctionsDir, name) + require.NoError(s.T, s.Fsys.MkdirAll(funcDir, 0755)) + require.NoError(s.T, afero.WriteFile(s.Fsys, filepath.Join(funcDir, "index.ts"), []byte(content), 0644)) +} + +// SetupEnvFile creates an environment file with given content +func (s *TestSetup) SetupEnvFile(path, content string) { + if path == "" { + path = utils.FallbackEnvFilePath + } + require.NoError(s.T, afero.WriteFile(s.Fsys, path, []byte(content), 0644)) +} + +// SetupImportMap creates an import map file with given content +func (s *TestSetup) SetupImportMap(path, content string) { + if path == "" { + path = utils.FallbackImportMapPath + } + require.NoError(s.T, afero.WriteFile(s.Fsys, path, []byte(content), 0644)) +} + +// SetupConfigWithFunctions creates a supabase config.toml with function configurations +func (s *TestSetup) SetupConfigWithFunctions() { + configContent := `[functions.hello] +enabled = true +verify_jwt = false + +[functions.protected] +enabled = true +verify_jwt = true + +[functions.goodbye] +enabled = false +verify_jwt = false` + + require.NoError(s.T, afero.WriteFile(s.Fsys, "supabase/config.toml", []byte(configContent), 0644)) +} + func TestServeCommand(t *testing.T) { t.Run("serves all functions", func(t *testing.T) { // Setup in-memory fs @@ -36,11 +116,42 @@ func TestServeCommand(t *testing.T) { Delete("/v" + utils.Docker.ClientVersion() + "/containers/" + containerId). Reply(http.StatusOK) apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.EdgeRuntime.Image), containerId) - require.NoError(t, apitest.MockDockerLogs(utils.Docker, containerId, "success")) - // Run test - err := Run(context.Background(), "", nil, "", RuntimeOption{}, fsys) - // Check error - assert.NoError(t, err) + // Mock streaming logs for the log streamer - first call returns logs, subsequent calls return empty + var streamBody bytes.Buffer + streamWriter := stdcopy.NewStdWriter(&streamBody, stdcopy.Stdout) + _, streamErr := streamWriter.Write([]byte("streaming logs")) + require.NoError(t, streamErr) + // First request returns the logs + gock.New(utils.Docker.DaemonHost()). + Get("/v"+utils.Docker.ClientVersion()+"/containers/"+containerId+"/logs"). + Reply(http.StatusOK). + SetHeader("Content-Type", "application/vnd.docker.raw-stream"). + Body(&streamBody) + // Subsequent requests return empty response (simulating no new logs) + gock.New(utils.Docker.DaemonHost()). + Get("/v"+utils.Docker.ClientVersion()+"/containers/"+containerId+"/logs"). + Persist(). + Reply(http.StatusOK). + SetHeader("Content-Type", "application/vnd.docker.raw-stream"). + Body(bytes.NewReader([]byte{})) + // Mock container inspection for exit code check + gock.New(utils.Docker.DaemonHost()). + Get("/v" + utils.Docker.ClientVersion() + "/containers/" + containerId + "/json"). + Persist(). + Reply(http.StatusOK). + JSON(container.InspectResponse{ContainerJSONBase: &container.ContainerJSONBase{ + State: &container.State{ + ExitCode: 0, + }}}) + + // Create a context with timeout to prevent test from hanging + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + + // Run test with timeout context + err := Run(ctx, "", nil, "", RuntimeOption{}, fsys) + // Check error - expect context.DeadlineExceeded because the server runs until cancelled + assert.ErrorIs(t, err, context.DeadlineExceeded) assert.Empty(t, apitest.ListUnmatchedRequests()) }) @@ -48,6 +159,7 @@ func TestServeCommand(t *testing.T) { // Setup in-memory fs fsys := afero.NewMemMapFs() require.NoError(t, afero.WriteFile(fsys, utils.ConfigPath, []byte("malformed"), 0644)) + // Run test err := Run(context.Background(), "", nil, "", RuntimeOption{}, fsys) // Check error @@ -64,6 +176,7 @@ func TestServeCommand(t *testing.T) { gock.New(utils.Docker.DaemonHost()). Get("/v" + utils.Docker.ClientVersion() + "/containers/supabase_db_test/json"). Reply(http.StatusNotFound) + // Run test err := Run(context.Background(), "", nil, "", RuntimeOption{}, fsys) // Check error @@ -81,6 +194,7 @@ func TestServeCommand(t *testing.T) { Get("/v" + utils.Docker.ClientVersion() + "/containers/supabase_db_test/json"). Reply(http.StatusOK). JSON(container.InspectResponse{}) + // Run test err := Run(context.Background(), ".env", nil, "", RuntimeOption{}, fsys) // Check error @@ -102,9 +216,165 @@ func TestServeCommand(t *testing.T) { Get("/v" + utils.Docker.ClientVersion() + "/containers/supabase_db_test/json"). Reply(http.StatusOK). JSON(container.InspectResponse{}) + // Run test err := Run(context.Background(), ".env", cast.Ptr(true), "import_map.json", RuntimeOption{}, fsys) // Check error assert.ErrorIs(t, err, os.ErrNotExist) }) } + +func TestParseEnvFile(t *testing.T) { + // Save original CurrentDirAbs + originalCurrentDirAbs := utils.CurrentDirAbs + defer func() { + utils.CurrentDirAbs = originalCurrentDirAbs + }() + + t.Run("parses env file successfully", func(t *testing.T) { + setup := NewTestSetup(t) + defer setup.Cleanup() + + envContent := `DATABASE_URL=postgresql://localhost:5432/test +API_KEY=secret123 +DEBUG=true` + envPath := "/project/.env" + setup.SetupEnvFile(envPath, envContent) + + env, err := parseEnvFile(envPath, setup.Fsys) + assert.NoError(t, err) + assert.Contains(t, env, "DATABASE_URL=postgresql://localhost:5432/test") + assert.Contains(t, env, "API_KEY=secret123") + assert.Contains(t, env, "DEBUG=true") + }) + + t.Run("uses fallback env file when path is empty", func(t *testing.T) { + setup := NewTestSetup(t) + defer setup.Cleanup() + + envContent := `FALLBACK_VAR=fallback_value` + setup.SetupEnvFile("", envContent) + + env, err := parseEnvFile("", setup.Fsys) + assert.NoError(t, err) + assert.Contains(t, env, "FALLBACK_VAR=fallback_value") + }) +} + +func TestPopulatePerFunctionConfigs(t *testing.T) { + // Save original values + originalFunctionsDir := utils.FunctionsDir + defer func() { + utils.FunctionsDir = originalFunctionsDir + }() + + t.Run("populates function configs successfully", func(t *testing.T) { + setup := NewTestSetup(t) + defer setup.Cleanup() + + utils.FunctionsDir = "functions" + setup.SetupFunction("hello", "export default () => 'hello'") + setup.SetupConfigWithFunctions() + + binds, configString, err := populatePerFunctionConfigs("/project", "", cast.Ptr(false), setup.Fsys) + assert.NoError(t, err) + assert.NotEmpty(t, binds) + assert.NotEmpty(t, configString) + + var config map[string]interface{} + err = json.Unmarshal([]byte(configString), &config) + assert.NoError(t, err) + assert.Contains(t, config, "hello") + }) + + t.Run("handles function config creation", func(t *testing.T) { + setup := NewTestSetup(t) + defer setup.Cleanup() + + utils.FunctionsDir = "functions" + setup.SetupFunction("enabled", "export default () => 'enabled'") + + _, configString, err := populatePerFunctionConfigs("/project", "", nil, setup.Fsys) + assert.NoError(t, err) + + var resultConfig map[string]interface{} + err = json.Unmarshal([]byte(configString), &resultConfig) + assert.NoError(t, err) + assert.Contains(t, resultConfig, "enabled") + + enabledConfig := resultConfig["enabled"].(map[string]interface{}) + assert.Contains(t, enabledConfig, "entrypointPath") + assert.Contains(t, enabledConfig, "verifyJWT") + }) + + t.Run("handles import map path", func(t *testing.T) { + setup := NewTestSetup(t) + defer setup.Cleanup() + + utils.FunctionsDir = "functions" + setup.SetupFunction("hello", "export default () => 'hello'") + setup.SetupImportMap("import_map.json", "{}") + + binds, configString, err := populatePerFunctionConfigs("/project", "import_map.json", nil, setup.Fsys) + assert.NoError(t, err) + assert.NotEmpty(t, binds) + assert.NotEmpty(t, configString) + }) + + t.Run("returns empty config when no functions exist", func(t *testing.T) { + setup := NewTestSetup(t) + defer setup.Cleanup() + + utils.FunctionsDir = "functions" + require.NoError(t, setup.Fsys.MkdirAll("functions", 0755)) + + _, configString, err := populatePerFunctionConfigs("/project", "", nil, setup.Fsys) + assert.NoError(t, err) + + var resultConfig map[string]interface{} + err = json.Unmarshal([]byte(configString), &resultConfig) + assert.NoError(t, err) + assert.Empty(t, resultConfig) + }) +} + +func TestServeFunctions(t *testing.T) { + // Save original values + originalConfig := utils.Config + originalDebug := viper.Get("DEBUG") + originalFunctionsDir := utils.FunctionsDir + defer func() { + utils.Config = originalConfig + viper.Set("DEBUG", originalDebug) + utils.FunctionsDir = originalFunctionsDir + }() + + t.Run("returns error on env file parsing failure", func(t *testing.T) { + // Setup in-memory fs + fsys := afero.NewMemMapFs() + // Test with nonexistent file to trigger error + + // Test function + err := ServeFunctions(context.Background(), "nonexistent.env", nil, "", "postgresql://localhost:5432/test", RuntimeOption{}, fsys) + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to open env file") + }) + + t.Run("returns error on function config failure", func(t *testing.T) { + // Setup config + utils.Config.Auth.AnonKey.Value = "anon_key" + utils.Config.Auth.ServiceRoleKey.Value = "service_role_key" + utils.Config.Auth.JwtSecret.Value = "jwt_secret" + utils.Config.Api.Port = 8000 + utils.Config.EdgeRuntime.Policy = "permissive" + utils.KongAliases = []string{"supabase_kong_test"} + + // Setup in-memory fs with invalid functions directory + fsys := afero.NewMemMapFs() + utils.FunctionsDir = "nonexistent" + + // Test function + err := ServeFunctions(context.Background(), "", nil, "", "postgresql://localhost:5432/test", RuntimeOption{}, fsys) + assert.Error(t, err) + }) +} diff --git a/internal/utils/container_output.go b/internal/utils/container_output.go index 8f417afd1..ccbbab033 100644 --- a/internal/utils/container_output.go +++ b/internal/utils/container_output.go @@ -12,8 +12,6 @@ import ( "strings" "github.com/docker/docker/pkg/jsonmessage" - "github.com/docker/docker/pkg/stdcopy" - "github.com/go-errors/errors" ) func ProcessPullOutput(out io.ReadCloser, p Program) error { @@ -205,36 +203,3 @@ func ProcessDiffOutput(diffBytes []byte) ([]byte, error) { } return []byte(diffHeader + "\n\n" + strings.Join(filteredDiffDdls, "\n\n") + "\n"), nil } - -func ProcessPsqlOutput(out io.Reader, p Program) error { - r, w := io.Pipe() - doneCh := make(chan struct{}, 1) - - go func() { - scanner := bufio.NewScanner(r) - - for scanner.Scan() { - select { - case <-doneCh: - return - default: - } - - line := scanner.Text() - p.Send(PsqlMsg(&line)) - } - }() - - var errBuf bytes.Buffer - if _, err := stdcopy.StdCopy(w, &errBuf, out); err != nil { - return err - } - if errBuf.Len() > 0 { - return errors.New("Error running SQL: " + errBuf.String()) - } - - doneCh <- struct{}{} - p.Send(PsqlMsg(nil)) - - return nil -} diff --git a/internal/utils/container_output_test.go b/internal/utils/container_output_test.go index cf3e529bb..325084618 100644 --- a/internal/utils/container_output_test.go +++ b/internal/utils/container_output_test.go @@ -1,7 +1,6 @@ package utils import ( - "bytes" "encoding/json" "io" "sync" @@ -9,7 +8,6 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/docker/docker/pkg/jsonmessage" - "github.com/docker/docker/pkg/stdcopy" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -65,41 +63,6 @@ func TestProcessDiffOutput(t *testing.T) { }) } -func TestProcessPsqlOutput(t *testing.T) { - t.Run("processes psql output", func(t *testing.T) { - var buf bytes.Buffer - writer := stdcopy.NewStdWriter(&buf, stdcopy.Stdout) - _, err := writer.Write([]byte("test output\n")) - require.NoError(t, err) - - var lastLine *string - p := NewMockProgram(func(msg tea.Msg) { - if m, ok := msg.(PsqlMsg); ok { - lastLine = m - } - }) - - err = ProcessPsqlOutput(&buf, p) - - assert.NoError(t, err) - assert.Nil(t, lastLine) - }) - - t.Run("handles stderr output", func(t *testing.T) { - var buf bytes.Buffer - writer := stdcopy.NewStdWriter(&buf, stdcopy.Stderr) - _, err := writer.Write([]byte("error message\n")) - require.NoError(t, err) - - p := NewMockProgram(nil) - - err = ProcessPsqlOutput(&buf, p) - - assert.Error(t, err) - assert.Contains(t, err.Error(), "error message") - }) -} - func TestProcessPullOutput(t *testing.T) { t.Run("processes docker pull messages", func(t *testing.T) { messages := []jsonmessage.JSONMessage{ From bd6fcd9f4c2b3467f767bf396aef518bf5b2fd08 Mon Sep 17 00:00:00 2001 From: avallete Date: Fri, 13 Jun 2025 13:30:37 +0200 Subject: [PATCH 3/7] chore: fix lints --- internal/functions/serve/watcher.go | 10 ---------- internal/functions/serve/watcher_test.go | 14 +++++++------- 2 files changed, 7 insertions(+), 17 deletions(-) diff --git a/internal/functions/serve/watcher.go b/internal/functions/serve/watcher.go index a3c089685..87d9f215c 100644 --- a/internal/functions/serve/watcher.go +++ b/internal/functions/serve/watcher.go @@ -22,16 +22,6 @@ const ( ) var ( - // Directories to ignore. - ignoredDirNames = []string{ - ".git", - "node_modules", - ".vscode", - ".idea", - ".DS_Store", - "vendor", - } - // Patterns for ignoring file events. ignoredFilePatterns = []struct { Prefix string // File basename prefix diff --git a/internal/functions/serve/watcher_test.go b/internal/functions/serve/watcher_test.go index f5a953ccf..d17908113 100644 --- a/internal/functions/serve/watcher_test.go +++ b/internal/functions/serve/watcher_test.go @@ -61,7 +61,7 @@ func (s *WatcherIntegrationSetup) SetupSupabaseDirectory() string { func (s *WatcherIntegrationSetup) createFunction(name, content string) { funcDir := filepath.Join(s.TempDir, "supabase", "functions", name) require.NoError(s.T, os.MkdirAll(funcDir, 0755)) - require.NoError(s.T, os.WriteFile(filepath.Join(funcDir, "index.ts"), []byte(content), 0644)) + require.NoError(s.T, os.WriteFile(filepath.Join(funcDir, "index.ts"), []byte(content), 0600)) } // CreateFileWatcher creates and configures a debounce file watcher for testing @@ -106,7 +106,7 @@ func TestFileWatcherIntegration(t *testing.T) { // Modify a function file funcFile := filepath.Join(functionsDir, "hello", "index.ts") newContent := `export default () => new Response("Hello Modified World")` - require.NoError(t, os.WriteFile(funcFile, []byte(newContent), 0644)) + require.NoError(t, os.WriteFile(funcFile, []byte(newContent), 0600)) // Wait for restart signal select { @@ -145,7 +145,7 @@ func TestFileWatcherIntegration(t *testing.T) { } for _, tempFile := range tempFiles { - require.NoError(t, os.WriteFile(tempFile, []byte("temp content"), 0644)) + require.NoError(t, os.WriteFile(tempFile, []byte("temp content"), 0600)) time.Sleep(50 * time.Millisecond) } @@ -184,7 +184,7 @@ func TestFileWatcherIntegration(t *testing.T) { configContent := `[functions.hello] enabled = true verify_jwt = false` - require.NoError(t, os.WriteFile(configFile, []byte(configContent), 0644)) + require.NoError(t, os.WriteFile(configFile, []byte(configContent), 0600)) // Wait for restart signal select { @@ -241,7 +241,7 @@ verify_jwt = false` funcFile := filepath.Join(functionsDir, "hello", "index.ts") for i := 0; i < 5; i++ { content := fmt.Sprintf(`export default () => new Response("Hello %d")`, i) - require.NoError(t, os.WriteFile(funcFile, []byte(content), 0644)) + require.NoError(t, os.WriteFile(funcFile, []byte(content), 0600)) time.Sleep(50 * time.Millisecond) // Less than debounce duration } @@ -276,7 +276,7 @@ verify_jwt = false` // Create a utility file in lib directory utilFile := filepath.Join(libDir, "utils.ts") - require.NoError(t, os.WriteFile(utilFile, []byte(`export function util() { return "utility"; }`), 0644)) + require.NoError(t, os.WriteFile(utilFile, []byte(`export function util() { return "utility"; }`), 0600)) watcher, err := NewDebounceFileWatcher() require.NoError(t, err) @@ -296,7 +296,7 @@ verify_jwt = false` time.Sleep(100 * time.Millisecond) // Modify file in lib directory - require.NoError(t, os.WriteFile(utilFile, []byte(`export function util() { return "modified utility"; }`), 0644)) + require.NoError(t, os.WriteFile(utilFile, []byte(`export function util() { return "modified utility"; }`), 0600)) // Wait for restart signal select { From ebd88111ea85ed256f03804e9f7a2778ff061d8d Mon Sep 17 00:00:00 2001 From: Qiao Han Date: Fri, 13 Jun 2025 22:45:47 +0800 Subject: [PATCH 4/7] chore: remove sleep in tests --- internal/functions/serve/serve.go | 2 +- internal/functions/serve/watcher.go | 31 ++- internal/functions/serve/watcher_test.go | 240 ++++++++--------------- 3 files changed, 96 insertions(+), 177 deletions(-) diff --git a/internal/functions/serve/serve.go b/internal/functions/serve/serve.go index ab5b11806..4a7f7852f 100644 --- a/internal/functions/serve/serve.go +++ b/internal/functions/serve/serve.go @@ -75,7 +75,7 @@ func Run(ctx context.Context, envFilePath string, noVerifyJWT *bool, importMapPa if err != nil { return err } - go watcher.Start(ctx) + go watcher.Start() defer watcher.Close() // TODO: refactor this to edge runtime service runtimeOption.fileWatcher = watcher diff --git a/internal/functions/serve/watcher.go b/internal/functions/serve/watcher.go index db3bc3357..49b5f5d8b 100644 --- a/internal/functions/serve/watcher.go +++ b/internal/functions/serve/watcher.go @@ -1,7 +1,6 @@ package serve import ( - "context" "fmt" "io/fs" "os" @@ -115,22 +114,21 @@ func NewDebounceFileWatcher() (*debounceFileWatcher, error) { }, nil } -func (w *debounceFileWatcher) Start(ctx context.Context) { +func (w *debounceFileWatcher) Start() { for { event, ok := <-w.watcher.Events + if event.Has(restartEvents) && !isIgnoredFileEvent(event.Name, event.Op) { + fmt.Fprintf(os.Stderr, "File change detected: %s (%s)\n", event.Name, event.Op.String()) + // Fire immediately when timer is inactive, without blocking this thread + if active := w.restartTimer.Reset(0); active { + w.restartTimer.Reset(debounceDuration) + } + } + // Ensure the last event is fired before channel close if !ok { return } - - if !event.Has(restartEvents) || isIgnoredFileEvent(event.Name, event.Op) { - fmt.Fprintf(utils.GetDebugLogger(), "Ignoring file event: %s (%s)\n", event.Name, event.Op.String()) - continue - } - - fmt.Fprintf(os.Stderr, "File change detected: %s (%s)\n", event.Name, event.Op.String()) - if !w.restartTimer.Reset(debounceDuration) { - fmt.Fprintln(utils.GetDebugLogger(), "Failed to restart debounce timer.") - } + fmt.Fprintf(utils.GetDebugLogger(), "Ignoring file event: %s (%s)\n", event.Name, event.Op.String()) } } @@ -184,11 +182,6 @@ func (w *debounceFileWatcher) SetWatchPaths(watchPaths []string, fsys afero.Fs) } func (r *debounceFileWatcher) Close() error { - if r.watcher != nil { - return r.watcher.Close() - } - if r.restartTimer != nil { - r.restartTimer.Stop() - } - return nil + // Don't stop the timer to allow debounced events to fire + return r.watcher.Close() } diff --git a/internal/functions/serve/watcher_test.go b/internal/functions/serve/watcher_test.go index d17908113..0e6391de5 100644 --- a/internal/functions/serve/watcher_test.go +++ b/internal/functions/serve/watcher_test.go @@ -89,31 +89,27 @@ func TestFileWatcherIntegration(t *testing.T) { defer setup.Cleanup() functionsDir := setup.SetupFunctionsDirectory() - watcher, err := setup.CreateFileWatcher() require.NoError(t, err) - defer watcher.Close() - - // Start the watcher - ctx, cancel := context.WithTimeout(setup.Context, 5*time.Second) - defer cancel() - go watcher.Start(ctx) + // Modify a function file in background + go func() { + defer watcher.Close() + funcFile := filepath.Join(functionsDir, "hello", "index.ts") + newContent := `export default () => new Response("Hello Modified World")` + require.NoError(t, os.WriteFile(funcFile, []byte(newContent), 0600)) + }() - // Give watcher time to initialize - time.Sleep(100 * time.Millisecond) - - // Modify a function file - funcFile := filepath.Join(functionsDir, "hello", "index.ts") - newContent := `export default () => new Response("Hello Modified World")` - require.NoError(t, os.WriteFile(funcFile, []byte(newContent), 0600)) + // Run watcher on main thread to avoid sleeping + watcher.Start() // Wait for restart signal select { - case <-watcher.RestartCh: - // Expected - file change should trigger restart + case ts, ok := <-watcher.RestartCh: + assert.NotZero(t, ts, "file change should trigger restart") + assert.True(t, ok, "timer channel should be closed") case <-time.After(2 * time.Second): - t.Error("Expected restart signal after modifying TypeScript file") + assert.Fail(t, "missing restart signal after modifying TypeScript file") } }) @@ -122,42 +118,35 @@ func TestFileWatcherIntegration(t *testing.T) { defer setup.Cleanup() functionsDir := setup.SetupFunctionsDirectory() - watcher, err := setup.CreateFileWatcher() require.NoError(t, err) - defer watcher.Close() - - ctx, cancel := context.WithTimeout(setup.Context, 3*time.Second) - defer cancel() - - go watcher.Start(ctx) - - // Give watcher time to initialize - time.Sleep(100 * time.Millisecond) // Create various temporary/editor files that should be ignored - tempFiles := []string{ - filepath.Join(functionsDir, "hello", "test.txt~"), // Backup file - filepath.Join(functionsDir, "hello", ".test.swp"), // Vim swap - filepath.Join(functionsDir, "hello", ".#test.ts"), // Emacs lock - filepath.Join(functionsDir, "hello", "test.tmp"), // Temp file - filepath.Join(functionsDir, "hello", "___deno_temp___"), // Deno temp - } - - for _, tempFile := range tempFiles { - require.NoError(t, os.WriteFile(tempFile, []byte("temp content"), 0600)) - time.Sleep(50 * time.Millisecond) - } + go func() { + defer watcher.Close() + tempFiles := []string{ + filepath.Join(functionsDir, "hello", "test.txt~"), // Backup file + filepath.Join(functionsDir, "hello", ".test.swp"), // Vim swap + filepath.Join(functionsDir, "hello", ".#test.ts"), // Emacs lock + filepath.Join(functionsDir, "hello", "test.tmp"), // Temp file + filepath.Join(functionsDir, "hello", "___deno_temp___"), // Deno temp + } + for _, tempFile := range tempFiles { + require.NoError(t, os.WriteFile(tempFile, []byte("temp content"), 0600)) + } + }() - // Wait for debounce period - time.Sleep(600 * time.Millisecond) + // Run watcher on main thread to avoid sleeping + watcher.Start() - // Should not receive any restart signals from ignored files - select { - case <-watcher.RestartCh: - t.Error("Received unexpected restart signal from ignored files") - case <-time.After(100 * time.Millisecond): - // Expected - no restart for ignored files + // Wait multiple times for out of order events + for range 3 { + select { + case <-watcher.RestartCh: + assert.Fail(t, "should not receive any restart signals from ignored files") + case err := <-watcher.ErrCh: + assert.NoError(t, err) + } } }) @@ -166,56 +155,30 @@ func TestFileWatcherIntegration(t *testing.T) { defer setup.Cleanup() supabaseDir := setup.SetupSupabaseDirectory() - watcher, err := setup.CreateFileWatcher() require.NoError(t, err) - defer watcher.Close() - - ctx, cancel := context.WithTimeout(setup.Context, 3*time.Second) - defer cancel() - - go watcher.Start(ctx) - - // Give watcher time to initialize - time.Sleep(100 * time.Millisecond) // Create and modify a config.toml file - configFile := filepath.Join(supabaseDir, "config.toml") - configContent := `[functions.hello] -enabled = true -verify_jwt = false` - require.NoError(t, os.WriteFile(configFile, []byte(configContent), 0600)) + go func() { + defer watcher.Close() + configFile := filepath.Join(supabaseDir, "config.toml") + require.NoError(t, os.WriteFile(configFile, []byte(` + [functions.hello] + enabled = true + verify_jwt = false + `), 0600)) + }() + + // Run watcher on main thread to avoid sleeping + watcher.Start() // Wait for restart signal select { - case <-watcher.RestartCh: - // Expected - config change should trigger restart + case ts, ok := <-watcher.RestartCh: + assert.NotZero(t, ts, "config change should trigger restart") + assert.True(t, ok, "timer channel should be closed") case <-time.After(2 * time.Second): - t.Error("Expected restart signal after modifying config file") - } - }) - - t.Run("handles file watcher errors gracefully", func(t *testing.T) { - setup := NewWatcherIntegrationSetup(t) - defer setup.Cleanup() - - watcher, err := setup.CreateFileWatcher() - require.NoError(t, err) - defer watcher.Close() - - ctx, cancel := context.WithTimeout(setup.Context, 1*time.Second) - defer cancel() - - // Start watcher - go watcher.Start(ctx) - - // Monitor for errors - select { - case err := <-watcher.ErrCh: - // If we get an error, it should be handled gracefully - t.Logf("Watcher error (handled gracefully): %v", err) - case <-ctx.Done(): - // Expected - timeout without critical errors + assert.Fail(t, "missing restart signal after modifying config file") } }) @@ -224,45 +187,34 @@ verify_jwt = false` defer setup.Cleanup() functionsDir := setup.SetupFunctionsDirectory() - watcher, err := setup.CreateFileWatcher() require.NoError(t, err) defer watcher.Close() - ctx, cancel := context.WithTimeout(setup.Context, 5*time.Second) - defer cancel() - - go watcher.Start(ctx) - - // Give watcher time to initialize - time.Sleep(100 * time.Millisecond) + go watcher.Start() // Make rapid changes to a file funcFile := filepath.Join(functionsDir, "hello", "index.ts") - for i := 0; i < 5; i++ { + for i := range 5 { content := fmt.Sprintf(`export default () => new Response("Hello %d")`, i) require.NoError(t, os.WriteFile(funcFile, []byte(content), 0600)) - time.Sleep(50 * time.Millisecond) // Less than debounce duration } - // Should only get one restart signal due to debouncing - restartCount := 0 - timeout := time.After(1 * time.Second) - - for { - select { - case <-watcher.RestartCh: - restartCount++ - // Continue to see if more signals come through - case <-timeout: - // Done counting - goto done - } + // Wait for debounce duration + select { + case ts, ok := <-watcher.RestartCh: + assert.NotZero(t, ts) + assert.True(t, ok) + case <-time.After(debounceDuration): + assert.Fail(t, "missing restart signal after rapid file changes") + } + select { + case <-watcher.RestartCh: + assert.Fail(t, "should only get one restart signal due to debouncing") + case ts, ok := <-time.After(debounceDuration): + assert.NotZero(t, ts) + assert.True(t, ok) } - - done: - // Should have only one restart signal due to debouncing - assert.Equal(t, 1, restartCount, "Expected exactly one restart signal due to debouncing") }) t.Run("watches multiple directories", func(t *testing.T) { @@ -280,53 +232,27 @@ verify_jwt = false` watcher, err := NewDebounceFileWatcher() require.NoError(t, err) - defer watcher.Close() - // Set up watch paths to include both directories - fsys := afero.NewOsFs() - watchPaths := []string{functionsDir, libDir} - require.NoError(t, watcher.SetWatchPaths(watchPaths, fsys)) + go func() { + defer watcher.Close() + // Set up watch paths to include both directories + fsys := afero.NewOsFs() + watchPaths := []string{functionsDir, libDir} + require.NoError(t, watcher.SetWatchPaths(watchPaths, fsys)) + // Modify file in lib directory + require.NoError(t, os.WriteFile(utilFile, []byte(`export function util() { return "modified utility"; }`), 0600)) + }() - ctx, cancel := context.WithTimeout(setup.Context, 3*time.Second) - defer cancel() - - go watcher.Start(ctx) - - // Give watcher time to initialize - time.Sleep(100 * time.Millisecond) - - // Modify file in lib directory - require.NoError(t, os.WriteFile(utilFile, []byte(`export function util() { return "modified utility"; }`), 0600)) + // Run watcher on main thread to avoid sleeping + watcher.Start() // Wait for restart signal select { - case <-watcher.RestartCh: - // Expected - change in watched lib directory should trigger restart + case ts, ok := <-watcher.RestartCh: + assert.NotZero(t, ts, "change in watched lib directory should trigger restart") + assert.True(t, ok, "timer channel should be closed") case <-time.After(2 * time.Second): - t.Error("Expected restart signal after modifying file in watched lib directory") + assert.Fail(t, "missing restart signal after modifying file in watched lib directory") } }) - - t.Run("stops watching when context is cancelled", func(t *testing.T) { - setup := NewWatcherIntegrationSetup(t) - defer setup.Cleanup() - - setup.SetupFunctionsDirectory() - - watcher, err := setup.CreateFileWatcher() - require.NoError(t, err) - defer watcher.Close() - - ctx, cancel := context.WithTimeout(setup.Context, 500*time.Millisecond) - defer cancel() - - // Start watcher - it should respect context cancellation - go watcher.Start(ctx) - - // Wait for context to be cancelled - <-ctx.Done() - - // Watcher should have stopped gracefully - // This test mainly ensures no goroutine leaks or panics occur - }) } From 0156dfd109f8db1a2d167bf3a80783b9027b335b Mon Sep 17 00:00:00 2001 From: Qiao Han Date: Fri, 13 Jun 2025 23:01:47 +0800 Subject: [PATCH 5/7] chore: simulate fs event when possible --- internal/functions/serve/watcher_test.go | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/internal/functions/serve/watcher_test.go b/internal/functions/serve/watcher_test.go index 0e6391de5..1cbee13ca 100644 --- a/internal/functions/serve/watcher_test.go +++ b/internal/functions/serve/watcher_test.go @@ -8,6 +8,7 @@ import ( "testing" "time" + "github.com/fsnotify/fsnotify" "github.com/spf13/afero" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -114,25 +115,25 @@ func TestFileWatcherIntegration(t *testing.T) { }) t.Run("ignores editor temporary files", func(t *testing.T) { - setup := NewWatcherIntegrationSetup(t) - defer setup.Cleanup() - - functionsDir := setup.SetupFunctionsDirectory() - watcher, err := setup.CreateFileWatcher() + watcher, err := NewDebounceFileWatcher() require.NoError(t, err) // Create various temporary/editor files that should be ignored go func() { defer watcher.Close() tempFiles := []string{ - filepath.Join(functionsDir, "hello", "test.txt~"), // Backup file - filepath.Join(functionsDir, "hello", ".test.swp"), // Vim swap - filepath.Join(functionsDir, "hello", ".#test.ts"), // Emacs lock - filepath.Join(functionsDir, "hello", "test.tmp"), // Temp file - filepath.Join(functionsDir, "hello", "___deno_temp___"), // Deno temp + filepath.Join("/tmp", "test.txt~"), // Backup file + filepath.Join("/tmp", ".test.swp"), // Vim swap + filepath.Join("/tmp", ".#test.ts"), // Emacs lock + filepath.Join("/tmp", "test.tmp"), // Temp file + filepath.Join("/tmp", "___deno_temp___"), // Deno temp } for _, tempFile := range tempFiles { - require.NoError(t, os.WriteFile(tempFile, []byte("temp content"), 0600)) + // Fire events directly since we only care about ignore files + watcher.watcher.Events <- fsnotify.Event{ + Name: tempFile, + Op: fsnotify.Create, + } } }() From 2114204ef85d9ccc2e27c7e0f76487143ceb0567 Mon Sep 17 00:00:00 2001 From: Qiao Han Date: Fri, 13 Jun 2025 23:05:58 +0800 Subject: [PATCH 6/7] chore: avoid flaky tests --- internal/functions/serve/watcher_test.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/internal/functions/serve/watcher_test.go b/internal/functions/serve/watcher_test.go index 1cbee13ca..eb91790d2 100644 --- a/internal/functions/serve/watcher_test.go +++ b/internal/functions/serve/watcher_test.go @@ -99,6 +99,8 @@ func TestFileWatcherIntegration(t *testing.T) { funcFile := filepath.Join(functionsDir, "hello", "index.ts") newContent := `export default () => new Response("Hello Modified World")` require.NoError(t, os.WriteFile(funcFile, []byte(newContent), 0600)) + // https://github.com/fsnotify/fsnotify/blob/main/fsnotify_test.go#L181 + time.Sleep(50 * time.Millisecond) }() // Run watcher on main thread to avoid sleeping @@ -168,6 +170,8 @@ func TestFileWatcherIntegration(t *testing.T) { enabled = true verify_jwt = false `), 0600)) + // https://github.com/fsnotify/fsnotify/blob/main/fsnotify_test.go#L181 + time.Sleep(50 * time.Millisecond) }() // Run watcher on main thread to avoid sleeping @@ -199,6 +203,8 @@ func TestFileWatcherIntegration(t *testing.T) { for i := range 5 { content := fmt.Sprintf(`export default () => new Response("Hello %d")`, i) require.NoError(t, os.WriteFile(funcFile, []byte(content), 0600)) + // https://github.com/fsnotify/fsnotify/blob/main/fsnotify_test.go#L181 + time.Sleep(50 * time.Millisecond) } // Wait for debounce duration @@ -242,6 +248,8 @@ func TestFileWatcherIntegration(t *testing.T) { require.NoError(t, watcher.SetWatchPaths(watchPaths, fsys)) // Modify file in lib directory require.NoError(t, os.WriteFile(utilFile, []byte(`export function util() { return "modified utility"; }`), 0600)) + // https://github.com/fsnotify/fsnotify/blob/main/fsnotify_test.go#L181 + time.Sleep(50 * time.Millisecond) }() // Run watcher on main thread to avoid sleeping From 1ff8144cebff14abc7a788896b359e12b71dcf6a Mon Sep 17 00:00:00 2001 From: Qiao Han Date: Fri, 13 Jun 2025 23:13:05 +0800 Subject: [PATCH 7/7] chore: undo debounce test --- internal/functions/serve/watcher_test.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/internal/functions/serve/watcher_test.go b/internal/functions/serve/watcher_test.go index eb91790d2..2f958edbf 100644 --- a/internal/functions/serve/watcher_test.go +++ b/internal/functions/serve/watcher_test.go @@ -203,8 +203,6 @@ func TestFileWatcherIntegration(t *testing.T) { for i := range 5 { content := fmt.Sprintf(`export default () => new Response("Hello %d")`, i) require.NoError(t, os.WriteFile(funcFile, []byte(content), 0600)) - // https://github.com/fsnotify/fsnotify/blob/main/fsnotify_test.go#L181 - time.Sleep(50 * time.Millisecond) } // Wait for debounce duration @@ -218,7 +216,7 @@ func TestFileWatcherIntegration(t *testing.T) { select { case <-watcher.RestartCh: assert.Fail(t, "should only get one restart signal due to debouncing") - case ts, ok := <-time.After(debounceDuration): + case ts, ok := <-time.After(debounceDuration + 50*time.Millisecond): assert.NotZero(t, ts) assert.True(t, ok) }