[minor] Add a test project - #98
Merged
Merged
Conversation
The solution had one project and no tests. This is a tool that deletes files, so an off-by-one in grouping or a mistake in the keep/delete decision destroys user data, and nothing stood between a refactor and that outcome. Adds FileDeduplicator.Test with 21 tests over the three core types. The tests drive the real filesystem through throwaway directories under the temp path rather than an abstraction, because that is what the production code uses: FileScanner calls Directory.EnumerateFiles and Deduplicator calls File.Delete directly. Faking those would exercise a seam that does not exist and would not catch a mistake in the delete path, which is the part that destroys data. TempTree names each directory after the calling test, so a leaked directory names the test that leaked it. The most important test is DryRunEquivalenceTests, which asserts that the set of files DryRun reports it would delete is exactly the set Deduplicate does delete. DryRun is the safety net users rely on before letting this tool remove anything, so a divergence would be a lie about something irreversible. The two verbs share GroupByHash, FindDuplicates and SelectFileToKeep, so today they agree by construction -- that holds only while both keep calling the same three methods and while SelectFileToKeep stays deterministic, neither of which was enforced by anything. Also covered: grouping across nested directories, unique files never being offered for deletion, empty files as duplicates of each other, the shortest-filename keep rule, that only the file name and not the path length decides it, deterministic tie-breaking regardless of input order, that every group retains exactly one survivor, reclaimed byte counting, that a second pass over a deduplicated tree is a no-op, scanning recursion and the missing- and empty-directory cases, hash agreement across names, and that parallel hashing matches single-file hashing. Verified by mutation. Making SelectFileToKeep pick the last candidate instead of the first fails 3 tests; making DeleteDuplicates stop sparing the keeper -- which would delete every copy and lose the content entirely -- fails 5. One thing surfaced while wiring this up: InternalsVisibleTo names the test assembly FileDeduplicator.Test, without the ktsu. prefix every other repository in the organization uses. This repository has no AUTHORS.md, so ktsu.Sdk resolves an empty AuthorsNamespace and names the assembly FileDeduplicator rather than ktsu.FileDeduplicator. That is left alone here because correcting it renames the published artifact, which does not belong in a test-only change; it is raised separately and noted at the attribute so the two stay in step. Fixes #96 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DTHNXgSNEHUSQ5KMLgivno
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Fixes #96. Surfaced #97 along the way.
Why
The solution had one project and no tests. This tool deletes files — an off-by-one in grouping, or a mistake in the keep/delete decision, destroys user data, and nothing stood between a refactor and that outcome.
21 tests over
Deduplicator,FileHasherandFileScanner.The test that matters most
DryRunEquivalenceTestsasserts that the set of files DryRun says it would delete is exactly the set Deduplicate does delete.DryRun is the safety net users rely on before letting this tool remove anything, so a divergence between the two would be the worst defect this codebase could have: the preview would be a lie, and the thing it lied about is irreversible.
The two verbs share
GroupByHash,FindDuplicatesandSelectFileToKeep, so today they agree by construction. That is worth pinning rather than assuming — it holds only while both keep calling the same three methods and whileSelectFileToKeepstays deterministic, and neither was enforced by anything.It also asserts every group retains exactly one survivor, since deleting a whole group would lose the content entirely.
Real filesystem, not a fake
The tests drive throwaway directories under the temp path rather than an abstraction, because that is what the production code uses:
FileScannercallsDirectory.EnumerateFilesandDeduplicatorcallsFile.Deletedirectly. Faking those would exercise a seam that does not exist and would not catch a mistake in the delete path — the part that destroys data.TempTreenames each directory after the calling test, so a leaked directory names the test that leaked it.Also covered
Grouping across nested directories; unique files never offered for deletion; empty files as duplicates of each other; the shortest-filename keep rule; that only the file name and not the path length decides it; deterministic tie-breaking regardless of input order; reclaimed byte counting; a second pass over a deduplicated tree being a no-op; scanning recursion plus the missing- and empty-directory cases; hash agreement across names; the empty-file SHA-256; and that parallel hashing matches single-file hashing.
Verification
21/21 pass. Mutation-checked, by substitution rather than deletion:
SelectFileToKeepreturns.Last()instead of.First()DeleteDuplicatesstops sparing the keeper (deletes every copy)The tree was restored from a backup and re-verified after each run —
git diffclean, noLast(), no&& falseresidue.One thing to know before merging
InternalsVisibleTonames the test assemblyFileDeduplicator.Test, without thektsu.prefix every other repository uses. That is not a typo: this repository has noAUTHORS.md, soktsu.Sdkresolves an emptyAuthorsNamespaceand names the assemblyFileDeduplicatorrather thanktsu.FileDeduplicator. It is the only one of six repositories I checked that is missing the file.I left it alone here — correcting it renames the published artifact, which does not belong in a test-only change. #97 has the detail, including the warning that fixing it must update this literal in the same commit or the test project silently loses access to internals. There is a comment at the attribute saying so.
Caveat
I could not run the repo's own analyzers locally: this sandbox's SDK is 10.0.111 (Roslyn
5.0.0.0) andktsu.Sdk.Analyzers2.28.0 requires5.9.0.0, so CSC refuses it withCS9057. The .NET analyzers andEnforceCodeStyleInBuilddid run and are clean. CI is the first place KTSU0001–0007 will actually see this — the new test project is the likeliest place for a KTSU0001 standard-package complaint.Generated by Claude Code