8000 Code coverage incorrect for Kotlin data classes with kotlinx.serialization in JaCoCo 0.8.13 · Issue #1855 · jacoco/jacoco · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content
8000

Code coverage incorrect for Kotlin data classes with kotlinx.serialization in JaCoCo 0.8.13 #1855

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
Dukoff92 opened this issue Mar 6, 2025 · 4 comments · May be fixed by #1885
Open

Code coverage incorrect for Kotlin data classes with kotlinx.serialization in JaCoCo 0.8.13 #1855

Dukoff92 opened this issue Mar 6, 2025 · 4 comments · May be fixed by #1885
Labels
language: Kotlin type: bug 🐛 Something isn't working

Comments

@Dukoff92
Copy link
Dukoff92 commented Mar 6, 2025

I am experiencing incorrect code coverage when using JaCoCo version 0.8.13 with Kotlin data classes that utilize kotlinx.serialization. It appears that generated parameters and functions are not properly excluded from coverage, which leads to incorrect results.

This issue did not occur in version 0.8.12, where the coverage was reported correctly.

Steps to Reproduce:
gradle version: 8.12.1
kotlin version: 2.1.10
Use Kotlin data classes with kotlinx.serialization
Run JaCoCo with version 0.8.13

Example data class:

@Serializable
data class User(
    @SerialName("name")
    val name: String? = null,

    @SerialName("age")
    val age: Int? = null
)

Expected behavior:
Generated serialization-related code should be excluded from the coverage report, as it was in version 0.8.12

Actual Behavior:
The report includes functions and constructors that should be excluded or covered
Coverage for these generated elements is reported as 0%

Version 0.8.13
Image

Version 0.8.12
Image

@Dukoff92 Dukoff92 added the type: bug 🐛 Something isn't working label Mar 6, 2025
@Dukoff92 Dukoff92 changed the title Code coverage incorrect for Kotlin data classes with kotlinx.serialization in 0.8.13-SNAPSHOT Code coverage incorrect for Kotlin data classes with kotlinx.serialization in 0.8.13 Apr 2, 2025
@Dukoff92 Dukoff92 changed the title Code coverage incorrect for Kotlin data classes with kotlinx.serialization in 0.8.13 Code coverage incorrect for Kotlin data classes with kotlinx.serialization in JaCoCo 0.8.13 Apr 2, 2025
@Dukoff92
Copy link
Author
Dukoff92 commented Apr 3, 2025

I tried to narrow down what changed from version 0.8.12 and why these generated methods are not ignored but without much success
I noticed that the methods which are not covered are all marked as synthetic methods. Here is the User class decompiled if it helps:

@Serializable
@Metadata(
   mv = {2, 1, 0},
   k = 1,
   xi = 48,
   d1 = {"\u0000>\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0000\n\u0002\u0010\u000e\n\u0000\n\u0002\u0010\b\n\u0002\b\u0004\n\u0002\u0018\u0002\n\u0002\b\u000e\n\u0002\u0010\u000b\n\u0002\b\u0004\n\u0002\u0010\u0002\n\u0002\b\u0002\n\u0002\u0018\u0002\n\u0000\n\u0002\u0018\u0002\n\u0002\b\u0004\b\u0087\b\u0018\u0000 &2\u00020\u0001:\u0002%&B\u001f\u0012\n\b\u0002\u0010\u0002\u001a\u0004\u0018\u00010\u0003\u0012\n\b\u0002\u0010\u0004\u001a\u0004\u0018\u00010\u0005¢\u0006\u0004\b\u0006\u0010\u0007B(\b\u0010\u0012\u0006\u0010\b\u001a\u00020\u0005\u0012\b\u0010\u0002\u001a\u0004\u0018\u00010\u0003\u0012\b\u0010\u0004\u001a\u0004\u0018\u00010\u0005\u0012\b\u0010\t\u001a\u0004\u0018\u00010\nB/\b\u0010\u0012\u0006\u0010\b\u001a\u00020\u0005\u0012\b\u0010\u0002\u001a\u0004\u0018\u00010\u0003\u0012\b\u0010\u0004\u001a\u0004\u0018\u00010\u0005\u0012\b\u0010\t\u001a\u0004\u0018\u00010\n¢\u0006\u0004\b\u0006\u0010\u000bJ\u000b\u0010\u0014\u001a\u0004\u0018\u00010\u0003HÆ\u0003J\u0010\u0010\u0015\u001a\u0004\u0018\u00010\u0005HÆ\u0003¢\u0006\u0002\u0010\u0012J&\u0010\u0016\u001a\u00020\u00002\n\b\u0002\u0010\u0002\u001a\u0004\u0018\u00010\u00032\n\b\u0002\u0010\u0004\u001a\u0004\u0018\u00010\u0005HÆ\u0001¢\u0006\u0002\u0010\u0017J\u0013\u0010\u0018\u001a\u00020\u00192\b\u0010\u001a\u001a\u0004\u0018\u00010\u0001HÖ\u0003J\t\u0010\u001b\u001a\u00020\u0005HÖ\u0001J\t\u0010\u001c\u001a\u00020\u0003HÖ\u0001J \u0010\u001d\u001a\u00020\u001e2\u0006\u0010\u001f\u001a\u00020\u00002\u0006\u0010 \u001a\u00020!2\u0006\u0010\"\u001a\u00020#H\u0001J%\u0010\u001d\u001a\u00020\u001e2\u0006\u0010\u001f\u001a\u00020\u00002\u0006\u0010 \u001a\u00020!2\u0006\u0010\"\u001a\u00020#H\u0001¢\u0006\u0002\b$R\u001e\u0010\u0002\u001a\u0004\u0018\u00010\u00038\u0006X\u0087\u0004¢\u0006\u000e\n\u0000\u0012\u0004\b\f\u0010\r\u001a\u0004\b\u000e\u0010\u000fR \u0010\u0004\u001a\u0004\u0018\u00010\u00058\u0006X\u0087\u0004¢\u0006\u0010\n\u0002\u0010\u0013\u0012\u0004\b\u0010\u0010\r\u001a\u0004\b\u0011\u0010\u0012¨\u0006'"},
   d2 = {"Lcom/mycompany/myapp/data/entity/User;", "", "name", "", "age", "", "<init>", "(Ljava/lang/String;Ljava/lang/Integer;)V", "seen0", "serializationConstructorMarker", "Lkotlinx/serialization/internal/SerializationConstructorMarker;", "(ILjava/lang/String;Ljava/lang/Integer;Lkotlinx/serialization/internal/SerializationConstructorMarker;)V", "getName$annotations", "()V", "getName", "()Ljava/lang/String;", "getAge$annotations", "getAge", "()Ljava/lang/Integer;", "Ljava/lang/Integer;", "component1", "component2", "copy", "(Ljava/lang/String;Ljava/lang/Integer;)Lcom/mycompany/myapp/data/entity/User;", "equals", "", "other", "hashCode", "toString", "write$Self", "", "self", "output", "Lkotlinx/serialization/encoding/CompositeEncoder;", "serialDesc", "Lkotlinx/serialization/descriptors/SerialDescriptor;", "write$Self$Sources_of_myapp_app_main", "$serializer", "Companion", "Sources of  myapp.app.main"}
)
@StabilityInferred(
   parameters = 1
)
public final class User {
   @NotNull
   public static final Companion Companion = new Companion((DefaultConstructorMarker)null);
   @Nullable
   private final String name;
   @Nullable
   private final Integer age;
   public static final int $stable;

   public User(@Nullable String name, @Nullable Integer age) {
      this.name = name;
      this.age = age;
   }

   // $FF: synthetic method
   public User(String var1, Integer var2, int var3, DefaultConstructorMarker var4) {
      if ((var3 & 1) != 0) {
         var1 = null;
      }

      if ((var3 & 2) != 0) {
         var2 = null;
      }

      this(var1, var2);
   }

   @Nullable
   public final String getName() {
      return this.name;
   }

   /** @deprecated */
   // $FF: synthetic method
   @SerialName("name")
   public static void getName$annotations() {
   }

   @Nullable
   public final Integer getAge() {
      return this.age;
   }

   /** @deprecated */
   // $FF: synthetic method
   @SerialName("age")
   public static void getAge$annotations() {
   }

   @Nullable
   public final String component1() {
      return this.name;
   }

   @Nullable
   public final Integer component2() {
      return this.age;
   }

   @NotNull
   public final User copy(@Nullable String name, @Nullable Integer age) {
      return new User(name, age);
   }

   // $FF: synthetic method
   public static User copy$default(User var0, String var1, Integer var2, int var3, Object var4) {
      if ((var3 & 1) != 0) {
         var1 = var0.name;
      }

      if ((var3 & 2) != 0) {
         var2 = var0.age;
      }

      return var0.copy(var1, var2);
   }

   @NotNull
   public String toString() {
      return "User(name=" + this.name + ", age=" + this.age + ')';
   }

   public int hashCode() {
      int result = this.name == null ? 0 : this.name.hashCode();
      result = result * 31 + (this.age == null ? 0 : this.age.hashCode());
      return result;
   }

   public boolean equals(@Nullable Object other) {
      if (this == other) {
         return true;
      } else if (!(other instanceof User)) {
         return false;
      } else {
         User var2 = (User)other;
         if (!Intrinsics.areEqual(this.name, var2.name)) {
            return false;
         } else {
            return Intrinsics.areEqual(this.age, var2.age);
         }
      }
   }

   // $FF: synthetic method
   @JvmStatic
   public static final void write$Self$Sources_of_myapp_app_main(User self, CompositeEncoder output, SerialDescriptor serialDesc) {
      if (output.shouldEncodeElementDefault(serialDesc, 0) ? true : self.name != null) {
         output.encodeNullableSerializableElement(serialDesc, 0, (SerializationStrategy)StringSerializer.INSTANCE, self.name);
      }

      if (output.shouldEncodeElementDefault(serialDesc, 1) ? true : self.age != null) {
         output.encodeNullableSerializableElement(serialDesc, 1, (SerializationStrategy)IntSerializer.INSTANCE, self.age);
      }

   }

   // $FF: synthetic method
   public User(int seen0, String name, Integer age, SerializationConstructorMarker serializationConstructorMarker) {
      if ((0 & seen0) != 0) {
         PluginExceptionsKt.throwMissingFieldException(seen0, 0, User.$serializer.INSTANCE.getDescriptor());
      }

      super();
      if ((seen0 & 1) == 0) {
         this.name = null;
      } else {
         this.name = name;
      }

      if ((seen0 & 2) == 0) {
         this.age = null;
      } else {
         this.age = age;
      }

   }

   public User() {
      this((String)null, (Integer)null, 3, (DefaultConstructorMarker)null);
   }

   @Metadata(
      mv = {2, 1, 0},
      k = 1,
      xi = 48,
      d1 = {"\u0000\u0016\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0003\n\u0002\u0018\u0002\n\u0002\u0018\u0002\n\u0000\b\u0086\u0003\u0018\u00002\u00020\u0001B\t\b\u0002¢\u0006\u0004\b\u0002\u0010\u0003J\f\u0010\u0004\u001a\b\u0012\u0004\u0012\u00020\u00060\u0005¨\u0006\u0007"},
      d2 = {"Lcom/mycompany/myapp/data/entity/User$Companion;", "", "<init>", "()V", "serializer", "Lkotlinx/serialization/KSerializer;", "Lcom/mycompany/myapp/data/entity/User;", "Sources of myapp.app.main"}
   )
   public static final class Companion {
      private Companion() {
      }

      @NotNull
      public final KSerializer serializer() {
         return (KSerializer)User.$serializer.INSTANCE;
      }

      // $FF: synthetic method
      public Companion(DefaultConstructorMarker $constructor_marker) {
         this();
      }
   }

   /** @deprecated */
   // $FF: synthetic class
   @Deprecated(
      message = "This synthesized declaration should not be used directly",
      level = DeprecationLevel.HIDDEN
   )
   @Metadata(
      mv = {2, 1, 0},
      k = 1,
      xi = 48,
      d1 = {"\u00006\n\u0000\n\u0002\u0018\u0002\n\u0002\u0018\u0002\n\u0002\b\u0003\n\u0002\u0010\u0011\n\u0002\u0018\u0002\n\u0002\b\u0003\n\u0002\u0018\u0002\n\u0000\n\u0002\u0010\u0002\n\u0000\n\u0002\u0018\u0002\n\u0002\b\u0002\n\u0002\u0018\u0002\n\u0002\b\u0003\bÇ\u0002\u0018\u00002\b\u0012\u0004\u0012\u00020\u00020\u0001B\t\b\u0002¢\u0006\u0004\b\u0003\u0010\u0004J\u0015\u0010\u0005\u001a\f\u0012\b\u0012\u0006\u0012\u0002\b\u00030\u00070\u0006¢\u0006\u0002\u0010\bJ\u000e\u0010\t\u001a\u00020\u00022\u0006\u0010\n\u001a\u00020\u000bJ\u0016\u0010\f\u001a\u00020\r2\u0006\u0010\u000e\u001a\u00020\u000f2\u0006\u0010\u0010\u001a\u00020\u0002R\u0011\u0010\u0011\u001a\u00020\u0012¢\u0006\b\n\u0000\u001a\u0004\b\u0013\u0010\u0014¨\u0006\u0015"},
      d2 = {"com/mycompany/myapp/data/entity/User.$serializer", "Lkotlinx/serialization/internal/GeneratedSerializer;", "Lcom/mycompany/myapp/data/entity/User;", "<init>", "()V", "childSerializers", "", "Lkotlinx/serialization/KSerializer;", "()[Lkotlinx/serialization/KSerializer;", "deserialize", "decoder", "Lkotlinx/serialization/encoding/Decoder;", "serialize", "", "encoder", "Lkotlinx/serialization/encoding/Encoder;", "value", "descriptor", "Lkotlinx/serialization/descriptors/SerialDescriptor;", "getDescriptor", "()Lkotlinx/serialization/descriptors/SerialDescriptor;", "Sources of myapp.app.main"}
   )
   @StabilityInferred(
      parameters = 0
   )
   public static final class $serializer implements GeneratedSerializer {
      @NotNull
      public static final $serializer INSTANCE = new $serializer();
      @NotNull
      private static final SerialDescriptor descriptor;
      public static final int $stable = 8;

      private $serializer() {
      }

      public final void serialize(@NotNull Encoder encoder, @NotNull User value) {
         Intrinsics.checkNotNullParameter(encoder, "encoder");
         Intrinsics.checkNotNullParameter(value, "value");
         SerialDescriptor var3 = descriptor;
         CompositeEncoder var4 = encoder.beginStructure(var3);
         User.write$Self$Sources_of_myapp_app_main(value, var4, var3);
         var4.endStructure(var3);
      }

      @NotNull
      public final User deserialize(@NotNull Decoder decoder) {
         Intrinsics.checkNotNullParameter(decoder, "decoder");
         SerialDescriptor var2 = descriptor;
         boolean var3 = true;
         int var5 = 0;
         String var6 = null;
         Integer var7 = null;
         CompositeDecoder var8 = decoder.beginStructure(var2);
         if (var8.decodeSequentially()) {
            var6 = (String)var8.decodeNullableSerializableElement(var2, 0, (DeserializationStrategy)StringSerializer.INSTANCE, var6);
            var5 |= 1;
            var7 = (Integer)var8.decodeNullableSerializableElement(var2, 1, (DeserializationStrategy)IntSerializer.INSTANCE, var7);
            var5 |= 2;
         } else {
            while(var3) {
               int var4 = var8.decodeElementIndex(var2);
               switch (var4) {
                  case -1:
                     var3 = false;
                     break;
                  case 0:
                     var6 = (String)var8.decodeNullableSerializableElement(var2, 0, (DeserializationStrategy)StringSerializer.INSTANCE, var6);
                     var5 |= 1;
                     break;
                  case 1:
                     var7 = (Integer)var8.decodeNullableSerializableElement(var2, 1, (DeserializationStrategy)IntSerializer.INSTANCE, var7);
                     var5 |= 2;
                     break;
                  default:
                     throw new UnknownFieldException(var4);
               }
            }
         }

         var8.endStructure(var2);
         return new User(var5, var6, var7, (SerializationConstructorMarker)null);
      }

      @NotNull
      public final SerialDescriptor getDescriptor() {
         return descriptor;
      }

      @NotNull
      public final KSerializer[] childSerializers() {
         KSerializer[] var1 = new KSerializer[]{BuiltinSerializersKt.getNullable((KSerializer)StringSerializer.INSTANCE), BuiltinSerializersKt.getNullable((KSerializer)IntSerializer.INSTANCE)};
         return var1;
      }

      @NotNull
      public KSerializer[] typeParametersSerializers() {
         return super.typeParametersSerializers();
      }

      // $FF: synthetic method
      // $FF: bridge method
      public void serialize(Encoder encoder, Object value) {
         this.serialize(encoder, (User)value);
      }

      // $FF: synthetic method
      // $FF: bridge method
      public Object deserialize(Decoder decoder) {
         return this.deserialize(decoder);
      }

      static {
         PluginGeneratedSerialDescriptor var0 = new PluginGeneratedSerialDescriptor("com.mycompany.myapp.data.entity.User", INSTANCE, 2);
         var0.addElement("name", true);
         var0.addElement("age", true);
         descriptor = (SerialDescriptor)var0;
      }
   }
}

@Godin
Copy link
Member
Godin commented Apr 3, 2025

@Dukoff92 first of all thank you for testing snapshot version and for the report ❤ This seems to be consequence of #1700 unfortunately unforeseen and overlooked. Unfortunately we had no time to look into this regression and there were pressure to release 0.8.13 for Java 24. Thank you for your understanding.

@Godin Godin added this to Filtering Apr 3, 2025
@github-project-automation github-project-automation bot moved this to Awaiting triage in Filtering Apr 3, 2025
@Godin Godin moved this from Awaiting triage to To Do in Filtering Apr 3, 2025
@vdurante
Copy link
vdurante commented Apr 15, 2025

@Dukoff92 first of all thank you for testing snapshot version and for the report ❤ This seems to be consequence of #1700 unfortunately unforeseen and overlooked. Unfortunately we had no time to look into this regression and there were pressure to release 0.8.13 for Java 24. Thank you for your understanding.

Any recommendation on a workaround? I can't downgrade to 0.8.12, but I also don't want to manually implement code to cover for all this.

Is there a way to add these generated methods to exclusions without actually ignoring the entire classes?

@Dukoff92
Copy link
Author

Hey, @vdurante!

Unfortunately, I tried multiple things, but could not find a way to bypass the generated methods only.

I am waiting for it to be fixed (there is a task in the To-do section) and then I will be using version 0.8.14-SNAPSHOT until the official version gets released.

@Godin Godin moved this from To Do to In Progress in Filtering Apr 29, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
language: Kotlin type: bug 🐛 Something isn't working
Projects
Status: In Progress
Development

Successfully merging a pull request may close this issue.

3 participants
0