|
| 1 | +package main |
| 2 | + |
| 3 | +import ( |
| 4 | + "encoding/json" |
| 5 | + "net" |
| 6 | + "os" |
| 7 | + "path/filepath" |
| 8 | + "sync" |
| 9 | + "testing" |
| 10 | + "time" |
| 11 | +) |
| 12 | + |
| 13 | +// ---- detectEnv / resolveProject ---- |
| 14 | + |
| 15 | +func TestDetectEnv_FindsProjectToml(t *testing.T) { |
| 16 | + root := t.TempDir() |
| 17 | + sub := filepath.Join(root, "a", "b") |
| 18 | + if err := os.MkdirAll(sub, 0755); err != nil { |
| 19 | + t.Fatal(err) |
| 20 | + } |
| 21 | + if err := os.WriteFile(filepath.Join(root, "Project.toml"), []byte{}, 0644); err != nil { |
| 22 | + t.Fatal(err) |
| 23 | + } |
| 24 | + |
| 25 | + got := detectEnv(sub) |
| 26 | + if got != root { |
| 27 | + t.Errorf("detectEnv(%q) = %q, want %q", sub, got, root) |
| 28 | + } |
| 29 | +} |
| 30 | + |
| 31 | +func TestDetectEnv_NoneFound(t *testing.T) { |
| 32 | + dir := t.TempDir() |
| 33 | + // Make sure there's no Project.toml anywhere up the tree |
| 34 | + // (TempDir is under /tmp which never has one) |
| 35 | + got := detectEnv(dir) |
| 36 | + if got != "" { |
| 37 | + t.Errorf("detectEnv(%q) = %q, want empty", dir, got) |
| 38 | + } |
| 39 | +} |
| 40 | + |
| 41 | +func TestResolveProject_Empty(t *testing.T) { |
| 42 | + // When empty, result is either a detected env or "". |
| 43 | + // Just ensure it doesn't panic and returns a valid absolute path or "". |
| 44 | + got := resolveProject("") |
| 45 | + if got != "" { |
| 46 | + if !filepath.IsAbs(got) { |
| 47 | + t.Errorf("resolveProject(\"\") = %q, want absolute path or empty", got) |
| 48 | + } |
| 49 | + } |
| 50 | +} |
| 51 | + |
| 52 | +func TestResolveProject_Relative(t *testing.T) { |
| 53 | + root := t.TempDir() |
| 54 | + sub := filepath.Join(root, "proj") |
| 55 | + os.Mkdir(sub, 0755) |
| 56 | + |
| 57 | + orig, _ := os.Getwd() |
| 58 | + os.Chdir(root) |
| 59 | + defer os.Chdir(orig) |
| 60 | + |
| 61 | + got := resolveProject("proj") |
| 62 | + // Resolve symlinks on both sides (macOS /var → /private/var) |
| 63 | + gotR, _ := filepath.EvalSymlinks(got) |
| 64 | + subR, _ := filepath.EvalSymlinks(sub) |
| 65 | + if gotR != subR { |
| 66 | + t.Errorf("resolveProject(\"proj\") = %q, want %q", got, sub) |
| 67 | + } |
| 68 | +} |
| 69 | + |
| 70 | +// ---- pkgPattern ---- |
| 71 | + |
| 72 | +func TestPkgPattern(t *testing.T) { |
| 73 | + hits := []string{ |
| 74 | + "Pkg.add(\"Example\")", |
| 75 | + "using Pkg; Pkg.update()", |
| 76 | + "Pkg.resolve()", |
| 77 | + } |
| 78 | + misses := []string{ |
| 79 | + "println(\"hello\")", |
| 80 | + "x = 1 + 2", |
| 81 | + "# no package ops here", |
| 82 | + } |
| 83 | + for _, s := range hits { |
| 84 | + if !pkgPattern.MatchString(s) { |
| 85 | + t.Errorf("pkgPattern should match %q", s) |
| 86 | + } |
| 87 | + } |
| 88 | + for _, s := range misses { |
| 89 | + if pkgPattern.MatchString(s) { |
| 90 | + t.Errorf("pkgPattern should not match %q", s) |
| 91 | + } |
| 92 | + } |
| 93 | +} |
| 94 | + |
| 95 | +// ---- handleRequest (no Julia needed) ---- |
| 96 | + |
| 97 | +func newTestState() *daemonState { |
| 98 | + return &daemonState{ |
| 99 | + manager: newSessionManager(), |
| 100 | + lastRequest: time.Now(), |
| 101 | + stopCh: make(chan struct{}), |
| 102 | + } |
| 103 | +} |
| 104 | + |
| 105 | +func TestHandleRequest_Ping(t *testing.T) { |
| 106 | + state := newTestState() |
| 107 | + resp := handleRequest(state, map[string]any{"action": "ping"}) |
| 108 | + if resp["output"] != "pong" { |
| 109 | + t.Errorf("ping response = %v, want pong", resp["output"]) |
| 110 | + } |
| 111 | +} |
| 112 | + |
| 113 | +func TestHandleRequest_SessionsEmpty(t *testing.T) { |
| 114 | + state := newTestState() |
| 115 | + resp := handleRequest(state, map[string]any{"action": "sessions"}) |
| 116 | + out, _ := resp["output"].(string) |
| 117 | + if out != "No active Julia sessions." { |
| 118 | + t.Errorf("sessions response = %q", out) |
| 119 | + } |
| 120 | +} |
| 121 | + |
| 122 | +func TestHandleRequest_UnknownAction(t *testing.T) { |
| 123 | + state := newTestState() |
| 124 | + resp := handleRequest(state, map[string]any{"action": "bogus"}) |
| 125 | + if resp["error"] == nil { |
| 126 | + t.Error("expected error for unknown action") |
| 127 | + } |
| 128 | +} |
| 129 | + |
| 130 | +func TestHandleRequest_Stop(t *testing.T) { |
| 131 | + state := newTestState() |
| 132 | + resp := handleRequest(state, map[string]any{"action": "stop"}) |
| 133 | + if resp["output"] != "Daemon stopping." { |
| 134 | + t.Errorf("stop response = %v", resp["output"]) |
| 135 | + } |
| 136 | + select { |
| 137 | + case <-state.stopCh: |
| 138 | + // closed as expected |
| 139 | + default: |
| 140 | + t.Error("stopCh not closed after stop action") |
| 141 | + } |
| 142 | +} |
| 143 | + |
| 144 | +// ---- daemon socket integration (no Julia) ---- |
| 145 | + |
| 146 | +func TestDaemonPingOverSocket(t *testing.T) { |
| 147 | + socketPath := filepath.Join(t.TempDir(), "test.sock") |
| 148 | + |
| 149 | + var wg sync.WaitGroup |
| 150 | + wg.Add(1) |
| 151 | + go func() { |
| 152 | + defer wg.Done() |
| 153 | + serveDaemon(socketPath, time.Hour) |
| 154 | + }() |
| 155 | + |
| 156 | + // Wait for socket to appear |
| 157 | + deadline := time.Now().Add(5 * time.Second) |
| 158 | + for time.Now().Before(deadline) { |
| 159 | + if _, err := os.Stat(socketPath); err == nil { |
| 160 | + break |
| 161 | + } |
| 162 | + time.Sleep(20 * time.Millisecond) |
| 163 | + } |
| 164 | + |
| 165 | + conn, err := net.Dial("unix", socketPath) |
| 166 | + if err != nil { |
| 167 | + t.Fatalf("dial: %v", err) |
| 168 | + } |
| 169 | + defer conn.Close() |
| 170 | + |
| 171 | + if err := json.NewEncoder(conn).Encode(map[string]any{"action": "ping"}); err != nil { |
| 172 | + t.Fatal(err) |
| 173 | + } |
| 174 | + var resp map[string]any |
| 175 | + if err := json.NewDecoder(conn).Decode(&resp); err != nil { |
| 176 | + t.Fatal(err) |
| 177 | + } |
| 178 | + if resp["output"] != "pong" { |
| 179 | + t.Errorf("ping over socket = %v, want pong", resp["output"]) |
| 180 | + } |
| 181 | + |
| 182 | + // Stop the daemon so the goroutine exits |
| 183 | + conn2, _ := net.Dial("unix", socketPath) |
| 184 | + json.NewEncoder(conn2).Encode(map[string]any{"action": "stop"}) |
| 185 | + conn2.Close() |
| 186 | + wg.Wait() |
| 187 | +} |
| 188 | + |
| 189 | +// ---- Julia integration ---- |
| 190 | + |
| 191 | +func TestEvalBasic(t *testing.T) { |
| 192 | + socketPath := filepath.Join(t.TempDir(), "test.sock") |
| 193 | + |
| 194 | + var wg sync.WaitGroup |
| 195 | + wg.Add(1) |
| 196 | + go func() { |
| 197 | + defer wg.Done() |
| 198 | + serveDaemon(socketPath, time.Hour) |
| 199 | + }() |
| 200 | + |
| 201 | + // Wait for socket |
| 202 | + deadline := time.Now().Add(5 * time.Second) |
| 203 | + for time.Now().Before(deadline) { |
| 204 | + if _, err := os.Stat(socketPath); err == nil { |
| 205 | + break |
| 206 | + } |
| 207 | + time.Sleep(20 * time.Millisecond) |
| 208 | + } |
| 209 | + |
| 210 | + send := func(payload map[string]any) map[string]any { |
| 211 | + conn, err := net.Dial("unix", socketPath) |
| 212 | + if err != nil { |
| 213 | + t.Fatalf("dial: %v", err) |
| 214 | + } |
| 215 | + defer conn.Close() |
| 216 | + json.NewEncoder(conn).Encode(payload) |
| 217 | + var resp map[string]any |
| 218 | + json.NewDecoder(conn).Decode(&resp) |
| 219 | + return resp |
| 220 | + } |
| 221 | + |
| 222 | + // Eval basic expression |
| 223 | + resp := send(map[string]any{"action": "eval", "code": `println("hello world")`}) |
| 224 | + if resp["error"] != nil { |
| 225 | + t.Fatalf("eval error: %v", resp["error"]) |
| 226 | + } |
| 227 | + out, _ := resp["output"].(string) |
| 228 | + if out != "hello world" { |
| 229 | + t.Errorf("eval output = %q, want %q", out, "hello world") |
| 230 | + } |
| 231 | + |
| 232 | + // State persists across calls |
| 233 | + send(map[string]any{"action": "eval", "code": "x = 42"}) |
| 234 | + resp2 := send(map[string]any{"action": "eval", "code": "println(x)"}) |
| 235 | + out2, _ := resp2["output"].(string) |
| 236 | + if out2 != "42" { |
| 237 | + t.Errorf("state not persisted: x = %q, want 42", out2) |
| 238 | + } |
| 239 | + |
| 240 | + // Restart clears state |
| 241 | + send(map[string]any{"action": "restart"}) |
| 242 | + resp3 := send(map[string]any{"action": "eval", "code": "println(isdefined(Main, :x))"}) |
| 243 | + out3, _ := resp3["output"].(string) |
| 244 | + if out3 != "false" { |
| 245 | + t.Errorf("after restart x should be undefined, got %q", out3) |
| 246 | + } |
| 247 | + |
| 248 | + // Stop daemon |
| 249 | + conn, _ := net.Dial("unix", socketPath) |
| 250 | + json.NewEncoder(conn).Encode(map[string]any{"action": "stop"}) |
| 251 | + conn.Close() |
| 252 | + wg.Wait() |
| 253 | +} |
0 commit comments