Skip to content

Commit 9c332a8

Browse files
committed
Add attribute management
1 parent 55801df commit 9c332a8

File tree

8 files changed

+375
-1
lines changed

8 files changed

+375
-1
lines changed

phpunit.xml.dist

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,6 @@
1414
</php>
1515
<extensions>
1616
<extension class="Zalas\PHPUnit\Globals\AnnotationExtension"/>
17+
<extension class="Zalas\PHPUnit\Globals\AttributeExtension"/>
1718
</extensions>
1819
</phpunit>

src/Attribute/Env.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Zalas\PHPUnit\Globals\Attribute;
5+
6+
use Attribute;
7+
8+
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
9+
final class Env
10+
{
11+
use TargetAware;
12+
13+
public function __construct(
14+
public string $name,
15+
public string $value = '',
16+
public bool $unset = false,
17+
) {
18+
}
19+
}

src/Attribute/Putenv.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Zalas\PHPUnit\Globals\Attribute;
5+
6+
use Attribute;
7+
8+
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
9+
final class Putenv
10+
{
11+
use TargetAware;
12+
13+
public function __construct(
14+
public string $name,
15+
public string $value = '',
16+
public bool $unset = false,
17+
) {
18+
}
19+
}

src/Attribute/Server.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Zalas\PHPUnit\Globals\Attribute;
5+
6+
use Attribute;
7+
8+
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
9+
final class Server
10+
{
11+
use TargetAware;
12+
13+
public function __construct(
14+
public string $name,
15+
public string $value = '',
16+
public bool $unset = false,
17+
) {
18+
}
19+
}

src/Attribute/TargetAware.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Zalas\PHPUnit\Globals\Attribute;
5+
6+
trait TargetAware
7+
{
8+
private int $target = 0;
9+
10+
/**
11+
* @internal
12+
*/
13+
public function withTarget(int $target): self
14+
{
15+
$clone = clone $this;
16+
$clone->target = $target;
17+
18+
return $clone;
19+
}
20+
21+
/**
22+
* @internal
23+
*/
24+
public function getTarget(): int
25+
{
26+
return $this->target;
27+
}
28+
}

src/AttributeExtension.php

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Zalas\PHPUnit\Globals;
5+
6+
use PHPUnit\Runner\AfterTestHook;
7+
use PHPUnit\Runner\BeforeTestHook;
8+
use Zalas\PHPUnit\Globals\Attribute\Env;
9+
use Zalas\PHPUnit\Globals\Attribute\Putenv;
10+
use Zalas\PHPUnit\Globals\Attribute\Server;
11+
12+
final class AttributeExtension implements BeforeTestHook, AfterTestHook
13+
{
14+
private $server;
15+
private $env;
16+
private $getenv;
17+
18+
public function executeBeforeTest(string $test): void
19+
{
20+
$this->backupGlobals();
21+
$this->readGlobalAttributes($test);
22+
}
23+
24+
public function executeAfterTest(string $test, float $time): void
25+
{
26+
$this->restoreGlobals();
27+
}
28+
29+
private function backupGlobals(): void
30+
{
31+
$this->server = $_SERVER;
32+
$this->env = $_ENV;
33+
$this->getenv = \getenv();
34+
}
35+
36+
private function restoreGlobals(): void
37+
{
38+
$_SERVER = $this->server;
39+
$_ENV = $this->env;
40+
41+
foreach (\array_diff_assoc($this->getenv, \getenv()) as $name => $value) {
42+
\putenv(\sprintf('%s=%s', $name, $value));
43+
}
44+
foreach (\array_diff_assoc(\getenv(), $this->getenv) as $name => $value) {
45+
\putenv($name);
46+
}
47+
}
48+
49+
private function readGlobalAttributes(string $test)
50+
{
51+
$globalVars = $this->parseGlobalAttributes($test);
52+
53+
foreach ($globalVars['env'] as $name => $value) {
54+
$_ENV[$name] = $value;
55+
}
56+
foreach ($globalVars['server'] as $name => $value) {
57+
$_SERVER[$name] = $value;
58+
}
59+
foreach ($globalVars['putenv'] as $name => $value) {
60+
\putenv(\sprintf('%s=%s', $name, $value));
61+
}
62+
63+
$unsetVars = $this->findUnsetVarAttributes($test);
64+
65+
foreach ($unsetVars['unset-env'] as $name) {
66+
unset($_ENV[$name]);
67+
}
68+
foreach ($unsetVars['unset-server'] as $name) {
69+
unset($_SERVER[$name]);
70+
}
71+
foreach ($unsetVars['unset-getenv'] as $name) {
72+
\putenv($name);
73+
}
74+
}
75+
76+
private function parseGlobalAttributes(string $test): array
77+
{
78+
$globals = ['env' => [], 'server' => [], 'putenv' => []];
79+
80+
$attributes = $this->findSetVarAttributes($test);
81+
foreach ($attributes as $attribute) {
82+
match (true) {
83+
$attribute instanceof Env => $globals['env'][$attribute->name] = $attribute->value,
84+
$attribute instanceof Server => $globals['server'][$attribute->name] = $attribute->value,
85+
$attribute instanceof Putenv => $globals['putenv'][$attribute->name] = $attribute->value,
86+
};
87+
}
88+
89+
return $globals;
90+
}
91+
92+
private function findSetVarAttributes(string $test): array
93+
{
94+
$attributes = $this->parseTestMethodAttributes($test);
95+
96+
$attributes = \array_filter(
97+
$attributes,
98+
static fn (Env|Server|Putenv $attribute) => false === $attribute->unset,
99+
ARRAY_FILTER_USE_BOTH
100+
);
101+
102+
\usort($attributes, static fn (Env|Server|Putenv $a, Env|Server|Putenv $b) => $a->getTarget() <=> $b->getTarget());
103+
104+
return $attributes;
105+
}
106+
107+
private function findUnsetVarAttributes(string $test): array
108+
{
109+
$unset = ['unset-env' => [], 'unset-server' => [], 'unset-getenv' => []];
110+
111+
$attributes = $this->parseTestMethodAttributes($test);
112+
foreach ($attributes as $attribute) {
113+
if (false === $attribute->unset) {
114+
continue;
115+
}
116+
117+
match (true) {
118+
$attribute instanceof Env => $unset['unset-env'][] = $attribute->name,
119+
$attribute instanceof Server => $unset['unset-server'][] = $attribute->name,
120+
$attribute instanceof Putenv => $unset['unset-getenv'][] = $attribute->name,
121+
};
122+
}
123+
124+
return $unset;
125+
}
126+
127+
private function parseTestMethodAttributes(string $test): array
128+
{
129+
$parts = \preg_split('/ |::/', $test);
130+
131+
if (!\class_exists($parts[0])) {
132+
return [];
133+
}
134+
135+
$methodAttributes = [];
136+
if (!empty($parts[1])) {
137+
$methodAttributes = $this->collectGlobalsFromAttributes(
138+
(new \ReflectionMethod($parts[0], $parts[1]))->getAttributes()
139+
);
140+
}
141+
142+
return \array_merge(
143+
$this->collectGlobalsFromAttributes((new \ReflectionClass($parts[0]))->getAttributes()),
144+
$methodAttributes,
145+
);
146+
}
147+
148+
private function collectGlobalsFromAttributes(array $attributes): array
149+
{
150+
$globals = [];
151+
152+
foreach ($attributes as $attribute) {
153+
if (!\str_starts_with($attribute->getName(), 'Zalas\\PHPUnit\\Globals\\Attribute\\')) {
154+
continue;
155+
}
156+
157+
/** @var Env|Server|Putenv $instance */
158+
$instance = $attribute->newInstance();
159+
$globals[] = $instance->withTarget($attribute->getTarget());
160+
}
161+
162+
return $globals;
163+
}
164+
}

tests/AnnotationExtensionTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ public function test_it_backups_the_state()
110110
}
111111

112112
/**
113-
* @depends test_it_backups_the_state
113+
* @depends self::test_it_backups_the_state
114114
*/
115115
public function test_it_cleans_up_after_itself()
116116
{

tests/AttributeExtensionTest.php

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Zalas\PHPUnit\Globals\Tests;
5+
6+
use PHPUnit\Framework\TestCase;
7+
use PHPUnit\Runner\AfterTestHook;
8+
use PHPUnit\Runner\BeforeTestHook;
9+
use Zalas\PHPUnit\Globals\AnnotationExtension;
10+
use Zalas\PHPUnit\Globals\Attribute\Env;
11+
use Zalas\PHPUnit\Globals\Attribute\Putenv;
12+
use Zalas\PHPUnit\Globals\Attribute\Server;
13+
14+
#[Env('APP_ENV', 'test')]
15+
#[Server('APP_DEBUG', '0')]
16+
#[Putenv('APP_HOST', 'localhost')]
17+
class AttributeExtensionTest extends TestCase
18+
{
19+
public function test_it_is_a_test_hook()
20+
{
21+
$this->assertInstanceOf(BeforeTestHook::class, new AnnotationExtension());
22+
$this->assertInstanceOf(AfterTestHook::class, new AnnotationExtension());
23+
}
24+
25+
#[Env('APP_ENV', 'test_foo')]
26+
#[Server('APP_DEBUG', '1')]
27+
#[Putenv('APP_HOST', 'dev')]
28+
public function test_it_reads_global_variables_from_method_annotations()
29+
{
30+
$this->assertArraySubset(['APP_ENV' => 'test_foo'], $_ENV);
31+
$this->assertArraySubset(['APP_DEBUG' => '1'], $_SERVER);
32+
$this->assertArraySubset(['APP_HOST' => 'dev'], \getenv());
33+
}
34+
35+
public function test_it_reads_global_variables_from_class_annotations()
36+
{
37+
$this->assertArraySubset(['APP_ENV' => 'test'], $_ENV);
38+
$this->assertArraySubset(['APP_DEBUG' => '0'], $_SERVER);
39+
$this->assertArraySubset(['APP_HOST' => 'localhost'], \getenv());
40+
}
41+
42+
#[Env('FOO', 'foo')]
43+
#[Server('BAR', 'bar')]
44+
#[Putenv('BAZ', 'baz')]
45+
public function test_it_reads_additional_global_variables_from_methods()
46+
{
47+
$this->assertArraySubset(['APP_ENV' => 'test'], $_ENV);
48+
$this->assertArraySubset(['APP_DEBUG' => '0'], $_SERVER);
49+
$this->assertArraySubset(['APP_HOST' => 'localhost'], \getenv());
50+
$this->assertArraySubset(['FOO' => 'foo'], $_ENV);
51+
$this->assertArraySubset(['BAR' => 'bar'], $_SERVER);
52+
$this->assertArraySubset(['BAZ' => 'baz'], \getenv());
53+
}
54+
55+
#[Env('APP_ENV', 'test_foo')]
56+
#[Env('APP_ENV', 'test_foo_bar')]
57+
#[Server('APP_DEBUG', '1')]
58+
#[Server('APP_DEBUG', '2')]
59+
#[PutEnv('APP_HOST', 'host1')]
60+
#[PutEnv('APP_HOST', 'host2')]
61+
public function test_it_reads_the_latest_var_defined()
62+
{
63+
$this->assertArraySubset(['APP_ENV' => 'test_foo_bar'], $_ENV);
64+
$this->assertArraySubset(['APP_DEBUG' => '2'], $_SERVER);
65+
$this->assertArraySubset(['APP_HOST' => 'host2'], \getenv());
66+
}
67+
68+
#[Env('APP_ENV')]
69+
#[Server('APP_DEBUG')]
70+
#[PutEnv('APP_HOST')]
71+
public function test_it_reads_empty_vars()
72+
{
73+
$this->assertArraySubset(['APP_ENV' => ''], $_ENV);
74+
$this->assertArraySubset(['APP_DEBUG' => ''], $_SERVER);
75+
$this->assertArraySubset(['APP_HOST' => ''], \getenv());
76+
}
77+
78+
#[Env('APP_ENV', unset: true)]
79+
#[Server('APP_DEBUG', unset: true)]
80+
#[PutEnv('APP_HOST', unset: true)]
81+
public function test_it_unsets_vars()
82+
{
83+
$this->assertArrayNotHasKey('APP_ENV', $_ENV);
84+
$this->assertArrayNotHasKey('APP_DEBUG', $_SERVER);
85+
$this->assertArrayNotHasKey('APP_HOST', \getenv());
86+
}
87+
88+
public function test_it_backups_the_state()
89+
{
90+
// this test is only here so the next one could verify the state is brought back
91+
92+
$_ENV['FOO'] = 'env_foo';
93+
$_SERVER['BAR'] = 'server_bar';
94+
\putenv('FOO=putenv_foo');
95+
\putenv('USER=foobar');
96+
97+
$this->assertArrayHasKey('FOO', $_ENV);
98+
$this->assertArrayHasKey('BAR', $_SERVER);
99+
$this->assertSame('putenv_foo', \getenv('FOO'));
100+
$this->assertSame('foobar', \getenv('USER'));
101+
}
102+
103+
/**
104+
* @depends self::test_it_backups_the_state
105+
*/
106+
public function test_it_cleans_up_after_itself()
107+
{
108+
$this->assertArrayNotHasKey('FOO', $_ENV);
109+
$this->assertArrayNotHasKey('BAR', $_SERVER);
110+
$this->assertFalse(\getenv('FOO'), 'It removes environment variables initialised in a test.');
111+
$this->assertNotSame('foobar', \getenv('USER'), 'It restores environment variables changed in a test.');
112+
$this->assertNotFalse(\getenv('USER'), 'It restores environment variables changed in a test.');
113+
}
114+
115+
/**
116+
* Provides a replacement for the assertion deprecated in PHPUnit 8 and removed in PHPUnit 9.
117+
* @param array $subset
118+
* @param array $array
119+
*/
120+
public static function assertArraySubset($subset, $array, bool $checkForObjectIdentity = false, string $message = ''): void
121+
{
122+
self::assertSame($array, \array_replace_recursive($array, $subset));
123+
}
124+
}

0 commit comments

Comments
 (0)