| package ioc.specs |
| |
| import org.apache.tapestry5.internal.plastic.PlasticClassLoader |
| import org.apache.tapestry5.internal.plastic.PlasticInternalUtils |
| import org.apache.tapestry5.internal.plastic.asm.ClassWriter |
| import org.apache.tapestry5.ioc.def.ServiceDef3 |
| import org.apache.tapestry5.ioc.internal.services.PlasticProxyFactoryImpl |
| import org.apache.tapestry5.ioc.services.PlasticProxyFactory |
| import org.slf4j.Logger |
| import spock.lang.Shared |
| import spock.lang.Specification |
| import spock.lang.Unroll |
| import org.apache.tapestry5.ioc.* |
| import org.apache.tapestry5.ioc.internal.* |
| |
| import static org.apache.tapestry5.internal.plastic.asm.Opcodes.* |
| |
| class DefaultModuleDefImplSpec extends Specification { |
| |
| @Shared |
| PlasticProxyFactory proxyFactory = new PlasticProxyFactoryImpl(Thread.currentThread().contextClassLoader, null) |
| |
| @Shared |
| OperationTracker tracker = new QuietOperationTracker() |
| |
| Logger logger = Mock() |
| |
| def "toString() of module lists services in the module"() { |
| when: |
| |
| def md = module SimpleModule |
| |
| then: |
| |
| md.toString() == "ModuleDef[$SimpleModule.name Barney, Fred, Wilma]" |
| } |
| |
| def "serviceIds contains all service ids"() { |
| def md = module SimpleModule |
| |
| expect: |
| |
| md.serviceIds == ["Fred", "Barney", "Wilma"] as Set |
| } |
| |
| def "ServiceDef obtainable by service id"() { |
| def md = module SimpleModule |
| |
| when: |
| |
| def sd = md.getServiceDef "fred" |
| |
| then: |
| |
| sd.serviceId == "Fred" |
| sd.serviceInterface == FieService |
| sd.toString().contains "${SimpleModule.name}.buildFred()" |
| sd.serviceScope == ScopeConstants.DEFAULT |
| !sd.eagerLoad |
| sd.markers.empty |
| |
| when: |
| |
| sd = md.getServiceDef("Wilma") |
| |
| then: |
| |
| sd.eagerLoad |
| } |
| |
| def "ModuleDef exposes decorator methods as DecoratorDefs"() { |
| def md = module SimpleModule |
| |
| when: |
| |
| def decos = md.decoratorDefs |
| |
| then: |
| |
| decos.size() == 1 |
| |
| def deco = decos.find() |
| |
| deco.decoratorId == "Logging" |
| deco.toString().contains "${SimpleModule.name}.decorateLogging(Class, Object)" |
| } |
| |
| def "@ServiceId annotation on service builder method overrides naming convention"() { |
| when: |
| |
| def md = module ServiceIdViaAnnotationModule |
| |
| then: |
| |
| md.getServiceDef("FooService") != null |
| } |
| |
| def "@ServiceId on implementation class overrides default id from ServiceBinder.bind() default"() { |
| when: |
| |
| def md = module ServiceIdViaAnnotationModule |
| |
| then: |
| |
| md.getServiceDef("BarneyService") != null |
| } |
| |
| def "@Named annotation on service builder method overrides naming convention"() { |
| when: |
| |
| def md = module NamedServiceModule |
| |
| then: |
| |
| md.getServiceDef("BazService") != null |
| } |
| |
| def "@Named annotation on service implementation class overrides ServiceBinder.bind() default"() { |
| when: |
| |
| def md = module NamedServiceModule |
| |
| then: |
| |
| md.getServiceDef("QuuxService") != null |
| } |
| |
| def "naming convention for a service builder method named build() is derived from the return type"() { |
| when: |
| |
| def md = module DefaultServiceIdModule |
| |
| then: |
| |
| md.getServiceDef("FieService") != null |
| } |
| |
| def "conflicting service ids result in an exception"() { |
| when: |
| |
| module ServiceIdConflictMethodModule |
| |
| then: |
| |
| RuntimeException ex = thrown() |
| |
| ex.message.contains "Service Fred (defined by ${ServiceIdConflictMethodModule.name}.buildFred()" |
| ex.message.contains "conflicts with previously defined service defined by ${ServiceIdConflictMethodModule.name}.buildFred(Object)" |
| } |
| |
| def "a service builder method may not return void"() { |
| when: |
| |
| module VoidBuilderMethodModule |
| |
| then: |
| |
| RuntimeException ex = thrown() |
| |
| ex.message.contains "${VoidBuilderMethodModule.name}.buildNull()" |
| ex.message.contains "but the return type (void) is not acceptable" |
| } |
| |
| def "a service builder method may not return an array"() { |
| when: |
| |
| module BuilderMethodModule |
| |
| then: |
| |
| RuntimeException ex = thrown() |
| |
| ex.message.contains "${BuilderMethodModule.name}.buildStringArray()" |
| ex.message.contains "but the return type (java.lang.String[])" |
| } |
| |
| @Unroll |
| def "A decorator method #desc"() { |
| when: |
| |
| module moduleClass |
| |
| then: |
| |
| RuntimeException e = thrown() |
| |
| e.message.contains expectedText |
| |
| where: |
| |
| moduleClass | expectedText | desc |
| PrimitiveDecoratorMethodModule | "decoratePrimitive" | "may not return a primitive type" |
| ArrayDecoratorMethodModule | "decorateArray" | "may not return an array" |
| } |
| |
| @Unroll |
| def "#desc"() { |
| when: |
| |
| def md = module moduleClass |
| |
| then: |
| |
| def defs = md.contributionDefs |
| |
| defs.size() == 1 |
| |
| def cd = defs.find() |
| |
| cd.serviceId == serviceId |
| |
| cd.toString().contains "${moduleClass.name}.$methodSignature" |
| |
| where: |
| |
| moduleClass | serviceId | methodSignature | desc |
| SimpleModule | "Barney" | "contributeBarney(Configuration)" | "contribution without annotation to configuration" |
| OrderedConfigurationModule | "Ordered" | "contributeOrdered(OrderedConfiguration)" | "contribution to ordered configuration" |
| MappedConfigurationModule | "Mapped" | "contributeMapped(MappedConfiguration)" | "contribution to mapped configuration" |
| } |
| |
| @Unroll |
| def "service contribution method that #desc throws an exception"() { |
| |
| when: |
| |
| module moduleClass |
| |
| then: |
| |
| RuntimeException e = thrown() |
| |
| e.message.contains message |
| |
| where: |
| |
| moduleClass | message | desc |
| |
| NoUsableContributionParameterModule | "does not contain a parameter of type Configuration, OrderedConfiguration or MappedConfiguration" | "does not include configuration parameter" |
| TooManyContributionParametersModule | "contains more than one parameter of type Configuration, OrderedConfiguration, or MappedConfiguration" | "includes more than one configuration parameter" |
| } |
| |
| def "using defaults for ServiceBinder.bind()"() { |
| |
| when: |
| |
| def md = module AutobuildModule |
| ServiceDef3 sd = md.getServiceDef "stringholder" |
| |
| then: |
| |
| sd.serviceInterface == StringHolder |
| sd.serviceId == "StringHolder" |
| sd.serviceScope == ScopeConstants.DEFAULT |
| !sd.isEagerLoad() |
| sd.markers.empty |
| !sd.preventDecoration |
| } |
| |
| def "overriding defaults for ServiceBinder.bind()"() { |
| |
| when: |
| |
| def md = module ComplexAutobuildModule |
| ServiceDef3 sd = md.getServiceDef "sh" |
| |
| then: |
| |
| sd.serviceInterface == StringHolder |
| sd.serviceId == "SH" |
| sd.serviceScope == "magic" |
| sd.eagerLoad |
| sd.preventDecoration |
| } |
| |
| def "implementation class for ServiceBinder.bind() must have a public constructor"() { |
| when: |
| |
| module UninstantiableAutobuildServiceModule |
| |
| then: |
| |
| RuntimeException e = thrown() |
| |
| e.message.contains "Class org.apache.tapestry5.ioc.internal.RunnableServiceImpl (implementation of service 'Runnable') does not contain any public constructors." |
| } |
| |
| def "the bind() method of a module class must be a static method"() { |
| when: |
| |
| module NonStaticBindMethodModule |
| |
| then: |
| |
| RuntimeException e = thrown() |
| |
| e.message.contains "Method org.apache.tapestry5.ioc.internal.NonStaticBindMethodModule.bind(ServiceBinder)" |
| e.message.contains "appears to be a service binder method, but is an instance method, not a static method" |
| } |
| |
| def "when autobuilding a service implementation, the constructor with the most parameters is chosen"() { |
| ServiceBuilderResources resources = Mock() |
| |
| when: |
| |
| def md = module MutlipleAutobuildServiceConstructorsModule |
| |
| def sd = md.getServiceDef "stringholder" |
| |
| then: |
| |
| sd != null |
| |
| 0 * _ |
| |
| when: |
| |
| def oc = sd.createServiceCreator(resources) |
| def holder = oc.createObject() |
| |
| holder.value = "foo" |
| |
| then: |
| |
| holder instanceof StringHolder |
| holder.value == "FOO" |
| |
| _ * resources.serviceId >> "StringHolder" |
| _ * resources.logger >> logger |
| _ * resources.serviceInterface >> StringHolder |
| 1 * resources.getService("ToUpperCaseStringHolder", StringHolder) >> new ToUpperCaseStringHolder() |
| _ * resources.tracker >> tracker |
| |
| 1 * logger.debug(_) >> { args -> |
| assert args[0].contains( |
| "Invoking constructor org.apache.tapestry5.ioc.internal.MultipleConstructorsAutobuildService(StringHolder)") |
| } |
| |
| 0 * _ |
| } |
| |
| def "an exception inside a bind() method bubbles up"() { |
| when: |
| |
| module ExceptionInBindMethod |
| |
| then: |
| |
| RuntimeException e = thrown() |
| |
| e.message.contains "Error invoking service binder method org.apache.tapestry5.ioc.internal.ExceptionInBindMethod.bind(ServiceBinder)" |
| e.message.contains "at ExceptionInBindMethod.java" |
| e.message.contains "Really, how often is this going to happen?" |
| } |
| |
| def "@EagerLoad annotation on service implementation class is reflected in the ServiceDef"() { |
| when: |
| |
| def md = module EagerLoadViaAnnotationModule |
| def sd = md.getServiceDef "runnable" |
| |
| then: |
| |
| sd.eagerLoad |
| } |
| |
| private DefaultModuleDefImpl module(moduleClass) { |
| new DefaultModuleDefImpl(moduleClass, logger, proxyFactory) |
| } |
| |
| def "marker annotations on the service builder method are available in the ServiceDef"() { |
| |
| when: |
| |
| def md = module MarkerModule |
| def sd = md.getServiceDef "greeter" |
| |
| then: |
| |
| sd.markers == [BlueMarker] as Set |
| } |
| |
| def "marker annotations specified via ServiceBinder is available in the ServiceDef"() { |
| when: |
| |
| def md = module MarkerModule |
| def sd = md.getServiceDef "redgreeter" |
| |
| then: |
| |
| sd.markers == [RedMarker] as Set |
| } |
| |
| def "marker annotation on the implementation class is available in the ServiceDef"() { |
| when: |
| |
| def md = module MarkerModule |
| def sd = md.getServiceDef "SecondRedGreeter" |
| |
| then: |
| |
| sd.markers == [RedMarker] as Set |
| } |
| |
| def "marker annotation from ServiceBinder and implementation class are merged"() { |
| when: |
| |
| def md = module MarkerModule |
| def sd = md.getServiceDef "SurprisinglyBlueGreeter" |
| |
| then: |
| |
| sd.markers == [RedMarker, BlueMarker] as Set |
| } |
| |
| def "Multiple marker annotations can be added to service via ServiceBindingOptions"() { |
| when: |
| |
| def md = module MarkerModule |
| def sd = md.getServiceDef "ColorfulGreeter" |
| |
| then: |
| |
| sd.markers == [RedMarker, BlueMarker] as Set |
| } |
| |
| def "public synthetic methods on module class are ignored"() { |
| def moduleClass = createSyntheticModuleClass() |
| |
| when: |
| |
| def md = module moduleClass |
| |
| then: |
| |
| md.serviceIds.size() == 1 |
| } |
| |
| def "Methods overridden from Object are ignored"() { |
| |
| when: |
| |
| def md = module ModuleWithOverriddenObjectMethods |
| |
| then: |
| |
| md.serviceIds.size() == 1 |
| } |
| |
| |
| private createSyntheticModuleClass() { |
| |
| def cw = new ClassWriter(ClassWriter.COMPUTE_MAXS + ClassWriter.COMPUTE_FRAMES) |
| |
| cw.visit(V1_5, ACC_PUBLIC, "EnhancedSyntheticMethodModule", null, |
| PlasticInternalUtils.toInternalName(SyntheticMethodModule.name), null); |
| |
| def mv = cw.visitMethod ACC_PUBLIC | ACC_STATIC | ACC_SYNTHETIC, "synth", "()V", null, null |
| mv.visitCode() |
| mv.visitInsn RETURN |
| mv.visitEnd() |
| |
| cw.visitEnd() |
| |
| def bytecode = cw.toByteArray() |
| |
| ClassLoader loader = Thread.currentThread().contextClassLoader |
| |
| PlasticClassLoader plasticLoader = new PlasticClassLoader(loader, new NoopClassLoaderDelegate()) |
| |
| return plasticLoader.defineClassWithBytecode("EnhancedSyntheticMethodModule", bytecode) |
| } |
| } |