Skip to content
This repository has been archived by the owner on Nov 19, 2023. It is now read-only.

Use new xp-framework/reflection library #43

Closed
wants to merge 32 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
a00651f
Migrate to new xp-framework/reflect library
thekid Sep 19, 2020
d85783e
MFH
thekid Sep 20, 2020
014abc5
Use new reflection API
thekid Sep 20, 2020
8c915c4
MFH
thekid Oct 4, 2020
e8aeeea
Query annotaton via class kind
thekid Oct 4, 2020
be2bf8e
Import Action attribute type
thekid Oct 4, 2020
622eab9
Fully qualify Test attribute
thekid Oct 4, 2020
60ee320
Support named arguments
thekid Oct 4, 2020
fe82274
Support standalone actions
thekid Oct 4, 2020
cc1afbe
Remove special case handling of eval argument, done by xp-framework/r…
thekid Oct 4, 2020
c4a7e92
Merge branch 'master' into refactor/use-reflection
thekid Oct 4, 2020
7a4f052
Use standalone actions
thekid Oct 4, 2020
814813f
Use xp-framework/reflection library version 0.1
thekid Dec 12, 2020
1f0bdc1
Fix PHP 7 compatiblity
thekid Dec 12, 2020
f15899f
Upgrade xp-framework/reflection to newest pre-release
thekid Dec 12, 2020
010b65e
Upgrade xp-framework/reflection to newest pre-release
thekid Dec 13, 2020
0e5149e
Rely on methods() returning method name as key
thekid Dec 13, 2020
9f59782
Use new Annotation::newInstance() method
thekid Dec 13, 2020
e6a82d2
Rely on methods() returning method name as key
thekid Dec 13, 2020
23626bd
Use Type::newInstance() and argument unpacking
thekid Dec 13, 2020
a1db82d
Prefer PHP 8 idiomatic over legacy `Action` annotations
thekid Dec 13, 2020
c6a834c
Remove optional parameter
thekid Dec 13, 2020
4305b0e
QA: Simplify code in valuesFor()
thekid Dec 14, 2020
de34d67
Do not yield name from beforeGroup() / afterGroup()
thekid Dec 14, 2020
5dfcc7c
Make use of new InvocationFailed::target() method
thekid Dec 14, 2020
9499579
Bump xp-framework/reflection to 0.6.0 for Methods::annotated()
thekid Dec 19, 2020
1f6f8e4
Migrate runner to new reflection library
thekid Dec 19, 2020
e6a78a8
Use stream_isatty() instead of posix_isatty()
thekid Dec 19, 2020
5979018
Add GitHub actions
thekid Dec 19, 2020
b5a1790
Use xp-framework/reflection version 1.0.0
thekid Dec 20, 2020
e65fe4f
Make use of package reflection
thekid Dec 20, 2020
0e03011
Remove obsolete runner
thekid Dec 20, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: Tests

on:
push:
branches:
- main
pull_request:

jobs:
tests:
if: "!contains(github.event.head_commit.message, 'skip ci')"
name: PHP ${{ matrix.php-versions }} on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
continue-on-error: ${{ matrix.php-versions == '8.1' }}
strategy:
fail-fast: false
matrix:
php-versions: ['7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1']
os: [ubuntu-latest, windows-latest]

steps:
- name: Configure git
if: runner.os == 'Windows'
run: git config --system core.autocrlf false; git config --system core.eol lf

- name: Checkout
uses: actions/checkout@v2

- name: Set up PHP ${{ matrix.php-versions }}
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-versions }}
ini-values: date.timezone=Europe/Berlin

- name: Setup Problem Matchers for PHP
run: echo "::add-matcher::${{ runner.tool_cache }}/php.json"

- name: Validate composer.json and composer.lock
run: composer validate

- name: Get Composer Cache Directory
id: composer-cache
run: echo "::set-output name=dir::$(composer config cache-files-dir)"

- name: Cache dependencies
uses: actions/cache@v2.1.3
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: ${{ runner.os }}-composer-

- name: Install dependencies
run: >
curl -sSL https://dl.bintray.com/xp-runners/generic/xp-run-8.2.0.sh > xp-run &&
composer install --prefer-dist &&
echo "vendor/autoload.php" > composer.pth

- name: Run test suite
run: sh xp-run xp.unittest.TestRunner src/test/php
5 changes: 5 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ Unittests change log

## ?.?.? / ????-??-??

* Added support for standalone actions: The `@action` annotation, e.g.
`#[@action(new Verify(...))]`, becomes `#[Verify(...)]`. See #40
(@thekid)
* Migrated codebase to use `xp-framework/reflection` library - @thekid

## 11.1.0 / 2020-09-20

* Fixed issue #42: No constructor error for class::method - @thekid
Expand Down
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
Unittests
=========

[![Build Status on TravisCI](https://secure.travis-ci.org/xp-framework/unittest.svg)](http://travis-ci.org/xp-framework/unittest)
[![Build status on GitHub](https://github.com/xp-framework/reflection/workflows/Tests/badge.svg)](https://github.com/xp-framework/reflection/actions)
[![XP Framework Module](https://raw.githubusercontent.com/xp-framework/web/master/static/xp-framework-badge.png)](https://github.com/xp-framework/core)
[![BSD Licence](https://raw.githubusercontent.com/xp-framework/web/master/static/licence-bsd.png)](https://github.com/xp-framework/core/blob/master/LICENCE.md)
[![Required PHP 7.0+](https://raw.githubusercontent.com/xp-framework/web/master/static/php-7_0plus.png)](http://php.net/)
[![Requires PHP 7.0+](https://raw.githubusercontent.com/xp-framework/web/master/static/php-7_0plus.svg)](http://php.net/)
[![Supports PHP 8.0+](https://raw.githubusercontent.com/xp-framework/web/master/static/php-8_0plus.svg)](http://php.net/)
[![Latest Stable Version](https://poser.pugx.org/xp-framework/unittest/version.png)](https://packagist.org/packages/xp-framework/unittest)

Unittests for the XP Framework
Expand Down Expand Up @@ -148,12 +149,12 @@ use unittest\{Test, Action};

class FileSystemTest {

#[Test, Action(eval: 'new IsPlatform("!WIN")')]
#[Test, new IsPlatform('!WIN')]
public function not_run_on_windows() {
// ...
}

#[Test, Action(eval: 'new VerifyThat(fn() => file_exists("/\$Recycle.Bin");')]
#[Test, new VerifyThat(eval: 'fn() => file_exists("/\$Recycle.Bin")')]
public function run_when_recycle_bin_exists() {
// ...
}
Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"keywords": ["module", "xp"],
"require" : {
"xp-framework/core": "^10.0 | ^9.0 | ^8.0 | ^7.0",
"xp-framework/reflection": "^1.0",
"php" : ">=7.0.0"
},
"bin": ["bin/xp.xp-framework.unittest.test"],
Expand Down
6 changes: 3 additions & 3 deletions src/main/php/unittest/DidNotCatch.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ class DidNotCatch implements AssertionFailedMessage {
/**
* Constructor
*
* @param lang.XPClass $expected
* @param lang.reflection.Type $expected
* @param lang.Throwable $thrown
*/
public function __construct($expected, $thrown= null) {
Expand All @@ -22,9 +22,9 @@ public function __construct($expected, $thrown= null) {
*/
public function format() {
if ($this->thrown) {
return 'Caught '.$this->thrown->compoundMessage().' instead of expected '.$this->expected->getName();
return 'Caught '.$this->thrown->compoundMessage().' instead of expected '.$this->expected->name();
} else {
return 'Expected '.$this->expected->getName().' not caught';
return 'Expected '.$this->expected->name().' not caught';
}
}
}
66 changes: 39 additions & 27 deletions src/main/php/unittest/Test.class.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php namespace unittest;

use lang\{Value, XPClass};
use lang\{Value, Reflection, XPClass};

abstract class Test implements Value {
public $instance, $method, $actions;
Expand Down Expand Up @@ -28,35 +28,52 @@ public abstract function hashCode();

/** @return ?string */
public function ignored() {
return $this->method->hasAnnotation('ignore') ? ($this->method->getAnnotation('ignore') ?: '(n/a)') : null;
if ($annotation= $this->method->annotation(Ignore::class)) {
return $annotation->argument(0) ?? '(n/a)';
}
return null;
}

/** @return ?int */
public function timeLimit() {
return $this->method->hasAnnotation('limit') ? $this->method->getAnnotation('limit', 'time') : 0;
if ($annotation= $this->method->annotation(Limit::class)) {

// Support both `Limit(time: ...)` and `Limit(['time' => ...])`
return $annotation->argument('time') ?? $annotation->argument(0)['time'];
}
return null;
}

/** @return ?var[] */
public function expected() {
if ($this->method->hasAnnotation('expect', 'class')) {
$message= $this->method->getAnnotation('expect', 'withMessage');
if ('' === $message || '/' === $message[0]) {
$pattern= $message;
} else {
$pattern= '/'.preg_quote($message, '/').'/';
}
return [XPClass::forName($this->method->getAnnotation('expect', 'class')), $pattern];
} else if ($this->method->hasAnnotation('expect')) {
return [XPClass::forName($this->method->getAnnotation('expect')), null];
if (null === ($annotation= $this->method->annotation(Expect::class))) return null;

// Support both `Expect(class: ...)` and `Expect(['class' => ...])`
$arguments= $annotation->arguments();
if (isset($arguments['class'])) {
$class= $arguments['class'];
$message= $arguments['withMessage'] ?? '';
} else if (is_array($arguments[0])) {
$class= $arguments[0]['class'];
$message= $arguments[0]['withMessage'];
} else {
return null;
$class= $arguments[0];
$message= '';
}

if ('' === $message || '/' === $message[0]) {
$pattern= $message;
} else {
$pattern= '/'.preg_quote($message, '/').'/';
}

return [Reflection::of($class), $pattern];
}

/** @return iterable */
public function variations() {
if ($this->method->hasAnnotation('values')) {
foreach ($this->valuesFor($this->instance, $this->method->getAnnotation('values')) as $args) {
if ($annotation= $this->method->annotation(Values::class)) {
foreach ($this->valuesFor($this->instance, $annotation->argument(0)) as $args) {
yield new TestVariation($this, is_array($args) ? $args : [$args]);
}
} else {
Expand Down Expand Up @@ -91,19 +108,14 @@ protected function valuesFor($test, $annotation) {
// Route "ClassName::methodName" -> static method of the given class,
// "self::method" -> static method of the test class, and "method"
// -> the run test's instance method
if (false === ($p= strpos($source, '::'))) {
return typeof($test)->getMethod($source)->setAccessible(true)->invoke($test, $args);
$p= strpos($source, '::');
if (false === $p) {
return Reflection::of($test)->method($source)->invoke($test, $args, $test);
}

$ref= substr($source, 0, $p);
if ('self' === $ref) {
$class= typeof($test);
} else if (strstr($ref, '.')) {
$class= XPClass::forName($ref);
} else {
$class= new XPClass($ref);
}
return $class->getMethod(substr($source, $p + 2))->invoke(null, $args);
$type= substr($source, 0, $p);
$reflect= Reflection::of('self' === $type ? $test : $type);
return $reflect->method(substr($source, $p + 2))->invoke(null, $args, $test);
}

/**
Expand Down
4 changes: 2 additions & 2 deletions src/main/php/unittest/TestCaseInstance.class.php
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
<?php namespace unittest;

use lang\Throwable;
use lang\{Throwable, Reflection};

class TestCaseInstance extends Test {

public function __construct($instance, $method= null, $actions= []) {
parent::__construct($instance, $method ?: typeof($instance)->getMethod($instance->name), $actions);
parent::__construct($instance, $method ?: Reflection::of($instance)->method($instance->name), $actions);
}

/**
Expand Down
41 changes: 22 additions & 19 deletions src/main/php/unittest/TestClass.class.php
Original file line number Diff line number Diff line change
@@ -1,59 +1,62 @@
<?php namespace unittest;

use lang\IllegalArgumentException;
use util\{NoSuchElementException, Objects};
use lang\{Reflect, IllegalArgumentException, IllegalStateException};
use util\NoSuchElementException;

class TestClass extends TestGroup {
private $class, $actions, $arguments;
private $reflect, $actions, $arguments;
private $tests= [];

static function __static() { }

/**
* Creates an instance from a testcase
*
* @param lang.XPClass $class
* @param lang.reflection.Type $reflect
* @param var[] $args
* @throws lang.IllegalArgumentException in case given argument is not a testcase class
* @throws lang.IllegalStateException in case a test method is overridden
* @throws util.NoSuchElementException in case given testcase class does not contain any tests
*/
public function __construct($class, $arguments) {
if (!$class->isSubclassOf(self::$base)) {
throw new IllegalArgumentException('Given argument is not a TestCase class ('.Objects::stringOf($class).')');
public function __construct($reflect, $arguments) {
if (!$reflect->is(self::$base)) {
throw new IllegalArgumentException('Given argument is not a TestCase class ('.$reflect->name().')');
}

foreach ($class->getMethods() as $method) {
if ($method->hasAnnotation('test')) {
$name= $method->getName();
if (self::$base->hasMethod($name)) {
throw $this->cannotOverride($method);
foreach ($reflect->methods() as $name => $method) {
if ($method->annotations()->provides(Test::class)) {
if (self::$base->method($name)) {
throw new IllegalStateException(sprintf(
'Cannot override %s::%s with test method in %s',
self::$base->name(),
$name,
$method->declaredIn()->name()
));
}
$this->tests[$name]= $method;
}
}

if (empty($this->tests)) {
throw new NoSuchElementException('No tests found in '.$class->getName());
throw new NoSuchElementException('No tests found in '.$reflect->name());
}

$this->class= $class;
$this->actions= iterator_to_array($this->actionsFor($class, TestAction::class));
$this->reflect= $reflect;
$this->actions= iterator_to_array($this->actionsFor($reflect, TestAction::class));
$this->arguments= (array)$arguments;
}

/** @return lang.XPClass */
public function type() { return $this->class; }
/** @return lang.reflection.Type */
public function reflect() { return $this->reflect; }

/** @return int */
public function numTests() { return sizeof($this->tests); }

/** @return iterable */
public function tests() {
$constructor= $this->class->getConstructor();
foreach ($this->tests as $name => $method) {
yield new TestCaseInstance(
$constructor->newInstance(array_merge([$name], $this->arguments)),
$this->reflect->newInstance($name, ...$this->arguments),
$method,
array_merge($this->actions, iterator_to_array($this->actionsFor($method, TestAction::class)))
);
Expand Down
Loading