I created a PeriodicWorkRequest from my SyncDatabaseWorker, which like below:
class SyncDatabaseWorker(ctx: Context, params: WorkerParameters) : RxWorker(ctx, params) {
private val dataManager: DataManager = App.getDataManager()
override fun createWork(): Single<Result> {
return Single.create { emitter ->
dataManager.loadStoresFromServer()
.subscribe(object : SingleObserver<List<Store>> {
override fun onSubscribe(d: Disposable) {
}
override fun onSuccess(storeList: List<Store>) {
if (!storeList.isEmpty()) {
emitter.onSuccess(Result.success())
} else {
emitter.onSuccess(Result.retry())
}
}
override fun onError(e: Throwable) {
emitter.onSuccess(Result.failure())
}
})
}
}
companion object {
fun prepareSyncDBWorker(): PeriodicWorkRequest {
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
val myWorkBuilder = PeriodicWorkRequest.Builder(SyncDatabaseWorker::class.java, 7, TimeUnit.DAYS)
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 1, TimeUnit.DAYS) // Backoff retry after 1 day
.setConstraints(constraints)
return myWorkBuilder.build()
}
}
}
Then I write unit test base on Google's guide like this:
#RunWith(AndroidJUnit4::class)
class SyncDatabaseWorkerTest {
private lateinit var context: Context
#Before
fun setup() {
context = InstrumentationRegistry.getInstrumentation().targetContext
val config = Configuration.Builder()
.setMinimumLoggingLevel(Log.DEBUG)
.setExecutor(SynchronousExecutor())
.build()
// Initialize WorkManager for instrumentation tests.
WorkManagerTestInitHelper.initializeTestWorkManager(context, config)
}
#Test
#Throws(Exception::class)
fun testPeriodicWork_WithConstrains() {
// Create request
val request = SyncDatabaseWorker.prepareSyncDBWorker()
val workManager = WorkManager.getInstance(context)
val testDriver = WorkManagerTestInitHelper.getTestDriver(context)
// Enqueue and wait for result.
workManager.enqueue(request).result.get()
// Check work request is enqueued
var workInfo = workManager.getWorkInfoById(request.id).get()
assertThat(workInfo.state, `is`(WorkInfo.State.ENQUEUED))
// Tells the testing framework the period delay & all constrains is met
testDriver!!.setPeriodDelayMet(request.id)
testDriver.setAllConstraintsMet(request.id)
// Check work request is running
workInfo = workManager.getWorkInfoById(request.id).get()
assertThat(workInfo.state, `is`(WorkInfo.State.RUNNING))
}
}
It's always in state ENQUEUED, even when period delay & all constrains is met.
Expected: is <RUNNING>
but: was <ENQUEUED>
When I debugged the test & find out that the createWork(): Single<Result> method is also triggered, but why the state is not RUNNING?
May be I'm wrong about the approach, but the documents about unit testing WorkManager is very few now, and I don't know the right way to do it.
Since you're using a synchronous executor, you will never actually see your work in the RUNNING state - it should have already executed. I suspect your work is actually being marked for retry and therefore enters the ENQUEUED state again. You should be able to verify this by either setting breakpoints or looking at your logs.
Given you are executing a PeriodicWorkRequest with a SynchronousExecutor, you will never see the WorkRequest in RUNNING state. It will be done executing before you can assert that it was in RUNNING.
After you return a Result.success() or a Result.failure() in your doWork(), the WorkRequest goes back to ENQUEUED for the next period (given it's a periodic request).
Related
I have the following adapter:
#Component
class MyCustomFlow : IntegrationFlowAdapter() {
fun singleThreadTaskExecutor(): TaskExecutor {
val executor = ThreadPoolTaskExecutor()
executor.maxPoolSize = 1
executor.initialize()
return executor
}
#Filter
fun filter(data: SomeData): Boolean = ...
#Transformer
fun transform(customer: Data): Message<SomeData> {
....
}
#ServiceActivator
fun handle(data: Data): SomeData {
....
}
#Bean(name = [PollerMetadata.DEFAULT_POLLER])
fun poller(): PollerSpec? {
return Pollers.fixedRate(500)
}
#Bean
override fun buildFlow(): IntegrationFlowDefinition<*> {
return from(MessageChannels.queue("updateCustomersLocation"))
.channel(MessageChannels.executor(singleThreadTaskExecutor()))
.split()
.filter(this)
.transform(this)
.handle(this)
.channel("customerLocationFetched")
}
}
And if I understand the Java DSL, the handle is:
handle → ServiceActivator
Given a ServiceActivator, outside of the IntegrationFlowAdapter, I have an option to define a poller. Within that poller I could add a delay before the next messages is processed.
#ServiceActivator(
poller = [Poller(fixedDelay = "3000", maxMessagesPerPoll = "1", fixedRate = "3000")]
)
Would it be possible, within the adapter, to add a delay for the ServiceActivator (the method named handle, similar to the logic I get if I add another channel.
#Bean
override fun buildFlow(): IntegrationFlowDefinition<*> {
return from(MessageChannels.queue("updateCustomersLocation"))
.channel(MessageChannels.executor(singleThreadTaskExecutor()))
.split()
.filter(this)
.transform(this)
.channel("myNewChannel")
//.handle(this)
//.channel("customerLocationFetched")
}
And then from outside the adapter I could just define a new ServiceActivator:
#ServiceActivator(
inputChannel = "myNewChannel"
poller = [Poller(fixedDelay = "3000", maxMessagesPerPoll = "1", fixedRate = "3000")]
)
The reason I want a delay between the messages in the handle/service activator is that the method sends requests and I wanna control the rate the request are sent.
First of all the buildFlow() method must not be marked with #Bean : the IntegrationFlowAdapter does the trick to register everything.
You can add poller to the endpoint spec. See a second arg of that handle(). Only the point that input channel for this polling endpoint must be pollable - the QueueChannel exists out-of-the-box and you can place it just before your handle().
I'm using spring boot (2.5.5). I'm seeing inconsistent behavior using rabbitmq queue listener.
Issue is, accountService.incrementMediaCount executes, even if neoPostService.createNeoPost throws exeception, thus executes multiple times, as retry policy is set to 5, which I guess seems weird as accountService.incrementMediaCount should execute only if neoPostService.createNeoPost execute successfully (without exception). Also, loggingRepository.save() execute lesser times than number of messages coming to queue. What is causing such behavior? Is it relared to distributed nature of RabbitMQ?
Queue listener
#RabbitListener(queues = [RabbitConfiguration.Companion.Queues.POST_CREATION_QUEUE], concurrency = "3")
fun postCreationListener(postMessage: PostMessage) {
neoPostService.createNeoPost(postMessage.postId)
accountService.incrementMediaCount(postMessage.userId)
loggingRepository.save(
Log(
resourceType = ResourceType.Post,
entityEventType = EntityEventType.Created,
message = "Post ${postMessage.postId} created for user ${postMessage.userId}"
)
)
}
fun createNeoPost(postId: Long): NeoPost {
// Get post by id from postgresql
val post = postRepository.findByIdOrNull(postId)
?: throw ResourceNotFoundException("Post $postId not found")
val neoUser = neoUserService.getUserById(post.user.userId!!)
// Save post to Neo4j
return neoPostRepository.save(post.toNeoPost().copy(user = neoUser))
}
//Increment counter in postgresql
fun incrementMediaCount(userId: Long, amount: Int = 1) {
accountRepository.incrementPostCounter(userId, amount)
}
Rabbit Config
#EnableRabbit
#Configuration
class RabbitConfiguration {
// other code
....
#Bean
fun retryInterceptor(): RetryOperationsInterceptor? {
return RetryInterceptorBuilder.stateless()
.backOffOptions(100, 5.0, 10000)
.maxAttempts(5)
.recoverer(RejectAndDontRequeueRecoverer())
.build()
}
....
}
I'm trying to write a unit test waiting for completion of a kotlin suspended function before checking results like this :
#Test
fun shouldSetupThingsProperly() {
val context = InstrumentationRegistry.getInstrumentation().context
runBlocking { MyObject.enable(context, false) }
Assert.assertTrue( /* whatever usefull */ true)
}
The suspending methods are as follow :
object MyObject {
#JvmStatic
suspend fun enable(context: Context, enable: Boolean) {
withContext(Dispatchers.IO) {
// ... do some work
wakeup(context)
}
}
private suspend fun wakeup(context: Context) {
withContext(Dispatchers.IO) {
try {
// setup things ...
} catch (ignore: Exception) {}
}
}
}
Test run ends with :
java.lang.VerifyError: Verifier rejected class MyObject: java.lang.Object MyObject.enable(android.content.Context, boolean, kotlin.coroutines.Continuation) failed to verify: java.lang.Object MyObject.enable(android.content.Context, boolean, kotlin.coroutines.Continuation): [0x16] register v7 has type Reference: android.content.Context but expected Precise Reference: MyObject (declaration of 'MyObject' appears in /data/app/test-_rphd0tDrOp0KM-Bz09NWA==/base.apk!classes2.dex)
at MyObject.enable(Unknown Source:0)
I'm not familiar with coroutine and I was wondering how to achieve waiting for completion of the enable suspended function inside the test properly or if error was due to some other mistake...
If it happend to coroutines- withContext on Android or Flutter, revert coroutines lib to 1.3.6 solved crash issue for me.
It seems that there is VerifyError bug in android coroutines lib version 1.3.7-1.3.8, and will be fixed after 1.4.0.
Details see links:
https://github.com/Kotlin/kotlinx.coroutines/issues/2049 https://github.com/Kotlin/kotlinx.coroutines/issues/2041
Testing coroutines is a trick, even after some experience.
If you can import, this will be very helpful: https://github.com/Kotlin/kotlinx.coroutines/tree/master/kotlinx-coroutines-test
If you have this dependency testing coroutines becomes much more manageable.
First off, if you can have the dispatcher you are running this a variable or parameter that can be set or overridden it will help you increase your testability.
As far as writing the test you can do something like:
#Before
fun before() {
Dispatchers.setMain(mainThreadSurrogate)
}
#Test
fun shouldSetupThingsProperly() = runBlockingTest {
val context = InstrumentationRegistry.getInstrumentation().context
MyObject.enable(context, false, Dispatchers.Main)
Assert.assertTrue( /* whatever useful */ true)
}
Your object itself will have I would say more of the changes
object MyObject {
#JvmStatic
suspend fun enable(context: Context, enable: Boolean, dispatcher: CoroutineDispatcher = Dispatchers.IO) {
// If you need a return feel free to use withContext such as:
// val result = withContext(dispatcher) { /* Return Value */ Any() }
CoroutineScope(dispatcher).run {
// ... do some work
wakeup(context)
}
}
private suspend fun wakeup(context: Context) {
// Another coroutine scope is unnecessary here, it will inherit the parent scope automatically, so you can call
// async functions here
delay(200)
try {
// setup things ...
} catch (exc: Exception) {
// We had an issue
}
}
}
I have been doing TDD in Kotlin for these past few weeks now in Android using MVP. Things have been going well.
I use Mockito to mock classes but I can't seem to get over on how to implement one of the tests I wanted to run.
The following are my tests:
Call api, receive list of data, then show list. loadAllPlacesTest()
Call api, receive empty data, then show list. loadEmptyPlacesTest()
Call api, some exception happen on the way, then show error message. loadExceptionPlacesTest()
I have tests for #1 and #2 successfully. The problem is with #3, I'm not sure how to approach the test in code.
RestApiInterface.kt
interface RestApiInterface {
#GET(RestApiManager.PLACES_URL)
fun getPlacesPagedObservable(
#Header("header_access_token") accessToken: String?,
#Query("page") page: Int?
): Observable<PlacesWrapper>
}
RestApiManager.kt
the manager class implementing the interface looks like this:
open class RestApiManager: RestApiInterface{
var api: RestApiInterface
internal set
internal var retrofit: Retrofit
init {
val logging = HttpLoggingInterceptor()
// set your desired log level
logging.setLevel(HttpLoggingInterceptor.Level.BODY)
val client = okhttp3.OkHttpClient().newBuilder()
.readTimeout(60, TimeUnit.SECONDS)
.connectTimeout(60, TimeUnit.SECONDS)
.addInterceptor(LoggingInterceptor())
.build()
retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())//very important for RXJAVA and retrofit
.build()
api = retrofit.create(RestApiInterface::class.java)
}
override fun getPlacesPagedObservable(accessToken: String?, page: Int?): Observable<PlacesWrapper> {
//return throw Exception("sorry2")
return api.getPlacesPagedObservable(
accessToken,
page)
}
}
}
Here is my unit test:
class PlacesPresenterImplTest : AndroidTest(){
lateinit var presenter:PlacesPresenterImpl
lateinit var view:PlacesView
lateinit var apiManager:RestApiManager
//lateinit var apiManager:RestApiManager
val EXCEPTION_MESSAGE1 = "SORRY"
val MANY_PLACES = Arrays.asList(PlaceItem(), PlaceItem());
var EXCEPTION_PLACES = Arrays.asList(PlaceItem(), PlaceItem());
val manyPlacesWrapper = PlacesWrapper(MANY_PLACES)
var exceptionPlacesWrapper = PlacesWrapper(EXCEPTION_PLACES)
val emptyPlacesWrapper = PlacesWrapper(Collections.emptyList())
#After
fun clear(){
RxJavaPlugins.reset()
}
#Before
fun init(){
//MOCKS THE subscribeOn(Schedulers.io()) to use the same thread the test is being run on
//Schedulers.trampoline() runs the test in the same thread used by the test
RxJavaPlugins.setIoSchedulerHandler { t -> Schedulers.trampoline() }
view = Mockito.mock<PlacesView>(PlacesView::class.java)
apiManager = Mockito.mock(RestApiManager::class.java)
presenter = PlacesPresenterImpl(view,context(), Bundle(), Schedulers.trampoline())
presenter.apiManager = apiManager
//exceptionPlacesWrapper = throw Exception(EXCEPTION_MESSAGE1);
}
#Test
fun loadAllPlacesTest() {
Mockito.`when`(apiManager.getPlacesPagedObservable(Mockito.anyString(), Mockito.anyInt())).thenReturn(Observable.just(manyPlacesWrapper))
presenter.__populate()
Mockito.verify(view, Mockito.atLeastOnce()).__showLoading()
Mockito.verify(view, Mockito.atLeastOnce())._showList()
Mockito.verify(view).__hideLoading()
Mockito.verify(view).__showFullScreenMessage(Mockito.anyString())
}
#Test
fun loadEmptyPlacesTest() {
Mockito.`when`(apiManager.getPlacesPagedObservable(Mockito.anyString(), Mockito.anyInt())).thenReturn(Observable.just(emptyPlacesWrapper))
presenter.__populate()
Mockito.verify(view, Mockito.atLeastOnce()).__showLoading()
Mockito.verify(view, Mockito.atLeastOnce())._showList()
Mockito.verify(view).__hideLoading()
Mockito.verify(view).__showFullScreenMessage(Mockito.anyString())
}
#Test
fun loadExceptionPlacesTest() {
Mockito.`when`(apiManager.getPlacesPagedObservable(Mockito.anyString(), Mockito.anyInt())).thenThrow(Exception(EXCEPTION_MESSAGE1))
presenter.__populate()
Mockito.verify(view, Mockito.atLeastOnce()).__showLoading()
Mockito.verify(view, Mockito.never())._showList()
Mockito.verify(view).__hideLoading()
Mockito.verify(view).__showFullScreenMessage(EXCEPTION_MESSAGE1)
}
}
PlacesPresenterImpl.kt
This is the presenter.
class PlacesPresenterImpl
constructor(var view: PlacesView, var context: Context, var savedInstanceState:Bundle?, var mainThread: Scheduler)
: BasePresenter(), BasePresenterInterface, PlacesPresenterInterface {
lateinit var apiManager:RestApiInterface
var placeListRequest: Disposable? = null
override fun __firstInit() {
apiManager = RestApiManager()
}
override fun __init(context: Context, savedInstanceState: Bundle, view: BaseView?) {
this.view = view as PlacesView
if (__isFirstTimeLoad())
__firstInit()
}
override fun __destroy() {
placeListRequest?.dispose()
}
override fun __populate() {
_callPlacesApi()
}
override fun _callPlacesApi() {
view.__showLoading()
apiManager.getPlacesPagedObservable("", 0)
.subscribeOn(Schedulers.io())
.observeOn(mainThread)
.subscribe (object : DisposableObserver<PlacesWrapper>() {
override fun onNext(placesWrapper: PlacesWrapper) {
placesWrapper?.let {
val size = placesWrapper.place?.size
view.__hideLoading()
view._showList()
System.out.println("Great I found " + size + " records of places.")
view.__showFullScreenMessage("Great I found " + size + " records of places.")
}
System.out.println("onNext()")
}
override fun onError(e: Throwable) {
System.out.println("onError()")
//e.printStackTrace()
view.__hideLoading()
if (ExceptionsUtil.isNoNetworkException(e)){
view.__showFullScreenMessage("So sad, can not connect to network to get place list.")
}else{
view.__showFullScreenMessage("Oops, something went wrong. ["+e.localizedMessage+"]")
}
this.dispose()
}
override fun onComplete() {
this.dispose()
//System.out.printf("onComplete()")
}
})
}
private fun _getEventCompletionObserver(): DisposableObserver<String> {
return object : DisposableObserver<String>() {
override fun onNext(taskType: String) {
//_log(String.format("onNext %s task", taskType))
}
override fun onError(e: Throwable) {
//_log(String.format("Dang a task timeout"))
//Timber.e(e, "Timeout Demo exception")
}
override fun onComplete() {
//_log(String.format("task was completed"))
}
}
}}
Problem/Questions for the loadExceptionPlacesTest()
I'm not sure why the code doesn't go to the Presenter's onError().
correct me if I'm wrong the following but this is what I think:
a - `apiManager.getPlacesPagedObservable("", 0)` observable itself throws an Exception that is why the `.subscribe()` can not happen/proceed and the methods of the observer won't get called,
b - it will only go to onError() when the operations inside the observable encounters an Exception like JSONException
For loadExceptionPlacesTest() I think the 1b above is the way to go to make the presenter's onError() get called and make the test pass. Is this correct? If it is how to do it on the test. If it is not can you guys point out what I am missing or doing wrong?
I'll leave this here for future reference and to be able to elaborate a bit more, even though I've answered in the comments.
What you're trying to accomplish is to put the stream in the onError flow. Unfortunately, by mocking it like this:
Mockito.`when`(apiManager.getPlacesPagedObservable(
Mockito.anyString(), Mockito.anyInt()))
.thenThrow(Exception(EXCEPTION_MESSAGE1))
You're actually telling Mockito to setup your mock in a way that just calling apiManager.getPlacesPagedObservable(anystring, anystring) should thrown an exception.
It is indeed true that throwing an exception inside an Rx stream will cause the entire stream to stop and end up in the onError method. However, this is exactly the problem with the approach you're using. You're not inside the stream when the exception is thrown.
Instead what you want to do is tell Mockito that once you call apiManager.getPlacesPagedObservable(anystring, anystring) you want to return a stream that will end up in the onError. This can be easily achieved with Observable.error() like so:
Mockito.`when`(apiManager.getPlacesPagedObservable(
Mockito.anyString(), Mockito.anyInt()))
.thenReturn(Observable.error(
Exception(EXCEPTION_MESSAGE1)))
(It might be possible that you need to add some type information in this part here Observable.error(), you might also need to use something else instead of an observable - single, completable, etc.)
The mocking above will tell Mockito to setup your mock to return an observable that will error as soon as it's subscribed to. This will in turn put your subscriber directly in the onError stream with the specified exception.
Below is an example of a Test that invoke a REST service through Repository from a ViewModel according to the MVVM pattern.
The REST service returns an Exception, here is the test case:
#RunWith(AndroidJUnit4::class)
class StargazersViewModelTest {
#get:Rule
var instantExecutorRule = InstantTaskExecutorRule()
// Subject under test
private lateinit var viewModel: MyViewModel
#Mock
private lateinit var repositoryMock: MyRepository
#Before
fun setup() {
MockitoAnnotations.openMocks(this)
val appContext = ApplicationProvider.getApplicationContext<Application>()
viewModel = MyViewModel(repositoryMock, appContext)
RxJavaPlugins.setIoSchedulerHandler { Schedulers.trampoline() }
RxJavaPlugins.setComputationSchedulerHandler { Schedulers.trampoline() }
RxJavaPlugins.setNewThreadSchedulerHandler { Schedulers.trampoline() }
RxAndroidPlugins.setInitMainThreadSchedulerHandler { Schedulers.trampoline() }
}
#Test
fun `invoke rest with failure`() {
whenever(
repositoryMock.loadDataSingle(Mockito.anyString(), Mockito.anyInt())
).thenAnswer {
Single.error<retrofit2.HttpException>(
retrofit2.HttpException(
Response.error<String>(
404,
"Response.error()".toResponseBody("text/plain; charset=utf-8".toMediaType())
)
)
)
}
}
}
I try to see a difference between Spring synchronous REST Controller vs async version of same controller.
Each controller do the same thing : take a RequestBody and save it in a Mongo database.
#RestController
#RequestMapping ("/api/1/ticks")
public class TickController {
#Autowired
private TickManager tickManager;
#RequestMapping (method = RequestMethod.POST)
public ResponseEntity save(#RequestBody List<Tick> ticks) {
tickManager.save(ticks);
return new ResponseEntity(HttpStatus.OK);
}
#RequestMapping (value = "/async", method = RequestMethod.POST)
public #ResponseBody Callable<ResponseEntity> saveAsync(#RequestBody List<Tick> ticks) {
return () -> {
tickManager.save(ticks);
return new ResponseEntity(HttpStatus.OK);
};
}
}
The tickManager has only a dependency on a tickRepository and just do call to sub-layer.
The tickRepository is based on Spring Data Mongodb:
#Repository
public interface TickRepository extends MongoRepository<Tick, String> {}
I use Gatling to test those controllers.
This is my scenario:
import io.gatling.core.Predef._
import io.gatling.http.Predef._
import scala.concurrent.duration._
class TicksSaveSyncSimulation extends Simulation {
val rampUpTimeSecs = 20
val testTimeSecs = 5
val noOfUsers = 1000
val minWaitMs = 1000 milliseconds
val maxWaitMs = 3000 milliseconds
val baseURL = "http://localhost:9080"
val requestName = "ticks-save-sync-request"
val scenarioName = "ticks-save-sync-scenario"
val URI = "/api/1/ticks"
val httpConf = http.baseURL(baseURL)
val http_headers = Map(
"Accept-Encoding" -> "gzip,deflate",
"Content-Type" -> "application/json;charset=UTF-8",
"Keep-Alive" -> "115"
)
val scn = scenario(scenarioName)
.repeat(100) {
exec(
http(requestName)
.post(URI)
.headers(http_headers)
.body(StringBody(
"""[{
| "type": "temperature",
| "datas": {}
|}]""".stripMargin))
.check(status.is(200))
)
}
setUp(scn.inject(rampUsers(1000) over (1 seconds))).protocols(httpConf)
}
I tried several situations and the sync version always handle 2 times more request by second than the async version.
When I increase the number of users the two versions crash.
I tried to override the taskExecutor for the async version with no more success:
#Configuration
public class TaskExecutorConfig implements AsyncConfigurer {
#Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
taskExecutor.setMaxPoolSize(1000);
taskExecutor.setThreadNamePrefix("LULExecutor-");
taskExecutor.initialize();
return taskExecutor;
}
#Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return new SimpleAsyncUncaughtExceptionHandler();
}
}
I thought see a difference in favor of the async implementation.
What am I doing wrong?
Your test looks to be flawed. It doesn't make any sense being non blocking at one end of the pipeline (here, your controllers), and being blocking at the other end (tickManager.save really looks like a blocking call). You're just paying the extra cost of jumping into a ThreadPoolTaskExecutor.
Then, generally speaking, you won't gain anything from a non blocking architecture when all your tasks are very fast, like a tick. You can expect gains when some tasks take some longer time, so you don't want to waste resources (threads, CPU cycles) just waiting for those to complete, and you want to use them to perform other tasks in the meanwhile.
Regarding your Too many open files exception, you probably haven't properly tuned your OS for load testing, check relevant documentation. There's also a good chance that you're running your app and Gatling (and possibly your database too) on the same host, which is bad as they'll compete for resources.