23
"os"
24
"os/exec"
25
"path/filepath"
26
+ "runtime"
27
+ "slices"
28
"strings"
27
- "syscall"
29
"testing"
30
"time"
31
62
node := setupStaticV15Repo(t)
63
64
// Create mock migration binary for 15→16 (16→17 will use embedded migration)
64
- createMockMigrationBinary(t, "15", "16")
65
+ mockBinDir := createMockMigrationBinary(t, "15", "16")
66
+ customPath := buildCustomPath(mockBinDir)
67
68
configPath := filepath.Join(node.Dir, "config")
69
versionPath := filepath.Join(node.Dir, "version")
82
originalPeerID := getNestedValue(originalConfig, "Identity.PeerID")
83
84
// Run dual migration using daemon --migrate
83
- stdoutOutput, migrationSuccess := runDaemonWithLegacyMigrationMonitoring(t, node)
85
+ stdoutOutput, migrationSuccess := runDaemonWithLegacyMigrationMonitoring(t, node, customPath)
86
87
// Debug output
88
t.Logf("Daemon output:\n%s", stdoutOutput)
126
node := setupStaticV15Repo(t)
127
128
// Create mock migration binary for 15→16 (16→17 will use embedded migration)
127
- createMockMigrationBinary(t, "15", "16")
129
+ mockBinDir := createMockMigrationBinary(t, "15", "16")
130
+ customPath := buildCustomPath(mockBinDir)
131
132
configPath := filepath.Join(node.Dir, "config")
133
versionPath := filepath.Join(node.Dir, "version")
138
require.Equal(t, "15", strings.TrimSpace(string(versionData)), "Should start at version 15")
139
140
// Run migration using 'ipfs repo migrate' with custom PATH
138
- result := node.Runner.Run(harness.RunRequest{
139
- Path: node.IPFSBin,
140
- Args: []string{"repo", "migrate"},
141
- CmdOpts: []harness.CmdOpt{
142
- func(cmd *exec.Cmd) {
143
- // Ensure the command inherits our modified PATH with mock binaries
144
- cmd.Env = append(cmd.Env, "PATH="+os.Getenv("PATH"))
145
- },
146
- },
147
- })
141
+ result := runMigrationWithCustomPath(node, customPath, "repo", "migrate")
142
require.Empty(t, result.Stderr.String(), "Migration should succeed without errors")
143
144
// Verify final version is latest
178
}
179
180
// runDaemonWithLegacyMigrationMonitoring monitors for hybrid migration patterns
187
-func runDaemonWithLegacyMigrationMonitoring(t *testing.T, node *harness.Node) (string, bool) {
181
+func runDaemonWithLegacyMigrationMonitoring(t *testing.T, node *harness.Node, customPath string) (string, bool) {
182
// Monitor for hybrid migration completion - use "Hybrid migration completed successfully" as success pattern
183
stdoutOutput, daemonStarted := runDaemonWithMigrationMonitoringCustomEnv(t, node, "Using hybrid migration strategy", "Hybrid migration completed successfully", map[string]string{
190
- "PATH": os.Getenv("PATH"), // Pass current PATH which includes our mock binaries
184
+ "PATH": customPath, // Pass custom PATH with our mock binaries
185
})
186
187
// Check for hybrid migration patterns in output
265
t.Log("Daemon startup timed out")
266
}
267
274
- // Stop the daemon
268
+ // Stop the daemon using ipfs shutdown command for graceful shutdown
269
if cmd.Process != nil {
276
- _ = cmd.Process.Signal(syscall.SIGTERM)
270
+ shutdownCmd := exec.Command(node.IPFSBin, "shutdown")
271
+ shutdownCmd.Dir = node.Dir
272
+ for k, v := range node.Runner.Env {
273
+ shutdownCmd.Env = append(shutdownCmd.Env, k+"="+v)
274
+ }
275
+
276
+ if err := shutdownCmd.Run(); err != nil {
277
+ // If graceful shutdown fails, force kill
278
+ _ = cmd.Process.Kill()
279
+ }
280
+
281
+ // Wait for process to exit
282
_ = cmd.Wait()
283
}
284
285
return outputBuffer.String(), daemonReady && migrationStarted && migrationCompleted
286
}
287
283
-// createMockMigrationBinary creates a platform-agnostic Go binary for migration on PATH
284
-func createMockMigrationBinary(t *testing.T, fromVer, toVer string) {
288
+// buildCustomPath creates a custom PATH with mock migration binaries prepended.
289
+// This is necessary for test isolation when running tests in parallel with t.Parallel().
290
+// Without isolated PATH handling, parallel tests can interfere with each other through
291
+// global PATH modifications, causing tests to download real migration binaries instead
292
+// of using the test mocks.
293
+func buildCustomPath(mockBinDirs ...string) string {
294
+ // Prepend mock directories to ensure they're found first
295
+ pathElements := append(mockBinDirs, os.Getenv("PATH"))
296
+ return strings.Join(pathElements, string(filepath.ListSeparator))
297
+}
298
+
299
+// runMigrationWithCustomPath runs a migration command with a custom PATH environment.
300
+// This ensures the migration uses our mock binaries instead of downloading real ones.
301
+func runMigrationWithCustomPath(node *harness.Node, customPath string, args ...string) *harness.RunResult {
302
+ return node.Runner.Run(harness.RunRequest{
303
+ Path: node.IPFSBin,
304
+ Args: args,
305
+ CmdOpts: []harness.CmdOpt{
306
+ func(cmd *exec.Cmd) {
307
+ // Remove existing PATH entries using slices.DeleteFunc
308
+ cmd.Env = slices.DeleteFunc(cmd.Env, func(s string) bool {
309
+ return strings.HasPrefix(s, "PATH=")
310
+ })
311
+ // Add custom PATH
312
+ cmd.Env = append(cmd.Env, "PATH="+customPath)
313
+ },
314
+ },
315
+ })
316
+}
317
+
318
+// createMockMigrationBinary creates a platform-agnostic Go binary for migration testing.
319
+// Returns the directory containing the binary to be added to PATH.
320
+func createMockMigrationBinary(t *testing.T, fromVer, toVer string) string {
321
// Create bin directory for migration binaries
322
binDir := t.TempDir()
323
325
scriptName := fmt.Sprintf("fs-repo-%s-to-%s", fromVer, toVer)
326
sourceFile := filepath.Join(binDir, scriptName+".go")
327
binaryPath := filepath.Join(binDir, scriptName)
328
+ if runtime.GOOS == "windows" {
329
+ binaryPath += ".exe"
330
+ }
331
332
+ // Generate minimal mock migration binary code
333
goSource := fmt.Sprintf(`package main
294
-
295
-import (
296
- "fmt"
297
- "os"
298
- "path/filepath"
299
- "strings"
300
-)
301
-
334
+import ("fmt"; "os"; "path/filepath"; "strings"; "time")
335
func main() {
303
- // Parse command line arguments - real migration binaries expect -path=<repo-path>
304
- var repoPath string
336
+ var path string
337
var revert bool
306
- for _, arg := range os.Args[1:] {
307
- if strings.HasPrefix(arg, "-path=") {
308
- repoPath = strings.TrimPrefix(arg, "-path=")
309
- } else if arg == "-revert" {
310
- revert = true
311
- }
338
+ for _, a := range os.Args[1:] {
339
+ if strings.HasPrefix(a, "-path=") { path = a[6:] }
340
+ if a == "-revert" { revert = true }
341
}
313
-
314
- if repoPath == "" {
315
- fmt.Fprintf(os.Stderr, "Usage: %%s -path=<repo-path> [-verbose=true] [-revert]\n", os.Args[0])
342
+ if path == "" { fmt.Fprintln(os.Stderr, "missing -path="); os.Exit(1) }
343
+
344
+ from, to := "%s", "%s"
345
+ if revert { from, to = to, from }
346
+ fmt.Printf("fake applying %%s-to-%%s repo migration\n", from, to)
347
+
348
+ // Create and immediately remove lock file to simulate proper locking behavior
349
+ lockPath := filepath.Join(path, "repo.lock")
350
+ lockFile, err := os.Create(lockPath)
351
+ if err != nil && !os.IsExist(err) {
352
+ fmt.Fprintf(os.Stderr, "Error creating lock: %%v\n", err)
353
os.Exit(1)
354
}
318
-
319
- // Determine source and target versions based on revert flag
320
- var sourceVer, targetVer string
321
- if revert {
322
- // When reverting, we go backwards: fs-repo-15-to-16 with -revert goes 16→15
323
- sourceVer = "%s"
324
- targetVer = "%s"
325
- } else {
326
- // Normal forward migration: fs-repo-15-to-16 goes 15→16
327
- sourceVer = "%s"
328
- targetVer = "%s"
355
+ if lockFile != nil {
356
+ lockFile.Close()
357
+ defer os.Remove(lockPath)
358
}
330
-
331
- // Print migration message (same format as real migrations)
332
- fmt.Printf("fake applying %%s-to-%%s repo migration\n", sourceVer, targetVer)
333
-
334
- // Update version file
335
- versionFile := filepath.Join(repoPath, "version")
336
- err := os.WriteFile(versionFile, []byte(targetVer), 0644)
337
- if err != nil {
338
- fmt.Fprintf(os.Stderr, "Error updating version: %%v\n", err)
359
+
360
+ // Small delay to simulate migration work
361
+ time.Sleep(10 * time.Millisecond)
362
+
363
+ if err := os.WriteFile(filepath.Join(path, "version"), []byte(to), 0644); err != nil {
364
+ fmt.Fprintf(os.Stderr, "Error: %%v\n", err)
365
os.Exit(1)
366
}
341
-}
342
-`, toVer, fromVer, fromVer, toVer)
367
+}`, fromVer, toVer)
368
369
require.NoError(t, os.WriteFile(sourceFile, []byte(goSource), 0644))
370
371
// Compile the Go binary
347
- require.NoError(t, os.Setenv("CGO_ENABLED", "0")) // Ensure static binary
348
- require.NoError(t, exec.Command("go", "build", "-o", binaryPath, sourceFile).Run())
349
-
350
- // Add bin directory to PATH for this test
351
- currentPath := os.Getenv("PATH")
352
- newPath := binDir + string(filepath.ListSeparator) + currentPath
353
- require.NoError(t, os.Setenv("PATH", newPath))
354
- t.Cleanup(func() { os.Setenv("PATH", currentPath) })
372
+ cmd := exec.Command("go", "build", "-o", binaryPath, sourceFile)
373
+ cmd.Env = append(os.Environ(), "CGO_ENABLED=0") // Ensure static binary
374
+ require.NoError(t, cmd.Run())
375
376
// Verify the binary exists and is executable
377
_, err := os.Stat(binaryPath)
378
require.NoError(t, err, "Mock binary should exist")
379
+
380
+ // Return the bin directory to be added to PATH
381
+ return binDir
382
}
383
384
// expectedMigrationSteps generates the expected migration step strings for a version range.
439
// Start with v15 fixture and migrate forward to latest to create proper backup files
440
node := setupStaticV15Repo(t)
441
419
- // Create mock migration binary for 15→16 (needed for forward migration)
420
- createMockMigrationBinary(t, "15", "16")
421
- // Create mock migration binary for 16→15 (needed for downgrade)
422
- createMockMigrationBinary(t, "16", "15")
442
+ // Create mock migration binaries for both forward and reverse migrations
443
+ mockBinDirs := []string{
444
+ createMockMigrationBinary(t, "15", "16"), // for forward migration
445
+ createMockMigrationBinary(t, "16", "15"), // for downgrade
446
+ }
447
+ customPath := buildCustomPath(mockBinDirs...)
448
449
configPath := filepath.Join(node.Dir, "config")
450
versionPath := filepath.Join(node.Dir, "version")
451
452
// Step 1: Forward migration from v15 to latest to create backup files
453
t.Logf("Step 1: Forward migration v15 → v%d", ipfs.RepoVersion)
429
- result := node.Runner.Run(harness.RunRequest{
430
- Path: node.IPFSBin,
431
- Args: []string{"repo", "migrate"},
432
- CmdOpts: []harness.CmdOpt{
433
- func(cmd *exec.Cmd) {
434
- // Ensure the command inherits our modified PATH with mock binaries
435
- cmd.Env = append(cmd.Env, "PATH="+os.Getenv("PATH"))
436
- },
437
- },
438
- })
454
+ result := runMigrationWithCustomPath(node, customPath, "repo", "migrate")
455
456
// Debug: print the output to see what happened
457
t.Logf("Forward migration stdout:\n%s", result.Stdout.String())
475
476
// Step 2: Reverse hybrid migration from latest to v15
477
t.Logf("Step 2: Reverse hybrid migration v%d → v15", ipfs.RepoVersion)
462
- result = node.Runner.Run(harness.RunRequest{
463
- Path: node.IPFSBin,
464
- Args: []string{"repo", "migrate", "--to=15", "--allow-downgrade"},
465
- CmdOpts: []harness.CmdOpt{
466
- func(cmd *exec.Cmd) {
467
- // Ensure the command inherits our modified PATH with mock binaries
468
- cmd.Env = append(cmd.Env, "PATH="+os.Getenv("PATH"))
469
- },
470
- },
471
- })
478
+ result = runMigrationWithCustomPath(node, customPath, "repo", "migrate", "--to=15", "--allow-downgrade")
479
require.Empty(t, result.Stderr.String(), "Reverse hybrid migration should succeed without errors")
480
481
// Debug output