From 82c92853152457b1133fba44b0e88cb4a4dfb1c3 Mon Sep 17 00:00:00 2001 From: Alex Younger Date: Sat, 9 May 2026 20:16:51 -0500 Subject: [PATCH 1/2] test: add tests for new file search replace command This change migrates tests from the php-search-replace repository for testing various file edge-cases. Using actual sql files proved a necessary step that prevents running into oddities with how php handles strings. There may be a better way around that problem, but I never found it, and it seems a better test to use actual sql files since that is the exact behavior of the command. --- tests/FileSearchReplacerTest.php | 327 ++++++++++++++++++ .../serialized/different-lengths.expected.sql | 1 + .../serialized/different-lengths.input.sql | 1 + .../serialized/double-encoded.expected.sql | 1 + .../serialized/double-encoded.input.sql | 1 + .../serialized/emoji-from.expected.sql | 1 + .../Fixtures/serialized/emoji-from.input.sql | 1 + .../Fixtures/serialized/emoji-to.expected.sql | 1 + tests/Fixtures/serialized/emoji-to.input.sql | 1 + .../escaped-delimiters.expected.sql | 1 + .../serialized/escaped-delimiters.input.sql | 1 + .../serialized/heavy-escaping.expected.sql | 1 + .../serialized/heavy-escaping.input.sql | 1 + .../serialized/http-to-https.expected.sql | 1 + .../serialized/http-to-https.input.sql | 1 + .../long-different-lengths.expected.sql | 1 + .../long-different-lengths.input.sql | 1 + .../multiple-occurrences.expected.sql | 1 + .../serialized/multiple-occurrences.input.sql | 1 + .../mydumper-delimiters.expected.sql | 1 + .../serialized/mydumper-delimiters.input.sql | 1 + .../non-serialized-mixed.expected.sql | 1 + .../serialized/non-serialized-mixed.input.sql | 1 + .../serialized/null-bytes.expected.sql | 1 + .../Fixtures/serialized/null-bytes.input.sql | 1 + .../overlapping-non-serialized.expected.sql | 1 + .../overlapping-non-serialized.input.sql | 1 + .../serialized/serialized-css.expected.sql | 1 + .../serialized/serialized-css.input.sql | 1 + .../serialized/skip-updated.expected.sql | 1 + .../serialized/skip-updated.input.sql | 1 + 31 files changed, 357 insertions(+) create mode 100644 tests/FileSearchReplacerTest.php create mode 100644 tests/Fixtures/serialized/different-lengths.expected.sql create mode 100644 tests/Fixtures/serialized/different-lengths.input.sql create mode 100644 tests/Fixtures/serialized/double-encoded.expected.sql create mode 100644 tests/Fixtures/serialized/double-encoded.input.sql create mode 100644 tests/Fixtures/serialized/emoji-from.expected.sql create mode 100644 tests/Fixtures/serialized/emoji-from.input.sql create mode 100644 tests/Fixtures/serialized/emoji-to.expected.sql create mode 100644 tests/Fixtures/serialized/emoji-to.input.sql create mode 100644 tests/Fixtures/serialized/escaped-delimiters.expected.sql create mode 100644 tests/Fixtures/serialized/escaped-delimiters.input.sql create mode 100644 tests/Fixtures/serialized/heavy-escaping.expected.sql create mode 100644 tests/Fixtures/serialized/heavy-escaping.input.sql create mode 100644 tests/Fixtures/serialized/http-to-https.expected.sql create mode 100644 tests/Fixtures/serialized/http-to-https.input.sql create mode 100644 tests/Fixtures/serialized/long-different-lengths.expected.sql create mode 100644 tests/Fixtures/serialized/long-different-lengths.input.sql create mode 100644 tests/Fixtures/serialized/multiple-occurrences.expected.sql create mode 100644 tests/Fixtures/serialized/multiple-occurrences.input.sql create mode 100644 tests/Fixtures/serialized/mydumper-delimiters.expected.sql create mode 100644 tests/Fixtures/serialized/mydumper-delimiters.input.sql create mode 100644 tests/Fixtures/serialized/non-serialized-mixed.expected.sql create mode 100644 tests/Fixtures/serialized/non-serialized-mixed.input.sql create mode 100644 tests/Fixtures/serialized/null-bytes.expected.sql create mode 100644 tests/Fixtures/serialized/null-bytes.input.sql create mode 100644 tests/Fixtures/serialized/overlapping-non-serialized.expected.sql create mode 100644 tests/Fixtures/serialized/overlapping-non-serialized.input.sql create mode 100644 tests/Fixtures/serialized/serialized-css.expected.sql create mode 100644 tests/Fixtures/serialized/serialized-css.input.sql create mode 100644 tests/Fixtures/serialized/skip-updated.expected.sql create mode 100644 tests/Fixtures/serialized/skip-updated.input.sql diff --git a/tests/FileSearchReplacerTest.php b/tests/FileSearchReplacerTest.php new file mode 100644 index 00000000..3ae01c3b --- /dev/null +++ b/tests/FileSearchReplacerTest.php @@ -0,0 +1,327 @@ +replacer = new FileSearchReplacer(); + } + + /** + * @param array $replacements + */ + #[DataProvider( 'provideSerializedFixtures' )] + public function testProcessLineMatchesGoImplementation( + string $inputFixture, + string $expectedFixture, + array $replacements, + ): void { + $input = file_get_contents( $inputFixture ); + $expected = file_get_contents( $expectedFixture ); + + self::assertNotFalse( $input ); + self::assertNotFalse( $expected ); + + self::assertSame( $expected, $this->replacer->process_line( $input, $replacements ) ); + } + + /** + * @return array}> + */ + public static function provideSerializedFixtures(): array { + $base = __DIR__ . '/Fixtures/serialized'; + $doubleEncodedFrom = <<<'TXT' + http:\\/\\/example\\.com + TXT; + $doubleEncodedTo = <<<'TXT' + http:\\/\\/example2\\.com + TXT; + $doubleEncodedSerializedFrom = <<<'TXT' + \\s=\\shttp_get\(\'http:\\/\\/example\\.com + TXT; + $doubleEncodedSerializedTo = <<<'TXT' + \\s=\\shttp_get\(\'http:\\/\\/example2\\.com + TXT; + $heavyEscapingFrom = <<<'TXT' + \\c\\d\\e + TXT; + $heavyEscapingTo = <<<'TXT' + \\x + TXT; + + return [ + 'http to https' => [ + $base . '/http-to-https.input.sql', + $base . '/http-to-https.expected.sql', + [ + [ + 'from' => 'http://automattic.com', + 'to' => 'https://automattic.com', + ], + ], + ], + 'multiple occurrences on line' => [ + $base . '/multiple-occurrences.input.sql', + $base . '/multiple-occurrences.expected.sql', + [ + [ + 'from' => 'http://automattic.com', + 'to' => 'https://automattic.com', + ], + ], + ], + 'skip already replaced value' => [ + $base . '/skip-updated.input.sql', + $base . '/skip-updated.expected.sql', + [ + [ + 'from' => 'http://automattic.com', + 'to' => 'https://automattic.com', + ], + ], + ], + 'emoji from' => [ + $base . '/emoji-from.input.sql', + $base . '/emoji-from.expected.sql', + [ + [ + 'from' => 'http://🖖.com', + 'to' => 'https://spock.com', + ], + ], + ], + 'emoji to' => [ + $base . '/emoji-to.input.sql', + $base . '/emoji-to.expected.sql', + [ + [ + 'from' => 'https://spock.com', + 'to' => 'http://🖖.com', + ], + ], + ], + 'null characters' => [ + $base . '/null-bytes.input.sql', + $base . '/null-bytes.expected.sql', + [ + [ + 'from' => 'EnvironmentObject', + 'to' => 'Yeehaw', + ], + ], + ], + 'different lengths' => [ + $base . '/different-lengths.input.sql', + $base . '/different-lengths.expected.sql', + [ + [ + 'from' => 'hello', + 'to' => 'goodbye', + ], + ], + ], + 'longer replacements' => [ + $base . '/long-different-lengths.input.sql', + $base . '/long-different-lengths.expected.sql', + [ + [ + 'from' => 'bbbbbbbbbb', + 'to' => 'ccccccccccccccc', + ], + ], + ], + 'serialized css' => [ + $base . '/serialized-css.input.sql', + $base . '/serialized-css.expected.sql', + [ + [ + 'from' => 'https://uss-enterprise.com', + 'to' => 'https://ncc-1701-d.space', + ], + ], + ], + 'double encoded string' => [ + $base . '/double-encoded.input.sql', + $base . '/double-encoded.expected.sql', + [ + [ + 'from' => $doubleEncodedFrom, + 'to' => $doubleEncodedTo, + ], + ], + ], + 'non serialized section with serialized data' => [ + $base . '/non-serialized-mixed.input.sql', + $base . '/non-serialized-mixed.expected.sql', + [ + [ + 'from' => 'example', + 'to' => 'example2', + ], + [ + 'from' => $doubleEncodedFrom, + 'to' => $doubleEncodedTo, + ], + ], + ], + 'heavy escaping' => [ + $base . '/heavy-escaping.input.sql', + $base . '/heavy-escaping.expected.sql', + [ + [ + 'from' => $heavyEscapingFrom, + 'to' => $heavyEscapingTo, + ], + ], + ], + 'escaped delimiters' => [ + $base . '/escaped-delimiters.input.sql', + $base . '/escaped-delimiters.expected.sql', + [ + [ + 'from' => 'hello', + 'to' => 'helloworld', + ], + ], + ], + 'mydumper delimiters' => [ + $base . '/mydumper-delimiters.input.sql', + $base . '/mydumper-delimiters.expected.sql', + [ + [ + 'from' => 'hello', + 'to' => 'helloworld', + ], + ], + ], + 'overlapping replacements without serialization' => [ + $base . '/overlapping-non-serialized.input.sql', + $base . '/overlapping-non-serialized.expected.sql', + [ + [ + 'from' => 'http:', + 'to' => 'https:', + ], + [ + 'from' => '//automattic.com', + 'to' => '//automattic.org', + ], + ], + ], + ]; + } + + public function testProcessLineWithEmptyReplacementsReturnsOriginal(): void { + $line = 'plain text line'; + self::assertSame( $line, $this->replacer->process_line( $line, [] ) ); + } + + public function testProcessLineRejectsInvalidReplacements(): void { + $this->expectException( RuntimeException::class ); + $this->replacer->process_line( 'anything', [ [ 'from' => 'only-from' ] ] ); + } + + public function testReplaceInFileProcessesEntireContents(): void { + $input = tempnam( sys_get_temp_dir(), 'sql-src-' ); + $output = tempnam( sys_get_temp_dir(), 'sql-out-' ); + + self::assertNotFalse( $input ); + self::assertNotFalse( $output ); + + $fixture = <<<'SQL' + s:21:\"http://automattic.com\"; + http://example.com + + SQL; + + file_put_contents( $input, $fixture ); + + $this->replacer->replace_in_file( + $input, + $output, + [ + [ + 'from' => 'http://automattic.com', + 'to' => 'https://automattic.com', + ], + [ + 'from' => 'http://example.com', + 'to' => 'https://example.com', + ], + ] + ); + + $result = file_get_contents( $output ); + + $expected = <<<'SQL' + s:22:\"https://automattic.com\"; + https://example.com + + SQL; + + self::assertSame( $expected, $result ); + + @unlink( $input ); + @unlink( $output ); + } + + public function testFixturesMatchGoBinaryOutput(): void { + $input = $this->extractSqlFixture( __DIR__ . '/Fixtures/wpbreakstufflocalhost.sql.zip' ); + $expected = $this->extractSqlFixture( __DIR__ . '/Fixtures/wpbreakstufflol.sql.zip' ); + + $output = tempnam( sys_get_temp_dir(), 'sql-fixture-' ); + self::assertNotFalse( $output ); + + try { + $this->replacer->replace_in_file( + $input, + $output, + [ + [ + 'from' => 'wp.breakstuff.localhost', + 'to' => 'wp.breakstuff.lol', + ], + ] + ); + + self::assertFileEquals( $expected, $output ); + } finally { + @unlink( $input ); + @unlink( $expected ); + @unlink( $output ); + } + } + + private function extractSqlFixture( string $zip_path ): string { + if ( ! is_file( $zip_path ) ) { + throw new RuntimeException( sprintf( 'Fixture "%s" does not exist.', $zip_path ) ); + } + + $zip = new \ZipArchive(); + if ( $zip->open( $zip_path ) !== true ) { + throw new RuntimeException( sprintf( 'Unable to open fixture "%s".', $zip_path ) ); + } + + $contents = $zip->getFromIndex( 0 ); + $zip->close(); + + if ( false === $contents ) { + throw new RuntimeException( sprintf( 'Unable to read contents of fixture "%s".', $zip_path ) ); + } + + $temp_path = tempnam( sys_get_temp_dir(), 'sql-fixture-' ); + if ( false === $temp_path ) { + throw new RuntimeException( 'Unable to create temporary fixture file.' ); + } + + file_put_contents( $temp_path, $contents ); + + return $temp_path; + } +} diff --git a/tests/Fixtures/serialized/different-lengths.expected.sql b/tests/Fixtures/serialized/different-lengths.expected.sql new file mode 100644 index 00000000..95555792 --- /dev/null +++ b/tests/Fixtures/serialized/different-lengths.expected.sql @@ -0,0 +1 @@ +s:13:\"goodbye-world\"; diff --git a/tests/Fixtures/serialized/different-lengths.input.sql b/tests/Fixtures/serialized/different-lengths.input.sql new file mode 100644 index 00000000..ccd4e6ce --- /dev/null +++ b/tests/Fixtures/serialized/different-lengths.input.sql @@ -0,0 +1 @@ +s:11:\"hello-world\"; diff --git a/tests/Fixtures/serialized/double-encoded.expected.sql b/tests/Fixtures/serialized/double-encoded.expected.sql new file mode 100644 index 00000000..b800d219 --- /dev/null +++ b/tests/Fixtures/serialized/double-encoded.expected.sql @@ -0,0 +1 @@ +s:38:\"\\s=\\shttp_get\\(\'http:\\/\\/example2\\.com\"; diff --git a/tests/Fixtures/serialized/double-encoded.input.sql b/tests/Fixtures/serialized/double-encoded.input.sql new file mode 100644 index 00000000..413cfd97 --- /dev/null +++ b/tests/Fixtures/serialized/double-encoded.input.sql @@ -0,0 +1 @@ +s:37:\"\\s=\\shttp_get\\(\'http:\\/\\/example\\.com\"; diff --git a/tests/Fixtures/serialized/emoji-from.expected.sql b/tests/Fixtures/serialized/emoji-from.expected.sql new file mode 100644 index 00000000..0ec528f1 --- /dev/null +++ b/tests/Fixtures/serialized/emoji-from.expected.sql @@ -0,0 +1 @@ +s:17:\"https://spock.com\"; diff --git a/tests/Fixtures/serialized/emoji-from.input.sql b/tests/Fixtures/serialized/emoji-from.input.sql new file mode 100644 index 00000000..d5069b15 --- /dev/null +++ b/tests/Fixtures/serialized/emoji-from.input.sql @@ -0,0 +1 @@ +s:15:\"http://🖖.com\"; diff --git a/tests/Fixtures/serialized/emoji-to.expected.sql b/tests/Fixtures/serialized/emoji-to.expected.sql new file mode 100644 index 00000000..d5069b15 --- /dev/null +++ b/tests/Fixtures/serialized/emoji-to.expected.sql @@ -0,0 +1 @@ +s:15:\"http://🖖.com\"; diff --git a/tests/Fixtures/serialized/emoji-to.input.sql b/tests/Fixtures/serialized/emoji-to.input.sql new file mode 100644 index 00000000..0ec528f1 --- /dev/null +++ b/tests/Fixtures/serialized/emoji-to.input.sql @@ -0,0 +1 @@ +s:17:\"https://spock.com\"; diff --git a/tests/Fixtures/serialized/escaped-delimiters.expected.sql b/tests/Fixtures/serialized/escaped-delimiters.expected.sql new file mode 100644 index 00000000..815b7c98 --- /dev/null +++ b/tests/Fixtures/serialized/escaped-delimiters.expected.sql @@ -0,0 +1 @@ +('s:39:\"\";\";\";\";\";\\\";\\\";\\\"; helloworld \\\\\";\\\\\";\";') diff --git a/tests/Fixtures/serialized/escaped-delimiters.input.sql b/tests/Fixtures/serialized/escaped-delimiters.input.sql new file mode 100644 index 00000000..b7a806a3 --- /dev/null +++ b/tests/Fixtures/serialized/escaped-delimiters.input.sql @@ -0,0 +1 @@ +('s:34:\"\";\";\";\";\";\\\";\\\";\\\"; hello \\\\\";\\\\\";\";') diff --git a/tests/Fixtures/serialized/heavy-escaping.expected.sql b/tests/Fixtures/serialized/heavy-escaping.expected.sql new file mode 100644 index 00000000..4ff835d9 --- /dev/null +++ b/tests/Fixtures/serialized/heavy-escaping.expected.sql @@ -0,0 +1 @@ +s:14:\"\\a\\b\\x\\f\\g\\h\";\"; diff --git a/tests/Fixtures/serialized/heavy-escaping.input.sql b/tests/Fixtures/serialized/heavy-escaping.input.sql new file mode 100644 index 00000000..3f006e6b --- /dev/null +++ b/tests/Fixtures/serialized/heavy-escaping.input.sql @@ -0,0 +1 @@ +s:18:\"\\a\\b\\c\\d\\e\\f\\g\\h\";\"; diff --git a/tests/Fixtures/serialized/http-to-https.expected.sql b/tests/Fixtures/serialized/http-to-https.expected.sql new file mode 100644 index 00000000..0ef2d229 --- /dev/null +++ b/tests/Fixtures/serialized/http-to-https.expected.sql @@ -0,0 +1 @@ +s:22:\"https://automattic.com\"; diff --git a/tests/Fixtures/serialized/http-to-https.input.sql b/tests/Fixtures/serialized/http-to-https.input.sql new file mode 100644 index 00000000..f6bb5dd0 --- /dev/null +++ b/tests/Fixtures/serialized/http-to-https.input.sql @@ -0,0 +1 @@ +s:21:\"http://automattic.com\"; diff --git a/tests/Fixtures/serialized/long-different-lengths.expected.sql b/tests/Fixtures/serialized/long-different-lengths.expected.sql new file mode 100644 index 00000000..1d656cd5 --- /dev/null +++ b/tests/Fixtures/serialized/long-different-lengths.expected.sql @@ -0,0 +1 @@ +s:25:\"aaaaacccccccccccccccaaaaa\"; diff --git a/tests/Fixtures/serialized/long-different-lengths.input.sql b/tests/Fixtures/serialized/long-different-lengths.input.sql new file mode 100644 index 00000000..1fb6aadf --- /dev/null +++ b/tests/Fixtures/serialized/long-different-lengths.input.sql @@ -0,0 +1 @@ +s:20:\"aaaaabbbbbbbbbbaaaaa\"; diff --git a/tests/Fixtures/serialized/multiple-occurrences.expected.sql b/tests/Fixtures/serialized/multiple-occurrences.expected.sql new file mode 100644 index 00000000..81ad5d72 --- /dev/null +++ b/tests/Fixtures/serialized/multiple-occurrences.expected.sql @@ -0,0 +1 @@ +('s:22:\"https://automattic.com\";'),('s:22:\"https://automattic.com\";') diff --git a/tests/Fixtures/serialized/multiple-occurrences.input.sql b/tests/Fixtures/serialized/multiple-occurrences.input.sql new file mode 100644 index 00000000..8653e697 --- /dev/null +++ b/tests/Fixtures/serialized/multiple-occurrences.input.sql @@ -0,0 +1 @@ +('s:21:\"http://automattic.com\";'),('s:21:\"http://automattic.com\";') diff --git a/tests/Fixtures/serialized/mydumper-delimiters.expected.sql b/tests/Fixtures/serialized/mydumper-delimiters.expected.sql new file mode 100644 index 00000000..c6f5301d --- /dev/null +++ b/tests/Fixtures/serialized/mydumper-delimiters.expected.sql @@ -0,0 +1 @@ +("s:39:\"\";\";\";\";\";\\\";\\\";\\\"; helloworld \\\\\";\\\\\";\";") diff --git a/tests/Fixtures/serialized/mydumper-delimiters.input.sql b/tests/Fixtures/serialized/mydumper-delimiters.input.sql new file mode 100644 index 00000000..84be1010 --- /dev/null +++ b/tests/Fixtures/serialized/mydumper-delimiters.input.sql @@ -0,0 +1 @@ +("s:34:\"\";\";\";\";\";\\\";\\\";\\\"; hello \\\\\";\\\\\";\";") diff --git a/tests/Fixtures/serialized/non-serialized-mixed.expected.sql b/tests/Fixtures/serialized/non-serialized-mixed.expected.sql new file mode 100644 index 00000000..87c34b8e --- /dev/null +++ b/tests/Fixtures/serialized/non-serialized-mixed.expected.sql @@ -0,0 +1 @@ +('example2'),('s:38:\"\\s=\\shttp_get\\(\'http:\\/\\/example2\\.com\";') diff --git a/tests/Fixtures/serialized/non-serialized-mixed.input.sql b/tests/Fixtures/serialized/non-serialized-mixed.input.sql new file mode 100644 index 00000000..ec1f0de8 --- /dev/null +++ b/tests/Fixtures/serialized/non-serialized-mixed.input.sql @@ -0,0 +1 @@ +('example'),('s:37:\"\\s=\\shttp_get\\(\'http:\\/\\/example\\.com\";') diff --git a/tests/Fixtures/serialized/null-bytes.expected.sql b/tests/Fixtures/serialized/null-bytes.expected.sql new file mode 100644 index 00000000..c77f2654 --- /dev/null +++ b/tests/Fixtures/serialized/null-bytes.expected.sql @@ -0,0 +1 @@ +s:19:\"\0Yeehaw\0wp_site_url\"; diff --git a/tests/Fixtures/serialized/null-bytes.input.sql b/tests/Fixtures/serialized/null-bytes.input.sql new file mode 100644 index 00000000..1dde485e --- /dev/null +++ b/tests/Fixtures/serialized/null-bytes.input.sql @@ -0,0 +1 @@ +s:30:\"\0EnvironmentObject\0wp_site_url\"; diff --git a/tests/Fixtures/serialized/overlapping-non-serialized.expected.sql b/tests/Fixtures/serialized/overlapping-non-serialized.expected.sql new file mode 100644 index 00000000..e7bfe666 --- /dev/null +++ b/tests/Fixtures/serialized/overlapping-non-serialized.expected.sql @@ -0,0 +1 @@ +https://automattic.org diff --git a/tests/Fixtures/serialized/overlapping-non-serialized.input.sql b/tests/Fixtures/serialized/overlapping-non-serialized.input.sql new file mode 100644 index 00000000..e7b50e5a --- /dev/null +++ b/tests/Fixtures/serialized/overlapping-non-serialized.input.sql @@ -0,0 +1 @@ +http://automattic.com diff --git a/tests/Fixtures/serialized/serialized-css.expected.sql b/tests/Fixtures/serialized/serialized-css.expected.sql new file mode 100644 index 00000000..23577912 --- /dev/null +++ b/tests/Fixtures/serialized/serialized-css.expected.sql @@ -0,0 +1 @@ +a:2:{s:3:\"key\";s:5:\"value\";s:3:\"css\";s:206:\"body { color: #123456;\r\nborder-bottom: none; }\r\ndiv.bg { background: url('https://ncc-1701-d.space/wp-content/uploads/main-bg.gif');\r\n background-position: left center;\r\n background-repeat: no-repeat; }\";} diff --git a/tests/Fixtures/serialized/serialized-css.input.sql b/tests/Fixtures/serialized/serialized-css.input.sql new file mode 100644 index 00000000..e4c6c55c --- /dev/null +++ b/tests/Fixtures/serialized/serialized-css.input.sql @@ -0,0 +1 @@ +a:2:{s:3:\"key\";s:5:\"value\";s:3:\"css\";s:208:\"body { color: #123456;\r\nborder-bottom: none; }\r\ndiv.bg { background: url('https://uss-enterprise.com/wp-content/uploads/main-bg.gif');\r\n background-position: left center;\r\n background-repeat: no-repeat; }\";} diff --git a/tests/Fixtures/serialized/skip-updated.expected.sql b/tests/Fixtures/serialized/skip-updated.expected.sql new file mode 100644 index 00000000..22f917d7 --- /dev/null +++ b/tests/Fixtures/serialized/skip-updated.expected.sql @@ -0,0 +1 @@ +('s:22:\"https://automattic.com\";'),('s:21:\"https://a8c.com\";') diff --git a/tests/Fixtures/serialized/skip-updated.input.sql b/tests/Fixtures/serialized/skip-updated.input.sql new file mode 100644 index 00000000..fc57c39c --- /dev/null +++ b/tests/Fixtures/serialized/skip-updated.input.sql @@ -0,0 +1 @@ +('s:21:\"http://automattic.com\";'),('s:21:\"https://a8c.com\";') From dedb2051776eb5629222b16e28ce1fe29d8844a0 Mon Sep 17 00:00:00 2001 From: Alex Younger Date: Sat, 9 May 2026 20:19:39 -0500 Subject: [PATCH 2/2] feat: add `file` subcommand for SQL file processing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces a new `file` subcommand that performs search/replace directly on SQL dump files (or streams) using a WP-CLI-compliant port of Automattic's go-search-replace algorithm. This complements the existing database-centric `search-replace` command by providing a text-level engine that correctly handles serialized PHP strings and updates their length markers — including when the search string appears as an array key. Usage: wp search-replace file [ []] wp search-replace file --old= --new= input.sql output.sql wp search-replace file old new --in-place dump.sql cat dump.sql | wp search-replace file old new - - Supported flags: --old, --new Alternative to positional arguments (for strings starting with '--') --in-place Edit the input file in place --dry-run Preview changes without writing output --verbose Show per-line processing information The implementation follows existing project conventions: - `FileSearchReplacer` and `Serialized_Replace_Result` live under the `WP_CLI` namespace with proper PHPCS exclusions in phpcs.xml.dist - All error handling uses exceptions (CLI layer converts to WP_CLI::error) - Full `composer test` passes (production code is zero-warning) - 19 unit tests pass, including large fixture parity tests against the original go-search-replace binary Refs: https://github.com/AlextheYounga/php-search-replace https://github.com/Automattic/go-search-replace --- phpcs.xml.dist | 8 + search-replace-command.php | 1 + src/Search_Replace_File_Command.php | 271 ++++++++++++++++++ src/WP_CLI/FileSearchReplacer.php | 337 +++++++++++++++++++++++ src/WP_CLI/Serialized_Replace_Result.php | 41 +++ 5 files changed, 658 insertions(+) create mode 100644 src/Search_Replace_File_Command.php create mode 100644 src/WP_CLI/FileSearchReplacer.php create mode 100644 src/WP_CLI/Serialized_Replace_Result.php diff --git a/phpcs.xml.dist b/phpcs.xml.dist index c58a9fd8..3866aa40 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -12,6 +12,7 @@ . + tests/FileSearchReplacerTest\.php @@ -60,9 +61,16 @@ */src/Search_Replace_Command\.php$ + */src/Search_Replace_File_Command\.php$ + + + */tests/FileSearchReplacerTest\.php$ + */src/WP_CLI/SearchReplacer\.php$ + */src/WP_CLI/FileSearchReplacer\.php$ + */src/WP_CLI/Serialized_Replace_Result\.php$