diff --git a/assets/js/aspire-update.js b/assets/js/aspire-update.js index eeaa95c..0c0ca1b 100644 --- a/assets/js/aspire-update.js +++ b/assets/js/aspire-update.js @@ -97,7 +97,7 @@ class ViewLog { jQuery.ajax(parameters) .done(function (response) { if ((true == response.success) && ('' != response.data.content)) { - let lines = response.data.content.split(aspireupdate.line_ending); + let lines = response.data.content; jQuery.each(lines, function (index, line) { jQuery('
') .append( diff --git a/includes/class-debug.php b/includes/class-debug.php index 81ab276..cab628e 100644 --- a/includes/class-debug.php +++ b/includes/class-debug.php @@ -19,6 +19,13 @@ class Debug { */ private static $log_file = 'debug-aspire-update.log'; + /** + * The filesystem. + * + * @var Filesystem_Direct + */ + private static $filesystem; + /** * Get the Log file path. * @@ -31,44 +38,17 @@ private static function get_file_path() { /** * Initializes the WordPress Filesystem. * - * @return WP_Filesystem_Base|false The filesystem object or false on failure. + * @return Filesystem_Direct The filesystem object. */ private static function init_filesystem() { - global $wp_filesystem; - - if ( ! $wp_filesystem ) { + if ( ! self::$filesystem instanceof Filesystem_Direct ) { require_once ABSPATH . 'wp-admin/includes/file.php'; + require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-base.php'; + require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-direct.php'; WP_Filesystem(); + self::$filesystem = new Filesystem_Direct( false ); } - - return $wp_filesystem; - } - - /** - * Checks the filesystem status and logs error to debug log. - * - * @param WP_Filesystem_Base $wp_filesystem The filesystem object. - * - * @return boolean true on success and false on failure. - */ - private static function verify_filesystem( $wp_filesystem ) { - if ( ! $wp_filesystem ) { - if ( - defined( 'WP_DEBUG' ) && - ( true === WP_DEBUG ) && - defined( 'WP_DEBUG_LOG' ) && - ( true === WP_DEBUG_LOG ) - ) { - // phpcs:disable WordPress.PHP.DevelopmentFunctions - /** - * Log error in file write fails only if debug is set to true. This is a valid use case. - */ - error_log( 'AspireUpdate - Could not open or write to the file system. Check file system permissions to debug log directory.' ); // @codeCoverageIgnore - // phpcs:enable - } - return false; - } - return true; + return self::$filesystem; } /** @@ -76,34 +56,23 @@ private static function verify_filesystem( $wp_filesystem ) { * * @param integer $limit Max no of lines to return. Defaults to a 1000 lines. * - * @return string|WP_Error The File content truncate upto the number of lines set in the limit parameter. + * @return array|WP_Error An array of lines in the file, limited to $limit, or a WP_Error object on failure. */ public static function read( $limit = 1000 ) { $wp_filesystem = self::init_filesystem(); $file_path = self::get_file_path(); - if ( ! self::verify_filesystem( $wp_filesystem ) || ! $wp_filesystem->exists( $file_path ) || ! $wp_filesystem->is_readable( $file_path ) ) { + + if ( ! $wp_filesystem->exists( $file_path ) || ! $wp_filesystem->is_readable( $file_path ) ) { return new \WP_Error( 'not_readable', __( 'Error: Unable to read the log file.', 'aspireupdate' ) ); } - $file_content = $wp_filesystem->get_contents_array( $file_path ); - $content = ''; - $index = 0; - foreach ( $file_content as $file_content_lines ) { - if ( ( $index < $limit ) ) { - $content .= $file_content_lines . PHP_EOL; - ++$index; - } - } - if ( '' === trim( $content ) ) { - $content = esc_html__( '*****Log file is empty.*****', 'aspireupdate' ); - } elseif ( $limit < count( $file_content ) ) { - $content .= PHP_EOL . sprintf( - /* translators: 1: The number of lines at which the content was truncated. */ - esc_html__( '*****Log truncated at %s lines.*****', 'aspireupdate' ), - $limit - ); + $file_content = $wp_filesystem->get_contents_array( $file_path, $limit, true ); + + if ( ( false === $file_content ) || ( 0 === count( array_filter( $file_content ) ) ) ) { + $file_content = [ esc_html__( '*****Log file is empty.*****', 'aspireupdate' ) ]; } - return $content; + + return $file_content; } /** @@ -114,7 +83,8 @@ public static function read( $limit = 1000 ) { public static function clear() { $wp_filesystem = self::init_filesystem(); $file_path = self::get_file_path(); - if ( ! self::verify_filesystem( $wp_filesystem ) || ! $wp_filesystem->exists( $file_path ) || ! $wp_filesystem->is_writable( $file_path ) ) { + + if ( ! $wp_filesystem->exists( $file_path ) || ! $wp_filesystem->is_writable( $file_path ) ) { return new \WP_Error( 'not_accessible', __( 'Error: Unable to access the log file.', 'aspireupdate' ) ); } @@ -133,30 +103,22 @@ public static function clear() { * @param string $type The log level ('string', 'request', 'response'). */ public static function log( $message, $type = 'string' ) { - $wp_filesystem = self::init_filesystem(); - if ( self::verify_filesystem( $wp_filesystem ) ) { - $timestamp = gmdate( 'Y-m-d H:i:s' ); - $formatted_message = sprintf( - '[%s] [%s]: %s', - $timestamp, - strtoupper( $type ), - self::format_message( $message ) - ) . PHP_EOL; - - $file_path = self::get_file_path(); - - $content = ''; - if ( $wp_filesystem->exists( $file_path ) ) { - if ( $wp_filesystem->is_readable( $file_path ) ) { - $content = $wp_filesystem->get_contents( $file_path ); - } - } - $wp_filesystem->put_contents( - $file_path, - $formatted_message . $content, - FS_CHMOD_FILE - ); - } + $wp_filesystem = self::init_filesystem(); + $timestamp = gmdate( 'Y-m-d H:i:s' ); + $formatted_message = sprintf( + '[%s] [%s]: %s', + $timestamp, + strtoupper( $type ), + self::format_message( $message ) + ) . PHP_EOL; + + $file_path = self::get_file_path(); + $wp_filesystem->put_contents( + $file_path, + $formatted_message, + FS_CHMOD_FILE, + 'a' + ); } /** diff --git a/includes/class-filesystem-direct.php b/includes/class-filesystem-direct.php new file mode 100644 index 0000000..9f7e097 --- /dev/null +++ b/includes/class-filesystem-direct.php @@ -0,0 +1,104 @@ +exists( $file ) ) { + return false; + } + + if ( -1 === $number_of_lines ) { + return @file( $file ); + } + + $handle = @fopen( $file, 'r' ); + if ( ! $handle ) { + return false; + } + + $lines = []; + $line_count = 0; + + // phpcs:disable Generic.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition + /** + * This is a valid and intentional use. + */ + while ( ( $line = fgets( $handle ) ) !== false ) { + $lines[] = trim( $line ); + ++$line_count; + + if ( $count_bottom_to_top ) { + if ( $number_of_lines > 0 && $line_count > $number_of_lines ) { + array_shift( $lines ); + } + } elseif ( $number_of_lines > 0 && $line_count >= $number_of_lines ) { + break; + } + } + // phpcs:enable + + fclose( $handle ); + + return $lines; + } + + /** + * Write contents to a file with additional modes. + * + * @param string $file The path to the file. + * @param string $contents The content to write. + * @param int|false $mode Optional. The file permissions as octal number, usually 0644. + * Default false. + * @param string $write_mode The write mode: + * 'w' - Overwrite the file (default). + * 'a' - Append to the file. + * 'x' - Create a new file and write, fail if the file exists. + * 'c' - Open the file for writing, but do not truncate. + * @return bool True on success, false on failure. + */ + public function put_contents( $file, $contents, $mode = false, $write_mode = 'w' ) { + $valid_write_modes = [ 'w', 'a', 'x', 'c' ]; + if ( ! in_array( $write_mode, $valid_write_modes, true ) ) { + return false; + } + + $handle = @fopen( $file, $write_mode ); + if ( ! $handle ) { + return false; + } + + mbstring_binary_safe_encoding(); + $data_length = strlen( $contents ); + $bytes_written = fwrite( $handle, $contents ); + reset_mbstring_encoding(); + + fclose( $handle ); + + if ( $data_length !== $bytes_written ) { + return false; + } + + $this->chmod( $file, $mode ); + + return true; + } +} diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 66ac36d..75aaf4e 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -59,6 +59,23 @@ + + + includes/class-filesystem-direct\.php + + + + includes/class-filesystem-direct\.php + + + + includes/class-filesystem-direct\.php + + + + includes/class-filesystem-direct\.php + + /tests/phpunit/* @@ -82,4 +99,10 @@ /tests/phpunit/* + + /tests/phpunit/* + + + /tests/phpunit/* + diff --git a/tests/phpunit/includes/Debug_UnitTestCase.php b/tests/phpunit/includes/Debug_UnitTestCase.php index 0058fd0..a5a9df0 100644 --- a/tests/phpunit/includes/Debug_UnitTestCase.php +++ b/tests/phpunit/includes/Debug_UnitTestCase.php @@ -13,19 +13,26 @@ abstract class Debug_UnitTestCase extends WP_UnitTestCase { protected static $log_file; /** - * Previously created filesystems. + * The class for use with the Reflection API. * - * @var array + * @var \ReflectionClass */ - protected static $filesystems = []; + protected static $reflection; /** - * The original value of $wp_filesystem before any tests run. + * The default filesystem. * - * @var WP_Filesystem_Base|null|false False if not already set. + * @var \AspireUpdate\Filesystem_Direct|null */ protected static $default_filesystem; + /** + * Previously created filesystems. + * + * @var array + */ + protected static $filesystems = []; + /** * Gets the log file's path, and deletes if it exists before any tests run. * Backs up the default filesystem. @@ -48,11 +55,8 @@ public static function set_up_before_class() { unlink( self::$log_file ); } - if ( isset( $GLOBALS['wp_filesystem'] ) ) { - self::$default_filesystem = $GLOBALS['wp_filesystem']; - } else { - self::$default_filesystem = false; - } + self::$reflection = new ReflectionClass( '\AspireUpdate\Debug' ); + self::$default_filesystem = self::$reflection->getStaticPropertyValue( 'filesystem' ); } /** @@ -83,11 +87,7 @@ public function tear_down() { unlink( self::$log_file ); } - if ( false === self::$default_filesystem ) { - unset( $GLOBALS['wp_filesystem'] ); - } else { - $GLOBALS['wp_filesystem'] = self::$default_filesystem; - } + self::$reflection->setStaticPropertyValue( 'filesystem', self::$default_filesystem ); parent::tear_down(); } diff --git a/tests/phpunit/includes/class-ap-fake-filesystem.php b/tests/phpunit/includes/class-ap-fake-filesystem.php index d2de759..0f09ab8 100644 --- a/tests/phpunit/includes/class-ap-fake-filesystem.php +++ b/tests/phpunit/includes/class-ap-fake-filesystem.php @@ -3,7 +3,7 @@ require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-base.php'; require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-direct.php'; -class AP_FakeFilesystem extends WP_Filesystem_Direct { +class AP_FakeFilesystem extends AspireUpdate\Filesystem_Direct { /** * Whether paths should exist. * diff --git a/tests/phpunit/tests/Debug/Debug_ClearTest.php b/tests/phpunit/tests/Debug/Debug_ClearTest.php index d6882c9..3bc1bba 100644 --- a/tests/phpunit/tests/Debug/Debug_ClearTest.php +++ b/tests/phpunit/tests/Debug/Debug_ClearTest.php @@ -8,34 +8,19 @@ /** * Tests for Debug::clear() * + * These tests cause constants to be defined. + * They must run in separate processes and must not preserve global state. + * + * @runTestsInSeparateProcesses + * @preserveGlobalState disabled + * * @covers \AspireUpdate\Debug::clear */ class Debug_ClearTest extends Debug_UnitTestCase { - /** - * Test that a WP_Error object is returned when the filesystem isn't available. - * - * @covers \AspireUpdate\Debug::init_filesystem - * @covers \AspireUpdate\Debug::verify_filesystem - */ - public function test_should_return_wp_error_when_filesystem_is_not_available() { - add_filter( 'filesystem_method', '__return_false' ); - - $this->assertWPError( - AspireUpdate\Debug::clear(), - 'A WP_Error object was not returned.' - ); - - $this->assertFileDoesNotExist( - self::$log_file, - 'The log file was created.' - ); - } - /** * Test that a WP_Error object is returned when the log file doesn't exist. * * @covers \AspireUpdate\Debug::init_filesystem - * @covers \AspireUpdate\Debug::verify_filesystem * @covers \AspireUpdate\Debug::get_file_path */ public function test_should_return_wp_error_when_log_file_does_not_exist() { @@ -54,14 +39,13 @@ public function test_should_return_wp_error_when_log_file_does_not_exist() { * Test that a WP_Error object is returned when the log file isn't writable. * * @covers \AspireUpdate\Debug::init_filesystem - * @covers \AspireUpdate\Debug::verify_filesystem * @covers \AspireUpdate\Debug::get_file_path */ public function test_should_return_wp_error_when_log_file_is_not_writable() { - global $wp_filesystem; + file_put_contents( self::$log_file, '' ); - // Backup and replace the filesystem object. - $wp_filesystem = $this->get_fake_filesystem( true, true, false ); + // Replace the filesystem object. + self::$reflection->setStaticPropertyValue( 'filesystem', $this->get_fake_filesystem( true, true, false ) ); $actual = AspireUpdate\Debug::clear(); @@ -69,11 +53,6 @@ public function test_should_return_wp_error_when_log_file_is_not_writable() { $actual, 'A WP_Error was not returned.' ); - - $this->assertFileDoesNotExist( - self::$log_file, - 'The log file was created.' - ); } /** diff --git a/tests/phpunit/tests/Debug/Debug_LogTest.php b/tests/phpunit/tests/Debug/Debug_LogTest.php index c95036f..4876d20 100644 --- a/tests/phpunit/tests/Debug/Debug_LogTest.php +++ b/tests/phpunit/tests/Debug/Debug_LogTest.php @@ -8,25 +8,15 @@ /** * Tests for Debug::log() * + * These tests cause constants to be defined. + * They must run in separate processes and must not preserve global state. + * + * @runTestsInSeparateProcesses + * @preserveGlobalState disabled + * * @covers \AspireUpdate\Debug::log */ class Debug_LogTest extends Debug_UnitTestCase { - /** - * Test that nothing is written to the log file when the filesystem isn't available. - * - * @covers \AspireUpdate\Debug::init_filesystem - * @covers \AspireUpdate\Debug::verify_filesystem - */ - public function test_should_not_write_to_log_file_when_filesystem_is_not_available() { - add_filter( 'filesystem_method', '__return_false' ); - - AspireUpdate\Debug::log( 'Test log message.' ); - - $this->assertFileDoesNotExist( - self::$log_file, - 'The log file was created.' - ); - } /** * Test that the log file is created when it doesn't already exist. @@ -75,27 +65,41 @@ public function test_should_add_message_to_log_file() { } /** - * Test that the message is prepended to an existing log file. + * Test that the message is appended to an existing log file. * * @covers \AspireUpdate\Debug::format_message */ - public function test_should_add_message_to_an_existing_log_file() { - $existing_content = 'An existing log file.'; - file_put_contents( self::$log_file, $existing_content ); + public function test_should_append_message_to_an_existing_log_file() { + $previous_message = "A previously logged message.\n"; + file_put_contents( self::$log_file, $previous_message ); - $message = 'Test log message.'; + $new_message = 'New log message.'; - AspireUpdate\Debug::log( $message ); + AspireUpdate\Debug::log( $new_message ); $this->assertFileExists( self::$log_file, 'The log file was not created.' ); + $actual = file_get_contents( self::$log_file ); + $this->assertStringContainsString( - "$message\n$existing_content", - file_get_contents( self::$log_file ), - 'The message was not prepended to the log file.' + $previous_message, + $actual, + 'The previous message does not exist.' + ); + + $this->assertStringContainsString( + $new_message, + $actual, + 'The new message does not exist.' + ); + + $this->assertLessThan( + strpos( $actual, $new_message ), + strpos( $actual, $previous_message ), + 'The new message was not appended to the log file.' ); } diff --git a/tests/phpunit/tests/Debug/Debug_ReadTest.php b/tests/phpunit/tests/Debug/Debug_ReadTest.php index 5f275e1..591a354 100644 --- a/tests/phpunit/tests/Debug/Debug_ReadTest.php +++ b/tests/phpunit/tests/Debug/Debug_ReadTest.php @@ -11,22 +11,10 @@ * @covers \AspireUpdate\Debug::read */ class Debug_ReadTest extends Debug_UnitTestCase { - /** - * Test that a WP_Error object is returned when the filesystem isn't available. - * - * @covers \AspireUpdate\Debug::init_filesystem - * @covers \AspireUpdate\Debug::verify_filesystem - */ - public function test_should_return_wp_error_when_filesystem_is_not_available() { - add_filter( 'filesystem_method', '__return_false' ); - $this->assertWPError( AspireUpdate\Debug::read() ); - } - /** * Test that a WP_Error object is returned when the log file doesn't exist. * * @covers \AspireUpdate\Debug::init_filesystem - * @covers \AspireUpdate\Debug::verify_filesystem * @covers \AspireUpdate\Debug::get_file_path */ public function test_should_return_wp_error_when_log_file_does_not_exist() { @@ -37,17 +25,14 @@ public function test_should_return_wp_error_when_log_file_does_not_exist() { * Test that a WP_Error object is returned when the log file isn't readable. * * @covers \AspireUpdate\Debug::init_filesystem - * @covers \AspireUpdate\Debug::verify_filesystem * @covers \AspireUpdate\Debug::get_file_path */ public function test_should_return_wp_error_when_log_file_is_not_readable() { - global $wp_filesystem; - // Create the log file. file_put_contents( self::$log_file, '' ); - // Backup and replace the filesystem object. - $wp_filesystem = $this->get_fake_filesystem( true, false, true ); + // Replace the filesystem object. + self::$reflection->setStaticPropertyValue( 'filesystem', $this->get_fake_filesystem( true, false, true ) ); $actual = AspireUpdate\Debug::read(); @@ -61,14 +46,26 @@ public function test_should_return_an_empty_log_message_when_log_file_is_empty() file_put_contents( self::$log_file, '' ); $actual = AspireUpdate\Debug::read(); - $this->assertIsString( + $this->assertIsArray( $actual, - 'A string was not returned.' + 'An array was not returned.' + ); + + $this->assertCount( + 1, + $actual, + 'An incorrect number of entries was returned.' + ); + + $entry = reset( $actual ); + $this->assertIsString( + $entry, + 'The entry is not a string.' ); $this->assertStringContainsString( 'Log file is empty', - $actual, + $entry, 'The empty log file message was not returned.' ); } @@ -81,14 +78,26 @@ public function test_should_return_an_empty_log_message_when_log_file_only_has_e file_put_contents( self::$log_file, " \n\r\t\v\x00" ); $actual = AspireUpdate\Debug::read(); - $this->assertIsString( + $this->assertIsArray( + $actual, + 'An array was not returned.' + ); + + $this->assertCount( + 1, $actual, - 'A string was not returned.' + 'An incorrect number of entries was returned.' + ); + + $entry = reset( $actual ); + $this->assertIsString( + $entry, + 'The entry is not a string.' ); $this->assertStringContainsString( 'Log file is empty', - $actual, + $entry, 'The empty log file message was not returned.' ); } @@ -101,23 +110,35 @@ public function test_should_not_return_an_empty_log_message_when_log_file_has_co file_put_contents( self::$log_file, 'Some contents' ); $actual = AspireUpdate\Debug::read(); - $this->assertIsString( + $this->assertIsArray( $actual, - 'A string was not returned.' + 'An array was not returned.' + ); + + $this->assertCount( + 1, + $actual, + 'An incorrect number of entries was returned.' + ); + + $entry = reset( $actual ); + $this->assertIsString( + $entry, + 'The entry is not a string.' ); $this->assertStringNotContainsString( 'Log file is empty', - $actual, + $entry, 'The empty log file message was returned.' ); } /** - * Test that a truncation message is added when the log file has more + * Test that truncation is performed when the log file has more * lines than requested. */ - public function test_should_add_a_truncation_message_when_log_file_has_more_lines_than_requested() { + public function test_should_add_truncate_when_log_file_has_more_lines_than_requested() { file_put_contents( self::$log_file, "First line\r\nSecond line\r\nThird line" @@ -125,23 +146,23 @@ public function test_should_add_a_truncation_message_when_log_file_has_more_line $actual = AspireUpdate\Debug::read( 2 ); - $this->assertIsString( + $this->assertIsArray( $actual, - 'A string was not returned.' + 'An array was not returned.' ); - $this->assertStringContainsString( - 'Log truncated', + $this->assertCount( + 2, $actual, - 'The truncation message was not returned.' + 'An incorrect number of entries was returned.' ); } /** - * Test that no truncation message is added when the log file has the same + * Test that the whole log is returned when the log file has the same * number of lines as requested. */ - public function test_should_not_add_a_truncation_message_when_log_file_has_the_same_number_of_lines_as_requested() { + public function test_should_return_whole_log_when_log_file_has_the_same_number_of_lines_as_requested() { file_put_contents( self::$log_file, "First line\r\nSecond line\r\nThird line" @@ -149,23 +170,23 @@ public function test_should_not_add_a_truncation_message_when_log_file_has_the_s $actual = AspireUpdate\Debug::read( 3 ); - $this->assertIsString( + $this->assertIsArray( $actual, - 'A string was not returned.' + 'An array was not returned.' ); - $this->assertStringNotContainsString( - 'Log truncated', + $this->assertCount( + 3, $actual, - 'The truncation message was added.' + 'An incorrect number of entries was returned.' ); } /** - * Test that no truncation message is added when the log file has fewer than + * Test that truncation is not performed when the log file has fewer than * lines than requested. */ - public function test_should_not_add_a_truncation_message_when_log_file_has_fewer_lines_than_requested() { + public function test_should_not_truncate_when_log_file_has_fewer_lines_than_requested() { file_put_contents( self::$log_file, "First line\r\nSecond line\r\nThird line" @@ -173,15 +194,15 @@ public function test_should_not_add_a_truncation_message_when_log_file_has_fewer $actual = AspireUpdate\Debug::read( 4 ); - $this->assertIsString( + $this->assertIsArray( $actual, - 'A string was not returned.' + 'An array was not returned.' ); - $this->assertStringNotContainsString( - 'Log truncated', + $this->assertCount( + 3, $actual, - 'The truncation message was added.' + 'An incorrect number of entries was returned.' ); } } diff --git a/tests/phpunit/tests/FilesystemDirect/GetContentsArrayTest.php b/tests/phpunit/tests/FilesystemDirect/GetContentsArrayTest.php new file mode 100644 index 0000000..73ba579 --- /dev/null +++ b/tests/phpunit/tests/FilesystemDirect/GetContentsArrayTest.php @@ -0,0 +1,263 @@ +assertFalse( $filesystem->get_contents_array( 'non_existent_file.txt', 1 ) ); + } + + /** + * Test that false is returned when the file is not readable. + * + * This test fakes a true exists() result, despite the file not existing. + * + * Since the file doesn't exist, read checks should fail. + */ + public function test_should_return_false_when_the_file_cannot_be_read() { + $filesystem = new AP_FakeFilesystem( true, false, false ); + + $this->assertFalse( $filesystem->get_contents_array( self::$test_file, 1 ) ); + } + + /** + * Test that the entire log is returned if the number of requested lines is -1. + */ + public function test_should_return_the_entire_log_if_number_of_requested_lines_is_minus_one() { + $filesystem = new AP_FakeFilesystem( true, true, true ); + $contents = 'First line' . PHP_EOL . 'Second line' . PHP_EOL . 'Third line'; + file_put_contents( self::$test_file, $contents ); + + $actual = $filesystem->get_contents_array( self::$test_file, -1 ); + + $this->assertIsArray( + $actual, + 'The contents were not read into an array.' + ); + + $this->assertSame( + [ + 'First line' . PHP_EOL, + 'Second line' . PHP_EOL, + 'Third line', + ], + $actual, + 'The entire log was not read.' + ); + } + + /** + * Test that the lines returned are from the bottom of the log file up when requested. + * + * @dataProvider data_count_bottom_to_top_enabled + * + * @param mixed $count_bottom_to_top Whether to count the lines from the bottom up. + */ + public function test_should_read_from_bottom_to_top_when_requested( $count_bottom_to_top ) { + $filesystem = new AP_FakeFilesystem( true, true, true ); + $contents = 'First line' . PHP_EOL . 'Second line' . PHP_EOL . 'Third line'; + file_put_contents( self::$test_file, $contents ); + + $actual = $filesystem->get_contents_array( self::$test_file, 2, $count_bottom_to_top ); + + $this->assertIsArray( + $actual, + 'The contents were not read into an array.' + ); + + $this->assertCount( + 2, + $actual, + 'The number of lines read does not match the requested number of lines.' + ); + + $this->assertSame( + [ + 'Second line', + 'Third line', + ], + $actual, + 'The lines were not read from the bottom up.' + ); + } + + /** + * Test that the whole log is returned when the log file has the same + * number of lines as requested. + * + * @dataProvider data_count_bottom_to_top_enabled + * @dataProvider data_count_bottom_to_top_disabled + * + * @param mixed $count_bottom_to_top Whether to count the lines from the bottom up. + */ + public function test_should_return_whole_log_when_log_file_has_the_same_number_of_lines_as_requested( $count_bottom_to_top ) { + $filesystem = new AP_FakeFilesystem( true, true, true ); + $contents = 'First line' . PHP_EOL . 'Second line' . PHP_EOL . 'Third line'; + file_put_contents( self::$test_file, $contents ); + + $actual = $filesystem->get_contents_array( self::$test_file, 3, $count_bottom_to_top ); + + $this->assertIsArray( + $actual, + 'The contents were not read into an array.' + ); + + $this->assertSame( + [ + 'First line', + 'Second line', + 'Third line', + ], + $actual, + 'The entire log was not read.' + ); + } + + /** + * Test that only the requested number of lines is read. + * + * @dataProvider data_count_bottom_to_top_enabled + * @dataProvider data_count_bottom_to_top_disabled + * + * @param mixed $count_bottom_to_top Whether to count the lines from the bottom up. + */ + public function test_should_only_read_the_requested_number_of_lines( $count_bottom_to_top ) { + $filesystem = new AP_FakeFilesystem( true, true, true ); + $contents = 'First line' . PHP_EOL . 'Second line' . PHP_EOL . 'Third line'; + file_put_contents( self::$test_file, $contents ); + + $actual = $filesystem->get_contents_array( self::$test_file, 2, $count_bottom_to_top ); + + $this->assertIsArray( + $actual, + 'The contents were not read into an array.' + ); + + if ( $count_bottom_to_top ) { + $expected = [ + 'Second line', + 'Third line', + ]; + } else { + $expected = [ + 'First line', + 'Second line', + ]; + } + + $this->assertSame( + $expected, + $actual, + 'The lines read do not match the expected lines.' + ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_count_bottom_to_top_enabled() { + return [ + '$count_bottom_to_top as (bool) true' => [ + 'count_bottom_to_top' => true, + ], + '$count_bottom_to_top as (int) 1' => [ + 'count_bottom_to_top' => 1, + ], + '$count_bottom_to_top as (float) 1.0' => [ + 'count_bottom_to_top' => 1.0, + ], + '$count_bottom_to_top as (float) -1.0' => [ + 'count_bottom_to_top' => -1.0, + ], + '$count_bottom_to_top as (string) "1"' => [ + 'count_bottom_to_top' => '1', + ], + '$count_bottom_to_top as a string with spaces' => [ + 'count_bottom_to_top' => " \t\r\n", + ], + '$count_bottom_to_top as a non-empty array' => [ + 'count_bottom_to_top' => [ 'not empty' ], + ], + '$count_bottom_to_top as an object' => [ + 'count_bottom_to_top' => new stdClass(), + ], + '$count_bottom_to_top as NAN' => [ + 'count_bottom_to_top' => NAN, + ], + '$count_bottom_to_top as INF' => [ + 'count_bottom_to_top' => INF, + ], + ]; + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_count_bottom_to_top_disabled() { + return [ + '$count_bottom_to_top as (bool) false' => [ + 'count_bottom_to_top' => false, + ], + '$count_bottom_to_top as (int) 0' => [ + 'count_bottom_to_top' => 0, + ], + '$count_bottom_to_top as (string) "0"' => [ + 'count_bottom_to_top' => '0', + ], + '$count_bottom_to_top as (float) 0.0' => [ + 'count_bottom_to_top' => 0.0, + ], + '$count_bottom_to_top as (float) -0.0' => [ + 'count_bottom_to_top' => -0.0, + ], + '$count_bottom_to_top as an empty string' => [ + 'count_bottom_to_top' => '', + ], + '$count_bottom_to_top as an empty array' => [ + 'count_bottom_to_top' => [], + ], + '$count_bottom_to_top as NULL' => [ + 'count_bottom_to_top' => null, + ], + ]; + } +} diff --git a/tests/phpunit/tests/FilesystemDirect/PutContentsTest.php b/tests/phpunit/tests/FilesystemDirect/PutContentsTest.php new file mode 100644 index 0000000..5c6e50d --- /dev/null +++ b/tests/phpunit/tests/FilesystemDirect/PutContentsTest.php @@ -0,0 +1,131 @@ +assertFalse( $filesystem->put_contents( self::$test_file, '', false, 'g' ) ); + } + + /** + * Test that the log file is created when it doesn't already exist. + * + * This test causes constants to be defined. + * It must run in a separate process and must not preserve global state. + * + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function test_should_create_log_file_if_it_does_not_already_exist() { + define( 'FS_CHMOD_FILE', 0644 ); + + $filesystem = new AP_FakeFilesystem( false, true, true ); + $filesystem->put_contents( self::$test_file, '', false, 'w' ); + + $this->assertFileExists( + self::$test_file, + 'The log file was not created.' + ); + } + + /** + * Test that false is returned when the path is a directory. + */ + public function test_should_return_false_when_the_path_is_a_directory() { + $test_dir = '/tmp/aspireupdate-putcontents-test-dir'; + $filesystem = new AP_FakeFilesystem( false, true, true ); + mkdir( $test_dir ); + + $this->assertDirectoryExists( + $test_dir, + 'The test directory was not created.' + ); + + $actual = $filesystem->put_contents( $test_dir, '', false, 'w' ); + rmdir( $test_dir ); + + $this->assertFalse( + $actual, + 'Passing a directory path did not return false.' + ); + } + + /** + * Test that content is appended to the file when the write mode is 'a'. + * + * This test causes constants to be defined. + * It must run in a separate process and must not preserve global state. + * + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function test_should_append_to_file_when_the_write_mode_is_a() { + define( 'FS_CHMOD_FILE', 0644 ); + + $existing_content = 'This is existing content.'; + $new_content = PHP_EOL . 'This is new content'; + file_put_contents( self::$test_file, $existing_content ); + + $this->assertFileExists( + self::$test_file, + 'The file was not created before testing.' + ); + + $this->assertSame( + $existing_content, + file_get_contents( self::$test_file ), + 'The contents of the test file are not correct before testing.' + ); + + $filesystem = new AP_FakeFilesystem( true, true, true ); + $filesystem->put_contents( self::$test_file, $new_content, false, 'a' ); + $contents = file_get_contents( self::$test_file ); + + $this->assertSame( + $contents, + $existing_content . $new_content, + 'The contents of the file are unexpected.' + ); + + $this->assertLessThan( + strpos( $contents, $new_content ), + strpos( $contents, $existing_content ), + 'The new content was not appended to the file.' + ); + } +}