toBeFalse();
});
it('hashes an existing file', function () {
$path = tempnam(sys_get_temp_dir(), 'pest_').'.php';
file_put_contents($path, "toBeString()->not->toBeEmpty();
} finally {
@unlink($path);
}
});
});
describe('PHP files', function () {
it('produces the same hash regardless of whitespace differences', function () {
$a = ContentHash::ofContent('a.php', "toBe($b);
});
it('ignores single-line comments', function () {
$a = ContentHash::ofContent('a.php', "toBe($b);
});
it('ignores hash-style comments', function () {
$a = ContentHash::ofContent('a.php', "toBe($b);
});
it('ignores multi-line comments', function () {
$a = ContentHash::ofContent('a.php', "toBe($b);
});
it('ignores doc comments', function () {
$a = ContentHash::ofContent('a.php', "toBe($b);
});
it('detects code changes', function () {
$a = ContentHash::ofContent('a.php', 'not->toBe($b);
});
it('preserves whitespace inside string literals', function () {
$a = ContentHash::ofContent('a.php', "not->toBe($b);
});
it('treats variable renames as a change', function () {
$a = ContentHash::ofContent('a.php', 'not->toBe($b);
});
it('falls back to a raw hash for unparseable PHP', function () {
$hash = ContentHash::ofContent('a.php', 'not valid php at all');
expect($hash)->toBeString()->not->toBeEmpty();
});
it('is case-insensitive on the file extension', function () {
$a = ContentHash::ofContent('a.PHP', "toBe($b);
});
});
describe('Blade files', function () {
it('strips blade comments', function () {
$a = ContentHash::ofContent('a.blade.php', '
{{-- a comment --}}Hello
');
$b = ContentHash::ofContent('a.blade.php', 'Hello
');
expect($a)->toBe($b);
});
it('strips multi-line blade comments', function () {
$a = ContentHash::ofContent('a.blade.php', "\n{{--\n multi\n line\n--}}\nHello\n
");
$b = ContentHash::ofContent('a.blade.php', ' Hello
');
expect($a)->toBe($b);
});
it('collapses whitespace', function () {
$a = ContentHash::ofContent('a.blade.php', "\n Hello\n World\n
");
$b = ContentHash::ofContent('a.blade.php', ' Hello World
');
expect($a)->toBe($b);
});
it('detects content changes', function () {
$a = ContentHash::ofContent('a.blade.php', 'Hello
');
$b = ContentHash::ofContent('a.blade.php', 'Goodbye
');
expect($a)->not->toBe($b);
});
it('keeps blade directives intact', function () {
$a = ContentHash::ofContent('a.blade.php', '@if($user)Hi @endif');
$b = ContentHash::ofContent('a.blade.php', '@if($user)Bye @endif');
expect($a)->not->toBe($b);
});
it('does not use the PHP tokenizer for blade files', function () {
$a = ContentHash::ofContent('a.blade.php', ' hello');
$b = ContentHash::ofContent('a.blade.php', ' hello');
expect($a)->not->toBe($b);
});
});
describe('JavaScript-like files', function () {
it('strips line comments', function () {
$a = ContentHash::ofContent('a.js', "// a comment\nconst foo = 1;");
$b = ContentHash::ofContent('a.js', 'const foo = 1;');
expect($a)->toBe($b);
});
it('strips block comments on their own lines', function () {
$a = ContentHash::ofContent('a.js', "/* block */\nconst foo = 1;");
$b = ContentHash::ofContent('a.js', 'const foo = 1;');
expect($a)->toBe($b);
});
it('collapses whitespace', function () {
$a = ContentHash::ofContent('a.js', "const foo = 1;\n\nconst bar = 2;");
$b = ContentHash::ofContent('a.js', 'const foo = 1; const bar = 2;');
expect($a)->toBe($b);
});
it('detects code changes', function () {
$a = ContentHash::ofContent('a.js', 'const foo = 1;');
$b = ContentHash::ofContent('a.js', 'const foo = 2;');
expect($a)->not->toBe($b);
});
it('does not strip inline trailing comments', function () {
$a = ContentHash::ofContent('a.js', 'const foo = 1; // inline');
$b = ContentHash::ofContent('a.js', 'const foo = 1;');
expect($a)->not->toBe($b);
});
it('applies the same rules to .ts files', function () {
$a = ContentHash::ofContent('a.ts', "// comment\nconst foo: number = 1;");
$b = ContentHash::ofContent('a.ts', 'const foo: number = 1;');
expect($a)->toBe($b);
});
it('applies the same rules to .tsx files', function () {
$a = ContentHash::ofContent('a.tsx', "// comment\nconst Foo = () => ;");
$b = ContentHash::ofContent('a.tsx', 'const Foo = () => ;');
expect($a)->toBe($b);
});
it('applies the same rules to .jsx files', function () {
$a = ContentHash::ofContent('a.jsx', "// comment\nconst Foo = () => ;");
$b = ContentHash::ofContent('a.jsx', 'const Foo = () => ;');
expect($a)->toBe($b);
});
it('applies the same rules to .vue files', function () {
$a = ContentHash::ofContent('a.vue', "");
$b = ContentHash::ofContent('a.vue', '');
expect($a)->toBe($b);
});
it('applies the same rules to .svelte files', function () {
$a = ContentHash::ofContent('a.svelte', "");
$b = ContentHash::ofContent('a.svelte', '');
expect($a)->toBe($b);
});
it('applies the same rules to .mjs, .cjs, and .mts files', function () {
foreach (['mjs', 'cjs', 'mts'] as $ext) {
$a = ContentHash::ofContent("a.$ext", "// comment\nexport const foo = 1;");
$b = ContentHash::ofContent("a.$ext", 'export const foo = 1;');
expect($a)->toBe($b);
}
});
});
describe('unknown extensions', function () {
it('hashes the raw content for unknown extensions', function () {
$a = ContentHash::ofContent('a.txt', 'hello world');
$b = ContentHash::ofContent('a.txt', 'hello world');
expect($a)->toBe($b);
});
it('does not normalise whitespace for unknown extensions', function () {
$a = ContentHash::ofContent('a.txt', 'hello world');
$b = ContentHash::ofContent('a.txt', 'hello world');
expect($a)->not->toBe($b);
});
it('does not strip comments for unknown extensions', function () {
$a = ContentHash::ofContent('a.txt', "// not a comment here\nhello");
$b = ContentHash::ofContent('a.txt', 'hello');
expect($a)->not->toBe($b);
});
it('hashes files with no extension as raw content', function () {
$a = ContentHash::ofContent('Makefile', "all:\n\techo hi");
$b = ContentHash::ofContent('Makefile', "all:\n\techo hi");
expect($a)->toBe($b);
});
});
describe('output format', function () {
it('returns a 32-character hex xxh128 hash', function () {
$hash = ContentHash::ofContent('a.php', 'toMatch('/^[a-f0-9]{32}$/');
});
it('returns a stable hash for empty content', function () {
$a = ContentHash::ofContent('a.php', '');
$b = ContentHash::ofContent('a.php', '');
expect($a)->toBe($b);
});
});