zenovich / runkit

Runkit (official PECL PHP Runkit extension)
http://pecl.php.net/runkit
Other
611 stars 136 forks source link

segfault with runkit_class_emancipate, runkit_class_adopt and phpunit #59

Closed pprkut closed 9 years ago

pprkut commented 10 years ago

I'm trying the use runkit to replace the parent class of the class with a Mock version of it, which could make phpunit tests a bit simpler. Unfortunately I get a segfault when I try to execute the code. I put together a small testcase:

<?php

abstract class A
{

    public function to_be_mocked()
    {
        return 1;
    }

}

class B extends A
{

    public function foo()
    {
        return 1 + $this->to_be_mocked();
    }

}

class BTest extends PHPUnit_Framework_TestCase
{

    public function test()
    {
        $parent = $this->getMockForAbstractClass('A');

        runkit_class_emancipate('B');
        runkit_class_adopt('B', get_class($parent));

        $class = new B();

        $class->expects($this->once())
              ->method('to_be_mocked')
              ->will($this-returnValue(2));

        $this->assertSame(3, $class->foo());
    }

}

?>

running this using "phpunit testcase.php" gives:

PHPUnit 3.7.28 by Sebastian Bergmann.

Segmentation fault

Tested with php 5.5.7, phpunit 3.7.28 and runkit master.

tony2001 commented 10 years ago

Here's an easier testcase:

<?
class C {                                                                                                                                                    
    public function to_be_mocked()                                                                                                                           
    {                                                                                                                                                        
        return 1;                                                                                                                                            
    }                                                                                                                                                        
}                                                                                                                                                            

class A                                                                                                                                                      
{                                                                                                                                                            
    private $test = 1; //private var
    public function to_be_mocked()                                                                                                                           
    { 
        //this method is executed in the context of B, so the properties are looked up there
        var_dump($this->test); 
        return 1;                                                                                                                                            
    }                                                                                                                                                        
}                                                                                                                                                            

class B extends C                                                                                                                                            
{                                                                                                                                                            
    public function foo()                                                                                                                                    
    {                                                                                                                                                        
        return 1 + $this->to_be_mocked();                                                                                                                    
    }                                                                                                                                                        

}                                                                                                                                                            

runkit_class_emancipate('B');                                                                                                                                
runkit_class_adopt('B', "A");                                                                                                                                

$o = new B();                                                                                                                                                
$o->foo();      
?>
tony2001 commented 10 years ago

The problem is that there is a property 'cache' - properties_table for each zend_object. This table is created and filled using the default class properties (and parent class properties) when the object initialized. In this case the initial parent (class C) doesn't have any properties, so the table is empty. The "adopted" parent does have a property, hence PHP expects to find it in the object that should have inherited it. To fix this particular case it's enough to update B class default properties when adopting it, but there is a more complex version of the reproduce case, when the object is created before changing the classes:

$o = new B();
runkit_class_emancipate('B');
runkit_class_adopt('B', "A");

In this case updating B won't be enough since the object is already initialized at the time of the change, so all the object of class B have to be updated too to reflect these changes.

zenovich commented 9 years ago

runkit_class_adopt doesn't add anything except ancestral methods to the adopted class (see http://php.net/manual/en/function.runkit-class-adopt.php). Trying to access nonexisting properties leads to segmentation fault.

zenovich commented 9 years ago

Fixed in https://github.com/zenovich/runkit/commit/22628ac773103b1b0073c56b63fcc356a01919c0