Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F2893442
ArcanistPhutilTestCase.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Advanced/Developer...
View Handle
View Hovercard
Size
14 KB
Referenced Files
None
Subscribers
None
ArcanistPhutilTestCase.php
View Options
<?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.
*/
/**
* Base test case for the very simple libphutil test framework.
*
* @task assert Making Test Assertions
* @task exceptions Exception Handling
* @task hook Hooks for Setup and Teardown
* @task internal Internals
*
* @group unitrun
*/
abstract
class
ArcanistPhutilTestCase
{
private
$assertions
=
0
;
private
$runningTest
;
private
$testStartTime
;
private
$results
=
array
(
)
;
private
$enableCoverage
;
private
$coverage
=
array
(
)
;
private
$projectRoot
;
private
$paths
;
/* -( Making Test Assertions )--------------------------------------------- */
/**
* Assert that two values are equal. The test fails if they are not.
*
* NOTE: This method uses PHP's strict equality test operator ("===") to
* compare values. This means values and types must be equal, key order must
* be identical in arrays, and objects must be referentially identical.
*
* @param wild The theoretically expected value, generated by careful
* reasoning about the properties of the system.
* @param wild The empirically derived value, generated by executing the
* test.
* @param string A human-readable description of what these values represent,
* and particularly of what a discrepancy means.
*
* @return void
* @task assert
*/
final
protected
function
assertEqual
(
$expect
,
$result
,
$message
=
null
)
{
if
(
$expect
===
$result
)
{
$this
->
assertions
++
;
return
;
}
$expect
=
PhutilReadableSerializer
::
printableValue
(
$expect
)
;
$result
=
PhutilReadableSerializer
::
printableValue
(
$result
)
;
$where
=
debug_backtrace
(
)
;
$where
=
array_shift
(
$where
)
;
$line
=
idx
(
$where
,
'line'
)
;
$file
=
basename
(
idx
(
$where
,
'file'
)
)
;
$output
=
"Assertion failed at line {$line} in {$file}"
;
if
(
$message
)
{
$output
.=
": {$message}"
;
}
$output
.=
"\n"
;
if
(
strpos
(
$expect
,
"\n"
)
===
false
&&
strpos
(
$result
,
"\n"
)
===
false
)
{
$output
.=
"Expected: {$expect}\n"
;
$output
.=
"Actual: {$result}"
;
}
else
{
$output
.=
"Expected vs Actual Output Diff\n"
;
$output
.=
ArcanistDiffUtils
::
renderDifferences
(
$expect
,
$result
,
$lines
=
0xFFFF
)
;
}
$this
->
failTest
(
$output
)
;
throw
new
ArcanistPhutilTestTerminatedException
(
$output
)
;
}
/**
* Assert an unconditional failure. This is just a convenience method that
* better indicates intent than using dummy values with assertEqual(). This
* causes test failure.
*
* @param string Human-readable description of the reason for test failure.
* @return void
* @task assert
*/
final
protected
function
assertFailure
(
$message
)
{
$this
->
failTest
(
$message
)
;
throw
new
ArcanistPhutilTestTerminatedException
(
$message
)
;
}
/**
* End this test by asserting that the test should be skipped for some
* reason.
*
* @param string Reason for skipping this test.
* @return void
* @task assert
*/
final
protected
function
assertSkipped
(
$message
)
{
$this
->
skipTest
(
$message
)
;
throw
new
ArcanistPhutilTestSkippedException
(
$message
)
;
}
/* -( Exception Handling )------------------------------------------------- */
/**
* This simplest way to assert exceptions are thrown.
*
* @param exception The expected exception.
* @param callable The thing which throws the exception.
*
* @return void
* @task exceptions
*/
final
protected
function
assertException
(
$expected_exception_class
,
$callable
)
{
$this
->
tryTestCases
(
array
(
'assertException'
=>
array
(
)
)
,
array
(
false
)
,
$callable
,
$expected_exception_class
)
;
}
/**
* Straightforward method for writing unit tests which check if some block of
* code throws an exception. For example, this allows you to test the
* exception behavior of ##is_a_fruit()## on various inputs:
*
* public function testFruit() {
* $this->tryTestCases(
* array(
* 'apple is a fruit' => new Apple(),
* 'rock is not a fruit' => new Rock(),
* ),
* array(
* true,
* false,
* ),
* array($this, 'tryIsAFruit'),
* 'NotAFruitException');
* }
*
* protected function tryIsAFruit($input) {
* is_a_fruit($input);
* }
*
* @param map Map of test case labels to test case inputs.
* @param list List of expected results, true to indicate that the case
* is expected to succeed and false to indicate that the case
* is expected to throw.
* @param callable Callback to invoke for each test case.
* @param string Optional exception class to catch, defaults to
* 'Exception'.
* @return void
* @task exceptions
*/
final
protected
function
tryTestCases
(
array
$inputs
,
array
$expect
,
$callable
,
$exception_class
=
'Exception'
)
{
if
(
count
(
$inputs
)
!==
count
(
$expect
)
)
{
$this
->
assertFailure
(
"Input and expectations must have the same number of values."
)
;
}
$labels
=
array_keys
(
$inputs
)
;
$inputs
=
array_values
(
$inputs
)
;
$expecting
=
array_values
(
$expect
)
;
foreach
(
$inputs
as
$idx
=>
$input
)
{
$expect
=
$expecting
[
$idx
]
;
$label
=
$labels
[
$idx
]
;
$caught
=
null
;
try
{
call_user_func
(
$callable
,
$input
)
;
}
catch
(
Exception
$ex
)
{
if
(
$ex
instanceof
ArcanistPhutilTestTerminatedException
)
{
throw
$ex
;
}
if
(
!
(
$ex
instanceof
$exception_class
)
)
{
throw
$ex
;
}
$caught
=
$ex
;
}
$actual
=
!
(
$caught
instanceof
Exception
)
;
if
(
$expect
===
$actual
)
{
if
(
$expect
)
{
$message
=
"Test case '{$label}' did not throw, as expected."
;
}
else
{
$message
=
"Test case '{$label}' threw, as expected."
;
}
}
else
{
if
(
$expect
)
{
$message
=
"Test case '{$label}' was expected to succeed, but it "
.
"raised an exception of class "
.
get_class
(
$ex
)
.
" with "
.
"message: "
.
$ex
->
getMessage
(
)
;
}
else
{
$message
=
"Test case '{$label}' was expected to raise an "
.
"exception, but it did not throw anything."
;
}
}
$this
->
assertEqual
(
$expect
,
$actual
,
$message
)
;
}
}
/**
* Convenience wrapper around @{method:tryTestCases} for cases where your
* inputs are scalar. For example:
*
* public function testFruit() {
* $this->tryTestCaseMap(
* array(
* 'apple' => true,
* 'rock' => false,
* ),
* array($this, 'tryIsAFruit'),
* 'NotAFruitException');
* }
*
* protected function tryIsAFruit($input) {
* is_a_fruit($input);
* }
*
* For cases where your inputs are not scalar, use @{method:tryTestCases}.
*
* @param map Map of scalar test inputs to expected success (true
* expects success, false expects an exception).
* @param callable Callback to invoke for each test case.
* @param string Optional exception class to catch, defaults to
* 'Exception'.
* @return void
* @task exceptions
*/
final
protected
function
tryTestCaseMap
(
array
$map
,
$callable
,
$exception_class
=
'Exception'
)
{
return
$this
->
tryTestCases
(
array_combine
(
array_keys
(
$map
)
,
array_keys
(
$map
)
)
,
array_values
(
$map
)
,
$callable
,
$exception_class
)
;
}
/* -( Hooks for Setup and Teardown )--------------------------------------- */
/**
* This hook is invoked once, before any tests in this class are run. It
* gives you an opportunity to perform setup steps for the entire class.
*
* @return void
* @task hook
*/
protected
function
willRunTests
(
)
{
return
;
}
/**
* This hook is invoked once, after any tests in this class are run. It gives
* you an opportunity to perform teardown steps for the entire class.
*
* @return void
* @task hook
*/
protected
function
didRunTests
(
)
{
return
;
}
/**
* This hook is invoked once per test, before the test method is invoked.
*
* @param string Method name of the test which will be invoked.
* @return void
* @task hook
*/
protected
function
willRunOneTest
(
$test_method_name
)
{
return
;
}
/**
* This hook is invoked once per test, after the test method is invoked.
*
* @param string Method name of the test which was invoked.
* @return void
* @task hook
*/
protected
function
didRunOneTest
(
$test_method_name
)
{
return
;
}
/* -( Internals )---------------------------------------------------------- */
/**
* Construct a new test case. This method is ##final##, use willRunTests() to
* provide test-wide setup logic.
*
* @task internal
*/
final
public
function
__construct
(
)
{
}
/**
* Mark the currently-running test as a failure.
*
* @param string Human-readable description of problems.
* @return void
*
* @task internal
*/
final
private
function
failTest
(
$reason
)
{
$this
->
resultTest
(
ArcanistUnitTestResult
::
RESULT_FAIL
,
$reason
)
;
}
/**
* This was a triumph. I'm making a note here: HUGE SUCCESS.
*
* @param string Human-readable overstatement of satisfaction.
* @return void
*
* @task internal
*/
final
private
function
passTest
(
$reason
)
{
$this
->
resultTest
(
ArcanistUnitTestResult
::
RESULT_PASS
,
$reason
)
;
}
/**
* Mark the current running test as skipped.
*
* @param string Description for why this test was skipped.
* @return void
* @task internal
*/
final
private
function
skipTest
(
$reason
)
{
$this
->
resultTest
(
ArcanistUnitTestResult
::
RESULT_SKIP
,
$reason
)
;
}
final
private
function
resultTest
(
$test_result
,
$reason
)
{
$coverage
=
$this
->
endCoverage
(
)
;
$result
=
new
ArcanistUnitTestResult
(
)
;
$result
->
setCoverage
(
$coverage
)
;
$result
->
setName
(
$this
->
runningTest
)
;
$result
->
setLink
(
$this
->
getLink
(
$this
->
runningTest
)
)
;
$result
->
setResult
(
$test_result
)
;
$result
->
setDuration
(
microtime
(
true
)
-
$this
->
testStartTime
)
;
$result
->
setUserData
(
$reason
)
;
$this
->
results
[
]
=
$result
;
}
/**
* Execute the tests in this test case. You should not call this directly;
* use @{class:PhutilUnitTestEngine} to orchestrate test execution.
*
* @return void
* @task internal
*/
final
public
function
run
(
)
{
$this
->
results
=
array
(
)
;
$reflection
=
new
ReflectionClass
(
$this
)
;
$methods
=
$reflection
->
getMethods
(
)
;
// Try to ensure that poorly-written tests which depend on execution order
// (and are thus not properly isolated) will fail.
shuffle
(
$methods
)
;
$this
->
willRunTests
(
)
;
foreach
(
$methods
as
$method
)
{
$name
=
$method
->
getName
(
)
;
if
(
preg_match
(
'/^test/'
,
$name
)
)
{
$this
->
runningTest
=
$name
;
$this
->
assertions
=
0
;
$this
->
testStartTime
=
microtime
(
true
)
;
try
{
$this
->
willRunOneTest
(
$name
)
;
$this
->
beginCoverage
(
)
;
$test_exception
=
null
;
try
{
call_user_func_array
(
array
(
$this
,
$name
)
,
array
(
)
)
;
$this
->
passTest
(
pht
(
'%d assertion(s) passed.'
,
$this
->
assertions
)
)
;
}
catch
(
Exception
$ex
)
{
$test_exception
=
$ex
;
}
$this
->
didRunOneTest
(
$name
)
;
if
(
$test_exception
)
{
throw
$test_exception
;
}
}
catch
(
ArcanistPhutilTestTerminatedException
$ex
)
{
// Continue with the next test.
}
catch
(
ArcanistPhutilTestSkippedException
$ex
)
{
// Continue with the next test.
}
catch
(
Exception
$ex
)
{
$ex_class
=
get_class
(
$ex
)
;
$ex_message
=
$ex
->
getMessage
(
)
;
$ex_trace
=
$ex
->
getTraceAsString
(
)
;
$message
=
"EXCEPTION ({$ex_class}): {$ex_message}\n{$ex_trace}"
;
$this
->
failTest
(
$message
)
;
}
}
}
$this
->
didRunTests
(
)
;
return
$this
->
results
;
}
final
public
function
setEnableCoverage
(
$enable_coverage
)
{
$this
->
enableCoverage
=
$enable_coverage
;
return
$this
;
}
/**
* @phutil-external-symbol function xdebug_start_code_coverage
*/
final
private
function
beginCoverage
(
)
{
if
(
!
$this
->
enableCoverage
)
{
return
;
}
$this
->
assertCoverageAvailable
(
)
;
xdebug_start_code_coverage
(
XDEBUG_CC_UNUSED
|
XDEBUG_CC_DEAD_CODE
)
;
}
/**
* @phutil-external-symbol function xdebug_get_code_coverage
* @phutil-external-symbol function xdebug_stop_code_coverage
*/
final
private
function
endCoverage
(
)
{
if
(
!
$this
->
enableCoverage
)
{
return
;
}
$result
=
xdebug_get_code_coverage
(
)
;
xdebug_stop_code_coverage
(
$cleanup
=
false
)
;
$coverage
=
array
(
)
;
foreach
(
$result
as
$file
=>
$report
)
{
if
(
strncmp
(
$file
,
$this
->
projectRoot
,
strlen
(
$this
->
projectRoot
)
)
)
{
continue
;
}
$max
=
max
(
array_keys
(
$report
)
)
;
$str
=
''
;
for
(
$ii
=
1
;
$ii
<=
$max
;
$ii
++
)
{
$c
=
idx
(
$report
,
$ii
)
;
if
(
$c
===
-
1
)
{
$str
.=
'U'
;
// Un-covered.
}
else
if
(
$c
===
-
2
)
{
// TODO: This indicates "unreachable", but it flags the closing braces
// of functions which end in "return", which is super ridiculous. Just
// ignore it for now.
$str
.=
'N'
;
// Not executable.
}
else
if
(
$c
===
1
)
{
$str
.=
'C'
;
// Covered.
}
else
{
$str
.=
'N'
;
// Not executable.
}
}
$coverage
[
substr
(
$file
,
strlen
(
$this
->
projectRoot
)
+
1
)
]
=
$str
;
}
// Only keep coverage information for files modified by the change.
$coverage
=
array_select_keys
(
$coverage
,
$this
->
paths
)
;
return
$coverage
;
}
final
private
function
assertCoverageAvailable
(
)
{
if
(
!
function_exists
(
'xdebug_start_code_coverage'
)
)
{
throw
new
Exception
(
"You've enabled code coverage but XDebug is not installed."
)
;
}
}
final
public
function
setProjectRoot
(
$project_root
)
{
$this
->
projectRoot
=
$project_root
;
return
$this
;
}
final
public
function
setPaths
(
array
$paths
)
{
$this
->
paths
=
$paths
;
return
$this
;
}
protected
function
getLink
(
$method
)
{
return
null
;
}
}
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Sun, Jan 19, 18:25 (1 w, 4 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1127411
Default Alt Text
ArcanistPhutilTestCase.php (14 KB)
Attached To
Mode
rARC Arcanist
Attached
Detach File
Event Timeline
Log In to Comment