I have updated to Android 31 and Android Studio Dolphin and now my tests are failing because of TextUtils.isEmpty() returns wrong result.
I have this method to mock TextUtils.isEmpty().
protected void mockTextUtilsIsEmpty() {
PowerMockito.mockStatic(TextUtils.class);
PowerMockito.when(TextUtils.isEmpty(any(CharSequence.class))).thenAnswer(invocation -> {
String val = (String) invocation.getArguments()[0];
return val == null || val.length() == 0;
});
}
This is my test class.
#RunWith(PowerMockRunner.class) #PrepareForTest(TextUtils.class)
public class CustomerDetailsPresenterTest extends BaseTest {
#Rule TrampolineSchedulerRule trampolineSchedulerRule = new TrampolineSchedulerRule();
#Mock GetCustomerUseCase getCustomerUseCase;
#Mock GetMenuItemsUseCase getMenuItemsUseCase;
#Mock RolesManager rolesManager;
#Mock CustomerDetailsPresenter.View view;
private CustomerDetailsPresenter presenter;
private final int customerId = 1;
#Before public void setUp() {
mockTextUtilsIsEmpty();
presenter = new CustomerDetailsPresenter(getCustomerUseCase, getMenuItemsUseCase, rolesManager);
presenter.setView(view);
}
#Test public void shouldDisplayCustomerWithEmptyData() {
// Given
CustomerDetails customerDetails = CustomerDetails.newBuilder()
.build();
// When
Mockito.when(getCustomerUseCase.execute(customerId)).thenReturn(Single.just(customerDetails));
presenter.getCustomerDetails(customerId);
//Then
Mockito.verify(view).showRefreshing();
Mockito.verify(view).hideRefreshing();
Mockito.verify(view).displayCustomerEmailUnknown();
Mockito.verify(view).displayCustomerNoteUnknown();
}
}
This is my actual class that I want to test.
public class CustomerDetailsPresenter implements Presenter{
private final GetCustomerUseCase getCustomerUseCase;
private final GetMenuItemsUseCase getMenuItemsUseCase;
private final RolesManager rolesManager;
private CompositeDisposable disposables;
private View view;
#Inject public CustomerDetailsPresenter(
GetCustomerUseCase getCustomerUseCase,
GetMenuItemsUseCase getMenuItemsUseCase,
RolesManager rolesManager
) {
this.getCustomerUseCase = getCustomerUseCase;
this.getMenuItemsUseCase = getMenuItemsUseCase;
this.rolesManager = rolesManager;
}
public void setView(View view) {
this.view = view;
}
public void getCustomerDetails(int id) {
disposables = RxUtil.initDisposables(disposables);
if (rolesManager.isUserReadOnly()) {
view.showScreenAsReadOnly();
}
view.showRefreshing();
Disposable disposable = getCustomerUseCase.execute(id)
.doOnSuccess(customerDetails -> view.hideRefreshing())
.subscribe(customerDetails -> {
if (customerDetails != null) {
if (TextUtils.isEmpty(customerDetails.getInvoiceEmail())) {
view.displayCustomerEmailUnknown();
} else {
view.displayCustomerEmail(customerDetails.getInvoiceEmail());
}
if (TextUtils.isEmpty(customerDetails.getBillingNote())) {
view.displayCustomerNoteUnknown();
} else {
view.displayCustomerNote(customerDetails.getBillingNote());
}
view.displayCustomerAddress(customerDetails);
view.displayLastModified(customerDetails);
} else {
view.hideCustomerSecondAndThirdPhones();
}
} else {
view.hideCustomerDetails();
}
},
view::handleError
);
disposables.add(disposable);
What could be the problem?
So, in this part, it always goes to the else part of the statement, and as you can in my unit test that should not happen since I am providing null to invoice email field.
if (TextUtils.isEmpty(customerDetails.getInvoiceEmail())) {
view.displayCustomerEmailUnknown();
} else {
view.displayCustomerEmail(customerDetails.getInvoiceEmail());
}
Any ideas?
Related
Currently this line "observer = data -> createTokenMap(data.getPositions());" calls a function and do some logic.
How can I accept a return value from this function and pass it into another function and so on.
public class PositionViewModel extends AndroidViewModel {
private final LiveData<PositionResponse> positionResponseLiveData;
private final Observer<PositionResponse> observer;
private final Map<String, Long> tokenMap = new HashMap<>();
public PositionViewModel(#NonNull Application application) {
super(application);
observer = data -> createTokenMap(data.getPositions());
positionResponseLiveData = PositionRepository.getInstance().getPositions();
positionResponseLiveData.observeForever(observer);
}
public LiveData<PositionResponse> getPositions() {
return positionResponseLiveData;
}
private void createTokenMap(Positions positions) {
for (Net position : positions.getNet()) {
tokenMap.put(position.getTradingsymbol(), position.getInstrument_token());
}
for (Day position : positions.getDay()) {
tokenMap.put(position.getTradingsymbol(), position.getInstrument_token());
}
}
public Map<String, Long> getTokenMap() {
return tokenMap;
}
#Override
protected void onCleared() {
super.onCleared();
positionResponseLiveData.removeObserver(this.observer);
}
}
Any help will be highly appreciated.
Here my Espresso's tests:
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import java.util.concurrent.TimeUnit
#RunWith(AndroidJUnit4::class)
class TradersActivityTest {
private lateinit var mockServer: MockWebServer
#Rule
#JvmField
var tradersIntentTestRule = IntentsTestRule(TradersActivity::class.java, false, false)
#Before
fun setup() {
mockServer = MockWebServer()
mockServer.start(8081)
}
#Test
fun hasTraders_noTradersTextView_isNotDisplayed() {
mockServer.enqueue(MockResponse()
.setResponseCode(200)
.setBody(FileUtil.getStringFromFile(context, ONE_TRADER_NO_WALLETS_LIST)));
tradersIntentTestRule.launchActivity(Intent())
onView(withId(R.id.noTradersTextView))
.check(matches(not(isDisplayed())))
}
#Test
fun toolBar_height() {
onView(withId(R.id.toolBar))
.check(matches(withHeightResId(R.dimen.tool_bar_height)))
}
Test hasTraders_noTradersTextView_isNotDisplayed success pass.
But test toolBar_height fail with message:
java.lang.RuntimeException: No activities found. Did you forget to launch the activity by calling getActivity() or startActivitySync or similar?
at androidx.test.espresso.base.RootViewPicker.waitForAtLeastOneActivityToBeResumed(RootViewPicker.java:169)
at androidx.test.espresso.base.RootViewPicker.get(RootViewPicker.java:83)
So I change setup method:
#Before
fun setup() {
mockServer = MockWebServer()
mockServer.start(8081)
mockServer.enqueue(MockResponse()
.setResponseCode(200)
.setBody(FileUtil.getStringFromFile(context, ONE_TRADER_NO_WALLETS_LIST)));
tradersIntentTestRule.launchActivity(Intent())
Debug.d(TAG, "SUCCCESS_START_MOCKWEBSRVER")
}
Now test toolBar_height pass, but hasTraders_noTradersTextView_isNotDisplayed fail with message:
java.lang.RuntimeException: Could not launch intent Intent { flg=0x10000000 com.myproject.android.ui.activity.TradersActivity } within 45 seconds. Perhaps the main thread has not gone idle within a reasonable amount of time? There could be an animation or something constantly repainting the screen. Or the activity is doing network calls on creation? See the threaddump logs. For your reference the last time the event queue was idle before your activity launch request was 1556373134135 and now the last time the queue went idle was: 1556373143180. If these numbers are the same your activity might be hogging the event queue.
at androidx.test.runner.MonitoringInstrumentation.startActivitySync(MonitoringInstrumentation.java:459)
P.S. I need to start activity in test hasTraders_noTradersTextView_isNotDisplayed because it's a specific test.
But I wan't to start activity in test toolBar_height
In my UI automated tests, I have this classes:
public class UiTestHelper {
public static Matcher<View> childAtPosition(
final Matcher<View> parentMatcher, final int position) {
return new TypeSafeMatcher<View>() {
#Override
public void describeTo(Description description) {
description.appendText("Child at position " + position + " in parent ");
parentMatcher.describeTo(description);
}
#Override
public boolean matchesSafely(View view) {
ViewParent parent = view.getParent();
return parent instanceof ViewGroup && parentMatcher.matches(parent)
&& view.equals(((ViewGroup) parent).getChildAt(position));
}
};
}
public static void clickOn(View view) {
waitUntilVisible(view);
onView(withId(view.getId())).perform(click());
}
public static void waitUntilVisible(View view) {
ViewVisibilityIdlingResource idlingResource = new ViewVisibilityIdlingResource(view, VISIBLE);
IdlingRegistry.getInstance().register(idlingResource);
}
public static void unregisterAllIdlingResources() {
IdlingRegistry idlingRegistry = IdlingRegistry.getInstance();
for (IdlingResource resource: idlingRegistry.getResources()) {
idlingRegistry.unregister(resource);
}
}
}
and
public class ViewVisibilityIdlingResource implements IdlingResource {
private final View view;
private final int expectedVisibility;
private boolean idle;
private ResourceCallback resourceCallback;
public ViewVisibilityIdlingResource(final View view, final int expectedVisibility) {
this.view = view;
this.expectedVisibility = expectedVisibility;
this.idle = false;
this.resourceCallback = null;
}
#Override
public final String getName() {
return ViewVisibilityIdlingResource.class.getSimpleName();
}
#Override
public final boolean isIdleNow() {
idle = idle || (view.getVisibility() == expectedVisibility && view.getWidth() > 0 && view.getHeight() > 0);
if (idle) {
if (resourceCallback != null) {
resourceCallback.onTransitionToIdle();
}
}
return idle;
}
#Override
public void registerIdleTransitionCallback(ResourceCallback resourceCallback) {
this.resourceCallback = resourceCallback;
}
}
Then when I want to check that a view is there I call
UiTestHelper.waitUntilVisible(view);
I have done like docs here but Live data'a value is not changing. Please tell me what am i doing wrong.
MainActivity
public class MainActivity extends AppCompatActivity {
private NameViewModel mModel;
private ActivityMainBinding binding;
int index = 0;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
binding.button.setOnClickListener((v) -> {
mModel.getCurrentName().setValue("Test");
});
mModel = ViewModelProviders.of(this).get(NameViewModel.class);
final Observer<String> nameObserver = (text) -> {
binding.textInputLayout.getEditText().setText(text);
};
mModel.getCurrentName().observe(this, nameObserver);
}
}
NameViewModel.java
public class NameViewModel extends ViewModel {
private MutableLiveData<String> mCurrentName;
public MutableLiveData<String> getCurrentName() {
if (mCurrentName == null) {
return new MutableLiveData<>();
}
return mCurrentName;
}
}
This is because, your logic returns new instance of mCurrentName each time. Please use the following function.
public class NameViewModel extends ViewModel {
private MutableLiveData<String> mCurrentName;
public MutableLiveData<String> getCurrentName() {
// Ensure there is only 1 instance of mCurrentName
if (mCurrentName == null) {
mCurrentName = new MutableLiveData<>();
}
return mCurrentName;
}
}
A much better and safer way (reduce chance of making such mistake), is to initialize mCurrentName in constructor, and mark it as final.
public class NameViewModel extends ViewModel {
private final MutableLiveData<String> mCurrentName;
public NameViewModel() {
mCurrentName = new MutableLiveData<>();
}
public MutableLiveData<String> getCurrentName() {
return mCurrentName;
}
}
I've never create unit testing before. I'm planning to create UI test & Unit test for my presenter & datasource. I use Retrofit, RxJava, and Dagger in my apps.
Here's what i've tried so far
DataSource (My Datasource is coming from API)
public class DataSource implements DataSourceContract {
private static DataSource dataSource;
#Inject
SharedPreferences sharedPreferences;
#Inject
NewsService newsService;
private DataSource(Context context) {
DaggerAppComponent.builder()
.networkModule(new NetworkModule(API_URL))
.appModule(new AppModule(context.getApplicationContext()))
.preferencesModule(new PreferencesModule())
.build()
.inject(this);
}
public static synchronized DataSource getInstance(Context context) {
if(dataSource == null) {
dataSource = new DataSource(context);
}
return dataSource;
}
public String parseError(Throwable e) {
if(e instanceof SocketTimeoutException) {
return ERROR_TIMEOUT;
}
else if(e instanceof SocketException) {
return ERROR_NO_CONNECTION;
}
else {
return ERROR_SERVER;
}
}
#Override
public DisposableObserver<NewsResponse> getNews(final Callback<NewsResponse> callback) {
return newsService.getNews()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeWith(new DisposableObserver<NewsResponse>() {
#Override
public void onNext(NewsResponse value) {
callback.onSuccess(value);
}
#Override
public void onError(Throwable e) {
callback.onFailure(e);
}
#Override
public void onComplete() {
}
});
}
}
Presenter
public class MainPresenter implements MainContract.Presenter {
private MainContract.View view;
private DataSource dataSource;
private Disposable dispossable;
public MainPresenter(MainContract.View view, DataSource dataSource) {
this.view = view;
this.dataSource = dataSource;
}
#Override
public void onStart() {
getNews();
}
#Override
public void onStop() {
if(dispossable != null && !dispossable.isDisposed()) {
dispossable.dispose();
}
}
#Override
public void getNews() {
view.setLoading(true);
dispossable = dataSource.getNews(new DataSourceContract.Callback<NewsResponse>() {
#Override
public void onSuccess(NewsResponse responseData) {
try {
switch (responseData.getStatus()) {
case API_SUCCESS:
view.setLoading(false);
view.getNewsSuccess(responseData.getArticles());
break;
default:
view.setLoading(false);
view.getNewsFailed(responseData.getStatus());
break;
}
}
catch (Exception e) {
view.setLoading(false);
view.getNewsFailed(ERROR_SERVER);
}
}
#Override
public void onFailure(Throwable e) {
view.setLoading(false);
view.isNetworkFailed(dataSource.parseError(e), false);
}
});
}
}
And this is the test of my presenter
public class MainPresenterTest {
#Mock
DataSource dataSource;
#Mock
MainContract.View view;
MainContract.Presenter presenter;
#Before
public void setup() {
MockitoAnnotations.initMocks(this);
presenter = new MainPresenter(view, dataSource);
}
#Test
public void getNews() throws Exception {
List<Article> articleList = new ArrayList<>();
presenter.getNews();
Mockito.verify(view, Mockito.only()).getNewsSuccess(articleList);
}
}
But there is error when I run the test
Wanted but not invoked:
view.getNewsSuccess([]);
-> at com.java.mvp.view.main.MainPresenterTest.getNews(MainPresenterTest.java:37)
I have no problem running this apps on the device, but I can't make it work on testing
Any idea how to fix this presenter test? Am I doing it right?
And how do I test my datasource? I have no idea how to test this one
Thank you
Keep things simple. You are testing your presenter, not the data source. Add new methods to your presenter for the success and error responses. Then add two tests: one for the success and one for the error.
#Override
public void getNews() {
view.setLoading(true);
dispossable = dataSource.getNews(new DataSourceContract.Callback<NewsResponse>() {
#Override
public void onSuccess(NewsResponse responseData) {
onSuccessNewsResponse(responseData);
}
#Override
public void onFailure(Throwable e) {
onErrorNewsResponse(e);
}
});
}
Add #VisibleForTesting annotation to the new methods.
Success test:
#Test
public void getNewsSuccess() {
presenter.onSuccessNewsResponse(your_response);
Mockito.verify(...);
}
Error test:
#Test
public void getNewsError() {
presenter.onErrorNewsResponse(your_error);
Mockito.verify(...);
}
You have to mock also :
dataSource.getNews() using Mockito when :
e.g.
when(dataSource.getNews()).thenReturn(new SuccessCallback());
So you have to lead your test code into the success callback and check there what methods are called.
The same goes with the eroor case.
I have a form with 4 possible options that need to be checked (could be less as well depending on circumstances). There are 2 editexts, one for email and one for a reference field when creating an order.
The email and reference fields may or may not be left empty based on conditions (which are available when the form is created). Additionally we may need to show an alert dialog to tell the user that it may not be possible to show the reference value (to the recipient of the order) and they may also need to agree to a terms and condition alert dialog.
Currently the onConfirm checks is something like this,
void onCreateOrderConfirmed(#Nullable final String receiverEmail,
#Nullable final String reference,
#Nullable final Boolean noRefAgreed,
#Nullable final Boolean termsAndConditionsAgreed) {
if (!reviewCompletionState.emailRequirementSatisfied()) {
if (!isValidEmail(receiverEmail)) {
view.showEmailError();
return;
}
reviewCompletionState = reviewCompletionState.newBuilder()
.receiverEmail(receiverEmail)
.emailRequirementSatisfied(true)
.build();
}
if (!reviewCompletionState.referenceRequirementSatisfied()) {
if (isEmpty(reference)) {
view.showReferenceError();
return;
}
reviewCompletionState = reviewCompletionState.newBuilder()
.reference(reference)
.referenceRequirementSatisfied(true)
.build();
}
if (!reviewCompletionState.noRefAgreed()) {
if (noRefAgreed == null || !noRefAgreed) {
view.showNoReferenceAlert();
return;
}
reviewCompletionState = reviewCompletionState.newBuilder()
.noRefAgreed(true)
.build();
}
if (!reviewCompletionState.termsAndConditionsAgreed()) {
if (termsAndConditionsAgreed == null || !termsAndConditionsAgreed) {
view.showTermsDisclaimerAlert();
return;
}
reviewCompletionState = reviewCompletionState.newBuilder()
.termsAndConditionsAgreed(true)
.build();
}
createOrder();
}
I would love to know if there is a way to make this validation simpler with RxJava2? (but don't currently know enough to be able to do this)
TIA
This can be a simple. There will be a lot of code, I'll show the result first.
private ReviewValidator reviewValidator = new ReviewValidator();
void onCreateOrderConfirmed(#Nullable final String receiverEmail,
#Nullable final String reference,
#Nullable final Boolean noRefAgreed,
#Nullable final Boolean termsAndConditionsAgreed) {
ReviewState reviewState = new ReviewState(receiverEmail,
reference,
noRefAgreed,
termsAndConditionsAgreed);//another model for simplicity
reviewValidator.validate(reviewState)
.flatMap(reviewState -> /* create order */)
.subscribe(this::onOrderCreated, this::onOrderCreatingError);
}
void onOrderCreated(Object order) {//or what you need here
//handle positive result
}
void onOrderCreatingError(Throwable throwable) {
if (throwable instanceof ValidateException) {
List<ValidateError> errors = ((ValidateException) throwable).getValidateErrors();
for (ValidateError error: errors) {
switch (error.getField()) {
case EMAIL: {
view.showEmailError();
return;//or break if you want show all errors
}
case REFERENCE: {
view.showReferenceError();
return;
}
//handle another errors....
}
}
//handle another error cases...
}
First, create model for reviewState:
public class ReviewState {
private String receiverEmail;
private String reference;
private Boolean noRefAgreed;
private Boolean termsAndConditionsAgree;
public ReviewState(String receiverEmail,
String reference,
Boolean noRefAgreed,
Boolean termsAndConditionsAgree) {
this.receiverEmail = receiverEmail;
this.reference = reference;
this.noRefAgreed = noRefAgreed;
this.termsAndConditionsAgree = termsAndConditionsAgree;
}
public String getReceiverEmail() {
return receiverEmail;
}
public String getReference() {
return reference;
}
public Boolean getNoRefAgreed() {
return noRefAgreed;
}
public Boolean getTermsAndConditionsAgree() {
return termsAndConditionsAgree;
}
}
Then create you own validator. It is not necessary to create a whole model, you can create validator for every field and and link them with flatMap(), your choice.
public class ReviewValidator extends Validator<ReviewState> {
#Override
protected List<ValidateFunction> getValidateFunctions(ReviewState reviewState) {
List<ValidateFunction> validateFunctions = new LinkedList<>();
validateFunctions.add(() -> validateEmail(reviewState.getReceiverEmail()));
validateFunctions.add(() -> validateReference(reviewState.getReference()));
//another validation methods
return validateFunctions;
}
private ValidateError validateEmail(String email) {
if (TextUtils.isEmpty(email)) {
return new ValidateError(Field.EMAIL);//Field.EMAIL - just enum
}
return null;
}
private ValidateError validateReference(String reference) {
if (TextUtils.isEmpty(reference)) {
return new ValidateError(Field.REFERENCE);
}
return null;
}
//....
//another validation methods
}
Abstract class for validator:
public abstract class Validator<Model> {
public Single<Model> validate(Model model) {
return Single.just(model)
.map(this::validateModel)
.flatMap(this::processResult);
}
private Single<Model> processResult(ValidateResultModel<Model> validateResultModel) {
return Single.create(subscriber -> {
List<ValidateError> validateErrors = validateResultModel.getValidateErrors();
if (validateErrors.isEmpty()) {
subscriber.onSuccess(validateResultModel.getModel());
} else {
subscriber.onError(new ValidateException(validateErrors));
}
});
}
private ValidateResultModel<Model> validateModel(Model model) {
List<ValidateError> errors = new LinkedList<>();
for (ValidateFunction validateFunctions : getValidateFunctions(model)) {
ValidateError error = validateFunctions.validate();
if (error != null) {
errors.add(error);
}
}
return new ValidateResultModel<>(model, errors);
}
protected abstract List<ValidateFunction> getValidateFunctions(Model model);
protected interface ValidateFunction {
#Nullable
ValidateError validate();
}
}
Helper classes for validator...
public class ValidateError {
private Field field;
public ValidateError(Field field) {
this.field = field;
}
public Field getField() {
return field;
}
}
class ValidateResultModel<T> {
private T model;
private List<ValidateError> validateErrors;
ValidateResultModel(T model, List<ValidateError> validateErrors) {
this.model = model;
this.validateErrors = validateErrors;
}
T getModel() {
return model;
}
List<ValidateError> getValidateErrors() {
return validateErrors;
}
}
public class ValidateException extends RuntimeException {
private List<ValidateError> validateErrors;
ValidateException(List<ValidateError> validateErrors) {
this.validateErrors = validateErrors;
}
public List<ValidateError> getValidateErrors() {
return validateErrors;
}
}
Initially, I took the idea from here: https://github.com/matzuk/TestableCodeMobius/tree/master/app/src/main/java/com/matsyuk/testablecodemobius/business/transfer/validation
i think you should the RxJava CombineLatest, so you need all of the form input are producing an observable, then you just combine it and adjust the view
as a reference you can check:
https://medium.com/#etiennelawlor/rxjava-on-the-sign-in-screen-9ecb66b88572
Using RxJava for email login validation, an observable is emitting twice
========
example:
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.Toast;
import com.jakewharton.rxbinding2.view.RxView;
import com.jakewharton.rxbinding2.widget.RxCompoundButton;
import com.jakewharton.rxbinding2.widget.RxTextView;
import io.reactivex.Observable;
public class MainActivity extends AppCompatActivity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
EditText receiverText = findViewById(R.id.input_receiver);
EditText referenceText = findViewById(R.id.input_reference);
CheckBox checkRef = findViewById(R.id.check_ref);
CheckBox checkTerms = findViewById(R.id.check_terms);
Button buttonLogin = findViewById(R.id.button_login);
Observable<CharSequence> receiverObservable = RxTextView.textChanges(receiverText).skip(1); // can add more logic
Observable<CharSequence> referenceObservable = RxTextView.textChanges(referenceText).skip(1); // can add more logic
Observable<Boolean> refCheckObservable = RxCompoundButton.checkedChanges(checkRef); // can add more logic
Observable<Boolean> termsCheckObservable = RxCompoundButton.checkedChanges(checkTerms); // can add more logic
Observable<String> combineObservable = Observable.combineLatest(
receiverObservable,
referenceObservable,
refCheckObservable,
termsCheckObservable, (receiverCharSequence, referenceCharSequence, refBoolean, termsBoolean) -> {
// add logic here for now it is only combine the input
return receiverCharSequence + " " + referenceCharSequence + " " + refBoolean + " " + termsBoolean ;}
);
RxView.clicks(buttonLogin).flatMap(o -> { return combineObservable;}).distinctUntilChanged().subscribe(string -> {
Toast.makeText(this, string, Toast.LENGTH_LONG).show();
});
}
}