Turn the project's rules into tests, and generate a map of where everything lives
The first question about any change is which file it belongs in, and the three documents that
kept being asked to answer it - README (the tour), ROADMAP (what is done) and CHANGELOG (why it is
like that) - were never going to. docs/CODE_MAP.md answers it: all 74 source files with the purpose
taken from the KDoc already sitting next to them, their size, and the tests that cover them,
followed by every test name in the suite. Those names are sentences about behaviour in this project
("a batch rename is a single undo step"), so the index doubles as the specification, and it is the
cheapest way for a reader, human or otherwise, to find out whether something is already covered.
The map is generated by tools/make-code-map.py rather than written, because a hand-maintained
description of 74 files is wrong within a milestone and nobody notices. The script harvests
declarations and comments; --check fails the moment the file stops matching the tree, and it caught
this very change (350 tests to 355); and the file says so about itself in its first line. A file is
described by the KDoc of the declaration that shares its name, which is the convention this
codebase already follows, and where no declaration matches, the first KDoc is used and the file is
listed at the end - so 15 fallbacks out of 74 are visible rather than assumed. Test files are
attached by the class they declare (NameTest -> Name.kt), which is a name-based match and therefore
approximate: the one honest weakness, and worth knowing before trusting a row that says a file has
no tests.
AGENTS.md is the one page that points at the map: what the app is, the two files to read before the
source, the exact commands, the layout with its dependency direction (ui -> domain -> data -> core,
with core free of Android), the rules that have tests behind them, what to read on demand and what
not to read at all, the definition of done, and the traps that have cost real time here - MIUI's two
install gates, assembleDebug racing an adb install, R8 stripping what only a resource names, and adb
input text not delivering a slash.
The same pass made four of the project's stated rules fail instead of merely being stated, in
ProjectInvariantsTest, which reads the source tree: core/ imports no Android (the engine stays
testable without Robolectric), no Text(...) in ui/ is handed a string literal (text comes from
strings.xml, which the pseudo-locale build exercises), the manifest requests exactly the six
permissions the app is built around and never INTERNET, and separator handling lives in core/ alone.
Three of the four were already true. The fourth found a bug: createFolder checked only that the name
was not empty, so "notes/final" was resolved by StoragePath into the notes folder that happened to
exist - the dialog offered one folder in the folder on screen and the app created a different one.
Rename refused the same input, because the rule has exactly one implementation and only one of its
two callers used it.
Both callers now go through isUsableName, and the shared name dialog says why a name is refused -
"A name cannot contain a slash", while it is typed, with its button disabled - rather than appearing
to do nothing when it is pressed. The batch dialog and both single-name dialogs read the same
string, so the rule reads the same wherever a name is entered. Verified on the phone: a slash typed
into New folder shows the note with Create the only disabled node in the dump, and deleting the
slash clears the note and re-enables it.
355 JVM tests (5 added: the four invariants and the folder-name refusal), lint clean, the map
current. The source-level rules state their own limits in their comments - the literal rule catches
a literal as the whole text of a Text(...) and not one concatenated into resource text, because
seeing that would take a Kotlin parser, and a rule that pretends otherwise is worse than one that
says where it stops.
This commit is contained in:
@@ -0,0 +1,614 @@
|
||||
# Code map
|
||||
|
||||
Generated from the source by `tools/make-code-map.py` - do not edit by hand. Regenerate it after a change (`python3 tools/make-code-map.py`); `--check` exits non-zero when this file is stale.
|
||||
|
||||
74 source files, 12036 lines - 28 test files, 355 tests. The behaviour index at the end lists every test name, and in this project a test name is a sentence about what the app does, so it doubles as the specification.
|
||||
|
||||
Start at `AGENTS.md`. What is done, and why: `docs/ROADMAP.md` and `CHANGELOG.md`. What was verified by hand rather than by a test: `docs/EDGE_CASE_TESTING.md`.
|
||||
|
||||
## core/
|
||||
|
||||
Pure Kotlin, **no Android imports**: paths, file names, kinds, sorting, formatting, matching, the batch-rename planner
|
||||
|
||||
| file | lines | what it holds | tests |
|
||||
|---|---|---|---|
|
||||
| `ArchiveFormats.kt` | 102 | Which archive formats the app can handle, by file name. ArchiveFormat, ArchiveFormats | `ArchivePathTest` (18) |
|
||||
| `ArchivePath.kt` | 43 | Guards archive extraction against writing outside the destination folder. ArchivePath | `ArchivePathTest` (18) |
|
||||
| `BatchRename.kt` | 125 | Renames a batch of files to one name with a number after each: Holiday (1).jpg, Holiday (2).jpg, and so on. BatchRenameMove, BatchRenamePlan, BatchRename | `BatchRenameTest` (16) |
|
||||
| `FileCategory.kt` | 53 | A library the home screen can open: everything on the device of a given kind, rather than everything in a given folder. FileCategory | `FileCategoryTest` (6) |
|
||||
| `FileEntry.kt` | 29 | One row in a directory listing. FileEntry, isHiddenName() | `FileEntryTest` (8) |
|
||||
| `FileKind.kt` | 73 | The coarse category a file belongs to, derived from its name. FileKind | `FileKindTest` (6) |
|
||||
| `FileNames.kt` | 13 | Whether name can be the name of a file in a folder. isUsableName() | `FileNamesTest` (4) |
|
||||
| `FileSize.kt` | 27 | Human-readable byte sizes for the file list ("1.5 MB"). FileSize | `FileSizeTest` (2) |
|
||||
| `MimeTypes.kt` | 110 | Best-effort MIME lookup for the "open with" intent. MimeTypes | `FileEntryTest` (8) |
|
||||
| `NameAllocator.kt` | 30 | Picks a name that is not already in use, in the familiar "name (1)" style. NameAllocator | `NameMatcherTest` (10) |
|
||||
| `NameMatcher.kt` | 63 | The in-folder search matcher. NameMatcher | `NameMatcherTest` (10) |
|
||||
| `SortSpec.kt` | 82 | A complete, serialisable description of the ordering of a directory listing. SortKey, SortDirection, SortSpec, NaturalOrder | `SortSpecTest` (8) |
|
||||
| `StoragePath.kt` | 89 | A normalized, absolute, backend-agnostic storage path. StoragePath | `StoragePathTest` (8) |
|
||||
| `Timestamps.kt` | 20 | Timestamps for the details sheet. Timestamps | - |
|
||||
| `TransferPlanner.kt` | 105 | Turns "copy/paste these items into that folder" into a per-item decision, before a single byte is written. ConflictPolicy, TransferAction, TransferSource, TransferItem, TransferPlanner | `TransferPlannerTest` (5) |
|
||||
|
||||
## data/
|
||||
|
||||
Storage backends (all-files access and SAF), volumes, incoming shares, the SQLite search index and trash index
|
||||
|
||||
| file | lines | what it holds | tests |
|
||||
|---|---|---|---|
|
||||
| `AllFilesSource.kt` | 125 | The "All files access" backend: plain java.io.File over absolute paths. AllFilesSource, File() | `AllFilesSourceTest` (13) |
|
||||
| `ApkInspector.kt` | 100 | What an APK says about itself, read without installing it. ApkInfo, interface(), PackageManagerApkInspector | - |
|
||||
| `IncomingSource.kt` | 48 | Something another app has handed to this one. IncomingSource, ContentResolverIncomingSource | - |
|
||||
| `MediaStoreDeleter.kt` | 73 | A best-effort DeleteRequestFactory backed by MediaStore. interface(), MediaStoreDeleter | - |
|
||||
| `SafDocumentIds.kt` | 40 | Maps a Storage Access Framework tree onto StoragePaths. SafDocumentIds | `SafDocumentIdsTest` (6) |
|
||||
| `SafGrant.kt` | 41 | Picking and keeping a folder tree for the SAF fallback. SafGrant | - |
|
||||
| `SafSource.kt` | 187 | The Storage Access Framework fallback backend. SafSource | `SafSourceTest` (5) |
|
||||
| `SearchIndex.kt` | 196 | A by-name index of what is on the volumes, so a search does not have to walk them. IndexedFile, VolumeStamp, SearchIndex | - |
|
||||
| `SqliteSearchIndex.kt` | 438 | The search index, on SQLite. SqliteSearchIndex | `SqliteSearchIndexTest` (27) |
|
||||
| `SqliteTrashIndex.kt` | 162 | The trash index, on SQLite. SqliteTrashIndex | `SqliteTrashIndexTest` (7) |
|
||||
| `StorageAccess.kt` | 80 | The Android-facing side of the storage-access decision. StorageAccess | - |
|
||||
| `StorageAccessPolicy.kt` | 68 | The decision rules behind StorageAccess. StorageAccessMode, StorageAccessPolicy | `StorageAccessPolicyTest` (6) |
|
||||
| `StorageLocation.kt` | 87 | Everything the browser needs to know about where files live and what it may touch. StorageLocation, StorageProvider | - |
|
||||
| `StorageSource.kt` | 96 | The primitive file operations a storage backend must provide. StorageException, StorageSource | - |
|
||||
| `TrashIndex.kt` | 61 | The durable memory of what is in the trash. TrashRecord, TrashIndex | - |
|
||||
| `VolumeRepository.kt` | 121 | Discovers the device's storage volumes and reports their space. VolumeInfo, SpaceInfo, VolumeRepository | `VolumeRepositoryTest` (6) |
|
||||
|
||||
## domain/
|
||||
|
||||
Operations over a source: copy/move/delete/rename, archives, search walking, duplicate finding, trash, usage analysis
|
||||
|
||||
| file | lines | what it holds | tests |
|
||||
|---|---|---|---|
|
||||
| `ArchiveEngine.kt` | 658 | Creates and extracts archives, on top of the storage backends. ArchiveEntry, ArchiveEngine | `ArchiveEngineTest` (30) |
|
||||
| `DuplicateFinder.kt` | 202 | Finds files that hold the same bytes. DuplicateGroup, DuplicateProgress, DuplicateScan, DuplicateFinder | `DuplicateFinderTest` (8) |
|
||||
| `FileOperations.kt` | 272 | Copy, move and delete, assembled from the StorageSource primitives. FileOperations | `FileOperationsTest` (19) |
|
||||
| `FileSearch.kt` | 79 | Recursive name search. FileSearch | `FileSearchTest` (8) |
|
||||
| `OperationModels.kt` | 42 | The kind of long-running operation a progress notification can describe. OperationKind, OperationProgress, OperationFailure, OperationResult | - |
|
||||
| `OperationNotifier.kt` | 25 | Keeps the foreground service in step with a running operation. OperationNotifier | - |
|
||||
| `SearchIndexer.kt` | 173 | Fills a SearchIndex by walking a volume. SearchIndexer | `SearchIndexerTest` (14) |
|
||||
| `Trash.kt` | 338 | A staging folder that makes a delete undoable. TrashedEntry, TrashBatch, Trash | `TrashTest` (19) |
|
||||
| `UsageAnalysis.kt` | 95 | Works out where the space went. UsageNode, UsageAnalysis | `UsageAnalysisTest` (6) |
|
||||
|
||||
## service/
|
||||
|
||||
The foreground service that keeps a long operation alive and reports it
|
||||
|
||||
| file | lines | what it holds | tests |
|
||||
|---|---|---|---|
|
||||
| `FileOpNotifications.kt` | 64 | Builds the progress notification for a running file operation. FileOpNotifications | - |
|
||||
| `FileOpService.kt` | 107 | Keeps a long copy, move or delete alive while the app is backgrounded, and mirrors the engine's progress into an ongoing notification. FileOpService | `FileOpServiceTest` (4) |
|
||||
| `ServiceOperationNotifier.kt` | 19 | The production OperationNotifier: forwards to FileOpService, which owns the foreground notification and keeps the process alive. ServiceOperationNotifier | - |
|
||||
|
||||
## text/
|
||||
|
||||
Domain enum to string-resource mapping
|
||||
|
||||
| file | lines | what it holds | tests |
|
||||
|---|---|---|---|
|
||||
| `TextRes.kt` | 58 | Maps domain enums onto their resource ids. OperationKind(), FileKind(), FileCategory() | - |
|
||||
|
||||
## ui/browser/
|
||||
|
||||
The browser screen: view model, state, list and grid, dialogs, selection bar
|
||||
|
||||
| file | lines | what it holds | tests |
|
||||
|---|---|---|---|
|
||||
| `BrowserComponents.kt` | 301 | The thumbnail an entry's leading visual gets in a list row. ListThumbnailSize, GridThumbnailSize, GridCellMinSize, EntryIcon(), FileEntry(), FileEntry(), VIDEO_FRAME_MILLIS, iconFor(), iconFor(), EntryRow(), EntryGridCell(), FileEntry() | - |
|
||||
| `BrowserContent.kt` | 420 | Everything below the app bar: the permission prompt, the roots, or a directory. WIDE_LAYOUT_MIN_WIDTH, MasterPaneWidth, BrowserContent(), LazyListScope(), LIBRARY_SECTION_KEY, DirectoryView(), FileEntry(), IndexStatusRow(), FileEntry() | - |
|
||||
| `BrowserDetailPane.kt` | 131 | The second pane of the wide layout: what is inside the folder the user picked, or a preview and the details of the file they picked. DetailPane(), FolderPreview(), ImagePreview() | - |
|
||||
| `BrowserDialogs.kt` | 279 | Renaming several files at once, with the numbering worked out before anything moves. BatchRenameDialog(), PREVIEW_NAMES, batchRenameNote(), NameDialog(), ConfirmDialog(), ConflictDialog() | - |
|
||||
| `BrowserRoots.kt` | 193 | A row on the root screen: a volume, with its free space when the platform reports it. RootRow(), itemCountLabel(), EmptyState(), LibrarySectionHeader(), LibraryRow(), Breadcrumbs() | - |
|
||||
| `BrowserScreen.kt` | 585 | BrowserScreen() | - |
|
||||
| `BrowserTopBar.kt` | 210 | The browser's app bar: title, up, and the search / sort / overflow actions. BrowserTopBar(), SEARCH_FIELD_WIDTH | - |
|
||||
| `BrowserUiState.kt` | 287 | One immutable description of everything the browser shows. ViewMode, SearchScope, Clipboard, UiMessage, ConflictPrompt, RootItem, DetailFocus, Undo, UndoMove, Overlay, BrowserUiState | - |
|
||||
| `BrowserViewModel.kt` | 2011 | Drives the browser screen. BrowserViewModel, ArchivePreview | `BrowserViewModelTest` (82) |
|
||||
| `DetailsSheet.kt` | 109 | Name, location, size, type and modified date for one entry. DetailsSheet(), DetailLines(), DetailLine() | - |
|
||||
| `EntryIntents.kt` | 58 | Wraps an entry's path in a content:// URI another app may read. contentUriFor(), viewIntent(), shareIntent() | - |
|
||||
| `IncomingBar.kt` | 60 | The bar shown while another app is waiting on this one. IncomingBar() | - |
|
||||
| `IncomingRequest.kt` | 46 | What another app asked this one to do. IncomingRequest | - |
|
||||
| `SelectionBar.kt` | 176 | The contextual action bar shown while entries are selected. StorageAccessPrompt(), SelectionBar() | - |
|
||||
| `SortSheet.kt` | 128 | Sort key, direction, hidden files and list/grid — all in one sheet. sortKeyLabel(), SortSheet() | - |
|
||||
|
||||
## ui/viewer/
|
||||
|
||||
The built-in viewers (text, image, PDF, media, markup, archive contents)
|
||||
|
||||
| file | lines | what it holds | tests |
|
||||
|---|---|---|---|
|
||||
| `ApkInfoScreen.kt` | 201 | What an APK says about itself, read from its own manifest without installing it. ApkInfoScreen(), ApkDetails(), ApkLine() | - |
|
||||
| `ArchivePreviewScreen.kt` | 198 | What an archive holds, read from its index without unpacking any of it. ArchivePreviewScreen(), ArchiveEntryRow(), INDENT_DP | - |
|
||||
| `ImageViewerScreen.kt` | 64 | The built-in image viewer. ImageViewerScreen() | - |
|
||||
| `MarkupPreviewScreen.kt` | 116 | Markup drawn by the platform's own web engine: rendered HTML, and the SVG that the raster image viewer cannot decode at all. MarkupPreviewScreen(), ENCODING | - |
|
||||
| `MediaPlayerScreen.kt` | 250 | Audio and video, played from the file itself. MediaPlayerScreen(), VideoPreview(), AudioPreview(), Unplayable(), timeLabel(), POSITION_POLL_MILLIS | - |
|
||||
| `PdfViewerScreen.kt` | 276 | A PDF, drawn a page at a time by the platform's own renderer. OpenPdf, PdfViewerScreen(), PdfPages(), PdfPage(), PageBar() | - |
|
||||
| `TextEditorScreen.kt` | 79 | A plain text editor for the text formats the browser classifies as FileKind.TEXT. TextEditorScreen() | - |
|
||||
|
||||
## ui/usage/
|
||||
|
||||
The storage-usage screen
|
||||
|
||||
| file | lines | what it holds | tests |
|
||||
|---|---|---|---|
|
||||
| `UsageScreen.kt` | 146 | Where the space went: each child of the current folder with its recursive size, biggest first, and a bar showing its share of the folder. UsageScreen(), UsageRow() | - |
|
||||
|
||||
## ui/trash/
|
||||
|
||||
The trash screen
|
||||
|
||||
| file | lines | what it holds | tests |
|
||||
|---|---|---|---|
|
||||
| `TrashScreen.kt` | 192 | What the trash still holds, and the way to put it back. TrashScreen(), BatchHeader(), TrashEntryRow() | - |
|
||||
|
||||
## ui/duplicates/
|
||||
|
||||
The duplicate-finder screen
|
||||
|
||||
| file | lines | what it holds | tests |
|
||||
|---|---|---|---|
|
||||
| `DuplicateScreen.kt` | 246 | The duplicate scan: what was compared, what matched, and which copies to remove. DuplicateScreen(), ScanRow(), LazyListScope(), GroupHeader(), EmptyDuplicates() | - |
|
||||
|
||||
## ui/theme/
|
||||
|
||||
Compose theme, colours, type
|
||||
|
||||
| file | lines | what it holds | tests |
|
||||
|---|---|---|---|
|
||||
| `Color.kt` | 27 | BluePrimaryLight, BlueOnPrimaryLight, BlueContainerLight, BlueOnContainerLight, TealSecondaryLight, SurfaceLight, SurfaceVariantLight, OnSurfaceLight, ErrorLight, BluePrimaryDark, BlueOnPrimaryDark, BlueContainerDark, BlueOnContainerDark, TealSecondaryDark, SurfaceDark, SurfaceVariantDark, OnSurfaceDark, ErrorDark | - |
|
||||
| `Theme.kt` | 64 | App theme. LightColors, DarkColors, FileManagerTheme() | - |
|
||||
| `Type.kt` | 9 | The app sticks with the Material 3 default type scale; a dedicated object keeps a single place to tune typography later. FileManagerTypography | - |
|
||||
|
||||
## ./
|
||||
|
||||
| file | lines | what it holds | tests |
|
||||
|---|---|---|---|
|
||||
| `FileManagerApplication.kt` | 90 | Application entry point and the app's tiny object graph. FileManagerApplication | - |
|
||||
| `MainActivity.kt` | 65 | MainActivity | - |
|
||||
|
||||
## Behaviour index
|
||||
|
||||
Every test name in the suite, by file: the fastest way to see whether something is already covered, and what the app claims to do.
|
||||
|
||||
### `ProjectInvariantsTest.kt` (4)
|
||||
|
||||
- core is pure Kotlin, with no Android imports
|
||||
- user-visible text comes from string resources, not from literals
|
||||
- the app asks for no permission that could reach the network
|
||||
- a name is one path segment, and that rule lives in one place
|
||||
|
||||
### `core/ArchivePathTest.kt` (18)
|
||||
|
||||
- plain names are kept as they are
|
||||
- redundant segments are normalized away
|
||||
- absolute paths are rejected
|
||||
- windows drive letters are rejected
|
||||
- climbing out of the destination is rejected
|
||||
- backslashes are treated as separators
|
||||
- empty and dot-only names are rejected
|
||||
- a climb that stays inside the destination is allowed
|
||||
- an unsafe name never yields a path with a parent segment
|
||||
- detects the formats it can extract
|
||||
- detects 7z, which is readable without native code
|
||||
- recognises but refuses formats that would need native code
|
||||
- an ordinary file is not an archive
|
||||
- detection is case-insensitive and tgz wins over gz
|
||||
- single-file wrappers lose their own suffix
|
||||
- a compressed tar is recognised as a tar, not as a bare compressed file
|
||||
- every compressed tar names the tar inside it
|
||||
- only zip can be created
|
||||
|
||||
### `core/BatchRenameTest.kt` (16)
|
||||
|
||||
- a second batch carries on where the first stopped
|
||||
- a file named exactly like the base counts as number zero
|
||||
- it continues from the highest number rather than from the first gap
|
||||
- the series is counted across extensions
|
||||
- names that merely resemble the pattern are ignored
|
||||
- a base that contains regex characters is matched literally
|
||||
- an empty folder starts at one
|
||||
- a plan keeps each file's own extension and numbers it in the order shown
|
||||
- a plan can continue the folder's series
|
||||
- a name that is already in the folder blocks the whole batch
|
||||
- a chain through a selected file is refused rather than half-done
|
||||
- files that already have the pattern's names are left alone
|
||||
- a file that already has its target name is counted and skipped, not renamed to itself
|
||||
- a start below one is treated as one, because there is no zeroth copy
|
||||
- a file with no extension gains none
|
||||
- a base has to be a name, not a path
|
||||
|
||||
### `core/FileCategoryTest.kt` (6)
|
||||
|
||||
- the kinds that belong to no library are exactly the three that were decided
|
||||
- documents covers text as well as documents
|
||||
- every kind maps back to the library that claims it
|
||||
- an uncollected kind answers null
|
||||
- recent is the only library that is not about a kind
|
||||
- no kind is claimed by two libraries
|
||||
|
||||
### `core/FileEntryTest.kt` (8)
|
||||
|
||||
- derives name and kind from the path
|
||||
- hidden follows the leading-dot convention
|
||||
- maps known extensions
|
||||
- falls back to the binary type
|
||||
- directories report the directory type
|
||||
- an intent takes the type its items share
|
||||
- a mixed batch falls back to the wildcard rather than hiding half the apps
|
||||
- an unknown extension is still shared as a file
|
||||
|
||||
### `core/FileKindTest.kt` (6)
|
||||
|
||||
- a directory wins over its extension
|
||||
- classifies common extensions case-insensitively
|
||||
- only the last extension counts
|
||||
- apk is recognised separately from archives
|
||||
- unknown and hidden names are other
|
||||
- extensionOf ignores dotfiles and trailing dots
|
||||
|
||||
### `core/FileNamesTest.kt` (4)
|
||||
|
||||
- an ordinary name is usable
|
||||
- a separator is not part of a name
|
||||
- a name has to say something
|
||||
- a null byte cannot be in a name
|
||||
|
||||
### `core/FileSizeTest.kt` (2)
|
||||
|
||||
- bytes are shown without decimals
|
||||
- kilobytes and above use one decimal
|
||||
|
||||
### `core/NameMatcherTest.kt` (10)
|
||||
|
||||
- an empty query matches everything
|
||||
- a plain query is a case-insensitive substring
|
||||
- a query with wildcards is treated as a glob
|
||||
- glob is case-insensitive
|
||||
- glob handles stars against empty and partial text
|
||||
- keeps the desired name when it is free
|
||||
- appends a counter before the extension
|
||||
- handles names without an extension
|
||||
- treats a dotfile as having no extension
|
||||
- preserves the base of multi-dot names
|
||||
|
||||
### `core/SortSpecTest.kt` (8)
|
||||
|
||||
- directories come first in both directions
|
||||
- name sort is natural, so file2 precedes file10
|
||||
- size sorts ascending and descending
|
||||
- date descending puts the newest first
|
||||
- ties break by name ascending
|
||||
- digit runs compare numerically
|
||||
- comparison ignores case
|
||||
- a prefix sorts before a longer name
|
||||
|
||||
### `core/StoragePathTest.kt` (8)
|
||||
|
||||
- normalizes separators and dot segments
|
||||
- adds a leading slash
|
||||
- the root is its own path
|
||||
- cannot escape above the root
|
||||
- exposes segments, name and parent
|
||||
- resolve handles relative and absolute input
|
||||
- ancestor and descendant are strict
|
||||
- is a value type
|
||||
|
||||
### `core/TransferPlannerTest.kt` (5)
|
||||
|
||||
- no conflict copies straight in
|
||||
- conflicts follow the chosen policy
|
||||
- two identical names in one batch do not collide
|
||||
- a folder cannot be moved into itself or a descendant
|
||||
- a move into itself is skipped in the plan
|
||||
|
||||
### `data/AllFilesSourceTest.kt` (13)
|
||||
|
||||
- lists the children of a directory
|
||||
- names lists a folder without reading metadata
|
||||
- names of a folder that cannot be read is null, not empty
|
||||
- names of a file is null too, because a file holds no names
|
||||
- listing a missing directory yields nothing
|
||||
- stat and exists agree
|
||||
- creates missing directories and empty files
|
||||
- delete removes a tree recursively
|
||||
- delete reports false for something that is not there
|
||||
- rename moves within the source and refuses to overwrite
|
||||
- writing then reading round-trips, and append works
|
||||
- free space comes from the injected probe
|
||||
- reports its backend name
|
||||
|
||||
### `data/SafDocumentIdsTest.kt` (6)
|
||||
|
||||
- the root maps onto the tree document id
|
||||
- children append their relative path
|
||||
- every path round-trips
|
||||
- documents outside the tree are rejected
|
||||
- labels are derived from the tree id
|
||||
- trees are not confused with similarly prefixed ones
|
||||
|
||||
### `data/SafSourceTest.kt` (5)
|
||||
|
||||
- lists the children of the tree root
|
||||
- stats a single document
|
||||
- a document the provider does not know does not exist
|
||||
- exposes a label and exactly one root
|
||||
- reports no free space, since the framework cannot
|
||||
|
||||
### `data/SqliteSearchIndexTest.kt` (27)
|
||||
|
||||
- a plain query is a substring of the name, case-insensitively
|
||||
- a query with a wildcard is a glob over the whole name
|
||||
- wildcards the user did not type stay literal
|
||||
- results are capped and ordered by name
|
||||
- a volume is replaced wholesale, and the stalest one decides the age
|
||||
- a half-finished pass leaves the old timestamp in place
|
||||
- forgetting a row takes it out of the results
|
||||
- clearing empties the rows and the timestamps
|
||||
- nothing indexed yet reports no age at all
|
||||
- a schema bump with no migration reports itself
|
||||
- children returns one folder's rows and nothing deeper
|
||||
- forgetting a folder takes its subtree and leaves its neighbours
|
||||
- forgetting a folder whose name contains a wildcard takes only that subtree
|
||||
- a catch-up keeps the time of the last full walk
|
||||
- the stamp remembers which hidden-files preference built it
|
||||
- a library query returns only the kinds it was asked for
|
||||
- a library query is newest first, so the cap keeps the newest
|
||||
- a library of two kinds holds both of them
|
||||
- a library that collects no kind answers nothing rather than everything
|
||||
- recent is ordered by date and leaves folders out
|
||||
- counts are grouped by kind
|
||||
- an index that has never been filled counts nothing
|
||||
- migrating from version 1 keeps the rows and adds the new columns
|
||||
- migrating from version 2 rebuilds the files table and forgets the stamps
|
||||
- duplicate candidates share a size, largest first
|
||||
- folders are never candidates, however their size falls out
|
||||
- the floor and the cap bound the candidate list
|
||||
|
||||
### `data/SqliteTrashIndexTest.kt` (7)
|
||||
|
||||
- a recorded item reads back whole
|
||||
- recording the same staged path twice replaces instead of duplicating
|
||||
- records come back newest batch first
|
||||
- forgetting one batch leaves the others alone
|
||||
- an empty index reads back empty rather than throwing
|
||||
- a schema bump with no migration reports itself instead of emptying the index
|
||||
- an already current schema is left alone
|
||||
|
||||
### `data/StorageAccessPolicyTest.kt` (6)
|
||||
|
||||
- android 11 and up key off the all-files special permission
|
||||
- android 10 and below key off the legacy write grant
|
||||
- from android 13 notifications are the only grant left to ask for
|
||||
- storage grants are asked for up to android 12
|
||||
- already granted permissions are not requested again
|
||||
- the permission constants match the platform strings
|
||||
|
||||
### `data/VolumeRepositoryTest.kt` (6)
|
||||
|
||||
- volumes are listed primary first
|
||||
- roots are built as directories in order
|
||||
- space is summed across volumes
|
||||
- an unknown volume reports no space
|
||||
- a volume without a space reading contributes nothing
|
||||
- used bytes never go negative
|
||||
|
||||
### `domain/ArchiveEngineTest.kt` (30)
|
||||
|
||||
- a zip round-trips folders and files
|
||||
- createZip reports progress over the files it will write
|
||||
- a zip-slip entry cannot escape the destination
|
||||
- a single-file gzip is unwrapped to its inner name
|
||||
- a single-file xz is unwrapped, which needs the optional xz library
|
||||
- a plain tar round-trips its files
|
||||
- a gzip tar round-trips its files
|
||||
- a bzip2 tar round-trips its files
|
||||
- an xz tar round-trips its files
|
||||
- an unsupported format is refused rather than guessed at
|
||||
- a 7z round-trips its files
|
||||
- a 7z-slip entry cannot escape the destination
|
||||
- 7z is refused with a readable reason when the backend cannot seek
|
||||
- extracting something that is not there is reported
|
||||
- listing reads names, sizes and nesting without extracting anything
|
||||
- a listing is the same when the backend cannot seek
|
||||
- a 7z is listed from its index
|
||||
- a compressed file is listed as the one name inside it
|
||||
- an unsupported archive cannot be listed either
|
||||
- one entry can be extracted without unpacking the rest
|
||||
- one entry can be extracted when the backend cannot seek
|
||||
- one entry can be extracted from a 7z
|
||||
- extracting a single hostile entry is refused
|
||||
- an entry can be read without extracting anything
|
||||
- an entry can be read when the backend cannot seek
|
||||
- an entry can be read from a 7z, which has no stream per entry
|
||||
- an entry inside a compressed wrapper is read under the name its archive implies
|
||||
- reading refuses to hand out more than the limit
|
||||
- an entry can be written straight to a file outside the backend
|
||||
- reading an entry the archive does not hold is null
|
||||
|
||||
### `domain/DuplicateFinderTest.kt` (8)
|
||||
|
||||
- files holding the same bytes are one group, and the group says what it would give back
|
||||
- three copies are one group of three
|
||||
- files of the same length but different contents are never a group
|
||||
- files that share a beginning but differ later are caught by the full pass
|
||||
- a file that cannot be read is counted rather than guessed at
|
||||
- groups are ordered by the space they would give back
|
||||
- a file whose length nothing else shares is never read
|
||||
- nothing to compare is nothing to report
|
||||
|
||||
### `domain/FileOperationsTest.kt` (19)
|
||||
|
||||
- copy brings a file across with its content
|
||||
- copy recurses into folders
|
||||
- copy creates the target folder when it does not exist
|
||||
- copy reports progress that reaches the total
|
||||
- keep both renames the incoming copy
|
||||
- skip leaves the existing file untouched
|
||||
- replace overwrites the existing file
|
||||
- ask is surfaced to the caller and skipped by the engine
|
||||
- move renames within the backend
|
||||
- move falls back to copy and delete when rename is unavailable
|
||||
- move keeps the source when the copy failed
|
||||
- a failing file is reported and the batch continues
|
||||
- an item that vanished is reported
|
||||
- cancellation stops between entries and reports partial work
|
||||
- delete removes files and folders
|
||||
- delete reports an item that is already gone
|
||||
- rename falls back to a copy and delete
|
||||
- rename refuses to clobber an existing target
|
||||
- createDirectory makes the folder
|
||||
|
||||
### `domain/FileSearchTest.kt` (8)
|
||||
|
||||
- finds a match however deep it is
|
||||
- a hidden folder is skipped whole, not just hidden from the results
|
||||
- hidden entries are searched when the preference asks for them
|
||||
- a matching folder is still descended into
|
||||
- matches stream out as they are found
|
||||
- the walk stops at the limit
|
||||
- globs work as they do everywhere else
|
||||
- a query that matches nothing finds nothing
|
||||
|
||||
### `domain/SearchIndexerTest.kt` (14)
|
||||
|
||||
- a walk records every file under the volume
|
||||
- a hidden folder is skipped whole when the preference says so
|
||||
- rows are handed over in batches rather than one write per file
|
||||
- progress is reported before the first batch is written
|
||||
- a catch-up reads names and the metadata of nothing it already knows
|
||||
- a catch-up finds a file that arrived and leaves the rest alone
|
||||
- a catch-up drops a file that has gone
|
||||
- a catch-up takes a folder that has gone with its whole subtree
|
||||
- a folder that cannot be read is left exactly as it was
|
||||
- a catch-up records that it was a catch-up and not a walk
|
||||
- a catch-up with the other hidden-files preference forgets what it should not show
|
||||
- a walk and a catch-up over a real tree keep the index level with it
|
||||
- the timestamp is stamped from the clock when the pass finishes
|
||||
- a second pass replaces the first rather than doubling it
|
||||
|
||||
### `domain/TrashTest.kt` (19)
|
||||
|
||||
- staging moves an item aside and records where it came from
|
||||
- restoring puts an item back exactly where it was
|
||||
- a folder comes back whole
|
||||
- two items sharing a name stage without one overwriting the other
|
||||
- restoring never overwrites a name that was taken again meanwhile
|
||||
- expiry drops only the batches that are old enough
|
||||
- staging nothing yields no undo handle
|
||||
- restoring a batch that is already gone is reported, not thrown
|
||||
- a batch folder is recognised by its name, and nothing else is
|
||||
- the trash folder is hidden, the way Android hides dotfiles
|
||||
- staging records where the item came from
|
||||
- a staged delete is still recoverable after the process forgets everything
|
||||
- the index is emptied once a restore has succeeded
|
||||
- a row whose file has gone is not offered back
|
||||
- purging forgets the rows it deletes
|
||||
- purging drops rows whose folder has gone, so the index cannot grow forever
|
||||
- what is offered back carries the name, the type, the size and where it came from
|
||||
- one item can be put back without disturbing the rest of its batch
|
||||
- a batch is forgotten once its last item is back
|
||||
|
||||
### `domain/UsageAnalysisTest.kt` (6)
|
||||
|
||||
- folder sizes are recursive and sorted largest first
|
||||
- rows are reported as they are measured
|
||||
- the largest files are found anywhere below the folder
|
||||
- the total rolls up the whole tree
|
||||
- an empty folder measures zero
|
||||
- a folder that is gone measures nothing rather than blowing up
|
||||
|
||||
### `service/FileOpServiceTest.kt` (4)
|
||||
|
||||
- the channel is created once and reused
|
||||
- an indeterminate operation says it is preparing
|
||||
- a determinate operation shows the counter
|
||||
- starting the service puts it in the foreground with that title
|
||||
|
||||
### `ui/browser/BrowserViewModelTest.kt` (82)
|
||||
|
||||
- starts on the root list
|
||||
- opening a folder lists it with directories first and natural order
|
||||
- hidden files are filtered out until asked for
|
||||
- search narrows the listing
|
||||
- sorting by size reorders the listing
|
||||
- navigating up goes to the parent then to the roots
|
||||
- selection survives toggling and clears on navigation
|
||||
- a file with no preview of its own is handed to the system
|
||||
- an image still opens in the built-in viewer, but can also be handed on
|
||||
- the SAF fallback will not hand out a file, and says why
|
||||
- a zip opens its contents instead of unpacking them
|
||||
- an archive that cannot be read closes again and says so
|
||||
- documents and media open in their own viewers
|
||||
- markup is drawn, and the text editor stays one action away
|
||||
- an apk is inspected rather than being handed to another app
|
||||
- a text entry inside an archive is previewed without writing anything
|
||||
- markup inside an archive is rendered, and not offered for editing
|
||||
- an image entry is copied into the scratch space, and the copy goes with the preview
|
||||
- a preview that would be too large is refused before anything is read
|
||||
- an entry with no viewer is offered an extraction instead
|
||||
- closing a preview of an entry returns to the archive it came from
|
||||
- searching everywhere answers from the index, across folders
|
||||
- switching scope re-answers the search rather than leaving the old results
|
||||
- the index is rebuilt when it has never been built
|
||||
- a fresh index is neither walked nor caught up
|
||||
- a stale index is caught up, and the answer includes what the catch-up found
|
||||
- a changed hidden-files preference asks for a walk rather than a catch-up
|
||||
- a search from the volume list is answered everywhere, with no folder open
|
||||
- opening a library lists the index's files of that kind and no folders
|
||||
- a library brings the index up to date behind the answer
|
||||
- a library row whose file has gone is dropped and forgotten
|
||||
- returning to the app keeps the library open rather than dropping to the roots
|
||||
- the home counts come from the index, and there are none without one
|
||||
- finding duplicates compares the index's candidates and reports what matched
|
||||
- a scan with nothing to compare says so rather than reporting a clean device
|
||||
- deleting the ticked copies closes the scan rather than patching it
|
||||
- ticking a copy does not tick every copy
|
||||
- a search from inside a library is answered everywhere, because a library is not a folder
|
||||
- a preview that needs a real file is refused in the SAF fallback
|
||||
- createFolder makes a directory in the current folder
|
||||
- createFolder refuses a name that is a path
|
||||
- rename moves the entry to the new name
|
||||
- delete removes the selected entries directly when the platform has no dialog
|
||||
- delete prefers the platform dialog and leaves the files alone until it returns
|
||||
- copy then paste duplicates into the current folder
|
||||
- cut then paste moves and clears the clipboard
|
||||
- a pasted name that already exists asks before overwriting
|
||||
- without any grant the browser asks for a folder
|
||||
- a granted tree becomes the only root
|
||||
- focusing a folder in the master pane lists it for the detail pane
|
||||
- focusing a file keeps the entry and previews no children
|
||||
- the detail pane honours the hidden-files preference
|
||||
- navigating clears the focus so the pane cannot show a stale folder
|
||||
- a focused volume previews the contents of that volume
|
||||
- the browsed folder and view settings survive a restart
|
||||
- a search query is not restored, so the folder is not hidden on launch
|
||||
- a selection and a clipboard do not survive a restart
|
||||
- restarting on the roots screen stays on the roots screen
|
||||
- search reaches into a subfolder, not just the one being browsed
|
||||
- clearing the query brings the folder back
|
||||
- navigating abandons a search, because its results belonged to the old folder
|
||||
- the search honours the hidden-files preference
|
||||
- a delete only moves the item aside, so it can be taken back
|
||||
- undoing a delete puts the file back
|
||||
- batch rename numbers the files in the order they are listed
|
||||
- batch rename continues a series the folder already has
|
||||
- a batch rename is a single undo step
|
||||
- batch rename refuses a batch whose names are already taken
|
||||
- a filename cannot be a path
|
||||
- a batch rename cannot be given a path either
|
||||
- undoing a rename puts the old name back
|
||||
- undoing a cut-paste moves the item back
|
||||
- a copy is not undoable, because nothing was changed
|
||||
- the undo handle is cleared once it has been used
|
||||
- a shared file is saved into the folder the user chose
|
||||
- a shared file never overwrites one already in the folder
|
||||
- a file the sender cannot name still lands under a usable one
|
||||
- a share that cannot be read leaves nothing behind
|
||||
- cancelling a share leaves an ordinary browser
|
||||
- with no reader wired up, a share is not silently pretended
|
||||
- while providing, tapping a file chooses it instead of opening it
|
||||
- while providing, a folder still navigates so the file can actually be reached
|
||||
|
||||
### Test files with no source file of the same name
|
||||
|
||||
- `ProjectInvariantsTest.kt` (4 tests) - ProjectInvariants, ProjectInvariants
|
||||
|
||||
---
|
||||
|
||||
Purpose taken from the first KDoc rather than from a declaration sharing the file's name (15 of 74): `MainActivity.kt`, `ArchiveFormats.kt`, `FileNames.kt`, `ApkInspector.kt`, `OperationModels.kt`, `TextRes.kt`, `BrowserComponents.kt`, `BrowserDetailPane.kt`, `BrowserDialogs.kt`, `BrowserRoots.kt`, `BrowserScreen.kt`, `EntryIntents.kt`, `Color.kt`, `Theme.kt`, `Type.kt`
|
||||
Reference in New Issue
Block a user