/* * Copyright (c) 2017-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. */ package com.facebook.litho.specmodels.model; import javax.lang.model.element.Modifier; import java.lang.annotation.Annotation; import java.util.List; import com.facebook.litho.annotations.FromBind; import com.facebook.litho.annotations.FromPrepare; import com.facebook.litho.annotations.OnBind; import com.facebook.litho.annotations.OnCreateLayout; import com.facebook.litho.annotations.OnCreateLayoutWithSizeSpec; import com.facebook.litho.annotations.OnCreateMountContent; import com.facebook.litho.annotations.OnMount; import com.facebook.litho.annotations.OnPrepare; import com.facebook.litho.annotations.OnUnmount; import com.facebook.litho.specmodels.internal.ImmutableList; import com.facebook.litho.testing.specmodels.TestMethodParamModel; import com.squareup.javapoet.ClassName; import com.squareup.javapoet.TypeName; import org.junit.Before; import org.junit.Test; import static org.assertj.core.api.Java6Assertions.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; /** * Tests {@link DelegateMethodValidation} */ public class DelegateMethodValidationTest { private final LayoutSpecModel mLayoutSpecModel = mock(LayoutSpecModel.class); private final MountSpecModel mMountSpecModel = mock(MountSpecModel.class); private final Object mModelRepresentedObject = new Object(); private final Object mMountSpecObject = new Object(); private final Object mDelegateMethodObject1 = new Object(); private final Object mDelegateMethodObject2 = new Object(); private final Object mMethodParamObject1 = new Object(); private final Object mMethodParamObject2 = new Object(); private final Object mMethodParamObject3 = new Object(); private DelegateMethodModel mOnCreateMountContent; private final Object mOnCreateMountContentObject = new Object(); @Before public void setup() { when(mLayoutSpecModel.getRepresentedObject()).thenReturn(mModelRepresentedObject); when(mMountSpecModel.getRepresentedObject()).thenReturn(mMountSpecObject); mOnCreateMountContent = new DelegateMethodModel( ImmutableList.<Annotation>of(new Annotation() { @Override public Class<? extends Annotation> annotationType() { return OnCreateMountContent.class; } }), ImmutableList.of(Modifier.STATIC), "onCreateMountContent", ClassName.bestGuess("java.lang.MadeUpClass"), ImmutableList.<MethodParamModel>of( TestMethodParamModel.newBuilder() .name("c") .type(ClassNames.COMPONENT_CONTEXT) .representedObject(new Object()) .build()), mOnCreateMountContentObject); } @Test public void testNoDelegateMethods() { when(mLayoutSpecModel.getDelegateMethods()).thenReturn(ImmutableList.<DelegateMethodModel>of()); List<SpecModelValidationError> validationErrors = DelegateMethodValidation.validateLayoutSpecModel(mLayoutSpecModel); assertThat(validationErrors).hasSize(1); assertThat(validationErrors.get(0).element).isEqualTo(mModelRepresentedObject); assertThat(validationErrors.get(0).message).isEqualTo( "You need to have a method annotated with either @OnCreateLayout " + "or @OnCreateLayoutWithSizeSpec in your spec. In most cases, @OnCreateLayout " + "is what you want."); } @Test public void testOnCreateLayoutAndOnCreateLayoutWithSizeSpec() { when(mLayoutSpecModel.getDelegateMethods()).thenReturn( ImmutableList.of( new DelegateMethodModel( ImmutableList.<Annotation>of(new Annotation() { @Override public Class<? extends Annotation> annotationType() { return OnCreateLayout.class; } }), ImmutableList.of(Modifier.STATIC), "name", ClassNames.COMPONENT_LAYOUT, ImmutableList.<MethodParamModel>of( TestMethodParamModel.newBuilder() .name("c") .type(ClassNames.COMPONENT_CONTEXT) .representedObject(new Object()) .build()), new Object()), new DelegateMethodModel( ImmutableList.<Annotation>of(new Annotation() { @Override public Class<? extends Annotation> annotationType() { return OnCreateLayoutWithSizeSpec.class; } }), ImmutableList.of(Modifier.STATIC), "name", ClassNames.COMPONENT_LAYOUT, ImmutableList.<MethodParamModel>of( TestMethodParamModel.newBuilder() .name("c") .type(ClassNames.COMPONENT_CONTEXT) .representedObject(new Object()) .build(), TestMethodParamModel.newBuilder() .name("widthSpec") .type(TypeName.INT) .representedObject(new Object()) .build(), TestMethodParamModel.newBuilder() .name("heightSpec") .type(TypeName.INT) .representedObject(new Object()) .build()), new Object()))); List<SpecModelValidationError> validationErrors = DelegateMethodValidation.validateLayoutSpecModel(mLayoutSpecModel); assertThat(validationErrors).hasSize(1); assertThat(validationErrors.get(0).element).isEqualTo(mModelRepresentedObject); assertThat(validationErrors.get(0).message).isEqualTo( "Your LayoutSpec should have a method annotated with either @OnCreateLayout " + "or @OnCreateLayoutWithSizeSpec, but not both. In most cases, @OnCreateLayout " + "is what you want."); } @Test public void testDelegateMethodDoesNotDefineEnoughParams() { when(mLayoutSpecModel.getDelegateMethods()).thenReturn( ImmutableList.<DelegateMethodModel>of( new DelegateMethodModel( ImmutableList.<Annotation>of(new Annotation() { @Override public Class<? extends Annotation> annotationType() { return OnCreateLayoutWithSizeSpec.class; } }), ImmutableList.of(Modifier.STATIC), "name", ClassNames.COMPONENT_LAYOUT, ImmutableList.<MethodParamModel>of(), mDelegateMethodObject1))); List<SpecModelValidationError> validationErrors = DelegateMethodValidation.validateLayoutSpecModel(mLayoutSpecModel); assertThat(validationErrors).hasSize(1); assertThat(validationErrors.get(0).element).isEqualTo(mDelegateMethodObject1); assertThat(validationErrors.get(0).message).isEqualTo( "Methods annotated with interface " + "com.facebook.litho.annotations.OnCreateLayoutWithSizeSpec " + "must have at least 3 parameters, and they should be of type " + "com.facebook.litho.ComponentContext, int, int."); } @Test public void testDelegateMethodDoesNotDefinedParamsOfCorrectType() { when(mLayoutSpecModel.getDelegateMethods()).thenReturn( ImmutableList.<DelegateMethodModel>of( new DelegateMethodModel( ImmutableList.<Annotation>of(new Annotation() { @Override public Class<? extends Annotation> annotationType() { return OnCreateLayout.class; } }), ImmutableList.of(Modifier.STATIC), "name", ClassNames.COMPONENT_LAYOUT, ImmutableList.<MethodParamModel>of( TestMethodParamModel.newBuilder() .type(TypeName.BOOLEAN) .representedObject(mMethodParamObject1) .build()), mDelegateMethodObject1))); List<SpecModelValidationError> validationErrors = DelegateMethodValidation.validateLayoutSpecModel(mLayoutSpecModel); assertThat(validationErrors).hasSize(1); assertThat(validationErrors.get(0).element).isEqualTo(mMethodParamObject1); assertThat(validationErrors.get(0).message).isEqualTo( "Parameter in position 0 of a method annotated with interface " + "com.facebook.litho.annotations.OnCreateLayout should be of type " + "com.facebook.litho.ComponentContext."); } @Test public void testDelegateMethodHasIncorrectOptionalParams() { when(mLayoutSpecModel.getDelegateMethods()).thenReturn( ImmutableList.<DelegateMethodModel>of( new DelegateMethodModel( ImmutableList.<Annotation>of(new Annotation() { @Override public Class<? extends Annotation> annotationType() { return OnCreateLayout.class; } }), ImmutableList.of(Modifier.STATIC), "name", ClassNames.COMPONENT_LAYOUT, ImmutableList.<MethodParamModel>of( TestMethodParamModel.newBuilder() .type(ClassNames.COMPONENT_CONTEXT) .representedObject(mMethodParamObject1) .build(), TestMethodParamModel.newBuilder() .type(TypeName.INT) .representedObject(mMethodParamObject2) .build()), mDelegateMethodObject1))); List<SpecModelValidationError> validationErrors = DelegateMethodValidation.validateLayoutSpecModel(mLayoutSpecModel); assertThat(validationErrors).hasSize(1); assertThat(validationErrors.get(0).element).isEqualTo(mMethodParamObject2); assertThat(validationErrors.get(0).message).isEqualTo( "Not a valid parameter, should be one of the following: @Prop T somePropName. " + "@TreeProp T someTreePropName. @State T someStateName. "); } @Test public void testDelegateMethodIsNotStatic() { when(mLayoutSpecModel.getDelegateMethods()).thenReturn( ImmutableList.of( new DelegateMethodModel( ImmutableList.<Annotation>of(new Annotation() { @Override public Class<? extends Annotation> annotationType() { return OnCreateLayout.class; } }), ImmutableList.<Modifier>of(), "name", ClassNames.COMPONENT_LAYOUT, ImmutableList.<MethodParamModel>of( TestMethodParamModel.newBuilder() .type(ClassNames.COMPONENT_CONTEXT) .representedObject(mMethodParamObject1) .build()), mDelegateMethodObject1))); List<SpecModelValidationError> validationErrors = DelegateMethodValidation.validateLayoutSpecModel(mLayoutSpecModel); assertThat(validationErrors).hasSize(1); assertThat(validationErrors.get(0).element).isEqualTo(mDelegateMethodObject1); assertThat(validationErrors.get(0).message).isEqualTo( "Methods in a spec that doesn't have dependency injection must be static."); } @Test public void testMountSpecHasOnCreateMountContent() { when(mMountSpecModel.getDelegateMethods()).thenReturn(ImmutableList.<DelegateMethodModel>of()); List<SpecModelValidationError> validationErrors = DelegateMethodValidation.validateMountSpecModel(mMountSpecModel); assertThat(validationErrors).hasSize(1); assertThat(validationErrors.get(0).element).isEqualTo(mMountSpecObject); assertThat(validationErrors.get(0).message).isEqualTo( "All MountSpecs need to have a method annotated with @OnCreateMountContent."); } @Test public void testSecondParameter() { when(mMountSpecModel.getDelegateMethods()).thenReturn( ImmutableList.of( mOnCreateMountContent, new DelegateMethodModel( ImmutableList.<Annotation>of(new Annotation() { @Override public Class<? extends Annotation> annotationType() { return OnMount.class; } }), ImmutableList.of(Modifier.STATIC), "onMount", TypeName.VOID, ImmutableList.<MethodParamModel>of( TestMethodParamModel.newBuilder() .type(ClassNames.COMPONENT_CONTEXT) .representedObject(mMethodParamObject1) .build(), TestMethodParamModel.newBuilder() .type(ClassNames.OBJECT) .representedObject(mMethodParamObject2) .build()), mDelegateMethodObject1), new DelegateMethodModel( ImmutableList.<Annotation>of(new Annotation() { @Override public Class<? extends Annotation> annotationType() { return OnBind.class; } }), ImmutableList.of(Modifier.STATIC), "onBind", TypeName.VOID, ImmutableList.<MethodParamModel>of( TestMethodParamModel.newBuilder() .type(ClassNames.COMPONENT_CONTEXT) .representedObject(mMethodParamObject1) .build(), TestMethodParamModel.newBuilder() .type(ClassNames.OBJECT) .representedObject(mMethodParamObject2) .build()), mDelegateMethodObject2))); List<SpecModelValidationError> validationErrors = DelegateMethodValidation.validateMountSpecModel(mMountSpecModel); assertThat(validationErrors).hasSize(2); assertThat(validationErrors.get(0).element).isEqualTo(mDelegateMethodObject1); assertThat(validationErrors.get(0).message).isEqualTo( "The second parameter of a method annotated with interface " + "com.facebook.litho.annotations.OnMount must have the same type as the " + "return type of the method annotated with @OnCreateMountContent (i.e. " + "java.lang.MadeUpClass)."); assertThat(validationErrors.get(1).element).isEqualTo(mDelegateMethodObject2); assertThat(validationErrors.get(1).message).isEqualTo( "The second parameter of a method annotated with interface " + "com.facebook.litho.annotations.OnBind must have the same type as the " + "return type of the method annotated with @OnCreateMountContent (i.e. " + "java.lang.MadeUpClass)."); } @Test public void testInterStageInputParamAnnotationNotValid() { InterStageInputParamModel interStageInputParamModel = mock(InterStageInputParamModel.class); when(interStageInputParamModel.getAnnotations()).thenReturn( ImmutableList.<Annotation>of(new Annotation() { @Override public Class<? extends Annotation> annotationType() { return FromBind.class; } })); when(interStageInputParamModel.getRepresentedObject()).thenReturn(mMethodParamObject3); when(mMountSpecModel.getDelegateMethods()).thenReturn( ImmutableList.of( mOnCreateMountContent, new DelegateMethodModel( ImmutableList.<Annotation>of(new Annotation() { @Override public Class<? extends Annotation> annotationType() { return OnUnmount.class; } }), ImmutableList.of(Modifier.STATIC), "onUnmount", TypeName.VOID, ImmutableList.<MethodParamModel>of( TestMethodParamModel.newBuilder() .type(ClassNames.COMPONENT_CONTEXT) .representedObject(mMethodParamObject1) .build(), TestMethodParamModel.newBuilder() .type(ClassName.bestGuess("java.lang.MadeUpClass")) .representedObject(mMethodParamObject2) .build(), interStageInputParamModel), mDelegateMethodObject1))); List<SpecModelValidationError> validationErrors = DelegateMethodValidation.validateMountSpecModel(mMountSpecModel); assertThat(validationErrors).hasSize(1); assertThat(validationErrors.get(0).element).isEqualTo(mMethodParamObject3); assertThat(validationErrors.get(0).message).isEqualTo( "Inter-stage input annotation is not valid for this method, please use one of the " + "following: [interface com.facebook.litho.annotations.FromPrepare, " + "interface com.facebook.litho.annotations.FromMeasure, " + "interface com.facebook.litho.annotations.FromMeasureBaseline, " + "interface com.facebook.litho.annotations.FromBoundsDefined]"); } @Test public void testInterStageInputMethodDoesNotExist() { InterStageInputParamModel interStageInputParamModel = mock(InterStageInputParamModel.class); when(interStageInputParamModel.getAnnotations()).thenReturn( ImmutableList.<Annotation>of(new Annotation() { @Override public Class<? extends Annotation> annotationType() { return FromPrepare.class; } })); when(interStageInputParamModel.getName()).thenReturn("interStageInput"); when(interStageInputParamModel.getType()).thenReturn(TypeName.INT); when(interStageInputParamModel.getRepresentedObject()).thenReturn(mMethodParamObject3); when(mMountSpecModel.getDelegateMethods()).thenReturn( ImmutableList.of( mOnCreateMountContent, new DelegateMethodModel( ImmutableList.<Annotation>of(new Annotation() { @Override public Class<? extends Annotation> annotationType() { return OnMount.class; } }), ImmutableList.of(Modifier.STATIC), "onMount", TypeName.VOID, ImmutableList.<MethodParamModel>of( TestMethodParamModel.newBuilder() .type(ClassNames.COMPONENT_CONTEXT) .representedObject(mMethodParamObject1) .build(), TestMethodParamModel.newBuilder() .type(ClassName.bestGuess("java.lang.MadeUpClass")) .representedObject(mMethodParamObject2) .build(), interStageInputParamModel), mDelegateMethodObject1))); List<SpecModelValidationError> validationErrors = DelegateMethodValidation.validateMountSpecModel(mMountSpecModel); for (SpecModelValidationError validationError : validationErrors) { System.out.println(validationError.message); } assertThat(validationErrors).hasSize(1); assertThat(validationErrors.get(0).element).isEqualTo(mMethodParamObject3); assertThat(validationErrors.get(0).message).isEqualTo( "To use interface com.facebook.litho.annotations.FromPrepare on param interStageInput " + "you must have a method annotated with interface " + "com.facebook.litho.annotations.OnPrepare that has a param Output<java.lang.Integer> " + "interStageInput"); } @Test public void testInterStageInputOutputDoesNotExist() { InterStageInputParamModel interStageInputParamModel = mock(InterStageInputParamModel.class); when(interStageInputParamModel.getAnnotations()).thenReturn( ImmutableList.<Annotation>of(new Annotation() { @Override public Class<? extends Annotation> annotationType() { return FromPrepare.class; } })); when(interStageInputParamModel.getName()).thenReturn("interStageInput"); when(interStageInputParamModel.getType()).thenReturn(TypeName.INT); when(interStageInputParamModel.getRepresentedObject()).thenReturn(mMethodParamObject3); when(mMountSpecModel.getDelegateMethods()).thenReturn( ImmutableList.of( mOnCreateMountContent, new DelegateMethodModel( ImmutableList.<Annotation>of(new Annotation() { @Override public Class<? extends Annotation> annotationType() { return OnMount.class; } }), ImmutableList.of(Modifier.STATIC), "onMount", TypeName.VOID, ImmutableList.<MethodParamModel>of( TestMethodParamModel.newBuilder() .name("c") .type(ClassNames.COMPONENT_CONTEXT) .representedObject(mMethodParamObject1) .build(), TestMethodParamModel.newBuilder() .name("param") .type(ClassName.bestGuess("java.lang.MadeUpClass")) .representedObject(mMethodParamObject2) .build(), interStageInputParamModel), mDelegateMethodObject1), new DelegateMethodModel( ImmutableList.<Annotation>of(new Annotation() { @Override public Class<? extends Annotation> annotationType() { return OnPrepare.class; } }), ImmutableList.of(Modifier.STATIC), "onPrepare", TypeName.VOID, ImmutableList.<MethodParamModel>of( TestMethodParamModel.newBuilder() .name("c") .type(ClassNames.COMPONENT_CONTEXT) .representedObject(mMethodParamObject1) .build()), mDelegateMethodObject2))); List<SpecModelValidationError> validationErrors = DelegateMethodValidation.validateMountSpecModel(mMountSpecModel); for (SpecModelValidationError validationError : validationErrors) { System.out.println(validationError.message); } assertThat(validationErrors).hasSize(1); assertThat(validationErrors.get(0).element).isEqualTo(mMethodParamObject3); assertThat(validationErrors.get(0).message).isEqualTo( "To use interface com.facebook.litho.annotations.FromPrepare on param interStageInput " + "your method annotated with interface com.facebook.litho.annotations.OnPrepare must " + "have a param Output<java.lang.Integer> interStageInput"); } }