【问题标题】:Unserialize error when @runInSeparateProcess@runInSeparateProcess 时出现反序列化错误
【发布时间】:2013-01-07 16:28:06
【问题描述】:

我正在实现一个模块,该模块将提供一个 API 来处理和管理 PHP 会话。我正在测试Session\Manager 实现,它允许用户启动会话、设置ID、获取ID、销毁会话等。我正在使用PHPUnit 的@runInSeparateProcess 注释在单独的进程中测试此类中的方法。当我使用这个注释时,由于反序列化错误,我得到了 PHPUnit 抛出的异常。当我不使用注释时,测试按预期运行,并且在 null 不等于 false 时失败。

这是导致错误的测试。目前还没有实现细节,接口的所有方法都存在但不执行任何操作。

class ManagerTest extends PHPUnitTestCase {

    /**
     * Ensures that if the session has not been started yet the sessionExists
     * method returns false.
     *
     * @runInSeparateProcess
     */
    public function testSessionExistsWhenSessionHasNotBeenStarted() {
        $Manager = new \Session\Manager();
        $this->assertFalse($Manager->sessionExists());
    }

}

我能够将问题追溯到以下PHPUnit_Util_PHP::runJob() 方法。我正在运行 PHPUnit 3.7.5,被调用的 runJob 方法是:

/**
 * Runs a single job (PHP code) using a separate PHP process.
 *
 * @param  string                       $job
 * @param  PHPUnit_Framework_TestCase   $test
 * @param  PHPUnit_Framework_TestResult $result
 * @return array|null
 * @throws PHPUnit_Framework_Exception
 */
public function runJob($job, PHPUnit_Framework_Test $test = NULL, PHPUnit_Framework_TestResult $result = NULL)
{
    $process = proc_open(
      $this->getPhpBinary(),
      array(
        0 => array('pipe', 'r'),
        1 => array('pipe', 'w'),
        2 => array('pipe', 'w')
      ),
      $pipes
    );

    if (!is_resource($process)) {
        throw new PHPUnit_Framework_Exception(
          'Unable to create process for process isolation.'
        );
    }

    if ($result !== NULL) {
        $result->startTest($test);
    }

    $this->process($pipes[0], $job);
    fclose($pipes[0]);

    $stdout = stream_get_contents($pipes[1]);
    fclose($pipes[1]);

    $stderr = stream_get_contents($pipes[2]);
    fclose($pipes[2]);

    proc_close($process);
    $this->cleanup();

    if ($result !== NULL) {
        $this->processChildResult($test, $result, $stdout, $stderr);
    } else {
        return array('stdout' => $stdout, 'stderr' => $stderr);
    }
}

$stdout = stream_get_contents($pipes[1]); 行导致$stdout 等于? 的长字符串。在$this->processChildResult 中,$stdout 中的值未序列化,传递给此函数的无效值会触发警告,从而引发异常。我还能够确定$this->getPhpBinary() 的返回值是/usr/bin/php

抛出异常消息:

PHPUnit_Framework_Exception: ???...???"*???...??


Caused by
ErrorException: unserialize(): Error at offset 0 of 10081 bytes

感谢 hek2mgl,$job 中的 PHP 代码可以在 at this gist holding the output of a var_dump on $job 中查看。我创建了一个链接,因为它是相当多的代码,而且这个问题已经很长了。

我对这个特定领域的知识已经到了尽头,不知道如何进一步调试这个问题。我不确定为什么 @runInSeparateProcess 失败以及为什么 $stdout 运行单独的进程会导致一长串 ?分数。什么可能导致此问题,我该如何解决?我对这个模块处于停顿状态,因为未来的测试需要在单独的进程中运行,以确保会话的启动和销毁不会影响测试。

  • PHP:5.3.15
  • PHPUnit:3.7.5
  • 在 IDE 和命令行测试运行程序中导致错误

【问题讨论】:

  • 嘿!有趣的问题。不久前我遇到了runJob() 的一些问题。也许你可以从 github 克隆我的 hexdump 包,看看里面到底是什么???...???字符串。
  • 另外(也许更容易)你可以做die($job); 或其他任何事情来获得这份工作.. 这份工作是一个 php 文件,正如你提到的那样,将使用/usr/bin/php 执行。该脚本返回序列化的 php 数据作为输出。真的很想看到那个输出!!! :) 请在抓取代码后手动执行并将其添加到问题中
  • @hek2mgl 获取$job 变量的输出的好主意。我现在正在审查它,以确保没有任何可能保密的信息被泄露。我将使用输出链接更新答案。
  • 如果没有其他必需的文件,我无法执行测试,因此不能说太多。当你像我在回答中提到的那样执行它时,脚本会输出什么?
  • @hek2mgl 我能够将问题的一部分追溯到 $GLOBAL['__PHPUNIT_BOOTSTRAP'] 要求失败并导致致命错误。但是,即使我注释掉或临时修复到已知路径并尝试反序列化输出,对于指示错误的返回值,我也会得到 false。还有一些其他的事情正在发生,我需要尝试追踪。但恐怕这一切都得等到明天了。

标签: php phpunit


【解决方案1】:

好的,我知道这里发生了什么,男孩是不是很糟糕。

在测试期间,框架会尝试保留全局状态。在PHPUnit_Framework_TestCase::run() 函数中,当前的全局变量由PHPUnit_Util_GlobalState::getGlobalsAsString() 转换为PHP 代码字符串。我目前正在使用自动加载库来处理需要适当的类文件。此类在全局范围内的变量中声明,导致将该对象的序列化放入由getGlobalsAsString() 创建的$GLOBALS 数组中。无论出于何种原因,此序列化都包含空字节字符\u0000。当您尝试反序列化时,您会收到错误并最终导致输出也损坏。

我通过在我的测试引导程序中创建一个函数来解决此问题,该函数在全局范围之外创建自动加载对象。现在脚本不再尝试反序列化空字节,脚本不会产生错误并且测试用例按预期工作。这是一个非常有趣的问题,不确定是我的代码、PHPUnit 的代码中的错误还是serialize 函数的内部错误。


或者,您可以覆盖测试用例中的 run() 函数,并将测试用例显式设置为不保留全局状态。这可能是一个更简洁的解决方案,因为您很容易遇到其他问题,即按照默认行为再次要求所有包含的文件。

class ManagerTest extends PHPUnit_Framework_TestCase {

    public function run(PHPUnit_Framework_TestResult $result = null) {
        $this->setPreserveGlobalState(false);
        parent::run($result);
    }

}

必须在调用PHPUnit_Framework_TestCase::run() 函数之前设置全局状态。这可能是正确设置全局状态保存的唯一方法。

【讨论】:

  • 自 2015 年起,可以使用 @preserveGlobalState disabled 注释。
  • 这真是一个有用的答案。节省了我的时间。
  • 我最近遇到了这个问题,在我的情况下,结果证明@runInSeparateProcess 的测试明确调用了exit()。这是一个执行完整的 PHP 脚本并捕获输出缓冲的集成测试。
【解决方案2】:

PHPUnit_Util_PHP::runJob() 的 $job 参数是由 PHPUnit 生成的 php 代码,它以可以作为命令行脚本执行的方式包装测试方法的代码(聪明的技巧!)。 隔离测试会将测试结果的序列化php数据输出回phpunit。

如果您在 pf PHPUnit_Util_PHP::runJob() 顶部添加以下行,您可以调查此代码

file_put_contents('isolated.php', $job);

再次执行失败的测试后,文件isolated.php 应该已在当前目录中创建。您可以通过键入

查看可以执行它的代码(在当前文件夹中)
php isolated.php

您是否看到任何看起来凌乱的输出?

您还应该调试方法PHPUnit_Util_PHP::processChildResult()。该方法负责反序列化和检查测试结果。在那里放置一些 echos 或 var_dumps 或使用调试器。查看此方法时,很可能是由测试方法的非法输出引起的。

【讨论】:

    【解决方案3】:

    请尝试在 php.ini 中设置:

    detect_unicode = 关闭

    为我工作。

    【讨论】:

      【解决方案4】:

      当你的测试代码调用exit时,可能会抛出同样的错误是不值得的。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2022-11-17
        • 2013-12-18
        相关资源
        最近更新 更多