| 1 | package cli |
| 2 | |
| 3 | import ( |
| 4 | "bufio" |
| 5 | "encoding/json" |
| 6 | "os" |
| 7 | "path/filepath" |
| 8 | "strings" |
| 9 | "testing" |
| 10 | "time" |
| 11 | |
| 12 | "github.com/ipfs/kubo/test/cli/harness" |
| 13 | "github.com/stretchr/testify/assert" |
| 14 | "github.com/stretchr/testify/require" |
| 15 | ) |
| 16 | |
| 17 | const ( |
| 18 | provideStatEventuallyTimeout = 15 * time.Second |
| 19 | provideStatEventuallyTick = 100 * time.Millisecond |
| 20 | ) |
| 21 | |
| 22 | // sweepStats mirrors the subset of JSON fields actually used by tests. |
| 23 | // This type is intentionally independent from upstream types to detect breaking changes. |
| 24 | // Only includes fields that tests actually access to keep it simple and maintainable. |
| 25 | type sweepStats struct { |
| 26 | Sweep struct { |
| 27 | Closed bool `json:"closed"` |
| 28 | Connectivity struct { |
| 29 | Status string `json:"status"` |
| 30 | } `json:"connectivity"` |
| 31 | Queues struct { |
| 32 | PendingKeyProvides int `json:"pending_key_provides"` |
| 33 | } `json:"queues"` |
| 34 | Schedule struct { |
| 35 | Keys int `json:"keys"` |
| 36 | } `json:"schedule"` |
| 37 | } `json:"Sweep"` |
| 38 | } |
| 39 | |
| 40 | // parseSweepStats parses JSON output from ipfs provide stat command. |
| 41 | // Tests will naturally fail if upstream removes/renames fields we depend on. |
| 42 | func parseSweepStats(t *testing.T, jsonOutput string) sweepStats { |
| 43 | t.Helper() |
| 44 | var stats sweepStats |
| 45 | err := json.Unmarshal([]byte(jsonOutput), &stats) |
| 46 | require.NoError(t, err, "failed to parse provide stat JSON output") |
| 47 | return stats |
| 48 | } |
| 49 | |
| 50 | // TestProvideStatAllMetricsDocumented verifies that all metrics output by |
| 51 | // `ipfs provide stat --all` are documented in docs/provide-stats.md. |
| 52 | // |
| 53 | // The test works as follows: |
| 54 | // 1. Starts an IPFS node with Provide.DHT.SweepEnabled=true |
| 55 | // 2. Runs `ipfs provide stat --all` to get all metrics |
| 56 | // 3. Parses the output and extracts all lines with exactly 2 spaces indent |
| 57 | // (these are the actual metric lines) |
| 58 | // 4. Reads docs/provide-stats.md and extracts all ### section headers |
| 59 | // 5. Ensures every metric in the output has a corresponding ### section in the docs |
| 60 | func TestProvideStatAllMetricsDocumented(t *testing.T) { |
| 61 | t.Parallel() |
| 62 | |
| 63 | h := harness.NewT(t) |
| 64 | node := h.NewNode().Init() |
| 65 | |
| 66 | // Enable sweep provider |
| 67 | node.SetIPFSConfig("Provide.DHT.SweepEnabled", true) |
| 68 | node.SetIPFSConfig("Provide.Enabled", true) |
| 69 | |
| 70 | node.StartDaemon() |
| 71 | defer node.StopDaemon() |
| 72 | |
| 73 | // Run `ipfs provide stat --all` to get all metrics |
| 74 | res := node.IPFS("provide", "stat", "--all") |
| 75 | require.NoError(t, res.Err) |
| 76 | |
| 77 | // Parse metrics from the command output |
| 78 | // Only consider lines with exactly two spaces of padding (" ") |
| 79 | // These are the actual metric lines as shown in provide.go |
| 80 | outputMetrics := make(map[string]bool) |
| 81 | scanner := bufio.NewScanner(strings.NewReader(res.Stdout.String())) |
| 82 | // Only consider lines that start with exactly two spaces |
| 83 | indent := " " |
| 84 | for scanner.Scan() { |
| 85 | line := scanner.Text() |
| 86 | if !strings.HasPrefix(line, indent) || strings.HasPrefix(line, indent) { |
| 87 | continue |
| 88 | } |
| 89 | |
| 90 | // Remove the indent |
| 91 | line = strings.TrimPrefix(line, indent) |
| 92 | |
| 93 | // Extract metric name - everything before the first ':' |
| 94 | parts := strings.SplitN(line, ":", 2) |
| 95 | if len(parts) >= 1 { |
| 96 | metricName := strings.TrimSpace(parts[0]) |
| 97 | if metricName != "" { |
| 98 | outputMetrics[metricName] = true |
| 99 | } |
| 100 | } |
| 101 | } |
| 102 | require.NoError(t, scanner.Err()) |
| 103 | |
| 104 | // Read docs/provide-stats.md |
| 105 | // Find the repo root by looking for go.mod |
| 106 | repoRoot := ".." |
| 107 | for range 6 { |
| 108 | if _, err := os.Stat(filepath.Join(repoRoot, "go.mod")); err == nil { |
| 109 | break |
| 110 | } |
| 111 | repoRoot = filepath.Join("..", repoRoot) |
| 112 | } |
| 113 | docsPath := filepath.Join(repoRoot, "docs", "provide-stats.md") |
| 114 | docsFile, err := os.Open(docsPath) |
| 115 | require.NoError(t, err, "Failed to open provide-stats.md") |
| 116 | defer docsFile.Close() |
| 117 | |
| 118 | // Parse all ### metric headers from the docs |
| 119 | documentedMetrics := make(map[string]bool) |
| 120 | docsScanner := bufio.NewScanner(docsFile) |
| 121 | for docsScanner.Scan() { |
| 122 | line := docsScanner.Text() |
| 123 | if metricName, found := strings.CutPrefix(line, "### "); found { |
| 124 | metricName = strings.TrimSpace(metricName) |
| 125 | documentedMetrics[metricName] = true |
| 126 | } |
| 127 | } |
| 128 | require.NoError(t, docsScanner.Err()) |
| 129 | |
| 130 | // Check that all output metrics are documented |
| 131 | var undocumentedMetrics []string |
| 132 | for metric := range outputMetrics { |
| 133 | if !documentedMetrics[metric] { |
| 134 | undocumentedMetrics = append(undocumentedMetrics, metric) |
| 135 | } |
| 136 | } |
| 137 | |
| 138 | require.Empty(t, undocumentedMetrics, |
| 139 | "The following metrics from 'ipfs provide stat --all' are not documented in docs/provide-stats.md: %v\n"+ |
| 140 | "All output metrics: %v\n"+ |
| 141 | "Documented metrics: %v", |
| 142 | undocumentedMetrics, outputMetrics, documentedMetrics) |
| 143 | } |
| 144 | |
| 145 | // TestProvideStatBasic tests basic functionality of ipfs provide stat |
| 146 | func TestProvideStatBasic(t *testing.T) { |
| 147 | t.Parallel() |
| 148 | |
| 149 | t.Run("works with Sweep provider and shows brief output", func(t *testing.T) { |
| 150 | t.Parallel() |
| 151 | |
| 152 | h := harness.NewT(t) |
| 153 | node := h.NewNode().Init() |
| 154 | node.SetIPFSConfig("Provide.DHT.SweepEnabled", true) |
| 155 | node.SetIPFSConfig("Provide.Enabled", true) |
| 156 | node.StartDaemon() |
| 157 | defer node.StopDaemon() |
| 158 | |
| 159 | res := node.IPFS("provide", "stat") |
| 160 | require.NoError(t, res.Err) |
| 161 | assert.Empty(t, res.Stderr.String()) |
| 162 | |
| 163 | output := res.Stdout.String() |
| 164 | // Brief output should contain specific full labels |
| 165 | assert.Contains(t, output, "Provide queue:") |
| 166 | assert.Contains(t, output, "Reprovide queue:") |
| 167 | assert.Contains(t, output, "CIDs scheduled:") |
| 168 | assert.Contains(t, output, "Regions scheduled:") |
| 169 | assert.Contains(t, output, "Avg record holders:") |
| 170 | assert.Contains(t, output, "Ongoing provides:") |
| 171 | assert.Contains(t, output, "Ongoing reprovides:") |
| 172 | assert.Contains(t, output, "Total CIDs provided:") |
| 173 | }) |
| 174 | |
| 175 | t.Run("requires daemon to be online", func(t *testing.T) { |
| 176 | t.Parallel() |
| 177 | |
| 178 | h := harness.NewT(t) |
| 179 | node := h.NewNode().Init() |
| 180 | |
| 181 | res := node.RunIPFS("provide", "stat") |
| 182 | assert.Error(t, res.Err) |
| 183 | assert.Contains(t, res.Stderr.String(), "this command must be run in online mode") |
| 184 | }) |
| 185 | } |
| 186 | |
| 187 | // TestProvideStatFlags tests various command flags |
| 188 | func TestProvideStatFlags(t *testing.T) { |
| 189 | t.Parallel() |
| 190 | |
| 191 | t.Run("--all flag shows all sections with headings", func(t *testing.T) { |
| 192 | t.Parallel() |
| 193 | |
| 194 | h := harness.NewT(t) |
| 195 | node := h.NewNode().Init() |
| 196 | node.SetIPFSConfig("Provide.DHT.SweepEnabled", true) |
| 197 | node.SetIPFSConfig("Provide.Enabled", true) |
| 198 | node.StartDaemon() |
| 199 | defer node.StopDaemon() |
| 200 | |
| 201 | res := node.IPFS("provide", "stat", "--all") |
| 202 | require.NoError(t, res.Err) |
| 203 | |
| 204 | output := res.Stdout.String() |
| 205 | // Should contain section headings with colons |
| 206 | assert.Contains(t, output, "Connectivity:") |
| 207 | assert.Contains(t, output, "Queues:") |
| 208 | assert.Contains(t, output, "Schedule:") |
| 209 | assert.Contains(t, output, "Timings:") |
| 210 | assert.Contains(t, output, "Network:") |
| 211 | assert.Contains(t, output, "Operations:") |
| 212 | assert.Contains(t, output, "Workers:") |
| 213 | |
| 214 | // Should contain detailed metrics not in brief mode |
| 215 | assert.Contains(t, output, "Uptime:") |
| 216 | assert.Contains(t, output, "Cycle started:") |
| 217 | assert.Contains(t, output, "Reprovide interval:") |
| 218 | assert.Contains(t, output, "Peers swept:") |
| 219 | assert.Contains(t, output, "Full keyspace coverage:") |
| 220 | }) |
| 221 | |
| 222 | t.Run("--compact requires --all", func(t *testing.T) { |
| 223 | t.Parallel() |
| 224 | |
| 225 | h := harness.NewT(t) |
| 226 | node := h.NewNode().Init() |
| 227 | node.SetIPFSConfig("Provide.DHT.SweepEnabled", true) |
| 228 | node.SetIPFSConfig("Provide.Enabled", true) |
| 229 | node.StartDaemon() |
| 230 | defer node.StopDaemon() |
| 231 | |
| 232 | res := node.RunIPFS("provide", "stat", "--compact") |
| 233 | assert.Error(t, res.Err) |
| 234 | assert.Contains(t, res.Stderr.String(), "--compact requires --all flag") |
| 235 | }) |
| 236 | |
| 237 | t.Run("--compact with --all shows 2-column layout", func(t *testing.T) { |
| 238 | t.Parallel() |
| 239 | |
| 240 | h := harness.NewT(t) |
| 241 | node := h.NewNode().Init() |
| 242 | node.SetIPFSConfig("Provide.DHT.SweepEnabled", true) |
| 243 | node.SetIPFSConfig("Provide.Enabled", true) |
| 244 | node.StartDaemon() |
| 245 | defer node.StopDaemon() |
| 246 | |
| 247 | res := node.IPFS("provide", "stat", "--all", "--compact") |
| 248 | require.NoError(t, res.Err) |
| 249 | |
| 250 | output := res.Stdout.String() |
| 251 | lines := strings.Split(strings.TrimSpace(output), "\n") |
| 252 | require.NotEmpty(t, lines) |
| 253 | |
| 254 | // In compact mode, find a line that has both Schedule and Connectivity metrics |
| 255 | // This confirms 2-column layout is working |
| 256 | foundTwoColumns := false |
| 257 | for _, line := range lines { |
| 258 | if strings.Contains(line, "CIDs scheduled:") && strings.Contains(line, "Status:") { |
| 259 | foundTwoColumns = true |
| 260 | break |
| 261 | } |
| 262 | } |
| 263 | assert.True(t, foundTwoColumns, "Should have at least one line with both 'CIDs scheduled:' and 'Status:' confirming 2-column layout") |
| 264 | }) |
| 265 | |
| 266 | t.Run("individual section flags work with full labels", func(t *testing.T) { |
| 267 | t.Parallel() |
| 268 | |
| 269 | h := harness.NewT(t) |
| 270 | node := h.NewNode().Init() |
| 271 | node.SetIPFSConfig("Provide.DHT.SweepEnabled", true) |
| 272 | node.SetIPFSConfig("Provide.Enabled", true) |
| 273 | node.StartDaemon() |
| 274 | defer node.StopDaemon() |
| 275 | |
| 276 | testCases := []struct { |
| 277 | flag string |
| 278 | contains []string |
| 279 | }{ |
| 280 | { |
| 281 | flag: "--connectivity", |
| 282 | contains: []string{"Status:"}, |
| 283 | }, |
| 284 | { |
| 285 | flag: "--queues", |
| 286 | contains: []string{"Provide queue:", "Reprovide queue:"}, |
| 287 | }, |
| 288 | { |
| 289 | flag: "--schedule", |
| 290 | contains: []string{"CIDs scheduled:", "Regions scheduled:", "Avg prefix length:", "Next region prefix:", "Next region reprovide:"}, |
| 291 | }, |
| 292 | { |
| 293 | flag: "--timings", |
| 294 | contains: []string{"Uptime:", "Current time offset:", "Cycle started:", "Reprovide interval:"}, |
| 295 | }, |
| 296 | { |
| 297 | flag: "--network", |
| 298 | contains: []string{"Avg record holders:", "Peers swept:", "Full keyspace coverage:", "Reachable peers:", "Avg region size:", "Replication factor:"}, |
| 299 | }, |
| 300 | { |
| 301 | flag: "--operations", |
| 302 | contains: []string{"Ongoing provides:", "Ongoing reprovides:", "Total CIDs provided:", "Total records provided:", "Total provide errors:"}, |
| 303 | }, |
| 304 | { |
| 305 | flag: "--workers", |
| 306 | contains: []string{"Active workers:", "Free workers:", "Workers stats:", "Periodic", "Burst"}, |
| 307 | }, |
| 308 | } |
| 309 | |
| 310 | for _, tc := range testCases { |
| 311 | res := node.IPFS("provide", "stat", tc.flag) |
| 312 | require.NoError(t, res.Err, "flag %s should work", tc.flag) |
| 313 | output := res.Stdout.String() |
| 314 | for _, expected := range tc.contains { |
| 315 | assert.Contains(t, output, expected, "flag %s should contain '%s'", tc.flag, expected) |
| 316 | } |
| 317 | } |
| 318 | }) |
| 319 | |
| 320 | t.Run("multiple section flags can be combined", func(t *testing.T) { |
| 321 | t.Parallel() |
| 322 | |
| 323 | h := harness.NewT(t) |
| 324 | node := h.NewNode().Init() |
| 325 | node.SetIPFSConfig("Provide.DHT.SweepEnabled", true) |
| 326 | node.SetIPFSConfig("Provide.Enabled", true) |
| 327 | node.StartDaemon() |
| 328 | defer node.StopDaemon() |
| 329 | |
| 330 | res := node.IPFS("provide", "stat", "--network", "--operations") |
| 331 | require.NoError(t, res.Err) |
| 332 | |
| 333 | output := res.Stdout.String() |
| 334 | // Should have section headings when multiple flags combined |
| 335 | assert.Contains(t, output, "Network:") |
| 336 | assert.Contains(t, output, "Operations:") |
| 337 | assert.Contains(t, output, "Avg record holders:") |
| 338 | assert.Contains(t, output, "Ongoing provides:") |
| 339 | }) |
| 340 | } |
| 341 | |
| 342 | // TestProvideStatLegacyProvider tests Legacy provider specific behavior |
| 343 | func TestProvideStatLegacyProvider(t *testing.T) { |
| 344 | t.Parallel() |
| 345 | |
| 346 | h := harness.NewT(t) |
| 347 | node := h.NewNode().Init() |
| 348 | node.SetIPFSConfig("Provide.DHT.SweepEnabled", false) |
| 349 | node.SetIPFSConfig("Provide.Enabled", true) |
| 350 | node.StartDaemon() |
| 351 | defer node.StopDaemon() |
| 352 | |
| 353 | t.Run("shows legacy stats from old provider system", func(t *testing.T) { |
| 354 | res := node.IPFS("provide", "stat") |
| 355 | require.NoError(t, res.Err) |
| 356 | |
| 357 | // Legacy provider shows stats from the old reprovider system |
| 358 | output := res.Stdout.String() |
| 359 | assert.Contains(t, output, "TotalReprovides:") |
| 360 | assert.Contains(t, output, "AvgReprovideDuration:") |
| 361 | assert.Contains(t, output, "LastReprovideDuration:") |
| 362 | }) |
| 363 | |
| 364 | t.Run("rejects flags with legacy provider", func(t *testing.T) { |
| 365 | flags := []string{"--all", "--connectivity", "--queues", "--network", "--workers"} |
| 366 | for _, flag := range flags { |
| 367 | res := node.RunIPFS("provide", "stat", flag) |
| 368 | assert.Error(t, res.Err, "flag %s should be rejected for legacy provider", flag) |
| 369 | assert.Contains(t, res.Stderr.String(), "cannot use flags with legacy provide stats") |
| 370 | } |
| 371 | }) |
| 372 | |
| 373 | t.Run("rejects --lan flag with legacy provider", func(t *testing.T) { |
| 374 | res := node.RunIPFS("provide", "stat", "--lan") |
| 375 | assert.Error(t, res.Err) |
| 376 | assert.Contains(t, res.Stderr.String(), "LAN stats only available for Sweep provider with Dual DHT") |
| 377 | }) |
| 378 | } |
| 379 | |
| 380 | // TestProvideStatOutputFormats tests different output formats |
| 381 | func TestProvideStatOutputFormats(t *testing.T) { |
| 382 | t.Parallel() |
| 383 | |
| 384 | t.Run("JSON output with Sweep provider", func(t *testing.T) { |
| 385 | t.Parallel() |
| 386 | |
| 387 | h := harness.NewT(t) |
| 388 | node := h.NewNode().Init() |
| 389 | node.SetIPFSConfig("Provide.DHT.SweepEnabled", true) |
| 390 | node.SetIPFSConfig("Provide.Enabled", true) |
| 391 | node.StartDaemon() |
| 392 | defer node.StopDaemon() |
| 393 | |
| 394 | res := node.IPFS("provide", "stat", "--enc=json") |
| 395 | require.NoError(t, res.Err) |
| 396 | |
| 397 | // Parse JSON to verify structure |
| 398 | var result struct { |
| 399 | Sweep map[string]any `json:"Sweep"` |
| 400 | Legacy map[string]any `json:"Legacy"` |
| 401 | } |
| 402 | err := json.Unmarshal([]byte(res.Stdout.String()), &result) |
| 403 | require.NoError(t, err, "Output should be valid JSON") |
| 404 | assert.NotNil(t, result.Sweep, "Sweep stats should be present") |
| 405 | assert.Nil(t, result.Legacy, "Legacy stats should not be present") |
| 406 | }) |
| 407 | |
| 408 | t.Run("JSON output with Legacy provider", func(t *testing.T) { |
| 409 | t.Parallel() |
| 410 | |
| 411 | h := harness.NewT(t) |
| 412 | node := h.NewNode().Init() |
| 413 | node.SetIPFSConfig("Provide.DHT.SweepEnabled", false) |
| 414 | node.SetIPFSConfig("Provide.Enabled", true) |
| 415 | node.StartDaemon() |
| 416 | defer node.StopDaemon() |
| 417 | |
| 418 | res := node.IPFS("provide", "stat", "--enc=json") |
| 419 | require.NoError(t, res.Err) |
| 420 | |
| 421 | // Parse JSON to verify structure |
| 422 | var result struct { |
| 423 | Sweep map[string]any `json:"Sweep"` |
| 424 | Legacy map[string]any `json:"Legacy"` |
| 425 | } |
| 426 | err := json.Unmarshal([]byte(res.Stdout.String()), &result) |
| 427 | require.NoError(t, err, "Output should be valid JSON") |
| 428 | assert.Nil(t, result.Sweep, "Sweep stats should not be present") |
| 429 | assert.NotNil(t, result.Legacy, "Legacy stats should be present") |
| 430 | }) |
| 431 | } |
| 432 | |
| 433 | // TestProvideStatIntegration tests integration with provide operations |
| 434 | func TestProvideStatIntegration(t *testing.T) { |
| 435 | t.Parallel() |
| 436 | |
| 437 | t.Run("stats reflect content being added to schedule", func(t *testing.T) { |
| 438 | t.Parallel() |
| 439 | |
| 440 | h := harness.NewT(t) |
| 441 | node := h.NewNode().Init() |
| 442 | node.SetIPFSConfig("Provide.DHT.SweepEnabled", true) |
| 443 | node.SetIPFSConfig("Provide.Enabled", true) |
| 444 | node.SetIPFSConfig("Provide.DHT.Interval", "1h") |
| 445 | node.StartDaemon() |
| 446 | defer node.StopDaemon() |
| 447 | |
| 448 | // Get initial scheduled CID count |
| 449 | res1 := node.IPFS("provide", "stat", "--enc=json") |
| 450 | require.NoError(t, res1.Err) |
| 451 | initialKeys := parseSweepStats(t, res1.Stdout.String()).Sweep.Schedule.Keys |
| 452 | |
| 453 | // Add content - this should increase CIDs scheduled |
| 454 | node.IPFSAddStr("test content for stats") |
| 455 | |
| 456 | // Wait for content to appear in schedule (with timeout) |
| 457 | // The buffered provider may take a moment to schedule items |
| 458 | require.Eventually(t, func() bool { |
| 459 | res := node.IPFS("provide", "stat", "--enc=json") |
| 460 | require.NoError(t, res.Err) |
| 461 | stats := parseSweepStats(t, res.Stdout.String()) |
| 462 | return stats.Sweep.Schedule.Keys > initialKeys |
| 463 | }, provideStatEventuallyTimeout, provideStatEventuallyTick, "Content should appear in schedule after adding") |
| 464 | }) |
| 465 | |
| 466 | t.Run("stats work with all documented strategies", func(t *testing.T) { |
| 467 | t.Parallel() |
| 468 | |
| 469 | // Test all strategies documented in docs/config.md#providestrategy |
| 470 | strategies := []string{"all", "pinned", "roots", "mfs", "pinned+mfs"} |
| 471 | for _, strategy := range strategies { |
| 472 | h := harness.NewT(t) |
| 473 | node := h.NewNode().Init() |
| 474 | node.SetIPFSConfig("Provide.DHT.SweepEnabled", true) |
| 475 | node.SetIPFSConfig("Provide.Enabled", true) |
| 476 | node.SetIPFSConfig("Provide.Strategy", strategy) |
| 477 | node.StartDaemon() |
| 478 | |
| 479 | res := node.IPFS("provide", "stat") |
| 480 | require.NoError(t, res.Err, "stats should work with strategy %s", strategy) |
| 481 | output := res.Stdout.String() |
| 482 | assert.NotEmpty(t, output) |
| 483 | assert.Contains(t, output, "CIDs scheduled:") |
| 484 | |
| 485 | node.StopDaemon() |
| 486 | } |
| 487 | }) |
| 488 | } |
| 489 | |
| 490 | // TestProvideStatDisabledConfig tests behavior when provide system is disabled |
| 491 | func TestProvideStatDisabledConfig(t *testing.T) { |
| 492 | t.Parallel() |
| 493 | |
| 494 | t.Run("Provide.Enabled=false returns error stats not available", func(t *testing.T) { |
| 495 | t.Parallel() |
| 496 | |
| 497 | h := harness.NewT(t) |
| 498 | node := h.NewNode().Init() |
| 499 | node.SetIPFSConfig("Provide.DHT.SweepEnabled", true) |
| 500 | node.SetIPFSConfig("Provide.Enabled", false) |
| 501 | node.StartDaemon() |
| 502 | defer node.StopDaemon() |
| 503 | |
| 504 | res := node.RunIPFS("provide", "stat") |
| 505 | assert.Error(t, res.Err) |
| 506 | assert.Contains(t, res.Stderr.String(), "stats not available") |
| 507 | }) |
| 508 | |
| 509 | t.Run("Provide.Enabled=true with Provide.DHT.Interval=0 returns stats with zero schedule fields", func(t *testing.T) { |
| 510 | t.Parallel() |
| 511 | |
| 512 | h := harness.NewT(t) |
| 513 | node := h.NewNode().Init() |
| 514 | node.SetIPFSConfig("Provide.DHT.SweepEnabled", true) |
| 515 | node.SetIPFSConfig("Provide.Enabled", true) |
| 516 | node.SetIPFSConfig("Provide.DHT.Interval", "0") |
| 517 | node.StartDaemon() |
| 518 | defer node.StopDaemon() |
| 519 | |
| 520 | // Interval=0 disables only the periodic schedule; the provider |
| 521 | // is still wired and 'provide stat' returns valid stats with |
| 522 | // the schedule-related timing fields zeroed out. |
| 523 | res := node.RunIPFS("provide", "stat") |
| 524 | assert.Equal(t, 0, res.ExitCode()) |
| 525 | assert.NotContains(t, res.Stderr.String(), "stats not available") |
| 526 | }) |
| 527 | } |