From 032adca656c4cb8595397ef3dd7b5f78c24c16fb Mon Sep 17 00:00:00 2001 From: Julien Maurel Date: Thu, 16 Jan 2025 16:05:14 +0100 Subject: [PATCH] Add php 8.4 compatibility (#1255) --- .ci/generate_package_lifecycle_test_matrix.sh | 7 +- .ci/packer_cache.sh | 3 +- .ci/shared.sh | 2 +- .github/workflows/loop.yml | 1 + .github/workflows/phpt.yml | 1 + .github/workflows/test.yml | 2 + agent/native/CMakeLists.txt | 3 +- .../building/dependencies/php84/conandata.yml | 37 +++++ .../building/dependencies/php84/conanfile.py | 56 ++++++++ agent/native/ext/AST_debug.cpp | 7 +- agent/native/ext/AST_instrumentation.cpp | 42 ++++-- agent/native/ext/tests_util/tests_util.php | 2 +- agent/native/loader/code/phpdetection.cpp | 5 +- .../AutoInstrument/CurlHandleWrappedTrait.php | 1 + .../TransactionForExtensionRequest.php | 1 + .../Impl/Config/IniRawSnapshotSource.php | 1 + .../ElasticApm/Impl/InferredSpansManager.php | 13 +- .../Impl/Log/LoggableToJsonEncodable.php | 1 + agent/php/ElasticApm/Impl/Util/ArrayUtil.php | 19 +-- .../php/ElasticApm/Impl/Util/PhpErrorUtil.php | 106 +++++++++----- composer.json | 8 +- docs/setup.asciidoc | 2 +- docs/supported-technologies.asciidoc | 2 +- packaging/Makefile | 18 ++- packaging/post-install.sh | 5 +- packaging/test/centos/Dockerfile | 1 - packaging/test/docker-compose.yml | 38 +++++ packaging/test/fedora/Dockerfile | 61 ++++++++ packaging/test/fedora/entrypoint.sh | 135 ++++++++++++++++++ .../ComponentTests/ConfigSettingTest.php | 1 + .../GenerateUnpackScriptsTest.php | 18 ++- .../MySQLiAutoInstrumentationTest.php | 17 ++- .../Util/SpawnedProcessBase.php | 2 + .../Util/TestInfraHttpServerProcessBase.php | 32 +++-- .../WordPressAutoInstrumentationTest.php | 26 +++- .../UnitTests/InferredSpansBuilderTest.php | 1 + .../UtilTests/StackTraceUtilTest.php | 80 +++++++---- .../WordPressAutoInstrumentationUnitTest.php | 2 + .../Util/AssertMessageStack.php | 14 +- .../Util/AssertMessageStackScopeAutoRef.php | 8 +- .../Util/AssertMessageStackScopeData.php | 14 +- .../DeserializationException.php | 2 +- .../Deserialization/DeserializationUtil.php | 2 +- .../ServerApiSchemaValidationException.php | 2 +- .../Util/DummyExceptionForTests.php | 2 +- .../Util/InvalidEventDataException.php | 2 +- tests/ElasticApmTests/Util/MixedMap.php | 2 + .../Util/PhpUnitExtensionBase.php | 1 + .../Util/StackTraceExpectations.php | 2 + .../Util/StackTraceFrameExpectations.php | 48 ++++++- tests/ElasticApmTests/Util/TestCaseBase.php | 23 ++- .../ElasticApmTests/Util/TextUtilForTests.php | 1 + tests/bootstrap.php | 8 +- tests/polyfills/load.php | 2 - 54 files changed, 743 insertions(+), 149 deletions(-) create mode 100644 agent/native/building/dependencies/php84/conandata.yml create mode 100644 agent/native/building/dependencies/php84/conanfile.py create mode 100644 packaging/test/fedora/Dockerfile create mode 100755 packaging/test/fedora/entrypoint.sh diff --git a/.ci/generate_package_lifecycle_test_matrix.sh b/.ci/generate_package_lifecycle_test_matrix.sh index d18852b66..6ed00d39a 100755 --- a/.ci/generate_package_lifecycle_test_matrix.sh +++ b/.ci/generate_package_lifecycle_test_matrix.sh @@ -106,8 +106,13 @@ function generateAgentUpgradeRows () { local testingType=agent-upgrade local appHostKindShortName=all local testsGroup=smoke + for phpVersion in 7.4 ; do + for linuxPackageType in rpm ; do + echo "${phpVersion},${linuxPackageType},${testingType},${appHostKindShortName},${testsGroup}" + done + done for phpVersion in 7.4 "$(latestSupportedPhpVersion)" ; do - for linuxPackageType in deb rpm ; do + for linuxPackageType in deb ; do echo "${phpVersion},${linuxPackageType},${testingType},${appHostKindShortName},${testsGroup}" done done diff --git a/.ci/packer_cache.sh b/.ci/packer_cache.sh index 12b96fead..3b947b728 100755 --- a/.ci/packer_cache.sh +++ b/.ci/packer_cache.sh @@ -15,6 +15,7 @@ php:8.0-fpm php:8.1-fpm php:8.2-fpm php:8.3-fpm +php:8.4-fpm ruby:2.7.1-alpine3.12 ubuntu:20.04 " @@ -26,7 +27,7 @@ if [ -x "$(command -v docker)" ]; then # Make sure list of PHP versions supported by the Elastic APM PHP Agent is in sync. # See the comment in .ci/shared.sh - for version in 7.2 7.3 7.4 8.0 8.1 8.2 8.3 + for version in 7.2 7.3 7.4 8.0 8.1 8.2 8.3 8.4 do PHP_VERSION=${version} make -f .ci/Makefile prepare || true DOCKERFILE=Dockerfile.alpine PHP_VERSION=${version} make -f .ci/Makefile prepare || true diff --git a/.ci/shared.sh b/.ci/shared.sh index 63963943e..6d1641581 100644 --- a/.ci/shared.sh +++ b/.ci/shared.sh @@ -16,7 +16,7 @@ set -e # *) docker-compose.yml in packaging/test # -export ELASTIC_APM_PHP_TESTS_SUPPORTED_PHP_VERSIONS=(7.2 7.3 7.4 8.0 8.1 8.2 8.3) +export ELASTIC_APM_PHP_TESTS_SUPPORTED_PHP_VERSIONS=(7.2 7.3 7.4 8.0 8.1 8.2 8.3 8.4) export ELASTIC_APM_PHP_TESTS_SUPPORTED_LINUX_NATIVE_PACKAGE_TYPES=(apk deb rpm) export ELASTIC_APM_PHP_TESTS_SUPPORTED_LINUX_PACKAGE_TYPES=("${ELASTIC_APM_PHP_TESTS_SUPPORTED_LINUX_NATIVE_PACKAGE_TYPES[@]}" tar) diff --git a/.github/workflows/loop.yml b/.github/workflows/loop.yml index a5b7169d8..871f45137 100644 --- a/.github/workflows/loop.yml +++ b/.github/workflows/loop.yml @@ -23,6 +23,7 @@ jobs: - "8.1" - "8.2" - "8.3" + - "8.4" dockerfile: - "Dockerfile" - "Dockerfile.alpine" diff --git a/.github/workflows/phpt.yml b/.github/workflows/phpt.yml index 9dd4c8fe5..7cd3bc569 100644 --- a/.github/workflows/phpt.yml +++ b/.github/workflows/phpt.yml @@ -28,6 +28,7 @@ jobs: - "8.1" - "8.2" - "8.3" + - "8.4" steps: # - uses: actions/checkout@v4 # - name: Fetch and extract latest release of apm-agent-php diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index eea59f994..fd056f607 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -58,6 +58,7 @@ jobs: - "8.1" - "8.2" - "8.3" + - "8.4" data: ${{ fromJson(needs.setup-build-matrix.outputs.matrix-combinations).include }} env: PHP_VERSION: ${{ matrix.php-version }} @@ -90,6 +91,7 @@ jobs: - "8.1" - "8.2" - "8.3" + - "8.4" dockerfile: - "Dockerfile" - "Dockerfile.alpine" diff --git a/agent/native/CMakeLists.txt b/agent/native/CMakeLists.txt index 2758c03c8..22252ec7b 100644 --- a/agent/native/CMakeLists.txt +++ b/agent/native/CMakeLists.txt @@ -43,7 +43,7 @@ include(elastic_conan_debugsymbols) # Install project dependencies -set(_supported_php_versions 72 73 74 80 81 82 83) +set(_supported_php_versions 72 73 74 80 81 82 83 84) function(get_php_api_from_release php_version ret_val) block(SCOPE_FOR VARIABLES) @@ -54,6 +54,7 @@ function(get_php_api_from_release php_version ret_val) set(_php_release_81 20210902) set(_php_release_82 20220829) set(_php_release_83 20230831) + set(_php_release_84 20240924) set(${ret_val} ${_php_release_${php_version}}) return(PROPAGATE ${ret_val}) diff --git a/agent/native/building/dependencies/php84/conandata.yml b/agent/native/building/dependencies/php84/conandata.yml new file mode 100644 index 000000000..55abcef12 --- /dev/null +++ b/agent/native/building/dependencies/php84/conandata.yml @@ -0,0 +1,37 @@ +name: "php-headers-84" +version: "1.0" +php_source_version: 8.4.1 + +sources: + 7.2.34: + linux: + - url: "https://www.php.net/distributions/php-7.2.34.tar.gz" + contentsRoot: "php-7.2.34" + 7.3.33: + linux: + - url: "https://www.php.net/distributions/php-7.3.33.tar.gz" + contentsRoot: "php-7.3.33" + 7.4.33: + linux: + - url: "https://www.php.net/distributions/php-7.4.33.tar.gz" + contentsRoot: "php-7.4.33" + 8.0.28: + linux: + - url: "https://www.php.net/distributions/php-8.0.28.tar.gz" + contentsRoot: "php-8.0.28" + 8.1.18: + linux: + - url: "https://www.php.net/distributions/php-8.1.18.tar.gz" + contentsRoot: "php-8.1.18" + 8.2.5: + linux: + - url: "https://www.php.net/distributions/php-8.2.5.tar.gz" + contentsRoot: "php-8.2.5" + 8.3.2: + linux: + - url: "https://www.php.net/distributions/php-8.3.2.tar.gz" + contentsRoot: "php-8.3.2" + 8.4.1: + linux: + - url: "https://www.php.net/distributions/php-8.4.1.tar.gz" + contentsRoot: "php-8.4.1" diff --git a/agent/native/building/dependencies/php84/conanfile.py b/agent/native/building/dependencies/php84/conanfile.py new file mode 100644 index 000000000..2f1ec5fcc --- /dev/null +++ b/agent/native/building/dependencies/php84/conanfile.py @@ -0,0 +1,56 @@ +import os +import shutil + +from conans import tools, ConanFile, AutoToolsBuildEnvironment + +class PhpHeadersForPHP81Conan(ConanFile): + description = "PHP headers package required to build Elastic APM agent without additional PHP dependencies" + license = "The PHP License, version 3.01" + homepage = "https://php.net/" + url = "https://php.net/" + author = "pawel.filipczak@elastic.co" + + settings = "os", "compiler", "build_type", "arch" + platform = "linux" + + def init(self): + self.name = self.conan_data["name"] + self.version = self.conan_data["version"] # version of the package + self.php_version = self.conan_data["php_source_version"] # version of the PHP to build + self.source_temp_dir = "php-src" + + def requirements(self): + self.requires("libxml2/2.9.9") + self.requires("sqlite3/3.29.0") + + def source(self): + for source in self.conan_data["sources"][self.php_version][self.platform]: + + if "contentsRoot" in source: + # small hack - it can't contain custom fields, so we're removing it from source (got an unexpected keyword argument) + contentRoot = source["contentsRoot"] + del source["contentsRoot"] + tools.get(**source) + os.rename(contentRoot, self.source_temp_dir) + else: + self.output.error("Could not find 'contentsRoot' in conandata.yml") + raise Exception("Could not find 'contentsRoot' in conandata.yml") + + def build(self): + with tools.chdir(os.path.join(self.source_folder, self.source_temp_dir)): + buildEnv = AutoToolsBuildEnvironment(self) + envVariables = buildEnv.vars + envVariables['ac_cv_php_xml2_config_path'] = os.path.join(self.deps_cpp_info["libxml2"].rootpath, "bin/xml2-config") + envVariables['LIBXML_LIBS'] = os.path.join(self.deps_cpp_info["libxml2"].rootpath, self.deps_cpp_info["libxml2"].libdirs[0]) + envVariables['LIBXML_CFLAGS'] = "-I{}".format(os.path.join(self.deps_cpp_info["libxml2"].rootpath, self.deps_cpp_info["libxml2"].includedirs[0])) + envVariables['SQLITE_LIBS'] = os.path.join(self.deps_cpp_info["sqlite3"].rootpath, self.deps_cpp_info["sqlite3"].libdirs[0]) + envVariables['SQLITE_CFLAGS'] = "-I{}".format(os.path.join(self.deps_cpp_info["sqlite3"].rootpath, self.deps_cpp_info["sqlite3"].includedirs[0])) + self.run("./buildconf --force") + buildEnv.configure(args=[""], vars=envVariables, build=False, host=False) + + def package(self): + source = os.path.join(self.source_folder, self.source_temp_dir) + self.copy("*.h", src=source, dst='include', keep_path=True) + + def package_id(self): + del self.info.settings.compiler.version diff --git a/agent/native/ext/AST_debug.cpp b/agent/native/ext/AST_debug.cpp index 4794d91cd..cbe148df1 100644 --- a/agent/native/ext/AST_debug.cpp +++ b/agent/native/ext/AST_debug.cpp @@ -224,11 +224,16 @@ String zendAstKindToString( zend_ast_kind kind ) ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_TYPE_INTERSECTION ); #endif - #if PHP_VERSION_ID >= ELASTIC_APM_BUILD_PHP_VERSION_ID( 8, 3, 0 ) /* if PHP version from 8.3.0 */ ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_MODIFIER_LIST ); #endif + #if PHP_VERSION_ID >= ELASTIC_APM_BUILD_PHP_VERSION_ID( 8, 4, 0 ) /* if PHP version from 8.4.0 */ + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_PARENT_PROPERTY_HOOK_CALL ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_PROPERTY_HOOK ); + ELASTIC_APM_GEN_ENUM_TO_STRING_SWITCH_CASE( ZEND_AST_PROPERTY_HOOK_SHORT_BODY ); + #endif + default: return NULL; } diff --git a/agent/native/ext/AST_instrumentation.cpp b/agent/native/ext/AST_instrumentation.cpp index 45a096269..360f79aa2 100644 --- a/agent/native/ext/AST_instrumentation.cpp +++ b/agent/native/ext/AST_instrumentation.cpp @@ -181,25 +181,47 @@ bool isZendAstListKind( zend_ast_kind kind ) } /** - * zend_ast_create and zend_ast_create_ex allowed up to 4 child* parameters for version before PHP v8 - * and the limit was increased to 5 in PHP v8 + * Max number of children for AST nodes is + * 4 for PHP before 8.0 + * 5 for PHP from 8.0 but before 8.4 + * 6 for PHP from 8.4 * * @see ZEND_AST_SPEC_CALL_EX + * + * When adding support for a new PHP version: + * - Make sure g_astNodeMaxChildCount is correct + * - If g_astNodeMaxChildCount changed then update createAstEx() + * - Make sure zendAstKindToString() in AST_debug.cpp includes all the enum cases from enum _zend_ast_kind in /Zend/zend_ast.h + * - Increment minor part of PHP version in static_assert below */ -static size_t g_maxCreateAstChildCount = +static_assert( + PHP_VERSION_ID < ELASTIC_APM_BUILD_PHP_VERSION_ID( 8, 5, 0 ), + "Make sure g_astNodeMaxChildCount is correct. See max number of children in enum _zend_ast_kind in /Zend/zend_ast.h" +); +static constexpr size_t g_astNodeMaxChildCount = + #if PHP_VERSION_ID < ELASTIC_APM_BUILD_PHP_VERSION_ID( 8, 0, 0 ) + 4 // PHP before 8.0 + #elif PHP_VERSION_ID < ELASTIC_APM_BUILD_PHP_VERSION_ID( 8, 4, 0 ) + 5 // PHP from 8.0 but before 8.4 + #else + 6 // PHP from 8.4 + #endif +; +static constexpr size_t g_zendKindWithLargestChildNodesCount = #if PHP_VERSION_ID < ELASTIC_APM_BUILD_PHP_VERSION_ID( 8, 0, 0 ) - 4 + ZEND_AST_FOR // PHP before 8.0 #else - 5 + ZEND_AST_PARAM // PHP from 8.0 #endif ; +static_assert(g_zendKindWithLargestChildNodesCount == (g_astNodeMaxChildCount << ZEND_AST_NUM_CHILDREN_SHIFT)); zend_ast* createAstEx( zend_ast_kind kind, zend_ast_attr attr, ZendAstPtrArrayView children ) { char txtOutStreamBuf[ELASTIC_APM_TEXT_OUTPUT_STREAM_ON_STACK_BUFFER_SIZE]; TextOutputStream txtOutStream = ELASTIC_APM_TEXT_OUTPUT_STREAM_FROM_STATIC_BUFFER( txtOutStreamBuf ); - ELASTIC_APM_ASSERT_LE_UINT64( children.count, g_maxCreateAstChildCount ); + ELASTIC_APM_ASSERT_LE_UINT64( children.count, g_astNodeMaxChildCount ); ELASTIC_APM_ASSERT( ! isZendAstListKind( kind ), "kind: %s", streamZendAstKind( kind, &txtOutStream ) ); switch( children.count ) @@ -218,6 +240,10 @@ zend_ast* createAstEx( zend_ast_kind kind, zend_ast_attr attr, ZendAstPtrArrayVi case 5: return zend_ast_create_ex( kind, attr, children.values[ 0 ], children.values[ 1 ], children.values[ 2 ], children.values[ 3 ], children.values[ 4 ] ); #endif + #if PHP_VERSION_ID >= ELASTIC_APM_BUILD_PHP_VERSION_ID( 8, 4, 0 ) + case 6: + return zend_ast_create_ex( kind, attr, children.values[ 0 ], children.values[ 1 ], children.values[ 2 ], children.values[ 3 ], children.values[ 4 ], children.values[ 5 ] ); + #endif default: // silence compiler warning return nullptr; } @@ -227,9 +253,9 @@ ResultCode createAstExCheckChildrenCount( zend_ast_kind kind, zend_ast_attr attr { ResultCode resultCode; - if ( children.count > g_maxCreateAstChildCount ) + if ( children.count > g_astNodeMaxChildCount ) { - ELASTIC_APM_LOG_ERROR( "Number of children is larger than max; children.count: %u, g_maxCreateAstChildCount: %u", (unsigned)children.count, (unsigned)g_maxCreateAstChildCount ); + ELASTIC_APM_LOG_ERROR( "Number of children is larger than max; children.count: %u, g_astNodeMaxChildCount: %u", (unsigned)children.count, (unsigned)g_astNodeMaxChildCount ); ELASTIC_APM_SET_RESULT_CODE_AND_GOTO_FAILURE_EX( resultFailure ); } diff --git a/agent/native/ext/tests_util/tests_util.php b/agent/native/ext/tests_util/tests_util.php index 0b3a60f67..b7528ae83 100644 --- a/agent/native/ext/tests_util/tests_util.php +++ b/agent/native/ext/tests_util/tests_util.php @@ -21,7 +21,7 @@ declare(strict_types=1); -error_reporting(E_ALL | E_STRICT); +error_reporting(E_ALL); function elasticApmOnAssertFailure(string $condDesc, string $expr, $actual, $expected) { diff --git a/agent/native/loader/code/phpdetection.cpp b/agent/native/loader/code/phpdetection.cpp index 5f618308d..42f58e653 100644 --- a/agent/native/loader/code/phpdetection.cpp +++ b/agent/native/loader/code/phpdetection.cpp @@ -56,9 +56,10 @@ bool isThreadSafe() { std::tuple getZendModuleApiVersion(std::string_view zendVersion) { using namespace std::string_view_literals; - constexpr size_t knownVersionsCount = 16; + constexpr size_t knownVersionsCount = 17; constexpr std::array, knownVersionsCount> knownPhpVersions {{ + {"4.4"sv, 20240924, true}, // PHP 8.4 {"4.3"sv, 20230831, true}, // PHP 8.3 {"4.2"sv, 20220829, true}, // PHP 8.2 {"4.1"sv, 20210902, true}, // PHP 8.1 @@ -91,4 +92,4 @@ std::tuple getZendModuleApiVersion(std::string_view -} \ No newline at end of file +} diff --git a/agent/php/ElasticApm/Impl/AutoInstrument/CurlHandleWrappedTrait.php b/agent/php/ElasticApm/Impl/AutoInstrument/CurlHandleWrappedTrait.php index 094c542bd..3080869f5 100644 --- a/agent/php/ElasticApm/Impl/AutoInstrument/CurlHandleWrappedTrait.php +++ b/agent/php/ElasticApm/Impl/AutoInstrument/CurlHandleWrappedTrait.php @@ -100,6 +100,7 @@ public function setOpt(int $option, $value): bool public function asInt(): int { + /** @phpstan-ignore-next-line */ return is_resource($this->curlHandle) ? intval($this->curlHandle) : spl_object_id($this->curlHandle); } diff --git a/agent/php/ElasticApm/Impl/AutoInstrument/TransactionForExtensionRequest.php b/agent/php/ElasticApm/Impl/AutoInstrument/TransactionForExtensionRequest.php index 85102126b..c3ab63af7 100644 --- a/agent/php/ElasticApm/Impl/AutoInstrument/TransactionForExtensionRequest.php +++ b/agent/php/ElasticApm/Impl/AutoInstrument/TransactionForExtensionRequest.php @@ -572,6 +572,7 @@ private function discoverStartTime(float $requestInitStartTime): float return $requestInitStartTime; } + /** @phpstan-ignore-next-line */ $serverRequestTimeInSeconds = floatval($serverRequestTimeAsString); $serverRequestTimeInMicroseconds = $serverRequestTimeInSeconds * TimeUtil::NUMBER_OF_MICROSECONDS_IN_SECOND; if ($requestInitStartTime < $serverRequestTimeInMicroseconds) { diff --git a/agent/php/ElasticApm/Impl/Config/IniRawSnapshotSource.php b/agent/php/ElasticApm/Impl/Config/IniRawSnapshotSource.php index 56970a272..1ce0ebf40 100644 --- a/agent/php/ElasticApm/Impl/Config/IniRawSnapshotSource.php +++ b/agent/php/ElasticApm/Impl/Config/IniRawSnapshotSource.php @@ -79,6 +79,7 @@ private static function iniValueToString($iniValue): string return $iniValue ? 'true' : 'false'; } + /** @phpstan-ignore-next-line */ return strval($iniValue); } } diff --git a/agent/php/ElasticApm/Impl/InferredSpansManager.php b/agent/php/ElasticApm/Impl/InferredSpansManager.php index b8a316e04..433d9d086 100644 --- a/agent/php/ElasticApm/Impl/InferredSpansManager.php +++ b/agent/php/ElasticApm/Impl/InferredSpansManager.php @@ -205,11 +205,14 @@ private function onNewCurrentTransactionHasBegun(Transaction $transaction): void ($assertProxy = Assert::ifEnabled()) && $assertProxy->that($this->onCurrentSpanChangedCallback === null) && $assertProxy->withContext('$this->onCurrentSpanChangedCallback === null', ['this' => $this]); - $this->currentTransaction->onCurrentSpanChanged->add( - $this->onCurrentSpanChangedCallback = function (?Span $span): void { - $this->onCurrentSpanChanged($span); - } - ); + + if ($this->currentTransaction !== null) { + $this->currentTransaction->onCurrentSpanChanged->add( + $this->onCurrentSpanChangedCallback = function (?Span $span): void { + $this->onCurrentSpanChanged($span); + } + ); + } $this->builder = new InferredSpansBuilder($this->tracer); $this->state = self::STATE_RUNNING; diff --git a/agent/php/ElasticApm/Impl/Log/LoggableToJsonEncodable.php b/agent/php/ElasticApm/Impl/Log/LoggableToJsonEncodable.php index 895dd255c..923eaa29c 100644 --- a/agent/php/ElasticApm/Impl/Log/LoggableToJsonEncodable.php +++ b/agent/php/ElasticApm/Impl/Log/LoggableToJsonEncodable.php @@ -108,6 +108,7 @@ public static function convert($value, int $depth) return self::convertObject($value, $depth); } + /** @phpstan-ignore-next-line */ return [LogConsts::TYPE_KEY => DbgUtil::getType($value), LogConsts::VALUE_AS_STRING_KEY => strval($value)]; } diff --git a/agent/php/ElasticApm/Impl/Util/ArrayUtil.php b/agent/php/ElasticApm/Impl/Util/ArrayUtil.php index b79f7161d..881806d6c 100644 --- a/agent/php/ElasticApm/Impl/Util/ArrayUtil.php +++ b/agent/php/ElasticApm/Impl/Util/ArrayUtil.php @@ -33,17 +33,18 @@ final class ArrayUtil use StaticClassTrait; /** - * @param string $key - * @param array $array - * @param mixed $valueDst + * @template TKey of array-key + * @template TValue * - * @return bool + * @param TKey $key + * @param array $array + * @param TValue &$valueDst * - * @template T - * @phpstan-param T[] $array - * @phpstan-param T $valueDst + * @param-out TValue $valueDst + * + * @return bool */ - public static function getValueIfKeyExists(string $key, array $array, &$valueDst): bool + public static function getValueIfKeyExists($key, array $array, /* out */ &$valueDst): bool { if (!array_key_exists($key, $array)) { return false; @@ -54,7 +55,7 @@ public static function getValueIfKeyExists(string $key, array $array, &$valueDst } /** - * @template TKey of string|int + * @template TKey of array-key * @template TValue * * @param TKey $key diff --git a/agent/php/ElasticApm/Impl/Util/PhpErrorUtil.php b/agent/php/ElasticApm/Impl/Util/PhpErrorUtil.php index 3e6fdc727..9514823b5 100644 --- a/agent/php/ElasticApm/Impl/Util/PhpErrorUtil.php +++ b/agent/php/ElasticApm/Impl/Util/PhpErrorUtil.php @@ -32,41 +32,79 @@ final class PhpErrorUtil { use StaticClassTrait; - public static function getTypeName(int $type): ?string + /** + * @return array + */ + private static function flagsValueToNameMap(): array { - switch ($type) { - case E_ERROR: - return 'E_ERROR'; - case E_WARNING: - return 'E_WARNING'; - case E_PARSE: - return 'E_PARSE'; - case E_NOTICE: - return 'E_NOTICE'; - case E_CORE_ERROR: - return 'E_CORE_ERROR'; - case E_CORE_WARNING: - return 'E_CORE_WARNING'; - case E_COMPILE_ERROR: - return 'E_COMPILE_ERROR'; - case E_COMPILE_WARNING: - return 'E_COMPILE_WARNING'; - case E_USER_ERROR: - return 'E_USER_ERROR'; - case E_USER_WARNING: - return 'E_USER_WARNING'; - case E_USER_NOTICE: - return 'E_USER_NOTICE'; - case E_STRICT: - return 'E_STRICT'; - case E_RECOVERABLE_ERROR: - return 'E_RECOVERABLE_ERROR'; - case E_DEPRECATED: - return 'E_DEPRECATED'; - case E_USER_DEPRECATED: - return 'E_USER_DEPRECATED'; - default: - return null; + /** @var ?array $flags */ + static $flags = null; + + if ($flags === null) { + $flags = []; + $addToFlagsIfDefined = function (string $flagName, ?int $flagValue = null) use (&$flags): void { + if (defined($flagName)) { + $flagValueToUse = $flagValue ?? constant($flagName); + if (is_int($flagValueToUse)) { + $flags[$flagValueToUse] = $flagName; + } + } + }; + + $addToFlagsIfDefined('E_ERROR'); + $addToFlagsIfDefined('E_RECOVERABLE_ERROR'); + $addToFlagsIfDefined('E_WARNING'); + $addToFlagsIfDefined('E_PARSE'); + $addToFlagsIfDefined('E_NOTICE'); + // PHP 8.4: E_STRICT constant deprecated + if (PHP_VERSION_ID < 80400) { + $addToFlagsIfDefined('E_STRICT'); + } else { + $addToFlagsIfDefined('E_STRICT', /* E_STRICT: */ 2048); + } + $addToFlagsIfDefined('E_DEPRECATED'); + $addToFlagsIfDefined('E_CORE_ERROR'); + $addToFlagsIfDefined('E_CORE_WARNING'); + $addToFlagsIfDefined('E_COMPILE_ERROR'); + $addToFlagsIfDefined('E_COMPILE_WARNING'); + $addToFlagsIfDefined('E_USER_ERROR'); + $addToFlagsIfDefined('E_USER_WARNING'); + $addToFlagsIfDefined('E_USER_NOTICE'); + $addToFlagsIfDefined('E_USER_DEPRECATED'); + } + + return $flags; + } + + public static function convertErrorReportingValueToHumanReadableString(int $errorReporting): string + { + $flags = self::flagsValueToNameMap(); + $result = ''; + $appendToResult = function (string $separator, string $textToAppend) use (&$result): void { + if (!TextUtil::isEmptyString($result)) { + $result .= $separator; + } + $result .= $textToAppend; + }; + + $remaingValue = $errorReporting; + foreach ($flags as $flagValue => $flagName) { + $partToAddToResult = (($errorReporting & $flagValue) === 0 ? '~' : '') . $flagName; + $appendToResult(' & ', $partToAddToResult); + $remaingValue &= ~$flagValue; } + + if ($remaingValue !== 0) { + $appendToResult(' ', '[remaining value: ' . $remaingValue . ']'); + } + + $appendToResult(' ', '[value as int: ' . $errorReporting . ']'); + + return $result; + } + + public static function getTypeName(int $type): ?string + { + return ArrayUtil::getValueIfKeyExistsElse($type, self::flagsValueToNameMap(), null); } } diff --git a/composer.json b/composer.json index 8ca7db8c5..923abda65 100644 --- a/composer.json +++ b/composer.json @@ -8,7 +8,7 @@ { "name": "Sergey Kleyman" } ], "require": { - "php": "^7.2||8.0.*||8.1.*||8.2.*||8.3.*", + "php": "^7.2||8.0.*||8.1.*||8.2.*||8.3.*||8.4.*", "ext-json": "*", "ext-pcntl": "*", "psr/log": "^1.0" @@ -24,11 +24,11 @@ "guzzlehttp/guzzle": "^6.5.5||^7.4.4", "justinrainbow/json-schema": "^5.2.12", "monolog/monolog": "^2.7", - "php-ds/php-ds": "^1.4.1", + "php-ds/php-ds": "^1.5.0", "php-parallel-lint/php-console-highlighter": "^1.0", "php-parallel-lint/php-parallel-lint": "1.3.2", - "phpstan/phpstan": "1.10.14", - "phpstan/phpstan-phpunit": "^1.1.1", + "phpstan/phpstan": "^1.12", + "phpstan/phpstan-phpunit": "^1.4", "phpunit/phpunit": "^8.5||^9.5", "react/async": "^3.0", "react/http": "^1.6", diff --git a/docs/setup.asciidoc b/docs/setup.asciidoc index a426d0cfe..fbe5b315d 100644 --- a/docs/setup.asciidoc +++ b/docs/setup.asciidoc @@ -18,7 +18,7 @@ NOTE: Experimentally, we also provide packages for the ARM64 architecture - plea [discrete] ==== PHP -The agent supports PHP versions 7.2-8.3. +The agent supports PHP versions 7.2-8.4. [discrete] ==== curl diff --git a/docs/supported-technologies.asciidoc b/docs/supported-technologies.asciidoc index c5bb09069..4537ef199 100644 --- a/docs/supported-technologies.asciidoc +++ b/docs/supported-technologies.asciidoc @@ -21,7 +21,7 @@ Experimentally, we also provide packages for the ARM64 architecture - please not [[supported-php-versions]] === PHP versions -The agent supports PHP versions 7.2-8.3. +The agent supports PHP versions 7.2-8.4. [float] [[unsupported-php-sapis]] diff --git a/packaging/Makefile b/packaging/Makefile index 687b78644..88b886324 100644 --- a/packaging/Makefile +++ b/packaging/Makefile @@ -13,6 +13,12 @@ RELEASE_VERSION?= GITHUB_RELEASES_URL=https://github.com/elastic/apm-agent-php/releases/download BUILD_ARCH := $(if $(BUILD_ARCH),$(BUILD_ARCH),"x86-64") +ifeq ($(shell [ $(PHP_VERSION) \< 8.4 ] && echo true), true) + RPM_IMAGE_VERSION = 0.0.3 # PHP < 8.4 - image using centos +else + RPM_IMAGE_VERSION = 0.1.1 # PHP 8.4+ - image using fedora +endif + export FPM_FLAGS= ifneq ($(PHP_VERSION), 7.2) @@ -199,7 +205,7 @@ tar-install: ## Install the tar installer to run some smoke tests .PHONY: rpm-install rpm-install: ## Install the rpm installer to run some smoke tests @echo "::group::$@" # Helping to group logs in GitHub actions - TYPE=rpm $(PWD)/.ci/run_docker_with_component_tests.sh elasticobservability/apm-agent-php-dev:packages-test-rpm-php-$(PHP_VERSION)-0.0.3 + TYPE=rpm $(PWD)/.ci/run_docker_with_component_tests.sh elasticobservability/apm-agent-php-dev:packages-test-rpm-php-$(PHP_VERSION)-$(RPM_IMAGE_VERSION) @echo "::endgroup::" .PHONY: deb-install-in-apache @@ -232,7 +238,7 @@ deb-install-release-github: ## Install the deb installer from a given release to .PHONY: rpm-install-release-github rpm-install-release-github: ## Install the rpm installer from a given release to run some smoke tests @echo "::group::$@" # Helping to group logs in GitHub actions - VERSION=$(RELEASE_VERSION) GITHUB_RELEASES_URL=$(GITHUB_RELEASES_URL) TYPE=release-github $(PWD)/.ci/run_docker_with_component_tests.sh elasticobservability/apm-agent-php-dev:packages-test-rpm-php-$(PHP_VERSION)-0.0.3 + VERSION=$(RELEASE_VERSION) GITHUB_RELEASES_URL=$(GITHUB_RELEASES_URL) TYPE=release-github $(PWD)/.ci/run_docker_with_component_tests.sh elasticobservability/apm-agent-php-dev:packages-test-rpm-php-$(PHP_VERSION)-$(RPM_IMAGE_VERSION) @echo "::endgroup::" .PHONY: tar-install-release-github @@ -274,7 +280,7 @@ deb-lifecycle-testing-in-fpm: ## Lifecycle testing for the deb installer with fp .PHONY: rpm-lifecycle-testing rpm-lifecycle-testing: ## Lifecycle testing for the rpm installer @echo "::group::$@" # Helping to group logs in GitHub actions - TYPE=rpm-uninstall PACKAGE=$(NAME) $(PWD)/.ci/run_docker_with_component_tests.sh elasticobservability/apm-agent-php-dev:packages-test-rpm-php-$(PHP_VERSION)-0.0.3 + TYPE=rpm-uninstall PACKAGE=$(NAME) $(PWD)/.ci/run_docker_with_component_tests.sh elasticobservability/apm-agent-php-dev:packages-test-rpm-php-$(PHP_VERSION)-$(RPM_IMAGE_VERSION) @echo "::endgroup::" .PHONY: tar-lifecycle-testing @@ -289,13 +295,13 @@ rpm-php-upgrade-testing: PHP_VERSION=7.2 rpm-php-upgrade-testing: ## PHP upgrade, from 7.2 to 7.4, testing for the rpm installer @echo "::group::$@" # Helping to group logs in GitHub actions echo "'$(PHP_VERSION)'" - TYPE=php-upgrade PHP_VERSION=$(PHP_VERSION) PACKAGE=$(NAME) $(PWD)/.ci/run_docker_with_component_tests.sh elasticobservability/apm-agent-php-dev:packages-test-rpm-php-$(PHP_VERSION)-0.0.3 + TYPE=php-upgrade PHP_VERSION=$(PHP_VERSION) PACKAGE=$(NAME) $(PWD)/.ci/run_docker_with_component_tests.sh elasticobservability/apm-agent-php-dev:packages-test-rpm-php-$(PHP_VERSION)-$(RPM_IMAGE_VERSION) @echo "::endgroup::" .PHONY: rpm-agent-upgrade-testing rpm-agent-upgrade-testing: ## Agent upgrade, from 1.0.0 to the current generated one, testing for the rpm installer @echo "::group::$@" # Helping to group logs in GitHub actions - VERSION=$(RELEASE_VERSION) GITHUB_RELEASES_URL=$(GITHUB_RELEASES_URL) TYPE=agent-upgrade PACKAGE=$(NAME) $(PWD)/.ci/run_docker_with_component_tests.sh elasticobservability/apm-agent-php-dev:packages-test-rpm-php-$(PHP_VERSION)-0.0.3 + VERSION=$(RELEASE_VERSION) GITHUB_RELEASES_URL=$(GITHUB_RELEASES_URL) TYPE=agent-upgrade PACKAGE=$(NAME) $(PWD)/.ci/run_docker_with_component_tests.sh elasticobservability/apm-agent-php-dev:packages-test-rpm-php-$(PHP_VERSION)-$(RPM_IMAGE_VERSION) @echo "::endgroup::" .PHONY: deb-agent-upgrade-testing @@ -307,7 +313,7 @@ deb-agent-upgrade-testing: ## Agent upgrade, from 1.0.0 to the current generated .PHONY: rpm-agent-upgrade-testing-local rpm-agent-upgrade-testing-local: ## Agent upgrade, from 1.0.0 to the current generated one, testing for the rpm installer @echo "::group::$@" # Helping to group logs in GitHub actions - VERSION=$(RELEASE_VERSION) GITHUB_RELEASES_URL=$(GITHUB_RELEASES_URL) TYPE=agent-upgrade-local PACKAGE=$(NAME) $(PWD)/.ci/run_docker_with_component_tests.sh elasticobservability/apm-agent-php-dev:packages-test-rpm-php-$(PHP_VERSION)-0.0.3 + VERSION=$(RELEASE_VERSION) GITHUB_RELEASES_URL=$(GITHUB_RELEASES_URL) TYPE=agent-upgrade-local PACKAGE=$(NAME) $(PWD)/.ci/run_docker_with_component_tests.sh elasticobservability/apm-agent-php-dev:packages-test-rpm-php-$(PHP_VERSION)-$(RPM_IMAGE_VERSION) @echo "::endgroup::" .PHONY: deb-agent-upgrade-testing-local diff --git a/packaging/post-install.sh b/packaging/post-install.sh index c40f5da2f..472af4852 100755 --- a/packaging/post-install.sh +++ b/packaging/post-install.sh @@ -205,11 +205,12 @@ function is_php_supported() { [ "${PHP_MAJOR_MINOR}" == "8.0" ] || \ [ "${PHP_MAJOR_MINOR}" == "8.1" ] || \ [ "${PHP_MAJOR_MINOR}" == "8.2" ] || \ - [ "${PHP_MAJOR_MINOR}" == "8.3" ] + [ "${PHP_MAJOR_MINOR}" == "8.3" ] || \ + [ "${PHP_MAJOR_MINOR}" == "8.4" ] then return 0 else - echo 'Failed. The supported PHP versions are 7.2-8.3.' + echo 'Failed. The supported PHP versions are 7.2-8.4.' return 1 fi } diff --git a/packaging/test/centos/Dockerfile b/packaging/test/centos/Dockerfile index c8682c468..12a203703 100644 --- a/packaging/test/centos/Dockerfile +++ b/packaging/test/centos/Dockerfile @@ -25,7 +25,6 @@ RUN export PHP_VERSION_TRANSFORMED=$(echo "${PHP_VERSION}" | sed 's#\.##g') \ && yum install -y \ php \ php-mbstring \ - php-mysql \ php-mysqli \ php-pcntl \ php-posix \ diff --git a/packaging/test/docker-compose.yml b/packaging/test/docker-compose.yml index f738f3e97..85e9ff9ab 100644 --- a/packaging/test/docker-compose.yml +++ b/packaging/test/docker-compose.yml @@ -1,5 +1,13 @@ version: "3" services: + deb-fpm-php84: + image: elasticobservability/apm-agent-php-dev:packages-test-deb-fpm-php-8.4-0.0.1 + build: + context: ubuntu + dockerfile: fpm/Dockerfile + args: + - PHP_VERSION=8.4 + - SEL_DISTRO=bullseye deb-fpm-php83: image: elasticobservability/apm-agent-php-dev:packages-test-deb-fpm-php-8.3-0.0.1 build: @@ -54,6 +62,14 @@ services: + deb-apache-php84: + image: elasticobservability/apm-agent-php-dev:packages-test-deb-apache-php-8.4-0.0.1 + build: + context: ubuntu + dockerfile: apache/Dockerfile + args: + - PHP_VERSION=8.4 + - SEL_DISTRO=bullseye deb-apache-php83: image: elasticobservability/apm-agent-php-dev:packages-test-deb-apache-php-8.3-0.0.1 build: @@ -106,6 +122,14 @@ services: - PHP_VERSION=7.2 + deb-php84: + image: elasticobservability/apm-agent-php-dev:packages-test-deb-php-8.4-0.0.1 + build: + context: ubuntu + dockerfile: Dockerfile + args: + - PHP_VERSION=8.4 + - SEL_DISTRO=bullseye deb-php83: image: elasticobservability/apm-agent-php-dev:packages-test-deb-php-8.3-0.0.1 build: @@ -157,6 +181,13 @@ services: args: - PHP_VERSION=7.2 + rpm-php84: + image: elasticobservability/apm-agent-php-dev:packages-test-rpm-php-8.4-0.1.1 + build: + context: fedora + dockerfile: Dockerfile + args: + - PHP_VERSION=8.4 rpm-php83: image: elasticobservability/apm-agent-php-dev:packages-test-rpm-php-8.3-0.0.3 build: @@ -207,6 +238,13 @@ services: args: - PHP_VERSION=7.2 + apk-php84: + image: elasticobservability/apm-agent-php-dev:packages-test-apk-php-8.4-0.0.1 + build: + context: alpine + dockerfile: Dockerfile + args: + - PHP_VERSION=8.4 apk-php83: image: elasticobservability/apm-agent-php-dev:packages-test-apk-php-8.3-0.0.1 build: diff --git a/packaging/test/fedora/Dockerfile b/packaging/test/fedora/Dockerfile new file mode 100644 index 000000000..de2f42b1a --- /dev/null +++ b/packaging/test/fedora/Dockerfile @@ -0,0 +1,61 @@ +FROM fedora:41 +ARG PHP_VERSION=8.4 +ENV PHP_VERSION ${PHP_VERSION} + +RUN echo "PHP_VERSION: $PHP_VERSION" +RUN echo "ls -R -l" && echo `ls -R -l` +RUN ls -1 /etc/*release | xargs -i sh -c 'echo {} && cat {}' + +RUN dnf -y update && dnf install -y \ + git \ + gnupg2 \ + logrotate \ + lsof \ + perl-Digest-SHA \ + rsyslog \ + unzip \ + wget \ + which \ + procps + + # && yum-config-manager --enable remi-php${PHP_VERSION_TRANSFORMED} \ +RUN export PHP_VERSION_TRANSFORMED=$(echo "${PHP_VERSION}" | sed 's#\.##g') \ + FEDORA_VERSION_ID=$(cat /etc/*release | grep VERSION_ID | cut -d '=' -f 2) && echo "FEDORA_VERSION_ID: $FEDORA_VERSION_ID" \ + && dnf -y install https://rpms.remirepo.net/fedora/remi-release-$FEDORA_VERSION_ID.rpm +RUN dnf -y install dnf-plugins-core + +RUN dnf -y module reset php \ + && dnf -y module enable php:remi-$PHP_VERSION -y + +# composer \ +RUN dnf -y install \ + php \ + php-cli \ + php-curl \ + php-json \ + php-mbstring \ + php-mysqli \ + php-pdo_sqlite \ + php-pcntl \ + php-posix \ + php-xml + + +COPY --from=composer:latest /usr/bin/composer /usr/bin/composer + +# Disable agent for auxiliary PHP processes to reduce noise in logs +ENV ELASTIC_APM_ENABLED=false + +# It seems that even though we install rsyslog and start it (search for ensureSyslogIsRunning in .ci/validate_agent_installation.sh) +# for some reason on CentOS rsyslog does not receive messages from the agent +# so in order to work around this issue we escalate all log levels on CentOS +ENV ELASTIC_APM_PHP_TESTS_ESCALATED_RERUNS_PROD_CODE_LOG_LEVEL_OPTION_NAME=log_level + +# To support tar and rpm packages +ENV TYPE=rpm +ENV VERSION= +ENV GITHUB_RELEASES_URL= +COPY entrypoint.sh /bin +WORKDIR /app + +ENTRYPOINT ["/bin/entrypoint.sh"] diff --git a/packaging/test/fedora/entrypoint.sh b/packaging/test/fedora/entrypoint.sh new file mode 100755 index 000000000..649798cd6 --- /dev/null +++ b/packaging/test/fedora/entrypoint.sh @@ -0,0 +1,135 @@ +#!/usr/bin/env bash +set -xe + +################### +#### VARIABLES #### +################### +BUILD_RELEASES_FOLDER=build/releases +BUILD_PACKAGES=build/packages + +################### +#### FUNCTIONS #### +################### +function download() { + package=$1 + folder=$2 + url=$3 + mkdir -p "${folder}" + wget -q https://artifacts.elastic.co/GPG-KEY-elasticsearch -O "${folder}/GPG-KEY-elasticsearch" + wget -q "${url}/${package}" -O "${folder}/${package}" + wget -q "${url}/${package}.sha512" -O "${folder}/${package}.sha512" + wget -q "${url}/${package}.asc" -O "${folder}/${package}.asc" + cd "${folder}" || exit + gpg --import "GPG-KEY-elasticsearch" + shasum -a 512 -c "${package}.sha512" + gpg --verify "${package}.asc" "${package}" + cd - +} + +function validate_if_agent_is_uninstalled() { + ## Validate if the elastic php agent has been uninstalled + php -m + if php -m | grep -q "Unable to load dynamic library '/opt/elastic/apm-agent-php/extensions" ; then + echo 'Extension has not been uninstalled.' + exit 1 + fi + if php -m | grep -q 'elastic' ; then + echo 'Extension has not been uninstalled.' + exit 1 + fi +} + +function validate_if_agent_is_enabled() { + ## Validate if the elastic php agent is enabled + if ! php -m | grep -q 'elastic' ; then + echo 'Extension has not been installed.' + exit 1 + fi +} + +function validate_agent_installation() { + .ci/validate_agent_installation.sh || exit $? +} + +############## +#### MAIN #### +############## +if [[ "${TYPE}" == "rpm" || "${TYPE}" == "rpm-uninstall" || "${TYPE}" == "php-upgrade" ]] ; then + ls -l $BUILD_PACKAGES + ## Install rpm package and configure the agent accordingly + rpm -ivh $BUILD_PACKAGES/*.rpm +elif [ "${TYPE}" == "release-github" ] ; then + ## fpm replaces - with _ in the version for rpms. + PACKAGE=apm-agent-php-${VERSION/-/_}-1.noarch.rpm + download "${PACKAGE}" "${BUILD_RELEASES_FOLDER}" "${GITHUB_RELEASES_URL}/v${VERSION}" + rpm -ivh "${BUILD_RELEASES_FOLDER}/${PACKAGE}" +elif [ "${TYPE}" == "release-tar-github" ] ; then + PACKAGE=apm-agent-php-linux-x86-64.tar + download "${PACKAGE}" "${BUILD_RELEASES_FOLDER}" "${GITHUB_RELEASES_URL}/v${VERSION}" + ## Install tar package and configure the agent accordingly + tar -xf ${BUILD_RELEASES_FOLDER}/${PACKAGE} -C / + # shellcheck disable=SC1091 + source /opt/elastic/apm-agent-php/bin/post-install.sh +elif [ "${TYPE}" == "agent-upgrade" ] ; then + ## fpm replaces - with _ in the version for rpms. + PACKAGE=apm-agent-php-${VERSION/-/_}-1.noarch.rpm + download "${PACKAGE}" "${BUILD_RELEASES_FOLDER}" "${GITHUB_RELEASES_URL}/v${VERSION}" + rpm -ivh "${BUILD_RELEASES_FOLDER}/${PACKAGE}" +elif [ "${TYPE}" == "agent-upgrade-local" ] ; then + rpm -ivh build/local/*.rpm +else + ## Install tar package and configure the agent accordingly + tar -xf $BUILD_PACKAGES/apm-agent-php-linux-x86-64.tar -C / + # shellcheck disable=SC1091 + source /opt/elastic/apm-agent-php/bin/post-install.sh +fi + +validate_if_agent_is_enabled + +if case $TYPE in agent-upgrade*) ;; *) false;; esac; then + echo 'Validate installation runs after the agent upgrade.' +else + validate_agent_installation +fi + +## Validate the uninstallation works as expected +set -ex +if [ "${TYPE}" == "rpm-uninstall" ] ; then + rpm -e "${PACKAGE}" + validate_if_agent_is_uninstalled +elif [ "${TYPE}" == "tar-uninstall" ] ; then + # shellcheck disable=SC1091 + source /opt/elastic/apm-agent-php/bin/before-uninstall.sh + validate_if_agent_is_uninstalled +elif [ "${TYPE}" == "php-upgrade" ] ; then + ## Copy existing configuration file to compare with + cp /opt/elastic/apm-agent-php/etc/elastic-apm.ini /tmp/elastic-apm-previous.ini + + ## Uninstall existing installation + rpm -e "${PACKAGE}" + + ## Upgrade PHP version + yum-config-manager --enable remi-php74 + yum install -y php php-mbstring php-mysql php-xml + + ## Install rpm package and configure the agent accordingly + rpm -ivh $BUILD_PACKAGES/*.rpm + if ! diff --report-identical-files /opt/elastic/apm-agent-php/etc/elastic-apm.ini /tmp/elastic-apm-previous.ini ; then + echo 'Configuration file has been modified but should be identical.' + exit 1 + fi + ## Validate agent is enabled + validate_if_agent_is_enabled + + ## Run some tests + validate_agent_installation +elif case $TYPE in agent-upgrade*) ;; *) false;; esac; then + ## Upgrade the agent version with the rpm package and configure the agent accordingly + rpm -Uvh build/packages/*.rpm + + ## Validate agent is enabled + validate_if_agent_is_enabled + + ## Run some tests + validate_agent_installation +fi diff --git a/tests/ElasticApmTests/ComponentTests/ConfigSettingTest.php b/tests/ElasticApmTests/ComponentTests/ConfigSettingTest.php index a7b553b45..a7ff0240e 100644 --- a/tests/ElasticApmTests/ComponentTests/ConfigSettingTest.php +++ b/tests/ElasticApmTests/ComponentTests/ConfigSettingTest.php @@ -228,6 +228,7 @@ public function dataProviderForTestAllWaysToSetConfig(): iterable $optRawVal = str_replace("\n", "\t", $optRawVal); } $optExpectedVal = $optExpectedVal ?? AllOptionsMetadata::get()[$optName]->defaultValue(); + /** @phpstan-ignore-next-line */ yield [$agentConfigSourceKind, $optName, strval($optRawVal), $optExpectedVal]; } } diff --git a/tests/ElasticApmTests/ComponentTests/GenerateUnpackScriptsTest.php b/tests/ElasticApmTests/ComponentTests/GenerateUnpackScriptsTest.php index c5f82c498..60322db0d 100644 --- a/tests/ElasticApmTests/ComponentTests/GenerateUnpackScriptsTest.php +++ b/tests/ElasticApmTests/ComponentTests/GenerateUnpackScriptsTest.php @@ -52,7 +52,7 @@ final class GenerateUnpackScriptsTest extends ComponentTestCaseBase implements L private const PHP_VERSION_7_4 = '7.4'; // Make sure list of PHP versions supported by the Elastic APM PHP Agent is in sync. // See the comment in .ci/shared.sh - private const SUPPORTED_PHP_VERSIONS = ['7.2', '7.3', self::PHP_VERSION_7_4, '8.0', '8.1', '8.2', '8.3']; + private const SUPPORTED_PHP_VERSIONS = ['7.2', '7.3', self::PHP_VERSION_7_4, '8.0', '8.1', '8.2', '8.3', '8.4']; private const LINUX_PACKAGE_TYPE_DEB = 'deb'; private const LINUX_PACKAGE_TYPE_RPM = 'rpm'; @@ -393,8 +393,22 @@ private function assertAllTestsAreLeaf(array $whereEnvVars): void private function assertSufficientCoverageAgentUpgrade(): void { + /** @noinspection PhpForeachOverSingleElementArrayLiteralInspection */ + foreach ([self::PHP_VERSION_7_4] as $phpVersion) { + /** @noinspection PhpForeachOverSingleElementArrayLiteralInspection */ + foreach ([self::LINUX_PACKAGE_TYPE_RPM] as $linuxPackageType) { + $this->assertAllTestsAreSmoke( + [ + self::PHP_VERSION_KEY => $phpVersion, + self::LINUX_PACKAGE_TYPE_KEY => $linuxPackageType, + self::TESTING_TYPE_KEY => self::AGENT_UPGRADE_TESTING_TYPE, + ] + ); + } + } foreach ([self::PHP_VERSION_7_4, self::latestSupportedPhpVersion()] as $phpVersion) { - foreach ([self::LINUX_PACKAGE_TYPE_DEB, self::LINUX_PACKAGE_TYPE_RPM] as $linuxPackageType) { + /** @noinspection PhpForeachOverSingleElementArrayLiteralInspection */ + foreach ([self::LINUX_PACKAGE_TYPE_DEB] as $linuxPackageType) { $this->assertAllTestsAreSmoke( [ self::PHP_VERSION_KEY => $phpVersion, diff --git a/tests/ElasticApmTests/ComponentTests/MySQLiAutoInstrumentationTest.php b/tests/ElasticApmTests/ComponentTests/MySQLiAutoInstrumentationTest.php index 14eb41dab..258ef816f 100644 --- a/tests/ElasticApmTests/ComponentTests/MySQLiAutoInstrumentationTest.php +++ b/tests/ElasticApmTests/ComponentTests/MySQLiAutoInstrumentationTest.php @@ -134,7 +134,10 @@ public function testPrerequisitesSatisfied(): void AmbientContextForTests::testConfig()->mysqlDb ); self::assertNotNull($mySQLi); - self::assertTrue($mySQLi->ping()); + // Method mysqli::ping() is deprecated since PHP 8.4 + if (PHP_VERSION_ID < 80400) { + self::assertTrue($mySQLi->ping()); + } } public function testIsAutoInstrumentationEnabled(): void @@ -351,7 +354,11 @@ public static function appCodeForTestAutoInstrumentation(MixedMap $appCodeArgs): $mySQLiApiFacade = new ApiFacade($isOOPApi); $mySQLi = $mySQLiApiFacade->connect($host, $port, $user, $password, $connectDbName); self::assertNotNull($mySQLi); - self::assertTrue($mySQLi->ping()); + + // Method mysqli::ping() is deprecated since PHP 8.4 + if (PHP_VERSION_ID < 80400) { + self::assertTrue($mySQLi->ping()); + } if ($connectDbName !== $workDbName) { self::assertTrue($mySQLi->query(self::CREATE_DATABASE_IF_NOT_EXISTS_SQL_PREFIX . $workDbName)); @@ -447,7 +454,11 @@ private function implTestAutoInstrumentation(MixedMap $testArgs): void $expectedSpans = []; if ($isInstrumentationEnabled) { $expectedSpans[] = $expectationsBuilder->fromNames('mysqli', '__construct', 'mysqli_connect'); - $expectedSpans[] = $expectationsBuilder->fromNames('mysqli', 'ping'); + + // Method mysqli::ping() is deprecated since PHP 8.4 + if (PHP_VERSION_ID < 80400) { + $expectedSpans[] = $expectationsBuilder->fromNames('mysqli', 'ping'); + } if ($connectDbName !== $workDbName) { $expectedSpans[] = $expectationsBuilder->fromStatement(self::CREATE_DATABASE_IF_NOT_EXISTS_SQL_PREFIX . $workDbName); diff --git a/tests/ElasticApmTests/ComponentTests/Util/SpawnedProcessBase.php b/tests/ElasticApmTests/ComponentTests/Util/SpawnedProcessBase.php index fabebbc75..9a7f23f83 100644 --- a/tests/ElasticApmTests/ComponentTests/Util/SpawnedProcessBase.php +++ b/tests/ElasticApmTests/ComponentTests/Util/SpawnedProcessBase.php @@ -35,6 +35,7 @@ use Elastic\Apm\Impl\Util\BoolUtil; use Elastic\Apm\Impl\Util\ClassNameUtil; use Elastic\Apm\Impl\Util\ExceptionUtil; +use Elastic\Apm\Impl\Util\PhpErrorUtil; use Elastic\Apm\Impl\Util\UrlParts; use ElasticApmTests\Util\LogCategoryForTests; use PHPUnit\Framework\TestCase; @@ -61,6 +62,7 @@ protected function __construct() [ 'AmbientContext::testConfig()' => AmbientContextForTests::testConfig(), 'Environment variables' => EnvVarUtilForTests::getAll(), + 'error_reporting()' => PhpErrorUtil::convertErrorReportingValueToHumanReadableString(error_reporting()), ] ); } diff --git a/tests/ElasticApmTests/ComponentTests/Util/TestInfraHttpServerProcessBase.php b/tests/ElasticApmTests/ComponentTests/Util/TestInfraHttpServerProcessBase.php index 4064051e3..38cfeb162 100644 --- a/tests/ElasticApmTests/ComponentTests/Util/TestInfraHttpServerProcessBase.php +++ b/tests/ElasticApmTests/ComponentTests/Util/TestInfraHttpServerProcessBase.php @@ -29,7 +29,9 @@ use Elastic\Apm\Impl\Log\Logger; use Elastic\Apm\Impl\Util\ArrayUtil; use Elastic\Apm\Impl\Util\ExceptionUtil; +use Elastic\Apm\Impl\Util\PhpErrorUtil; use ElasticApmTests\Util\LogCategoryForTests; +use ElasticApmTests\Util\TestCaseBase; use ErrorException; use Exception; use PHPUnit\Framework\Assert; @@ -72,23 +74,23 @@ public function __construct() __FILE__ )->addContext('this', $this); - set_error_handler( - function ( - int $type, - string $message, - string $srcFile, - int $srcLine - ): bool { - $msgForEx = LoggableToString::convert( - [ - 'message' => $message, - 'error type' => $type, - 'srcFile:srcLine' => $srcFile . ':' . $srcLine, - ] - ); + $prevHandler = set_error_handler( + function (int $type, string $message, string $srcFile, int $srcLine, ?array $context = null): bool { + $msgCtx = [ + 'message' => $message, + 'error type' => PhpErrorUtil::getTypeName($type) . ' (as int :' . $type . ')', + 'srcFile:srcLine' => $srcFile . ':' . $srcLine, + ]; + if ($context !== null) { + $msgCtx['context'] = $context; + } + + $msgForEx = LoggableToString::convert($msgCtx); throw new ErrorException($msgForEx, /* code: */ 0, $type, $srcFile, $srcLine); - } + }, + E_ALL & ~E_DEPRECATED ); + TestCaseBase::assertNull($prevHandler); } /** @inheritDoc */ diff --git a/tests/ElasticApmTests/ComponentTests/WordPressAutoInstrumentationTest.php b/tests/ElasticApmTests/ComponentTests/WordPressAutoInstrumentationTest.php index 716340a53..d41099400 100644 --- a/tests/ElasticApmTests/ComponentTests/WordPressAutoInstrumentationTest.php +++ b/tests/ElasticApmTests/ComponentTests/WordPressAutoInstrumentationTest.php @@ -41,6 +41,7 @@ use ElasticApmTests\ComponentTests\Util\ExpectedEventCounts; use ElasticApmTests\ComponentTests\WordPress\WordPressSpanExpectationsBuilder; use ElasticApmTests\ComponentTests\WordPress\WordPressMockBridge; +use ElasticApmTests\Util\ArrayUtilForTests; use ElasticApmTests\Util\AssertMessageStack; use ElasticApmTests\Util\DataProviderForTestBuilder; use ElasticApmTests\Util\FileUtilForTests; @@ -51,6 +52,7 @@ use ElasticApmTests\Util\SpanExpectations; use ElasticApmTests\Util\SpanSequenceValidator; use ElasticApmTests\Util\StackTraceExpectations; +use ElasticApmTests\Util\TestCaseBase; use ElasticApmTests\Util\TextUtilForTests; use SplFileInfo; @@ -218,6 +220,7 @@ public static function foldTextWithMarkersIntoOneLine(string $fileContents): str $adaptedLines[] = $line . $endOfLine; } + $dbgCtx->pop(); return implode(/* separator */ '', $adaptedLines); } @@ -281,6 +284,8 @@ private static function adaptSourceTree(bool $isExpectedVariant, string $fromDir $loggerProxyDebug && $loggerProxyDebug->log(__LINE__, 'Created file', ['adaptedSrcFileFullPath' => $adaptedSrcFileFullPath]); } $dbgCtx->popSubScope(); + + $dbgCtx->pop(); } /** @@ -343,6 +348,8 @@ private static function adaptManuallyInstrumentedGeneratedFile(/* in,out */ stri self::assertNotFalse(file_put_contents($adaptedFilePath, $adaptedFileContents)); $filePath = $adaptedFilePath; $fileContents = $adaptedFileContents; + + $dbgCtx->pop(); } private static function logFileContentOnMismatch(string $filePath, string $fileContents): void @@ -359,8 +366,7 @@ private static function logFileContentOnMismatch(string $filePath, string $fileC private static function verifyAstProcessGeneratedFiles(string $astProcessDebugDumpOutDir, string $phpFileRelativePath): void { - AssertMessageStack::newScope(/* out */ $dbgCtx); - $dbgCtx->add(['astProcessDebugDumpOutDir' => $astProcessDebugDumpOutDir, 'phpFileRelativePath' => $phpFileRelativePath]); + AssertMessageStack::newScope(/* out */ $dbgCtx, AssertMessageStack::funcArgs()); $logger = self::getLoggerForThisClass()->addAllContext(['astProcessDebugDumpOutDir' => $astProcessDebugDumpOutDir, 'phpFileRelativePath' => $phpFileRelativePath]); @@ -388,6 +394,8 @@ private static function verifyAstProcessGeneratedFiles(string $astProcessDebugDu self::assertFileExists($fileFullPath); $fileContents = file_get_contents($fileFullPath); self::assertNotFalse($fileContents); + + $dbgCtx->pop(); }; ($loggerProxy = $logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__)) && $loggerProxy->log('Starting...'); @@ -414,9 +422,11 @@ private static function verifyAstProcessGeneratedFiles(string $astProcessDebugDu return; } - $logCtx = ['astMatches' => $astMatches]; + $logCtx = compact('astMatches'); + $dbgCtx->add(compact('astMatches', 'actualAstFilePath', 'expectedAstFilePath')); if (AmbientContextForTests::testConfig()->compareAstConvertedBackToSource) { - $logCtx['phpMatches'] = $phpMatches; + ArrayUtilForTests::append(compact('phpMatches'), /* in,out */ $logCtx); + $dbgCtx->add(compact('phpMatches', 'actualPhpFilePath', 'expectedPhpFilePath')); } ($loggerProxy = $logger->ifCriticalLevelEnabled(__LINE__, __FUNCTION__)) @@ -432,9 +442,9 @@ private static function verifyAstProcessGeneratedFiles(string $astProcessDebugDu } if (!$astMatches) { - self::assertSame($expectedAstFilePath, $actualAstFilePath); + TestCaseBase::fail('Dumpted ASTs do not match'); } elseif (AmbientContextForTests::testConfig()->compareAstConvertedBackToSource) { - self::assertSame($expectedPhpFilePath, $actualPhpFileContents); + TestCaseBase::fail('ASTs converted back to source do not match'); } } @@ -523,6 +533,8 @@ public static function appCodeForTestOnMockSource(MixedMap $appCodeArgs): void WordPressMockBridge::loadMockSource($srcVariantBaseDir, /* isExpectedVariant */ false); WordPressMockBridge::runMockSource($appCodeArgs); + + $dbgCtx->pop(); } public static function isWordPressDataToBeExpected(MixedMap $testArgs): bool @@ -737,6 +749,8 @@ function (AppCodeRequestParams $appCodeRequestParams) use ($testArgs): void { self::assertSame($expectedServiceFramework->version, $metadata->service->framework->version); } } + + $dbgCtx->pop(); } /** diff --git a/tests/ElasticApmTests/UnitTests/InferredSpansBuilderTest.php b/tests/ElasticApmTests/UnitTests/InferredSpansBuilderTest.php index bfacb222a..28e43bc26 100644 --- a/tests/ElasticApmTests/UnitTests/InferredSpansBuilderTest.php +++ b/tests/ElasticApmTests/UnitTests/InferredSpansBuilderTest.php @@ -337,6 +337,7 @@ function (TracerBuilderForTests $tracerBuilder) use ($inputOptions): void { // Enable span stack trace collection for span with any duration $tracerBuilder->withConfig(OptionNames::SPAN_STACK_TRACE_MIN_DURATION, '0'); foreach ($inputOptions as $optName => $optVal) { + /** @phpstan-ignore-next-line */ $tracerBuilder->withConfig($optName, strval($optVal)); } } diff --git a/tests/ElasticApmTests/UnitTests/UtilTests/StackTraceUtilTest.php b/tests/ElasticApmTests/UnitTests/UtilTests/StackTraceUtilTest.php index c08e04229..bda9daa80 100644 --- a/tests/ElasticApmTests/UnitTests/UtilTests/StackTraceUtilTest.php +++ b/tests/ElasticApmTests/UnitTests/UtilTests/StackTraceUtilTest.php @@ -85,6 +85,8 @@ public function testClosureExpections(): void $closureFrameExpections->assertMatches($actualStackTrace[0]); self::assertNull($actualStackTrace[0]->function); $thisFuncFrameExpections->assertMatches($actualStackTrace[1]); + + $dbgCtx->pop(); } public static function testStaticClosureExpections(): void @@ -143,6 +145,8 @@ public function testConvertClassAndMethodToFunctionName(?string $classicName, ?b $dbgCtx->add(['actualFuncName' => $actualFuncName]); self::assertSame($expectedFuncName, $actualFuncName); + + $dbgCtx->pop(); } /** @@ -207,6 +211,8 @@ public function testCaptureInApmFormatOneTestFame(MixedMap $testArgs): void self::assertNotNull($frame->function); self::assertStringNotContainsString(__FUNCTION__, $frame->function); } + + $dbgCtx->pop(); } /** @@ -336,6 +342,8 @@ public function testCaptureInApmFormatMultipleTestFrames(MixedMap $testArgs): vo self::assertNotNull($frame->function); self::assertStringNotContainsString(__FUNCTION__, $frame->function); } + + $dbgCtx->pop(); } /** @@ -493,6 +501,8 @@ public function testConvertPhpToApmFormat(MixedMap $testArgs): void $dbgCtx->add(['actualOutputFrames' => $actualOutputFrames]); $expectedOutput = $maxNumberOfFrames === null ? $fullExpectedOutput : array_slice($fullExpectedOutput, /* offset */ 0, /* length */ $maxNumberOfFrames); StackTraceExpectations::fromFramesExpectations($expectedOutput)->assertMatches($actualOutputFrames); + + $dbgCtx->pop(); } private const CALL_KINDS_SEQUENCE_KEY = 'call_kinds_sequence'; @@ -611,6 +621,8 @@ public function testCaptureInApmFormatOnDummyCode(MixedMap $testArgs): void $retVal = ($firstCall->callable)(...$firstCallArgs); $dbgCtx->add(['retVal' => $retVal]); StackTraceExpectations::fromFramesExpectations($retVal->expectations, /* allowToBePrefixOfActual */ true)->assertMatches($retVal->actual); + + $dbgCtx->pop(); } private const DEPTH_BEFORE_CALL_USER_FUNC_KEY = 'depth_before_call_user_func'; @@ -644,32 +656,39 @@ public function dataProviderForTestCaptureInApmFormatWithCallUserFunc(): iterabl private function helperForTestCaptureInApmFormatWithCallUserFunc(MixedMap $testArgs, int $depth, array $framesExpectations): array { AssertMessageStack::newScope(/* out */ $dbgCtx, AssertMessageStack::funcArgs()); - $thisFuncAsCallable = [__CLASS__, __FUNCTION__]; - $depthBeforeCallUserFunc = $testArgs->getInt(self::DEPTH_BEFORE_CALL_USER_FUNC_KEY); - $depthAfterCallUserFunc = $testArgs->getInt(self::DEPTH_AFTER_CALL_USER_FUNC_KEY); - self::assertLessThanOrEqual($depthBeforeCallUserFunc + $depthAfterCallUserFunc, $depth); - if ($depth === $depthBeforeCallUserFunc + $depthAfterCallUserFunc) { - $numberOfStackFramesToSkip = $testArgs->getPositiveOrZeroInt(self::NUMBER_OF_STACK_FRAMES_TO_SKIP_KEY); - array_unshift($framesExpectations, StackTraceFrameExpectations::fromLocationOnly(__FILE__, __LINE__ + 1)); - $actuallyCatpuredStackTrace = self::stackTraceUtil()->captureInApmFormat($numberOfStackFramesToSkip, /* maxNumberOfFrames */ null); - return [array_slice($framesExpectations, $numberOfStackFramesToSkip), $actuallyCatpuredStackTrace]; - } - if ($depth < $depthBeforeCallUserFunc || $depth > $depthBeforeCallUserFunc) { - array_unshift($framesExpectations, StackTraceFrameExpectations::fromClassMethod(__FILE__, __LINE__ + 1, __CLASS__, /* isStatic */ false, __FUNCTION__)); - return $this->helperForTestCaptureInApmFormatWithCallUserFunc($testArgs, $depth + 1, $framesExpectations); - } + try { + $thisFuncAsCallable = [__CLASS__, __FUNCTION__]; + $depthBeforeCallUserFunc = $testArgs->getInt(self::DEPTH_BEFORE_CALL_USER_FUNC_KEY); + $depthAfterCallUserFunc = $testArgs->getInt(self::DEPTH_AFTER_CALL_USER_FUNC_KEY); + self::assertLessThanOrEqual($depthBeforeCallUserFunc + $depthAfterCallUserFunc, $depth); + if ($depth === $depthBeforeCallUserFunc + $depthAfterCallUserFunc) { + $numberOfStackFramesToSkip = $testArgs->getPositiveOrZeroInt(self::NUMBER_OF_STACK_FRAMES_TO_SKIP_KEY); + array_unshift($framesExpectations, StackTraceFrameExpectations::fromLocationOnly(__FILE__, __LINE__ + 1)); + $actuallyCatpuredStackTrace = self::stackTraceUtil()->captureInApmFormat($numberOfStackFramesToSkip, /* maxNumberOfFrames */ null); + return [array_slice($framesExpectations, $numberOfStackFramesToSkip), $actuallyCatpuredStackTrace]; + } + if ($depth < $depthBeforeCallUserFunc || $depth > $depthBeforeCallUserFunc) { + array_unshift($framesExpectations, StackTraceFrameExpectations::fromClassMethod(__FILE__, __LINE__ + 1, __CLASS__, /* isStatic */ false, __FUNCTION__)); + return $this->helperForTestCaptureInApmFormatWithCallUserFunc($testArgs, $depth + 1, $framesExpectations); + } - self::assertSame($depthBeforeCallUserFunc, $depth); - $isCallUserFuncArrayVariant = $testArgs->getBool(self::IS_CALL_USER_FUNC_ARRAY_VARIANT_KEY); - $framesExpectationForCallToThisFuncByCallUserFunc = StackTraceFrameExpectations::fromClassMethodNoLocation(__CLASS__, /* isStatic */ false, __FUNCTION__); - $callUserFuncLine = __LINE__ + 6; - array_unshift($framesExpectations, StackTraceFrameExpectations::fromStandaloneFunction(__FILE__, $callUserFuncLine, $isCallUserFuncArrayVariant ? 'call_user_func_array' : 'call_user_func')); - array_unshift($framesExpectations, $framesExpectationForCallToThisFuncByCallUserFunc); - $callArgs = [$testArgs, $depth + 1, $framesExpectations]; - self::assertSame(__LINE__ + 2, $callUserFuncLine); - /** @var array{StackTraceFrameExpectations[], StackTraceFrame[]} $retVal */ - $retVal = $isCallUserFuncArrayVariant ? call_user_func_array($thisFuncAsCallable, $callArgs) : call_user_func($thisFuncAsCallable, ...$callArgs); - return $retVal; + self::assertSame($depthBeforeCallUserFunc, $depth); + $isCallUserFuncArrayVariant = $testArgs->getBool(self::IS_CALL_USER_FUNC_ARRAY_VARIANT_KEY); + $framesExpectationForCallToThisFuncByCallUserFunc = StackTraceFrameExpectations::fromClassMethodNoLocation(__CLASS__, /* isStatic */ false, __FUNCTION__); + $callUserFuncLine = __LINE__ + 9; // it should have the line number with call_user_func_array($thisFuncAsCallable) or call_user_func($thisFuncAsCallable) + array_unshift( + $framesExpectations, + StackTraceFrameExpectations::fromStandaloneFunction(__FILE__, $callUserFuncLine, $isCallUserFuncArrayVariant ? 'call_user_func_array' : 'call_user_func') + ); + array_unshift($framesExpectations, $framesExpectationForCallToThisFuncByCallUserFunc); + $callArgs = [$testArgs, $depth + 1, $framesExpectations]; + self::assertSame(__LINE__ + 2, $callUserFuncLine); + /** @var array{StackTraceFrameExpectations[], StackTraceFrame[]} $retVal */ + $retVal = $isCallUserFuncArrayVariant ? call_user_func_array($thisFuncAsCallable, $callArgs) : call_user_func($thisFuncAsCallable, ...$callArgs); + return $retVal; + } finally { + $dbgCtx->pop(); + } } /** @@ -678,12 +697,15 @@ private function helperForTestCaptureInApmFormatWithCallUserFunc(MixedMap $testA public function testCaptureInApmFormatWithCallUserFunc(MixedMap $testArgs): void { AssertMessageStack::newScope(/* out */ $dbgCtx, AssertMessageStack::funcArgs()); + $framesExpectations = [StackTraceFrameExpectations::fromClassMethodUnknownLocation(__CLASS__, /* isStatic */ false, 'testCaptureInApmFormatWithCallUserFunc')]; array_unshift($framesExpectations, StackTraceFrameExpectations::fromClassMethod(__FILE__, __LINE__ + 1, __CLASS__, /* isStatic */ false, 'helperForTestCaptureInApmFormatWithCallUserFunc')); $expectedActual = $this->helperForTestCaptureInApmFormatWithCallUserFunc($testArgs, /* depth */ 1, $framesExpectations); $dbgCtx->add(['expectedActual' => $expectedActual]); self::assertCount(2, $expectedActual); StackTraceExpectations::fromFramesExpectations($expectedActual[0], /* allowExpectedStackTraceToBePrefix */ true)->assertMatches($expectedActual[1]); + + $dbgCtx->pop(); } /** @@ -798,6 +820,8 @@ public function testCaptureInClassicFormatOneTestFrame(MixedMap $testArgs): void $frame = $actualCapturedStackTrace[0]; self::assertNotEquals(__FUNCTION__, $frame->function); } + + $dbgCtx->pop(); } /** @@ -943,6 +967,8 @@ public function testCaptureInClassicFormatMultipleTestFrames(MixedMap $testArgs) $frame = $actualCapturedStackTrace[$actualCapturedStackTraceFrameIndex]; self::assertNotEquals(__FUNCTION__, $frame->function); } + + $dbgCtx->pop(); } /** @@ -1078,6 +1104,8 @@ public function testConvertClassicToApmFormat(MixedMap $testArgs): void $dbgCtx->add(['actualOutputFrames' => $actualOutputFrames]); $expectedOutput = $maxNumberOfFrames === null ? $fullExpectedOutput : array_slice($fullExpectedOutput, /* offset */ 0, /* length */ $maxNumberOfFrames); StackTraceExpectations::fromFrames($expectedOutput)->assertMatches($actualOutputFrames); + + $dbgCtx->pop(); } /** @@ -1200,6 +1228,8 @@ public function testConvertThrowableTraceToApmFormat(MixedMap $testArgs): void self::assertNotNull($frame->function); self::assertStringNotContainsString(__FUNCTION__, $frame->function); } + + $dbgCtx->pop(); } public function testLimitConfigToMaxNumberOfFrames(): void diff --git a/tests/ElasticApmTests/UnitTests/WordPressAutoInstrumentationUnitTest.php b/tests/ElasticApmTests/UnitTests/WordPressAutoInstrumentationUnitTest.php index 7d3adc9b4..007c044e8 100644 --- a/tests/ElasticApmTests/UnitTests/WordPressAutoInstrumentationUnitTest.php +++ b/tests/ElasticApmTests/UnitTests/WordPressAutoInstrumentationUnitTest.php @@ -45,6 +45,8 @@ public function testFindAddonNameInFilePath(): void $dbgCtx->add(['actualGroupKind' => $actualGroupKind, 'actualGroupName' => $actualGroupName]); self::assertSame($expectedGroupKind, $actualGroupKind); self::assertSame($expectedGroupName, $actualGroupName); + + $dbgCtx->pop(); }; $testImpl = function (string $filePath, string $expectedGroupKind, ?string $expectedGroupName) use ($testImplFilePathAsIs): void { diff --git a/tests/ElasticApmTests/Util/AssertMessageStack.php b/tests/ElasticApmTests/Util/AssertMessageStack.php index 56883efa3..35fb1a374 100644 --- a/tests/ElasticApmTests/Util/AssertMessageStack.php +++ b/tests/ElasticApmTests/Util/AssertMessageStack.php @@ -145,12 +145,12 @@ public static function funcArgs(): array return $result; } - public function autoPopScope(AssertMessageStackScopeData $expectedTopData): void + public function popTopScope(AssertMessageStackScopeData $expectedTopScope): void { - $dbgCtx = ['this' => $this, 'expectedTopData' => $expectedTopData]; + $actualTopScope = $this->scopesStack[count($this->scopesStack) - 1]; + $dbgCtx = compact('actualTopScope', 'expectedTopScope', 'this'); Assert::assertNotEmpty($this->scopesStack, LoggableToString::convert($dbgCtx)); - $actualTopData = $this->scopesStack[count($this->scopesStack) - 1]; - Assert::assertSame($expectedTopData, $actualTopData, LoggableToString::convert($dbgCtx)); + Assert::assertSame($expectedTopScope, $actualTopScope, LoggableToString::convert($dbgCtx)); array_pop(/* ref */ $this->scopesStack); } @@ -187,6 +187,12 @@ public static function getContextsStack(): array return $result; } + public static function reset(): void + { + self::ensureSingleton()->scopesStack = []; + AssertMessageStackScopeData::$nextId = 1; + } + public function toLog(LogStreamInterface $stream): void { $stream->toLogAs(['scopesStack count' => count($this->scopesStack), 'isEnabled' => self::$isEnabled]); diff --git a/tests/ElasticApmTests/Util/AssertMessageStackScopeAutoRef.php b/tests/ElasticApmTests/Util/AssertMessageStackScopeAutoRef.php index bc2644665..836614475 100644 --- a/tests/ElasticApmTests/Util/AssertMessageStackScopeAutoRef.php +++ b/tests/ElasticApmTests/Util/AssertMessageStackScopeAutoRef.php @@ -40,12 +40,18 @@ public function __construct(AssertMessageStack $stack, ?AssertMessageStackScopeD } public function __destruct() + { + $this->pop(); + } + + public function pop(): void { if ($this->data === null) { return; } - $this->stack->autoPopScope($this->data); + $this->stack->popTopScope($this->data); + $this->data = null; } /** diff --git a/tests/ElasticApmTests/Util/AssertMessageStackScopeData.php b/tests/ElasticApmTests/Util/AssertMessageStackScopeData.php index 3ed63b159..38b6a85d2 100644 --- a/tests/ElasticApmTests/Util/AssertMessageStackScopeData.php +++ b/tests/ElasticApmTests/Util/AssertMessageStackScopeData.php @@ -25,11 +25,18 @@ use Elastic\Apm\Impl\Log\LoggableInterface; use Elastic\Apm\Impl\Log\LogStreamInterface; +use Elastic\Apm\Impl\Util\ArrayUtil; use Elastic\Apm\Impl\Util\DbgUtil; use PHPUnit\Framework\Assert; final class AssertMessageStackScopeData implements LoggableInterface { + /** @var int */ + public static $nextId = 1; + + /** @var int */ + public $id; + /** @var Pair>[] */ public $subScopesStack; @@ -39,6 +46,10 @@ final class AssertMessageStackScopeData implements LoggableInterface */ public function __construct(string $name, array $initialCtx) { + if (self::$nextId === PHP_INT_MAX) { + self::$nextId = 1; + } + $this->id = self::$nextId++; $this->subScopesStack = [new Pair($name, $initialCtx)]; } @@ -73,6 +84,7 @@ public static function buildContextName(int $numberOfStackFramesToSkip): string public function toLog(LogStreamInterface $stream): void { - $stream->toLogAs(['subScopesStack count' => count($this->subScopesStack)]); + $name = ArrayUtil::isEmpty($this->subScopesStack) ? 'N/A' : ArrayUtilForTests::getFirstValue($this->subScopesStack)->first; + $stream->toLogAs(['ID' => $this->id, 'name' => $name, 'subScopesStack count' => count($this->subScopesStack)]); } } diff --git a/tests/ElasticApmTests/Util/Deserialization/DeserializationException.php b/tests/ElasticApmTests/Util/Deserialization/DeserializationException.php index 6d0501d2f..cca039288 100644 --- a/tests/ElasticApmTests/Util/Deserialization/DeserializationException.php +++ b/tests/ElasticApmTests/Util/Deserialization/DeserializationException.php @@ -28,7 +28,7 @@ class DeserializationException extends RuntimeException { - public function __construct(string $message, int $code = 0, Throwable $previous = null) + public function __construct(string $message, int $code = 0, ?Throwable $previous = null) { parent::__construct($message, $code, $previous); } diff --git a/tests/ElasticApmTests/Util/Deserialization/DeserializationUtil.php b/tests/ElasticApmTests/Util/Deserialization/DeserializationUtil.php index fff1e4594..af90208e6 100644 --- a/tests/ElasticApmTests/Util/Deserialization/DeserializationUtil.php +++ b/tests/ElasticApmTests/Util/Deserialization/DeserializationUtil.php @@ -43,7 +43,7 @@ public static function buildUnknownKeyException($key): DeserializationException return DeserializationUtil::buildException('Unknown key: ' . $key); } - public static function buildException(?string $msgDetails = null, int $code = 0, Throwable $previous = null): DeserializationException + public static function buildException(?string $msgDetails = null, int $code = 0, ?Throwable $previous = null): DeserializationException { $msgStart = 'Deserialization failed'; if ($msgDetails !== null) { diff --git a/tests/ElasticApmTests/Util/Deserialization/ServerApiSchemaValidationException.php b/tests/ElasticApmTests/Util/Deserialization/ServerApiSchemaValidationException.php index 02945b045..5b53c6c58 100644 --- a/tests/ElasticApmTests/Util/Deserialization/ServerApiSchemaValidationException.php +++ b/tests/ElasticApmTests/Util/Deserialization/ServerApiSchemaValidationException.php @@ -28,7 +28,7 @@ class ServerApiSchemaValidationException extends RuntimeException { - public function __construct(string $message, int $code = 0, Throwable $previous = null) + public function __construct(string $message, int $code = 0, ?Throwable $previous = null) { parent::__construct($message, $code, $previous); } diff --git a/tests/ElasticApmTests/Util/DummyExceptionForTests.php b/tests/ElasticApmTests/Util/DummyExceptionForTests.php index 577e8c8db..00f1f19e6 100644 --- a/tests/ElasticApmTests/Util/DummyExceptionForTests.php +++ b/tests/ElasticApmTests/Util/DummyExceptionForTests.php @@ -31,7 +31,7 @@ class DummyExceptionForTests extends RuntimeException public const NAMESPACE = __NAMESPACE__; public const FQ_CLASS_NAME = __CLASS__; - public function __construct(string $message, int $code = 0, Throwable $previous = null) + public function __construct(string $message, int $code = 0, ?Throwable $previous = null) { parent::__construct($message, $code, $previous); } diff --git a/tests/ElasticApmTests/Util/InvalidEventDataException.php b/tests/ElasticApmTests/Util/InvalidEventDataException.php index 4571ece0b..6dc3198f4 100644 --- a/tests/ElasticApmTests/Util/InvalidEventDataException.php +++ b/tests/ElasticApmTests/Util/InvalidEventDataException.php @@ -28,7 +28,7 @@ class InvalidEventDataException extends RuntimeException { - public function __construct(string $message, int $code = 0, Throwable $previous = null) + public function __construct(string $message, int $code = 0, ?Throwable $previous = null) { parent::__construct($message, $code, $previous); } diff --git a/tests/ElasticApmTests/Util/MixedMap.php b/tests/ElasticApmTests/Util/MixedMap.php index 6815f8bff..26836503c 100644 --- a/tests/ElasticApmTests/Util/MixedMap.php +++ b/tests/ElasticApmTests/Util/MixedMap.php @@ -194,6 +194,7 @@ public function getNullablePositiveOrZeroInt(string $key): ?int TestCaseBase::assertGreaterThanOrEqual(0, $value); } /** @var null|positive-int|0 $value */ + $dbgCtx->pop(); return $value; } @@ -216,6 +217,7 @@ public function getPositiveOrZeroInt(string $key): int $value = $this->getInt($key); TestCaseBase::assertGreaterThanOrEqual(0, $value); /** @var positive-int|0 $value */ + $dbgCtx->pop(); return $value; } diff --git a/tests/ElasticApmTests/Util/PhpUnitExtensionBase.php b/tests/ElasticApmTests/Util/PhpUnitExtensionBase.php index 868258ea8..f917926cf 100644 --- a/tests/ElasticApmTests/Util/PhpUnitExtensionBase.php +++ b/tests/ElasticApmTests/Util/PhpUnitExtensionBase.php @@ -64,6 +64,7 @@ public function __construct(string $dbgProcessName) */ public function executeBeforeTest(string $test): void { + AssertMessageStack::reset(); self::$timestampBeforeTest = AmbientContextForTests::clock()->getSystemClockCurrentTime(); ($loggerProxy = $this->logger->ifDebugLevelEnabled(__LINE__, __FUNCTION__)) && $loggerProxy->includeStackTrace()->log('', ['timestampBeforeTest' => TimeUtilForTests::timestampToLoggable(self::$timestampBeforeTest)]); diff --git a/tests/ElasticApmTests/Util/StackTraceExpectations.php b/tests/ElasticApmTests/Util/StackTraceExpectations.php index aafa4a773..03c34c6e4 100644 --- a/tests/ElasticApmTests/Util/StackTraceExpectations.php +++ b/tests/ElasticApmTests/Util/StackTraceExpectations.php @@ -89,5 +89,7 @@ public function assertMatches(array $actual): void } else { TestCaseBase::assertSame(count($this->frames), count($actual)); } + + $dbgCtx->pop(); } } diff --git a/tests/ElasticApmTests/Util/StackTraceFrameExpectations.php b/tests/ElasticApmTests/Util/StackTraceFrameExpectations.php index cf4b8f4b9..295a271b2 100644 --- a/tests/ElasticApmTests/Util/StackTraceFrameExpectations.php +++ b/tests/ElasticApmTests/Util/StackTraceFrameExpectations.php @@ -38,6 +38,9 @@ final class StackTraceFrameExpectations extends ExpectationsBase /** @var Optional */ public $function; + /** @var bool */ + public $isFunctionRegex = false; + /** @var Optional */ public $lineno; @@ -101,9 +104,35 @@ public static function fromStandaloneFunction(string $fileName, int $lineNumber, return $result; } + private static function convertFunctionNameToRegPattern(string $text): string + { + $result = $text; + $result = str_replace('\\', '\\\\', $result); + $result = str_replace('{', '\\{', $result); + $result = str_replace('}', '\\}', $result); + $result = str_replace('(', '\\(', $result); + $result = str_replace(')', '\\)', $result); + return '/^' . $result . '$/'; + } + public static function fromClosure(string $fileName, int $lineNumber, ?string $namespace, string $class, bool $isStatic): self { - $result = self::fromClassMethodUnknownLocation($class, $isStatic, ($namespace === null ? '' : ($namespace . '\\')) . '{closure}'); + // Before PHP 8.4: ElasticApmTests\\TestsSharedCode\\SpanStackTraceTestSharedCode::ElasticApmTests\\TestsSharedCode\\{closure} + // PHP 8.4: ElasticApmTests\\TestsSharedCode\\SpanStackTraceTestSharedCode::{closure:ElasticApmTests\\TestsSharedCode\\SpanStackTraceTestSharedCode::allSpanCreatingApis():207} + if (PHP_VERSION_ID < 80400) { + $result = self::fromClassMethodUnknownLocation($class, $isStatic, ($namespace === null ? '' : ($namespace . '\\')) . '{closure}'); + } else { + $result = self::fromClassMethodUnknownLocation($class, $isStatic, '{closure:__CLASS__::__METHOD__():__LINE__}'); + $expectedFunc = $result->function->getValue(); + TestCaseBase::assertNotNull($expectedFunc); + $expectedFuncRegex = self::convertFunctionNameToRegPattern($expectedFunc); + $expectedFuncRegex = str_replace('__CLASS__', '[a-zA-Z0-9\\\\]+', $expectedFuncRegex); + $expectedFuncRegex = str_replace('__METHOD__', '[a-zA-Z0-9]+', $expectedFuncRegex); + $expectedFuncRegex = str_replace('__LINE__', '[0-9]+', $expectedFuncRegex); + $result->function->setValue($expectedFuncRegex); + $result->isFunctionRegex = true; + } + $result->filename->setValue($fileName); $result->lineno->setValue($lineNumber); return $result; @@ -143,7 +172,22 @@ public function assertMatches(StackTraceFrame $actual): void $dbgCtx->add(['this' => $this]); TestCaseBase::assertSameExpectedOptional($this->filename, $actual->filename); - TestCaseBase::assertSameExpectedOptional($this->function, $actual->function); + if ($this->isFunctionRegex) { + if ($this->function->isValueSet()) { + $expectedFuncRegex = $this->function->getValue(); + $actualFunc = $actual->function; + if ($expectedFuncRegex === null) { + TestCaseBase::assertNull($actualFunc); + } else { + TestCaseBase::assertNotNull($actualFunc); + TestCaseBase::assertMatchesRegularExpression($expectedFuncRegex, $actualFunc); + } + } + } else { + TestCaseBase::assertSameExpectedOptional($this->function, $actual->function); + } TestCaseBase::assertSameExpectedOptional($this->lineno, $actual->lineno); + + $dbgCtx->pop(); } } diff --git a/tests/ElasticApmTests/Util/TestCaseBase.php b/tests/ElasticApmTests/Util/TestCaseBase.php index e121509d3..46870b5f5 100644 --- a/tests/ElasticApmTests/Util/TestCaseBase.php +++ b/tests/ElasticApmTests/Util/TestCaseBase.php @@ -85,7 +85,7 @@ public static function assertThrows( string $class, callable $execute, string $message = '', - callable $inspect = null + ?callable $inspect = null ): void { try { $execute(); @@ -140,6 +140,7 @@ public static function assertSameEx($expected, $actual, string $message = ''): v return is_float($value) || is_int($value); }; if ($isNumeric($expected) && $isNumeric($actual) && (is_float($expected) !== is_float($actual))) { + /** @phpstan-ignore-next-line */ self::assertSame(floatval($expected), floatval($actual), $message); } else { self::assertSame($expected, $actual, $message); @@ -748,6 +749,7 @@ public static function assertCountAtLeast(int $expectedMinCount, $haystack): voi { AssertMessageStack::newScope(/* out */ $dbgCtx, AssertMessageStack::funcArgs()); self::assertGreaterThanOrEqual($expectedMinCount, count($haystack)); + $dbgCtx->pop(); } /** @@ -820,6 +822,8 @@ public static function assertSame($expected, $actual, string $message = ''): voi self::addMessageStackToException($ex); throw $ex; } + + $dbgCtx->pop(); } /** @@ -1132,6 +1136,23 @@ public static function assertContainsEx($needle, iterable $haystack, string $mes } } + /** + * @inheritDoc + */ + public static function assertMatchesRegularExpression(string $pattern, string $string, string $message = ''): void + { + AssertMessageStack::newScope(/* out */ $dbgCtx, AssertMessageStack::funcArgs()); + + try { + $pregMatchRetVal = preg_match($pattern, $string); + $dbgCtx->add(compact('pregMatchRetVal')); + Assert::assertTrue($pregMatchRetVal > 0, $message); + } catch (AssertionFailedError $ex) { + self::addMessageStackToException($ex); + throw $ex; + } + } + public static function assertDirectoryDoesNotExist(string $directory, string $message = ''): void { /** diff --git a/tests/ElasticApmTests/Util/TextUtilForTests.php b/tests/ElasticApmTests/Util/TextUtilForTests.php index 57ca99839..c9ffd7ca4 100644 --- a/tests/ElasticApmTests/Util/TextUtilForTests.php +++ b/tests/ElasticApmTests/Util/TextUtilForTests.php @@ -123,6 +123,7 @@ public static function combineWithSeparatorIfNotEmpty(string $separator, string */ public static function emptyIfNull($input): string { + /** @phpstan-ignore-next-line */ return $input === null ? '' : strval($input); } } diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 795658bf1..ed22184c2 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -21,15 +21,17 @@ declare(strict_types=1); -error_reporting(E_ALL | E_STRICT); - use ElasticApmTests\TestsRootDir; // Ensure that composer has installed all dependencies if (!file_exists(dirname(__DIR__) . '/composer.lock')) { - die("Dependencies must be installed using composer\n"); + die('Dependencies must be installed using composer' . PHP_EOL); } +// Disable deprecation notices starting from PHP 8.4 +// Deprecated: funcAbc(): Implicitly marking parameter $xyz as nullable is deprecated, the explicit nullable type must be used instead +error_reporting(PHP_VERSION_ID < 80400 ? E_ALL : (E_ALL & ~E_DEPRECATED)); + require __DIR__ . '/../vendor/autoload.php'; require __DIR__ . '/polyfills/load.php'; diff --git a/tests/polyfills/load.php b/tests/polyfills/load.php index 4abcd45bd..ef3a9a4dd 100644 --- a/tests/polyfills/load.php +++ b/tests/polyfills/load.php @@ -21,8 +21,6 @@ declare(strict_types=1); -error_reporting(E_ALL | E_STRICT); - if (!function_exists('array_key_first')) { require __DIR__ . '/array_key_first.php'; }