blob: 4309d7182d95b11ed83777a1e5c1096dd5fa1bef [file]
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.networkstack.ipmemorystore
import android.app.job.JobInfo
import android.app.job.JobParameters
import android.app.job.JobScheduler
import android.content.Context
import android.net.ipmemorystore.IOnStatusListener
import android.net.ipmemorystore.Status
import android.os.Looper
import android.testing.AndroidTestingRunner
import android.util.Log
import androidx.test.filters.SmallTest
import com.android.networkstack.ipmemorystore.RegularMaintenanceJobService.InterruptMaintenance
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Mockito.doReturn
import org.mockito.Mockito.mock
import org.mockito.Mockito.verify
import java.util.concurrent.ArrayBlockingQueue
import java.util.concurrent.CompletableFuture
import java.util.concurrent.TimeUnit
import kotlin.reflect.full.staticFunctions
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.assertTrue
private const val TIMEOUT_MS = 2_000L
@RunWith(AndroidTestingRunner::class)
@SmallTest
class RegularMaintenanceJobServiceTest {
private val mockContext = mock(Context::class.java)
private val mockJobScheduler = mock(JobScheduler::class.java)
private val mockIPMS = mock(IpMemoryStoreService::class.java)
private val jobService = InstrumentedRegularMaintenanceJobService()
private class InstrumentedRegularMaintenanceJobService : RegularMaintenanceJobService() {
// Store calls to jobFinished, which takes 2 arguments : params of the job to finish,
// and whether to reschedule
val events = ArrayBlockingQueue<Pair<JobParameters, Boolean>>(1 /* capacity */)
override fun getMainLooper() = mock(Looper::class.java)
override fun callJobFinished(params: JobParameters, reschedule: Boolean) {
events.offer(params to reschedule)
}
}
@Before
fun setUp() {
doReturn(mockJobScheduler)
.`when`(mockContext).getSystemService(Context.JOB_SCHEDULER_SERVICE)
// At this time JobService.onBind ignores its argument but must be called for it to work
// because JobService uses this method call to initialize some internal structures.
jobService.onBind(null)
}
private fun startMaintenance(): Triple<JobParameters, IOnStatusListener, InterruptMaintenance> {
val jobId = 12
RegularMaintenanceJobService.schedule(mockContext, mockIPMS)
val jobInfoCaptor = ArgumentCaptor.forClass(JobInfo::class.java)
verify(mockJobScheduler).schedule(jobInfoCaptor.capture())
val jobInfo = jobInfoCaptor.value
assertTrue(jobInfo.isRequireCharging)
assertTrue(jobInfo.isRequireBatteryNotLow)
assertTrue(jobInfo.isRequireDeviceIdle)
assertTrue(jobInfo.isPeriodic)
// AOSP sets the maintenance to once every 24h. It should be infrequent enough to not
// take up a lot of battery but also not so infrequent that it never gets done
assertTrue(jobInfo.intervalMillis in TimeUnit.HOURS.toMillis(12)..TimeUnit.DAYS.toMillis(5))
val params = mock(JobParameters::class.java)
doReturn(jobId).`when`(params).jobId
assertTrue(jobService.onStartJob(params))
val listenerCaptor = ArgumentCaptor.forClass(IOnStatusListener::class.java)
val imCaptor = ArgumentCaptor.forClass(InterruptMaintenance::class.java)
verify(mockIPMS).fullMaintenance(listenerCaptor.capture(), imCaptor.capture())
assertEquals(imCaptor.value.jobId, jobId)
assertFalse(imCaptor.value.isInterrupted)
return Triple(params, listenerCaptor.value, imCaptor.value)
}
private fun cleanupMaintenance(params: JobParameters) {
RegularMaintenanceJobService.unschedule(mockContext)
verify(mockJobScheduler).cancel(anyInt())
// This test may be compiled against the public SDK which prevents it from using
// Log#setWtfHandler. In this case there is no way to test that Log.wtf has been
// called if the onStartJob is called again on the same instance of the service.
// Only do this if the method is available.
if (Log::class.staticFunctions.any { it.name == "setWtfHandler" }) {
val call = CompletableFuture<String>()
val oldHandler = Log.setWtfHandler { _, what, _ -> call.complete(what.message) }
assertFalse(jobService.onStartJob(params))
assertNotNull(call.get())
Log.setWtfHandler(oldHandler)
}
}
@Test
fun testOnStopJob() {
val (params, _, im) = startMaintenance()
assertTrue(jobService.onStopJob(params))
assertTrue(im.isInterrupted)
cleanupMaintenance(params)
}
// Helper method to test completing maintenance with different statuses just in case, but
// in practice the only difference between success and other cases is that failures emit a
// log message.
private fun testListenToMaintenance(status: Int) {
val (params, listener, _) = startMaintenance()
assertTrue(listener.getInterfaceVersion() > 0)
assertNotNull(listener.getInterfaceHash())
assertNull(listener.asBinder())
listener.onComplete(Status(status).toParcelable())
val (completeParams, reschedule) = jobService.events.poll(TIMEOUT_MS, TimeUnit.MILLISECONDS)
assertEquals(completeParams.jobId, params.jobId)
if (Status.SUCCESS == status) {
assertFalse(reschedule)
} else {
assertTrue(reschedule)
}
cleanupMaintenance(params)
}
@Test fun testListenToMaintenanceSuccess() = testListenToMaintenance(Status.SUCCESS)
@Test fun testListenToMaintenanceFailure() = testListenToMaintenance(Status.ERROR_GENERIC)
// There is very little point in testing for each error status, as they all behave the same
}