<commit_before>package patcher_test

import (
	"bytes"
	"context"
	"io/ioutil"
	"os"
	"path/filepath"
	"testing"

	"github.com/itchio/headway/state"
	"github.com/itchio/lake/pools/fspool"
	"github.com/itchio/lake/tlc"
	"github.com/itchio/savior/seeksource"
	"github.com/itchio/wharf/archiver"
	"github.com/itchio/wharf/pwr"
	"github.com/itchio/wharf/pwr/bowl"
	"github.com/itchio/wharf/pwr/patcher"
	"github.com/itchio/wharf/wtest"
	"github.com/pkg/errors"
	"github.com/stretchr/testify/assert"
)

func Test_Scenarios(t *testing.T) {
	runPatchingScenario(t, patchScenario{
		name: "change one",
		v1: testDirSettings{
			entries: []testDirEntry{
				{path: "subdir/file-1", seed: 0x1, size: pwr.BlockSize*11 + 14},
				{path: "file-1", seed: 0x2},
				{path: "dir2/file-2", seed: 0x3},
			},
		},
		corruptions: &testDirSettings{
			entries: []testDirEntry{
				{path: "subdir/file-1", seed: 0x22, size: pwr.BlockSize*11 + 14},
			},
		},
		v2: testDirSettings{
			entries: []testDirEntry{
				{path: "subdir/file-1", seed: 0x1, size: pwr.BlockSize*17 + 14},
				{path: "file-1", seed: 0x2},
				{path: "dir2/file-2", seed: 0x3},
			},
		},
	})

	runPatchingScenario(t, patchScenario{
		name: "one became short",
		v1: testDirSettings{
			entries: []testDirEntry{
				{path: "short", chunks: []testDirChunk{
					testDirChunk{seed: 0x111, size: pwr.BlockSize},
				}},
			},
		},
		corruptions: &testDirSettings{
			entries: []testDirEntry{
				{path: "short", chunks: []testDirChunk{
					testDirChunk{seed: 0x111, size: pwr.BlockSize - 17},
				}},
			},
		},
		v2: testDirSettings{
			entries: []testDirEntry{
				{path: "short", chunks: []testDirChunk{
					testDirChunk{seed: 0x111, size: pwr.BlockSize},
					testDirChunk{seed: 0x222, size: 17},
				}},
			},
		},
	})

	runPatchingScenario(t, patchScenario{
		name: "early small wound",
		v1: testDirSettings{
			entries: []testDirEntry{
				{path: "short", chunks: []testDirChunk{
					testDirChunk{seed: 0x111, size: pwr.BlockSize * 16},
				}},
			},
		},
		corruptions: &testDirSettings{
			entries: []testDirEntry{
				{path: "short", chunks: []testDirChunk{
					testDirChunk{seed: 0x111, size: pwr.BlockSize},
					testDirChunk{seed: 0x222, size: 1},
					testDirChunk{seed: 0x111, size: pwr.BlockSize*15 - 1},
				}},
			},
		},
		v2: testDirSettings{
			entries: []testDirEntry{
				{path: "short", chunks: []testDirChunk{
					testDirChunk{seed: 0x111, size: pwr.BlockSize * 16},
					testDirChunk{seed: 0x333, size: pwr.BlockSize * 16},
				}},
			},
		},
	})

	runPatchingScenario(t, patchScenario{
		name: "change one in the middle",
		v1: testDirSettings{
			entries: []testDirEntry{
				{path: "subdir/file-1", chunks: []testDirChunk{
					testDirChunk{seed: 0x111, size: pwr.BlockSize*12 + 1},
					testDirChunk{seed: 0x222, size: pwr.BlockSize*12 + 3},
					testDirChunk{seed: 0x333, size: pwr.BlockSize*12 + 4},
				}},
			},
		},
		corruptions: &testDirSettings{
			entries: []testDirEntry{
				{path: "subdir/file-1", chunks: []testDirChunk{
					testDirChunk{seed: 0x111, size: pwr.BlockSize*12 + 1},
					testDirChunk{seed: 0x222, size: pwr.BlockSize*12 + 3},
					testDirChunk{seed: 0x333, size: pwr.BlockSize * 12},
				}},
			},
		},
		v2: testDirSettings{
			entries: []testDirEntry{
				{path: "subdir/file-1", chunks: []testDirChunk{
					testDirChunk{seed: 0x111, size: pwr.BlockSize*12 + 1},
					testDirChunk{seed: 0x444, size: pwr.BlockSize*12 + 3},
					testDirChunk{seed: 0x333, size: pwr.BlockSize*12 + 4},
				}},
			},
		},
	})

	runPatchingScenario(t, patchScenario{
		name: "add one, remove one",
		v1: testDirSettings{
			entries: []testDirEntry{
				{path: "dir1/subdir/file-1", seed: 0x1},
				{path: "dir2/file-1", seed: 0x2},
			},
		},
		v2: testDirSettings{
			entries: []testDirEntry{
				{path: "dir1/subdir/file-1", seed: 0x1},
				{path: "dir2/file-2", seed: 0x3},
			},
		},
	})

	runPatchingScenario(t, patchScenario{
		name: "rename one",
		v1: testDirSettings{
			entries: []testDirEntry{
				{path: "dir1/subdir/file-1", seed: 0x1},
				{path: "dir2/subdir/file-1", seed: 0x2, size: pwr.BlockSize*12 + 13},
			},
		},
		v2: testDirSettings{
			entries: []testDirEntry{
				{path: "dir1/subdir/file-1", seed: 0x1},
				{path: "dir3/subdir/subdir/file-2", seed: 0x2, size: pwr.BlockSize*12 + 13},
			},
		},
	})

	runPatchingScenario(t, patchScenario{
		name: "delete folder, one generated",
		v1: testDirSettings{
			entries: []testDirEntry{
				{path: "dir1/subdir/file-1", seed: 0x1},
				{path: "dir2/subdir/file-1", seed: 0x2, size: pwr.BlockSize*12 + 13},
			},
		},
		intermediate: &testDirSettings{
			entries: []testDirEntry{
				{path: "dir2/subdir/file-1-generated", seed: 0x999, size: pwr.BlockSize * 3},
			},
		},
		v2: testDirSettings{
			entries: []testDirEntry{
				{path: "dir1/subdir/file-1", seed: 0x1},
				{path: "dir3/subdir/subdir/file-2", seed: 0x289, size: pwr.BlockSize*3 + 12},
			},
		},
	})

	runPatchingScenario(t, patchScenario{
		name: "move 4 files",
		v1: testDirSettings{
			entries: []testDirEntry{
				{path: "old/file-1", seed: 0x111},
				{path: "old/subdir/file-1", seed: 0x222},
				{path: "old/subdir/file-2", seed: 0x333},
				{path: "old/subdir/subdir/file-4", seed: 0x444},
			},
		},
		intermediate: &testDirSettings{
			entries: []testDirEntry{
				{path: "old/subdir/file-1-generated", seed: 0x999, size: pwr.BlockSize * 3},
			},
		},
		v2: testDirSettings{
			entries: []testDirEntry{
				{path: "new/file-1", seed: 0x111},
				{path: "new/subdir/file-1", seed: 0x222},
				{path: "new/subdir/file-2", seed: 0x333},
				{path: "new/subdir/subdir/file-4", seed: 0x444},
			},
		},
	})

	runPatchingScenario(t, patchScenario{
		name: "move 4 files into a subdirectory",
		v1: testDirSettings{
			entries: []testDirEntry{
				{path: "old/file-1", seed: 0x1},
				{path: "old/subdir/file-1", seed: 0x2},
				{path: "old/subdir/file-2", seed: 0x3},
				{path: "old/subdir/subdir/file-4", seed: 0x4},
			},
		},
		intermediate: &testDirSettings{
			entries: []testDirEntry{
				{path: "old/subdir/file-1-generated", seed: 0x999, size: pwr.BlockSize * 3},
			},
		},
		v2: testDirSettings{
			entries: []testDirEntry{
				{path: "old/new/file-1", seed: 0x1},
				{path: "old/new/subdir/file-1", seed: 0x2},
				{path: "old/new/subdir/file-2", seed: 0x3},
				{path: "old/new/subdir/subdir/file-4", seed: 0x4},
			},
		},
	})

	runPatchingScenario(t, patchScenario{
		name: "one file is duplicated twice",
		v1: testDirSettings{
			entries: []testDirEntry{
				{path: "dir1/file-1", seed: 0x1},
			},
		},
		v2: testDirSettings{
			entries: []testDirEntry{
				{path: "dir1/file-1", seed: 0x1},
				{path: "dir2/file-1", seed: 0x1},
				{path: "dir2/file-1bis", seed: 0x1},
			},
		},
	})

	runPatchingScenario(t, patchScenario{
		name: "one file is renamed + duplicated twice",
		v1: testDirSettings{
			entries: []testDirEntry{
				{path: "dir1/file-1", seed: 0x1},
			},
		},
		v2: testDirSettings{
			entries: []testDirEntry{
				{path: "dir2/file-1", seed: 0x1},
				{path: "dir3/file-1", seed: 0x1},
				{path: "dir3/file-1bis", seed: 0x1},
			},
		},
	})

	runPatchingScenario(t, patchScenario{
		name: "four large unchanged",
		v1: testDirSettings{
			entries: []testDirEntry{
				{path: "subdir/file-1", seed: 0x11, size: pwr.BlockSize*largeAmount + 17},
				{path: "subdir/file-2", seed: 0x22, size: pwr.BlockSize*largeAmount + 17},
				{path: "subdir/file-3", seed: 0x33, size: pwr.BlockSize*largeAmount + 17},
				{path: "subdir/file-4", seed: 0x44, size: pwr.BlockSize*largeAmount + 17},
			},
		},
		corruptions: &testDirSettings{
			entries: []testDirEntry{
				{path: "subdir/file-1", seed: 0x99, size: pwr.BlockSize*largeAmount + 17},
			},
		},
		v2: testDirSettings{
			entries: []testDirEntry{
				{path: "subdir/file-1", seed: 0x11, size: pwr.BlockSize*largeAmount + 17},
				{path: "subdir/file-2", seed: 0x22, size: pwr.BlockSize*largeAmount + 17},
				{path: "subdir/file-3", seed: 0x33, size: pwr.BlockSize*largeAmount + 17},
				{path: "subdir/file-4", seed: 0x44, size: pwr.BlockSize*largeAmount + 17},
			},
		},
	})

	runPatchingScenario(t, patchScenario{
		name: "four large, two swap",
		v1: testDirSettings{
			entries: []testDirEntry{
				{path: "subdir/file-1", seed: 0x11, size: pwr.BlockSize*largeAmount + 17},
				{path: "subdir/file-2", seed: 0x22, size: pwr.BlockSize*largeAmount + 17},
				{path: "subdir/file-3", seed: 0x33, size: pwr.BlockSize*largeAmount + 17},
				{path: "subdir/file-4", seed: 0x44, size: pwr.BlockSize*largeAmount + 17},
			},
		},
		corruptions: &testDirSettings{
			entries: []testDirEntry{
				{path: "subdir/file-1", seed: 0x99, size: pwr.BlockSize*largeAmount + 17},
			},
		},
		v2: testDirSettings{
			entries: []testDirEntry{
				{path: "subdir/file-1", seed: 0x11, size: pwr.BlockSize*largeAmount + 17},
				{path: "subdir/file-2", seed: 0x22, size: pwr.BlockSize*largeAmount + 17},
				{path: "subdir/file-3", seed: 0x44, size: pwr.BlockSize*largeAmount + 17},
				{path: "subdir/file-4", seed: 0x33, size: pwr.BlockSize*largeAmount + 17},
			},
		},
	})

	runPatchingScenario(t, patchScenario{
		name: "four large, two swap + duplicate (option A)",
		v1: testDirSettings{
			entries: []testDirEntry{
				{path: "subdir/file-1", seed: 0x11, size: pwr.BlockSize*largeAmount + 17},
				{path: "subdir/file-2", seed: 0x22, size: pwr.BlockSize*largeAmount + 17},
				{path: "subdir/file-3", seed: 0x33, size: pwr.BlockSize*largeAmount + 17},
				{path: "subdir/file-4", seed: 0x44, size: pwr.BlockSize*largeAmount + 17},
			},
		},
		corruptions: &testDirSettings{
			entries: []testDirEntry{
				{path: "subdir/file-1", seed: 0x99, size: pwr.BlockSize*largeAmount + 17},
			},
		},
		v2: testDirSettings{
			entries: []testDirEntry{
				{path: "subdir/file-1", seed: 0x22, size: pwr.BlockSize*largeAmount + 17},
				{path: "subdir/file-2", seed: 0x11, size: pwr.BlockSize*largeAmount + 17},
				{path: "subdir/file-3", seed: 0x22, size: pwr.BlockSize*largeAmount + 17},
				{path: "subdir/file-4", seed: 0x22, size: pwr.BlockSize*largeAmount + 17},
			},
		},
	})

	runPatchingScenario(t, patchScenario{
		name: "four large, two swap + duplicate (option B)",
		v1: testDirSettings{
			entries: []testDirEntry{
				{path: "subdir/file-1", seed: 0x11, size: pwr.BlockSize*largeAmount + 17},
				{path: "subdir/file-2", seed: 0x22, size: pwr.BlockSize*largeAmount + 17},
				{path: "subdir/file-3", seed: 0x33, size: pwr.BlockSize*largeAmount + 17},
				{path: "subdir/file-4", seed: 0x44, size: pwr.BlockSize*largeAmount + 17},
			},
		},
		corruptions: &testDirSettings{
			entries: []testDirEntry{
				{path: "subdir/file-1", seed: 0x99, size: pwr.BlockSize*largeAmount + 17},
			},
		},
		v2: testDirSettings{
			entries: []testDirEntry{
				{path: "subdir/file-1", seed: 0x22, size: pwr.BlockSize*largeAmount + 17},
				{path: "subdir/file-2", seed: 0x11, size: pwr.BlockSize*largeAmount + 17},
				{path: "subdir/file-3", seed: 0x11, size: pwr.BlockSize*largeAmount + 17},
				{path: "subdir/file-4", seed: 0x11, size: pwr.BlockSize*largeAmount + 17},
			},
		},
	})

	if testSymlinks {
		runPatchingScenario(t, patchScenario{
			name: "symlinks are added by patch",
			v1: testDirSettings{
				entries: []testDirEntry{
					{path: "dir1/file", seed: 0x1},
				},
			},
			v2: testDirSettings{
				entries: []testDirEntry{
					{path: "dir1/file", seed: 0x1},
					{path: "dir1/link", dest: "file"},
				},
			},
		})
	}

	if testSymlinks {
		runPatchingScenario(t, patchScenario{
			name: "symlinks are changed by patch",
			v1: testDirSettings{
				entries: []testDirEntry{
					{path: "dir1/file1", seed: 0x1},
					{path: "dir1/file2", seed: 0x2},
					{path: "dir1/link", dest: "file1"},
				},
			},
			v2: testDirSettings{
				entries: []testDirEntry{
					{path: "dir1/file1", seed: 0x1},
					{path: "dir1/file2", seed: 0x2},
					{path: "dir1/link", dest: "file2"},
				},
			},
		})
	}

	if testSymlinks {
		runPatchingScenario(t, patchScenario{
			name: "symlinks are removed by patch",
			v1: testDirSettings{
				entries: []testDirEntry{
					{path: "dir1/file", seed: 0x1},
					{path: "dir1/link", dest: "file"},
				},
			},
			v2: testDirSettings{
				entries: []testDirEntry{
					{path: "dir1/file", seed: 0x1},
				},
			},
		})
	}

	if testSymlinks {
		runPatchingScenario(t, patchScenario{
			name: "symlink becomes normal file (itchio/itch#2315)",
			v1: testDirSettings{
				entries: []testDirEntry{
					{path: "test2.txt", seed: 0x1},
					{path: "test.txt", dest: "test2.txt"},
				},
			},
			v2: testDirSettings{
				entries: []testDirEntry{
					{path: "test2.txt", seed: 0x1},
					{path: "test.txt", seed: 0x2},
				},
			},
		})
	}

	if testSymlinks {
		runPatchingScenario(t, patchScenario{
			name: "symlink becomes directory",
			v1: testDirSettings{
				entries: []testDirEntry{
					{path: "test2.txt", seed: 0x1},
					{path: "test.txt", dest: "test2.txt"},
				},
			},
			v2: testDirSettings{
				entries: []testDirEntry{
					{path: "test2.txt", seed: 0x1},
					{path: "test.txt/woop", seed: 0x2},
				},
			},
		})
	}
}

func runPatchingScenario(t *testing.T, scenario patchScenario) {
	forwardScenario := *&scenario
	forwardScenario.name += "/forward"
	runSinglePatchingScenario(t, forwardScenario)

	backScenario := *&scenario
	backScenario.name += "/back"
	backScenario.v1, backScenario.v2 = backScenario.v2, backScenario.v1
	runSinglePatchingScenario(t, backScenario)
}

func runSinglePatchingScenario(t *testing.T, scenario patchScenario) {
	t.Run(scenario.name, func(t *testing.T) {
		log := t.Logf

		mainDir, err := ioutil.TempDir("", "patch-cycle")
		wtest.Must(t, err)
		defer os.RemoveAll(mainDir)

		v1 := filepath.Join(mainDir, "v1")
		makeTestDir(t, v1, scenario.v1)

		v2 := filepath.Join(mainDir, "v2")
		makeTestDir(t, v2, scenario.v2)

		v1zip := filepath.Join(mainDir, "v1.zip")
		v2zip := filepath.Join(mainDir, "v2.zip")
<commit_msg>Add 'symlink becomes dangling' test<commit_after>package patcher_test

import (
	"bytes"
	"context"
	"io/ioutil"
	"os"
	"path/filepath"
	"testing"

	"github.com/itchio/headway/state"
	"github.com/itchio/lake/pools/fspool"
	"github.com/itchio/lake/tlc"
	"github.com/itchio/savior/seeksource"
	"github.com/itchio/wharf/archiver"
	"github.com/itchio/wharf/pwr"
	"github.com/itchio/wharf/pwr/bowl"
	"github.com/itchio/wharf/pwr/patcher"
	"github.com/itchio/wharf/wtest"
	"github.com/pkg/errors"
	"github.com/stretchr/testify/assert"
)

func Test_Scenarios(t *testing.T) {
	runPatchingScenario(t, patchScenario{
		name: "change one",
		v1: testDirSettings{
			entries: []testDirEntry{
				{path: "subdir/file-1", seed: 0x1, size: pwr.BlockSize*11 + 14},
				{path: "file-1", seed: 0x2},
				{path: "dir2/file-2", seed: 0x3},
			},
		},
		corruptions: &testDirSettings{
			entries: []testDirEntry{
				{path: "subdir/file-1", seed: 0x22, size: pwr.BlockSize*11 + 14},
			},
		},
		v2: testDirSettings{
			entries: []testDirEntry{
				{path: "subdir/file-1", seed: 0x1, size: pwr.BlockSize*17 + 14},
				{path: "file-1", seed: 0x2},
				{path: "dir2/file-2", seed: 0x3},
			},
		},
	})

	runPatchingScenario(t, patchScenario{
		name: "one became short",
		v1: testDirSettings{
			entries: []testDirEntry{
				{path: "short", chunks: []testDirChunk{
					testDirChunk{seed: 0x111, size: pwr.BlockSize},
				}},
			},
		},
		corruptions: &testDirSettings{
			entries: []testDirEntry{
				{path: "short", chunks: []testDirChunk{
					testDirChunk{seed: 0x111, size: pwr.BlockSize - 17},
				}},
			},
		},
		v2: testDirSettings{
			entries: []testDirEntry{
				{path: "short", chunks: []testDirChunk{
					testDirChunk{seed: 0x111, size: pwr.BlockSize},
					testDirChunk{seed: 0x222, size: 17},
				}},
			},
		},
	})

	runPatchingScenario(t, patchScenario{
		name: "early small wound",
		v1: testDirSettings{
			entries: []testDirEntry{
				{path: "short", chunks: []testDirChunk{
					testDirChunk{seed: 0x111, size: pwr.BlockSize * 16},
				}},
			},
		},
		corruptions: &testDirSettings{
			entries: []testDirEntry{
				{path: "short", chunks: []testDirChunk{
					testDirChunk{seed: 0x111, size: pwr.BlockSize},
					testDirChunk{seed: 0x222, size: 1},
					testDirChunk{seed: 0x111, size: pwr.BlockSize*15 - 1},
				}},
			},
		},
		v2: testDirSettings{
			entries: []testDirEntry{
				{path: "short", chunks: []testDirChunk{
					testDirChunk{seed: 0x111, size: pwr.BlockSize * 16},
					testDirChunk{seed: 0x333, size: pwr.BlockSize * 16},
				}},
			},
		},
	})

	runPatchingScenario(t, patchScenario{
		name: "change one in the middle",
		v1: testDirSettings{
			entries: []testDirEntry{
				{path: "subdir/file-1", chunks: []testDirChunk{
					testDirChunk{seed: 0x111, size: pwr.BlockSize*12 + 1},
					testDirChunk{seed: 0x222, size: pwr.BlockSize*12 + 3},
					testDirChunk{seed: 0x333, size: pwr.BlockSize*12 + 4},
				}},
			},
		},
		corruptions: &testDirSettings{
			entries: []testDirEntry{
				{path: "subdir/file-1", chunks: []testDirChunk{
					testDirChunk{seed: 0x111, size: pwr.BlockSize*12 + 1},
					testDirChunk{seed: 0x222, size: pwr.BlockSize*12 + 3},
					testDirChunk{seed: 0x333, size: pwr.BlockSize * 12},
				}},
			},
		},
		v2: testDirSettings{
			entries: []testDirEntry{
				{path: "subdir/file-1", chunks: []testDirChunk{
					testDirChunk{seed: 0x111, size: pwr.BlockSize*12 + 1},
					testDirChunk{seed: 0x444, size: pwr.BlockSize*12 + 3},
					testDirChunk{seed: 0x333, size: pwr.BlockSize*12 + 4},
				}},
			},
		},
	})

	runPatchingScenario(t, patchScenario{
		name: "add one, remove one",
		v1: testDirSettings{
			entries: []testDirEntry{
				{path: "dir1/subdir/file-1", seed: 0x1},
				{path: "dir2/file-1", seed: 0x2},
			},
		},
		v2: testDirSettings{
			entries: []testDirEntry{
				{path: "dir1/subdir/file-1", seed: 0x1},
				{path: "dir2/file-2", seed: 0x3},
			},
		},
	})

	runPatchingScenario(t, patchScenario{
		name: "rename one",
		v1: testDirSettings{
			entries: []testDirEntry{
				{path: "dir1/subdir/file-1", seed: 0x1},
				{path: "dir2/subdir/file-1", seed: 0x2, size: pwr.BlockSize*12 + 13},
			},
		},
		v2: testDirSettings{
			entries: []testDirEntry{
				{path: "dir1/subdir/file-1", seed: 0x1},
				{path: "dir3/subdir/subdir/file-2", seed: 0x2, size: pwr.BlockSize*12 + 13},
			},
		},
	})

	runPatchingScenario(t, patchScenario{
		name: "delete folder, one generated",
		v1: testDirSettings{
			entries: []testDirEntry{
				{path: "dir1/subdir/file-1", seed: 0x1},
				{path: "dir2/subdir/file-1", seed: 0x2, size: pwr.BlockSize*12 + 13},
			},
		},
		intermediate: &testDirSettings{
			entries: []testDirEntry{
				{path: "dir2/subdir/file-1-generated", seed: 0x999, size: pwr.BlockSize * 3},
			},
		},
		v2: testDirSettings{
			entries: []testDirEntry{
				{path: "dir1/subdir/file-1", seed: 0x1},
				{path: "dir3/subdir/subdir/file-2", seed: 0x289, size: pwr.BlockSize*3 + 12},
			},
		},
	})

	runPatchingScenario(t, patchScenario{
		name: "move 4 files",
		v1: testDirSettings{
			entries: []testDirEntry{
				{path: "old/file-1", seed: 0x111},
				{path: "old/subdir/file-1", seed: 0x222},
				{path: "old/subdir/file-2", seed: 0x333},
				{path: "old/subdir/subdir/file-4", seed: 0x444},
			},
		},
		intermediate: &testDirSettings{
			entries: []testDirEntry{
				{path: "old/subdir/file-1-generated", seed: 0x999, size: pwr.BlockSize * 3},
			},
		},
		v2: testDirSettings{
			entries: []testDirEntry{
				{path: "new/file-1", seed: 0x111},
				{path: "new/subdir/file-1", seed: 0x222},
				{path: "new/subdir/file-2", seed: 0x333},
				{path: "new/subdir/subdir/file-4", seed: 0x444},
			},
		},
	})

	runPatchingScenario(t, patchScenario{
		name: "move 4 files into a subdirectory",
		v1: testDirSettings{
			entries: []testDirEntry{
				{path: "old/file-1", seed: 0x1},
				{path: "old/subdir/file-1", seed: 0x2},
				{path: "old/subdir/file-2", seed: 0x3},
				{path: "old/subdir/subdir/file-4", seed: 0x4},
			},
		},
		intermediate: &testDirSettings{
			entries: []testDirEntry{
				{path: "old/subdir/file-1-generated", seed: 0x999, size: pwr.BlockSize * 3},
			},
		},
		v2: testDirSettings{
			entries: []testDirEntry{
				{path: "old/new/file-1", seed: 0x1},
				{path: "old/new/subdir/file-1", seed: 0x2},
				{path: "old/new/subdir/file-2", seed: 0x3},
				{path: "old/new/subdir/subdir/file-4", seed: 0x4},
			},
		},
	})

	runPatchingScenario(t, patchScenario{
		name: "one file is duplicated twice",
		v1: testDirSettings{
			entries: []testDirEntry{
				{path: "dir1/file-1", seed: 0x1},
			},
		},
		v2: testDirSettings{
			entries: []testDirEntry{
				{path: "dir1/file-1", seed: 0x1},
				{path: "dir2/file-1", seed: 0x1},
				{path: "dir2/file-1bis", seed: 0x1},
			},
		},
	})

	runPatchingScenario(t, patchScenario{
		name: "one file is renamed + duplicated twice",
		v1: testDirSettings{
			entries: []testDirEntry{
				{path: "dir1/file-1", seed: 0x1},
			},
		},
		v2: testDirSettings{
			entries: []testDirEntry{
				{path: "dir2/file-1", seed: 0x1},
				{path: "dir3/file-1", seed: 0x1},
				{path: "dir3/file-1bis", seed: 0x1},
			},
		},
	})

	runPatchingScenario(t, patchScenario{
		name: "four large unchanged",
		v1: testDirSettings{
			entries: []testDirEntry{
				{path: "subdir/file-1", seed: 0x11, size: pwr.BlockSize*largeAmount + 17},
				{path: "subdir/file-2", seed: 0x22, size: pwr.BlockSize*largeAmount + 17},
				{path: "subdir/file-3", seed: 0x33, size: pwr.BlockSize*largeAmount + 17},
				{path: "subdir/file-4", seed: 0x44, size: pwr.BlockSize*largeAmount + 17},
			},
		},
		corruptions: &testDirSettings{
			entries: []testDirEntry{
				{path: "subdir/file-1", seed: 0x99, size: pwr.BlockSize*largeAmount + 17},
			},
		},
		v2: testDirSettings{
			entries: []testDirEntry{
				{path: "subdir/file-1", seed: 0x11, size: pwr.BlockSize*largeAmount + 17},
				{path: "subdir/file-2", seed: 0x22, size: pwr.BlockSize*largeAmount + 17},
				{path: "subdir/file-3", seed: 0x33, size: pwr.BlockSize*largeAmount + 17},
				{path: "subdir/file-4", seed: 0x44, size: pwr.BlockSize*largeAmount + 17},
			},
		},
	})

	runPatchingScenario(t, patchScenario{
		name: "four large, two swap",
		v1: testDirSettings{
			entries: []testDirEntry{
				{path: "subdir/file-1", seed: 0x11, size: pwr.BlockSize*largeAmount + 17},
				{path: "subdir/file-2", seed: 0x22, size: pwr.BlockSize*largeAmount + 17},
				{path: "subdir/file-3", seed: 0x33, size: pwr.BlockSize*largeAmount + 17},
				{path: "subdir/file-4", seed: 0x44, size: pwr.BlockSize*largeAmount + 17},
			},
		},
		corruptions: &testDirSettings{
			entries: []testDirEntry{
				{path: "subdir/file-1", seed: 0x99, size: pwr.BlockSize*largeAmount + 17},
			},
		},
		v2: testDirSettings{
			entries: []testDirEntry{
				{path: "subdir/file-1", seed: 0x11, size: pwr.BlockSize*largeAmount + 17},
				{path: "subdir/file-2", seed: 0x22, size: pwr.BlockSize*largeAmount + 17},
				{path: "subdir/file-3", seed: 0x44, size: pwr.BlockSize*largeAmount + 17},
				{path: "subdir/file-4", seed: 0x33, size: pwr.BlockSize*largeAmount + 17},
			},
		},
	})

	runPatchingScenario(t, patchScenario{
		name: "four large, two swap + duplicate (option A)",
		v1: testDirSettings{
			entries: []testDirEntry{
				{path: "subdir/file-1", seed: 0x11, size: pwr.BlockSize*largeAmount + 17},
				{path: "subdir/file-2", seed: 0x22, size: pwr.BlockSize*largeAmount + 17},
				{path: "subdir/file-3", seed: 0x33, size: pwr.BlockSize*largeAmount + 17},
				{path: "subdir/file-4", seed: 0x44, size: pwr.BlockSize*largeAmount + 17},
			},
		},
		corruptions: &testDirSettings{
			entries: []testDirEntry{
				{path: "subdir/file-1", seed: 0x99, size: pwr.BlockSize*largeAmount + 17},
			},
		},
		v2: testDirSettings{
			entries: []testDirEntry{
				{path: "subdir/file-1", seed: 0x22, size: pwr.BlockSize*largeAmount + 17},
				{path: "subdir/file-2", seed: 0x11, size: pwr.BlockSize*largeAmount + 17},
				{path: "subdir/file-3", seed: 0x22, size: pwr.BlockSize*largeAmount + 17},
				{path: "subdir/file-4", seed: 0x22, size: pwr.BlockSize*largeAmount + 17},
			},
		},
	})

	runPatchingScenario(t, patchScenario{
		name: "four large, two swap + duplicate (option B)",
		v1: testDirSettings{
			entries: []testDirEntry{
				{path: "subdir/file-1", seed: 0x11, size: pwr.BlockSize*largeAmount + 17},
				{path: "subdir/file-2", seed: 0x22, size: pwr.BlockSize*largeAmount + 17},
				{path: "subdir/file-3", seed: 0x33, size: pwr.BlockSize*largeAmount + 17},
				{path: "subdir/file-4", seed: 0x44, size: pwr.BlockSize*largeAmount + 17},
			},
		},
		corruptions: &testDirSettings{
			entries: []testDirEntry{
				{path: "subdir/file-1", seed: 0x99, size: pwr.BlockSize*largeAmount + 17},
			},
		},
		v2: testDirSettings{
			entries: []testDirEntry{
				{path: "subdir/file-1", seed: 0x22, size: pwr.BlockSize*largeAmount + 17},
				{path: "subdir/file-2", seed: 0x11, size: pwr.BlockSize*largeAmount + 17},
				{path: "subdir/file-3", seed: 0x11, size: pwr.BlockSize*largeAmount + 17},
				{path: "subdir/file-4", seed: 0x11, size: pwr.BlockSize*largeAmount + 17},
			},
		},
	})

	if testSymlinks {
		runPatchingScenario(t, patchScenario{
			name: "symlinks are added by patch",
			v1: testDirSettings{
				entries: []testDirEntry{
					{path: "dir1/file", seed: 0x1},
				},
			},
			v2: testDirSettings{
				entries: []testDirEntry{
					{path: "dir1/file", seed: 0x1},
					{path: "dir1/link", dest: "file"},
				},
			},
		})
	}

	if testSymlinks {
		runPatchingScenario(t, patchScenario{
			name: "symlinks are changed by patch",
			v1: testDirSettings{
				entries: []testDirEntry{
					{path: "dir1/file1", seed: 0x1},
					{path: "dir1/file2", seed: 0x2},
					{path: "dir1/link", dest: "file1"},
				},
			},
			v2: testDirSettings{
				entries: []testDirEntry{
					{path: "dir1/file1", seed: 0x1},
					{path: "dir1/file2", seed: 0x2},
					{path: "dir1/link", dest: "file2"},
				},
			},
		})
	}

	if testSymlinks {
		runPatchingScenario(t, patchScenario{
			name: "symlinks are removed by patch",
			v1: testDirSettings{
				entries: []testDirEntry{
					{path: "dir1/file", seed: 0x1},
					{path: "dir1/link", dest: "file"},
				},
			},
			v2: testDirSettings{
				entries: []testDirEntry{
					{path: "dir1/file", seed: 0x1},
				},
			},
		})
	}

	if testSymlinks {
		runPatchingScenario(t, patchScenario{
			name: "symlink becomes normal file (itchio/itch#2315)",
			v1: testDirSettings{
				entries: []testDirEntry{
					{path: "test2.txt", seed: 0x1},
					{path: "test.txt", dest: "test2.txt"},
				},
			},
			v2: testDirSettings{
				entries: []testDirEntry{
					{path: "test2.txt", seed: 0x1},
					{path: "test.txt", seed: 0x2},
				},
			},
		})
	}

	if testSymlinks {
		runPatchingScenario(t, patchScenario{
			name: "symlink becomes directory",
			v1: testDirSettings{
				entries: []testDirEntry{
					{path: "test2.txt", seed: 0x1},
					{path: "test.txt", dest: "test2.txt"},
				},
			},
			v2: testDirSettings{
				entries: []testDirEntry{
					{path: "test2.txt", seed: 0x1},
					{path: "test.txt/woop", seed: 0x2},
				},
			},
		})
	}

	if testSymlinks {
		runPatchingScenario(t, patchScenario{
			name: "symlink becomes dangling",
			v1: testDirSettings{
				entries: []testDirEntry{
					{path: "test2.txt", seed: 0x1},
					{path: "test.txt", dest: "test2.txt"},
				},
			},
			v2: testDirSettings{
				entries: []testDirEntry{
					{path: "test.txt", dest: "test2.txt"},
				},
			},
		})
	}
}

func runPatchingScenario(t *testing.T, scenario patchScenario) {
	forwardScenario := *&scenario
	forwardScenario.name += "/forward"
	runSinglePatchingScenario(t, forwardScenario)

	backScenario := *&scenario
	backScenario.name += "/back"
	backScenario.v1, backScenario.v2 = backScenario.v2, backScenario.v1
	runSinglePatchingScenario(t, backScenario)
}

func runSinglePatchingScenario(t *testing.T, scenario patchScenario) {
	t.Run(scenario.name, func(t *testing.T) {
		log := t.Logf

		mainDir, err := ioutil.TempDir("", "patch-cycle")
		wtest.Must(t, err)
		defer os.RemoveAll(mainDir)

		v1 := filepath.Join(mainDir, "v1")
		makeTestDir(t, v1, scenario.v1)

		v2 := filepath.Join(mainDir, "v2")
		makeTestDir(t, v2, scenario.v2)

		v1zip := filepath.Join(mainDir, "v1.zip")
		v2zip := filepath.Join(mainDir, "v2.zip")

{'fraction_non_alphanumeric': 0.17850818162499116, 'fraction_numerical': 0.047354253736629597, 'mean_word_length': 3.5850925625202987, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 3, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7531704', 'n_tokens_mistral': 13848, 'n_tokens_neox': 11355, 'n_words': 2258}