sdk icon indicating copy to clipboard operation
sdk copied to clipboard

Regression on test coverage of switch expressions.

Open renancaraujo opened this issue 1 year ago • 2 comments
trafficstars

Setup

Take into consderation the following library code:

sealed class SealedType {}

class A extends SealedType {}

class B extends SealedType {}

String runSwitch(SealedType type) {
  return switch (type) {
    final A a => 'A',
    final B b => 'B',
  };
}

And the following test code:


void main() {
  test('run', () {
    expect(runSwitch(A()), 'A');
    expect(runSwitch(B()), 'B');
  });
}

(To make your lives easier, i created this sample project)

The problem

When running tests with coverage on dart 3.2.6, we have a report on 100% coverage.

When we do the same on Dart 3.3.0, coverage reports the switch expression as not covered.

screenshot

Extra info

This was reproduced on MacOS Sonoma using the Dart SDK bundled with Flutter (versions 3.16 and 3.19).

renancaraujo avatar Feb 16 '24 10:02 renancaraujo

Item Details
Summary Switch expressions are not covered by test coverage in Dart 3.3.0.
Triage to area-test (high confidence)

(what's this?)

dart-github-bot avatar Feb 16 '24 17:02 dart-github-bot

/cc @liamappelbe @johnniwinther

alexmarkov avatar Feb 16 '24 18:02 alexmarkov

The same is happening with me. My Dart version is 3.3.0-279.3.beta, my Flutter version is 3.19.0-0.4.pre, and I'm on windows.

What I found especially weird is the fact that if the code is formatted like this:

@override // covered
String runSwitch(SealedType type) => switch (type) { // not covered
    final A a => 'A', // covered
    final B b => 'B', // covered
  };

Then the line containing the method gets marked as not covered, even though the method was called. Somehow the fact that the switch statement is there makes the line get marked as not covered.

joranmulderij avatar Feb 25 '24 13:02 joranmulderij

@johnniwinther Does the CFE insert a throw on the switch's line? Is it marked with the new error handling flag?

liamappelbe avatar Feb 25 '24 22:02 liamappelbe

Yes. The generated AST for the code is (using dart pkg/front_end/tool/_fasta/compile.dart --dump-ir)

static method runSwitch(self::SealedType type) → core::String
  return block {
    core::String #t1;
    final synthesized self::SealedType #0#0 = type;
    #L1:
    {
      {
        final hoisted self::A a;
        if(#0#0 is self::A) {
          a = #0#0{self::A};
          #t1 = "A";
          break #L1;
        }
      }
      {
        final hoisted self::B b;
        if(#0#0 is self::B) {
          b = #0#0{self::B};
          #t1 = "B";
          break #L1;
        }
      }
      throw{for-error-handling} new _in::ReachabilityError::•("`null` encountered as case in a switch expression with a non-nullable type.");
    }
  } =>#t1;

Printing this with offsets (using dart pkg/front_end/tool/_fasta/compile.dart --dump-ir --show-offsets) we get

[95]static method runSwitch([116] self::SealedType type) → core::String
[125]  return [125] [125] block {
[-1]    [-1] core::String #t1;
[133]    [133] final synthesized self::SealedType #0#0 = [133] [133] type;
[125]    #L1:
[125]    {
[170]      {
[168]        [168] final hoisted self::A a;
[170]        if([168] [168] [-1] [-1] #0#0 is self::A) {
[168]          [168] [168] a = [-1] [-1] #0#0{self::A};
[-1]          [-1] [-1] #t1 = [173] [173] "A";
[170]          break #L1;
        }
      }
[203]      {
[201]        [201] final hoisted self::B b;
[203]        if([201] [201] [-1] [-1] #0#0 is self::B) {
[201]          [201] [201] b = [-1] [-1] #0#0{self::B};
[-1]          [-1] [-1] #t1 = [206] [206] "B";
[203]          break #L1;
        }
      }
[125]      [125] [125] throw{for-error-handling} [125] [125] new _in::ReachabilityError::• [125]([125] "`null` encountered as case in a switch expression with a non-nullable type.");
    }
  } =>[-1] [-1] #t1;

where offsets 88-140 correspond to the first line.

So except for the ThrowExpression marked as for error handling (throw{for-error-handling}), offsets for the first line are used on the method signature, the ReturnStatement, the contained BlockExpression, the synthesized #0#0 variable, the LabeledStatement and some of the Blocks.

johnniwinther avatar Feb 26 '24 10:02 johnniwinther

Ok, then this should be fixed when I land the VM plumbing for that flag.

liamappelbe avatar Feb 26 '24 21:02 liamappelbe

We have a couple customers blocked on using 3.19.x until this is resolved.

Just to confirm my understanding. It looks like this was fixed by https://github.com/dart-lang/sdk/commit/44094abe2f5111e3cbf674855da82c4899d10660 alone and then separately https://github.com/dart-lang/sdk/issues/54005 was filed to clean up the fix to be nicer?

eseidel avatar Apr 03 '24 17:04 eseidel

and then separately #54005 was filed to clean up the fix to be nicer?

Actually I think https://github.com/dart-lang/sdk/issues/54005 can be closed. It was just tracking the flag feature.

liamappelbe avatar Apr 03 '24 19:04 liamappelbe