-
Notifications
You must be signed in to change notification settings - Fork 1
[Part 2] Add support for Go cover files #2447
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
Changes from all commits
aa502a1
4d483dc
286902a
db68848
99f80a5
6c5e95f
309c368
d722eae
35ceda1
4a64bbc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,309 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace App\Strategy\GoCover; | ||
|
|
||
| use App\Exception\ParseException; | ||
| use App\Model\Coverage; | ||
| use App\Model\File; | ||
| use App\Model\Line\Branch; | ||
| use App\Model\Line\Statement; | ||
| use App\Service\PathFixingService; | ||
| use App\Strategy\ParseStrategyInterface; | ||
| use Exception; | ||
| use LogicException; | ||
| use OutOfBoundsException; | ||
| use Override; | ||
| use Packages\Contracts\Format\CoverageFormat; | ||
| use Packages\Contracts\Provider\Provider; | ||
| use Psr\Log\LoggerInterface; | ||
|
|
||
| final readonly class GoCoverParseStrategy implements ParseStrategyInterface | ||
| { | ||
| /** | ||
| * Each line is formatted as `name.go:line.column,line.column numberOfStatements count`. | ||
| */ | ||
| // phpcs:ignore | ||
| private const string LINE_STRUCTURE = '/^(?<file>.*?\.go):(?<startLine>[0-9]+).(?<startColumn>[0-9]+),(?<endLine>[0-9]+).(?<endColumn>[0-9]+)\s(?<statements>[0-9]+)\s(?<count>[0-9]+)$/'; | ||
|
|
||
| /** | ||
| * If arguments such as mode are provided, we can skip the line. | ||
| */ | ||
| private const string MODE_STRUCTURE = 'mode:'; | ||
|
|
||
| public function __construct( | ||
| private LoggerInterface $parseStrategyLogger, | ||
| private PathFixingService $pathFixingService | ||
| ) { | ||
| } | ||
|
|
||
| #[Override] | ||
| public function supports(string $content): bool | ||
| { | ||
| /** @var string[] $lines */ | ||
| $lines = preg_split('/\n|\r\n?/', $content); | ||
|
|
||
| foreach ($lines as $line) { | ||
| $line = trim($line); | ||
|
|
||
| // Skip empty lines | ||
| if ($line === '') { | ||
| continue; | ||
| } | ||
|
|
||
| // If a mode (i.e. atomic) is provided, skip the line | ||
| if (str_starts_with($line, self::MODE_STRUCTURE)) { | ||
| continue; | ||
| } | ||
|
|
||
| // Match the record type and its data | ||
| if (preg_match(self::LINE_STRUCTURE, $line) !== 1) { | ||
| $this->parseStrategyLogger->error( | ||
| 'Unable to validate structure of line in Go Cover file.', | ||
| [ | ||
| 'line' => $line | ||
| ] | ||
| ); | ||
|
|
||
| return false; | ||
| } | ||
| } | ||
|
|
||
| return true; | ||
| } | ||
|
|
||
| #[Override] | ||
| public function parse( | ||
| Provider $provider, | ||
| string $owner, | ||
| string $repository, | ||
| string $projectRoot, | ||
| string $content | ||
| ): Coverage { | ||
| if (!$this->supports($content)) { | ||
| throw ParseException::notSupportedException(); | ||
| } | ||
|
|
||
| try { | ||
| $coverage = new Coverage( | ||
| CoverageFormat::GO_COVER, | ||
| root: $projectRoot, | ||
| ); | ||
| } catch (Exception $exception) { | ||
| throw new ParseException( | ||
| sprintf( | ||
| 'Failed to create coverage model for %s %s %s. Error was %s', | ||
| $provider->value, | ||
| $owner, | ||
| $repository, | ||
| $exception | ||
| ), | ||
| previous: $exception | ||
| ); | ||
| } | ||
|
|
||
| /** @var string[] $blocks */ | ||
| $blocks = preg_split('/\n|\r\n?/', $content); | ||
|
|
||
| try { | ||
| foreach ($blocks as $block) { | ||
| $block = trim($block); | ||
|
|
||
| // Skip empty lines | ||
| if ($block === '') { | ||
| continue; | ||
| } | ||
|
|
||
| // If a mode (i.e. atomic) is provided, skip the line | ||
| if (str_starts_with($block, self::MODE_STRUCTURE)) { | ||
| continue; | ||
| } | ||
|
|
||
| $coverage = $this->handleBlock( | ||
| $provider, | ||
| $owner, | ||
| $repository, | ||
| $coverage, | ||
| $block | ||
| ); | ||
| } | ||
| } catch (LogicException $logicException) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The next 9 lines are not covered by any tests. |
||
| throw new ParseException( | ||
| sprintf( | ||
| 'Unable to parse coverage of line in Go Cover file for %s %s %s.', | ||
| $provider->value, | ||
| $owner, | ||
| $repository, | ||
| ), | ||
| previous: $logicException | ||
| ); | ||
| } | ||
|
|
||
| return $coverage; | ||
| } | ||
|
|
||
| /** | ||
| * Each line of a Go Coverage file is a 'block' of coverage - where a block has a start and end line (and | ||
| * start and end columns), but all have the same hit count. | ||
| * | ||
| * For example, a block which spans 2 lines (and has 2 statements) but is never hit will look | ||
| * like this: | ||
| * | ||
| * ``` | ||
| * github.com/some-owner/some-repo/internal/cmd/ipv6/main.go:28.2,30.16 2 0 | ||
| * ``` | ||
| * | ||
| * @throws LogicException | ||
| */ | ||
| private function handleBlock( | ||
| Provider $provider, | ||
| string $owner, | ||
| string $repository, | ||
| Coverage $coverage, | ||
| string $line | ||
| ): Coverage { | ||
| $line = trim($line); | ||
|
|
||
| // Skip empty lines | ||
| if ($line === '') { | ||
| return $coverage; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This line is not covered by any tests. |
||
| } | ||
|
|
||
| $parts = []; | ||
|
|
||
| // Match the record type and its data | ||
| if (preg_match(self::LINE_STRUCTURE, $line, $parts) !== 1) { | ||
| throw new ParseException( | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The next 5 lines are not covered by any tests. |
||
| sprintf( | ||
| 'Unable to parse structure of line in Go Cover file: %s', | ||
| $line | ||
| ) | ||
| ); | ||
| } | ||
|
|
||
| // Perform any path fixings we need to do | ||
| $filePath = $this->pathFixingService->fixPath( | ||
| $provider, | ||
| $owner, | ||
| $repository, | ||
| $parts['file'], | ||
| $coverage->getRoot() | ||
| ); | ||
|
|
||
| $files = $coverage->getFiles(); | ||
|
|
||
| /** | ||
| * Generally Go cover files are in sequential order - where all of the blocks for | ||
| * a particular file are together sequentially. So most of the time the last item | ||
| * of the coverage should be the one we're looking for. | ||
| * | ||
| * If its not, it _should_ be the first time we've seen the file path - in which case, | ||
| * we need to setup a new file! | ||
| */ | ||
| $file = end($files); | ||
|
|
||
| if (!$file || $file->getFileName() !== $filePath) { | ||
| $file = new File( | ||
| fileName: $filePath, | ||
| lines: [] | ||
| ); | ||
| $coverage->addFile($file); | ||
| } | ||
|
|
||
| $lineHits = (int)$parts['count']; | ||
|
|
||
| $startLine = (int)$parts['startLine']; | ||
| $endLine = (int)$parts['endLine']; | ||
|
|
||
| try { | ||
| /** | ||
| * Each block of a Go cover file (represented as a single line) will span between two lines (with | ||
| * start and end columns recorded). | ||
| * | ||
| * It's possible that between two blocks, one block ends at column 39, and another block starts | ||
| * from 39 onwards (non-inclusive). | ||
| * | ||
| * For example: | ||
| * | ||
| * ``` | ||
| * github.com/some-owner/some-repo/internal/ipv4/ipv4.go:54.2,54.46 1 1 | ||
| * github.com/some-owner/some-repo/internal/ipv4/ipv4.go:54.46,56.3 1 1 | ||
| * ``` | ||
| * | ||
| * If one starts while the other ends on the same line, that means we've uncovered an if/elseif/else | ||
| * line - and so we should any existing line data we have into a branch, and anything else in the block | ||
| * should continue as statements. | ||
| */ | ||
| $alreadyParsedLine = $file->getLine((string)$startLine); | ||
|
|
||
| if ($alreadyParsedLine instanceof Branch) { | ||
| // Increment the existing branch | ||
| $alreadyParsedLine->setLineHits($alreadyParsedLine->getLineHits() + $lineHits); | ||
| $alreadyParsedLine->addToBranchHits(count($alreadyParsedLine->getBranchHits()) + 1, $lineHits); | ||
| } else { | ||
| // The line we already have tracked is not a branch (it wont be when running through the | ||
| // individual block data), meaning we should convert it to a branch now we officially know its | ||
| // type isn't a simple statement. | ||
| $file->setLine( | ||
| new Branch( | ||
| lineNumber: $startLine, | ||
| lineHits: $alreadyParsedLine->getLineHits() + $lineHits, | ||
| branchHits: [ | ||
| $alreadyParsedLine->getLineHits(), | ||
| $lineHits | ||
| ] | ||
| ) | ||
| ); | ||
| } | ||
|
|
||
| if ($startLine === $endLine) { | ||
| // This block only spans the single line - we've already recorded it, so can finish | ||
| // here | ||
| return $coverage; | ||
| } | ||
|
|
||
| ++$startLine; | ||
| } catch (OutOfBoundsException) { | ||
| // No line recorded with this line number - we can start from here. | ||
| } | ||
|
|
||
| if ($startLine > $endLine) { | ||
| $this->parseStrategyLogger->error( | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The next 22 lines are not covered by any tests. |
||
| sprintf( | ||
| 'Invalid Go cover file. The end of a block (%s) started before the start line (%s)', | ||
| $endLine, | ||
| $startLine, | ||
| ), | ||
| [ | ||
| 'startLine' => $startLine, | ||
| 'endLine' => $endLine, | ||
| 'line' => $line, | ||
| ] | ||
| ); | ||
|
|
||
| throw new LogicException( | ||
| sprintf( | ||
| 'Invalid Go cover file for %s %s %s. The start and end lines did not match up (%d > %d).', | ||
| $provider->value, | ||
| $owner, | ||
| $repository, | ||
| $startLine, | ||
| $endLine, | ||
| ), | ||
| ); | ||
| } | ||
|
|
||
| for ($i = $startLine; $i <= $endLine; ++$i) { | ||
| // Everything is a statement in Go cover files | ||
| $line = new Statement( | ||
| lineNumber: $i, | ||
| lineHits: $lineHits | ||
| ); | ||
|
|
||
| $file->setLine($line); | ||
| } | ||
|
|
||
| return $coverage; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| mode: atomic | ||
| github.com/some-owner/some-repo/examples/domain/main.go:11.13,18.16 5 0 | ||
| github.com/some-owner/some-repo/examples/domain/main.go:18.16,20.3 1 0 | ||
|
|
||
| github.com/some-owner/some-repo/examples/domain/main.go:22.2,23.16 2 0 | ||
| github.com/some-owner/some-repo/examples/domain/main.go:23.16,25.3 1 0 | ||
|
|
||
| github.com/some-owner/some-repo/examples/domain/main.go:27.2,27.25 1 0 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| mode: atomic | ||
| mock/path/to/replace/github.com/some-owner/some-repo/examples/domain/main.go:11.13,18.16 5 0 | ||
| mock/path/to/replace/github.com/some-owner/some-repo/examples/domain/main.go:18.16,20.3 1 0 | ||
| mock/path/to/replace/github.com/some-owner/some-repo/examples/domain/main.go:22.2,23.16 2 0 | ||
| github.com/some-owner/some-repo/examples/domain/main.go:23.16,25.3 1 0 | ||
| mock/path/to/replace/github.com/some-owner/some-repo/examples/domain/main.go:27.2,27.25 1 0 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The next 10 lines are not covered by any tests.