Uploaded image for project: 'Spring Framework'
  1. Spring Framework
  2. SPR-15692

Kotlin unable to inherit type for WebTestClient#BodySpec

    Details

    • Type: Bug
    • Status: Resolved
    • Priority: Minor
    • Resolution: Won't Fix
    • Affects Version/s: 5.0 RC2
    • Fix Version/s: None
    • Component/s: Test, Web
    • Labels:

      Description

      It seems that due to recursive generics in BodySpec interface

      interface BodySpec<B, S extends BodySpec<B, S>>
      

      and due to expectBody method returns

      <B> BodySpec<B, ?> expectBody(Class<B> bodyType);
      

      WebTestClient cannot be used in Kotlin.

      Kotlin inherits the result of .expectBody(Person::class.java) as BodySpec<Person, *> and thus the following methods in chain cannot be constructed due to the following error:

      Error:(25, 20) Kotlin: Type inference failed: Not enough information to infer parameter T in fun <T : Nothing!> isEqualTo(p0: Controller.Person!): T!
      Please specify it explicitly.

      And it applies only Nothing as a type parameter.
      But in this case generated bytecode contains the following line

      throw null
      

      Example:

          @Test
          fun `test get`() {
              val expectBody: BodySpec<Person, *> = client.get().uri("/person/42").exchange()
                      .expectBody(Person::class.java)
              expectBody.isEqualTo(Person("42", "Ivan"))                            // doesn't compile here
              expectBody.isEqualTo<BodySpec<Person, *>>(Person("42", "Ivan"))       // doesn't compile here
              expectBody.isEqualTo<Nothing>(Person("42", "Ivan"))                   // compile but lead to "throw null" in bytecode
          }
      

      If you work with list the situation is a bit better - Kotlin still cannot inherit type param automatically but you can specify it explicitly due to method expectBodyList in interface ListBodySpec doesn't return wildcards

      <E> ListBodySpec<E> expectBodyList(Class<E> elementType);
      

      Example:

      @Test
          fun `test list`() {
              val expectBodyList: ListBodySpec<Person> = client.get().uri("/person").exchange()
                      .expectBodyList(Person::class.java)
              expectBodyList.consumeWith<ListBodySpec<Person>> { list -> Assert.assertTrue(true) }   // need to specify type param explicitly
          }
      

      Full example with java and kotlin can be found here.
      Tests in java works well in these cases.

        Activity

        Hide
        sdeleuze Sébastien Deleuze added a comment -

        Good catch, I have raised the point on Kotlin issue tracker (see this KT-5464 comment) cc Rossen Stoyanchev.

        Show
        sdeleuze Sébastien Deleuze added a comment - Good catch, I have raised the point on Kotlin issue tracker (see this KT-5464 comment ) cc Rossen Stoyanchev .
        Hide
        mskonovalov Mikhail Konovalov added a comment - - edited

        I'm not sure the problem is with Kotlin compiler.
        Maybe we can avoid somehow wildcard recursive type params?

        Show
        mskonovalov Mikhail Konovalov added a comment - - edited I'm not sure the problem is with Kotlin compiler. Maybe we can avoid somehow wildcard recursive type params?
        Hide
        mskonovalov Mikhail Konovalov added a comment - - edited

        Sébastien Deleuze, I've used your approach you've mentioned in KT-5464 and it started to compile

        @Test
            fun `test get 3`() {
                val bar: BodySpec<Person, *> = client.get().uri("/person/27").exchange()
                        .expectBody(Person::class.java).consumeWith { person -> Assert.assertTrue(true) }  // compile but leads to NPE
            }
        

        But if I generate Kotlin bytecode and then decompile it I get

        @Test
           public final void test_get_3/* $FF was: test get 3*/() {
              this.client.get().uri("/person/27", new Object[0]).exchange().expectBody(Person.class).consumeWith((Consumer)null.INSTANCE);
              throw null;
           }
        

        (also updated the gist with new case )

        Show
        mskonovalov Mikhail Konovalov added a comment - - edited Sébastien Deleuze , I've used your approach you've mentioned in KT-5464 and it started to compile @Test fun `test get 3 `() { val bar: BodySpec<Person, *> = client.get().uri( "/person/27" ).exchange() .expectBody(Person:: class .java).consumeWith { person -> Assert.assertTrue( true ) } // compile but leads to NPE } But if I generate Kotlin bytecode and then decompile it I get @Test public final void test_get_3 /* $FF was: test get 3*/ () { this .client.get().uri( "/person/27" , new Object[ 0 ]).exchange().expectBody(Person. class ).consumeWith((Consumer) null .INSTANCE); throw null ; } (also updated the gist with new case )
        Hide
        sdeleuze Sébastien Deleuze added a comment -

        Latest comment seems to show this is an issue on Kotlin side that could be fixed in an upcoming Kotlin major version. They also suggest a workaround expectBody.isEqualTo<Nothing?>(Person("42", "Ivan")). They are going to provide a roadmap shortly.

        Show
        sdeleuze Sébastien Deleuze added a comment - Latest comment seems to show this is an issue on Kotlin side that could be fixed in an upcoming Kotlin major version. They also suggest a workaround expectBody.isEqualTo<Nothing?>(Person("42", "Ivan")) . They are going to provide a roadmap shortly.
        Hide
        mskonovalov Mikhail Konovalov added a comment -

        Looks like <Noting?> fixes NPE but call chain will be finished on this.
        Don't you think it can be fixed if redesign this

        interface BodySpec<B, S extends BodySpec<B, S>>
        

        Show
        mskonovalov Mikhail Konovalov added a comment - Looks like <Noting?> fixes NPE but call chain will be finished on this. Don't you think it can be fixed if redesign this interface BodySpec<B, S extends BodySpec<B, S>>
        Hide
        sdeleuze Sébastien Deleuze added a comment -

        Not sure, do you have a alternative proposal?

        It seems they plan to fix this in Kotlin 1.2 or 1.3, so without any concrete proposal / PR to discuss with Rossen Stoyanchev, I tend to think we should not enforce artificially another design for a temporary Kotlin issue.

        Show
        sdeleuze Sébastien Deleuze added a comment - Not sure, do you have a alternative proposal? It seems they plan to fix this in Kotlin 1.2 or 1.3, so without any concrete proposal / PR to discuss with Rossen Stoyanchev , I tend to think we should not enforce artificially another design for a temporary Kotlin issue.
        Hide
        mskonovalov Mikhail Konovalov added a comment - - edited

        I agree not to break everything for Kotlin issue.
        But for me it looks not perfect in Java way too.
        I mean this

        BodySpec<B, ?>
        

        It looks like we create strict recursive structure in interface and then relax it with wildcards.
        I'll try to think of the proposal, but I think it'd be too late to change considering GA.

        Show
        mskonovalov Mikhail Konovalov added a comment - - edited I agree not to break everything for Kotlin issue. But for me it looks not perfect in Java way too. I mean this BodySpec<B, ?> It looks like we create strict recursive structure in interface and then relax it with wildcards. I'll try to think of the proposal, but I think it'd be too late to change considering GA.
        Hide
        sdeleuze Sébastien Deleuze added a comment -

        KT-5464 is expected to be fixed for Kotlin 1.2, and is likely to fix the issue described here, so I prefer keep things as they are if Kotlin is the only relevant reason. Please vote for KT-5464 in order to make sure it will remain high priority.

        Show
        sdeleuze Sébastien Deleuze added a comment - KT-5464 is expected to be fixed for Kotlin 1.2, and is likely to fix the issue described here, so I prefer keep things as they are if Kotlin is the only relevant reason. Please vote for KT-5464 in order to make sure it will remain high priority.

          People

          • Assignee:
            sdeleuze Sébastien Deleuze
            Reporter:
            mskonovalov Mikhail Konovalov
            Last updater:
            Sébastien Deleuze
          • Votes:
            0 Vote for this issue
            Watchers:
            3 Start watching this issue

            Dates

            • Created:
              Updated:
              Resolved:
              Days since last comment:
              11 weeks, 6 days ago