Skip to content

Commit 8830652

Browse files
committed
test(android): cover the DocumentsProvider document-id path logic
Extract the VFS document-id path logic (child/parent/name) into an internal DocIdLogic object as the single source of truth and add JVM unit tests for it (runs on the host, no emulator). The cursor-building and openDocument paths still need Robolectric/an emulator; this covers the fiddly trailing-slash and root edge cases that are easy to break, and runs in the Android CI job.
1 parent 8d93953 commit 8830652

4 files changed

Lines changed: 82 additions & 7 deletions

File tree

.github/workflows/ci.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,9 @@ jobs:
246246
- name: Assemble the debug APK
247247
working-directory: android
248248
run: ./gradlew :app:assembleDebug --no-daemon
249+
- name: Run JVM unit tests
250+
working-directory: android
251+
run: ./gradlew :app:testDebugUnitTest --no-daemon
249252
- name: Upload the debug APK (CI artifact)
250253
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
251254
with:

android/app/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,4 +54,5 @@ dependencies {
5454
// UniFFI Kotlin bindings load the native lib through JNA; the Android aar
5555
// variant bundles the JNI shim JNA itself needs.
5656
implementation 'net.java.dev.jna:jna:5.14.0@aar'
57+
testImplementation 'junit:junit:4.13.2'
5758
}

android/app/src/main/java/co/mearman/cascade/CascadeDocumentsProvider.kt

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -159,15 +159,11 @@ class CascadeDocumentsProvider : DocumentsProvider() {
159159
}
160160

161161
private fun childDocId(parent: String, name: String): String =
162-
if (parent == ROOT_DOC_ID) "/$name" else "$parent/$name"
162+
DocIdLogic.childDocId(parent, name)
163163

164-
private fun parentOf(documentId: String): String {
165-
val trimmed = documentId.trimEnd('/')
166-
val idx = trimmed.lastIndexOf('/')
167-
return if (idx <= 0) ROOT_DOC_ID else trimmed.substring(0, idx)
168-
}
164+
private fun parentOf(documentId: String): String = DocIdLogic.parentOf(documentId)
169165

170-
private fun nameOf(documentId: String): String = documentId.trimEnd('/').substringAfterLast('/')
166+
private fun nameOf(documentId: String): String = DocIdLogic.nameOf(documentId)
171167

172168
private fun mimeOf(name: String): String {
173169
val ext = name.substringAfterLast('.', "").lowercase()
@@ -181,3 +177,21 @@ class CascadeDocumentsProvider : DocumentsProvider() {
181177
signal: CancellationSignal?,
182178
) = null
183179
}
180+
181+
/**
182+
* Document-id path logic, extracted so it has a single source of truth and can
183+
* be unit-tested on the JVM without an emulator. Document ids are VFS-absolute
184+
* paths with the root represented as `/`.
185+
*/
186+
internal object DocIdLogic {
187+
fun childDocId(parent: String, name: String): String =
188+
if (parent == "/") "/$name" else "$parent/$name"
189+
190+
fun parentOf(documentId: String): String {
191+
val trimmed = documentId.trimEnd('/')
192+
val idx = trimmed.lastIndexOf('/')
193+
return if (idx <= 0) "/" else trimmed.substring(0, idx)
194+
}
195+
196+
fun nameOf(documentId: String): String = documentId.trimEnd('/').substringAfterLast('/')
197+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package co.mearman.cascade
2+
3+
import org.junit.Assert.assertEquals
4+
import org.junit.Test
5+
6+
/**
7+
* JVM unit tests for the document-id path logic. These run on the host JVM
8+
* (no emulator) because the logic is pure string manipulation over VFS-absolute
9+
* paths — the fiddly trailing-slash and root edge cases that are easy to break.
10+
*/
11+
class DocIdLogicTest {
12+
13+
@Test
14+
fun childDocId_under_root_gets_leading_slash() {
15+
assertEquals("/Documents", DocIdLogic.childDocId("/", "Documents"))
16+
}
17+
18+
@Test
19+
fun childDocId_under_nested_parent_joins_with_slash() {
20+
assertEquals("/local/Documents/report.txt", DocIdLogic.childDocId("/local/Documents", "report.txt"))
21+
}
22+
23+
@Test
24+
fun parentOf_root_is_root() {
25+
assertEquals("/", DocIdLogic.parentOf("/"))
26+
}
27+
28+
@Test
29+
fun parentOf_top_level_child_is_root() {
30+
assertEquals("/", DocIdLogic.parentOf("/local"))
31+
}
32+
33+
@Test
34+
fun parentOf_nested_path_strips_last_segment() {
35+
assertEquals("/local/Documents", DocIdLogic.parentOf("/local/Documents/report.txt"))
36+
}
37+
38+
@Test
39+
fun parentOf_ignores_trailing_slash() {
40+
assertEquals("/local", DocIdLogic.parentOf("/local/Documents/"))
41+
}
42+
43+
@Test
44+
fun nameOf_root_is_empty() {
45+
assertEquals("", DocIdLogic.nameOf("/"))
46+
}
47+
48+
@Test
49+
fun nameOf_returns_last_segment() {
50+
assertEquals("report.txt", DocIdLogic.nameOf("/local/Documents/report.txt"))
51+
}
52+
53+
@Test
54+
fun nameOf_ignores_trailing_slash() {
55+
assertEquals("Documents", DocIdLogic.nameOf("/local/Documents/"))
56+
}
57+
}

0 commit comments

Comments
 (0)