Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ android {

debug {
enableUnitTestCoverage = project.hasProperty("coverage")
enableAndroidTestCoverage = project.hasProperty("coverage")
Copy link
Contributor Author

@PhilLab PhilLab Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tobiasKaminsky with this, Drone now actually runs the instrumented tests again. See https://drone.nextcloud.com/nextcloud/android/28585/1/3 as a proof of failure, i.e. broken tests on master which were never detected.

I'll go ahead addressing the test failures. But there is several other things I noticed:

  1. We should have Drone only run the instrumentation tests. Currently, drone CI runs everything for which coverage is enabled. Why this is chosen as filter, I don't know. But what this means is that Drone wastes time on the unit tests which we already run elsewhere.
    • Unit tests were the only tests Drone ran since Chore: Improve Gradle Configuration #15859. Another weird side-effect of that PR.
    • Easiest for saving drone time would be to disable the coverage for unit tests in the line above, but I guess this is not desired?
  2. Actually, what about the coverage reporting? It doesn't appear anymore. It looks broken, in several ways: - I am not seeing the coverage report on any PR I have recently checked. Don't know when this stopped. Many of the pulls on Codecov just show errors: https://app.codecov.io/github/nextcloud/android/pulls
  3. Coverage reporting should also be extended by the instrumentation tests. I don't see them uploading anything

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

regarding

We should have Drone only run the instrumentation tests

This seems to be the case already 🤔 I don't know how this works, but adding coverage to the androidTests somehow removed the unitTests automatically. Quite unexpected, but the outcome is the desired one, i.e. Drone is not executing unit tests anymore.

resConfigs("xxxhdpi")
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,14 @@ class SetOnlineStatusBottomSheetIT : AbstractIT() {
launchActivity<FileDisplayActivity>().use { scenario ->
onView(isRoot()).check(matches(isDisplayed()))

onView(withId(R.id.clearStatusAfterSpinner))
.check(matches(isDisplayed()))

scenario.onActivity { activity ->
val sut = SetOnlineStatusBottomSheet(
Status(StatusType.DND, "Working hard…", "🤖", -1)
)
sut.show(activity.supportFragmentManager, "")
}
onView(withId(R.id.onlineStatus))
.check(matches(isDisplayed()))
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.isRoot
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.rule.GrantPermissionRule
import com.owncloud.android.AbstractIT
import com.owncloud.android.R
import com.owncloud.android.lib.resources.users.ClearAt
import com.owncloud.android.lib.resources.users.PredefinedStatus
import com.owncloud.android.lib.resources.users.Status
Expand All @@ -41,7 +43,6 @@ class SetStatusMessageBottomSheetIT : AbstractIT() {
user,
Status(StatusType.DND, "Working hard…", "🤖", -1)
)
sut.show(activity.supportFragmentManager, "")
val predefinedStatus: ArrayList<PredefinedStatus> = arrayListOf(
PredefinedStatus("meeting", "📅", "In a meeting", ClearAt("period", "3600")),
PredefinedStatus("commuting", "🚌", "Commuting", ClearAt("period", "1800")),
Expand All @@ -51,7 +52,11 @@ class SetStatusMessageBottomSheetIT : AbstractIT() {
PredefinedStatus("vacationing", "🌴", "Vacationing", null)
)
sut.setPredefinedStatus(predefinedStatus)
sut.show(activity.supportFragmentManager, "")
}

onView(withId(R.id.predefinedStatusList))
.check(matches(isDisplayed()))
}
}
}
46 changes: 43 additions & 3 deletions app/src/androidTest/java/com/nextcloud/utils/AutoRenameTests.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2026 Philipp Hasper <vcs@hasper.info>
* SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
Expand Down Expand Up @@ -241,13 +242,52 @@ class AutoRenameTests : AbstractOnServerIT() {
assert(result == expectedFilename) { "Expected $expectedFilename but got $result" }
}

/**
* For documentation see [com.nextcloud.utils.extensions.checkWCFRestrictions]
*/
@Test
fun skipAutoRenameWhenWCFDisabled() {
capability = capability.apply {
fun testWCFDisabledOnNextcloud32ShouldSkipRestrictions() {
val filename = " readme.txt "
val nc32Capability = capability.apply {
versionMayor = NextcloudVersion.nextcloud_32.majorVersionNumber
isWCFEnabled = CapabilityBooleanType.FALSE
}
val result = AutoRename.rename(filename, nc32Capability, isFolderPath = true)
assert(result == filename) { "Expected $filename but got $result" }
}

@Test
fun testWCFEnabledOnNextcloud32ShouldApplyRestrictions() {
val filename = " readme.txt "
val result = AutoRename.rename(filename, capability, isFolderPath = true)
val nc32Capability = capability.apply {
versionMayor = NextcloudVersion.nextcloud_32.majorVersionNumber
isWCFEnabled = CapabilityBooleanType.TRUE
}
val result = AutoRename.rename(filename, nc32Capability, isFolderPath = true)
val expectedFilename = "readme.txt"
assert(result == expectedFilename) { "Expected $expectedFilename but got $result" }
}

@Test
fun testWCFDisabledOnNextcloud30to31ShouldStillApplyRestrictions() {
val filename = " readme.txt "
val nc30Capability = capability.apply {
versionMayor = NextcloudVersion.nextcloud_30.majorVersionNumber
isWCFEnabled = CapabilityBooleanType.FALSE
}
val result = AutoRename.rename(filename, nc30Capability, isFolderPath = true)
val expectedFilename = "readme.txt"
assert(result == expectedFilename) { "Expected $expectedFilename but got $result" }
}

@Test
fun testWCFOnNextcloudBelow30ShouldSkipRestrictions() {
val filename = " readme.txt "
val nc29Capability = capability.apply {
versionMayor = NextcloudVersion.nextcloud_29.majorVersionNumber
isWCFEnabled = CapabilityBooleanType.TRUE
}
val result = AutoRename.rename(filename, nc29Capability, isFolderPath = true)
assert(result == filename) { "Expected $filename but got $result" }
}
}
155 changes: 114 additions & 41 deletions app/src/androidTest/java/com/nextcloud/utils/UploadDateTests.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2026 Philipp Hasper <vcs@hasper.info>
* SPDX-FileCopyrightText: 2026 Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
Expand All @@ -13,14 +14,11 @@ import androidx.test.platform.app.InstrumentationRegistry
import com.nextcloud.client.database.entity.UploadEntity
import com.nextcloud.client.database.entity.toOCUpload
import com.nextcloud.client.database.entity.toUploadEntity
import com.nextcloud.utils.date.DateFormatPattern
import com.owncloud.android.R
import com.owncloud.android.utils.DisplayUtils
import io.mockk.MockKAnnotations
import io.mockk.every
import io.mockk.mockkStatic
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import java.text.SimpleDateFormat
Expand All @@ -30,25 +28,22 @@ import java.util.Locale
class UploadDateTests {

companion object {
private const val JANUARY_27_2026 = 1769505718000
private const val ONE_YEAR = 365L * 24 * 60 * 60 * 1000
private const val ONE_MONTH = 30L * 24 * 60 * 60 * 1000
private const val ONE_WEEK = 7 * 24 * 60 * 60 * 1000
private const val TWO_HOURS = 2 * 60 * 60 * 1000
private const val ONE_MINUTE = 60_000
private const val THIRTY_SECONDS = 30_000

private const val DATE_FORMATTER_PATTERN = "MMM dd, yyyy, hh:mm:ss a"
private const val THIRTY_SECONDS = 30_000L
private const val ONE_MINUTE = 60_000L
private const val ONE_HOUR = 60 * ONE_MINUTE
private const val ONE_DAY = 24 * ONE_HOUR

private const val ONE_YEAR = 365L * ONE_DAY
private const val ONE_MONTH = 30L * ONE_DAY
private const val ONE_WEEK = 7L * ONE_DAY
private const val TWO_HOURS = 2L * ONE_HOUR
}

private lateinit var context: Context

@Before
fun setup() {
context = InstrumentationRegistry.getInstrumentation().context
MockKAnnotations.init(this, relaxed = true)
mockkStatic(System::class)
every { System.currentTimeMillis() } returns JANUARY_27_2026
context = InstrumentationRegistry.getInstrumentation().targetContext
}

@Test
Expand Down Expand Up @@ -92,62 +87,140 @@ class UploadDateTests {
fun getRelativeDateTimeStringReturnsSecondsAgoForRecentPast() {
val result = DisplayUtils.getRelativeDateTimeString(
context,
JANUARY_27_2026 - THIRTY_SECONDS,
DateUtils.SECOND_IN_MILLIS,
System.currentTimeMillis() - THIRTY_SECONDS,
DateUtils.MINUTE_IN_MILLIS,
DateUtils.WEEK_IN_MILLIS,
0
)
assertTrue(result.toString() == context.getString(R.string.file_list_seconds_ago))
assertEquals(context.getString(R.string.file_list_seconds_ago), result.toString())
}

@Test
fun getRelativeDateTimeStringReturnsFutureAsHumanReadableWhenShowFutureIsFalse() {
val formatter = SimpleDateFormat(DATE_FORMATTER_PATTERN, Locale.US)
val time = JANUARY_27_2026 + ONE_MINUTE
val expected = formatter.format(Date(time))
fun getRelativeDateTimeStringReturnsFutureAsAbsoluteWhenShowFutureIsFalse() {
val formatter = SimpleDateFormat("MMM d, yyyy h:mm:ss a", Locale.US)
val expected = formatter.format(Date(System.currentTimeMillis() + ONE_MINUTE))

assertRelativeDateTimeString(time, expected, DateUtils.SECOND_IN_MILLIS)
val result = DisplayUtils.getRelativeDateTimeString(
context,
System.currentTimeMillis() + ONE_MINUTE,
DateUtils.SECOND_IN_MILLIS,
DateUtils.WEEK_IN_MILLIS,
0,
false
)
assertEquals(expected, result.toString())
}

@Test
fun getRelativeDateTimeStringReturnsFutureAsRelativeWhenShowFutureIsTrue() {
val expected = "In 1 minute"
val time = System.currentTimeMillis() + ONE_MINUTE

assertRelativeDateTimeString(time, expected, DateUtils.MINUTE_IN_MILLIS, showFuture = true)
}

@Test
fun getRelativeDateTimeStringReturnsProperRelativeStringForHoursAgo() {
fun getRelativeDateTimeStringReturnsRelativeStringForHoursAgo() {
val expected = "2 hours ago"
val time = JANUARY_27_2026 - TWO_HOURS
val time = System.currentTimeMillis() - TWO_HOURS

assertRelativeDateTimeString(time, expected, DateUtils.MINUTE_IN_MILLIS)
assertRelativeDateTimeString(time, expected, DateUtils.SECOND_IN_MILLIS)
}

@Test
fun getRelativeDateTimeStringReturnsRelativeStringForOneWeekAgo() {
val expected = "Jan 20"
val time = JANUARY_27_2026 - ONE_WEEK
fun getRelativeDateTimeStringReturnsAbbreviatedStringForOneWeekAgo() {
val time = System.currentTimeMillis() - ONE_WEEK
val formatter = SimpleDateFormat(DateFormatPattern.MonthWithDate.pattern, Locale.US)
val expected = formatter.format(Date(time))

assertRelativeDateTimeString(time, expected)
}

@Test
fun getRelativeDateTimeStringReturnsRelativeStringForOneMonthAgo() {
val expected = "12/28/2025"
val time = JANUARY_27_2026 - ONE_MONTH
fun getRelativeDateTimeStringReturnsAbbreviatedStringForOneMonthAgo() {
val time = System.currentTimeMillis() - ONE_MONTH
val formatter = SimpleDateFormat(DateFormatPattern.MonthWithDate.pattern, Locale.US)
val expected = formatter.format(Date(time))

assertRelativeDateTimeString(time, expected, DateUtils.DAY_IN_MILLIS)
assertRelativeDateTimeString(time, expected, DateUtils.SECOND_IN_MILLIS)
}

@Test
fun getRelativeDateTimeStringReturnsRelativeStringForOneYearAgo() {
val expected = "1/27/2025"
val time = JANUARY_27_2026 - ONE_YEAR
fun getRelativeDateTimeStringReturnsAbsoluteStringForOneYearAgo() {
val time = System.currentTimeMillis() - ONE_YEAR
val formatter = SimpleDateFormat("M/d/YYYY", Locale.US)
val expected = formatter.format(Date(time))

assertRelativeDateTimeString(time, expected, DateUtils.DAY_IN_MILLIS)
assertRelativeDateTimeString(time, expected, DateUtils.SECOND_IN_MILLIS)
}

@Suppress("MagicNumber")
@Test
fun getRelativeDateTimeStringReturnsDaysForDayInMillis() {
var testTimestamp = System.currentTimeMillis()
var expected = "Today"
var result = DisplayUtils.getRelativeDateTimeString(
context,
testTimestamp,
DateUtils.DAY_IN_MILLIS,
DateUtils.WEEK_IN_MILLIS,
0,
false
)
assertEquals(expected, result)

testTimestamp = System.currentTimeMillis() - DateUtils.DAY_IN_MILLIS
expected = "Yesterday"
result = DisplayUtils.getRelativeDateTimeString(
context,
testTimestamp,
DateUtils.DAY_IN_MILLIS,
DateUtils.WEEK_IN_MILLIS,
0,
false
)
assertEquals(expected, result)

testTimestamp = System.currentTimeMillis() - 2 * DateUtils.DAY_IN_MILLIS
expected = "2 days ago"
result = DisplayUtils.getRelativeDateTimeString(
context,
testTimestamp,
DateUtils.DAY_IN_MILLIS,
DateUtils.WEEK_IN_MILLIS,
0,
false
)
assertEquals(expected, result)

testTimestamp = System.currentTimeMillis() - 7 * DateUtils.DAY_IN_MILLIS
expected = SimpleDateFormat(DateFormatPattern.MonthWithDate.pattern, Locale.US).format(testTimestamp)
result = DisplayUtils.getRelativeDateTimeString(
context,
testTimestamp,
DateUtils.DAY_IN_MILLIS,
DateUtils.WEEK_IN_MILLIS,
0,
false
)
assertEquals(expected, result)
}

private fun assertRelativeDateTimeString(
time: Long,
expected: String,
minResolution: Long = DateUtils.MINUTE_IN_MILLIS,
transitionResolution: Long = DateUtils.WEEK_IN_MILLIS
transitionResolution: Long = DateUtils.WEEK_IN_MILLIS,
showFuture: Boolean = false
) {
val result = DisplayUtils.getRelativeDateTimeString(context, time, minResolution, transitionResolution, 0)
val result = DisplayUtils.getRelativeDateTimeString(
context,
time,
minResolution,
transitionResolution,
0,
showFuture
)
assertEquals(expected.normalizeResult(), result.toString().normalizeResult())
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,10 +113,12 @@ public void largeTest() {
}

OCUpload[] storedUploads = uploadsStorageManager.getAllStoredUploads();
assertEquals(size, uploadsStorageManager.getAllStoredUploads().length);
assertEquals(size, storedUploads.length);
assertEquals(uploads.size(), storedUploads.length);

for (int i = 0; i < size; i++) {
assertTrue(contains(uploads, storedUploads[i]));
assertTrue("Upload " + (i+1) + "/" + size + " not found in stored uploads: " + storedUploads[i].getLocalPath(),
contains(uploads, storedUploads[i]));
}
}

Expand All @@ -138,7 +140,7 @@ public void testIsSame() {

private boolean contains(ArrayList<OCUpload> uploads, OCUpload storedUpload) {
for (int i = 0; i < uploads.size(); i++) {
if (storedUpload.isSame(uploads.get(i))) {
if (storedUpload.isSame(uploads.get(i), true)) {
return true;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,10 @@ class SetStatusMessageBottomSheet(val user: User, val currentStatus: Status?) :
@SuppressLint("NotifyDataSetChanged")
@VisibleForTesting
fun setPredefinedStatus(predefinedStatus: ArrayList<PredefinedStatus>) {
adapter.list = predefinedStatus
binding.predefinedStatusList.adapter?.notifyDataSetChanged()
this.predefinedStatus = predefinedStatus
if (this::adapter.isInitialized) {
adapter.list = predefinedStatus
binding.predefinedStatusList.adapter?.notifyDataSetChanged()
}
}
}
Loading
Loading