Skip to content
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

Debugger: resolve source location #844

Merged
merged 15 commits into from
Jan 19, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ matrix:

before_script:
- pecl install grpc || echo 'Failed to install grpc'
- if [[ $TRAVIS_PHP_VERSION =~ ^7 ]]; then pecl install stackdriver_debugger-alpha || echo 'Failed to install stackdriver_debugger'; fi
- composer install
- if [[ $TRAVIS_PHP_VERSION =~ ^hhvm ]]; then composer --no-interaction --dev remove google/protobuf google/gax google/proto-client; fi
- ./dev/sh/system-test-credentials
Expand Down
48 changes: 42 additions & 6 deletions src/Debugger/Breakpoint.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,13 @@ class Breakpoint implements \JsonSerializable
*/
private $location;

/**
* @var SourceLocation Resolved breakpoint location. The requested location
* may not exactly match the path to the deployed source. This value
* will be resolved by the Daemon to an existing file (if found).
*/
private $resolvedLocation;

/**
* @var string Condition that triggers the breakpoint. The condition is a
* compound boolean expression composed using expressions in a
Expand Down Expand Up @@ -314,7 +321,7 @@ public function action()
*/
public function location()
{
return $this->location;
return $this->resolvedLocation ?: $this->location;
}

/**
Expand Down Expand Up @@ -560,6 +567,25 @@ public function validate()
$this->validateExpressions();
}

/**
* Attempts to resolve the real (full) path to the specified source
* location. Returns true if a location was resolved.
*
* Example:
* ```
* $found = $breakpoint->resolveLocation();
* ```
*
* @return bool
*/
public function resolveLocation()
{
$resolver = new SourceLocationResolver();
$this->resolvedLocation = $resolver->resolve($this->location);

return $this->resolvedLocation !== null;
}

private function setError($type, $message, array $parameters = [])
{
$this->status = new StatusMessage(
Expand Down Expand Up @@ -608,7 +634,17 @@ private function validateSourceLocation()
return false;
}

$path = $this->location->path();
if (!$this->resolveLocation()) {
$this->setError(
StatusMessage::REFERENCE_BREAKPOINT_SOURCE_LOCATION,
'Could not find source location: $0',
[$this->location->path()]
);
return false;
}

$path = $this->resolvedLocation->path();
$lineNumber = $this->resolvedLocation->line();
$info = new \SplFileInfo($path);

// Ensure the file exists and is readable
Expand All @@ -632,25 +668,25 @@ private function validateSourceLocation()
}

$file = $info->openFile('r');
$file->seek($this->location->line() - 1);
$file->seek($lineNumber - 1);
$line = ltrim($file->current() ?: '');

// Ensure the line exists and is not empty
if ($line === '') {
$this->setError(
StatusMessage::REFERENCE_BREAKPOINT_SOURCE_LOCATION,
'Invalid breakpoint location - Invalid file line: $0.',
[$this->location->line()]
[$lineNumber]
);
return false;
}

// Check that the line is not a comment
if ($line[0] == '/' || ($line[0] == '*' && $this->inMultilineComment($file, $this->location->line() - 1))) {
if ($line[0] == '/' || ($line[0] == '*' && $this->inMultilineComment($file, $lineNumber - 1))) {
$this->setError(
StatusMessage::REFERENCE_BREAKPOINT_SOURCE_LOCATION,
'Invalid breakpoint location - Invalid file line: $0.',
[$this->location->line()]
[$lineNumber]
);
return false;
}
Expand Down
72 changes: 72 additions & 0 deletions src/Debugger/MatchingFileIterator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php
/**
* Copyright 2018 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

namespace Google\Cloud\Debugger;

/**
* This iterator returns files that match the provided file in the provided
* search path.
*
* Example:
* ```
* $iterator = new MatchingFileIterator('.', 'src/Debugger/DebuggerClient.php');
* $matches = iterator_to_array($iterator);
* ```
*
* @access private
*/
class MatchingFileIterator extends \FilterIterator
{
/**
* @var string The file pattern to search for.
*/
private $file;

/**
* Create a new MatchingFileIterator.
*
* @param string $searchPath The root path to search in
* @param string $file The file to search for
*/
public function __construct($searchPath, $file)
{
parent::__construct(
new \RecursiveIteratorIterator(

This comment was marked as spam.

This comment was marked as spam.

new \RecursiveDirectoryIterator(
realpath($searchPath),
\FilesystemIterator::SKIP_DOTS
)
)
);
$this->file = $file;
}

/**
* FilterIterator callback to determine whether or not the value should be
* accepted.
*
* @access private
* @return boolean
*/
public function accept()
{
$candidate = $this->getInnerIterator()->current();

// Check that the candidate file (a full file path) ends in the pattern we are searching for.
return strrpos($candidate, $this->file) === strlen($candidate) - strlen($this->file);
}
}
120 changes: 120 additions & 0 deletions src/Debugger/SourceLocationResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
<?php
/**
* Copyright 2018 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

namespace Google\Cloud\Debugger;

/**
* This class handles searching for a source file in the application's source
* tree. A debugger breakpoint may be requested for a source path that has
* extra or missing folders.
*
* Example:
* ```
* $location = new SourceLocation('src/Debugger/DebuggerClient.php', 1);
* $resolver = new SourceLocationResolver();
* $resolvedLocation = $resolver->resolve($location);
* ```
*/
class SourceLocationResolver
{
/**
* Resolve the full path of an existing file in the application's source.
* If no matching source file is found, then return null. If found, the
* resolved location will include the full, absolute path to the source
* file.
*
* There are 3 cases for resolving a SourceLocation:
*
* Case 1: The exact path is found
*
* Example:
* ```
* $location = new SourceLocation('src/Debugger/DebuggerClient.php', 1);
* $resolver = new SourceLocationResolver();
* $resolvedLocation = $resolver->resolve($location);
* ```
*
* Case 2: There are extra folder(s) in the requested breakpoint path
*
* Example:
* ```
* $location = new SourceLocation('extra/folder/src/Debugger/DebuggerClient.php', 1);
* $resolver = new SourceLocationResolver();
* $resolvedLocation = $resolver->resolve($location);
* ```
*
* Case 3: There are fewer folders in the requested breakpoint path
*
* Example:
* ```
* $location = new SourceLocation('Debugger/DebuggerClient.php', 1);
* $resolver = new SourceLocationResolver();
* $resolvedLocation = $resolver->resolve($location);
*
* @param SourceLocation $location The location to resolve.
* @return SourceLocation|null
*/
public function resolve(SourceLocation $location)
{
$basename = basename($location->path());
$prefixes = $this->searchPrefixes($location->path());
$includePaths = explode(PATH_SEPARATOR, get_include_path());

// Phase 1: search for an exact file match and try stripping off extra
// folders
foreach ($prefixes as $prefix) {
foreach ($includePaths as $path) {
$file = implode(DIRECTORY_SEPARATOR, [$path, $prefix, $basename]);
if (file_exists($file)) {
return new SourceLocation(realpath($file), $location->line());
}
}
}

// Phase 2: recursively search folders for
foreach ($includePaths as $includePath) {
$iterator = new MatchingFileIterator(
$includePath,
$location->path()
);
foreach ($iterator as $file => $info) {
return new SourceLocation(realpath($file), $location->line());
}
}

return null;
}

/**
* Returns an array of relative paths for this file by recursively removing
* each leading directory.
*
* @param string $path The source path
* @return string[]
*/
private function searchPrefixes($path)
{
$dirname = dirname($path);
$directoryParts = explode(DIRECTORY_SEPARATOR, $dirname);
$directories = [];
while ($directoryParts) {
$directories[] = implode(DIRECTORY_SEPARATOR, $directoryParts);
array_shift($directoryParts);
}
return $directories;
}
}
15 changes: 15 additions & 0 deletions tests/snippets/Debugger/BreakpointTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -180,4 +180,19 @@ public function testValidate()

$res = $snippet->invoke('valid');
}

public function testResolveLocation()
{
$breakpoint = new Breakpoint([
'location' => [
'path' => __FILE__,
'line' => 1
]
]);
$snippet = $this->snippetFromMethod(Breakpoint::class, 'resolveLocation');
$snippet->addLocal('breakpoint', $breakpoint);

$res = $snippet->invoke('found');
$this->assertTrue($res->returnVal());
}
}
35 changes: 35 additions & 0 deletions tests/snippets/Debugger/MatchingFileIteratorTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php
/**
* Copyright 2018 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

namespace Google\Cloud\Tests\Snippets\Debugger;

use Google\Cloud\Dev\Snippet\SnippetTestCase;
use Google\Cloud\Debugger\MatchingFileIterator;

/**
* @group debugger
*/
class MatchingFileIteratorTest extends SnippetTestCase
{
public function testClass()
{
$snippet = $this->snippetFromClass(MatchingFileIterator::class);
$snippet->addUse(MatchingFileIterator::class);
$matches = $snippet->invoke('matches')->returnVal();
$this->assertCount(1, $matches);
}
}
Loading