coverlet icon indicating copy to clipboard operation
coverlet copied to clipboard

Instrumenting non used assemblies produces non existing branches (whith hits =0) in branch coverage. Lowering coverage when using /p:MergeWith

Open pape77 opened this issue 6 years ago • 23 comments

@tonerdo When calculating the coverage from an assembly that's referenced in another test project, I get some differences on branch coverage than when i run the tests for the actual test project that is ment to test that assembly

+----------------------+--------+--------+--------+
| Module               | Line   | Branch | Method |
+----------------------+--------+--------+--------+
| ProjectName.Api         | 100%   | 100%   | 100%   |
+----------------------+--------+--------+--------+
| ProjectName.Application | 100%   | 60%    | 100%   |
+----------------------+--------+--------+--------+
| ProjectName.Data        | 100%   | 62.5%  | 100%   |
+----------------------+--------+--------+--------+

But separately:

ProjectName.Data:

+---------------+--------+--------+--------+
| Module        | Line   | Branch | Method |
+---------------+--------+--------+--------+
| ProjectName.Data | 100%   | 100%   | 100%   |
+---------------+--------+--------+--------+
ProjectName.Application:

+----------------------+--------+--------+--------+
| Module               | Line   | Branch | Method |
+----------------------+--------+--------+--------+
| ProjectName.Application | 100%   | 75%    | 100%   |
+----------------------+--------+--------+--------+
|ProjectName.Data        | 0%     | 0%     | 0%     |
+----------------------+--------+--------+--------+
ProjectName.Api:

+----------------------+--------+--------+--------+
| Module               | Line   | Branch | Method |
+----------------------+--------+--------+--------+
| ProjectName.Api         | 100%   | 100%   | 100%   |
+----------------------+--------+--------+--------+
| ProjectNameApplication | 0%     | 0%     | 0%     |
+----------------------+--------+--------+--------+
| ProjectName.Data        | 0%     | 0%     | 0%     |
+----------------------+--------+--------+--------+

So my idea was to get:

+----------------------+--------+--------+--------+
| Module               | Line   | Branch | Method |
+----------------------+--------+--------+--------+
| ProjectName.Application | 100%   | 75%    | 100%   |
+---------------+--------+--------+--------+
| ProjectName.Data | 100%   | 100%   | 100%   |
+---------------+--------+--------+--------+
| ProjectName.Api         | 100%   | 100%   | 100%   |
+----------------------+--------+--------+--------+

Just noticed ProjectName.Application detects some branches of the ProjectName.Data that ProjectName.Data does not detect when running the coverage for itself alone.

Is this normal/possible?

"ProjectName.Repositories.KeyRepository/<AddAsync>d__4": {
        "System.Void ProjectName.Repositories.KeyRepository/<AddAsync>d__4::MoveNext()": {
          "Lines": {
            "34": 4,
            "35": 4,
            "36": 1,
            "37": 1,
            "40": 3,
            "41": 3,
            "42": 3
          },
"Branches": [
            {
              "Line": 41,
              "Offset": 98,
              "EndOffset": 100,
              "Path": 0,
              "Ordinal": 2,
              "Hits": 0
            },
            {
              "Line": 41,
              "Offset": 98,
              "EndOffset": 165,
              "Path": 1,
              "Ordinal": 3,
              "Hits": 0
            }
          ]
"ProjectName.Data.Repositories.KeyRepository/<AddAsync>d__4": {
        "System.Void ProjectName.Data.Repositories.KeyRepository/<AddAsync>d__4::MoveNext()": {
          "Lines": {
            "34": 4,
            "35": 4,
            "36": 1,
            "37": 1,
            "40": 3,
            "41": 3,
            "42": 3
          },
          "Branches": [
            {
              "Line": 41,
              "Offset": 98,
              "EndOffset": 165,
              "Path": 1,
              "Ordinal": 3,
              "Hits": 1
            }
          ]

pape77 avatar Oct 23 '18 08:10 pape77

@pape77 I don't quite understand what the problem is. Naturally if you run the coverage under different contexts, the final output would be different. Can you perhaps share your project structure and what commands you run at each different stage

tonerdo avatar Oct 26 '18 03:10 tonerdo

@tonerdo I'll try to explain better:

ProjectName.Api references ProjectName.Application and ProjectName.Data

ProjectName.Application references ProjectName.Data

ProjectName.Data doesn't reference any of the other two projects

I named my projects ProjectName.X.Tests where X is the corresponding project that I want to test. In these test projects I only (and emphasis in only) test functionality that's in X module. The rest I mock it out.

So for example when I run ProjectName.Application.Tests, since ProjectName.Application references ProjectName.Data, coverlet instruments these two. And includes coverage for ProjectName.Data in the resulting coverage file too (with all coverage being 0, so hits = 0 everywhere).

This is as expected. The problem is that this coverage file includes some branchs with hits = 0 that don't exist at all when running a particular coverlet command over ProjectName.Data.Tests

As a result of the latest, when merging ProjectName.Application.Tests and ProjectName.Data.Test coverage files, it will indicate that ProjectName.Data has less coverage that it has on ProjectName.Data.Test on it's own, because of the mentioned non existing but generated branches with hits = 0 in ProjectName.Application.Test result

Codes to exec this are really simple:

dotnet test .\test\ProjectName.Application.Tests\  -l trx -r /results /p:CollectCoverage=true /p:Coverlet
OutputFormat='json' /p:CoverletOutput=/results/coverage

and then compare that output file with coverage file for ProjectName.Data module on it's own

dotnet test .\test\ProjectName.Data.Tests\  -l trx -r /results /p:CollectCoverage=true /p:Coverlet
OutputFormat='json' /p:CoverletOutput=/results/coverage

With the pr at #225 what I do is don't instrument the dlls that are referenced in test projects and not used/tested at all (as in my case, in which i mock everything, but still have the reference because i use the project's classes, and because it's referenced in the project I'm actually testing)

Here are the two files that I got generated with the commands above Coverage.json for Application.Tests: https://textuploader.com/dwtb4

Coverage.json for Data.Tests: https://textuploader.com/dwtbp

Methods like FindAsync and AddAsync have branches with 0 hits in ProjectName.Application.Tests which don't exist in ProjectName.Data.Tests and the instrumented dll is the same.

I think this is not ok, I've tried writing all kind of tests for those silly 2 methods and I couldn't get the branch that is marked with 0 hits in Application to pop in the Data coverage file even with 0 hits whatsoever

Hopefully it's more clear now

Note: I only made focus on Application and Data projects. But same happens for Api Project in my first comment of the issue, since it references Application and Data projects

Note2: Note that the merging does work correctly but I just made a quick mention of it here since it's not part of the real issue i want to illustrate, if i run


dotnet test .\ProjectName.Data.Tests -l trx -r /results /p:CollectCoverage=true /p:CoverletOutputFormat='json%2
copencover' /p:CoverletOutput=/results/coverage

dotnet test .\test\ProjectName.Application.Tests\  -l trx -r /results /p:CollectCoverage=true /p:Coverlet
OutputFormat='json' /p:CoverletOutput=/results/coverage  /p:MergeWith='/results/coverage.json'

the result file will be the merged one between these 2 projects. Problem being, since the second run produces branches with 0 hits that don't exist in the output of the first command line, the merged coverage file will indeed increment hits only in the branches that are in the first run. Making the global coverage for ProjectName.Data to decrease (since branches with 0 hits exist in the final, merged, output)

pape77 avatar Oct 26 '18 08:10 pape77

In when trying to solve this (pr #225) i found a comment in the code that called my attention

In Coverage.cs line 169 : // File not instrumented, or nothing in it called. Warn about this? So when nothing in it is called, it's still instrumented and included in coverage.json, this is what i want to avoid, because if would be ok if it didn't pollute coverage, but since it does and we are not calling anything within this dll, then why should it pop in the final coverage file? This is why, when finding something like this, i remove it from the _results variable

pape77 avatar Oct 26 '18 09:10 pape77

@tonerdo have you had the chance to take a second look at this? Hope it's clear now

pape77 avatar Nov 01 '18 09:11 pape77

@pape77 I think I get it now. I've noticed branch coverage reporting defaults to 0 when there are no branches, which shouldn't be the case. In terms of nothing being called I think I want to keep that default behaviour and I suggest you simply exclude the offending assembly

tonerdo avatar Nov 04 '18 02:11 tonerdo

@tonerdo I cannot exclude the assembly since it's being used by the classes at my other project . If you ment by command line param, again i can't since i run this for the whole solution in one line.

But most importantly, the problem is not branch coverage defaulting to 0 if not covered. The problem is that the non used assembly is generating NON EXISTING branches, with coverage 0 which will never be covered by the actual tests in other projects and therefore not overrided when merging. This will make code coverage % to go down, not reflecting reality. That's why i want to exclude the assembly if nothing there is used, which... Make sense right? What's your reason for keeping it?

pape77 avatar Nov 04 '18 02:11 pape77

@tonerdo you mean the /p:Exclude param then. I have to specify what to exclude and i cannot make a generic test run for all my projects if I do, which is my final goal (probably not the only one with it). Moreover, I would have to make one command line per test proj to exclude what i need, instead of one for the solution doing all the job. Why should we keep this if it's polluting the coverage? :/ I doubt if anybody has noticed this behaviour, or they just see the file being generated and don't pay attention to what's happening.

pape77 avatar Nov 04 '18 02:11 pape77

@tonerdo One option could be adding a command line option param to exclude non used assemblies, if you really want to keep current behaviour as default. Wouldn't mind modifying my pr to do so. But I really think the default bahaviour should be outputing the correct coverage when merging 😅

pape77 avatar Nov 04 '18 03:11 pape77

@tonerdo i updated my pr to add this as an option so default behaviour remains the same and fixes the issues for those who need to exclude non used files from coverage (#225)

I got an error when doing some http post call for a vs 2015 test on app veyor. Probably if you re run it, it will work. Would you consider this for merging with this as an option? :)

pape77 avatar Nov 09 '18 16:11 pape77

Any news on this issue? I can't really tell my coworkers to use Coverlet in our builds if the coverage decreases in complex solutions where many project references are used. Our coverage will be lower numbers that don't make much sense, if I understand the issue correctly.

Like pape77, we really insist on running our tests per solution and having good results without any manual setup in our builds.

Thanks!

ghost avatar Feb 01 '19 21:02 ghost

@vlef i already gave up and forked away. If you use current coverlet, even more things that I don't care about (and maybe you don't either) gets into the report. Like certain external dlls i use. So decided to implement this in my own fork, sadly

pape77 avatar Feb 01 '19 21:02 pape77

Thanks for the heads up! That is quite unfortunate, especially since even the official Microsoft docs link to Coverlet.

ghost avatar Feb 01 '19 21:02 ghost

Yep. I first wanted to exclude them by default. Then came with this option variable to keep the original behavior for whoever uses it. But no luck it seems 😢

pape77 avatar Feb 01 '19 21:02 pape77

@vlef if you plan to fork, you can copy the code from my original pr (so not the last commit but the first one). That will instrument only the dlls that you are touching with your tests. Although, i believe (as i mentioned) that now more things get instrumented even, not only the dlls with pdb files, but all. So you may have to remove a couple more things to make it look like the version i changed in my pr by that time

pape77 avatar Feb 01 '19 22:02 pape77

@MarcoRossignoli why is it marked as feature request? As I understand this is a bug:

The problem is that the non used assembly is generating NON EXISTING branches

That is coverlet might detect fake branches.

pkrukp avatar Jan 26 '21 07:01 pkrukp

Is this issue still present?

So when nothing in it is called, it's still instrumented and included in coverage.json, this is what i want to avoid, because if would be ok if it didn't pollute coverage, but since it does and we are not calling anything within this dll,....

I moved to feature because IIRW the idea is to remove module with no coverage to avoid to mess stats and to me is better add a feat like --exlude... I don't think it's a bug, can you provide a repro with the issue just in case?

MarcoRossignoli avatar Jan 26 '21 07:01 MarcoRossignoli

I don't know if this issue is still present, don't have repro, maybe @pape77 could comment?

pkrukp avatar Jan 26 '21 08:01 pkrukp

Hi guys, i forked away from this project since as @MarcoRossignoli knows, i made a pr for trying to fix this and other issues i have when running coverlet and trying to merge coverage when having tests in different projects. That pr didn't have the blessing of the repo owner, not even adding optional parameters so the original functionality of coverlet is not affected. I once in a while try the newest version of coverlet only to see the same issues are still there. I haven't tried the latest in a while, feel free to try to repro. For now, my fork (and quite an old one sadly) works for my purposes just good. So a valid repro would be having a project with some code in it (preferably a real one, samples usually don't have many branches or code complexity at all), create at least two test projects (csproj) and try running coverlet on both merging afterwards. What i saw back then was:

  • Weird dlls were instrumented, which weren't related to the project itself. Like you include a library you want to use, and it gets instrumented as part of your project, with coverage 0 obviously
  • Projects containing only models, which were referenced by another projects and naturally had 0 branches to cover(since they are all models), were showing branch coverage 100% when running coverage on the project itself, but were showing insufficient branch coverage (and these "ghost" branches) when running coverage on a project that references them
  • Merge of coverage being completely off when running coverage on different test projects covering the same codebase, and merging the results

Mainly problem 1 i tried to fix in an old pr, and i think it was fixing 1&2.

You can try to repro this if you want, or ignore it. As you wish. I'm not using coverlet these days as it doesn't suffice my purposes (or my company's)

Good luck!

pape77 avatar Jan 26 '21 08:01 pape77

Thanks, I hoped you have some small repro that shows the "ghost branches" that you could share, I understand if you don't have it.

pkrukp avatar Jan 26 '21 09:01 pkrukp

I don't, i tested this with a repo of my company which I can't share Was long time ago, so I don't even remember which one it was, sorry

pape77 avatar Jan 26 '21 20:01 pape77

how does it work currently for merging coverage in different test projects? I see no more merge option in the docs, is that done automatically? In case i run it again with latest version

pape77 avatar Jan 26 '21 20:01 pape77

Hi @pape77 it works like in the past nothing changed on that side we're waiting to use new extension point from vstest plat https://github.com/coverlet-coverage/coverlet/issues/1015#issuecomment-746174446

https://github.com/coverlet-coverage/coverlet/blob/master/Documentation/DriversFeatures.md https://github.com/coverlet-coverage/coverlet/blob/master/Documentation/MSBuildIntegration.md#merging-results https://github.com/coverlet-coverage/coverlet/blob/master/Documentation/GlobalTool.md#coverlet-as-a-global-tool And your trick is documented for collectors https://github.com/coverlet-coverage/coverlet/blob/master/Documentation/VSTestIntegration.md#coverlet-options-supported-by-vstest-integration (thanks)

MarcoRossignoli avatar Jan 27 '21 07:01 MarcoRossignoli