Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
17 changes: 15 additions & 2 deletions system/Test/CIUnitTestCase.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -349,8 +349,6 @@ protected function mockSession()
* @param string|null $expectedMessage
*
* @return bool
*
* @throws Exception
*/
public function assertLogged(string $level, $expectedMessage = null)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not related to this PR, but why $expectedMessage can be null?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure either.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤦‍♂️

{
Expand All@@ -365,6 +363,21 @@ public function assertLogged(string $level, $expectedMessage = null)
return $result;
}

/**
* Asserts that there is a log record that contains `$logMessage` in the message.
*/
public function assertLogContains(string $level, string $logMessage, string $message = ''): void

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$expectedMessage is better? assertLogged() uses it.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My understanding is $expectedMessage is used since the assertion compares the message as expected in the logs.
For assertLogContains, I think it is counter-intuitive to use $expectedMessage when it can only be a part of the whole expected message. I was also thinking whether to use $needle or $needleMessage as alternative name.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No preference from me

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am a little uncomfortable with both $needle and $needleMessage.
needle needs haystack.

So let's leave it as it is.

{
$this->assertTrue(
TestLogger::didLog($level, $logMessage, false),
$message ?: sprintf(
'Failed asserting that logs have a record of message containing "%s" with level "%s".',
$logMessage,
$level
)
);
}

/**
* Hooks into CodeIgniter's Events system to check if a specific
* event was triggered or not.
Expand Down
18 changes: 16 additions & 2 deletions system/Test/TestLogger.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -59,10 +59,24 @@ public function log($level, $message, array $context = []): bool
*
* @return bool
*/
public static function didLog(string $level, $message)
public static function didLog(string $level, $message, bool $useExactComparison = true)
{
$lowerLevel = strtolower($level);

foreach (self::$op_logs as $log) {
if (strtolower($log['level']) === strtolower($level) && $message === $log['message']) {
if (strtolower($log['level']) !== $lowerLevel) {
continue;
}

if ($useExactComparison) {
if ($log['message'] === $message) {
return true;
}

continue;
}

if (strpos($log['message'], $message) !== false) {
return true;
}
}
Expand Down
6 changes: 6 additions & 0 deletions tests/system/Test/TestCaseTest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -45,6 +45,12 @@ public function testLogging()
$this->assertLogged('error', 'Some variable did not contain a value.');
}

public function testAssertLogContains()
{
log_message('error', 'Some variable did not contain a value.');
$this->assertLogContains('error', 'variable did not');
}

public function testEventTriggering()
{
Events::on('foo', static function ($arg) use (&$result) {
Expand Down
71 changes: 71 additions & 0 deletions tests/system/Test/TestLoggerTest.php
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
<?php

/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/

namespace CodeIgniter\Test;

use Config\Logger;

/**
* @internal
*/
final class TestLoggerTest extends CIUnitTestCase
{
/**
* @dataProvider provideDidLogCases
*/
public function testDidLogMethod(bool $expected, string $level, string $message, bool $exact): void
{
(new TestLogger(new Logger()))->log('error', 'Some variable did not contain a value.');

$this->assertSame(
$expected,
TestLogger::didLog($level, $message, $exact),
);
}

public function provideDidLogCases(): iterable
{
yield 'exact' => [
true,
'error',
'Some variable did not contain a value.',
true,
];

yield 'wrong level' => [
false,
'warning',
'Some variable did not contain a value.',
true,
];

yield 'wrong message' => [
false,
'error',
'Some variables did not contain a value.',
true,
];

yield 'approximate' => [
true,
'error',
'Some variable did not',
false,
];

yield 'approximate but wrong level' => [
false,
'warning',
'Some variable did not',
false,
];
}
}
2 changes: 2 additions & 0 deletions user_guide_src/source/changelogs/v4.3.0.rst
Original file line numberDiff line numberDiff line change
Expand Up@@ -128,6 +128,8 @@ Testing
- The CITestStreamFilter filter class now implements methods for adding a filter to streams. See :ref:`testing-cli-output`.
- Added the ``PhpStreamWrapper`` to make it easier to work with setting data to ``php://stdin``. See :ref:`testing-cli-input`.
- Added method :ref:`benchmark-timer-record` to measure performance in a callable. Also enhanced common function ``timer()`` to accept optional callable.
- A boolean third parameter ``$useExactComparison`` is added to ``TestLogger::didLog()`` which sets whether log messages are checked verbatim. This defaults to ``true``.
- Added method ``CIUnitTestCase::assertLogContains()`` which compares log messages by parts instead of the whole of the message.

Database
========
Expand Down
7 changes: 6 additions & 1 deletion user_guide_src/source/testing/overview.rst
Original file line numberDiff line numberDiff line change
Expand Up@@ -126,7 +126,12 @@ Additional Assertions
assertLogged($level, $expectedMessage)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Ensure that something you expected to be logged actually was:
Ensure that something you expected to be logged was actually logged:

assertLogContains($level, $logMessage)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Ensure that there's a record in the logs which contains a message part.

.. literalinclude:: overview/007.php

Expand Down
12 changes: 8 additions & 4 deletions user_guide_src/source/testing/overview/007.php
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
<?php

$config = new LoggerConfig();
$logger = new Logger($config);
$config = new Config\Logger();
$logger = new CodeIgniter\Log\Logger($config);

// ... do something that you expect a log entry from
// check verbatim the log message
$logger->log('error', "That's no moon");

$this->assertLogged('error', "That's no moon");

// check that a portion of the message is found in the logs
$exception = new RuntimeException('Hello world.');
$logger->log('error', $exception->getTraceAsString());
$this->assertLogContains('error', '{main}');
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
17 changes: 15 additions & 2 deletions system/Test/CIUnitTestCase.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -349,8 +349,6 @@ protected function mockSession()
* @param string|null $expectedMessage
*
* @return bool
*
* @throws Exception
*/
public function assertLogged(string $level, $expectedMessage = null)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not related to this PR, but why $expectedMessage can be null?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure either.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤦‍♂️

{
Expand All@@ -365,6 +363,21 @@ public function assertLogged(string $level, $expectedMessage = null)
return $result;
}

/**
* Asserts that there is a log record that contains `$logMessage` in the message.
*/
public function assertLogContains(string $level, string $logMessage, string $message = ''): void

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$expectedMessage is better? assertLogged() uses it.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My understanding is $expectedMessage is used since the assertion compares the message as expected in the logs.
For assertLogContains, I think it is counter-intuitive to use $expectedMessage when it can only be a part of the whole expected message. I was also thinking whether to use $needle or $needleMessage as alternative name.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No preference from me

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am a little uncomfortable with both $needle and $needleMessage.
needle needs haystack.

So let's leave it as it is.

{
$this->assertTrue(
TestLogger::didLog($level, $logMessage, false),
$message ?: sprintf(
'Failed asserting that logs have a record of message containing "%s" with level "%s".',
$logMessage,
$level
)
);
}

/**
* Hooks into CodeIgniter's Events system to check if a specific
* event was triggered or not.
Expand Down
18 changes: 16 additions & 2 deletions system/Test/TestLogger.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -59,10 +59,24 @@ public function log($level, $message, array $context = []): bool
*
* @return bool
*/
public static function didLog(string $level, $message)
public static function didLog(string $level, $message, bool $useExactComparison = true)
{
$lowerLevel = strtolower($level);

foreach (self::$op_logs as $log) {
if (strtolower($log['level']) === strtolower($level) && $message === $log['message']) {
if (strtolower($log['level']) !== $lowerLevel) {
continue;
}

if ($useExactComparison) {
if ($log['message'] === $message) {
return true;
}

continue;
}

if (strpos($log['message'], $message) !== false) {
return true;
}
}
Expand Down
6 changes: 6 additions & 0 deletions tests/system/Test/TestCaseTest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -45,6 +45,12 @@ public function testLogging()
$this->assertLogged('error', 'Some variable did not contain a value.');
}

public function testAssertLogContains()
{
log_message('error', 'Some variable did not contain a value.');
$this->assertLogContains('error', 'variable did not');
}

public function testEventTriggering()
{
Events::on('foo', static function ($arg) use (&$result) {
Expand Down
71 changes: 71 additions & 0 deletions tests/system/Test/TestLoggerTest.php
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
<?php

/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/

namespace CodeIgniter\Test;

use Config\Logger;

/**
* @internal
*/
final class TestLoggerTest extends CIUnitTestCase
{
/**
* @dataProvider provideDidLogCases
*/
public function testDidLogMethod(bool $expected, string $level, string $message, bool $exact): void
{
(new TestLogger(new Logger()))->log('error', 'Some variable did not contain a value.');

$this->assertSame(
$expected,
TestLogger::didLog($level, $message, $exact),
);
}

public function provideDidLogCases(): iterable
{
yield 'exact' => [
true,
'error',
'Some variable did not contain a value.',
true,
];

yield 'wrong level' => [
false,
'warning',
'Some variable did not contain a value.',
true,
];

yield 'wrong message' => [
false,
'error',
'Some variables did not contain a value.',
true,
];

yield 'approximate' => [
true,
'error',
'Some variable did not',
false,
];

yield 'approximate but wrong level' => [
false,
'warning',
'Some variable did not',
false,
];
}
}
2 changes: 2 additions & 0 deletions user_guide_src/source/changelogs/v4.3.0.rst
Original file line numberDiff line numberDiff line change
Expand Up@@ -128,6 +128,8 @@ Testing
- The CITestStreamFilter filter class now implements methods for adding a filter to streams. See :ref:`testing-cli-output`.
- Added the ``PhpStreamWrapper`` to make it easier to work with setting data to ``php://stdin``. See :ref:`testing-cli-input`.
- Added method :ref:`benchmark-timer-record` to measure performance in a callable. Also enhanced common function ``timer()`` to accept optional callable.
- A boolean third parameter ``$useExactComparison`` is added to ``TestLogger::didLog()`` which sets whether log messages are checked verbatim. This defaults to ``true``.
- Added method ``CIUnitTestCase::assertLogContains()`` which compares log messages by parts instead of the whole of the message.

Database
========
Expand Down
7 changes: 6 additions & 1 deletion user_guide_src/source/testing/overview.rst
Original file line numberDiff line numberDiff line change
Expand Up@@ -126,7 +126,12 @@ Additional Assertions
assertLogged($level, $expectedMessage)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Ensure that something you expected to be logged actually was:
Ensure that something you expected to be logged was actually logged:

assertLogContains($level, $logMessage)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Ensure that there's a record in the logs which contains a message part.

.. literalinclude:: overview/007.php

Expand Down
12 changes: 8 additions & 4 deletions user_guide_src/source/testing/overview/007.php
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
<?php

$config = new LoggerConfig();
$logger = new Logger($config);
$config = new Config\Logger();
$logger = new CodeIgniter\Log\Logger($config);

// ... do something that you expect a log entry from
// check verbatim the log message
$logger->log('error', "That's no moon");

$this->assertLogged('error', "That's no moon");

// check that a portion of the message is found in the logs
$exception = new RuntimeException('Hello world.');
$logger->log('error', $exception->getTraceAsString());
$this->assertLogContains('error', '{main}');
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
17 changes: 15 additions & 2 deletions system/Test/CIUnitTestCase.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -349,8 +349,6 @@ protected function mockSession()
* @param string|null $expectedMessage
*
* @return bool
*
* @throws Exception
*/
public function assertLogged(string $level, $expectedMessage = null)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not related to this PR, but why $expectedMessage can be null?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure either.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤦‍♂️

{
Expand All@@ -365,6 +363,21 @@ public function assertLogged(string $level, $expectedMessage = null)
return $result;
}

/**
* Asserts that there is a log record that contains `$logMessage` in the message.
*/
public function assertLogContains(string $level, string $logMessage, string $message = ''): void

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$expectedMessage is better? assertLogged() uses it.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My understanding is $expectedMessage is used since the assertion compares the message as expected in the logs.
For assertLogContains, I think it is counter-intuitive to use $expectedMessage when it can only be a part of the whole expected message. I was also thinking whether to use $needle or $needleMessage as alternative name.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No preference from me

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am a little uncomfortable with both $needle and $needleMessage.
needle needs haystack.

So let's leave it as it is.

{
$this->assertTrue(
TestLogger::didLog($level, $logMessage, false),
$message ?: sprintf(
'Failed asserting that logs have a record of message containing "%s" with level "%s".',
$logMessage,
$level
)
);
}

/**
* Hooks into CodeIgniter's Events system to check if a specific
* event was triggered or not.
Expand Down
18 changes: 16 additions & 2 deletions system/Test/TestLogger.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -59,10 +59,24 @@ public function log($level, $message, array $context = []): bool
*
* @return bool
*/
public static function didLog(string $level, $message)
public static function didLog(string $level, $message, bool $useExactComparison = true)
{
$lowerLevel = strtolower($level);

foreach (self::$op_logs as $log) {
if (strtolower($log['level']) === strtolower($level) && $message === $log['message']) {
if (strtolower($log['level']) !== $lowerLevel) {
continue;
}

if ($useExactComparison) {
if ($log['message'] === $message) {
return true;
}

continue;
}

if (strpos($log['message'], $message) !== false) {
return true;
}
}
Expand Down
6 changes: 6 additions & 0 deletions tests/system/Test/TestCaseTest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -45,6 +45,12 @@ public function testLogging()
$this->assertLogged('error', 'Some variable did not contain a value.');
}

public function testAssertLogContains()
{
log_message('error', 'Some variable did not contain a value.');
$this->assertLogContains('error', 'variable did not');
}

public function testEventTriggering()
{
Events::on('foo', static function ($arg) use (&$result) {
Expand Down
71 changes: 71 additions & 0 deletions tests/system/Test/TestLoggerTest.php
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
<?php

/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/

namespace CodeIgniter\Test;

use Config\Logger;

/**
* @internal
*/
final class TestLoggerTest extends CIUnitTestCase
{
/**
* @dataProvider provideDidLogCases
*/
public function testDidLogMethod(bool $expected, string $level, string $message, bool $exact): void
{
(new TestLogger(new Logger()))->log('error', 'Some variable did not contain a value.');

$this->assertSame(
$expected,
TestLogger::didLog($level, $message, $exact),
);
}

public function provideDidLogCases(): iterable
{
yield 'exact' => [
true,
'error',
'Some variable did not contain a value.',
true,
];

yield 'wrong level' => [
false,
'warning',
'Some variable did not contain a value.',
true,
];

yield 'wrong message' => [
false,
'error',
'Some variables did not contain a value.',
true,
];

yield 'approximate' => [
true,
'error',
'Some variable did not',
false,
];

yield 'approximate but wrong level' => [
false,
'warning',
'Some variable did not',
false,
];
}
}
2 changes: 2 additions & 0 deletions user_guide_src/source/changelogs/v4.3.0.rst
Original file line numberDiff line numberDiff line change
Expand Up@@ -128,6 +128,8 @@ Testing
- The CITestStreamFilter filter class now implements methods for adding a filter to streams. See :ref:`testing-cli-output`.
- Added the ``PhpStreamWrapper`` to make it easier to work with setting data to ``php://stdin``. See :ref:`testing-cli-input`.
- Added method :ref:`benchmark-timer-record` to measure performance in a callable. Also enhanced common function ``timer()`` to accept optional callable.
- A boolean third parameter ``$useExactComparison`` is added to ``TestLogger::didLog()`` which sets whether log messages are checked verbatim. This defaults to ``true``.
- Added method ``CIUnitTestCase::assertLogContains()`` which compares log messages by parts instead of the whole of the message.

Database
========
Expand Down
7 changes: 6 additions & 1 deletion user_guide_src/source/testing/overview.rst
Original file line numberDiff line numberDiff line change
Expand Up@@ -126,7 +126,12 @@ Additional Assertions
assertLogged($level, $expectedMessage)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Ensure that something you expected to be logged actually was:
Ensure that something you expected to be logged was actually logged:

assertLogContains($level, $logMessage)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Ensure that there's a record in the logs which contains a message part.

.. literalinclude:: overview/007.php

Expand Down
12 changes: 8 additions & 4 deletions user_guide_src/source/testing/overview/007.php
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
<?php

$config = new LoggerConfig();
$logger = new Logger($config);
$config = new Config\Logger();
$logger = new CodeIgniter\Log\Logger($config);

// ... do something that you expect a log entry from
// check verbatim the log message
$logger->log('error', "That's no moon");

$this->assertLogged('error', "That's no moon");

// check that a portion of the message is found in the logs
$exception = new RuntimeException('Hello world.');
$logger->log('error', $exception->getTraceAsString());
$this->assertLogContains('error', '{main}');
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
17 changes: 15 additions & 2 deletions system/Test/CIUnitTestCase.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -349,8 +349,6 @@ protected function mockSession()
* @param string|null $expectedMessage
*
* @return bool
*
* @throws Exception
*/
public function assertLogged(string $level, $expectedMessage = null)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not related to this PR, but why $expectedMessage can be null?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure either.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤦‍♂️

{
Expand All@@ -365,6 +363,21 @@ public function assertLogged(string $level, $expectedMessage = null)
return $result;
}

/**
* Asserts that there is a log record that contains `$logMessage` in the message.
*/
public function assertLogContains(string $level, string $logMessage, string $message = ''): void

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$expectedMessage is better? assertLogged() uses it.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My understanding is $expectedMessage is used since the assertion compares the message as expected in the logs.
For assertLogContains, I think it is counter-intuitive to use $expectedMessage when it can only be a part of the whole expected message. I was also thinking whether to use $needle or $needleMessage as alternative name.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No preference from me

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am a little uncomfortable with both $needle and $needleMessage.
needle needs haystack.

So let's leave it as it is.

{
$this->assertTrue(
TestLogger::didLog($level, $logMessage, false),
$message ?: sprintf(
'Failed asserting that logs have a record of message containing "%s" with level "%s".',
$logMessage,
$level
)
);
}

/**
* Hooks into CodeIgniter's Events system to check if a specific
* event was triggered or not.
Expand Down
18 changes: 16 additions & 2 deletions system/Test/TestLogger.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -59,10 +59,24 @@ public function log($level, $message, array $context = []): bool
*
* @return bool
*/
public static function didLog(string $level, $message)
public static function didLog(string $level, $message, bool $useExactComparison = true)
{
$lowerLevel = strtolower($level);

foreach (self::$op_logs as $log) {
if (strtolower($log['level']) === strtolower($level) && $message === $log['message']) {
if (strtolower($log['level']) !== $lowerLevel) {
continue;
}

if ($useExactComparison) {
if ($log['message'] === $message) {
return true;
}

continue;
}

if (strpos($log['message'], $message) !== false) {
return true;
}
}
Expand Down
6 changes: 6 additions & 0 deletions tests/system/Test/TestCaseTest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -45,6 +45,12 @@ public function testLogging()
$this->assertLogged('error', 'Some variable did not contain a value.');
}

public function testAssertLogContains()
{
log_message('error', 'Some variable did not contain a value.');
$this->assertLogContains('error', 'variable did not');
}

public function testEventTriggering()
{
Events::on('foo', static function ($arg) use (&$result) {
Expand Down
71 changes: 71 additions & 0 deletions tests/system/Test/TestLoggerTest.php
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
<?php

/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/

namespace CodeIgniter\Test;

use Config\Logger;

/**
* @internal
*/
final class TestLoggerTest extends CIUnitTestCase
{
/**
* @dataProvider provideDidLogCases
*/
public function testDidLogMethod(bool $expected, string $level, string $message, bool $exact): void
{
(new TestLogger(new Logger()))->log('error', 'Some variable did not contain a value.');

$this->assertSame(
$expected,
TestLogger::didLog($level, $message, $exact),
);
}

public function provideDidLogCases(): iterable
{
yield 'exact' => [
true,
'error',
'Some variable did not contain a value.',
true,
];

yield 'wrong level' => [
false,
'warning',
'Some variable did not contain a value.',
true,
];

yield 'wrong message' => [
false,
'error',
'Some variables did not contain a value.',
true,
];

yield 'approximate' => [
true,
'error',
'Some variable did not',
false,
];

yield 'approximate but wrong level' => [
false,
'warning',
'Some variable did not',
false,
];
}
}
2 changes: 2 additions & 0 deletions user_guide_src/source/changelogs/v4.3.0.rst
Original file line numberDiff line numberDiff line change
Expand Up@@ -128,6 +128,8 @@ Testing
- The CITestStreamFilter filter class now implements methods for adding a filter to streams. See :ref:`testing-cli-output`.
- Added the ``PhpStreamWrapper`` to make it easier to work with setting data to ``php://stdin``. See :ref:`testing-cli-input`.
- Added method :ref:`benchmark-timer-record` to measure performance in a callable. Also enhanced common function ``timer()`` to accept optional callable.
- A boolean third parameter ``$useExactComparison`` is added to ``TestLogger::didLog()`` which sets whether log messages are checked verbatim. This defaults to ``true``.
- Added method ``CIUnitTestCase::assertLogContains()`` which compares log messages by parts instead of the whole of the message.

Database
========
Expand Down
7 changes: 6 additions & 1 deletion user_guide_src/source/testing/overview.rst
Original file line numberDiff line numberDiff line change
Expand Up@@ -126,7 +126,12 @@ Additional Assertions
assertLogged($level, $expectedMessage)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Ensure that something you expected to be logged actually was:
Ensure that something you expected to be logged was actually logged:

assertLogContains($level, $logMessage)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Ensure that there's a record in the logs which contains a message part.

.. literalinclude:: overview/007.php

Expand Down
12 changes: 8 additions & 4 deletions user_guide_src/source/testing/overview/007.php
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
<?php

$config = new LoggerConfig();
$logger = new Logger($config);
$config = new Config\Logger();
$logger = new CodeIgniter\Log\Logger($config);

// ... do something that you expect a log entry from
// check verbatim the log message
$logger->log('error', "That's no moon");

$this->assertLogged('error', "That's no moon");

// check that a portion of the message is found in the logs
$exception = new RuntimeException('Hello world.');
$logger->log('error', $exception->getTraceAsString());
$this->assertLogContains('error', '{main}');
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
17 changes: 15 additions & 2 deletions system/Test/CIUnitTestCase.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -349,8 +349,6 @@ protected function mockSession()
* @param string|null $expectedMessage
*
* @return bool
*
* @throws Exception
*/
public function assertLogged(string $level, $expectedMessage = null)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not related to this PR, but why $expectedMessage can be null?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure either.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤦‍♂️

{
Expand All@@ -365,6 +363,21 @@ public function assertLogged(string $level, $expectedMessage = null)
return $result;
}

/**
* Asserts that there is a log record that contains `$logMessage` in the message.
*/
public function assertLogContains(string $level, string $logMessage, string $message = ''): void

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$expectedMessage is better? assertLogged() uses it.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My understanding is $expectedMessage is used since the assertion compares the message as expected in the logs.
For assertLogContains, I think it is counter-intuitive to use $expectedMessage when it can only be a part of the whole expected message. I was also thinking whether to use $needle or $needleMessage as alternative name.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No preference from me

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am a little uncomfortable with both $needle and $needleMessage.
needle needs haystack.

So let's leave it as it is.

{
$this->assertTrue(
TestLogger::didLog($level, $logMessage, false),
$message ?: sprintf(
'Failed asserting that logs have a record of message containing "%s" with level "%s".',
$logMessage,
$level
)
);
}

/**
* Hooks into CodeIgniter's Events system to check if a specific
* event was triggered or not.
Expand Down
18 changes: 16 additions & 2 deletions system/Test/TestLogger.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -59,10 +59,24 @@ public function log($level, $message, array $context = []): bool
*
* @return bool
*/
public static function didLog(string $level, $message)
public static function didLog(string $level, $message, bool $useExactComparison = true)
{
$lowerLevel = strtolower($level);

foreach (self::$op_logs as $log) {
if (strtolower($log['level']) === strtolower($level) && $message === $log['message']) {
if (strtolower($log['level']) !== $lowerLevel) {
continue;
}

if ($useExactComparison) {
if ($log['message'] === $message) {
return true;
}

continue;
}

if (strpos($log['message'], $message) !== false) {
return true;
}
}
Expand Down
6 changes: 6 additions & 0 deletions tests/system/Test/TestCaseTest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -45,6 +45,12 @@ public function testLogging()
$this->assertLogged('error', 'Some variable did not contain a value.');
}

public function testAssertLogContains()
{
log_message('error', 'Some variable did not contain a value.');
$this->assertLogContains('error', 'variable did not');
}

public function testEventTriggering()
{
Events::on('foo', static function ($arg) use (&$result) {
Expand Down
71 changes: 71 additions & 0 deletions tests/system/Test/TestLoggerTest.php
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
<?php

/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/

namespace CodeIgniter\Test;

use Config\Logger;

/**
* @internal
*/
final class TestLoggerTest extends CIUnitTestCase
{
/**
* @dataProvider provideDidLogCases
*/
public function testDidLogMethod(bool $expected, string $level, string $message, bool $exact): void
{
(new TestLogger(new Logger()))->log('error', 'Some variable did not contain a value.');

$this->assertSame(
$expected,
TestLogger::didLog($level, $message, $exact),
);
}

public function provideDidLogCases(): iterable
{
yield 'exact' => [
true,
'error',
'Some variable did not contain a value.',
true,
];

yield 'wrong level' => [
false,
'warning',
'Some variable did not contain a value.',
true,
];

yield 'wrong message' => [
false,
'error',
'Some variables did not contain a value.',
true,
];

yield 'approximate' => [
true,
'error',
'Some variable did not',
false,
];

yield 'approximate but wrong level' => [
false,
'warning',
'Some variable did not',
false,
];
}
}
2 changes: 2 additions & 0 deletions user_guide_src/source/changelogs/v4.3.0.rst
Original file line numberDiff line numberDiff line change
Expand Up@@ -128,6 +128,8 @@ Testing
- The CITestStreamFilter filter class now implements methods for adding a filter to streams. See :ref:`testing-cli-output`.
- Added the ``PhpStreamWrapper`` to make it easier to work with setting data to ``php://stdin``. See :ref:`testing-cli-input`.
- Added method :ref:`benchmark-timer-record` to measure performance in a callable. Also enhanced common function ``timer()`` to accept optional callable.
- A boolean third parameter ``$useExactComparison`` is added to ``TestLogger::didLog()`` which sets whether log messages are checked verbatim. This defaults to ``true``.
- Added method ``CIUnitTestCase::assertLogContains()`` which compares log messages by parts instead of the whole of the message.

Database
========
Expand Down
7 changes: 6 additions & 1 deletion user_guide_src/source/testing/overview.rst
Original file line numberDiff line numberDiff line change
Expand Up@@ -126,7 +126,12 @@ Additional Assertions
assertLogged($level, $expectedMessage)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Ensure that something you expected to be logged actually was:
Ensure that something you expected to be logged was actually logged:

assertLogContains($level, $logMessage)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Ensure that there's a record in the logs which contains a message part.

.. literalinclude:: overview/007.php

Expand Down
12 changes: 8 additions & 4 deletions user_guide_src/source/testing/overview/007.php
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
<?php

$config = new LoggerConfig();
$logger = new Logger($config);
$config = new Config\Logger();
$logger = new CodeIgniter\Log\Logger($config);

// ... do something that you expect a log entry from
// check verbatim the log message
$logger->log('error', "That's no moon");

$this->assertLogged('error', "That's no moon");

// check that a portion of the message is found in the logs
$exception = new RuntimeException('Hello world.');
$logger->log('error', $exception->getTraceAsString());
$this->assertLogContains('error', '{main}');
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
17 changes: 15 additions & 2 deletions system/Test/CIUnitTestCase.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -349,8 +349,6 @@ protected function mockSession()
* @param string|null $expectedMessage
*
* @return bool
*
* @throws Exception
*/
public function assertLogged(string $level, $expectedMessage = null)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not related to this PR, but why $expectedMessage can be null?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure either.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤦‍♂️

{
Expand All@@ -365,6 +363,21 @@ public function assertLogged(string $level, $expectedMessage = null)
return $result;
}

/**
* Asserts that there is a log record that contains `$logMessage` in the message.
*/
public function assertLogContains(string $level, string $logMessage, string $message = ''): void

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$expectedMessage is better? assertLogged() uses it.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My understanding is $expectedMessage is used since the assertion compares the message as expected in the logs.
For assertLogContains, I think it is counter-intuitive to use $expectedMessage when it can only be a part of the whole expected message. I was also thinking whether to use $needle or $needleMessage as alternative name.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No preference from me

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am a little uncomfortable with both $needle and $needleMessage.
needle needs haystack.

So let's leave it as it is.

{
$this->assertTrue(
TestLogger::didLog($level, $logMessage, false),
$message ?: sprintf(
'Failed asserting that logs have a record of message containing "%s" with level "%s".',
$logMessage,
$level
)
);
}

/**
* Hooks into CodeIgniter's Events system to check if a specific
* event was triggered or not.
Expand Down
18 changes: 16 additions & 2 deletions system/Test/TestLogger.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -59,10 +59,24 @@ public function log($level, $message, array $context = []): bool
*
* @return bool
*/
public static function didLog(string $level, $message)
public static function didLog(string $level, $message, bool $useExactComparison = true)
{
$lowerLevel = strtolower($level);

foreach (self::$op_logs as $log) {
if (strtolower($log['level']) === strtolower($level) && $message === $log['message']) {
if (strtolower($log['level']) !== $lowerLevel) {
continue;
}

if ($useExactComparison) {
if ($log['message'] === $message) {
return true;
}

continue;
}

if (strpos($log['message'], $message) !== false) {
return true;
}
}
Expand Down
6 changes: 6 additions & 0 deletions tests/system/Test/TestCaseTest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -45,6 +45,12 @@ public function testLogging()
$this->assertLogged('error', 'Some variable did not contain a value.');
}

public function testAssertLogContains()
{
log_message('error', 'Some variable did not contain a value.');
$this->assertLogContains('error', 'variable did not');
}

public function testEventTriggering()
{
Events::on('foo', static function ($arg) use (&$result) {
Expand Down
71 changes: 71 additions & 0 deletions tests/system/Test/TestLoggerTest.php
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
<?php

/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/

namespace CodeIgniter\Test;

use Config\Logger;

/**
* @internal
*/
final class TestLoggerTest extends CIUnitTestCase
{
/**
* @dataProvider provideDidLogCases
*/
public function testDidLogMethod(bool $expected, string $level, string $message, bool $exact): void
{
(new TestLogger(new Logger()))->log('error', 'Some variable did not contain a value.');

$this->assertSame(
$expected,
TestLogger::didLog($level, $message, $exact),
);
}

public function provideDidLogCases(): iterable
{
yield 'exact' => [
true,
'error',
'Some variable did not contain a value.',
true,
];

yield 'wrong level' => [
false,
'warning',
'Some variable did not contain a value.',
true,
];

yield 'wrong message' => [
false,
'error',
'Some variables did not contain a value.',
true,
];

yield 'approximate' => [
true,
'error',
'Some variable did not',
false,
];

yield 'approximate but wrong level' => [
false,
'warning',
'Some variable did not',
false,
];
}
}
2 changes: 2 additions & 0 deletions user_guide_src/source/changelogs/v4.3.0.rst
Original file line numberDiff line numberDiff line change
Expand Up@@ -128,6 +128,8 @@ Testing
- The CITestStreamFilter filter class now implements methods for adding a filter to streams. See :ref:`testing-cli-output`.
- Added the ``PhpStreamWrapper`` to make it easier to work with setting data to ``php://stdin``. See :ref:`testing-cli-input`.
- Added method :ref:`benchmark-timer-record` to measure performance in a callable. Also enhanced common function ``timer()`` to accept optional callable.
- A boolean third parameter ``$useExactComparison`` is added to ``TestLogger::didLog()`` which sets whether log messages are checked verbatim. This defaults to ``true``.
- Added method ``CIUnitTestCase::assertLogContains()`` which compares log messages by parts instead of the whole of the message.

Database
========
Expand Down
7 changes: 6 additions & 1 deletion user_guide_src/source/testing/overview.rst
Original file line numberDiff line numberDiff line change
Expand Up@@ -126,7 +126,12 @@ Additional Assertions
assertLogged($level, $expectedMessage)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Ensure that something you expected to be logged actually was:
Ensure that something you expected to be logged was actually logged:

assertLogContains($level, $logMessage)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Ensure that there's a record in the logs which contains a message part.

.. literalinclude:: overview/007.php

Expand Down
12 changes: 8 additions & 4 deletions user_guide_src/source/testing/overview/007.php
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
<?php

$config = new LoggerConfig();
$logger = new Logger($config);
$config = new Config\Logger();
$logger = new CodeIgniter\Log\Logger($config);

// ... do something that you expect a log entry from
// check verbatim the log message
$logger->log('error', "That's no moon");

$this->assertLogged('error', "That's no moon");

// check that a portion of the message is found in the logs
$exception = new RuntimeException('Hello world.');
$logger->log('error', $exception->getTraceAsString());
$this->assertLogContains('error', '{main}');
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
17 changes: 15 additions & 2 deletions system/Test/CIUnitTestCase.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -349,8 +349,6 @@ protected function mockSession()
* @param string|null $expectedMessage
*
* @return bool
*
* @throws Exception
*/
public function assertLogged(string $level, $expectedMessage = null)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not related to this PR, but why $expectedMessage can be null?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure either.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤦‍♂️

{
Expand All@@ -365,6 +363,21 @@ public function assertLogged(string $level, $expectedMessage = null)
return $result;
}

/**
* Asserts that there is a log record that contains `$logMessage` in the message.
*/
public function assertLogContains(string $level, string $logMessage, string $message = ''): void

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$expectedMessage is better? assertLogged() uses it.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My understanding is $expectedMessage is used since the assertion compares the message as expected in the logs.
For assertLogContains, I think it is counter-intuitive to use $expectedMessage when it can only be a part of the whole expected message. I was also thinking whether to use $needle or $needleMessage as alternative name.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No preference from me

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am a little uncomfortable with both $needle and $needleMessage.
needle needs haystack.

So let's leave it as it is.

{
$this->assertTrue(
TestLogger::didLog($level, $logMessage, false),
$message ?: sprintf(
'Failed asserting that logs have a record of message containing "%s" with level "%s".',
$logMessage,
$level
)
);
}

/**
* Hooks into CodeIgniter's Events system to check if a specific
* event was triggered or not.
Expand Down
18 changes: 16 additions & 2 deletions system/Test/TestLogger.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -59,10 +59,24 @@ public function log($level, $message, array $context = []): bool
*
* @return bool
*/
public static function didLog(string $level, $message)
public static function didLog(string $level, $message, bool $useExactComparison = true)
{
$lowerLevel = strtolower($level);

foreach (self::$op_logs as $log) {
if (strtolower($log['level']) === strtolower($level) && $message === $log['message']) {
if (strtolower($log['level']) !== $lowerLevel) {
continue;
}

if ($useExactComparison) {
if ($log['message'] === $message) {
return true;
}

continue;
}

if (strpos($log['message'], $message) !== false) {
return true;
}
}
Expand Down
6 changes: 6 additions & 0 deletions tests/system/Test/TestCaseTest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -45,6 +45,12 @@ public function testLogging()
$this->assertLogged('error', 'Some variable did not contain a value.');
}

public function testAssertLogContains()
{
log_message('error', 'Some variable did not contain a value.');
$this->assertLogContains('error', 'variable did not');
}

public function testEventTriggering()
{
Events::on('foo', static function ($arg) use (&$result) {
Expand Down
71 changes: 71 additions & 0 deletions tests/system/Test/TestLoggerTest.php
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
<?php

/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/

namespace CodeIgniter\Test;

use Config\Logger;

/**
* @internal
*/
final class TestLoggerTest extends CIUnitTestCase
{
/**
* @dataProvider provideDidLogCases
*/
public function testDidLogMethod(bool $expected, string $level, string $message, bool $exact): void
{
(new TestLogger(new Logger()))->log('error', 'Some variable did not contain a value.');

$this->assertSame(
$expected,
TestLogger::didLog($level, $message, $exact),
);
}

public function provideDidLogCases(): iterable
{
yield 'exact' => [
true,
'error',
'Some variable did not contain a value.',
true,
];

yield 'wrong level' => [
false,
'warning',
'Some variable did not contain a value.',
true,
];

yield 'wrong message' => [
false,
'error',
'Some variables did not contain a value.',
true,
];

yield 'approximate' => [
true,
'error',
'Some variable did not',
false,
];

yield 'approximate but wrong level' => [
false,
'warning',
'Some variable did not',
false,
];
}
}
2 changes: 2 additions & 0 deletions user_guide_src/source/changelogs/v4.3.0.rst
Original file line numberDiff line numberDiff line change
Expand Up@@ -128,6 +128,8 @@ Testing
- The CITestStreamFilter filter class now implements methods for adding a filter to streams. See :ref:`testing-cli-output`.
- Added the ``PhpStreamWrapper`` to make it easier to work with setting data to ``php://stdin``. See :ref:`testing-cli-input`.
- Added method :ref:`benchmark-timer-record` to measure performance in a callable. Also enhanced common function ``timer()`` to accept optional callable.
- A boolean third parameter ``$useExactComparison`` is added to ``TestLogger::didLog()`` which sets whether log messages are checked verbatim. This defaults to ``true``.
- Added method ``CIUnitTestCase::assertLogContains()`` which compares log messages by parts instead of the whole of the message.

Database
========
Expand Down
7 changes: 6 additions & 1 deletion user_guide_src/source/testing/overview.rst
Original file line numberDiff line numberDiff line change
Expand Up@@ -126,7 +126,12 @@ Additional Assertions
assertLogged($level, $expectedMessage)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Ensure that something you expected to be logged actually was:
Ensure that something you expected to be logged was actually logged:

assertLogContains($level, $logMessage)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Ensure that there's a record in the logs which contains a message part.

.. literalinclude:: overview/007.php

Expand Down
12 changes: 8 additions & 4 deletions user_guide_src/source/testing/overview/007.php
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
<?php

$config = new LoggerConfig();
$logger = new Logger($config);
$config = new Config\Logger();
$logger = new CodeIgniter\Log\Logger($config);

// ... do something that you expect a log entry from
// check verbatim the log message
$logger->log('error', "That's no moon");

$this->assertLogged('error', "That's no moon");

// check that a portion of the message is found in the logs
$exception = new RuntimeException('Hello world.');
$logger->log('error', $exception->getTraceAsString());
$this->assertLogContains('error', '{main}');
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
17 changes: 15 additions & 2 deletions system/Test/CIUnitTestCase.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -349,8 +349,6 @@ protected function mockSession()
* @param string|null $expectedMessage
*
* @return bool
*
* @throws Exception
*/
public function assertLogged(string $level, $expectedMessage = null)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not related to this PR, but why $expectedMessage can be null?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure either.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤦‍♂️

{
Expand All@@ -365,6 +363,21 @@ public function assertLogged(string $level, $expectedMessage = null)
return $result;
}

/**
* Asserts that there is a log record that contains `$logMessage` in the message.
*/
public function assertLogContains(string $level, string $logMessage, string $message = ''): void

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$expectedMessage is better? assertLogged() uses it.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My understanding is $expectedMessage is used since the assertion compares the message as expected in the logs.
For assertLogContains, I think it is counter-intuitive to use $expectedMessage when it can only be a part of the whole expected message. I was also thinking whether to use $needle or $needleMessage as alternative name.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No preference from me

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am a little uncomfortable with both $needle and $needleMessage.
needle needs haystack.

So let's leave it as it is.

{
$this->assertTrue(
TestLogger::didLog($level, $logMessage, false),
$message ?: sprintf(
'Failed asserting that logs have a record of message containing "%s" with level "%s".',
$logMessage,
$level
)
);
}

/**
* Hooks into CodeIgniter's Events system to check if a specific
* event was triggered or not.
Expand Down
18 changes: 16 additions & 2 deletions system/Test/TestLogger.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -59,10 +59,24 @@ public function log($level, $message, array $context = []): bool
*
* @return bool
*/
public static function didLog(string $level, $message)
public static function didLog(string $level, $message, bool $useExactComparison = true)
{
$lowerLevel = strtolower($level);

foreach (self::$op_logs as $log) {
if (strtolower($log['level']) === strtolower($level) && $message === $log['message']) {
if (strtolower($log['level']) !== $lowerLevel) {
continue;
}

if ($useExactComparison) {
if ($log['message'] === $message) {
return true;
}

continue;
}

if (strpos($log['message'], $message) !== false) {
return true;
}
}
Expand Down
6 changes: 6 additions & 0 deletions tests/system/Test/TestCaseTest.php
Original file line numberDiff line numberDiff line change
Expand Up@@ -45,6 +45,12 @@ public function testLogging()
$this->assertLogged('error', 'Some variable did not contain a value.');
}

public function testAssertLogContains()
{
log_message('error', 'Some variable did not contain a value.');
$this->assertLogContains('error', 'variable did not');
}

public function testEventTriggering()
{
Events::on('foo', static function ($arg) use (&$result) {
Expand Down
71 changes: 71 additions & 0 deletions tests/system/Test/TestLoggerTest.php
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
<?php

/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/

namespace CodeIgniter\Test;

use Config\Logger;

/**
* @internal
*/
final class TestLoggerTest extends CIUnitTestCase
{
/**
* @dataProvider provideDidLogCases
*/
public function testDidLogMethod(bool $expected, string $level, string $message, bool $exact): void
{
(new TestLogger(new Logger()))->log('error', 'Some variable did not contain a value.');

$this->assertSame(
$expected,
TestLogger::didLog($level, $message, $exact),
);
}

public function provideDidLogCases(): iterable
{
yield 'exact' => [
true,
'error',
'Some variable did not contain a value.',
true,
];

yield 'wrong level' => [
false,
'warning',
'Some variable did not contain a value.',
true,
];

yield 'wrong message' => [
false,
'error',
'Some variables did not contain a value.',
true,
];

yield 'approximate' => [
true,
'error',
'Some variable did not',
false,
];

yield 'approximate but wrong level' => [
false,
'warning',
'Some variable did not',
false,
];
}
}
2 changes: 2 additions & 0 deletions user_guide_src/source/changelogs/v4.3.0.rst
Original file line numberDiff line numberDiff line change
Expand Up@@ -128,6 +128,8 @@ Testing
- The CITestStreamFilter filter class now implements methods for adding a filter to streams. See :ref:`testing-cli-output`.
- Added the ``PhpStreamWrapper`` to make it easier to work with setting data to ``php://stdin``. See :ref:`testing-cli-input`.
- Added method :ref:`benchmark-timer-record` to measure performance in a callable. Also enhanced common function ``timer()`` to accept optional callable.
- A boolean third parameter ``$useExactComparison`` is added to ``TestLogger::didLog()`` which sets whether log messages are checked verbatim. This defaults to ``true``.
- Added method ``CIUnitTestCase::assertLogContains()`` which compares log messages by parts instead of the whole of the message.

Database
========
Expand Down
7 changes: 6 additions & 1 deletion user_guide_src/source/testing/overview.rst
Original file line numberDiff line numberDiff line change
Expand Up@@ -126,7 +126,12 @@ Additional Assertions
assertLogged($level, $expectedMessage)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Ensure that something you expected to be logged actually was:
Ensure that something you expected to be logged was actually logged:

assertLogContains($level, $logMessage)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Ensure that there's a record in the logs which contains a message part.

.. literalinclude:: overview/007.php

Expand Down
12 changes: 8 additions & 4 deletions user_guide_src/source/testing/overview/007.php
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
<?php

$config = new LoggerConfig();
$logger = new Logger($config);
$config = new Config\Logger();
$logger = new CodeIgniter\Log\Logger($config);

// ... do something that you expect a log entry from
// check verbatim the log message
$logger->log('error', "That's no moon");

$this->assertLogged('error', "That's no moon");

// check that a portion of the message is found in the logs
$exception = new RuntimeException('Hello world.');
$logger->log('error', $exception->getTraceAsString());
$this->assertLogContains('error', '{main}');