1
0
Fork 0
mirror of https://we.phorge.it/source/arcanist.git synced 2024-11-22 14:52:40 +01:00

Wrapper for 'nose' unittest and code coverage tools.

Summary:
Wrapper for Python 'nose' (http://readthedocs.org/docs/nose/en/latest/)
testing tool.

Test Plan:
Install latest 'nose' v1.1.3. Currently it is available through
Github only (``pep install git+https://github.com/nose-devs/nose.git``).

Create a Python project with following structure:

  /package_name/module_name.py
  /tests/package_name/test_module_name.py

Write some tests

Run ``arc unit``

Reviewers: epriestley

Reviewed By: epriestley

CC: aran, Koolvin, zeeg

Differential Revision: https://secure.phabricator.com/D2322
This commit is contained in:
Julius Seporaitis 2012-04-27 12:11:41 -07:00 committed by epriestley
parent dd11fee5ef
commit cb051d8568
3 changed files with 279 additions and 0 deletions

View file

@ -112,6 +112,7 @@ phutil_register_library_map(array(
'BranchInfo' => 'branch',
'ComprehensiveLintEngine' => 'lint/engine/comprehensive',
'ExampleLintEngine' => 'lint/engine/example',
'NoseTestEngine' => 'unit/engine/nose',
'PhutilLintEngine' => 'lint/engine/phutil',
'PhutilModuleRequirements' => 'parser/phutilmodule',
'PhutilUnitTestEngine' => 'unit/engine/phutil',
@ -188,6 +189,7 @@ phutil_register_library_map(array(
'ArcanistXHPASTLinterTestCase' => 'ArcanistLinterTestCase',
'ComprehensiveLintEngine' => 'ArcanistLintEngine',
'ExampleLintEngine' => 'ArcanistLintEngine',
'NoseTestEngine' => 'ArcanistBaseUnitTestEngine',
'PhutilLintEngine' => 'ArcanistLintEngine',
'PhutilUnitTestEngine' => 'ArcanistBaseUnitTestEngine',
'PhutilUnitTestEngineTestCase' => 'ArcanistPhutilTestCase',

View file

@ -0,0 +1,257 @@
<?php
/*
* Copyright 2012 Facebook, 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.
*/
/**
* Very basic 'nose' unit test engine wrapper.
*
* Requires nose 1.1.3 for code coverage.
*
* @group unitrun
*/
final class NoseTestEngine extends ArcanistBaseUnitTestEngine {
public function run() {
$paths = $this->getPaths();
$affected_tests = array();
foreach ($paths as $path) {
$absolute_path = Filesystem::resolvePath($path);
if (is_dir($absolute_path)) {
$absolute_test_path = Filesystem::resolvePath("tests/".$path);
if (is_readable($absolute_test_path)) {
$affected_tests[] = $absolute_test_path;
}
}
if (is_readable($absolute_path)) {
$filename = basename($path);
$directory = dirname($path);
// assumes directory layout: tests/<package>/test_<module>.py
$relative_test_path = "tests/".$directory."/test_".$filename;
$absolute_test_path = Filesystem::resolvePath($relative_test_path);
if (is_readable($absolute_test_path)) {
$affected_tests[] = $absolute_test_path;
}
}
}
if (empty($affected_tests)) {
return array();
}
$futures = array();
$tmpfiles = array();
foreach ($affected_tests as $test_path) {
$xunit_tmp = new TempFile();
$cover_tmp = new TempFile();
$future = $this->buildTestFuture($test_path,
$xunit_tmp,
$cover_tmp);
$futures[$test_path] = $future;
$tmpfiles[$test_path] = array(
'xunit' => $xunit_tmp,
'cover' => $cover_tmp,
);
}
$results = array();
foreach (Futures($futures)->limit(4) as $test_path => $future) {
try {
list($stdout, $stderr) = $future->resolvex();
} catch(CommandException $exc) {
if ($exc->getError() > 1) {
// 'nose' returns 1 when tests are failing/broken.
throw $exc;
}
}
$xunit_tmp = $tmpfiles[$test_path]['xunit'];
$cover_tmp = $tmpfiles[$test_path]['cover'];
$results[] = $this->parseTestResults($test_path,
$xunit_tmp,
$cover_tmp);
}
return array_mergev($results);
}
public function buildTestFuture($path, $xunit_tmp, $cover_tmp) {
$cmd_line = csprintf("nosetests --with-xunit --xunit-file=%s",
$xunit_tmp);
if ($this->getEnableCoverage() !== false) {
$cmd_line .= csprintf(" --with-coverage --cover-xml " .
"--cover-xml-file=%s",
$cover_tmp);
}
return new ExecFuture("%C %s", $cmd_line, $path);
}
public function parseTestResults($path, $xunit_tmp, $cover_tmp) {
// xunit xsd: https://gist.github.com/959290
$xunit_dom = new DOMDocument();
$xunit_dom->loadXML(Filesystem::readFile($xunit_tmp));
// coverage is for all testcases in the executed $path
$coverage = array();
if ($this->getEnableCoverage() !== false) {
$coverage = $this->readCoverage($cover_tmp);
}
$results = array();
$testcases = $xunit_dom->getElementsByTagName("testcase");
foreach ($testcases as $testcase) {
$classname = $testcase->getAttribute("classname");
$name = $testcase->getAttribute("name");
$time = $testcase->getAttribute("time");
$status = ArcanistUnitTestResult::RESULT_PASS;
$user_data = "";
// A skipped test is a test which was ignored using framework
// mechanizms (e.g. @skip decorator)
$skipped = $testcase->getElementsByTagName("skipped");
if ($skipped->length > 0) {
$status = ArcanistUnitTestResult::RESULT_SKIP;
$messages = array();
for ($ii = 0; $ii < $skipped->length; $ii++) {
$messages[] = trim($skipped->item($ii)->nodeValue, " \n");
}
$user_data .= implode("\n", $messages);
}
// Failure is a test which the code has explicitly failed by using
// the mechanizms for that purpose. e.g., via an assertEquals
$failures = $testcase->getElementsByTagName("failure");
if ($failures->length > 0) {
$status = ArcanistUnitTestResult::RESULT_FAIL;
$messages = array();
for ($ii = 0; $ii < $failures->length; $ii++) {
$messages[] = trim($failures->item($ii)->nodeValue, " \n");
}
$user_data .= implode("\n", $messages)."\n";
}
// An errored test is one that had an unanticipated problem. e.g., an
// unchecked throwable, or a problem with an implementation of the
// test.
$errors = $testcase->getElementsByTagName("error");
if ($errors->length > 0) {
$status = ArcanistUnitTestResult::RESULT_BROKEN;
$messages = array();
for ($ii = 0; $ii < $errors->length; $ii++) {
$messages[] = trim($errors->item($ii)->nodeValue, " \n");
}
$user_data .= implode("\n", $messages)."\n";
}
$result = new ArcanistUnitTestResult();
$result->setName($classname.".".$name);
$result->setResult($status);
$result->setDuration($time);
$result->setCoverage($coverage);
$result->setUserData($user_data);
$results[] = $result;
}
return $results;
}
public function readCoverage($path) {
$coverage_dom = new DOMDocument();
$coverage_dom->loadXML(Filesystem::readFile($path));
$reports = array();
$classes = $coverage_dom->getElementsByTagName("class");
foreach ($classes as $class) {
// filename is actually python module path with ".py" at the end,
// e.g.: tornado.web.py
$relative_path = explode(".", $class->getAttribute("filename"));
array_pop($relative_path);
$relative_path = implode("/", $relative_path);
// first we check if the path is a directory (a Python package), if it is
// set relative and absolute paths to have __init__.py at the end.
$absolute_path = Filesystem::resolvePath($relative_path);
if (is_dir($absolute_path)) {
$relative_path .= "/__init__.py";
$absolute_path .= "/__init__.py";
}
// then we check if the path with ".py" at the end is file (a Python
// submodule), if it is - set relative and absolute paths to have
// ".py" at the end.
if (is_file($absolute_path.".py")) {
$relative_path .= ".py";
$absolute_path .= ".py";
}
if (!file_exists($absolute_path)) {
continue;
}
// get total line count in file
$line_count = count(file($absolute_path));
$coverage = "";
$start_line = 1;
$lines = $class->getElementsByTagName("line");
for ($ii = 0; $ii < $lines->length; $ii++) {
$line = $lines->item($ii);
$next_line = intval($line->getAttribute("number"));
for ($start_line; $start_line < $next_line; $start_line++) {
$coverage .= "N";
}
if (intval($line->getAttribute("hits")) == 0) {
$coverage .= "U";
}
else if (intval($line->getAttribute("hits")) > 0) {
$coverage .= "C";
}
$start_line++;
}
if ($start_line < $line_count) {
foreach (range($start_line, $line_count) as $line_num) {
$coverage .= "N";
}
}
$reports[$relative_path] = $coverage;
}
return $reports;
}
}

View file

@ -0,0 +1,20 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('arcanist', 'unit/engine/base');
phutil_require_module('arcanist', 'unit/result');
phutil_require_module('phutil', 'filesystem');
phutil_require_module('phutil', 'filesystem/tempfile');
phutil_require_module('phutil', 'future');
phutil_require_module('phutil', 'future/exec');
phutil_require_module('phutil', 'utils');
phutil_require_module('phutil', 'xsprintf/csprintf');
phutil_require_source('NoseTestEngine.php');