Skip to content

Commit 776c827

Browse files
committed
Implement new config and output format
1 parent ada6552 commit 776c827

File tree

16 files changed

+678
-254
lines changed

16 files changed

+678
-254
lines changed

.devcontainer/devcontainer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
"[php]": {
1212
"editor.defaultFormatter": "wongjn.php-sniffer",
1313
"editor.formatOnSave": true,
14-
"editor.rulers": [120]
14+
"editor.rulers": [120],
15+
"editor.tabSize": 4
1516
},
1617
"php.suggest.basic": false,
1718
"phpSniffer.autoDetect": true

.phpcs.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,6 @@
88
<arg name="colors"/>
99

1010
<rule ref="PSR12" />
11+
12+
<rule ref="Generic.Arrays.ArrayIndent" />
1113
</ruleset>

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
},
99
"scripts": {
1010
"post-install-cmd": [
11-
"composer --working-dir=tests/integration/minimal install"
11+
"php tests/integration/install.php"
1212
],
1313
"lint": "phpcs",
1414
"lint:fix": "phpcbf"

src/BaseTest.php

Lines changed: 2 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -2,97 +2,12 @@
22

33
namespace Skills17\PHPUnit;
44

5-
use PDO;
65
use PHPUnit\Framework\TestCase;
76

87
abstract class BaseTest extends TestCase
98
{
10-
11-
public $db = null;
12-
13-
/**
14-
* Initialize database connection and setup http client
15-
*/
16-
public function __construct(...$args)
17-
{
18-
parent::__construct(...$args);
19-
$config = Config::get();
20-
21-
if (!isset($config['database']) || $config['database']) {
22-
$dsn = 'mysql:host=127.0.0.1;dbname=bikesharing;charset=utf8mb4';
23-
24-
try {
25-
$this->db = new PDO($dsn, 'root', '', [PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES \'UTF8\'']);
26-
$this->db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
27-
} catch (\PDOException $e) {
28-
echo 'Unable to connect to the database! ' . $e->getMessage() . "\n";
29-
echo 'DSN: ' . $dsn . ', User: root, Password: no' . "\n";
30-
exit(1);
31-
}
32-
}
33-
}
34-
35-
/**
36-
* Reset the database with the dump located in data/db-dump.sql
37-
*/
38-
protected function resetDb()
39-
{
40-
if ($this->db === null) {
41-
return;
42-
}
43-
44-
$dumpFile = __DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . '..' .
45-
DIRECTORY_SEPARATOR . 'db-dump.sql';
46-
47-
if (!file_exists($dumpFile)) {
48-
echo "Database dump (db-dump.sql) does not exist\n";
49-
exit(1);
50-
}
51-
52-
$lines = file($dumpFile);
53-
$statement = '';
54-
55-
$this->db->beginTransaction();
56-
57-
// parse the dump because PDO can only execute one statement at a time
58-
foreach ($lines as $line) {
59-
$trimmedLine = trim($line);
60-
if ($trimmedLine === '' || substr($trimmedLine, 0, 2) === '--') {
61-
continue;
62-
}
63-
64-
$statement .= $trimmedLine;
65-
if (substr($trimmedLine, -1) === ';') {
66-
try {
67-
$this->db->exec($statement);
68-
} catch (\PDOException $e) {
69-
$this->db->rollBack();
70-
echo 'Error during DB reset: ' . $e->getMessage() . "\n";
71-
echo 'Statement: ' . $statement . "\n";
72-
exit(1);
73-
}
74-
$statement = '';
75-
}
76-
}
77-
78-
$this->db->commit();
79-
}
80-
81-
/**
82-
* Resets the timezone in both PHP and MySQL to avoid issues when working with dates.
83-
*/
84-
private function resetTimezone()
85-
{
86-
date_default_timezone_set('Europe/Zurich');
87-
$this->db->exec('SET time_zone = "+02:00";');
88-
}
89-
90-
/**
91-
* For the tests, reset the database before every test
92-
*/
93-
public function setUp(): void
9+
public function writeLine(...$args)
9410
{
95-
$this->resetDb();
96-
$this->resetTimezone();
11+
echo implode(' ', $args) . "\n";
9712
}
9813
}

src/Config.php

Lines changed: 174 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,36 +2,200 @@
22

33
namespace Skills17\PHPUnit;
44

5+
use Error;
56
use ReflectionClass;
67

78
class Config
89
{
10+
private static $instance;
11+
private $config;
12+
private $defaultConfig = [
13+
'database' => [
14+
'enabled' => false,
15+
'dump' => './database.sql',
16+
'name' => 'skills17',
17+
'user' => 'root',
18+
'password' => '',
19+
'host' => '127.0.0.1',
20+
],
21+
'points' => [
22+
'defaultPoints' => 1.0,
23+
'strategy' => 'add',
24+
],
25+
'groups' => [],
26+
];
27+
928
/**
10-
* Get the test configuration.
11-
* Default values can be overwritten with environment variables.
29+
* Create a new config instance and merge it with the default config values.
1230
*/
13-
public static function get()
31+
private function __construct($config)
32+
{
33+
$this->config = $this->mergeConfig($this->defaultConfig, $config);
34+
$this->validate();
35+
}
36+
37+
/**
38+
* Get the project root folder
39+
*/
40+
public static function getProjectRoot(): string
1441
{
1542
// get project root
1643
$classLoader = new ReflectionClass(\Composer\Autoload\ClassLoader::class);
1744
$projectRoot = dirname($classLoader->getFileName(), 3);
1845

19-
$configFile = $projectRoot() . 'config.json';
46+
return $projectRoot . '/';
47+
}
48+
49+
/**
50+
* Get the test configuration instance.
51+
*/
52+
public static function getInstance(): Config
53+
{
54+
if (self::$instance) {
55+
return self::$instance;
56+
}
57+
58+
$configFile = self::getProjectRoot() . 'config.json';
2059

2160
if (!file_exists($configFile)) {
22-
echo "Config file (config.json) does not exist\n";
23-
exit(1);
61+
throw new Error('Config file (' . $configFile . ') does not exist');
2462
}
2563

2664
$jsonConfig = json_decode(file_get_contents($configFile), true);
2765

2866
if ($jsonConfig === null) {
29-
echo "Could not decode config file (config.json)\n";
30-
exit(1);
67+
throw new Error('Could not decode config file (config.json)');
3168
}
3269

33-
return array_merge($jsonConfig, [
34-
'format' => getenv('FORMAT') ?? 'normal',
70+
self::$instance = new self($jsonConfig);
71+
72+
return self::$instance;
73+
}
74+
75+
/**
76+
* Checks if a database should be used for the tests.
77+
*/
78+
public function hasDatabase(): bool
79+
{
80+
return $this->config['database']['enabled'];
81+
}
82+
83+
/**
84+
* Gets the database config.
85+
*/
86+
public function getDatabaseConfig(): array
87+
{
88+
$envConfig = array_filter([
89+
'name' => getenv('DB_NAME'),
90+
'user' => getenv('DB_USER'),
91+
'password' => getenv('DB_PASSWORD'),
92+
'host' => getenv('DB_HOST'),
3593
]);
94+
95+
return array_merge($this->config['database'], $envConfig);
96+
}
97+
98+
/**
99+
* Get the default points.
100+
*/
101+
public function getDefaultPoints(): float
102+
{
103+
return $this->config['points']['defaultPoints'];
104+
}
105+
106+
/**
107+
* Get the points strategy.
108+
* Can be either 'add' or 'deduct'.
109+
*/
110+
public function getPointsStrategy(): string
111+
{
112+
return $this->config['points']['strategy'];
113+
}
114+
115+
/**
116+
* Gets all test groups
117+
*/
118+
public function getGroups(): array
119+
{
120+
return $this->config['groups'] ?? [];
121+
}
122+
123+
/**
124+
* Get the output format.
125+
* Can be either 'json' or 'text'.
126+
*/
127+
public function getFormat(): string
128+
{
129+
return getenv('FORMAT') ?: 'text';
130+
}
131+
132+
/**
133+
* Merge two configuration arrays.
134+
*/
135+
private function mergeConfig(array $config1, array $config2): array
136+
{
137+
$merged = $config1;
138+
139+
foreach ($config2 as $key => & $value) {
140+
if (is_array($value) && isset($merged[$key]) && is_array($merged[$key])) {
141+
$merged[$key] = $this->mergeConfig($merged[$key], $value);
142+
} elseif (is_numeric($key)) {
143+
if (!in_array($value, $merged)) {
144+
$merged[] = $value;
145+
}
146+
} else {
147+
$merged[$key] = $value;
148+
}
149+
}
150+
151+
return $merged;
152+
}
153+
154+
/**
155+
* Validates the current configuration.
156+
*/
157+
private function validate()
158+
{
159+
$format = $this->getFormat();
160+
$strategy = $this->getPointsStrategy();
161+
162+
// validate if a valid format is specified
163+
if ($format !== 'text' && $format !== 'json') {
164+
throw new Error('config.json validation error: Invalid output format: ' . $format);
165+
}
166+
167+
// validate points strategy
168+
if ($strategy !== 'add' && $strategy !== 'deduct') {
169+
throw new Error('config.json validation error: Invalid points strategy: ' . $format);
170+
}
171+
172+
// validate test groups
173+
foreach ($this->getGroups() as $groupIndex => $group) {
174+
if (!isset($group['match'])) {
175+
throw new Error('config.json validation error: Group #' . $groupIndex .
176+
' does not contain a "match" property');
177+
}
178+
179+
if (isset($group['strategy']) && $group['strategy'] !== 'add' && $group['strategy'] !== 'deduct') {
180+
throw new Error('config.json validation error: Invalid points strategy: ' . $format);
181+
}
182+
183+
foreach (($group['tests'] ?? []) as $testIndex => $test) {
184+
if (!isset($test['match'])) {
185+
throw new Error('config.json validation error: Test #' . $testIndex .
186+
' in group #' . $groupIndex . ' (' . $group['match'] . ') does not contain a "match" property');
187+
}
188+
}
189+
190+
if (
191+
isset($group['maxPoints']) && (
192+
(isset($group['strategy']) && $group['strategy'] !== 'deduct') ||
193+
(!isset($group['strategy']) && $this->getPointsStrategy() !== 'deduct')
194+
)
195+
) {
196+
throw new Error('config.json validation error: Property "maxPoints" can only be set for strategy ' .
197+
'"deduct". Found in group #' . $groupIndex . ' (' . $group['match'] . ')');
198+
}
199+
}
36200
}
37201
}

0 commit comments

Comments
 (0)