Skip to content

Commit 3b0bd2b

Browse files
committed
Merge remote-tracking branch 'upstream/develop' into 4.8
2 parents d636fe5 + c67b0a7 commit 3b0bd2b

File tree

13 files changed

+297
-53
lines changed

13 files changed

+297
-53
lines changed

.github/scripts/run-random-tests.sh

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -486,6 +486,7 @@ run_component_tests() {
486486
"$test_dir"
487487
"--colors=never"
488488
"--no-coverage"
489+
"--do-not-cache-result"
489490
"--order-by=random"
490491
"--random-order-seed=${random_seed}"
491492
"--log-events-text"
@@ -611,7 +612,7 @@ run_component_tests() {
611612
fi
612613

613614
{
614-
echo "> ${phpunit_args[@]:0:6}"
615+
echo "> ${phpunit_args[@]:0:7}"
615616
echo ""
616617
echo "$output"
617618
echo "$predecessor_info"

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
"phpunit/phpcov": "^9.0.2 || ^10.0",
2929
"phpunit/phpunit": "^10.5.16 || ^11.2",
3030
"predis/predis": "^3.0",
31-
"rector/rector": "2.3.9",
31+
"rector/rector": "2.4.1",
3232
"shipmonk/phpstan-baseline-per-identifier": "^2.0"
3333
},
3434
"replace": {

system/Autoloader/Autoloader.php

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
namespace CodeIgniter\Autoloader;
1515

16+
use Closure;
1617
use CodeIgniter\Exceptions\ConfigException;
1718
use CodeIgniter\Exceptions\InvalidArgumentException;
1819
use CodeIgniter\Exceptions\RuntimeException;
@@ -93,6 +94,14 @@ class Autoloader
9394
*/
9495
protected $helpers = ['url'];
9596

97+
/**
98+
* Stores the closures registered with spl_autoload_register()
99+
* so that unregister() can remove the exact same instances.
100+
*
101+
* @var list<Closure(string): void>
102+
*/
103+
private array $registeredClosures = [];
104+
96105
public function __construct(private readonly string $composerPath = COMPOSER_PATH)
97106
{
98107
}
@@ -170,8 +179,17 @@ private function loadComposerAutoloader(Modules $modules): void
170179
*/
171180
public function register()
172181
{
173-
spl_autoload_register($this->loadClassmap(...), true);
174-
spl_autoload_register($this->loadClass(...), true);
182+
// Store the exact Closure instances so unregister() can remove them.
183+
// First-class callable syntax (e.g. $this->loadClass(...)) creates a
184+
// new Closure object on every call, so we must reuse the same instances.
185+
$loadClassmap = $this->loadClassmap(...);
186+
$loadClass = $this->loadClass(...);
187+
188+
$this->registeredClosures[] = $loadClassmap;
189+
$this->registeredClosures[] = $loadClass;
190+
191+
spl_autoload_register($loadClassmap, true);
192+
spl_autoload_register($loadClass, true);
175193

176194
foreach ($this->files as $file) {
177195
$this->includeFile($file);
@@ -183,8 +201,11 @@ public function register()
183201
*/
184202
public function unregister(): void
185203
{
186-
spl_autoload_unregister($this->loadClass(...));
187-
spl_autoload_unregister($this->loadClassmap(...));
204+
foreach ($this->registeredClosures as $closure) {
205+
spl_autoload_unregister($closure);
206+
}
207+
208+
$this->registeredClosures = [];
188209
}
189210

190211
/**

system/Commands/Cache/ClearCache.php

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313

1414
namespace CodeIgniter\Commands\Cache;
1515

16-
use CodeIgniter\Cache\CacheFactory;
1716
use CodeIgniter\CLI\BaseCommand;
1817
use CodeIgniter\CLI\CLI;
1918
use Config\Cache;
@@ -69,22 +68,21 @@ public function run(array $params)
6968
$handler = $params[0] ?? $config->handler;
7069

7170
if (! array_key_exists($handler, $config->validHandlers)) {
72-
CLI::error($handler . ' is not a valid cache handler.');
71+
CLI::error(lang('Cache.invalidHandler', [$handler]));
7372

74-
return;
73+
return EXIT_ERROR;
7574
}
7675

7776
$config->handler = $handler;
78-
$cache = CacheFactory::getHandler($config);
7977

80-
if (! $cache->clean()) {
81-
// @codeCoverageIgnoreStart
78+
if (! service('cache', $config)->clean()) {
8279
CLI::error('Error while clearing the cache.');
8380

84-
return;
85-
// @codeCoverageIgnoreEnd
81+
return EXIT_ERROR;
8682
}
8783

8884
CLI::write(CLI::color('Cache cleared.', 'green'));
85+
86+
return EXIT_SUCCESS;
8987
}
9088
}

system/Commands/Housekeeping/ClearDebugbar.php

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,15 +58,13 @@ public function run(array $params)
5858
helper('filesystem');
5959

6060
if (! delete_files(WRITEPATH . 'debugbar', false, true)) {
61-
// @codeCoverageIgnoreStart
6261
CLI::error('Error deleting the debugbar JSON files.');
63-
CLI::newLine();
6462

65-
return;
66-
// @codeCoverageIgnoreEnd
63+
return EXIT_ERROR;
6764
}
6865

6966
CLI::write('Debugbar cleared.', 'green');
70-
CLI::newLine();
67+
68+
return EXIT_SUCCESS;
7169
}
7270
}

system/Commands/Housekeeping/ClearLogs.php

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -67,27 +67,22 @@ public function run(array $params)
6767
$force = array_key_exists('force', $params) || CLI::getOption('force');
6868

6969
if (! $force && CLI::prompt('Are you sure you want to delete the logs?', ['n', 'y']) === 'n') {
70-
// @codeCoverageIgnoreStart
71-
CLI::error('Deleting logs aborted.', 'light_gray', 'red');
72-
CLI::error('If you want, use the "-force" option to force delete all log files.', 'light_gray', 'red');
73-
CLI::newLine();
70+
CLI::error('Deleting logs aborted.');
71+
CLI::error('If you want, use the "--force" option to force delete all log files.');
7472

75-
return;
76-
// @codeCoverageIgnoreEnd
73+
return EXIT_ERROR;
7774
}
7875

7976
helper('filesystem');
8077

8178
if (! delete_files(WRITEPATH . 'logs', false, true)) {
82-
// @codeCoverageIgnoreStart
83-
CLI::error('Error in deleting the logs files.', 'light_gray', 'red');
84-
CLI::newLine();
79+
CLI::error('Error in deleting the logs files.');
8580

86-
return;
87-
// @codeCoverageIgnoreEnd
81+
return EXIT_ERROR;
8882
}
8983

9084
CLI::write('Logs cleared.', 'green');
91-
CLI::newLine();
85+
86+
return EXIT_SUCCESS;
9287
}
9388
}

system/Language/en/Cache.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
// Cache language settings
1515
return [
1616
'unableToWrite' => 'Cache unable to write to "{0}".',
17+
'invalidHandler' => 'Cache driver "{0}" is not a valid cache handler.',
1718
'invalidHandlers' => 'Cache config must have an array of $validHandlers.',
1819
'noBackup' => 'Cache config must have a handler and backupHandler set.',
1920
'handlerNotFound' => 'Cache config has an invalid handler or backup handler specified.',

tests/system/Autoloader/AutoloaderTest.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,45 @@ public function testServiceAutoLoaderFromShareInstances(): void
121121
$this->assertSame($expected, $actual);
122122
}
123123

124+
public function testUnregisterRemovesClosuresFromSplStack(): void
125+
{
126+
$countBefore = count(spl_autoload_functions());
127+
128+
$config = new Autoload();
129+
$modules = new Modules();
130+
$modules->discoverInComposer = false;
131+
$config->psr4 = ['CodeIgniter' => SYSTEMPATH];
132+
133+
$loader = new Autoloader();
134+
$loader->initialize($config, $modules)->register();
135+
136+
$this->assertCount($countBefore + 2, spl_autoload_functions());
137+
138+
$loader->unregister();
139+
140+
$this->assertCount($countBefore, spl_autoload_functions());
141+
}
142+
143+
public function testUnregisterRemovesAllClosuresAfterMultipleRegistrations(): void
144+
{
145+
$countBefore = count(spl_autoload_functions());
146+
147+
$config = new Autoload();
148+
$modules = new Modules();
149+
$modules->discoverInComposer = false;
150+
$config->psr4 = ['CodeIgniter' => SYSTEMPATH];
151+
152+
$loader = new Autoloader();
153+
$loader->initialize($config, $modules)->register();
154+
$loader->register();
155+
156+
$this->assertCount($countBefore + 4, spl_autoload_functions());
157+
158+
$loader->unregister();
159+
160+
$this->assertCount($countBefore, spl_autoload_functions());
161+
}
162+
124163
public function testServiceAutoLoader(): void
125164
{
126165
$autoloader = service('autoloader', false);

tests/system/Commands/ClearCacheTest.php

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
namespace CodeIgniter\Commands;
1515

1616
use CodeIgniter\Cache\CacheFactory;
17+
use CodeIgniter\Cache\Handlers\FileHandler;
1718
use CodeIgniter\Test\CIUnitTestCase;
1819
use CodeIgniter\Test\StreamFilterTrait;
1920
use Config\Services;
@@ -31,15 +32,27 @@ protected function setUp(): void
3132
{
3233
parent::setUp();
3334

35+
$this->resetServices();
36+
3437
// Make sure we are testing with the correct handler (override injections)
3538
Services::injectMock('cache', CacheFactory::getHandler(config('Cache')));
3639
}
3740

41+
protected function tearDown(): void
42+
{
43+
parent::tearDown();
44+
45+
$this->resetServices();
46+
}
47+
3848
public function testClearCacheInvalidHandler(): void
3949
{
4050
command('cache:clear junk');
4151

42-
$this->assertStringContainsString('junk is not a valid cache handler.', $this->getStreamFilterBuffer());
52+
$this->assertSame(
53+
"Cache driver \"junk\" is not a valid cache handler.\n",
54+
preg_replace('/\e\[[^m]+m/', '', $this->getStreamFilterBuffer()),
55+
);
4356
}
4457

4558
public function testClearCacheWorks(): void
@@ -52,4 +65,22 @@ public function testClearCacheWorks(): void
5265
$this->assertNull(cache('foo'));
5366
$this->assertStringContainsString('Cache cleared.', $this->getStreamFilterBuffer());
5467
}
68+
69+
public function testClearCacheFails(): void
70+
{
71+
$cache = $this->getMockBuilder(FileHandler::class)
72+
->setConstructorArgs([config('Cache')])
73+
->onlyMethods(['clean'])
74+
->getMock();
75+
$cache->expects($this->once())->method('clean')->willReturn(false);
76+
77+
Services::injectMock('cache', $cache);
78+
79+
command('cache:clear');
80+
81+
$this->assertSame(
82+
"Error while clearing the cache.\n",
83+
preg_replace('/\e\[[^m]+m/', '', $this->getStreamFilterBuffer()),
84+
);
85+
}
5586
}

tests/system/Commands/ClearDebugbarTest.php

Lines changed: 59 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use CodeIgniter\Test\CIUnitTestCase;
1717
use CodeIgniter\Test\StreamFilterTrait;
1818
use PHPUnit\Framework\Attributes\Group;
19+
use PHPUnit\Framework\Attributes\RequiresOperatingSystem;
1920

2021
/**
2122
* @internal
@@ -31,10 +32,22 @@ protected function setUp(): void
3132
{
3233
parent::setUp();
3334

35+
command('debugbar:clear');
36+
$this->resetStreamFilterBuffer();
37+
3438
$this->time = time();
39+
$this->createDummyDebugbarJson();
40+
}
41+
42+
protected function tearDown(): void
43+
{
44+
command('debugbar:clear');
45+
$this->resetStreamFilterBuffer();
46+
47+
parent::tearDown();
3548
}
3649

37-
protected function createDummyDebugbarJson(): void
50+
private function createDummyDebugbarJson(): void
3851
{
3952
$time = $this->time;
4053
$path = WRITEPATH . 'debugbar' . DIRECTORY_SEPARATOR . "debugbar_{$time}.json";
@@ -50,18 +63,56 @@ protected function createDummyDebugbarJson(): void
5063

5164
public function testClearDebugbarWorks(): void
5265
{
53-
// test clean debugbar dir
54-
$this->assertFileDoesNotExist(WRITEPATH . 'debugbar' . DIRECTORY_SEPARATOR . "debugbar_{$this->time}.json");
55-
56-
// test dir is now populated with json
57-
$this->createDummyDebugbarJson();
5866
$this->assertFileExists(WRITEPATH . 'debugbar' . DIRECTORY_SEPARATOR . "debugbar_{$this->time}.json");
5967

6068
command('debugbar:clear');
61-
$result = $this->getStreamFilterBuffer();
6269

6370
$this->assertFileDoesNotExist(WRITEPATH . 'debugbar' . DIRECTORY_SEPARATOR . "debugbar_{$this->time}.json");
6471
$this->assertFileExists(WRITEPATH . 'debugbar' . DIRECTORY_SEPARATOR . 'index.html');
65-
$this->assertStringContainsString('Debugbar cleared.', $result);
72+
$this->assertSame(
73+
"Debugbar cleared.\n",
74+
preg_replace('/\e\[[^m]+m/', '', $this->getStreamFilterBuffer()),
75+
);
76+
}
77+
78+
#[RequiresOperatingSystem('Darwin|Linux')]
79+
public function testClearDebugbarWithError(): void
80+
{
81+
$path = WRITEPATH . 'debugbar' . DIRECTORY_SEPARATOR . "debugbar_{$this->time}.json";
82+
83+
// Attempt to make the file itself undeletable by setting the
84+
// immutable/uchg flag on supported platforms.
85+
$immutableSet = false;
86+
if (str_starts_with(PHP_OS, 'Darwin')) {
87+
@exec(sprintf('chflags uchg %s', escapeshellarg($path)), $output, $rc);
88+
$immutableSet = $rc === 0;
89+
} else {
90+
// Try chattr on Linux with sudo (for containerized environments)
91+
@exec('which chattr', $whichOut, $whichRc);
92+
93+
if ($whichRc === 0) {
94+
@exec(sprintf('sudo chattr +i %s', escapeshellarg($path)), $output, $rc);
95+
$immutableSet = $rc === 0;
96+
}
97+
}
98+
99+
if (! $immutableSet) {
100+
$this->markTestSkipped('Cannot set file immutability in this environment');
101+
}
102+
103+
command('debugbar:clear');
104+
105+
// Restore attributes so other tests are not affected.
106+
if (str_starts_with(PHP_OS, 'Darwin')) {
107+
@exec(sprintf('chflags nouchg %s', escapeshellarg($path)));
108+
} else {
109+
@exec(sprintf('sudo chattr -i %s', escapeshellarg($path)));
110+
}
111+
112+
$this->assertFileExists($path);
113+
$this->assertSame(
114+
"Error deleting the debugbar JSON files.\n",
115+
preg_replace('/\e\[[^m]+m/', '', $this->getStreamFilterBuffer()),
116+
);
66117
}
67118
}

0 commit comments

Comments
 (0)