Skip to content

[Improvement] Improve PhpUnit integration and prepare reusable constraints, resolves #340#350

Merged
lisachenko merged 9 commits into
goaop:2.xfrom
RunOpenCode:feature/improve-base-support-for-functional-testing
Aug 21, 2017
Merged

[Improvement] Improve PhpUnit integration and prepare reusable constraints, resolves #340#350
lisachenko merged 9 commits into
goaop:2.xfrom
RunOpenCode:feature/improve-base-support-for-functional-testing

Conversation

@TheCelavi

Copy link
Copy Markdown
Contributor

Fixes #340, created PhpUnit constraints which makes code more readable and maintainable.

$validIndex = (($index = $other->getIndex()) === null);


foreach ($joinPoints[$access][$other->getMethod()] as $position => $expression) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expected 1 newline after opening brace; 2 found


return $this->configuration['cacheDir'] . '/_proxies' . $suffix;
}
} No newline at end of file

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expected 1 newline at end of file; 0 found

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a newline

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added, don't worry about it, I do implement Nitpick suggestions if I miss something.

return true;
}

foreach ($joinPoints[$access][$other->getMethod()] as $expression) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expected 1 newline after opening brace; 2 found

*/
public function enterNode(Node $node)
{
if (

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expected 0 spaces after opening bracket; newline found

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Too long expression, one liner would make it unreadable.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For future reviews: always try to use simple checks in your if block. Extract meaningful variables and combine them with logic operator. So code will be more readable.

namespace Go\PhpUnit;

use \PHPUnit_Framework_Constraint as Constraint;
use \ReflectionClass;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It isn't required to add leading slashes there, please remove them to be consistent with all codebase.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didnt see that - IDEA added those, sorry

*/
public function matches($other)
{
$filename = (new ReflectionClass($other))->getFileName();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if $other isn't a class instance? Maybe add additional guard for that? To prevent uncontrolled errors

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No -> actually it should fail with exception, and it should fail hard! Like, it should blow your computer up.

Constraints here are internal classes (I didn't annotated them with @internal - I didn't noticed that that you are using those) -> and they are not reusable outside AOP framework, on top of that, they should not be used outside assertThat*.

So I do not see why we should add extra guards, if it is used wrongly -> it should and it will throw exception.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, accepted

return
!file_exists($this->configuration['cacheDir'] . $suffix)
&&
!file_exists($this->configuration['cacheDir'] . '/_proxies' . $suffix);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, please don't use such constructions )

We should write code for humans, not for computers, so it would be nice to do following:

  • add variable $transformedFileExists = file_exists($this->configuration['cacheDir'] . $suffix)
  • add variable $proxyFileExists = file_exists($this->configuration['cacheDir'] . '/_proxies' . $suffix)
  • add variable $fileHasBeenWoven = $transformedFileExists && $proxyFileExists;
  • just return: return !$fileHasBeenWoven;

It's simple, self-documented and readable. Also look at your expression one more time, I have a guess, that you should use || instead of && for you expression, whereas my check is valid. So it's important to give names for variables and do not inline it directly into if expressions.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If both file exists -> it is woven.
If none of file exists -> it is not woven.

If one exists and one does not -> there has been an error and assert must fail. Therefore, || should not be used.

In regards to expressions -> you have to have in mind that I am 16+ years in development and these kind of expressions are "human readable" for me.

As I said already, it is your library and I will try to follow your style and correct code, even when I disagree. But you have to cut me some slack sometimes - I do have my own style, habits and so on... these things will happen.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry if my review was very personal ( When I'm looking at code and perform reviewing, I'm checking for all possible ways how to make it better and can react very emotional but it relies only to the code, no to the person who make this changes.
Of course, I feel your experience and knowledge, thank you for contributing to the framework. Excuse me if I was too rude (

namespace Go\PhpUnit;

use \PHPUnit_Framework_Constraint as Constraint;
use \ReflectionClass;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here with leading slashes

/*
* Go! AOP framework
*
* @copyright Copyright 2011, Lisachenko Alexander <lisachenko.it@gmail.com>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about defining your template? ) You could use your credentials here for all new files. Also, the year will be correct. Of course, not a big deal, but why not

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is your library, I don't mind adding only your name here.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:)

$exists = false;
$validIndex = (($index = $other->getIndex()) === null);

foreach ($joinPoints[$access][$other->getMethod()] as $position => $expression) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could replace all this foreach code logic with following:

$advisorIndex = array_search($joinPoints[$access][$other->getMethod()], $advisorId, true);
$isIndexValid = ($index === null) || ($advisorIndex === $index);

return $advisorIndex !== false && $isIndexValid;


return $this->configuration['cacheDir'] . '/_proxies' . $suffix;
}
} No newline at end of file

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a newline

/**
* Extracts join points definitions from woven class proxy by parsing and traversing class AST.
*/
final class JoinPointsExtractor extends NodeVisitorAbstract

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow, looks very complicated for me.

Why not just to use ReflectionClass::getStaticPropertyValue() for asking the value of static property in the class? Of course, it will be already replaced with joinpoints, but it will be much easier than using AST for that task.

Alternatively, if you still want to parse source file, then give this job to goaop/parser-reflection, it's already on board. Just ask the same getStaticPropertyValue() to get this value for free :)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd vote to remove this class at all, no need in it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, here is the deal:

  • If it is done like I have implemented, woven class (proxy and transformed class) WILL NOT be loaded into memory, but I will manage to load join point definition in order to execute assertions. In that matter, one test case can have several tests which tests different weavings of one single class (in general, using different configurations - which is now supported). On top of that, you can load into memory original class and in same time introspect woven versions of that class.
  • If I use reflection class to load joint point definition, woven class will be loaded into memory, which means that one test case can test only one weaving of one single class, or for different weaving, tests must be executed in separate thread. Problem can occur especially if original class is loaded in test, and then you are trying to load same woven classes -> there will be "class is already loaded exception"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about goaop\parser-reflection? It provides reliable API, but without loading classes into PHP memory, so you can quickly reflect any file and just return joinpoints value, do you need my help with example? And it's working on top of AST.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was my first guess, then I introspected implementation of class Go\ParserReflection\ReflectionProperty because I needed that one.

However - not that tests are executed in one thread (PHPUnit), commands (different scope) in other. They do not share settings, they do not share class loader (Composer and AOP proxied composer).

That means that Go\ParserReflection\ReflectionEngine does not have in context of PHPUnit idea how to locate Proxy class in command scope.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That could be used if I can implement my own Locator and append it to ReflectionEngine, or, to re-init ReflectionEngine, but that will probably crash PHPUnit test context (for other tests), right?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

F**** it, ReflectionEngine is singleton... tell me how to manipulate with locator and I will use your library.

To use reflections to temporarily use my locator when I need it, and then to revert back previously used one? It is hackish, but I am out of ideas....

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can easily inject own configurator with second call to the ReflectionEngine::init() and pass your own locator for test proxies or just use autoload-dev in composer for specific namespace. In this case all should work from the box. But I missed that we run code in different threads, this can make things harder.

If you have troubles with making this working, then leave it as is, it's ok.
Maybe I will have a look later to integrate existing code for loading joinpoint property value.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can manage to work with ReflectionEngine by using reflections to access its protected static $locator property and temporarily set my own locator when I need it.

If you are OK with that -> consider it done then.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this solution is ok.
One more idea to consider: you can use ReflectionFile API to avoid even custom locator. File=>Namespace=>Class=>Static property. It doesn't require locator to be present.

*/
public function enterNode(Node $node)
{
if (

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For future reviews: always try to use simple checks in your if block. Extract meaningful variables and combine them with logic operator. So code will be more readable.


$ast = self::$extractor->getExtractedAst();

return eval('return ' . self::$printer->prettyPrint([$ast]));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😈 eval detected )

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Eval is evil if it used without caution.

Here -> eval is used having in mind its intent and purpose and it is used properly. It is safe because source of code for eval is known and predictable. On top of that I have to be honest, I do not know how to evaluate PHP code and load it into memory (after I have extracted desired part of AST) without using eval.

If you know different method - no problemo, will implement that!

Now, you have to first make decision wether you are going to allow extraction of join points without loading woven class into memory. After that decision, we can discuss about using eval here.

BTW: https://github.com/sebastianbergmann/phpunit-mock-objects/blob/master/src/Generator.php#L263

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

goaop/parser-reflection can analyze and build value from AST only without using eval, you can give it a try )

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I know, I saw that implementation, a lot of work is taken there in order to avoid eval.

However - I disagree that eval is always evil, eval is evil when it is used without caution on code from unreliable source.

In this particular case, eval is not evil, it is used with care.

Problem is locator for Go\ParserReflection\ReflectionEngine -> there are two execution contexts here.

@lisachenko

lisachenko commented Aug 19, 2017

Copy link
Copy Markdown
Member

Today I learned about custom PhpUnit constraints 👍 Thanks!

@lisachenko lisachenko changed the title Fixed #340 [Improvement] Improve PhpUnit integration and prepare reusable constraints, resolves #340 Aug 19, 2017
@TheCelavi

Copy link
Copy Markdown
Contributor Author

In general, I will:

  • Clean up and simplify code
  • Extend constraints to assert other weaving possibilities for class

You have to decide how do you want to load join points from woven classes, but again, have in mind that if it is not done via AST parsing, there can be conflicts in future when original class is previoysly loaded in memory during tests.

@TheCelavi

Copy link
Copy Markdown
Contributor Author

WIP - don't merge it, not finished yet. However, I did some refactoring, renaming, so if you have time you could look at it at share some thoughts and suggestions, of course - and if you like me to change something - shoot!

*/
protected function assertMethodNotWoven($class, $methodName, $advisorIdentifier, $message = '')
{
self::assertThat(new ClassAdvisorIdentifier($class, $methodName, $advisorIdentifier, AspectContainer::METHOD_PREFIX), new ClassMemberNotWovenConstraint($this->configuration), $message);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we care about line length? 120-140 symbols?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, I can split that, no problem at all.

* @param null|int $index Index of advisor identifier, or null if order is not important.
* @param string $message Assertion info message.
*/
protected function assertPropertyWoven($class, $propertyName, $advisorIdentifier, $index = null, $message = '')

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just quickly think about making advosorIdentifier optional. Because we can just check that there is at least key for our property. Same can be applied to methods as well. What do you think?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is excellent idea, will do that.

$suffix = substr($filename, strlen(PathResolver::realpath($this->configuration['appDir'])));

$transformedFileExists = file_exists($this->configuration['cacheDir'] . $suffix);
$proxyFileExists = file_exists($this->configuration['cacheDir'] . '/_proxies' . $suffix);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

$proxyFileExists = file_exists($this->configuration['cacheDir'] . '/_proxies' . $suffix);

// if any of files is missing, assert has to fail
$classWoven = $transformedFileExists && $proxyFileExists;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@lisachenko

Copy link
Copy Markdown
Member

All other stuff is 👍 ready for merge.

Comment thread tests/Go/PhpUnit/ClassLocator.php Outdated
use ReflectionClass;
use ReflectionProperty;

final class ClassLocator implements LocatorInterface

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[NEED HELP!] You can check this class, but in general, I have did my debugging, it works, locates files as I intended to be located.

{
ClassLocator::initialize($this->configuration);

$advisorIdentifiers = (new ReflectionClass($class))->getStaticPropertyValue('__joinPoints')->getValue();

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[NEED HELP!] @lisachenko Ok, I am stuck here. I have annotated class locator, however, I do not have any issue with that. I have tracked down issue up to:

Go\ParserReflection\Traits\ReflectionClassLikeTrait, method getStaticPropertyValue.

If I dump $properties on line no 831, I get empty array.

Do you have any idea what is going on?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I figure out what is happening. If Reflection class gets true for class_exists() at any point of time, it will not parse AST. If any of successive tests initializes original class, well, that is the end of the game...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, now I am 100% sure, goaop\parser-reflection can not be used for this task. Its usage is fragile, if for any reason original class is loaded into memory, every successive test will fail. I have jumped over hoops to make it work - it is useless, sorry, I am not able deliver that in such way.

In regards to using ReflectionEngine methods for parsing classes, I do not have any benefit from it, I will get a raw AST which I will have to eventually traverse by my self manually. Tree walker gives my much more control and I can solve problem easier with less code.

In regards of using eval, since you are against it, I will try to use NodeExpressionResolver instead. If it is possible to make it work, fine, if not, eval it is, sorry.

@lisachenko

Copy link
Copy Markdown
Member

@TheCelavi tests are green, changes are minimal, however need to extract logic of getting joinpoint property value into base class.

@TheCelavi

Copy link
Copy Markdown
Contributor Author

Thanks!

need to extract logic of getting joinpoint property value into base class.

I am not sure, $__joinpoints property is not only thing that we need to fully assert that something is woven or not. Moreover, I didn't finished all planned asserts, there are other weavings that can be checked against, let see if this is going to be a base class, or trait, or maybe utility class....

Full picture will be known after all asserts are setup.

Thanks for help!

@lisachenko

Copy link
Copy Markdown
Member

Should we squash and merge this PR? Or you want to add/polish something in it?

@TheCelavi

Copy link
Copy Markdown
Contributor Author

Well, that is up to you, I have understood that you want me to work on other class weaving assertions, like properties, introductions... And I have started to work on that. So, I guess, you could wait a tiny bitsy more :)

@lisachenko

Copy link
Copy Markdown
Member

Well, that is up to you, I have understood that you want me to work on other class weaving assertions, like properties, introductions

Hmmm ) We have some misunderstanding occasionally. We need to define rules, like MAY, SHOULD and MUST.

I don't want to make you doing something that wasn't planned by you earlier, we only discuss code review. So, if there was a phrase about "what if it will be property access, etc" - it was only for generalization, just additional information that MAY help you with decomposition and clean code ) That's my vision about that.

If something is really incorrect or wrong in PR, it will be described in review with direct sentence to remove/refactor/clean/etc.

I really appreciate your work and want to collaborate in effective way with you, without extra pressure from me 😄

Current PR can be merged into 2.x and you can continue your work on top of that to prevent big discussion in this PR

@TheCelavi

Copy link
Copy Markdown
Contributor Author

Ok, I agree, let's split it into smaller chunks of introduced features.

I don't feel pressurized, I liked idea to have all required asserts.

Constraints are internal classes so I have locked them with final keyword. If we need to modify them later, we can do that with ease without worrying about BC breaks. If we need to extend them in the future, we can just remove final from class definition.

You can merge.

@lisachenko
lisachenko merged commit c996662 into goaop:2.x Aug 21, 2017
@lisachenko

Copy link
Copy Markdown
Member

Squashed and merged into 2.x, thanks!

@lisachenko lisachenko added this to the 2.x milestone Aug 21, 2017
lisachenko added a commit that referenced this pull request Aug 21, 2017
* origin/2.x:
  [Improvement] Improve PhpUnit integration and prepare reusable constraints, resolves #340 (#350)
@lisachenko

Copy link
Copy Markdown
Member

Merged into master with applied type-hints.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Development

Successfully merging this pull request may close these issues.

3 participants